summaryrefslogtreecommitdiffstats
path: root/testing/mach_commands.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mach_commands.py')
-rw-r--r--testing/mach_commands.py1222
1 files changed, 1222 insertions, 0 deletions
diff --git a/testing/mach_commands.py b/testing/mach_commands.py
new file mode 100644
index 0000000000..5a8b3d794d
--- /dev/null
+++ b/testing/mach_commands.py
@@ -0,0 +1,1222 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import logging
+import os
+import subprocess
+import sys
+
+import requests
+from mach.decorators import Command, CommandArgument, SettingsProvider, SubCommand
+from mozbuild.base import BuildEnvironmentNotFoundException
+from mozbuild.base import MachCommandConditions as conditions
+
+UNKNOWN_TEST = """
+I was unable to find tests from the given argument(s).
+
+You should specify a test directory, filename, test suite name, or
+abbreviation.
+
+It's possible my little brain doesn't know about the type of test you are
+trying to execute. If you suspect this, please request support by filing
+a bug at
+https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=General.
+""".strip()
+
+UNKNOWN_FLAVOR = """
+I know you are trying to run a %s%s test. Unfortunately, I can't run those
+tests yet. Sorry!
+""".strip()
+
+TEST_HELP = """
+Test or tests to run. Tests can be specified by filename, directory, suite
+name or suite alias.
+
+The following test suites and aliases are supported: {}
+""".strip()
+
+
+@SettingsProvider
+class TestConfig(object):
+ @classmethod
+ def config_settings(cls):
+ from mozlog.commandline import log_formatters
+ from mozlog.structuredlog import log_levels
+
+ format_desc = "The default format to use when running tests with `mach test`."
+ format_choices = list(log_formatters)
+ level_desc = "The default log level to use when running tests with `mach test`."
+ level_choices = [l.lower() for l in log_levels]
+ return [
+ ("test.format", "string", format_desc, "mach", {"choices": format_choices}),
+ ("test.level", "string", level_desc, "info", {"choices": level_choices}),
+ ]
+
+
+def get_test_parser():
+ from mozlog.commandline import add_logging_group
+ from moztest.resolve import TEST_SUITES
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "what",
+ default=None,
+ nargs="+",
+ help=TEST_HELP.format(", ".join(sorted(TEST_SUITES))),
+ )
+ parser.add_argument(
+ "extra_args",
+ default=None,
+ nargs=argparse.REMAINDER,
+ help="Extra arguments to pass to the underlying test command(s). "
+ "If an underlying command doesn't recognize the argument, it "
+ "will fail.",
+ )
+ parser.add_argument(
+ "--debugger",
+ default=None,
+ action="store",
+ nargs="?",
+ help="Specify a debugger to use.",
+ )
+ add_logging_group(parser)
+ return parser
+
+
+ADD_TEST_SUPPORTED_SUITES = [
+ "mochitest-chrome",
+ "mochitest-plain",
+ "mochitest-browser-chrome",
+ "web-platform-tests-testharness",
+ "web-platform-tests-reftest",
+ "xpcshell",
+]
+ADD_TEST_SUPPORTED_DOCS = ["js", "html", "xhtml", "xul"]
+
+SUITE_SYNONYMS = {
+ "wpt": "web-platform-tests-testharness",
+ "wpt-testharness": "web-platform-tests-testharness",
+ "wpt-reftest": "web-platform-tests-reftest",
+}
+
+MISSING_ARG = object()
+
+
+def create_parser_addtest():
+ import addtest
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--suite",
+ choices=sorted(ADD_TEST_SUPPORTED_SUITES + list(SUITE_SYNONYMS.keys())),
+ help="suite for the test. "
+ "If you pass a `test` argument this will be determined "
+ "based on the filename and the folder it is in",
+ )
+ parser.add_argument(
+ "-o",
+ "--overwrite",
+ action="store_true",
+ help="Overwrite an existing file if it exists.",
+ )
+ parser.add_argument(
+ "--doc",
+ choices=ADD_TEST_SUPPORTED_DOCS,
+ help="Document type for the test (if applicable)."
+ "If you pass a `test` argument this will be determined "
+ "based on the filename.",
+ )
+ parser.add_argument(
+ "-e",
+ "--editor",
+ action="store",
+ nargs="?",
+ default=MISSING_ARG,
+ help="Open the created file(s) in an editor; if a "
+ "binary is supplied it will be used otherwise the default editor for "
+ "your environment will be opened",
+ )
+
+ for base_suite in addtest.TEST_CREATORS:
+ cls = addtest.TEST_CREATORS[base_suite]
+ if hasattr(cls, "get_parser"):
+ group = parser.add_argument_group(base_suite)
+ cls.get_parser(group)
+
+ parser.add_argument("test", nargs="?", help=("Test to create."))
+ return parser
+
+
+@Command(
+ "addtest",
+ category="testing",
+ description="Generate tests based on templates",
+ parser=create_parser_addtest,
+)
+def addtest(
+ command_context,
+ suite=None,
+ test=None,
+ doc=None,
+ overwrite=False,
+ editor=MISSING_ARG,
+ **kwargs,
+):
+ import io
+
+ import addtest
+ from moztest.resolve import TEST_SUITES
+
+ if not suite and not test:
+ return create_parser_addtest().parse_args(["--help"])
+
+ if suite in SUITE_SYNONYMS:
+ suite = SUITE_SYNONYMS[suite]
+
+ if test:
+ if not overwrite and os.path.isfile(os.path.abspath(test)):
+ print("Error: can't generate a test that already exists:", test)
+ return 1
+
+ abs_test = os.path.abspath(test)
+ if doc is None:
+ doc = guess_doc(abs_test)
+ if suite is None:
+ guessed_suite, err = guess_suite(abs_test)
+ if err:
+ print(err)
+ return 1
+ suite = guessed_suite
+
+ else:
+ test = None
+ if doc is None:
+ doc = "html"
+
+ if not suite:
+ print(
+ "We couldn't automatically determine a suite. "
+ "Please specify `--suite` with one of the following options:\n{}\n"
+ "If you'd like to add support to a new suite, please file a bug "
+ "blocking https://bugzilla.mozilla.org/show_bug.cgi?id=1540285.".format(
+ ADD_TEST_SUPPORTED_SUITES
+ )
+ )
+ return 1
+
+ if doc not in ADD_TEST_SUPPORTED_DOCS:
+ print(
+ "Error: invalid `doc`. Either pass in a test with a valid extension"
+ "({}) or pass in the `doc` argument".format(ADD_TEST_SUPPORTED_DOCS)
+ )
+ return 1
+
+ creator_cls = addtest.creator_for_suite(suite)
+
+ if creator_cls is None:
+ print("Sorry, `addtest` doesn't currently know how to add {}".format(suite))
+ return 1
+
+ creator = creator_cls(command_context.topsrcdir, test, suite, doc, **kwargs)
+
+ creator.check_args()
+
+ paths = []
+ added_tests = False
+ for path, template in creator:
+ if not template:
+ continue
+ added_tests = True
+ if path:
+ paths.append(path)
+ print("Adding a test file at {} (suite `{}`)".format(path, suite))
+
+ try:
+ os.makedirs(os.path.dirname(path))
+ except OSError:
+ pass
+
+ with io.open(path, "w", newline="\n") as f:
+ f.write(template)
+ else:
+ # write to stdout if you passed only suite and doc and not a file path
+ print(template)
+
+ if not added_tests:
+ return 1
+
+ if test:
+ creator.update_manifest()
+
+ # Small hack, should really do this better
+ if suite.startswith("wpt-"):
+ suite = "web-platform-tests"
+
+ mach_command = TEST_SUITES[suite]["mach_command"]
+ print(
+ "Please make sure to add the new test to your commit. "
+ "You can now run the test with:\n ./mach {} {}".format(
+ mach_command, test
+ )
+ )
+
+ if editor is not MISSING_ARG:
+ if editor is not None:
+ editor = editor
+ elif "VISUAL" in os.environ:
+ editor = os.environ["VISUAL"]
+ elif "EDITOR" in os.environ:
+ editor = os.environ["EDITOR"]
+ else:
+ print("Unable to determine editor; please specify a binary")
+ editor = None
+
+ proc = None
+ if editor:
+ import subprocess
+
+ proc = subprocess.Popen("%s %s" % (editor, " ".join(paths)), shell=True)
+
+ if proc:
+ proc.wait()
+
+ return 0
+
+
+def guess_doc(abs_test):
+ filename = os.path.basename(abs_test)
+ return os.path.splitext(filename)[1].strip(".")
+
+
+def guess_suite(abs_test):
+ # If you pass a abs_test, try to detect the type based on the name
+ # and folder. This detection can be skipped if you pass the `type` arg.
+ err = None
+ guessed_suite = None
+ parent = os.path.dirname(abs_test)
+ filename = os.path.basename(abs_test)
+
+ has_browser_ini = os.path.isfile(os.path.join(parent, "browser.ini"))
+ has_chrome_ini = os.path.isfile(os.path.join(parent, "chrome.ini"))
+ has_plain_ini = os.path.isfile(os.path.join(parent, "mochitest.ini"))
+ has_xpcshell_ini = os.path.isfile(os.path.join(parent, "xpcshell.ini"))
+
+ in_wpt_folder = abs_test.startswith(
+ os.path.abspath(os.path.join("testing", "web-platform"))
+ )
+
+ if in_wpt_folder:
+ guessed_suite = "web-platform-tests-testharness"
+ if "/css/" in abs_test:
+ guessed_suite = "web-platform-tests-reftest"
+ elif (
+ filename.startswith("test_")
+ and has_xpcshell_ini
+ and guess_doc(abs_test) == "js"
+ ):
+ guessed_suite = "xpcshell"
+ else:
+ if filename.startswith("browser_") and has_browser_ini:
+ guessed_suite = "mochitest-browser-chrome"
+ elif filename.startswith("test_"):
+ if has_chrome_ini and has_plain_ini:
+ err = (
+ "Error: directory contains both a chrome.ini and mochitest.ini. "
+ "Please set --suite=mochitest-chrome or --suite=mochitest-plain."
+ )
+ elif has_chrome_ini:
+ guessed_suite = "mochitest-chrome"
+ elif has_plain_ini:
+ guessed_suite = "mochitest-plain"
+ return guessed_suite, err
+
+
+@Command(
+ "test",
+ category="testing",
+ description="Run tests (detects the kind of test and runs it).",
+ parser=get_test_parser,
+)
+def test(command_context, what, extra_args, **log_args):
+ """Run tests from names or paths.
+
+ mach test accepts arguments specifying which tests to run. Each argument
+ can be:
+
+ * The path to a test file
+ * A directory containing tests
+ * A test suite name
+ * An alias to a test suite name (codes used on TreeHerder)
+
+ When paths or directories are given, they are first resolved to test
+ files known to the build system.
+
+ If resolved tests belong to more than one test type/flavor/harness,
+ the harness for each relevant type/flavor will be invoked. e.g. if
+ you specify a directory with xpcshell and browser chrome mochitests,
+ both harnesses will be invoked.
+
+ Warning: `mach test` does not automatically re-build.
+ Please remember to run `mach build` when necessary.
+
+ EXAMPLES
+
+ Run all test files in the devtools/client/shared/redux/middleware/xpcshell/
+ directory:
+
+ `./mach test devtools/client/shared/redux/middleware/xpcshell/`
+
+ The below command prints a short summary of results instead of
+ the default more verbose output.
+ Do not forget the - (minus sign) after --log-grouped!
+
+ `./mach test --log-grouped - devtools/client/shared/redux/middleware/xpcshell/`
+
+ To learn more about arguments for each test type/flavor/harness, please run
+ `./mach <test-harness> --help`. For example, `./mach mochitest --help`.
+ """
+ from mozlog.commandline import setup_logging
+ from mozlog.handlers import StreamHandler
+ from moztest.resolve import TEST_SUITES, TestResolver, get_suite_definition
+
+ resolver = command_context._spawn(TestResolver)
+ run_suites, run_tests = resolver.resolve_metadata(what)
+
+ if not run_suites and not run_tests:
+ print(UNKNOWN_TEST)
+ return 1
+
+ if log_args.get("debugger", None):
+ import mozdebug
+
+ if not mozdebug.get_debugger_info(log_args.get("debugger")):
+ sys.exit(1)
+ extra_args_debugger_notation = "=".join(
+ ["--debugger", log_args.get("debugger")]
+ )
+ if extra_args:
+ extra_args.append(extra_args_debugger_notation)
+ else:
+ extra_args = [extra_args_debugger_notation]
+
+ # Create shared logger
+ format_args = {"level": command_context._mach_context.settings["test"]["level"]}
+ if not run_suites and len(run_tests) == 1:
+ format_args["verbose"] = True
+ format_args["compact"] = False
+
+ default_format = command_context._mach_context.settings["test"]["format"]
+ log = setup_logging(
+ "mach-test", log_args, {default_format: sys.stdout}, format_args
+ )
+ for handler in log.handlers:
+ if isinstance(handler, StreamHandler):
+ handler.formatter.inner.summary_on_shutdown = True
+
+ status = None
+ for suite_name in run_suites:
+ suite = TEST_SUITES[suite_name]
+ kwargs = suite["kwargs"]
+ kwargs["log"] = log
+ kwargs.setdefault("subsuite", None)
+
+ if "mach_command" in suite:
+ res = command_context._mach_context.commands.dispatch(
+ suite["mach_command"],
+ command_context._mach_context,
+ argv=extra_args,
+ **kwargs,
+ )
+ if res:
+ status = res
+
+ buckets = {}
+ for test in run_tests:
+ key = (test["flavor"], test.get("subsuite", ""))
+ buckets.setdefault(key, []).append(test)
+
+ for (flavor, subsuite), tests in sorted(buckets.items()):
+ _, m = get_suite_definition(flavor, subsuite)
+ if "mach_command" not in m:
+ substr = "-{}".format(subsuite) if subsuite else ""
+ print(UNKNOWN_FLAVOR % (flavor, substr))
+ status = 1
+ continue
+
+ kwargs = dict(m["kwargs"])
+ kwargs["log"] = log
+ kwargs.setdefault("subsuite", None)
+
+ res = command_context._mach_context.commands.dispatch(
+ m["mach_command"],
+ command_context._mach_context,
+ argv=extra_args,
+ test_objects=tests,
+ **kwargs,
+ )
+ if res:
+ status = res
+
+ log.shutdown()
+ return status
+
+
+@Command(
+ "cppunittest", category="testing", description="Run cpp unit tests (C++ tests)."
+)
+@CommandArgument(
+ "test_files",
+ nargs="*",
+ metavar="N",
+ help="Test to run. Can be specified as one or more files or "
+ "directories, or omitted. If omitted, the entire test suite is "
+ "executed.",
+)
+def run_cppunit_test(command_context, **params):
+ from mozlog import commandline
+
+ log = params.get("log")
+ if not log:
+ log = commandline.setup_logging("cppunittest", {}, {"tbpl": sys.stdout})
+
+ # See if we have crash symbols
+ symbols_path = os.path.join(command_context.distdir, "crashreporter-symbols")
+ if not os.path.isdir(symbols_path):
+ symbols_path = None
+
+ # If no tests specified, run all tests in main manifest
+ tests = params["test_files"]
+ if not tests:
+ tests = [os.path.join(command_context.distdir, "cppunittests")]
+ manifest_path = os.path.join(
+ command_context.topsrcdir, "testing", "cppunittest.ini"
+ )
+ else:
+ manifest_path = None
+
+ utility_path = command_context.bindir
+
+ if conditions.is_android(command_context):
+ from mozrunner.devices.android_device import (
+ InstallIntent,
+ verify_android_device,
+ )
+
+ verify_android_device(command_context, install=InstallIntent.NO)
+ return run_android_test(tests, symbols_path, manifest_path, log)
+
+ return run_desktop_test(
+ command_context, tests, symbols_path, manifest_path, utility_path, log
+ )
+
+
+def run_desktop_test(
+ command_context, tests, symbols_path, manifest_path, utility_path, log
+):
+ import runcppunittests as cppunittests
+ from mozlog import commandline
+
+ parser = cppunittests.CPPUnittestOptions()
+ commandline.add_logging_group(parser)
+ options, args = parser.parse_args()
+
+ options.symbols_path = symbols_path
+ options.manifest_path = manifest_path
+ options.utility_path = utility_path
+ options.xre_path = command_context.bindir
+
+ try:
+ result = cppunittests.run_test_harness(options, tests)
+ except Exception as e:
+ log.error("Caught exception running cpp unit tests: %s" % str(e))
+ result = False
+ raise
+
+ return 0 if result else 1
+
+
+def run_android_test(command_context, tests, symbols_path, manifest_path, log):
+ import remotecppunittests
+ from mozlog import commandline
+
+ parser = remotecppunittests.RemoteCPPUnittestOptions()
+ commandline.add_logging_group(parser)
+ options, args = parser.parse_args()
+
+ if not options.adb_path:
+ from mozrunner.devices.android_device import get_adb_path
+
+ options.adb_path = get_adb_path(command_context)
+ options.symbols_path = symbols_path
+ options.manifest_path = manifest_path
+ options.xre_path = command_context.bindir
+ options.local_lib = command_context.bindir.replace("bin", "fennec")
+ for file in os.listdir(os.path.join(command_context.topobjdir, "dist")):
+ if file.endswith(".apk") and file.startswith("fennec"):
+ options.local_apk = os.path.join(command_context.topobjdir, "dist", file)
+ log.info("using APK: " + options.local_apk)
+ break
+
+ try:
+ result = remotecppunittests.run_test_harness(options, tests)
+ except Exception as e:
+ log.error("Caught exception running cpp unit tests: %s" % str(e))
+ result = False
+ raise
+
+ return 0 if result else 1
+
+
+def executable_name(name):
+ return name + ".exe" if sys.platform.startswith("win") else name
+
+
+@Command(
+ "jstests",
+ category="testing",
+ description="Run SpiderMonkey JS tests in the JS shell.",
+ ok_if_tests_disabled=True,
+)
+@CommandArgument("--shell", help="The shell to be used")
+@CommandArgument(
+ "params",
+ nargs=argparse.REMAINDER,
+ help="Extra arguments to pass down to the test harness.",
+)
+def run_jstests(command_context, shell, params):
+ import subprocess
+
+ command_context.virtualenv_manager.ensure()
+ python = command_context.virtualenv_manager.python_path
+
+ js = shell or os.path.join(command_context.bindir, executable_name("js"))
+ jstest_cmd = [
+ python,
+ os.path.join(command_context.topsrcdir, "js", "src", "tests", "jstests.py"),
+ js,
+ ] + params
+
+ return subprocess.call(jstest_cmd)
+
+
+@Command(
+ "jit-test",
+ category="testing",
+ description="Run SpiderMonkey jit-tests in the JS shell.",
+ ok_if_tests_disabled=True,
+)
+@CommandArgument("--shell", help="The shell to be used")
+@CommandArgument(
+ "--cgc",
+ action="store_true",
+ default=False,
+ help="Run with the SM(cgc) job's env vars",
+)
+@CommandArgument(
+ "params",
+ nargs=argparse.REMAINDER,
+ help="Extra arguments to pass down to the test harness.",
+)
+def run_jittests(command_context, shell, cgc, params):
+ import subprocess
+
+ command_context.virtualenv_manager.ensure()
+ python = command_context.virtualenv_manager.python_path
+
+ js = shell or os.path.join(command_context.bindir, executable_name("js"))
+ jittest_cmd = [
+ python,
+ os.path.join(command_context.topsrcdir, "js", "src", "jit-test", "jit_test.py"),
+ js,
+ ] + params
+
+ env = os.environ.copy()
+ if cgc:
+ env["JS_GC_ZEAL"] = "IncrementalMultipleSlices"
+
+ return subprocess.call(jittest_cmd, env=env)
+
+
+@Command("jsapi-tests", category="testing", description="Run SpiderMonkey JSAPI tests.")
+@CommandArgument(
+ "--list",
+ action="store_true",
+ default=False,
+ help="List all tests",
+)
+@CommandArgument(
+ "--frontend-only",
+ action="store_true",
+ default=False,
+ help="Run tests for frontend-only APIs, with light-weight entry point",
+)
+@CommandArgument(
+ "test_name",
+ nargs="?",
+ metavar="N",
+ help="Test to run. Can be a prefix or omitted. If "
+ "omitted, the entire test suite is executed.",
+)
+def run_jsapitests(command_context, list=False, frontend_only=False, test_name=None):
+ import subprocess
+
+ jsapi_tests_cmd = [
+ os.path.join(command_context.bindir, executable_name("jsapi-tests"))
+ ]
+ if list:
+ jsapi_tests_cmd.append("--list")
+ if frontend_only:
+ jsapi_tests_cmd.append("--frontend-only")
+ if test_name:
+ jsapi_tests_cmd.append(test_name)
+
+ test_env = os.environ.copy()
+ test_env["TOPSRCDIR"] = command_context.topsrcdir
+
+ result = subprocess.call(jsapi_tests_cmd, env=test_env)
+ if result != 0:
+ print(f"jsapi-tests failed, exit code {result}")
+ return result
+
+
+def run_check_js_msg(command_context):
+ import subprocess
+
+ command_context.virtualenv_manager.ensure()
+ python = command_context.virtualenv_manager.python_path
+
+ check_cmd = [
+ python,
+ os.path.join(command_context.topsrcdir, "config", "check_js_msg_encoding.py"),
+ ]
+
+ return subprocess.call(check_cmd)
+
+
+def get_jsshell_parser():
+ from jsshell.benchmark import get_parser
+
+ return get_parser()
+
+
+@Command(
+ "jsshell-bench",
+ category="testing",
+ parser=get_jsshell_parser,
+ description="Run benchmarks in the SpiderMonkey JS shell.",
+)
+def run_jsshelltests(command_context, **kwargs):
+ from jsshell import benchmark
+
+ return benchmark.run(**kwargs)
+
+
+@Command(
+ "cramtest",
+ category="testing",
+ description="Mercurial style .t tests for command line applications.",
+)
+@CommandArgument(
+ "test_paths",
+ nargs="*",
+ metavar="N",
+ help="Test paths to run. Each path can be a test file or directory. "
+ "If omitted, the entire suite will be run.",
+)
+@CommandArgument(
+ "cram_args",
+ nargs=argparse.REMAINDER,
+ help="Extra arguments to pass down to the cram binary. See "
+ "'./mach python -m cram -- -h' for a list of available options.",
+)
+def cramtest(command_context, cram_args=None, test_paths=None, test_objects=None):
+ command_context.activate_virtualenv()
+ import mozinfo
+ from manifestparser import TestManifest
+
+ if test_objects is None:
+ from moztest.resolve import TestResolver
+
+ resolver = command_context._spawn(TestResolver)
+ if test_paths:
+ # If we were given test paths, try to find tests matching them.
+ test_objects = resolver.resolve_tests(paths=test_paths, flavor="cram")
+ else:
+ # Otherwise just run everything in CRAMTEST_MANIFESTS
+ test_objects = resolver.resolve_tests(flavor="cram")
+
+ if not test_objects:
+ message = "No tests were collected, check spelling of the test paths."
+ command_context.log(logging.WARN, "cramtest", {}, message)
+ return 1
+
+ mp = TestManifest()
+ mp.tests.extend(test_objects)
+ tests = mp.active_tests(disabled=False, **mozinfo.info)
+
+ python = command_context.virtualenv_manager.python_path
+ cmd = [python, "-m", "cram"] + cram_args + [t["relpath"] for t in tests]
+ return subprocess.call(cmd, cwd=command_context.topsrcdir)
+
+
+from datetime import date, timedelta
+
+
+@Command(
+ "test-info", category="testing", description="Display historical test results."
+)
+def test_info(command_context):
+ """
+ All functions implemented as subcommands.
+ """
+
+
+@SubCommand(
+ "test-info",
+ "tests",
+ description="Display historical test result summary for named tests.",
+)
+@CommandArgument("test_names", nargs=argparse.REMAINDER, help="Test(s) of interest.")
+@CommandArgument(
+ "--start",
+ default=(date.today() - timedelta(7)).strftime("%Y-%m-%d"),
+ help="Start date (YYYY-MM-DD)",
+)
+@CommandArgument(
+ "--end", default=date.today().strftime("%Y-%m-%d"), help="End date (YYYY-MM-DD)"
+)
+@CommandArgument(
+ "--show-info",
+ action="store_true",
+ help="Retrieve and display general test information.",
+)
+@CommandArgument(
+ "--show-bugs",
+ action="store_true",
+ help="Retrieve and display related Bugzilla bugs.",
+)
+@CommandArgument("--verbose", action="store_true", help="Enable debug logging.")
+def test_info_tests(
+ command_context,
+ test_names,
+ start,
+ end,
+ show_info,
+ show_bugs,
+ verbose,
+):
+ import testinfo
+
+ ti = testinfo.TestInfoTests(verbose)
+ ti.report(
+ test_names,
+ start,
+ end,
+ show_info,
+ show_bugs,
+ )
+
+
+@SubCommand(
+ "test-info",
+ "report",
+ description="Generate a json report of test manifests and/or tests "
+ "categorized by Bugzilla component and optionally filtered "
+ "by path, component, and/or manifest annotations.",
+)
+@CommandArgument(
+ "--components",
+ default=None,
+ help="Comma-separated list of Bugzilla components."
+ " eg. Testing::General,Core::WebVR",
+)
+@CommandArgument(
+ "--flavor",
+ help='Limit results to tests of the specified flavor (eg. "xpcshell").',
+)
+@CommandArgument(
+ "--subsuite",
+ help='Limit results to tests of the specified subsuite (eg. "devtools").',
+)
+@CommandArgument(
+ "paths", nargs=argparse.REMAINDER, help="File system paths of interest."
+)
+@CommandArgument(
+ "--show-manifests",
+ action="store_true",
+ help="Include test manifests in report.",
+)
+@CommandArgument(
+ "--show-tests", action="store_true", help="Include individual tests in report."
+)
+@CommandArgument(
+ "--show-summary", action="store_true", help="Include summary in report."
+)
+@CommandArgument(
+ "--show-annotations",
+ action="store_true",
+ help="Include list of manifest annotation conditions in report.",
+)
+@CommandArgument(
+ "--show-testruns",
+ action="store_true",
+ help="Include total number of runs the test has if there are failures.",
+)
+@CommandArgument(
+ "--filter-values",
+ help="Comma-separated list of value regular expressions to filter on; "
+ "displayed tests contain all specified values.",
+)
+@CommandArgument(
+ "--filter-keys",
+ help="Comma-separated list of test keys to filter on, "
+ 'like "skip-if"; only these fields will be searched '
+ "for filter-values.",
+)
+@CommandArgument(
+ "--no-component-report",
+ action="store_false",
+ dest="show_components",
+ default=True,
+ help="Do not categorize by bugzilla component.",
+)
+@CommandArgument("--output-file", help="Path to report file.")
+@CommandArgument("--verbose", action="store_true", help="Enable debug logging.")
+@CommandArgument(
+ "--start",
+ default=(date.today() - timedelta(30)).strftime("%Y-%m-%d"),
+ help="Start date (YYYY-MM-DD)",
+)
+@CommandArgument(
+ "--end", default=date.today().strftime("%Y-%m-%d"), help="End date (YYYY-MM-DD)"
+)
+def test_report(
+ command_context,
+ components,
+ flavor,
+ subsuite,
+ paths,
+ show_manifests,
+ show_tests,
+ show_summary,
+ show_annotations,
+ filter_values,
+ filter_keys,
+ show_components,
+ output_file,
+ verbose,
+ start,
+ end,
+ show_testruns,
+):
+ import testinfo
+ from mozbuild import build_commands
+
+ try:
+ command_context.config_environment
+ except BuildEnvironmentNotFoundException:
+ print("Looks like configure has not run yet, running it now...")
+ build_commands.configure(command_context)
+
+ ti = testinfo.TestInfoReport(verbose)
+ ti.report(
+ components,
+ flavor,
+ subsuite,
+ paths,
+ show_manifests,
+ show_tests,
+ show_summary,
+ show_annotations,
+ filter_values,
+ filter_keys,
+ show_components,
+ output_file,
+ start,
+ end,
+ show_testruns,
+ )
+
+
+@SubCommand(
+ "test-info",
+ "report-diff",
+ description='Compare two reports generated by "test-info reports".',
+)
+@CommandArgument(
+ "--before",
+ default=None,
+ help="The first (earlier) report file; path to local file or url.",
+)
+@CommandArgument(
+ "--after", help="The second (later) report file; path to local file or url."
+)
+@CommandArgument(
+ "--output-file",
+ help="Path to report file to be written. If not specified, report"
+ "will be written to standard output.",
+)
+@CommandArgument("--verbose", action="store_true", help="Enable debug logging.")
+def test_report_diff(command_context, before, after, output_file, verbose):
+ import testinfo
+
+ ti = testinfo.TestInfoReport(verbose)
+ ti.report_diff(before, after, output_file)
+
+
+@SubCommand(
+ "test-info",
+ "testrun-report",
+ description="Generate report of number of runs for each test group (manifest)",
+)
+@CommandArgument("--output-file", help="Path to report file.")
+def test_info_testrun_report(command_context, output_file):
+ import json
+
+ import testinfo
+
+ ti = testinfo.TestInfoReport(verbose=True)
+ if os.environ.get("GECKO_HEAD_REPOSITORY", "") in [
+ "https://hg.mozilla.org/mozilla-central",
+ "https://hg.mozilla.org/try",
+ ]:
+ runcounts = ti.get_runcounts()
+ if output_file:
+ output_file = os.path.abspath(output_file)
+ output_dir = os.path.dirname(output_file)
+ if not os.path.isdir(output_dir):
+ os.makedirs(output_dir)
+ with open(output_file, "w") as f:
+ json.dump(runcounts, f)
+ else:
+ print(runcounts)
+
+
+@SubCommand(
+ "test-info",
+ "failure-report",
+ description="Display failure line groupings and frequencies for "
+ "single tracking intermittent bugs.",
+)
+@CommandArgument(
+ "--start",
+ default=(date.today() - timedelta(30)).strftime("%Y-%m-%d"),
+ help="Start date (YYYY-MM-DD)",
+)
+@CommandArgument(
+ "--end", default=date.today().strftime("%Y-%m-%d"), help="End date (YYYY-MM-DD)"
+)
+@CommandArgument(
+ "--bugid",
+ default=None,
+ help="bugid for treeherder intermittent failures data query.",
+)
+def test_info_failures(
+ command_context,
+ start,
+ end,
+ bugid,
+):
+ # bugid comes in as a string, we need an int:
+ try:
+ bugid = int(bugid)
+ except ValueError:
+ bugid = None
+ if not bugid:
+ print("Please enter a valid bugid (i.e. '1760132')")
+ return
+
+ # get bug info
+ url = (
+ "https://bugzilla.mozilla.org/rest/bug?include_fields=summary,depends_on&id=%s"
+ % bugid
+ )
+ r = requests.get(url, headers={"User-agent": "mach-test-info/1.0"})
+ if r.status_code != 200:
+ print("%s error retrieving url: %s" % (r.status_code, url))
+
+ data = r.json()
+ if not data:
+ print("unable to get bugzilla information for %s" % bugid)
+ return
+
+ summary = data["bugs"][0]["summary"]
+ parts = summary.split("|")
+ if not summary.endswith("single tracking bug") or len(parts) != 2:
+ print("this query only works with single tracking bugs")
+ return
+
+ # get depends_on bugs:
+ buglist = [bugid]
+ if "depends_on" in data["bugs"][0]:
+ buglist.extend(data["bugs"][0]["depends_on"])
+
+ testname = parts[0].strip().split(" ")[-1]
+
+ # now query treeherder to get details about annotations
+ data = []
+ for b in buglist:
+ url = "https://treeherder.mozilla.org/api/failuresbybug/"
+ url += "?startday=%s&endday=%s&tree=trunk&bug=%s" % (start, end, b)
+ r = requests.get(url, headers={"User-agent": "mach-test-info/1.0"})
+ r.raise_for_status()
+
+ bdata = r.json()
+ data.extend(bdata)
+
+ if len(data) == 0:
+ print("no failures were found for given bugid, please ensure bug is")
+ print("accessible via: https://treeherder.mozilla.org/intermittent-failures")
+ return
+
+ # query VCS to get current list of variants:
+ import yaml
+
+ url = "https://hg.mozilla.org/mozilla-central/raw-file/tip/taskcluster/ci/test/variants.yml"
+ r = requests.get(url, headers={"User-agent": "mach-test-info/1.0"})
+ variants = yaml.safe_load(r.text)
+
+ print(
+ "\nQuerying data for bug %s annotated from %s to %s on trunk.\n\n"
+ % (buglist, start, end)
+ )
+ jobs = {}
+ lines = {}
+ for failure in data:
+ # config = platform/buildtype
+ # testsuite (<suite>[-variant][-<chunk>])
+ # lines - group by patterns that contain test name
+ config = "%s/%s" % (failure["platform"], failure["build_type"])
+
+ variant = ""
+ suite = ""
+ varpos = len(failure["test_suite"])
+ for v in variants.keys():
+ var = "-%s" % variants[v]["suffix"]
+ if var in failure["test_suite"]:
+ if failure["test_suite"].find(var) < varpos:
+ variant = var
+
+ if variant:
+ suite = failure["test_suite"].split(variant)[0]
+
+ parts = failure["test_suite"].split("-")
+ try:
+ int(parts[-1])
+ suite = "-".join(parts[:-1])
+ except ValueError:
+ pass # if this works, then the last '-X' is a number :)
+
+ if suite == "":
+ print("Error: failure to find variant in %s" % failure["test_suite"])
+
+ job = "%s-%s%s" % (config, suite, variant)
+ if job not in jobs.keys():
+ jobs[job] = 0
+ jobs[job] += 1
+
+ # lines - sum(hash) of all lines where we match testname
+ hvalue = 0
+ for line in failure["lines"]:
+ if len(line.split(testname)) <= 1:
+ continue
+ # strip off timestamp and mozharness status
+ parts = line.split("TEST-UNEXPECTED")
+ l = "TEST-UNEXPECTED%s" % parts[-1]
+
+ # only keep 25 characters of the failure, often longer is random numbers
+ parts = l.split(testname)
+ l = "%s%s%s" % (parts[0], testname, parts[1][:25])
+
+ hvalue += hash(l)
+
+ if not failure["lines"]:
+ hvalue = 1
+
+ if not hvalue:
+ continue
+
+ if hvalue not in lines.keys():
+ lines[hvalue] = {"lines": failure["lines"], "config": []}
+ lines[hvalue]["config"].append(job)
+
+ for h in lines.keys():
+ print("%s errors with:" % (len(lines[h]["config"])))
+ for l in lines[h]["lines"]:
+ print(l)
+ else:
+ print(
+ "... no failure lines recorded in"
+ " https://treeherder.mozilla.org/intermittent-failures ..."
+ )
+
+ for job in jobs:
+ count = len([x for x in lines[h]["config"] if x == job])
+ if count > 0:
+ print(" %s: %s" % (job, count))
+ print("")
+
+
+@Command(
+ "rusttests",
+ category="testing",
+ conditions=[conditions.is_non_artifact_build],
+ description="Run rust unit tests (via cargo test).",
+)
+def run_rusttests(command_context, **kwargs):
+ return command_context._mach_context.commands.dispatch(
+ "build",
+ command_context._mach_context,
+ what=["pre-export", "export", "recurse_rusttests"],
+ )
+
+
+@Command(
+ "fluent-migration-test",
+ category="testing",
+ description="Test Fluent migration recipes.",
+)
+@CommandArgument("test_paths", nargs="*", metavar="N", help="Recipe paths to test.")
+def run_migration_tests(command_context, test_paths=None, **kwargs):
+ if not test_paths:
+ test_paths = []
+ command_context.activate_virtualenv()
+ from test_fluent_migrations import fmt
+
+ rv = 0
+ with_context = []
+ for to_test in test_paths:
+ try:
+ context = fmt.inspect_migration(to_test)
+ for issue in context["issues"]:
+ command_context.log(
+ logging.ERROR,
+ "fluent-migration-test",
+ {
+ "error": issue["msg"],
+ "file": to_test,
+ },
+ "ERROR in {file}: {error}",
+ )
+ if context["issues"]:
+ continue
+ with_context.append(
+ {
+ "to_test": to_test,
+ "references": context["references"],
+ }
+ )
+ except Exception as e:
+ command_context.log(
+ logging.ERROR,
+ "fluent-migration-test",
+ {"error": str(e), "file": to_test},
+ "ERROR in {file}: {error}",
+ )
+ rv |= 1
+ obj_dir = fmt.prepare_object_dir(command_context)
+ for context in with_context:
+ rv |= fmt.test_migration(command_context, obj_dir, **context)
+ return rv