~free.ekanayaka/landscape-client/lucid-1.5.0-0ubuntu0.10.04.0

« back to all changes in this revision

Viewing changes to landscape/tests/helpers.py

  • Committer: Bazaar Package Importer
  • Author(s): Rick Clark
  • Date: 2008-09-08 16:35:57 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080908163557-l3ixzj5dxz37wnw2
Tags: 1.0.18-0ubuntu1
New upstream release 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from cStringIO import StringIO
 
2
import logging
 
3
import shutil
 
4
import pprint
 
5
import re
 
6
import os
 
7
import tempfile
 
8
import sys
 
9
 
 
10
import dbus
 
11
 
 
12
from twisted.trial.unittest import TestCase
 
13
from twisted.internet.defer import Deferred
 
14
 
 
15
from landscape.tests.subunit import run_isolated
 
16
from landscape.tests.mocker import MockerTestCase
 
17
from landscape.watchdog import bootstrap_list
 
18
 
 
19
from landscape.lib.dbus_util import get_object
 
20
from landscape.lib import bpickle_dbus
 
21
from landscape.lib.persist import Persist
 
22
 
 
23
from landscape.reactor import FakeReactor
 
24
 
 
25
from landscape.broker.deployment import (BrokerService, BrokerConfiguration)
 
26
from landscape.deployment import BaseConfiguration
 
27
from landscape.broker.remote import RemoteBroker, FakeRemoteBroker
 
28
from landscape.broker.transport import FakeTransport
 
29
 
 
30
from landscape.monitor.monitor import MonitorPluginRegistry
 
31
from landscape.manager.manager import ManagerPluginRegistry
 
32
from landscape.manager.deployment import ManagerConfiguration
 
33
 
 
34
 
 
35
class LandscapeTest(MockerTestCase, TestCase):
 
36
 
 
37
    helpers = []
 
38
 
 
39
    def setUp(self):
 
40
        super(LandscapeTest, self).setUp()
 
41
 
 
42
        self._old_config_filenames = BaseConfiguration.default_config_filenames
 
43
        BaseConfiguration.default_config_filenames = []
 
44
        # make_path-related stuff
 
45
        self.dirname = tempfile.mkdtemp()
 
46
        self.counter = 0
 
47
 
 
48
        self._helper_instances = []
 
49
        if LogKeeperHelper not in self.helpers:
 
50
            self.helpers.insert(0, LogKeeperHelper)
 
51
        for helper_factory in self.helpers:
 
52
            helper = helper_factory()
 
53
            helper.set_up(self)
 
54
            self._helper_instances.append(helper)
 
55
 
 
56
    def tearDown(self):
 
57
        BaseConfiguration.default_config_filenames = self._old_config_filenames
 
58
        for helper in reversed(self._helper_instances):
 
59
            helper.tear_down(self)
 
60
        shutil.rmtree(self.dirname)
 
61
        super(LandscapeTest, self).tearDown()
 
62
 
 
63
    def assertMessage(self, obtained, expected):
 
64
        obtained = obtained.copy()
 
65
        for key in ["api", "timestamp"]:
 
66
            if key not in expected and key in obtained:
 
67
                obtained.pop(key)
 
68
        if obtained != expected:
 
69
            raise self.failureException("Messages don't match.\n"
 
70
                                        "Expected:\n%s\nObtained:\n%s\n"
 
71
                                        % (pprint.pformat(expected),
 
72
                                           pprint.pformat(obtained)))
 
73
 
 
74
    def assertMessages(self, obtained, expected):
 
75
        self.assertEquals(type(obtained), list)
 
76
        self.assertEquals(type(expected), list)
 
77
        for obtained_message, expected_message in zip(obtained, expected):
 
78
            self.assertMessage(obtained_message, expected_message)
 
79
        obtained_len = len(obtained)
 
80
        expected_len = len(expected)
 
81
        diff = abs(expected_len - obtained_len)
 
82
        if obtained_len < expected_len:
 
83
            extra = pprint.pformat(expected[-diff:])
 
84
            raise self.failureException("Expected the following %d additional "
 
85
                                        "messages:\n%s" % (diff, extra))
 
86
        elif expected_len < obtained_len:
 
87
            extra = pprint.pformat(obtained[-diff:])
 
88
            raise self.failureException("Got %d more messages than expected:\n"
 
89
                                        "%s" % (diff, extra))
 
90
 
 
91
    def assertDeferredSucceeded(self, deferred):
 
92
        self.assertTrue(isinstance(deferred, Deferred))
 
93
        called = []
 
94
        def callback(result):
 
95
            called.append(True)
 
96
        deferred.addCallback(callback)
 
97
        self.assertTrue(called)
 
98
 
 
99
    def make_dir(self):
 
100
        path = self.make_path()
 
101
        os.mkdir(path)
 
102
        return path
 
103
 
 
104
    def make_path(self, content=None, path=None):
 
105
        if path is None:
 
106
            self.counter += 1
 
107
            path = "%s/%03d" % (self.dirname, self.counter)
 
108
        if content is not None:
 
109
            file = open(path, "w")
 
110
            try:
 
111
                file.write(content)
 
112
            finally:
 
113
                file.close()
 
114
        return path
 
115
 
 
116
 
 
117
class LandscapeIsolatedTest(LandscapeTest):
 
118
    """TestCase that also runs all test methods in a subprocess."""
 
119
 
 
120
    def run(self, result):
 
121
        run_isolated(LandscapeTest, self, result)
 
122
 
 
123
 
 
124
class DBusHelper(object):
 
125
    """Create a temporary D-Bus."""
 
126
 
 
127
    def set_up(self, test_case):
 
128
        if not getattr(test_case, "I_KNOW", False):
 
129
            test_case.assertTrue(isinstance(test_case, LandscapeIsolatedTest),
 
130
                                 "DBusHelper must only be used on "
 
131
                                 "LandscapeIsolatedTests")
 
132
        bpickle_dbus.install()
 
133
        test_case.bus = dbus.SessionBus()
 
134
 
 
135
    def tear_down(self, test_case):
 
136
        bpickle_dbus.uninstall()
 
137
 
 
138
 
 
139
from logging import Handler, ERROR, Formatter
 
140
 
 
141
class ErrorHandler(Handler):
 
142
    def __init__(self, *args, **kwargs):
 
143
        Handler.__init__(self, *args, **kwargs)
 
144
        self.errors = []
 
145
 
 
146
    def emit(self, record):
 
147
        if record.levelno >= ERROR:
 
148
            self.errors.append(record)
 
149
 
 
150
 
 
151
class LoggedErrorsError(Exception):
 
152
    def __str__(self):
 
153
        out = "The following errors were logged\n"
 
154
        formatter = Formatter()
 
155
        for error in self.args[0]:
 
156
            out += formatter.format(error) + "\n"
 
157
        return out
 
158
 
 
159
 
 
160
class LogKeeperHelper(object):
 
161
    """Record logging information.
 
162
 
 
163
    Puts a 'logfile' attribute on your test case, which is a StringIO
 
164
    containing all log output.
 
165
    """
 
166
 
 
167
    def set_up(self, test_case):
 
168
        self.ignored_exception_regexes = []
 
169
        self.ignored_exception_types = []
 
170
        self.error_handler = ErrorHandler()
 
171
        test_case.log_helper = self
 
172
        test_case.logger = logger = logging.getLogger()
 
173
        test_case.logfile = StringIO()
 
174
        handler = logging.StreamHandler(test_case.logfile)
 
175
        format = ("%(levelname)8s: %(message)s")
 
176
        handler.setFormatter(logging.Formatter(format))
 
177
        self.old_handlers = logger.handlers
 
178
        self.old_level = logger.level
 
179
        logger.handlers = [handler, self.error_handler]
 
180
        logger.setLevel(logging.NOTSET)
 
181
 
 
182
    def tear_down(self, test_case):
 
183
        logger = logging.getLogger()
 
184
        logger.setLevel(self.old_level)
 
185
        logger.handlers = self.old_handlers
 
186
        errors = []
 
187
        for record in self.error_handler.errors:
 
188
            for ignored_type in self.ignored_exception_types:
 
189
                if (record.exc_info and record.exc_info[0]
 
190
                    and issubclass(record.exc_info[0], ignored_type)):
 
191
                    break
 
192
            else:
 
193
                for ignored_regex in self.ignored_exception_regexes:
 
194
                    if ignored_regex.match(record.message):
 
195
                        break
 
196
                else:
 
197
                    errors.append(record)
 
198
        if errors:
 
199
            raise LoggedErrorsError(errors)
 
200
 
 
201
    def ignore_errors(self, type_or_regex):
 
202
        if isinstance(type_or_regex, basestring):
 
203
            self.ignored_exception_regexes.append(re.compile(type_or_regex))
 
204
        else:
 
205
            self.ignored_exception_types.append(type_or_regex)
 
206
 
 
207
 
 
208
class MakePathHelper(object):
 
209
 
 
210
    def set_up(self, test_case):
 
211
        pass
 
212
 
 
213
    def tear_down(self, test_case):
 
214
        pass
 
215
 
 
216
 
 
217
class EnvironSnapshot(object):
 
218
 
 
219
    def __init__(self):
 
220
        self._snapshot = os.environ.copy()
 
221
 
 
222
    def restore(self):
 
223
        os.environ.update(self._snapshot)
 
224
        for key in list(os.environ):
 
225
            if key not in self._snapshot:
 
226
                del os.environ[key]
 
227
 
 
228
 
 
229
class EnvironSaverHelper(object):
 
230
 
 
231
    def set_up(self, test_case):
 
232
        self._snapshot = EnvironSnapshot()
 
233
 
 
234
    def tear_down(self, test_case):
 
235
        self._snapshot.restore()
 
236
 
 
237
 
 
238
class FakeRemoteBrokerHelper(object):
 
239
    """
 
240
    The following attributes will be set on your test case:
 
241
      - broker_service: A L{landscape.broker.deployment.BrokerService}.
 
242
      - config_filename: The name of the configuration file that was used to
 
243
        generate the C{broker}.
 
244
      - data_path: The data path that the broker will use.
 
245
    """
 
246
 
 
247
    def set_up(self, test_case):
 
248
 
 
249
        bpickle_dbus.install()
 
250
 
 
251
        test_case.config_filename = test_case.make_path(
 
252
            "[client]\n"
 
253
            "url = http://localhost:91919\n"
 
254
            "computer_title = Default Computer Title\n"
 
255
            "account_name = default_account_name\n"
 
256
            "ping_url = http://localhost:91910/\n")
 
257
 
 
258
        test_case.data_path = test_case.make_dir()
 
259
        test_case.log_dir = test_case.make_dir()
 
260
 
 
261
        bootstrap_list.bootstrap(data_path=test_case.data_path,
 
262
                                 log_dir=test_case.log_dir)
 
263
 
 
264
        class MyBrokerConfiguration(BrokerConfiguration):
 
265
            default_config_filenames = [test_case.config_filename]
 
266
 
 
267
        config = MyBrokerConfiguration()
 
268
        config.load(["--bus", "session", "--data-path", test_case.data_path])
 
269
 
 
270
        class FakeBrokerService(BrokerService):
 
271
            """A broker which uses a fake reactor and fake transport."""
 
272
            reactor_factory = FakeReactor
 
273
            transport_factory = FakeTransport
 
274
 
 
275
        test_case.broker_service = service = FakeBrokerService(config)
 
276
        test_case.remote = FakeRemoteBroker(service.exchanger,
 
277
                                            service.message_store)
 
278
 
 
279
    def tear_down(self, test_case):
 
280
        bpickle_dbus.uninstall()
 
281
 
 
282
 
 
283
class RemoteBrokerHelper(FakeRemoteBrokerHelper):
 
284
    """
 
285
    Provides what L{FakeRemoteBrokerHelper} does, and makes it a
 
286
    'live' service. Since it uses DBUS, your test case must be a
 
287
    subclass of L{LandscapeIsolatedTest}.
 
288
 
 
289
    This adds the following attributes to your test case:
 
290
     - remote: A L{landscape.broker.remote.RemoteBroker}.
 
291
     - remote_service: The low level DBUS object that refers to the
 
292
       L{landscape.broker.broker.BrokerDBusObject}.
 
293
    """
 
294
 
 
295
    def set_up(self, test_case):
 
296
        if not getattr(test_case, "I_KNOW", False):
 
297
            test_case.assertTrue(isinstance(test_case, LandscapeIsolatedTest),
 
298
                                 "RemoteBrokerHelper must only be used on "
 
299
                                 "LandscapeIsolatedTests")
 
300
        super(RemoteBrokerHelper, self).set_up(test_case)
 
301
        service = test_case.broker_service
 
302
        service.startService()
 
303
        test_case.remote = RemoteBroker(service.bus)
 
304
        test_case.remote_service = get_object(service.bus,
 
305
                                              service.dbus_object.bus_name,
 
306
                                              service.dbus_object.object_path)
 
307
 
 
308
    def tear_down(self, test_case):
 
309
        test_case.broker_service.stopService()
 
310
        super(RemoteBrokerHelper, self).tear_down(test_case)
 
311
 
 
312
 
 
313
class ExchangeHelper(FakeRemoteBrokerHelper):
 
314
    """
 
315
    Backwards compatibility layer for tests that want a bunch of attributes
 
316
    jammed on to them instead of having C{self.broker_service}.
 
317
    """
 
318
 
 
319
    def set_up(self, test_case):
 
320
        super(ExchangeHelper, self).set_up(test_case)
 
321
 
 
322
        service = test_case.broker_service
 
323
 
 
324
        test_case.persist_filename = service.persist_filename
 
325
        test_case.message_directory = service.config.message_store_path
 
326
        test_case.transport = service.transport
 
327
        test_case.reactor = service.reactor
 
328
        test_case.persist = service.persist
 
329
        test_case.mstore = service.message_store
 
330
        test_case.exchanger = service.exchanger
 
331
        test_case.identity = service.identity
 
332
 
 
333
 
 
334
class MonitorHelper(ExchangeHelper):
 
335
    """
 
336
    Provides everything that L{ExchangeHelper} does plus a
 
337
    L{landscape.monitor.monitor.Monitor}.
 
338
    """
 
339
 
 
340
    def set_up(self, test_case):
 
341
        super(MonitorHelper, self).set_up(test_case)
 
342
        persist = Persist()
 
343
        persist_filename = test_case.make_path()
 
344
        test_case.monitor = MonitorPluginRegistry(
 
345
            test_case.broker_service.reactor, test_case.remote,
 
346
            test_case.broker_service.config,
 
347
            # XXX Ugh, the fake broker service doesn't have a bus.
 
348
            # We should get rid of the fake broker service.
 
349
            getattr(test_case.broker_service, "bus", None),
 
350
            persist, persist_filename)
 
351
 
 
352
 
 
353
class ManagerHelper(FakeRemoteBrokerHelper):
 
354
    """
 
355
    Provides everything that L{FakeRemoteBrokerHelper} does plus a
 
356
    L{landscape.manager.manager.Manager}.
 
357
    """
 
358
    def set_up(self, test_case):
 
359
        super(ManagerHelper, self).set_up(test_case)
 
360
        class MyManagerConfiguration(ManagerConfiguration):
 
361
            default_config_filenames = [test_case.config_filename]
 
362
        config = MyManagerConfiguration()
 
363
        test_case.manager = ManagerPluginRegistry(
 
364
            test_case.broker_service.reactor, test_case.remote,
 
365
            config)
 
366
 
 
367
 
 
368
class MockPopen(object):
 
369
 
 
370
    def __init__(self, output, return_codes=None):
 
371
        self.output = output
 
372
        self.stdout = StringIO(output)
 
373
        self.popen_inputs = []
 
374
        self.return_codes = return_codes
 
375
 
 
376
    def __call__(self, args, stdout=None, stderr=None):
 
377
        return self.popen(args, stdout=stdout, stderr=stderr)
 
378
 
 
379
    def popen(self, args, stdout=None, stderr=None):
 
380
        self.popen_inputs.append(args)
 
381
        return self
 
382
 
 
383
    def wait(self):
 
384
        if self.return_codes is None:
 
385
            return 0
 
386
        return self.return_codes.pop(0)
 
387
 
 
388
 
 
389
class StandardIOHelper(object):
 
390
 
 
391
    def set_up(self, test_case):
 
392
        from StringIO import StringIO
 
393
 
 
394
        test_case.old_stdout = sys.stdout
 
395
        test_case.old_stdin = sys.stdin
 
396
        test_case.stdout = sys.stdout = StringIO()
 
397
        test_case.stdin = sys.stdin = StringIO()
 
398
        test_case.stdin.encoding = "UTF-8"
 
399
 
 
400
    def tear_down(self, test_case):
 
401
        sys.stdout = test_case.old_stdout
 
402
        sys.stdin = test_case.old_stdin
 
403
 
 
404
 
 
405
class MockCoverageMonitor(object):
 
406
 
 
407
    def __init__(self, count=None, expected_count=None, percent=None,
 
408
                 since_reset=None, warn=None):
 
409
        self.count = count or 0
 
410
        self.expected_count = expected_count or 0
 
411
        self.percent = percent or 0.0
 
412
        self.since_reset_value = since_reset or 0
 
413
        self.warn_value = bool(warn)
 
414
 
 
415
    def since_reset(self):
 
416
        return self.since_reset_value
 
417
 
 
418
    def warn(self):
 
419
        return self.warn_value
 
420
 
 
421
    def reset(self):
 
422
        pass
 
423
 
 
424
 
 
425
class MockFrequencyMonitor(object):
 
426
 
 
427
    def __init__(self, count=None, expected_count=None, warn=None):
 
428
        self.count = count or 0
 
429
        self.expected_count = expected_count or 0
 
430
        self.warn_value = bool(warn)
 
431
 
 
432
    def warn(self):
 
433
        return self.warn_value
 
434
 
 
435
    def reset(self):
 
436
        pass
 
437
 
 
438
 
 
439
def mock_counter(i=0):
 
440
    """Generator starts at zero and yields integers that grow by one."""
 
441
    while True:
 
442
        yield i
 
443
        i += 1
 
444
 
 
445
 
 
446
def mock_time():
 
447
    """Generator starts at 100 and yields int timestamps that grow by one."""
 
448
    return mock_counter(100)
 
449
 
 
450
 
 
451
class StubProcessFactory(object):
 
452
    """
 
453
    A L{IReactorProcess} provider which records L{spawnProcess} calls and
 
454
    allows tests to get at the protocol.
 
455
    """
 
456
    def __init__(self):
 
457
        self.spawns = []
 
458
 
 
459
    def spawnProcess(self, protocol, executable, args=(), env={}, path=None,
 
460
                    uid=None, gid=None, usePTY=0, childFDs=None):
 
461
        self.spawns.append((protocol, executable, args,
 
462
                            env, path, uid, gid, usePTY, childFDs))
 
463
 
 
464
 
 
465
class DummyProcess(object):
 
466
    """A process (transport) that doesn't do anything."""
 
467
    def __init__(self):
 
468
        self.signals = []
 
469
 
 
470
    def signalProcess(self, signal):
 
471
        self.signals.append(signal)
 
472
 
 
473
    def closeChildFD(self, fd):
 
474
        pass
 
475
 
 
476
 
 
477
 
 
478
class ProcessDataBuilder(object):
 
479
    """Builder creates sample data for the process info plugin to consume."""
 
480
 
 
481
    RUNNING = "R (running)"
 
482
    STOPPED = "T (stopped)"
 
483
    TRACING_STOP = "T (tracing stop)"
 
484
    DISK_SLEEP = "D (disk sleep)"
 
485
    SLEEPING = "S (sleeping)"
 
486
    DEAD = "X (dead)"
 
487
    ZOMBIE = "Z (zombie)"
 
488
 
 
489
    def __init__(self, sample_dir):
 
490
        """Initialize factory with directory for sample data."""
 
491
        self._sample_dir = sample_dir
 
492
 
 
493
    def create_data(self, process_id, state, uid, gid,
 
494
                    started_after_boot=0, process_name=None,
 
495
                    generate_cmd_line=True, stat_data=None, vmsize=11676):
 
496
 
 
497
        """Creates sample data for a process.
 
498
 
 
499
        @param started_after_boot: The amount of time, in jiffies,
 
500
            between the system uptime and start of the process.
 
501
        @param process_name: Used to generate the process name that appears in
 
502
            /proc/%(pid)s/status
 
503
        @param generate_cmd_line: If true, place the process_name in
 
504
            /proc/%(pid)s/cmdline, otherwise leave it empty (this simulates a
 
505
            kernel process)
 
506
        @param stat_data: Array of items to write to the /proc/<pid>/stat file.
 
507
        """
 
508
        sample_data = """
 
509
Name:   %(process_name)s
 
510
State:  %(state)s
 
511
Tgid:   24759
 
512
Pid:    24759
 
513
PPid:   17238
 
514
TracerPid:      0
 
515
Uid:    %(uid)d    0    0    0
 
516
Gid:    %(gid)d    0    0    0
 
517
FDSize: 256
 
518
Groups: 4 20 24 25 29 30 44 46 106 110 112 1000
 
519
VmPeak:    11680 kB
 
520
VmSize:    %(vmsize)d kB
 
521
VmLck:         0 kB
 
522
VmHWM:      6928 kB
 
523
VmRSS:      6924 kB
 
524
VmData:     1636 kB
 
525
VmStk:       196 kB
 
526
VmExe:      1332 kB
 
527
VmLib:      4240 kB
 
528
VmPTE:        20 kB
 
529
Threads:        1
 
530
SigQ:   0/4294967295
 
531
SigPnd: 0000000000000000
 
532
ShdPnd: 0000000000000000
 
533
SigBlk: 0000000000000000
 
534
SigIgn: 0000000000000000
 
535
SigCgt: 0000000059816eff
 
536
CapInh: 0000000000000000
 
537
CapPrm: 0000000000000000
 
538
CapEff: 0000000000000000
 
539
""" % ({"process_name": process_name[:15], "state": state, "uid": uid,
 
540
        "gid": gid, "vmsize": vmsize})
 
541
        process_dir = os.path.join(self._sample_dir, str(process_id))
 
542
        os.mkdir(process_dir)
 
543
        filename = os.path.join(process_dir, "status")
 
544
 
 
545
        file = open(filename, "w+")
 
546
        try:
 
547
            file.write(sample_data)
 
548
        finally:
 
549
            file.close()
 
550
        if stat_data is None:
 
551
            stat_data = """\
 
552
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 %d\
 
553
""" % (started_after_boot,)
 
554
        filename = os.path.join(process_dir, "stat")
 
555
 
 
556
        file = open(filename, "w+")
 
557
        try:
 
558
            file.write(stat_data)
 
559
        finally:
 
560
            file.close()
 
561
 
 
562
        if generate_cmd_line:
 
563
            sample_data = """\
 
564
/usr/sbin/%(process_name)s\0--pid-file\0/var/run/%(process_name)s.pid\0
 
565
""" % {"process_name": process_name}
 
566
        else:
 
567
            sample_data = ""
 
568
        filename = os.path.join(process_dir, "cmdline")
 
569
 
 
570
        file = open(filename, "w+")
 
571
        try:
 
572
            file.write(sample_data)
 
573
        finally:
 
574
            file.close()
 
575
 
 
576
    def remove_data(self, process_id):
 
577
        """Remove sample data for the process that matches C{process_id}."""
 
578
        process_dir = os.path.join(self._sample_dir, str(process_id))
 
579
        shutil.rmtree(process_dir)
 
580
 
 
581
 
 
582
 
 
583
from twisted.python import log
 
584
from twisted.python import failure
 
585
from twisted.trial import reporter
 
586
 
 
587
def install_trial_hack():
 
588
    """
 
589
    Trial's TestCase in Twisted 2.2 had a bug which would prevent
 
590
    certain errors from being reported when being run in a non-trial
 
591
    test runner. This function monkeypatches trial to fix the bug, and
 
592
    only takes effect if using Twisted 2.2.
 
593
    """
 
594
    from twisted.trial.itrial import IReporter
 
595
    if "addError" in IReporter:
 
596
        # We have no need for this monkey patch with newer versions of Twisted.
 
597
        return
 
598
    def run(self, result):
 
599
        """
 
600
        Copied from twisted.trial.unittest.TestCase.run, but some
 
601
        lines from Twisted 2.5.
 
602
        """
 
603
        log.msg("--> %s <--" % (self.id()))
 
604
 
 
605
        # From Twisted 2.5
 
606
        if not isinstance(result, reporter.TestResult):
 
607
            result = PyUnitResultAdapter(result)
 
608
        # End from Twisted 2.5
 
609
 
 
610
        self._timedOut = False
 
611
        if self._shared and self not in self.__class__._instances:
 
612
            self.__class__._instances.add(self)
 
613
        result.startTest(self)
 
614
        if self.getSkip(): # don't run test methods that are marked as .skip
 
615
            result.addSkip(self, self.getSkip())
 
616
            result.stopTest(self)
 
617
            return
 
618
        # From twisted 2.5
 
619
        if hasattr(self, "_installObserver"):
 
620
            self._installObserver()
 
621
        # End from Twisted 2.5
 
622
        self._passed = False
 
623
        first = False
 
624
        if self._shared:
 
625
            first = self._isFirst()
 
626
            self.__class__._instancesRun.add(self)
 
627
        if first:
 
628
            d = self.deferSetUpClass(result)
 
629
        else:
 
630
            d = self.deferSetUp(None, result)
 
631
        try:
 
632
            self._wait(d)
 
633
        finally:
 
634
            self._cleanUp(result)
 
635
            result.stopTest(self)
 
636
            if self._shared and self._isLast():
 
637
                self._initInstances()
 
638
                self._classCleanUp(result)
 
639
            if not self._shared:
 
640
                self._classCleanUp(result)
 
641
    TestCase.run = run
 
642
 
 
643
### Copied from Twisted, to fix a bug in trial in Twisted 2.2! ###
 
644
 
 
645
class UnsupportedTrialFeature(Exception):
 
646
    """A feature of twisted.trial was used that pyunit cannot support."""
 
647
 
 
648
 
 
649
class PyUnitResultAdapter(object):
 
650
    """
 
651
    Wrap a C{TestResult} from the standard library's C{unittest} so that it
 
652
    supports the extended result types from Trial, and also supports
 
653
    L{twisted.python.failure.Failure}s being passed to L{addError} and
 
654
    L{addFailure}.
 
655
    """
 
656
 
 
657
    def __init__(self, original):
 
658
        """
 
659
        @param original: A C{TestResult} instance from C{unittest}.
 
660
        """
 
661
        self.original = original
 
662
 
 
663
    def _exc_info(self, err):
 
664
        if isinstance(err, failure.Failure):
 
665
            # Unwrap the Failure into a exc_info tuple.
 
666
            err = (err.type, err.value, err.tb)
 
667
        return err
 
668
 
 
669
    def startTest(self, method):
 
670
        # We'll need this later in cleanupErrors.
 
671
        self.__currentTest = method
 
672
        self.original.startTest(method)
 
673
 
 
674
    def stopTest(self, method):
 
675
        self.original.stopTest(method)
 
676
 
 
677
    def addFailure(self, test, fail):
 
678
        self.original.addFailure(test, self._exc_info(fail))
 
679
 
 
680
    def addError(self, test, error):
 
681
        self.original.addError(test, self._exc_info(error))
 
682
 
 
683
    def _unsupported(self, test, feature, info):
 
684
        self.original.addFailure(
 
685
            test,
 
686
            (UnsupportedTrialFeature,
 
687
             UnsupportedTrialFeature(feature, info),
 
688
             None))
 
689
 
 
690
    def addSkip(self, test, reason):
 
691
        """
 
692
        Report the skip as a failure.
 
693
        """
 
694
        self._unsupported(test, 'skip', reason)
 
695
 
 
696
    def addUnexpectedSuccess(self, test, todo):
 
697
        """
 
698
        Report the unexpected success as a failure.
 
699
        """
 
700
        self._unsupported(test, 'unexpected success', todo)
 
701
 
 
702
    def addExpectedFailure(self, test, error):
 
703
        """
 
704
        Report the expected failure (i.e. todo) as a failure.
 
705
        """
 
706
        self._unsupported(test, 'expected failure', error)
 
707
 
 
708
    def addSuccess(self, test):
 
709
        self.original.addSuccess(test)
 
710
 
 
711
    def upDownError(self, method, error, warn, printStatus):
 
712
        pass
 
713
 
 
714
    def cleanupErrors(self, errs):
 
715
        # Let's consider cleanupErrors as REAL errors. In recent
 
716
        # Twisted this is the default behavior, and cleanupErrors
 
717
        # isn't even called.
 
718
        self.addError(self.__currentTest, errs)
 
719
 
 
720
    def startSuite(self, name):
 
721
        pass
 
722
 
 
723
### END COPY FROM TWISTED ###
 
724
 
 
725
install_trial_hack()