diff options
Diffstat (limited to 'waflib/extras')
-rw-r--r-- | waflib/extras/autowaf.py | 547 |
1 files changed, 337 insertions, 210 deletions
diff --git a/waflib/extras/autowaf.py b/waflib/extras/autowaf.py index 92d0e57e..5739b58b 100644 --- a/waflib/extras/autowaf.py +++ b/waflib/extras/autowaf.py @@ -4,7 +4,7 @@ import subprocess import sys import time -from waflib import Build, Context, Logs, Options, Utils +from waflib import Configure, ConfigSet, Build, Context, Logs, Options, Utils from waflib.TaskGen import feature, before, after global g_is_child @@ -17,16 +17,19 @@ g_step = 0 global line_just line_just = 40 +NONEMPTY = -10 + +if sys.platform == 'win32': + lib_path_name = 'PATH' +elif sys.platform == 'darwin': + lib_path_name = 'DYLD_LIBRARY_PATH' +else: + lib_path_name = 'LD_LIBRARY_PATH' + # Compute dependencies globally # import preproc # preproc.go_absolute = True -# Test context that inherits build context to make configuration available -class TestContext(Build.BuildContext): - "Run tests" - cmd = 'test' - fun = 'test' - @feature('c', 'cxx') @after('apply_incpaths') def include_config_h(self): @@ -95,6 +98,21 @@ def add_flags(opt, flags): opt.add_option('--' + name, action='store_true', dest=name.replace('-', '_'), help=desc) +class ConfigureContext(Configure.ConfigurationContext): + """configures the project""" + + def __init__(self, **kwargs): + super(ConfigureContext, self).__init__(**kwargs) + self.run_env = ConfigSet.ConfigSet() + + def store(self): + self.env.AUTOWAF_RUN_ENV = self.run_env.get_merged_dict() + super(ConfigureContext, self).store() + + def build_path(self, path): + """Return `path` within the build directory""" + return str(self.path.get_bld().find_node(path)) + def get_check_func(conf, lang): if lang == 'c': return conf.check_cc @@ -463,13 +481,15 @@ def set_lib_env(conf, name, version): major_ver = version.split('.')[0] pkg_var_name = 'PKG_' + name.replace('-', '_') + '_' + major_ver lib_name = '%s-%s' % (name, major_ver) + lib_path = [str(conf.path.get_bld())] if conf.env.PARDEBUG: lib_name += 'D' conf.env[pkg_var_name] = lib_name conf.env['INCLUDES_' + NAME] = ['${INCLUDEDIR}/%s-%s' % (name, major_ver)] - conf.env['LIBPATH_' + NAME] = [conf.env.LIBDIR] + conf.env['LIBPATH_' + NAME] = lib_path conf.env['LIB_' + NAME] = [lib_name] + conf.run_env.append_unique(lib_path_name, lib_path) conf.define(NAME + '_VERSION', version) def set_line_just(conf, width): @@ -744,18 +764,57 @@ def build_i18n(bld, srcdir, dir, name, sources, copyright_holder=None): build_i18n_po(bld, srcdir, dir, name, sources, copyright_holder) build_i18n_mo(bld, srcdir, dir, name, sources, copyright_holder) -def cd_to_build_dir(ctx, appname): - top_level = (len(ctx.stack_path) > 1) - if top_level: - os.chdir(os.path.join('build', appname)) - else: - os.chdir('build') +class ExecutionEnvironment: + """Context that sets system environment variables for program execution""" + def __init__(self, changes): + self.original_environ = os.environ.copy() -def cd_to_orig_dir(ctx, child): - if child: - os.chdir(os.path.join('..', '..')) - else: - os.chdir('..') + self.diff = {} + for path_name, paths in changes.items(): + value = os.pathsep.join(paths) + if path_name in os.environ: + value += os.pathsep + os.environ[path_name] + + self.diff[path_name] = value + + os.environ.update(self.diff) + + def __str__(self): + return '\n'.join({'%s="%s"' % (k, v) for k, v in self.diff.items()}) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + os.environ = self.original_environ + +def show_diff(from_lines, to_lines, from_filename, to_filename): + import difflib + import sys + + for line in difflib.unified_diff( + from_lines, to_lines, + fromfile=os.path.abspath(from_filename), + tofile=os.path.abspath(to_filename)): + sys.stderr.write(line) + +def test_file_equals(patha, pathb): + import filecmp + import io + + for path in (patha, pathb): + if not os.access(path, os.F_OK): + Logs.pprint('RED', 'error: missing file %s' % path) + return False + + if filecmp.cmp(patha, pathb, shallow=False): + return True + + with io.open(patha, 'rU', encoding='utf-8') as fa: + with io.open(pathb, 'rU', encoding='utf-8') as fb: + show_diff(fa.readlines(), fb.readlines(), patha, pathb) + + return False def bench_time(): if hasattr(time, 'perf_counter'): # Added in Python 3.3 @@ -763,207 +822,275 @@ def bench_time(): else: return time.time() -def pre_test(ctx, appname, dirs=['src']): - Logs.pprint('GREEN', '\n[==========] Running %s tests' % appname) - - if not hasattr(ctx, 'autowaf_tests_total'): - ctx.autowaf_tests_start_time = bench_time() - ctx.autowaf_tests_total = 0 - ctx.autowaf_tests_failed = 0 - ctx.autowaf_local_tests_total = 0 - ctx.autowaf_local_tests_failed = 0 - ctx.autowaf_tests = {} - - ctx.autowaf_tests[appname] = {'total': 0, 'failed': 0} - - cd_to_build_dir(ctx, appname) - if not ctx.env.NO_COVERAGE: - diropts = '' - for i in dirs: - diropts += ' -d ' + i - clear_log = open('lcov-clear.log', 'w') +class TestScope: + """Scope that maintains pass/fail statistics for a test group""" + def __init__(self, name): + self.name = name + self.n_failed = 0 + self.n_total = 0 + +class TestContext(Build.BuildContext): + "runs test suite" + fun = cmd = 'test' + + def __init__(self, **kwargs): + super(TestContext, self).__init__(**kwargs) + self.defaults = {'verbosity': 2 if Options.options.verbose else 0} + self.n_tests_failed = 0 + self.n_tests_total = 0 + self.start_time = bench_time() + self.stack = [TestScope(Context.g_module.APPNAME)] + + def finalize(self): + if self.n_tests_failed > 0: + sys.exit(1) + + super(TestContext, self).finalize() + + def log_header(self, fmt, *args): + Logs.info('') + self.log_good('=' * 10, fmt % args) + + def log_footer(self, fmt, *args): + if self.defaults['verbosity'] > 0: + Logs.info('') + self.log_good('=' * 10, fmt % args) + + def log_good(self, title, fmt, *args): + Logs.pprint('GREEN', '[%s] %s' % (title.center(10), fmt % args)) + + def log_bad(self, title, fmt, *args): + Logs.pprint('RED', '[%s] %s' % (title.center(10), fmt % args)) + + def pre_recurse(self, node): + import importlib + wscript_module = Context.load_module(node.abspath()) + group_name = wscript_module.APPNAME + self.stack.append(TestScope(group_name)) + + bld_dir = str(node.get_bld().parent) + Logs.info("Waf: Entering directory `%s'", bld_dir) + os.chdir(bld_dir) + + self.log_header('Running %s tests', group_name) + if str(node.parent) == Context.top_dir: + self.clear_coverage() + super(TestContext, self).pre_recurse(node) + + def test_result(self, success): + self.stack[-1].n_total += 1 + self.stack[-1].n_failed += 1 if not success else 0 + + def pop(self): + scope = self.stack.pop() + self.stack[-1].n_total += scope.n_total + self.stack[-1].n_failed += scope.n_failed + return scope + + def post_recurse(self, node): + super(TestContext, self).post_recurse(node) + + scope = self.pop() + duration = (bench_time() - self.start_time) * 1000.0 + self.log_footer('%d tests from %s ran (%d ms total)', + scope.n_total, scope.name, duration) + + if not self.env.NO_COVERAGE: + if str(node.parent) == Context.top_dir: + self.gen_coverage() + + if os.path.exists('coverage/index.html'): + self.log_good('COVERAGE', '<file://%s>', + os.path.abspath('coverage/index.html')) + + successes = scope.n_total - scope.n_failed + Logs.pprint('GREEN', '[ PASSED ] %d tests' % successes) + if scope.n_failed > 0: + Logs.pprint('RED', '[ FAILED ] %d tests' % scope.n_failed) + + Logs.info("\nWaf: Leaving directory `%s'", os.getcwd()) + os.chdir(str(self.path)) + + def execute(self): + self.restore() + if not self.all_envs: + self.load_envs() + + if not self.env.BUILD_TESTS: + self.fatal('Configuration does not include tests') + + with ExecutionEnvironment(self.env.AUTOWAF_RUN_ENV) as env: + if self.defaults['verbosity'] > 0: + print(env) + self.recurse([self.run_dir]) + + def src_path(self, path): + return os.path.relpath(os.path.join(str(self.path), path)) + + def args(self, **kwargs): + all_kwargs = self.defaults.copy() + all_kwargs.update(kwargs) + return all_kwargs + + def test_group(self, name, **kwargs): + return TestGroup(self, self.stack[-1].name, name, **self.args(**kwargs)) + + def set_test_defaults(self, **kwargs): + """Set default arguments to be passed to all tests""" + self.defaults.update(kwargs) + + def clear_coverage(self): + """Zero old coverage data""" try: - try: - # Clear coverage data - subprocess.call(('lcov %s -z' % diropts).split(), - stdout=clear_log, stderr=clear_log) - except Exception: - Logs.warn('Failed to run lcov, no coverage report generated') - finally: - clear_log.close() - -class TestFailed(Exception): - pass - -def post_test(ctx, appname, dirs=['src'], remove=['*boost*', 'c++*']): - if not ctx.env.NO_COVERAGE: - diropts = '' - for i in dirs: - diropts += ' -d ' + i - coverage_log = open('lcov-coverage.log', 'w') - coverage_lcov = open('coverage.lcov', 'w') - coverage_stripped_lcov = open('coverage-stripped.lcov', 'w') + with open('cov-clear.log', 'w') as log: + subprocess.call(['lcov', '-z', '-d', str(self.path)], + stdout=log, stderr=log) + + except Exception: + Logs.warn('Failed to run lcov to clear old coverage data') + + def gen_coverage(self): + """Generate coverage data and report""" try: - try: - base = '.' - if g_is_child: - base = '..' - - # Generate coverage data - lcov_cmd = 'lcov -c %s -b %s' % (diropts, base) - if ctx.env.LLVM_COV: - lcov_cmd += ' --gcov-tool %s' % ctx.env.LLVM_COV[0] - subprocess.call(lcov_cmd.split(), - stdout=coverage_lcov, stderr=coverage_log) - - # Strip unwanted stuff - subprocess.call( - ['lcov', '--remove', 'coverage.lcov'] + remove, - stdout=coverage_stripped_lcov, stderr=coverage_log) - - # Generate HTML coverage output - if not os.path.isdir('coverage'): - os.makedirs('coverage') - subprocess.call( - 'genhtml -o coverage coverage-stripped.lcov'.split(), - stdout=coverage_log, stderr=coverage_log) - - except Exception: - Logs.warn('Failed to run lcov, no coverage report generated') - finally: - coverage_stripped_lcov.close() - coverage_lcov.close() - coverage_log.close() - - duration = (bench_time() - ctx.autowaf_tests_start_time) * 1000.0 - total_tests = ctx.autowaf_tests[appname]['total'] - failed_tests = ctx.autowaf_tests[appname]['failed'] - passed_tests = total_tests - failed_tests - Logs.pprint('GREEN', '\n[==========] %d tests from %s ran (%d ms total)' % ( - total_tests, appname, duration)) - if not ctx.env.NO_COVERAGE: - Logs.pprint('GREEN', '[----------] Coverage: <file://%s>' - % os.path.abspath('coverage/index.html')) - - Logs.pprint('GREEN', '[ PASSED ] %d tests' % passed_tests) - if failed_tests > 0: - Logs.pprint('RED', '[ FAILED ] %d tests' % failed_tests) - raise TestFailed('Tests from %s failed' % appname) - Logs.pprint('', '') - - top_level = (len(ctx.stack_path) > 1) - if top_level: - cd_to_orig_dir(ctx, top_level) + with open('cov.lcov', 'w') as out: + with open('cov.log', 'w') as err: + subprocess.call(['lcov', '-c', '--no-external', + '--rc', 'lcov_branch_coverage=1', + '-b', '.', + '-d', str(self.path)], + stdout=out, stderr=err) + + if not os.path.isdir('coverage'): + os.makedirs('coverage') + + with open('genhtml.log', 'w') as log: + subprocess.call(['genhtml', + '-o', 'coverage', + '--rc', 'genhtml_branch_coverage=1', + 'cov.lcov'], + stdout=log, stderr=log) + + except Exception: + Logs.warn('Failed to run lcov to generate coverage report') def run_test(ctx, appname, test, - desired_status=0, - dirs=['src'], + expected=0, name='', - header=False, - quiet=False): - """Run an individual test. - - `test` is either a shell command string, or a list of [name, return status] - for displaying tests implemented in the calling Python code. - """ - - ctx.autowaf_tests_total += 1 - ctx.autowaf_local_tests_total += 1 - ctx.autowaf_tests[appname]['total'] += 1 - - out = (None, None) + stdin=None, + stdout=None, + stderr=None, + verbosity=1): + class TestOutput(object): + """Test output that is truthy if result is as expected""" + def __init__(self, expected_result): + self.expected_result = expected_result + self.status = self.stdout = self.stderr = None + + def __bool__(self): + return (self.expected_result is None or + self.result == self.expected_result) + + __nonzero__ = __bool__ + + def stream(s): + return open(s, 'wb') if type(s) == str else s + + def is_string(s): + if sys.version_info[0] < 3: + return isinstance(s, basestring) + return isinstance(s, str) + + output = TestOutput(expected) if type(test) == list: - name = test[0] - returncode = test[1] - elif callable(test): - returncode = test() - else: - s = test - if isinstance(test, type([])): - s = ' '.join(test) - if header and not quiet: - Logs.pprint('Green', '\n[ RUN ] %s' % s) - cmd = test - if Options.options.test_wrapper: - cmd = Options.options.test_wrapper + ' ' + test - if name == '': - name = test - - proc = subprocess.Popen(cmd, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out = proc.communicate() - returncode = proc.returncode - - success = desired_status is None or returncode == desired_status - if success: - if not quiet: - Logs.pprint('GREEN', '[ OK ] %s' % name) - else: - Logs.pprint('RED', '[ FAILED ] %s' % name) - ctx.autowaf_tests_failed += 1 - ctx.autowaf_local_tests_failed += 1 - ctx.autowaf_tests[appname]['failed'] += 1 - if type(test) != list and not callable(test): - Logs.pprint('RED', test) - - if Options.options.verbose and type(test) != list and not callable(test): - sys.stdout.write(out[0].decode('utf-8')) - sys.stderr.write(out[1].decode('utf-8')) - - return (success, out) - -def tests_name(ctx, appname, name='*'): - if name == '*': - return appname - else: - return '%s.%s' % (appname, name) - -def begin_tests(ctx, appname, name='*'): - ctx.autowaf_local_tests_failed = 0 - ctx.autowaf_local_tests_total = 0 - ctx.autowaf_local_tests_start_time = bench_time() - Logs.pprint('GREEN', '\n[----------] %s' % ( - tests_name(ctx, appname, name))) - - class Handle: - def __enter__(self): - pass - - def __exit__(self, type, value, traceback): - end_tests(ctx, appname, name) + import pipes + name = name if name else ' '.join(map(pipes.quote, test)) + if verbosity > 1: + ctx.log_good('RUN ', name) - return Handle() - -def end_tests(ctx, appname, name='*'): - duration = (bench_time() - ctx.autowaf_local_tests_start_time) * 1000.0 - total = ctx.autowaf_local_tests_total - failures = ctx.autowaf_local_tests_failed - if failures == 0: - Logs.pprint('GREEN', '[----------] %d tests from %s (%d ms total)' % ( - ctx.autowaf_local_tests_total, tests_name(ctx, appname, name), duration)) + if Options.options.test_wrapper: + test = [Options.options.test_wrapper] + test + + out_stream = open(stdout, 'wb') if is_string(stdout) else None + err_stream = open(stderr, 'wb') if is_string(stderr) else None + stdout = out_stream if out_stream else stdout + stderr = err_stream if err_stream else stderr + + with open(os.devnull, 'wb') as null: + out = null if verbosity < 3 and not stdout else stdout + err = null if verbosity < 2 and not stderr else stderr + proc = subprocess.Popen(test, stdin=stdin, stdout=out, stderr=err) + output.stdout, output.stderr = proc.communicate() + output.result = proc.returncode + + out_stream = out_stream.close() if out_stream else None + err_stream = err_stream.close() if err_stream else None + elif callable(test): + output.result = test() else: - Logs.pprint('RED', '[----------] %d/%d tests from %s (%d ms total)' % ( - total - failures, total, tests_name(ctx, appname, name), duration)) - -def run_tests(ctx, - appname, - tests, - desired_status=0, - dirs=['src'], - name='*', - headers=False): - begin_tests(ctx, appname, name) - - diropts = '' - for i in dirs: - diropts += ' -d ' + i - - for i in tests: - run_test(ctx, appname, i, desired_status, dirs, i, headers) - - end_tests(ctx, appname, name) + ctx.log_bad('ERROR', name) + return output + + if output and verbosity > 0: + ctx.log_good(' OK', name) + elif not output: + ctx.log_bad('FAILED', name) + + return output + +class TestGroup: + def __init__(self, tst, suitename, name, **kwargs): + self.tst = tst + self.suitename = suitename + self.name = name + self.kwargs = kwargs + self.start_time = bench_time() + tst.stack.append(TestScope(name)) + + def label(self): + return self.suitename + '.%s' % self.name if self.name else '' + + def args(self, **kwargs): + all_kwargs = self.tst.args(**self.kwargs) + all_kwargs.update(kwargs) + return all_kwargs + + def run(self, test, **kwargs): + all_kwargs = self.args(**kwargs) + + if 'stderr' in all_kwargs and all_kwargs['stderr'] == NONEMPTY: + import tempfile + with tempfile.TemporaryFile(mode='w') as stderr: + all_kwargs['stderr'] = stderr + output = run_test(self.tst, self.suitename, test, **all_kwargs) + if output: + output = self.run(lambda: stderr.tell() > 0, + expected=True, + verbosity=0, + name=kwargs['name'] + ' error message') + else: + output = run_test(self.tst, self.suitename, test, **all_kwargs) + + self.tst.test_result(output) + return output + + def __enter__(self): + if 'verbosity' in self.kwargs and self.kwargs['verbosity'] > 0: + Logs.info('') + self.tst.log_good('-' * 10, self.label()) + return self + + def __exit__(self, type, value, traceback): + duration = (bench_time() - self.start_time) * 1000.0 + scope = self.tst.pop() + n_passed = scope.n_total - scope.n_failed + if scope.n_failed == 0: + self.tst.log_good('-' * 10, '%d tests from %s (%d ms total)', + scope.n_total, self.label(), duration) + else: + self.tst.log_bad('-' * 10, '%d/%d tests from %s (%d ms total)', + n_passed, scope.n_total, self.label(), duration) def run_ldconfig(ctx): should_run = (ctx.cmd == 'install' and |