1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 from __future__
import absolute_import
, print_function
, unicode_literals
13 from mach
.decorators
import (
21 from mozbuild
.base
import (
22 BuildEnvironmentNotFoundException
,
24 MachCommandConditions
as conditions
,
28 I was unable to find tests from the given argument(s).
30 You should specify a test directory, filename, test suite name, or
33 It's possible my little brain doesn't know about the type of test you are
34 trying to execute. If you suspect this, please request support by filing
36 https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=General.
40 I know you are trying to run a %s%s test. Unfortunately, I can't run those
45 Test or tests to run. Tests can be specified by filename, directory, suite
48 The following test suites and aliases are supported: {}
53 class TestConfig(object):
55 def config_settings(cls
):
56 from mozlog
.commandline
import log_formatters
57 from mozlog
.structuredlog
import log_levels
59 format_desc
= "The default format to use when running tests with `mach test`."
60 format_choices
= list(log_formatters
)
61 level_desc
= "The default log level to use when running tests with `mach test`."
62 level_choices
= [l
.lower() for l
in log_levels
]
64 ("test.format", "string", format_desc
, "mach", {"choices": format_choices
}),
65 ("test.level", "string", level_desc
, "info", {"choices": level_choices
}),
69 def get_test_parser():
70 from mozlog
.commandline
import add_logging_group
71 from moztest
.resolve
import TEST_SUITES
73 parser
= argparse
.ArgumentParser()
78 help=TEST_HELP
.format(", ".join(sorted(TEST_SUITES
))),
83 nargs
=argparse
.REMAINDER
,
84 help="Extra arguments to pass to the underlying test command(s). "
85 "If an underlying command doesn't recognize the argument, it "
93 help="Specify a debugger to use.",
95 add_logging_group(parser
)
99 ADD_TEST_SUPPORTED_SUITES
= [
102 "mochitest-browser-chrome",
103 "web-platform-tests-testharness",
104 "web-platform-tests-reftest",
107 ADD_TEST_SUPPORTED_DOCS
= ["js", "html", "xhtml", "xul"]
110 "wpt": "web-platform-tests-testharness",
111 "wpt-testharness": "web-platform-tests-testharness",
112 "wpt-reftest": "web-platform-tests-reftest",
115 MISSING_ARG
= object()
118 def create_parser_addtest():
121 parser
= argparse
.ArgumentParser()
124 choices
=sorted(ADD_TEST_SUPPORTED_SUITES
+ list(SUITE_SYNONYMS
.keys())),
125 help="suite for the test. "
126 "If you pass a `test` argument this will be determined "
127 "based on the filename and the folder it is in",
133 help="Overwrite an existing file if it exists.",
137 choices
=ADD_TEST_SUPPORTED_DOCS
,
138 help="Document type for the test (if applicable)."
139 "If you pass a `test` argument this will be determined "
140 "based on the filename.",
148 help="Open the created file(s) in an editor; if a "
149 "binary is supplied it will be used otherwise the default editor for "
150 "your environment will be opened",
153 for base_suite
in addtest
.TEST_CREATORS
:
154 cls
= addtest
.TEST_CREATORS
[base_suite
]
155 if hasattr(cls
, "get_parser"):
156 group
= parser
.add_argument_group(base_suite
)
157 cls
.get_parser(group
)
159 parser
.add_argument("test", nargs
="?", help=("Test to create."))
164 class AddTest(MachCommandBase
):
168 description
="Generate tests based on templates",
169 parser
=create_parser_addtest
,
182 from moztest
.resolve
import TEST_SUITES
184 if not suite
and not test
:
185 return create_parser_addtest().parse_args(["--help"])
187 if suite
in SUITE_SYNONYMS
:
188 suite
= SUITE_SYNONYMS
[suite
]
191 if not overwrite
and os
.path
.isfile(os
.path
.abspath(test
)):
192 print("Error: can't generate a test that already exists:", test
)
195 abs_test
= os
.path
.abspath(test
)
197 doc
= self
.guess_doc(abs_test
)
199 guessed_suite
, err
= self
.guess_suite(abs_test
)
203 suite
= guessed_suite
212 "We couldn't automatically determine a suite. "
213 "Please specify `--suite` with one of the following options:\n{}\n"
214 "If you'd like to add support to a new suite, please file a bug "
215 "blocking https://bugzilla.mozilla.org/show_bug.cgi?id=1540285.".format(
216 ADD_TEST_SUPPORTED_SUITES
221 if doc
not in ADD_TEST_SUPPORTED_DOCS
:
223 "Error: invalid `doc`. Either pass in a test with a valid extension"
224 "({}) or pass in the `doc` argument".format(ADD_TEST_SUPPORTED_DOCS
)
228 creator_cls
= addtest
.creator_for_suite(suite
)
230 if creator_cls
is None:
231 print("Sorry, `addtest` doesn't currently know how to add {}".format(suite
))
234 creator
= creator_cls(self
.topsrcdir
, test
, suite
, doc
, **kwargs
)
240 for path
, template
in creator
:
246 print("Adding a test file at {} (suite `{}`)".format(path
, suite
))
249 os
.makedirs(os
.path
.dirname(path
))
253 with io
.open(path
, "w", newline
="\n") as f
:
256 # write to stdout if you passed only suite and doc and not a file path
263 creator
.update_manifest()
265 # Small hack, should really do this better
266 if suite
.startswith("wpt-"):
267 suite
= "web-platform-tests"
269 mach_command
= TEST_SUITES
[suite
]["mach_command"]
271 "Please make sure to add the new test to your commit. "
272 "You can now run the test with:\n ./mach {} {}".format(
277 if editor
is not MISSING_ARG
:
278 if editor
is not None:
280 elif "VISUAL" in os
.environ
:
281 editor
= os
.environ
["VISUAL"]
282 elif "EDITOR" in os
.environ
:
283 editor
= os
.environ
["EDITOR"]
285 print("Unable to determine editor; please specify a binary")
292 proc
= subprocess
.Popen("%s %s" % (editor
, " ".join(paths
)), shell
=True)
299 def guess_doc(self
, abs_test
):
300 filename
= os
.path
.basename(abs_test
)
301 return os
.path
.splitext(filename
)[1].strip(".")
303 def guess_suite(self
, abs_test
):
304 # If you pass a abs_test, try to detect the type based on the name
305 # and folder. This detection can be skipped if you pass the `type` arg.
308 parent
= os
.path
.dirname(abs_test
)
309 filename
= os
.path
.basename(abs_test
)
311 has_browser_ini
= os
.path
.isfile(os
.path
.join(parent
, "browser.ini"))
312 has_chrome_ini
= os
.path
.isfile(os
.path
.join(parent
, "chrome.ini"))
313 has_plain_ini
= os
.path
.isfile(os
.path
.join(parent
, "mochitest.ini"))
314 has_xpcshell_ini
= os
.path
.isfile(os
.path
.join(parent
, "xpcshell.ini"))
316 in_wpt_folder
= abs_test
.startswith(
317 os
.path
.abspath(os
.path
.join("testing", "web-platform"))
321 guessed_suite
= "web-platform-tests-testharness"
322 if "/css/" in abs_test
:
323 guessed_suite
= "web-platform-tests-reftest"
325 filename
.startswith("test_")
327 and self
.guess_doc(abs_test
) == "js"
329 guessed_suite
= "xpcshell"
331 if filename
.startswith("browser_") and has_browser_ini
:
332 guessed_suite
= "mochitest-browser-chrome"
333 elif filename
.startswith("test_"):
334 if has_chrome_ini
and has_plain_ini
:
336 "Error: directory contains both a chrome.ini and mochitest.ini. "
337 "Please set --suite=mochitest-chrome or --suite=mochitest-plain."
340 guessed_suite
= "mochitest-chrome"
342 guessed_suite
= "mochitest-plain"
343 return guessed_suite
, err
347 class Test(MachCommandBase
):
351 description
="Run tests (detects the kind of test and runs it).",
352 parser
=get_test_parser
,
354 def test(self
, what
, extra_args
, **log_args
):
355 """Run tests from names or paths.
357 mach test accepts arguments specifying which tests to run. Each argument
360 * The path to a test file
361 * A directory containing tests
363 * An alias to a test suite name (codes used on TreeHerder)
365 When paths or directories are given, they are first resolved to test
366 files known to the build system.
368 If resolved tests belong to more than one test type/flavor/harness,
369 the harness for each relevant type/flavor will be invoked. e.g. if
370 you specify a directory with xpcshell and browser chrome mochitests,
371 both harnesses will be invoked.
373 Warning: `mach test` does not automatically re-build.
374 Please remember to run `mach build` when necessary.
378 Run all test files in the devtools/client/shared/redux/middleware/xpcshell/
381 `./mach test devtools/client/shared/redux/middleware/xpcshell/`
383 The below command prints a short summary of results instead of
384 the default more verbose output.
385 Do not forget the - (minus sign) after --log-grouped!
387 `./mach test --log-grouped - devtools/client/shared/redux/middleware/xpcshell/`
389 from mozlog
.commandline
import setup_logging
390 from mozlog
.handlers
import StreamHandler
391 from moztest
.resolve
import get_suite_definition
, TestResolver
, TEST_SUITES
393 resolver
= self
._spawn
(TestResolver
)
394 run_suites
, run_tests
= resolver
.resolve_metadata(what
)
396 if not run_suites
and not run_tests
:
400 if log_args
.get("debugger", None):
403 if not mozdebug
.get_debugger_info(log_args
.get("debugger")):
405 extra_args_debugger_notation
= "=".join(
406 ["--debugger", log_args
.get("debugger")]
409 extra_args
.append(extra_args_debugger_notation
)
411 extra_args
= [extra_args_debugger_notation
]
413 # Create shared logger
414 format_args
= {"level": self
._mach
_context
.settings
["test"]["level"]}
415 if not run_suites
and len(run_tests
) == 1:
416 format_args
["verbose"] = True
417 format_args
["compact"] = False
419 default_format
= self
._mach
_context
.settings
["test"]["format"]
421 "mach-test", log_args
, {default_format
: sys
.stdout
}, format_args
423 for handler
in log
.handlers
:
424 if isinstance(handler
, StreamHandler
):
425 handler
.formatter
.inner
.summary_on_shutdown
= True
428 for suite_name
in run_suites
:
429 suite
= TEST_SUITES
[suite_name
]
430 kwargs
= suite
["kwargs"]
432 kwargs
.setdefault("subsuite", None)
434 if "mach_command" in suite
:
435 res
= self
._mach
_context
.commands
.dispatch(
436 suite
["mach_command"], self
._mach
_context
, argv
=extra_args
, **kwargs
442 for test
in run_tests
:
443 key
= (test
["flavor"], test
.get("subsuite", ""))
444 buckets
.setdefault(key
, []).append(test
)
446 for (flavor
, subsuite
), tests
in sorted(buckets
.items()):
447 _
, m
= get_suite_definition(flavor
, subsuite
)
448 if "mach_command" not in m
:
449 substr
= "-{}".format(subsuite
) if subsuite
else ""
450 print(UNKNOWN_FLAVOR
% (flavor
, substr
))
454 kwargs
= dict(m
["kwargs"])
456 kwargs
.setdefault("subsuite", None)
458 res
= self
._mach
_context
.commands
.dispatch(
473 class MachCommands(MachCommandBase
):
475 "cppunittest", category
="testing", description
="Run cpp unit tests (C++ tests)."
478 "--enable-webrender",
481 dest
="enable_webrender",
482 help="Enable the WebRender compositor in Gecko.",
488 help="Test to run. Can be specified as one or more files or "
489 "directories, or omitted. If omitted, the entire test suite is "
492 def run_cppunit_test(self
, **params
):
493 from mozlog
import commandline
495 log
= params
.get("log")
497 log
= commandline
.setup_logging("cppunittest", {}, {"tbpl": sys
.stdout
})
499 # See if we have crash symbols
500 symbols_path
= os
.path
.join(self
.distdir
, "crashreporter-symbols")
501 if not os
.path
.isdir(symbols_path
):
504 # If no tests specified, run all tests in main manifest
505 tests
= params
["test_files"]
507 tests
= [os
.path
.join(self
.distdir
, "cppunittests")]
508 manifest_path
= os
.path
.join(self
.topsrcdir
, "testing", "cppunittest.ini")
512 utility_path
= self
.bindir
514 if conditions
.is_android(self
):
515 from mozrunner
.devices
.android_device
import (
516 verify_android_device
,
520 verify_android_device(self
, install
=InstallIntent
.NO
)
521 return self
.run_android_test(tests
, symbols_path
, manifest_path
, log
)
523 return self
.run_desktop_test(
524 tests
, symbols_path
, manifest_path
, utility_path
, log
527 def run_desktop_test(self
, tests
, symbols_path
, manifest_path
, utility_path
, log
):
528 import runcppunittests
as cppunittests
529 from mozlog
import commandline
531 parser
= cppunittests
.CPPUnittestOptions()
532 commandline
.add_logging_group(parser
)
533 options
, args
= parser
.parse_args()
535 options
.symbols_path
= symbols_path
536 options
.manifest_path
= manifest_path
537 options
.utility_path
= utility_path
538 options
.xre_path
= self
.bindir
541 result
= cppunittests
.run_test_harness(options
, tests
)
542 except Exception as e
:
543 log
.error("Caught exception running cpp unit tests: %s" % str(e
))
547 return 0 if result
else 1
549 def run_android_test(self
, tests
, symbols_path
, manifest_path
, log
):
550 import remotecppunittests
as remotecppunittests
551 from mozlog
import commandline
553 parser
= remotecppunittests
.RemoteCPPUnittestOptions()
554 commandline
.add_logging_group(parser
)
555 options
, args
= parser
.parse_args()
557 if not options
.adb_path
:
558 from mozrunner
.devices
.android_device
import get_adb_path
560 options
.adb_path
= get_adb_path(self
)
561 options
.symbols_path
= symbols_path
562 options
.manifest_path
= manifest_path
563 options
.xre_path
= self
.bindir
564 options
.local_lib
= self
.bindir
.replace("bin", "fennec")
565 for file in os
.listdir(os
.path
.join(self
.topobjdir
, "dist")):
566 if file.endswith(".apk") and file.startswith("fennec"):
567 options
.local_apk
= os
.path
.join(self
.topobjdir
, "dist", file)
568 log
.info("using APK: " + options
.local_apk
)
572 result
= remotecppunittests
.run_test_harness(options
, tests
)
573 except Exception as e
:
574 log
.error("Caught exception running cpp unit tests: %s" % str(e
))
578 return 0 if result
else 1
581 def executable_name(name
):
582 return name
+ ".exe" if sys
.platform
.startswith("win") else name
586 class SpiderMonkeyTests(MachCommandBase
):
590 description
="Run SpiderMonkey JS tests in the JS shell.",
592 @CommandArgument("--shell", help="The shell to be used")
595 nargs
=argparse
.REMAINDER
,
596 help="Extra arguments to pass down to the test harness.",
598 def run_jstests(self
, shell
, params
):
601 self
.virtualenv_manager
.ensure()
602 python
= self
.virtualenv_manager
.python_path
604 js
= shell
or os
.path
.join(self
.bindir
, executable_name("js"))
607 os
.path
.join(self
.topsrcdir
, "js", "src", "tests", "jstests.py"),
611 return subprocess
.call(jstest_cmd
)
616 description
="Run SpiderMonkey jit-tests in the JS shell.",
617 ok_if_tests_disabled
=True,
619 @CommandArgument("--shell", help="The shell to be used")
624 help="Run with the SM(cgc) job's env vars",
628 nargs
=argparse
.REMAINDER
,
629 help="Extra arguments to pass down to the test harness.",
631 def run_jittests(self
, shell
, cgc
, params
):
634 self
.virtualenv_manager
.ensure()
635 python
= self
.virtualenv_manager
.python_path
637 js
= shell
or os
.path
.join(self
.bindir
, executable_name("js"))
640 os
.path
.join(self
.topsrcdir
, "js", "src", "jit-test", "jit_test.py"),
644 env
= os
.environ
.copy()
646 env
["JS_GC_ZEAL"] = "IncrementalMultipleSlices"
648 return subprocess
.call(jittest_cmd
, env
=env
)
651 "jsapi-tests", category
="testing", description
="Run SpiderMonkey JSAPI tests."
657 help="Test to run. Can be a prefix or omitted. If "
658 "omitted, the entire test suite is executed.",
660 def run_jsapitests(self
, test_name
=None):
663 jsapi_tests_cmd
= [os
.path
.join(self
.bindir
, executable_name("jsapi-tests"))]
665 jsapi_tests_cmd
.append(test_name
)
667 test_env
= os
.environ
.copy()
668 test_env
["TOPSRCDIR"] = self
.topsrcdir
670 return subprocess
.call(jsapi_tests_cmd
, env
=test_env
)
672 def run_check_js_msg(self
):
675 self
.virtualenv_manager
.ensure()
676 python
= self
.virtualenv_manager
.python_path
680 os
.path
.join(self
.topsrcdir
, "config", "check_js_msg_encoding.py"),
683 return subprocess
.call(check_cmd
)
686 def get_jsshell_parser():
687 from jsshell
.benchmark
import get_parser
693 class JsShellTests(MachCommandBase
):
697 parser
=get_jsshell_parser
,
698 description
="Run benchmarks in the SpiderMonkey JS shell.",
700 def run_jsshelltests(self
, **kwargs
):
701 self
.activate_virtualenv()
702 from jsshell
import benchmark
704 return benchmark
.run(**kwargs
)
708 class CramTest(MachCommandBase
):
712 description
="Mercurial style .t tests for command line applications.",
718 help="Test paths to run. Each path can be a test file or directory. "
719 "If omitted, the entire suite will be run.",
723 nargs
=argparse
.REMAINDER
,
724 help="Extra arguments to pass down to the cram binary. See "
725 "'./mach python -m cram -- -h' for a list of available options.",
727 def cramtest(self
, cram_args
=None, test_paths
=None, test_objects
=None):
728 self
.activate_virtualenv()
730 from manifestparser
import TestManifest
732 if test_objects
is None:
733 from moztest
.resolve
import TestResolver
735 resolver
= self
._spawn
(TestResolver
)
737 # If we were given test paths, try to find tests matching them.
738 test_objects
= resolver
.resolve_tests(paths
=test_paths
, flavor
="cram")
740 # Otherwise just run everything in CRAMTEST_MANIFESTS
741 test_objects
= resolver
.resolve_tests(flavor
="cram")
744 message
= "No tests were collected, check spelling of the test paths."
745 self
.log(logging
.WARN
, "cramtest", {}, message
)
749 mp
.tests
.extend(test_objects
)
750 tests
= mp
.active_tests(disabled
=False, **mozinfo
.info
)
752 python
= self
.virtualenv_manager
.python_path
753 cmd
= [python
, "-m", "cram"] + cram_args
+ [t
["relpath"] for t
in tests
]
754 return subprocess
.call(cmd
, cwd
=self
.topsrcdir
)
758 class TestInfoCommand(MachCommandBase
):
759 from datetime
import date
, timedelta
762 "test-info", category
="testing", description
="Display historical test results."
766 All functions implemented as subcommands.
772 description
="Display historical test result summary for named tests.",
775 "test_names", nargs
=argparse
.REMAINDER
, help="Test(s) of interest."
779 default
=(date
.today() - timedelta(7)).strftime("%Y-%m-%d"),
780 help="Start date (YYYY-MM-DD)",
783 "--end", default
=date
.today().strftime("%Y-%m-%d"), help="End date (YYYY-MM-DD)"
788 help="Retrieve and display general test information.",
793 help="Retrieve and display related Bugzilla bugs.",
795 @CommandArgument("--verbose", action
="store_true", help="Enable debug logging.")
807 ti
= testinfo
.TestInfoTests(verbose
)
819 description
="Generate a json report of test manifests and/or tests "
820 "categorized by Bugzilla component and optionally filtered "
821 "by path, component, and/or manifest annotations.",
826 help="Comma-separated list of Bugzilla components."
827 " eg. Testing::General,Core::WebVR",
831 help='Limit results to tests of the specified flavor (eg. "xpcshell").',
835 help='Limit results to tests of the specified subsuite (eg. "devtools").',
838 "paths", nargs
=argparse
.REMAINDER
, help="File system paths of interest."
843 help="Include test manifests in report.",
846 "--show-tests", action
="store_true", help="Include individual tests in report."
849 "--show-summary", action
="store_true", help="Include summary in report."
852 "--show-annotations",
854 help="Include list of manifest annotation conditions in report.",
858 help="Comma-separated list of value regular expressions to filter on; "
859 "displayed tests contain all specified values.",
863 help="Comma-separated list of test keys to filter on, "
864 'like "skip-if"; only these fields will be searched '
865 "for filter-values.",
868 "--no-component-report",
869 action
="store_false",
870 dest
="show_components",
872 help="Do not categorize by bugzilla component.",
874 @CommandArgument("--output-file", help="Path to report file.")
875 @CommandArgument("--verbose", action
="store_true", help="Enable debug logging.")
893 from mozbuild
.build_commands
import Build
896 self
.config_environment
897 except BuildEnvironmentNotFoundException
:
898 print("Looks like configure has not run yet, running it now...")
899 builder
= Build(self
._mach
_context
, None)
902 ti
= testinfo
.TestInfoReport(verbose
)
921 description
='Compare two reports generated by "test-info reports".',
926 help="The first (earlier) report file; path to local file or url.",
929 "--after", help="The second (later) report file; path to local file or url."
933 help="Path to report file to be written. If not specified, report"
934 "will be written to standard output.",
936 @CommandArgument("--verbose", action
="store_true", help="Enable debug logging.")
937 def test_report_diff(self
, before
, after
, output_file
, verbose
):
940 ti
= testinfo
.TestInfoReport(verbose
)
941 ti
.report_diff(before
, after
, output_file
)
945 class RustTests(MachCommandBase
):
949 conditions
=[conditions
.is_non_artifact_build
],
950 description
="Run rust unit tests (via cargo test).",
952 def run_rusttests(self
, **kwargs
):
953 return self
._mach
_context
.commands
.dispatch(
956 what
=["pre-export", "export", "recurse_rusttests"],
961 class TestFluentMigration(MachCommandBase
):
963 "fluent-migration-test",
965 description
="Test Fluent migration recipes.",
967 @CommandArgument("test_paths", nargs
="*", metavar
="N", help="Recipe paths to test.")
968 def run_migration_tests(self
, test_paths
=None, **kwargs
):
971 self
.activate_virtualenv()
972 from test_fluent_migrations
import fmt
976 for to_test
in test_paths
:
978 context
= fmt
.inspect_migration(to_test
)
979 for issue
in context
["issues"]:
982 "fluent-migration-test",
984 "error": issue
["msg"],
987 "ERROR in {file}: {error}",
989 if context
["issues"]:
994 "references": context
["references"],
997 except Exception as e
:
1000 "fluent-migration-test",
1001 {"error": str(e
), "file": to_test
},
1002 "ERROR in {file}: {error}",
1005 obj_dir
= fmt
.prepare_object_dir(self
)
1006 for context
in with_context
:
1007 rv |
= fmt
.test_migration(self
, obj_dir
, **context
)