summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS7
-rw-r--r--CHANGELOG255
-rw-r--r--MANIFEST.in10
-rw-r--r--NEWS19
-rw-r--r--NEWS_0.959
-rw-r--r--NOTES30
-rw-r--r--README.txt287
-rw-r--r--TODO_0_974
-rw-r--r--examples/attrib_plugin.py82
-rw-r--r--examples/html_plugin/htmlplug.py88
-rw-r--r--examples/html_plugin/setup.py24
-rw-r--r--examples/plugin/plug.py4
-rw-r--r--examples/plugin/setup.py27
-rw-r--r--ez_setup.py219
-rw-r--r--index.html.tpl251
-rw-r--r--lgpl.txt504
-rw-r--r--nose/__init__.py309
-rw-r--r--nose/case.py147
-rw-r--r--nose/commands.py117
-rw-r--r--nose/config.py70
-rw-r--r--nose/core.py481
-rw-r--r--nose/exc.py11
-rw-r--r--nose/importer.py131
-rw-r--r--nose/inspector.py202
-rw-r--r--nose/loader.py370
-rw-r--r--nose/plugins/__init__.py151
-rw-r--r--nose/plugins/attrib.py177
-rw-r--r--nose/plugins/base.py409
-rw-r--r--nose/plugins/cover.py139
-rw-r--r--nose/plugins/doctests.py121
-rw-r--r--nose/plugins/missed.py51
-rw-r--r--nose/plugins/prof.py79
-rw-r--r--nose/proxy.py109
-rw-r--r--nose/result.py250
-rw-r--r--nose/selector.py467
-rw-r--r--nose/suite.py264
-rw-r--r--nose/tools.py103
-rw-r--r--nose/twistedtools.py144
-rw-r--r--nose/util.py240
-rw-r--r--notes59
-rwxr-xr-xscripts/mkindex.py54
-rwxr-xr-xscripts/mkrelease.py94
-rwxr-xr-xscripts/mkwiki.py146
-rw-r--r--setup.cfg12
-rw-r--r--setup.py62
-rw-r--r--unit_tests/helpers.py6
-rw-r--r--unit_tests/mock.py53
-rw-r--r--unit_tests/support/foo/__init__.py7
-rw-r--r--unit_tests/support/foo/bar/__init__.py1
-rw-r--r--unit_tests/support/foo/bar/buz.py8
-rw-r--r--unit_tests/support/foo/doctests.txt7
-rw-r--r--unit_tests/support/foo/test_foo.py1
-rw-r--r--unit_tests/support/foo/tests/dir_test_file.py3
-rw-r--r--unit_tests/support/other/file.txt1
-rw-r--r--unit_tests/support/pkgorg/lib/modernity.py1
-rw-r--r--unit_tests/support/pkgorg/tests/test_mod.py4
-rwxr-xr-xunit_tests/support/script.py3
-rw-r--r--unit_tests/support/test-dir/test.py1
-rw-r--r--unit_tests/support/test.py13
-rw-r--r--unit_tests/test_cases.py54
-rw-r--r--unit_tests/test_collector.py80
-rw-r--r--unit_tests/test_config.py36
-rw-r--r--unit_tests/test_core.py41
-rw-r--r--unit_tests/test_importer.py53
-rw-r--r--unit_tests/test_inspector.py125
-rw-r--r--unit_tests/test_lazy_suite.py50
-rw-r--r--unit_tests/test_loader.py254
-rw-r--r--unit_tests/test_logging.py40
-rw-r--r--unit_tests/test_plugin_interfaces.py44
-rw-r--r--unit_tests/test_plugins.py455
-rw-r--r--unit_tests/test_proxy.py193
-rw-r--r--unit_tests/test_result.py163
-rw-r--r--unit_tests/test_selector.py357
-rw-r--r--unit_tests/test_selector_plugins.py38
-rw-r--r--unit_tests/test_suite.py37
-rw-r--r--unit_tests/test_tools.py95
-rw-r--r--unit_tests/test_twisted.py65
-rw-r--r--unit_tests/test_utils.py130
-rw-r--r--work.py198
79 files changed, 9526 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..509d893
--- /dev/null
+++ b/AUTHORS
@@ -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
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..4ec2e97
--- /dev/null
+++ b/NEWS
@@ -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...
diff --git a/NOTES b/NOTES
new file mode 100644
index 0000000..77ca191
--- /dev/null
+++ b/NOTES
@@ -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)
diff --git a/notes b/notes
new file mode 100644
index 0000000..7854e0f
--- /dev/null
+++ b/notes
@@ -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()
diff --git a/work.py b/work.py
new file mode 100644
index 0000000..75b8258
--- /dev/null
+++ b/work.py
@@ -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'