diff --git a/CHANGELOG b/CHANGELOG index 51941d0cd..579b30121 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ NEXT ----------- +- Added function pytest.freeze_includes(), which makes it easy to embed + pytest into executables using tools like cx_freeze. + See docs for examples and rationale. Thanks Bruno Oliveira. + - Improve assertion rewriting cache invalidation precision. - fixed issue561: adapt autouse fixture example for python3. diff --git a/_pytest/genscript.py b/_pytest/genscript.py index 9104dcab8..f41230b8a 100755 --- a/_pytest/genscript.py +++ b/_pytest/genscript.py @@ -1,6 +1,12 @@ """ generate a single-file self-contained version of pytest """ -import py +import os import sys +import pkgutil + +import py + +import _pytest + def find_toplevel(name): @@ -80,3 +86,42 @@ def pytest_cmdline_main(config): tw.line("generated pytest standalone script: %s" % genscript, bold=True) return 0 + + +def pytest_namespace(): + return {'freeze_includes': freeze_includes} + + +def freeze_includes(): + """ + Returns a list of module names used by py.test that should be + included by cx_freeze. + """ + result = list(_iter_all_modules(py)) + result += list(_iter_all_modules(_pytest)) + return result + + +def _iter_all_modules(package, prefix=''): + """ + Iterates over the names of all modules that can be found in the given + package, recursively. + + Example: + _iter_all_modules(_pytest) -> + ['_pytest.assertion.newinterpret', + '_pytest.capture', + '_pytest.core', + ... + ] + """ + if type(package) is not str: + path, prefix = package.__path__[0], package.__name__ + '.' + else: + path = package + for _, name, is_package in pkgutil.iter_modules([path]): + if is_package: + for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'): + yield prefix + m + else: + yield prefix + name diff --git a/doc/en/example/simple.txt b/doc/en/example/simple.txt index 1973623d9..380f6139f 100644 --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -682,33 +682,27 @@ 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 embedding the ``pytest`` runner into a frozen executable using -``cx_freeze`` is not as straightforward as one would like, -because ``pytest`` makes heavy use of dynamic module loading which -``cx_freeze`` can't resolve by itself. - -To solve this, you have to manually include ``pytest`` and ``py`` -modules by using the ``build_exe`` option in your ``setup.py`` script, like this:: +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 - includes = [ - '_pytest.doctest', - '_pytest.unittest', - # ... lots more - ] setup( - name="runtests", - options={"build_exe": {'includes': includes}}, + name="app_main", + executables=[Executable("app_main.py")], + options={"build_exe": + { + 'includes': pytest.freeze_includes()} + }, # ... other options ) -(For the complete list, check out the modules under ``_pytest`` in your -site-packages). - -With that, you can make your program check for a certain flag and pass control -over to ``pytest``:: +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:: # contents of app_main.py import sys @@ -722,7 +716,6 @@ over to ``pytest``:: ... This makes it convenient to execute your tests from within your frozen -application, using standard ``py.test`` command-line:: +application, using standard ``py.test`` command-line options:: - $ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/ /bin/sh: 1: ./app_main: not found - /bin/sh: 1: ./app_main: not found + $ ./app_main --pytest --verbose --tb=long --junit-xml=results.xml test-suite/ diff --git a/testing/cx_freeze/runtests_script.py b/testing/cx_freeze/runtests_script.py new file mode 100644 index 000000000..f2b032d76 --- /dev/null +++ b/testing/cx_freeze/runtests_script.py @@ -0,0 +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 + sys.exit(pytest.main()) \ No newline at end of file diff --git a/testing/cx_freeze/runtests_setup.py b/testing/cx_freeze/runtests_setup.py new file mode 100644 index 000000000..a2874a655 --- /dev/null +++ b/testing/cx_freeze/runtests_setup.py @@ -0,0 +1,15 @@ +""" +Sample setup.py script that generates an executable with pytest runner embedded. +""" +if __name__ == '__main__': + from cx_Freeze import setup, Executable + import pytest + + setup( + name="runtests", + version="0.1", + description="exemple of how embedding py.test into an executable using cx_freeze", + executables=[Executable("runtests_script.py")], + options={"build_exe": {'includes': pytest.freeze_includes()}}, + ) + diff --git a/testing/cx_freeze/tests/test_doctest.txt b/testing/cx_freeze/tests/test_doctest.txt new file mode 100644 index 000000000..e18a4b68c --- /dev/null +++ b/testing/cx_freeze/tests/test_doctest.txt @@ -0,0 +1,6 @@ + + +Testing doctest:: + + >>> 1 + 1 + 2 diff --git a/testing/cx_freeze/tests/test_trivial.py b/testing/cx_freeze/tests/test_trivial.py new file mode 100644 index 000000000..d8a572baa --- /dev/null +++ b/testing/cx_freeze/tests/test_trivial.py @@ -0,0 +1,6 @@ + +def test_upper(): + assert 'foo'.upper() == 'FOO' + +def test_lower(): + assert 'FOO'.lower() == 'foo' \ No newline at end of file diff --git a/testing/cx_freeze/tox_run.py b/testing/cx_freeze/tox_run.py new file mode 100644 index 000000000..e8df2684b --- /dev/null +++ b/testing/cx_freeze/tox_run.py @@ -0,0 +1,15 @@ +""" +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' + sys.exit(os.system('%s tests' % executable)) \ No newline at end of file diff --git a/testing/test_genscript.py b/testing/test_genscript.py index 02bbabb6f..1c65fec14 100644 --- a/testing/test_genscript.py +++ b/testing/test_genscript.py @@ -36,3 +36,13 @@ def test_gen(testdir, anypython, standalone): result = standalone.run(anypython, testdir, p) assert result.ret != 0 + +def test_freeze_includes(): + """ + Smoke test for freeze_includes(), to ensure that it works across all + supported python versions. + """ + includes = pytest.freeze_includes() + assert len(includes) > 1 + assert '_pytest.genscript' in includes + diff --git a/tox.ini b/tox.ini index 4359f22c9..9e2c23d55 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py32,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting +envlist=flakes,py26,py27,py34,pypy,py27-pexpect,py33-pexpect,py27-nobyte,py32,py33,py27-xdist,py33-xdist,py27-trial,py33-trial,doctesting,py27-cxfreeze [testenv] changedir=testing @@ -123,6 +123,14 @@ commands= {envpython} {envbindir}/py.test-jython \ -rfsxX --junitxml={envlogdir}/junit-{envname}2.xml [] +[testenv:py27-cxfreeze] +deps=cx_freeze +changedir=testing/cx_freeze +basepython=python2.7 +commands= + {envpython} runtests_setup.py build --build-exe build + {envpython} tox_run.py + [pytest] minversion=2.0 plugins=pytester