~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/__init__.py

Merge bzr.dev to resolve news conflict

Show diffs side-by-side

added added

removed removed

Lines of Context:
36
36
import errno
37
37
import itertools
38
38
import logging
39
 
import math
40
39
import os
 
40
import platform
41
41
import pprint
42
42
import random
43
43
import re
71
71
    lock as _mod_lock,
72
72
    memorytree,
73
73
    osutils,
74
 
    progress,
 
74
    pyutils,
75
75
    ui,
76
76
    urlutils,
77
77
    registry,
90
90
except ImportError:
91
91
    # lsprof not available
92
92
    pass
93
 
from bzrlib.merge import merge_inner
94
93
import bzrlib.merge3
95
94
import bzrlib.plugin
96
 
from bzrlib.smart import client, request, server
 
95
from bzrlib.smart import client, request
97
96
import bzrlib.store
98
97
from bzrlib import symbol_versioning
99
98
from bzrlib.symbol_versioning import (
117
116
from bzrlib.ui import NullProgressView
118
117
from bzrlib.ui.text import TextUIFactory
119
118
import bzrlib.version_info_formats.format_custom
120
 
from bzrlib.workingtree import WorkingTree, WorkingTreeFormat2
121
119
 
122
120
# Mark this python module as being part of the implementation
123
121
# of unittest: this gives us better tracebacks where the last
194
192
        self.count = 0
195
193
        self._overall_start_time = time.time()
196
194
        self._strict = strict
 
195
        self._first_thread_leaker_id = None
 
196
        self._tests_leaking_threads_count = 0
 
197
        self._traceback_from_test = None
197
198
 
198
199
    def stopTestRun(self):
199
200
        run = self.testsRun
238
239
            ok = self.wasStrictlySuccessful()
239
240
        else:
240
241
            ok = self.wasSuccessful()
241
 
        if TestCase._first_thread_leaker_id:
 
242
        if self._first_thread_leaker_id:
242
243
            self.stream.write(
243
244
                '%s is leaking threads among %d leaking tests.\n' % (
244
 
                TestCase._first_thread_leaker_id,
245
 
                TestCase._leaking_threads_tests))
 
245
                self._first_thread_leaker_id,
 
246
                self._tests_leaking_threads_count))
246
247
            # We don't report the main thread as an active one.
247
248
            self.stream.write(
248
249
                '%d non-main threads were left active in the end.\n'
249
 
                % (TestCase._active_threads - 1))
 
250
                % (len(self._active_threads) - 1))
250
251
 
251
252
    def getDescription(self, test):
252
253
        return test.id()
259
260
 
260
261
    def _elapsedTestTimeString(self):
261
262
        """Return a time string for the overall time the current test has taken."""
262
 
        return self._formatTime(time.time() - self._start_time)
 
263
        return self._formatTime(self._delta_to_float(
 
264
            self._now() - self._start_datetime))
263
265
 
264
266
    def _testTimeString(self, testCase):
265
267
        benchmark_time = self._extractBenchmarkTime(testCase)
279
281
        what = re.sub(r'^bzrlib\.tests\.', '', what)
280
282
        return what
281
283
 
 
284
    # GZ 2010-10-04: Cloned tests may end up harmlessly calling this method
 
285
    #                multiple times in a row, because the handler is added for
 
286
    #                each test but the container list is shared between cases.
 
287
    #                See lp:498869 lp:625574 and lp:637725 for background.
 
288
    def _record_traceback_from_test(self, exc_info):
 
289
        """Store the traceback from passed exc_info tuple till"""
 
290
        self._traceback_from_test = exc_info[2]
 
291
 
282
292
    def startTest(self, test):
283
293
        super(ExtendedTestResult, self).startTest(test)
284
294
        if self.count == 0:
285
295
            self.startTests()
 
296
        self.count += 1
286
297
        self.report_test_start(test)
287
298
        test.number = self.count
288
299
        self._recordTestStartTime()
 
300
        # Make testtools cases give us the real traceback on failure
 
301
        addOnException = getattr(test, "addOnException", None)
 
302
        if addOnException is not None:
 
303
            addOnException(self._record_traceback_from_test)
 
304
        # Only check for thread leaks if the test case supports cleanups
 
305
        addCleanup = getattr(test, "addCleanup", None)
 
306
        if addCleanup is not None:
 
307
            addCleanup(self._check_leaked_threads, test)
289
308
 
290
309
    def startTests(self):
291
 
        import platform
292
 
        if getattr(sys, 'frozen', None) is None:
293
 
            bzr_path = osutils.realpath(sys.argv[0])
294
 
        else:
295
 
            bzr_path = sys.executable
296
 
        self.stream.write(
297
 
            'bzr selftest: %s\n' % (bzr_path,))
298
 
        self.stream.write(
299
 
            '   %s\n' % (
300
 
                    bzrlib.__path__[0],))
301
 
        self.stream.write(
302
 
            '   bzr-%s python-%s %s\n' % (
303
 
                    bzrlib.version_string,
304
 
                    bzrlib._format_version_tuple(sys.version_info),
305
 
                    platform.platform(aliased=1),
306
 
                    ))
307
 
        self.stream.write('\n')
 
310
        self.report_tests_starting()
 
311
        self._active_threads = threading.enumerate()
 
312
 
 
313
    def stopTest(self, test):
 
314
        self._traceback_from_test = None
 
315
 
 
316
    def _check_leaked_threads(self, test):
 
317
        """See if any threads have leaked since last call
 
318
 
 
319
        A sample of live threads is stored in the _active_threads attribute,
 
320
        when this method runs it compares the current live threads and any not
 
321
        in the previous sample are treated as having leaked.
 
322
        """
 
323
        now_active_threads = set(threading.enumerate())
 
324
        threads_leaked = now_active_threads.difference(self._active_threads)
 
325
        if threads_leaked:
 
326
            self._report_thread_leak(test, threads_leaked, now_active_threads)
 
327
            self._tests_leaking_threads_count += 1
 
328
            if self._first_thread_leaker_id is None:
 
329
                self._first_thread_leaker_id = test.id()
 
330
            self._active_threads = now_active_threads
308
331
 
309
332
    def _recordTestStartTime(self):
310
333
        """Record that a test has started."""
311
 
        self._start_time = time.time()
312
 
 
313
 
    def _cleanupLogFile(self, test):
314
 
        # We can only do this if we have one of our TestCases, not if
315
 
        # we have a doctest.
316
 
        setKeepLogfile = getattr(test, 'setKeepLogfile', None)
317
 
        if setKeepLogfile is not None:
318
 
            setKeepLogfile()
 
334
        self._start_datetime = self._now()
319
335
 
320
336
    def addError(self, test, err):
321
337
        """Tell result that test finished with an error.
323
339
        Called from the TestCase run() method when the test
324
340
        fails with an unexpected error.
325
341
        """
326
 
        self._post_mortem()
 
342
        self._post_mortem(self._traceback_from_test)
327
343
        super(ExtendedTestResult, self).addError(test, err)
328
344
        self.error_count += 1
329
345
        self.report_error(test, err)
330
346
        if self.stop_early:
331
347
            self.stop()
332
 
        self._cleanupLogFile(test)
333
348
 
334
349
    def addFailure(self, test, err):
335
350
        """Tell result that test failed.
337
352
        Called from the TestCase run() method when the test
338
353
        fails because e.g. an assert() method failed.
339
354
        """
340
 
        self._post_mortem()
 
355
        self._post_mortem(self._traceback_from_test)
341
356
        super(ExtendedTestResult, self).addFailure(test, err)
342
357
        self.failure_count += 1
343
358
        self.report_failure(test, err)
344
359
        if self.stop_early:
345
360
            self.stop()
346
 
        self._cleanupLogFile(test)
347
361
 
348
362
    def addSuccess(self, test, details=None):
349
363
        """Tell result that test completed successfully.
357
371
                    self._formatTime(benchmark_time),
358
372
                    test.id()))
359
373
        self.report_success(test)
360
 
        self._cleanupLogFile(test)
361
374
        super(ExtendedTestResult, self).addSuccess(test)
362
375
        test._log_contents = ''
363
376
 
387
400
        self.not_applicable_count += 1
388
401
        self.report_not_applicable(test, reason)
389
402
 
390
 
    def _post_mortem(self):
 
403
    def _post_mortem(self, tb=None):
391
404
        """Start a PDB post mortem session."""
392
405
        if os.environ.get('BZR_TEST_PDB', None):
393
 
            import pdb;pdb.post_mortem()
 
406
            import pdb
 
407
            pdb.post_mortem(tb)
394
408
 
395
409
    def progress(self, offset, whence):
396
410
        """The test is adjusting the count of tests to run."""
401
415
        else:
402
416
            raise errors.BzrError("Unknown whence %r" % whence)
403
417
 
404
 
    def report_cleaning_up(self):
405
 
        pass
 
418
    def report_tests_starting(self):
 
419
        """Display information before the test run begins"""
 
420
        if getattr(sys, 'frozen', None) is None:
 
421
            bzr_path = osutils.realpath(sys.argv[0])
 
422
        else:
 
423
            bzr_path = sys.executable
 
424
        self.stream.write(
 
425
            'bzr selftest: %s\n' % (bzr_path,))
 
426
        self.stream.write(
 
427
            '   %s\n' % (
 
428
                    bzrlib.__path__[0],))
 
429
        self.stream.write(
 
430
            '   bzr-%s python-%s %s\n' % (
 
431
                    bzrlib.version_string,
 
432
                    bzrlib._format_version_tuple(sys.version_info),
 
433
                    platform.platform(aliased=1),
 
434
                    ))
 
435
        self.stream.write('\n')
 
436
 
 
437
    def report_test_start(self, test):
 
438
        """Display information on the test just about to be run"""
 
439
 
 
440
    def _report_thread_leak(self, test, leaked_threads, active_threads):
 
441
        """Display information on a test that leaked one or more threads"""
 
442
        # GZ 2010-09-09: A leak summary reported separately from the general
 
443
        #                thread debugging would be nice. Tests under subunit
 
444
        #                need something not using stream, perhaps adding a
 
445
        #                testtools details object would be fitting.
 
446
        if 'threads' in selftest_debug_flags:
 
447
            self.stream.write('%s is leaking, active is now %d\n' %
 
448
                (test.id(), len(active_threads)))
406
449
 
407
450
    def startTestRun(self):
408
451
        self.startTime = time.time()
445
488
        self.pb.finished()
446
489
        super(TextTestResult, self).stopTestRun()
447
490
 
448
 
    def startTestRun(self):
449
 
        super(TextTestResult, self).startTestRun()
 
491
    def report_tests_starting(self):
 
492
        super(TextTestResult, self).report_tests_starting()
450
493
        self.pb.update('[test 0/%d] Starting' % (self.num_tests))
451
494
 
452
 
    def printErrors(self):
453
 
        # clear the pb to make room for the error listing
454
 
        self.pb.clear()
455
 
        super(TextTestResult, self).printErrors()
456
 
 
457
495
    def _progress_prefix_text(self):
458
496
        # the longer this text, the less space we have to show the test
459
497
        # name...
481
519
        return a
482
520
 
483
521
    def report_test_start(self, test):
484
 
        self.count += 1
485
522
        self.pb.update(
486
523
                self._progress_prefix_text()
487
524
                + ' '
514
551
    def report_unsupported(self, test, feature):
515
552
        """test cannot be run because feature is missing."""
516
553
 
517
 
    def report_cleaning_up(self):
518
 
        self.pb.update('Cleaning up')
519
 
 
520
554
 
521
555
class VerboseTestResult(ExtendedTestResult):
522
556
    """Produce long output, with one line per test run plus times"""
529
563
            result = a_string
530
564
        return result.ljust(final_width)
531
565
 
532
 
    def startTestRun(self):
533
 
        super(VerboseTestResult, self).startTestRun()
 
566
    def report_tests_starting(self):
534
567
        self.stream.write('running %d tests...\n' % self.num_tests)
 
568
        super(VerboseTestResult, self).report_tests_starting()
535
569
 
536
570
    def report_test_start(self, test):
537
 
        self.count += 1
538
571
        name = self._shortened_test_description(test)
539
572
        width = osutils.terminal_width()
540
573
        if width is not None:
790
823
    routine, and to build and check bzr trees.
791
824
 
792
825
    In addition to the usual method of overriding tearDown(), this class also
793
 
    allows subclasses to register functions into the _cleanups list, which is
 
826
    allows subclasses to register cleanup functions via addCleanup, which are
794
827
    run in order as the object is torn down.  It's less likely this will be
795
828
    accidentally overlooked.
796
829
    """
797
830
 
798
 
    _active_threads = None
799
 
    _leaking_threads_tests = 0
800
 
    _first_thread_leaker_id = None
801
831
    _log_file = None
802
832
    # record lsprof data when performing benchmark calls.
803
833
    _gather_lsprof_in_benchmarks = False
804
834
 
805
835
    def __init__(self, methodName='testMethod'):
806
836
        super(TestCase, self).__init__(methodName)
807
 
        self._cleanups = []
808
837
        self._directory_isolation = True
809
838
        self.exception_handlers.insert(0,
810
839
            (UnavailableFeature, self._do_unsupported_or_skip))
828
857
        self._track_transports()
829
858
        self._track_locks()
830
859
        self._clear_debug_flags()
831
 
        TestCase._active_threads = threading.activeCount()
832
 
        self.addCleanup(self._check_leaked_threads)
 
860
        # Isolate global verbosity level, to make sure it's reproducible
 
861
        # between tests.  We should get rid of this altogether: bug 656694. --
 
862
        # mbp 20101008
 
863
        self.overrideAttr(bzrlib.trace, '_verbosity_level', 0)
833
864
 
834
865
    def debug(self):
835
866
        # debug a frame up.
836
867
        import pdb
837
868
        pdb.Pdb().set_trace(sys._getframe().f_back)
838
869
 
839
 
    def _check_leaked_threads(self):
840
 
        active = threading.activeCount()
841
 
        leaked_threads = active - TestCase._active_threads
842
 
        TestCase._active_threads = active
843
 
        # If some tests make the number of threads *decrease*, we'll consider
844
 
        # that they are just observing old threads dieing, not agressively kill
845
 
        # random threads. So we don't report these tests as leaking. The risk
846
 
        # is that we have false positives that way (the test see 2 threads
847
 
        # going away but leak one) but it seems less likely than the actual
848
 
        # false positives (the test see threads going away and does not leak).
849
 
        if leaked_threads > 0:
850
 
            if 'threads' in selftest_debug_flags:
851
 
                print '%s is leaking, active is now %d' % (self.id(), active)
852
 
            TestCase._leaking_threads_tests += 1
853
 
            if TestCase._first_thread_leaker_id is None:
854
 
                TestCase._first_thread_leaker_id = self.id()
 
870
    def discardDetail(self, name):
 
871
        """Extend the addDetail, getDetails api so we can remove a detail.
 
872
 
 
873
        eg. bzr always adds the 'log' detail at startup, but we don't want to
 
874
        include it for skipped, xfail, etc tests.
 
875
 
 
876
        It is safe to call this for a detail that doesn't exist, in case this
 
877
        gets called multiple times.
 
878
        """
 
879
        # We cheat. details is stored in __details which means we shouldn't
 
880
        # touch it. but getDetails() returns the dict directly, so we can
 
881
        # mutate it.
 
882
        details = self.getDetails()
 
883
        if name in details:
 
884
            del details[name]
855
885
 
856
886
    def _clear_debug_flags(self):
857
887
        """Prevent externally set debug flags affecting tests.
868
898
 
869
899
    def _clear_hooks(self):
870
900
        # prevent hooks affecting tests
 
901
        known_hooks = hooks.known_hooks
871
902
        self._preserved_hooks = {}
872
 
        for key, factory in hooks.known_hooks.items():
873
 
            parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)
874
 
            current_hooks = hooks.known_hooks_key_to_object(key)
 
903
        for key, (parent, name) in known_hooks.iter_parent_objects():
 
904
            current_hooks = getattr(parent, name)
875
905
            self._preserved_hooks[parent] = (name, current_hooks)
876
906
        self.addCleanup(self._restoreHooks)
877
 
        for key, factory in hooks.known_hooks.items():
878
 
            parent, name = hooks.known_hooks_key_to_parent_and_attribute(key)
 
907
        for key, (parent, name) in known_hooks.iter_parent_objects():
 
908
            factory = known_hooks.get(key)
879
909
            setattr(parent, name, factory())
880
910
        # this hook should always be installed
881
911
        request._install_hook()
975
1005
            try:
976
1006
                workingtree.WorkingTree.open(path)
977
1007
            except (errors.NotBranchError, errors.NoWorkingTree):
978
 
                return
 
1008
                raise TestSkipped('Needs a working tree of bzr sources')
979
1009
        finally:
980
1010
            self.enable_directory_isolation()
981
1011
 
1487
1517
        """
1488
1518
        debug.debug_flags.discard('strict_locks')
1489
1519
 
1490
 
    def addCleanup(self, callable, *args, **kwargs):
1491
 
        """Arrange to run a callable when this case is torn down.
1492
 
 
1493
 
        Callables are run in the reverse of the order they are registered,
1494
 
        ie last-in first-out.
1495
 
        """
1496
 
        self._cleanups.append((callable, args, kwargs))
1497
 
 
1498
1520
    def overrideAttr(self, obj, attr_name, new=_unitialized_attr):
1499
1521
        """Overrides an object attribute restoring it after the test.
1500
1522
 
1584
1606
        """This test has failed for some known reason."""
1585
1607
        raise KnownFailure(reason)
1586
1608
 
 
1609
    def _suppress_log(self):
 
1610
        """Remove the log info from details."""
 
1611
        self.discardDetail('log')
 
1612
 
1587
1613
    def _do_skip(self, result, reason):
 
1614
        self._suppress_log()
1588
1615
        addSkip = getattr(result, 'addSkip', None)
1589
1616
        if not callable(addSkip):
1590
1617
            result.addSuccess(result)
1593
1620
 
1594
1621
    @staticmethod
1595
1622
    def _do_known_failure(self, result, e):
 
1623
        self._suppress_log()
1596
1624
        err = sys.exc_info()
1597
1625
        addExpectedFailure = getattr(result, 'addExpectedFailure', None)
1598
1626
        if addExpectedFailure is not None:
1606
1634
            reason = 'No reason given'
1607
1635
        else:
1608
1636
            reason = e.args[0]
 
1637
        self._suppress_log ()
1609
1638
        addNotApplicable = getattr(result, 'addNotApplicable', None)
1610
1639
        if addNotApplicable is not None:
1611
1640
            result.addNotApplicable(self, reason)
1613
1642
            self._do_skip(result, reason)
1614
1643
 
1615
1644
    @staticmethod
 
1645
    def _report_skip(self, result, err):
 
1646
        """Override the default _report_skip.
 
1647
 
 
1648
        We want to strip the 'log' detail. If we waint until _do_skip, it has
 
1649
        already been formatted into the 'reason' string, and we can't pull it
 
1650
        out again.
 
1651
        """
 
1652
        self._suppress_log()
 
1653
        super(TestCase, self)._report_skip(self, result, err)
 
1654
 
 
1655
    @staticmethod
 
1656
    def _report_expected_failure(self, result, err):
 
1657
        """Strip the log.
 
1658
 
 
1659
        See _report_skip for motivation.
 
1660
        """
 
1661
        self._suppress_log()
 
1662
        super(TestCase, self)._report_expected_failure(self, result, err)
 
1663
 
 
1664
    @staticmethod
1616
1665
    def _do_unsupported_or_skip(self, result, e):
1617
1666
        reason = e.args[0]
 
1667
        self._suppress_log()
1618
1668
        addNotSupported = getattr(result, 'addNotSupported', None)
1619
1669
        if addNotSupported is not None:
1620
1670
            result.addNotSupported(self, reason)
2401
2451
                self.addCleanup(t.disconnect)
2402
2452
            return t
2403
2453
 
2404
 
        orig_get_transport = self.overrideAttr(_mod_transport, 'get_transport',
 
2454
        orig_get_transport = self.overrideAttr(_mod_transport, '_get_transport',
2405
2455
                                               get_transport_with_cleanup)
2406
2456
        self._make_test_root()
2407
2457
        self.addCleanup(os.chdir, os.getcwdu())
2970
3020
 
2971
3021
 
2972
3022
def fork_decorator(suite):
 
3023
    if getattr(os, "fork", None) is None:
 
3024
        raise errors.BzrCommandError("platform does not support fork,"
 
3025
            " try --parallel=subprocess instead.")
2973
3026
    concurrency = osutils.local_concurrency()
2974
3027
    if concurrency == 1:
2975
3028
        return suite
3299
3352
    return result
3300
3353
 
3301
3354
 
3302
 
class ForwardingResult(unittest.TestResult):
3303
 
 
3304
 
    def __init__(self, target):
3305
 
        unittest.TestResult.__init__(self)
3306
 
        self.result = target
3307
 
 
3308
 
    def startTest(self, test):
3309
 
        self.result.startTest(test)
3310
 
 
3311
 
    def stopTest(self, test):
3312
 
        self.result.stopTest(test)
3313
 
 
3314
 
    def startTestRun(self):
3315
 
        self.result.startTestRun()
3316
 
 
3317
 
    def stopTestRun(self):
3318
 
        self.result.stopTestRun()
3319
 
 
3320
 
    def addSkip(self, test, reason):
3321
 
        self.result.addSkip(test, reason)
3322
 
 
3323
 
    def addSuccess(self, test):
3324
 
        self.result.addSuccess(test)
3325
 
 
3326
 
    def addError(self, test, err):
3327
 
        self.result.addError(test, err)
3328
 
 
3329
 
    def addFailure(self, test, err):
3330
 
        self.result.addFailure(test, err)
3331
 
ForwardingResult = testtools.ExtendedToOriginalDecorator
3332
 
 
3333
 
 
3334
 
class ProfileResult(ForwardingResult):
 
3355
class ProfileResult(testtools.ExtendedToOriginalDecorator):
3335
3356
    """Generate profiling data for all activity between start and success.
3336
3357
    
3337
3358
    The profile data is appended to the test's _benchcalls attribute and can
3349
3370
        # unavoidably fail.
3350
3371
        bzrlib.lsprof.BzrProfiler.profiler_block = 0
3351
3372
        self.profiler.start()
3352
 
        ForwardingResult.startTest(self, test)
 
3373
        testtools.ExtendedToOriginalDecorator.startTest(self, test)
3353
3374
 
3354
3375
    def addSuccess(self, test):
3355
3376
        stats = self.profiler.stop()
3359
3380
            test._benchcalls = []
3360
3381
            calls = test._benchcalls
3361
3382
        calls.append(((test.id(), "", ""), stats))
3362
 
        ForwardingResult.addSuccess(self, test)
 
3383
        testtools.ExtendedToOriginalDecorator.addSuccess(self, test)
3363
3384
 
3364
3385
    def stopTest(self, test):
3365
 
        ForwardingResult.stopTest(self, test)
 
3386
        testtools.ExtendedToOriginalDecorator.stopTest(self, test)
3366
3387
        self.profiler = None
3367
3388
 
3368
3389
 
3741
3762
        'bzrlib.tests.test_permissions',
3742
3763
        'bzrlib.tests.test_plugins',
3743
3764
        'bzrlib.tests.test_progress',
 
3765
        'bzrlib.tests.test_pyutils',
3744
3766
        'bzrlib.tests.test_read_bundle',
3745
3767
        'bzrlib.tests.test_reconcile',
3746
3768
        'bzrlib.tests.test_reconfigure',
3755
3777
        'bzrlib.tests.test_rio',
3756
3778
        'bzrlib.tests.test_rules',
3757
3779
        'bzrlib.tests.test_sampler',
 
3780
        'bzrlib.tests.test_scenarios',
3758
3781
        'bzrlib.tests.test_script',
3759
3782
        'bzrlib.tests.test_selftest',
3760
3783
        'bzrlib.tests.test_serializer',
3824
3847
        'bzrlib.lockdir',
3825
3848
        'bzrlib.merge3',
3826
3849
        'bzrlib.option',
 
3850
        'bzrlib.pyutils',
3827
3851
        'bzrlib.symbol_versioning',
3828
3852
        'bzrlib.tests',
3829
3853
        'bzrlib.tests.fixtures',
3830
3854
        'bzrlib.timestamp',
 
3855
        'bzrlib.transport.http',
3831
3856
        'bzrlib.version_info_formats.format_custom',
3832
3857
        ]
3833
3858
 
3936
3961
    return suite
3937
3962
 
3938
3963
 
3939
 
def multiply_scenarios(scenarios_left, scenarios_right):
 
3964
def multiply_scenarios(*scenarios):
 
3965
    """Multiply two or more iterables of scenarios.
 
3966
 
 
3967
    It is safe to pass scenario generators or iterators.
 
3968
 
 
3969
    :returns: A list of compound scenarios: the cross-product of all 
 
3970
        scenarios, with the names concatenated and the parameters
 
3971
        merged together.
 
3972
    """
 
3973
    return reduce(_multiply_two_scenarios, map(list, scenarios))
 
3974
 
 
3975
 
 
3976
def _multiply_two_scenarios(scenarios_left, scenarios_right):
3940
3977
    """Multiply two sets of scenarios.
3941
3978
 
3942
3979
    :returns: the cartesian product of the two sets of scenarios, that is
4028
4065
    """
4029
4066
    new_test = copy.copy(test)
4030
4067
    new_test.id = lambda: new_id
 
4068
    # XXX: Workaround <https://bugs.launchpad.net/testtools/+bug/637725>, which
 
4069
    # causes cloned tests to share the 'details' dict.  This makes it hard to
 
4070
    # read the test output for parameterized tests, because tracebacks will be
 
4071
    # associated with irrelevant tests.
 
4072
    try:
 
4073
        details = new_test._TestCase__details
 
4074
    except AttributeError:
 
4075
        # must be a different version of testtools than expected.  Do nothing.
 
4076
        pass
 
4077
    else:
 
4078
        # Reset the '__details' dict.
 
4079
        new_test._TestCase__details = {}
4031
4080
    return new_test
4032
4081
 
4033
4082
 
4054
4103
        the module is available.
4055
4104
    """
4056
4105
 
4057
 
    py_module = __import__(py_module_name, {}, {}, ['NO_SUCH_ATTRIB'])
 
4106
    py_module = pyutils.get_named_object(py_module_name)
4058
4107
    scenarios = [
4059
4108
        ('python', {'module': py_module}),
4060
4109
    ]
4213
4262
            symbol_versioning.warn(depr_msg + use_msg, DeprecationWarning)
4214
4263
            # Import the new feature and use it as a replacement for the
4215
4264
            # deprecated one.
4216
 
            mod = __import__(self._replacement_module, {}, {},
4217
 
                             [self._replacement_name])
4218
 
            self._feature = getattr(mod, self._replacement_name)
 
4265
            self._feature = pyutils.get_named_object(
 
4266
                self._replacement_module, self._replacement_name)
4219
4267
 
4220
4268
    def _probe(self):
4221
4269
        self._ensure()
4459
4507
try:
4460
4508
    from subunit import TestProtocolClient
4461
4509
    from subunit.test_results import AutoTimingTestResultDecorator
 
4510
    class SubUnitBzrProtocolClient(TestProtocolClient):
 
4511
 
 
4512
        def addSuccess(self, test, details=None):
 
4513
            # The subunit client always includes the details in the subunit
 
4514
            # stream, but we don't want to include it in ours.
 
4515
            if details is not None and 'log' in details:
 
4516
                del details['log']
 
4517
            return super(SubUnitBzrProtocolClient, self).addSuccess(
 
4518
                test, details)
 
4519
 
4462
4520
    class SubUnitBzrRunner(TextTestRunner):
4463
4521
        def run(self, test):
4464
4522
            result = AutoTimingTestResultDecorator(
4465
 
                TestProtocolClient(self.stream))
 
4523
                SubUnitBzrProtocolClient(self.stream))
4466
4524
            test.run(result)
4467
4525
            return result
4468
4526
except ImportError: