blob: c792dca332a4bdd74164ed5e7f4c441c575849ba [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2012 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Runs the whole set of swarming client unit tests on swarming itself.
This is done in a few steps:
- Archive the whole directory as a single .isolated file.
- Create one test-specific .isolated for each test to run. The file is created
directly and archived manually with isolateserver.py.
- Trigger each of these test-specific .isolated file per OS.
- Get all results out of order.
"""
__version__ = '0.1'
import glob
import logging
import os
import subprocess
import sys
import tempfile
import time
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(
__file__.decode(sys.getfilesystemencoding()))))
# Must be first import.
import parallel_execution
from third_party import colorama
from third_party.depot_tools import fix_encoding
from utils import file_path
from utils import tools
def check_output(cmd):
return subprocess.check_output([sys.executable] + cmd, cwd=ROOT_DIR)
def archive_tree(isolate_server):
"""Archives a whole tree and return the sha1 of the .isolated file.
Manually creates a temporary isolated file and archives it.
"""
cmd = [
'isolateserver.py', 'archive', '--isolate-server', isolate_server,
ROOT_DIR, '--blacklist="\\.git"',
]
if logging.getLogger().isEnabledFor(logging.INFO):
cmd.append('--verbose')
out = check_output(cmd)
return out.split()[0]
def archive_isolated_triggers(isolate_server, tree_isolated, tests):
"""Creates and archives all the .isolated files for the tests at once.
Archiving them in one batch is faster than archiving each file individually.
Also the .isolated files can be reused across OSes, reducing the amount of
I/O.
Returns:
list of (test, sha1) tuples.
"""
logging.info('archive_isolated_triggers(%s, %s)', tree_isolated, tests)
tempdir = tempfile.mkdtemp(prefix=u'run_swarming_tests_on_swarming_')
try:
isolateds = []
for test in tests:
test_name = os.path.basename(test)
# Creates a manual .isolated file. See
# https://chromium.googlesource.com/infra/luci/luci-py.git/+/master/appengine/isolate/doc/Design.md#file-format
isolated = {
'algo': 'sha-1',
'command': ['python', test],
'includes': [tree_isolated],
'read_only': 0,
'version': '1.4',
}
v = os.path.join(tempdir, test_name + '.isolated')
tools.write_json(v, isolated, True)
isolateds.append(v)
cmd = [
'isolateserver.py', 'archive', '--isolate-server', isolate_server,
] + isolateds
if logging.getLogger().isEnabledFor(logging.INFO):
cmd.append('--verbose')
items = [i.split() for i in check_output(cmd).splitlines()]
assert len(items) == len(tests)
assert all(
items[i][1].endswith(os.path.basename(tests[i]) + '.isolated')
for i in xrange(len(tests)))
return zip(tests, [i[0] for i in items])
finally:
file_path.rmtree(tempdir)
def run_swarming_tests_on_swarming(
swarming_server, isolate_server, priority, oses, tests, logs,
no_idempotent):
"""Archives, triggers swarming jobs and gets results."""
print('Archiving the whole tree.')
start = time.time()
tree_isolated = archive_tree(isolate_server)
# Create and archive all the .isolated files.
isolateds = archive_isolated_triggers(isolate_server, tree_isolated, tests)
print('Archival took %3.2fs' % (time.time() - start))
exploded = []
for test_path, isolated_hash in isolateds:
logging.debug('%s: %s', test_path, isolated_hash)
test_name = os.path.basename(test_path).split('.')[0]
for platform in oses:
exploded.append((test_name, platform, isolated_hash))
tasks = [
(
parallel_execution.task_to_name(name, {'os': platform}, isolated_hash),
isolated_hash,
{'os': platform},
) for name, platform, isolated_hash in exploded
]
extra_args = [
'--hard-timeout', '180',
]
if not no_idempotent:
extra_args.append('--idempotent')
if priority:
extra_args.extend(['--priority', str(priority)])
print('Using priority %s' % priority)
result = 0
for failed_task in parallel_execution.run_swarming_tasks_parallel(
swarming_server, isolate_server, extra_args, tasks):
test_name, dimensions, stdout = failed_task
if logs:
# Write the logs are they are retrieved.
if not os.path.isdir(logs):
os.makedirs(logs)
name = '%s_%s.log' % (dimensions['os'], test_name.split('/', 1)[0])
with open(os.path.join(logs, name), 'wb') as f:
f.write(stdout)
result = 1
return result
def main():
parser = parallel_execution.OptionParser(
usage='%prog [options]', version=__version__)
parser.add_option(
'--logs',
help='Destination where to store the failure logs (recommended!)')
parser.add_option('-o', '--os', help='Run tests only on this OS')
parser.add_option(
'-t', '--test', action='append',
help='Run only these test, can be specified multiple times')
parser.add_option(
'--no-idempotent', action='store_true',
help='Do not use --idempotent to detect flaky tests')
options, args = parser.parse_args()
if args:
parser.error('Unsupported argument %s' % args)
oses = ['Linux', 'Mac', 'Windows']
tests = [
os.path.relpath(i, ROOT_DIR)
for i in (
glob.glob(os.path.join(ROOT_DIR, 'tests', '*_test.py')) +
glob.glob(os.path.join(ROOT_DIR, 'googletest', 'tests', '*_test.py')))
]
valid_tests = sorted(map(os.path.basename, tests))
assert len(valid_tests) == len(set(valid_tests)), (
'Can\'t have 2 tests with the same base name')
if options.test:
for t in options.test:
if not t in valid_tests:
parser.error(
'--test %s is unknown. Valid values are:\n%s' % (
t, '\n'.join(' ' + i for i in valid_tests)))
filters = tuple(os.path.sep + t for t in options.test)
tests = [t for t in tests if t.endswith(filters)]
if options.os:
if options.os not in oses:
parser.error(
'--os %s is unknown. Valid values are %s' % (
options.os, ', '.join(sorted(oses))))
oses = [options.os]
if sys.platform in ('win32', 'cygwin'):
# If we are on Windows, don't generate the tests for Linux and Mac since
# they use symlinks and we can't create symlinks on windows.
oses = ['Windows']
if options.os != 'win32':
print('Linux and Mac tests skipped since running on Windows.')
return run_swarming_tests_on_swarming(
options.swarming,
options.isolate_server,
options.priority,
oses,
tests,
options.logs,
options.no_idempotent)
if __name__ == '__main__':
fix_encoding.fix_encoding()
tools.disable_buffering()
colorama.init()
sys.exit(main())