1
from cStringIO import StringIO
12
from twisted.trial.unittest import TestCase
13
from twisted.internet.defer import Deferred
15
from landscape.tests.subunit import run_isolated
16
from landscape.tests.mocker import MockerTestCase
17
from landscape.watchdog import bootstrap_list
19
from landscape.lib.dbus_util import get_object
20
from landscape.lib import bpickle_dbus
21
from landscape.lib.persist import Persist
23
from landscape.reactor import FakeReactor
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
30
from landscape.monitor.monitor import MonitorPluginRegistry
31
from landscape.manager.manager import ManagerPluginRegistry
32
from landscape.manager.deployment import ManagerConfiguration
35
class LandscapeTest(MockerTestCase, TestCase):
40
super(LandscapeTest, self).setUp()
42
self._old_config_filenames = BaseConfiguration.default_config_filenames
43
BaseConfiguration.default_config_filenames = []
44
# make_path-related stuff
45
self.dirname = tempfile.mkdtemp()
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()
54
self._helper_instances.append(helper)
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()
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:
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)))
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"
91
def assertDeferredSucceeded(self, deferred):
92
self.assertTrue(isinstance(deferred, Deferred))
96
deferred.addCallback(callback)
97
self.assertTrue(called)
100
path = self.make_path()
104
def make_path(self, content=None, path=None):
107
path = "%s/%03d" % (self.dirname, self.counter)
108
if content is not None:
109
file = open(path, "w")
117
class LandscapeIsolatedTest(LandscapeTest):
118
"""TestCase that also runs all test methods in a subprocess."""
120
def run(self, result):
121
run_isolated(LandscapeTest, self, result)
124
class DBusHelper(object):
125
"""Create a temporary D-Bus."""
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()
135
def tear_down(self, test_case):
136
bpickle_dbus.uninstall()
139
from logging import Handler, ERROR, Formatter
141
class ErrorHandler(Handler):
142
def __init__(self, *args, **kwargs):
143
Handler.__init__(self, *args, **kwargs)
146
def emit(self, record):
147
if record.levelno >= ERROR:
148
self.errors.append(record)
151
class LoggedErrorsError(Exception):
153
out = "The following errors were logged\n"
154
formatter = Formatter()
155
for error in self.args[0]:
156
out += formatter.format(error) + "\n"
160
class LogKeeperHelper(object):
161
"""Record logging information.
163
Puts a 'logfile' attribute on your test case, which is a StringIO
164
containing all log output.
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)
182
def tear_down(self, test_case):
183
logger = logging.getLogger()
184
logger.setLevel(self.old_level)
185
logger.handlers = self.old_handlers
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)):
193
for ignored_regex in self.ignored_exception_regexes:
194
if ignored_regex.match(record.message):
197
errors.append(record)
199
raise LoggedErrorsError(errors)
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))
205
self.ignored_exception_types.append(type_or_regex)
208
class MakePathHelper(object):
210
def set_up(self, test_case):
213
def tear_down(self, test_case):
217
class EnvironSnapshot(object):
220
self._snapshot = os.environ.copy()
223
os.environ.update(self._snapshot)
224
for key in list(os.environ):
225
if key not in self._snapshot:
229
class EnvironSaverHelper(object):
231
def set_up(self, test_case):
232
self._snapshot = EnvironSnapshot()
234
def tear_down(self, test_case):
235
self._snapshot.restore()
238
class FakeRemoteBrokerHelper(object):
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.
247
def set_up(self, test_case):
249
bpickle_dbus.install()
251
test_case.config_filename = test_case.make_path(
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")
258
test_case.data_path = test_case.make_dir()
259
test_case.log_dir = test_case.make_dir()
261
bootstrap_list.bootstrap(data_path=test_case.data_path,
262
log_dir=test_case.log_dir)
264
class MyBrokerConfiguration(BrokerConfiguration):
265
default_config_filenames = [test_case.config_filename]
267
config = MyBrokerConfiguration()
268
config.load(["--bus", "session", "--data-path", test_case.data_path])
270
class FakeBrokerService(BrokerService):
271
"""A broker which uses a fake reactor and fake transport."""
272
reactor_factory = FakeReactor
273
transport_factory = FakeTransport
275
test_case.broker_service = service = FakeBrokerService(config)
276
test_case.remote = FakeRemoteBroker(service.exchanger,
277
service.message_store)
279
def tear_down(self, test_case):
280
bpickle_dbus.uninstall()
283
class RemoteBrokerHelper(FakeRemoteBrokerHelper):
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}.
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}.
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)
308
def tear_down(self, test_case):
309
test_case.broker_service.stopService()
310
super(RemoteBrokerHelper, self).tear_down(test_case)
313
class ExchangeHelper(FakeRemoteBrokerHelper):
315
Backwards compatibility layer for tests that want a bunch of attributes
316
jammed on to them instead of having C{self.broker_service}.
319
def set_up(self, test_case):
320
super(ExchangeHelper, self).set_up(test_case)
322
service = test_case.broker_service
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
334
class MonitorHelper(ExchangeHelper):
336
Provides everything that L{ExchangeHelper} does plus a
337
L{landscape.monitor.monitor.Monitor}.
340
def set_up(self, test_case):
341
super(MonitorHelper, self).set_up(test_case)
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)
353
class ManagerHelper(FakeRemoteBrokerHelper):
355
Provides everything that L{FakeRemoteBrokerHelper} does plus a
356
L{landscape.manager.manager.Manager}.
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,
368
class MockPopen(object):
370
def __init__(self, output, return_codes=None):
372
self.stdout = StringIO(output)
373
self.popen_inputs = []
374
self.return_codes = return_codes
376
def __call__(self, args, stdout=None, stderr=None):
377
return self.popen(args, stdout=stdout, stderr=stderr)
379
def popen(self, args, stdout=None, stderr=None):
380
self.popen_inputs.append(args)
384
if self.return_codes is None:
386
return self.return_codes.pop(0)
389
class StandardIOHelper(object):
391
def set_up(self, test_case):
392
from StringIO import StringIO
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"
400
def tear_down(self, test_case):
401
sys.stdout = test_case.old_stdout
402
sys.stdin = test_case.old_stdin
405
class MockCoverageMonitor(object):
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)
415
def since_reset(self):
416
return self.since_reset_value
419
return self.warn_value
425
class MockFrequencyMonitor(object):
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)
433
return self.warn_value
439
def mock_counter(i=0):
440
"""Generator starts at zero and yields integers that grow by one."""
447
"""Generator starts at 100 and yields int timestamps that grow by one."""
448
return mock_counter(100)
451
class StubProcessFactory(object):
453
A L{IReactorProcess} provider which records L{spawnProcess} calls and
454
allows tests to get at the protocol.
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))
465
class DummyProcess(object):
466
"""A process (transport) that doesn't do anything."""
470
def signalProcess(self, signal):
471
self.signals.append(signal)
473
def closeChildFD(self, fd):
478
class ProcessDataBuilder(object):
479
"""Builder creates sample data for the process info plugin to consume."""
481
RUNNING = "R (running)"
482
STOPPED = "T (stopped)"
483
TRACING_STOP = "T (tracing stop)"
484
DISK_SLEEP = "D (disk sleep)"
485
SLEEPING = "S (sleeping)"
487
ZOMBIE = "Z (zombie)"
489
def __init__(self, sample_dir):
490
"""Initialize factory with directory for sample data."""
491
self._sample_dir = sample_dir
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):
497
"""Creates sample data for a process.
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
503
@param generate_cmd_line: If true, place the process_name in
504
/proc/%(pid)s/cmdline, otherwise leave it empty (this simulates a
506
@param stat_data: Array of items to write to the /proc/<pid>/stat file.
509
Name: %(process_name)s
518
Groups: 4 20 24 25 29 30 44 46 106 110 112 1000
520
VmSize: %(vmsize)d kB
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")
545
file = open(filename, "w+")
547
file.write(sample_data)
550
if stat_data is None:
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")
556
file = open(filename, "w+")
558
file.write(stat_data)
562
if generate_cmd_line:
564
/usr/sbin/%(process_name)s\0--pid-file\0/var/run/%(process_name)s.pid\0
565
""" % {"process_name": process_name}
568
filename = os.path.join(process_dir, "cmdline")
570
file = open(filename, "w+")
572
file.write(sample_data)
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)
583
from twisted.python import log
584
from twisted.python import failure
585
from twisted.trial import reporter
587
def install_trial_hack():
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.
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.
598
def run(self, result):
600
Copied from twisted.trial.unittest.TestCase.run, but some
601
lines from Twisted 2.5.
603
log.msg("--> %s <--" % (self.id()))
606
if not isinstance(result, reporter.TestResult):
607
result = PyUnitResultAdapter(result)
608
# End from Twisted 2.5
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)
619
if hasattr(self, "_installObserver"):
620
self._installObserver()
621
# End from Twisted 2.5
625
first = self._isFirst()
626
self.__class__._instancesRun.add(self)
628
d = self.deferSetUpClass(result)
630
d = self.deferSetUp(None, result)
634
self._cleanUp(result)
635
result.stopTest(self)
636
if self._shared and self._isLast():
637
self._initInstances()
638
self._classCleanUp(result)
640
self._classCleanUp(result)
643
### Copied from Twisted, to fix a bug in trial in Twisted 2.2! ###
645
class UnsupportedTrialFeature(Exception):
646
"""A feature of twisted.trial was used that pyunit cannot support."""
649
class PyUnitResultAdapter(object):
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
657
def __init__(self, original):
659
@param original: A C{TestResult} instance from C{unittest}.
661
self.original = original
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)
669
def startTest(self, method):
670
# We'll need this later in cleanupErrors.
671
self.__currentTest = method
672
self.original.startTest(method)
674
def stopTest(self, method):
675
self.original.stopTest(method)
677
def addFailure(self, test, fail):
678
self.original.addFailure(test, self._exc_info(fail))
680
def addError(self, test, error):
681
self.original.addError(test, self._exc_info(error))
683
def _unsupported(self, test, feature, info):
684
self.original.addFailure(
686
(UnsupportedTrialFeature,
687
UnsupportedTrialFeature(feature, info),
690
def addSkip(self, test, reason):
692
Report the skip as a failure.
694
self._unsupported(test, 'skip', reason)
696
def addUnexpectedSuccess(self, test, todo):
698
Report the unexpected success as a failure.
700
self._unsupported(test, 'unexpected success', todo)
702
def addExpectedFailure(self, test, error):
704
Report the expected failure (i.e. todo) as a failure.
706
self._unsupported(test, 'expected failure', error)
708
def addSuccess(self, test):
709
self.original.addSuccess(test)
711
def upDownError(self, method, error, warn, printStatus):
714
def cleanupErrors(self, errs):
715
# Let's consider cleanupErrors as REAL errors. In recent
716
# Twisted this is the default behavior, and cleanupErrors
718
self.addError(self.__currentTest, errs)
720
def startSuite(self, name):
723
### END COPY FROM TWISTED ###