diff options
34 files changed, 537 insertions, 236 deletions
@@ -6,3 +6,4 @@ Scot Doyle James Casbon Antoine Pitrou John J Lee +Allen Bierbaum @@ -1,3 +1,42 @@ +0.10.1 + +- Fixed bug in capture plugin that caused it to record captured output + on the test in the wrong attribute (#113). +- Fixed bug in result proxy that caused tests to fail if they accessed + certain result attibutes directly (#114). Thanks to Neilen Marais + for the bug report. +- Fixed bug in capture plugin that caused other error formatters + changes to be lost if no output was captured (#124). Thanks to + someone at ilorentz.org for the bug report. +- Fixed several bugs in the nosetests setup command that made some + options unusable and the command itself unusable when no options + were set (#125, #126, #128). Thanks to Alain Poirier for the bug + reports. +- Fixed bug in handling of string errors (#130). Thanks to schl... at + uni-oldenburg.de for the bug report. +- Fixed bug in coverage plugin option handling that prevented + --cover-package=mod1,mod2 from working (#117). Thanks to Allen + Bierbaum for the patch. +- Fixed bug in profiler plugin that prevented output from being + produced when output capture was enabled on python 2.5 + (#129). Thanks to James Casbon for the patch. +- Fixed bug in adapting 0.9 plugins to 0.10 (#119 part one). Thanks to + John J Lee for the bug report and tests. +- Fixed bug in handling of argv in config and plugin test utilities + (#119 part two). Thanks to John J Lee for the bug report and patch. +- Fixed bug where Failure cases due to invalid test name + specifications were passed to plugins makeTest (#120). Thanks to + John J Lee for the bug report and patch. +- Fixed bugs in doc css that mangled display in small windows. Thanks + to Ben Hoyt for the bug report and Michal Kwiatkowski for the fix. +- Made it possible to pass a list or comma-separated string as + defaultTest to main(). Thanks to Allen Bierbaum for the suggestion + and patch. +- Fixed a bug in nose.selector and nose.util.getpackage that caused + directories with names that are not legal python identifiers to be + collected as packages (#143). Thanks to John J Lee for the bug + report. + 0.10.0 - Fixed bug that broke plugins with names containing underscores or diff --git a/functional_tests/doc_tests/test_issue097/plugintest_environment.rst b/functional_tests/doc_tests/test_issue097/plugintest_environment.rst index da7982c..1b3f914 100644 --- a/functional_tests/doc_tests/test_issue097/plugintest_environment.rst +++ b/functional_tests/doc_tests/test_issue097/plugintest_environment.rst @@ -1,15 +1,16 @@ -nose.plugins.plugintest and os.environ --------------------------------------- +nose.plugins.plugintest, os.environ and sys.argv +------------------------------------------------ `nose.plugins.plugintest.PluginTester`_ and `nose.plugins.plugintest.run()`_ are utilities for testing nose plugins. When testing plugins, it should be possible to control the environment seen plugins under test, and that environment should never -be affected by ``os.environ``. +be affected by ``os.environ`` or ``sys.argv``. >>> import os + >>> import sys >>> import unittest - >>> from nose.config import Config + >>> import nose.config >>> from nose.plugins import Plugin >>> from nose.plugins.builtin import FailureDetail, Capture >>> from nose.plugins.plugintest import PluginTester @@ -28,7 +29,21 @@ environment it's given by nose. ... self.conf = conf ... ... def options(self, parser, env={}): - ... print env + ... print "env:", env + +To test the argv, we use a config class that prints the argv it's +given by nose. We need to monkeypatch nose.config.Config, so that we +can test the cases where that is used as the default. + + >>> old_config = nose.config.Config + + >>> class PrintArgvConfig(old_config): + ... + ... def configure(self, argv=None, doc=None): + ... print "argv:", argv + ... old_config.configure(self, argv, doc) + + >>> nose.config.Config = PrintArgvConfig The class under test, PluginTester, is designed to be used by subclassing. @@ -44,16 +59,21 @@ subclassing. ... return unittest.TestSuite(tests=[]) -For the purposes of this test, we need a known ``os.environ``. +For the purposes of this test, we need a known ``os.environ`` and +``sys.argv``. >>> old_environ = os.environ + >>> old_argv = sys.argv >>> os.environ = {"spam": "eggs"} + >>> sys.argv = ["spamtests"] +PluginTester always uses the [nosetests, self.activate] as its argv. If ``env`` is not overridden, the default is an empty ``env``. >>> tester = Tester() >>> tester.setUp() - {} + argv: ['nosetests', '-v'] + env: {} An empty ``env`` is respected... @@ -62,7 +82,8 @@ An empty ``env`` is respected... >>> tester = EmptyEnvTester() >>> tester.setUp() - {} + argv: ['nosetests', '-v'] + env: {} ... as is a non-empty ``env``. @@ -71,7 +92,8 @@ An empty ``env`` is respected... >>> tester = NonEmptyEnvTester() >>> tester.setUp() - {'foo': 'bar'} + argv: ['nosetests', '-v'] + env: {'foo': 'bar'} ``nose.plugins.plugintest.run()`` should work analogously. @@ -80,7 +102,8 @@ An empty ``env`` is respected... >>> run(suite=unittest.TestSuite(tests=[]), ... plugins=[PrintEnvPlugin()]) # doctest: +REPORT_NDIFF - {} + argv: ['nosetests', '-v'] + env: {} ---------------------------------------------------------------------- Ran 0 tests in ...s <BLANKLINE> @@ -89,7 +112,8 @@ An empty ``env`` is respected... >>> run(env={}, ... suite=unittest.TestSuite(tests=[]), ... plugins=[PrintEnvPlugin()]) # doctest: +REPORT_NDIFF - {} + argv: ['nosetests', '-v'] + env: {} ---------------------------------------------------------------------- Ran 0 tests in ...s <BLANKLINE> @@ -98,7 +122,35 @@ An empty ``env`` is respected... >>> run(env={"foo": "bar"}, ... suite=unittest.TestSuite(tests=[]), ... plugins=[PrintEnvPlugin()]) # doctest: +REPORT_NDIFF - {'foo': 'bar'} + argv: ['nosetests', '-v'] + env: {'foo': 'bar'} + ---------------------------------------------------------------------- + Ran 0 tests in ...s + <BLANKLINE> + OK + +An explicit argv parameter is honoured: + + >>> run(argv=["spam"], + ... suite=unittest.TestSuite(tests=[]), + ... plugins=[PrintEnvPlugin()]) # doctest: +REPORT_NDIFF + argv: ['spam'] + env: {} + ---------------------------------------------------------------------- + Ran 0 tests in ...s + <BLANKLINE> + OK + +An explicit config parameter with an env is honoured: + + >>> from nose.plugins.manager import PluginManager + + >>> manager = PluginManager(plugins=[PrintEnvPlugin()]) + >>> config = PrintArgvConfig(env={"foo": "bar"}, plugins=manager) + >>> run(config=config, + ... suite=unittest.TestSuite(tests=[])) # doctest: +REPORT_NDIFF + argv: ['nosetests', '-v'] + env: {'foo': 'bar'} ---------------------------------------------------------------------- Ran 0 tests in ...s <BLANKLINE> @@ -108,3 +160,5 @@ An empty ``env`` is respected... Clean up. >>> os.environ = old_environ + >>> sys.argv = old_argv + >>> nose.config.Config = old_config diff --git a/functional_tests/doc_tests/test_issue119/empty_plugin.rst b/functional_tests/doc_tests/test_issue119/empty_plugin.rst new file mode 100644 index 0000000..644af32 --- /dev/null +++ b/functional_tests/doc_tests/test_issue119/empty_plugin.rst @@ -0,0 +1,61 @@ +Minimal plugin +-------------- + +Plugins work as long as they implement the minimal interface required +by nose.plugins.base . They do not have to derive from +nose.plugins.Plugin . + + >>> class NullPlugin(object): + ... + ... enabled = True + ... name = "null" + ... score = 100 + ... + ... def options(self, parser, env): + ... pass + ... + ... def configure(self, options, conf): + ... pass + + >>> import unittest + >>> from nose.plugins.plugintest import run + + >>> run(suite=unittest.TestSuite(tests=[]), + ... plugins=[NullPlugin()]) # doctest: +REPORT_NDIFF + ---------------------------------------------------------------------- + Ran 0 tests in ...s + <BLANKLINE> + OK + +Plugins can derive from nose.plugins.base and do nothing except set a +name. + + >>> import os + >>> from nose.plugins import Plugin + + >>> class DerivedNullPlugin(Plugin): + ... + ... name = "derived-null" + +Enabled plugin that's otherwise empty + + >>> class EnabledDerivedNullPlugin(Plugin): + ... + ... enabled = True + ... name = "enabled-derived-null" + ... + ... def options(self, parser, env=os.environ): + ... pass + ... + ... def configure(self, options, conf): + ... if not self.can_configure: + ... return + ... self.conf = conf + + >>> run(suite=unittest.TestSuite(tests=[]), + ... plugins=[DerivedNullPlugin(), EnabledDerivedNullPlugin()]) + ... # doctest: +REPORT_NDIFF + ---------------------------------------------------------------------- + Ran 0 tests in ...s + <BLANKLINE> + OK diff --git a/functional_tests/doc_tests/test_issue119/test_zeronine.py b/functional_tests/doc_tests/test_issue119/test_zeronine.py new file mode 100644 index 0000000..6a4f450 --- /dev/null +++ b/functional_tests/doc_tests/test_issue119/test_zeronine.py @@ -0,0 +1,26 @@ +import os +import unittest +from nose.plugins import Plugin +from nose.plugins.plugintest import PluginTester +from nose.plugins.manager import ZeroNinePlugin + +here = os.path.abspath(os.path.dirname(__file__)) + +support = os.path.join(os.path.dirname(os.path.dirname(here)), 'support') + + +class EmptyPlugin(Plugin): + pass + +class TestEmptyPlugin(PluginTester, unittest.TestCase): + activate = '--with-empty' + plugins = [ZeroNinePlugin(EmptyPlugin())] + suitepath = os.path.join(here, 'empty_plugin.rst') + + def test_empty_zero_nine_does_not_crash(self): + print self.output + assert "'EmptyPlugin' object has no attribute 'loadTestsFromPath'" \ + not in self.output + + + diff --git a/functional_tests/support/fdp/test_fdp_no_capt.py b/functional_tests/support/fdp/test_fdp_no_capt.py new file mode 100644 index 0000000..30b7b99 --- /dev/null +++ b/functional_tests/support/fdp/test_fdp_no_capt.py @@ -0,0 +1,9 @@ +def test_err(): + raise TypeError("I can't type") + +def test_fail(): + a = 2 + assert a == 4, "a is not 4" + +def test_ok(): + pass diff --git a/functional_tests/support/issue130/test.py b/functional_tests/support/issue130/test.py new file mode 100644 index 0000000..9778eef --- /dev/null +++ b/functional_tests/support/issue130/test.py @@ -0,0 +1,5 @@ +def setup(): + raise "KABOOM" + +def test_foo(): + assert(1==1) diff --git a/functional_tests/support/issue143/not-a-package/__init__.py b/functional_tests/support/issue143/not-a-package/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/functional_tests/support/issue143/not-a-package/__init__.py @@ -0,0 +1 @@ +pass diff --git a/functional_tests/support/issue143/not-a-package/test.py b/functional_tests/support/issue143/not-a-package/test.py new file mode 100644 index 0000000..c1fb1c2 --- /dev/null +++ b/functional_tests/support/issue143/not-a-package/test.py @@ -0,0 +1,2 @@ +def test(): + raise Exception("do not run") diff --git a/functional_tests/test_failuredetail_plugin.py b/functional_tests/test_failuredetail_plugin.py index b97e432..284cf49 100644 --- a/functional_tests/test_failuredetail_plugin.py +++ b/functional_tests/test_failuredetail_plugin.py @@ -2,6 +2,7 @@ import os import sys import unittest from nose.plugins.failuredetail import FailureDetail +from nose.plugins.capture import Capture from nose.plugins import PluginTester support = os.path.join(os.path.dirname(__file__), 'support') @@ -25,5 +26,25 @@ class TestFailureDetail(PluginTester, unittest.TestCase): assert expect in self.output + +class TestFailureDetailWithCapture(PluginTester, unittest.TestCase): + activate = "-d" + args = ['-v'] + plugins = [FailureDetail(), Capture()] + suitepath = os.path.join(support, 'fdp/test_fdp_no_capt.py') + + def runTest(self): + print '*' * 70 + print str(self.output) + print '*' * 70 + + expect = \ + 'AssertionError: a is not 4\n' + ' print "Hello"\n' + ' 2 = 2\n' + '>> assert 2 == 4, "a is not 4"' + + assert expect in self.output + if __name__ == '__main__': unittest.main() diff --git a/functional_tests/test_issue120/support/some_test.py b/functional_tests/test_issue120/support/some_test.py new file mode 100644 index 0000000..9947266 --- /dev/null +++ b/functional_tests/test_issue120/support/some_test.py @@ -0,0 +1,3 @@ +def some_test(): + pass + diff --git a/functional_tests/test_issue120/test_named_test_with_doctest.rst b/functional_tests/test_issue120/test_named_test_with_doctest.rst new file mode 100644 index 0000000..d6980c2 --- /dev/null +++ b/functional_tests/test_issue120/test_named_test_with_doctest.rst @@ -0,0 +1,25 @@ +Naming a non-existent test using the colon syntax (foo.py:my_test) +with plugin doctests enabled used to cause a failure with a ValueError +from module doctest, losing the original failure (failure to find the +test). + + >>> import os + >>> from nose.plugins.plugintest import run + >>> from nose.plugins.doctests import Doctest + + >>> support = os.path.join(os.path.dirname(__file__), 'support') + >>> test_name = os.path.join(support, 'some_test.py') + ':nonexistent' + >>> run(argv=['nosetests', '--with-doctest', test_name], + ... plugins=[Doctest()]) + E + ====================================================================== + ERROR: Failure: ValueError (No such test nonexistent) + ---------------------------------------------------------------------- + Traceback (most recent call last): + ... + ValueError: No such test nonexistent + <BLANKLINE> + ---------------------------------------------------------------------- + Ran 1 test in ...s + <BLANKLINE> + FAILED (errors=1) diff --git a/functional_tests/test_program.py b/functional_tests/test_program.py index b0db11e..afa72fe 100644 --- a/functional_tests/test_program.py +++ b/functional_tests/test_program.py @@ -3,6 +3,7 @@ import unittest from cStringIO import StringIO from nose.core import TestProgram from nose.config import Config +from nose.plugins.manager import DefaultPluginManager here = os.path.dirname(__file__) support = os.path.join(here, 'support') @@ -118,6 +119,63 @@ class TestTestProgram(unittest.TestCase): assert not res.wasSuccessful() assert len(res.errors) == 1 assert len(res.failures) == 2 + + def test_issue_130(self): + """Collect and run tests in support/issue130 without error. + + This tests that the result and error classes can handle string + exceptions. + """ + import warnings + warnings.filterwarnings('ignore', category=DeprecationWarning, + module='test') + + stream = StringIO() + runner = TestRunner(stream=stream, verbosity=2) + + prog = TestProgram(defaultTest=os.path.join(support, 'issue130'), + argv=['test_issue_130'], + testRunner=runner, + config=Config(stream=stream, + plugins=DefaultPluginManager()), + exit=False) + res = runner.result + print stream.getvalue() + self.assertEqual(res.testsRun, 0) # error is in setup + assert not res.wasSuccessful() + assert res.errors + assert not res.failures + + def test_defaultTest_list(self): + stream = StringIO() + runner = TestRunner(stream=stream, verbosity=2) + tests = [os.path.join(support, 'package2'), + os.path.join(support, 'package3')] + prog = TestProgram(defaultTest=tests, + argv=['test_run_support_package2_3', '-v'], + testRunner=runner, + config=Config(), + exit=False) + res = runner.result + print stream.getvalue() + self.assertEqual(res.testsRun, 7) + + def test_illegal_packages_not_selected(self): + stream = StringIO() + runner = TestRunner(stream=stream, verbosity=2) + + prog = TestProgram(defaultTest=os.path.join(support, 'issue143'), + argv=['test_issue_143'], + testRunner=runner, + config=Config(stream=stream, + plugins=DefaultPluginManager()), + exit=False) + res = runner.result + print stream.getvalue() + self.assertEqual(res.testsRun, 0) + assert res.wasSuccessful() + assert not res.errors + assert not res.failures if __name__ == '__main__': diff --git a/nose/__init__.py b/nose/__init__.py index 7ec2f06..7c8fa53 100644 --- a/nose/__init__.py +++ b/nose/__init__.py @@ -397,7 +397,7 @@ from nose.exc import SkipTest, DeprecatedTest from nose.tools import with_setup __author__ = 'Jason Pellerin' -__versioninfo__ = (0, 11, 0) +__versioninfo__ = (0, 10, 1) __version__ = '.'.join(map(str, __versioninfo__)) __all__ = [ diff --git a/nose/case.py b/nose/case.py index 30b78fe..739b608 100644 --- a/nose/case.py +++ b/nose/case.py @@ -26,7 +26,8 @@ class Failure(unittest.TestCase): unittest.TestCase.__init__(self) def __str__(self): - return "Failure: %s(%s)" % (self.exc_class, self.exc_val) + return "Failure: %s (%s)" % ( + getattr(self.exc_class, '__name__', self.exc_class), self.exc_val) def runTest(self): if self.tb is not None: diff --git a/nose/commands.py b/nose/commands.py index 80913e3..30743b4 100644 --- a/nose/commands.py +++ b/nose/commands.py @@ -59,7 +59,7 @@ else: if opt._long_opts[0][2:] in option_blacklist: continue long_name = opt._long_opts[0][2:] - if opt.action != 'store_true': + if opt.action not in ('store_true', 'store_false'): long_name = long_name + "=" short_name = None if opt._short_opts: @@ -76,7 +76,10 @@ else: user_options = get_user_options(__parser) def initialize_options(self): - """create the member variables, but change hyphens to underscores""" + """create the member variables, but change hyphens to + underscores + """ + self.option_to_cmds = {} for opt in self.__parser.option_list: cmd_name = opt._long_opts[0][2:] @@ -102,7 +105,7 @@ else: self.distribution.fetch_build_eggs( self.distribution.tests_require) - argv = [] + argv = ['nosetests'] for (option_name, cmd_name) in self.option_to_cmds.items(): if option_name in option_blacklist: continue diff --git a/nose/config.py b/nose/config.py index 203f4d6..5bc3798 100644 --- a/nose/config.py +++ b/nose/config.py @@ -457,10 +457,15 @@ def all_config_files(): # used when parsing config files def flag(val): """Does the value look like an on/off flag?""" + if val == 1: + return True + elif val == 0: + return False + val = str(val) if len(val) > 5: return False return val.upper() in ('1', '0', 'F', 'T', 'TRUE', 'FALSE', 'ON', 'OFF') def _bool(val): - return val.upper() in ('1', 'T', 'TRUE', 'ON') + return str(val).upper() in ('1', 'T', 'TRUE', 'ON') diff --git a/nose/core.py b/nose/core.py index 76581ac..2ebdea2 100644 --- a/nose/core.py +++ b/nose/core.py @@ -262,7 +262,8 @@ class TestProgram(unittest.TestProgram): if self.config.testNames: self.testNames = self.config.testNames else: - self.testNames = (self.defaultTest,) + self.testNames = tolist(self.defaultTest) + log.debug('defaultTest %s', self.defaultTest) log.debug('Test names are %s', self.testNames) if self.config.workingDir is not None: os.chdir(self.config.workingDir) diff --git a/nose/loader.py b/nose/loader.py index df60a91..c605a1f 100644 --- a/nose/loader.py +++ b/nose/loader.py @@ -341,8 +341,11 @@ class TestLoader(unittest.TestLoader): if addr.call: name = addr.call parent, obj = self.resolve(name, module) - return suite(ContextList([self.makeTest(obj, parent)], - context=parent)) + if isinstance(obj, Failure): + return suite([obj]) + else: + return suite(ContextList([self.makeTest(obj, parent)], + context=parent)) else: if addr.module: try: diff --git a/nose/plugins/capture.py b/nose/plugins/capture.py index 9bda3ec..cf2a734 100644 --- a/nose/plugins/capture.py +++ b/nose/plugins/capture.py @@ -56,10 +56,13 @@ class Capture(Plugin): self.start() def formatError(self, test, err): - test.captured_output = output = self.buffer + test.capturedOutput = output = self.buffer self._buf = None if not output: - return + # Don't return None as that will prevent other + # formatters from formatting and remove earlier formatters + # formats, instead return the err we got + return err ec, ev, tb = err return (ec, self.addCaptureToErr(ev, output), tb) diff --git a/nose/plugins/cover.py b/nose/plugins/cover.py index 3a34e46..fd9e578 100644 --- a/nose/plugins/cover.py +++ b/nose/plugins/cover.py @@ -72,7 +72,10 @@ class Coverage(Plugin): self.conf = config self.coverErase = options.cover_erase self.coverTests = options.cover_tests - self.coverPackages = tolist(options.cover_packages) + self.coverPackages = [] + if options.cover_packages: + for pkgs in [tolist(x) for x in options.cover_packages]: + self.coverPackages.extend(pkgs) self.coverInclusive = options.cover_inclusive if self.coverPackages: log.info("Coverage report will include only packages: %s", diff --git a/nose/plugins/doctests.py b/nose/plugins/doctests.py index f15fd78..8fe9854 100644 --- a/nose/plugins/doctests.py +++ b/nose/plugins/doctests.py @@ -183,11 +183,11 @@ class Doctest(Plugin): class DocTestCase(doctest.DocTestCase): - """Proxy for DocTestCase: provides an address() method that - returns the correct address for the doctest case. Otherwise - acts as a proxy to the test case. To provide hints for address(), - an obj may also be passed -- this will be used as the test object - for purposes of determining the test address, if it is provided. + """Overrides DocTestCase to + provide an address() method that returns the correct address for + the doctest case. To provide hints for address(), an obj may also + be passed -- this will be used as the test object for purposes of + determining the test address, if it is provided. """ def __init__(self, test, optionflags=0, setUp=None, tearDown=None, checker=None, obj=None): @@ -224,7 +224,8 @@ class DocTestCase(doctest.DocTestCase): class DocFileCase(doctest.DocFileCase): - """Overrides to provide filename + """Overrides to provide address() method that returns the correct + address for the doc file case. """ def address(self): return (self._dt_test.filename, None, None) diff --git a/nose/plugins/manager.py b/nose/plugins/manager.py index 6b72b8c..8ecfc4a 100644 --- a/nose/plugins/manager.py +++ b/nose/plugins/manager.py @@ -263,7 +263,8 @@ class ZeroNinePlugin: return self.plugin.addError(test.test, err, capt) def loadTestsFromFile(self, filename): - return self.plugin.loadTestsFromPath(filename) + if hasattr(self.plugin, 'loadTestsFromPath'): + return self.plugin.loadTestsFromPath(filename) def addFailure(self, test, err): if not hasattr(self.plugin, 'addFailure'): diff --git a/nose/plugins/plugintest.py b/nose/plugins/plugintest.py index 7ead48e..b766543 100644 --- a/nose/plugins/plugintest.py +++ b/nose/plugins/plugintest.py @@ -191,6 +191,8 @@ def run(*arg, **kw): plugins = kw.pop('plugins', None) env = kw.pop('env', {}) kw['config'] = Config(env=env, plugins=PluginManager(plugins=plugins)) + if 'argv' not in kw: + kw['argv'] = ['nosetests', '-v'] kw['config'].stream = buffer run(*arg, **kw) out = buffer.getvalue() diff --git a/nose/plugins/prof.py b/nose/plugins/prof.py index f255e87..f48ff1c 100644 --- a/nose/plugins/prof.py +++ b/nose/plugins/prof.py @@ -70,16 +70,28 @@ class Profile(Plugin): self.prof.close() stats = hotshot.stats.load(self.pfile) stats.sort_stats(self.sort) - try: + + # 2.5 has completely different stream handling from 2.4 and earlier. + # Before 2.5, stats objects have no stream attribute; in 2.5 and later + # a reference sys.stdout is stored before we can tweak it. + compat_25 = hasattr(stats, 'stream') + if compat_25: + tmp = stats.stream + stats.stream = stream + else: tmp = sys.stdout sys.stdout = stream + try: if self.restrict: log.debug('setting profiler restriction to %s', self.restrict) stats.print_stats(*self.restrict) else: stats.print_stats() finally: - sys.stdout = tmp + if compat_25: + stats.stream = tmp + else: + sys.stdout = tmp def finalize(self, result): try: diff --git a/nose/proxy.py b/nose/proxy.py index b163cd5..f7aa968 100644 --- a/nose/proxy.py +++ b/nose/proxy.py @@ -21,6 +21,20 @@ from nose.config import Config log = logging.getLogger(__name__) + +def proxied_attribute(local_attr, proxied_attr, doc): + """Create a property that proxies attribute ``proxied_attr`` through + the local attribute ``local_attr``. + """ + def fget(self): + return getattr(getattr(self, local_attr), proxied_attr) + def fset(self, value): + setattr(getattr(self, local_attr), proxied_attr, value) + def fdel(self): + delattr(getattr(self, local_attr), proxied_attr) + return property(fget, fset, fdel, doc) + + class ResultProxyFactory(object): """Factory for result proxies. Generates a ResultProxy bound to each test and the result passed to the test. @@ -53,10 +67,12 @@ class ResultProxyFactory(object): class ResultProxy(object): """Proxy to TestResults (or other results handler). - One ResultProxy is created for each nose.case.Test. The result proxy - calls plugins with the nose.case.Test instance (instead of the - wrapped test case) as each result call is made. Finally, the real result - method is called with the wrapped test. + One ResultProxy is created for each nose.case.Test. The result + proxy calls plugins with the nose.case.Test instance (instead of + the wrapped test case) as each result call is made. Finally, the + real result method is called, also with the nose.case.Test + instance as the test parameter. + """ def __init__(self, result, test, config=None): if config is None: @@ -141,12 +157,14 @@ class ResultProxy(object): self.assertMyTest(test) self.plugins.stopTest(self.test) self.result.stopTest(self.test) - - def get_shouldStop(self): - return self.result.shouldStop - def set_shouldStop(self, shouldStop): - self.result.shouldStop = shouldStop + # proxied attributes + shouldStop = proxied_attribute('result', 'shouldStop', + """Should the test run stop?""") + errors = proxied_attribute('result', 'errors', + """Tests that raised an exception""") + failures = proxied_attribute('result', 'failures', + """Tests that failed""") + testsRun = proxied_attribute('result', 'testsRun', + """Number of tests run""") - shouldStop = property(get_shouldStop, set_shouldStop, None, - """Should the test run stop?""") diff --git a/nose/result.py b/nose/result.py index be665da..d5fff1f 100644 --- a/nose/result.py +++ b/nose/result.py @@ -11,7 +11,7 @@ reporting. import logging from unittest import _TextTestResult from nose.config import Config -from nose.util import odict, ln as _ln # backwards compat +from nose.util import isclass, odict, ln as _ln # backwards compat log = logging.getLogger('nose.result') @@ -44,7 +44,7 @@ class TextTestResult(_TextTestResult): # 2.3 compat exc_info = self._exc_info_to_string(err) for cls, (storage, label, isfail) in self.errorClasses.items(): - if issubclass(ec, cls): + if isclass(ec) and issubclass(ec, cls): storage.append((test, exc_info)) # Might get patched into a streamless result if stream is not None: diff --git a/nose/selector.py b/nose/selector.py index a794288..2390c0a 100644 --- a/nose/selector.py +++ b/nose/selector.py @@ -10,7 +10,7 @@ import logging import os import unittest from nose.config import Config -from nose.util import split_test_name, src, getfilename, getpackage +from nose.util import split_test_name, src, getfilename, getpackage, ispackage log = logging.getLogger(__name__) @@ -86,9 +86,8 @@ class Selector(object): All package directories match, so long as they do not match exclude. All other directories must match test requirements. """ - init = op_join(dirname, '__init__.py') tail = op_basename(dirname) - if op_exists(init): + if ispackage(dirname): wanted = (not self.exclude or not filter(None, [exc.search(tail) for exc in self.exclude] diff --git a/nose/util.py b/nose/util.py index 440f52a..f0bdd91 100644 --- a/nose/util.py +++ b/nose/util.py @@ -134,12 +134,14 @@ def ispackage(path): >>> ispackage('nose/loader.py') False """ - if os.path.isdir(path): - init = [e for e in os.listdir(path) - if os.path.isfile(os.path.join(path, e)) - and src(e) == '__init__.py'] - if init: - return True + if os.path.isdir(path): + # at least the end of the path must be a legal python identifier + # and __init__.py[co] must exist + end = os.path.basename(path) + if ident_re.match(end): + for init in ('__init__.py', '__init__.pyc', '__init__.pyo'): + if os.path.isfile(os.path.join(path, init)): + return True return False @@ -263,32 +265,51 @@ def split_test_name(test): Either side of the : may be dotted. To change the splitting behavior, you can alter nose.util.split_test_re. """ - parts = test.split(':') - num = len(parts) - if num == 1: + norm = os.path.normpath + file_or_mod = test + fn = None + if not ':' in test: # only a file or mod part if file_like(test): - return (test, None, None) + return (norm(test), None, None) else: return (None, test, None) - elif num >= 3: - # definitely popped off a windows driveletter - file_or_mod = ':'.join(parts[0:-1]) - fn = parts[-1] + + # could be path|mod:callable, or a : in the file path someplace + head, tail = os.path.split(test) + if not head: + # this is a case like 'foo:bar' -- generally a module + # name followed by a callable, but also may be a windows + # drive letter followed by a path + try: + file_or_mod, fn = test.split(':') + if file_like(fn): + # must be a funny path + file_or_mod, fn = test, None + except ValueError: + # more than one : in the test + # this is a case like c:\some\path.py:a_test + parts = test.split(':') + if len(parts[0]) == 1: + file_or_mod, fn = ':'.join(parts[:-1]), parts[-1] + else: + # nonsense like foo:bar:baz + raise ValueError("Test name '%s' could not be parsed. Please " + "format test names as path:callable or " + "module:callable.") + elif not tail: + # this is a case like 'foo:bar/' + # : must be part of the file path, so ignore it + file_or_mod = test else: - # only a file or mod part, or a test part, or - # we mistakenly split off a windows driveletter - file_or_mod, fn = parts - if len(file_or_mod) == 1: - # windows drive letter: must be a file - if not file_like(fn): - raise ValueError("Test name '%s' is ambiguous; can't tell " - "if ':%s' refers to a module or callable" - % (test, fn)) - return (test, None, None) + if ':' in tail: + file_part, fn = tail.split(':') + else: + file_part = tail + file_or_mod = os.sep.join([head, file_part]) if file_or_mod: if file_like(file_or_mod): - return (file_or_mod, None, fn) + return (norm(file_or_mod), None, fn) else: return (None, file_or_mod, fn) else: @@ -411,6 +432,13 @@ def match_last(a, b, regex): def tolist(val): """Convert a value that may be a list or a (possibly comma-separated) string into a list. The exception: None is returned as None, not [None]. + + >>> tolist(["one", "two"]) + ['one', 'two'] + >>> tolist("hello") + ['hello'] + >>> tolist("separate,values, with, commas, spaces , are ,ok") + ['separate', 'values', 'with', 'commas', 'spaces', 'are', 'ok'] """ if val is None: return None diff --git a/scripts/mkrelease.py b/scripts/mkrelease.py index 14e21c4..a96b7f8 100755 --- a/scripts/mkrelease.py +++ b/scripts/mkrelease.py @@ -13,11 +13,20 @@ current = os.getcwd() version = nose.__version__ here = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) parts = here.split('/') -branchindex = parts.index('branches') -svnroot = os.path.join('/', *parts[:branchindex]) +if 'branches' in parts: + lindex = parts.index('branches') +elif 'tags' in parts: + lindex = parts.index('tags') +elif 'trunk' in parts: + lindex = parts.index('trunk') +else: + raise Exception("Unable to find svnroot from %s" % here) +svnroot = os.path.join('/', *parts[:lindex]) + branchroot = os.path.join(svnroot, 'branches') tagroot = os.path.join(svnroot, 'tags') svntrunk = os.path.join(svnroot, 'trunk') +svn_base_url = 'https://python-nose.googlecode.com/svn' svn_trunk_url = 'https://python-nose.googlecode.com/svn/trunk' SIMULATE = 'exec' not in sys.argv @@ -45,6 +54,9 @@ def main(): branch = 'branches/%s-stable' % version tag = 'tags/%s-release' % version + svn_branch_url = '%s/%s' % (svn_base_url, branch) + svn_tag_url = '%s/%s' % (svn_base_url, tag) + if os.path.isdir(tag): raise Exception( "Tag path %s already exists. Can't release same version twice!" @@ -52,46 +64,47 @@ def main(): # make branch, if needed if not os.path.isdir(os.path.join(svnroot, branch)): - # update trunk - cd(svntrunk) - runcmd('svn up') - cd(svnroot) - runcmd('svn copy %s %s' % (svn_trunk_url, branch)) - - # clean up setup.cfg and check in branch - cd(branch) - - # remove dev tag from setup - runcmd('cp setup.cfg.release setup.cfg') - runcmd('svn rm setup.cfg.release --force') - + # make branch + runcmd("svn copy %s %s -m 'Release branch for %s'" + % (svn_trunk_url, svn_branch_url, version)) + # clean up setup.cfg and check in tag cd(branchroot) - runcmd("svn ci -m 'Release branch for %s'" % version) - + runcmd('svn co %s' % svn_branch_url) else: # re-releasing branch cd(branch) runcmd('svn up') # make tag from branch - cd(svnroot) - runcmd('svn copy %s %s' % (branch, tag)) + runcmd('svn copy %s %s -m "Release tag for %s"' + % (svn_branch_url, svn_tag_url, version)) - # check in tag + # check out tag cd(tagroot) - runcmd("svn ci -m 'Release tag for %s'" % version) - - # make docs - cd(svnroot) + runcmd('svn co %s' % svn_tag_url) cd(tag) - runcmd('scripts/mkindex.py') - runcmd('scripts/mkdocs.py') + # remove dev tag from setup + runcmd('cp setup.cfg.release setup.cfg') + runcmd('svn rm setup.cfg.release --force') + runcmd("svn ci -m 'Updated setup.cfg to release status'") + + # wiki pages must be built from tag checkout runcmd('scripts/mkwiki.py') - # FIXME need to do this from an *export* to limit files included + # need to build dist from an *export* to limit files included # (setuptools includes too many files when run under a checkout) - # setup sdist + + # export tag + cd('/tmp') + runcmd('svn export %s nose_rel_%s' % (svn_tag_url, version)) + cd('nose_rel_%s' % version) + + # make docs + runcmd('scripts/mkindex.py') + runcmd('scripts/mkdocs.py') + + # make sdist runcmd('python setup.py sdist') # upload docs and distribution diff --git a/scripts/rst2wiki.py b/scripts/rst2wiki.py deleted file mode 100644 index 1a55223..0000000 --- a/scripts/rst2wiki.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python - -import sys -from docutils.nodes import SparseNodeVisitor, paragraph, title_reference, \ - emphasis -from docutils.writers import Writer -from docutils.core import publish_string - -class WikiWriter(Writer): - def translate(self): - visitor = WikiVisitor(self.document) - self.document.walkabout(visitor) - self.output = visitor.astext() - -class WikiVisitor(SparseNodeVisitor): - - def __init__(self, document): - SparseNodeVisitor.__init__(self, document) - self.list_depth = 0 - self.list_item_prefix = None - self.indent = self.old_indent = '' - self.output = [] - self.preformat = False - - def astext(self): - return '\n>>>\n\n'+ ''.join(self.output) + '\n\n<<<\n' - - def visit_Text(self, node): - #print "Text", node - data = node.astext() - if not self.preformat: - data = data.lstrip('\n\r') - data = data.replace('\r', '') - data = data.replace('\n', ' ') - self.output.append(data) - - def visit_bullet_list(self, node): - self.list_depth += 1 - self.list_item_prefix = (' ' * self.list_depth) + '* ' - - def depart_bullet_list(self, node): - self.list_depth -= 1 - if self.list_depth == 0: - self.list_item_prefix = None - else: - (' ' * self.list_depth) + '* ' - self.output.append('\n\n') - - def visit_list_item(self, node): - self.old_indent = self.indent - self.indent = self.list_item_prefix - - def depart_list_item(self, node): - self.indent = self.old_indent - - def visit_literal_block(self, node): - self.output.extend(['{{{', '\n']) - self.preformat = True - - def depart_literal_block(self, node): - self.output.extend(['\n', '}}}', '\n\n']) - self.preformat = False - - def visit_paragraph(self, node): - self.output.append(self.indent) - - def depart_paragraph(self, node): - self.output.append('\n\n') - if self.indent == self.list_item_prefix: - # we're in a sub paragraph of a list item - self.indent = ' ' * self.list_depth - - def visit_reference(self, node): - if node.has_key('refuri'): - href = node['refuri'] - elif node.has_key('refid'): - href = '#' + node['refid'] - else: - href = None - self.output.append('[' + href + ' ') - - def depart_reference(self, node): - self.output.append(']') - - def visit_subtitle(self, node): - self.output.append('=== ') - - def depart_subtitle(self, node): - self.output.append(' ===\n\n') - self.list_depth = 0 - self.indent = '' - - def visit_title(self, node): - self.output.append('== ') - - def depart_title(self, node): - self.output.append(' ==\n\n') - self.list_depth = 0 - self.indent = '' - - def visit_title_reference(self, node): - self.output.append("`") - - def depart_title_reference(self, node): - self.output.append("`") - - def visit_emphasis(self, node): - self.output.append('*') - - def depart_emphasis(self, node): - self.output.append('*') - - def visit_literal(self, node): - self.output.append('`') - - def depart_literal(self, node): - self.output.append('`') - - -def main(source): - output = publish_string(source, writer=WikiWriter()) - print output - -if __name__ == '__main__': - main(sys.stdin.read()) diff --git a/unit_tests/test_capture_plugin.py b/unit_tests/test_capture_plugin.py index 091b0bd..8988665 100644 --- a/unit_tests/test_capture_plugin.py +++ b/unit_tests/test_capture_plugin.py @@ -73,7 +73,7 @@ class TestCapturePlugin(unittest.TestCase): self.assertEqual(ec, fec) self.assertEqual(tb, ftb) assert 'Oh my!' in fev, "Output not found in error message" - assert 'Oh my!' in d.captured_output, "Output not attached to test" + assert 'Oh my!' in d.capturedOutput, "Output not attached to test" if __name__ == '__main__': unittest.main() diff --git a/unit_tests/test_result_proxy.py b/unit_tests/test_result_proxy.py index 6233ca8..9ed1e11 100644 --- a/unit_tests/test_result_proxy.py +++ b/unit_tests/test_result_proxy.py @@ -63,6 +63,29 @@ class TestResultProxy(unittest.TestCase): assert method in res.called, "%s was not proxied" self.assertEqual(res.shouldStop, 'yes please') + def test_attributes_are_proxied(self): + res = unittest.TestResult() + proxy = ResultProxy(res, test=None) + proxy.errors + proxy.failures + proxy.shouldStop + proxy.testsRun + + def test_test_cases_can_access_result_attributes(self): + from nose.case import Test + class TC(unittest.TestCase): + def run(self, result): + unittest.TestCase.run(self, result) + print "errors", result.errors + print "failures", result.failures + def runTest(self): + pass + test = TC() + case = Test(test) + res = unittest.TestResult() + proxy = ResultProxy(res, test=case) + case(proxy) + def test_proxy_handles_missing_methods(self): from nose.case import Test class TC(unittest.TestCase): diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py index ad01e25..f982cc2 100644 --- a/unit_tests/test_utils.py +++ b/unit_tests/test_utils.py @@ -1,9 +1,12 @@ +import os import unittest import nose from nose import case # don't import * -- some util functions look testlike from nose import util +np = os.path.normpath + class TestUtils(unittest.TestCase): def test_file_like(self): @@ -23,29 +26,31 @@ class TestUtils(unittest.TestCase): assert split_test_name('some.module') == \ (None, 'some.module', None) assert split_test_name('this/file.py:func') == \ - ('this/file.py', None, 'func') + (np('this/file.py'), None, 'func') assert split_test_name('some/file.py') == \ - ('some/file.py', None, None) + (np('some/file.py'), None, None) assert split_test_name(':Baz') == \ (None, None, 'Baz') + assert split_test_name('foo:bar/baz.py') == \ + (np('foo:bar/baz.py'), None, None) def test_split_test_name_windows(self): # convenience stn = util.split_test_name self.assertEqual(stn(r'c:\some\path.py:a_test'), - (r'c:\some\path.py', None, 'a_test')) + (np(r'c:\some\path.py'), None, 'a_test')) self.assertEqual(stn(r'c:\some\path.py'), - (r'c:\some\path.py', None, None)) + (np(r'c:\some\path.py'), None, None)) self.assertEqual(stn(r'c:/some/other/path.py'), - (r'c:/some/other/path.py', None, None)) + (np(r'c:/some/other/path.py'), None, None)) self.assertEqual(stn(r'c:/some/other/path.py:Class.test'), - (r'c:/some/other/path.py', None, 'Class.test')) + (np(r'c:/some/other/path.py'), None, 'Class.test')) try: - stn('c:something') + stn('cat:dog:something') except ValueError: pass else: - self.fail("Ambiguous test name should throw ValueError") + self.fail("Nonsense test name should throw ValueError") def test_test_address(self): # test addresses are specified as |