mach_commands.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  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/.
  4. from __future__ import absolute_import, print_function, unicode_literals
  5. import json
  6. import os
  7. import sys
  8. import tempfile
  9. import subprocess
  10. import shutil
  11. from collections import defaultdict
  12. from mach.decorators import (
  13. CommandArgument,
  14. CommandProvider,
  15. Command,
  16. )
  17. from mozbuild.base import MachCommandBase
  18. from mozbuild.base import MachCommandConditions as conditions
  19. import mozpack.path as mozpath
  20. from argparse import ArgumentParser
  21. UNKNOWN_TEST = '''
  22. I was unable to find tests from the given argument(s).
  23. You should specify a test directory, filename, test suite name, or
  24. abbreviation. If no arguments are given, there must be local file
  25. changes and corresponding IMPACTED_TESTS annotations in moz.build
  26. files relevant to those files.
  27. It's possible my little brain doesn't know about the type of test you are
  28. trying to execute. If you suspect this, please request support by filing
  29. a bug at
  30. https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=General.
  31. '''.strip()
  32. UNKNOWN_FLAVOR = '''
  33. I know you are trying to run a %s test. Unfortunately, I can't run those
  34. tests yet. Sorry!
  35. '''.strip()
  36. MOCHITEST_CHUNK_BY_DIR = 4
  37. MOCHITEST_TOTAL_CHUNKS = 5
  38. TEST_SUITES = {
  39. 'cppunittest': {
  40. 'aliases': ('Cpp', 'cpp'),
  41. 'mach_command': 'cppunittest',
  42. 'kwargs': {'test_file': None},
  43. },
  44. 'crashtest': {
  45. 'aliases': ('C', 'Rc', 'RC', 'rc'),
  46. 'mach_command': 'crashtest',
  47. 'kwargs': {'test_file': None},
  48. },
  49. 'firefox-ui-functional': {
  50. 'aliases': ('Fxfn',),
  51. 'mach_command': 'firefox-ui-functional',
  52. 'kwargs': {},
  53. },
  54. 'firefox-ui-update': {
  55. 'aliases': ('Fxup',),
  56. 'mach_command': 'firefox-ui-update',
  57. 'kwargs': {},
  58. },
  59. 'jetpack': {
  60. 'aliases': ('J',),
  61. 'mach_command': 'jetpack-test',
  62. 'kwargs': {},
  63. },
  64. 'check-spidermonkey': {
  65. 'aliases': ('Sm', 'sm'),
  66. 'mach_command': 'check-spidermonkey',
  67. 'kwargs': {'valgrind': False},
  68. },
  69. 'mochitest-a11y': {
  70. 'mach_command': 'mochitest',
  71. 'kwargs': {'flavor': 'a11y', 'test_paths': None},
  72. },
  73. 'mochitest-browser': {
  74. 'aliases': ('bc', 'BC', 'Bc'),
  75. 'mach_command': 'mochitest',
  76. 'kwargs': {'flavor': 'browser-chrome', 'test_paths': None},
  77. },
  78. 'mochitest-chrome': {
  79. 'mach_command': 'mochitest',
  80. 'kwargs': {'flavor': 'chrome', 'test_paths': None},
  81. },
  82. 'mochitest-devtools': {
  83. 'aliases': ('dt', 'DT', 'Dt'),
  84. 'mach_command': 'mochitest',
  85. 'kwargs': {'subsuite': 'devtools', 'test_paths': None},
  86. },
  87. 'mochitest-plain': {
  88. 'mach_command': 'mochitest',
  89. 'kwargs': {'flavor': 'plain', 'test_paths': None},
  90. },
  91. 'python': {
  92. 'mach_command': 'python-test',
  93. 'kwargs': {'tests': None},
  94. },
  95. 'reftest': {
  96. 'aliases': ('RR', 'rr', 'Rr'),
  97. 'mach_command': 'reftest',
  98. 'kwargs': {'tests': None},
  99. },
  100. 'web-platform-tests': {
  101. 'aliases': ('wpt',),
  102. 'mach_command': 'web-platform-tests',
  103. 'kwargs': {}
  104. },
  105. 'valgrind': {
  106. 'aliases': ('V', 'v'),
  107. 'mach_command': 'valgrind-test',
  108. 'kwargs': {},
  109. },
  110. 'xpcshell': {
  111. 'aliases': ('X', 'x'),
  112. 'mach_command': 'xpcshell-test',
  113. 'kwargs': {'test_file': 'all'},
  114. },
  115. }
  116. # Maps test flavors to metadata on how to run that test.
  117. TEST_FLAVORS = {
  118. 'a11y': {
  119. 'mach_command': 'mochitest',
  120. 'kwargs': {'flavor': 'a11y', 'test_paths': []},
  121. },
  122. 'browser-chrome': {
  123. 'mach_command': 'mochitest',
  124. 'kwargs': {'flavor': 'browser-chrome', 'test_paths': []},
  125. },
  126. 'crashtest': {},
  127. 'chrome': {
  128. 'mach_command': 'mochitest',
  129. 'kwargs': {'flavor': 'chrome', 'test_paths': []},
  130. },
  131. 'firefox-ui-functional': {
  132. 'mach_command': 'firefox-ui-functional',
  133. 'kwargs': {'tests': []},
  134. },
  135. 'firefox-ui-update': {
  136. 'mach_command': 'firefox-ui-update',
  137. 'kwargs': {'tests': []},
  138. },
  139. 'marionette': {
  140. 'mach_command': 'marionette-test',
  141. 'kwargs': {'tests': []},
  142. },
  143. 'mochitest': {
  144. 'mach_command': 'mochitest',
  145. 'kwargs': {'flavor': 'mochitest', 'test_paths': []},
  146. },
  147. 'python': {
  148. 'mach_command': 'python-test',
  149. 'kwargs': {},
  150. },
  151. 'reftest': {
  152. 'mach_command': 'reftest',
  153. 'kwargs': {'tests': []}
  154. },
  155. 'steeplechase': {},
  156. 'web-platform-tests': {
  157. 'mach_command': 'web-platform-tests',
  158. 'kwargs': {'include': []}
  159. },
  160. 'xpcshell': {
  161. 'mach_command': 'xpcshell-test',
  162. 'kwargs': {'test_paths': []},
  163. },
  164. }
  165. for i in range(1, MOCHITEST_TOTAL_CHUNKS + 1):
  166. TEST_SUITES['mochitest-%d' %i] = {
  167. 'aliases': ('M%d' % i, 'm%d' % i),
  168. 'mach_command': 'mochitest',
  169. 'kwargs': {
  170. 'flavor': 'mochitest',
  171. 'subsuite': 'default',
  172. 'chunk_by_dir': MOCHITEST_CHUNK_BY_DIR,
  173. 'total_chunks': MOCHITEST_TOTAL_CHUNKS,
  174. 'this_chunk': i,
  175. 'test_paths': None,
  176. },
  177. }
  178. TEST_HELP = '''
  179. Test or tests to run. Tests can be specified by filename, directory, suite
  180. name or suite alias.
  181. The following test suites and aliases are supported: %s
  182. ''' % ', '.join(sorted(TEST_SUITES))
  183. TEST_HELP = TEST_HELP.strip()
  184. @CommandProvider
  185. class Test(MachCommandBase):
  186. @Command('test', category='testing', description='Run tests (detects the kind of test and runs it).')
  187. @CommandArgument('what', default=None, nargs='*', help=TEST_HELP)
  188. def test(self, what):
  189. """Run tests from names or paths.
  190. mach test accepts arguments specifying which tests to run. Each argument
  191. can be:
  192. * The path to a test file
  193. * A directory containing tests
  194. * A test suite name
  195. * An alias to a test suite name (codes used on TreeHerder)
  196. If no input is provided, tests will be run based on files changed in
  197. the local tree. Relevant tests, tags, or flavors are determined by
  198. IMPACTED_TESTS annotations in moz.build files relevant to the
  199. changed files.
  200. When paths or directories are given, they are first resolved to test
  201. files known to the build system.
  202. If resolved tests belong to more than one test type/flavor/harness,
  203. the harness for each relevant type/flavor will be invoked. e.g. if
  204. you specify a directory with xpcshell and browser chrome mochitests,
  205. both harnesses will be invoked.
  206. """
  207. from mozbuild.testing import TestResolver
  208. # Parse arguments and assemble a test "plan."
  209. run_suites = set()
  210. run_tests = []
  211. resolver = self._spawn(TestResolver)
  212. for entry in what:
  213. # If the path matches the name or alias of an entire suite, run
  214. # the entire suite.
  215. if entry in TEST_SUITES:
  216. run_suites.add(entry)
  217. continue
  218. suitefound = False
  219. for suite, v in TEST_SUITES.items():
  220. if entry in v.get('aliases', []):
  221. run_suites.add(suite)
  222. suitefound = True
  223. if suitefound:
  224. continue
  225. # Now look for file/directory matches in the TestResolver.
  226. relpath = self._wrap_path_argument(entry).relpath()
  227. tests = list(resolver.resolve_tests(paths=[relpath]))
  228. run_tests.extend(tests)
  229. if not tests:
  230. print('UNKNOWN TEST: %s' % entry, file=sys.stderr)
  231. if not what:
  232. # TODO: This isn't really related to try, and should be
  233. # extracted to a common library for vcs interactions when it is
  234. # introduced in bug 1185599.
  235. from autotry import AutoTry
  236. at = AutoTry(self.topsrcdir, resolver, self._mach_context)
  237. changed_files = at.find_changed_files()
  238. if changed_files:
  239. print("Tests will be run based on modifications to the "
  240. "following files:\n\t%s" % "\n\t".join(changed_files))
  241. from mozbuild.frontend.reader import (
  242. BuildReader,
  243. EmptyConfig,
  244. )
  245. config = EmptyConfig(self.topsrcdir)
  246. reader = BuildReader(config)
  247. files_info = reader.files_info(changed_files)
  248. paths, tags, flavors = set(), set(), set()
  249. for info in files_info.values():
  250. paths |= info.test_files
  251. tags |= info.test_tags
  252. flavors |= info.test_flavors
  253. # This requires multiple calls to resolve_tests, because the test
  254. # resolver returns tests that match every condition, while we want
  255. # tests that match any condition. Bug 1210213 tracks implementing
  256. # more flexible querying.
  257. if tags:
  258. run_tests = list(resolver.resolve_tests(tags=tags))
  259. if paths:
  260. run_tests += [t for t in resolver.resolve_tests(paths=paths)
  261. if not (tags & set(t.get('tags', '').split()))]
  262. if flavors:
  263. run_tests = [t for t in run_tests if t['flavor'] not in flavors]
  264. for flavor in flavors:
  265. run_tests += list(resolver.resolve_tests(flavor=flavor))
  266. if not run_suites and not run_tests:
  267. print(UNKNOWN_TEST)
  268. return 1
  269. status = None
  270. for suite_name in run_suites:
  271. suite = TEST_SUITES[suite_name]
  272. if 'mach_command' in suite:
  273. res = self._mach_context.commands.dispatch(
  274. suite['mach_command'], self._mach_context,
  275. **suite['kwargs'])
  276. if res:
  277. status = res
  278. buckets = {}
  279. for test in run_tests:
  280. key = (test['flavor'], test.get('subsuite', ''))
  281. buckets.setdefault(key, []).append(test)
  282. for (flavor, subsuite), tests in sorted(buckets.items()):
  283. if flavor not in TEST_FLAVORS:
  284. print(UNKNOWN_FLAVOR % flavor)
  285. status = 1
  286. continue
  287. m = TEST_FLAVORS[flavor]
  288. if 'mach_command' not in m:
  289. print(UNKNOWN_FLAVOR % flavor)
  290. status = 1
  291. continue
  292. kwargs = dict(m['kwargs'])
  293. kwargs['subsuite'] = subsuite
  294. res = self._mach_context.commands.dispatch(
  295. m['mach_command'], self._mach_context,
  296. test_objects=tests, **kwargs)
  297. if res:
  298. status = res
  299. return status
  300. @CommandProvider
  301. class MachCommands(MachCommandBase):
  302. @Command('cppunittest', category='testing',
  303. description='Run cpp unit tests (C++ tests).')
  304. @CommandArgument('test_files', nargs='*', metavar='N',
  305. help='Test to run. Can be specified as one or more files or ' \
  306. 'directories, or omitted. If omitted, the entire test suite is ' \
  307. 'executed.')
  308. def run_cppunit_test(self, **params):
  309. import mozinfo
  310. from mozlog import commandline
  311. log = commandline.setup_logging("cppunittest",
  312. {},
  313. {"tbpl": sys.stdout})
  314. # See if we have crash symbols
  315. symbols_path = os.path.join(self.distdir, 'crashreporter-symbols')
  316. if not os.path.isdir(symbols_path):
  317. symbols_path = None
  318. # If no tests specified, run all tests in main manifest
  319. tests = params['test_files']
  320. if len(tests) == 0:
  321. tests = [os.path.join(self.distdir, 'cppunittests')]
  322. manifest_path = os.path.join(self.topsrcdir, 'testing', 'cppunittest.ini')
  323. else:
  324. manifest_path = None
  325. if conditions.is_android(self):
  326. from mozrunner.devices.android_device import verify_android_device
  327. verify_android_device(self, install=False)
  328. return self.run_android_test(tests, symbols_path, manifest_path, log)
  329. return self.run_desktop_test(tests, symbols_path, manifest_path, log)
  330. def run_desktop_test(self, tests, symbols_path, manifest_path, log):
  331. import runcppunittests as cppunittests
  332. from mozlog import commandline
  333. parser = cppunittests.CPPUnittestOptions()
  334. commandline.add_logging_group(parser)
  335. options, args = parser.parse_args()
  336. options.symbols_path = symbols_path
  337. options.manifest_path = manifest_path
  338. options.xre_path = self.bindir
  339. try:
  340. result = cppunittests.run_test_harness(options, tests)
  341. except Exception as e:
  342. log.error("Caught exception running cpp unit tests: %s" % str(e))
  343. result = False
  344. raise
  345. return 0 if result else 1
  346. def run_android_test(self, tests, symbols_path, manifest_path, log):
  347. import remotecppunittests as remotecppunittests
  348. from mozlog import commandline
  349. parser = remotecppunittests.RemoteCPPUnittestOptions()
  350. commandline.add_logging_group(parser)
  351. options, args = parser.parse_args()
  352. options.symbols_path = symbols_path
  353. options.manifest_path = manifest_path
  354. options.xre_path = self.bindir
  355. options.dm_trans = "adb"
  356. options.local_lib = self.bindir.replace('bin', 'fennec')
  357. for file in os.listdir(os.path.join(self.topobjdir, "dist")):
  358. if file.endswith(".apk") and file.startswith("fennec"):
  359. options.local_apk = os.path.join(self.topobjdir, "dist", file)
  360. log.info("using APK: " + options.local_apk)
  361. break
  362. try:
  363. result = remotecppunittests.run_test_harness(options, tests)
  364. except Exception as e:
  365. log.error("Caught exception running cpp unit tests: %s" % str(e))
  366. result = False
  367. raise
  368. return 0 if result else 1
  369. def executable_name(name):
  370. return name + '.exe' if sys.platform.startswith('win') else name
  371. @CommandProvider
  372. class CheckSpiderMonkeyCommand(MachCommandBase):
  373. @Command('check-spidermonkey', category='testing', description='Run SpiderMonkey tests (JavaScript engine).')
  374. @CommandArgument('--valgrind', action='store_true', help='Run jit-test suite with valgrind flag')
  375. def run_checkspidermonkey(self, **params):
  376. import subprocess
  377. import sys
  378. js = os.path.join(self.bindir, executable_name('js'))
  379. print('Running jit-tests')
  380. jittest_cmd = [os.path.join(self.topsrcdir, 'js', 'src', 'jit-test', 'jit_test.py'),
  381. js, '--no-slow', '--jitflags=all']
  382. if params['valgrind']:
  383. jittest_cmd.append('--valgrind')
  384. jittest_result = subprocess.call(jittest_cmd)
  385. print('running jstests')
  386. jstest_cmd = [os.path.join(self.topsrcdir, 'js', 'src', 'tests', 'jstests.py'),
  387. js, '--jitflags=all']
  388. jstest_result = subprocess.call(jstest_cmd)
  389. print('running jsapi-tests')
  390. jsapi_tests_cmd = [os.path.join(self.bindir, executable_name('jsapi-tests'))]
  391. jsapi_tests_result = subprocess.call(jsapi_tests_cmd)
  392. print('running check-style')
  393. check_style_cmd = [sys.executable, os.path.join(self.topsrcdir, 'config', 'check_spidermonkey_style.py')]
  394. check_style_result = subprocess.call(check_style_cmd, cwd=os.path.join(self.topsrcdir, 'js', 'src'))
  395. print('running check-masm')
  396. check_masm_cmd = [sys.executable, os.path.join(self.topsrcdir, 'config', 'check_macroassembler_style.py')]
  397. check_masm_result = subprocess.call(check_masm_cmd, cwd=os.path.join(self.topsrcdir, 'js', 'src'))
  398. print('running check-js-msg-encoding')
  399. check_js_msg_cmd = [sys.executable, os.path.join(self.topsrcdir, 'config', 'check_js_msg_encoding.py')]
  400. check_js_msg_result = subprocess.call(check_js_msg_cmd, cwd=self.topsrcdir)
  401. all_passed = jittest_result and jstest_result and jsapi_tests_result and check_style_result and check_masm_result and check_js_msg_result
  402. return all_passed
  403. @CommandProvider
  404. class JsapiTestsCommand(MachCommandBase):
  405. @Command('jsapi-tests', category='testing', description='Run jsapi tests (JavaScript engine).')
  406. @CommandArgument('test_name', nargs='?', metavar='N',
  407. help='Test to run. Can be a prefix or omitted. If omitted, the entire ' \
  408. 'test suite is executed.')
  409. def run_jsapitests(self, **params):
  410. import subprocess
  411. bin_suffix = ''
  412. if sys.platform.startswith('win'):
  413. bin_suffix = '.exe'
  414. print('running jsapi-tests')
  415. jsapi_tests_cmd = [os.path.join(self.bindir, executable_name('jsapi-tests'))]
  416. if params['test_name']:
  417. jsapi_tests_cmd.append(params['test_name'])
  418. jsapi_tests_result = subprocess.call(jsapi_tests_cmd)
  419. return jsapi_tests_result
  420. def autotry_parser():
  421. from autotry import arg_parser
  422. return arg_parser()
  423. @CommandProvider
  424. class PushToTry(MachCommandBase):
  425. def normalise_list(self, items, allow_subitems=False):
  426. from autotry import parse_arg
  427. rv = defaultdict(list)
  428. for item in items:
  429. parsed = parse_arg(item)
  430. for key, values in parsed.iteritems():
  431. rv[key].extend(values)
  432. if not allow_subitems:
  433. if not all(item == [] for item in rv.itervalues()):
  434. raise ValueError("Unexpected subitems in argument")
  435. return rv.keys()
  436. else:
  437. return rv
  438. def validate_args(self, **kwargs):
  439. from autotry import AutoTry
  440. if not kwargs["paths"] and not kwargs["tests"] and not kwargs["tags"]:
  441. print("Paths, tags, or tests must be specified as an argument to autotry.")
  442. sys.exit(1)
  443. if kwargs["platforms"] is None:
  444. if 'AUTOTRY_PLATFORM_HINT' in os.environ:
  445. kwargs["platforms"] = [os.environ['AUTOTRY_PLATFORM_HINT']]
  446. else:
  447. print("Platforms must be specified as an argument to autotry.")
  448. sys.exit(1)
  449. try:
  450. platforms = self.normalise_list(kwargs["platforms"])
  451. except ValueError as e:
  452. print("Error parsing -p argument:\n%s" % e.message)
  453. sys.exit(1)
  454. try:
  455. tests = (self.normalise_list(kwargs["tests"], allow_subitems=True)
  456. if kwargs["tests"] else {})
  457. except ValueError as e:
  458. print("Error parsing -u argument (%s):\n%s" % (kwargs["tests"], e.message))
  459. sys.exit(1)
  460. try:
  461. talos = (self.normalise_list(kwargs["talos"], allow_subitems=True)
  462. if kwargs["talos"] else [])
  463. except ValueError as e:
  464. print("Error parsing -t argument:\n%s" % e.message)
  465. sys.exit(1)
  466. paths = []
  467. for p in kwargs["paths"]:
  468. p = mozpath.normpath(os.path.abspath(p))
  469. if not (os.path.isdir(p) and p.startswith(self.topsrcdir)):
  470. print('Specified path "%s" is not a directory under the srcdir,'
  471. ' unable to specify tests outside of the srcdir' % p)
  472. sys.exit(1)
  473. if len(p) <= len(self.topsrcdir):
  474. print('Specified path "%s" is at the top of the srcdir and would'
  475. ' select all tests.' % p)
  476. sys.exit(1)
  477. paths.append(os.path.relpath(p, self.topsrcdir))
  478. try:
  479. tags = self.normalise_list(kwargs["tags"]) if kwargs["tags"] else []
  480. except ValueError as e:
  481. print("Error parsing --tags argument:\n%s" % e.message)
  482. sys.exit(1)
  483. extra_values = {k['dest'] for k in AutoTry.pass_through_arguments.values()}
  484. extra_args = {k: v for k, v in kwargs.items()
  485. if k in extra_values and v}
  486. return kwargs["builds"], platforms, tests, talos, paths, tags, extra_args
  487. @Command('try',
  488. category='testing',
  489. description='Push selected tests to the try server',
  490. parser=autotry_parser)
  491. def autotry(self, **kwargs):
  492. """Autotry is in beta, please file bugs blocking 1149670.
  493. Push the current tree to try, with the specified syntax.
  494. Build options, platforms and regression tests may be selected
  495. using the usual try options (-b, -p and -u respectively). In
  496. addition, tests in a given directory may be automatically
  497. selected by passing that directory as a positional argument to the
  498. command. For example:
  499. mach try -b d -p linux64 dom testing/web-platform/tests/dom
  500. would schedule a try run for linux64 debug consisting of all
  501. tests under dom/ and testing/web-platform/tests/dom.
  502. Test selection using positional arguments is available for
  503. mochitests, reftests, xpcshell tests and web-platform-tests.
  504. Tests may be also filtered by passing --tag to the command,
  505. which will run only tests marked as having the specified
  506. tags e.g.
  507. mach try -b d -p win64 --tag media
  508. would run all tests tagged 'media' on Windows 64.
  509. If both positional arguments or tags and -u are supplied, the
  510. suites in -u will be run in full. Where tests are selected by
  511. positional argument they will be run in a single chunk.
  512. If no build option is selected, both debug and opt will be
  513. scheduled. If no platform is selected a default is taken from
  514. the AUTOTRY_PLATFORM_HINT environment variable, if set.
  515. The command requires either its own mercurial extension ("push-to-try",
  516. installable from mach mercurial-setup) or a git repo using git-cinnabar
  517. (available at https://github.com/glandium/git-cinnabar).
  518. """
  519. from mozbuild.testing import TestResolver
  520. from autotry import AutoTry
  521. print("mach try is under development, please file bugs blocking 1149670.")
  522. resolver_func = lambda: self._spawn(TestResolver)
  523. at = AutoTry(self.topsrcdir, resolver_func, self._mach_context)
  524. if kwargs["list"]:
  525. at.list_presets()
  526. sys.exit()
  527. if kwargs["load"] is not None:
  528. defaults = at.load_config(kwargs["load"])
  529. if defaults is None:
  530. print("No saved configuration called %s found in autotry.ini" % kwargs["load"],
  531. file=sys.stderr)
  532. for key, value in kwargs.iteritems():
  533. if value in (None, []) and key in defaults:
  534. kwargs[key] = defaults[key]
  535. if kwargs["push"] and at.find_uncommited_changes():
  536. print('ERROR please commit changes before continuing')
  537. sys.exit(1)
  538. if not any(kwargs[item] for item in ("paths", "tests", "tags")):
  539. kwargs["paths"], kwargs["tags"] = at.find_paths_and_tags(kwargs["verbose"])
  540. builds, platforms, tests, talos, paths, tags, extra = self.validate_args(**kwargs)
  541. if paths or tags:
  542. paths = [os.path.relpath(os.path.normpath(os.path.abspath(item)), self.topsrcdir)
  543. for item in paths]
  544. paths_by_flavor = at.paths_by_flavor(paths=paths, tags=tags)
  545. if not paths_by_flavor and not tests:
  546. print("No tests were found when attempting to resolve paths:\n\n\t%s" %
  547. paths)
  548. sys.exit(1)
  549. if not kwargs["intersection"]:
  550. paths_by_flavor = at.remove_duplicates(paths_by_flavor, tests)
  551. else:
  552. paths_by_flavor = {}
  553. try:
  554. msg = at.calc_try_syntax(platforms, tests, talos, builds, paths_by_flavor, tags,
  555. extra, kwargs["intersection"])
  556. except ValueError as e:
  557. print(e.message)
  558. sys.exit(1)
  559. if kwargs["verbose"] and paths_by_flavor:
  560. print('The following tests will be selected: ')
  561. for flavor, paths in paths_by_flavor.iteritems():
  562. print("%s: %s" % (flavor, ",".join(paths)))
  563. if kwargs["verbose"] or not kwargs["push"]:
  564. print('The following try syntax was calculated:\n%s' % msg)
  565. if kwargs["push"]:
  566. at.push_to_try(msg, kwargs["verbose"])
  567. if kwargs["save"] is not None:
  568. at.save_config(kwargs["save"], msg)
  569. def get_parser(argv=None):
  570. parser = ArgumentParser()
  571. parser.add_argument(dest="suite_name",
  572. nargs=1,
  573. choices=['mochitest'],
  574. type=str,
  575. help="The test for which chunk should be found. It corresponds "
  576. "to the mach test invoked (only 'mochitest' currently).")
  577. parser.add_argument(dest="test_path",
  578. nargs=1,
  579. type=str,
  580. help="The test (any mochitest) for which chunk should be found.")
  581. parser.add_argument('--total-chunks',
  582. type=int,
  583. dest='total_chunks',
  584. required=True,
  585. help='Total number of chunks to split tests into.',
  586. default=None)
  587. parser.add_argument('--chunk-by-runtime',
  588. action='store_true',
  589. dest='chunk_by_runtime',
  590. help='Group tests such that each chunk has roughly the same runtime.',
  591. default=False)
  592. parser.add_argument('--chunk-by-dir',
  593. type=int,
  594. dest='chunk_by_dir',
  595. help='Group tests together in the same chunk that are in the same top '
  596. 'chunkByDir directories.',
  597. default=None)
  598. parser.add_argument('--disable-e10s',
  599. action='store_false',
  600. dest='e10s',
  601. help='Find test on chunk with electrolysis preferences disabled.',
  602. default=True)
  603. parser.add_argument('-p', '--platform',
  604. choices=['linux', 'linux64', 'mac', 'macosx64', 'win32', 'win64'],
  605. dest='platform',
  606. help="Platform for the chunk to find the test.",
  607. default=None)
  608. parser.add_argument('--debug',
  609. action='store_true',
  610. dest='debug',
  611. help="Find the test on chunk in a debug build.",
  612. default=False)
  613. return parser
  614. def download_mozinfo(platform=None, debug_build=False):
  615. temp_dir = tempfile.mkdtemp()
  616. temp_path = os.path.join(temp_dir, "mozinfo.json")
  617. args = [
  618. 'mozdownload',
  619. '-t', 'tinderbox',
  620. '--ext', 'mozinfo.json',
  621. '-d', temp_path,
  622. ]
  623. if platform:
  624. if platform == 'macosx64':
  625. platform = 'mac64'
  626. args.extend(['-p', platform])
  627. if debug_build:
  628. args.extend(['--debug-build'])
  629. subprocess.call(args)
  630. return temp_dir, temp_path
  631. @CommandProvider
  632. class ChunkFinder(MachCommandBase):
  633. @Command('find-test-chunk', category='testing',
  634. description='Find which chunk a test belongs to (works for mochitest).',
  635. parser=get_parser)
  636. def chunk_finder(self, **kwargs):
  637. total_chunks = kwargs['total_chunks']
  638. test_path = kwargs['test_path'][0]
  639. suite_name = kwargs['suite_name'][0]
  640. _, dump_tests = tempfile.mkstemp()
  641. from mozbuild.testing import TestResolver
  642. resolver = self._spawn(TestResolver)
  643. relpath = self._wrap_path_argument(test_path).relpath()
  644. tests = list(resolver.resolve_tests(paths=[relpath]))
  645. if len(tests) != 1:
  646. print('No test found for test_path: %s' % test_path)
  647. sys.exit(1)
  648. flavor = tests[0]['flavor']
  649. subsuite = tests[0]['subsuite']
  650. args = {
  651. 'totalChunks': total_chunks,
  652. 'dump_tests': dump_tests,
  653. 'chunkByDir': kwargs['chunk_by_dir'],
  654. 'chunkByRuntime': kwargs['chunk_by_runtime'],
  655. 'e10s': kwargs['e10s'],
  656. 'subsuite': subsuite,
  657. }
  658. temp_dir = None
  659. if kwargs['platform'] or kwargs['debug']:
  660. self._activate_virtualenv()
  661. self.virtualenv_manager.install_pip_package('mozdownload==1.17')
  662. temp_dir, temp_path = download_mozinfo(kwargs['platform'], kwargs['debug'])
  663. args['extra_mozinfo_json'] = temp_path
  664. found = False
  665. for this_chunk in range(1, total_chunks+1):
  666. args['thisChunk'] = this_chunk
  667. try:
  668. self._mach_context.commands.dispatch(suite_name, self._mach_context, flavor=flavor, resolve_tests=False, **args)
  669. except SystemExit:
  670. pass
  671. except KeyboardInterrupt:
  672. break
  673. fp = open(os.path.expanduser(args['dump_tests']), 'r')
  674. tests = json.loads(fp.read())['active_tests']
  675. for test in tests:
  676. if test_path == test['path']:
  677. if 'disabled' in test:
  678. print('The test %s for flavor %s is disabled on the given platform' % (test_path, flavor))
  679. else:
  680. print('The test %s for flavor %s is present in chunk number: %d' % (test_path, flavor, this_chunk))
  681. found = True
  682. break
  683. if found:
  684. break
  685. if not found:
  686. raise Exception("Test %s not found." % test_path)
  687. # Clean up the file
  688. os.remove(dump_tests)
  689. if temp_dir:
  690. shutil.rmtree(temp_dir)