# Copyright 2013 The Servo Project Developers. See the COPYRIGHT # file at the top-level directory of this distribution. # # Licensed under the Apache License, Version 2.0 or the MIT license # , at your # option. This file may not be copied, modified, or distributed # except according to those terms. from __future__ import print_function, unicode_literals import argparse import re import sys import os import os.path as path import copy from collections import OrderedDict from time import time from mach.registrar import Registrar from mach.decorators import ( CommandArgument, CommandProvider, Command, ) from servo.command_base import CommandBase, call, check_call, host_triple from wptrunner import wptcommandline from update import updatecommandline from servo_tidy import tidy from servo_tidy_tests import test_tidy SCRIPT_PATH = os.path.split(__file__)[0] PROJECT_TOPLEVEL_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..")) WEB_PLATFORM_TESTS_PATH = os.path.join("tests", "wpt", "web-platform-tests") SERVO_TESTS_PATH = os.path.join("tests", "wpt", "mozilla", "tests") TEST_SUITES = OrderedDict([ ("tidy", {"kwargs": {"all_files": False, "no_progress": False, "self_test": False}, "include_arg": "include"}), ("wpt", {"kwargs": {"release": False}, "paths": [path.abspath(WEB_PLATFORM_TESTS_PATH), path.abspath(SERVO_TESTS_PATH)], "include_arg": "include"}), ("css", {"kwargs": {"release": False}, "paths": [path.abspath(path.join("tests", "wpt", "css-tests"))], "include_arg": "include"}), ("unit", {"kwargs": {}, "paths": [path.abspath(path.join("tests", "unit"))], "include_arg": "test_name"}), ("compiletest", {"kwargs": {"release": False}, "paths": [path.abspath(path.join("tests", "compiletest"))], "include_arg": "test_name"}) ]) TEST_SUITES_BY_PREFIX = {path: k for k, v in TEST_SUITES.iteritems() if "paths" in v for path in v["paths"]} def create_parser_wpt(): parser = wptcommandline.create_parser() parser.add_argument('--release', default=False, action="store_true", help="Run with a release build of servo") parser.add_argument('--chaos', default=False, action="store_true", help="Run under chaos mode in rr until a failure is captured") parser.add_argument('--pref', default=[], action="append", dest="prefs", help="Pass preferences to servo") return parser @CommandProvider class MachCommands(CommandBase): DEFAULT_RENDER_MODE = "cpu" HELP_RENDER_MODE = "Value can be 'cpu', 'gpu' or 'both' (default " + DEFAULT_RENDER_MODE + ")" def __init__(self, context): CommandBase.__init__(self, context) if not hasattr(self.context, "built_tests"): self.context.built_tests = False @Command('test', description='Run specified Servo tests', category='testing') @CommandArgument('params', default=None, nargs="...", help="Optionally select test based on " "test file directory") @CommandArgument('--render-mode', '-rm', default=DEFAULT_RENDER_MODE, help="The render mode to be used on all tests. " + HELP_RENDER_MODE) @CommandArgument('--release', default=False, action="store_true", help="Run with a release build of servo") @CommandArgument('--tidy-all', default=False, action="store_true", help="Check all files, and run the WPT lint in tidy, " "even if unchanged") @CommandArgument('--no-progress', default=False, action="store_true", help="Don't show progress for tidy") @CommandArgument('--self-test', default=False, action="store_true", help="Run unit tests for tidy") @CommandArgument('--all', default=False, action="store_true", dest="all_suites", help="Run all test suites") def test(self, params, render_mode=DEFAULT_RENDER_MODE, release=False, tidy_all=False, no_progress=False, self_test=False, all_suites=False): suites = copy.deepcopy(TEST_SUITES) suites["tidy"]["kwargs"] = {"all_files": tidy_all, "no_progress": no_progress, "self_test": self_test} suites["wpt"]["kwargs"] = {"release": release} suites["css"]["kwargs"] = {"release": release} suites["unit"]["kwargs"] = {} suites["compiletest"]["kwargs"] = {"release": release} selected_suites = OrderedDict() if params is None: if all_suites: params = suites.keys() else: print("Specify a test path or suite name, or pass --all to run all test suites.\n\nAvailable suites:") for s in suites: print(" %s" % s) return 1 for arg in params: found = False if arg in suites and arg not in selected_suites: selected_suites[arg] = [] found = True else: suite = self.suite_for_path(arg) if suite is not None: if suite not in selected_suites: selected_suites[suite] = [] selected_suites[suite].append(arg) found = True break if not found: print("%s is not a valid test path or suite name" % arg) return 1 test_start = time() for suite, tests in selected_suites.iteritems(): props = suites[suite] kwargs = props.get("kwargs", {}) if tests: kwargs[props["include_arg"]] = tests Registrar.dispatch("test-%s" % suite, context=self.context, **kwargs) elapsed = time() - test_start print("Tests completed in %0.2fs" % elapsed) # Helper to determine which test suite owns the path def suite_for_path(self, path_arg): if os.path.exists(path.abspath(path_arg)): abs_path = path.abspath(path_arg) for prefix, suite in TEST_SUITES_BY_PREFIX.iteritems(): if abs_path.startswith(prefix): return suite return None @Command('test-geckolib', description='Test geckolib sanity checks', category='testing') def test_geckolib(self): self.ensure_bootstrapped() env = self.build_env() env["RUST_BACKTRACE"] = "1" return call(["cargo", "test"], env=env, cwd=path.join("ports", "geckolib")) @Command('test-unit', description='Run unit tests', category='testing') @CommandArgument('--package', '-p', default=None, help="Specific package to test") @CommandArgument('test_name', nargs=argparse.REMAINDER, help="Only run tests that match this pattern or file path") def test_unit(self, test_name=None, package=None): if test_name is None: test_name = [] self.ensure_bootstrapped() if package: packages = {package} else: packages = set() test_patterns = [] for test in test_name: # add package if 'tests/unit/' match = re.search("tests/unit/(\\w+)/?$", test) if match: packages.add(match.group(1)) # add package & test if '/', 'tests/unit//.rs', or similar elif re.search("\\w/\\w", test): tokens = test.split("/") packages.add(tokens[-2]) test_prefix = tokens[-1] if test_prefix.endswith(".rs"): test_prefix = test_prefix[:-3] test_prefix += "::" test_patterns.append(test_prefix) # add test as-is otherwise else: test_patterns.append(test) if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) args = ["cargo", "test"] for crate in packages: args += ["-p", "%s_tests" % crate] args += test_patterns features = self.servo_features() if features: args += ["--features", "%s" % ' '.join(features)] env = self.build_env() env["RUST_BACKTRACE"] = "1" if sys.platform in ("win32", "msys"): if "msvc" in host_triple(): # on MSVC, we need some DLLs in the path. They were copied # in to the servo.exe build dir, so just point PATH to that. env["PATH"] = "%s%s%s" % (path.dirname(self.get_binary_path(False, False)), os.pathsep, env["PATH"]) else: env["RUSTFLAGS"] = "-C link-args=-Wl,--subsystem,windows" result = call(args, env=env, cwd=self.servo_crate()) if result != 0: return result @Command('test-compiletest', description='Run compiletests', category='testing') @CommandArgument('--package', '-p', default=None, help="Specific package to test") @CommandArgument('test_name', nargs=argparse.REMAINDER, help="Only run tests that match this pattern or file path") @CommandArgument('--release', default=False, action="store_true", help="Run with a release build of servo") def test_compiletest(self, test_name=None, package=None, release=False): if test_name is None: test_name = [] self.ensure_bootstrapped() if package: packages = {package} else: packages = set() test_patterns = [] for test in test_name: # add package if 'tests/compiletest/' match = re.search("tests/compiletest/(\\w+)/?$", test) if match: packages.add(match.group(1)) # add package & test if '/', 'tests/compiletest//.rs', or similar elif re.search("\\w/\\w", test): tokens = test.split("/") packages.add(tokens[-2]) test_prefix = tokens[-1] if test_prefix.endswith(".rs"): test_prefix = test_prefix[:-3] test_prefix += "::" test_patterns.append(test_prefix) # add test as-is otherwise else: test_patterns.append(test) if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "compiletest"))) packages.remove("helper") args = ["cargo", "test"] for crate in packages: args += ["-p", "%s_compiletest" % crate] args += test_patterns env = self.build_env() if release: env["BUILD_MODE"] = "release" args += ["--release"] else: env["BUILD_MODE"] = "debug" result = call(args, env=env, cwd=self.servo_crate()) if result != 0: return result @Command('test-content', description='Run the content tests', category='testing') def test_content(self): print("Content tests have been replaced by web-platform-tests under " "tests/wpt/mozilla/.") return 0 @Command('test-tidy', description='Run the source code tidiness check', category='testing') @CommandArgument('--all', default=False, action="store_true", dest="all_files", help="Check all files, and run the WPT lint in tidy, " "even if unchanged") @CommandArgument('--no-progress', default=False, action="store_true", help="Don't show progress for tidy") @CommandArgument('--self-test', default=False, action="store_true", help="Run unit tests for tidy") def test_tidy(self, all_files, no_progress, self_test): if self_test: return test_tidy.do_tests() else: return tidy.scan(not all_files, not no_progress) @Command('test-webidl', description='Run the WebIDL parser tests', category='testing') @CommandArgument('--quiet', '-q', default=False, action="store_true", help="Don't print passing tests.") @CommandArgument('tests', default=None, nargs="...", help="Specific tests to run, relative to the tests directory") def test_webidl(self, quiet, tests): self.ensure_bootstrapped() test_file_dir = path.abspath(path.join(PROJECT_TOPLEVEL_PATH, "components", "script", "dom", "bindings", "codegen", "parser")) # For the `import WebIDL` in runtests.py sys.path.insert(0, test_file_dir) run_file = path.abspath(path.join(test_file_dir, "runtests.py")) run_globals = {"__file__": run_file} execfile(run_file, run_globals) verbose = not quiet return run_globals["run_tests"](tests, verbose) @Command('test-wpt-failure', description='Run the web platform tests', category='testing') def test_wpt_failure(self): self.ensure_bootstrapped() return not call([ "bash", path.join("tests", "wpt", "run.sh"), "--no-pause-after-test", "--include", "infrastructure/failing-test.html" ], env=self.build_env()) @Command('test-wpt', description='Run the web platform tests', category='testing', parser=create_parser_wpt) def test_wpt(self, **kwargs): self.ensure_bootstrapped() return self.run_test_list_or_dispatch(kwargs["test_list"], "wpt", self._test_wpt, **kwargs) def _test_wpt(self, **kwargs): hosts_file_path = path.join(self.context.topdir, 'tests', 'wpt', 'hosts') os.environ["hosts_file_path"] = hosts_file_path run_file = path.abspath(path.join(self.context.topdir, "tests", "wpt", "run_wpt.py")) return self.wptrunner(run_file, **kwargs) # Helper to ensure all specified paths are handled, otherwise dispatch to appropriate test suite. def run_test_list_or_dispatch(self, requested_paths, correct_suite, correct_function, **kwargs): if not requested_paths: return correct_function(**kwargs) else: # Paths specified on command line. Ensure they can be handled, re-dispatch otherwise. all_handled = True for test_path in requested_paths: suite = self.suite_for_path(test_path) if suite is not None and correct_suite != suite: all_handled = False print("Warning: %s is not a %s test. Delegating to test-%s." % (test_path, correct_suite, suite)) if all_handled: return correct_function(**kwargs) else: # Dispatch each test to the correct suite via test() Registrar.dispatch("test", context=self.context, params=requested_paths) # Helper for test_css and test_wpt: def wptrunner(self, run_file, **kwargs): os.environ["RUST_BACKTRACE"] = "1" kwargs["debug"] = not kwargs["release"] if kwargs.pop("chaos"): kwargs["debugger"] = "rr" kwargs["debugger_args"] = "record --chaos" kwargs["repeat_until_unexpected"] = True # TODO: Delete rr traces from green test runs? prefs = kwargs.pop("prefs") if prefs: binary_args = [] for pref in prefs: binary_args.append("--pref=" + pref) kwargs["binary_args"] = binary_args run_globals = {"__file__": run_file} execfile(run_file, run_globals) return run_globals["run_tests"](**kwargs) @Command('update-manifest', description='run test-wpt --manifest-update SKIP_TESTS to regenerate MANIFEST.json', category='testing', parser=create_parser_wpt) def update_manifest(self, **kwargs): kwargs['test_list'].append(str('SKIP_TESTS')) kwargs['manifest_update'] = True return self.test_wpt(**kwargs) @Command('update-wpt', description='Update the web platform tests', category='testing', parser=updatecommandline.create_parser()) @CommandArgument('--patch', action='store_true', default=False, help='Create an mq patch or git commit containing the changes') def update_wpt(self, patch, **kwargs): self.ensure_bootstrapped() run_file = path.abspath(path.join("tests", "wpt", "update.py")) kwargs["no_patch"] = not patch if kwargs["no_patch"] and kwargs["sync"]: print("Are you sure you don't want a patch?") return 1 run_globals = {"__file__": run_file} execfile(run_file, run_globals) return run_globals["update_tests"](**kwargs) @Command('test-jquery', description='Run the jQuery test suite', category='testing') @CommandArgument('--release', '-r', action='store_true', help='Run the release build') @CommandArgument('--dev', '-d', action='store_true', help='Run the dev build') def test_jquery(self, release, dev): return self.jquery_test_runner("test", release, dev) @Command('test-dromaeo', description='Run the Dromaeo test suite', category='testing') @CommandArgument('tests', default=["recommended"], nargs="...", help="Specific tests to run") @CommandArgument('--release', '-r', action='store_true', help='Run the release build') @CommandArgument('--dev', '-d', action='store_true', help='Run the dev build') def test_dromaeo(self, tests, release, dev): return self.dromaeo_test_runner(tests, release, dev) @Command('update-jquery', description='Update the jQuery test suite expected results', category='testing') @CommandArgument('--release', '-r', action='store_true', help='Run the release build') @CommandArgument('--dev', '-d', action='store_true', help='Run the dev build') def update_jquery(self, release, dev): return self.jquery_test_runner("update", release, dev) @Command('test-css', description='Run the web platform tests', category='testing', parser=create_parser_wpt) def test_css(self, **kwargs): self.ensure_bootstrapped() return self.run_test_list_or_dispatch(kwargs["test_list"], "css", self._test_css, **kwargs) def _test_css(self, **kwargs): run_file = path.abspath(path.join("tests", "wpt", "run_css.py")) return self.wptrunner(run_file, **kwargs) @Command('update-css', description='Update the web platform tests', category='testing', parser=updatecommandline.create_parser()) @CommandArgument('--patch', action='store_true', default=False, help='Create an mq patch or git commit containing the changes') def update_css(self, patch, **kwargs): self.ensure_bootstrapped() run_file = path.abspath(path.join("tests", "wpt", "update_css.py")) kwargs["no_patch"] = not patch if kwargs["no_patch"] and kwargs["sync"]: print("Are you sure you don't want a patch?") return 1 run_globals = {"__file__": run_file} execfile(run_file, run_globals) return run_globals["update_tests"](**kwargs) @Command('compare_dromaeo', description='compare outputs of two runs of ./mach test-dromaeo command', category='testing') @CommandArgument('params', default=None, nargs="...", help=" filepaths of output files of two runs of dromaeo test ") def compare_dromaeo(self, params): prev_op_filename = params[0] cur_op_filename = params[1] result = {'Test': [], 'Prev_Time': [], 'Cur_Time': [], 'Difference(%)': []} with open(prev_op_filename, 'r') as prev_op, open(cur_op_filename, 'r') as cur_op: l1 = prev_op.readline() l2 = cur_op.readline() while ((l1.find('[dromaeo] Saving...') and l2.find('[dromaeo] Saving...'))): l1 = prev_op.readline() l2 = cur_op.readline() reach = 3 while (reach > 0): l1 = prev_op.readline() l2 = cur_op.readline() reach -= 1 while True: l1 = prev_op.readline() l2 = cur_op.readline() if not l1: break result['Test'].append(str(l1).split('|')[0].strip()) result['Prev_Time'].append(float(str(l1).split('|')[1].strip())) result['Cur_Time'].append(float(str(l2).split('|')[1].strip())) a = float(str(l1).split('|')[1].strip()) b = float(str(l2).split('|')[1].strip()) result['Difference(%)'].append(((b - a) / a) * 100) width_col1 = max([len(x) for x in result['Test']]) width_col2 = max([len(str(x)) for x in result['Prev_Time']]) width_col3 = max([len(str(x)) for x in result['Cur_Time']]) width_col4 = max([len(str(x)) for x in result['Difference(%)']]) for p, q, r, s in zip(['Test'], ['First Run'], ['Second Run'], ['Difference(%)']): print ("\033[1m" + "{}|{}|{}|{}".format(p.ljust(width_col1), q.ljust(width_col2), r.ljust(width_col3), s.ljust(width_col4)) + "\033[0m" + "\n" + "--------------------------------------------------" + "-------------------------------------------------------------------------") for a1, b1, c1, d1 in zip(result['Test'], result['Prev_Time'], result['Cur_Time'], result['Difference(%)']): if d1 > 0: print ("\033[91m" + "{}|{}|{}|{}".format(a1.ljust(width_col1), str(b1).ljust(width_col2), str(c1).ljust(width_col3), str(d1).ljust(width_col4)) + "\033[0m") elif d1 < 0: print ("\033[92m" + "{}|{}|{}|{}".format(a1.ljust(width_col1), str(b1).ljust(width_col2), str(c1).ljust(width_col3), str(d1).ljust(width_col4)) + "\033[0m") else: print ("{}|{}|{}|{}".format(a1.ljust(width_col1), str(b1).ljust(width_col2), str(c1).ljust(width_col3), str(d1).ljust(width_col4))) def jquery_test_runner(self, cmd, release, dev): self.ensure_bootstrapped() base_dir = path.abspath(path.join("tests", "jquery")) jquery_dir = path.join(base_dir, "jquery") run_file = path.join(base_dir, "run_jquery.py") # Clone the jQuery repository if it doesn't exist if not os.path.isdir(jquery_dir): check_call( ["git", "clone", "-b", "servo", "--depth", "1", "https://github.com/servo/jquery", jquery_dir]) # Run pull in case the jQuery repo was updated since last test run check_call( ["git", "-C", jquery_dir, "pull"]) # Check that a release servo build exists bin_path = path.abspath(self.get_binary_path(release, dev)) return call([run_file, cmd, bin_path, base_dir]) def dromaeo_test_runner(self, tests, release, dev): self.ensure_bootstrapped() base_dir = path.abspath(path.join("tests", "dromaeo")) dromaeo_dir = path.join(base_dir, "dromaeo") run_file = path.join(base_dir, "run_dromaeo.py") # Clone the Dromaeo repository if it doesn't exist if not os.path.isdir(dromaeo_dir): check_call( ["git", "clone", "-b", "servo", "--depth", "1", "https://github.com/notriddle/dromaeo", dromaeo_dir]) # Run pull in case the Dromaeo repo was updated since last test run check_call( ["git", "-C", dromaeo_dir, "pull"]) # Compile test suite check_call( ["make", "-C", dromaeo_dir, "web"]) # Check that a release servo build exists bin_path = path.abspath(self.get_binary_path(release, dev)) return check_call( [run_file, "|".join(tests), bin_path, base_dir]) def create_parser_create(): import argparse p = argparse.ArgumentParser() p.add_argument("--no-editor", action="store_true", help="Don't try to open the test in an editor") p.add_argument("-e", "--editor", action="store", help="Editor to use") p.add_argument("--no-run", action="store_true", help="Don't try to update the wpt manifest or open the test in a browser") p.add_argument('--release', action="store_true", help="Run with a release build of servo") p.add_argument("--long-timeout", action="store_true", help="Test should be given a long timeout (typically 60s rather than 10s," "but varies depending on environment)") p.add_argument("--overwrite", action="store_true", help="Allow overwriting an existing test file") p.add_argument("-r", "--reftest", action="store_true", help="Create a reftest rather than a testharness (js) test"), p.add_argument("-ref", "--reference", dest="ref", help="Path to the reference file") p.add_argument("--mismatch", action="store_true", help="Create a mismatch reftest") p.add_argument("--wait", action="store_true", help="Create a reftest that waits until takeScreenshot() is called") p.add_argument("path", action="store", help="Path to the test file") return p @CommandProvider class WebPlatformTestsCreator(CommandBase): template_prefix = """ %(documentElement)s """ template_long_timeout = "\n" template_body_th = """ """ template_body_reftest = """ """ template_body_reftest_wait = """ """ def make_test_file_url(self, absolute_file_path): # Make the path relative to the project top-level directory so that # we can more easily find the right test directory. file_path = os.path.relpath(absolute_file_path, PROJECT_TOPLEVEL_PATH) if file_path.startswith(WEB_PLATFORM_TESTS_PATH): url = file_path[len(WEB_PLATFORM_TESTS_PATH):] elif file_path.startswith(SERVO_TESTS_PATH): url = "/mozilla" + file_path[len(SERVO_TESTS_PATH):] else: # This test file isn't in any known test directory. return None return url.replace(os.path.sep, "/") def make_test_and_reference_urls(self, test_path, reference_path): test_path = os.path.normpath(os.path.abspath(test_path)) test_url = self.make_test_file_url(test_path) if test_url is None: return (None, None) if reference_path is None: return (test_url, '') reference_path = os.path.normpath(os.path.abspath(reference_path)) # If the reference is in the same directory, the URL can just be the # name of the refernce file itself. reference_path_parts = os.path.split(reference_path) if reference_path_parts[0] == os.path.split(test_path)[0]: return (test_url, reference_path_parts[1]) return (test_url, self.make_test_file_url(reference_path)) @Command("create-wpt", category="testing", parser=create_parser_create) def run_create(self, **kwargs): import subprocess test_path = kwargs["path"] reference_path = kwargs["ref"] if reference_path: kwargs["reftest"] = True (test_url, reference_url) = self.make_test_and_reference_urls( test_path, reference_path) if test_url is None: print("""Test path %s is not in wpt directories: tests/wpt/web-platform-tests for tests that may be shared tests/wpt/mozilla/tests for Servo-only tests""" % test_path) return 1 if reference_url is None: print("""Reference path %s is not in wpt directories: testing/web-platform/tests for tests that may be shared testing/web-platform/mozilla/tests for Servo-only tests""" % reference_path) return 1 if os.path.exists(test_path) and not kwargs["overwrite"]: print("Test path already exists, pass --overwrite to replace") return 1 if kwargs["mismatch"] and not kwargs["reftest"]: print("--mismatch only makes sense for a reftest") return 1 if kwargs["wait"] and not kwargs["reftest"]: print("--wait only makes sense for a reftest") return 1 args = {"documentElement": "\n" if kwargs["wait"] else ""} template = self.template_prefix % args if kwargs["long_timeout"]: template += self.template_long_timeout if kwargs["reftest"]: args = {"match": "match" if not kwargs["mismatch"] else "mismatch", "ref": reference_url} template += self.template_body_reftest % args if kwargs["wait"]: template += self.template_body_reftest_wait else: template += self.template_body_th with open(test_path, "w") as f: f.write(template) if kwargs["no_editor"]: editor = None elif kwargs["editor"]: editor = kwargs["editor"] elif "VISUAL" in os.environ: editor = os.environ["VISUAL"] elif "EDITOR" in os.environ: editor = os.environ["EDITOR"] else: editor = None if editor: proc = subprocess.Popen("%s %s" % (editor, test_path), shell=True) if not kwargs["no_run"]: p = create_parser_wpt() args = ["--manifest-update"] if kwargs["release"]: args.append("--release") args.append(test_path) wpt_kwargs = vars(p.parse_args(args)) self.context.commands.dispatch("test-wpt", self.context, **wpt_kwargs) if editor: proc.wait() @Command('update-net-cookies', description='Update the net unit tests with cookie tests from http-state', category='testing') def update_net_cookies(self): cache_dir = path.join(self.config["tools"]["cache-dir"], "tests") run_file = path.abspath(path.join(PROJECT_TOPLEVEL_PATH, "tests", "unit", "net", "cookie_http_state_utils.py")) run_globals = {"__file__": run_file} execfile(run_file, run_globals) return run_globals["update_test_file"](cache_dir)