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"""
5These transforms construct a task description to run the given test, based on a
6test description.  The implementation here is shared among all test kinds, but
7contains specific support for how we run tests in Gecko (via mozharness,
8invoked in particular ways).
9
10This is a good place to translate a test-description option such as
11`single-core: true` to the implementation of that option in a task description
12(worker options, mozharness commandline, environment variables, etc.)
13
14The test description should be fully formed by the time it reaches these
15transforms, and these transforms should not embody any specific knowledge about
16what should run where. this is the wrong place for special-casing platforms,
17for example - use `all_tests.py` instead.
18"""
19
20from __future__ import absolute_import, print_function, unicode_literals
21
22from taskgraph.transforms.base import TransformSequence
23from taskgraph.util.schema import resolve_keyed_by, OptimizationSchema
24from taskgraph.util.treeherder import split_symbol, join_symbol, add_suffix
25from taskgraph.util.platforms import platform_family
26from taskgraph.util.schema import (
27    optionally_keyed_by,
28    Schema,
29)
30from taskgraph.util.taskcluster import get_artifact_path
31from mozbuild.schedules import INCLUSIVE_COMPONENTS
32
33from voluptuous import (
34    Any,
35    Optional,
36    Required,
37    Exclusive,
38)
39
40import copy
41import logging
42
43# default worker types keyed by instance-size
44LINUX_WORKER_TYPES = {
45    'large': 'aws-provisioner-v1/gecko-t-linux-large',
46    'xlarge': 'aws-provisioner-v1/gecko-t-linux-xlarge',
47    'default': 'aws-provisioner-v1/gecko-t-linux-large',
48}
49
50# windows worker types keyed by test-platform and virtualization
51WINDOWS_WORKER_TYPES = {
52    'windows7-32': {
53      'virtual': 'aws-provisioner-v1/gecko-t-win7-32',
54      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win7-32-gpu',
55      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
56    },
57    'windows7-32-pgo': {
58      'virtual': 'aws-provisioner-v1/gecko-t-win7-32',
59      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win7-32-gpu',
60      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
61    },
62    'windows7-32-nightly': {
63      'virtual': 'aws-provisioner-v1/gecko-t-win7-32',
64      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win7-32-gpu',
65      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
66    },
67    'windows7-32-devedition': {
68      'virtual': 'aws-provisioner-v1/gecko-t-win7-32',
69      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win7-32-gpu',
70      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
71    },
72    'windows10-64': {
73      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
74      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
75      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
76    },
77    'windows10-64-ccov': {
78      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
79      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
80      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
81    },
82    'windows10-64-pgo': {
83      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
84      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
85      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
86    },
87    'windows10-64-devedition': {
88      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
89      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
90      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
91    },
92    'windows10-64-nightly': {
93      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
94      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
95      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
96    },
97    'windows10-64-asan': {
98      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
99      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
100      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
101    },
102    'windows10-64-qr': {
103      'virtual': 'aws-provisioner-v1/gecko-t-win10-64',
104      'virtual-with-gpu': 'aws-provisioner-v1/gecko-t-win10-64-gpu',
105      'hardware': 'releng-hardware/gecko-t-win10-64-hw',
106    },
107}
108
109# os x worker types keyed by test-platform
110MACOSX_WORKER_TYPES = {
111    'macosx64': 'releng-hardware/gecko-t-osx-1010',
112}
113
114logger = logging.getLogger(__name__)
115
116transforms = TransformSequence()
117
118# Schema for a test description
119#
120# *****WARNING*****
121#
122# This is a great place for baffling cruft to accumulate, and that makes
123# everyone move more slowly.  Be considerate of your fellow hackers!
124# See the warnings in taskcluster/docs/how-tos.rst
125#
126# *****WARNING*****
127test_description_schema = Schema({
128    # description of the suite, for the task metadata
129    'description': basestring,
130
131    # test suite name, or <suite>/<flavor>
132    Required('suite'): optionally_keyed_by(
133        'test-platform',
134        basestring),
135
136    # the name by which this test suite is addressed in try syntax; defaults to
137    # the test-name.  This will translate to the `unittest_try_name` or
138    # `talos_try_name` attribute.
139    Optional('try-name'): basestring,
140
141    # additional tags to mark up this type of test
142    Optional('tags'): {basestring: object},
143
144    # the symbol, or group(symbol), under which this task should appear in
145    # treeherder.
146    'treeherder-symbol': basestring,
147
148    # the value to place in task.extra.treeherder.machine.platform; ideally
149    # this is the same as build-platform, and that is the default, but in
150    # practice it's not always a match.
151    Optional('treeherder-machine-platform'): basestring,
152
153    # attributes to appear in the resulting task (later transforms will add the
154    # common attributes)
155    Optional('attributes'): {basestring: object},
156
157    # relative path (from config.path) to the file task was defined in
158    Optional('job-from'): basestring,
159
160    # The `run_on_projects` attribute, defaulting to "all".  This dictates the
161    # projects on which this task should be included in the target task set.
162    # See the attributes documentation for details.
163    #
164    # Note that the special case 'built-projects', the default, uses the parent
165    # build task's run-on-projects, meaning that tests run only on platforms
166    # that are built.
167    Optional('run-on-projects'): optionally_keyed_by(
168        'test-platform',
169        Any([basestring], 'built-projects')),
170
171    # the sheriffing tier for this task (default: set based on test platform)
172    Optional('tier'): optionally_keyed_by(
173        'test-platform',
174        Any(int, 'default')),
175
176    # number of chunks to create for this task.  This can be keyed by test
177    # platform by passing a dictionary in the `by-test-platform` key.  If the
178    # test platform is not found, the key 'default' will be tried.
179    Required('chunks'): optionally_keyed_by(
180        'test-platform',
181        int),
182
183    # the time (with unit) after which this task is deleted; default depends on
184    # the branch (see below)
185    Optional('expires-after'): basestring,
186
187    # Whether to run this task with e10s (desktop-test only).  If false, run
188    # without e10s; if true, run with e10s; if 'both', run one task with and
189    # one task without e10s.  E10s tasks have "-e10s" appended to the test name
190    # and treeherder group.
191    Required('e10s'): optionally_keyed_by(
192        'test-platform', 'project',
193        Any(bool, 'both')),
194
195    # Whether the task should run with WebRender enabled or not.
196    Optional('webrender'): bool,
197
198    # The EC2 instance size to run these tests on.
199    Required('instance-size'): optionally_keyed_by(
200        'test-platform',
201        Any('default', 'large', 'xlarge')),
202
203    # type of virtualization or hardware required by test.
204    Required('virtualization'): optionally_keyed_by(
205        'test-platform',
206        Any('virtual', 'virtual-with-gpu', 'hardware')),
207
208    # Whether the task requires loopback audio or video (whatever that may mean
209    # on the platform)
210    Required('loopback-audio'): bool,
211    Required('loopback-video'): bool,
212
213    # Whether the test can run using a software GL implementation on Linux
214    # using the GL compositor. May not be used with "legacy" sized instances
215    # due to poor LLVMPipe performance (bug 1296086).  Defaults to true for
216    # unit tests on linux platforms and false otherwise
217    Optional('allow-software-gl-layers'): bool,
218
219    # For tasks that will run in docker-worker or docker-engine, this is the
220    # name of the docker image or in-tree docker image to run the task in.  If
221    # in-tree, then a dependency will be created automatically.  This is
222    # generally `desktop-test`, or an image that acts an awful lot like it.
223    Required('docker-image'): optionally_keyed_by(
224        'test-platform',
225        Any(
226            # a raw Docker image path (repo/image:tag)
227            basestring,
228            # an in-tree generated docker image (from `taskcluster/docker/<name>`)
229            {'in-tree': basestring},
230            # an indexed docker image
231            {'indexed': basestring},
232        )
233    ),
234
235    # seconds of runtime after which the task will be killed.  Like 'chunks',
236    # this can be keyed by test pltaform.
237    Required('max-run-time'): optionally_keyed_by(
238        'test-platform',
239        int),
240
241    # the exit status code that indicates the task should be retried
242    Optional('retry-exit-status'): [int],
243
244    # Whether to perform a gecko checkout.
245    Required('checkout'): bool,
246
247    # Wheter to perform a machine reboot after test is done
248    Optional('reboot'):
249        Any(False, 'always', 'on-exception', 'on-failure'),
250
251    # What to run
252    Required('mozharness'): {
253        # the mozharness script used to run this task
254        Required('script'): optionally_keyed_by(
255            'test-platform',
256            basestring),
257
258        # the config files required for the task
259        Required('config'): optionally_keyed_by(
260            'test-platform',
261            [basestring]),
262
263        # mochitest flavor for mochitest runs
264        Optional('mochitest-flavor'): basestring,
265
266        # any additional actions to pass to the mozharness command
267        Optional('actions'): [basestring],
268
269        # additional command-line options for mozharness, beyond those
270        # automatically added
271        Required('extra-options'): optionally_keyed_by(
272            'test-platform',
273            [basestring]),
274
275        # the artifact name (including path) to test on the build task; this is
276        # generally set in a per-kind transformation
277        Optional('build-artifact-name'): basestring,
278
279        # If true, tooltool downloads will be enabled via relengAPIProxy.
280        Required('tooltool-downloads'): bool,
281
282        # Add --blob-upload-branch=<project> mozharness parameter
283        Optional('include-blob-upload-branch'): bool,
284
285        # The setting for --download-symbols (if omitted, the option will not
286        # be passed to mozharness)
287        Optional('download-symbols'): Any(True, 'ondemand'),
288
289        # If set, then MOZ_NODE_PATH=/usr/local/bin/node is included in the
290        # environment.  This is more than just a helpful path setting -- it
291        # causes xpcshell tests to start additional servers, and runs
292        # additional tests.
293        Required('set-moz-node-path'): bool,
294
295        # If true, include chunking information in the command even if the number
296        # of chunks is 1
297        Required('chunked'): optionally_keyed_by(
298            'test-platform',
299            bool),
300
301        # The chunking argument format to use
302        Required('chunking-args'): Any(
303            # Use the usual --this-chunk/--total-chunk arguments
304            'this-chunk',
305            # Use --test-suite=<suite>-<chunk-suffix>; see chunk-suffix, below
306            'test-suite-suffix',
307        ),
308
309        # the string to append to the `--test-suite` arugment when
310        # chunking-args = test-suite-suffix; "<CHUNK>" in this string will
311        # be replaced with the chunk number.
312        Optional('chunk-suffix'): basestring,
313
314        Required('requires-signed-builds'): optionally_keyed_by(
315            'test-platform',
316            bool),
317    },
318
319    # The current chunk; this is filled in by `all_kinds.py`
320    Optional('this-chunk'): int,
321
322    # os user groups for test task workers; required scopes, will be
323    # added automatically
324    Optional('os-groups'): optionally_keyed_by(
325        'test-platform',
326        [basestring]),
327
328    Optional('run-as-administrator'): optionally_keyed_by(
329        'test-platform',
330        bool),
331
332    # -- values supplied by the task-generation infrastructure
333
334    # the platform of the build this task is testing
335    'build-platform': basestring,
336
337    # the label of the build task generating the materials to test
338    'build-label': basestring,
339
340    # the label of the signing task generating the materials to test.
341    # Signed builds are used in xpcshell tests on Windows, for instance.
342    Optional('build-signing-label'): basestring,
343
344    # the build's attributes
345    'build-attributes': {basestring: object},
346
347    # the platform on which the tests will run
348    'test-platform': basestring,
349
350    # the name of the test (the key in tests.yml)
351    'test-name': basestring,
352
353    # the product name, defaults to firefox
354    Optional('product'): basestring,
355
356    # conditional files to determine when these tests should be run
357    Exclusive(Optional('when'), 'optimization'): {
358        Optional('files-changed'): [basestring],
359    },
360
361    # Optimization to perform on this task during the optimization phase.
362    # Optimizations are defined in taskcluster/taskgraph/optimize.py.
363    Exclusive(Optional('optimization'), 'optimization'): OptimizationSchema,
364
365    # The SCHEDULES component for this task; this defaults to the suite
366    # (not including the flavor) but can be overridden here.
367    Exclusive(Optional('schedules-component'), 'optimization'): basestring,
368
369    Optional('worker-type'): optionally_keyed_by(
370        'test-platform',
371        Any(basestring, None),
372    ),
373
374    Optional(
375        'require-signed-extensions',
376        description="Whether the build being tested requires extensions be signed.",
377    ): optionally_keyed_by('release-type', 'test-platform', bool),
378
379    # The target name, specifying the build artifact to be tested.
380    # If None or not specified, a transform sets the target based on OS:
381    # target.dmg (Mac), target.apk (Android), target.tar.bz2 (Linux),
382    # or target.zip (Windows).
383    Optional('target'): optionally_keyed_by(
384        'test-platform',
385        Any(basestring, None),
386    ),
387
388    # A list of artifacts to install from 'fetch' tasks.
389    Optional('fetches'): {
390        basestring: [basestring],
391    },
392}, required=True)
393
394
395@transforms.add
396def handle_keyed_by_mozharness(config, tests):
397    """Resolve a mozharness field if it is keyed by something"""
398    for test in tests:
399        resolve_keyed_by(test, 'mozharness', item_name=test['test-name'])
400        yield test
401
402
403@transforms.add
404def set_defaults(config, tests):
405    for test in tests:
406        build_platform = test['build-platform']
407        if build_platform.startswith('android'):
408            # all Android test tasks download internal objects from tooltool
409            test['mozharness']['tooltool-downloads'] = True
410            test['mozharness']['actions'] = ['get-secrets']
411            # Android doesn't do e10s
412            test['e10s'] = False
413            # loopback-video is always true for Android, but false for other
414            # platform phyla
415            test['loopback-video'] = True
416        else:
417            # all non-android tests want to run the bits that require node
418            test['mozharness']['set-moz-node-path'] = True
419            test.setdefault('e10s', True)
420
421        # software-gl-layers is only meaningful on linux unittests, where it defaults to True
422        if test['test-platform'].startswith('linux') and test['suite'] != 'talos':
423            test.setdefault('allow-software-gl-layers', True)
424        else:
425            test['allow-software-gl-layers'] = False
426
427        # Enable WebRender by default on the QuantumRender test platforms, since
428        # the whole point of QuantumRender is to run with WebRender enabled.
429        # This currently matches linux64-qr and windows10-64-qr; both of these
430        # have /opt and /debug variants.
431        if "-qr/" in test['test-platform']:
432            test['webrender'] = True
433        else:
434            test.setdefault('webrender', False)
435
436        test.setdefault('try-name', test['test-name'])
437
438        test.setdefault('os-groups', [])
439        test.setdefault('run-as-administrator', False)
440        test.setdefault('chunks', 1)
441        test.setdefault('run-on-projects', 'built-projects')
442        test.setdefault('instance-size', 'default')
443        test.setdefault('max-run-time', 3600)
444        test.setdefault('reboot', False)
445        test.setdefault('virtualization', 'virtual')
446        test.setdefault('loopback-audio', False)
447        test.setdefault('loopback-video', False)
448        test.setdefault('docker-image', {'in-tree': 'desktop1604-test'})
449        test.setdefault('checkout', False)
450        test.setdefault('require-signed-extensions', False)
451
452        test['mozharness'].setdefault('extra-options', [])
453        test['mozharness'].setdefault('requires-signed-builds', False)
454        test['mozharness'].setdefault('tooltool-downloads', False)
455        test['mozharness'].setdefault('set-moz-node-path', False)
456        test['mozharness'].setdefault('chunked', False)
457        test['mozharness'].setdefault('chunking-args', 'this-chunk')
458        yield test
459
460
461transforms.add_validate(test_description_schema)
462
463
464@transforms.add
465def resolve_keys(config, tests):
466    for test in tests:
467        resolve_keyed_by(
468            test, 'require-signed-extensions',
469            item_name=test['test-name'],
470            **{
471                'release-type': config.params['release_type'],
472            }
473        )
474        yield test
475
476
477@transforms.add
478def setup_talos(config, tests):
479    """Add options that are specific to talos jobs (identified by suite=talos)"""
480    for test in tests:
481        if test['suite'] != 'talos':
482            yield test
483            continue
484
485        extra_options = test.setdefault('mozharness', {}).setdefault('extra-options', [])
486        extra_options.append('--use-talos-json')
487        # win7 needs to test skip
488        if test['build-platform'].startswith('win32'):
489            extra_options.append('--add-option')
490            extra_options.append('--setpref,gfx.direct2d.disabled=true')
491
492        yield test
493
494
495@transforms.add
496def handle_artifact_prefix(config, tests):
497    """Handle translating `artifact_prefix` appropriately"""
498    for test in tests:
499        if test['build-attributes'].get('artifact_prefix'):
500            test.setdefault("attributes", {}).setdefault(
501                'artifact_prefix', test['build-attributes']['artifact_prefix']
502            )
503        yield test
504
505
506@transforms.add
507def set_target(config, tests):
508    for test in tests:
509        build_platform = test['build-platform']
510        target = None
511        if 'target' in test:
512            resolve_keyed_by(test, 'target', item_name=test['test-name'])
513            target = test['target']
514        if not target:
515            if build_platform.startswith('macosx'):
516                target = 'target.dmg'
517            elif build_platform.startswith('android'):
518                target = 'target.apk'
519            elif build_platform.startswith('win'):
520                target = 'target.zip'
521            else:
522                target = 'target.tar.bz2'
523        test['mozharness']['build-artifact-name'] = get_artifact_path(test, target)
524
525        yield test
526
527
528@transforms.add
529def set_treeherder_machine_platform(config, tests):
530    """Set the appropriate task.extra.treeherder.machine.platform"""
531    translation = {
532        # Linux64 build platforms for asan and pgo are specified differently to
533        # treeherder.
534        'linux64-asan/opt': 'linux64/asan',
535        'linux64-pgo/opt': 'linux64/pgo',
536        'macosx64/debug': 'osx-10-10/debug',
537        'macosx64/opt': 'osx-10-10/opt',
538        'win64-asan/opt': 'windows10-64/asan',
539        'win32-pgo/opt': 'windows7-32/pgo',
540        'win64-pgo/opt': 'windows10-64/pgo',
541        # The build names for Android platforms have partially evolved over the
542        # years and need to be translated.
543        'android-api-16/debug': 'android-4-3-armv7-api16/debug',
544        'android-api-16/opt': 'android-4-3-armv7-api16/opt',
545        'android-x86/opt': 'android-4-2-x86/opt',
546        'android-api-16-gradle/opt': 'android-api-16-gradle/opt',
547    }
548    for test in tests:
549        # For most desktop platforms, the above table is not used for "regular"
550        # builds, so we'll always pick the test platform here.
551        # On macOS though, the regular builds are in the table.  This causes a
552        # conflict in `verify_task_graph_symbol` once you add a new test
553        # platform based on regular macOS builds, such as for Stylo.
554        # Since it's unclear if the regular macOS builds can be removed from
555        # the table, workaround the issue for Stylo.
556        test['treeherder-machine-platform'] = translation.get(
557            test['build-platform'], test['test-platform'])
558        yield test
559
560
561@transforms.add
562def set_tier(config, tests):
563    """Set the tier based on policy for all test descriptions that do not
564    specify a tier otherwise."""
565    for test in tests:
566        if 'tier' in test:
567            resolve_keyed_by(test, 'tier', item_name=test['test-name'])
568
569        # only override if not set for the test
570        if 'tier' not in test or test['tier'] == 'default':
571            if test['test-platform'] in ['linux32/opt',
572                                         'linux32/debug',
573                                         'linux32-nightly/opt',
574                                         'linux32-devedition/opt',
575                                         'linux64/opt',
576                                         'linux64-nightly/opt',
577                                         'linux64/debug',
578                                         'linux64-pgo/opt',
579                                         'linux64-devedition/opt',
580                                         'linux64-asan/opt',
581                                         'windows7-32/debug',
582                                         'windows7-32/opt',
583                                         'windows7-32-pgo/opt',
584                                         'windows7-32-devedition/opt',
585                                         'windows7-32-nightly/opt',
586                                         'windows10-64/debug',
587                                         'windows10-64/opt',
588                                         'windows10-64-pgo/opt',
589                                         'windows10-64-devedition/opt',
590                                         'windows10-64-nightly/opt',
591                                         'macosx64/opt',
592                                         'macosx64/debug',
593                                         'macosx64-nightly/opt',
594                                         'macosx64-devedition/opt',
595                                         'android-4.3-arm7-api-16/opt',
596                                         'android-4.3-arm7-api-16/debug',
597                                         'android-4.2-x86/opt']:
598                test['tier'] = 1
599            else:
600                test['tier'] = 2
601
602        yield test
603
604
605@transforms.add
606def set_expires_after(config, tests):
607    """Try jobs expire after 2 weeks; everything else lasts 1 year.  This helps
608    keep storage costs low."""
609    for test in tests:
610        if 'expires-after' not in test:
611            if config.params.is_try():
612                test['expires-after'] = "14 days"
613            else:
614                test['expires-after'] = "1 year"
615        yield test
616
617
618@transforms.add
619def set_download_symbols(config, tests):
620    """In general, we download symbols immediately for debug builds, but only
621    on demand for everything else. ASAN builds shouldn't download
622    symbols since they don't product symbol zips see bug 1283879"""
623    for test in tests:
624        if test['test-platform'].split('/')[-1] == 'debug':
625            test['mozharness']['download-symbols'] = True
626        elif test['build-platform'] == 'linux64-asan/opt' or \
627                test['build-platform'] == 'windows10-64-asan/opt':
628            if 'download-symbols' in test['mozharness']:
629                del test['mozharness']['download-symbols']
630        else:
631            test['mozharness']['download-symbols'] = 'ondemand'
632        yield test
633
634
635@transforms.add
636def handle_keyed_by(config, tests):
637    """Resolve fields that can be keyed by platform, etc."""
638    fields = [
639        'instance-size',
640        'docker-image',
641        'max-run-time',
642        'chunks',
643        'e10s',
644        'suite',
645        'run-on-projects',
646        'os-groups',
647        'run-as-administrator',
648        'mozharness.chunked',
649        'mozharness.config',
650        'mozharness.extra-options',
651        'mozharness.requires-signed-builds',
652        'mozharness.script',
653        'worker-type',
654        'virtualization',
655    ]
656    for test in tests:
657        for field in fields:
658            resolve_keyed_by(test, field, item_name=test['test-name'],
659                             project=config.params['project'])
660        yield test
661
662
663@transforms.add
664def handle_suite_category(config, tests):
665    for test in tests:
666        if '/' in test['suite']:
667            suite, flavor = test['suite'].split('/', 1)
668        else:
669            suite = flavor = test['suite']
670
671        test.setdefault('attributes', {})
672        test['attributes']['unittest_suite'] = suite
673        test['attributes']['unittest_flavor'] = flavor
674
675        script = test['mozharness']['script']
676        category_arg = None
677        if suite == 'test-verify':
678            pass
679        elif script == 'android_emulator_unittest.py':
680            category_arg = '--test-suite'
681        elif script == 'desktop_unittest.py':
682            category_arg = '--{}-suite'.format(suite)
683
684        if category_arg:
685            test['mozharness'].setdefault('extra-options', [])
686            extra = test['mozharness']['extra-options']
687            if not any(arg.startswith(category_arg) for arg in extra):
688                extra.append('{}={}'.format(category_arg, flavor))
689
690        yield test
691
692
693@transforms.add
694def enable_code_coverage(config, tests):
695    """Enable code coverage for the linux64-ccov/opt & linux64-jsdcov/opt & win64-ccov/debug
696    build-platforms"""
697    for test in tests:
698        if 'ccov' in test['build-platform'] and not test['test-name'].startswith('test-verify'):
699            test['mozharness'].setdefault('extra-options', []).append('--code-coverage')
700            test['instance-size'] = 'xlarge'
701            # Ensure we don't run on inbound/autoland/beta, but if the test is try only, ignore it
702            if 'mozilla-central' in test['run-on-projects'] or \
703                    test['run-on-projects'] == 'built-projects':
704                test['run-on-projects'] = ['mozilla-central', 'try']
705
706            # Ensure we don't optimize test suites out.
707            # We always want to run all test suites for coverage purposes.
708            test.pop('schedules-component', None)
709            test.pop('when', None)
710            test['optimization'] = None
711
712            if 'talos' in test['test-name']:
713                test['max-run-time'] = 7200
714                if 'linux' in test['build-platform']:
715                    test['docker-image'] = {"in-tree": "desktop1604-test"}
716                test['mozharness']['extra-options'].append('--add-option')
717                test['mozharness']['extra-options'].append('--cycles,1')
718                test['mozharness']['extra-options'].append('--add-option')
719                test['mozharness']['extra-options'].append('--tppagecycles,1')
720                test['mozharness']['extra-options'].append('--add-option')
721                test['mozharness']['extra-options'].append('--no-upload-results')
722                test['mozharness']['extra-options'].append('--add-option')
723                test['mozharness']['extra-options'].append('--tptimeout,15000')
724        elif 'jsdcov' in test['build-platform']:
725            # Ensure we don't run on inbound/autoland/beta, but if the test is try only, ignore it
726            if 'mozilla-central' in test['run-on-projects'] or \
727                    test['run-on-projects'] == 'built-projects':
728                test['run-on-projects'] = ['mozilla-central', 'try']
729            test['mozharness'].setdefault('extra-options', []).append('--jsd-code-coverage')
730        yield test
731
732
733@transforms.add
734def handle_run_on_projects(config, tests):
735    """Handle translating `built-projects` appropriately"""
736    for test in tests:
737        if test['run-on-projects'] == 'built-projects':
738            test['run-on-projects'] = test['build-attributes'].get('run_on_projects', ['all'])
739        yield test
740
741
742@transforms.add
743def split_e10s(config, tests):
744    for test in tests:
745        e10s = test['e10s']
746
747        test['e10s'] = False
748        test['attributes']['e10s'] = False
749
750        if e10s == 'both':
751            yield copy.deepcopy(test)
752            e10s = True
753        if e10s:
754            test['test-name'] += '-e10s'
755            test['try-name'] += '-e10s'
756            test['e10s'] = True
757            test['attributes']['e10s'] = True
758            group, symbol = split_symbol(test['treeherder-symbol'])
759            if group != '?':
760                group += '-e10s'
761            test['treeherder-symbol'] = join_symbol(group, symbol)
762            if test['suite'] == 'talos':
763                for i, option in enumerate(test['mozharness']['extra-options']):
764                    if option.startswith('--suite='):
765                        test['mozharness']['extra-options'][i] += '-e10s'
766            else:
767                test['mozharness']['extra-options'].append('--e10s')
768        yield test
769
770
771@transforms.add
772def split_chunks(config, tests):
773    """Based on the 'chunks' key, split tests up into chunks by duplicating
774    them and assigning 'this-chunk' appropriately and updating the treeherder
775    symbol."""
776    for test in tests:
777        if test['chunks'] == 1:
778            test['this-chunk'] = 1
779            yield test
780            continue
781
782        for this_chunk in range(1, test['chunks'] + 1):
783            # copy the test and update with the chunk number
784            chunked = copy.deepcopy(test)
785            chunked['this-chunk'] = this_chunk
786
787            # add the chunk number to the TH symbol
788            chunked['treeherder-symbol'] = add_suffix(
789                chunked['treeherder-symbol'], this_chunk)
790
791            yield chunked
792
793
794@transforms.add
795def allow_software_gl_layers(config, tests):
796    """
797    Handle the "allow-software-gl-layers" property for platforms where it
798    applies.
799    """
800    for test in tests:
801        if test.get('allow-software-gl-layers'):
802            # This should be set always once bug 1296086 is resolved.
803            test['mozharness'].setdefault('extra-options', [])\
804                              .append("--allow-software-gl-layers")
805
806        yield test
807
808
809@transforms.add
810def enable_webrender(config, tests):
811    """
812    Handle the "webrender" property by passing a flag to mozharness if it is
813    enabled.
814    """
815    for test in tests:
816        if test.get('webrender'):
817            test['mozharness'].setdefault('extra-options', [])\
818                              .append("--enable-webrender")
819
820        yield test
821
822
823@transforms.add
824def set_retry_exit_status(config, tests):
825    """Set the retry exit status to TBPL_RETRY, the value returned by mozharness
826       scripts to indicate a transient failure that should be retried."""
827    for test in tests:
828        test['retry-exit-status'] = [4]
829        yield test
830
831
832@transforms.add
833def set_profile(config, tests):
834    """Set profiling mode for tests."""
835    profile = None
836    if config.params['try_mode'] == 'try_option_syntax':
837        profile = config.params['try_options']['profile']
838    for test in tests:
839        if profile and test['suite'] == 'talos':
840            test['mozharness']['extra-options'].append('--geckoProfile')
841        yield test
842
843
844@transforms.add
845def set_tag(config, tests):
846    """Set test for a specific tag."""
847    tag = None
848    if config.params['try_mode'] == 'try_option_syntax':
849        tag = config.params['try_options']['tag']
850    for test in tests:
851        if tag:
852            test['mozharness']['extra-options'].extend(['--tag', tag])
853        yield test
854
855
856@transforms.add
857def set_test_type(config, tests):
858    for test in tests:
859        for test_type in ['mochitest', 'reftest']:
860            if test_type in test['suite'] and 'web-platform' not in test['suite']:
861                test.setdefault('tags', {})['test-type'] = test_type
862        yield test
863
864
865@transforms.add
866def single_stylo_traversal_tests(config, tests):
867    """Enable single traversal for all tests on the sequential Stylo platform."""
868
869    for test in tests:
870        if not test['test-platform'].startswith('linux64-stylo-sequential/'):
871            yield test
872            continue
873
874        # Bug 1356122 - Run Stylo tests in sequential mode
875        test['mozharness'].setdefault('extra-options', [])\
876                          .append('--single-stylo-traversal')
877        yield test
878
879
880@transforms.add
881def set_worker_type(config, tests):
882    """Set the worker type based on the test platform."""
883    for test in tests:
884        # during the taskcluster migration, this is a bit tortured, but it
885        # will get simpler eventually!
886        test_platform = test['test-platform']
887        if test.get('worker-type'):
888            # This test already has its worker type defined, so just use that (yields below)
889            pass
890        elif test_platform.startswith('macosx'):
891            test['worker-type'] = MACOSX_WORKER_TYPES['macosx64']
892        elif test_platform.startswith('win'):
893            # figure out what platform the job needs to run on
894            if test['virtualization'] == 'hardware':
895                # some jobs like talos and reftest run on real h/w - those are all win10
896                win_worker_type_platform = WINDOWS_WORKER_TYPES['windows10-64']
897            else:
898                # the other jobs run on a vm which may or may not be a win10 vm
899                win_worker_type_platform = WINDOWS_WORKER_TYPES[
900                    test_platform.split('/')[0]
901                ]
902            # now we have the right platform set the worker type accordingly
903            test['worker-type'] = win_worker_type_platform[test['virtualization']]
904        elif test_platform.startswith('linux') or test_platform.startswith('android'):
905            if test.get('suite', '') == 'talos' and test['build-platform'] != 'linux64-ccov/opt':
906                test['worker-type'] = 'releng-hardware/gecko-t-linux-talos'
907            else:
908                test['worker-type'] = LINUX_WORKER_TYPES[test['instance-size']]
909        else:
910            raise Exception("unknown test_platform {}".format(test_platform))
911
912        yield test
913
914
915@transforms.add
916def make_job_description(config, tests):
917    """Convert *test* descriptions to *job* descriptions (input to
918    taskgraph.transforms.job)"""
919
920    for test in tests:
921        label = '{}-{}-{}'.format(config.kind, test['test-platform'], test['test-name'])
922        if test['chunks'] > 1:
923            label += '-{}'.format(test['this-chunk'])
924
925        build_label = test['build-label']
926
927        try_name = test['try-name']
928        if test['suite'] == 'talos':
929            attr_try_name = 'talos_try_name'
930        else:
931            attr_try_name = 'unittest_try_name'
932
933        attr_build_platform, attr_build_type = test['build-platform'].split('/', 1)
934
935        attributes = test.get('attributes', {})
936        attributes.update({
937            'build_platform': attr_build_platform,
938            'build_type': attr_build_type,
939            'test_platform': test['test-platform'],
940            'test_chunk': str(test['this-chunk']),
941            attr_try_name: try_name,
942        })
943
944        jobdesc = {}
945        name = '{}-{}'.format(test['test-platform'], test['test-name'])
946        jobdesc['name'] = name
947        jobdesc['label'] = label
948        jobdesc['description'] = test['description']
949        jobdesc['attributes'] = attributes
950        jobdesc['dependencies'] = {'build': build_label}
951        jobdesc['job-from'] = test['job-from']
952
953        if test['mozharness']['requires-signed-builds'] is True:
954            jobdesc['dependencies']['build-signing'] = test['build-signing-label']
955
956        jobdesc['expires-after'] = test['expires-after']
957        jobdesc['routes'] = []
958        jobdesc['run-on-projects'] = test['run-on-projects']
959        jobdesc['scopes'] = []
960        jobdesc['tags'] = test.get('tags', {})
961        jobdesc['extra'] = {
962            'chunks': {
963                'current': test['this-chunk'],
964                'total': test['chunks'],
965            },
966            'suite': {
967                'name': attributes['unittest_suite'],
968                'flavor': attributes['unittest_flavor'],
969            },
970        }
971        jobdesc['treeherder'] = {
972            'symbol': test['treeherder-symbol'],
973            'kind': 'test',
974            'tier': test['tier'],
975            'platform': test.get('treeherder-machine-platform', test['build-platform']),
976        }
977
978        suite = test.get('schedules-component', attributes['unittest_suite'])
979        if suite in INCLUSIVE_COMPONENTS:
980            # if this is an "inclusive" test, then all files which might
981            # cause it to run are annotated with SCHEDULES in moz.build,
982            # so do not include the platform or any other components here
983            schedules = [suite]
984        else:
985            schedules = [suite, platform_family(test['build-platform'])]
986
987        if test.get('when'):
988            jobdesc['when'] = test['when']
989        elif 'optimization' in test:
990            jobdesc['optimization'] = test['optimization']
991        elif not config.params.is_try() and suite not in INCLUSIVE_COMPONENTS:
992            # for non-try branches and non-inclusive suites, include SETA
993            jobdesc['optimization'] = {'skip-unless-schedules-or-seta': schedules}
994        else:
995            # otherwise just use skip-unless-schedules
996            jobdesc['optimization'] = {'skip-unless-schedules': schedules}
997
998        run = jobdesc['run'] = {}
999        run['using'] = 'mozharness-test'
1000        run['test'] = test
1001
1002        jobdesc['worker-type'] = test.pop('worker-type')
1003
1004        yield jobdesc
1005
1006
1007def normpath(path):
1008    return path.replace('/', '\\')
1009
1010
1011def get_firefox_version():
1012    with open('browser/config/version.txt', 'r') as f:
1013        return f.readline().strip()
1014