diff options
79 files changed, 9526 insertions, 0 deletions
@@ -0,0 +1,7 @@ +Jason Pellerin +Kumar McMillan +Mika Eloranta +Jay Parlar +Scot Doyle +James Casbon +Antoine Pitrou
\ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..762723a --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,255 @@ +0.9.2 + +- Added make_decorator function to nose.tools. Used to construct decorator + functions that are well-behaved and preserve as much of the original + function's metadata as possible. Thanks to Antoine Pitrou for the patch. +- Added nose.twistedtools, contributed by Antoine Pitrou. This module adds + @deferred decorator that makes it simple to write deferred tests, with or + without timeouts. +- Added nosetests setuptools command. Now you can run python setup.py + nosetests and have access to all nose features and plugins. Thanks to James + Casbon for the patch. + +0.9.1 + +- New function nose.runmodule() finds and runs tests only in a + single module, which defaults to __main__ (like unittest.main() or + doctest.runmodule()). Thanks Greg Wilson for the suggestion. +- Multiple -w (--where) arguments can now be used in one command line, + to find and run tests in multiple locations. Thanks Titus Brown for + the suggestion. +- Multiple --include and --exclude arguments are now accepted in one command + line. Thanks Michal Kwiatkowski for the feature request. +- Coverage will now include modules not imported by any test when + using the new --cover-inclusive switch. Thanks James Casbon for the + patch. +- module:TestClass test selections now properly select all tests in the test + class. +- startTest and stopTest are now called in plugins at the beginning and end of + test suites, including test modules, as well as individual tests. Thanks + Michal Kwiatkowski for the suggestion. +- Fix bug in test selection when run as ``python setup.py test``: 'test' was + passing through and being used as the test name selection. Thanks Kumar + McMillan for the bug report. +- Fix bug in handling of -x/--stop option where the test run would stop on + skipped or deprecated tests. Thanks Kumar McMillan for the bug report. +- Fix bug in loading tests from projects with layouts that place modules in + /lib or /src dirs and tests in a parallel /tests dir. +- Fix bug in python version detection. Thanks Kevin Dangoor for the bug report + and fix. +- Fix log message in selector that could raise IndexError. Thanks Kumar + McMillan for the bug report and patch. +- Fix bug in handling doctest extension arguments specified in environment and + on command line. Thanks Ian Bicking for the bug report. +- Fix bug in running fixtures (setup/teardown) that are not functions, and + report a better error message when a fixture is not callable. Thanks Ian + Bicking for the bug report. + +0.9.0 + +- More unit tests and better test coverage. Numerous bugfixes deriving from + same. +- Make --exe option do what it says, and turn it on by default on + Windows. Add --noexe option so windows users can turn if off.Thanks + richard at artsalliancemedia dot com for the bug reports. +- Handle a working directory that happens to be in the middle of a package + more gracefully. Thanks Max Ischenko for the bug report and test case. +- Fix bugs in test name comparison when a test module is specified whose name + overlaps that of a non-test module. Thanks Max Ischenko for the bug report + and test case. +- Fix warning spam when a non-existent test file is requested on the command + line. Thanks Max Ischenko for the bug report. + +0.9.0b2 + +- Allow --debug to set any logger to DEBUG. Thanks to casbon at gmail dot com for + the patch. +- Fix doctest help, which was missing notes about the environment variables + that it accepts. Thanks to Kumar McMillan for the patch. +- Restore sys.stdout after run() in nose.core. Thanks to Titus Brown for the + bug report. +- Correct handling of trailing comma in attrib plugin args. Thanks Titus Brown + for the patch. + +0.9.0b1 + +- Fix bug in handling of OR conditions in attrib plugin. Thanks to Titus + Brown for the bug report. +- Fix bug in nose.importer that would cause an attribute error when a local + module shadowed a builtin, or other object in sys.modules, without a + __file__ attribute. Thanks to casbon at gmail dot com for the bug report. +- Fix bug in nose.tools decorators that would cause decorated tests to appear + with incorrect names in result output. + +0.9.0a2 + +- In TestLoader, use inspect's isfunction() and ismethod() to filter functions + and methods, instead of callable(). Thanks to Kumar McMillan for reporting + the bug. +- Fix doctest plugin: return an empty iterable when no tests are found in a + directory instead of None. Thanks to Kumar McMillan for the bug report and + patch. +- Ignore executable python modules, unless run with --exe file. This is a + partial defense against nose causing trouble by loading python modules that + are not import-safe. The full defense: don't write modules that aren't + import safe! +- Catch and warn about errors on plugin load instead of dying. +- Renamed builtin profile module from nose.plugins.profile to + nose.plugins.prof to avoid shadowing stdlib profile.py module. + +0.9.0a1 + +- Add support for plugins, with hooks for selecting, loading and reporting on + tests. Doctest and coverage are now plugins. +- Add builtin plugins for profiling with hotshot, selecting tests by + attribute (contributed by Mika Eloranta), and warning of missed tests + specified on command line. +- Change command line test selection syntax to match unittest. Thanks to Titus + Brown for the suggestion. +- Option to drop into pdb on error or failure. +- Option to stop running on first error or failure. Thanks to Kevin Dangoor + for the suggestion. +- Support for doctests in files other than python modules (python 2.4 only) +- Reimplement base test selection as single self-contained class. +- Reimplement test loading as unittest-compatible TestLoader class. +- Remove all monkeypatching. +- Reimplement output capture and assert introspection support in + unittest-compatible Result class. +- Better support for multiline constructions in assert introspection. +- More context output with assert introspections. +- Refactor setuptools test command support to use proxied result, which + enables output capture and assert introspection support without + monkeypatching. Thanks to Philip J. Eby for the suggestion and skeleton + implementation. +- Add support for generators in test classes. Thanks to Jay Parlar for the + suggestion and patch. +- Add nose.tools package with some helpful test-composition functions and + decorators, including @raises, contributed by Scot Doyle. +- Reimplement nose.main (TestProgram) to have unittest-compatible signature. +- All-new import path handling. You can even turn it off! (If you don't, + nose will ensure that all directories from which it imports anything are on + sys.path before the import.) +- Logging package used for verbose logging. +- Support for skipped and deprecated tests. +- Configuration is no longer global. + +0.8.7 + +- Add support for py.test-style test generators. Thanks to Jay Parlar for + the suggestion. +- Fix bug in doctest discovery. Thanks to Richard Cooper for the bug report. +- Fix bug in output capture being appended to later exceptions. Thanks to + Titus Brown for the patch that uncovered the bug. +- Fix bug(?) in Exception patch that caused masked hasattr/__getattr__ loops + to either become actual infinite loops, or at least take so long to finally + error out that they might as well be infinite. +- Add -m option to restrict test running to only tests in a particular package + or module. Like the -f option, -m does not restrict test *loading*, only + test *execution*. +- When loading and running a test module, ensure that the module's path is in + sys.path for the duration of the run, not just while importing the module. +- Add id() method to all callable test classes, for greater unittest + compatibility. + +0.8.6 + +- Fix bug with coverage output when sys.modules contains entries without + __file__ attributes +- Added -p (--cover-packages) switch that may be used to restrict coverage + report to modules in the indicated package(s) + +0.8.5 + +- Output capture and verbose assertion errors now work when run like + 'python setup.py test', as advertised. +- Code coverage improvements: now coverage will be output for all modules + imported by any means that were not in sys.modules at the start of the test + run. By default, test modules will be excluded from the coverage report, but + you can include them with the -t (--cover-tests) option. + +0.8.4 + +- Fix bugs in handling of setup/teardown fixtures that could cause TypeError + exceptions in fixtures to be silently ignored, or multiple fixtures of the + same type to run. Thanks to Titus Brown for the bug report. + +0.8.3 + +- Add -V (--version) switch to nosetests +- Fix bug where sys.path would not be set up correctly when running some + tests, producing spurious import errors (Thanks to Titus Brown and Mike + Thomson for the bug reports) +- For test classses not derived from unittest.TestCase, output (module.Class) + "doc string" as test description, when method doc string is available + (Thanks to David Keeney for the suggestion, even if this isn't quite what he + meant) + +0.8.2 + +- Revise import to bypass sys.path and manipulate sys.modules more + intelligently, ensuring that the test module we think we are loading is the + module we actually load, and that modules loaded by other imports are not + reloaded without cause +- Allow test directories inside of packages. Formerly directories matching + testMatch but lacking an __init__.py would cause an ImportError when located + inside of packages +- Fix bugs in different handling of -f switch in combination with -w and -o + +0.8.1 + +- Fix bug in main() that resulted in incorrect exit status for nosetests + script when tests fail +- Add missing test files to MANIFEST.in +- Miscellaneous pylint cleanups + +0.8 + +- Add doctest support +- Add optional code coverage support, using Ned Batchelder's coverage.py; + activate with --coverage switch or NOSE_COVERAGE environment variable +- More informative error message on import error +- Fix bug where module setup could be called twice and teardown skipped + for certain setup method names. +- main() returns success value, does not exit. run_exit() added to support + old behavior; nosetests script now calls nose.run_exit() + +0.7.5 + +- Fix bus error on exit +- Discover tests inside of non-TestCase classes that match testMatch +- Reorganize selftest: now selftest tests the output of a full nose run +- Add test_with_setup.py contributed by Kumar McMillan + +0.7.2 + +- Refactor and correct bugs in discovery and test loading +- Reorganize and expand documentation +- Add -f (run this test file only) switch + +0.7.1 + +- Bugfix release: test files in root of working directory were not being + stripped of file extension before import. + +0.7 + +- Change license to LGPL +- Major rework of output capture and assert introspection +- Improve test discovery: now finds tests in packages +- Replace -n switch ('no cwd') with -w switch ('look here') + +0.6 + +- New nosetests script +- Allow specification of names on command line that are loadable but not + directly loadable as modules (eg nosetests -o path/to/tests.py) +- Add optional py.test-like assert introspection. Thanks to Kevin Dangoor + for the suggestion. +- Improvements to selftest + +0.5.1 + +- Increased compatibility with python 2.3 (and maybe earlier) +- Increased compatibility with tests written for py.test: now calls + module.setup_module(module) if module.setup_module() fails + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7f6cbd7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +include AUTHORS +include ez_setup.py +include unit_tests/*/*.py +include unit_tests/*/*/*.py +include unit_tests/*/*/*/*.py +include unit_tests/*/*/*/*/*.py +include CHANGELOG +include NEWS +include README.txt +include lgpl.txt
\ No newline at end of file @@ -0,0 +1,19 @@ +New in version 0.9.1 +-------------------- + +Nose 0.9.1 is mainly a bug-fix release, but it does contain a few new +features. + +* The --where (-w), --include and --exclude arguments may now all appear + multiple times in a single command line, allowing easier running of + multiple test suites and test suites with more diverse layouts. +* For programmatic use, nose.runmodule() was added. Similar to + doctest.runmodule() and unittest.main(), nose.runmodule() will load and run + tests in the current module, which defaults to __main__. +* A number of changes to plugins and plugin hooks make current plugins work + better and allow more interesting plugins to be written. + +Just about everything in this release was driven by requests from +users. Thanks to the many folks who filed bug reports and suggested features, +ideas and solutions to thorny problems. +
\ No newline at end of file diff --git a/NEWS_0.9 b/NEWS_0.9 new file mode 100644 index 0000000..d7fa15f --- /dev/null +++ b/NEWS_0.9 @@ -0,0 +1,59 @@ +New in version 0.9 +------------------ + +0.9 Final is here! +================== + +nose 0.9 includes a host of new features, as well as numerous +backwards-incompatible changes to interfaces and implementation. + +Thanks to the many folks who have contributed patches and ideas and made bug +reports for the development version of 0.9, especially Mika Eloranta, Jay +Parlar, Kevin Dangoor, Scot Doyle, Titus Brown and Philip J.Eby. + +Here's a quick rundown of what's new in 0.9 + +- Plugins + + The most important new feature is support for plugins using setuptools + entrypoints. nose plugins can select and load tests (like the builtin + doctest plugin), reject tests (like the builtin attrib plugin, contributed + by Mika Eloranta, that allows users to select tests by attribute), + watch and report on tests (like the builtin coverage and profiler plugins), + completely replace test result output (like the html result plugin in the + examples directory) or any combination of the above. Writing plugins is + simple: subclass nose.plugins.Plugin and implement any of the methods in + nose.plugins.IPluginInterface. + +- Better compatibility with unittest + + Test loading has been consolidated into a test loader class that is drop-in + compatible with unittest.TestLoader. Likewise test result output, including + output capture, assert introspection, and support for skipped and deprecated + tests, in nose.result.TextTestResult. If you want those features and not the + rest of nose, you can use just those classes. nose.main() has also been + rewritten to have the same signature as unittest.main(). + +- Better command line interface + + Command line test selection is more intuitive and powerful, enabling easy + and correct running of single tests while ensuring that fixtures (setup and + teardown) are correctly executed at all levels. No more -f -m or -o options: + now simply specify the tests to run:: + + nosetests this/file.py that.module + + Tests may be specified down to the callable:: + + nosetests this/file.py:TestClass that.module:this_test + nosetests that.module:TestClass.test_method + + There are also new options for dropping into pdb on errors or failures, and + stopping the test run on the first error or failure (thanks to Kevin Dangoor + for the idea). + +- More! + + Helpful test decorators and functions in nose.tools. Support for generators + in test classes. Better import path handling -- that you can shut off! + Detailed verbose logging using the logging package. And more... @@ -0,0 +1,30 @@ +notes on loading from modules + +this pretty much all has to take place inside of the _tests iterator. + + +if the module is wanted + run setup + load tests (including submodules) and yield each test + run teardown +else if the module is not wanted: + * do not import the module * + if the module is a package: + recurse into the package looking for test modules + + +make suite.TestSuite +put run, call, setup, teardown, shortdescription there + +make LazySuite subclass it + +get rid of TestModule + +do module import in loadTestsFromModuleName; if an error, pass the error +to the module suite, whose run() should re-raise the error so that import +errors are seen only when we actually try to run the tests + +make ModuleSuite class with setUp, tearDown doing try_run, it gets +additional module and error keyword args + +rename TestDir to DirectorySuite diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..d1158ea --- /dev/null +++ b/README.txt @@ -0,0 +1,287 @@ +nose: a discovery-based unittest extension. + +nose provides an alternate test discovery and running process for +unittest, one that is intended to mimic the behavior of py.test as much +as is reasonably possible without resorting to too much magic. + +Basic usage +----------- + +Use the nosetests script (after installation by setuptools):: + + nosetests [options] [(optional) test files or directories] + +You may also use nose in a test script:: + + import nose + nose.main() + +If you don't want the test script to exit with 0 on success and 1 on failure +(like unittest.main), use nose.run() instead:: + + import nose + result = nose.run() + +`result` will be true if the test run succeeded, or false if any test failed +or raised an uncaught exception. Lastly, you can run nose.core directly, which +will run nose.main():: + + python /path/to/nose/core.py + +Please see the usage message for the nosetests script for information +about how to control which tests nose runs, which plugins are loaded, +and the test output. + +Features +-------- + +Run as collect +============== + +nose begins running tests as soon as the first test module is loaded, it +does not wait to collect all tests before running the first. + +Output capture +============== + +Unless called with the -s (--nocapture) switch, nose will capture stdout +during each test run, and print the captured output only for tests that +fail or have errors. The captured output is printed immediately +following the error or failure output for the test. (Note that output in +teardown methods is captured, but can't be output with failing tests, +because teardown has not yet run at the time of the failure.) + +Assert introspection +==================== + +When run with the -d (--detailed-errors) switch, nose will try to output +additional information about the assert expression that failed with each +failing test. Currently, this means that names in the assert expression +will be expanded into any values found for them in the locals or globals +in the frame in which the expression executed. + +In other words if you have a test like:: + + def test_integers(): + a = 2 + assert a == 4, "assert 2 is 4" + +You will get output like:: + + File "/path/to/file.py", line XX, in test_integers: + assert a == 4, "assert 2 is 4" + AssertionError: assert 2 is 4 + >> assert 2 == 4, "assert 2 is 4" + +Setuptools integration +====================== + +nose may be used with the setuptools_ test command. Simply specify +nose.collector as the test suite in your setup file:: + + setup ( + # ... + test_suite = 'nose.collector' + ) + +Then to find and run tests, you can run:: + + python setup.py test + +When running under setuptools, you can configure nose settings via the +environment variables detailed in the nosetests script usage message. + +Please note that when run under the setuptools test command, some plugins will +not be available, including the builtin coverage, profiler, and missed test +plugins. + +nose also includes its own setuptools command, `nosetests`, that provides +support for all plugins and command line options, as well as configuration +using the setup.cfg file. See nose.commands_ for more information about the +`nosetests` command. + +.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools +.. _nose.commands: #commands + +Writing tests +------------- + +As with py.test, nose tests need not be subclasses of TestCase. Any function +or class that matches the configured testMatch regular expression +('(?:^|[\b_\.-])[Tt]est)'' by default) and lives in a module that also +matches that expression will be run as a test. For the sake of compatibility +with legacy unittest test cases, nose will also load tests from +unittest.TestCase subclasses just like unittest does. Like py.test, functional +tests will be run in the order in which they appear in the module +file. TestCase derived tests and other test classes are run in alphabetical +order. + +Fixtures +======== + +nose supports fixtures (setup and teardown methods) at the package, +module, and test level. As with py.test or unittest fixtures, setup always +runs before any test (or collection of tests for test packages and modules); +teardown runs if setup has completed successfully, whether or not the test +or tests pass. For more detail on fixtures at each level, see below. + +Test packages +============= + +nose allows tests to be grouped into test packages. This allows +package-level setup; for instance, if you need to create a test database +or other data fixture for your tests, you may create it in package setup +and remove it in package teardown once per test run, rather than having to +create and tear it down once per test module or test case. + +To create package-level setup and teardown methods, define setup and/or +teardown functions in the __init__.py of a test package. Setup methods may +be named 'setup', 'setup_package', 'setUp',or 'setUpPackage'; teardown may +be named 'teardown', 'teardown_package', 'tearDown' or 'tearDownPackage'. +Execution of tests in a test package begins as soon as the first test +module is loaded from the test package. + +Test modules +============ + +A test module is a python module that matches the testMatch regular +expression. Test modules offer module-level setup and teardown; define the +method 'setup', 'setup_module', 'setUp' or 'setUpModule' for setup, +'teardown', 'teardown_module', or 'tearDownModule' for teardown. Execution +of tests in a test module begins after all tests are collected. + +Test classes +============ + +A test class is a class defined in a test module that is either a subclass +of unittest.TestCase, or matches testMatch. Test classes that don't +descend from unittest.TestCase are run in the same way as those that do: +methods in the class that match testMatch are discovered, and a test case +constructed to run each with a fresh instance of the test class. Like +unittest.TestCase subclasses, other test classes may define setUp and +tearDown methods that will be run before and after each test method. + +Test functions +============== + +Any function in a test module that matches testMatch will be wrapped in a +FunctionTestCase and run as a test. The simplest possible failing test is +therefore:: + + def test(): + assert False + +And the simplest passing test:: + + def test(): + pass + +Test functions may define setup and/or teardown attributes, which will be +run before and after the test function, respectively. A convenient way to +do this, especially when several test functions in the same module need +the same setup, is to use the provided with_setup decorator:: + + def setup_func(): + # ... + + def teardown_func(): + # ... + + @with_setup(setup_func,teardown_func) + def test(): + # ... + +For python 2.3, add the attributes by calling the decorator function like +so:: + + def test(): + # ... + test = with_setup(setup_func,teardown_func)(test) + +or by direct assignment:: + + test.setup = setup_func + test.teardown = teardown_func + +Test generators +=============== + +nose supports test functions and methods that are generators. A simple +example from nose's selftest suite is probably the best explanation:: + + def test_evens(): + for i in range(0, 5): + yield check_even, i, i*3 + + def check_even(n, nn): + assert n % 2 == 0 or nn % 2 == 0 + +This will result in 4 tests. nose will iterate the generator, creating a +function test case wrapper for each tuple it yields. As in the example, test +generators must yield tuples, the first element of which must be a callable +and the remaining elements the arguments to be passed to the callable. + +Setup and teardown functions may be used with test generators. The setup and +teardown attributes must be attached to the generator function:: + + @with_setup(setup_func, teardown_func) + def test_generator(): + ... + yield func, arg, arg ... + +The setup and teardown functions will be executed for each test that the +generator returns. + +For generator methods, the setUp and tearDown methods of the class (if any) +will be run before and after each generated test case. + +Please note that method generators `are not` supported in unittest.TestCase +subclasses. + +About the name +-------------- + +* nose is the least silly short synonym for discover in the dictionary.com + thesaurus that does not contain the word 'spy'. +* Pythons have noses +* The nose knows where to find your tests +* Nose Obviates Suite Employment + +Contact the author +------------------ + +To report bugs, ask questions, or request features, please use the trac +instance provided by the great folks at python hosting, here: +http://nose.python-hosting.com. Or, email the author at +jpellerin+nose at gmail dot com. Patches are welcome! + +Similar test runners +-------------------- + +nose was inspired mainly by py.test_, which is a great test runner, but +formerly was not all that easy to install, and is not based on unittest. + +Test suites written for use with nose should work equally well with py.test, +and vice versa, except for the differences in output capture and command line +arguments for the respective tools. + +.. _py.test: http://codespeak.net/py/current/doc/test.html + +License and copyright +--------------------- + +nose is copyright Jason Pellerin 2005-2006 + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2 of the License, or (at your +option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program; if not, write to the Free Software Foundation, +Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/TODO_0_9 b/TODO_0_9 new file mode 100644 index 0000000..69e1427 --- /dev/null +++ b/TODO_0_9 @@ -0,0 +1,74 @@ +BUGS +---- + +split_test_name is detected as a test: rename it + + +Refactor +-------- + - Make conf passed not global + - Use logging for messaging + + X split out test selection into selector class + X use in core + X move config to own module + X move capture to result module + X implement in result + - remove from core + X use text test result in core + X use plugins in core + X coverage + +Output capture handling +----------------------- + - Monkeypatch in a new _TextTestResult instead of all of the current chicanery + + +Assert introspection +-------------------- + - use in assertion error only -- only replace that class (replace the + reference(s) in unittest as well) + - introspect object instances? what about methods (probably not) + - fix the 'EOF in multi-line input' bug + + +Error handling +-------------- + - exit on error or fail + X pdb on error or fail + + +Path handling +------------- + X importer: + (all configurable!) + * before attempting import, ensure that the path to the module to + be imported is in sys.path; + * if the module is a file other than __init__.py in a dir containing an + __init__.py, walk up to find the root dir (with no __init__.py) and + ensure that directory is in sys.path + * after import, walk up package to package root and ensure that the + package root is in sys.path + * investigate Kumar's load/reload bug + + +Plugins +------- + - test selector: decide if a dir, module, file, class, or function is a + wanted test + X doctest + - test collector: given a context (module, directory, or file) return tests + X doctest + - test watcher: before_all, before_test, after_test, after_all .. + X coverage + - profile + + +Utilities +-------- +@raises(*exc) -- assert func call raises exception +@timed(under, over, exact) -- assert func execs in under, over, exact time + +SkipTest exception ... catch in addError, addSkip instead, print skipped in +output after failed +Deprecated too?
\ No newline at end of file diff --git a/examples/attrib_plugin.py b/examples/attrib_plugin.py new file mode 100644 index 0000000..c1f8458 --- /dev/null +++ b/examples/attrib_plugin.py @@ -0,0 +1,82 @@ +"""
+Examples of test function/method attribute usage with patched nose
+
+Simple syntax (-a, --attr) examples:
+ * nosetests -a status=stable
+ => only test cases with attribute "status" having value "stable"
+
+ * nosetests -a priority=2,status=stable
+ => both attributes must match
+
+ * nosetests -a tags=http
+ => attribute list "tags" must contain value "http" (see test_foobar()
+ below for definition)
+
+ * nosetests -a slow
+ => attribute "slow" must be defined and its value cannot equal to False
+ (False, [], "", etc...)
+
+ * nosetests -a !slow
+ => attribute "slow" must NOT be defined or its value must be equal to False
+
+Eval expression syntax (-A, --eval-attr) examples:
+ * nosetests -A "not slow"
+ * nosetests -A "(priority > 5) and not slow"
+
+This example and the accompanied patch is in public domain, free for any use.
+
+email: mika.eloranta@gmail.com
+
+"""
+
+__author__ = 'Mika Eloranta'
+
+def attr(**kwargs):
+ """Add attributes to a test function/method/class"""
+ def wrap(func):
+ func.__dict__.update(kwargs)
+ return func
+ return wrap
+
+# test function with single attribute
+@attr(priority = 1)
+def test_dummy():
+ print "dummy"
+
+# test function with multiple attributes
+@attr(status = "stable", # simple string attribute
+ slow = True, # attributes can be of any type
+ # (e.g. bool)
+ priority = 1, # ...or int
+ tags = ["http", "pop", "imap"]) # will be run if any of the list items
+ # matches
+def test_foobar():
+ print "foobar"
+
+# another way of adding attributes...
+def test_fluffy():
+ print "fluffy"
+test_fluffy.status = "unstable"
+test_fluffy.slow = True
+test_fluffy.priority = 2
+
+# works for class methods, too
+class TestSomething:
+ @attr(status = "stable", priority = 2)
+ def test_xyz(self):
+ print "xyz"
+
+# class methods "inherit" attributes from the class but can override them
+class TestOverride:
+ value = "class"
+ # run all methods with "nosetests -a value"
+
+ @attr(value = "method")
+ def test_override(self):
+ # run with "nosetests -a value=method"
+ print "override"
+
+ def test_inherit(self):
+ # run with "nosetests -a value=class"
+ print "inherit"
+
diff --git a/examples/html_plugin/htmlplug.py b/examples/html_plugin/htmlplug.py new file mode 100644 index 0000000..8394363 --- /dev/null +++ b/examples/html_plugin/htmlplug.py @@ -0,0 +1,88 @@ +"""This is a very basic example of a plugin that controls all test +output. In this case, it formats the output as ugly unstyled html. + +Upgrading this plugin into one that uses a template and css to produce +nice-looking, easily-modifiable html output is left as an exercise for +the reader who would like to see his or her name in the nose AUTHORS file. +""" +import traceback +from nose.plugins import Plugin + +class HtmlOutput(Plugin): + """Output test results as ugly, unstyled html. + """ + + name = 'html-output' + + def __init__(self): + super(HtmlOutput, self).__init__() + self.html = [ '<html><head>', + '<title>Test output</title>', + '</head><body>' ] + + def addSuccess(self, test, capt): + self.html.append('<span>ok</span>') + + def addSkip(self, test): + self.html.append('<span>SKIPPED</span>') + + def addDeprecated(self, test): + self.html.append('<span>DEPRECATED</span>') + + def addError(self, test, err, capt): + err = self.formatErr(err) + self.html.append('<span>ERROR</span>') + self.html.append('<pre>%s</pre>' % err) + if capt: + self.html.append('<pre>%s</pre>' % capt) + + def addFailure(self, test, err, capt, tb_info): + err = self.formatErr(err) + self.html.append('<span>FAIL</span>') + self.html.append('<pre>%s</pre>' % err) + if tb_info: + self.html.append('<pre>%s</pre>' % tb_info) + if capt: + self.html.append('<pre>%s</pre>' % capt) + + def finalize(self, result): + self.html.append('<div>') + self.html.append("Ran %d test%s" % + (result.testsRun, result.testsRun != 1 and "s" or "")) + self.html.append('</div>') + self.html.append('<div>') + if not result.wasSuccessful(): + self.html.extend(['<span>FAILED ( ', + 'failures=%d ' % len(result.failures), + 'errors=%d' % len(result.errors), + ')</span>']) + else: + self.html.append('OK') + self.html.append('</div></body></html>') + # print >> sys.stderr, self.html + for l in self.html: + self.stream.writeln(l) + + def formatErr(self, err): + exctype, value, tb = err + return traceback.format_exception(exctype, value, tb) + + def setOutputStream(self, stream): + # grab for own use + self.stream = stream + # return dummy stream + class dummy: + def write(self, *arg): + pass + def writeln(self, *arg): + pass + d = dummy() + return d + + def startTest(self, test): + self.html.extend([ '<div><span>', + test.shortDescription() or str(test), + '</span>' ]) + + def stopTest(self, test): + self.html.append('</div>') diff --git a/examples/html_plugin/setup.py b/examples/html_plugin/setup.py new file mode 100644 index 0000000..ecc839e --- /dev/null +++ b/examples/html_plugin/setup.py @@ -0,0 +1,24 @@ +import sys +try: + import ez_setup + ez_setup.use_setuptools() +except ImportError: + pass + +from setuptools import setup + +setup( + name='Example html output plugin', + version='0.1', + author='Jason Pellerin', + author_email = 'jpellerin+nose@gmail.com', + description = 'Example nose html output plugin', + license = 'GNU LGPL', + py_modules = ['htmlplug'], + entry_points = { + 'nose.plugins': [ + 'htmlout = htmlplug:HtmlOutput' + ] + } + + ) diff --git a/examples/plugin/plug.py b/examples/plugin/plug.py new file mode 100644 index 0000000..444226d --- /dev/null +++ b/examples/plugin/plug.py @@ -0,0 +1,4 @@ +from nose.plugins import Plugin + +class ExamplePlugin(Plugin): + pass diff --git a/examples/plugin/setup.py b/examples/plugin/setup.py new file mode 100644 index 0000000..92a42f3 --- /dev/null +++ b/examples/plugin/setup.py @@ -0,0 +1,27 @@ +""" +An example of how to create a simple nose plugin. + +""" +try: + import ez_setup + ez_setup.use_setuptools() +except ImportError: + pass + +from setuptools import setup + +setup( + name='Example plugin', + version='0.1', + author='Jason Pellerin', + author_email = 'jpellerin+nose@gmail.com', + description = 'Example nose plugin', + license = 'GNU LGPL', + py_modules = ['plug'], + entry_points = { + 'nose.plugins': [ + 'example = plug:ExamplePlugin' + ] + } + + ) diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..80426a8 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,219 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c2" +DEFAULT_URL = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', +} + +import sys, os + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + from md5 import md5 + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + try: + import setuptools + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + except ImportError: + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + + import pkg_resources + try: + pkg_resources.require("setuptools>="+version) + + except pkg_resources.VersionConflict, e: + # XXX could we install in a subprocess here? + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first.\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + # tell the user to uninstall obsolete version + use_setuptools(version) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + + + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + from md5 import md5 + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + diff --git a/index.html.tpl b/index.html.tpl new file mode 100644 index 0000000..235f8ca --- /dev/null +++ b/index.html.tpl @@ -0,0 +1,251 @@ +<html> + <head> + <title>nose: a discovery-based unittest extension</title> + <style> + body { + margin 0px; + padding: 10px 40px; + font: x-small Georgia,Serif; + font-size/* */:/**/small; + font-size: /**/small; + } + a:link { + color:#58a; + text-decoration:none; + } + a:visited { + color:#969; + text-decoration:none; + } + a:hover { + color:#c60; + text-decoration:underline; + } + + #menu { + padding-left 1em; + padding-right: 1em; + padding-bottom: 10px; + margin-left: 20px; + min-width: 200px; + width: 20%%; + border-left: 1px solid #ddd; + border-bottom: 1px solid #ddd; + background-color: #fff; + float: right; + } + + #main { + margin: 0px; + padding: 0px; + padding-right: 20px; + width: 70%%; + float: left; + } + + h1 { + font-size: 140%%; + margin-top: 0; + } + + .section h1 { + font-size: 120%%; + } + + .section h2 { + font-size: 105%%; + } + + pre.literal-block { + font: small; + background: #ddd; + } + + #menu ul { + margin: 0 1em .25em; + padding: 0; + list-style:none; + } + + #menu h2 { + font-size: 100%%; + color: #999; + margin: 0 .5em; + padding: 0; + } + + #menu ul li { + margin: 0px; + padding: 0px 0px 0px 15px; + text-indent:-15px; + /* line-height:1.5em; */ + } + + #menu p, #menu ol li { + font-size: 90%%; + color:#666; + /* line-height:1.5em; */ + margin: 0 1em .5em; + } + + #menu ul li { + font-size: 90%%; + color:#666; + } + + #menu dd { + margin: 0; + padding:0 0 .25em 15px; + } + + #news { + border: 1px solid #999; + background-color: #eef; + /* wouldn't it be nice if this worked */ + background-image: url(flake.svg); + padding: 4px; + padding-right: 8px; + } + + #news h2 { + margin-top: 0px; + font-size: 105%%; + } + + #news li p { + margin-left: 1.5em; + } + + #news li p.first { + margin-left: 0; + font-weight: bold; + } + + #news p { + margin-bottom: 0px; + } + + </style> + </head> + <body> + + + <div id="menu"> + <h2><a href="nose-%(version)s.tar.gz">Download</a></h2> + <p>Current version: %(version)s <br />(%(date)s)</p> + + <h2>Install</h2> + <p>Current version: <br /><tt>easy_install nose==%(version)s</tt></p> + <p>Unstable (trunk): <br /><tt>easy_install nose==dev</tt></p> + + <h2>Read</h2> + <ul> + <li> + <a href="http://ivory.idyll.org/articles/nose-intro.html"> + An Extended Introduction to the nose Unit Testing Framework + </a> + <br />Titus Brown's excellent article provides a great overview of + nose and its uses. + </li> + <li><a href="#usage">nosetests usage</a> + <br />How to use the command-line test runner. + </li> + </ul> + + <h2><a href="http://groups.google.com/group/nose-announce"> + Announcement list</a></h2> + <p>Sign up to receive email announcements + of new releases</p> + + <h2><a href="http://nose.python-hosting.com/">Trac</a></h2> + <p>Report bugs, request features, wik the wiki, browse source.</p> + + <h2>Get the code</h2> + <p><tt>svn co http://svn.nose.python-hosting.com/trunk</tt></p> + + <h2>Other links</h2> + <ul> + <li><a href="/mrl/">My blog</a></li> + <li> + <a href="http://codespeak.net/py/current/doc/test.html">py.test</a> + </li> + <li><a href="http://www.turbogears.com/testgears/">testgears</a></li> + <li> + <a href="http://peak.telecommunity.com/DevCenter/setuptools">setuptools</a> + </li> + </ul> + </div> + <div id="main"> + <h1>nose: a discovery-based unittest extension.</h1> + + <p>nose provides an alternate test discovery and running process for + unittest, one that is intended to mimic the behavior of py.test as much + as is reasonably possible without resorting to too much magic. + </p> + + <div id="news"> + <h2>News</h2> + %(news)s + <p>See the <a href="#changelog">changelog</a> for details.</p> + </div> + + <h2>Install</h2> + + <p>Install nose using setuptools: + <pre>easy_install nose</pre> + </p> + + <p>Or, if you don't have setuptools installed, use the download link at + right to download the source package, and install in the normal fashion: + Ungzip and untar the source package, cd to the new directory, and: + + <pre>python setup.py install</pre> + </p> + + %(body)s + + <h2><a name="tools"></a>nose.tools</h2> + + %(tools)s + + <p><b>FIXME:</b> use pudge to generate rst docs for all tools funcs</p> + + <h2><a name="commands"></a>nosetests setuptools command</h2> + + %(commands)s + + <h2><a name="usage"></a>nosetests usage</h2> + + %(usage)s + + <h2>Bug reports</h2> + + <p>Please report bugs and make feature + requests <a href="http://nose.python-hosting.com">here</a>.</p> + + <h2>Hack</h2> + + <p><a href="http://nose.python-hosting.com/wiki/WritingPlugins">Write + plugins!</a> It's easy and fun.</p> + + <p>Get the code: + <pre>svn co http://svn.nose.python-hosting.com/trunk</pre> + </p> + + <p><a href="mailto:jpellerin+nose@gmail.com">Patches are + welcome</a>. I'd suggest grabbing a copy + of <a href="http://svk.elixus.org/">svk</a> so that you can have + local version control and submit full patches against an up-to-date + tree easily. + </p> + + <p>Thanks to the great folks at python hosting for providing the + subversion repository and trac instance.</p> + + <h2><a name="changelog"></a>Changelog</h2> + %(changelog)s + + </div> + + </body> +</html> diff --git a/lgpl.txt b/lgpl.txt new file mode 100644 index 0000000..8add30a --- /dev/null +++ b/lgpl.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/nose/__init__.py b/nose/__init__.py new file mode 100644 index 0000000..5dc7a11 --- /dev/null +++ b/nose/__init__.py @@ -0,0 +1,309 @@ +"""nose: a discovery-based unittest extension. + +nose provides an alternate test discovery and running process for +unittest, one that is intended to mimic the behavior of py.test as much +as is reasonably possible without resorting to too much magic. + +Basic usage +----------- + +Use the nosetests script (after installation by setuptools):: + + nosetests [options] [(optional) test files or directories] + +You may also use nose in a test script:: + + import nose + nose.main() + +If you don't want the test script to exit with 0 on success and 1 on failure +(like unittest.main), use nose.run() instead:: + + import nose + result = nose.run() + +`result` will be true if the test run succeeded, or false if any test failed +or raised an uncaught exception. Lastly, you can run nose.core directly, which +will run nose.main():: + + python /path/to/nose/core.py + +Please see the usage message for the nosetests script for information +about how to control which tests nose runs, which plugins are loaded, +and the test output. + +Features +-------- + +Run as collect +============== + +nose begins running tests as soon as the first test module is loaded, it +does not wait to collect all tests before running the first. + +Output capture +============== + +Unless called with the -s (--nocapture) switch, nose will capture stdout +during each test run, and print the captured output only for tests that +fail or have errors. The captured output is printed immediately +following the error or failure output for the test. (Note that output in +teardown methods is captured, but can't be output with failing tests, +because teardown has not yet run at the time of the failure.) + +Assert introspection +==================== + +When run with the -d (--detailed-errors) switch, nose will try to output +additional information about the assert expression that failed with each +failing test. Currently, this means that names in the assert expression +will be expanded into any values found for them in the locals or globals +in the frame in which the expression executed. + +In other words if you have a test like:: + + def test_integers(): + a = 2 + assert a == 4, "assert 2 is 4" + +You will get output like:: + + File "/path/to/file.py", line XX, in test_integers: + assert a == 4, "assert 2 is 4" + AssertionError: assert 2 is 4 + >> assert 2 == 4, "assert 2 is 4" + +Setuptools integration +====================== + +nose may be used with the setuptools_ test command. Simply specify +nose.collector as the test suite in your setup file:: + + setup ( + # ... + test_suite = 'nose.collector' + ) + +Then to find and run tests, you can run:: + + python setup.py test + +When running under setuptools, you can configure nose settings via the +environment variables detailed in the nosetests script usage message. + +Please note that when run under the setuptools test command, some plugins will +not be available, including the builtin coverage, profiler, and missed test +plugins. + +nose also includes its own setuptools command, `nosetests`, that provides +support for all plugins and command line options, as well as configuration +using the setup.cfg file. See nose.commands_ for more information about the +`nosetests` command. + +.. _setuptools: http://peak.telecommunity.com/DevCenter/setuptools +.. _nose.commands: #commands + +Writing tests +------------- + +As with py.test, nose tests need not be subclasses of TestCase. Any function +or class that matches the configured testMatch regular expression +('(?:^|[\\b_\\.-])[Tt]est)'' by default) and lives in a module that also +matches that expression will be run as a test. For the sake of compatibility +with legacy unittest test cases, nose will also load tests from +unittest.TestCase subclasses just like unittest does. Like py.test, functional +tests will be run in the order in which they appear in the module +file. TestCase derived tests and other test classes are run in alphabetical +order. + +Fixtures +======== + +nose supports fixtures (setup and teardown methods) at the package, +module, and test level. As with py.test or unittest fixtures, setup always +runs before any test (or collection of tests for test packages and modules); +teardown runs if setup has completed successfully, whether or not the test +or tests pass. For more detail on fixtures at each level, see below. + +Test packages +============= + +nose allows tests to be grouped into test packages. This allows +package-level setup; for instance, if you need to create a test database +or other data fixture for your tests, you may create it in package setup +and remove it in package teardown once per test run, rather than having to +create and tear it down once per test module or test case. + +To create package-level setup and teardown methods, define setup and/or +teardown functions in the __init__.py of a test package. Setup methods may +be named 'setup', 'setup_package', 'setUp',or 'setUpPackage'; teardown may +be named 'teardown', 'teardown_package', 'tearDown' or 'tearDownPackage'. +Execution of tests in a test package begins as soon as the first test +module is loaded from the test package. + +Test modules +============ + +A test module is a python module that matches the testMatch regular +expression. Test modules offer module-level setup and teardown; define the +method 'setup', 'setup_module', 'setUp' or 'setUpModule' for setup, +'teardown', 'teardown_module', or 'tearDownModule' for teardown. Execution +of tests in a test module begins after all tests are collected. + +Test classes +============ + +A test class is a class defined in a test module that is either a subclass +of unittest.TestCase, or matches testMatch. Test classes that don't +descend from unittest.TestCase are run in the same way as those that do: +methods in the class that match testMatch are discovered, and a test case +constructed to run each with a fresh instance of the test class. Like +unittest.TestCase subclasses, other test classes may define setUp and +tearDown methods that will be run before and after each test method. + +Test functions +============== + +Any function in a test module that matches testMatch will be wrapped in a +FunctionTestCase and run as a test. The simplest possible failing test is +therefore:: + + def test(): + assert False + +And the simplest passing test:: + + def test(): + pass + +Test functions may define setup and/or teardown attributes, which will be +run before and after the test function, respectively. A convenient way to +do this, especially when several test functions in the same module need +the same setup, is to use the provided with_setup decorator:: + + def setup_func(): + # ... + + def teardown_func(): + # ... + + @with_setup(setup_func,teardown_func) + def test(): + # ... + +For python 2.3, add the attributes by calling the decorator function like +so:: + + def test(): + # ... + test = with_setup(setup_func,teardown_func)(test) + +or by direct assignment:: + + test.setup = setup_func + test.teardown = teardown_func + +Test generators +=============== + +nose supports test functions and methods that are generators. A simple +example from nose's selftest suite is probably the best explanation:: + + def test_evens(): + for i in range(0, 5): + yield check_even, i, i*3 + + def check_even(n, nn): + assert n % 2 == 0 or nn % 2 == 0 + +This will result in 4 tests. nose will iterate the generator, creating a +function test case wrapper for each tuple it yields. As in the example, test +generators must yield tuples, the first element of which must be a callable +and the remaining elements the arguments to be passed to the callable. + +Setup and teardown functions may be used with test generators. The setup and +teardown attributes must be attached to the generator function:: + + @with_setup(setup_func, teardown_func) + def test_generator(): + ... + yield func, arg, arg ... + +The setup and teardown functions will be executed for each test that the +generator returns. + +For generator methods, the setUp and tearDown methods of the class (if any) +will be run before and after each generated test case. + +Please note that method generators `are not` supported in unittest.TestCase +subclasses. + +About the name +-------------- + +* nose is the least silly short synonym for discover in the dictionary.com + thesaurus that does not contain the word 'spy'. +* Pythons have noses +* The nose knows where to find your tests +* Nose Obviates Suite Employment + +Contact the author +------------------ + +To report bugs, ask questions, or request features, please use the trac +instance provided by the great folks at python hosting, here: +http://nose.python-hosting.com. Or, email the author at +jpellerin+nose at gmail dot com. Patches are welcome! + +Similar test runners +-------------------- + +nose was inspired mainly by py.test_, which is a great test runner, but +formerly was not all that easy to install, and is not based on unittest. + +Test suites written for use with nose should work equally well with py.test, +and vice versa, except for the differences in output capture and command line +arguments for the respective tools. + +.. _py.test: http://codespeak.net/py/current/doc/test.html + +License and copyright +--------------------- + +nose is copyright Jason Pellerin 2005-2006 + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2 of the License, or (at your +option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program; if not, write to the Free Software Foundation, +Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" + +from nose.core import TestCollector, collector, configure, main, run, \ + run_exit, runmodule +from nose.exc import SkipTest, DeprecatedTest +from nose.loader import TestLoader +from nose.suite import LazySuite +from nose.result import TextTestResult +from nose.tools import with_setup # backwards compatibility +from nose.util import file_like, split_test_name, test_address + +__author__ = 'Jason Pellerin' +__versioninfo__ = (0, 10, '0a1') +__version__ = '.'.join(map(str, __versioninfo__)) + +__all__ = [ + 'TextTestResult', 'LazySuite', + 'SkipTest', 'DeprecatedTest', + 'TestCollector', 'TestLoader', + 'collector', 'configure', 'main', 'run', 'run_exit', 'runmodule', + 'with_setup', 'file_like', 'split_test_name', 'test_address' + ] diff --git a/nose/case.py b/nose/case.py new file mode 100644 index 0000000..1657d12 --- /dev/null +++ b/nose/case.py @@ -0,0 +1,147 @@ +"""nose unittest.TestCase subclasses. It is not necessary to subclass these +classes when writing tests; they are used internally by nose.loader.TestLoader +to create test cases from test functions and methods in test classes. +""" +import logging +import unittest +from nose.util import try_run + +log = logging.getLogger(__name__) + +class FunctionTestCase(unittest.TestCase): + """TestCase wrapper for functional tests. + + Don't use this class directly; it is used internally in nose to + create test cases for functional tests. + + This class is very similar to unittest.FunctionTestCase, with a few + extensions: + * The test descriptions are disambiguated by including the full + module path when a test with a similar name has been seen in + the test run. + * It allows setup and teardown functions to be defined as attributes + of the test function. A convenient way to set this up is via the + provided with_setup decorator: + + def setup_func(): + # ... + + def teardown_func(): + # ... + + @with_setup(setup_func, teardown_func) + def test_something(): + # ... + + """ + _seen = {} + + def __init__(self, testFunc, setUp=None, tearDown=None, description=None, + fromDirectory=None): + self.testFunc = testFunc + self.setUpFunc = setUp + self.tearDownFunc = tearDown + self.description = description + self.fromDirectory = fromDirectory + unittest.TestCase.__init__(self) + + def id(self): + return str(self) + + def runTest(self): + self.testFunc() + + def setUp(self): + """Run any setup function attached to the test function + """ + if self.setUpFunc: + self.setUpFunc() + else: + names = ('setup', 'setUp', 'setUpFunc') + try_run(self.testFunc, names) + + def tearDown(self): + """Run any teardown function attached to the test function + """ + if self.tearDownFunc: + self.tearDownFunc() + else: + names = ('teardown', 'tearDown', 'tearDownFunc') + try_run(self.testFunc, names) + + def __str__(self): + if hasattr(self.testFunc, 'compat_func_name'): + name = self.testFunc.compat_func_name + else: + name = self.testFunc.__name__ + name = "%s.%s" % (self.testFunc.__module__, name) + + if self._seen.has_key(name) and self.fromDirectory is not None: + # already seen this exact test name; put the + # module dir in front to disambiguate the tests + name = "%s: %s" % (self.fromDirectory, name) + self._seen[name] = True + return name + __repr__ = __str__ + + def shortDescription(self): + pass # FIXME + + +class MethodTestCase(unittest.TestCase): + """Test case that wraps one method in a test class. + """ + def __init__(self, cls, method, method_desc=None, *arg): + self.cls = cls + self.method = method + self.method_desc = method_desc + self.testInstance = self.cls() + self.testCase = getattr(self.testInstance, method) + self.arg = arg + log.debug('Test case: %s%s', self.testCase, self.arg) + unittest.TestCase.__init__(self) + + def __str__(self): + return self.id() + + def desc(self): + if self.method_desc is not None: + desc = self.method_desc + else: + desc = self.method + if self.arg: + desc = "%s:%s" % (desc, self.arg) + return desc + + def id(self): + return "%s.%s.%s" % (self.cls.__module__, + self.cls.__name__, + self.desc()) + + def setUp(self): + """Run any setup method declared in the test class to which this + method belongs + """ + names = ('setup', 'setUp') + try_run(self.testInstance, names) + + def runTest(self): + self.testCase(*self.arg) + + def tearDown(self): + """Run any teardown method declared in the test class to which + this method belongs + """ + if self.testInstance is not None: + names = ('teardown', 'tearDown') + try_run(self.testInstance, names) + + def shortDescription(self): + # FIXME ... diff output if is TestCase subclass, for back compat + if self.testCase.__doc__ is not None: + return '(%s.%s) "%s"' % (self.cls.__module__, + self.cls.__name__, + self.testCase.__doc__) + return None + + diff --git a/nose/commands.py b/nose/commands.py new file mode 100644 index 0000000..063f147 --- /dev/null +++ b/nose/commands.py @@ -0,0 +1,117 @@ +""" +nosetests setuptools command +---------------------------- + +You can run tests using the `nosetests` setuptools command:: + + python setup.py nosetests + +This command has a few benefits over the standard `test` command: all nose +plugins are supported, and you can configure the test run with both command +line arguments and settings in your setup.cfg file. + +To configure the `nosetests` command, add a [nosetests] section to your +setup.cfg. The [nosetests] section can contain any command line arguments that +nosetests supports. The differences between issuing an option on the command +line and adding it to setup.cfg are: + + * In setup.cfg, the -- prefix must be excluded + * In setup.cfg, command line flags that take no arguments must be given an + argument flag (1, T or TRUE for active, 0, F or FALSE for inactive) + +Here's an example [nosetests] setup.cfg section:: + + [nosetests] + verbosity=1 + detailed-errors + with-coverage=1 + cover-package=nose + debug=nose.loader + pdb=1 + pdb-failures=1 + +If you commonly run nosetests with a large number of options, using the +nosetests setuptools command and configuring with setup.cfg can make running +your tests much less tedious. +""" +import os +from setuptools import Command +from nose.core import get_parser, main + + +parser = get_parser(env={}) + +option_blacklist = ['help', 'verbose'] + +def get_user_options(): + """convert a optparse option list into a distutils option tuple list""" + opt_list = [] + for opt in parser.option_list: + if opt._long_opts[0][2:] in option_blacklist: + continue + + long_name = opt._long_opts[0][2:] + if opt.action != 'store_true': + long_name = long_name + "=" + + short_name = None + if opt._short_opts: + short_name = opt._short_opts[0][1:] + + opt_list.append((long_name, short_name, opt.help or "")) + + return opt_list + + +class nosetests(Command): + description = "Run unit tests using nosetests" + user_options = get_user_options() + + def initialize_options(self): + """create the member variables, but change hyphens to underscores""" + self.option_to_cmds = {} + for opt in parser.option_list: + cmd_name = opt._long_opts[0][2:] + option_name = cmd_name.replace('-', '_') + self.option_to_cmds[option_name] = cmd_name + setattr(self, option_name, None) + self.attr = None + + def finalize_options(self): + """nothing to do here""" + pass + + def run(self): + """ensure tests are capable of being run, then + run nose.main with a reconstructed argument list""" + self.run_command('egg_info') + + # Build extensions in-place + self.reinitialize_command('build_ext', inplace=1) + self.run_command('build_ext') + + if self.distribution.tests_require: + self.distribution.fetch_build_eggs(self.distribution.tests_require) + + argv = [] + for (option_name, cmd_name) in self.option_to_cmds.items(): + if option_name in option_blacklist: + continue + value = getattr(self, option_name) + if value is not None: + if flag(value): + if _bool(value): + argv.append('--' + cmd_name) + else: + argv.append('--' + cmd_name) + argv.append(value) + main(argv=argv, env=os.environ) + +def flag(val): + """Does the value look like an on/off flag?""" + 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') diff --git a/nose/config.py b/nose/config.py new file mode 100644 index 0000000..ade13e9 --- /dev/null +++ b/nose/config.py @@ -0,0 +1,70 @@ +import os +import re +import sys + +class Config(object): + """nose configuration. For internal use only. + """ + + def __init__(self, **kw): + self.testMatch = re.compile(r'(?:^|[\b_\.%s-])[Tt]est' % os.sep) + self.addPaths = True + self.capture = True + self.detailedErrors = False + self.debugErrors = False + self.debugFailures = False + self.exclude = None + self.includeExe = sys.platform=='win32' + self.ignoreFiles = [ re.compile(r'^\.'), + re.compile(r'^_'), + re.compile(r'^setup\.py$') + ] + self.include = None + self.plugins = [] + self.srcDirs = ['lib', 'src'] + self.stopOnError = False + self.tests = [] + self.verbosity = 1 + self._where = None + self._working_dir = None + self.update(kw) + self._orig = self.__dict__.copy() + + def get_where(self): + return self._where + + def set_where(self, val): + self._where = val + self._working_dir = None + + def get_working_dir(self): + val = self._working_dir + if val is None: + if isinstance(self.where, list) or isinstance(self.where, tuple): + val = self._working_dir = self.where[0] + else: + val = self._working_dir = self.where + return val + + def set_working_dir(self, val): + self._working_dir = val + + def __str__(self): + # FIXME -- in alpha order + return repr(self.__dict__) + + def reset(self): + self.__dict__.update(self._orig) + + def todict(self): + return self.__dict__.copy() + + def update(self, d): + self.__dict__.update(d) + + # properties + where = property(get_where, set_where, None, + "The list of directories where tests will be discovered") + working_dir = property(get_working_dir, set_working_dir, None, + "The current working directory (the root " + "directory of the current test run).") diff --git a/nose/core.py b/nose/core.py new file mode 100644 index 0000000..37fcfdd --- /dev/null +++ b/nose/core.py @@ -0,0 +1,481 @@ +"""Implements nose test program and collector. +""" +import logging +import os +import re +import sys +import types +import unittest +from optparse import OptionParser + +from nose.plugins import load_plugins, call_plugins +from nose.result import start_capture, end_capture, TextTestResult +from nose.config import Config +from nose.loader import defaultTestLoader +from nose.proxy import ResultProxySuite +from nose.result import Result +from nose.suite import LazySuite +from nose.util import absdir, tolist +from nose.importer import add_path + +log = logging.getLogger('nose.core') + + +class TestCollector(LazySuite): + """Main nose test collector. + + Uses a test loader to load tests from the directory given in conf + (conf.path). Uses the default test loader from nose.loader by + default. Any other loader may be used so long as it implements + loadTestsFromDir(). + """ + def __init__(self, conf, loader=None): + if loader is None: + loader = defaultTestLoader(conf) + self.conf = conf + self.loader = loader + self.path = conf.where + + def loadtests(self): + for path in tolist(self.path): + for test in self.loader.loadTestsFromDir(path): + yield test + + def __repr__(self): + return "collector in %s" % self.path + __str__ = __repr__ + +defaultTestCollector = TestCollector + + +def collector(): + """TestSuite replacement entry point. Use anywhere you might use a + unittest.TestSuite. Note: Except with testoob; currently (nose 0.9) + testoob's test loading is not compatible with nose's collector + implementation. + + Returns a TestCollector configured to use a TestLoader that returns + ResultProxySuite test suites, which use a proxy result object to + enable output capture and assert introspection. + """ + # plugins that implement any of these methods are disabled, since + # we don't control the test runner and won't be able to run them + setuptools_incompat = ( 'finalize', 'prepareTest', 'report', + 'setOutputStream') + + conf = configure(argv=[], env=os.environ, + disable_plugins=setuptools_incompat) + Result.conf = conf + loader = defaultTestLoader(conf) + loader.suiteClass = ResultProxySuite + return TestCollector(conf, loader) + + +class TextTestRunner(unittest.TextTestRunner): + """Test runner that uses nose's TextTestResult to enable output + capture and assert introspection, as well as providing hooks for + plugins to override or replace the test output stream, results, and + the test case itself. + """ + def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1, + conf=None): + unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity) + self.conf = conf + + def _makeResult(self): + return TextTestResult(self.stream, + self.descriptions, + self.verbosity, + self.conf) + + def run(self, test): + wrapper = call_plugins(self.conf.plugins, 'prepareTest', test) + if wrapper is not None: + test = wrapper + + # plugins can decorate or capture the output stream + wrapped = call_plugins(self.conf.plugins, 'setOutputStream', + self.stream) + if wrapped is not None: + self.stream = wrapped + + result = unittest.TextTestRunner.run(self, test) + call_plugins(self.conf.plugins, 'finalize', result) + return result + + +class TestProgram(unittest.TestProgram): + """usage: %prog [options] [names] + + nose provides an alternate test discovery and running process for + unittest, one that is intended to mimic the behavior of py.test as much as + is reasonably possible without resorting to magic. + + nose collects tests automatically from python source files, + directories and packages found in its working directory (which + defaults to the current working directory). Any python source file, + directory or package that matches the testMatch regular expression + (by default: (?:^|[\\b_\\.-])[Tt]est) will be collected as a test (or + source for collection of tests). In addition, all other packages + found in the working directory are examined for python source files + or directories that match testMatch. Package discovery descends all + the way down the tree, so package.tests and package.sub.tests and + package.sub.sub2.tests will all be collected. + + Within a test directory or package, any python source file matching + testMatch will be examined for test cases. Within a test file, + functions and classes whose names match testMatch and TestCase + subclasses with any name will be loaded and executed as tests. Tests + may use the assert keyword or raise AssertionErrors to indicate test + failure. TestCase subclasses may do the same or use the various + TestCase methods available. + + Tests may raise nose.SkipTest to indicate that they should be + skipped or nose.DeprecatedTest to indicate that they are + deprecated. Skipped and deprecated tests do not count as failures, + but details on them are printed at the end of the test run along + with any failures and errors. + + Selecting Tests + --------------- + + To specify which tests to run, pass test names on the command line: + + %prog only_test_this.py + + Test names specified may be file or module names, and may optionally + indicate the test case to run by separating the module or file name + from the test case name with a colon. Filenames may be relative or + absolute. Examples: + + %prog test.module + %prog another.test:TestCase.test_method + %prog a.test:TestCase + %prog /path/to/test/file.py:test_function + + Note however that specifying a test name will *not* cause nose to run + a test that it does not discover. Test names specified are compared + against tests discovered, and only the requested tests are + run. Setup and teardown methods are run at all stages. That means + that if you run: + + %prog some.tests.test_module:test_function + + And have defined setup or teardown methods in tests and test_module, + those setup methods will run before the test_function test, and + teardown after, just as if you were running all tests. + + You may also change the working directory where nose looks for tests, + use the -w switch: + + %prog -w /path/to/tests + + Further customization of test selection and loading is possible + through the use of plugins. + + Test result output is identical to that of unittest, except for the + additional features (output capture, assert introspection, and any plugins + that control or produce output) detailed in the options below. + """ + verbosity = 1 + + def __init__(self, module=None, defaultTest=defaultTestCollector, + argv=None, testRunner=None, testLoader=None, env=None, + stream=sys.stderr): + self.testRunner = testRunner + self.testCollector = defaultTest + self.testLoader = testLoader + self.stream = stream + self.success = False + self.module = module + + if not callable(self.testCollector): + raise ValueError("TestProgram argument defaultTest must be " + "a callable with the same signature as " + "nose.TestCollector") + + if argv is None: + argv = sys.argv + if env is None: + env = os.environ + self.parseArgs(argv, env) + self.createTests() + self.runTests() + + def parseArgs(self, argv, env): + """Parse argv and env and configure running environment. + """ + self.conf = configure(argv, env) + # append the requested module to the list of tests to run + if self.module: + try: + self.conf.tests.append(self.module.__name__) + except AttributeError: + self.conf.tests.append(str(self.module)) + + def createTests(self): + """Create the tests to run. Default behavior is to discover + tests using TestCollector using nose.loader.TestLoader as the + test loader. + """ + self.test = self.testCollector(self.conf, self.testLoader) + + def runTests(self): + """Run Tests. Returns true on success, false on failure, and sets + self.success to the same value. + """ + if self.testRunner is None: + self.testRunner = TextTestRunner(stream=self.stream, + verbosity=self.conf.verbosity, + conf=self.conf) + result = self.testRunner.run(self.test) + self.success = result.wasSuccessful() + return self.success + +def get_parser(env=None): + parser = OptionParser(TestProgram.__doc__) + parser.add_option("-V","--version",action="store_true", + dest="version",default=False, + help="Output nose version and exit") + parser.add_option("-v", "--verbose", + action="count", dest="verbosity", + default=int(env.get('NOSE_VERBOSE', 1)), + help="Be more verbose. [NOSE_VERBOSE]") + parser.add_option("--verbosity", action="store", dest="verbosity", + type="int", help="Set verbosity; --verbosity=2 is " + "the same as -vv") + parser.add_option("-l", "--debug", action="store", + dest="debug", default=env.get('NOSE_DEBUG'), + help="Activate debug logging for one or more systems. " + "Available debug loggers: nose, nose.importer, " + "nose.inspector, nose.plugins, nose.result and " + "nose.selector. Separate multiple names with a comma.") + parser.add_option("--debug-log", dest="debug_log", action="store", + default=env.get('NOSE_DEBUG_LOG'), + help="Log debug messages to this file " + "(default: sys.stderr)") + parser.add_option("-q", "--quiet", action="store_const", + const=0, dest="verbosity") + parser.add_option("-w", "--where", action="append", dest="where", + help="Look for tests in this directory [NOSE_WHERE]") + parser.add_option("-e", "--exclude", action="append", dest="exclude", + help="Don't run tests that match regular " + "expression [NOSE_EXCLUDE]") + parser.add_option("-i", "--include", action="append", dest="include", + help="Also run tests that match regular " + "expression [NOSE_INCLUDE]") + parser.add_option("-s", "--nocapture", action="store_false", + default=not env.get('NOSE_NOCAPTURE'), dest="capture", + help="Don't capture stdout (any stdout output " + "will be printed immediately) [NOSE_NOCAPTURE]") + parser.add_option("-d", "--detailed-errors", action="store_true", + default=env.get('NOSE_DETAILED_ERRORS'), + dest="detailedErrors", help="Add detail to error" + " output by attempting to evaluate failed" + " asserts [NOSE_DETAILED_ERRORS]") + parser.add_option("--pdb", action="store_true", dest="debugErrors", + default=env.get('NOSE_PDB'), help="Drop into debugger " + "on errors") + parser.add_option("--pdb-failures", action="store_true", + dest="debugFailures", + default=env.get('NOSE_PDB_FAILURES'), + help="Drop into debugger on failures") + parser.add_option("-x", "--stop", action="store_true", dest="stopOnError", + default=env.get('NOSE_STOP'), + help="Stop running tests after the first error or " + "failure") + parser.add_option("-P", "--no-path-adjustment", action="store_false", + dest="addPaths", + default=not env.get('NOSE_NOPATH'), + help="Don't make any changes to sys.path when " + "loading tests [NOSE_NOPATH]") + parser.add_option("--exe", action="store_true", dest="includeExe", + default=env.get('NOSE_INCLUDE_EXE', + sys.platform=='win32'), + help="Look for tests in python modules that are " + "executable. Normal behavior is to exclude executable " + "modules, since they may not be import-safe " + "[NOSE_INCLUDE_EXE]") + parser.add_option("--noexe", action="store_false", dest="includeExe", + help="DO NOT look for tests in python modules that are " + "executable. (The default on the windows platform is to " + "do so.)") + + # add opts from plugins + all_plugins = [] + # when generating the help message, load only builtin plugins + for plugcls in load_plugins(): + plug = plugcls() + try: + plug.add_options(parser, env) + except AttributeError: + pass + + return parser + +def configure(argv=None, env=None, help=False, disable_plugins=None): + """Configure the nose running environment. Execute configure before + collecting tests with nose.TestCollector to enable output capture and + other features. + """ + if argv is None: + argv = sys.argv + if env is None: + env = os.environ + + conf = Config() + parser = get_parser(env=env) + + options, args = parser.parse_args(argv) + if help: + return parser.format_help() + + try: + log.debug('Adding %s to tests to run' % args[1:]) + conf.tests.extend(args[1:]) + except IndexError: + pass + + if options.version: + from nose import __version__ + print "%s version %s" % (os.path.basename(sys.argv[0]), __version__) + sys.exit(0) + + # where is an append action, so it can't have a default value + # in the parser, or that default will always be in the list + if not options.where: + options.where = env.get('NOSE_WHERE', os.getcwd()) + + # include and exclude also + if not options.include: + options.include = env.get('NOSE_INCLUDE', []) + if not options.exclude: + options.exclude = env.get('NOSE_EXCLUDE', []) + + configure_logging(options) + + # hand options to plugins + all_plugins = [plug() for plug in load_plugins()] + for plug in all_plugins: + plug.configure(options, conf) + if plug.enabled and disable_plugins: + for meth in disable_plugins: + if hasattr(plug, meth): + plug.enabled = False + log.warning("Plugin %s disabled: not all methods " + "supported in this environment" % plug.name) + conf.addPaths = options.addPaths + conf.capture = options.capture + conf.detailedErrors = options.detailedErrors + conf.debugErrors = options.debugErrors + conf.debugFailures = options.debugFailures + conf.plugins = [ plug for plug in all_plugins if plug.enabled ] + conf.stopOnError = options.stopOnError + conf.verbosity = options.verbosity + conf.includeExe = options.includeExe + + if options.where is not None: + conf.where = [] + for path in tolist(options.where): + log.debug('Adding %s as nose working directory', path) + abs_path = absdir(path) + if abs_path is None: + raise ValueError("Working directory %s not found, or " + "not a directory" % path) + conf.where.append(abs_path) + log.info("Looking for tests in %s", abs_path) + if conf.addPaths and \ + os.path.exists(os.path.join(abs_path, '__init__.py')): + log.info("Working directory %s is a package; " + "adding to sys.path" % abs_path) + add_path(abs_path) + + if options.include: + conf.include = map(re.compile, tolist(options.include)) + log.info("Including tests matching %s", options.include) + + if options.exclude: + conf.exclude = map(re.compile, tolist(options.exclude)) + log.info("Excluding tests matching %s", options.exclude) + + if conf.capture: + start_capture() + + try: + # give plugins a chance to start + call_plugins(conf.plugins, 'begin') + except: + if conf.capture: + end_capture() + raise + return conf + +def configure_logging(options): + """Configure logging for nose, or optionally other packages. Any logger + name may be set with the debug option, and that logger will be set to + debug level and be assigned the same handler as the nose loggers, unless + it already has a handler. + """ + format = logging.Formatter('%(name)s: %(levelname)s: %(message)s') + if options.debug_log: + handler = logging.FileHandler(options.debug_log) + else: + handler = logging.StreamHandler(sys.stderr) # FIXME + handler.setFormatter(format) + + logger = logging.getLogger('nose') + logger.propagate = 0 + + # only add our default handler if there isn't already one there + # this avoids annoying duplicate log messages. + if not logger.handlers: + logger.addHandler(handler) + + # default level + lvl = logging.WARNING + if options.verbosity >= 5: + lvl = 0 + elif options.verbosity >= 4: + lvl = logging.DEBUG + elif options.verbosity >= 3: + lvl = logging.INFO + logger.setLevel(lvl) + + # individual overrides + if options.debug: + # no blanks + debug_loggers = [ name for name in options.debug.split(',') if name ] + for logger_name in debug_loggers: + l = logging.getLogger(logger_name) + l.setLevel(logging.DEBUG) + if not l.handlers and not logger_name.startswith('nose'): + l.addHandler(handler) + + +def main(*arg, **kw): + """Run and exit with 0 on success or 1 on failure. + """ + return sys.exit(not run(*arg, **kw)) + +# backwards compatibility +run_exit = main + +def run(*arg, **kw): + """Collect and run test, returning success or failure + """ + result = TestProgram(*arg, **kw).success + end_capture() + return result + +def runmodule(name='__main__'): + """Collect and run tests in a single module only. Defaults to running + tests in __main__. + """ + conf = configure() + testLoader = defaultTestLoader(conf) + def collector(conf, loader): + return loader.loadTestsFromModule(name=name) + main(defaultTest=collector, testLoader=testLoader) + +if __name__ == '__main__': + main() diff --git a/nose/exc.py b/nose/exc.py new file mode 100644 index 0000000..b0cd2dd --- /dev/null +++ b/nose/exc.py @@ -0,0 +1,11 @@ +"""Exceptions for marking tests as skipped or deprecated. +""" +class DeprecatedTest(Exception): + """Raise this exception to mark a test as deprecated. + """ + pass + +class SkipTest(Exception): + """Raise this exception to mark a test as skipped. + """ + pass diff --git a/nose/importer.py b/nose/importer.py new file mode 100644 index 0000000..c87f076 --- /dev/null +++ b/nose/importer.py @@ -0,0 +1,131 @@ +"""Implements an importer that looks only in specific path (ignoring +sys.path), and uses a per-path cache in addition to sys.modules. This is +necessary because test modules in different directories frequently have the +same names, which means that the first loaded would mask the rest when using +the builtin importer. +""" +import logging +import os +import sys +from imp import find_module, load_module, acquire_lock, release_lock, \ + load_source as _load_source + +log = logging.getLogger(__name__) +_modules = {} + +def add_path(path): + """Ensure that the path, or the root of the current package (if + path is in a package) is in sys.path. + """ + log.debug('Add path %s' % path) + if not path: + return + parent = os.path.dirname(path) + if (parent + and os.path.exists(os.path.join(path, '__init__.py'))): + add_path(parent) + elif not path in sys.path: + log.debug("insert %s into sys.path", path) + sys.path.insert(0, path) + + +def load_source(name, path, conf): + """Wrap load_source to make sure that the dir of the module (or package) + is in sys.path before the module is loaded. + """ + if conf.addPaths: + add_path(os.path.dirname(path)) + return _load_source(name, path) + +def _import(name, path, conf): + """Import a module *only* from path, ignoring sys.path and + reloading if the version in sys.modules is not the one we want. + """ + log.debug("Import %s from %s (addpaths: %s)", name, path, conf.addPaths) + + # special case for __main__ + if name == '__main__': + return sys.modules[name] + + # make sure we're doing an absolute import + # name, path = make_absolute(name, path) + + if conf.addPaths: + for p in path: + if p is not None: + add_path(p) + + path = [ p for p in path if p is not None ] + cache = _modules.setdefault(':'.join(path), {}) + + # quick exit for fully cached names + if cache.has_key(name): + return cache[name] + + parts = name.split('.') + fqname = '' + mod = parent = fh = None + + for part in parts: + if fqname == '': + fqname = part + else: + fqname = "%s.%s" % (fqname, part) + + if cache.has_key(fqname): + mod = cache[fqname] + else: + try: + acquire_lock() + log.debug("find module part %s (%s) at %s", part, fqname, path) + fh, filename, desc = find_module(part, path) + old = sys.modules.get(fqname) + if old: + # test modules frequently have name overlap; make sure + # we get a fresh copy of anything we are trying to load + # from a new path + if hasattr(old,'__path__'): + old_path = os.path.normpath(old.__path__[0]) + old_ext = None + elif hasattr(old, '__file__'): + old_norm = os.path.normpath(old.__file__) + old_path, old_ext = os.path.splitext(old_norm) + else: + # builtin or other module-like object that + # doesn't have __file__ + old_path, old_ext, old_norm = None, None, None + new_norm = os.path.normpath(filename) + new_path, new_ext = os.path.splitext(new_norm) + if old_path == new_path: + log.debug("module %s already loaded " + "old: %s %s new: %s %s", fqname, old_path, + old_ext, new_path, new_ext) + cache[fqname] = mod = old + continue + else: + del sys.modules[fqname] + log.debug("Loading %s from %s", fqname, filename) + mod = load_module(fqname, fh, filename, desc) + log.debug("%s from %s yields %s", fqname, filename, mod) + cache[fqname] = mod + finally: + if fh: + fh.close() + release_lock() + if parent: + setattr(parent, part, mod) + if hasattr(mod, '__path__'): + path = mod.__path__ + parent = mod + return mod + +def make_absolute(name, path): + """Given a module name and the path at which it is found, back up to find + the parent of the module, popping directories off of the path so long as + they contain __init__.py files. + """ + if not os.path.exists(os.path.join(path, '__init__.py')): + return (name, path) + path, parent = os.path.split(path) + name = "%s.%s" % (parent, path) + return make_absolute(name, path) diff --git a/nose/inspector.py b/nose/inspector.py new file mode 100644 index 0000000..97a0d48 --- /dev/null +++ b/nose/inspector.py @@ -0,0 +1,202 @@ +"""Simple traceback introspection. Used to add additional information to +AssertionErrors in tests, so that failure messages may be more informative. +""" +import exceptions +import inspect +import logging +import re +import sys +import textwrap +import tokenize +import traceback + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +log = logging.getLogger(__name__) + +def inspect_traceback(tb): + """Inspect a traceback and its frame, returning source for the expression + where the exception was raised, with simple variable replacement performed + and the line on which the exception was raised marked with '>>' + """ + log.debug('inspect traceback %s', tb) + + # we only want the innermost frame, where the exception was raised + while tb.tb_next: + tb = tb.tb_next + + frame = tb.tb_frame + lines, exc_line = tbsource(tb) + + # figure out the set of lines to grab. + inspect_lines, mark_line = find_inspectable_lines(lines, exc_line) + src = StringIO(textwrap.dedent(''.join(inspect_lines))) + + # FIXME + # if a token error results, try just doing the one line, + # stripped of any \ it might have + exp = Expander(frame.f_locals, frame.f_globals) + try: + tokenize.tokenize(src.readline, exp) + except tokenize.TokenError: + pass + + padded = [] + if exp.expanded_source: + exp_lines = exp.expanded_source.split('\n') + ep = 0 + for line in exp_lines: + if ep == mark_line: + padded.append('>> ' + line) + else: + padded.append(' ' + line) + ep += 1 + return '\n'.join(padded) + + +def tbsource(tb, context=6): + """Get source from a traceback object. + + A tuple of two things is returned: a list of lines of context from + the source code, and the index of the current line within that list. + The optional second argument specifies the number of lines of context + to return, which are centered around the current line. + + NOTE: + + This is adapted from inspect.py in the python 2.4 standard library, since + a bug in the 2.3 version of inspect prevents it from correctly locating + source lines in a traceback frame. + """ + + lineno = tb.tb_lineno + frame = tb.tb_frame + + if context > 0: + start = lineno - 1 - context//2 + log.debug("lineno: %s start: %s", lineno, start) + + + try: + lines, dummy = inspect.findsource(frame) + except IOError: + lines = index = None + else: + all_lines = lines + start = max(start, 1) + start = max(0, min(start, len(lines) - context)) + lines = lines[start:start+context] + index = lineno - 1 - start + + # python 2.5 compat: if previous line ends in a continuation, + # decrement start by 1 to match 2.4 behavior + if sys.version_info >= (2, 5) and index > 0: + while lines[index-1].strip().endswith('\\'): + start -= 1 + lines = all_lines[start:start+context] + else: + lines = index = None + # log.debug("Inspecting lines '''%s''' around index %s", lines, index) + return (lines, index) + + +def find_inspectable_lines(lines, pos): + """Find lines in home that are inspectable. + + Walk back from the err line up to 3 lines, but don't walk back over + changes in indent level. + + Walk forward up to 3 lines, counting \ separated lines as 1. Don't walk + over changes in indent level (unless part of an extended line) + """ + cnt = re.compile(r'\\[\s\n]*$') + df = re.compile(r':[\s\n]*$') + ind = re.compile(r'^(\s*)') + toinspect = [] + home = lines[pos] + home_indent = ind.match(home).groups()[0] + + before = lines[max(pos-3, 0):pos] + before.reverse() + after = lines[pos+1:min(pos+4, len(lines))] + + for line in before: + if ind.match(line).groups()[0] == home_indent: + toinspect.append(line) + else: + break + toinspect.reverse() + toinspect.append(home) + home_pos = len(toinspect)-1 + continued = cnt.search(home) + for line in after: + if ((continued or ind.match(line).groups()[0] == home_indent) + and not df.search(line)): + toinspect.append(line) + continued = cnt.search(line) + else: + break + return toinspect, home_pos + + +class Expander: + """Simple expression expander. Uses tokenize to find the names and + expands any that can be looked up in the frame. + """ + def __init__(self, locals, globals): + self.locals = locals + self.globals = globals + self.lpos = None + self.expanded_source = '' + + def __call__(self, ttype, tok, start, end, line): + # TODO + # deal with unicode properly + + # TODO + # Dealing with instance members + # always keep the last thing seen + # if the current token is a dot, + # get ready to getattr(lastthing, this thing) on the + # next call. + + if self.lpos is not None and start[1] >= self.lpos: + self.expanded_source += ' ' * (start[1]-self.lpos) + elif start[1] < self.lpos: + # newline, indent correctly + self.expanded_source += ' ' * start[1] + self.lpos = end[1] + + if ttype == tokenize.INDENT: + pass + elif ttype == tokenize.NAME: + # Clean this junk up + try: + val = self.locals[tok] + if callable(val): + val = tok + else: + val = repr(val) + except KeyError: + try: + val = self.globals[tok] + if callable(val): + val = tok + else: + val = repr(val) + + except KeyError: + val = tok + # FIXME... not sure how to handle things like funcs, classes + # FIXME this is broken for some unicode strings + self.expanded_source += val + else: + self.expanded_source += tok + # if this is the end of the line and the line ends with + # \, then tack a \ and newline onto the output + # print line[end[1]:] + if re.match(r'\s+\\\n', line[end[1]:]): + self.expanded_source += ' \\\n' diff --git a/nose/loader.py b/nose/loader.py new file mode 100644 index 0000000..7e8d7c1 --- /dev/null +++ b/nose/loader.py @@ -0,0 +1,370 @@ +"""Discovery-based test loader. +""" +import logging +import os +import sys +import types +import unittest + +from inspect import isfunction, ismethod +from nose.case import * +from nose.config import Config +from nose.importer import add_path, _import +from nose.plugins import call_plugins +from nose.selector import defaultSelector +from nose.suite import ModuleSuite, TestClass, TestDir, \ + GeneratorMethodTestSuite +from nose.util import is_generator, split_test_name, try_run + +log = logging.getLogger(__name__) + +class LoaderException(Exception): + pass + +class TestLoader(unittest.TestLoader): + """Default nose test loader. + + Methods that shadow those in unittest.TestLoader are compatible with the + usage in the base class. Others may be generators or interpret 'module' as + the module prefix of the thing to be loaded, not the module to be + examined, for example. Integrates closely with nose.selector.Selector to + determine what is a test, and classes in nose.suite to defer loading as + long as possible. + """ + suiteClass = ModuleSuite + + def __init__(self, conf=None, selector=None): + if conf is None: + conf = Config() + self.conf = conf + if selector is None: + selector = defaultSelector(conf) + self.selector = selector + self.plugins = self.conf.plugins + + def loadTestsFromDir(self, dirname, module=None, importPath=None): + """Find tests in a directory. + + Each item in the directory is tested against self.selector, wantFile + or wantDirectory as appropriate. Those that are wanted are returned. + """ + log.info("%s load tests in %s [%s]", self, dirname, module) + if dirname is None: + return + if not os.path.isabs(dirname): + raise ValueError("Dir paths must be specified as " + "absolute paths (%s)" % dirname) + self.conf.working_dir = dirname + if importPath is None: + importPath = dirname + + # Ensure that any directory we examine is on sys.path + if self.conf.addPaths: + add_path(dirname) + + # to ensure that lib paths are set up correctly before tests are + # run, examine directories that look like lib or module + # directories first and tests last + def test_last(a, b, m=self.conf.testMatch): + if m.search(a) and not m.search(b): + return 1 + elif m.search(b) and not m.search(a): + return -1 + return cmp(a, b) + + entries = os.listdir(dirname) + entries.sort(test_last) + for item in entries: + tests = None + log.debug("candidate %s in %s", item, dirname) + path = os.path.join(dirname, item) + for test in self.loadTestsFromName(path, + module=module, + importPath=importPath): + yield test + + def loadTestsFromModule(self, module=None, name=None, + package=None, importPath=None): + """Load tests from module at (optional) import path. One of module or + name must be supplied. If name is supplied, the module name (prepended + with package if that is not empty) will be imported. + """ + if module is None: + if name is None: + raise LoaderException("loadTestsFromModule: one of module or " + "name must be supplied") + if package is not None: + modname = "%s.%s" % (package, name) + else: + modname = name + try: + log.debug("Importing %s from %s", modname, importPath) + module = _import(modname, [importPath], self.conf) + except KeyboardInterrupt: + raise + except: + error = sys.exc_info() + return self.suiteClass(tests=[], error=error) + + log.debug("load from module %s (%s)", module, importPath) + tests = [] + if self.selector.wantModuleTests(module): + log.debug("load tests from %s", module.__name__) + for test in self.testsInModule(module, importPath): + tests.append(test) + # give plugins a chance + for plug in self.conf.plugins: + if hasattr(plug, 'loadTestsFromModule'): + log.debug("collect tests in %s with plugin %s", + module.__name__, plug.name) + for test in plug.loadTestsFromModule(module): + tests.append(test) + # recurse into all modules + if hasattr(module, '__path__') and module.__path__: + path = module.__path__[0] + # setting the module prefix means that we're + # loading from our own parent directory, since we're + # loading xxx.yyy, not just yyy, so ask the importer to + # import from self.path (the path we were imported from), + # not path (the path we're at now) + tests.append(TestDir(self.loadTestsFromDir, self.conf, path, + module.__name__, importPath)) + # compat w/unittest + return self.suiteClass(tests, module=module) + + def loadTestsFromName(self, name, module=None, importPath=None): + """Load tests from test name. Name may be a file, directory or + module. Specify module (or module) as name to load from a + particular module. Specify importPath to load + from that path. + """ + # compatibility shim + try: + module = module.__name__ + except AttributeError: + pass + + if importPath is None: + importPath = self.conf.working_dir + + tests = None + path, mod_name, fn = split_test_name(name) + log.debug('test name %s resolves to path %s, module %s, callable %s' + % (name, path, mod_name, fn)) + + if path: + if os.path.isfile(path): + log.debug("%s is a file", path) + if self.selector.wantFile(name, module): + tests = self.loadTestsFromPath(path, + module=module, + importPath=importPath) + elif os.path.isdir(path): + log.debug("%s is a directory", path) + if self.selector.wantDirectory(path): + init = os.path.join(path, '__init__.py') + if os.path.exists(init): + tests = self.loadTestsFromPath(path, + module=module, + importPath=importPath) + else: + # dirs inside of modules don't belong to the + # module, so module and importPath are not passed + tests = self.loadTestsFromDir(path) + else: + # ignore non-file, non-path item + log.warning("%s is neither file nor path", path) + elif mod_name: + # handle module-like names + log.debug("%s is a module name", name) + yield self.loadTestsFromModule(name=name, + package=module, + importPath=importPath) + elif module: + # handle func-like names in a module + raise ValueError("No module or file specified in test name") + if tests: + for test in tests: + yield test + # give plugins a chance + for plug in self.plugins: + if hasattr(plug, 'loadTestsFromName'): + for test in plug.loadTestsFromName(name, module, importPath): + yield test + + def loadTestsFromNames(self, names, module=None): + """Load tests from names. Behavior is compatible with unittest: + if module is specified, all names are translated to be relative + to that module; the tests are appended to conf.tests, and + loadTestsFromModule() is called. Otherwise, the names are + loaded one by one using loadTestsFromName. + """ + def rel(name, mod): + if not name.startswith(':'): + name = ':' + name + return "%s%s" % (mod, name) + + if module: + log.debug("load tests from module %r" % module) + # configure system to load only requested tests from module + if names: + self.conf.tests.extend([ rel(n, module.__name__) + for n in names ]) + try: + mpath = os.path.dirname(module.__path__[0]) + except AttributeError: + mpath = os.path.dirname(module.__file__) + + return self.loadTestsFromModule(module, importPath=mpath) + else: + tests = [] + for name in names: + for test in self.loadTestsFromName(name): + tests.append(test) + return self.suiteClass(tests) + + def loadTestsFromPath(self, path, module=None, importPath=None): + """Load tests from file or directory at path. + """ + head, test = os.path.split(path) + if importPath is None: + importPath = head + + log.debug("path %s is %s in %s", path, test, importPath) + ispymod = True + if os.path.isfile(path): + if path.endswith('.py'): + # trim the extension of python files + test = test[:-3] + else: + ispymod = False + elif not os.path.exists(os.path.join(path, '__init__.py')): + ispymod = False + if ispymod: + yield self.loadTestsFromModule(name=test, package=module, + importPath=importPath) + # give plugins a chance + for plug in self.plugins: + if hasattr(plug, 'loadTestsFromPath'): + for test in plug.loadTestsFromPath(path, module, importPath): + yield test + + def loadTestsFromTestCase(self, cls): + log.debug("collect tests in class %s", cls) + collected = self.testsInTestCase(cls) + if self.sortTestMethodsUsing: + collected.sort(self.sortTestMethodsUsing) + if issubclass(cls, unittest.TestCase): + maketest = cls + else: + maketest = method_test_case(cls) + return map(maketest, collected) + + def testsInModule(self, module, importPath=None): + """Find functions and classes matching testMatch, as well as + classes that descend from unittest.TestCase, return all found + (properly wrapped) as tests. + """ + def cmp_line(a, b): + """Compare functions by their line numbers + """ + try: + a_ln = a.func_code.co_firstlineno + b_ln = b.func_code.co_firstlineno + except AttributeError: + return 0 + return cmp(a_ln, b_ln) + + entries = dir(module) + tests = [] + func_tests = [] + for item in entries: + log.debug("module candidate %s", item) + test = getattr(module, item) + if isinstance(test, (type, types.ClassType)): + log.debug("candidate %s is a class", test) + if self.selector.wantClass(test): + tests.append(TestClass(self.loadTestsFromTestCase, + self.conf, test)) + elif isfunction(test): + log.debug("candidate %s is a function", test) + if not self.selector.wantFunction(test): + continue + # might be a generator + # FIXME LazySuite w/ generate...? + if is_generator(test): + log.debug("test %s is a generator", test) + func_tests.extend(self.generateTests(test)) + else: + # nope, simple functional test + func_tests.append(test) + # run functional tests in the order in which they are defined + func_tests.sort(cmp_line) + tests.extend([ FunctionTestCase(test, fromDirectory=importPath) + for test in func_tests ]) + log.debug("Loaded tests %s from module %s", tests, module.__name__) + return tests + + def testsInTestCase(self, cls): + collected = [] + if cls in (object, type): + return collected + for item in dir(cls): + attr = getattr(cls, item) + log.debug("Check if selector wants %s (%s)", attr, cls) + if ismethod(attr) and self.selector.wantMethod(attr): + collected.append(item) + # base class methods; include those not overridden + for base in cls.__bases__: + basetests = self.testsInTestCase(base) + for test in basetests: + if not test in collected: + collected.append(test) + return collected + + # FIXME this needs to be moved and generalized for methods? + def generateTests(self, test): + """Generate tests from a test function that is a generator. + Returns list of test functions. + """ + cases = [] + for expr in test(): + # build a closure to run the test, and give it a nice name + def run(expr=expr): + expr[0](*expr[1:]) + run.__module__ = test.__module__ + try: + run.__name__ = '%s:%s' % (test.__name__, expr[1:]) + except TypeError: + # can't set func name in python 2.3 + run.compat_func_name = '%s:%s' % (test.__name__, expr[1:]) + pass + setup = ('setup', 'setUp', 'setUpFunc') + teardown = ('teardown', 'tearDown', 'tearDownFunc') + for name in setup: + if hasattr(test, name): + setattr(run, name, getattr(test, name)) + break + for name in teardown: + if hasattr(test, name): + setattr(run, name, getattr(test, name)) + break + cases.append(run) + return cases + +defaultTestLoader = TestLoader + + +def method_test_case(cls): + """Return a method test case factory bound to cls. + """ + def make_test_case(test_name): + """Method test case factory. May return a method test case, or a + generator method test suite, if the test case is a generator. + """ + attr = getattr(cls, test_name) + if is_generator(attr): + return GeneratorMethodTestSuite(cls, test_name) + else: + return MethodTestCase(cls, test_name) + return make_test_case diff --git a/nose/plugins/__init__.py b/nose/plugins/__init__.py new file mode 100644 index 0000000..26e4dd7 --- /dev/null +++ b/nose/plugins/__init__.py @@ -0,0 +1,151 @@ +"""nose plugins + +nose supports setuptools entry point plugins for test collection, +selection, observation and reporting. + +Writing Plugins +--------------- + +Plugin classes should subclass nose.plugins.Plugin. + +Plugins may implement any of the methods described in the class +PluginInterface in nose.plugins.base. Please note that this class is for +documentary purposes only; plugins may not subclass PluginInterface. + +Registering +=========== + +For nose to find a plugin, it must be part of a package that uses +setuptools, and the plugin must be included in the entry points defined +in the setup.py for the package:: + + setup(name='Some plugin', + ... + entry_points = { + 'nose.plugins': [ + 'someplugin = someplugin:SomePlugin' + ] + }, + ... + ) + +Once the package is installed with install or develop, nose will be able +to load the plugin. + +Defining options +================ + +All plugins must implement the methods `add_options(self, parser, env)` +and `configure(self, options, conf)`. Subclasses of nose.plugins.Plugin +that want the standard options should call the superclass methods. + +nose uses optparse.OptionParser from the standard library to parse +arguments. A plugin's add_options() method receives a parser +instance. It's good form for a plugin to use that instance only to add +additional arguments that take only long arguments (--like-this). Most +of nose's built-in arguments get their default value from an environment +variable. This is a good practice because it allows options to be +utilized when run through some other means than the nosetests script. + +A plugin's configure() receives the parsed OptionParser options object, +as well as the current config object. Plugins should configure their +behavior based on the user-selected settings, and may raise exceptions +if the configured behavior is nonsensical. + +Logging +======= + +nose uses the logging classes from the standard library. To enable users +to view debug messages easily, plugins should use logging.getLogger() to +acquire a logger in the 'nose.plugins' namespace. + +Recipes +======= + + * Writing a plugin that monitors or controls test result output + + Implement any or all of addError, addFailure, etc., to monitor test + results. If you also want to monitor output, implement + setOutputStream and keep a reference to the output stream. If you + want to prevent the builtin TextTestResult output, implement + setOutputSteam and return a dummy stream and send your desired output + to the real stream. + + Example: examples/html_plugin/htmlplug.py + + * Writing a plugin that loads tests from files other than python modules + + Implement wantFile and loadTestsFromPath. In wantFile, return True + for files that you want to examine for tests. In loadTestsFromPath, + for those files, return a TestSuite or other iterable containing + TestCases. loadTestsFromPath may also be a generator. + + Example: nose.plugins.doctests + + * Writing a plugin that prints a report + + Implement begin if you need to perform setup before testing + begins. Implement report and output your report to the provided stream. + + Examples: nose.plugins.cover, nose.plugins.profile, nose.plugins.missed + + * Writing a plugin that selects or rejects tests + + Implement any or all want* methods. Return False to reject the test + candidate, True to accept it -- which means that the test candidate + will pass through the rest of the system, so you must be prepared to + load tests from it if tests can't be loaded by the core loader or + another plugin -- and None if you don't care. + + Examples: nose.plugins.attrib, nose.plugins.doctests + +Examples +======== + +See nose.plugins.attrib, nose.plugins.cover, nose.plugins.doctests and +nose.plugins.profile for examples. Further examples may be found the +examples directory in the nose source distribution. +""" +import logging +import pkg_resources +from warnings import warn +from nose.plugins.base import * + +log = logging.getLogger(__name__) + +def call_plugins(plugins, method, *arg, **kw): + """Call all method on plugins in list, that define it, with provided + arguments. The first response that is not None is returned. + """ + for plug in plugins: + func = getattr(plug, method, None) + if func is None: + continue + log.debug("call plugin %s: %s", plug.name, method) + result = func(*arg, **kw) + if result is not None: + return result + return None + +def load_plugins(builtin=True, others=True): + """Load plugins, either builtin, others, or both. + """ + for ep in pkg_resources.iter_entry_points('nose.plugins'): + log.debug("load plugin %s" % ep) + try: + plug = ep.load() + except KeyboardInterrupt: + raise + except Exception, e: + # never want a plugin load to kill the test run + # but we can't log here because the logger is not yet + # configured + warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning) + continue + if plug.__module__.startswith('nose.plugins'): + if builtin: + yield plug + elif others: + yield plug + + diff --git a/nose/plugins/attrib.py b/nose/plugins/attrib.py new file mode 100644 index 0000000..9094f77 --- /dev/null +++ b/nose/plugins/attrib.py @@ -0,0 +1,177 @@ +"""Attribute selector plugin. + +Simple syntax (-a, --attr) examples: + * nosetests -a status=stable + => only test cases with attribute "status" having value "stable" + + * nosetests -a priority=2,status=stable + => both attributes must match + + * nosetests -a priority=2 -a slow + => either attribute must match + + * nosetests -a tags=http + => attribute list "tags" must contain value "http" (see test_foobar() + below for definition) + + * nosetests -a slow + => attribute "slow" must be defined and its value cannot equal to False + (False, [], "", etc...) + + * nosetests -a !slow + => attribute "slow" must NOT be defined or its value must be equal to False + +Eval expression syntax (-A, --eval-attr) examples: + * nosetests -A "not slow" + * nosetests -A "(priority > 5) and not slow" + +""" +import os +import re +import sys +import textwrap + +from nose.plugins.base import Plugin +from nose.util import tolist + +compat_24 = sys.version_info >= (2, 4) + +class ContextHelper: + """Returns default values for dictionary lookups.""" + def __init__(self, obj): + self.obj = obj + + def __getitem__(self, name): + return self.obj.get(name, False) + +class AttributeSelector(Plugin): + """Selects test cases to be run based on their attributes. + """ + + def __init__(self): + Plugin.__init__(self) + self.attribs = [] + + def add_options(self, parser, env=os.environ): + """Add command-line options for this plugin.""" + + parser.add_option("-a", "--attr", + dest="attr", action="append", + default=env.get('NOSE_ATTR'), + help="Run only tests that have attributes " + "specified by ATTR [NOSE_ATTR]") + # disable in < 2.4: eval can't take needed args + if compat_24: + parser.add_option("-A", "--eval-attr", + dest="eval_attr", metavar="EXPR", action="append", + default=env.get('NOSE_EVAL_ATTR'), + help="Run only tests for whose attributes " + "the Python expression EXPR evaluates " + "to True [NOSE_EVAL_ATTR]") + + def configure(self, options, config): + """Configure the plugin and system, based on selected options. + + attr and eval_attr may each be lists. + + self.attribs will be a list of lists of tuples. In that list, each + list is a group of attributes, all of which must match for the rule to + match. + """ + self.attribs = [] + + # handle python eval-expression parameter + if compat_24 and options.eval_attr: + eval_attr = tolist(options.eval_attr) + for attr in eval_attr: + # "<python expression>" + # -> eval(expr) in attribute context must be True + def eval_in_context(expr, attribs): + return eval(expr, None, ContextHelper(attribs)) + self.attribs.append([(attr, eval_in_context)]) + + # attribute requirements are a comma separated list of + # 'key=value' pairs + if options.attr: + std_attr = tolist(options.attr) + for attr in std_attr: + # all attributes within an attribute group must match + attr_group = [] + for attrib in attr.split(","): + # don't die on trailing comma + if not attrib: + continue + items = attrib.split("=", 1) + if len(items) > 1: + # "name=value" + # -> 'str(obj.name) == value' must be True + key, value = items + else: + key = items[0] + if key[0] == "!": + # "!name" + # 'bool(obj.name)' must be False + key = key[1:] + value = False + else: + # "name" + # -> 'bool(obj.name)' must be True + value = True + attr_group.append((key, value)) + self.attribs.append(attr_group) + if self.attribs: + self.enabled = True + + def validateAttrib(self, attribs): + # TODO: is there a need for case-sensitive value comparison? + + # within each group, all must match for the group to match + # if any group matches, then the attribute set as a whole + # has matched + any = False + for group in self.attribs: + match = True + for key, value in group: + obj_value = attribs.get(key) + if callable(value): + if not value(key, attribs): + match = False + break + elif value is True: + # value must exist and be True + if not bool(obj_value): + match = False + break + elif value is False: + # value must not exist or be False + if bool(obj_value): + match = False + break + elif type(obj_value) in (list, tuple): + # value must be found in the list attribute + if not value in [str(x).lower() for x in obj_value]: + match = False + break + else: + # value must match, convert to string and compare + if (value != obj_value + and str(value).lower() != str(obj_value).lower()): + match = False + break + any = any or match + if any: + # not True because we don't want to FORCE the selection of the + # item, only say that it is acceptable + return None + return False + + def wantFunction(self, function): + return self.validateAttrib(function.__dict__) + + def wantMethod(self, method): + # start with class attributes... + cls = method.im_class + attribs = cls.__dict__.copy() + # method attributes override class attributes + attribs.update(method.__dict__) + return self.validateAttrib(attribs) diff --git a/nose/plugins/base.py b/nose/plugins/base.py new file mode 100644 index 0000000..bb6ed94 --- /dev/null +++ b/nose/plugins/base.py @@ -0,0 +1,409 @@ +import os +import re +import textwrap +from nose.util import tolist + +class Plugin(object): + """Base class for nose plugins. It's not *necessary* to subclass this + class to create a plugin; however, all plugins must implement + `add_options(self, parser, env)` and `configure(self, options, + conf)`, and must have the attributes `enabled` and `name`. + + Plugins should not be enabled by default. + + Subclassing Plugin will give your plugin some friendly default + behavior: + + * A --with-$name option will be added to the command line + interface to enable the plugin. The plugin class's docstring + will be used as the help for this option. + * The plugin will not be enabled unless this option is selected by + the user. + """ + enabled = False + enableOpt = None + name = None + + def __init__(self): + if self.name is None: + self.name = self.__class__.__name__.lower() + if self.enableOpt is None: + self.enableOpt = "enable_plugin_%s" % self.name + + def add_options(self, parser, env=os.environ): + """Add command-line options for this plugin. + + The base plugin class adds --with-$name by default, used to enable the + plugin. + """ + env_opt = 'NOSE_WITH_%s' % self.name.upper() + env_opt.replace('-', '_') + parser.add_option("--with-%s" % self.name, + action="store_true", + dest=self.enableOpt, + default=env.get(env_opt), + help="Enable plugin %s: %s [%s]" % + (self.__class__.__name__, self.help(), env_opt)) + + def configure(self, options, conf): + """Configure the plugin and system, based on selected options. + + The base plugin class sets the plugin to enabled if the enable option + for the plugin (self.enableOpt) is true. + """ + self.conf = conf + if hasattr(options, self.enableOpt): + self.enabled = getattr(options, self.enableOpt) + + def help(self): + """Return help for this plugin. This will be output as the help + section of the --with-$name option that enables the plugin. + """ + if self.__class__.__doc__: + # doc sections are often indented; compress the spaces + return textwrap.dedent(self.__class__.__doc__) + return "(no help available)" + + # Compatiblity shim + def tolist(self, val): + from warnings import warn + warn("Plugin.tolist is deprecated. Use nose.util.tolist instead", + DeprecationWarning) + return tolist(val) + +class IPluginInterface(object): + """ + Nose plugin API + --------------- + + While it is recommended that plugins subclass + nose.plugins.Plugin, the only requirements for a plugin are + that it implement the methods `add_options(self, parser, env)` and + `configure(self, options, conf)`, and have the attributes + `enabled` and `name`. + + Plugins may implement any or all of the methods documented + below. Please note that they `must not` subclass PluginInterface; + PluginInterface is a only description of the plugin API. + + When plugins are called, the first plugin that implements a method + and returns a non-None value wins, and plugin processing ends. The + only exceptions to are `loadTestsFromModule`, `loadTestsFromName`, + and `loadTestsFromPath`, which allow multiple plugins to load and + return tests. + + In general, plugin methods correspond directly to the methods of + nose.selector.Selector, nose.loader.TestLoader and + nose.result.TextTestResult are called by those methods when they are + called. In some cases, the plugin hook doesn't neatly match the + method in which it is called; for those, the documentation for the + hook will tell you where in the test process it is called. + + Plugin hooks fall into two broad categories: selecting and loading + tests, and watching and reporting on test results. + + Selecting and loading tests + =========================== + + To alter test selection behavior, implement any necessary want* + methods as outlined below. Keep in mind, though, that when your + plugin returns True from a want* method, you will send the requested + object through the normal test collection process. If the object + represents something from which normal tests can't be collected, you + must also implement a loader method to load the tests. + + Examples: + * The builtin doctests plugin, for python 2.4 only, implements + `wantFile` to enable loading of doctests from files that are not + python modules. It also implements `loadTestsFromModule` to load + doctests from python modules, and `loadTestsFromPath` to load tests + from the non-module files selected by `wantFile`. + * The builtin attrib plugin implements `wantFunction` and + `wantMethod` so that it can reject tests that don't match the + specified attributes. + + Watching or reporting on tests + ============================== + + To record information about tests or other modules imported during + the testing process, output additional reports, or entirely change + test report output, implement any of the methods outlined below that + correspond to TextTestResult methods. + + Examples: + * The builtin cover plugin implements `begin` and `report` to + capture and report code coverage metrics for all or selected modules + loaded during testing. + * The builtin profile plugin implements `begin`, `prepareTest` and + `report` to record and output profiling information. In this + case, the plugin's `prepareTest` method constructs a function that + runs the test through the hotshot profiler's runcall() method. + """ + def __new__(cls, *arg, **kw): + raise TypeError("IPluginInterface class is for documentation only") + + def addDeprecated(self, test): + """Called when a deprecated test is seen. DO NOT return a value + unless you want to stop other plugins from seeing the deprecated + test. + + Parameters: + * test: + the test case + """ + pass + + def addError(self, test, err, capt): + """Called when a test raises an uncaught exception. DO NOT return a + value unless you want to stop other plugins from seeing that the + test has raised an error. + + Parameters: + * test: + the test case + * err: + sys.exc_info() tuple + * capt: + Captured output, if any + """ + pass + + def addFailure(self, test, err, capt, tb_info): + """Called when a test fails. DO NOT return a value unless you + want to stop other plugins from seeing that the test has failed. + + Parameters: + * test: + the test case + * err: + sys.exc_info() tuple + * capt: + Captured output, if any + * tb_info: + Introspected traceback info, if any + """ + pass + + def addSkip(self, test): + """Called when a test is skipped. DO NOT return a value unless + you want to stop other plugins from seeing the skipped test. + + Parameters: + * test: + the test case + """ + pass + + def addSuccess(self, test, capt): + """Called when a test passes. DO NOT return a value unless you + want to stop other plugins from seeing the passing test. + + Parameters: + * test: + the test case + * capt: + Captured output, if any + """ + pass + + def begin(self): + """Called before any tests are collected or run. Use this to + perform any setup needed before testing begins. + """ + pass + + def finalize(self, result): + """Called after all report output, including output from all plugins, + has been sent to the stream. Use this to print final test + results. Return None to allow other plugins to continue + printing, any other value to stop them. + """ + pass + + def loadTestsFromModule(self, module): + """Return iterable of tests in a module. May be a + generator. Each item returned must be a runnable + unittest.TestCase subclass. Return None if your plugin cannot + collect any tests from module. + + Parameters: + * module: + The module object + """ + pass + + def loadTestsFromName(self, name, module=None, importPath=None): + """Return tests in this file or module. Return None if you are not able + to load any tests, or an iterable if you are. May be a + generator. + + Parameters: + * name: + The test name. May be a file or module name plus a test + callable. Use split_test_name to split into parts. + * module: + Module in which the file is found + * importPath: + Path from which file (must be a python module) was found + """ + pass + + def loadTestsFromPath(self, path, module=None, importPath=None): + """Return tests in this file or directory. Return None if you are not + able to load any tests, or an iterable if you are. May be a + generator. + + Parameters: + * path: + The full path to the file or directory. + * module: + Module in which the file/dir is found + * importPath: + Path from which file (must be a python module) was found + """ + pass + + def loadTestsFromTestCase(self, cls): + """Return tests in this test case class. Return None if you are + not able to load any tests, or an iterable if you are. May be a + generator. + + Parameters: + * cls: + The test case class + """ + pass + + def prepareTest(self, test): + """Called before the test is run by the test runner. Please note + the article *the* in the previous sentence: prepareTest is + called *only once*, and is passed the test case or test suite + that the test runner will execute. It is *not* called for each + individual test case. If you return a non-None value, + that return value will be run as the test. Use this hook to wrap + or decorate the test with another function. + + Parameters: + * test: + the test case + """ + pass + + def report(self, stream): + """Called after all error output has been printed. Print your + plugin's report to the provided stream. Return None to allow + other plugins to print reports, any other value to stop them. + + Parameters: + * stream: + stream object; send your output here + """ + pass + + def setOutputStream(self, stream): + """Called before test output begins. To direct test output to a + new stream, return a stream object, which must implement a + write(msg) method. If you only want to note the stream, not + capture or redirect it, then return None. + + Parameters: + * stream: + the original output stream + """ + + def startTest(self, test): + """Called before each test is run. DO NOT return a value unless + you want to stop other plugins from seeing the test start. + + Parameters: + * test: + the test case + """ + pass + + def stopTest(self, test): + """Called after each test is run. DO NOT return a value unless + you want to stop other plugins from seeing that the test has stopped. + + Parameters: + * test: + the test case + """ + pass + + def wantClass(self, cls): + """Return true if you want the main test selector to collect + tests from this class, false if you don't, and None if you don't + care. + + Parameters: + * cls: + The class + """ + pass + + def wantDirectory(self, dirname): + """Return true if you want test collection to descend into this + directory, false if you do not, and None if you don't care. + + Parameters: + * dirname: + Full path to directory + """ + pass + + def wantFile(self, file, package=None): + """Return true if you want to collect tests from this file, + false if you do not and None if you don't care. + + Parameters: + * file: + Full path to file + * package: + Package in which file is found, if any + """ + pass + + def wantFunction(self, function): + """Return true to collect this function as a test, false to + prevent it from being collected, and None if you don't care. + + Parameters: + * function: + The function object + """ + pass + + def wantMethod(self, method): + """Return true to collect this method as a test, false to + prevent it from being collected, and None if you don't care. + + Parameters: + * method: + The method object + """ + pass + + def wantModule(self, module): + """Return true if you want to collection to descend into this + module, false to prevent the collector from descending into the + module, and None if you don't care. + + Parameters: + * module: + The module object + """ + pass + + def wantModuleTests(self, module): + """Return true if you want the standard test loader to load + tests from this module, false if you want to prevent it from + doing so, and None if you don't care. DO NOT return true if your + plugin will be loading the tests itself! + + Parameters: + * module: + The module object + """ + pass + diff --git a/nose/plugins/cover.py b/nose/plugins/cover.py new file mode 100644 index 0000000..92c676b --- /dev/null +++ b/nose/plugins/cover.py @@ -0,0 +1,139 @@ +"""If you have Ned Batchelder's coverage_ module installed, you may activate a +coverage report with the --with-coverage switch or NOSE_WITH_COVERAGE +environment variable. The coverage report will cover any python source module +imported after the start of the test run, excluding modules that match +testMatch. If you want to include those modules too, use the --cover-tests +switch, or set the NOSE_COVER_TESTS environment variable to a true value. To +restrict the coverage report to modules from a particular package or packages, +use the --cover-packages switch or the NOSE_COVER_PACKAGES environment +variable. + +.. _coverage: http://www.nedbatchelder.com/code/modules/coverage.html +""" +import logging +import os +import sys +from nose.plugins.base import Plugin +from nose.util import tolist + +log = logging.getLogger(__name__) + +class Coverage(Plugin): + """ + If you have Ned Batchelder's coverage module installed, you may + activate a coverage report. The coverage report will cover any + python source module imported after the start of the test run, excluding + modules that match testMatch. If you want to include those modules too, + use the --cover-tests switch, or set the NOSE_COVER_TESTS environment + variable to a true value. To restrict the coverage report to modules from + a particular package or packages, use the --cover-packages switch or the + NOSE_COVER_PACKAGES environment variable. + """ + coverTests = False + coverPackages = None + + def add_options(self, parser, env=os.environ): + Plugin.add_options(self, parser, env) + parser.add_option("--cover-package", action="append", + default=env.get('NOSE_COVER_PACKAGE'), + dest="cover_packages", + help="Restrict coverage output to selected packages " + "[NOSE_COVER_PACKAGE]") + parser.add_option("--cover-erase", action="store_true", + default=env.get('NOSE_COVER_ERASE'), + dest="cover_erase", + help="Erase previously collected coverage " + "statistics before run") + parser.add_option("--cover-tests", action="store_true", + dest="cover_tests", + default=env.get('NOSE_COVER_TESTS'), + help="Include test modules in coverage report " + "[NOSE_COVER_TESTS]") + parser.add_option("--cover-inclusive", action="store_true", + dest="cover_inclusive", + default=env.get('NOSE_COVER_INCLUSIVE'), + help="Include all python files under working " + "directory in coverage report. Useful for " + "discovering holes in test coverage if not all " + "files are imported by the test suite. " + "[NOSE_COVER_INCLUSIVE]") + + + def configure(self, options, config): + Plugin.configure(self, options, config) + if self.enabled: + try: + import coverage + except ImportError: + log.error("Coverage not available: " + "unable to import coverage module") + self.enabled = False + return + self.conf = config + self.coverErase = options.cover_erase + self.coverTests = options.cover_tests + self.coverPackages = tolist(options.cover_packages) + self.coverInclusive = options.cover_inclusive + if self.coverPackages: + log.info("Coverage report will include only packages: %s", + self.coverPackages) + + def begin(self): + log.debug("Coverage begin") + import coverage + self.skipModules = sys.modules.keys()[:] + if self.coverErase: + log.debug("Clearing previously collected coverage statistics") + coverage.erase() + coverage.start() + + def report(self, stream): + log.debug("Coverage report") + import coverage + coverage.stop() + modules = [ module + for name, module in sys.modules.items() + if self.wantModuleCoverage(name, module) ] + log.debug("Coverage report will cover modules: %s", modules) + coverage.report(modules, file=stream) + + def wantModuleCoverage(self, name, module): + if not hasattr(module, '__file__'): + log.debug("no coverage of %s: no __file__", name) + return False + root, ext = os.path.splitext(module.__file__) + if not ext in ('.py', '.pyc', '.pyo'): + log.debug("no coverage of %s: not a python file", name) + return False + if self.coverPackages: + for package in self.coverPackages: + if (name.startswith(package) + and (self.coverTests + or not self.conf.testMatch.search(name))): + log.debug("coverage for %s", name) + return True + if name in self.skipModules: + log.debug("no coverage for %s: loaded before coverage start", + name) + return False + if self.conf.testMatch.search(name) and not self.coverTests: + log.debug("no coverage for %s: is a test", name) + return False + # accept any package that passed the previous tests, unless + # coverPackages is on -- in that case, if we wanted this + # module, we would have already returned True + return not self.coverPackages + + def wantFile(self, file, package=None): + """If inclusive coverage enabled, return true for all source files + in wanted packages.""" + if self.coverInclusive: + if file.endswith(".py"): + if package and self.coverPackages: + for want in self.coverPackages: + if package.startswith(want): + return True + else: + return True + return None + diff --git a/nose/plugins/doctests.py b/nose/plugins/doctests.py new file mode 100644 index 0000000..78f32ae --- /dev/null +++ b/nose/plugins/doctests.py @@ -0,0 +1,121 @@ +"""Use the Doctest plugin with --with-doctest or the NOSE_WITH_DOCTEST +environment variable to enable collection and execution of doctests. doctest_ +tests are usually included in the tested package, not grouped into packages or +modules of their own. For this reason, nose will try to detect and run doctest +tests only in the non-test packages it discovers in the working +directory. Doctests may also be placed into files other than python modules, +in which case they can be collected and executed by using the +--doctest-extension switch or NOSE_DOCTEST_EXTENSION environment variable to +indicate which file extension(s) to load. + +doctest tests are run like any other test, with the exception that output +capture does not work, because doctest does its own output capture in the +course of running a test. + +.. _doctest: http://docs.python.org/lib/module-doctest.html +""" +import doctest +import logging +import os +from nose.plugins.base import Plugin +from nose.util import anyp, tolist + +log = logging.getLogger(__name__) + +class Doctest(Plugin): + """ + Activate doctest plugin to find and run doctests in non-test modules. + """ + extension = None + + def add_options(self, parser, env=os.environ): + Plugin.add_options(self, parser, env) + parser.add_option('--doctest-tests', action='store_true', + dest='doctest_tests', + default=env.get('NOSE_DOCTEST_TESTS'), + help="Also look for doctests in test modules " + "[NOSE_DOCTEST_TESTS]") + try: + # 2.4 or better supports loading tests from non-modules + doctest.DocFileSuite + parser.add_option('--doctest-extension', action="append", + dest="doctestExtension", + help="Also look for doctests in files with " + "this extension [NOSE_DOCTEST_EXTENSION]") + # Set the default as a list, if given in env; otherwise + # an additional value set on the command line will cause + # an error. + env_setting = env.get('NOSE_DOCTEST_EXTENSION') + if env_setting is not None: + parser.set_defaults(doctestExtension=tolist(env_setting)) + except AttributeError: + pass + + def configure(self, options, config): + Plugin.configure(self, options, config) + self.doctest_tests = options.doctest_tests + try: + self.extension = tolist(options.doctestExtension) + except AttributeError: + # 2.3, no other-file option + self.extension = None + + def loadTestsFromModule(self, module): + if not self.matches(module.__name__): + log.debug("Doctest doesn't want module %s", module) + return + try: + doctests = doctest.DocTestSuite(module) + except ValueError: + log.debug("No doctests in %s", module) + return + else: + # < 2.4 doctest (and unittest) suites don't have iterators + log.debug("Doctests found in %s", module) + if hasattr(doctests, '__iter__'): + doctest_suite = doctests + else: + doctest_suite = doctests._tests + for test in doctest_suite: + yield test + + def loadTestsFromPath(self, filename, package=None, importPath=None): + if self.extension and anyp(filename.endswith, self.extension): + try: + return doctest.DocFileSuite(filename, module_relative=False) + except AttributeError: + raise Exception("Doctests in files other than .py " + "(python source) not supported in this " + "version of doctest") + else: + # Don't return None, users may iterate over result + return [] + + def matches(self, name): + """Doctest wants only non-test modules in general. + """ + if name == '__init__.py': + return False + # FIXME don't think we need include/exclude checks here? + return ((self.doctest_tests or not self.conf.testMatch.search(name) + or (self.conf.include + and filter(None, + [inc.search(name) + for inc in self.conf.include]))) + and (not self.conf.exclude + or not filter(None, + [exc.search(name) + for exc in self.conf.exclude]))) + + def wantFile(self, file, package=None): + # always want .py files + if file.endswith('.py'): + return True + # also want files that match my extension + if (self.extension + and anyp(file.endswith, self.extension) + and (self.conf.exclude is None + or not self.conf.exclude.search(file))): + return True + return None + diff --git a/nose/plugins/missed.py b/nose/plugins/missed.py new file mode 100644 index 0000000..7cdc236 --- /dev/null +++ b/nose/plugins/missed.py @@ -0,0 +1,51 @@ +from nose.plugins.base import Plugin +from nose.util import split_test_name, test_address + +class MissedTests(Plugin): + """ + Enable to get a warning when tests specified on the command line + are not found during the test run. + """ + name = 'missed-tests' + + def begin(self): + if not self.conf.tests: + self.missed = None + else: + self.missed = self.conf.tests[:] + + def finalize(self, result): + if self.missed: + for missed in self.missed: + result.stream.writeln("WARNING: missed test '%s'" % missed) + + def match(self, test, test_name): + adr_file, adr_mod, adr_tst = test_address(test) + chk_file, chk_mod, chk_tst = split_test_name(test_name) + + if chk_file is not None and not adr_file.startswith(chk_file): + return False + if chk_mod is not None and not adr_mod.startswith(chk_mod): + return False + if chk_tst is not None and chk_tst != adr_tst: + # could be a test like Class.test and a check like Class + if not '.' in chk_tst: + try: + cls, mth = adr_tst.split('.') + except ValueError: + return False + if cls != chk_tst: + return False + else: + return False + return True + + def startTest(self, test): + if not self.missed: + return + found = [] + for name in self.missed: + if self.match(test, name): + found.append(name) + for name in found: + self.missed.remove(name) diff --git a/nose/plugins/prof.py b/nose/plugins/prof.py new file mode 100644 index 0000000..abcc17f --- /dev/null +++ b/nose/plugins/prof.py @@ -0,0 +1,79 @@ +"""Use the profile plugin with --with-profile or NOSE_WITH_PROFILE to +enable profiling using the hotshot profiler. Profiler output can be +controlled with the --profile-sort and --profile-restrict, and the +profiler output file may be changed with --profile-stats-file. + +See the hotshot documentation in the standard library documentation for +more details on the various output options. +""" + +import hotshot, hotshot.stats +import logging +import os +import sys +import tempfile +from nose.plugins.base import Plugin +from nose.util import tolist + +log = logging.getLogger('nose.plugins') + +class Profile(Plugin): + """ + Use this plugin to run tests using the hotshot profiler. + """ + def add_options(self, parser, env=os.environ): + Plugin.add_options(self, parser, env) + parser.add_option('--profile-sort',action='store',dest='profile_sort', + default=env.get('NOSE_PROFILE_SORT','cumulative'), + help="Set sort order for profiler output") + parser.add_option('--profile-stats-file',action='store', + dest='profile_stats_file', + default=env.get('NOSE_PROFILE_STATS_FILE'), + help='Profiler stats file; default is a new ' + 'temp file on each run') + parser.add_option('--profile-restrict',action='append', + dest='profile_restrict', + default=env.get('NOSE_PROFILE_RESTRICT'), + help="Restrict profiler output. See help for " + "pstats.Stats for details") + + def begin(self): + self.prof = hotshot.Profile(self.pfile) + + def configure(self, options, conf): + Plugin.configure(self, options, conf) + self.options = options + self.conf = conf + + if options.profile_stats_file: + self.pfile = options.profile_stats_file + else: + fileno, filename = tempfile.mkstemp() + # close the open handle immediately, hotshot needs to open + # the file itself + os.close(fileno) + self.pfile = filename + self.sort = options.profile_sort + self.restrict = tolist(options.profile_restrict) + + def prepareTest(self, test): + log.debug('preparing test %s' % test) + def run_and_profile(result, prof=self.prof, test=test): + prof.runcall(test, result) + return run_and_profile + + def report(self, stream): + log.debug('printing profiler report') + self.prof.close() + stats = hotshot.stats.load(self.pfile) + stats.sort_stats(self.sort) + try: + tmp = sys.stdout + sys.stdout = stream + 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 diff --git a/nose/proxy.py b/nose/proxy.py new file mode 100644 index 0000000..930c58b --- /dev/null +++ b/nose/proxy.py @@ -0,0 +1,109 @@ +"""Compatibility shim for running under the setuptools test command. The +ResultProxy wraps the actual TestResult passed to a test and implements output +capture and plugin support. TestProxy wraps test cases and in those wrapped +test cases, wraps the TestResult with a ResultProxy. + +To enable this functionality, use ResultProxySuite as the suiteClass in a +TestLoader. +""" +import logging +import unittest +from nose.result import Result, ln +from nose.suite import TestSuite + +log = logging.getLogger(__name__) + +class ResultProxy(Result): + """Result proxy. Performs nose-specific result operations, such as + handling output capture, inspecting assertions and calling plugins, + then delegates to another result handler. + """ + def __init__(self, result): + self.result = result + + def addError(self, test, err): + log.debug('Proxy addError %s %s', test, err) + Result.addError(self, test, err) + + # compose a new error object that includes captured output + if self.capt is not None and len(self.capt): + ec, ev, tb = err + ev = '\n'.join([str(ev) , ln('>> begin captured stdout <<'), + self.capt, ln('>> end captured stdout <<')]) + err = (ec, ev, tb) + self.result.addError(test, err) + + def addFailure(self, test, err): + log.debug('Proxy addFailure %s %s', test, err) + Result.addFailure(self, test, err) + + # compose a new error object that includes captured output + # and assert introspection data + ec, ev, tb = err + if self.tbinfo is not None and len(self.tbinfo): + ev = '\n'.join([str(ev), self.tbinfo]) + if self.capt is not None and len(self.capt): + ev = '\n'.join([str(ev) , ln('>> begin captured stdout <<'), + self.capt, ln('>> end captured stdout <<')]) + err = (ec, ev, tb) + self.result.addFailure(test, err) + + def addSuccess(self, test): + Result.addSuccess(self, test) + self.result.addSuccess(test) + + def startTest(self, test): + Result.startTest(self, test) + self.result.startTest(test) + + def stopTest(self, test): + Result.stopTest(self, test) + self.result.stopTest(test) + + def _get_shouldStop(self): + return self.result.shouldStop + + def _set_shouldStop(self, val): + self.result.shouldStop = val + + shouldStop = property(_get_shouldStop, _set_shouldStop) + + +class ResultProxySuite(TestSuite): + """Test suite that supports output capture, etc, by wrapping each test in + a TestProxy. + """ + def addTest(self, test): + """Add test, first wrapping in TestProxy""" + self._tests.append(TestProxy(test)) + + +class TestProxy(unittest.TestCase): + """Test case that wraps the test result in a ResultProxy. + """ + resultProxy = ResultProxy + + def __init__(self, wrapped_test): + self.wrapped_test = wrapped_test + log.debug('%r.__init__', self) + + def __call__(self, *arg, **kw): + log.debug('%r.__call__', self) + self.run(*arg, **kw) + + def __repr__(self): + return "TestProxy for: %r" % self.wrapped_test + + def __str__(self): + return str(self.wrapped_test) + + def id(self): + return self.wrapped_test.id() + + def run(self, result): + log.debug('TestProxy run test %s in proxy %s for result %s', + self, self.resultProxy, result) + self.wrapped_test(self.resultProxy(result)) + + def shortDescription(self): + return self.wrapped_test.shortDescription() diff --git a/nose/result.py b/nose/result.py new file mode 100644 index 0000000..9e29618 --- /dev/null +++ b/nose/result.py @@ -0,0 +1,250 @@ +"""Test result handlers. Base class (Result) implements plugin handling, +output capture, and assert introspection, and handles deprecated and skipped +tests. TextTestResult is a drop-in replacement for unittest._TextTestResult +that uses the capabilities in Result. +""" +import inspect +import logging +import pdb +import sys +import tokenize +from unittest import _TextTestResult, TestSuite +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from nose.inspector import inspect_traceback +from nose.exc import DeprecatedTest, SkipTest +from nose.plugins import call_plugins + +# buffer = StringIO() +stdout = [] + +log = logging.getLogger('nose.result') + +class Result(object): + """Base class for results handlers. + """ + capt = None + conf = None + tbinfo = None + shouldStop = False + + def addDeprecated(self, test): + self.resetBuffer() + call_plugins(self.conf.plugins, 'addDeprecated', test) + + def addError(self, test, err): + if self.isDeprecated(err): + self.addDeprecated(test) + elif self.isSkip(err): + self.addSkip(test) + else: + self.capt = self.getBuffer() + if self.conf.debugErrors: + if self.conf.capture: + end_capture() + pdb.post_mortem(err[2]) + if self.conf.capture: + start_capture() + self.resetBuffer() + call_plugins(self.conf.plugins, 'addError', + test, err, self.capt) + if self.conf.stopOnError: + self.shouldStop = True + + def addFailure(self, test, err): + self.capt = self.getBuffer() + if self.conf.debugFailures: + if self.conf.capture: + end_capture() + pdb.post_mortem(err[2]) + if self.conf.capture: + start_capture() + if self.conf.detailedErrors: + try: + self.tbinfo = inspect_traceback(err[2]) + except tokenize.TokenError: + self.tbinfo = "ERR: unable to inspect traceback" + else: + self.tbinfo = '' + self.resetBuffer() + call_plugins(self.conf.plugins, 'addFailure', + test, err, self.capt, self.tbinfo) + if self.conf.stopOnError: + self.shouldStop = True + + def addSkip(self, test): + self.resetBuffer() + call_plugins(self.conf.plugins, 'addSkip', test) + + def addSuccess(self, test): + self.capt = self.getBuffer() + self.resetBuffer() + call_plugins(self.conf.plugins, 'addSuccess', test, self.capt) + + def getBuffer(self): + if stdout: + try: + return sys.stdout.getvalue() + except AttributeError: + pass + # capture is probably off + return '' + + def isDeprecated(self, err): + if err[0] is DeprecatedTest or isinstance(err[0], DeprecatedTest): + return True + return False + + def isSkip(self, err): + if err[0] is SkipTest or isinstance(err[0], SkipTest): + return True + return False + + def resetBuffer(self): + if stdout: + sys.stdout.truncate(0) + sys.stdout.seek(0) + + def startTest(self, test): + if self.conf.capture: + self.resetBuffer() + self.capt = None + self.tbinfo = None + call_plugins(self.conf.plugins, 'startTest', test) + + def stopTest(self, test): + if self.conf.capture: + self.resetBuffer() + self.capt = None + self.tbinfo = None + call_plugins(self.conf.plugins, 'stopTest', test) + + +class TextTestResult(Result, _TextTestResult): + """Text test result that extends unittest's default test result with + several optional features: + + - output capture + + Capture stdout while tests are running, and print captured output with + errors and failures. + + - debug on error/fail + + Drop into pdb on error or failure, in the frame where the exception + was raised. + + - deprecated or skipped tests + + raise DeprecatedTest or SkipTest to indicated that a test is + deprecated or has been skipped. Deprecated or skipped tests will be + printed with errors and failures, but don't cause the test run as a + whole to be considered non-successful. + """ + def __init__(self, stream, descriptions, verbosity, conf): + self.deprecated = [] + self.skip = [] + self.conf = conf + self.capture = conf.capture + _TextTestResult.__init__(self, stream, descriptions, verbosity) + + def addDeprecated(self, test): + Result.addDeprecated(self, test) + self.deprecated.append((test, '', '')) + self.writeRes('DEPRECATED','D') + + def addError(self, test, err): + Result.addError(self, test, err) + if not self.isDeprecated(err) and not self.isSkip(err): + self.errors.append((test, + self._exc_info_to_string(err, test), + self.capt)) + self.writeRes('ERROR','E') + + def addFailure(self, test, err): + Result.addFailure(self, test, err) + self.failures.append((test, + self._exc_info_to_string(err, test) + self.tbinfo, + self.capt)) + self.writeRes('FAIL','F') + + def addSkip(self, test): + Result.addSkip(self, test) + self.skip.append((test, '', '')) + self.writeRes('SKIP','S') + + def addSuccess(self, test): + Result.addSuccess(self, test) + self.writeRes('ok', '.') + + def printErrors(self): + log.debug('printErrors called') + _TextTestResult.printErrors(self) + self.printErrorList('DEPRECATED', self.deprecated) + self.printErrorList('SKIPPED', self.skip) + log.debug('calling plugin reports') + call_plugins(self.conf.plugins, 'report', self.stream) + + def printErrorList(self, flavor, errors): + for test, err, capt in errors: + self.stream.writeln(self.separator1) + self.stream.writeln("%s: %s" % (flavor,self.getDescription(test))) + self.stream.writeln(self.separator2) + self.stream.writeln("%s" % err) + if capt is not None and len(capt): + self.stream.writeln(ln('>> begin captured stdout <<')) + self.stream.writeln(capt) + self.stream.writeln(ln('>> end captured stdout <<')) + + def startTest(self, test): + Result.startTest(self, test) + if not isinstance(test, TestSuite): + _TextTestResult.startTest(self, test) + + def stopTest(self, test): + Result.stopTest(self, test) + if not isinstance(test, TestSuite): + _TextTestResult.stopTest(self, test) + + def writeRes(self, long, short): + if self.showAll: + self.stream.writeln(long) + else: + self.stream.write(short) + + def _exc_info_to_string(self, err, test): + try: + return _TextTestResult._exc_info_to_string(self, err, test) + except TypeError: + # 2.3: does not take test arg + return _TextTestResult._exc_info_to_string(self, err) + + +def start_capture(): + """Start capturing output to stdout. DOES NOT reset the buffer. + """ + log.debug('start capture from %r' % sys.stdout) + stdout.append(sys.stdout) + sys.stdout = StringIO() + log.debug('sys.stdout is now %r' % sys.stdout) + +def end_capture(): + """Stop capturing output to stdout. DOES NOT reset the buffer.x + """ + if stdout: + sys.stdout = stdout.pop() + log.debug('capture ended, sys.stdout is now %r' % sys.stdout) + + +def ln(label): + label_len = len(label) + 2 + chunk = (70 - label_len) / 2 + out = '%s %s %s' % ('-' * chunk, label, '-' * chunk) + pad = 70 - len(out) + if pad > 0: + out = out + ('-' * pad) + return out + diff --git a/nose/selector.py b/nose/selector.py new file mode 100644 index 0000000..d24ac43 --- /dev/null +++ b/nose/selector.py @@ -0,0 +1,467 @@ +import logging +import os +import re +import sys +import unittest +from nose.config import Config +from nose.plugins import call_plugins +from nose.util import absfile, file_like, split_test_name, src, test_address + +log = logging.getLogger(__name__) + +class Selector(object): + """Core test selector. Examines test candidates and determines whether, + given the specified configuration, the test candidate should be selected + as a test. + """ + def __init__(self, conf): + self.conf = conf + self.configure(conf) + + def classInTests(self, cls, tests=None): + if tests is None: + return True + return filter(None, + [ t.matches_class(cls) for t in tests]) + + def configure(self, conf): + self.exclude = conf.exclude + self.ignoreFiles = conf.ignoreFiles + self.include = conf.include + self.plugins = conf.plugins + self.match = conf.testMatch + + def fileInTests(self, file, tests=None): + if tests is None: + return True + else: + return filter(None, + [ t.matches_file(file) for t in tests ]) + + def funcInTests(self, func, tests=None): + if tests is None: + return True + return filter(None, + [ t.matches_function(func) for t in tests ]) + + def matches(self, name): + """Does the name match my requirements? + + To match, a name must match conf.testMatch OR conf.include + and it must not match conf.exclude + """ + return ((self.match.search(name) + or (self.include and + filter(None, + [inc.search(name) for inc in self.include]))) + and ((not self.exclude) + or not filter(None, + [exc.search(name) for exc in self.exclude]) + )) + + def methodInTests(self, method, tests=None): + """Determine if a method is listed in the requested tests. To + be consideed a match, the method's class must be in the class + part of the test address, and the function part of the test + address must match the method name or be None. + """ + if tests is None: + return True + return filter(None, + [ t.matches_method(method) for t in tests ]) + + def moduleInTests(self, module, tests=None, either=False): + """Return a function that can tell whether a module is in this + batch of tests. + + FIXME: it would be good to memoize this + """ + if tests is None: + return True + return filter(None, + [ t.matches_module(module, either) for t in tests ]) + + def wantClass(self, cls, tests=None): + """Is the class a wanted test class? + + A class must be a unittest.TestCase subclass, or match test name + requirements. + + If self.tests is defined, the class must match something in + self.tests: + """ + log.debug("Load tests from class %s?", cls) + + wanted = (not cls.__name__.startswith('_') + and (issubclass(cls, unittest.TestCase) + or self.match.search(cls.__name__))) + log.debug("%s is wanted? %s", cls, wanted) + plug_wants = call_plugins(self.plugins, 'wantClass', cls) + if plug_wants is not None: + log.debug("Plugin setting selection of %s to %s", cls, plug_wants) + wanted = plug_wants + return wanted and self.classInTests(cls, tests) + + def wantDirectory(self, dirname, tests=None): + """Is the directory a wanted test directory? + + All package directories match, so long as they do not match exclude. + All other directories must match test requirements. + """ + log.debug("Want directory %s (%s)?", dirname, tests) + + init = os.path.join(dirname, '__init__.py') + tail = os.path.basename(dirname) + if os.path.exists(init): + wanted = (not self.exclude + or not filter(None, + [exc.search(tail) for exc in self.exclude] + )) + else: + wanted = (self.matches(tail) + or (self.conf.srcDirs + and tail in self.conf.srcDirs)) + plug_wants = call_plugins(self.plugins, 'wantDirectory', + dirname) + if plug_wants is not None: + wanted = plug_wants + in_tests = self.fileInTests(dirname, tests) + log.debug("wantDirectory %s wanted %s, in_tests %s", + dirname, wanted, in_tests) + return wanted and in_tests + + def wantFile(self, file, package=None, tests=None): + """Is the file a wanted test file? + + If self.tests is defined, the file must match the file part of a test + address in self.tests. + + The default implementation ignores the package setting, but it is + passed in case plugins need to distinguish package from non-package + files. + """ + + # never, ever load files that match anything in ignore + # (.* _* and *setup*.py by default) + base = os.path.basename(file) + ignore_matches = [ ignore_this for ignore_this in self.ignoreFiles + if ignore_this.search(base) ] + if ignore_matches: + log.debug('%s matches ignoreFiles pattern; skipped', + base) + return False + if not self.conf.includeExe and os.access(file, os.X_OK): + log.info('%s is executable; skipped', file) + return False + in_tests = self.fileInTests(file, tests) + if not in_tests: + return False + dummy, ext = os.path.splitext(base) + pysrc = ext == '.py' + + wanted = pysrc and self.matches(base) + plug_wants = call_plugins(self.plugins, 'wantFile', + file, package) + if plug_wants is not None: + wanted = plug_wants + result = wanted or (pysrc and tests and in_tests) + log.debug("wantFile %s wanted %s pysrc %s in_tests %s", file, + wanted, pysrc, in_tests) + return result + + def wantFunction(self, function, tests=None): + """Is the function a test function? + + If conf.function_only is defined, the function name must match + function_only. Otherwise, the function name must match test + requirements. + """ + try: + funcname = function.__name__ + except AttributeError: + # not a function + return False + in_tests = self.funcInTests(function, tests) + if not in_tests: + return False + wanted = not funcname.startswith('_') and self.matches(funcname) + plug_wants = call_plugins(self.plugins, 'wantFunction', function) + if plug_wants is not None: + wanted = plug_wants + return wanted + + def wantMethod(self, method, tests=None): + """Is the method a test method? + + If conf.function_only is defined, the qualified method name + (class.method) must match function_only. Otherwise, the base method + name must match test requirements. + """ + try: + method_name = method.__name__ + except AttributeError: + # not a method + return False + if method_name.startswith('_'): + # never collect 'private' methods + return False + in_tests = self.methodInTests(method, tests) + if not in_tests: + return False + wanted = self.matches(method_name) + plug_wants = call_plugins(self.plugins, 'wantMethod', method) + if plug_wants is not None: + wanted = plug_wants + return wanted + + def wantModule(self, module, tests=None): + """Is the module a test module? + + The tail of the module name must match test requirements. + + If a module is wanted, it means that the module should be + imported and examined. It does not mean that tests will be + collected from the module; tests are only collected from + modules where wantModuleTests() is true. + """ + in_tests = self.moduleInTests(module, tests, either=True) + if not in_tests: + return False + wanted = self.matches(module.__name__.split('.')[-1]) + plug_wants = call_plugins(self.plugins, 'wantModule', module) + if plug_wants is not None: + wanted = plug_wants + return wanted or (tests and in_tests) + + def wantModuleTests(self, module, tests=None): + """Collect tests from this module? + + The tail of the module name must match test requirements. + + If the modules tests are wanted, they will be collected by the + standard test collector. If your plugin wants to collect tests + from a module in some other way, it MUST NOT return true for + wantModuleTests; that would not allow the plugin to collect + tests, but instead cause the standard collector to collect tests. + """ + in_tests = self.moduleInTests(module, tests) + if not in_tests: + return False + + # unittest compat: always load from __main__ + wanted = (self.matches(module.__name__.split('.')[-1]) + or module.__name__ == '__main__') + plug_wants = call_plugins(self.plugins, 'wantModuleTests', + module) + if plug_wants is not None: + wanted = plug_wants + return wanted or (tests and in_tests) + +defaultSelector = Selector + + +class TestAddress(object): + """A test address represents a user's request to run a particular + test. The user may specify a filename or module (or neither), + and/or a callable (a class, function, or method). The naming + format for test addresses is: + + filename_or_module:callable + + Filenames that are not absolute will be made absolute relative to + the working dir. + + The filename or module part will be considered a module name if it + doesn't look like a file, that is, if it doesn't exist on the file + system and it doesn't contain any directory separators and it + doesn't end in .py. + + Callables may be a class name, function name, method name, or + class.method specification. + """ + def __init__(self, name, working_dir=None): + if working_dir is None: + working_dir = os.getcwd() + self.name = name + self.working_dir = working_dir + self.filename, self.module, self.call = split_test_name(name) + if self.filename is not None: + self.filename = src(self.filename) + if not os.path.isabs(self.filename): + self.filename = os.path.abspath(os.path.join(working_dir, + self.filename)) + + def __str__(self): + return self.name + + def __repr__(self): + return "%s: (%s, %s, %s)" % (self.name, self.filename, + self.module, self.call) + + def matches_class(self, cls): + """Does the class match my call part? + """ + if self.call is None: + return True + try: + clsn, dummy = self.call.split('.') + except (ValueError, AttributeError): + # self.call is not dotted, like: foo or Foo + clsn, dummy = self.call, None + return cls.__name__ == clsn + + def matches_file(self, filename): + """Does the filename match my file part? + """ + log.debug("matches_file? %s == %s", filename, self.filename) + fn = self.filename + if fn is None: + return self.matches_file_as_module(filename) + if fn.endswith('__init__.py'): + dn = os.path.dn(fn) + elif os.path.isdir(fn): + dn = fn + else: + dn = None + if os.path.isdir(filename): + dirname = filename + else: + dirname = None + # filename might be a directory and fn a file in that directory + # if so the directory has to match for us to continue on? + return (fn == filename + or (dn is not None + and (filename.startswith(dn) + and len(filename) > len(dn) + and filename[len(dn)] == os.path.sep) + or (filename == dn)) + or (dirname is not None + and (dirname == fn + or (fn.startswith(dirname) + and len(fn) > len(dirname) + and fn[len(dirname)] == os.path.sep)))) + + def matches_file_as_module(self, filename): + """Match filename vs our module part. Convert our module into + a path fragment, and return True if the filename contains that + path fragment. This method should only be called when self.filename + is None. + """ + log.debug("Match file %s vs module %s", filename, self.module) + mn = self.module + if mn is None: + # No filename or modname part; so we match any module, because + # the only part we have defined is the call, which could be + # in any module + return True + + filename = src(filename) + base, ext = os.path.splitext(filename) + if ext and ext != '.py': + # not a python source file: can't be a module + log.debug("%s is not a python source file (%s)", filename, ext) + return False + + # Turn the module name into a path and compare against + # the filename, with the file extension and working_dir removed + sep = os.path.sep + mpath = os.path.sep.join(mn.split('.')) + base = base[len(self.working_dir):] + log.debug("Match file %s (from module %s) vs %s", mpath, mn, base) + mod_match_re = re.compile(r'(^|%s)%s(%s|$)' % (sep, mpath, sep)) + if mod_match_re.search(base): + # the file is likely to be a subpackage of my module + log.debug('%s is a subpackage of %s', filename, mn) + return True + # Now see if my module might be a subpackage of the file + rev_match_re = re.compile(r'%s(%s|$)' % (base, sep)) + if rev_match_re.match(sep + mpath): + log.debug('%s is a subpackage of %s', mn, filename) + return True + return False + + def matches_function(self, function): + """Does the function match my call part? + """ + if self.call is None: + return True + funcname = getattr(function, 'compat_func_name', function.__name__) + log.debug("Match function name: %s == %s", funcname, self.call) + return funcname == self.call + + def matches_method(self, method): + """Does the method match my call part? + """ + if self.call is None: + return True + mcls = method.im_class.__name__ + mname = getattr(method, 'compat_func_name', method.__name__) + log.debug("Match method %s.%s == %s", mcls, mname, self.call) + try: + cls, func = self.call.split('.') + except (ValueError, AttributeError): + cls, func = self.call, None + return mcls == cls and (mname == func or func is None) + + def matches_module(self, module, either=False): + """Either = either my module can be a child of module or + module can be a child of my module. Without either, the + match is valid only if module is a child of my module. + """ + log.debug("Match module %s == %s?", module.__name__, self.module) + if self.module is None: + if self.filename is None: + # This test only has a callable part, so it could + # match a callable in any module + return True + return self.matches_module_as_file(module) + mname = module.__name__ + result = (subpackage_of(mname, self.module) or + (either and subpackage_of(self.module, mname))) + log.debug("Module %s match %s (either: %s) result %s", + module.__name__, self.module, either, result) + return result + + def matches_module_as_file(self, module): + """Does this module match my filename property? The module name is + adjusted if it has been loaded from a .pyc or .pyo file, with the + extension replaced by .py. + """ + mod_file = src(module.__file__) + log.debug('Trying to matching module file %s as file', mod_file) + return self.matches_file(mod_file) + + +# Helpers +def match_all(*arg, **kw): + return True + + +def subpackage_of(modname, package): + """Is module modname a subpackage of package?""" + # quick negative case + log.debug('subpackage_of(%s,%s)', modname, package) + if not modname.startswith(package): + log.debug('not %s startswith %s' , modname, package) + return False + if len(package) > len(modname): + log.debug('package name longer than mod name') + return False + mod_parts = modname.split('.') + pkg_parts = package.split('.') + try: + for p in pkg_parts: + pp = mod_parts.pop(0) + log.debug('check part %s vs part %s', p, pp) + if p != pp: + return False + except IndexError: + log.debug('package %s more parts than modname %s', package, modname) + return False + return True + + +def test_addr(names, working_dir=None): + if names is None: + return None + return [ TestAddress(name, working_dir) for name in names ] diff --git a/nose/suite.py b/nose/suite.py new file mode 100644 index 0000000..e560a80 --- /dev/null +++ b/nose/suite.py @@ -0,0 +1,264 @@ +"""nose TestSuite subclasses that implement lazy test collection for modules, +classes and directories, and provide suite-level fixtures (setUp/tearDown +methods). +""" +import logging +import os +import sys +import unittest +from nose.case import MethodTestCase +from nose.config import Config +from nose.importer import load_source +from nose.util import try_run + +log = logging.getLogger('nose.suite') + +class StopTest(Exception): + pass + + +class TestCollector(unittest.TestSuite): + """A test suite with setup and teardown methods. + """ + def __init__(self, loader=None, **kw): + super(TestCollector, self).__init__(**kw) + self.loader = loader + self.conf = loader.conf + self._collected = False + + def __nonzero__(self): + self.collectTests() + return bool(self._tests) + + def __len__(self): + self.collectTests() + return len(self._tests) + + def __iter__(self): + self.collectTests() + return iter(self._tests) + + def __call__(self, *arg, **kw): + self.run(*arg, **kw) + + def id(self): + return self.__str__() + + def collectTests(self): + pass + + def run(self, result): + self.startTest(result) + try: + self.collectTests() + if not self: + return + try: + self.setUp() + except KeyboardInterrupt: + raise + except StopTest: + pass + except: + result.addError(self, sys.exc_info()) + return + for test in self: + log.debug("running test %s", test) + if result.shouldStop: + break + test(result) + try: + self.tearDown() + except KeyboardInterrupt: + raise + except StopTest: + pass + except: + result.addError(self, sys.exc_info()) + return result + finally: + self.stopTest(result) + + def setUp(self): + pass + + def shortDescription(self): + return str(self) # FIXME + + def startTest(self): + result.startTest(self) + + def stopTest(self): + result.stopTest(self) + + def tearDown(self): + pass + +# backwards compatibility +TestSuite = TestCollector + + +class ModuleSuite(TestCollector): + """Test Collector that collects tests in a single module or + package. For pakages, tests are collected depth-first. This is to + ensure that a module's setup and teardown fixtures are run only if + the module contains tests. + """ + def __init__(self, modulename=None, filename=None, working_dir=None, + testnames=None, **kw): + self.modulename = modulename + self.filename = filename + self.working_dir = working_dir + self.testnames = testnames + self.module = None + super(ModuleSuite, self).__init__(**kw) + + def __repr__(self): + path = os.path.dirname(self.filename) + while (os.path.exists(os.path.join(path, '__init__.py'))): + path = os.path.dirname(path) + return "test module %s in %s" % (self.modulename, path) + __str__ = __repr__ + + def addTest(self, test): + # depth-first? + if test: + self._tests.append(test) + + def collectTests(self): + # print "Collect Tests %s" % self + if self._collected or self._tests: + return + self._collected = True + self._tests = [] + if self.module is None: + # FIXME + # We know the exact source of each module so why not? + # We still need to add the module's parent dir (up to the top + # if it's a package) to sys.path first, though + self.module = load_source(self.name, self.path, self.conf) + for test in self.loader.loadTestsFromModule(self.module, + self.testnames): + self.addTest(test) + + def setUp(self): + """Run any package or module setup function found. For packages, setup + functions may be named 'setupPackage', 'setup_package', 'setUp', + or 'setup'. For modules, setup functions may be named + 'setupModule', 'setup_module', 'setUp', or 'setup'. The setup + function may optionally accept a single argument; in that case, + the test package or module will be passed to the setup function. + """ + log.debug('TestModule.setUp') + if hasattr(self.module, '__path__'): + names = ['setupPackage', 'setUpPackage', 'setup_package'] + else: + names = ['setupModule', 'setUpModule', 'setup_module'] + names += ['setUp', 'setup'] + try_run(self.module, names) + + def tearDown(self): + """Run any package or module teardown function found. For packages, + teardown functions may be named 'teardownPackage', + 'teardown_package' or 'teardown'. For modules, teardown functions + may be named 'teardownModule', 'teardown_module' or + 'teardown'. The teardown function may optionally accept a single + argument; in that case, the test package or module will be passed + to the teardown function. + + The teardown function will be run only if any package or module + setup function completed successfully. + """ + if hasattr(self.module, '__path__'): + names = ['teardownPackage', 'teardown_package'] + else: + names = ['teardownModule', 'teardown_module'] + names += ['tearDown', 'teardown'] + try_run(self.module, names) + +# backwards compatibility +TestModule = ModuleSuite + + +class LazySuite(TestSuite): + """Generator-based test suite. Pass a callable that returns an iterable of + tests, and a nose.config.Config. + """ + # _exc_info_to_string needs this property + failureException = unittest.TestCase.failureException + + def __init__(self, loadtests, conf=None, **kw): + self._loadtests = loadtests + if conf is None: + conf = Config() + self.conf = conf + + def loadtests(self): + for test in self._loadtests(): + yield test + + # lazy property so subclasses can override loadtests() + _tests = property(lambda self: self.loadtests(), + None, None, + 'Tests in this suite (iter)') + + +class GeneratorMethodTestSuite(LazySuite): + """Test suite for test methods that are generators. + """ + def __init__(self, cls, method): + self.cls = cls + self.method = method + + def loadtests(self): + inst = self.cls() + suite = getattr(inst, self.method) + + for test in suite(): + try: + test_method, arg = (test[0], test[1:]) + except ValueError: + test_method, arg = test[0], tuple() + log.debug('test_method: %s, arg: %s', test_method, arg) + if callable(test_method): + name = test_method.__name__ + else: + name = test_method + yield MethodTestCase(self.cls, name, self.method, *arg) + + +class TestClass(LazySuite): + """Lazy suite that collects tests from a class. + """ + def __init__(self, loadtests, conf, cls): + self.cls = cls + LazySuite.__init__(self, loadtests, conf) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return 'test class %s' % self.cls + + def loadtests(self): + for test in self._loadtests(self.cls): + yield test + + +class TestDir(LazySuite): + """Lazy suite that collects tests from a directory. + """ + def __init__(self, loadtests, conf, path, module=None, importPath=None): + self.path = path + self.module = module + self.importPath = importPath + LazySuite.__init__(self, loadtests, conf) + + def __repr__(self): + return "test directory %s in %s" % (self.path, self.module) + __str__ = __repr__ + + def loadtests(self): + for test in self._loadtests(self.path, self.module, + self.importPath): + yield test diff --git a/nose/tools.py b/nose/tools.py new file mode 100644 index 0000000..079b713 --- /dev/null +++ b/nose/tools.py @@ -0,0 +1,103 @@ +""" +Tools for testing +----------------- + +nose.tools provides a few convenience functions to make writing tests +easier. You don't have to use them; nothing in the rest of nose depends +on any of these methods. +""" +import time + + +class TimeExpired(AssertionError): + pass + +def ok_(expr, msg=None): + """Shorthand for assert. Saves 3 whole characters! + """ + assert expr, msg + +def eq_(a, b, msg=None): + """Shorthand for 'assert a == b, "%r != %r" % (a, b) + """ + assert a == b, msg or "%r != %r" % (a, b) + +def make_decorator(func): + """ + Wraps a test decorator so as to properly replicate metadata + of the decorated function, including nose's additional stuff + (namely, setup and teardown). + """ + def decorate(newfunc): + name = func.__name__ + try: + newfunc.__doc__ = func.__doc__ + newfunc.__module__ = func.__module__ + newfunc.__dict__ = func.__dict__ + newfunc.__name__ = name + except TypeError: + # can't set func name in 2.3 + newfunc.compat_func_name = name + return newfunc + return decorate + +def raises(*exceptions): + """Test must raise one of expected exceptions to pass. Example use:: + + @raises(TypeError, ValueError) + def test_raises_type_error(): + raise TypeError("This test passes") + + @raises(Exception): + def test_that_fails_by_passing(): + pass + """ + valid = ' or '.join([e.__name__ for e in exceptions]) + def decorate(func): + name = func.__name__ + def newfunc(*arg, **kw): + try: + func(*arg, **kw) + except exceptions: + pass + except: + raise + else: + message = "%s() did not raise %s" % (name, valid) + raise AssertionError(message) + newfunc = make_decorator(func)(newfunc) + return newfunc + return decorate + +def timed(limit): + """Test must finish within specified time limit to pass. Example use:: + + @timed(.1) + def test_that_fails(): + time.sleep(.2) + """ + def decorate(func): + def newfunc(*arg, **kw): + start = time.time() + func(*arg, **kw) + end = time.time() + if end - start > limit: + raise TimeExpired("Time limit (%s) exceeded" % limit) + newfunc = make_decorator(func)(newfunc) + return newfunc + return decorate + +def with_setup(setup=None, teardown=None): + """Decorator to add setup and/or teardown methods to a test function + + @with_setup(setup, teardown) + def test_something(): + # ... + """ + def decorate(func, setup=setup, teardown=teardown): + if setup: + func.setup = setup + if teardown: + func.teardown = teardown + return func + return decorate diff --git a/nose/twistedtools.py b/nose/twistedtools.py new file mode 100644 index 0000000..1aa0074 --- /dev/null +++ b/nose/twistedtools.py @@ -0,0 +1,144 @@ +""" +Twisted integration +------------------- + +This module provides a very simple way to integrate your tests with the +Twisted event loop. + +You must import this module *before* importing anything from Twisted itself! + +Example: + from nose.twistedtools import reactor, deferred + + @deferred() + def test_resolve(): + return reactor.resolve("nose.python-hosting.com") + +Or, more realistically: + + @deferred(timeout=5.0) + def test_resolve(): + d = reactor.resolve("nose.python-hosting.com") + def check_ip(ip): + assert ip == "67.15.36.43" + d.addCallback(check_ip) + return d + +""" + +import sys +from Queue import Queue, Empty + +from nose.tools import make_decorator, TimeExpired + +__all__ = [ + 'threaded_reactor', 'reactor', 'deferred', 'TimeExpired', +] + +_twisted_thread = None + +def threaded_reactor(): + """ + Start the Twisted reactor in a separate thread, if not already done. + Returns the reactor. + The thread will automatically be destroyed when all the tests are done. + """ + global _twisted_thread + from twisted.internet import reactor + if not _twisted_thread: + from twisted.python import threadable + from threading import Thread + _twisted_thread = Thread(target=lambda: reactor.run( \ + installSignalHandlers=False)) + _twisted_thread.setDaemon(True) + _twisted_thread.start() + return reactor + +# Export global reactor variable, as Twisted does +reactor = threaded_reactor() + + +def deferred(timeout=None): + """ + By wrapping a test function with this decorator, you can return a + twisted Deferred and the test will wait for the deferred to be triggered. + The whole test function will run inside the Twisted event loop. + + The optional timeout parameter specifies the maximum duration of the test. + The difference with timed() is that timed() will still wait for the test + to end, while deferred() will stop the test when its timeout has expired. + The latter is more desireable when dealing with network tests, because + the result may actually never arrive. + + If the callback is triggered, the test has passed. + If the errback is triggered or the timeout expires, the test has failed. + + Example: + @deferred(timeout=5.0) + def test_resolve(): + return reactor.resolve("nose.python-hosting.com") + + Attention! If you combine this decorator with other decorators (like + "raises"), deferred() must be called *first*! + + In other words, this is good: + @raises(DNSLookupError) + @deferred() + def test_error(): + return reactor.resolve("xxxjhjhj.biz") + + and this is bad: + @deferred() + @raises(DNSLookupError) + def test_error(): + return reactor.resolve("xxxjhjhj.biz") + """ + reactor = threaded_reactor() + + # Check for common syntax mistake + # (otherwise, tests can be silently ignored + # if one writes "@deferred" instead of "@deferred()") + try: + timeout is None or timeout + 0 + except TypeError: + raise TypeError("'timeout' argument must be a number or None") + + def decorate(func): + def wrapper(*args, **kargs): + q = Queue() + def callback(value): + q.put(None) + def errback(failure): + # Retrieve and save full exception info + try: + failure.raiseException() + except: + q.put(sys.exc_info()) + def g(): + try: + d = func(*args, **kargs) + try: + d.addCallbacks(callback, errback) + # Check for a common mistake and display a nice error + # message + except AttributeError: + raise TypeError("you must return a twisted Deferred " + "from your test case!") + # Catch exceptions raised in the test body (from the + # Twisted thread) + except: + q.put(sys.exc_info()) + reactor.callFromThread(g) + try: + error = q.get(timeout=timeout) + except Empty: + raise TimeExpired("timeout expired before end of test (%f s.)" + % timeout) + # Re-raise all exceptions + if error is not None: + exc_type, exc_value, tb = error + raise exc_type, exc_value, tb + wrapper = make_decorator(func)(wrapper) + return wrapper + return decorate + diff --git a/nose/util.py b/nose/util.py new file mode 100644 index 0000000..cda707a --- /dev/null +++ b/nose/util.py @@ -0,0 +1,240 @@ +"""Utility functions and classes used by nose internally. +""" +import inspect +import logging +import os +import re +import sys +import types +import unittest +from compiler.consts import CO_GENERATOR + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from nose.config import Config + +log = logging.getLogger('nose') + +ident_re = re.compile(r'^[A-Za-z_][A-Za-z0-9_.]*$') + +def absdir(path): + """Return absolute, normalized path to directory, if it exists; None + otherwise. + """ + if not os.path.isabs(path): + path = os.path.normpath(os.path.abspath(os.path.join(os.getcwd(), + path))) + if path is None or not os.path.isdir(path): + return None + return path + + +def absfile(path, where=None): + """Return absolute, normalized path to file (optionally in directory + where), or None if the file can't be found either in where or the current + working directory. + """ + orig = path + if where is None: + where = os.getcwd() + if isinstance(where, list) or isinstance(where, tuple): + for maybe_path in where: + maybe_abs = absfile(path, maybe_path) + if maybe_abs is not None: + return maybe_abs + return None + if not os.path.isabs(path): + path = os.path.normpath(os.path.abspath(os.path.join(where, path))) + if path is None or not os.path.exists(path): + if where != os.getcwd(): + # try the cwd instead + path = os.path.normpath(os.path.abspath(os.path.join(os.getcwd(), + orig))) + if path is None or not os.path.exists(path): + return None + if os.path.isdir(path): + # might want an __init__.py from pacakge + init = os.path.join(path,'__init__.py') + if os.path.isfile(init): + return init + elif os.path.isfile(path): + return path + return None + + +def anyp(predicate, iterable): + for item in iterable: + if predicate(item): + return True + return False + + +def file_like(name): + """A name is file-like if it is a path that exists, or it has a + directory part, or it ends in .py, or it isn't a legal python + identifier. + """ + return (os.path.exists(name) + or os.path.dirname(name) + or name.endswith('.py') + or not ident_re.match(os.path.splitext(name)[0])) + + +def is_generator(func): + try: + return func.func_code.co_flags & CO_GENERATOR != 0 + except AttributeError: + return False + + +def split_test_name(test): + """Split a test name into a 3-tuple containing file, module, and callable + names, any of which (but not all) may be blank. + + Test names are in the form: + + file_or_module:callable + + 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: + # only a file or mod part + if file_like(test): + return (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] + 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 file_or_mod: + if file_like(file_or_mod): + return (file_or_mod, None, fn) + else: + return (None, file_or_mod, fn) + else: + return (None, None, fn) + + +def test_address(test): + """Find the test address for a test, which may be a module, filename, + class, method or function. + """ + # type-based polymorphism sucks in general, but I believe is + # appropriate here + t = type(test) + if t == types.ModuleType: + return (os.path.abspath(test.__file__), test.__name__) + if t == types.FunctionType: + m = sys.modules[test.__module__] + return (os.path.abspath(m.__file__), test.__module__, test.__name__) + if t in (type, types.ClassType): + m = sys.modules[test.__module__] + return (os.path.abspath(m.__file__), test.__module__, test.__name__) + if t == types.InstanceType: + return test_address(test.__class__) + if t == types.MethodType: + cls_adr = test_address(test.im_class) + return (cls_adr[0], cls_adr[1], + "%s.%s" % (cls_adr[2], test.__name__)) + # handle unittest.TestCase instances + if isinstance(test, unittest.TestCase): + if hasattr(test, 'testFunc'): + # nose FunctionTestCase + return test_address(test.testFunc) + if hasattr(test, '_FunctionTestCase__testFunc'): + # unittest FunctionTestCase + return test_address(test._FunctionTestCase__testFunc) + if hasattr(test, 'testCase'): + # nose MethodTestCase + return test_address(test.testCase) + # regular unittest.TestCase + cls_adr = test_address(test.__class__) + # 2.5 compat: __testMethodName changed to _testMethodName + try: + method_name = test._TestCase__testMethodName + except AttributeError: + method_name = test._testMethodName + return (cls_adr[0], cls_adr[1], + "%s.%s" % (cls_adr[2], method_name)) + raise TypeError("I don't know what %s is (%s)" % (test, t)) + + +def try_run(obj, names): + """Given a list of possible method names, try to run them with the + provided object. Keep going until something works. Used to run + setup/teardown methods for module, package, and function tests. + """ + for name in names: + func = getattr(obj, name, None) + if func is not None: + if type(obj) == types.ModuleType: + # py.test compatibility + try: + args, varargs, varkw, defaults = inspect.getargspec(func) + except TypeError: + # Not a function. If it's callable, call it anyway + if hasattr(func, '__call__'): + func = func.__call__ + try: + args, varargs, varkw, defaults = \ + inspect.getargspec(func) + args.pop(0) # pop the self off + except TypeError: + raise TypeError("Attribute %s of %r is not a python " + "function. Only functions or callables" + " may be used as fixtures." % + (name, obj)) + if len(args): + log.debug("call fixture %s.%s(%s)", obj, name, obj) + return func(obj) + log.debug("call fixture %s.%s", obj, name) + return func() + + +def src(filename): + """Find the python source file for a .pyc or .pyo file. Returns the + filename provided if it is not a python source file. + """ + if filename is None: + return filename + base, ext = os.path.splitext(filename) + if ext in ('.pyc', '.pyo', '.py'): + return '.'.join((base, 'py')) + return filename + +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]. + """ + if val is None: + return None + try: + # might already be a list + val.extend([]) + return val + except AttributeError: + pass + # might be a string + try: + return re.split(r'\s*,\s*', val) + except TypeError: + # who knows... + return list(val) @@ -0,0 +1,59 @@ +try to make things less stateful + + - conf should be immutable? + - certainly conf.working_dir shouldn't change, or if it does it has to be a + stack + - things that are mutable should be removed from conf and passed separately + +tests and working dir should come out of conf and be passed to loader and +selector + +loader.loadTestsFromNames(names, module=None, working_dir=None) + -> split and absolutize all of the test names + -> give them to the selector (self.selector.tests = names) + -> start walking at working_dir + -> sort dirnames into test-last order + -> yield loadFromName for wanted files + -> ModuleSuite + -> for directories: + - keep descending if wanted and not a package + - remove from list if not wanted + - if a package, yield loadFromName for package + -> ModuleSuite + -> since module has a path, we need to restart the walk + and call loadTestsFromNames with the path end as the working dir + but we want to do that lazily, so we need to bundle up the + needed information into a callable and a LazySuite + +loader.collectTests(working_dir, names=[]): + -> yield each test suite as found + + +suites: + +ModuleSuite +ClassSuite +TestCaseSuite +GeneratorSuite +GeneratorMethodSuite + + +* +proxy suite may need to be mixed in by the collector when running under test +or, suite base class has a testProxy property, which if not None is called to +proxy the test + +* +module isolation plugin will break under depth-first loading. how to restore +it: + +preImport hook + - snapshot sys.modules: this is what to restore AFTER processing of the + test module is complete +postImport hook + - snapshot sys.modules: this is what to restore BEFORE running module tests +startTest + - if isa module, restore postImport sys.modules snapshot +stopTest + - if isa module, restore preImport sys.modules snapshot +
\ No newline at end of file diff --git a/scripts/mkindex.py b/scripts/mkindex.py new file mode 100755 index 0000000..9c55bcd --- /dev/null +++ b/scripts/mkindex.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +from docutils.core import publish_string, publish_parts +import nose +import nose.commands +import nose.tools +import os +import re +import time + +root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +print "Main..." +tpl = open(os.path.join(root, 'index.html.tpl'), 'r').read() + +pat = re.compile(r'^.*(Basic usage)', re.DOTALL) +txt = nose.__doc__.replace(':: python','::') +txt = pat.sub(r'\1', txt) +docs = publish_parts(txt, writer_name='html') +docs.update({'version': nose.__version__, + 'date': time.ctime()}) + +print "Tools..." +tools = publish_parts(nose.tools.__doc__, writer_name='html') +docs['tools'] = tools['body'] + +print "Commands..." +cmds = publish_parts(nose.commands.__doc__, writer_name='html') +docs['commands'] = cmds['body'] + +print "Changelog..." +changes = open(os.path.join(root, 'CHANGELOG'), 'r').read() +changes_html = publish_parts(changes, writer_name='html') +docs['changelog'] = changes_html['body'] + +print "News..." +news = open(os.path.join(root, 'NEWS'), 'r').read() +news_html = publish_parts(news, writer_name='html') +docs['news'] = news_html['body'] + +print "Usage..." +usage_txt = nose.configure(help=True).replace('mkindex.py', 'nosetests') +# FIXME remove example plugin & html output parts +docs['usage'] = '<pre>%s</pre>' % usage_txt + +out = tpl % docs + +index = open(os.path.join(root, 'index.html'), 'w') +index.write(out) +index.close() + +readme = open(os.path.join(root, 'README.txt'), 'w') +readme.write(nose.__doc__) +readme.close() diff --git a/scripts/mkrelease.py b/scripts/mkrelease.py new file mode 100755 index 0000000..be7f4af --- /dev/null +++ b/scripts/mkrelease.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# +# +# create and upload a release +import os +import nose +from commands import getstatusoutput + +success = 0 + +current = os.getcwd() + +here = os.path.dirname(os.path.dirname(__file__)) +svnroot = os.path.abspath(os.path.join(here, '..', '..', 'nose_svn')) +svntrunk = os.path.join(svnroot, 'trunk') + +def runcmd(cmd): + print cmd + (status,output) = getstatusoutput(cmd) + if status != success: + raise Exception(output) + +version = nose.__version__ +versioninfo = nose.__versioninfo__ + +# old: runcmd('bzr branch . ../nose_dev-%s' % version) + +os.chdir(svnroot) +print "cd %s" % svnroot + +branch = 'branches/%s.%s.%s-stable' % (versioninfo[0], + versioninfo[1], versioninfo[2]) +tag = 'tags/%s-release' % version +if os.path.isdir(tag): + raise Exception("Tag path %s already exists. Can't release same version " + "twice!") + +# make branch, if needed +if not os.path.isdir(branch): + # update trunk + os.chdir(svntrunk) + print "cd %s" % svntrunk + runcmd('svn up') + os.chdir(svnroot) + print "cd %s" % svnroot + runcmd('svn copy trunk %s' % branch) + base = 'trunk' +else: + # re-releasing branch + base = branch + os.chdir(branch) + print "cd %s" % branch + runcmd('svn up') + os.chdir(svnroot) + print "cd %s"% svnroot + +# make tag +runcmd('svn copy %s %s' % (base, tag)) + +if os.path.exists(os.path.join(branch, 'setup.cfg')): + os.chdir(branch) + print "cd %s" % branch + runcmd('svn rm setup.cfg --force') # remove dev tag from setup + print "cd %s" % svnroot + os.chdir(svnroot) + +os.chdir(tag) +print "cd %s" % tag +runcmd('svn rm setup.cfg --force') # remove dev tag from setup + +# check in +os.chdir(svnroot) +print "cd %s" % svnroot +runcmd("svn ci -m 'Release branch/tag for %s'" % version) + +# make docs +os.chdir(tag) +print "cd %s" % tag + +runcmd('scripts/mkindex.py') +runcmd('scripts/mkwiki.py') + +# setup sdist +runcmd('python setup.py sdist') + +# upload index.html, new dist version, new branch +# link current to dist version +if os.environ.has_key('NOSE_UPLOAD'): + cmd = ('scp -C dist/nose-%(version)s.tar.gz ' + 'index.html %(upload)s') % {'version':version, + 'upload': os.environ['NOSE_UPLOAD'] } + runcmd(cmd) + +os.chdir(current) diff --git a/scripts/mkwiki.py b/scripts/mkwiki.py new file mode 100755 index 0000000..5716fe0 --- /dev/null +++ b/scripts/mkwiki.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +from docutils.core import publish_string, publish_parts +import base64 +import os +import pudge.browser +import re +import sys +import textwrap +import time +from twill.commands import * +from twill import get_browser +import nose + +div = '\n----\n' + +def section(doc, name): + m = re.search(r'(%s\n%s.*?)\n[^\n-]{3,}\n-{3,}\n' % + (name, '-' * len(name)), doc, re.DOTALL) + if m: + return m.groups()[0] + raise Exception('Section %s not found' % name) + +def wikirst(doc): + # + # module -> page links (will be subbed into each page's string) + # + modlinks = { r'\bnose\.plugins\b': 'WritingPlugins' + + + } + + # not working at all.. + #for k in modlinks: + # doc = re.sub(k, '`' + modlinks[k] + '`:trac', doc) + + doc = '`This page is autogenerated. Please add comments only ' \ + 'beneath the horizontal rule at the bottom of the page. ' \ + 'Changes above that line will be lost when the page is '\ + 'regenerated.`\n\n' + doc + + return '{{{\n#!rst\n%s\n}}}\n' % doc + +def plugin_interface(): + """use pudge browser to generate interface docs + from nose.plugins.base.PluginInterface + """ + b = pudge.browser.Browser(['nose.plugins.base'], None) + m = b.modules()[0] + intf = list([ c for c in m.classes() if c.name == + 'IPluginInterface'])[0] + doc = '{{{\n#!rst\n' + intf.doc() + '\n}}}\n' + methods = [ m for m in intf.routines() if not m.name.startswith('_') ] + methods.sort(lambda a, b: cmp(a.name, b.name)) + doc = doc + '{{{\n#!html\n' + for m in methods: + doc = doc + '<b>' + m.name + m.formatargs() + '</b><br />' + doc = doc + m.doc(html=1) + doc = doc + '\n}}}\n' + return doc + +def example_plugin(): + # FIXME dump whole example plugin code from setup.py and plug.py + # into python source sections + root = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..')) + exp = os.path.join(root, 'examples', 'plugin') + setup = file(os.path.join(exp, 'setup.py'), 'r').read() + plug = file(os.path.join(exp, 'plug.py'), 'r').read() + + wik = "'''%s:'''\n{{{\n#!python\n%s\n}}}\n" + return wik % ('setup.py', setup) + wik % ('plug.py', plug) + +def mkwiki(url, realm, user, passwd): + # + # Pages to publish and the docstring(s) to load for that page + # + + pages = { #'SandBox': wikirst(section(nose.__doc__, 'Writing tests')) + 'WritingTests': wikirst(section(nose.__doc__, 'Writing tests')), + 'NoseFeatures': wikirst(section(nose.__doc__, 'Features')), + 'WritingPlugins': wikirst(nose.plugins.__doc__), + 'PluginInterface': plugin_interface(), + # FIXME finish example plugin doc... add some explanation + 'ExamplePlugin': example_plugin(), + + 'NosetestsUsage': '\n{{{\n' + + nose.configure(help=True).replace('mkwiki.py', 'nosetests') + + '\n}}}\n' + } + + w = TracWiki(url, realm, user, passwd) + + for page, doc in pages.items(): + print "====== %s ======" % page + w.update_docs(page, doc) + print "====== %s ======" % page + +class TracWiki(object): + doc_re = re.compile(r'(.*?)' + div, re.DOTALL) + + def __init__(self, url, realm, user, passwd): + self.url = url + self.b = get_browser() + go(url) + add_auth(realm, url, user, passwd) + go('/login') + + def get_page(self, page): + go('/wiki/%s?edit=yes' % page) + self.edit = self.b.get_form('edit') + return self.edit.get_value('text') + + def set_docs(self, page_src, docs): + wikified = docs + div + if self.doc_re.search(page_src): + print "! Updating doc section" + new_src = self.doc_re.sub(wikified, page_src, 1) + else: + print "! Adding new doc section" + new_src = wikified + page_src + if new_src == page_src: + print "! No changes" + return + fv(re.compile('edit'), 'text', new_src) + submit('save') + + def update_docs(self, page, doc): + current = self.get_page(page) + self.set_docs(current, doc) + + +def main(): + + try: + url = sys.argv[1] + except IndexError: + url = 'https://nose.python-hosting.com' + realm = os.environ.get('NOSE_WIKI_REALM') + user = os.environ.get('NOSE_WIKI_USER') + passwd = os.environ.get('NOSE_WIKI_PASSWD') + + mkwiki(url, realm, user, passwd) + +if __name__ == '__main__': + main() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c751aae --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[egg_info] +tag_build = .dev +tag_svn_revision = 1 + +[nosetests] +verbosity=2 +detailed-errors=1 +with-coverage=1 +cover-package=nose +pdb=1 +pdb-failures=1 +stop=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1cc3dbc --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +import sys +import ez_setup +ez_setup.use_setuptools() + +from setuptools import setup, find_packages +from nose import __version__ as VERSION + +setup( + name = 'nose', + version = VERSION, + author = 'Jason Pellerin', + author_email = 'jpellerin+nose@gmail.com', + description = ('A unittest extension offering automatic test suite ' + 'discovery, simplified test authoring, and output capture'), + long_description = ('nose provides an alternate test discovery and ' + 'running process for unittest, one that is intended ' + 'to mimic the behavior of py.test as much as is ' + 'reasonably possible without resorting to magic. ' + 'By default, nose will run tests in files or ' + 'directories under the current working directory ' + 'whose names include "test". nose also supports ' + 'doctest tests and may optionally provide a ' + 'test coverage report.\n\n' + 'If you have recently reported a bug marked as fixed, ' + 'or have a craving for the very latest, you may want ' + 'the development version instead: ' + 'http://svn.nose.python-hosting.com/trunk#egg=nose-dev' + ), + license = 'GNU LGPL', + keywords = 'test unittest doctest automatic discovery', + url = 'http://somethingaboutorange.com/mrl/projects/nose/', + download_url = \ + 'http://somethingaboutorange.com/mrl/projects/nose/nose-%s.tar.gz' \ + % VERSION, + package_data = { '': [ '*.txt' ] }, + packages = find_packages(), + entry_points = { + 'console_scripts': [ + 'nosetests = nose:run_exit' + ], + 'nose.plugins': [ + 'coverage = nose.plugins.cover:Coverage', + 'doctest = nose.plugins.doctests:Doctest', + 'profile = nose.plugins.prof:Profile', + 'attrib = nose.plugins.attrib:AttributeSelector', + 'missed = nose.plugins.missed:MissedTests' + ], + 'distutils.commands': [ + ' nosetests = nose.commands:nosetests' + ], + }, + test_suite = 'nose.collector', + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Testing' + ] + ) diff --git a/unit_tests/helpers.py b/unit_tests/helpers.py new file mode 100644 index 0000000..0a5d68d --- /dev/null +++ b/unit_tests/helpers.py @@ -0,0 +1,6 @@ +def iter_compat(suite): + try: + suite.__iter__ + return suite + except AttributeError: + return suite._tests diff --git a/unit_tests/mock.py b/unit_tests/mock.py new file mode 100644 index 0000000..745ed14 --- /dev/null +++ b/unit_tests/mock.py @@ -0,0 +1,53 @@ +"""Useful mock objects. +""" + +class Bucket(object): + def __init__(self, **kw): + self.__dict__['d'] = {} + self.__dict__['d'].update(kw) + + def __getattr__(self, attr): + if not self.__dict__.has_key('d'): + return None + return self.__dict__['d'].get(attr) + + def __setattr__(self, attr, val): + self.d[attr] = val + + +class MockOptParser(object): + def __init__(self): + self.opts = [] + def add_option(self, *args, **kw): + self.opts.append((args, kw)) + + +class Mod(object): + def __init__(self, name, **kw): + self.__name__ = name + if 'file' in kw: + self.__file__ = kw.pop('file') + else: + if 'path' in kw: + path = kw.pop('path') + else: + path = '' + self.__file__ = "%s/%s.pyc" % (path, name.replace('.', '/')) + self.__path__ = [ self.__file__ ] # FIXME? + self.__dict__.update(kw) + + +class Result(object): + def __init__(self): + from nose.result import Result + import types + self.errors = [] + for attr in dir(Result): + if type(getattr(Result, attr)) is types.MethodType: + if not hasattr(self, attr): + setattr(self, attr, lambda s, *a, **kw: None) + elif not attr.startswith('__'): + setattr(self, attr, None) + + def addError(self, test, err): + self.errors.append(err) diff --git a/unit_tests/support/foo/__init__.py b/unit_tests/support/foo/__init__.py new file mode 100644 index 0000000..66e0a5e --- /dev/null +++ b/unit_tests/support/foo/__init__.py @@ -0,0 +1,7 @@ +boodle = True + +def somefunc(): + """This is a doctest in somefunc. + >>> 'a' + 'a' + """ diff --git a/unit_tests/support/foo/bar/__init__.py b/unit_tests/support/foo/bar/__init__.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/unit_tests/support/foo/bar/__init__.py @@ -0,0 +1 @@ +pass diff --git a/unit_tests/support/foo/bar/buz.py b/unit_tests/support/foo/bar/buz.py new file mode 100644 index 0000000..48c886d --- /dev/null +++ b/unit_tests/support/foo/bar/buz.py @@ -0,0 +1,8 @@ +from foo import boodle + +def afunc(): + """This is a doctest + >>> 2 + 3 + 5 + """ + pass diff --git a/unit_tests/support/foo/doctests.txt b/unit_tests/support/foo/doctests.txt new file mode 100644 index 0000000..e4b8d5b --- /dev/null +++ b/unit_tests/support/foo/doctests.txt @@ -0,0 +1,7 @@ +Doctests in a text file. + + >>> 1 + 2 + 3 + + >>> ['a', 'b'] + ['c'] + ['a', 'b', 'c'] diff --git a/unit_tests/support/foo/test_foo.py b/unit_tests/support/foo/test_foo.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/unit_tests/support/foo/test_foo.py @@ -0,0 +1 @@ +pass diff --git a/unit_tests/support/foo/tests/dir_test_file.py b/unit_tests/support/foo/tests/dir_test_file.py new file mode 100644 index 0000000..79b86ec --- /dev/null +++ b/unit_tests/support/foo/tests/dir_test_file.py @@ -0,0 +1,3 @@ +# test file in test dir in a package +def test_foo(): + pass diff --git a/unit_tests/support/other/file.txt b/unit_tests/support/other/file.txt new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/unit_tests/support/other/file.txt @@ -0,0 +1 @@ +# diff --git a/unit_tests/support/pkgorg/lib/modernity.py b/unit_tests/support/pkgorg/lib/modernity.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/unit_tests/support/pkgorg/lib/modernity.py @@ -0,0 +1 @@ +pass diff --git a/unit_tests/support/pkgorg/tests/test_mod.py b/unit_tests/support/pkgorg/tests/test_mod.py new file mode 100644 index 0000000..2516258 --- /dev/null +++ b/unit_tests/support/pkgorg/tests/test_mod.py @@ -0,0 +1,4 @@ +import modernity + +def test(): + pass diff --git a/unit_tests/support/script.py b/unit_tests/support/script.py new file mode 100755 index 0000000..9e33d77 --- /dev/null +++ b/unit_tests/support/script.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +print "FAIL" diff --git a/unit_tests/support/test-dir/test.py b/unit_tests/support/test-dir/test.py new file mode 100644 index 0000000..2ae2839 --- /dev/null +++ b/unit_tests/support/test-dir/test.py @@ -0,0 +1 @@ +pass diff --git a/unit_tests/support/test.py b/unit_tests/support/test.py new file mode 100644 index 0000000..9ad04e0 --- /dev/null +++ b/unit_tests/support/test.py @@ -0,0 +1,13 @@ +import unittest + +class Something(unittest.TestCase): + def test_something(self): + pass + +class TestTwo: + + def __repr__(self): + return 'TestTwo' + + def test_whatever(self): + pass diff --git a/unit_tests/test_cases.py b/unit_tests/test_cases.py new file mode 100644 index 0000000..101e342 --- /dev/null +++ b/unit_tests/test_cases.py @@ -0,0 +1,54 @@ +import unittest +import pdb +import sys +import nose.case + +class TestNoseCases(unittest.TestCase): + + def test_function_test_case(self): + res = unittest.TestResult() + + a = [] + def func(a=a): + a.append(1) + + case = nose.case.FunctionTestCase(func) + case(res) + assert a[0] == 1 + + def test_method_test_case(self): + res = unittest.TestResult() + + a = [] + class TestClass(object): + def test_func(self, a=a): + a.append(1) + + case = nose.case.MethodTestCase(TestClass, 'test_func') + case(res) + assert a[0] == 1 + + def test_function_test_case_fixtures(self): + from nose.tools import with_setup + res = unittest.TestResult() + + called = {} + + def st(): + called['st'] = True + def td(): + called['td'] = True + + def func_exc(): + called['func'] = True + raise TypeError("An exception") + + func_exc = with_setup(st, td)(func_exc) + case = nose.case.FunctionTestCase(func_exc) + case(res) + assert 'st' in called + assert 'func' in called + assert 'td' in called + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_collector.py b/unit_tests/test_collector.py new file mode 100644 index 0000000..2059161 --- /dev/null +++ b/unit_tests/test_collector.py @@ -0,0 +1,80 @@ +import copy +import os +import sys +import unittest +import nose +from nose.config import Config +from nose.result import TextTestResult +from helpers import iter_compat + +class TestNoseCollector(unittest.TestCase): + + def setUp(self): + self.p = sys.path[:] + + def tearDown(self): + sys.path = self.p[:] + + def test_basic_collection(self): + # self.cfg.verbosity = 7 + c = Config() + c.where = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'support')) + tc = nose.TestCollector(c) + expect = [ 'test module foo in %s' % c.where, + 'test module test in %s/test-dir' % c.where, + 'test module test in %s' % c.where ] + found = [] + + for test in iter_compat(tc): + found.append(str(test)) + self.assertEqual(found, expect) + + def test_deep_collection(self): + # self.cfg.verbosity = 4 + c = Config() + c.where = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'support')) + tc = nose.TestCollector(c) + + buf = [] + class dummy: + pass + stream = dummy() + stream.writeln = lambda v='':buf.append(v + '\n') + rr = TextTestResult(stream, [], 1, c) + + expect = [ 'test module foo in %s' % c.where, + 'test directory %s/foo in foo' % c.where, + 'test module foo.bar in %s' % c.where, + 'test module foo.test_foo in %s' % c.where, + 'test module dir_test_file in %s/foo/tests' % c.where, + 'test module test in %s/test-dir' % c.where, + 'test module test in %s' % c.where, + "test class <class 'test.Something'>", + 'test_something (test.Something)', + 'test class test.TestTwo', + 'test.TestTwo.test_whatever' ] + found = [] + + for test in iter_compat(tc): + print test + found.append(str(test)) + test.setUp() + for t in iter_compat(test): + print ' ', t + #test(rr) + found.append(str(t)) + try: + for tt in iter_compat(t): + print ' ', tt + found.append(str(tt)) + except AttributeError: + pass + self.assertEqual(found, expect) + +if __name__ == '__main__': + #import logging + #logging.basicConfig() + #logging.getLogger('').setLevel(0) + unittest.main() diff --git a/unit_tests/test_config.py b/unit_tests/test_config.py new file mode 100644 index 0000000..38a9525 --- /dev/null +++ b/unit_tests/test_config.py @@ -0,0 +1,36 @@ +import re +import unittest +import nose.config +from nose.core import configure + +class TestNoseConfig(unittest.TestCase): + + def test_defaults(self): + c = nose.config.Config() + assert c.addPaths == True + assert c.capture == True + assert c.detailedErrors == False + # FIXME etc + + def test_reset(self): + c = nose.config.Config() + c.include = 'include' + assert c.include == 'include' + c.reset() + assert c.include is None + + def test_update(self): + c = nose.config.Config() + c.update({'exclude':'x'}) + assert c.exclude == 'x' + + def test_multiple_include(self): + conf = configure(['--include=a', '--include=b']) + self.assertEqual(conf.include, [re.compile('a'), re.compile('b')]) + + def test_single_include(self): + conf = configure(['--include=b']) + self.assertEqual(conf.include, [re.compile('b')]) + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_core.py b/unit_tests/test_core.py new file mode 100644 index 0000000..b25dbf2 --- /dev/null +++ b/unit_tests/test_core.py @@ -0,0 +1,41 @@ +import unittest +import nose.core + +from cStringIO import StringIO + +def nullcollector(conf, loader): + def nulltest(result): + pass + return nulltest + +class TestTestProgram(unittest.TestCase): + + def test_init_arg_defaultTest(self): + try: + t = nose.core.TestProgram(defaultTest='something', argv=[], env={}) + except ValueError: + pass + else: + self.fail("TestProgram with non-callable defaultTest should " + "have thrown ValueError") + + def test_init_arg_module(self): + s = StringIO() + t = nose.core.TestProgram('__main__', defaultTest=nullcollector, + argv=[], env={}, stream=s) + assert '__main__' in t.conf.tests + + +class TestAPI_run(unittest.TestCase): + + def test_restore_stdout(self): + import sys + s = StringIO() + stdout = sys.stdout + res = nose.core.run(defaultTest=nullcollector, argv=[], env={}, + stream=s) + stdout_after = sys.stdout + self.assertEqual(stdout, stdout_after) + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_importer.py b/unit_tests/test_importer.py new file mode 100644 index 0000000..d265393 --- /dev/null +++ b/unit_tests/test_importer.py @@ -0,0 +1,53 @@ +import os +import sys +import unittest +import nose.config +import nose.importer + +class TestImporter(unittest.TestCase): + + def setUp(self): + self.p = sys.path[:] + + def tearDown(self): + sys.path = self.p[:] + + def test_add_paths(self): + where = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'support')) + foo = os.path.join(where, 'foo') + foobar = os.path.join(foo, 'bar') + nose.importer.add_path(foobar) + + assert not foobar in sys.path + assert not foo in sys.path + assert where in sys.path + assert sys.path[0] == where, "%s first should be %s" % (sys.path, where) + + def test_import(self): + where = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'support')) + foo = os.path.join(where, 'foo') + foobar = os.path.join(foo, 'bar') + + mod = nose.importer._import('buz', [foobar], nose.config.Config()) + assert where in sys.path + # buz has an intra-package import that sets boodle + assert mod.boodle + + def test_module_no_file(self): + where = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'support')) + foo = os.path.join(where, 'foo') + foobar = os.path.join(foo, 'bar') + + # something that's not a real module and has no __file__ + sys.modules['buz'] = 'Whatever' + + mod = nose.importer._import('buz', [foobar], nose.config.Config()) + assert where in sys.path + # buz has an intra-package import that sets boodle + assert mod.boodle + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_inspector.py b/unit_tests/test_inspector.py new file mode 100644 index 0000000..d547e90 --- /dev/null +++ b/unit_tests/test_inspector.py @@ -0,0 +1,125 @@ +import inspect +import sys +import textwrap +import tokenize +import traceback +import unittest + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from nose.inspector import inspect_traceback, Expander, tbsource + +class TestExpander(unittest.TestCase): + + def test_simple_inspect_frame(self): + src = StringIO('a > 2') + lc = { 'a': 2} + gb = {} + exp = Expander(lc, gb) + + tokenize.tokenize(src.readline, exp) + # print "'%s'" % exp.expanded_source + self.assertEqual(exp.expanded_source.strip(), '2 > 2') + + def test_inspect_traceback_continued(self): + a = 6 + out = '' + try: + assert a < 1, \ + "This is a multline expression" + except AssertionError: + et, ev, tb = sys.exc_info() + out = inspect_traceback(tb) + # print "'%s'" % out.strip() + self.assertEqual(out.strip(), + '>> assert 6 < 1, \\\n ' + '"This is a multline expression"') + + def test_get_tb_source_simple(self): + # no func frame + try: + assert False + except AssertionError: + et, ev, tb = sys.exc_info() + lines, lineno = tbsource(tb, 1) + self.assertEqual(''.join(lines).strip(), 'assert False') + self.assertEqual(lineno, 0) + + def test_get_tb_source_func(self): + # func frame + def check_even(n): + print n + assert n % 2 == 0 + try: + check_even(1) + except AssertionError: + et, ev, tb = sys.exc_info() + lines, lineno = tbsource(tb) + out = textwrap.dedent(''.join(lines)) + self.assertEqual(out, + ' print n\n' + ' assert n % 2 == 0\n' + 'try:\n' + ' check_even(1)\n' + 'except AssertionError:\n' + ' et, ev, tb = sys.exc_info()\n' + ) + self.assertEqual(lineno, 3) + + # FIXME 2 func frames + + def test_pick_tb_lines(self): + try: + val = "fred" + def defred(n): + return n.replace('fred','') + assert defred(val) == 'barney', "Fred - fred != barney?" + except AssertionError: + et, ev, tb = sys.exc_info() + out = inspect_traceback(tb) + # print "'%s'" % out.strip() + self.assertEqual(out.strip(), + ">> assert defred('fred') == 'barney', " + '"Fred - fred != barney?"') + try: + val = "fred" + def defred(n): + return n.replace('fred','') + assert defred(val) == 'barney', \ + "Fred - fred != barney?" + def refred(n): + return n + 'fred' + except AssertionError: + et, ev, tb = sys.exc_info() + out = inspect_traceback(tb) + #print "'%s'" % out.strip() + self.assertEqual(out.strip(), + ">> assert defred('fred') == 'barney', " + '\\\n "Fred - fred != barney?"') + + S = {'setup':1} + def check_even(n, nn): + assert S['setup'] + print n, nn + assert n % 2 == 0 or nn % 2 == 0 + try: + check_even(1, 3) + except AssertionError: + et, ev, tb = sys.exc_info() + out = inspect_traceback(tb) + print "'%s'" % out.strip() + self.assertEqual(out.strip(), + "assert {'setup': 1}['setup']\n" + " print 1, 3\n" + ">> assert 1 % 2 == 0 or 3 % 2 == 0") + + +if __name__ == '__main__': + #import logging + #logging.basicConfig() + #logging.getLogger('').setLevel(0) + unittest.main() + diff --git a/unit_tests/test_lazy_suite.py b/unit_tests/test_lazy_suite.py new file mode 100644 index 0000000..db1869f --- /dev/null +++ b/unit_tests/test_lazy_suite.py @@ -0,0 +1,50 @@ +import unittest +from nose import LazySuite +from helpers import iter_compat + +def gen(): + for x in range(0, 10): + yield TestLazySuite.TC('test') + +class TestLazySuite(unittest.TestCase): + + class TC(unittest.TestCase): + def test(self): + pass + + def test_basic_iteration(self): + ls = LazySuite(gen) + for t in iter_compat(ls): + assert isinstance(t, unittest.TestCase) + + def test_setup_teardown(self): + class SetupTeardownLazySuite(LazySuite): + _setup = False + _teardown = False + + def setUp(self): + self._setup = True + + def tearDown(self): + if self._setup: + self._teardown = True + + class Result: + shouldStop = False + + def addSuccess(self, test): + pass + + def startTest(self, test): + pass + + def stopTest(self, test): + pass + + ls = SetupTeardownLazySuite(gen) + ls(Result()) + assert ls._setup + assert ls._teardown + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_loader.py b/unit_tests/test_loader.py new file mode 100644 index 0000000..2b3075b --- /dev/null +++ b/unit_tests/test_loader.py @@ -0,0 +1,254 @@ +import os +import sys +import unittest +from nose import case, loader +from nose.config import Config +from nose.importer import _import + +from helpers import iter_compat +from mock import * + +class TestNoseTestLoader(unittest.TestCase): + + def setUp(self): + cwd = os.path.dirname(__file__) + self.support = os.path.abspath(os.path.join(cwd, 'support')) + + def test_load_from_name_dir(self): + l = loader.TestLoader() + name = os.path.join(self.support, 'test-dir') + expect = [ 'test module test in %s' % name ] + found = [] + for test in l.loadTestsFromName(name): + found.append(str(test)) + # print found + self.assertEqual(found, expect) + + def test_load_from_name_file(self): + l = loader.TestLoader() + name = os.path.join(self.support, 'test.py') + expect = [ 'test module test in %s' % self.support ] + found = [] + for test in l.loadTestsFromName(name): + found.append(str(test)) + # print found + self.assertEqual(found, expect) + + def test_load_from_name_module(self): + c = Config() + c.where = self.support + l = loader.TestLoader(c) + name = 'test' + expect = [ 'test module test in %s' % self.support, + 'test module test in %s/test-dir' % self.support ] + found = [] + for test in l.loadTestsFromName(name): + found.append(str(test)) + + c.where = os.path.join(self.support, 'test-dir') + for test in l.loadTestsFromName(name): + found.append(str(test)) + print found + self.assertEqual(found, expect) + + def test_load_from_names(self): + c = Config() + c.where = self.support + l = loader.TestLoader(c) + + foo = _import('foo', [self.support], c) + + expect = [ 'test module test in %s' % self.support, + 'test module foo.test_foo in %s' % self.support ] + found = [] + tests = l.loadTestsFromNames(['test', 'foo.test_foo']) + for t in iter_compat(tests): + found.append(str(t)) + self.assertEqual(found, expect) + + expect = [ 'test module foo in %s' % self.support ] + tests = l.loadTestsFromNames(None, module=foo) + found = [ str(tests) ] + self.assertEqual(found, expect) + + def test_load_from_names_compat(self): + c = Config() + l = loader.TestLoader(c) + + # implicit : prepended when module specified + names = ['TestNoseTestLoader.test_load_from_names_compat'] + tests = l.loadTestsFromNames(names, sys.modules[__name__]) + + # should be... me + expect = [ 'test_load_from_names_compat ' + '(%s.TestNoseTestLoader)' % __name__ ] + found = [] + # print tests + for test in iter_compat(tests): + # print test + for t in iter_compat(test): + # print t + found.append(str(t)) + self.assertEqual(found, expect) + + # explict : ok too + c.tests = [] + found = [] + names[0] = ':' + names[0] + tests = l.loadTestsFromNames(names, sys.modules[__name__]) + for test in iter_compat(tests): + for t in iter_compat(test): + found.append(str(t)) + self.assertEqual(found, expect) + + def test_load_from_class(self): + c = Config() + class TC: + test_not = 1 + def test_me(self): + pass + def not_a_tes_t(self): + pass + + class TC2(unittest.TestCase): + def test_whatever(self): + pass + + class TC3(TC2): + def test_somethingelse(self): + pass + + l = loader.TestLoader() + cases = l.loadTestsFromTestCase(TC) + # print cases + assert isinstance(cases[0], case.MethodTestCase) + assert len(cases) == 1 + self.assertEqual(str(cases[0]), '%s.TC.test_me' % __name__) + + cases2 = l.loadTestsFromTestCase(TC2) + # print cases2 + assert isinstance(cases2[0], unittest.TestCase) + assert len(cases2) == 1 + self.assertEqual(str(cases2[0]), 'test_whatever (%s.TC2)' % __name__) + + cases3 = l.loadTestsFromTestCase(TC3) + # print cases3 + assert len(cases3) == 2 + self.assertEqual(str(cases3[0]), + 'test_somethingelse (%s.TC3)' % __name__) + self.assertEqual(str(cases3[1]), + 'test_whatever (%s.TC3)' % __name__) + + def test_load_generator_method(self): + class TC(object): + _setup = False + + def setUp(self): + assert not self._setup + self._setup = True + + def test_generator(self): + for a in range(0,5): + yield self.check, a + + def check(self, val): + assert self._setup + assert val >= 0 + assert val <= 5 + + l = loader.TestLoader() + cases = l.loadTestsFromTestCase(TC) + count = 0 + for suite in iter_compat(cases): + for case in iter_compat(suite): + assert str(case) == '%s.TC.test_generator:(%d,)' % \ + (__name__, count) + count += 1 + assert count == 5 + + def test_load_generator_func(self): + m = Mod('testo', __path__=None) + + def testfunc(i): + pass + + def testgenf(): + for i in range(0, 5): + yield testfunc, i + + m.testgenf = testgenf + + l = loader.TestLoader() + cases = l.loadTestsFromModule(m) + print cases + count = 0 + for case in iter_compat(cases): + # print case + self.assertEqual(str(case), + '%s.testgenf:(%d,)' % (__name__, count)) + count += 1 + assert count == 5 + + def test_get_module_funcs(self): + from StringIO import StringIO + + m = Mod('testo', __path__=None) + + def test_func(): + pass + + class NotTestFunc(object): + def __call__(self): + pass + + class Selector: + classes = [] + funcs = [] + + def wantClass(self, test): + self.classes.append(test) + return False + + def wantFunction(self, test): + self.funcs.append(test) + return True + sel = Selector() + + m.test_func = test_func + m.test_func_not_really = NotTestFunc() + m.StringIO = StringIO + m.buffer = StringIO() + + l = loader.TestLoader(selector=sel) + tests = l.testsInModule(m) + + print tests + print sel.funcs + assert test_func in sel.funcs + assert not m.test_func_not_really in sel.funcs + assert len(sel.funcs) == 1 + + def test_pkg_layout_lib_tests(self): + from mock import Result + from nose.util import absdir + r = Result() + l = loader.TestLoader() + where = absdir(os.path.join(os.path.dirname(__file__), + 'support/pkgorg')) + print "where", where + print "/lib on path before load?", where + '/lib' in sys.path + tests = l.loadTestsFromDir(where) + print "/lib on path after load?", where + '/lib' in sys.path + print "tests", tests + for t in tests: + print "test", t + # this will raise an importerror if /lib isn't on the path + t(r) + assert where + '/lib' in sys.path + +if __name__ == '__main__': + import logging + logging.basicConfig() + logging.getLogger('').setLevel(0) + #logging.getLogger('nose.importer').setLevel(0) + unittest.main() #testLoader=loader.TestLoader()) diff --git a/unit_tests/test_logging.py b/unit_tests/test_logging.py new file mode 100644 index 0000000..ab24d66 --- /dev/null +++ b/unit_tests/test_logging.py @@ -0,0 +1,40 @@ +import logging +import unittest +from nose.config import Config +from nose.core import configure_logging +from mock import * + + +class TestLoggingConfig(unittest.TestCase): + + def setUp(self): + # install mock root logger so that these tests don't stomp on + # the real logging config of the test runner + class MockLogger(logging.Logger): + root = logging.RootLogger(logging.WARNING) + manager = logging.Manager(root) + + self.real_logger = logging.Logger + self.real_root = logging.root + logging.Logger = MockLogger + logging.root = MockLogger.root + + def tearDown(self): + # reset real root logger + logging.Logger = self.real_logger + logging.root = self.real_root + + def test_isolation(self): + """root logger settings ignored""" + + root = logging.getLogger('') + nose = logging.getLogger('nose') + + opt = Bucket() + configure_logging(opt) + + root.setLevel(logging.DEBUG) + self.assertEqual(nose.level, logging.WARN) + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_plugin_interfaces.py b/unit_tests/test_plugin_interfaces.py new file mode 100644 index 0000000..9907bae --- /dev/null +++ b/unit_tests/test_plugin_interfaces.py @@ -0,0 +1,44 @@ +import unittest +from nose.plugins.base import IPluginInterface + +class TestPluginInterfaces(unittest.TestCase): + + def test_api_methods_present(self): + + from nose.loader import TestLoader + from nose.selector import Selector + + + exclude = [ 'loadTestsFromDir', 'loadTestsFromModuleName', + 'loadTestsFromNames' ] + + selfuncs = [ f for f in dir(Selector) + if f.startswith('want') ] + loadfuncs = [ f for f in dir(TestLoader) + if f.startswith('load') and not f in exclude ] + + others = ['addDeprecated', 'addError', 'addFailure', + 'addSkip', 'addSuccess', 'startTest', 'stopTest', + 'prepareTest', 'begin', 'report' + ] + + expect = selfuncs + loadfuncs + others + + pd = dir(IPluginInterface) + + for f in expect: + assert f in pd, "No %s in IPluginInterface" % f + assert getattr(IPluginInterface, f).__doc__, \ + "No docs for %f in IPluginInterface" % f + + def test_no_instantiate(self): + try: + p = IPluginInterface() + except TypeError: + pass + else: + assert False, \ + "Should not be able to instantiate IPluginInterface" + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_plugins.py b/unit_tests/test_plugins.py new file mode 100644 index 0000000..402c3ff --- /dev/null +++ b/unit_tests/test_plugins.py @@ -0,0 +1,455 @@ +import logging +import os +import sys +import unittest +import nose.plugins +from optparse import OptionParser +import tempfile +from warnings import warn, filterwarnings, resetwarnings + +from nose.config import Config +from nose.plugins.attrib import AttributeSelector +from nose.plugins.base import Plugin +from nose.plugins.cover import Coverage +from nose.plugins.doctests import Doctest +from nose.plugins.missed import MissedTests +from nose.plugins.prof import Profile + +from mock import * + +class P(Plugin): + """Plugin of destiny!""" + pass + +class ErrPlugin(object): + def load(self): + raise Exception("Failed to load the plugin") + +class ErrPkgResources(object): + def iter_entry_points(self, ep): + yield ErrPlugin() + + +# some plugins have 2.4-only features +compat_24 = sys.version_info >= (2, 4) + + +class TestBuiltinPlugins(unittest.TestCase): + + def setUp(self): + self.p = sys.path[:] + + def tearDown(self): + sys.path = self.p[:] + + def test_load(self): + plugs = list(nose.plugins.load_plugins(builtin=True, others=False)) + # print plugs + + assert Coverage in plugs + assert Doctest in plugs + assert AttributeSelector in plugs + assert Profile in plugs + assert MissedTests in plugs + assert len(plugs) == 5 + + for p in plugs: + assert not p.enabled + + def test_failing_load(self): + tmp = nose.plugins.pkg_resources + nose.plugins.pkg_resources = ErrPkgResources() + try: + # turn off warnings + filterwarnings('ignore', category=RuntimeWarning) + plugs = list(nose.plugins.load_plugins(builtin=True, others=True)) + self.assertEqual(plugs, []) + finally: + nose.plugins.pkg_resources = tmp + resetwarnings() + + def test_add_options(self): + conf = Config() + opt = Bucket() + parser = MockOptParser() + plug = P() + + plug.add_options(parser) + o, d = parser.opts[0] + # print d + assert o[0] == '--with-p' + assert d['action'] == 'store_true' + assert not d['default'] + assert d['dest'] == 'enable_plugin_p' + assert d['help'] == 'Enable plugin P: Plugin of destiny! [NOSE_WITH_P]' + + opt.enable_plugin_p = True + plug.configure(opt, conf) + assert plug.enabled + + +class TestDoctestPlugin(unittest.TestCase): + + def setUp(self): + self.p = sys.path[:] + + def tearDown(self): + sys.path = self.p[:] + + def test_add_options(self): + # doctest plugin adds some options... + conf = Config() + opt = Bucket() + parser = MockOptParser() + plug = Doctest() + + plug.add_options(parser, {}) + o, d = parser.opts[0] + assert o[0] == '--with-doctest' + + o2, d2 = parser.opts[1] + assert o2[0] == '--doctest-tests' + + if compat_24: + o3, d3 = parser.opts[2] + assert o3[0] == '--doctest-extension' + else: + assert len(parser.opts) == 2 + + def test_config(self): + # test that configuration works properly when both environment + # and command line specify a doctest extension + parser = OptionParser() + env = {'NOSE_DOCTEST_EXTENSION':'ext'} + argv = ['--doctest-extension', 'txt'] + dtp = Doctest() + dtp.add_options(parser, env) + options, args = parser.parse_args(argv) + + print options + print args + self.assertEqual(options.doctestExtension, ['ext', 'txt']) + + env = {} + parser = OptionParser() + dtp.add_options(parser, env) + options, args = parser.parse_args(argv) + print options + print args + self.assertEqual(options.doctestExtension, ['txt']) + + def test_want_file(self): + # doctest plugin can select module and/or non-module files + conf = Config() + opt = Bucket() + plug = Doctest() + plug.configure(opt, conf) + + assert plug.wantFile('foo.py') + assert not plug.wantFile('bar.txt') + assert not plug.wantFile('buz.rst') + assert not plug.wantFile('bing.mov') + + plug.extension = ['.txt', '.rst'] + assert plug.wantFile('/path/to/foo.py') + assert plug.wantFile('/path/to/bar.txt') + assert plug.wantFile('/path/to/buz.rst') + assert not plug.wantFile('/path/to/bing.mov') + + def test_matches(self): + # doctest plugin wants tests from all NON-test modules + conf = Config() + opt = Bucket() + plug = Doctest() + plug.configure(opt, conf) + assert not plug.matches('test') + assert plug.matches('foo') + + def test_collect_pymodule(self): + here = os.path.dirname(__file__) + support = os.path.join(here, 'support') + if not support in sys.path: + sys.path.insert(0, support) + import foo.bar.buz + + conf = Config() + opt = Bucket() + plug = Doctest() + plug.configure(opt, conf) + suite = plug.loadTestsFromModule(foo.bar.buz) + if compat_24: + expect = ['afunc (foo.bar.buz)'] + else: + expect = ['unittest.FunctionTestCase (runit)'] + for test in suite: + self.assertEqual(str(test), expect.pop(0)) + + def test_collect_txtfile(self): + if not compat_24: + warn("No support for doctests in files other than python modules" + " in python versions older than 2.4") + return + here = os.path.abspath(os.path.dirname(__file__)) + support = os.path.join(here, 'support') + fn = os.path.join(support, 'foo', 'doctests.txt') + + conf = Config() + opt = Bucket() + plug = Doctest() + plug.configure(opt, conf) + plug.extension = ['.txt'] + suite = plug.loadTestsFromPath(fn) + for test in suite: + assert str(test).endswith('doctests.txt') + + def test_collect_no_collect(self): + # bug http://nose.python-hosting.com/ticket/55 + # we got "iteration over non-sequence" when no files match + here = os.path.abspath(os.path.dirname(__file__)) + support = os.path.join(here, 'support') + plug = Doctest() + suite = plug.loadTestsFromPath(os.path.join(support, 'foo')) + for test in suite: + pass + + +class TestAttribPlugin(unittest.TestCase): + + def test_add_options(self): + plug = AttributeSelector() + parser = MockOptParser() + plug.add_options(parser) + + expect = [(('-a', '--attr'), + {'dest': 'attr', 'action': 'append', 'default': None, + 'help': 'Run only tests that have attributes ' + 'specified by ATTR [NOSE_ATTR]'})] + + if compat_24: + expect.append( + (('-A', '--eval-attr'), + {'dest': 'eval_attr', 'action': 'append', + 'default': None, 'metavar': 'EXPR', + 'help': 'Run only tests for whose attributes the ' + 'Python expression EXPR evaluates to True ' + '[NOSE_EVAL_ATTR]'})) + self.assertEqual(parser.opts, expect) + + opt = Bucket() + opt.attr = ['!slow'] + plug.configure(opt, Config()) + assert plug.enabled + self.assertEqual(plug.attribs, [[('slow', False)]]) + + opt.attr = ['fast,quick', 'weird=66'] + plug.configure(opt, Config()) + self.assertEqual(plug.attribs, [[('fast', True), + ('quick', True)], + [('weird', '66')]]) + + # don't die on trailing , + opt.attr = [ 'something,' ] + plug.configure(opt, Config()) + self.assertEqual(plug.attribs, [[('something', True)]] ) + + if compat_24: + opt.attr = None + opt.eval_attr = [ 'weird >= 66' ] + plug.configure(opt, Config()) + self.assertEqual(plug.attribs[0][0][0], 'weird >= 66') + assert callable(plug.attribs[0][0][1]) + + def test_basic_attr(self): + def f(): + pass + f.a = 1 + + def g(): + pass + + plug = AttributeSelector() + plug.attribs = [[('a', 1)]] + assert plug.wantFunction(f) is not False + assert not plug.wantFunction(g) + + def test_eval_attr(self): + if not compat_24: + warn("No support for eval attributes in python versions older" + " than 2.4") + return + def f(): + pass + f.monkey = 2 + + def g(): + pass + g.monkey = 6 + + def h(): + pass + h.monkey = 5 + + cnf = Config() + opt = Bucket() + opt.eval_attr = "monkey > 5" + plug = AttributeSelector() + plug.configure(opt, cnf) + + assert not plug.wantFunction(f) + assert plug.wantFunction(g) is not False + assert not plug.wantFunction(h) + + def test_attr_a_b(self): + def f1(): + pass + f1.tags = ['a', 'b'] + + def f2(): + pass + f2.tags = ['a', 'c'] + + def f3(): + pass + f3.tags = ['b', 'c'] + + def f4(): + pass + f4.tags = ['c', 'd'] + + cnf = Config() + parser = OptionParser() + plug = AttributeSelector() + + plug.add_options(parser) + + # OR + opt, args = parser.parse_args(['test', '-a', 'tags=a', + '-a', 'tags=b']) + print opt + plug.configure(opt, cnf) + + assert plug.wantFunction(f1) is None + assert plug.wantFunction(f2) is None + assert plug.wantFunction(f3) is None + assert not plug.wantFunction(f4) + + # AND + opt, args = parser.parse_args(['test', '-a', 'tags=a,tags=b']) + print opt + plug.configure(opt, cnf) + + assert plug.wantFunction(f1) is None + assert not plug.wantFunction(f2) + assert not plug.wantFunction(f3) + assert not plug.wantFunction(f4) + +class TestMissedTestsPlugin(unittest.TestCase): + + def test_options(self): + opt = Config() + parser = OptionParser() + plug = MissedTests() + plug.add_options(parser, {}) + opts = [ o._long_opts[0] for o in parser.option_list ] + assert '--with-missed-tests' in opts + + def test_match(self): + class FooTest(unittest.TestCase): + def test_bar(self): + pass + + class QuzTest: + def test_whatever(self): + pass + def test_baz(): + pass + foo = FooTest('test_bar') + baz = nose.case.FunctionTestCase(test_baz) + quz = nose.case.MethodTestCase(QuzTest, 'test_whatever') + + plug = MissedTests() + + here = os.path.abspath(__file__) + + # positive matches + assert plug.match(foo, ':FooTest.test_bar') + assert plug.match(foo, ':FooTest') + assert plug.match(foo, here) + assert plug.match(foo, os.path.dirname(here)) + assert plug.match(foo, __name__) + assert plug.match(baz, ':test_baz') + assert plug.match(baz, here) + assert plug.match(baz, __name__) + assert plug.match(quz, ':QuzTest.test_whatever') + assert plug.match(quz, ':QuzTest') + assert plug.match(quz, here) + assert plug.match(quz, __name__) + + # non-matches + assert not plug.match(foo, ':test_bar') + assert not plug.match(foo, ':FooTest.test_bart') + assert not plug.match(foo, 'some.module') + assert not plug.match(foo, __name__ + '.whatever') + assert not plug.match(foo, '/some/path') + + def test_begin(self): + plug = MissedTests() + plug.conf = Config() + plug.begin() + assert plug.missed is None + + plug.conf.tests = ['a'] + plug.begin() + self.assertEqual(plug.missed, ['a']) + assert plug.missed is not plug.conf.tests + + def test_finalize(self): + plug = MissedTests() + plug.missed = ['a'] + + out = [] + class dummy: + pass + + result = dummy() + result.stream = dummy() + result.stream.writeln = out.append + + plug.finalize(result) + self.assertEqual(out, ["WARNING: missed test 'a'"]) + +class TestProfPlugin(unittest.TestCase): + def test_options(self): + parser = OptionParser() + conf = Config() + plug = Profile() + + plug.add_options(parser, {}) + opts = [ o._long_opts[0] for o in parser.option_list ] + assert '--profile-sort' in opts + assert '--profile-stats-file' in opts + assert '--with-profile' in opts + assert '--profile-restrict' in opts + + def test_begin(self): + plug = Profile() + plug.pfile = tempfile.mkstemp()[1] + plug.begin() + assert plug.prof + + def test_prepare_test(self): + r = {} + class dummy: + def runcall(self, f, r): + r[1] = f(), "wrapped" + def func(): + return "func" + + plug = Profile() + plug.prof = dummy() + result = plug.prepareTest(func) + result(r) + assert r[1] == ("func", "wrapped") + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_proxy.py b/unit_tests/test_proxy.py new file mode 100644 index 0000000..79cb744 --- /dev/null +++ b/unit_tests/test_proxy.py @@ -0,0 +1,193 @@ +import sys +import unittest +from nose.config import Config +from nose.proxy import * +from nose.result import Result, start_capture, end_capture + + +class dummy: + def __init__(self): + self.buf = [] + def write(self, val): + if val is None: + return + if not self.buf: + self.buf.append('') + self.buf[-1] += val + def writeln(self, val=None): + self.write(val) + self.buf.append('') + + +class TestNoseProxy(unittest.TestCase): + + class TC(unittest.TestCase): + def runTest(self): + print "RUNTEST %s" % self + pass + + class ErrTC(unittest.TestCase): + def test_err(self): + print "Ahoy there!" + raise Exception("oh well") + + def test_fail(self): + a = 1 + print "a:", a + assert a == 2 + + def setUp(self): + self.real_conf = Result.conf + start_capture() + + def tearDown(self): + Result.conf = self.real_conf + end_capture() + + def test_proxy_result(self): + # set up configuration at class level + Result.conf = Config() + + res = unittest.TestResult() + pr = ResultProxy(res) + + # start is proxied + test = self.TC() + pr.startTest(test) + self.assertEqual(res.testsRun, 1) + + # success is proxied + pr.addSuccess(test) + self.assertEqual(res.errors, []) + self.assertEqual(res.failures, []) + + # stop is proxied + pr.stopTest(test) + + # error is proxied + try: + raise Exception("oh no!") + except: + e = sys.exc_info() + pr.addError(test, e) + + # failure is proxied + try: + raise AssertionError("not that!") + except: + e = sys.exc_info() + pr.addFailure(test, e) + + self.assertEqual(len(res.errors), 1) + self.assertEqual(len(res.failures), 1) + + # shouldStop is proxied + self.assertEqual(pr.shouldStop, res.shouldStop) + pr.shouldStop = True + assert res.shouldStop + + def test_output_capture(self): + + c = Config() + c.capture = True + c.detailedErrors = True + Result.conf = c + + res = unittest.TestResult() + pr = ResultProxy(res) + + errcase = self.ErrTC('test_err') + failcase = self.ErrTC('test_fail') + errcase.run(pr) + failcase.run(pr) + + assert len(res.errors) == 1 + assert len(res.failures) == 1 + + err = res.errors[0][1] + + assert 'Ahoy there!' in err + + fail = res.failures[0][1] + assert 'a: 1' in fail + assert '>> assert 1 == 2' in fail + + def test_proxy_suite(self): + c = Config() + c.capture = True + c.detailedErrors = True + Result.conf = c + + errcase = self.ErrTC('test_err') + failcase = self.ErrTC('test_fail') + passcase = self.TC() + + suite = ResultProxySuite([errcase, failcase, passcase]) + print list(suite) + + for test in suite: + print test + assert isinstance(test, TestProxy) + + d = dummy() + res = unittest._TextTestResult(d, 1, 1) + suite.run(res) + res.printErrors() + print d.buf + + # split internal \n in strings into own lines + buf = '\n'.join(d.buf).split('\n') + + assert 'Ahoy there!' in buf + assert 'a: 1' in buf + assert '>> assert 1 == 2' in buf + assert buf.index('>> assert 1 == 2') < buf.index('a: 1') + + def test_proxy_test(self): + c = Config() + c.capture = True + c.detailedErrors = True + Result.conf = c + + base_errcase = self.ErrTC('test_err') + base_failcase = self.ErrTC('test_fail') + base_passcase = self.TC() + errcase = TestProxy(base_errcase) + failcase = TestProxy(base_failcase) + passcase = TestProxy(base_passcase) + + self.assertEqual(errcase.id(), base_errcase.id()) + self.assertEqual(failcase.id(), base_failcase.id()) + self.assertEqual(passcase.id(), base_passcase.id()) + + self.assertEqual(errcase.shortDescription(), + base_errcase.shortDescription()) + self.assertEqual(failcase.shortDescription(), + base_failcase.shortDescription()) + self.assertEqual(passcase.shortDescription(), + base_passcase.shortDescription()) + + d = dummy() + + res = unittest._TextTestResult(d, 1, 1) + + errcase.run(res) + failcase.run(res) + passcase.run(res) + + #print >>sys.stderr, res.errors + #print >>sys.stderr, res.failures + + res.printErrors() + + # split internal \n in strings into own lines + buf = '\n'.join(d.buf).split('\n') + + assert 'Ahoy there!' in buf + assert 'a: 1' in buf + assert '>> assert 1 == 2' in buf + assert buf.index('>> assert 1 == 2') < buf.index('a: 1') + + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_result.py b/unit_tests/test_result.py new file mode 100644 index 0000000..5a9c04b --- /dev/null +++ b/unit_tests/test_result.py @@ -0,0 +1,163 @@ +import sys +import unittest +import nose.result +from nose.config import Config +from nose.exc import DeprecatedTest, SkipTest +from nose.result import start_capture, end_capture + +class TestResult(unittest.TestCase): + + class T(unittest.TestCase): + def runTest(self): + pass + + def setUp(self): + self.buf = [] + class dummy: + pass + stream = dummy() + stream.write = self.buf.append + stream.writeln = self.buf.append + self.tr = nose.result.TextTestResult(stream, None, 2, Config()) + +# def tearDown(self): +# nose.result.end_capture() + + def test_capture(self): + start_capture() + try: + print "Hello" + self.assertEqual(sys.stdout.getvalue(), "Hello\n") + finally: + end_capture() + + def test_init(self): + tr = self.tr + self.assertEqual(tr.errors, []) + self.assertEqual(tr.failures, []) + self.assertEqual(tr.deprecated, []) + self.assertEqual(tr.skip, []) + self.assertEqual(tr.testsRun, 0) + self.assertEqual(tr.shouldStop, 0) + + def test_add_error(self): + buf, tr = self.buf, self.tr + try: + raise Exception("oh no!") + except: + err = sys.exc_info() + test = self.T() + tr.addError(test, err) + self.assertEqual(tr.errors[0], + (test, tr._exc_info_to_string(err, test), '')) + self.assertEqual(buf, [ 'ERROR' ]) + + # test with capture + start_capture() + try: + tr.capture = True + print "some output" + tr.addError(test, err) + self.assertEqual(tr.errors[1], + (test, tr._exc_info_to_string(err, test), + 'some output\n')) + self.assertEqual(buf, [ 'ERROR', 'ERROR' ]) + finally: + end_capture() + + # test deprecated + try: + raise DeprecatedTest("deprecated") + except: + err = sys.exc_info() + tr.addError(test, err) + self.assertEqual(len(tr.errors), 2) + self.assertEqual(tr.deprecated, [ (test, '', '') ]) + + # test skip + try: + raise SkipTest("skip") + except: + err = sys.exc_info() + tr.addError(test, err) + self.assertEqual(len(tr.errors), 2) + self.assertEqual(tr.skip, [ (test, '', '') ]) + self.assertEqual(buf, ['ERROR', 'ERROR', 'DEPRECATED', 'SKIP']) + + def test_add_failure(self): + buf, tr = self.buf, self.tr + try: + assert False, "test add fail" + except: + err = sys.exc_info() + test = self.T() + tr.addFailure(test, err) + self.assertEqual(tr.failures[0], + (test, tr._exc_info_to_string(err, test), '')) + self.assertEqual(buf, [ 'FAIL' ]) + + # test with capture + start_capture() + try: + tr.capture = True + print "some output" + tr.addFailure(test, err) + self.assertEqual(tr.failures[1], + (test, tr._exc_info_to_string(err, test), + 'some output\n')) + self.assertEqual(buf, [ 'FAIL', 'FAIL' ]) + finally: + end_capture() + + def test_start_stop(self): + tr = self.tr + test = self.T() + tr.startTest(test) + tr.stopTest(test) + + def test_stop_on_error(self): + buf, tr = self.buf, self.tr + tr.conf.stopOnError = True + try: + raise Exception("oh no!") + except: + err = sys.exc_info() + test = self.T() + tr.addError(test, err) + assert tr.shouldStop + + def test_stop_on_error_skip(self): + buf, tr = self.buf, self.tr + tr.conf.stopOnError = True + try: + raise SkipTest("oh no!") + except: + err = sys.exc_info() + test = self.T() + tr.addError(test, err) + assert not tr.shouldStop + + def test_stop_on_error_deprecated(self): + buf, tr = self.buf, self.tr + tr.conf.stopOnError = True + try: + raise DeprecatedTest("oh no!") + except: + err = sys.exc_info() + test = self.T() + tr.addError(test, err) + assert not tr.shouldStop + + def test_stop_on_error_fail(self): + buf, tr = self.buf, self.tr + tr.conf.stopOnError = True + try: + assert False, "test add fail" + except: + err = sys.exc_info() + test = self.T() + tr.addFailure(test, err) + assert tr.shouldStop + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_selector.py b/unit_tests/test_selector.py new file mode 100644 index 0000000..d63175d --- /dev/null +++ b/unit_tests/test_selector.py @@ -0,0 +1,357 @@ +import logging +import os +import re +import unittest +import nose.selector +from nose.config import Config +from nose.selector import log, Selector, test_addr +from nose.util import absdir +from mock import Mod + +class TestSelector(unittest.TestCase): + + def tearDown(self): + logging.getLogger('nose.selector').setLevel(logging.WARN) + + def test_exclude(self): + s = Selector(Config()) + c = Config() + c.exclude = [re.compile(r'me')] + s2 = Selector(c) + + assert s.matches('test_foo') + assert s2.matches('test_foo') + assert s.matches('test_me') + assert not s2.matches('test_me') + + def test_include(self): + s = Selector(Config()) + c = Config() + c.include = [re.compile(r'me')] + s2 = Selector(c) + + assert s.matches('test') + assert s2.matches('test') + assert not s.matches('meatball') + assert s2.matches('meatball') + assert not s.matches('toyota') + assert not s2.matches('toyota') + + c.include.append(re.compile('toy')) + assert s.matches('test') + assert s2.matches('test') + assert not s.matches('meatball') + assert s2.matches('meatball') + assert not s.matches('toyota') + assert s2.matches('toyota') + + def test_want_class(self): + class Foo: + pass + class Bar(unittest.TestCase): + pass + class TestMe: + pass + + s = Selector(Config()) + assert not s.wantClass(Foo) + assert s.wantClass(Bar) + assert s.wantClass(TestMe) + + tests = test_addr([ ':Bar' ]) + assert s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert not s.wantClass(TestMe, tests) + + tests = test_addr([ ':Bar.baz' ]) + assert s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert not s.wantClass(TestMe, tests) + + tests = test_addr([ ':Blah' ]) + assert not s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert not s.wantClass(TestMe, tests) + + tests = test_addr([ ':Blah.baz' ]) + assert not s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert not s.wantClass(TestMe, tests) + + tests = test_addr([ __name__ ]) + assert s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert s.wantClass(TestMe, tests) + + tests = test_addr([ __file__ ]) + assert s.wantClass(Bar, tests) + assert not s.wantClass(Foo, tests) + assert s.wantClass(TestMe, tests) + + def test_want_directory(self): + s = Selector(Config()) + assert s.wantDirectory('test') + assert not s.wantDirectory('test/whatever') + assert s.wantDirectory('whatever/test') + assert not s.wantDirectory('/some/path/to/unit_tests/support') + + # default src directory + assert s.wantDirectory('lib') + assert s.wantDirectory('src') + + # this looks on disk for support/foo, which is a package + here = os.path.abspath(os.path.dirname(__file__)) + support = os.path.join(here, 'support') + tp = os.path.normpath(os.path.join(support, 'foo')) + assert s.wantDirectory(tp) + # this looks for support, which is not a package + assert not s.wantDirectory(support) + + def test_want_file(self): + + #logging.getLogger('nose.selector').setLevel(logging.DEBUG) + #logging.basicConfig() + + c = Config() + c.where = [absdir(os.path.join(os.path.dirname(__file__), 'support'))] + base = c.where[0] + s = Selector(c) + + assert not s.wantFile('setup.py') + assert not s.wantFile('/some/path/to/setup.py') + assert not s.wantFile('ez_setup.py') + assert not s.wantFile('.test.py') + assert not s.wantFile('_test.py') + assert not s.wantFile('setup_something.py') + + assert s.wantFile('test.py') + assert s.wantFile('foo/test_foo.py') + assert s.wantFile('bar/baz/test.py', package='baz') + assert not s.wantFile('foo.py', package='bar.baz') + assert not s.wantFile('test_data.txt') + assert not s.wantFile('data.text', package='bar.bz') + assert not s.wantFile('bar/baz/__init__.py', package='baz') + + tests = test_addr([ 'test.py', 'other/file.txt' ], base) + assert s.wantFile(os.path.join(base, 'test.py'), tests=tests) + assert not s.wantFile(os.path.join(base,'foo/test_foo.py'), + tests=tests) + assert not s.wantFile(os.path.join(base,'bar/baz/test.py'), + package='baz', tests=tests) + # still not a python module... some plugin might want it, + # but the default selector doesn't + assert not s.wantFile(os.path.join(base,'other/file.txt'), + tests=tests) + + tests = test_addr([ 'a.module' ], base) + assert not s.wantFile(os.path.join(base, 'test.py'), + tests=tests) + assert not s.wantFile(os.path.join(base, 'foo/test_foo.py'), + tests=tests) + assert not s.wantFile(os.path.join(base, 'test-dir/test.py'), + package='baz', tests=tests) + assert not s.wantFile(os.path.join(base, 'other/file.txt'), + tests=tests) + assert s.wantFile('/path/to/a/module.py', tests=tests) + assert s.wantFile('/another/path/to/a/module/file.py', tests=tests) + assert not s.wantFile('/path/to/a/module/data/file.txt', tests=tests) + + def test_want_function(self): + def foo(): + pass + def test_foo(): + pass + def test_bar(): + pass + + s = Selector(Config()) + assert s.wantFunction(test_bar) + assert s.wantFunction(test_foo) + assert not s.wantFunction(foo) + + tests = test_addr([ ':test_bar' ]) + assert s.wantFunction(test_bar, tests) + assert not s.wantFunction(test_foo, tests) + assert not s.wantFunction(foo, tests) + + tests = test_addr([ __file__ ]) + assert s.wantFunction(test_bar, tests) + assert s.wantFunction(test_foo, tests) + assert not s.wantFunction(foo, tests) + + def test_want_method(self): + class Baz: + def test_me(self): + pass + def test_too(self): + pass + def other(self): + pass + + s = Selector(Config()) + + assert s.wantMethod(Baz.test_me) + assert s.wantMethod(Baz.test_too) + assert not s.wantMethod(Baz.other) + + tests = test_addr([ ':Baz.test_too' ]) + assert s.wantMethod(Baz.test_too, tests) + assert not s.wantMethod(Baz.test_me, tests) + assert not s.wantMethod(Baz.other, tests) + + tests = test_addr([ ':Baz' ]) + assert s.wantMethod(Baz.test_too, tests) + assert s.wantMethod(Baz.test_me, tests) + assert not s.wantMethod(Baz.other, tests) + + tests = test_addr([ ':Spaz' ]) + assert not s.wantMethod(Baz.test_too, tests) + assert not s.wantMethod(Baz.test_me, tests) + assert not s.wantMethod(Baz.other, tests) + + def test_want_module(self): + m = Mod('whatever') + m2 = Mod('this.that') + m3 = Mod('this.that.another') + m4 = Mod('this.that.another.one') + m5 = Mod('test.something') + m6 = Mod('a.test') + m7 = Mod('my_tests') + m8 = Mod('__main__') + + s = Selector(Config()) + assert not s.wantModule(m) + assert not s.wantModule(m2) + assert not s.wantModule(m3) + assert not s.wantModule(m4) + assert not s.wantModule(m5) + assert s.wantModule(m6) + assert s.wantModule(m7) + assert not s.wantModule(m8) + + tests = test_addr([ 'this.that.another' ]) + assert not s.wantModule(m, tests) + assert s.wantModule(m2, tests) + assert s.wantModule(m3, tests) + assert s.wantModule(m4, tests) + assert not s.wantModule(m5, tests) + assert not s.wantModule(m6, tests) + assert not s.wantModule(m7, tests) + assert not s.wantModule(m8, tests) + + def test_want_module_tests(self): + m = Mod('whatever') + m2 = Mod('this.that') + m3 = Mod('this.that.another') + m4 = Mod('this.that.another.one') + m5 = Mod('test.something') + m6 = Mod('a.test') + m7 = Mod('my_tests') + m8 = Mod('__main__') + + s = Selector(Config()) + assert not s.wantModuleTests(m) + assert not s.wantModuleTests(m2) + assert not s.wantModuleTests(m3) + assert not s.wantModuleTests(m4) + assert not s.wantModuleTests(m5) + assert s.wantModuleTests(m6) + assert s.wantModuleTests(m7) + assert s.wantModuleTests(m8) + + tests = test_addr([ 'this.that.another' ]) + assert not s.wantModuleTests(m, tests) + assert not s.wantModuleTests(m2, tests) + assert s.wantModuleTests(m3, tests) + assert s.wantModuleTests(m4, tests) + assert not s.wantModuleTests(m5, tests) + assert not s.wantModuleTests(m6, tests) + assert not s.wantModuleTests(m7, tests) + assert not s.wantModuleTests(m8, tests) + + def test_module_in_tests(self): + s = Selector(Config()) + # s.tests = [ 'ever', 'what', 'what.ever' ] + + w = Mod('what') + we = Mod('whatever') + w_e = Mod('what.ever') + w_n = Mod('what.not') + f_e = Mod('for.ever') + + tests = test_addr([ 'what' ]) + assert s.moduleInTests(w, tests) + assert s.moduleInTests(w, tests, True) + assert s.moduleInTests(w_e, tests) + assert s.moduleInTests(w_e, tests, True) + assert s.moduleInTests(w_n, tests) + assert s.moduleInTests(w_n, tests, True) + assert not s.moduleInTests(we, tests) + assert not s.moduleInTests(we, tests, True) + assert not s.moduleInTests(f_e, tests) + assert not s.moduleInTests(f_e, tests, True) + + tests = test_addr([ 'what.ever' ]) + assert not s.moduleInTests(w, tests) + assert s.moduleInTests(w, tests, True) + assert s.moduleInTests(w_e, tests) + assert s.moduleInTests(w_e, tests, True) + assert not s.moduleInTests(w_n, tests) + assert not s.moduleInTests(w_n, tests, True) + assert not s.moduleInTests(we, tests) + assert not s.moduleInTests(we, tests, True) + assert not s.moduleInTests(f_e, tests) + assert not s.moduleInTests(f_e, tests, True) + + tests = test_addr([ 'what.ever', 'what.not' ]) + assert not s.moduleInTests(w, tests) + assert s.moduleInTests(w, tests, True) + assert s.moduleInTests(w_e, tests) + assert s.moduleInTests(w_e, tests, True) + assert s.moduleInTests(w_n, tests) + assert s.moduleInTests(w_n, tests, True) + assert not s.moduleInTests(we, tests) + assert not s.moduleInTests(we, tests, True) + assert not s.moduleInTests(f_e, tests) + assert not s.moduleInTests(f_e, tests, True) + + def test_module_in_tests_file(self): + base = absdir(os.path.join(os.path.dirname(__file__), 'support')) + c = Config() + c.where = [base] + s = Selector(c) + + f = Mod('foo', file=base+'/foo/__init__.pyc') + t = Mod('test', path=base) + f_t_f = Mod('foo.test_foo', path=base) + d_t_t = Mod('test', path=base+'/test-dir') + + tests = test_addr([ 'test.py' ], base) + assert not s.moduleInTests(f, tests) + assert s.moduleInTests(t, tests) + assert not s.moduleInTests(f_t_f, tests) + assert not s.moduleInTests(d_t_t, tests) + + tests = test_addr([ 'foo/' ], base) + assert s.moduleInTests(f, tests) + assert s.moduleInTests(f_t_f, tests) + assert not s.moduleInTests(t, tests) + assert not s.moduleInTests(d_t_t, tests) + + tests = test_addr([ 'foo/test_foo.py' ], base) + assert not s.moduleInTests(f, tests) + assert s.moduleInTests(f_t_f, tests) + assert not s.moduleInTests(t, tests) + assert not s.moduleInTests(d_t_t, tests) + + tests = test_addr([ 'test-dir/test.py' ], base) + assert not s.moduleInTests(f, tests) + assert not s.moduleInTests(t, tests) + assert not s.moduleInTests(f_t_f, tests) + assert s.moduleInTests(d_t_t, tests) + + + +if __name__ == '__main__': + # log.setLevel(logging.DEBUG) + unittest.main() diff --git a/unit_tests/test_selector_plugins.py b/unit_tests/test_selector_plugins.py new file mode 100644 index 0000000..614c766 --- /dev/null +++ b/unit_tests/test_selector_plugins.py @@ -0,0 +1,38 @@ +import unittest +import nose.selector +import test_selector +from nose.config import Config +from nose.plugins.base import Plugin + +class TestSelectorPlugins(unittest.TestCase): + + def test_null_selector(self): + # run the test_selector.TestSelector tests with + # a null selector config'd in, should still all pass + class NullSelector(Plugin): + pass + + + def test_rejection(self): + class EvilSelector(Plugin): + def wantFile(self, filename, package=None): + if 'good' in filename: + return False + return None + + c = Config() + c.plugins = [ EvilSelector() ] + s = nose.selector.Selector(c) + s2 = nose.selector.Selector(Config()) + + assert s.wantFile('test_neutral.py') + assert s2.wantFile('test_neutral.py') + + assert s.wantFile('test_evil.py') + assert s2.wantFile('test_evil.py') + + assert not s.wantFile('test_good.py') + assert s2.wantFile('test_good.py') + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_suite.py b/unit_tests/test_suite.py new file mode 100644 index 0000000..4adf40a --- /dev/null +++ b/unit_tests/test_suite.py @@ -0,0 +1,37 @@ +import os +import unittest +from nose.config import Config + +class TestNoseSuite(unittest.TestCase): + + def setUp(self): + cwd = os.path.dirname(__file__) + self.support = os.path.abspath(os.path.join(cwd, 'support')) + + def test_module_suite_repr(self): + from mock import Bucket + from nose.suite import ModuleSuite + + loader = Bucket() + conf = Config() + Bucket.conf = conf + s = ModuleSuite(loader=loader, modulename='test', + filename=os.path.join(self.support, 'test.py')) + self.assertEqual("%s" % s, + "test module test in %s" % self.support) + s = ModuleSuite(loader=loader, modulename='foo.test_foo', + filename=os.path.join(self.support, 'foo', + 'test_foo.py')) + print s + self.assertEqual("%s" % s, + "test module foo.test_foo in %s" % self.support) + s = ModuleSuite(loader=loader, modulename='foo', + filename=os.path.join(self.support, 'foo')) + print s + self.assertEqual("%s" % s, + "test module foo in %s" % self.support) + + + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_tools.py b/unit_tests/test_tools.py new file mode 100644 index 0000000..d682291 --- /dev/null +++ b/unit_tests/test_tools.py @@ -0,0 +1,95 @@ +import time +import unittest +from nose.tools import * + +class TestTools(unittest.TestCase): + + def test_ok(self): + ok_(True) + try: + ok_(False, "message") + except AssertionError, e: + assert str(e) == "message" + else: + self.fail("ok_(False) did not raise assertion error") + + def test_eq(self): + eq_(1, 1) + try: + eq_(1, 0, "message") + except AssertionError, e: + assert str(e) == "message" + else: + self.fail("eq_(1, 0) did not raise assertion error") + try: + eq_(1, 0) + except AssertionError, e: + assert str(e) == "1 != 0" + else: + self.fail("eq_(1, 0) did not raise assertion error") + + def test_raises(self): + from nose.case import FunctionTestCase + + def raise_typeerror(): + raise TypeError("foo") + + def noraise(): + pass + + raise_good = raises(TypeError)(raise_typeerror) + raise_other = raises(ValueError)(raise_typeerror) + no_raise = raises(TypeError)(noraise) + + tc = FunctionTestCase(raise_good) + self.assertEqual(str(tc), "%s.%s" % (__name__, 'raise_typeerror')) + + raise_good() + try: + raise_other() + except TypeError, e: + pass + else: + self.fail("raises did pass through unwanted exception") + + try: + no_raise() + except AssertionError, e: + pass + else: + self.fail("raises did not raise assertion error on no exception") + + def test_timed(self): + + def too_slow(): + time.sleep(.3) + too_slow = timed(.2)(too_slow) + + def quick(): + time.sleep(.1) + quick = timed(.2)(quick) + + quick() + try: + too_slow() + except TimeExpired: + pass + else: + self.fail("Slow test did not throw TimeExpired") + + def test_make_decorator(self): + def func(): + pass + func.setup = 'setup' + func.teardown = 'teardown' + + def f1(): + pass + + f2 = make_decorator(func)(f1) + + assert f2.setup == 'setup' + assert f2.teardown == 'teardown' + +if __name__ == '__main__': + unittest.main() diff --git a/unit_tests/test_twisted.py b/unit_tests/test_twisted.py new file mode 100644 index 0000000..928afd7 --- /dev/null +++ b/unit_tests/test_twisted.py @@ -0,0 +1,65 @@ +from nose.tools import * +from nose.twistedtools import * + +from twisted.internet.defer import Deferred +from twisted.internet.error import DNSLookupError + +class CustomError(Exception): + pass + +# Should succeed unless python-hosting is down +@deferred() +def test_resolve(): + return reactor.resolve("nose.python-hosting.com") + +# Raises TypeError because the function does not return a Deferred +@raises(TypeError) +@deferred() +def test_raises_bad_return(): + reactor.resolve("nose.python-hosting.com") + +# Check we propagate twisted Failures as Exceptions +# (XXX this test might take some time: find something better?) +@raises(DNSLookupError) +@deferred() +def test_raises_twisted_error(): + return reactor.resolve("x.y.z") + +# Check we detect Exceptions inside the callback chain +@raises(CustomError) +@deferred(timeout=1.0) +def test_raises_callback_error(): + d = Deferred() + def raise_error(_): + raise CustomError() + def finish(): + d.callback(None) + d.addCallback(raise_error) + reactor.callLater(0.01, finish) + return d + +# Check we detect Exceptions inside the test body +@raises(CustomError) +@deferred(timeout=1.0) +def test_raises_plain_error(): + raise CustomError + +# The deferred is triggered before the timeout: ok +@deferred(timeout=1.0) +def test_timeout_ok(): + d = Deferred() + def finish(): + d.callback(None) + reactor.callLater(0.01, finish) + return d + +# The deferred is triggered after the timeout: failure +@raises(TimeExpired) +@deferred(timeout=0.1) +def test_timeout_expired(): + d = Deferred() + def finish(): + d.callback(None) + reactor.callLater(1.0, finish) + return d + diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py new file mode 100644 index 0000000..5f41282 --- /dev/null +++ b/unit_tests/test_utils.py @@ -0,0 +1,130 @@ +import unittest +import nose +import nose.case +from nose.util import absfile + +class TestUtils(unittest.TestCase): + + def test_file_like(self): + assert nose.file_like('a/file') + assert nose.file_like('file.py') + assert nose.file_like('/some/file.py') + assert not nose.file_like('a.file') + assert not nose.file_like('some.package') + assert nose.file_like('a-file') + assert not nose.file_like('test') + + def test_split_test_name(self): + assert nose.split_test_name('a.package:Some.method') == \ + (None, 'a.package', 'Some.method') + assert nose.split_test_name('some.module') == \ + (None, 'some.module', None) + assert nose.split_test_name('this/file.py:func') == \ + ('this/file.py', None, 'func') + assert nose.split_test_name('some/file.py') == \ + ('some/file.py', None, None) + assert nose.split_test_name(':Baz') == \ + (None, None, 'Baz') + + def test_split_test_name_windows(self): + # convenience + stn = nose.split_test_name + self.assertEqual(stn(r'c:\some\path.py:a_test'), + (r'c:\some\path.py', None, 'a_test')) + self.assertEqual(stn(r'c:\some\path.py'), + (r'c:\some\path.py', None, None)) + self.assertEqual(stn(r'c:/some/other/path.py'), + (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')) + try: + stn('c:something') + except ValueError: + pass + else: + self.fail("Ambiguous test name should throw ValueError") + + def test_test_address(self): + # test addresses are specified as + # package.module:class.method + # /path/to/file.py:class.method + # converted into 3-tuples (file, module, callable) + # all terms optional + class Foo: + def bar(self): + pass + def baz(): + pass + + f = Foo() + + class FooTC(unittest.TestCase): + def test_one(self): + pass + def test_two(self): + pass + + foo_funct = nose.case.FunctionTestCase(baz) + foo_functu = unittest.FunctionTestCase(baz) + + foo_mtc = nose.case.MethodTestCase(Foo, 'bar') + + me = absfile(__file__) + self.assertEqual(nose.test_address(baz), + (me, __name__, 'baz')) + assert nose.test_address(Foo) == (me, __name__, 'Foo') + assert nose.test_address(Foo.bar) == (me, __name__, + 'Foo.bar') + assert nose.test_address(f) == (me, __name__, 'Foo') + assert nose.test_address(f.bar) == (me, __name__, 'Foo.bar') + assert nose.test_address(nose) == (absfile(nose.__file__), 'nose') + + # test passing the actual test callable, as the + # missed test plugin must do + self.assertEqual(nose.test_address(FooTC('test_one')), + (me, __name__, 'FooTC.test_one')) + self.assertEqual(nose.test_address(foo_funct), + (me, __name__, 'baz')) + self.assertEqual(nose.test_address(foo_functu), + (me, __name__, 'baz')) + self.assertEqual(nose.test_address(foo_mtc), + (me, __name__, 'Foo.bar')) + + def test_tolist(self): + from nose.util import tolist + assert tolist('foo') == ['foo'] + assert tolist(['foo', 'bar']) == ['foo', 'bar'] + assert tolist('foo,bar') == ['foo', 'bar'] + self.assertEqual(tolist('.*foo/.*,.1'), ['.*foo/.*', '.1']) + + def test_try_run(self): + from nose.util import try_run + import imp + + def bar(): + pass + + def bar_m(mod): + pass + + class Bar: + def __call__(self): + pass + + class Bar_m: + def __call__(self, mod): + pass + + foo = imp.new_module('foo') + foo.bar = bar + foo.bar_m = bar_m + foo.i_bar = Bar() + foo.i_bar_m = Bar_m() + + try_run(foo, ('bar',)) + try_run(foo, ('bar_m',)) + try_run(foo, ('i_bar',)) + try_run(foo, ('i_bar_m',)) + +if __name__ == '__main__': + unittest.main() @@ -0,0 +1,198 @@ +import os +import re +from imp import load_source +from nose.selector import Selector, TestAddress, test_addr +from nose.config import Config +from nose.importer import _import +from nose.util import split_test_name + +#import logging +#logging.basicConfig() +#logging.getLogger('').setLevel(0) + +conf = Config() +selector = Selector(conf) + + +def ispackage(dirname): + return os.path.exists(os.path.join(dirname, '__init__.py')) + + +def ispackageinit(module): + filename = module.__file__ + base, ext = os.path.splitext(os.path.basename(filename)) + return base == '__init__' and ext.startswith('.py') + + +def module_name(filename, package=None): + base, junk = os.path.splitext(filename) + if package is None: + return base + return "%s.%s" % (package, base) + + +class ModuleSuite: + def __init__(self, name, path, loader, working_dir, tests): + # FIXME name -> modulename + # path -> filename + # document tests vs _tests + self.name = name + self.path = path + self.module = None + self.loader = loader + self.working_dir = working_dir + self.tests = tests + self._collected = False + self._tests = [] + + def __nonzero__(self): + self.collectTests() + return bool(self._tests) + + def __len__(self): + self.collectTests() + return len(self._tests) + + def __iter__(self): + self.collectTests() + return iter(self._tests) + + def __str__(self): + return "ModuleSuite(%s, %s)" % (self.name, self.path) + + def addTest(self, test): + # depth-first? + if test: + self._tests.append(test) + + def collectTests(self): + # print "Collect Tests %s" % self + if self._collected or self._tests: + return + self._collected = True + self._tests = [] + if self.module is None: + # We know the exact source of each module so why not? + # We still need to add the module's parent dir (up to the top + # if it's a package) to sys.path first, though + self.module = load_source(self.name, self.path) + for test in self.loader.loadTestsFromModule(self.module, self.tests): + self.addTest(test) + + def run(self, result): + # startTest + self.collectTests() + if not self: + return + # FIXME this needs to be a real run() with exc handling + self.setUp() + for test in self._tests: + test(result) + self.tearDown() + # stopTest() + + def setUp(self): + print "SETUP %s" % self + + def tearDown(self): + print "TEARDOWN %s" % self + + +class TestLoader: + + # FIXME move collectTests to DirectorySuite + + def collectTests(self, working_dir, names=None): + if not os.path.isabs(working_dir): + working_dir = os.path.join(os.getcwd(), working_dir) + tests = test_addr(names, working_dir) + return self.findTests(working_dir, tests=tests) + + def findTests(self, working_dir, tests=None, package=None): + for dirpath, dirnames, filenames in os.walk(working_dir): + + # FIXME first sort dirnames into test-last order + + to_remove = set() + packages = [] + for dirname in dirnames: + + # FIXME if it looks like a lib dir, continue in + # FIXME and add it to sys.path + + remove = True + ldir = os.path.join(dirpath, dirname) + if selector.wantDirectory(ldir, tests=tests): + if ispackage(ldir): + remove = True + # we'll yield a ModuleSuite later + packages.append((dirname, + os.path.join(ldir, '__init__.py'))) + else: + remove = False + # print "Continue into %s" % ldir + if remove: + to_remove.add(dirname) + for dirname in to_remove: + dirnames.remove(dirname) + + # Process files after dirs so that any lib dirs will + # already be in sys.path before we start importing files + + for filename in filenames: + if filename.endswith('.pyc') or filename.endswith('.pyo'): + continue + lname = os.path.join(dirpath, filename) + if selector.wantFile(lname, package=package, tests=tests): + # print "**", lname + yield ModuleSuite( + name=module_name(filename, package=package), + path=lname, loader=self, + working_dir=working_dir, + tests=tests) + # FIXME yield a ModuleSuite if it's a python module + # FIXME yield a FileSuite if it's not + # yield ModuleSuites for all packages + for name, path in packages: + yield ModuleSuite( + name=module_name(name, package=package), + path=path, loader=self, + working_dir=working_dir, + tests=tests) + # At this point we're diving into directories that aren't packages + # so if we think we are in a package, we have to forget the + # package name, lest modules in the directory think their names + # are package.foo when they are really just foo + package = None + + def loadTestsFromModule(self, module, tests=None): + """Construct a TestSuite containing all of the tests in + the module, including tests in the package if it is a package + """ + # print "loadTestsFromModule %s" % module + if ispackageinit(module): + path = os.path.dirname(module.__file__) + for test in self.findTests(path, tests, package=module.__name__): + # FIXME + print " ", test + return [] + +if __name__ == '__main__': + import sys + l = TestLoader() + for test in l.collectTests('unit_tests/support', sys.argv[1:]): + print test + test.run('whatever') + #mods = sys.modules.keys()[:] + #mods.sort() + #print mods + +# note these testable possibilities +# need a test for each of these +#'test.py' => 'support/test.py' +#'foo' => 'support/foo' +#'test-dir/test.py' => 'support/test-dir/test.py' +#'test-dir' => 'support/test-dir/test.py' +#'test-dir/' => 'support/test-dir/test.py' +#'test' => 'support/test.py' +#'foo.bar' => 'support/foo/bar' |