diff --git a/.gitignore b/.gitignore index ce2f23b..fb02045 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,27 @@ -*.pyc +.idea/ + +# Python binaries +*.py[cod] + +# Editor backups, OS files *.swp *~ -*.egg-info .DS_Store +# Distutils cruft +*.egg-info +South-*.egg/ +build/ +dist/ + +# Unit test / coverage reports +.tox/ +.coverage +.coverage.* +.noseids +nosetests.xml +htmlcov/ +testapp.sqlite3 + +# Sphinx documentation build +docs/_build/* diff --git a/.travis.yml b/.travis.yml index ba336ab..2723b9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,43 @@ +sudo: false language: python -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" env: - - DJANGO="Django>=1.4,<1.5" - - DJANGO="Django>=1.6,<1.7" - - DJANGO="Django>=1.7,<1.8" + global: + - COVERAGE=1 RUNTEST_ARGS="-v --noinput" + matrix: + # Quality checks + - TOXENV=flake8 + - TOXENV=docs + # Supported Python / Django versions w/ SQLite + - TOXENV=py27-django-19 + - TOXENV=py35-django-19 + - TOXENV=py27-django-18 + - TOXENV=py34-django-18 + # Test with PostgreSQL + - TOXENV=py27-django-19-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-19-postgres" + - TOXENV=py35-django-19-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-19-postgres" + - TOXENV=py27-django-18-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-18-postgres" + # Test with MySQL + - TOXENV=py27-django-19-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-19-mysql" + - TOXENV=py35-django-19-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-19-mysql" + - TOXENV=py27-django-18-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-18-mysql" + # Django master is allowed to fail + - TOXENV=py27-django-master + - TOXENV=py35-django-master + - TOXENV=py27-django-master-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-master-mysql" + - TOXENV=py35-django-master-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-master-postgres" matrix: - exclude: - - python: "3.2" - env: DJANGO="Django>=1.4,<1.5" - - python: "3.3" - env: DJANGO="Django>=1.4,<1.5" - - python: "3.4" - env: DJANGO="Django>=1.4,<1.5" - - python: "2.6" - env: DJANGO="Django>=1.7,<1.8" + allow_failures: + # Master is allowed to fail + - env: TOXENV=py27-django-master + - env: TOXENV=py35-django-master + - env: TOXENV=py27-django-master-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-master-mysql" + - env: TOXENV=py35-django-master-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-master-postgres" + install: - - if [ $DJANGO = 'Django>=1.4,<1.5' -o $DJANGO = 'Django>=1.6,<1.7' ]; then pip install south; fi - - pip install "$DJANGO" - - pip install -e . -script: - - ./runtests.sh + - pip install tox coveralls +before_script: + - coverage erase + - bash -c "if [[ \"$DATABASE_URL\" == postgres* ]]; then psql -c 'create database \"$TOXENV\";' -U postgres; fi" + - bash -c "if [[ \"$DATABASE_URL\" == mysql* ]]; then mysql -e 'create database IF NOT EXISTS \`$TOXENV\`;'; fi" +script: tox +after_success: coveralls diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..635c195 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,61 @@ +======= +Credits +======= + +**django-nose** was created by Jeff Balogh in 2009, which is a really long time +ago in the Django world. It keeps running because of the contributions of +volunteers. Thank you to everyone who uses django-nose, keeps your projects +up-to-date, files detailed bugs, and submits patches. + +Maintainers +----------- +* Jeff Balogh (`jbalogh `_) +* Erik Rose (`erikrose `_) +* James Socol (`jscol `_) +* Rob Hudson (`robhudson `_) +* John Whitlock (`jwhitlock `_) + +Contributors +------------ +These non-maintainers have contributed code to a django-nose release: + +* Adam DePue (`adepue `_) +* Alexey Evseev (`st4lk `_) +* Alex (`alexjg `_) +* Antti Kaihola (`akaihola `_) +* Ash Christopher (`ashchristopher `_) +* Blake Winton (`bwinton `_) +* Brad Pitcher (`brad `_) +* Camilo Nova (`camilonova `_) +* Carl Meyer (`carljm `_) +* Conrado Buhrer (`conrado `_) +* David Baumgold (`singingwolfboy `_) +* David Cramer (`dcramer `_) +* Dmitry Gladkov (`dgladkov `_) +* Ederson Mota (`edrmp `_) +* Eric Zarowny (`ezarowny `_) +* Eron Villarreal (`eroninjapan `_) +* Fred Wenzel (`fwenzel `_) +* Fábio Santos (`fabiosantoscode `_) +* Ionel Cristian Mărieș (`ionelmc `_) +* Jeremy Satterfield (`jsatt `_) +* Johan Euphrosine (`proppy `_) +* Kyle Robertson (`dvelyk `_) +* Marius Gedminas (`mgedmin `_) +* Martin Chase (`outofculture `_) +* Matthias Bauer (`moeffju `_) +* Michael Elsdörfer (`miracle2k `_) +* Michael Kelly (`Osmose `_) +* Peter Baumgartner (`ipmb `_) +* Radek Simko (`radeksimko `_) +* Ramiro Morales (`ramiro `_) +* Rob Madole (`robmadole `_) +* Roger Hu (`rogerhu `_) +* Ross Lawley (`rozza `_) +* Scott Sexton (`scottsexton `_) +* Stephen Burrows (`melinath `_) +* Sverre Johansen (`sverrejoh `_) +* Tim Child (`timc3 `_) +* Walter Doekes (`wdoekes `_) +* Will Kahn-Greene (`willkg `_) +* Yegor Roganov (`roganov `_) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..2b3c4ac --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,137 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/django-nose/django-nose/issues. + +If you are reporting a bug, please include: + +* The version of django, nose, and django-nose you are using, and any other + applicable packages (``pip freeze`` will show current versions) +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +When someone submits a pull request to fix your bug, please try it out and +report if it worked for you. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything untagged or tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything untagged ot tagged with +"feature" is open to whoever wants to implement it. + +django-nose is built on nose, which supports plugins. Consider implementing +your feature as a plugin, maintained by the community using that feature, +rather than adding to the django-nose codebase. + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +django-nose could always use more documentation, whether as part of the +official django-nose, as code comments, or even on the web in blog posts, +articles, and such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at +https://github.com/django-nose/django-nose/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up django-nose +for local development. + +1. Fork the `django-nose` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/django-nose.git + +3. Install your local copy into a virtualenv. Assuming you have + virtualenvwrapper installed, this is how you set up your fork for local + development:: + + $ mkvirtualenv django-nose + $ cd django-nose/ + $ pip install -r requirements.txt + $ ./manage.py migrate + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. Make sure existing tests continue to pass with your new code:: + + $ make qa + +6. When you're done making changes, check that your changes pass flake8 and the + tests, including testing other Python versions with tox:: + + $ make qa-all + +6. Commit your changes and push your branch to GitHub:: + + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should be in a branch. +2. The pull request should include tests. +3. You agree to license your contribution under the BSD license. +4. If the pull request adds functionality, the docs should be updated. +5. Make liberal use of `git rebase` to ensure clean commits on top of master. +6. The pull request should pass QA tests and work for supported Python / Django + combinations. Check + https://travis-ci.org/django-nose/django-nose/pull_requests + and make sure that the tests pass for all supported Python versions. + +Tips +---- + +The django-nose testapp uses django-nose, so all of the features are available. +To run a subset of tests:: + + $ ./manage.py test testapp/tests.py + +To mark failed tests:: + + $ ./manage.py test --failed + +To re-run only the failed tests:: + + $ ./manage.py test --failed + diff --git a/MANIFEST.in b/MANIFEST.in index 974923d..3116c01 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,20 @@ +include AUTHORS.rst +include CONTRIBUTING.rst include LICENSE +include Makefile include README.rst +include changelog.rst +include contribute.json +include manage.py +include requirements.txt +include requirements-rtd.txt include runtests.sh -include changelog.txt -recursive-exclude django_nose *.py[co] -recursive-include testapp * -recursive-exclude testapp *.py[co] + +recursive-include docs Makefile conf.py *.rst make.bat .keep +recursive-include django_nose *.py +recursive-include testapp *.py +recursive-include testapp/fixtures *.json +recursive-include unittests *.py + +recursive-exclude * *.py[co] +recursive-exclude * __pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..98e39c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,78 @@ +.PHONY: clean clean-build clean-pyc clean-test docs qa lint coverage jslint qa-all install-jslint test test-all coverage-console release sdist + +help: + @echo "clean - remove all artifacts" + @echo "clean-build - remove build artifacts" + @echo "clean-pyc - remove Python file artifacts" + @echo "clean-test - remove test and coverage artifacts" + @echo "coverage - check code coverage quickly with the default Python" + @echo "docs - generate Sphinx HTML documentation" + @echo "lint - check style with flake8" + @echo "qa - run linters and test coverage" + @echo "qa-all - run QA plus tox and packaging" + @echo "release - package and upload a release" + @echo "sdist - package" + @echo "test - run tests quickly with the default Python" + @echo "test-all - run tests on every Python version with tox" + @echo "test-release - upload a release to the PyPI test server" + +clean: clean-build clean-pyc clean-test + +qa: lint coverage + +qa-all: qa sdist test-all + +clean-build: + rm -fr build/ + rm -fr dist/ + rm -fr *.egg-info + +clean-pyc: + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +docs: + $(MAKE) -C docs clean + $(MAKE) -C docs html + open docs/_build/html/index.html + +lint: + flake8 . + +test: + ./manage.py test + +test-all: + tox --skip-missing-interpreters + +coverage-console: + coverage erase + COVERAGE=1 ./runtests.sh + coverage combine + coverage report -m + +coverage: coverage-console + coverage html + open htmlcov/index.html + +release: clean + python setup.py sdist bdist_wheel upload + python -m webbrowser -n https://testpypi.python.org/pypi/django-nose + +test-release: + python setup.py register -r https://testpypi.python.org/pypi + python setup.py sdist bdist_wheel upload -r https://testpypi.python.org/pypi + python -m webbrowser -n https://testpypi.python.org/pypi/django-nose + +sdist: clean + python setup.py sdist + ls -l dist + check-manifest + pyroma dist/`ls -t dist | head -n1` diff --git a/README.rst b/README.rst index e7b18b2..d328559 100644 --- a/README.rst +++ b/README.rst @@ -2,13 +2,25 @@ django-nose =========== -.. image:: https://travis-ci.org/django-nose/django-nose.png - :target: https://travis-ci.org/django-nose/django-nose +.. image:: https://img.shields.io/pypi/v/django-nose.svg + :alt: The PyPI package + :target: https://pypi.python.org/pypi/django-nose -Features --------- +.. image:: https://img.shields.io/pypi/dw/django-nose.svg + :alt: PyPI download statistics + :target: https://pypi.python.org/pypi/django-nose -* All the goodness of `nose`_ in your Django tests, like... +.. image:: https://img.shields.io/travis/django-nose/django-nose/master.svg + :alt: TravisCI Build Status + :target: https://travis-ci.org/django-nose/django-nose + +.. image:: https://img.shields.io/coveralls/django-nose/django-nose/master.svg + :alt: Coveralls Test Coverage + :target: https://coveralls.io/r/django-nose/django-nose?branch=master + +.. Omit badges from docs + +**django-nose** provides all the goodness of `nose`_ in your Django tests, like: * Testing just your apps by default, not all the standard ones that happen to be in ``INSTALLED_APPS`` @@ -18,16 +30,11 @@ Features This not only saves busy-work but also eliminates the possibility of accidentally shadowing test classes. * Taking advantage of all the useful `nose plugins`_ -* Fixture bundling, an optional feature which speeds up your fixture-based - tests by a factor of 4 -* Reuse of previously created test DBs, cutting 10 seconds off startup time -* Hygienic TransactionTestCases, which can save you a DB flush per test -* Support for various databases. Tested with MySQL, PostgreSQL, and SQLite. - Others should work as well. - -.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ + +.. _nose: https://nose.readthedocs.org/en/latest/ .. _nose plugins: http://nose-plugins.jottit.com/ +It also provides: Installation ------------ @@ -91,7 +98,8 @@ django-nose includes a fixture bundler which drastically speeds up your tests by eliminating redundant setup of Django test fixtures. To use it... 1. Subclass ``django_nose.FastFixtureTestCase`` instead of - ``django.test.TestCase``. (I like to import it ``as TestCase`` in my + ``django.test.TestCase`` or ``django_nose.FastFixtureLiveServerTestCase`` instead of + ``django.test.LiveServerTestCase``. (I like to import it ``as TestCase`` in my project's ``tests/__init__.py`` and then import it from there into my actual tests. Then it's easy to sub the base class in and out.) This alone will cause fixtures to load once per class rather than once per test. @@ -129,9 +137,9 @@ sources of state leakage we have encountered: this automatically. It's also possible that you have ``post_save`` signal handlers which create -additional database rows while loading the fixtures. ``FastFixtureTestCase`` -isn't yet smart enough to notice this and clean up after it, so you'll have to -go back to plain old ``TestCase`` for now. +additional database rows while loading the fixtures. ``FastFixtureTestCase`` and +``FastFixtureLiveServerTestCase`` aren't yet smart enough to notice this and +clean up after it, so you'll have to go back to plain old ``TestCase`` for now. Exempting A Class From Bundling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -182,7 +190,7 @@ You can thus enjoy a big speed boost any time you make a TransactionTestCase clean up after itself: skipping a whole DB flush before every test. With a large schema, this can save minutes of IO. -django-nose's own FastFixtureTestCase uses this feature, even though it +django-nose's own FastFixtureTestCase and FastFixtureLiveServerTestCase uses this feature, even though it ultimately acts more like a TestCase than a TransactionTestCase. .. _can leave the DB in an unclean state: https://docs.djangoproject.com/en/1.4/topics/testing/#django.test.TransactionTestCase @@ -326,38 +334,24 @@ Recent Version History 1.2 (2013-07-23) * Python 3 support (melinath and jonashaag) * Django 1.5 compat (fabiosantoscode) - -1.1 (2012-05-19) - * Django TransactionTestCases don't clean up after themselves; they leave - junk in the DB and clean it up only on ``_pre_setup``. Thus, Django makes - sure these tests run last. Now django-nose does, too. This means one fewer - source of failures on existing projects. (Erik Rose) - * Add support for hygienic TransactionTestCases. (Erik Rose) - * Support models that are used only for tests. Just put them in any file - imported in the course of loading tests. No more crazy hacks necessary. - (Erik Rose) - * Make the fixture bundler more conservative, fixing some conceivable - situations in which fixtures would not appear as intended if a - TransactionTestCase found its way into the middle of a bundle. (Erik Rose) - * Fix an error that would surface when using SQLAlchemy with connection - pooling. (Roger Hu) - * Gracefully ignore the new ``--liveserver`` option introduced in Django 1.4; - don't let it through to nose. (Adam DePue) - -1.0 (2012-03-12) - * New fixture-bundling plugin for avoiding needless fixture setup (Erik Rose) - * Moved FastFixtureTestCase in from test-utils, so now all the - fixture-bundling stuff is in one library. (Erik Rose) - * Added the REUSE_DB setting for faster startup and shutdown. (Erik Rose) - * Fixed a crash when printing options with certain verbosities. (Daniel Abel) - * Broke hard dependency on MySQL. Support PostgreSQL. (Roger Hu) - * Support SQLite, both memory- and disk-based. (Roger Hu and Erik Rose) - * Nail down versions of the package requirements. (Daniel Mizyrycki) - -0.1.3 (2010-04-15) - * Even better coverage support (rozza) - * README fixes (carljm and ionelmc) - * optparse OptionGroups are handled better (outofculture) - * nose plugins are loaded before listing options - -See more in changelog.txt. + * Fixture bundling, an optional feature which speeds up your fixture-based + tests by a factor of 4 + * Reuse of previously created test DBs, cutting 10 seconds off startup time + * Hygienic TransactionTestCases, which can save you a DB flush per test + * Support for various databases. Tested with MySQL, PostgreSQL, and SQLite. + Others should work as well. + +django-nose requires nose 1.2.1 or later, and the `latest release`_ is +recommended. It follows the `Django's support policy`_, supporting: + + * Django 1.8 (LTS) with Python 2.7, 3.4, or 3.5 + * Django 1.9 with Python 2.7, 3.4, or 3.5 + +.. _latest release: https://pypi.python.org/pypi/nose +.. _Django's support policy: https://docs.djangoproject.com/en/1.8/internals/release-process/#supported-versions + +Development +----------- +:Code: https://github.com/django-nose/django-nose +:Issues: https://github.com/django-nose/django-nose/issues?state=open +:Docs: https://django-nose.readthedocs.org diff --git a/changelog.rst b/changelog.rst new file mode 100644 index 0000000..830a14f --- /dev/null +++ b/changelog.rst @@ -0,0 +1,113 @@ +Changelog +--------- + +1.4.3 (2015-12-28) +~~~~~~~~~~~~~~~~~~ +* Add Django 1.9 support +* Support long options without equals signs, such as "--attr selected" +* Support nose plugins using callback options +* Support nose options without default values (jsatt) +* Remove Django from install dependencies, to avoid accidental upgrades + (jsocol, willkg) +* Setting REUSE_DB to an empty value now disables REUSE_DB, instead of + enabling it (wdoekes) + +1.4.2 (2015-10-07) +~~~~~~~~~~~~~~~~~~ +* Warn against using REUSE_DB=1 and FastFixtureTestCase in docs +* REUSE_DB=1 uses new transaction management in Django 1.7, 1.8 (scottsexton) +* Try to avoid accidentally using production database with REUSE_DB=1 (alexjg, eroninjapan) +* Supported Django versions limited to current supported Django version 1.4, + 1.7, and 1.8, as well as relevant Python versions. + +1.4.1 (2015-06-29) +~~~~~~~~~~~~~~~~~~ +* Fix version number (ezarowny) +* Fix choice options, unbreaking nose-cover (aamirtharaj-rpx, jwhitlock) +* Support 1.8 app loading system (dgladkov) +* Support non-ASCII file names (singingwolfboy) +* Better PEP8'd assertion names (roganov) + +1.4 (2015-04-23) +~~~~~~~~~~~~~~~~ +* Django 1.8 support (timc3, adepue, jwhitlock) +* Support --testrunner option (st4lk) +* Fix REUSE_DB second run in py3k (edrmp) + +1.3 (2014-12-05) +~~~~~~~~~~~~~~~~ +* Django 1.6 and 1.7 support (conrado, co3k, Nepherhotep, mbertheau) +* Python 3.3 and 3.4 testing and support (frewsxcv, jsocol) + +1.2 (2013-07-23) +~~~~~~~~~~~~~~~~ +* Python 3 support (melinath and jonashaag) +* Django 1.5 compat (fabiosantoscode) + +1.1 (2012-05-19) +~~~~~~~~~~~~~~~~ +* Django TransactionTestCases don't clean up after themselves; they leave + junk in the DB and clean it up only on ``_pre_setup``. Thus, Django makes + sure these tests run last. Now django-nose does, too. This means one fewer + source of failures on existing projects. (Erik Rose) +* Add support for hygienic TransactionTestCases. (Erik Rose) +* Support models that are used only for tests. Just put them in any file + imported in the course of loading tests. No more crazy hacks necessary. + (Erik Rose) +* Make the fixture bundler more conservative, fixing some conceivable + situations in which fixtures would not appear as intended if a + TransactionTestCase found its way into the middle of a bundle. (Erik Rose) +* Fix an error that would surface when using SQLAlchemy with connection + pooling. (Roger Hu) +* Gracefully ignore the new ``--liveserver`` option introduced in Django 1.4; + don't let it through to nose. (Adam DePue) + +1.0 (2012-03-12) +~~~~~~~~~~~~~~~~ +* New fixture-bundling plugin for avoiding needless fixture setup (Erik Rose) +* Moved FastFixtureTestCase in from test-utils, so now all the + fixture-bundling stuff is in one library. (Erik Rose) +* Added the REUSE_DB setting for faster startup and shutdown. (Erik Rose) +* Fixed a crash when printing options with certain verbosities. (Daniel Abel) +* Broke hard dependency on MySQL. Support PostgreSQL. (Roger Hu) +* Support SQLite, both memory- and disk-based. (Roger Hu and Erik Rose) +* Nail down versions of the package requirements. (Daniel Mizyrycki) + +.. Omit older changes from package + +0.1.3 (2010-04-15) +~~~~~~~~~~~~~~~~~~ +* Even better coverage support (rozza) +* README fixes (carljm and ionelmc) +* optparse OptionGroups are handled better (outofculture) +* nose plugins are loaded before listing options + +0.1.2 (2010-08-14) +~~~~~~~~~~~~~~~~~~ +* run_tests API support (carjm) +* better coverage numbers (rozza & miracle2k) +* support for adding custom nose plugins (kumar303) + +0.1.1 (2010-06-01) +~~~~~~~~~~~~~~~~~~ +* Cleaner installation (Michael Fladischer) + +0.1 (2010-05-18) +~~~~~~~~~~~~~~~~ +* Class-based test runner (Antti Kaihola) +* Django 1.2 compatibility (Antti Kaihola) +* Mapping Django verbosity to nose verbosity + +0.0.3 (2009-12-31) +~~~~~~~~~~~~~~~~~~ +* Python 2.4 support (Blake Winton) +* GeoDjango spatial database support (Peter Baumgartner) +* Return the number of failing tests on the command line + +0.0.2 (2009-10-01) +~~~~~~~~~~~~~~~~~~ +* rst readme (Rob Madole) + +0.0.1 (2009-10-01) +~~~~~~~~~~~~~~~~~~ +* birth! diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index 0e24deb..0000000 --- a/changelog.txt +++ /dev/null @@ -1,28 +0,0 @@ -(Later entries are in the readme.) - -- 0.1.2 (08-14-10) - * run_tests API support (carjm) - * better coverage numbers (rozza & miracle2k) - * support for adding custom nose plugins (kumar303) - -- 0.1.1 (06-01-10) - * Cleaner installation (Michael Fladischer) - -- 0.1 (05-18-10) - * Class-based test runner (Antti Kaihola) - * Django 1.2 compatibility (Antti Kaihola) - * Mapping Django verbosity to nose verbosity - -- 0.0.3 (12-31-09) - - * Python 2.4 support (Blake Winton) - * GeoDjango spatial database support (Peter Baumgartner) - * Return the number of failing tests on the command line - -- 0.0.2 (10-01-09) - - * rst readme (Rob Madole) - -- 0.0.1 (10-01-09) - - * birth! diff --git a/contribute.json b/contribute.json index 8bbfab1..2b108f0 100644 --- a/contribute.json +++ b/contribute.json @@ -3,7 +3,7 @@ "description": "Django test runner using nose.", "repository": { "url": "https://github.com/django-nose/django-nose", - "license": "MPL2", + "license": "BSD", "tests": "https://travis-ci.org/django-nose/django-nose" }, "participate": { diff --git a/django_nose/__init__.py b/django_nose/__init__.py index cf2d924..a35003e 100644 --- a/django_nose/__init__.py +++ b/django_nose/__init__.py @@ -1,9 +1,12 @@ -VERSION = (1, 3, 0) -__version__ = '.'.join(map(str, VERSION)) - -from django_nose.runner import * -from django_nose.testcases import * +# coding: utf-8 +"""The django_nose module.""" +from __future__ import unicode_literals +from django_nose.runner import BasicNoseRunner, NoseTestSuiteRunner +from django_nose.testcases import FastFixtureTestCase +assert BasicNoseRunner +assert FastFixtureTestCase -# Django < 1.2 compatibility. +VERSION = (1, 4, 3) +__version__ = '.'.join(map(str, VERSION)) run_tests = run_gis_tests = NoseTestSuiteRunner diff --git a/django_nose/fixture_tables.py b/django_nose/fixture_tables.py index 7865e90..c727d8c 100644 --- a/django_nose/fixture_tables.py +++ b/django_nose/fixture_tables.py @@ -1,6 +1,9 @@ -"""A copy of Django 1.3.0's stock loaddata.py, adapted so that, instead of +"""Unload fixtures by truncating tables rather than rebuilding. + +A copy of Django 1.3.0's stock loaddata.py, adapted so that, instead of loading any data, it returns the tables referenced by a set of fixtures so we -can truncate them (and no others) quickly after we're finished with them.""" +can truncate them (and no others) quickly after we're finished with them. +""" import os import gzip @@ -10,7 +13,15 @@ from django.conf import settings from django.core import serializers from django.db import router, DEFAULT_DB_ALIAS -from django.db.models import get_apps + +try: + from django.db.models import get_apps +except ImportError: + from django.apps import apps + + def get_apps(): + """Emulate get_apps in Django 1.9 and later.""" + return [a.models_module for a in apps.get_app_configs()] try: import bz2 @@ -20,9 +31,12 @@ def tables_used_by_fixtures(fixture_labels, using=DEFAULT_DB_ALIAS): - """Act like Django's stock loaddata command, but, instead of loading data, + """Get tables used by a fixture. + + Acts like Django's stock loaddata command, but, instead of loading data, return an iterable of the names of the tables into which data would be - loaded.""" + loaded. + """ # Keep a count of the installed objects and fixtures fixture_count = 0 loaded_object_count = 0 @@ -33,12 +47,14 @@ class SingleZipReader(zipfile.ZipFile): def __init__(self, *args, **kwargs): zipfile.ZipFile.__init__(self, *args, **kwargs) if settings.DEBUG: - assert len(self.namelist()) == 1, "Zip-compressed fixtures must contain only one file." + assert len(self.namelist()) == 1, \ + "Zip-compressed fixtures must contain only one file." + def read(self): return zipfile.ZipFile.read(self, self.namelist()[0]) compression_types = { - None: file, + None: open, 'gz': gzip.GzipFile, 'zip': SingleZipReader } @@ -55,7 +71,9 @@ def read(self): # It's a models.py module app_module_paths.append(app.__file__) - app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths] + app_fixtures = [ + os.path.join(os.path.dirname(path), 'fixtures') + for path in app_module_paths] for fixture_label in fixture_labels: parts = fixture_label.split('.') @@ -118,13 +136,14 @@ def read(self): # stdout.write("Installing %s fixture '%s' from %s.\n" # % (format, fixture_name, humanize(fixture_dir))) try: - objects = serializers.deserialize(format, fixture, using=using) + objects = serializers.deserialize( + format, fixture, using=using) for obj in objects: objects_in_fixture += 1 - if router.allow_syncdb(using, obj.object.__class__): + cls = obj.object.__class__ + if router.allow_syncdb(using, cls): loaded_objects_in_fixture += 1 - tables.add( - obj.object.__class__._meta.db_table) + tables.add(cls._meta.db_table) loaded_object_count += loaded_objects_in_fixture fixture_object_count += objects_in_fixture label_found = True @@ -139,8 +158,8 @@ def read(self): return set() fixture.close() - # If the fixture we loaded contains 0 objects, assume that an - # error was encountered during fixture loading. + # If the fixture we loaded contains 0 objects, assume + # that an error was encountered during fixture loading. if objects_in_fixture == 0: # stderr.write( style.ERROR("No fixture data found # for '%s'. (File format may be invalid.)\n" % diff --git a/django_nose/management/__init__.py b/django_nose/management/__init__.py index e69de29..553a5f2 100644 --- a/django_nose/management/__init__.py +++ b/django_nose/management/__init__.py @@ -0,0 +1 @@ +"""django-nose management additions.""" diff --git a/django_nose/management/commands/__init__.py b/django_nose/management/commands/__init__.py index e69de29..5ec4f95 100644 --- a/django_nose/management/commands/__init__.py +++ b/django_nose/management/commands/__init__.py @@ -0,0 +1 @@ +"""django-nose management commands.""" diff --git a/django_nose/management/commands/test.py b/django_nose/management/commands/test.py index a5ed6ca..40390d7 100644 --- a/django_nose/management/commands/test.py +++ b/django_nose/management/commands/test.py @@ -1,26 +1,16 @@ +# coding: utf-8 """ -Add extra options from the test runner to the ``test`` command, so that you can -browse all the nose options from the command line. +Add extra options from the test runner to the ``test`` command. + +This enables browsing all the nose options from the command line. """ +from __future__ import unicode_literals + from django.conf import settings +from django.core.management.commands.test import Command from django.test.utils import get_runner -if 'south' in settings.INSTALLED_APPS: - from south.management.commands.test import Command -else: - from django.core.management.commands.test import Command - - -# Django < 1.2 compatibility -test_runner = settings.TEST_RUNNER -if test_runner.endswith('run_tests') or test_runner.endswith('run_gis_tests'): - import warnings - warnings.warn( - 'Use `django_nose.NoseTestSuiteRunner` instead of `%s`' % test_runner, - DeprecationWarning) - - TestRunner = get_runner(settings) if hasattr(TestRunner, 'options'): @@ -30,4 +20,6 @@ class Command(Command): - option_list = Command.option_list + tuple(extra_options) + """Implement the ``test`` command.""" + + option_list = getattr(Command, 'option_list', ()) + tuple(extra_options) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index 43f13e8..b164378 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -1,3 +1,7 @@ +# coding: utf-8 +"""Included django-nose plugins.""" +from __future__ import unicode_literals + import sys from nose.plugins.base import Plugin @@ -10,7 +14,7 @@ class AlwaysOnPlugin(Plugin): - """A plugin that takes no options and is always enabled""" + """A base plugin that takes no options and is always enabled.""" def options(self, parser, env): """Avoid adding a ``--with`` option for this plugin. @@ -18,39 +22,41 @@ def options(self, parser, env): We don't have any options, and this plugin is always enabled, so we don't want to use superclass's ``options()`` method which would add a ``--with-*`` option. - """ def configure(self, *args, **kw_args): + """Configure and enable this plugin.""" super(AlwaysOnPlugin, self).configure(*args, **kw_args) - self.enabled = True # Force this plugin to be always enabled. + self.enabled = True class ResultPlugin(AlwaysOnPlugin): - """Captures the TestResult object for later inspection + """Captures the TestResult object for later inspection. nose doesn't return the full test result object from any of its runner methods. Pass an instance of this plugin to the TestProgram and use ``result`` after running the tests to get the TestResult object. - """ + name = 'result' def finalize(self, result): + """Finalize test run by capturing the result.""" self.result = result class DjangoSetUpPlugin(AlwaysOnPlugin): - """Configures Django to set up and tear down the environment + """Configures Django to set up and tear down the environment. This allows coverage to report on all code imported and used during the initialization of the test runner. - """ + name = 'django setup' score = 150 def __init__(self, runner): + """Initialize the plugin with the test runner.""" super(DjangoSetUpPlugin, self).__init__() self.runner = runner self.sys_stdout = sys.stdout @@ -78,12 +84,16 @@ def prepareTest(self, test): sys.stdout = sys_stdout def finalize(self, result): + """Finalize test run by cleaning up databases and environment.""" self.runner.teardown_databases(self.old_names) self.runner.teardown_test_environment() class Bucketer(object): + """Collect tests into buckets with similar setup requirements.""" + def __init__(self): + """Initialize the test buckets.""" # { (frozenset(['users.json']), True): # [ContextSuite(...), ContextSuite(...)] } self.buckets = {} @@ -93,8 +103,11 @@ def __init__(self): self.remainder = [] def add(self, test): - """Put a test into a bucket according to its set of fixtures and the - value of its exempt_from_fixture_bundling attr.""" + """Add test into an initialization bucket. + + Tests are bucketed according to its set of fixtures and the + value of its exempt_from_fixture_bundling attr. + """ if is_subclass_at_all(test.context, FastFixtureTestCase): # We bucket even FFTCs that don't have any fixtures, but it # shouldn't matter. @@ -109,9 +122,11 @@ def add(self, test): class TestReorderer(AlwaysOnPlugin): """Reorder tests for various reasons.""" + name = 'django-nose-test-reorderer' def options(self, parser, env): + """Add --with-fixture-bundling to options.""" super(TestReorderer, self).options(parser, env) # pointless parser.add_option('--with-fixture-bundling', action='store_true', @@ -122,12 +137,12 @@ def options(self, parser, env): '[NOSE_WITH_FIXTURE_BUNDLING]') def configure(self, options, conf): + """Configure plugin, reading the with_fixture_bundling option.""" super(TestReorderer, self).configure(options, conf) self.should_bundle = options.with_fixture_bundling def _put_transaction_test_cases_last(self, test): - """Reorder tests in the suite so TransactionTestCase-based tests come - last. + """Reorder test suite so TransactionTestCase-based tests come last. Django has a weird design decision wherein TransactionTestCase doesn't clean up after itself. Instead, it resets the DB to a clean state only @@ -140,12 +155,9 @@ def _put_transaction_test_cases_last(self, test): after each unit test wouldn't necessarily clean up after doctests, so you'd have to clean on entry to a test anyway." was once uttered on #django-dev. - """ - def filthiness(test): - """Return a comparand based on whether a test is guessed to clean - up after itself. + """Return a score of how messy a test leaves the environment. Django's TransactionTestCase doesn't clean up the DB on teardown, but it's hard to guess whether subclasses (other than TestCase) do. @@ -173,7 +185,7 @@ def filthiness(test): test_class = test.context if (is_subclass_at_all(test_class, TestCase) or (is_subclass_at_all(test_class, TransactionTestCase) and - getattr(test_class, 'cleans_up_after_itself', False))): + getattr(test_class, 'cleans_up_after_itself', False))): return 1 return 2 @@ -183,8 +195,7 @@ def filthiness(test): return ContextSuite(flattened) def _bundle_fixtures(self, test): - """Reorder the tests in the suite so classes using identical - sets of fixtures are contiguous. + """Reorder tests to minimize fixture loading. I reorder FastFixtureTestCases so ones using identical sets of fixtures run adjacently. I then put attributes on them @@ -196,11 +207,9 @@ def _bundle_fixtures(self, test): nobody else, in practice, pays attention to the ``_fb`` advisory bits. We return those first, then any remaining tests in the order they were received. - """ def suite_sorted_by_fixtures(suite): - """Flatten and sort a tree of Suites by the ``fixtures`` members of - their contexts. + """Flatten and sort a tree of Suites by fixture. Add ``_fb_should_setup_fixtures`` and ``_fb_should_teardown_fixtures`` attrs to each test class to advise @@ -215,7 +224,8 @@ def suite_sorted_by_fixtures(suite): # Lay the bundles of common-fixture-having test classes end to end # in a single list so we can make a test suite out of them: flattened = [] - for ((fixtures, is_exempt), fixture_bundle) in bucketer.buckets.items(): + for (key, fixture_bundle) in bucketer.buckets.items(): + fixtures, is_exempt = key # Advise first and last test classes in each bundle to set up # and tear down fixtures and the rest not to: if fixtures and not is_exempt: diff --git a/django_nose/runner.py b/django_nose/runner.py index b99d7fb..e4055a8 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -1,3 +1,4 @@ +# coding: utf-8 """Django test runner that invokes nose. You can use... :: @@ -7,60 +8,29 @@ in settings.py for arguments that you want always passed to nose. """ -from __future__ import print_function +from __future__ import print_function, unicode_literals + import os import sys -from optparse import make_option +from importlib import import_module +from optparse import NO_DEFAULT from types import MethodType -import django +from django import setup +from django.apps import apps from django.conf import settings from django.core import exceptions from django.core.management.base import BaseCommand from django.core.management.color import no_style from django.core.management.commands.loaddata import Command from django.db import connections, transaction, DEFAULT_DB_ALIAS - -try: - from django.db.backends.base.creation import BaseDatabaseCreation -except ImportError: - # Django < 1.7 - from django.db.backends.creation import BaseDatabaseCreation - -try: - from importlib import import_module -except ImportError: - # Django < 1.7 and Python < 2.7 - from django.utils.importlib import import_module - -try: - from django.apps import apps -except ImportError: - # Django < 1.7 - from django.db.models.loading import cache as apps - -import nose.core +from django.test.runner import DiscoverRunner from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, TestReorderer from django_nose.utils import uses_mysql +import nose.core -try: - any -except NameError: - def any(iterable): - for element in iterable: - if element: - return True - return False - -try: - from django.test.runner import DiscoverRunner -except ImportError: - # Django < 1.8 - from django.test.simple import DjangoTestSuiteRunner as DiscoverRunner - - -__all__ = ['BasicNoseRunner', 'NoseTestSuiteRunner'] +__all__ = ('BasicNoseRunner', 'NoseTestSuiteRunner') # This is a table of Django's "manage.py test" options which @@ -76,18 +46,6 @@ def translate_option(opt): return OPTION_TRANSLATION.get(opt, opt) -# Django v1.2 does not have a _get_test_db_name() function. -if not hasattr(BaseDatabaseCreation, '_get_test_db_name'): - def _get_test_db_name(self): - TEST_DATABASE_PREFIX = 'test_' - - if self.connection.settings_dict['TEST_NAME']: - return self.connection.settings_dict['TEST_NAME'] - return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME'] - - BaseDatabaseCreation._get_test_db_name = _get_test_db_name - - def _get_plugins_from_settings(): plugins = (list(getattr(settings, 'NOSE_PLUGINS', [])) + ['django_nose.plugin.TestReorderer']) @@ -96,71 +54,178 @@ def _get_plugins_from_settings(): dot = plug_path.rindex('.') except ValueError: raise exceptions.ImproperlyConfigured( - "%s isn't a Nose plugin module" % plug_path) + "%s isn't a Nose plugin module" % plug_path) p_mod, p_classname = plug_path[:dot], plug_path[dot + 1:] try: mod = import_module(p_mod) except ImportError as e: raise exceptions.ImproperlyConfigured( - 'Error importing Nose plugin module %s: "%s"' % (p_mod, e)) + 'Error importing Nose plugin module %s: "%s"' % (p_mod, e)) try: p_class = getattr(mod, p_classname) except AttributeError: raise exceptions.ImproperlyConfigured( - 'Nose plugin module "%s" does not define a "%s"' % - (p_mod, p_classname)) + 'Nose plugin module "%s" does not define a "%s"' % + (p_mod, p_classname)) yield p_class() -def _get_options(): - """Return all nose options that don't conflict with django options.""" - cfg_files = nose.core.all_config_files() - manager = nose.core.DefaultPluginManager() - config = nose.core.Config(env=os.environ, files=cfg_files, plugins=manager) - config.plugins.addPlugins(list(_get_plugins_from_settings())) - options = config.getParser()._get_all_options() +class BaseRunner(DiscoverRunner): + """Runner that translates nose optparse arguments to argparse. - # copy nose's --verbosity option and rename to --nose-verbosity - verbosity = [o for o in options if o.get_opt_string() == '--verbosity'][0] - verbosity_attrs = dict((attr, getattr(verbosity, attr)) - for attr in verbosity.ATTRS - if attr not in ('dest', 'metavar')) - options.append(make_option('--nose-verbosity', - dest='nose_verbosity', - metavar='NOSE_VERBOSITY', - **verbosity_attrs)) - - # Django 1.6 introduces a "--pattern" option, which is shortened into "-p" - # do not allow "-p" to collide with nose's "--plugins" option. - plugins_option = [o for o in options if o.get_opt_string() == '--plugins'][0] - plugins_option._short_opts.remove('-p') + Django 1.8 and later uses argparse.ArgumentParser. Nose's optparse + arguments need to be translated to this format, so that the Django + command line parsing will pass. This parsing is (mostly) thrown out, + and reassembled into command line arguments for nose to reparse. + """ - django_opts = [opt.dest for opt in BaseCommand.option_list] + ['version'] - return tuple(o for o in options if o.dest not in django_opts and - o.action != 'help') + # Don't pass the following options to nosetests + django_opts = [ + '--noinput', '--liveserver', '-p', '--pattern', '--testrunner', + '--settings', + # 1.8 arguments + '--keepdb', '--reverse', '--debug-sql', + # 1.9 arguments + '--parallel', + ] + + # + # For optparse -> argparse conversion + # + # Option strings to remove from Django options if found + _argparse_remove_options = ( + '-p', # Short arg for nose's --plugins, not Django's --patterns + '-d', # Short arg for nose's --detailed-errors, not Django's + # --debug-sql + ) + + # Convert nose optparse options to argparse options + _argparse_type = { + 'int': int, + 'float': float, + 'complex': complex, + 'string': str, + 'choice': str, + } + # If optparse has a None argument, omit from call to add_argument + _argparse_omit_if_none = ( + 'action', 'nargs', 'const', 'default', 'type', 'choices', + 'required', 'help', 'metavar', 'dest') + + # Always ignore these optparse arguments + # Django will parse without calling the callback + # nose will then reparse with the callback + _argparse_callback_options = ( + 'callback', 'callback_args', 'callback_kwargs') + + # Keep track of nose options with nargs=1 + _has_nargs = set(['--verbosity']) + + @classmethod + def add_arguments(cls, parser): + """Convert nose's optparse arguments to argparse.""" + super(BaseRunner, cls).add_arguments(parser) + + # Read optparse options for nose and plugins + cfg_files = nose.core.all_config_files() + manager = nose.core.DefaultPluginManager() + config = nose.core.Config( + env=os.environ, files=cfg_files, plugins=manager) + config.plugins.addPlugins(list(_get_plugins_from_settings())) + options = config.getParser()._get_all_options() + + # Gather existing option strings` + django_options = set() + for action in parser._actions: + for override in cls._argparse_remove_options: + if override in action.option_strings: + # Emulate parser.conflict_handler='resolve' + parser._handle_conflict_resolve( + None, ((override, action),)) + django_options.update(action.option_strings) + + # Process nose optparse options + for option in options: + # Skip any options also in Django options + opt_long = option.get_opt_string() + if opt_long in django_options: + continue + if option._short_opts: + opt_short = option._short_opts[0] + if opt_short in django_options: + continue + else: + opt_short = None + + # Rename nose's --verbosity to --nose-verbosity + if opt_long == '--verbosity': + opt_long = '--nose-verbosity' + + # Convert optparse attributes to argparse attributes + option_attrs = {} + for attr in option.ATTRS: + # Ignore callback options + if attr in cls._argparse_callback_options: + continue + + value = getattr(option, attr) + + if attr == 'default' and value == NO_DEFAULT: + continue + + # Rename options for nose's --verbosity + if opt_long == '--nose-verbosity': + if attr == 'dest': + value = 'nose_verbosity' + elif attr == 'metavar': + value = 'NOSE_VERBOSITY' + + # Omit arguments that are None, use default + if attr in cls._argparse_omit_if_none and value is None: + continue + + # Convert type from optparse string to argparse type + if attr == 'type': + value = cls._argparse_type[value] + + # Convert action='callback' to action='store' + if attr == 'action' and value == 'callback': + action = 'store' + + # Keep track of nargs=1 + if attr == 'nargs': + assert value == 1, ( + 'argparse option nargs=%s is not supported' % + value) + cls._has_nargs.add(opt_long) + if opt_short: + cls._has_nargs.add(opt_short) + + # Pass converted attribute to optparse option + option_attrs[attr] = value + + # Add the optparse argument + if opt_short: + parser.add_argument(opt_short, opt_long, **option_attrs) + else: + parser.add_argument(opt_long, **option_attrs) -class BasicNoseRunner(DiscoverRunner): - """Facade that implements a nose runner in the guise of a Django runner +class BasicNoseRunner(BaseRunner): + """Facade that implements a nose runner in the guise of a Django runner. You shouldn't have to use this directly unless the additions made by ``NoseTestSuiteRunner`` really bother you. They shouldn't, because they're all off by default. - """ - __test__ = False - # Replace the builtin command options with the merged django/nose options: - options = _get_options() - - # Not add following options to nosetests - django_opts = ['--noinput', '--liveserver', '-p', '--pattern', - '--testrunner'] + __test__ = False def run_suite(self, nose_argv): + """Run the test suite.""" result_plugin = ResultPlugin() plugins_to_add = [DjangoSetUpPlugin(self), result_plugin, @@ -169,11 +234,7 @@ def run_suite(self, nose_argv): for plugin in _get_plugins_from_settings(): plugins_to_add.append(plugin) - try: - django.setup() - except AttributeError: - # Setup isn't necessary in Django < 1.7 - pass + setup() nose.core.TestProgram(argv=nose_argv, exit=False, addplugins=plugins_to_add) @@ -205,7 +266,6 @@ def run_tests(self, test_labels, extra_tests=None): but the extra tests will not be run. Maybe later. Returns the number of tests that failed. - """ nose_argv = (['nosetests'] + list(test_labels)) if hasattr(settings, 'NOSE_ARGS'): @@ -213,13 +273,29 @@ def run_tests(self, test_labels, extra_tests=None): # Skip over 'manage.py test' and any arguments handled by django. django_opts = self.django_opts[:] - for opt in BaseCommand.option_list: + for opt in getattr(BaseCommand, 'option_list', []): django_opts.extend(opt._long_opts) django_opts.extend(opt._short_opts) - nose_argv.extend(translate_option(opt) for opt in sys.argv[1:] - if opt.startswith('-') - and not any(opt.startswith(d) for d in django_opts)) + # Recreate the arguments in a nose-compatible format + arglist = sys.argv[1:] + has_nargs = getattr(self, '_has_nargs', set(['--verbosity'])) + while arglist: + opt = arglist.pop(0) + if not opt.startswith('-'): + # Discard test labels + continue + if any(opt.startswith(d) for d in django_opts): + # Discard options handled by Djangp + continue + + trans_opt = translate_option(opt) + nose_argv.append(trans_opt) + + if opt in has_nargs: + # Handle arguments without an equals sign + opt_value = arglist.pop(0) + nose_argv.append(opt_value) # if --nose-verbosity was omitted, pass Django verbosity to nose if ('--verbosity' not in nose_argv and @@ -238,11 +314,10 @@ def run_tests(self, test_labels, extra_tests=None): def _foreign_key_ignoring_handle(self, *fixture_labels, **options): - """Wrap the the stock loaddata to ignore foreign key - checks so we can load circular references from fixtures. - - This is monkeypatched into place in setup_databases(). + """Wrap the the stock loaddata to ignore foreign key checks. + This allows loading circular references from fixtures, and is + monkeypatched into place in setup_databases(). """ using = options.get('database', DEFAULT_DB_ALIAS) commit = options.get('commit', True) @@ -263,13 +338,13 @@ def _foreign_key_ignoring_handle(self, *fixture_labels, **options): connection.close() -def _skip_create_test_db(self, verbosity=1, autoclobber=False, serialize=True): - """``create_test_db`` implementation that skips both creation and flushing +def _skip_create_test_db(self, verbosity=1, autoclobber=False, serialize=True, + keepdb=True): + """``create_test_db`` implementation that skips both creation and flushing. The idea is to re-use the perfectly good test DB already created by an earlier test run, cutting the time spent before any tests run from 5-13s (depending on your I/O luck) down to 3. - """ # Notice that the DB supports transactions. Originally, this was done in # the method this overrides. The confirm method was added in Django v1.3 @@ -288,13 +363,12 @@ def _skip_create_test_db(self, verbosity=1, autoclobber=False, serialize=True): def _reusing_db(): - """Return whether the ``REUSE_DB`` flag was passed""" - return os.getenv('REUSE_DB', 'false').lower() in ('true', '1', '') + """Return whether the ``REUSE_DB`` flag was passed.""" + return os.getenv('REUSE_DB', 'false').lower() in ('true', '1') def _can_support_reuse_db(connection): - """Return whether it makes any sense to - use REUSE_DB with the backend of a connection.""" + """Return True if REUSE_DB is a sensible option for the backend.""" # Perhaps this is a SQLite in-memory DB. Those are created implicitly when # you try to connect to them, so our usual test doesn't work. return not connection.creation._get_test_db_name() == ':memory:' @@ -304,7 +378,6 @@ def _should_create_database(connection): """Return whether we should recreate the given DB. This is true if the DB doesn't exist or the REUSE_DB env var isn't truthy. - """ # TODO: Notice when the Model classes change and return True. Worst case, # we can generate sqlall and hash it, though it's a bit slow (2 secs) and @@ -317,6 +390,13 @@ def _should_create_database(connection): # Notice whether the DB exists, and create it if it doesn't: try: + # Connections are cached by some backends, if other code has connected + # to the database previously under a different database name the + # cached connection will be used and no exception will be raised. + # Avoiding this by closing connections and setting to null + for connection in connections.all(): + connection.close() + connection.connection = None connection.cursor() except Exception: # TODO: Be more discerning but still DB agnostic. return True @@ -324,11 +404,10 @@ def _should_create_database(connection): def _mysql_reset_sequences(style, connection): - """Return a list of SQL statements needed to - reset all sequences for Django tables.""" + """Return a SQL statements needed to reset Django tables.""" tables = connection.introspection.django_table_names(only_existing=True) flush_statements = connection.ops.sql_flush( - style, tables, connection.introspection.sequence_list()) + style, tables, connection.introspection.sequence_list()) # connection.ops.sequence_reset_sql() is not implemented for MySQL, # and the base class just returns []. TODO: Implement it by pulling @@ -341,14 +420,13 @@ def _mysql_reset_sequences(style, connection): class NoseTestSuiteRunner(BasicNoseRunner): - """A runner that optionally skips DB creation + """A runner that optionally skips DB creation. Monkeypatches connection.creation to let you skip creating databases if they already exist. Your tests will start up much faster. To opt into this behavior, set the environment variable ``REUSE_DB`` to - something that isn't "0" or "false" (case insensitive). - + "1" or "true" (case insensitive). """ def _get_models_for_connection(self, connection): @@ -358,6 +436,7 @@ def _get_models_for_connection(self, connection): m._meta.db_table in tables] def setup_databases(self): + """Setup databases, skipping DB creation if requested and possible.""" for alias in connections: connection = connections[alias] creation = connection.creation @@ -392,21 +471,22 @@ def setup_databases(self): style, connection) else: reset_statements = connection.ops.sequence_reset_sql( - style, self._get_models_for_connection(connection)) + style, self._get_models_for_connection(connection)) - for reset_statement in reset_statements: - cursor.execute(reset_statement) - - # Django v1.3 (https://code.djangoproject.com/ticket/9964) - # starts using commit_unless_managed() for individual - # connections. Backwards compatibility for Django 1.2 is to use - # the generic transaction function. - transaction.commit_unless_managed(using=connection.alias) + if hasattr(transaction, "atomic"): + with transaction.atomic(using=connection.alias): + for reset_statement in reset_statements: + cursor.execute(reset_statement) + else: + # Django < 1.6 + for reset_statement in reset_statements: + cursor.execute(reset_statement) + transaction.commit_unless_managed(using=connection.alias) # Each connection has its own creation object, so this affects # only a single connection: creation.create_test_db = MethodType( - _skip_create_test_db, creation, creation.__class__) + _skip_create_test_db, creation) Command.handle = _foreign_key_ignoring_handle @@ -418,5 +498,5 @@ def teardown_databases(self, *args, **kwargs): """Leave those poor, reusable databases alone if REUSE_DB is true.""" if not _reusing_db(): return super(NoseTestSuiteRunner, self).teardown_databases( - *args, **kwargs) + *args, **kwargs) # else skip tearing down the DB so we can reuse it next time diff --git a/django_nose/testcases.py b/django_nose/testcases.py index a899ec3..2603f18 100644 --- a/django_nose/testcases.py +++ b/django_nose/testcases.py @@ -1,18 +1,26 @@ +# coding: utf-8 +"""TestCases that enable extra django-nose functionality.""" +from __future__ import unicode_literals +import os +import sys + from django import test from django.conf import settings +from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.core import cache, mail +from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.db import connections, DEFAULT_DB_ALIAS, transaction - +from django.utils import six from django_nose.fixture_tables import tables_used_by_fixtures from django_nose.utils import uses_mysql -__all__ = ['FastFixtureTestCase'] +__all__ = ['FastFixtureTestCase', 'FastFixtureLiveServerTestCase'] class FastFixtureTestCase(test.TransactionTestCase): - """Test case that loads fixtures once and for all rather than once per test + """Test case that loads fixtures once rather than once per test. Using this can save huge swaths of time while still preserving test isolation. Fixture data is loaded at class setup time, and the transaction @@ -30,9 +38,10 @@ class FastFixtureTestCase(test.TransactionTestCase): For best speed, group tests using the same fixtures into as few classes as possible. Better still, don't do that, and instead use the fixture-bundling plugin from django-nose, which does it dynamically at test time. - """ + cleans_up_after_itself = True # This is the good kind of puppy. + transaction_state = {} # record whether state of autocommit for each database @classmethod def setUpClass(cls): @@ -40,31 +49,29 @@ def setUpClass(cls): if not test.testcases.connections_support_transactions(): raise NotImplementedError('%s supports only DBs with transaction ' 'capabilities.' % cls.__name__) - for db in cls._databases(): - # These MUST be balanced with one leave_* each: - transaction.enter_transaction_management(using=db) - # Don't commit unless we say so: - transaction.managed(True, using=db) + # set all database to 'not autocommit' and store their original autocommit state + for db in cls._databases(): + cls.transaction_state[db] = transaction.get_autocommit(using=db) + transaction.set_autocommit(False, using=db) cls._fixture_setup() @classmethod def tearDownClass(cls): """Truncate the world, and turn manual commit management back off.""" cls._fixture_teardown() + + # commit all changes in this transaction and set autocommit state back to original for db in cls._databases(): - # Finish off any transactions that may have happened in - # tearDownClass in a child method. - if transaction.is_dirty(using=db): - transaction.commit(using=db) - transaction.leave_transaction_management(using=db) + transaction.commit(using=db) + transaction.set_autocommit(cls.transaction_state[db], using=db) @classmethod def _fixture_setup(cls): """Load fixture data, and commit.""" for db in cls._databases(): if (hasattr(cls, 'fixtures') and - getattr(cls, '_fb_should_setup_fixtures', True)): + getattr(cls, '_fb_should_setup_fixtures', True)): # Iff the fixture-bundling test runner tells us we're the first # suite having these fixtures, set them up: call_command('loaddata', *cls.fixtures, **{'verbosity': 0, @@ -118,10 +125,8 @@ def _pre_setup(self): cache.cache.clear() settings.TEMPLATE_DEBUG = settings.DEBUG = False - test.testcases.disable_transaction_methods() - self.client = self.client_class() - #self._fixture_setup() + # self._fixture_setup() self._urlconf_setup() mail.outbox = [] @@ -138,12 +143,241 @@ def _post_teardown(self): """ # Rollback any mutations made by tests: - test.testcases.restore_transaction_methods() for db in self._databases(): transaction.rollback(using=db) + transaction.get_connection(using=db).needs_rollback = False - self._urlconf_teardown() + # We do not need to close the connection here to prevent + # http://code.djangoproject.com/ticket/7572, since we commit, not + # rollback, the test fixtures and thus any cursor startup statements. + + # Don't call through to superclass, because that would call + # _fixture_teardown() and close the connection. + @classmethod + def _databases(cls): + if getattr(cls, 'multi_db', False): + return connections + else: + return [DEFAULT_DB_ALIAS] + + +class FastFixtureLiveServerTestCase(StaticLiveServerTestCase): + + """ + Similar to django_nose.testcases.FastFixtureTestCase, except meant for Live Server tests. + + Test case that loads fixtures once and for all rather than once per test + Using this can save huge swaths of time while still preserving test + isolation. Fixture data is loaded at class setup time, and the transaction + is committed. Commit and rollback methods are then monkeypatched away (like + in Django's standard TestCase), and each test is run. After each test, the + monkeypatching is temporarily undone, and a rollback is issued, returning + the DB content to the pristine fixture state. Finally, upon class teardown, + the DB is restored to a post-syncdb-like state by deleting the contents of + any table that had been touched by a fixture (keeping infrastructure tables + like django_content_type and auth_permission intact). + + Also includes the setupclass and teardownclass stuff from the Django LiveServerTestCase + to start and stop a live server. + + Note that this is like Django's TestCase, not its TransactionTestCase, in + that you cannot do your own commits or rollbacks from within tests. + + For best speed, group tests using the same fixtures into as few classes as + possible. Better still, don't do that, and instead use the fixture-bundling + plugin from django-nose, which does it dynamically at test time. + """ + + # from django_nose.testcases.FastFixtureTestCase + cleans_up_after_itself = True # This is the good kind of puppy. + transaction_state = {} # record whether state of autocommit for each database + + @classmethod + def setUpClass(cls): + """ a mix of django.test.testcases.LiveServerTestCase and django_nose.testcases.FastFixtureTestCase """ + + # from django.test.testcases.LiveServerTestCase + connections_override = {} + for conn in connections.all(): + # If using in-memory sqlite databases, pass the connections to + # the server thread. + if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite') + and conn.settings_dict['NAME'] == ':memory:'): + # Explicitly enable thread-shareability for this connection + conn.allow_thread_sharing = True + connections_override[conn.alias] = conn + + # Launch the live server's thread + specified_address = os.environ.get( + 'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081') + + # The specified ports may be of the form '8000-8010,8080,9200-9300' + # i.e. a comma-separated list of ports or ranges of ports, so we break + # it down into a detailed list of all possible ports. + possible_ports = [] + try: + host, port_ranges = specified_address.split(':') + for port_range in port_ranges.split(','): + # A port range can be of either form: '8000' or '8000-8010'. + extremes = list(map(int, port_range.split('-'))) + assert len(extremes) in [1, 2] + if len(extremes) == 1: + # Port range of the form '8000' + possible_ports.append(extremes[0]) + else: + # Port range of the form '8000-8010' + for port in range(extremes[0], extremes[1] + 1): + possible_ports.append(port) + except Exception: + msg = 'Invalid address ("%s") for live server.' % specified_address + six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2]) + cls.server_thread = test.testcases.LiveServerThread( + host, possible_ports, cls.static_handler, connections_override=connections_override) + cls.server_thread.daemon = True + cls.server_thread.start() + # Wait for the live server to be ready + cls.server_thread.is_ready.wait() + if cls.server_thread.error: + # Clean up behind ourselves, since tearDownClass won't get called in + # case of errors. + cls._tearDownClassInternal() + raise cls.server_thread.error + + # from django_nose.testcases.FastFixtureTestCase + # Turn on manual commits. Load and commit the fixtures. + if not test.testcases.connections_support_transactions(): + raise NotImplementedError('%s supports only DBs with transaction ' + 'capabilities.' % cls.__name__) + + # set all database to 'not autocommit' and store their original autocommit state + for db in cls._databases(): + cls.transaction_state[db] = transaction.get_autocommit(using=db) + transaction.set_autocommit(False, using=db) + cls._fixture_setup() + + @classmethod + def _tearDownClassInternal(cls): + """ from django.test.testcases.LiveServerTestCase """ + # There may not be a 'server_thread' attribute if setUpClass() for some + # reasons has raised an exception. + if hasattr(cls, 'server_thread'): + # Terminate the live server's thread + cls.server_thread.terminate() + cls.server_thread.join() + + # Restore sqlite connections' non-sharability + for conn in connections.all(): + if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite') + and conn.settings_dict['NAME'] == ':memory:'): + conn.allow_thread_sharing = False + + @classmethod + def tearDownClass(cls): + """ a mix of django.test.testcases.LiveServerTestCase and django_nose.testcases.FastFixtureTestCase """ + + cls._tearDownClassInternal() # from django.test.testcases.LiveServerTestCase + + # from django_nose.testcases.FastFixtureTestCase + # Truncate the world, and turn manual commit management back off. + cls._fixture_teardown() + + # commit all changes in this transaction and set autocommit state back to original + for db in cls._databases(): + transaction.commit(using=db) + transaction.set_autocommit(cls.transaction_state[db], using=db) + + @classmethod + def _fixture_setup(cls): + """ + From django_nose.testcases.FastFixtureTestCase, except for first time run fixture teardown fix + Load fixture data, and commit. + """ + for db in cls._databases(): + if (hasattr(cls, 'fixtures') and + getattr(cls, '_fb_should_setup_fixtures', True)): + # Iff the fixture-bundling test runner tells us we're the first + # suite having these fixtures, set them up: + cls._fixture_teardown() # dfeinzeig added to deal with case of first time run, clearing out any fixtures/data loaded via migrations + call_command('loaddata', *cls.fixtures, **{'verbosity': 0, + 'commit': False, + 'database': db}) + # No matter what, to preserve the effect of cursor start-up + # statements... + transaction.commit(using=db) + + @classmethod + def _fixture_teardown(cls): + """ + From django_nose.testcases.FastFixtureTestCase + Empty (only) the tables we loaded fixtures into, then commit. + """ + if hasattr(cls, 'fixtures') and \ + getattr(cls, '_fb_should_teardown_fixtures', True): + # If the fixture-bundling test runner advises us that the next test + # suite is going to reuse these fixtures, don't tear them down. + for db in cls._databases(): + tables = tables_used_by_fixtures(cls.fixtures, using=db) + # TODO: Think about respecting _meta.db_tablespace, not just + # db_table. + if tables: + connection = connections[db] + cursor = connection.cursor() + + # TODO: Rather than assuming that anything added to by a + # fixture can be emptied, remove only what the fixture + # added. This would probably solve input.mozilla.com's + # failures (since worked around) with Site objects; they + # were loading additional Sites with a fixture, and then + # the Django-provided example.com site was evaporating. + if uses_mysql(connection): + cursor.execute('SET FOREIGN_KEY_CHECKS=0') + for table in tables: + # Truncate implicitly commits. + cursor.execute('TRUNCATE `%s`' % table) + # TODO: necessary? + cursor.execute('SET FOREIGN_KEY_CHECKS=1') + else: + for table in tables: + cursor.execute('DELETE FROM %s' % table) + + transaction.commit(using=db) + # cursor.close() # Should be unnecessary, since we committed + # any environment-setup statements that come with opening a new + # cursor when we committed the fixtures. + + def _pre_setup(self): + """ + From django_nose.testcases.FastFixtureTestCase + Disable transaction methods, and clear some globals. + """ + # Repeat stuff from TransactionTestCase, because I'm not calling its + # _pre_setup, because that would load fixtures again. + cache.cache.clear() + settings.TEMPLATE_DEBUG = settings.DEBUG = False + self.client = self.client_class() + # self._fixture_setup() + self._urlconf_setup() + mail.outbox = [] + + # Clear site cache in case somebody's mutated Site objects and then + # cached the mutated stuff: + from django.contrib.sites.models import Site + Site.objects.clear_cache() + + def _post_teardown(self): + """ + From django_nose.testcases.FastFixtureTestCase + Re-enable transaction methods, and roll back any changes. + Rollback clears any DB changes made by the test so the original fixture + data is again visible. + """ + # Rollback any mutations made by tests: + for db in self._databases(): + transaction.rollback(using=db) + transaction.get_connection(using=db).needs_rollback = False + + self._urlconf_teardown() # We do not need to close the connection here to prevent # http://code.djangoproject.com/ticket/7572, since we commit, not # rollback, the test fixtures and thus any cursor startup statements. @@ -153,7 +387,9 @@ def _post_teardown(self): @classmethod def _databases(cls): + """ From django_nose.testcases.FastFixtureTestCase """ if getattr(cls, 'multi_db', False): return connections else: return [DEFAULT_DB_ALIAS] + diff --git a/django_nose/tools.py b/django_nose/tools.py index 50fd4d8..4469691 100644 --- a/django_nose/tools.py +++ b/django_nose/tools.py @@ -1,74 +1,90 @@ +# coding: utf-8 # vim: tabstop=4 expandtab autoindent shiftwidth=4 fileencoding=utf-8 +"""Provides Nose and Django test case assert functions.""" -""" -Provides Nose and Django test case assert functions -""" +from __future__ import unicode_literals -from django.test.testcases import TransactionTestCase -from django.core import mail +def _get_nose_vars(): + """Collect assert_*, ok_, and eq_ from nose.tools.""" + from nose import tools + new_names = {} + for t in dir(tools): + if t.startswith('assert_') or t in ('ok_', 'eq_'): + new_names[t] = getattr(tools, t) + return new_names -import re +for _name, _value in _get_nose_vars().items(): + vars()[_name] = _value -## Python -from nose import tools -for t in dir(tools): - if t.startswith('assert_'): - vars()[t] = getattr(tools, t) +def _get_django_vars(): + """Collect assert_* methods from Django's TransactionTestCase.""" + import re + from django.test.testcases import TransactionTestCase + camelcase = re.compile('([a-z][A-Z]|[A-Z][a-z])') -## Django + def insert_underscore(m): + """Insert an appropriate underscore into the name.""" + a, b = m.group(0) + if b.islower(): + return '_{}{}'.format(a, b) + else: + return '{}_{}'.format(a, b) -caps = re.compile('([A-Z])') + def pep8(name): + """Replace camelcase name with PEP8 equivalent.""" + return str(camelcase.sub(insert_underscore, name).lower()) -def pep8(name): - return caps.sub(lambda m: '_' + m.groups()[0].lower(), name) + class Dummy(TransactionTestCase): + """A dummy test case for gathering current assertion helpers.""" + def nop(): + """A dummy test to get an initialized test case.""" + pass + dummy_test = Dummy('nop') -class Dummy(TransactionTestCase): - def nop(): - pass -_t = Dummy('nop') + new_names = {} + for assert_name in [at for at in dir(dummy_test) + if at.startswith('assert') and '_' not in at]: + pepd = pep8(assert_name) + new_names[pepd] = getattr(dummy_test, assert_name) + return new_names -for at in [ at for at in dir(_t) - if at.startswith('assert') and not '_' in at ]: - pepd = pep8(at) - vars()[pepd] = getattr(_t, at) -del Dummy -del _t -del pep8 +for _name, _value in _get_django_vars().items(): + vars()[_name] = _value -## New -def assert_code(response, status_code, msg_prefix=''): - """Asserts the response was returned with the given status code - """ +# +# Additional assertions +# +def assert_code(response, status_code, msg_prefix=''): + """Assert the response was returned with the given status code.""" if msg_prefix: msg_prefix = '%s: ' % msg_prefix assert response.status_code == status_code, \ - 'Response code was %d (expected %d)' % \ - (response.status_code, status_code) + 'Response code was %d (expected %d)' % ( + response.status_code, status_code) -def assert_ok(response, msg_prefix=''): - """Asserts the response was returned with status 200 (OK) - """ +def assert_ok(response, msg_prefix=''): + """Assert the response was returned with status 200 (OK).""" return assert_code(response, 200, msg_prefix=msg_prefix) + def assert_mail_count(count, msg=None): """Assert the number of emails sent. + The message here tends to be long, so allow for replacing the whole thing instead of prefixing. """ + from django.core import mail if msg is None: msg = ', '.join([e.subject for e in mail.outbox]) msg = '%d != %d %s' % (len(mail.outbox), count, msg) - assert_equals(len(mail.outbox), count, msg) - -# EOF - - + # assert_equals is dynamicaly added above + assert_equals(len(mail.outbox), count, msg) # nopep8 diff --git a/django_nose/utils.py b/django_nose/utils.py index 5c96ebd..139ae3a 100644 --- a/django_nose/utils.py +++ b/django_nose/utils.py @@ -1,5 +1,12 @@ +# coding: utf-8 +"""django-nose utility methods.""" +from __future__ import unicode_literals + + def process_tests(suite, process): - """Given a nested disaster of [Lazy]Suites, traverse to the first level + """Find and process the suite with setup/teardown methods. + + Given a nested disaster of [Lazy]Suites, traverse to the first level that has setup or teardown, and do something to them. If we were to traverse all the way to the leaves (the Tests) @@ -17,10 +24,9 @@ def process_tests(suite, process): :arg process: The thing to call once we get to a leaf or a test with setup or teardown - """ if (not hasattr(suite, '_tests') or - (hasattr(suite, 'hasFixtures') and suite.hasFixtures())): + (hasattr(suite, 'hasFixtures') and suite.hasFixtures())): # We hit a Test or something with setup, so do the thing. (Note that # "fixtures" here means setup or teardown routines, not Django # fixtures.) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..ad0b419 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-nose.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-nose.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-nose" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-nose" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/.keep b/docs/_static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..ad3dbc7 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../changelog.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..04b49df --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +"""django-nose build configuration file. + +Created by sphinx-quickstart on Mon Jul 21 13:24:51 2014. + +This file is execfile()d with the current directory set to its +containing dir. + +Note that not all possible configuration values are present in this +autogenerated file. + +All configuration values have a default; values that are commented out +serve to show the default. +""" +from __future__ import unicode_literals +from datetime import date +import sys +import os + +cwd = os.getcwd() +parent = os.path.dirname(cwd) +sys.path.append(parent) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") + +from django_nose import __version__ # flake8: noqa + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'django-nose' +copyright = ( + '2010-%d, Jeff Balogh and the django-nose team.' % date.today().year) + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = __version__ +# The full version, including alpha/beta/rc tags. +release = __version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-nose-doc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'django-nose.tex', 'django-nose Documentation', + 'Jeff Balogh and the django-nose team', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'django-nose', 'django-nose Documentation', + ['Jeff Balogh', 'the django-nose team'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'django-nose', 'django-nose Documentation', + 'Jeff Balogh and the django-nose team', 'django-nose', + 'Makes your Django tests simple and snappy') +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..3bdd7dc --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e73b91c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,29 @@ +.. django-nose documentation master file + +=========== +django-nose +=========== + +.. include:: ../README.rst + :start-after: .. Omit badges from docs + +Contents +-------- + +.. toctree:: + :maxdepth: 2 + + installation + usage + upgrading + contributing + authors + changelog + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..52327c4 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,33 @@ +Installation +------------ + +You can get django-nose from PyPI with... : + +.. code-block:: shell + + $ pip install django-nose + +The development version can be installed with... : + +.. code-block:: shell + + $ pip install -e git://github.com/django-nose/django-nose.git#egg=django-nose + +Since django-nose extends Django's built-in test command, you should add it to +your ``INSTALLED_APPS`` in ``settings.py``: + +.. code-block:: python + + INSTALLED_APPS = ( + ... + 'django_nose', + ... + ) + +Then set ``TEST_RUNNER`` in ``settings.py``: + +.. code-block:: python + + TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + + diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..2f3a0be --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-nose.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-nose.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/upgrading.rst b/docs/upgrading.rst new file mode 100644 index 0000000..77b9236 --- /dev/null +++ b/docs/upgrading.rst @@ -0,0 +1,51 @@ +Upgrading Django +================ + +Upgrading from Django <= 1.3 to Django 1.4 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +In versions of Django < 1.4 the project folder was in fact a python package as +well (note the __init__.py in your project root). In Django 1.4, there is no +such file and thus the project is not a python module. + +**When you upgrade your Django project to the Django 1.4 layout, you need to +remove the __init__.py file in the root of your project (and move any python +files that reside there other than the manage.py) otherwise you will get a +`ImportError: No module named urls` exception.** + +This happens because Nose will intelligently try to populate your sys.path, and +in this particular case includes your parent directory if your project has a +__init__.py file (see: https://github.com/nose-devs/nose/blob/release_1.1.2/nose/importer.py#L134). + +This means that even though you have set up your directory structure properly and +set your `ROOT_URLCONF='my_project.urls'` to match the new structure, when running +django-nose's test runner it will try to find your urls.py file in `'my_project.my_project.urls'`. + + +Upgrading from Django < 1.2 +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.2 switches to a `class-based test runner`_. To use django-nose +with Django 1.2, change your ``TEST_RUNNER`` from ``django_nose.run_tests`` to +``django_nose.NoseTestSuiteRunner``. + +``django_nose.run_tests`` will continue to work in Django 1.2 but will raise a +warning. In Django 1.3, it will stop working. + +If you were using ``django_nose.run_gis_tests``, you should also switch to +``django_nose.NoseTestSuiteRunner`` and use one of the `spatial backends`_ in +your ``DATABASES`` settings. + +.. _class-based test runner: http://docs.djangoproject.com/en/dev/releases/1.2/#function-based-test-runners +.. _spatial backends: http://docs.djangoproject.com/en/dev/ref/contrib/gis/db-api/#id1 + +Django 1.1 +~~~~~~~~~~ + +If you want to use django-nose with Django 1.1, use +https://github.com/django-nose/django-nose/tree/django-1.1 or +http://pypi.python.org/pypi/django-nose/0.0.3. + +Django 1.0 +~~~~~~~~~~ + +django-nose does not support Django 1.0. diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..ffbc1ca --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,217 @@ +Usage +===== + +The day-to-day use of django-nose is mostly transparent; just run ``./manage.py +test`` as usual. + +See ``./manage.py help test`` for all the options nose provides, and look to +the `nose docs`_ for more help with nose. + +.. _nose docs: https://nose.readthedocs.org + +Enabling Database Reuse +----------------------- + +.. warning:: There are several + `open issues `_ + with this feature, including + `reports of data loss `_. + +You can save several seconds at the beginning and end of your test suite by +reusing the test database from the last run. To do this, set the environment +variable ``REUSE_DB`` to 1:: + + REUSE_DB=1 ./manage.py test + +The one new wrinkle is that, whenever your DB schema changes, you should leave +the flag off the next time you run tests. This will cue the test runner to +reinitialize the test database. + +Also, REUSE_DB is not compatible with TransactionTestCases that leave junk in +the DB, so be sure to make your TransactionTestCases hygienic (see below) if +you want to use it. + + +Enabling Fast Fixtures +---------------------- + +.. warning:: There are several + `known issues `_ + with this feature. + +django-nose includes a fixture bundler which drastically speeds up your tests +by eliminating redundant setup of Django test fixtures. To use it... + +1. Subclass ``django_nose.FastFixtureTestCase`` instead of + ``django.test.TestCase``. (I like to import it ``as TestCase`` in my + project's ``tests/__init__.py`` and then import it from there into my actual + tests. Then it's easy to sub the base class in and out.) This alone will + cause fixtures to load once per class rather than once per test. +2. Activate fixture bundling by passing the ``--with-fixture-bundling`` option + to ``./manage.py test``. This loads each unique set of fixtures only once, + even across class, module, and app boundaries. + +How Fixture Bundling Works +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The fixture bundler reorders your test classes so that ones with identical sets +of fixtures run adjacently. It then advises the first of each series to load +the fixtures once for all of them (and the remaining ones not to bother). It +also advises the last to tear them down. Depending on the size and repetition +of your fixtures, you can expect a 25% to 50% speed increase. + +Incidentally, the author prefers to avoid Django fixtures, as they encourage +irrelevant coupling between tests and make tests harder to comprehend and +modify. For future tests, it is better to use the "model maker" pattern, +creating DB objects programmatically. This way, tests avoid setup they don't +need, and there is a clearer tie between a test and the exact state it +requires. The fixture bundler is intended to make existing tests, which have +already committed to fixtures, more tolerable. + +Troubleshooting +~~~~~~~~~~~~~~~ + +If using ``--with-fixture-bundling`` causes test failures, it likely indicates +an order dependency between some of your tests. Here are the most frequent +sources of state leakage we have encountered: + +* Locale activation, which is maintained in a threadlocal variable. Be sure to + reset your locale selection between tests. +* memcached contents. Be sure to flush between tests. Many test superclasses do + this automatically. + +It's also possible that you have ``post_save`` signal handlers which create +additional database rows while loading the fixtures. ``FastFixtureTestCase`` +isn't yet smart enough to notice this and clean up after it, so you'll have to +go back to plain old ``TestCase`` for now. + +Exempting A Class From Bundling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In some unusual cases, it is desirable to exempt a test class from fixture +bundling, forcing it to set up and tear down its fixtures at the class +boundaries. For example, we might have a ``TestCase`` subclass which sets up +some state outside the DB in ``setUpClass`` and tears it down in +``tearDownClass``, and it might not be possible to adapt those routines to heed +the advice of the fixture bundler. In such a case, simply set the +``exempt_from_fixture_bundling`` attribute of the test class to ``True``. + + +Speedy Hygienic TransactionTestCases +------------------------------------ + +Unlike the stock Django test runner, django-nose lets you write custom +TransactionTestCase subclasses which expect to start with an unmarred DB, +saving an entire DB flush per test. + +Background +~~~~~~~~~~ + +The default Django TransactionTestCase class `can leave the DB in an unclean +state`_ when it's done. To compensate, TransactionTestCase does a +time-consuming flush of the DB *before* each test to ensure it begins with a +clean slate. Django's stock test runner then runs TransactionTestCases last so +they don't wreck the environment for better-behaved tests. django-nose +replicates this behavior. + +Escaping the Grime +~~~~~~~~~~~~~~~~~~ + +Some people, however, have made subclasses of TransactionTestCase that clean up +after themselves (and can do so efficiently, since they know what they've +changed). Like TestCase, these may assume they start with a clean DB. However, +any TransactionTestCases that run before them and leave a mess could cause them +to fail spuriously. + +django-nose offers to fix this. If you include a special attribute on your +well-behaved TransactionTestCase... :: + + class MyNiceTestCase(TransactionTestCase): + cleans_up_after_itself = True + +...django-nose will run it before any of those nasty, trash-spewing test cases. +You can thus enjoy a big speed boost any time you make a TransactionTestCase +clean up after itself: skipping a whole DB flush before every test. With a +large schema, this can save minutes of IO. + +django-nose's own FastFixtureTestCase uses this feature, even though it +ultimately acts more like a TestCase than a TransactionTestCase. + +.. _can leave the DB in an unclean state: https://docs.djangoproject.com/en/1.4/topics/testing/#django.test.TransactionTestCase + + +Test-Only Models +---------------- + +If you have a model that is used only by tests (for example, to test an +abstract model base class), you can put it in any file that's imported in the +course of loading tests. For example, if the tests that need it are in +``test_models.py``, you can put the model in there, too. django-nose will make +sure its DB table gets created. + + +Assertions +---------- + +``django-nose.tools`` provides pep8 versions of Django's TestCase asserts +and some of its own as functions. :: + + assert_redirects(response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix='') + + assert_contains(response, text, count=None, status_code=200, msg_prefix='') + assert_not_contains(response, text, count=None, status_code=200, msg_prefix='') + + assert_form_error(response, form, field, errors, msg_prefix='') + + assert_template_used(response, template_name, msg_prefix='') + assert_template_not_used(response, template_name, msg_prefix='') + + assert_queryset_equal(qs, values, transform=repr) + + assert_num_queries(num, func=None, *args, **kwargs) + + assert_code(response, status_code, msg_prefix='') + + assert_ok(response, msg_prefix='') + + assert_mail_count(count, msg=None) + + +Using With South +---------------- + +`South`_ installs its own test command that turns off migrations during +testing. Make sure that django-nose comes *after* ``south`` in +``INSTALLED_APPS`` so that django_nose's test command is used. + +.. _South: http://south.aeracode.org/ + + +Always Passing The Same Options +------------------------------- + +To always set the same command line options you can use a `nose.cfg or +setup.cfg`_ (as usual) or you can specify them in settings.py like this:: + + NOSE_ARGS = ['--failed', '--stop'] + +.. _nose.cfg or setup.cfg: https://nose.readthedocs.org/en/latest/usage.html#configuration + + +Custom Plugins +-------------- + +If you need to `make custom plugins`_, you can define each plugin class +somewhere within your app and load them from settings.py like this:: + + NOSE_PLUGINS = [ + 'yourapp.tests.plugins.SystematicDysfunctioner', + # ... + ] + +Just like middleware or anything else, each string must be a dot-separated, +importable path to an actual class. Each plugin class will be instantiated and +added to the Nose test runner. + +.. _make custom plugins: https://nose.readthedocs.org/en/latest/plugins.html#writing-plugins + diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..a2b686c --- /dev/null +++ b/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +"""Django command-line utility for administrative tasks.""" +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements-rtd.txt b/requirements-rtd.txt new file mode 100644 index 0000000..bf4f197 --- /dev/null +++ b/requirements-rtd.txt @@ -0,0 +1,4 @@ +# Requirements for ReadTheDocs +# None right now, but avoid installing gnureadline, etc. +# Must be set in the RTD Admin, at: +# https://readthedocs.org/dashboard/django-nose/advanced/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fa345d8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,46 @@ +# +# Requirements for running django-nose testapp +# + +# Latest Django +Django==1.8.5 + +# This project +-e . + +# Load database config from environment +dj-database-url==0.3.0 + +# Packaging +wheel==0.26.0 + +# PEP8, PEP257, and static analysis +mccabe==0.3.1 +pep8==1.6.2 +pyflakes==1.0.0 +flake8==2.5.0 +pep257==0.7.0 +flake8-docstrings==0.2.1 + +# Code coverage +coverage==4.0 + +# Documentation +Pygments==2.0.2 +Sphinx==1.3.1 +docutils==0.12 + +# Packaging Linters +check-manifest==0.25 +pyroma==1.8.2 + +# Multi-env test runner +virtualenv==13.1.2 +py==1.4.30 +tox==2.1.1 + +# Better interactive debugging +gnureadline==6.3.3 +ipython==4.0.0 +ipdb==0.8.1 +ipdbplugin==1.4.2 diff --git a/runtests.sh b/runtests.sh index 7066672..1c3f144 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,55 +1,187 @@ #!/bin/bash +# Parse command line +VERBOSE=0 +HELP=0 +ERR=0 +NOINPUT="" +while [[ $# > 0 ]] +do + key="$1" + case $key in + -h|--help) + HELP=1 + ;; + -v|--verbose) + VERBOSE=1 + ;; + --noinput) + NOINPUT="--noinput" + ;; + *) + echo "Unknown option '$key'" + ERR=1 + HELP=1 + ;; + esac + shift +done + +if [ $HELP -eq 1 ] +then + echo "$0 [-vh] - Run django-nose integration tests." + echo " -v/--verbose - Print output of test commands." + echo " -h/--help - Print this help message." + exit $ERR +fi + export PYTHONPATH=. -PYTHONVERSION=$(python --version 2>&1) -PYTHONVERSION=${PYTHONVERSION##Python } +HAS_HOTSHOT=$(python -c "\ +try: + import hotshot +except ImportError: + print('0') +else: + print('1') +") -function version { echo $@ | gawk -F. '{ printf("%d.%d.%d\n", $1,$2,$3); }'; } +reset_env() { + unset TEST_RUNNER + unset NOSE_PLUGINS + unset REUSE_DB +} + +echo_output() { + STDOUT=$1 + STDERR=$2 + if [ $VERBOSE -ne 1 ] + then + echo "stdout" + echo "======" + cat $STDOUT + echo + echo "stderr" + echo "======" + cat $STDERR + fi + rm $STDOUT $STDERR +} django_test() { - TEST="$1" - OUTPUT=$($TEST 2>&1) + COMMAND=$1 + TEST_COUNT=$2 + DESCRIPTION=$3 + CAN_FAIL=${4:-0} + + if [ $VERBOSE -eq 1 ] + then + echo "================" + echo "Django settings:" + ./manage.py diffsettings + echo "================" + fi + + if [ -n "$COVERAGE" ] + then + TEST="coverage run -p $COMMAND" + else + TEST="$COMMAND" + fi + # Temp files on Linux / OSX + TMP_OUT=`mktemp 2>/dev/null || mktemp -t 'django-nose-runtests'` + TMP_ERR=`mktemp 2>/dev/null || mktemp -t 'django-nose-runtests'` + RETURN=0 + if [ $VERBOSE -eq 1 ] + then + echo $TEST + $TEST > >(tee $TMP_OUT) 2> >(tee $TMP_ERR >&2) + else + $TEST >$TMP_OUT 2>$TMP_ERR + fi if [ $? -gt 0 ] then - echo FAIL: $3 - $TEST - exit 1; + echo "FAIL (test failure): $DESCRIPTION" + echo_output $TMP_OUT $TMP_ERR + if [ "$CAN_FAIL" == "0" ] + then + exit 1 + else + return + fi fi - echo $OUTPUT | grep "Ran $2 test" > /dev/null + OUTPUT=`cat $TMP_OUT $TMP_ERR` + echo $OUTPUT | grep "Ran $TEST_COUNT test" > /dev/null if [ $? -gt 0 ] then - echo FAIL: $3 - $TEST - exit 1; + echo "FAIL (count!=$TEST_COUNT): $DESCRIPTION" + echo_output $TMP_OUT $TMP_ERR + if [ "$CAN_FAIL" == "0" ] + then + exit 1 + else + return + fi else - echo PASS: $3 + echo "PASS (count==$TEST_COUNT): $DESCRIPTION" fi + rm $TMP_OUT $TMP_ERR # Check that we're hijacking the help correctly. $TEST --help 2>&1 | grep 'NOSE_DETAILED_ERRORS' > /dev/null if [ $? -gt 0 ] then - echo FAIL: $3 '(--help)' - exit 1; + echo "FAIL (--help): $DESCRIPTION" + if [ "$CAN_FAIL" == 0 ] + then + exit 1; + else + return + fi else - echo PASS: $3 '(--help)' + echo "PASS ( --help): $DESCRIPTION" fi } -django_test 'django-admin.py test --settings=testapp.settings' '2' 'normal settings' -if [ "$DJANGO" = "Django==1.4.1" -o "$DJANGO" = "Django==1.5" -o "$DJANGO" = "Django==1.6" ] -then - django_test 'django-admin.py test --settings=testapp.settings_with_south' '2' 'with south in installed apps' -fi +TESTAPP_COUNT=6 + +reset_env +django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'normal settings' + +reset_env +export TEST_RUNNER="django_nose.run_tests" +django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'django_nose.run_tests format' + +reset_env +django_test "testapp/runtests.py testapp.test_only_this" 1 'via run_tests API' + +reset_env +export NOSE_PLUGINS="testapp.plugins.SanityCheckPlugin" +django_test "./manage.py test testapp/plugin_t $NOINPUT" 1 'with plugins' + +reset_env +django_test "./manage.py test unittests $NOINPUT" 4 'unittests' + +reset_env +django_test "./manage.py test unittests --verbosity 1 $NOINPUT" 4 'argument option without equals' + +reset_env +django_test "./manage.py test unittests --testrunner=testapp.custom_runner.CustomNoseTestSuiteRunner $NOINPUT" 4 'unittests with testrunner' + +reset_env +django_test "./manage.py test unittests --attr special $NOINPUT" 1 'select by attribute' + +reset_env +export REUSE_DB=1 +# For the many issues with REUSE_DB=1, see: +# https://github.com/django-nose/django-nose/milestones/Fix%20REUSE_DB=1 +django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'with REUSE_DB=1, call #1' 'can fail' +django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'with REUSE_DB=1, call #2' 'can fail' + -django_test 'django-admin.py test --settings=testapp.settings_old_style' '2' 'django_nose.run_tests format' -django_test 'testapp/runtests.py testapp.test_only_this' '1' 'via run_tests API' -django_test 'django-admin.py test --settings=testapp.settings_with_plugins testapp/plugin_t' '1' 'with plugins' -django_test 'django-admin.py test --settings=testapp.settings unittests' '4' 'unittests' -django_test 'django-admin.py test --settings=testapp.settings unittests --testrunner=testapp.custom_runner.CustomNoseTestSuiteRunner' '4' 'unittests' -if ! [ $(version $PYTHONVERSION) \> $(version 3.0.0) ] +if [ "$HAS_HOTSHOT" = "1" ] then -# Python 3 doesn't support the hotshot profiler. See nose#842. -django_test 'django-admin.py test --settings=testapp.settings --with-profile' '2' 'with profile plugin' + # Python 3 doesn't support the hotshot profiler. See nose#842. + reset_env + django_test "./manage.py test $NOINPUT --with-profile --profile-restrict less_output" $TESTAPP_COUNT 'with profile plugin' fi diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b9640ea --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[check-manifest] +ignore = + .travis.yml + tox.ini + +[coverage:run] +branch = True +source = django_nose,testapp,unittests + +[coverage:report] +omit = + testapp/south_migrations/*.py + testapp/migrations/*.py + +[flake8] +exclude = .tox/*,build/* + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py index 77b8103..2b9f8fc 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,44 @@ +"""django-nose packaging.""" +from __future__ import unicode_literals import os +from codecs import open from setuptools import setup, find_packages -ROOT = os.path.abspath(os.path.dirname(__file__)) +def get_long_description(title): + """Create the long_description from other files.""" + ROOT = os.path.abspath(os.path.dirname(__file__)) + + readme = open(os.path.join(ROOT, 'README.rst'), 'r', 'utf8').read() + body_tag = ".. Omit badges from docs" + readme_body_start = readme.index(body_tag) + assert readme_body_start + readme_body = readme[readme_body_start + len(body_tag):] + + changelog = open(os.path.join(ROOT, 'changelog.rst'), 'r', 'utf8').read() + old_tag = ".. Omit older changes from package" + changelog_body_end = changelog.index(old_tag) + assert changelog_body_end + changelog_body = changelog[:changelog_body_end] + + bars = '=' * len(title) + long_description = """ +%(bars)s +%(title)s +%(bars)s +%(readme_body)s + +%(changelog_body)s + +_(Older changes can be found in the full documentation)._ +""" % locals() + return long_description setup( name='django-nose', - version='1.3', + version='1.4.3', description='Makes your Django tests simple and snappy', - long_description=open(os.path.join(ROOT, 'README.rst')).read(), + long_description=get_long_description('django-nose'), author='Jeff Balogh', author_email='me@jeffbalogh.org', maintainer='Erik Rose', @@ -18,18 +48,20 @@ packages=find_packages(exclude=['testapp', 'testapp/*']), include_package_data=True, zip_safe=False, - install_requires=['nose>=1.2.1', 'Django>=1.4'], + install_requires=['nose>=1.2.1'], tests_require=['south>=0.7'], + test_suite='testapp.runtests.runtests', # This blows up tox runs that install django-nose into a virtualenv, # because it causes Nose to import django_nose.runner before the Django # settings are initialized, leading to a mess of errors. There's no reason # we need FixtureBundlingPlugin declared as an entrypoint anyway, since you # need to be using django-nose to find the it useful, and django-nose knows # about it intrinsically. - #entry_points=""" + # entry_points=""" # [nose.plugins.0.10] # fixture_bundler = django_nose.fixture_bundling:FixtureBundlingPlugin # """, + keywords='django nose django-nose', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -42,8 +74,8 @@ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: Software Development :: Testing' ] ) diff --git a/testapp/__init__.py b/testapp/__init__.py index e69de29..ea9da7b 100644 --- a/testapp/__init__.py +++ b/testapp/__init__.py @@ -0,0 +1 @@ +"""Sample Django application for django-nose testing.""" diff --git a/testapp/custom_runner.py b/testapp/custom_runner.py index b7e83ae..63b4b38 100644 --- a/testapp/custom_runner.py +++ b/testapp/custom_runner.py @@ -1,5 +1,6 @@ +"""Custom runner to test overriding runner.""" from django_nose import NoseTestSuiteRunner class CustomNoseTestSuiteRunner(NoseTestSuiteRunner): - pass + """Custom test runner, to test overring runner.""" diff --git a/testapp/fixtures/testdata.json b/testapp/fixtures/testdata.json new file mode 100644 index 0000000..42f836a --- /dev/null +++ b/testapp/fixtures/testdata.json @@ -0,0 +1,19 @@ +[ +{ + "pk": 1, + "model": "testapp.question", + "fields": { + "question_text": "What is your favorite color?", + "pub_date": "1975-04-09T00:00:00" + } +}, +{ + "pk": 1, + "model": "testapp.choice", + "fields": { + "choice_text": "Blue.", + "question": 1, + "votes": 3 + } +} +] diff --git a/testapp/migrations/0001_initial.py b/testapp/migrations/0001_initial.py new file mode 100644 index 0000000..b1dc553 --- /dev/null +++ b/testapp/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Choice', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('choice_text', models.CharField(max_length=200)), + ('votes', models.IntegerField(default=0)), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='Question', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('question_text', models.CharField(max_length=200)), + ('pub_date', models.DateTimeField(verbose_name=b'date published')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AddField( + model_name='choice', + name='question', + field=models.ForeignKey(to='testapp.Question'), + preserve_default=True, + ), + ] diff --git a/testapp/migrations/__init__.py b/testapp/migrations/__init__.py new file mode 100644 index 0000000..5531120 --- /dev/null +++ b/testapp/migrations/__init__.py @@ -0,0 +1 @@ +"""Django 1.7+ migrations.""" diff --git a/testapp/models.py b/testapp/models.py new file mode 100644 index 0000000..f4b398e --- /dev/null +++ b/testapp/models.py @@ -0,0 +1,30 @@ +"""Models for django-nose test application. + +Based on the Django tutorial: +https://docs.djangoproject.com/en/1.8/intro/tutorial01/ +""" + +from django.db import models + + +class Question(models.Model): + """A poll question.""" + + question_text = models.CharField(max_length=200) + pub_date = models.DateTimeField('date published') + + def __str__(self): + """Return string representation.""" + return self.question_text + + +class Choice(models.Model): + """A poll answer.""" + + question = models.ForeignKey(Question) + choice_text = models.CharField(max_length=200) + votes = models.IntegerField(default=0) + + def __str__(self): + """Return string representation.""" + return self.choice_text diff --git a/testapp/plugin_t/test_with_plugins.py b/testapp/plugin_t/test_with_plugins.py index ad5ac6a..61a9572 100644 --- a/testapp/plugin_t/test_with_plugins.py +++ b/testapp/plugin_t/test_with_plugins.py @@ -1,6 +1,8 @@ +"""Test loading of additional plugins.""" from nose.tools import eq_ def test_one(): + """Test that the test plugin was initialized.""" from testapp import plugins eq_(plugins.plugin_began, True) diff --git a/testapp/plugins.py b/testapp/plugins.py index 6a9ec85..9667ad7 100644 --- a/testapp/plugins.py +++ b/testapp/plugins.py @@ -1,9 +1,12 @@ +"""Additional plugins for testing plugins.""" from nose.plugins import Plugin - plugin_began = False + class SanityCheckPlugin(Plugin): + """Test plugin that registers that it ran.""" + enabled = True def options(self, parser, env): @@ -13,5 +16,6 @@ def configure(self, options, conf): """Configure plugin.""" def begin(self): + """Flag that the plugin was run.""" global plugin_began plugin_began = True diff --git a/testapp/runtests.py b/testapp/runtests.py index c6c9286..b1ac91e 100755 --- a/testapp/runtests.py +++ b/testapp/runtests.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +"""Configure enough Django to run the test suite.""" import sys from django.conf import settings @@ -6,14 +7,14 @@ if not settings.configured: settings.configure( DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, - INSTALLED_APPS=[ - 'django_nose', - ], + INSTALLED_APPS=['django_nose'], + MIDDLEWARE_CLASSES=[], ) -from django_nose import NoseTestSuiteRunner def runtests(*test_labels): + """Run the selected tests, or all tests if none selected.""" + from django_nose import NoseTestSuiteRunner runner = NoseTestSuiteRunner(verbosity=1, interactive=True) failures = runner.run_tests(test_labels) sys.exit(failures) diff --git a/testapp/settings.py b/testapp/settings.py index 7bc3ab1..d204545 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -1,16 +1,50 @@ +""" +Django settings for testing django-nose. + +Configuration is overriden by environment variables: + +DATABASE_URL - See https://github.com/kennethreitz/dj-database-url +USE_SOUTH - Set to 1 to include South in INSTALLED_APPS +TEST_RUNNER - Dotted path of test runner to use (can also use --test-runner) +NOSE_PLUGINS - Comma-separated list of plugins to add +""" +from __future__ import print_function +from os import environ, path + +import dj_database_url + +BASE_DIR = path.dirname(path.dirname(__file__)) + + +def rel_path(*subpaths): + """Construct the full path given a relative path.""" + return path.join(BASE_DIR, *subpaths) + DATABASES = { - 'default': { - 'NAME': 'django_master', - 'ENGINE': 'django.db.backends.sqlite3', - } + 'default': + dj_database_url.config( + default='sqlite:///' + rel_path('testapp.sqlite3')) } MIDDLEWARE_CLASSES = () -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django_nose', -) + 'testapp', +] +if environ.get('USE_SOUTH', '0') in (1, '1'): + import south # Raise ImportError if not installed + assert south + INSTALLED_APPS.insert(0, 'south') + +raw_test_runner = environ.get('TEST_RUNNER') +if raw_test_runner: + TEST_RUNNER = raw_test_runner +else: + TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +raw_plugins = environ.get('NOSE_PLUGINS') +if raw_plugins: + NOSE_PLUGINS = raw_plugins.split(',') SECRET_KEY = 'ssshhhh' diff --git a/testapp/settings_old_style.py b/testapp/settings_old_style.py deleted file mode 100644 index d60add2..0000000 --- a/testapp/settings_old_style.py +++ /dev/null @@ -1,15 +0,0 @@ -DATABASES = { - 'default': { - 'NAME': 'django_master', - 'ENGINE': 'django.db.backends.sqlite3', - } -} - -INSTALLED_APPS = ( - 'django_nose', -) - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -TEST_RUNNER = 'django_nose.run_tests' - -SECRET_KEY = 'sssshhh' diff --git a/testapp/settings_with_plugins.py b/testapp/settings_with_plugins.py deleted file mode 100644 index 5d93b03..0000000 --- a/testapp/settings_with_plugins.py +++ /dev/null @@ -1,6 +0,0 @@ -from .settings import * - - -NOSE_PLUGINS = [ - 'testapp.plugins.SanityCheckPlugin' -] diff --git a/testapp/settings_with_south.py b/testapp/settings_with_south.py deleted file mode 100644 index 4ee42fa..0000000 --- a/testapp/settings_with_south.py +++ /dev/null @@ -1,4 +0,0 @@ -from .settings import * - - -INSTALLED_APPS = ('south',) + INSTALLED_APPS diff --git a/testapp/south_migrations/0001_initial.py b/testapp/south_migrations/0001_initial.py new file mode 100644 index 0000000..021b439 --- /dev/null +++ b/testapp/south_migrations/0001_initial.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Question' + db.create_table('testapp_question', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('question_text', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('pub_date', self.gf('django.db.models.fields.DateTimeField')()), + )) + db.send_create_signal('testapp', ['Question']) + + # Adding model 'Choice' + db.create_table('testapp_choice', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['testapp.Question'])), + ('choice_text', self.gf('django.db.models.fields.CharField')(max_length=200)), + ('votes', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('testapp', ['Choice']) + + + def backwards(self, orm): + # Deleting model 'Question' + db.delete_table('testapp_question') + + # Deleting model 'Choice' + db.delete_table('testapp_choice') + + + models = { + 'testapp.choice': { + 'Meta': {'object_name': 'Choice'}, + 'choice_text': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['testapp.Question']"}), + 'votes': ('django.db.models.fields.IntegerField', [], {'default': '0'}) + }, + 'testapp.question': { + 'Meta': {'object_name': 'Question'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'pub_date': ('django.db.models.fields.DateTimeField', [], {}), + 'question_text': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + } + } + + complete_apps = ['testapp'] diff --git a/testapp/south_migrations/__init__.py b/testapp/south_migrations/__init__.py new file mode 100644 index 0000000..48f2e4f --- /dev/null +++ b/testapp/south_migrations/__init__.py @@ -0,0 +1 @@ +"""South migrations for Django 1.6 and earlier.""" diff --git a/testapp/test_for_nose.py b/testapp/test_for_nose.py index e4ab7e2..7ed6fd6 100644 --- a/testapp/test_for_nose.py +++ b/testapp/test_for_nose.py @@ -2,4 +2,5 @@ def test_addition(): + """Test some advanced maths.""" assert 1 + 1 == 2 diff --git a/testapp/test_only_this.py b/testapp/test_only_this.py index 8aa340d..38edfd5 100644 --- a/testapp/test_only_this.py +++ b/testapp/test_only_this.py @@ -2,4 +2,5 @@ def test_multiplication(): + """Check some advanced maths.""" assert 2 * 2 == 4 diff --git a/testapp/tests.py b/testapp/tests.py new file mode 100644 index 0000000..4a2522f --- /dev/null +++ b/testapp/tests.py @@ -0,0 +1,50 @@ +"""Django model tests.""" + +from datetime import datetime + +from django.test import TestCase +from testapp.models import Question, Choice + + +class NoDatabaseTestCase(TestCase): + """Tests that don't read or write to the database.""" + + def test_question_str(self): + """Test Question.__str__ method.""" + question = Question(question_text="What is your name?") + self.assertEqual("What is your name?", str(question)) + + def test_choice_str(self): + """Test Choice.__str__ method.""" + choice = Choice(choice_text='My name is Sir Lancelot of Camelot.') + self.assertEqual('My name is Sir Lancelot of Camelot.', str(choice)) + + +class UsesDatabaseTestCase(TestCase): + """Tests that read and write to the database.""" + + def test_question(self): + """Test that votes is initialized to 0.""" + question = Question.objects.create( + question_text="What is your quest?", pub_date=datetime(1975, 4, 9)) + Choice.objects.create( + question=question, choice_text="To seek the Holy Grail.") + self.assertTrue(question.choice_set.exists()) + the_choice = question.choice_set.get() + self.assertEqual(0, the_choice.votes) + + +class UsesFixtureTestCase(TestCase): + """Tests that use a test fixture.""" + + fixtures = ["testdata.json"] + + def test_fixture_loaded(self): + """Test that fixture was loaded.""" + question = Question.objects.get() + self.assertEqual( + 'What is your favorite color?', question.question_text) + self.assertEqual(datetime(1975, 4, 9), question.pub_date) + choice = question.choice_set.get() + self.assertEqual("Blue.", choice.choice_text) + self.assertEqual(3, choice.votes) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4256a6d --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] +envlist = + {py27,py34,py35}-django-{18,19} + {py27,py34,py35}-django-master + flake8 + docs +skip_missing_interpreters = True + +[testenv] +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH COVERAGE RUNTEST_ARGS DATABASE_URL +commands = + ./runtests.sh {env:RUNTEST_ARGS:} + coverage combine +deps = + coveralls + dj-database-url + django-18: Django>=1.8,<1.9 + django-19: Django==1.9,<1.10 + django-master: https://github.com/django/django/archive/master.tar.gz + {py27,py34,py35}-django-{18,19,master}-postgres: psycopg2 + {py27,py34,py35}-django-{18,19,master}-mysql: mysqlclient + +[testenv:flake8] +deps = + Django + pep257==0.7.0 + pep8==1.6.2 + flake8==2.5.0 + flake8-docstrings==0.2.1 +commands = flake8 + +[testenv:docs] +changedir = docs +deps = + Sphinx + dj-database-url + Django +commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html diff --git a/unittests/test_databases.py b/unittests/test_databases.py index 4951c08..a3f5268 100644 --- a/unittests/test_databases.py +++ b/unittests/test_databases.py @@ -1,12 +1,19 @@ +"""Test database access without a database.""" from contextlib import contextmanager from unittest import TestCase -from django.db.models.loading import cache +try: + from django.db.models.loading import cache as apps +except: + from django.apps import apps +from nose.plugins.attrib import attr from django_nose.runner import NoseTestSuiteRunner class GetModelsForConnectionTests(TestCase): + """Test runner._get_models_for_connection.""" + tables = ['test_table%d' % i for i in range(5)] def _connection_mock(self, tables): @@ -16,7 +23,9 @@ def get_table_list(*args, **kwargs): class FakeConnection(object): introspection = FakeIntrospection() - cursor = lambda x: None + + def cursor(self): + return None return FakeConnection() @@ -31,12 +40,13 @@ def _cache_mock(self, tables=[]): def get_models(*args, **kwargs): return [self._model_mock(t) for t in tables] - old = cache.get_models - cache.get_models = get_models + old = apps.get_models + apps.get_models = get_models yield - cache.get_models = old + apps.get_models = old def setUp(self): + """Initialize the runner.""" self.runner = NoseTestSuiteRunner() def test_no_models(self): @@ -53,18 +63,21 @@ def test_wrong_models(self): self.assertEqual( self.runner._get_models_for_connection(connection), []) + @attr("special") def test_some_models(self): - """If some of the models has appropriate table in the DB, return matching models.""" + """If some of the models are in the DB, return matching models.""" connection = self._connection_mock(self.tables) with self._cache_mock(self.tables[1:3]): - result_tables = [m._meta.db_table for m in - self.runner._get_models_for_connection(connection)] + result_tables = [ + m._meta.db_table for m in + self.runner._get_models_for_connection(connection)] self.assertEqual(result_tables, self.tables[1:3]) def test_all_models(self): - """If all the models have appropriate tables in the DB, return them all.""" + """If all the models have in the DB, return them all.""" connection = self._connection_mock(self.tables) with self._cache_mock(self.tables): - result_tables = [m._meta.db_table for m in - self.runner._get_models_for_connection(connection)] + result_tables = [ + m._meta.db_table for m in + self.runner._get_models_for_connection(connection)] self.assertEqual(result_tables, self.tables)