2a380bf10SimaromThis plugin adds a test id (like #1) to each test name output. After
3a380bf10Simaromyou've run once to generate test ids, you can re-run individual
4a380bf10Simaromtests by activating the plugin and passing the ids (with or
5a380bf10Simaromwithout the # prefix) instead of test names.
7a380bf10SimaromFor example, if your normal test run looks like::
9a380bf10Simarom  % nosetests -v
10a380bf10Simarom  tests.test_a ... ok
11a380bf10Simarom  tests.test_b ... ok
12a380bf10Simarom  tests.test_c ... ok
14a380bf10SimaromWhen adding ``--with-id`` you'll see::
16a380bf10Simarom  % nosetests -v --with-id
17a380bf10Simarom  #1 tests.test_a ... ok
18a380bf10Simarom  #2 tests.test_b ... ok
19a380bf10Simarom  #3 tests.test_c ... ok
21a380bf10SimaromThen you can re-run individual tests by supplying just an id number::
23a380bf10Simarom  % nosetests -v --with-id 2
24a380bf10Simarom  #2 tests.test_b ... ok
26a380bf10SimaromYou can also pass multiple id numbers::
28a380bf10Simarom  % nosetests -v --with-id 2 3
29a380bf10Simarom  #2 tests.test_b ... ok
30a380bf10Simarom  #3 tests.test_c ... ok
32a380bf10SimaromSince most shells consider '#' a special character, you can leave it out when
33a380bf10Simaromspecifying a test id.
35a380bf10SimaromNote that when run without the -v switch, no special output is displayed, but
36a380bf10Simaromthe ids file is still written.
38a380bf10SimaromLooping over failed tests
41a380bf10SimaromThis plugin also adds a mode that will direct the test runner to record
42a380bf10Simaromfailed tests. Subsequent test runs will then run only the tests that failed
43a380bf10Simaromlast time. Activate this mode with the ``--failed`` switch::
45a380bf10Simarom % nosetests -v --failed
46a380bf10Simarom #1 test.test_a ... ok
47a380bf10Simarom #2 test.test_b ... ERROR
48a380bf10Simarom #3 test.test_c ... FAILED
49a380bf10Simarom #4 test.test_d ... ok
51a380bf10SimaromOn the second run, only tests #2 and #3 will run::
53a380bf10Simarom % nosetests -v --failed
54a380bf10Simarom #2 test.test_b ... ERROR
55a380bf10Simarom #3 test.test_c ... FAILED
57a380bf10SimaromAs you correct errors and tests pass, they'll drop out of subsequent runs.
61a380bf10Simarom % nosetests -v --failed
62a380bf10Simarom #2 test.test_b ... ok
63a380bf10Simarom #3 test.test_c ... FAILED
67a380bf10Simarom % nosetests -v --failed
68a380bf10Simarom #3 test.test_c ... FAILED
70a380bf10SimaromWhen all tests pass, the full set will run on the next invocation.
74a380bf10Simarom % nosetests -v --failed
75a380bf10Simarom #3 test.test_c ... ok
79a380bf10Simarom % nosetests -v --failed
80a380bf10Simarom #1 test.test_a ... ok
81a380bf10Simarom #2 test.test_b ... ok
82a380bf10Simarom #3 test.test_c ... ok
83a380bf10Simarom #4 test.test_d ... ok
85a380bf10Simarom.. note ::
87a380bf10Simarom  If you expect to use ``--failed`` regularly, it's a good idea to always run
88a380bf10Simarom  using the ``--with-id`` option. This will ensure that an id file is always
89a380bf10Simarom  created, allowing you to add ``--failed`` to the command line as soon as
90a380bf10Simarom  you have failing tests. Otherwise, your first run using ``--failed`` will
91a380bf10Simarom  (perhaps surprisingly) run *all* tests, because there won't be an id file
92a380bf10Simarom  containing the record of failed tests from your previous run.
95a380bf10Simarom__test__ = False
97a380bf10Simaromimport logging
98a380bf10Simaromimport os
99a380bf10Simaromfrom nose.plugins import Plugin
100a380bf10Simaromfrom nose.util import src, set
103a380bf10Simarom    from pickle import dump, load
104a380bf10Simaromexcept ImportError:
105a380bf10Simarom    from pickle import dump, load
107a380bf10Simaromlog = logging.getLogger(__name__)
110a380bf10Simaromclass TestId(Plugin):
111a380bf10Simarom    """
112a380bf10Simarom    Activate to add a test id (like #1) to each test name output. Activate
113a380bf10Simarom    with --failed to rerun failing tests only.
114a380bf10Simarom    """
115a380bf10Simarom    name = 'id'
116a380bf10Simarom    idfile = None
117a380bf10Simarom    collecting = True
118a380bf10Simarom    loopOnFailed = False
120a380bf10Simarom    def options(self, parser, env):
121a380bf10Simarom        """Register commandline options.
122a380bf10Simarom        """
123a380bf10Simarom        Plugin.options(self, parser, env)
124a380bf10Simarom        parser.add_option('--id-file', action='store', dest='testIdFile',
125a380bf10Simarom                          default='.noseids', metavar="FILE",
126a380bf10Simarom                          help="Store test ids found in test runs in this "
127a380bf10Simarom                          "file. Default is the file .noseids in the "
128a380bf10Simarom                          "working directory.")
129a380bf10Simarom        parser.add_option('--failed', action='store_true',
130a380bf10Simarom                          dest='failed', default=False,
131a380bf10Simarom                          help="Run the tests that failed in the last "
132a380bf10Simarom                          "test run.")
134a380bf10Simarom    def configure(self, options, conf):
135a380bf10Simarom        """Configure plugin.
136a380bf10Simarom        """
137a380bf10Simarom        Plugin.configure(self, options, conf)
138a380bf10Simarom        if options.failed:
139a380bf10Simarom            self.enabled = True
140a380bf10Simarom            self.loopOnFailed = True
141a380bf10Simarom            log.debug("Looping on failed tests")
142a380bf10Simarom        self.idfile = os.path.expanduser(options.testIdFile)
143a380bf10Simarom        if not os.path.isabs(self.idfile):
144a380bf10Simarom            self.idfile = os.path.join(conf.workingDir, self.idfile)
145a380bf10Simarom        self.id = 1
146a380bf10Simarom        # Ids and tests are mirror images: ids are {id: test address} and
147a380bf10Simarom        # tests are {test address: id}
148a380bf10Simarom        self.ids = {}
149a380bf10Simarom        self.tests = {}
150a380bf10Simarom        self.failed = []
151a380bf10Simarom        self.source_names = []
152a380bf10Simarom        # used to track ids seen when tests is filled from
153a380bf10Simarom        # loaded ids file
154a380bf10Simarom        self._seen = {}
155a380bf10Simarom        self._write_hashes = conf.verbosity >= 2
157a380bf10Simarom    def finalize(self, result):
158a380bf10Simarom        """Save new ids file, if needed.
159a380bf10Simarom        """
160a380bf10Simarom        if result.wasSuccessful():
161a380bf10Simarom            self.failed = []
162a380bf10Simarom        if self.collecting:
163a380bf10Simarom            ids = dict(list(zip(list(self.tests.values()), list(self.tests.keys()))))
164a380bf10Simarom        else:
165a380bf10Simarom            ids = self.ids
166a380bf10Simarom        fh = open(self.idfile, 'wb')
167a380bf10Simarom        dump({'ids': ids,
168a380bf10Simarom              'failed': self.failed,
169a380bf10Simarom              'source_names': self.source_names}, fh)
170a380bf10Simarom        fh.close()
171a380bf10Simarom        log.debug('Saved test ids: %s, failed %s to %s',
172a380bf10Simarom                  ids, self.failed, self.idfile)
174a380bf10Simarom    def loadTestsFromNames(self, names, module=None):
175a380bf10Simarom        """Translate ids in the list of requested names into their
176a380bf10Simarom        test addresses, if they are found in my dict of tests.
177a380bf10Simarom        """
178a380bf10Simarom        log.debug('ltfn %s %s', names, module)
179a380bf10Simarom        try:
180a380bf10Simarom            fh = open(self.idfile, 'rb')
181a380bf10Simarom            data = load(fh)
182a380bf10Simarom            if 'ids' in data:
183a380bf10Simarom                self.ids = data['ids']
184a380bf10Simarom                self.failed = data['failed']
185a380bf10Simarom                self.source_names = data['source_names']
186a380bf10Simarom            else:
187a380bf10Simarom                # old ids field
188a380bf10Simarom                self.ids = data
189a380bf10Simarom                self.failed = []
190a380bf10Simarom                self.source_names = names
191a380bf10Simarom            if self.ids:
192a380bf10Simarom                self.id = max(self.ids) + 1
193a380bf10Simarom                self.tests = dict(list(zip(list(self.ids.values()), list(self.ids.keys()))))
194a380bf10Simarom            else:
195a380bf10Simarom                self.id = 1
196a380bf10Simarom            log.debug(
197a380bf10Simarom                'Loaded test ids %s tests %s failed %s sources %s from %s',
198a380bf10Simarom                self.ids, self.tests, self.failed, self.source_names,
199a380bf10Simarom                self.idfile)
200a380bf10Simarom            fh.close()
201a380bf10Simarom        except IOError:
202a380bf10Simarom            log.debug('IO error reading %s', self.idfile)
204a380bf10Simarom        if self.loopOnFailed and self.failed:
205a380bf10Simarom            self.collecting = False
206a380bf10Simarom            names = self.failed
207a380bf10Simarom            self.failed = []
208a380bf10Simarom        # I don't load any tests myself, only translate names like '#2'
209a380bf10Simarom        # into the associated test addresses
210a380bf10Simarom        translated = []
211a380bf10Simarom        new_source = []
212a380bf10Simarom        really_new = []
213a380bf10Simarom        for name in names:
214a380bf10Simarom            trans = self.tr(name)
215a380bf10Simarom            if trans != name:
216a380bf10Simarom                translated.append(trans)
217a380bf10Simarom            else:
218a380bf10Simarom                new_source.append(name)
219a380bf10Simarom        # names that are not ids and that are not in the current
220a380bf10Simarom        # list of source names go into the list for next time
221a380bf10Simarom        if new_source:
222a380bf10Simarom            new_set = set(new_source)
223a380bf10Simarom            old_set = set(self.source_names)
224a380bf10Simarom            log.debug("old: %s new: %s", old_set, new_set)
225a380bf10Simarom            really_new = [s for s in new_source
226a380bf10Simarom                          if not s in old_set]
227a380bf10Simarom            if really_new:
228a380bf10Simarom                # remember new sources
229a380bf10Simarom                self.source_names.extend(really_new)
230a380bf10Simarom            if not translated:
231a380bf10Simarom                # new set of source names, no translations
232a380bf10Simarom                # means "run the requested tests"
233a380bf10Simarom                names = new_source
234a380bf10Simarom        else:
235a380bf10Simarom            # no new names to translate and add to id set
236a380bf10Simarom            self.collecting = False
237a380bf10Simarom        log.debug("translated: %s new sources %s names %s",
238a380bf10Simarom                  translated, really_new, names)
239a380bf10Simarom        return (None, translated + really_new or names)
241a380bf10Simarom    def makeName(self, addr):
242a380bf10Simarom        log.debug("Make name %s", addr)
243a380bf10Simarom        filename, module, call = addr
244a380bf10Simarom        if filename is not None:
245a380bf10Simarom            head = src(filename)
246a380bf10Simarom        else:
247a380bf10Simarom            head = module
248a380bf10Simarom        if call is not None:
249a380bf10Simarom            return "%s:%s" % (head, call)
250a380bf10Simarom        return head
252a380bf10Simarom    def setOutputStream(self, stream):
253a380bf10Simarom        """Get handle on output stream so the plugin can print id #s
254a380bf10Simarom        """
255a380bf10Simarom        self.stream = stream
257a380bf10Simarom    def startTest(self, test):
258a380bf10Simarom        """Maybe output an id # before the test name.
260a380bf10Simarom        Example output::
262a380bf10Simarom          #1 test.test ... ok
263a380bf10Simarom          #2 test.test_two ... ok
265a380bf10Simarom        """
266a380bf10Simarom        adr = test.address()
267a380bf10Simarom        log.debug('start test %s (%s)', adr, adr in self.tests)
268a380bf10Simarom        if adr in self.tests:
269a380bf10Simarom            if adr in self._seen:
270a380bf10Simarom                self.write('   ')
271a380bf10Simarom            else:
272a380bf10Simarom                self.write('#%s ' % self.tests[adr])
273a380bf10Simarom                self._seen[adr] = 1
274a380bf10Simarom            return
275a380bf10Simarom        self.tests[adr] = self.id
276a380bf10Simarom        self.write('#%s ' % self.id)
277a380bf10Simarom        self.id += 1
279a380bf10Simarom    def afterTest(self, test):
280a380bf10Simarom        # None means test never ran, False means failed/err
281a380bf10Simarom        if test.passed is False:
282a380bf10Simarom            try:
283a380bf10Simarom                key = str(self.tests[test.address()])
284a380bf10Simarom            except KeyError:
285a380bf10Simarom                # never saw this test -- startTest didn't run
286a380bf10Simarom                pass
287a380bf10Simarom            else:
288a380bf10Simarom                if key not in self.failed:
289a380bf10Simarom                    self.failed.append(key)
291a380bf10Simarom    def tr(self, name):
292a380bf10Simarom        log.debug("tr '%s'", name)
293a380bf10Simarom        try:
294a380bf10Simarom            key = int(name.replace('#', ''))
295a380bf10Simarom        except ValueError:
296a380bf10Simarom            return name
297a380bf10Simarom        log.debug("Got key %s", key)
298a380bf10Simarom        # I'm running tests mapped from the ids file,
299a380bf10Simarom        # not collecting new ones
300a380bf10Simarom        if key in self.ids:
301a380bf10Simarom            return self.makeName(self.ids[key])
302a380bf10Simarom        return name
304a380bf10Simarom    def write(self, output):
305a380bf10Simarom        if self._write_hashes:
306a380bf10Simarom            self.stream.write(output)