├── docs ├── _static │ └── .keep ├── authors.rst ├── changelog.rst ├── contributing.rst ├── index.rst ├── installation.rst ├── upgrading.rst ├── Makefile ├── make.bat ├── usage.rst └── conf.py ├── testapp ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── __init__.py ├── test_for_nose.py ├── test_only_this.py ├── custom_runner.py ├── plugin_t │ └── test_with_plugins.py ├── fixtures │ └── testdata.json ├── plugins.py ├── runtests.py ├── models.py ├── settings.py └── tests.py ├── django_nose ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── test.py ├── __init__.py ├── utils.py ├── tools.py ├── fixture_tables.py ├── testcases.py ├── plugin.py └── runner.py ├── requirements-rtd.txt ├── setup.cfg ├── manage.py ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── contribute.json ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── LICENSE ├── tox.ini ├── Makefile ├── CODE_OF_CONDUCT.md ├── unittests └── test_databases.py ├── setup.py ├── AUTHORS.rst ├── README.rst ├── CONTRIBUTING.rst ├── runtests.sh └── changelog.rst /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../changelog.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst -------------------------------------------------------------------------------- /testapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """Django 1.7+ migrations.""" 2 | -------------------------------------------------------------------------------- /django_nose/management/__init__.py: -------------------------------------------------------------------------------- 1 | """django-nose management additions.""" 2 | -------------------------------------------------------------------------------- /testapp/__init__.py: -------------------------------------------------------------------------------- 1 | """Sample Django application for django-nose testing.""" 2 | -------------------------------------------------------------------------------- /django_nose/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """django-nose management commands.""" 2 | -------------------------------------------------------------------------------- /requirements-rtd.txt: -------------------------------------------------------------------------------- 1 | # Requirements for ReadTheDocs 2 | # Must be set in the RTD Admin, at: 3 | # https://readthedocs.org/dashboard/django-nose/advanced/ 4 | Django 5 | nose 6 | -------------------------------------------------------------------------------- /testapp/test_for_nose.py: -------------------------------------------------------------------------------- 1 | """Django's test runner won't find this, but nose will.""" 2 | 3 | 4 | def test_addition(): 5 | """Test some advanced maths.""" 6 | assert 1 + 1 == 2 7 | -------------------------------------------------------------------------------- /testapp/test_only_this.py: -------------------------------------------------------------------------------- 1 | """Django's test runner won't find this, but nose will.""" 2 | 3 | 4 | def test_multiplication(): 5 | """Check some advanced maths.""" 6 | assert 2 * 2 == 4 7 | -------------------------------------------------------------------------------- /testapp/custom_runner.py: -------------------------------------------------------------------------------- 1 | """Custom runner to test overriding runner.""" 2 | from django_nose import NoseTestSuiteRunner 3 | 4 | 5 | class CustomNoseTestSuiteRunner(NoseTestSuiteRunner): 6 | """Custom test runner, to test overring runner.""" 7 | -------------------------------------------------------------------------------- /testapp/plugin_t/test_with_plugins.py: -------------------------------------------------------------------------------- 1 | """Test loading of additional plugins.""" 2 | from nose.tools import eq_ 3 | 4 | 5 | def test_one(): 6 | """Test that the test plugin was initialized.""" 7 | from testapp import plugins 8 | 9 | eq_(plugins.plugin_began, True) 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [check-manifest] 2 | ignore = 3 | tox.ini 4 | 5 | [coverage:run] 6 | branch = True 7 | source = django_nose,testapp,unittests 8 | 9 | [coverage:report] 10 | omit = 11 | testapp/migrations/*.py 12 | 13 | [flake8] 14 | exclude = .tox/*,build/*,.eggs/* 15 | max-line-length = 88 16 | extend-ignore = E203, W503 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /testapp/fixtures/testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "testapp.question", 5 | "fields": { 6 | "question_text": "What is your favorite color?", 7 | "pub_date": "1975-04-09T00:00:00" 8 | } 9 | }, 10 | { 11 | "pk": 1, 12 | "model": "testapp.choice", 13 | "fields": { 14 | "choice_text": "Blue.", 15 | "question": 1, 16 | "votes": 3 17 | } 18 | } 19 | ] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python binaries 2 | *.py[cod] 3 | 4 | # Editor backups, OS files 5 | *.swp 6 | *~ 7 | .DS_Store 8 | 9 | # Distutils cruft 10 | *.egg-info 11 | South-*.egg/ 12 | build/ 13 | dist/ 14 | 15 | # Unit test / coverage reports 16 | .tox/ 17 | .coverage 18 | .coverage.* 19 | .noseids 20 | nosetests.xml 21 | htmlcov/ 22 | testapp.sqlite3 23 | 24 | # Sphinx documentation build 25 | docs/_build/* 26 | 27 | # pyenv 28 | .python-version 29 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-nose documentation master file 2 | 3 | =========== 4 | django-nose 5 | =========== 6 | 7 | .. include:: ../README.rst 8 | :start-after: .. Omit badges from docs 9 | 10 | Contents 11 | -------- 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installation 17 | usage 18 | upgrading 19 | contributing 20 | authors 21 | changelog 22 | 23 | Indices and tables 24 | ------------------ 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /django_nose/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """The django_nose module.""" 3 | from pkg_resources import get_distribution, DistributionNotFound 4 | 5 | from django_nose.runner import BasicNoseRunner, NoseTestSuiteRunner 6 | from django_nose.testcases import FastFixtureTestCase 7 | 8 | assert BasicNoseRunner 9 | assert NoseTestSuiteRunner 10 | assert FastFixtureTestCase 11 | 12 | try: 13 | __version__ = get_distribution("django-nose").version 14 | except DistributionNotFound: 15 | # package is not installed 16 | pass 17 | -------------------------------------------------------------------------------- /testapp/plugins.py: -------------------------------------------------------------------------------- 1 | """Additional plugins for testing plugins.""" 2 | from nose.plugins import Plugin 3 | 4 | plugin_began = False 5 | 6 | 7 | class SanityCheckPlugin(Plugin): 8 | """Test plugin that registers that it ran.""" 9 | 10 | enabled = True 11 | 12 | def options(self, parser, env): 13 | """Register commandline options.""" 14 | 15 | def configure(self, options, conf): 16 | """Configure plugin.""" 17 | 18 | def begin(self): 19 | """Flag that the plugin was run.""" 20 | global plugin_began 21 | plugin_began = True 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include LICENSE 4 | include Makefile 5 | include README.rst 6 | include changelog.rst 7 | include contribute.json 8 | include manage.py 9 | include requirements.txt 10 | include requirements-rtd.txt 11 | include runtests.sh 12 | 13 | recursive-include docs Makefile conf.py *.rst make.bat .keep 14 | recursive-include django_nose *.py 15 | recursive-include testapp *.py 16 | recursive-include testapp/fixtures *.json 17 | recursive-include unittests *.py 18 | 19 | recursive-exclude * *.py[co] 20 | recursive-exclude * __pycache__ 21 | -------------------------------------------------------------------------------- /django_nose/management/commands/test.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Add extra options from the test runner to the ``test`` command. 4 | 5 | This enables browsing all the nose options from the command line. 6 | """ 7 | from django.conf import settings 8 | from django.core.management.commands.test import Command 9 | from django.test.utils import get_runner 10 | 11 | 12 | TestRunner = get_runner(settings) 13 | 14 | if hasattr(TestRunner, "options"): 15 | extra_options = TestRunner.options 16 | else: 17 | extra_options = [] 18 | 19 | 20 | class Command(Command): 21 | """Implement the ``test`` command.""" 22 | 23 | option_list = getattr(Command, "option_list", ()) + tuple(extra_options) 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Requirements for running django-nose testapp 3 | # 4 | 5 | # Latest Django 6 | Django>=2.2,<3.0 7 | 8 | # This project 9 | -e . 10 | 11 | # Load database config from environment 12 | django-environ 13 | 14 | # Packaging 15 | wheel==0.29.0 16 | twine==1.9.1 17 | 18 | # PEP8, PEP257, and static analysis 19 | flake8==3.4.1 20 | flake8-docstrings==1.1.0 21 | 22 | # Code coverage 23 | coverage==4.4.1 24 | 25 | # Documentation 26 | Sphinx==1.6.3 27 | 28 | # Packaging Linters 29 | check-manifest==0.35 30 | pyroma==2.2 31 | 32 | # Multi-env test runner 33 | tox==2.7.0 34 | 35 | # Better interactive debugging 36 | gnureadline==6.3.3 37 | ipython==5.4.1 38 | ipdb==0.10.3 39 | ipdbplugin==1.4.5 40 | -------------------------------------------------------------------------------- /contribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-nose", 3 | "description": "Django test runner using nose.", 4 | "repository": { 5 | "url": "https://github.com/jazzband/django-nose", 6 | "license": "BSD", 7 | "tests": "https://github.com/jazzband/django-nose/actions" 8 | }, 9 | "participate": { 10 | "home": "https://github.com/jazzband/django-nose", 11 | "docs": "https://github.com/jazzband/django-nose" 12 | }, 13 | "bugs": { 14 | "list": "https://github.com/jazzband/django-nose/issues", 15 | "report": "https://github.com/jazzband/django-nose/issues/new" 16 | }, 17 | "keywords": [ 18 | "django", 19 | "python", 20 | "nose", 21 | "testing" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /testapp/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Configure enough Django to run the test suite.""" 3 | import sys 4 | 5 | from django.conf import settings 6 | 7 | if not settings.configured: 8 | settings.configure( 9 | DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3"}}, 10 | INSTALLED_APPS=["django_nose"], 11 | MIDDLEWARE_CLASSES=[], 12 | ) 13 | 14 | 15 | def runtests(*test_labels): 16 | """Run the selected tests, or all tests if none selected.""" 17 | from django_nose import NoseTestSuiteRunner 18 | 19 | runner = NoseTestSuiteRunner(verbosity=1, interactive=True) 20 | failures = runner.run_tests(test_labels) 21 | sys.exit(failures) 22 | 23 | 24 | if __name__ == "__main__": 25 | runtests(*sys.argv[1:]) 26 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | You can get django-nose from PyPI with... : 5 | 6 | .. code-block:: shell 7 | 8 | $ pip install django-nose 9 | 10 | The development version can be installed with... : 11 | 12 | .. code-block:: shell 13 | 14 | $ pip install -e git://github.com/jazzband/django-nose.git#egg=django-nose 15 | 16 | Since django-nose extends Django's built-in test command, you should add it to 17 | your ``INSTALLED_APPS`` in ``settings.py``: 18 | 19 | .. code-block:: python 20 | 21 | INSTALLED_APPS = ( 22 | ... 23 | 'django_nose', 24 | ... 25 | ) 26 | 27 | Then set ``TEST_RUNNER`` in ``settings.py``: 28 | 29 | .. code-block:: python 30 | 31 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 32 | 33 | 34 | -------------------------------------------------------------------------------- /testapp/models.py: -------------------------------------------------------------------------------- 1 | """Models for django-nose test application. 2 | 3 | Based on the Django tutorial: 4 | https://docs.djangoproject.com/en/1.8/intro/tutorial01/ 5 | """ 6 | 7 | from django.db import models 8 | 9 | 10 | class Question(models.Model): 11 | """A poll question.""" 12 | 13 | question_text = models.CharField(max_length=200) 14 | pub_date = models.DateTimeField("date published") 15 | 16 | def __str__(self): 17 | """Return string representation.""" 18 | return self.question_text 19 | 20 | 21 | class Choice(models.Model): 22 | """A poll answer.""" 23 | 24 | question = models.ForeignKey(Question, on_delete=models.CASCADE) 25 | choice_text = models.CharField(max_length=200) 26 | votes = models.IntegerField(default=0) 27 | 28 | def __str__(self): 29 | """Return string representation.""" 30 | return self.choice_text 31 | -------------------------------------------------------------------------------- /testapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for testing django-nose. 3 | 4 | Configuration is overriden by environment variables: 5 | 6 | DATABASE_URL - See https://github.com/joke2k/django-environ 7 | USE_SOUTH - Set to 1 to include South in INSTALLED_APPS 8 | TEST_RUNNER - Dotted path of test runner to use (can also use --test-runner) 9 | NOSE_PLUGINS - Comma-separated list of plugins to add 10 | """ 11 | from os import path 12 | import environ 13 | 14 | env = environ.Env( 15 | # set casting, default value 16 | DEBUG=(bool, False) 17 | ) 18 | 19 | BASE_DIR = path.dirname(path.dirname(__file__)) 20 | 21 | DATABASES = {"default": env.db("DATABASE_URL", default="sqlite:////tmp/test.sqlite")} 22 | 23 | MIDDLEWARE_CLASSES = () 24 | 25 | INSTALLED_APPS = [ 26 | "django_nose", 27 | "testapp", 28 | ] 29 | 30 | TEST_RUNNER = env("TEST_RUNNER", default="django_nose.NoseTestSuiteRunner") 31 | 32 | NOSE_PLUGINS = env.list("NOSE_PLUGINS", default=[]) 33 | 34 | SECRET_KEY = "ssshhhh" 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-nose' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-nose/upload 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Jeff Balogh. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-nose nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py35-dj{18,19,110}{,-postgres,-mysql} 4 | py{35,36}-dj111{,-postgres,-mysql} 5 | py{35,36,37}-dj20{,-postgres,-mysql} 6 | py{35,36,37}-dj{21,22}{,-postgres,-mysql} 7 | py38-djmain{,-postgres,-mysql} 8 | flake8 9 | docs 10 | skip_missing_interpreters = True 11 | 12 | [gh-actions] 13 | python = 14 | 3.5: py35 15 | 3.6: py36 16 | 3.7: py37 17 | 3.8: py38, flake8, docs 18 | 19 | [testenv] 20 | passenv= 21 | CI 22 | COVERAGE 23 | RUNTEST_ARGS 24 | GITHUB_* 25 | commands = 26 | ./runtests.sh {env:RUNTEST_ARGS:} 27 | - coverage combine 28 | - coverage xml 29 | deps = 30 | coverage 31 | django-environ 32 | dj18: Django>=1.8,<1.9 33 | dj19: Django>=1.9,<1.10 34 | dj110: Django>=1.10,<1.11 35 | dj111: Django>=1.11,<2.0 36 | dj20: Django>=2.0,<2.1 37 | dj21: Django>=2.1,<2.2 38 | dj22: Django>=2.2,<3.0 39 | djmain: https://github.com/django/django/archive/main.tar.gz 40 | postgres: psycopg2 41 | mysql: mysqlclient 42 | setenv = 43 | DATABASE_URL = sqlite:////tmp/test.db 44 | postgres: DATABASE_URL = postgres://postgres:postgres@localhost:5432/postgres 45 | mysql: DATABASE_URL = mysql://root:mysql@127.0.0.1:3306/mysql 46 | ignore_errors = 47 | djmain: True 48 | 49 | [testenv:flake8] 50 | deps = 51 | Django 52 | pep257 53 | pep8 54 | flake8 55 | flake8-docstrings 56 | commands = flake8 57 | 58 | [testenv:docs] 59 | changedir = docs 60 | deps = 61 | Sphinx 62 | django-environ 63 | Django 64 | commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 65 | -------------------------------------------------------------------------------- /testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # flake8: noqa 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Choice", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ("choice_text", models.CharField(max_length=200)), 24 | ("votes", models.IntegerField(default=0)), 25 | ], 26 | options={}, 27 | bases=(models.Model,), 28 | ), 29 | migrations.CreateModel( 30 | name="Question", 31 | fields=[ 32 | ( 33 | "id", 34 | models.AutoField( 35 | verbose_name="ID", 36 | serialize=False, 37 | auto_created=True, 38 | primary_key=True, 39 | ), 40 | ), 41 | ("question_text", models.CharField(max_length=200)), 42 | ("pub_date", models.DateTimeField(verbose_name=b"date published")), 43 | ], 44 | options={}, 45 | bases=(models.Model,), 46 | ), 47 | migrations.AddField( 48 | model_name="choice", 49 | name="question", 50 | field=models.ForeignKey(to="testapp.Question", on_delete=models.CASCADE), 51 | preserve_default=True, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /testapp/tests.py: -------------------------------------------------------------------------------- 1 | """Django model tests.""" 2 | 3 | from datetime import datetime 4 | 5 | from django.test import TestCase, TransactionTestCase 6 | from testapp.models import Question, Choice 7 | 8 | 9 | class NoDatabaseTestCase(TestCase): 10 | """Tests that don't read or write to the database.""" 11 | 12 | def test_question_str(self): 13 | """Test Question.__str__ method.""" 14 | question = Question(question_text="What is your name?") 15 | self.assertEqual("What is your name?", str(question)) 16 | 17 | def test_choice_str(self): 18 | """Test Choice.__str__ method.""" 19 | choice = Choice(choice_text="My name is Sir Lancelot of Camelot.") 20 | self.assertEqual("My name is Sir Lancelot of Camelot.", str(choice)) 21 | 22 | 23 | class UsesDatabaseTestCase(TestCase): 24 | """Tests that read and write to the database.""" 25 | 26 | def test_question(self): 27 | """Test that votes is initialized to 0.""" 28 | question = Question.objects.create( 29 | question_text="What is your quest?", pub_date=datetime(1975, 4, 9) 30 | ) 31 | Choice.objects.create(question=question, choice_text="To seek the Holy Grail.") 32 | self.assertTrue(question.choice_set.exists()) 33 | the_choice = question.choice_set.get() 34 | self.assertEqual(0, the_choice.votes) 35 | 36 | 37 | class UsesFixtureTestCase(TransactionTestCase): 38 | """Tests that use a test fixture.""" 39 | 40 | fixtures = ["testdata.json"] 41 | 42 | def test_fixture_loaded(self): 43 | """Test that fixture was loaded.""" 44 | question = Question.objects.get() 45 | self.assertEqual("What is your favorite color?", question.question_text) 46 | self.assertEqual(datetime(1975, 4, 9), question.pub_date) 47 | choice = question.choice_set.get() 48 | self.assertEqual("Blue.", choice.choice_text) 49 | self.assertEqual(3, choice.votes) 50 | -------------------------------------------------------------------------------- /django_nose/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """django-nose utility methods.""" 3 | 4 | 5 | def process_tests(suite, process): 6 | """Find and process the suite with setup/teardown methods. 7 | 8 | Given a nested disaster of [Lazy]Suites, traverse to the first level 9 | that has setup or teardown, and do something to them. 10 | 11 | If we were to traverse all the way to the leaves (the Tests) 12 | indiscriminately and return them, when the runner later calls them, they'd 13 | run without reference to the suite that contained them, so they'd miss 14 | their class-, module-, and package-wide setup and teardown routines. 15 | 16 | The nested suites form basically a double-linked tree, and suites will call 17 | up to their containing suites to run their setups and teardowns, but it 18 | would be hubris to assume that something you saw fit to setup or teardown 19 | at the module level is less costly to repeat than DB fixtures. Also, those 20 | sorts of setups and teardowns are extremely rare in our code. Thus, we 21 | limit the granularity of bucketing to the first level that has setups or 22 | teardowns. 23 | 24 | :arg process: The thing to call once we get to a leaf or a test with setup 25 | or teardown 26 | """ 27 | if not hasattr(suite, "_tests") or ( 28 | hasattr(suite, "hasFixtures") and suite.hasFixtures() 29 | ): 30 | # We hit a Test or something with setup, so do the thing. (Note that 31 | # "fixtures" here means setup or teardown routines, not Django 32 | # fixtures.) 33 | process(suite) 34 | else: 35 | for t in suite._tests: 36 | process_tests(t, process) 37 | 38 | 39 | def is_subclass_at_all(cls, class_info): 40 | """Return whether ``cls`` is a subclass of ``class_info``. 41 | 42 | Even if ``cls`` is not a class, don't crash. Return False instead. 43 | 44 | """ 45 | try: 46 | return issubclass(cls, class_info) 47 | except TypeError: 48 | return False 49 | 50 | 51 | def uses_mysql(connection): 52 | """Return whether the connection represents a MySQL DB.""" 53 | return "mysql" in connection.settings_dict["ENGINE"] 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | max-parallel: 5 11 | matrix: 12 | python-version: ['3.5', '3.6', '3.7', '3.8'] 13 | 14 | services: 15 | postgres: 16 | image: postgres:10 17 | env: 18 | POSTGRES_USER: postgres 19 | POSTGRES_PASSWORD: postgres 20 | POSTGRES_DB: postgres 21 | ports: 22 | - 5432:5432 23 | options: >- 24 | --health-cmd pg_isready 25 | --health-interval 10s 26 | --health-timeout 5s 27 | --health-retries 5 28 | 29 | mariadb: 30 | image: mariadb:10.3 31 | env: 32 | MYSQL_ROOT_PASSWORD: mysql 33 | MYSQL_DATABASE: mysql 34 | options: >- 35 | --health-cmd "mysqladmin ping" 36 | --health-interval 10s 37 | --health-timeout 5s 38 | --health-retries 5 39 | ports: 40 | - 3306:3306 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Set up Python ${{ matrix.python-version }} 46 | uses: actions/setup-python@v2 47 | with: 48 | python-version: ${{ matrix.python-version }} 49 | 50 | - name: Get pip cache dir 51 | id: pip-cache 52 | run: | 53 | echo "::set-output name=dir::$(pip cache dir)" 54 | 55 | - name: Cache 56 | uses: actions/cache@v2 57 | with: 58 | path: ${{ steps.pip-cache.outputs.dir }} 59 | key: 60 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} 61 | restore-keys: | 62 | ${{ matrix.python-version }}-v1- 63 | 64 | - name: Install dependencies 65 | run: | 66 | python -m pip install --upgrade pip 67 | python -m pip install --upgrade tox tox-gh-actions 68 | 69 | - name: Test with tox 70 | run: tox 71 | env: 72 | COVERAGE: 1 73 | RUNTEST_ARGS: "-v --noinput" 74 | 75 | - name: Upload coverage 76 | uses: codecov/codecov-action@v1 77 | with: 78 | name: Python ${{ matrix.python-version }} 79 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-build clean-pyc clean-test docs qa lint coverage jslint qa-all install-jslint test test-all coverage-console release sdist 2 | 3 | help: 4 | @echo "clean - remove all artifacts" 5 | @echo "clean-build - remove build artifacts" 6 | @echo "clean-pyc - remove Python file artifacts" 7 | @echo "clean-test - remove test and coverage artifacts" 8 | @echo "coverage - check code coverage quickly with the default Python" 9 | @echo "docs - generate Sphinx HTML documentation" 10 | @echo "lint - check style with flake8" 11 | @echo "qa - run linters and test coverage" 12 | @echo "qa-all - run QA plus tox and packaging" 13 | @echo "release - package and upload a release" 14 | @echo "sdist - package" 15 | @echo "test - run tests quickly with the default Python" 16 | @echo "test-all - run tests on every Python version with tox" 17 | @echo "test-release - upload a release to the PyPI test server" 18 | 19 | clean: clean-build clean-pyc clean-test 20 | 21 | qa: lint coverage 22 | 23 | qa-all: qa sdist test-all 24 | 25 | clean-build: 26 | rm -fr build/ 27 | rm -fr dist/ 28 | rm -fr *.egg-info 29 | 30 | clean-pyc: 31 | find . \( -name \*.pyc -o -name \*.pyo -o -name __pycache__ \) -delete 32 | find . -name '*~' -delete 33 | 34 | clean-test: 35 | rm -fr .tox/ 36 | rm -f .coverage 37 | rm -fr htmlcov/ 38 | 39 | docs: 40 | $(MAKE) -C docs clean 41 | $(MAKE) -C docs html 42 | open docs/_build/html/index.html 43 | 44 | lint: 45 | flake8 . 46 | 47 | test: 48 | ./manage.py test 49 | 50 | test-all: 51 | COVERAGE=1 tox --skip-missing-interpreters 52 | 53 | coverage-console: 54 | coverage erase 55 | COVERAGE=1 ./runtests.sh 56 | coverage combine 57 | coverage report -m 58 | 59 | coverage: coverage-console 60 | coverage html 61 | open htmlcov/index.html 62 | 63 | release: sdist 64 | twine upload dist/* 65 | python -m webbrowser -n https://pypi.python.org/pypi/django-nose 66 | 67 | # Add [test] section to ~/.pypirc, https://test.pypi.org/legacy/ 68 | test-release: sdist 69 | twine upload --repository test dist/* 70 | python -m webbrowser -n https://testpypi.python.org/pypi/django-nose 71 | 72 | sdist: clean 73 | python setup.py sdist bdist_wheel 74 | ls -l dist 75 | check-manifest 76 | pyroma dist/`ls -t dist | grep tar.gz | head -n1` 77 | -------------------------------------------------------------------------------- /docs/upgrading.rst: -------------------------------------------------------------------------------- 1 | Upgrading Django 2 | ================ 3 | 4 | Upgrading from Django <= 1.3 to Django 1.4 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | In versions of Django < 1.4 the project folder was in fact a python package as 7 | well (note the __init__.py in your project root). In Django 1.4, there is no 8 | such file and thus the project is not a python module. 9 | 10 | **When you upgrade your Django project to the Django 1.4 layout, you need to 11 | remove the __init__.py file in the root of your project (and move any python 12 | files that reside there other than the manage.py) otherwise you will get a 13 | `ImportError: No module named urls` exception.** 14 | 15 | This happens because Nose will intelligently try to populate your sys.path, and 16 | in this particular case includes your parent directory if your project has a 17 | __init__.py file (see: https://github.com/nose-devs/nose/blob/release_1.1.2/nose/importer.py#L134). 18 | 19 | This means that even though you have set up your directory structure properly and 20 | set your `ROOT_URLCONF='my_project.urls'` to match the new structure, when running 21 | django-nose's test runner it will try to find your urls.py file in `'my_project.my_project.urls'`. 22 | 23 | 24 | Upgrading from Django < 1.2 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | Django 1.2 switches to a `class-based test runner`_. To use django-nose 28 | with Django 1.2, change your ``TEST_RUNNER`` from ``django_nose.run_tests`` to 29 | ``django_nose.NoseTestSuiteRunner``. 30 | 31 | ``django_nose.run_tests`` will continue to work in Django 1.2 but will raise a 32 | warning. In Django 1.3, it will stop working. 33 | 34 | If you were using ``django_nose.run_gis_tests``, you should also switch to 35 | ``django_nose.NoseTestSuiteRunner`` and use one of the `spatial backends`_ in 36 | your ``DATABASES`` settings. 37 | 38 | .. _class-based test runner: http://docs.djangoproject.com/en/dev/releases/1.2/#function-based-test-runners 39 | .. _spatial backends: http://docs.djangoproject.com/en/dev/ref/contrib/gis/db-api/#id1 40 | 41 | Django 1.1 42 | ~~~~~~~~~~ 43 | 44 | If you want to use django-nose with Django 1.1, use 45 | https://github.com/jazzband/django-nose/tree/django-1.1 or 46 | http://pypi.python.org/pypi/django-nose/0.0.3. 47 | 48 | Django 1.0 49 | ~~~~~~~~~~ 50 | 51 | django-nose does not support Django 1.0. 52 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /unittests/test_databases.py: -------------------------------------------------------------------------------- 1 | """Test database access without a database.""" 2 | from contextlib import contextmanager 3 | from unittest import TestCase 4 | 5 | try: 6 | from django.db.models.loading import cache as apps 7 | except ImportError: 8 | from django.apps import apps 9 | 10 | from nose.plugins.attrib import attr 11 | from django_nose.runner import NoseTestSuiteRunner 12 | 13 | 14 | class GetModelsForConnectionTests(TestCase): 15 | """Test runner._get_models_for_connection.""" 16 | 17 | tables = ["test_table%d" % i for i in range(5)] 18 | 19 | def _connection_mock(self, tables): 20 | class FakeIntrospection(object): 21 | def get_table_list(*args, **kwargs): 22 | return tables 23 | 24 | class FakeConnection(object): 25 | introspection = FakeIntrospection() 26 | 27 | def cursor(self): 28 | return None 29 | 30 | return FakeConnection() 31 | 32 | def _model_mock(self, db_table): 33 | class FakeModel(object): 34 | _meta = type("meta", (object,), {"db_table": db_table})() 35 | 36 | return FakeModel() 37 | 38 | @contextmanager 39 | def _cache_mock(self, tables=[]): 40 | def get_models(*args, **kwargs): 41 | return [self._model_mock(t) for t in tables] 42 | 43 | old = apps.get_models 44 | apps.get_models = get_models 45 | yield 46 | apps.get_models = old 47 | 48 | def setUp(self): 49 | """Initialize the runner.""" 50 | self.runner = NoseTestSuiteRunner() 51 | 52 | def test_no_models(self): 53 | """For a DB with no tables, return nothing.""" 54 | connection = self._connection_mock([]) 55 | with self._cache_mock(["table1", "table2"]): 56 | self.assertEqual(self.runner._get_models_for_connection(connection), []) 57 | 58 | def test_wrong_models(self): 59 | """If no tables exists for models, return nothing.""" 60 | connection = self._connection_mock(self.tables) 61 | with self._cache_mock(["table1", "table2"]): 62 | self.assertEqual(self.runner._get_models_for_connection(connection), []) 63 | 64 | @attr("special") 65 | def test_some_models(self): 66 | """If some of the models are in the DB, return matching models.""" 67 | connection = self._connection_mock(self.tables) 68 | with self._cache_mock(self.tables[1:3]): 69 | result_tables = [ 70 | m._meta.db_table 71 | for m in self.runner._get_models_for_connection(connection) 72 | ] 73 | self.assertEqual(result_tables, self.tables[1:3]) 74 | 75 | def test_all_models(self): 76 | """If all the models have in the DB, return them all.""" 77 | connection = self._connection_mock(self.tables) 78 | with self._cache_mock(self.tables): 79 | result_tables = [ 80 | m._meta.db_table 81 | for m in self.runner._get_models_for_connection(connection) 82 | ] 83 | self.assertEqual(result_tables, self.tables) 84 | -------------------------------------------------------------------------------- /django_nose/tools.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # vim: tabstop=4 expandtab autoindent shiftwidth=4 fileencoding=utf-8 3 | """Provides Nose and Django test case assert functions.""" 4 | 5 | from __future__ import unicode_literals 6 | 7 | 8 | def _get_nose_vars(): 9 | """Collect assert_*, ok_, and eq_ from nose.tools.""" 10 | from nose import tools 11 | 12 | new_names = {} 13 | for t in dir(tools): 14 | if t.startswith("assert_") or t in ("ok_", "eq_"): 15 | new_names[t] = getattr(tools, t) 16 | return new_names 17 | 18 | 19 | for _name, _value in _get_nose_vars().items(): 20 | vars()[_name] = _value 21 | 22 | 23 | def _get_django_vars(): 24 | """Collect assert_* methods from Django's TransactionTestCase.""" 25 | import re 26 | from django.test.testcases import TransactionTestCase 27 | 28 | camelcase = re.compile("([a-z][A-Z]|[A-Z][a-z])") 29 | 30 | def insert_underscore(m): 31 | """Insert an appropriate underscore into the name.""" 32 | a, b = m.group(0) 33 | if b.islower(): 34 | return "_{}{}".format(a, b) 35 | else: 36 | return "{}_{}".format(a, b) 37 | 38 | def pep8(name): 39 | """Replace camelcase name with PEP8 equivalent.""" 40 | return str(camelcase.sub(insert_underscore, name).lower()) 41 | 42 | class Dummy(TransactionTestCase): 43 | """A dummy test case for gathering current assertion helpers.""" 44 | 45 | def nop(): 46 | """Do nothing, dummy test to get an initialized test case.""" 47 | pass 48 | 49 | dummy_test = Dummy("nop") 50 | 51 | new_names = {} 52 | for assert_name in [ 53 | at for at in dir(dummy_test) if at.startswith("assert") and "_" not in at 54 | ]: 55 | pepd = pep8(assert_name) 56 | new_names[pepd] = getattr(dummy_test, assert_name) 57 | return new_names 58 | 59 | 60 | for _name, _value in _get_django_vars().items(): 61 | vars()[_name] = _value 62 | 63 | 64 | # 65 | # Additional assertions 66 | # 67 | 68 | 69 | def assert_code(response, status_code, msg_prefix=""): 70 | """Assert the response was returned with the given status code.""" 71 | if msg_prefix: 72 | msg_prefix = "%s: " % msg_prefix 73 | 74 | assert response.status_code == status_code, "Response code was %d (expected %d)" % ( 75 | response.status_code, 76 | status_code, 77 | ) 78 | 79 | 80 | def assert_ok(response, msg_prefix=""): 81 | """Assert the response was returned with status 200 (OK).""" 82 | return assert_code(response, 200, msg_prefix=msg_prefix) 83 | 84 | 85 | def assert_mail_count(count, msg=None): 86 | """Assert the number of emails sent. 87 | 88 | The message here tends to be long, so allow for replacing the whole 89 | thing instead of prefixing. 90 | """ 91 | from django.core import mail 92 | 93 | if msg is None: 94 | msg = ", ".join([e.subject for e in mail.outbox]) 95 | msg = "%d != %d %s" % (len(mail.outbox), count, msg) 96 | # assert_equals is dynamicaly added above. F821 is undefined name error 97 | assert_equals(len(mail.outbox), count, msg) # noqa: F821 98 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """django-nose packaging.""" 4 | import os 5 | from codecs import open 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def get_long_description(title): 10 | """Create the long_description from other files.""" 11 | ROOT = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | readme = open(os.path.join(ROOT, "README.rst"), "r", "utf8").read() 14 | body_tag = ".. Omit badges from docs" 15 | readme_body_start = readme.index(body_tag) 16 | assert readme_body_start 17 | readme_body = readme[readme_body_start + len(body_tag) :] 18 | 19 | changelog = open(os.path.join(ROOT, "changelog.rst"), "r", "utf8").read() 20 | old_tag = ".. Omit older changes from package" 21 | changelog_body_end = changelog.index(old_tag) 22 | assert changelog_body_end 23 | changelog_body = changelog[:changelog_body_end] 24 | 25 | bars = "=" * len(title) 26 | long_description = ( 27 | """ 28 | %(bars)s 29 | %(title)s 30 | %(bars)s 31 | %(readme_body)s 32 | 33 | %(changelog_body)s 34 | 35 | _(Older changes can be found in the full documentation)._ 36 | """ 37 | % locals() 38 | ) 39 | return long_description 40 | 41 | 42 | setup( 43 | name="django-nose", 44 | use_scm_version={"version_scheme": "post-release"}, 45 | setup_requires=["setuptools_scm"], 46 | description="Makes your Django tests simple and snappy", 47 | long_description=get_long_description("django-nose"), 48 | author="Jeff Balogh", 49 | author_email="me@jeffbalogh.org", 50 | maintainer="John Whitlock", 51 | maintainer_email="jwhitlock@mozilla.com", 52 | url="http://github.com/jazzband/django-nose", 53 | license="BSD", 54 | packages=find_packages(exclude=["testapp", "testapp/*"]), 55 | include_package_data=True, 56 | zip_safe=False, 57 | install_requires=["nose>=1.2.1"], 58 | test_suite="testapp.runtests.runtests", 59 | # This blows up tox runs that install django-nose into a virtualenv, 60 | # because it causes Nose to import django_nose.runner before the Django 61 | # settings are initialized, leading to a mess of errors. There's no reason 62 | # we need FixtureBundlingPlugin declared as an entrypoint anyway, since you 63 | # need to be using django-nose to find the it useful, and django-nose knows 64 | # about it intrinsically. 65 | # entry_points=""" 66 | # [nose.plugins.0.10] 67 | # fixture_bundler = django_nose.fixture_bundling:FixtureBundlingPlugin 68 | # """, 69 | keywords="django nose django-nose", 70 | classifiers=[ 71 | "Development Status :: 5 - Production/Stable", 72 | "Environment :: Web Environment", 73 | "Framework :: Django", 74 | "Framework :: Django :: 1.8", 75 | "Framework :: Django :: 1.9", 76 | "Framework :: Django :: 1.10", 77 | "Framework :: Django :: 1.11", 78 | "Framework :: Django :: 2.0", 79 | "Framework :: Django :: 2.1", 80 | "Framework :: Django :: 2.2", 81 | "Intended Audience :: Developers", 82 | "License :: OSI Approved :: BSD License", 83 | "Operating System :: OS Independent", 84 | "Programming Language :: Python", 85 | "Programming Language :: Python :: 3", 86 | "Programming Language :: Python :: 3 :: Only", 87 | "Programming Language :: Python :: 3.5", 88 | "Programming Language :: Python :: 3.6", 89 | "Programming Language :: Python :: 3.7", 90 | "Programming Language :: Python :: 3.8", 91 | "Topic :: Software Development :: Testing", 92 | ], 93 | ) 94 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | **django-nose** was created by Jeff Balogh in 2009, which is a really long time 6 | ago in the Django world. It keeps running because of the contributions of 7 | volunteers. Thank you to everyone who uses django-nose, keeps your projects 8 | up-to-date, files detailed bugs, and submits patches. 9 | 10 | Project Leads 11 | ------------- 12 | * John Whitlock (`jwhitlock `_) 13 | 14 | This is a `Jazzband `_ project, supported by the members, 15 | who agree to abide by the 16 | `Contributor Code of Conduct `_ and follow the 17 | `guidelines `_. 18 | If you'd like to become a project lead, please take a look at the 19 | `Releases documentation `_ and reach out to us. 20 | 21 | Emeritus Maintainers 22 | -------------------- 23 | Thank you for years of maintaining this project! 24 | 25 | * Jeff Balogh (`jbalogh `_) 26 | * Erik Rose (`erikrose `_) 27 | * James Socol (`jscol `_) 28 | * Rob Hudson (`robhudson `_) 29 | 30 | Contributors 31 | ------------ 32 | These non-maintainers have contributed code to a django-nose release: 33 | 34 | * Adam DePue (`adepue `_) 35 | * Alexey Evseev (`st4lk `_) 36 | * Alex (`alexjg `_) 37 | * Antti Kaihola (`akaihola `_) 38 | * Ash Christopher (`ashchristopher `_) 39 | * Blake Winton (`bwinton `_) 40 | * Brad Pitcher (`brad `_) 41 | * Camilo Nova (`camilonova `_) 42 | * Carl Meyer (`carljm `_) 43 | * Conrado Buhrer (`conrado `_) 44 | * David Baumgold (`singingwolfboy `_) 45 | * David Cramer (`dcramer `_) 46 | * Dillon Lareau (`dlareau `_) 47 | * Dmitry Gladkov (`dgladkov `_) 48 | * Ederson Mota (`edrmp `_) 49 | * Eric Zarowny (`ezarowny `_) 50 | * Eron Villarreal (`eroninjapan `_) 51 | * Fred Wenzel (`fwenzel `_) 52 | * Fábio Santos (`fabiosantoscode `_) 53 | * Ionel Cristian Mărieș (`ionelmc `_) 54 | * Jannis Leidel (`jezdez `_) 55 | * Jeremy Satterfield (`jsatt `_) 56 | * Johan Euphrosine (`proppy `_) 57 | * Kyle Robertson (`dvelyk `_) 58 | * Marius Gedminas (`mgedmin `_) 59 | * Martin Chase (`outofculture `_) 60 | * Matthias Bauer (`moeffju `_) 61 | * Michael Elsdörfer (`miracle2k `_) 62 | * Michael Kelly (`Osmose `_) 63 | * Peter Baumgartner (`ipmb `_) 64 | * Radek Simko (`radeksimko `_) 65 | * Ramiro Morales (`ramiro `_) 66 | * Rob Madole (`robmadole `_) 67 | * Roger Hu (`rogerhu `_) 68 | * Ross Lawley (`rozza `_) 69 | * Scott Sexton (`scottsexton `_) 70 | * Stephen Burrows (`melinath `_) 71 | * Sverre Johansen (`sverrejoh `_) 72 | * Tim Child (`timc3 `_) 73 | * Walter Doekes (`wdoekes `_) 74 | * Will Kahn-Greene (`willkg `_) 75 | * Yegor Roganov (`roganov `_) 76 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | django-nose 3 | =========== 4 | 5 | .. image:: https://img.shields.io/pypi/v/django-nose.svg 6 | :alt: The PyPI package 7 | :target: https://pypi.python.org/pypi/django-nose 8 | 9 | .. image:: https://github.com/jazzband/django-nose/workflows/Test/badge.svg 10 | :target: https://github.com/jazzband/django-nose/actions 11 | :alt: GitHub Actions 12 | 13 | .. image:: https://codecov.io/gh/jazzband/django-nose/branch/master/graph/badge.svg 14 | :alt: Coverage 15 | :target: https://codecov.io/gh/jazzband/django-nose 16 | 17 | .. image:: https://jazzband.co/static/img/badge.svg 18 | :alt: Jazzband 19 | :target: https://jazzband.co/ 20 | 21 | .. Omit badges from docs 22 | 23 | **django-nose** provides all the goodness of `nose`_ in your Django tests, like: 24 | 25 | * Testing just your apps by default, not all the standard ones that happen to 26 | be in ``INSTALLED_APPS`` 27 | * Running the tests in one or more specific modules (or apps, or classes, or 28 | folders, or just running a specific test) 29 | * Obviating the need to import all your tests into ``tests/__init__.py``. 30 | This not only saves busy-work but also eliminates the possibility of 31 | accidentally shadowing test classes. 32 | * Taking advantage of all the useful `nose plugins`_ 33 | 34 | .. _nose: https://nose.readthedocs.io/en/latest/ 35 | .. _nose plugins: http://nose-plugins.jottit.com/ 36 | 37 | It also provides: 38 | 39 | * Fixture bundling, an optional feature which speeds up your fixture-based 40 | tests by a factor of 4 41 | * Reuse of previously created test DBs, cutting 10 seconds off startup time 42 | * Hygienic TransactionTestCases, which can save you a DB flush per test 43 | * Support for various databases. Tested with MySQL, PostgreSQL, and SQLite. 44 | Others should work as well. 45 | 46 | django-nose requires nose 1.2.1 or later, and the `latest release`_ is 47 | recommended. It follows the `Django's support policy`_, supporting: 48 | 49 | * Django 1.8 (LTS) with Python 3.5 50 | * Django 1.9 with Python 3.5 51 | * Django 1.10 with Python 3.5 52 | * Django 1.11 (LTS) with Python 3.5 or 3.6 53 | * Django 2.0 with Python 3.5, 3.6, or 3.7 54 | * Django 2.1 with Python 3.5, 3.6, or 3.7 55 | * Django 2.2 with Python 3.5, 3.6, or 3.7 56 | 57 | .. _latest release: https://pypi.python.org/pypi/nose 58 | .. _Django's support policy: https://docs.djangoproject.com/en/1.8/internals/release-process/#supported-versions 59 | 60 | 61 | Note to users 62 | ------------- 63 | 64 | `nose`_ has been in maintenance mode since at least 2015. ``django-nose`` is in 65 | maintenance mode as well, and the sole maintainer is no longer an active user. 66 | See `Jazzband.co`_ to learn how ``django-nose`` is maintained and how you can 67 | help. New projects should consider using `pytest`_, or `unittest`_ with the 68 | `Django testing framework`_. 69 | 70 | .. _Jazzband.co: https://jazzband.co 71 | .. _pytest: https://docs.pytest.org/en/stable/ 72 | .. _unittest: https://docs.python.org/3/library/unittest.html 73 | .. _Django testing framework: https://docs.djangoproject.com/en/3.1/topics/testing/ 74 | 75 | Installation 76 | ------------ 77 | 78 | You can get django-nose from PyPI with... : 79 | 80 | .. code-block:: shell 81 | 82 | $ pip install django-nose 83 | 84 | The development version can be installed with... : 85 | 86 | .. code-block:: shell 87 | 88 | $ pip install -e git://github.com/jazzband/django-nose.git#egg=django-nose 89 | 90 | Since django-nose extends Django's built-in test command, you should add it to 91 | your ``INSTALLED_APPS`` in ``settings.py``: 92 | 93 | .. code-block:: python 94 | 95 | INSTALLED_APPS = ( 96 | ... 97 | 'django_nose', 98 | ... 99 | ) 100 | 101 | Then set ``TEST_RUNNER`` in ``settings.py``: 102 | 103 | .. code-block:: python 104 | 105 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 106 | 107 | Development 108 | ----------- 109 | :Code: https://github.com/jazzband/django-nose 110 | :Issues: https://github.com/jazzband/django-nose/issues?state=open 111 | :Docs: https://django-nose.readthedocs.io 112 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | .. image:: https://jazzband.co/static/img/jazzband.svg 6 | :target: https://jazzband.co/ 7 | :alt: Jazzband 8 | 9 | This is a `Jazzband `_ project. By contributing you agree 10 | to abide by the 11 | `Contributor Code of Conduct `_ and follow the 12 | `guidelines `_. 13 | 14 | Contributions are welcome, and they are greatly appreciated! Every 15 | little bit helps, and credit will always be given. 16 | 17 | You can contribute in many ways: 18 | 19 | Types of Contributions 20 | ---------------------- 21 | 22 | Report Bugs 23 | ~~~~~~~~~~~ 24 | 25 | Report bugs at https://github.com/jazzband/django-nose/issues. 26 | 27 | If you are reporting a bug, please include: 28 | 29 | * The version of django, nose, and django-nose you are using, and any other 30 | applicable packages (``pip freeze`` will show current versions) 31 | * Any details about your local setup that might be helpful in troubleshooting. 32 | * Detailed steps to reproduce the bug. 33 | 34 | When someone submits a pull request to fix your bug, please try it out and 35 | report if it worked for you. 36 | 37 | Fix Bugs 38 | ~~~~~~~~ 39 | 40 | Look through the GitHub issues for bugs. Anything untagged or tagged with "bug" 41 | is open to whoever wants to implement it. 42 | 43 | Implement Features 44 | ~~~~~~~~~~~~~~~~~~ 45 | 46 | Look through the GitHub issues for features. Anything untagged ot tagged with 47 | "feature" is open to whoever wants to implement it. 48 | 49 | django-nose is built on nose, which supports plugins. Consider implementing 50 | your feature as a plugin, maintained by the community using that feature, 51 | rather than adding to the django-nose codebase. 52 | 53 | Write Documentation 54 | ~~~~~~~~~~~~~~~~~~~ 55 | 56 | django-nose could always use more documentation, whether as part of the 57 | official django-nose, as code comments, or even on the web in blog posts, 58 | articles, and such. 59 | 60 | Submit Feedback 61 | ~~~~~~~~~~~~~~~ 62 | 63 | The best way to send feedback is to file an issue at 64 | https://github.com/jazzband/django-nose/issues. 65 | 66 | If you are proposing a feature: 67 | 68 | * Explain in detail how it would work. 69 | * Keep the scope as narrow as possible, to make it easier to implement. 70 | * Remember that this is a volunteer-driven project, and that contributions 71 | are welcome :) 72 | 73 | Get Started! 74 | ------------ 75 | 76 | Ready to contribute? Here's how to set up django-nose 77 | for local development. 78 | 79 | 1. Fork the `django-nose` repo on GitHub. 80 | 2. Clone your fork locally:: 81 | 82 | $ git clone git@github.com:your_name_here/django-nose.git 83 | 84 | 3. Install your local copy into a virtualenv. Assuming you have 85 | virtualenvwrapper installed, this is how you set up your fork for local 86 | development:: 87 | 88 | $ mkvirtualenv django-nose 89 | $ cd django-nose/ 90 | $ pip install -r requirements.txt 91 | $ ./manage.py migrate 92 | 93 | 4. Create a branch for local development:: 94 | 95 | $ git checkout -b name-of-your-bugfix-or-feature 96 | 97 | Now you can make your changes locally. 98 | 99 | 5. Make sure existing tests continue to pass with your new code:: 100 | 101 | $ make qa 102 | 103 | 6. When you're done making changes, check that your changes pass flake8 and the 104 | tests, including testing other Python versions with tox:: 105 | 106 | $ make qa-all 107 | 108 | 6. Commit your changes and push your branch to GitHub:: 109 | 110 | $ git add . 111 | $ git commit -m "Your detailed description of your changes." 112 | $ git push origin name-of-your-bugfix-or-feature 113 | 114 | 7. Submit a pull request through the GitHub website. 115 | 116 | Pull Request Guidelines 117 | ----------------------- 118 | 119 | Before you submit a pull request, check that it meets these guidelines: 120 | 121 | 1. The pull request should be in a branch. 122 | 2. The pull request should include tests. 123 | 3. You agree to license your contribution under the BSD license. 124 | 4. If the pull request adds functionality, the docs should be updated. 125 | 5. Make liberal use of `git rebase` to ensure clean commits on top of master. 126 | 6. The pull request should pass QA tests and work for supported Python / Django 127 | combinations. Check 128 | https://github.com/jazzband/django-nose/actions 129 | and make sure that the tests pass for all supported Python versions. 130 | 131 | Tips 132 | ---- 133 | 134 | The django-nose testapp uses django-nose, so all of the features are available. 135 | To run a subset of tests:: 136 | 137 | $ ./manage.py test testapp/tests.py 138 | 139 | To mark failed tests:: 140 | 141 | $ ./manage.py test --failed 142 | 143 | To re-run only the failed tests:: 144 | 145 | $ ./manage.py test --failed 146 | 147 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Parse command line 4 | VERBOSE=0 5 | HELP=0 6 | ERR=0 7 | NOINPUT="" 8 | while [[ $# > 0 ]] 9 | do 10 | key="$1" 11 | case $key in 12 | -h|--help) 13 | HELP=1 14 | ;; 15 | -v|--verbose) 16 | VERBOSE=1 17 | ;; 18 | --noinput) 19 | NOINPUT="--noinput" 20 | ;; 21 | *) 22 | echo "Unknown option '$key'" 23 | ERR=1 24 | HELP=1 25 | ;; 26 | esac 27 | shift 28 | done 29 | 30 | if [ $HELP -eq 1 ] 31 | then 32 | echo "$0 [-vh] - Run django-nose integration tests." 33 | echo " -v/--verbose - Print output of test commands." 34 | echo " -h/--help - Print this help message." 35 | exit $ERR 36 | fi 37 | 38 | export PYTHONPATH=. 39 | export DATABASE_URL=${DATABASE_URL:-"sqlite:////tmp/test.db"} 40 | 41 | HAS_HOTSHOT=$(python -c "\ 42 | try: 43 | import hotshot 44 | except ImportError: 45 | print('0') 46 | else: 47 | print('1') 48 | ") 49 | 50 | reset_env() { 51 | unset TEST_RUNNER 52 | unset NOSE_PLUGINS 53 | unset REUSE_DB 54 | } 55 | 56 | echo_output() { 57 | STDOUT=$1 58 | STDERR=$2 59 | if [ $VERBOSE -ne 1 ] 60 | then 61 | echo "stdout" 62 | echo "======" 63 | cat $STDOUT 64 | echo 65 | echo "stderr" 66 | echo "======" 67 | cat $STDERR 68 | fi 69 | rm $STDOUT $STDERR 70 | } 71 | 72 | django_test() { 73 | COMMAND=$1 74 | TEST_COUNT=$2 75 | DESCRIPTION=$3 76 | CAN_FAIL=${4:-0} 77 | 78 | if [ $VERBOSE -eq 1 ] 79 | then 80 | echo "================" 81 | echo "Django settings:" 82 | ./manage.py diffsettings 83 | echo "================" 84 | fi 85 | 86 | if [ -n "$COVERAGE" ] 87 | then 88 | TEST="coverage run -p $COMMAND" 89 | else 90 | TEST="$COMMAND" 91 | fi 92 | # Temp files on Linux / OSX 93 | TMP_OUT=`mktemp 2>/dev/null || mktemp -t 'django-nose-runtests'` 94 | TMP_ERR=`mktemp 2>/dev/null || mktemp -t 'django-nose-runtests'` 95 | RETURN=0 96 | if [ $VERBOSE -eq 1 ] 97 | then 98 | echo $TEST 99 | $TEST > >(tee $TMP_OUT) 2> >(tee $TMP_ERR >&2) 100 | else 101 | $TEST >$TMP_OUT 2>$TMP_ERR 102 | fi 103 | if [ $? -gt 0 ] 104 | then 105 | echo "FAIL (test failure): $DESCRIPTION" 106 | echo_output $TMP_OUT $TMP_ERR 107 | if [ "$CAN_FAIL" == "0" ] 108 | then 109 | exit 1 110 | else 111 | return 112 | fi 113 | fi 114 | OUTPUT=`cat $TMP_OUT $TMP_ERR` 115 | echo $OUTPUT | grep "Ran $TEST_COUNT test" > /dev/null 116 | if [ $? -gt 0 ] 117 | then 118 | echo "FAIL (count!=$TEST_COUNT): $DESCRIPTION" 119 | echo_output $TMP_OUT $TMP_ERR 120 | if [ "$CAN_FAIL" == "0" ] 121 | then 122 | exit 1 123 | else 124 | return 125 | fi 126 | else 127 | echo "PASS (count==$TEST_COUNT): $DESCRIPTION" 128 | fi 129 | rm $TMP_OUT $TMP_ERR 130 | 131 | # Check that we're hijacking the help correctly. 132 | $TEST --help 2>&1 | grep 'NOSE_DETAILED_ERRORS' > /dev/null 133 | if [ $? -gt 0 ] 134 | then 135 | echo "FAIL (--help): $DESCRIPTION" 136 | if [ "$CAN_FAIL" == 0 ] 137 | then 138 | exit 1; 139 | else 140 | return 141 | fi 142 | else 143 | echo "PASS ( --help): $DESCRIPTION" 144 | fi 145 | } 146 | 147 | TESTAPP_COUNT=6 148 | 149 | reset_env 150 | django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'normal settings' 151 | 152 | reset_env 153 | export TEST_RUNNER="django_nose.NoseTestSuiteRunner" 154 | django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'test runner from environment' 155 | 156 | reset_env 157 | django_test "testapp/runtests.py testapp.test_only_this" 1 'via run_tests API' 158 | 159 | reset_env 160 | export NOSE_PLUGINS="testapp.plugins.SanityCheckPlugin" 161 | django_test "./manage.py test testapp/plugin_t $NOINPUT" 1 'with plugins' 162 | 163 | reset_env 164 | django_test "./manage.py test unittests $NOINPUT" 4 'unittests' 165 | 166 | reset_env 167 | django_test "./manage.py test unittests --verbosity 1 $NOINPUT" 4 'argument option without equals' 168 | 169 | reset_env 170 | django_test "./manage.py test unittests --nose-verbosity=2 $NOINPUT" 4 'argument with equals' 171 | 172 | reset_env 173 | django_test "./manage.py test unittests --testrunner=testapp.custom_runner.CustomNoseTestSuiteRunner $NOINPUT" 4 'unittests with testrunner' 174 | 175 | reset_env 176 | django_test "./manage.py test unittests --attr special $NOINPUT" 1 'select by attribute' 177 | 178 | reset_env 179 | export REUSE_DB=1 180 | # For the many issues with REUSE_DB=1, see: 181 | # https://github.com/jazzband/django-nose/milestones/Fix%20REUSE_DB=1 182 | django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'with REUSE_DB=1, call #1' 'can fail' 183 | django_test "./manage.py test $NOINPUT" $TESTAPP_COUNT 'with REUSE_DB=1, call #2' 'can fail' 184 | 185 | 186 | if [ "$HAS_HOTSHOT" = "1" ] 187 | then 188 | # Python 3 doesn't support the hotshot profiler. See nose#842. 189 | reset_env 190 | django_test "./manage.py test $NOINPUT --with-profile --profile-restrict less_output" $TESTAPP_COUNT 'with profile plugin' 191 | fi 192 | -------------------------------------------------------------------------------- /changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | unreleased 5 | ~~~~~~~~~~ 6 | 7 | * Dropped Python 2 support. 8 | * Moved CI to 9 | `GitHub Actions `_. 10 | 11 | 1.4.7 (2020-08-19) 12 | ~~~~~~~~~~~~~~~~~~ 13 | * Document Django 2.2 support, no changes needed 14 | * Move project to `Jazzband.co `_ 15 | * Fix fixture loading on MySQL 16 | (`issue 307 `_, 17 | `dlareau `_) 18 | 19 | 1.4.6 (2018-10-03) 20 | ~~~~~~~~~~~~~~~~~~ 21 | * Document Django 2.0 and 2.1 support, no changes needed 22 | * Document Python 3.7 support 23 | 24 | 1.4.5 (2017-08-22) 25 | ~~~~~~~~~~~~~~~~~~ 26 | * Add Django 1.11 support 27 | 28 | 1.4.4 (2016-06-27) 29 | ~~~~~~~~~~~~~~~~~~ 30 | * Add Django 1.10 support 31 | * Drop Django 1.4 - 1.7, and Python 2.6 support 32 | * Drop South support 33 | 34 | 1.4.3 (2015-12-28) 35 | ~~~~~~~~~~~~~~~~~~ 36 | * Add Django 1.9 support 37 | * Support long options without equals signs, such as "--attr selected" 38 | * Support nose plugins using callback options 39 | * Support nose options without default values (jsatt) 40 | * Remove Django from install dependencies, to avoid accidental upgrades 41 | (jsocol, willkg) 42 | * Setting REUSE_DB to an empty value now disables REUSE_DB, instead of 43 | enabling it (wdoekes) 44 | 45 | 1.4.2 (2015-10-07) 46 | ~~~~~~~~~~~~~~~~~~ 47 | * Warn against using REUSE_DB=1 and FastFixtureTestCase in docs 48 | * REUSE_DB=1 uses new transaction management in Django 1.7, 1.8 (scottsexton) 49 | * Try to avoid accidentally using production database with REUSE_DB=1 (alexjg, eroninjapan) 50 | * Supported Django versions limited to current supported Django version 1.4, 51 | 1.7, and 1.8, as well as relevant Python versions. 52 | 53 | 1.4.1 (2015-06-29) 54 | ~~~~~~~~~~~~~~~~~~ 55 | * Fix version number (ezarowny) 56 | * Fix choice options, unbreaking nose-cover (aamirtharaj-rpx, jwhitlock) 57 | * Support 1.8 app loading system (dgladkov) 58 | * Support non-ASCII file names (singingwolfboy) 59 | * Better PEP8'd assertion names (roganov) 60 | 61 | 1.4 (2015-04-23) 62 | ~~~~~~~~~~~~~~~~ 63 | * Django 1.8 support (timc3, adepue, jwhitlock) 64 | * Support --testrunner option (st4lk) 65 | * Fix REUSE_DB second run in py3k (edrmp) 66 | 67 | 1.3 (2014-12-05) 68 | ~~~~~~~~~~~~~~~~ 69 | * Django 1.6 and 1.7 support (conrado, co3k, Nepherhotep, mbertheau) 70 | * Python 3.3 and 3.4 testing and support (frewsxcv, jsocol) 71 | 72 | 1.2 (2013-07-23) 73 | ~~~~~~~~~~~~~~~~ 74 | * Python 3 support (melinath and jonashaag) 75 | * Django 1.5 compat (fabiosantoscode) 76 | 77 | 1.1 (2012-05-19) 78 | ~~~~~~~~~~~~~~~~ 79 | * Django TransactionTestCases don't clean up after themselves; they leave 80 | junk in the DB and clean it up only on ``_pre_setup``. Thus, Django makes 81 | sure these tests run last. Now django-nose does, too. This means one fewer 82 | source of failures on existing projects. (Erik Rose) 83 | * Add support for hygienic TransactionTestCases. (Erik Rose) 84 | * Support models that are used only for tests. Just put them in any file 85 | imported in the course of loading tests. No more crazy hacks necessary. 86 | (Erik Rose) 87 | * Make the fixture bundler more conservative, fixing some conceivable 88 | situations in which fixtures would not appear as intended if a 89 | TransactionTestCase found its way into the middle of a bundle. (Erik Rose) 90 | * Fix an error that would surface when using SQLAlchemy with connection 91 | pooling. (Roger Hu) 92 | * Gracefully ignore the new ``--liveserver`` option introduced in Django 1.4; 93 | don't let it through to nose. (Adam DePue) 94 | 95 | 1.0 (2012-03-12) 96 | ~~~~~~~~~~~~~~~~ 97 | * New fixture-bundling plugin for avoiding needless fixture setup (Erik Rose) 98 | * Moved FastFixtureTestCase in from test-utils, so now all the 99 | fixture-bundling stuff is in one library. (Erik Rose) 100 | * Added the REUSE_DB setting for faster startup and shutdown. (Erik Rose) 101 | * Fixed a crash when printing options with certain verbosities. (Daniel Abel) 102 | * Broke hard dependency on MySQL. Support PostgreSQL. (Roger Hu) 103 | * Support SQLite, both memory- and disk-based. (Roger Hu and Erik Rose) 104 | * Nail down versions of the package requirements. (Daniel Mizyrycki) 105 | 106 | .. Omit older changes from package 107 | 108 | 0.1.3 (2010-04-15) 109 | ~~~~~~~~~~~~~~~~~~ 110 | * Even better coverage support (rozza) 111 | * README fixes (carljm and ionelmc) 112 | * optparse OptionGroups are handled better (outofculture) 113 | * nose plugins are loaded before listing options 114 | 115 | 0.1.2 (2010-08-14) 116 | ~~~~~~~~~~~~~~~~~~ 117 | * run_tests API support (carjm) 118 | * better coverage numbers (rozza & miracle2k) 119 | * support for adding custom nose plugins (kumar303) 120 | 121 | 0.1.1 (2010-06-01) 122 | ~~~~~~~~~~~~~~~~~~ 123 | * Cleaner installation (Michael Fladischer) 124 | 125 | 0.1 (2010-05-18) 126 | ~~~~~~~~~~~~~~~~ 127 | * Class-based test runner (Antti Kaihola) 128 | * Django 1.2 compatibility (Antti Kaihola) 129 | * Mapping Django verbosity to nose verbosity 130 | 131 | 0.0.3 (2009-12-31) 132 | ~~~~~~~~~~~~~~~~~~ 133 | * Python 2.4 support (Blake Winton) 134 | * GeoDjango spatial database support (Peter Baumgartner) 135 | * Return the number of failing tests on the command line 136 | 137 | 0.0.2 (2009-10-01) 138 | ~~~~~~~~~~~~~~~~~~ 139 | * rst readme (Rob Madole) 140 | 141 | 0.0.1 (2009-10-01) 142 | ~~~~~~~~~~~~~~~~~~ 143 | * birth! 144 | -------------------------------------------------------------------------------- /django_nose/fixture_tables.py: -------------------------------------------------------------------------------- 1 | """Unload fixtures by truncating tables rather than rebuilding. 2 | 3 | A copy of Django 1.3.0's stock loaddata.py, adapted so that, instead of 4 | loading any data, it returns the tables referenced by a set of fixtures so we 5 | can truncate them (and no others) quickly after we're finished with them. 6 | """ 7 | 8 | import os 9 | import gzip 10 | import zipfile 11 | from itertools import product 12 | 13 | from django.conf import settings 14 | from django.core import serializers 15 | from django.db import router, DEFAULT_DB_ALIAS 16 | 17 | try: 18 | from django.db.models import get_apps 19 | except ImportError: 20 | from django.apps import apps 21 | 22 | def get_apps(): 23 | """Emulate get_apps in Django 1.9 and later.""" 24 | return [a.models_module for a in apps.get_app_configs()] 25 | 26 | 27 | try: 28 | import bz2 29 | 30 | has_bz2 = True 31 | except ImportError: 32 | has_bz2 = False 33 | 34 | 35 | def tables_used_by_fixtures(fixture_labels, using=DEFAULT_DB_ALIAS): 36 | """Get tables used by a fixture. 37 | 38 | Acts like Django's stock loaddata command, but, instead of loading data, 39 | return an iterable of the names of the tables into which data would be 40 | loaded. 41 | """ 42 | # Keep a count of the installed objects and fixtures 43 | fixture_count = 0 44 | loaded_object_count = 0 45 | fixture_object_count = 0 46 | tables = set() 47 | 48 | class SingleZipReader(zipfile.ZipFile): 49 | def __init__(self, *args, **kwargs): 50 | zipfile.ZipFile.__init__(self, *args, **kwargs) 51 | if settings.DEBUG: 52 | assert ( 53 | len(self.namelist()) == 1 54 | ), "Zip-compressed fixtures must contain only one file." 55 | 56 | def read(self): 57 | return zipfile.ZipFile.read(self, self.namelist()[0]) 58 | 59 | compression_types = {None: open, "gz": gzip.GzipFile, "zip": SingleZipReader} 60 | if has_bz2: 61 | compression_types["bz2"] = bz2.BZ2File 62 | 63 | app_module_paths = [] 64 | for app in get_apps(): 65 | if hasattr(app, "__path__"): 66 | # It's a 'models/' subpackage 67 | for path in app.__path__: 68 | app_module_paths.append(path) 69 | else: 70 | # It's a models.py module 71 | app_module_paths.append(app.__file__) 72 | 73 | app_fixtures = [ 74 | os.path.join(os.path.dirname(path), "fixtures") for path in app_module_paths 75 | ] 76 | for fixture_label in fixture_labels: 77 | parts = fixture_label.split(".") 78 | 79 | if len(parts) > 1 and parts[-1] in compression_types: 80 | compression_formats = [parts[-1]] 81 | parts = parts[:-1] 82 | else: 83 | compression_formats = list(compression_types.keys()) 84 | 85 | if len(parts) == 1: 86 | fixture_name = parts[0] 87 | formats = serializers.get_public_serializer_formats() 88 | else: 89 | fixture_name, format = ".".join(parts[:-1]), parts[-1] 90 | if format in serializers.get_public_serializer_formats(): 91 | formats = [format] 92 | else: 93 | formats = [] 94 | 95 | if not formats: 96 | # stderr.write(style.ERROR("Problem installing fixture '%s': %s is 97 | # not a known serialization format.\n" % (fixture_name, format))) 98 | return set() 99 | 100 | if os.path.isabs(fixture_name): 101 | fixture_dirs = [fixture_name] 102 | else: 103 | fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [""] 104 | 105 | for fixture_dir in fixture_dirs: 106 | # stdout.write("Checking %s for fixtures...\n" % 107 | # humanize(fixture_dir)) 108 | 109 | label_found = False 110 | for combo in product([using, None], formats, compression_formats): 111 | database, format, compression_format = combo 112 | file_name = ".".join( 113 | p for p in [fixture_name, database, format, compression_format] if p 114 | ) 115 | 116 | # stdout.write("Trying %s for %s fixture '%s'...\n" % \ 117 | # (humanize(fixture_dir), file_name, fixture_name)) 118 | full_path = os.path.join(fixture_dir, file_name) 119 | open_method = compression_types[compression_format] 120 | try: 121 | fixture = open_method(full_path, "r") 122 | if label_found: 123 | fixture.close() 124 | # stderr.write(style.ERROR("Multiple fixtures named 125 | # '%s' in %s. Aborting.\n" % (fixture_name, 126 | # humanize(fixture_dir)))) 127 | return set() 128 | else: 129 | fixture_count += 1 130 | objects_in_fixture = 0 131 | loaded_objects_in_fixture = 0 132 | # stdout.write("Installing %s fixture '%s' from %s.\n" 133 | # % (format, fixture_name, humanize(fixture_dir))) 134 | try: 135 | objects = serializers.deserialize( 136 | format, fixture, using=using 137 | ) 138 | for obj in objects: 139 | objects_in_fixture += 1 140 | cls = obj.object.__class__ 141 | if router.allow_syncdb(using, cls): 142 | loaded_objects_in_fixture += 1 143 | tables.add(cls._meta.db_table) 144 | loaded_object_count += loaded_objects_in_fixture 145 | fixture_object_count += objects_in_fixture 146 | label_found = True 147 | except (SystemExit, KeyboardInterrupt): 148 | raise 149 | except Exception: 150 | fixture.close() 151 | # stderr.write( style.ERROR("Problem installing 152 | # fixture '%s': %s\n" % (full_path, ''.join(tra 153 | # ceback.format_exception(sys.exc_type, 154 | # sys.exc_value, sys.exc_traceback))))) 155 | return set() 156 | fixture.close() 157 | 158 | # If the fixture we loaded contains 0 objects, assume 159 | # that an error was encountered during fixture loading. 160 | if objects_in_fixture == 0: 161 | # stderr.write( style.ERROR("No fixture data found 162 | # for '%s'. (File format may be invalid.)\n" % 163 | # (fixture_name))) 164 | return set() 165 | 166 | except Exception: 167 | # stdout.write("No %s fixture '%s' in %s.\n" % \ (format, 168 | # fixture_name, humanize(fixture_dir))) 169 | pass 170 | 171 | return tables 172 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -W -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-nose.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-nose.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-nose" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-nose" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-nose.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-nose.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /django_nose/testcases.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """TestCases that enable extra django-nose functionality.""" 3 | from django import test 4 | from django.conf import settings 5 | from django.core import cache, mail 6 | from django.core.management import call_command 7 | from django.db import connections, DEFAULT_DB_ALIAS, transaction 8 | 9 | from django_nose.fixture_tables import tables_used_by_fixtures 10 | from django_nose.utils import uses_mysql 11 | 12 | 13 | __all__ = ("FastFixtureTestCase",) 14 | 15 | 16 | class FastFixtureTestCase(test.TransactionTestCase): 17 | """Test case that loads fixtures once rather than once per test. 18 | 19 | Using this can save huge swaths of time while still preserving test 20 | isolation. Fixture data is loaded at class setup time, and the transaction 21 | is committed. Commit and rollback methods are then monkeypatched away (like 22 | in Django's standard TestCase), and each test is run. After each test, the 23 | monkeypatching is temporarily undone, and a rollback is issued, returning 24 | the DB content to the pristine fixture state. Finally, upon class teardown, 25 | the DB is restored to a post-syncdb-like state by deleting the contents of 26 | any table that had been touched by a fixture (keeping infrastructure tables 27 | like django_content_type and auth_permission intact). 28 | 29 | Note that this is like Django's TestCase, not its TransactionTestCase, in 30 | that you cannot do your own commits or rollbacks from within tests. 31 | 32 | For best speed, group tests using the same fixtures into as few classes as 33 | possible. Better still, don't do that, and instead use the fixture-bundling 34 | plugin from django-nose, which does it dynamically at test time. 35 | """ 36 | 37 | cleans_up_after_itself = True # This is the good kind of puppy. 38 | 39 | @classmethod 40 | def setUpClass(cls): 41 | """Turn on manual commits. Load and commit the fixtures.""" 42 | if not test.testcases.connections_support_transactions(): 43 | raise NotImplementedError( 44 | "%s supports only DBs with transaction " "capabilities." % cls.__name__ 45 | ) 46 | for db in cls._databases(): 47 | # These MUST be balanced with one leave_* each: 48 | transaction.enter_transaction_management(using=db) 49 | # Don't commit unless we say so: 50 | transaction.managed(True, using=db) 51 | 52 | cls._fixture_setup() 53 | 54 | @classmethod 55 | def tearDownClass(cls): 56 | """Truncate the world, and turn manual commit management back off.""" 57 | cls._fixture_teardown() 58 | for db in cls._databases(): 59 | # Finish off any transactions that may have happened in 60 | # tearDownClass in a child method. 61 | if transaction.is_dirty(using=db): 62 | transaction.commit(using=db) 63 | transaction.leave_transaction_management(using=db) 64 | 65 | @classmethod 66 | def _fixture_setup(cls): 67 | """Load fixture data, and commit.""" 68 | for db in cls._databases(): 69 | if hasattr(cls, "fixtures") and getattr( 70 | cls, "_fb_should_setup_fixtures", True 71 | ): 72 | # Iff the fixture-bundling test runner tells us we're the first 73 | # suite having these fixtures, set them up: 74 | call_command( 75 | "loaddata", 76 | *cls.fixtures, 77 | **{"verbosity": 0, "commit": False, "database": db} 78 | ) 79 | # No matter what, to preserve the effect of cursor start-up 80 | # statements... 81 | transaction.commit(using=db) 82 | 83 | @classmethod 84 | def _fixture_teardown(cls): 85 | """Empty (only) the tables we loaded fixtures into, then commit.""" 86 | if hasattr(cls, "fixtures") and getattr( 87 | cls, "_fb_should_teardown_fixtures", True 88 | ): 89 | # If the fixture-bundling test runner advises us that the next test 90 | # suite is going to reuse these fixtures, don't tear them down. 91 | for db in cls._databases(): 92 | tables = tables_used_by_fixtures(cls.fixtures, using=db) 93 | # TODO: Think about respecting _meta.db_tablespace, not just 94 | # db_table. 95 | if tables: 96 | connection = connections[db] 97 | cursor = connection.cursor() 98 | 99 | # TODO: Rather than assuming that anything added to by a 100 | # fixture can be emptied, remove only what the fixture 101 | # added. This would probably solve input.mozilla.com's 102 | # failures (since worked around) with Site objects; they 103 | # were loading additional Sites with a fixture, and then 104 | # the Django-provided example.com site was evaporating. 105 | if uses_mysql(connection): 106 | cursor.execute("SET FOREIGN_KEY_CHECKS=0") 107 | for table in tables: 108 | # Truncate implicitly commits. 109 | cursor.execute("TRUNCATE `%s`" % table) 110 | # TODO: necessary? 111 | cursor.execute("SET FOREIGN_KEY_CHECKS=1") 112 | else: 113 | for table in tables: 114 | cursor.execute("DELETE FROM %s" % table) 115 | 116 | transaction.commit(using=db) 117 | # cursor.close() # Should be unnecessary, since we committed 118 | # any environment-setup statements that come with opening a new 119 | # cursor when we committed the fixtures. 120 | 121 | def _pre_setup(self): 122 | """Disable transaction methods, and clear some globals.""" 123 | # Repeat stuff from TransactionTestCase, because I'm not calling its 124 | # _pre_setup, because that would load fixtures again. 125 | cache.cache.clear() 126 | settings.TEMPLATE_DEBUG = settings.DEBUG = False 127 | 128 | test.testcases.disable_transaction_methods() 129 | 130 | self.client = self.client_class() 131 | # self._fixture_setup() 132 | self._urlconf_setup() 133 | mail.outbox = [] 134 | 135 | # Clear site cache in case somebody's mutated Site objects and then 136 | # cached the mutated stuff: 137 | from django.contrib.sites.models import Site 138 | 139 | Site.objects.clear_cache() 140 | 141 | def _post_teardown(self): 142 | """Re-enable transaction methods, and roll back any changes. 143 | 144 | Rollback clears any DB changes made by the test so the original fixture 145 | data is again visible. 146 | 147 | """ 148 | # Rollback any mutations made by tests: 149 | test.testcases.restore_transaction_methods() 150 | for db in self._databases(): 151 | transaction.rollback(using=db) 152 | 153 | self._urlconf_teardown() 154 | 155 | # We do not need to close the connection here to prevent 156 | # http://code.djangoproject.com/ticket/7572, since we commit, not 157 | # rollback, the test fixtures and thus any cursor startup statements. 158 | 159 | # Don't call through to superclass, because that would call 160 | # _fixture_teardown() and close the connection. 161 | 162 | @classmethod 163 | def _databases(cls): 164 | if getattr(cls, "multi_db", False): 165 | return connections 166 | else: 167 | return [DEFAULT_DB_ALIAS] 168 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | The day-to-day use of django-nose is mostly transparent; just run ``./manage.py 5 | test`` as usual. 6 | 7 | See ``./manage.py help test`` for all the options nose provides, and look to 8 | the `nose docs`_ for more help with nose. 9 | 10 | .. _nose docs: https://nose.readthedocs.io/en/latest/ 11 | 12 | Enabling Database Reuse 13 | ----------------------- 14 | 15 | .. warning:: There are several 16 | `open issues `_ 17 | with this feature, including 18 | `reports of data loss `_. 19 | 20 | You can save several seconds at the beginning and end of your test suite by 21 | reusing the test database from the last run. To do this, set the environment 22 | variable ``REUSE_DB`` to 1:: 23 | 24 | REUSE_DB=1 ./manage.py test 25 | 26 | The one new wrinkle is that, whenever your DB schema changes, you should leave 27 | the flag off the next time you run tests. This will cue the test runner to 28 | reinitialize the test database. 29 | 30 | Also, REUSE_DB is not compatible with TransactionTestCases that leave junk in 31 | the DB, so be sure to make your TransactionTestCases hygienic (see below) if 32 | you want to use it. 33 | 34 | 35 | Enabling Fast Fixtures 36 | ---------------------- 37 | 38 | .. warning:: There are several 39 | `known issues `_ 40 | with this feature. 41 | 42 | django-nose includes a fixture bundler which drastically speeds up your tests 43 | by eliminating redundant setup of Django test fixtures. To use it... 44 | 45 | 1. Subclass ``django_nose.FastFixtureTestCase`` instead of 46 | ``django.test.TestCase``. (I like to import it ``as TestCase`` in my 47 | project's ``tests/__init__.py`` and then import it from there into my actual 48 | tests. Then it's easy to sub the base class in and out.) This alone will 49 | cause fixtures to load once per class rather than once per test. 50 | 2. Activate fixture bundling by passing the ``--with-fixture-bundling`` option 51 | to ``./manage.py test``. This loads each unique set of fixtures only once, 52 | even across class, module, and app boundaries. 53 | 54 | How Fixture Bundling Works 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | The fixture bundler reorders your test classes so that ones with identical sets 58 | of fixtures run adjacently. It then advises the first of each series to load 59 | the fixtures once for all of them (and the remaining ones not to bother). It 60 | also advises the last to tear them down. Depending on the size and repetition 61 | of your fixtures, you can expect a 25% to 50% speed increase. 62 | 63 | Incidentally, the author prefers to avoid Django fixtures, as they encourage 64 | irrelevant coupling between tests and make tests harder to comprehend and 65 | modify. For future tests, it is better to use the "model maker" pattern, 66 | creating DB objects programmatically. This way, tests avoid setup they don't 67 | need, and there is a clearer tie between a test and the exact state it 68 | requires. The fixture bundler is intended to make existing tests, which have 69 | already committed to fixtures, more tolerable. 70 | 71 | Troubleshooting 72 | ~~~~~~~~~~~~~~~ 73 | 74 | If using ``--with-fixture-bundling`` causes test failures, it likely indicates 75 | an order dependency between some of your tests. Here are the most frequent 76 | sources of state leakage we have encountered: 77 | 78 | * Locale activation, which is maintained in a threadlocal variable. Be sure to 79 | reset your locale selection between tests. 80 | * memcached contents. Be sure to flush between tests. Many test superclasses do 81 | this automatically. 82 | 83 | It's also possible that you have ``post_save`` signal handlers which create 84 | additional database rows while loading the fixtures. ``FastFixtureTestCase`` 85 | isn't yet smart enough to notice this and clean up after it, so you'll have to 86 | go back to plain old ``TestCase`` for now. 87 | 88 | Exempting A Class From Bundling 89 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 90 | 91 | In some unusual cases, it is desirable to exempt a test class from fixture 92 | bundling, forcing it to set up and tear down its fixtures at the class 93 | boundaries. For example, we might have a ``TestCase`` subclass which sets up 94 | some state outside the DB in ``setUpClass`` and tears it down in 95 | ``tearDownClass``, and it might not be possible to adapt those routines to heed 96 | the advice of the fixture bundler. In such a case, simply set the 97 | ``exempt_from_fixture_bundling`` attribute of the test class to ``True``. 98 | 99 | 100 | Speedy Hygienic TransactionTestCases 101 | ------------------------------------ 102 | 103 | Unlike the stock Django test runner, django-nose lets you write custom 104 | TransactionTestCase subclasses which expect to start with an unmarred DB, 105 | saving an entire DB flush per test. 106 | 107 | Background 108 | ~~~~~~~~~~ 109 | 110 | The default Django TransactionTestCase class `can leave the DB in an unclean 111 | state`_ when it's done. To compensate, TransactionTestCase does a 112 | time-consuming flush of the DB *before* each test to ensure it begins with a 113 | clean slate. Django's stock test runner then runs TransactionTestCases last so 114 | they don't wreck the environment for better-behaved tests. django-nose 115 | replicates this behavior. 116 | 117 | Escaping the Grime 118 | ~~~~~~~~~~~~~~~~~~ 119 | 120 | Some people, however, have made subclasses of TransactionTestCase that clean up 121 | after themselves (and can do so efficiently, since they know what they've 122 | changed). Like TestCase, these may assume they start with a clean DB. However, 123 | any TransactionTestCases that run before them and leave a mess could cause them 124 | to fail spuriously. 125 | 126 | django-nose offers to fix this. If you include a special attribute on your 127 | well-behaved TransactionTestCase... :: 128 | 129 | class MyNiceTestCase(TransactionTestCase): 130 | cleans_up_after_itself = True 131 | 132 | ...django-nose will run it before any of those nasty, trash-spewing test cases. 133 | You can thus enjoy a big speed boost any time you make a TransactionTestCase 134 | clean up after itself: skipping a whole DB flush before every test. With a 135 | large schema, this can save minutes of IO. 136 | 137 | django-nose's own FastFixtureTestCase uses this feature, even though it 138 | ultimately acts more like a TestCase than a TransactionTestCase. 139 | 140 | .. _can leave the DB in an unclean state: https://docs.djangoproject.com/en/1.4/topics/testing/#django.test.TransactionTestCase 141 | 142 | 143 | Test-Only Models 144 | ---------------- 145 | 146 | If you have a model that is used only by tests (for example, to test an 147 | abstract model base class), you can put it in any file that's imported in the 148 | course of loading tests. For example, if the tests that need it are in 149 | ``test_models.py``, you can put the model in there, too. django-nose will make 150 | sure its DB table gets created. 151 | 152 | 153 | Assertions 154 | ---------- 155 | 156 | ``django-nose.tools`` provides pep8 versions of Django's TestCase asserts 157 | and some of its own as functions. :: 158 | 159 | assert_redirects(response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix='') 160 | 161 | assert_contains(response, text, count=None, status_code=200, msg_prefix='') 162 | assert_not_contains(response, text, count=None, status_code=200, msg_prefix='') 163 | 164 | assert_form_error(response, form, field, errors, msg_prefix='') 165 | 166 | assert_template_used(response, template_name, msg_prefix='') 167 | assert_template_not_used(response, template_name, msg_prefix='') 168 | 169 | assert_queryset_equal(qs, values, transform=repr) 170 | 171 | assert_num_queries(num, func=None, *args, **kwargs) 172 | 173 | assert_code(response, status_code, msg_prefix='') 174 | 175 | assert_ok(response, msg_prefix='') 176 | 177 | assert_mail_count(count, msg=None) 178 | 179 | 180 | Always Passing The Same Options 181 | ------------------------------- 182 | 183 | To always set the same command line options you can use a `nose.cfg or 184 | setup.cfg`_ (as usual) or you can specify them in settings.py like this:: 185 | 186 | NOSE_ARGS = ['--failed', '--stop'] 187 | 188 | .. _nose.cfg or setup.cfg: https://nose.readthedocs.io/en/latest/usage.html#configuration 189 | 190 | 191 | Custom Plugins 192 | -------------- 193 | 194 | If you need to `make custom plugins`_, you can define each plugin class 195 | somewhere within your app and load them from settings.py like this:: 196 | 197 | NOSE_PLUGINS = [ 198 | 'yourapp.tests.plugins.SystematicDysfunctioner', 199 | # ... 200 | ] 201 | 202 | Just like middleware or anything else, each string must be a dot-separated, 203 | importable path to an actual class. Each plugin class will be instantiated and 204 | added to the Nose test runner. 205 | 206 | .. _make custom plugins: https://nose.readthedocs.io/en/latest/plugins.html#writing-plugins 207 | 208 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """django-nose build configuration file. 3 | 4 | Created by sphinx-quickstart on Mon Jul 21 13:24:51 2014. 5 | 6 | This file is execfile()d with the current directory set to its 7 | containing dir. 8 | 9 | Note that not all possible configuration values are present in this 10 | autogenerated file. 11 | 12 | All configuration values have a default; values that are commented out 13 | serve to show the default. 14 | """ 15 | from datetime import date 16 | import sys 17 | import os 18 | 19 | cwd = os.getcwd() 20 | parent = os.path.dirname(cwd) 21 | sys.path.append(parent) 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 23 | 24 | from django_nose import __version__ # noqa 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = ".rst" 41 | 42 | # The encoding of source files. 43 | # source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "django-nose" 50 | copyright = "2010-%d, Jeff Balogh and the django-nose team." % date.today().year 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = __version__ 58 | # The full version, including alpha/beta/rc tags. 59 | release = __version__ 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | # today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | # today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ["_build"] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | # default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | # add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | # add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | # show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = "sphinx" 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | # modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | # keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = "alabaster" 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | # html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | # html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | # html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | # html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | # html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ["_static"] 134 | 135 | # Add any extra paths that contain custom files (such as robots.txt or 136 | # .htaccess) here, relative to this directory. These files are copied 137 | # directly to the root of the documentation. 138 | # html_extra_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | # html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | # html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | # html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | # html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | # html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | # html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | # html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | # html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | # html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | # html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | # html_use_opensearch = '' 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | # html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = "django-nose-doc" 183 | 184 | 185 | # -- Options for LaTeX output --------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ('letterpaper' or 'a4paper'). 189 | # 'papersize': 'letterpaper', 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | # 'pointsize': '10pt', 192 | # Additional stuff for the LaTeX preamble. 193 | # 'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, 198 | # author, documentclass [howto, manual, or own class]). 199 | latex_documents = [ 200 | ( 201 | "index", 202 | "django-nose.tex", 203 | "django-nose Documentation", 204 | "Jeff Balogh and the django-nose team", 205 | "manual", 206 | ), 207 | ] 208 | 209 | # The name of an image file (relative to this directory) to place at the top of 210 | # the title page. 211 | # latex_logo = None 212 | 213 | # For "manual" documents, if this is true, then toplevel headings are parts, 214 | # not chapters. 215 | # latex_use_parts = False 216 | 217 | # If true, show page references after internal links. 218 | # latex_show_pagerefs = False 219 | 220 | # If true, show URL addresses after external links. 221 | # latex_show_urls = False 222 | 223 | # Documents to append as an appendix to all manuals. 224 | # latex_appendices = [] 225 | 226 | # If false, no module index is generated. 227 | # latex_domain_indices = True 228 | 229 | 230 | # -- Options for manual page output --------------------------------------- 231 | 232 | # One entry per manual page. List of tuples 233 | # (source start file, name, description, authors, manual section). 234 | man_pages = [ 235 | ( 236 | "index", 237 | "django-nose", 238 | "django-nose Documentation", 239 | ["Jeff Balogh", "the django-nose team"], 240 | 1, 241 | ) 242 | ] 243 | 244 | # If true, show URL addresses after external links. 245 | # man_show_urls = False 246 | 247 | 248 | # -- Options for Texinfo output ------------------------------------------- 249 | 250 | # Grouping the document tree into Texinfo files. List of tuples 251 | # (source start file, target name, title, author, 252 | # dir menu entry, description, category) 253 | texinfo_documents = [ 254 | ( 255 | "index", 256 | "django-nose", 257 | "django-nose Documentation", 258 | "Jeff Balogh and the django-nose team", 259 | "django-nose", 260 | "Makes your Django tests simple and snappy", 261 | ) 262 | ] 263 | 264 | # Documents to append as an appendix to all manuals. 265 | # texinfo_appendices = [] 266 | 267 | # If false, no module index is generated. 268 | # texinfo_domain_indices = True 269 | 270 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 271 | # texinfo_show_urls = 'footnote' 272 | 273 | # If true, do not generate a @detailmenu in the "Top" node's menu. 274 | # texinfo_no_detailmenu = False 275 | -------------------------------------------------------------------------------- /django_nose/plugin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Included django-nose plugins.""" 3 | import sys 4 | 5 | from nose.plugins.base import Plugin 6 | from nose.suite import ContextSuite 7 | 8 | from django.test.testcases import TransactionTestCase, TestCase 9 | 10 | from django_nose.testcases import FastFixtureTestCase 11 | from django_nose.utils import process_tests, is_subclass_at_all 12 | 13 | 14 | class AlwaysOnPlugin(Plugin): 15 | """A base plugin that takes no options and is always enabled.""" 16 | 17 | def options(self, parser, env): 18 | """Avoid adding a ``--with`` option for this plugin. 19 | 20 | We don't have any options, and this plugin is always enabled, so we 21 | don't want to use superclass's ``options()`` method which would add a 22 | ``--with-*`` option. 23 | """ 24 | 25 | def configure(self, *args, **kw_args): 26 | """Configure and enable this plugin.""" 27 | super(AlwaysOnPlugin, self).configure(*args, **kw_args) 28 | self.enabled = True 29 | 30 | 31 | class ResultPlugin(AlwaysOnPlugin): 32 | """Captures the TestResult object for later inspection. 33 | 34 | nose doesn't return the full test result object from any of its runner 35 | methods. Pass an instance of this plugin to the TestProgram and use 36 | ``result`` after running the tests to get the TestResult object. 37 | """ 38 | 39 | name = "result" 40 | 41 | def finalize(self, result): 42 | """Finalize test run by capturing the result.""" 43 | self.result = result 44 | 45 | 46 | class DjangoSetUpPlugin(AlwaysOnPlugin): 47 | """Configures Django to set up and tear down the environment. 48 | 49 | This allows coverage to report on all code imported and used during the 50 | initialization of the test runner. 51 | """ 52 | 53 | name = "django setup" 54 | score = 150 55 | 56 | def __init__(self, runner): 57 | """Initialize the plugin with the test runner.""" 58 | super(DjangoSetUpPlugin, self).__init__() 59 | self.runner = runner 60 | self.sys_stdout = sys.stdout 61 | 62 | def prepareTest(self, test): 63 | """Create the Django DB and model tables, and do other setup. 64 | 65 | This isn't done in begin() because that's too early--the DB has to be 66 | set up *after* the tests are imported so the model registry contains 67 | models defined in tests.py modules. Models are registered at 68 | declaration time by their metaclass. 69 | 70 | prepareTestRunner() might also have been a sane choice, except that, if 71 | some plugin returns something from it, none of the other ones get 72 | called. I'd rather not dink with scores if I don't have to. 73 | 74 | """ 75 | # What is this stdout switcheroo for? 76 | sys_stdout = sys.stdout 77 | sys.stdout = self.sys_stdout 78 | 79 | self.runner.setup_test_environment() 80 | self.old_names = self.runner.setup_databases() 81 | 82 | sys.stdout = sys_stdout 83 | 84 | def finalize(self, result): 85 | """Finalize test run by cleaning up databases and environment.""" 86 | self.runner.teardown_databases(self.old_names) 87 | self.runner.teardown_test_environment() 88 | 89 | 90 | class Bucketer(object): 91 | """Collect tests into buckets with similar setup requirements.""" 92 | 93 | def __init__(self): 94 | """Initialize the test buckets.""" 95 | # { (frozenset(['users.json']), True): 96 | # [ContextSuite(...), ContextSuite(...)] } 97 | self.buckets = {} 98 | 99 | # All the non-FastFixtureTestCase tests we saw, in the order they came 100 | # in: 101 | self.remainder = [] 102 | 103 | def add(self, test): 104 | """Add test into an initialization bucket. 105 | 106 | Tests are bucketed according to its set of fixtures and the 107 | value of its exempt_from_fixture_bundling attr. 108 | """ 109 | if is_subclass_at_all(test.context, FastFixtureTestCase): 110 | # We bucket even FFTCs that don't have any fixtures, but it 111 | # shouldn't matter. 112 | key = ( 113 | frozenset(getattr(test.context, "fixtures", [])), 114 | getattr(test.context, "exempt_from_fixture_bundling", False), 115 | ) 116 | self.buckets.setdefault(key, []).append(test) 117 | else: 118 | self.remainder.append(test) 119 | 120 | 121 | class TestReorderer(AlwaysOnPlugin): 122 | """Reorder tests for various reasons.""" 123 | 124 | name = "django-nose-test-reorderer" 125 | 126 | def options(self, parser, env): 127 | """Add --with-fixture-bundling to options.""" 128 | super(TestReorderer, self).options(parser, env) # pointless 129 | parser.add_option( 130 | "--with-fixture-bundling", 131 | action="store_true", 132 | dest="with_fixture_bundling", 133 | default=env.get("NOSE_WITH_FIXTURE_BUNDLING", False), 134 | help="Load a unique set of fixtures only once, even " 135 | "across test classes. " 136 | "[NOSE_WITH_FIXTURE_BUNDLING]", 137 | ) 138 | 139 | def configure(self, options, conf): 140 | """Configure plugin, reading the with_fixture_bundling option.""" 141 | super(TestReorderer, self).configure(options, conf) 142 | self.should_bundle = options.with_fixture_bundling 143 | 144 | def _put_transaction_test_cases_last(self, test): 145 | """Reorder test suite so TransactionTestCase-based tests come last. 146 | 147 | Django has a weird design decision wherein TransactionTestCase doesn't 148 | clean up after itself. Instead, it resets the DB to a clean state only 149 | at the *beginning* of each test: 150 | https://docs.djangoproject.com/en/dev/topics/testing/?from=olddocs# 151 | django. test.TransactionTestCase. Thus, Django reorders tests so 152 | TransactionTestCases all come last. Here we do the same. 153 | 154 | "I think it's historical. We used to have doctests also, adding cleanup 155 | after each unit test wouldn't necessarily clean up after doctests, so 156 | you'd have to clean on entry to a test anyway." was once uttered on 157 | #django-dev. 158 | """ 159 | 160 | def filthiness(test): 161 | """Return a score of how messy a test leaves the environment. 162 | 163 | Django's TransactionTestCase doesn't clean up the DB on teardown, 164 | but it's hard to guess whether subclasses (other than TestCase) do. 165 | We will assume they don't, unless they have a 166 | ``cleans_up_after_itself`` attr set to True. This is reasonable 167 | because the odd behavior of TransactionTestCase is documented, so 168 | subclasses should by default be assumed to preserve it. 169 | 170 | Thus, things will get these comparands (and run in this order): 171 | 172 | * 1: TestCase subclasses. These clean up after themselves. 173 | * 1: TransactionTestCase subclasses with 174 | cleans_up_after_itself=True. These include 175 | FastFixtureTestCases. If you're using the 176 | FixtureBundlingPlugin, it will pull the FFTCs out, reorder 177 | them, and run them first of all. 178 | * 2: TransactionTestCase subclasses. These leave a mess. 179 | * 2: Anything else (including doctests, I hope). These don't care 180 | about the mess you left, because they don't hit the DB or, if 181 | they do, are responsible for ensuring that it's clean (as per 182 | https://docs.djangoproject.com/en/dev/topics/testing/?from= 183 | olddocs#writing-doctests) 184 | 185 | """ 186 | test_class = test.context 187 | if is_subclass_at_all(test_class, TestCase) or ( 188 | is_subclass_at_all(test_class, TransactionTestCase) 189 | and getattr(test_class, "cleans_up_after_itself", False) 190 | ): 191 | return 1 192 | return 2 193 | 194 | flattened = [] 195 | process_tests(test, flattened.append) 196 | flattened.sort(key=filthiness) 197 | return ContextSuite(flattened) 198 | 199 | def _bundle_fixtures(self, test): 200 | """Reorder tests to minimize fixture loading. 201 | 202 | I reorder FastFixtureTestCases so ones using identical sets 203 | of fixtures run adjacently. I then put attributes on them 204 | to advise them to not reload the fixtures for each class. 205 | 206 | This takes support.mozilla.com's suite from 123s down to 94s. 207 | 208 | FastFixtureTestCases are the only ones we care about, because 209 | nobody else, in practice, pays attention to the ``_fb`` advisory 210 | bits. We return those first, then any remaining tests in the 211 | order they were received. 212 | """ 213 | 214 | def suite_sorted_by_fixtures(suite): 215 | """Flatten and sort a tree of Suites by fixture. 216 | 217 | Add ``_fb_should_setup_fixtures`` and 218 | ``_fb_should_teardown_fixtures`` attrs to each test class to advise 219 | it whether to set up or tear down (respectively) the fixtures. 220 | 221 | Return a Suite. 222 | 223 | """ 224 | bucketer = Bucketer() 225 | process_tests(suite, bucketer.add) 226 | 227 | # Lay the bundles of common-fixture-having test classes end to end 228 | # in a single list so we can make a test suite out of them: 229 | flattened = [] 230 | for (key, fixture_bundle) in bucketer.buckets.items(): 231 | fixtures, is_exempt = key 232 | # Advise first and last test classes in each bundle to set up 233 | # and tear down fixtures and the rest not to: 234 | if fixtures and not is_exempt: 235 | # Ones with fixtures are sure to be classes, which means 236 | # they're sure to be ContextSuites with contexts. 237 | 238 | # First class with this set of fixtures sets up: 239 | first = fixture_bundle[0].context 240 | first._fb_should_setup_fixtures = True 241 | 242 | # Set all classes' 1..n should_setup to False: 243 | for cls in fixture_bundle[1:]: 244 | cls.context._fb_should_setup_fixtures = False 245 | 246 | # Last class tears down: 247 | last = fixture_bundle[-1].context 248 | last._fb_should_teardown_fixtures = True 249 | 250 | # Set all classes' 0..(n-1) should_teardown to False: 251 | for cls in fixture_bundle[:-1]: 252 | cls.context._fb_should_teardown_fixtures = False 253 | 254 | flattened.extend(fixture_bundle) 255 | flattened.extend(bucketer.remainder) 256 | 257 | return ContextSuite(flattened) 258 | 259 | return suite_sorted_by_fixtures(test) 260 | 261 | def prepareTest(self, test): 262 | """Reorder the tests.""" 263 | test = self._put_transaction_test_cases_last(test) 264 | if self.should_bundle: 265 | test = self._bundle_fixtures(test) 266 | return test 267 | -------------------------------------------------------------------------------- /django_nose/runner.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Django test runner that invokes nose. 3 | 4 | You can use... :: 5 | 6 | NOSE_ARGS = ['list', 'of', 'args'] 7 | 8 | in settings.py for arguments that you want always passed to nose. 9 | 10 | """ 11 | import os 12 | import sys 13 | from importlib import import_module 14 | from optparse import NO_DEFAULT 15 | from types import MethodType 16 | 17 | from django import setup 18 | from django.apps import apps 19 | from django.conf import settings 20 | from django.core import exceptions 21 | from django.core.management.color import no_style 22 | from django.core.management.commands.loaddata import Command 23 | from django.db import connections, transaction, DEFAULT_DB_ALIAS 24 | from django.test.runner import DiscoverRunner 25 | 26 | from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, TestReorderer 27 | from django_nose.utils import uses_mysql 28 | import nose.core 29 | 30 | __all__ = ("BasicNoseRunner", "NoseTestSuiteRunner") 31 | 32 | 33 | # This is a table of Django's "manage.py test" options which 34 | # correspond to nosetests options with a different name: 35 | OPTION_TRANSLATION = {"--failfast": "-x", "--nose-verbosity": "--verbosity"} 36 | 37 | 38 | def translate_option(opt): 39 | if "=" in opt: 40 | long_opt, value = opt.split("=", 1) 41 | return "%s=%s" % (translate_option(long_opt), value) 42 | return OPTION_TRANSLATION.get(opt, opt) 43 | 44 | 45 | def _get_plugins_from_settings(): 46 | settings_plugins = list(getattr(settings, "NOSE_PLUGINS", [])) 47 | for plug_path in settings_plugins + ["django_nose.plugin.TestReorderer"]: 48 | try: 49 | dot = plug_path.rindex(".") 50 | except ValueError: 51 | raise exceptions.ImproperlyConfigured( 52 | "%s isn't a Nose plugin module" % plug_path 53 | ) 54 | p_mod, p_classname = plug_path[:dot], plug_path[dot + 1 :] 55 | 56 | try: 57 | mod = import_module(p_mod) 58 | except ImportError as e: 59 | raise exceptions.ImproperlyConfigured( 60 | 'Error importing Nose plugin module %s: "%s"' % (p_mod, e) 61 | ) 62 | 63 | try: 64 | p_class = getattr(mod, p_classname) 65 | except AttributeError: 66 | raise exceptions.ImproperlyConfigured( 67 | 'Nose plugin module "%s" does not define a "%s"' % (p_mod, p_classname) 68 | ) 69 | 70 | yield p_class() 71 | 72 | 73 | class BaseRunner(DiscoverRunner): 74 | """Runner that translates nose optparse arguments to argparse. 75 | 76 | Django 1.8 and later uses argparse.ArgumentParser. Nose's optparse 77 | arguments need to be translated to this format, so that the Django 78 | command line parsing will pass. This parsing is (mostly) thrown out, 79 | and reassembled into command line arguments for nose to reparse. 80 | """ 81 | 82 | # Don't pass the following options to nosetests 83 | django_opts = [ 84 | "--noinput", 85 | "--liveserver", 86 | "-p", 87 | "--pattern", 88 | "--testrunner", 89 | "--settings", 90 | # 1.8 arguments 91 | "--keepdb", 92 | "--reverse", 93 | "--debug-sql", 94 | # 1.9 arguments 95 | "--parallel", 96 | # 1.10 arguments 97 | "--tag", 98 | "--exclude-tag", 99 | # 1.11 arguments 100 | "--debug-mode", 101 | ] 102 | 103 | # 104 | # For optparse -> argparse conversion 105 | # 106 | # Option strings to remove from Django options if found 107 | _argparse_remove_options = ( 108 | "-p", # Short arg for nose's --plugins, not Django's --patterns 109 | "-d", # Short arg for nose's --detailed-errors, not Django's 110 | # --debug-sql 111 | ) 112 | 113 | # Convert nose optparse options to argparse options 114 | _argparse_type = { 115 | "int": int, 116 | "float": float, 117 | "complex": complex, 118 | "string": str, 119 | "choice": str, 120 | } 121 | # If optparse has a None argument, omit from call to add_argument 122 | _argparse_omit_if_none = ( 123 | "action", 124 | "nargs", 125 | "const", 126 | "default", 127 | "type", 128 | "choices", 129 | "required", 130 | "help", 131 | "metavar", 132 | "dest", 133 | ) 134 | 135 | # Always ignore these optparse arguments 136 | # Django will parse without calling the callback 137 | # nose will then reparse with the callback 138 | _argparse_callback_options = ("callback", "callback_args", "callback_kwargs") 139 | 140 | # Keep track of nose options with nargs=1 141 | _has_nargs = set(["--verbosity"]) 142 | 143 | @classmethod 144 | def add_arguments(cls, parser): 145 | """Convert nose's optparse arguments to argparse.""" 146 | super(BaseRunner, cls).add_arguments(parser) 147 | 148 | # Read optparse options for nose and plugins 149 | cfg_files = nose.core.all_config_files() 150 | manager = nose.core.DefaultPluginManager() 151 | config = nose.core.Config(env=os.environ, files=cfg_files, plugins=manager) 152 | config.plugins.addPlugins(list(_get_plugins_from_settings())) 153 | options = config.getParser()._get_all_options() 154 | 155 | # Gather existing option strings` 156 | django_options = set() 157 | for action in parser._actions: 158 | for override in cls._argparse_remove_options: 159 | if override in action.option_strings: 160 | # Emulate parser.conflict_handler='resolve' 161 | parser._handle_conflict_resolve(None, ((override, action),)) 162 | django_options.update(action.option_strings) 163 | 164 | # Process nose optparse options 165 | for option in options: 166 | # Gather options 167 | opt_long = option.get_opt_string() 168 | if option._short_opts: 169 | opt_short = option._short_opts[0] 170 | else: 171 | opt_short = None 172 | 173 | # Rename nose's --verbosity to --nose-verbosity 174 | if opt_long == "--verbosity": 175 | opt_long = "--nose-verbosity" 176 | 177 | # Skip any options also in Django options 178 | if opt_long in django_options: 179 | continue 180 | if opt_short and opt_short in django_options: 181 | opt_short = None 182 | 183 | # Convert optparse attributes to argparse attributes 184 | option_attrs = {} 185 | for attr in option.ATTRS: 186 | # Ignore callback options 187 | if attr in cls._argparse_callback_options: 188 | continue 189 | 190 | value = getattr(option, attr) 191 | 192 | if attr == "default" and value == NO_DEFAULT: 193 | continue 194 | 195 | # Rename options for nose's --verbosity 196 | if opt_long == "--nose-verbosity": 197 | if attr == "dest": 198 | value = "nose_verbosity" 199 | elif attr == "metavar": 200 | value = "NOSE_VERBOSITY" 201 | 202 | # Omit arguments that are None, use default 203 | if attr in cls._argparse_omit_if_none and value is None: 204 | continue 205 | 206 | # Convert type from optparse string to argparse type 207 | if attr == "type": 208 | value = cls._argparse_type[value] 209 | 210 | # Convert action='callback' to action='store' 211 | if attr == "action" and value == "callback": 212 | action = "store" 213 | 214 | # Keep track of nargs=1 215 | if attr == "nargs": 216 | assert value == 1, ( 217 | "argparse option nargs=%s is not supported" % value 218 | ) 219 | cls._has_nargs.add(opt_long) 220 | if opt_short: 221 | cls._has_nargs.add(opt_short) 222 | 223 | # Pass converted attribute to optparse option 224 | option_attrs[attr] = value 225 | 226 | # Add the optparse argument 227 | if opt_short: 228 | parser.add_argument(opt_short, opt_long, **option_attrs) 229 | else: 230 | parser.add_argument(opt_long, **option_attrs) 231 | 232 | 233 | class BasicNoseRunner(BaseRunner): 234 | """Facade that implements a nose runner in the guise of a Django runner. 235 | 236 | You shouldn't have to use this directly unless the additions made by 237 | ``NoseTestSuiteRunner`` really bother you. They shouldn't, because they're 238 | all off by default. 239 | """ 240 | 241 | __test__ = False 242 | 243 | def run_suite(self, nose_argv): 244 | """Run the test suite.""" 245 | result_plugin = ResultPlugin() 246 | plugins_to_add = [DjangoSetUpPlugin(self), result_plugin, TestReorderer()] 247 | 248 | for plugin in _get_plugins_from_settings(): 249 | plugins_to_add.append(plugin) 250 | 251 | setup() 252 | 253 | nose.core.TestProgram(argv=nose_argv, exit=False, addplugins=plugins_to_add) 254 | return result_plugin.result 255 | 256 | def run_tests(self, test_labels, extra_tests=None): 257 | """ 258 | Run the unit tests for all the test names in the provided list. 259 | 260 | Test names specified may be file or module names, and may optionally 261 | indicate the test case to run by separating the module or file name 262 | from the test case name with a colon. Filenames may be relative or 263 | absolute. 264 | 265 | N.B.: The test_labels argument *MUST* be a sequence of 266 | strings, *NOT* just a string object. (Or you will be 267 | specifying tests for for each character in your string, and 268 | not the whole string. 269 | 270 | Examples: 271 | runner.run_tests( ('test.module',) ) 272 | runner.run_tests(['another.test:TestCase.test_method']) 273 | runner.run_tests(['a.test:TestCase']) 274 | runner.run_tests(['/path/to/test/file.py:test_function']) 275 | runner.run_tests( ('test.module', 'a.test:TestCase') ) 276 | 277 | Note: the extra_tests argument is currently ignored. You can 278 | run old non-nose code that uses it without totally breaking, 279 | but the extra tests will not be run. Maybe later. 280 | 281 | Returns the number of tests that failed. 282 | 283 | """ 284 | nose_argv = ["nosetests"] + list(test_labels) 285 | if hasattr(settings, "NOSE_ARGS"): 286 | nose_argv.extend(settings.NOSE_ARGS) 287 | 288 | # Recreate the arguments in a nose-compatible format 289 | arglist = sys.argv[1:] 290 | has_nargs = getattr(self, "_has_nargs", set(["--verbosity"])) 291 | while arglist: 292 | opt = arglist.pop(0) 293 | if not opt.startswith("-"): 294 | # Discard test labels 295 | continue 296 | if any(opt.startswith(d) for d in self.django_opts): 297 | # Discard options handled by Djangp 298 | continue 299 | 300 | trans_opt = translate_option(opt) 301 | nose_argv.append(trans_opt) 302 | 303 | if opt in has_nargs: 304 | # Handle arguments without an equals sign 305 | opt_value = arglist.pop(0) 306 | nose_argv.append(opt_value) 307 | 308 | # if --nose-verbosity was omitted, pass Django verbosity to nose 309 | if "--verbosity" not in nose_argv and not any( 310 | opt.startswith("--verbosity=") for opt in nose_argv 311 | ): 312 | nose_argv.append("--verbosity=%s" % str(self.verbosity)) 313 | 314 | if self.verbosity >= 1: 315 | print(" ".join(nose_argv)) 316 | 317 | result = self.run_suite(nose_argv) 318 | # suite_result expects the suite as the first argument. Fake it. 319 | return self.suite_result({}, result) 320 | 321 | 322 | _old_handle = Command.handle 323 | 324 | 325 | def _foreign_key_ignoring_handle(self, *fixture_labels, **options): 326 | """Wrap the the stock loaddata to ignore foreign key checks. 327 | 328 | This allows loading circular references from fixtures, and is 329 | monkeypatched into place in setup_databases(). 330 | """ 331 | using = options.get("database", DEFAULT_DB_ALIAS) 332 | connection = connections[using] 333 | 334 | # MySQL stinks at loading circular references: 335 | if uses_mysql(connection): 336 | cursor = connection.cursor() 337 | cursor.execute("SET foreign_key_checks = 0") 338 | 339 | _old_handle(self, *fixture_labels, **options) 340 | 341 | if uses_mysql(connection): 342 | cursor = connection.cursor() 343 | cursor.execute("SET foreign_key_checks = 1") 344 | 345 | 346 | def _skip_create_test_db( 347 | self, verbosity=1, autoclobber=False, serialize=True, keepdb=True 348 | ): 349 | """``create_test_db`` implementation that skips both creation and flushing. 350 | 351 | The idea is to re-use the perfectly good test DB already created by an 352 | earlier test run, cutting the time spent before any tests run from 5-13s 353 | (depending on your I/O luck) down to 3. 354 | """ 355 | # Notice that the DB supports transactions. Originally, this was done in 356 | # the method this overrides. The confirm method was added in Django v1.3 357 | # (https://code.djangoproject.com/ticket/12991) but removed in Django v1.5 358 | # (https://code.djangoproject.com/ticket/17760). In Django v1.5 359 | # supports_transactions is a cached property evaluated on access. 360 | if callable(getattr(self.connection.features, "confirm", None)): 361 | # Django v1.3-4 362 | self.connection.features.confirm() 363 | elif hasattr(self, "_rollback_works"): 364 | # Django v1.2 and lower 365 | can_rollback = self._rollback_works() 366 | self.connection.settings_dict["SUPPORTS_TRANSACTIONS"] = can_rollback 367 | 368 | return self._get_test_db_name() 369 | 370 | 371 | def _reusing_db(): 372 | """Return whether the ``REUSE_DB`` flag was passed.""" 373 | return os.getenv("REUSE_DB", "false").lower() in ("true", "1") 374 | 375 | 376 | def _can_support_reuse_db(connection): 377 | """Return True if REUSE_DB is a sensible option for the backend.""" 378 | # Perhaps this is a SQLite in-memory DB. Those are created implicitly when 379 | # you try to connect to them, so our usual test doesn't work. 380 | return not connection.creation._get_test_db_name() == ":memory:" 381 | 382 | 383 | def _should_create_database(connection): 384 | """Return whether we should recreate the given DB. 385 | 386 | This is true if the DB doesn't exist or the REUSE_DB env var isn't truthy. 387 | """ 388 | # TODO: Notice when the Model classes change and return True. Worst case, 389 | # we can generate sqlall and hash it, though it's a bit slow (2 secs) and 390 | # hits the DB for no good reason. Until we find a faster way, I'm inclined 391 | # to keep making people explicitly saying REUSE_DB if they want to reuse 392 | # the DB. 393 | 394 | if not _can_support_reuse_db(connection): 395 | return True 396 | 397 | # Notice whether the DB exists, and create it if it doesn't: 398 | try: 399 | # Connections are cached by some backends, if other code has connected 400 | # to the database previously under a different database name the 401 | # cached connection will be used and no exception will be raised. 402 | # Avoiding this by closing connections and setting to null 403 | for connection in connections.all(): 404 | connection.close() 405 | connection.connection = None 406 | connection.cursor() 407 | except Exception: # TODO: Be more discerning but still DB agnostic. 408 | return True 409 | return not _reusing_db() 410 | 411 | 412 | def _mysql_reset_sequences(style, connection): 413 | """Return a SQL statements needed to reset Django tables.""" 414 | tables = connection.introspection.django_table_names(only_existing=True) 415 | flush_statements = connection.ops.sql_flush( 416 | style, tables, connection.introspection.sequence_list() 417 | ) 418 | 419 | # connection.ops.sequence_reset_sql() is not implemented for MySQL, 420 | # and the base class just returns []. TODO: Implement it by pulling 421 | # the relevant bits out of sql_flush(). 422 | return [s for s in flush_statements if s.startswith("ALTER")] 423 | # Being overzealous and resetting the sequences on non-empty tables 424 | # like django_content_type seems to be fine in MySQL: adding a row 425 | # afterward does find the correct sequence number rather than 426 | # crashing into an existing row. 427 | 428 | 429 | class NoseTestSuiteRunner(BasicNoseRunner): 430 | """A runner that optionally skips DB creation. 431 | 432 | Monkeypatches connection.creation to let you skip creating databases if 433 | they already exist. Your tests will start up much faster. 434 | 435 | To opt into this behavior, set the environment variable ``REUSE_DB`` to 436 | "1" or "true" (case insensitive). 437 | """ 438 | 439 | def _get_models_for_connection(self, connection): 440 | """Return a list of models for a connection.""" 441 | tables = connection.introspection.get_table_list(connection.cursor()) 442 | return [m for m in apps.get_models() if m._meta.db_table in tables] 443 | 444 | def setup_databases(self): 445 | """Set up databases. Skip DB creation if requested and possible.""" 446 | for alias in connections: 447 | connection = connections[alias] 448 | creation = connection.creation 449 | test_db_name = creation._get_test_db_name() 450 | 451 | # Mess with the DB name so other things operate on a test DB 452 | # rather than the real one. This is done in create_test_db when 453 | # we don't monkeypatch it away with _skip_create_test_db. 454 | orig_db_name = connection.settings_dict["NAME"] 455 | connection.settings_dict["NAME"] = test_db_name 456 | 457 | if _should_create_database(connection): 458 | # We're not using _skip_create_test_db, so put the DB name 459 | # back: 460 | connection.settings_dict["NAME"] = orig_db_name 461 | 462 | # Since we replaced the connection with the test DB, closing 463 | # the connection will avoid pooling issues with SQLAlchemy. The 464 | # issue is trying to CREATE/DROP the test database using a 465 | # connection to a DB that was established with that test DB. 466 | # MySQLdb doesn't allow it, and SQLAlchemy attempts to reuse 467 | # the existing connection from its pool. 468 | connection.close() 469 | else: 470 | # Reset auto-increment sequences. Apparently, SUMO's tests are 471 | # horrid and coupled to certain numbers. 472 | cursor = connection.cursor() 473 | style = no_style() 474 | 475 | if uses_mysql(connection): 476 | reset_statements = _mysql_reset_sequences(style, connection) 477 | else: 478 | reset_statements = connection.ops.sequence_reset_sql( 479 | style, self._get_models_for_connection(connection) 480 | ) 481 | 482 | if hasattr(transaction, "atomic"): 483 | with transaction.atomic(using=connection.alias): 484 | for reset_statement in reset_statements: 485 | cursor.execute(reset_statement) 486 | else: 487 | # Django < 1.6 488 | for reset_statement in reset_statements: 489 | cursor.execute(reset_statement) 490 | transaction.commit_unless_managed(using=connection.alias) 491 | 492 | # Each connection has its own creation object, so this affects 493 | # only a single connection: 494 | creation.create_test_db = MethodType(_skip_create_test_db, creation) 495 | 496 | Command.handle = _foreign_key_ignoring_handle 497 | 498 | # With our class patch, does nothing but return some connection 499 | # objects: 500 | return super(NoseTestSuiteRunner, self).setup_databases() 501 | 502 | def teardown_databases(self, *args, **kwargs): 503 | """Leave those poor, reusable databases alone if REUSE_DB is true.""" 504 | if not _reusing_db(): 505 | return super(NoseTestSuiteRunner, self).teardown_databases(*args, **kwargs) 506 | # else skip tearing down the DB so we can reuse it next time 507 | --------------------------------------------------------------------------------