~allanlesage/uci-engine/coverage-extractor

« back to all changes in this revision

Viewing changes to testing/run_tests.py

Merge with trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import os
20
20
import subunit
21
21
import sys
 
22
import testtools
22
23
 
23
24
 
24
25
HERE = os.path.abspath(os.path.dirname(__file__))
25
26
 
26
27
# deploy.py isn't in our pythonpath when run as a script
27
 
sys.path.append(os.path.join(HERE, 'juju-deployer'))
 
28
sys.path.append(os.path.join(HERE, '..', 'juju-deployer'))
28
29
import deploy
29
30
from ucitests import (
30
31
    filters,
35
36
)
36
37
 
37
38
 
 
39
def rename_tests(suite, renamer):
 
40
    """Rename all tests in a suite.
 
41
 
 
42
    :param suite: The tests to be renamed.
 
43
 
 
44
    :param renamer: A callable returning a callable 'id' method for the
 
45
       test. It should accept a callable that returns the current test id.
 
46
 
 
47
    Note: Care should be taken to ensure that 'renamer' preserve unique ids.
 
48
    """
 
49
    # We modify the tests in place so it's ok to use iter_flat
 
50
    for t in filters.iter_flat(suite):
 
51
        # 'renamer' needs to be a two-level lambda or the wrong variable is
 
52
        # captured. See tests for examples.
 
53
        t.id = renamer(t.id)
 
54
 
 
55
 
 
56
def prefix_test_ids(suite, prefix):
 
57
    """Change the test ids to embed a prefix for easier matching.
 
58
 
 
59
    :param suite: The tests to be renamed.
 
60
 
 
61
    :param prefix: A path from the branch root to the root python module (found
 
62
       in PYTHONPATH).
 
63
    """
 
64
    prefix = prefix.replace(os.path.sep, '.')
 
65
    # The renamer receives the old_id callable and return a lambda that will be
 
66
    # called as test.id().
 
67
    renamer = lambda old_id: lambda: prefix + '.' + old_id()
 
68
    rename_tests(suite, renamer)
 
69
 
 
70
 
38
71
class SysPath(object):
39
72
    """Add paths in front of sys.path temporarily."""
40
73
 
79
112
    components = ['ci-utils',
80
113
                  'cli',
81
114
                  'branch-source-builder',
 
115
                  'britney_proxy',
82
116
                  'gatekeeper',
83
117
                  'image-builder',
84
118
                  'lander',
85
119
                  'test_runner',
86
120
                  'publisher',
 
121
                  'ppa-creator',
 
122
                  'validator',
87
123
                  ]
88
124
    loader = loaders.Loader()
89
125
    # setuptools tends to leave eggs all over the place
93
129
                                                        ])
94
130
    suite = loader.suiteClass()
95
131
    for c in components:
96
 
        suite.addTests(load_component_tests(loader, c))
97
 
    suite = filters.include_regexps(include_regexps, suite)
98
 
    suite = filters.exclude_regexps(exclude_regexps, suite)
99
 
    return suite
100
 
 
101
 
 
102
 
def get_runner():
 
132
        component_suite = load_component_tests(loader, c)
 
133
        prefix_test_ids(component_suite, c)
 
134
        suite.addTests(component_suite)
 
135
    suite = filters.include_regexps(include_regexps, suite)
 
136
    suite = filters.exclude_regexps(exclude_regexps, suite)
 
137
    return suite
 
138
 
 
139
 
 
140
def load_testing_tests(include_regexps, exclude_regexps=None):
 
141
    loader = loaders.Loader()
 
142
    suite = loader.suiteClass()
 
143
    suite.addTests(loader.loadTestsFromTree('testing'))
 
144
    suite = filters.include_regexps(include_regexps, suite)
 
145
    suite = filters.exclude_regexps(exclude_regexps, suite)
 
146
    return suite
 
147
 
 
148
 
 
149
def get_django_runner(component_base_dir):
103
150
    """Return a test runner class tailored to django needs.
104
151
 
105
152
 
110
157
    Since the imports needs to be delayed, that DjangoTestRunner class has to
111
158
    be defined inside a function so it can inherit from DjangoTestSuiteRunner.
112
159
    """
113
 
    from django.test import (
114
 
        simple,
115
 
        utils,
116
 
    )
 
160
    from django.test import simple
117
161
 
118
162
    class DjangoTestRunner(simple.DjangoTestSuiteRunner):
119
163
 
140
184
                return self._cached_test_suite
141
185
            loader = loaders.Loader()
142
186
            suite = load_component_tests(loader, self.component_base_dir)
 
187
            prefix_test_ids(suite, component_base_dir)
143
188
            # Filtered as required by the user
144
189
            suite = filters.include_regexps(
145
190
                self.options.include_regexps, suite)
174
219
                test_labels, extra_tests=None, **kwargs)
175
220
 
176
221
        def run_suite(self, suite, **kwargs):
 
222
            suite = parallelize_suite_round_robin(suite,
 
223
                                                  self.options.concurrency)
177
224
            suite.run(self.result)
178
225
            return self.result
179
226
 
245
292
        # values defined in the project settings.py
246
293
        settings_path = '{}.settings'.format(component_name)
247
294
        os.environ["DJANGO_SETTINGS_MODULE"] = settings_path
248
 
        kls = get_runner()
 
295
        kls = get_django_runner(component_base_dir)
249
296
        test_runner = kls(component_base_dir, requires_south,
250
297
                          out, result, options)
251
298
        failures = test_runner.run_tests([])
266
313
    suite = loader.suiteClass()
267
314
    for c in components:
268
315
        suite.addTests(load_component_tests(loader, c))
 
316
        prefix_test_ids(suite, c)
269
317
    suite = filters.include_regexps(include_regexps, suite)
270
318
    suite = filters.exclude_regexps(exclude_regexps, suite)
271
319
    return suite
280
328
 
281
329
    :return: The test suite for all collected tests.
282
330
    """
283
 
    components = ['tests']
284
331
    loader = loaders.Loader()
285
332
    suite = loader.suiteClass()
286
 
    for c in components:
287
 
        suite.addTests(load_component_tests(loader, c))
 
333
    suite.addTests(loader.loadTestsFromTree('tests'))
288
334
    suite = filters.include_regexps(include_regexps, suite)
289
335
    suite = filters.exclude_regexps(exclude_regexps, suite)
290
336
    return suite
292
338
 
293
339
def load_charm_tests(include_regexps, exclude_regexps=None):
294
340
    """Load charm unittest from <charm>/unit_test."""
295
 
    charms = ['charms/precise/restish']
 
341
    charms = [
 
342
        'charms/precise/lander',
 
343
        'charms/precise/rabbitmq-worker',
 
344
        'charms/precise/wsgi-app',
 
345
        'charms/precise/webui',
 
346
        'charms/precise/key-secret-subordinate',
 
347
    ]
296
348
    loader = loaders.Loader()
297
349
    suite = loader.suiteClass()
298
350
    for c in charms:
299
351
        with SysPath([c, os.path.join(c, 'hooks')]):
300
352
            sub_loader = loader.SubLoader(root=c)
 
353
            charm_suite = loader.suiteClass()
301
354
            # Load only python tests from 'unit_tests' (that's specific
302
355
            # for uci-engine charms).
303
 
            suite.addTests(sub_loader.loadTestsFromTree('unit_tests'))
 
356
            charm_suite.addTests(sub_loader.loadTestsFromTree('unit_tests'))
 
357
            # Change the test ids to embed the leading path for easier matching
 
358
            prefix_test_ids(charm_suite, c)
 
359
        suite.addTests(charm_suite)
304
360
    suite = filters.include_regexps(include_regexps, suite)
305
361
    suite = filters.exclude_regexps(exclude_regexps, suite)
306
362
    return suite
333
389
        return ret
334
390
 
335
391
 
 
392
def parallelize_suite_round_robin(suite, concurrency):
 
393
    """Parallelize a suite.
 
394
 
 
395
    :param suite: The input suite to transform.
 
396
 
 
397
    :param concurrency: The level of concurrency.
 
398
 
 
399
    :returns: A suite that will spread the execution of the input suite tests
 
400
        across ``concurrency`` forked processes. The suite is evenly split
 
401
        across the processes in a round-robin fashion. While this doesn't take
 
402
        into account the duration of each test, it's doing a good job on
 
403
        average as slower tests are most often grouped by class/module in the
 
404
        input suite.
 
405
    """
 
406
    # Nothing to parallelize if there is only one process
 
407
    if concurrency > 1:
 
408
        suite = testtools.ConcurrentTestSuite(
 
409
            suite, runners.fork_for_tests(concurrency))
 
410
    return suite
 
411
 
 
412
 
 
413
class RunTestsArgParser(runners.RunTestsArgParser):
 
414
 
 
415
    def __init__(self):
 
416
        super(RunTestsArgParser, self).__init__('run-tests')
 
417
        self.add_argument(
 
418
            '--no-skip', action='store_true',
 
419
            dest='no_skip',
 
420
            help='Errors if at lest one test is skipped.')
 
421
 
 
422
 
336
423
def main(args, stdout, stderr):
337
424
    # Interpret user wishes
338
 
    parser = runners.RunTestsArgParser()
 
425
    parser = RunTestsArgParser()
339
426
    options = parser.parse_args(args)
340
427
    if options.list_only:
341
428
        # We won't run tests, we won't need a test result
348
435
            result = subunit.TestProtocolClient(stdout)
349
436
        # We'll run tests, setup the result accordingly
350
437
        result.startTestRun()
351
 
    # Load the tests, keeping only the ones required by the user
 
438
    ret = 0
 
439
    # Regular components
352
440
    suite = load_regular_component_tests(options.include_regexps,
353
441
                                         options.exclude_regexps)
354
 
    if options.concurrency > 1:
355
 
        suite = testtools.ConcurrentTestSuite(
356
 
            suite, runners.fork_for_tests(options.concurrency))
357
 
    ret = 0
358
 
    # Regular components
 
442
    suite = parallelize_suite_round_robin(suite, options.concurrency)
359
443
    reg_rc = run_regular_tests(suite, stdout, result, options)
360
444
    ret = reg_rc or ret
 
445
    # The testing infrastructure itself
 
446
    suite = load_testing_tests(options.include_regexps,
 
447
                               options.exclude_regexps)
 
448
    suite = parallelize_suite_round_robin(suite, options.concurrency)
 
449
    tst_rc = run_regular_tests(suite, stdout, result, options)
 
450
    ret = tst_rc or ret
361
451
    # orphan tests that don't have a proper home yet
362
452
    suite = load_orphan_tests(options.include_regexps,
363
453
                              options.exclude_regexps)
 
454
    suite = parallelize_suite_round_robin(suite, options.concurrency)
364
455
    orphan_rc = run_regular_tests(suite, stdout, result, options)
365
456
    ret = orphan_rc or ret
366
 
    # Django-based components
367
 
    with IsolatedImportAndLogging():
368
 
        ppa_rc =  run_django_tests('ppa-assigner', 'ppa_assigner', True,
369
 
                                   stdout, result, options)
370
 
        ret = ppa_rc or ret
371
457
    with IsolatedImportAndLogging():
372
458
        ts_rc = run_django_tests('ticket_system', 'ticket_system', True,
373
459
                                 stdout, result, options)
376
462
    with IsolatedImportAndLogging(), SysPath(['ci-utils']):
377
463
        # FIXME: We need some more setup here that tests themselves should do
378
464
        # but they aren't ripe for that yet -- vila 2014-04-11
379
 
        branches = os.path.join(HERE, 'branches')
 
465
        branches = os.path.join(HERE, '..', 'branches')
380
466
        deploy.build_sourcedeps(branches)
381
 
        deploy.setup(for_tests=True, list_only=options.list_only)
382
 
        # Charm tests
383
 
        charm_suite = load_charm_tests(options.include_regexps,
384
 
                                       options.exclude_regexps)
385
 
 
386
 
        charm_rc = run_regular_tests(charm_suite, stdout, result, options)
387
 
        ret = charm_rc or ret
388
 
        # Integration tests
389
 
        suite = load_integration_tests(options.include_regexps,
390
 
                                       options.exclude_regexps)
391
 
        itg_rc = run_regular_tests(suite, stdout, result, options)
392
 
        ret = itg_rc or ret
 
467
        try:
 
468
            deploy.setup(for_tests=True, list_only=options.list_only)
 
469
        except SystemExit:
 
470
            # The setup includes checking the environment and raising
 
471
            # SystemExit if one var is missing. Since this is entangled with
 
472
            # building the charms, we skip the lot.
 
473
            pass
 
474
        else:
 
475
            # Charm tests
 
476
            charm_suite = load_charm_tests(
 
477
                options.include_regexps, options.exclude_regexps)
 
478
            charm_rc = run_regular_tests(
 
479
                charm_suite, stdout, result, options)
 
480
            ret = charm_rc or ret
 
481
            # Integration tests
 
482
            suite = load_integration_tests(
 
483
                options.include_regexps, options.exclude_regexps)
 
484
            itg_rc = run_regular_tests(suite, stdout, result, options)
 
485
            ret = itg_rc or ret
393
486
 
394
487
    if not options.list_only:
395
488
        # We did run tests, stop the run
396
489
        result.stopTestRun()
397
490
    # Fails if at least one fail
 
491
    if not options.list_only and options.no_skip and result.skip_reasons:
 
492
        # Some tests were skipped
 
493
        return 1
398
494
    return int(ret)
399
 
 
400
 
 
401
 
if __name__ == '__main__':
402
 
    sys.exit(main(sys.argv[1:], sys.stdout, sys.stderr))