123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311 |
- """
- This plugin adds a test id (like #1) to each test name output. After
- you've run once to generate test ids, you can re-run individual
- tests by activating the plugin and passing the ids (with or
- without the # prefix) instead of test names.
- For example, if your normal test run looks like::
- % nosetests -v
- tests.test_a ... ok
- tests.test_b ... ok
- tests.test_c ... ok
- When adding ``--with-id`` you'll see::
- % nosetests -v --with-id
- #1 tests.test_a ... ok
- #2 tests.test_b ... ok
- #3 tests.test_c ... ok
- Then you can re-run individual tests by supplying just an id number::
- % nosetests -v --with-id 2
- #2 tests.test_b ... ok
- You can also pass multiple id numbers::
- % nosetests -v --with-id 2 3
- #2 tests.test_b ... ok
- #3 tests.test_c ... ok
-
- Since most shells consider '#' a special character, you can leave it out when
- specifying a test id.
- Note that when run without the -v switch, no special output is displayed, but
- the ids file is still written.
- Looping over failed tests
- -------------------------
- This plugin also adds a mode that will direct the test runner to record
- failed tests. Subsequent test runs will then run only the tests that failed
- last time. Activate this mode with the ``--failed`` switch::
- % nosetests -v --failed
- #1 test.test_a ... ok
- #2 test.test_b ... ERROR
- #3 test.test_c ... FAILED
- #4 test.test_d ... ok
-
- On the second run, only tests #2 and #3 will run::
- % nosetests -v --failed
- #2 test.test_b ... ERROR
- #3 test.test_c ... FAILED
- As you correct errors and tests pass, they'll drop out of subsequent runs.
- First::
- % nosetests -v --failed
- #2 test.test_b ... ok
- #3 test.test_c ... FAILED
- Second::
- % nosetests -v --failed
- #3 test.test_c ... FAILED
- When all tests pass, the full set will run on the next invocation.
- First::
- % nosetests -v --failed
- #3 test.test_c ... ok
- Second::
-
- % nosetests -v --failed
- #1 test.test_a ... ok
- #2 test.test_b ... ok
- #3 test.test_c ... ok
- #4 test.test_d ... ok
- .. note ::
- If you expect to use ``--failed`` regularly, it's a good idea to always run
- using the ``--with-id`` option. This will ensure that an id file is always
- created, allowing you to add ``--failed`` to the command line as soon as
- you have failing tests. Otherwise, your first run using ``--failed`` will
- (perhaps surprisingly) run *all* tests, because there won't be an id file
- containing the record of failed tests from your previous run.
-
- """
- __test__ = False
- import logging
- import os
- from nose.plugins import Plugin
- from nose.util import src, set
- try:
- from cPickle import dump, load
- except ImportError:
- from pickle import dump, load
- log = logging.getLogger(__name__)
- class TestId(Plugin):
- """
- Activate to add a test id (like #1) to each test name output. Activate
- with --failed to rerun failing tests only.
- """
- name = 'id'
- idfile = None
- collecting = True
- loopOnFailed = False
- def options(self, parser, env):
- """Register commandline options.
- """
- Plugin.options(self, parser, env)
- parser.add_option('--id-file', action='store', dest='testIdFile',
- default='.noseids', metavar="FILE",
- help="Store test ids found in test runs in this "
- "file. Default is the file .noseids in the "
- "working directory.")
- parser.add_option('--failed', action='store_true',
- dest='failed', default=False,
- help="Run the tests that failed in the last "
- "test run.")
- def configure(self, options, conf):
- """Configure plugin.
- """
- Plugin.configure(self, options, conf)
- if options.failed:
- self.enabled = True
- self.loopOnFailed = True
- log.debug("Looping on failed tests")
- self.idfile = os.path.expanduser(options.testIdFile)
- if not os.path.isabs(self.idfile):
- self.idfile = os.path.join(conf.workingDir, self.idfile)
- self.id = 1
- # Ids and tests are mirror images: ids are {id: test address} and
- # tests are {test address: id}
- self.ids = {}
- self.tests = {}
- self.failed = []
- self.source_names = []
- # used to track ids seen when tests is filled from
- # loaded ids file
- self._seen = {}
- self._write_hashes = conf.verbosity >= 2
- def finalize(self, result):
- """Save new ids file, if needed.
- """
- if result.wasSuccessful():
- self.failed = []
- if self.collecting:
- ids = dict(list(zip(list(self.tests.values()), list(self.tests.keys()))))
- else:
- ids = self.ids
- fh = open(self.idfile, 'wb')
- dump({'ids': ids,
- 'failed': self.failed,
- 'source_names': self.source_names}, fh)
- fh.close()
- log.debug('Saved test ids: %s, failed %s to %s',
- ids, self.failed, self.idfile)
- def loadTestsFromNames(self, names, module=None):
- """Translate ids in the list of requested names into their
- test addresses, if they are found in my dict of tests.
- """
- log.debug('ltfn %s %s', names, module)
- try:
- fh = open(self.idfile, 'rb')
- data = load(fh)
- if 'ids' in data:
- self.ids = data['ids']
- self.failed = data['failed']
- self.source_names = data['source_names']
- else:
- # old ids field
- self.ids = data
- self.failed = []
- self.source_names = names
- if self.ids:
- self.id = max(self.ids) + 1
- self.tests = dict(list(zip(list(self.ids.values()), list(self.ids.keys()))))
- else:
- self.id = 1
- log.debug(
- 'Loaded test ids %s tests %s failed %s sources %s from %s',
- self.ids, self.tests, self.failed, self.source_names,
- self.idfile)
- fh.close()
- except ValueError, e:
- # load() may throw a ValueError when reading the ids file, if it
- # was generated with a newer version of Python than we are currently
- # running.
- log.debug('Error loading %s : %s', self.idfile, str(e))
- except IOError:
- log.debug('IO error reading %s', self.idfile)
- if self.loopOnFailed and self.failed:
- self.collecting = False
- names = self.failed
- self.failed = []
- # I don't load any tests myself, only translate names like '#2'
- # into the associated test addresses
- translated = []
- new_source = []
- really_new = []
- for name in names:
- trans = self.tr(name)
- if trans != name:
- translated.append(trans)
- else:
- new_source.append(name)
- # names that are not ids and that are not in the current
- # list of source names go into the list for next time
- if new_source:
- new_set = set(new_source)
- old_set = set(self.source_names)
- log.debug("old: %s new: %s", old_set, new_set)
- really_new = [s for s in new_source
- if not s in old_set]
- if really_new:
- # remember new sources
- self.source_names.extend(really_new)
- if not translated:
- # new set of source names, no translations
- # means "run the requested tests"
- names = new_source
- else:
- # no new names to translate and add to id set
- self.collecting = False
- log.debug("translated: %s new sources %s names %s",
- translated, really_new, names)
- return (None, translated + really_new or names)
- def makeName(self, addr):
- log.debug("Make name %s", addr)
- filename, module, call = addr
- if filename is not None:
- head = src(filename)
- else:
- head = module
- if call is not None:
- return "%s:%s" % (head, call)
- return head
- def setOutputStream(self, stream):
- """Get handle on output stream so the plugin can print id #s
- """
- self.stream = stream
- def startTest(self, test):
- """Maybe output an id # before the test name.
- Example output::
- #1 test.test ... ok
- #2 test.test_two ... ok
- """
- adr = test.address()
- log.debug('start test %s (%s)', adr, adr in self.tests)
- if adr in self.tests:
- if adr in self._seen:
- self.write(' ')
- else:
- self.write('#%s ' % self.tests[adr])
- self._seen[adr] = 1
- return
- self.tests[adr] = self.id
- self.write('#%s ' % self.id)
- self.id += 1
- def afterTest(self, test):
- # None means test never ran, False means failed/err
- if test.passed is False:
- try:
- key = str(self.tests[test.address()])
- except KeyError:
- # never saw this test -- startTest didn't run
- pass
- else:
- if key not in self.failed:
- self.failed.append(key)
- def tr(self, name):
- log.debug("tr '%s'", name)
- try:
- key = int(name.replace('#', ''))
- except ValueError:
- return name
- log.debug("Got key %s", key)
- # I'm running tests mapped from the ids file,
- # not collecting new ones
- if key in self.ids:
- return self.makeName(self.ids[key])
- return name
- def write(self, output):
- if self._write_hashes:
- self.stream.write(output)
|