diff --git a/.gitignore b/.gitignore index 25aacff..1689fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ build/ dist/ *.egg-info/ +wheelhouse/ +.eggs/ +*.egg/ +*.pyc +*.sw? +.coverage +test-support/django/db.sqlite3 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7cc3d8d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "pypy" +env: + - EGGS="nose==dev" + - EGGS="" +# command to install dependencies, e.g. pip install -r requirements.txt +install: + - pip='travis_retry pip' + - if [[ $TRAVIS_PYTHON_VERSION =~ '2.6' ]] ; then $pip install 'Django<1.7' ; fi + - if [[ -n "$EGGS" ]] ; then $pip install $EGGS --allow-external=nose --allow-unverified=nose ; fi + - python setup.py develop +# command to run tests, e.g. python setup.py test +script: + - python setup.py nosetests --verbosity=3 diff --git a/nosepipe.py b/nosepipe.py index 68cda17..e46cc2c 100644 --- a/nosepipe.py +++ b/nosepipe.py @@ -147,12 +147,16 @@ def __call__(self, result): useshell = True self.logger.debug("Executing %s", " ".join(argv)) - popen = subprocess.Popen(argv, - cwd=self._cwd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - shell=useshell, - ) + try: + popen = subprocess.Popen(argv, + cwd=self._cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=useshell, + ) + except OSError as e: + raise Exception("Error running %s [%s]" % (argv[0], e)) + try: stdout = popen.stdout while True: @@ -191,18 +195,49 @@ def __init__(self): nose.plugins.Plugin.__init__(self) self._test = None self._test_proxy = None - self._argv = [os.path.abspath(sys.argv[0]), - '--with-process-isolation-reporter'] - self._argv += ProcessIsolationPlugin._get_nose_whitelisted_argv() + + # Normally, nose is called as: + # nosetests {opt1} {opt2} ... + # However, we can also be run as: + # setup.py nosetests {opt1} {opt2} ... + # When not running directly as nosetests, we need to run the + # sub-processes as `nosetests`, not `setup.py nosetests` as the output + # of setup.py interferes with the nose output. So, we need to find + # where nosetests is on the command-line and then run the + # sub-processes command-line using the args from that location. + + nosetests_index = None + # Find where nosetests is in argv and start the new argv from there + # in case we're running as something like `setup.py nosetests` + for i in range(0, len(sys.argv)): + if 'nosetests' in sys.argv[i]: + self._argv = [sys.argv[i]] + nosetests_index = i + break + + # If we can't find nosetests in the command-line we must be running + # from some other test runner like django's `manage.py test`. Replace + # the runner with `nosttests` and proceed... + if nosetests_index is None: + nosetests_index = 0 + self._argv = ['nosetests'] + + self._argv += ['--with-process-isolation-reporter'] + # add the rest of the args that appear in argv after `nosetests` + self._argv += ProcessIsolationPlugin._get_nose_whitelisted_argv( + offset=nosetests_index + 1) # Getting cwd inside SubprocessTestProxy.__call__ is too late - it is # already changed by nose self._cwd = os.getcwd() @staticmethod - def _get_nose_whitelisted_argv(): + def _get_nose_whitelisted_argv(offset=1): # This is the list of nose options which should be passed through to # the launched process; boolean value defines whether the option # takes a value or not. + # + # offset: int, the argv index of the first nosetests option. + # whitelist = { '--debug-log': True, '--logging-config': True, @@ -228,7 +263,7 @@ def _get_nose_whitelisted_argv(): '--doctest-options': True, '--no-skip': False, } - filtered = set(whitelist.keys()).intersection(set(sys.argv[1:])) + filtered = set(whitelist.keys()).intersection(set(sys.argv[offset:])) result = [] for key in filtered: result.append(key) @@ -237,7 +272,7 @@ def _get_nose_whitelisted_argv(): # We are not finished yet: options with '=' were not handled whitelist_keyval = [(k + "=") for k, v in whitelist.items() if v] - for arg in sys.argv[1:]: + for arg in sys.argv[offset:]: for keyval in whitelist_keyval: if arg.startswith(keyval): result.append(arg) @@ -255,18 +290,20 @@ def options(self, parser, env): def configure(self, options, config): self.individual = options.with_process_isolation_individual nose.plugins.Plugin.configure(self, options, config) - if self.enabled and options.enable_plugin_coverage: - from coverage import coverage - - def nothing(*args, **kwargs): - pass - - # Monkey patch coverage to fix the reporting and friends - coverage.start = nothing - coverage.stop = nothing - coverage.combine = nothing - coverage.save = coverage.load - + try: + if self.enabled and options.enable_plugin_coverage: + from coverage import coverage + + def nothing(*args, **kwargs): + pass + + # Monkey patch coverage to fix the reporting and friends + coverage.start = nothing + coverage.stop = nothing + coverage.combine = nothing + coverage.save = coverage.load + except: + pass def do_isolate(self, test): # XXX is there better way to access 'nosepipe_isolate'? diff --git a/setup.py b/setup.py index 3172728..ee4ff15 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,9 @@ license = "BSD", platforms = ["any"], - install_requires = ["nose>=0.1.0, ==dev"], + install_requires = ["nose>=0.1.0"], + + tests_require = ["django-nose"], url = "http://github.com/dmccombs/nosepipe/", diff --git a/test-support/django/manage.py b/test-support/django/manage.py new file mode 100644 index 0000000..e30e858 --- /dev/null +++ b/test-support/django/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/test-support/django/sample/__init__.py b/test-support/django/sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test-support/django/sample/settings.py b/test-support/django/sample/settings.py new file mode 100644 index 0000000..b3e7558 --- /dev/null +++ b/test-support/django/sample/settings.py @@ -0,0 +1,52 @@ +""" +Django settings for nosepipe project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'sa5ia(bu*m1j@hkiap#1yx3#vp0-(_ige!da_zk6=3*knhp1p7' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + +INSTALLED_APPS = ( + 'django_nose', +) + +MIDDLEWARE_CLASSES = () + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +# Use django_nose to run tests + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +# Enable nosepipe + +NOSE_ARGS = ['--verbose', '--with-process-isolation'] diff --git a/test-support/django/sample/tests.py b/test-support/django/sample/tests.py new file mode 100644 index 0000000..60041b0 --- /dev/null +++ b/test-support/django/sample/tests.py @@ -0,0 +1,6 @@ +from django.test import TestCase + + +class BaseTestCase(TestCase): + def test_simple(self): + self.assertTrue(1) diff --git a/test_nosepipe.rst b/test_nosepipe.rst index 0239044..17b726e 100644 --- a/test_nosepipe.rst +++ b/test_nosepipe.rst @@ -89,7 +89,7 @@ Multiple failing tests: ... argv=["nosetests", "-v", "--with-process-isolation", ... os.path.join(directory_with_tests, "failing")], ... plugins=plugins) - ... # doctest: +REPORT_NDIFF + ... # doctest: +REPORT_NDIFF +ELLIPSIS failing_tests.erroring_test ... ERROR failing_tests.failing_test ... FAIL @@ -110,4 +110,58 @@ Multiple failing tests: ---------------------------------------------------------------------- Ran 2 tests in ...s - FAILED (failures=1, errors=1) + FAILED (errors=1, failures=1) + +Django nose: + + >>> import subprocess + >>> import re + + >>> # run a command and return it's output or error output + >>> def run_cmd(argv): + ... try: # python 2.7+ + ... output = subprocess.check_output(argv, stderr=subprocess.STDOUT).decode('ascii') + ... except subprocess.CalledProcessError as e: + ... output = "Error running:\n{0}\nOutput:\n{1}".format(argv, e.output) + ... except Exception as subprocess_e: + ... try: + ... useshell = False + ... if sys.platform == 'win32': + ... useshell = True + ... popen = subprocess.Popen(argv, + ... shell=useshell, + ... stdout=subprocess.PIPE, + ... stderr=subprocess.PIPE, + ... ) + ... stdout, stderr = popen.communicate() + ... output = "{0}{1}".format(stderr, stdout) + ... except OSError as popen_e: + ... output = "Error running:\n{0}\nSubprocess Output:\n{1}\nPopen Output".format( + ... argv, subprocess_e, popen_e) + ... return output + + >>> # find all .egg directories to add to the path (were installed by setup.py develop/test) + >>> top_dir = os.getcwd() + >>> eggs = [] + >>> iseggdir = re.compile('\.egg$') + >>> for top, dirs, f in os.walk(top_dir): + ... for dir in dirs: + ... if iseggdir.search(dir): + ... eggs += [os.path.join(top, dir)] + + >>> django_dir = os.path.join(directory_with_tests, "django") + >>> os.chdir(django_dir) + >>> print(run_cmd(["env", + ... "PYTHONPATH={0}".format(":".join(eggs)), + ... "python", "manage.py", "test", "--verbosity=1"])) + ... # doctest: +REPORT_NDIFF +ELLIPSIS + . + ---------------------------------------------------------------------- + Ran 1 test in ...s + + OK + nosetests --verbose --with-process-isolation --verbosity=1 + Creating test database for alias 'default'... + Destroying test database for alias 'default'... + + >>> os.chdir(top_dir)