Skip to content

Commit

Permalink
Merge pull request #1773 from nicoddemus/fix-freeze
Browse files Browse the repository at this point in the history
Use PyInstaller for freeze test env
  • Loading branch information
The-Compiler authored Jul 27, 2016
2 parents 7b15206 + ae9d3bf commit ffb583a
Show file tree
Hide file tree
Showing 13 changed files with 72 additions and 149 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ env:
- TESTENV=py35-trial
- TESTENV=py27-nobyte
- TESTENV=doctesting
- TESTENV=py27-cxfreeze
- TESTENV=freeze

script: tox --recreate -e $TESTENV

Expand Down
3 changes: 2 additions & 1 deletion _pytest/genscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def pytest_namespace():
def freeze_includes():
"""
Returns a list of module names used by py.test that should be
included by cx_freeze.
included by cx_freeze/pyinstaller to generate a standalone
pytest executable.
"""
result = list(_iter_all_modules(py))
result += list(_iter_all_modules(_pytest))
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ environment:
# builds timing out in AppVeyor
- TOXENV: "linting,py26,py27,py33,py34,py35,pypy"
- TOXENV: "py27-pexpect,py27-xdist,py27-trial,py35-pexpect,py35-xdist,py35-trial"
- TOXENV: "py27-nobyte,doctesting,py27-cxfreeze"
- TOXENV: "py27-nobyte,doctesting,freeze"

install:
- echo Installed Pythons
Expand Down
57 changes: 23 additions & 34 deletions doc/en/example/simple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -699,40 +699,29 @@ and run it::
You'll see that the fixture finalizers could use the precise reporting
information.

Integrating pytest runner and cx_freeze
-----------------------------------------------------------
Freezing pytest
---------------

If you freeze your application using a tool like
`cx_freeze <https://cx-freeze.readthedocs.io>`_ in order to distribute it
to your end-users, it is a good idea to also package your test runner and run
your tests using the frozen application.

This way packaging errors such as dependencies not being
included into the executable can be detected early while also allowing you to
send test files to users so they can run them in their machines, which can be
invaluable to obtain more information about a hard to reproduce bug.

Unfortunately ``cx_freeze`` can't discover them
automatically because of ``pytest``'s use of dynamic module loading, so you
must declare them explicitly by using ``pytest.freeze_includes()``::

# contents of setup.py
from cx_Freeze import setup, Executable
import pytest

setup(
name="app_main",
executables=[Executable("app_main.py")],
options={"build_exe":
{
'includes': pytest.freeze_includes()}
},
# ... other options
)

If you don't want to ship a different executable just in order to run your tests,
you can make your program check for a certain flag and pass control
over to ``pytest`` instead. For example::
`PyInstaller <https://pyinstaller.readthedocs.io>`_
in order to distribute it to your end-users, it is a good idea to also package
your test runner and run your tests using the frozen application. This way packaging
errors such as dependencies not being included into the executable can be detected early
while also allowing you to send test files to users so they can run them in their
machines, which can be useful to obtain more information about a hard to reproduce bug.

Fortunately recent ``PyInstaller`` releases already have a custom hook
for pytest, but if you are using another tool to freeze executables
such as ``cx_freeze`` or ``py2exe``, you can use ``pytest.freeze_includes()``
to obtain the full list of internal pytest modules. How to configure the tools
to find the internal modules varies from tool to tool, however.

Instead of freezing the pytest runner as a separate executable, you can make
your frozen program work as the pytest runner by some clever
argument handling during program startup. This allows you to
have a single executable, which is usually more convenient.

.. code-block:: python
# contents of app_main.py
import sys
Expand All @@ -745,7 +734,7 @@ over to ``pytest`` instead. For example::
# by your argument-parsing library of choice as usual
...
This makes it convenient to execute your tests from within your frozen
application, using standard ``py.test`` command-line options::
This allows you to execute tests using the frozen
application with standard ``py.test`` command-line options::

./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
64 changes: 0 additions & 64 deletions testing/cx_freeze/install_cx_freeze.py

This file was deleted.

15 changes: 0 additions & 15 deletions testing/cx_freeze/runtests_setup.py

This file was deleted.

3 changes: 3 additions & 0 deletions testing/freeze/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
dist/
*.spec
13 changes: 13 additions & 0 deletions testing/freeze/create_executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
Generates an executable with pytest runner embedded using PyInstaller.
"""
if __name__ == '__main__':
import pytest
import subprocess

hidden = []
for x in pytest.freeze_includes():
hidden.extend(['--hidden-import', x])
args = ['pyinstaller', '--noconfirm'] + hidden + ['runtests_script.py']
subprocess.check_call(' '.join(args), shell=True)

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""
This is the script that is actually frozen into an executable: simply executes
py.test main().
"""

if __name__ == '__main__':
import sys
import pytest
"""
This is the script that is actually frozen into an executable: simply executes
py.test main().
"""

if __name__ == '__main__':
import sys
import pytest
sys.exit(pytest.main())
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

def test_upper():
assert 'foo'.upper() == 'FOO'

def test_lower():

def test_upper():
assert 'foo'.upper() == 'FOO'

def test_lower():
assert 'FOO'.lower() == 'foo'
25 changes: 11 additions & 14 deletions testing/cx_freeze/tox_run.py → testing/freeze/tox_run.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""
Called by tox.ini: uses the generated executable to run the tests in ./tests/
directory.
.. note:: somehow calling "build/runtests_script" directly from tox doesn't
seem to work (at least on Windows).
"""
if __name__ == '__main__':
import os
import sys

executable = os.path.join(os.getcwd(), 'build', 'runtests_script')
if sys.platform.startswith('win'):
executable += '.exe'
"""
Called by tox.ini: uses the generated executable to run the tests in ./tests/
directory.
"""
if __name__ == '__main__':
import os
import sys

executable = os.path.join(os.getcwd(), 'dist', 'runtests_script', 'runtests_script')
if sys.platform.startswith('win'):
executable += '.exe'
sys.exit(os.system('%s tests' % executable))
11 changes: 5 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ distshare={homedir}/.tox/distshare
envlist=
linting,py26,py27,py33,py34,py35,pypy,
{py27,py35}-{pexpect,xdist,trial},
py27-nobyte,doctesting,py27-cxfreeze
py27-nobyte,doctesting,freeze

[testenv]
commands= py.test --lsof -rfsxX {posargs:testing}
Expand Down Expand Up @@ -124,12 +124,11 @@ changedir=testing
commands=
{envpython} {envbindir}/py.test-jython -rfsxX {posargs}

[testenv:py27-cxfreeze]
changedir=testing/cx_freeze
platform=linux|darwin
[testenv:freeze]
changedir=testing/freeze
deps=pyinstaller
commands=
{envpython} install_cx_freeze.py
{envpython} runtests_setup.py build --build-exe build
{envpython} create_executable.py
{envpython} tox_run.py


Expand Down

0 comments on commit ffb583a

Please sign in to comment.