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
22import copy
23import logging
24import re
25
26from mozbuild.schedules import INCLUSIVE_COMPONENTS
27from six import string_types, text_type
28from voluptuous import (
29    Any,
30    Optional,
31    Required,
32    Exclusive,
33)
34
35import taskgraph
36from taskgraph.transforms.base import TransformSequence
37from taskgraph.util.attributes import keymatch
38from taskgraph.util.keyed_by import evaluate_keyed_by
39from taskgraph.util.templates import merge
40from taskgraph.util.treeherder import split_symbol, join_symbol
41from taskgraph.util.platforms import platform_family
42from taskgraph.util.schema import (
43    resolve_keyed_by,
44    optionally_keyed_by,
45    Schema,
46)
47from taskgraph.optimize.schema import OptimizationSchema
48from taskgraph.util.chunking import (
49    chunk_manifests,
50    get_manifest_loader,
51    get_runtimes,
52    guess_mozinfo_from_task,
53    manifest_loaders,
54    DefaultLoader,
55)
56from taskgraph.util.taskcluster import (
57    get_artifact_path,
58    get_index_url,
59)
60from taskgraph.util.perfile import perfile_number_of_chunks
61
62
63# default worker types keyed by instance-size
64LINUX_WORKER_TYPES = {
65    "large": "t-linux-large",
66    "xlarge": "t-linux-xlarge",
67    "default": "t-linux-large",
68}
69
70# windows worker types keyed by test-platform and virtualization
71WINDOWS_WORKER_TYPES = {
72    "windows10-32-mingwclang-qr": {
73        "virtual": "t-win10-64",
74        "virtual-with-gpu": "t-win10-64-gpu-s",
75        "hardware": "t-win10-64-1803-hw",
76    },
77    "windows7-32-qr": {
78        "virtual": "t-win7-32",
79        "virtual-with-gpu": "t-win7-32-gpu",
80        "hardware": "t-win10-64-1803-hw",
81    },
82    "windows7-32-shippable-qr": {
83        "virtual": "t-win7-32",
84        "virtual-with-gpu": "t-win7-32-gpu",
85        "hardware": "t-win10-64-1803-hw",
86    },
87    "windows7-32-devedition-qr": {  # build only, tests have no value
88        "virtual": "t-win7-32",
89        "virtual-with-gpu": "t-win7-32-gpu",
90        "hardware": "t-win10-64-1803-hw",
91    },
92    "windows10-32-qr": {
93        "virtual": "t-win10-64",
94        "virtual-with-gpu": "t-win10-64-gpu-s",
95        "hardware": "t-win10-64-1803-hw",
96    },
97    "windows10-32-shippable-qr": {
98        "virtual": "t-win10-64",
99        "virtual-with-gpu": "t-win10-64-gpu-s",
100        "hardware": "t-win10-64-1803-hw",
101    },
102    "windows10-64": {
103        "virtual": "t-win10-64",
104        "virtual-with-gpu": "t-win10-64-gpu-s",
105        "hardware": "t-win10-64-1803-hw",
106    },
107    "windows10-aarch64-qr": {
108        "virtual": "t-win64-aarch64-laptop",
109        "virtual-with-gpu": "t-win64-aarch64-laptop",
110        "hardware": "t-win64-aarch64-laptop",
111    },
112    "windows10-64-ccov": {
113        "virtual": "t-win10-64",
114        "virtual-with-gpu": "t-win10-64-gpu-s",
115        "hardware": "t-win10-64-1803-hw",
116    },
117    "windows10-64-ccov-qr": {
118        "virtual": "t-win10-64",
119        "virtual-with-gpu": "t-win10-64-gpu-s",
120        "hardware": "t-win10-64-1803-hw",
121    },
122    "windows10-64-devedition": {
123        "virtual": "t-win10-64",
124        "virtual-with-gpu": "t-win10-64-gpu-s",
125        "hardware": "t-win10-64-1803-hw",
126    },
127    "windows10-64-shippable": {
128        "virtual": "t-win10-64",
129        "virtual-with-gpu": "t-win10-64-gpu-s",
130        "hardware": "t-win10-64-1803-hw",
131    },
132    "windows10-64-qr": {
133        "virtual": "t-win10-64",
134        "virtual-with-gpu": "t-win10-64-gpu-s",
135        "hardware": "t-win10-64-1803-hw",
136    },
137    "windows10-64-shippable-qr": {
138        "virtual": "t-win10-64",
139        "virtual-with-gpu": "t-win10-64-gpu-s",
140        "hardware": "t-win10-64-1803-hw",
141    },
142    "windows10-64-devedition-qr": {
143        "virtual": "t-win10-64",
144        "virtual-with-gpu": "t-win10-64-gpu-s",
145        "hardware": "t-win10-64-1803-hw",
146    },
147    "windows10-64-asan-qr": {
148        "virtual": "t-win10-64",
149        "virtual-with-gpu": "t-win10-64-gpu-s",
150        "hardware": "t-win10-64-1803-hw",
151    },
152    "windows10-64-mingwclang-qr": {
153        "virtual": "t-win10-64",
154        "virtual-with-gpu": "t-win10-64-gpu-s",
155        "hardware": "t-win10-64-1803-hw",
156    },
157    "windows10-64-ref-hw-2017": {
158        "virtual": "t-win10-64",
159        "virtual-with-gpu": "t-win10-64-gpu-s",
160        "hardware": "t-win10-64-ref-hw",
161    },
162    "windows10-32-2004-mingwclang-qr": {
163        "virtual": "win10-64-2004",
164        "virtual-with-gpu": "win10-64-2004-gpu",
165    },
166    "windows10-32-2004-qr": {
167        "virtual": "win10-64-2004",
168        "virtual-with-gpu": "win10-64-2004-gpu",
169    },
170    "windows10-32-2004-shippable-qr": {
171        "virtual": "win10-64-2004",
172        "virtual-with-gpu": "win10-64-2004-gpu",
173    },
174    "windows10-64-2004": {
175        "virtual": "win10-64-2004",
176        "virtual-with-gpu": "win10-64-2004-gpu",
177    },
178    "windows10-64-2004-ccov": {
179        "virtual": "win10-64-2004",
180        "virtual-with-gpu": "win10-64-2004-gpu",
181    },
182    "windows10-64-2004-ccov-qr": {
183        "virtual": "win10-64-2004",
184        "virtual-with-gpu": "win10-64-2004-gpu",
185    },
186    "windows10-64-2004-devedition": {
187        "virtual": "win10-64-2004",
188        "virtual-with-gpu": "win10-64-2004-gpu",
189    },
190    "windows10-64-2004-shippable": {
191        "virtual": "win10-64-2004",
192        "virtual-with-gpu": "win10-64-2004-gpu",
193    },
194    "windows10-64-2004-qr": {
195        "virtual": "win10-64-2004",
196        "virtual-with-gpu": "win10-64-2004-gpu",
197    },
198    "windows10-64-2004-shippable-qr": {
199        "virtual": "win10-64-2004",
200        "virtual-with-gpu": "win10-64-2004-gpu",
201    },
202    "windows10-64-2004-devedition-qr": {
203        "virtual": "win10-64-2004",
204        "virtual-with-gpu": "win10-64-2004-gpu",
205    },
206    "windows10-64-2004-asan-qr": {
207        "virtual": "win10-64-2004",
208        "virtual-with-gpu": "win10-64-2004-gpu",
209    },
210    "windows10-64-2004-mingwclang-qr": {
211        "virtual": "win10-64-2004",
212        "virtual-with-gpu": "win10-64-2004-gpu",
213    },
214}
215
216# os x worker types keyed by test-platform
217MACOSX_WORKER_TYPES = {
218    "macosx1014-64": "t-osx-1014",
219    "macosx1014-64-power": "t-osx-1014-power",
220    "macosx1015-64": "t-osx-1015-r8",
221    "macosx1100-64": "t-osx-1100-m1",
222}
223
224
225def gv_e10s_filter(task):
226    return get_mobile_project(task) == "geckoview" and task["e10s"]
227
228
229def fission_filter(task):
230    return task.get("e10s") in (True, "both")
231
232
233TEST_VARIANTS = {
234    "noqr": {
235        "description": "{description} with no webrender or software webrender",
236        "suffix": "noqr",
237        "merge": {
238            "webrender": False,
239            "mozharness": {
240                "extra-options": [
241                    "--setpref=gfx.webrender.software=false",
242                    "--setpref=layers.acceleration.disabled=true",
243                ],
244            },
245        },
246    },
247    "a11y-checks": {
248        "description": "{description} with accessibility checks enabled",
249        "suffix": "a11y-checks",
250        "replace": {
251            "run-on-projects": {
252                "by-test-platform": {
253                    "linux.*64(-shippable)?-qr/opt": ["trunk"],
254                    "default": [],
255                },
256            },
257            "tier": 2,
258        },
259        "merge": {
260            "mozharness": {
261                "extra-options": [
262                    "--enable-a11y-checks",
263                ],
264            },
265        },
266    },
267    "geckoview-e10s-single": {
268        "description": "{description} with single-process e10s",
269        "filterfn": gv_e10s_filter,
270        "replace": {
271            "run-on-projects": ["trunk"],
272        },
273        "suffix": "e10s-single",
274        "merge": {
275            "mozharness": {
276                "extra-options": [
277                    "--setpref=dom.ipc.processCount=1",
278                ],
279            },
280        },
281    },
282    "geckoview-fission": {
283        "description": "{description} with fission enabled",
284        "filterfn": gv_e10s_filter,
285        "suffix": "fis",
286        "merge": {
287            "mozharness": {
288                "extra-options": [
289                    "--enable-fission",
290                ],
291            },
292        },
293    },
294    "fission": {
295        "description": "{description} with fission enabled",
296        "filterfn": fission_filter,
297        "suffix": "fis",
298        "replace": {
299            "e10s": True,
300        },
301        "merge": {
302            "mozharness": {
303                "extra-options": [
304                    "--setpref=fission.autostart=true",
305                ],
306            },
307        },
308    },
309    "fission-xorigin": {
310        "description": "{description} with cross-origin and fission enabled",
311        "filterfn": fission_filter,
312        "suffix": "fis-xorig",
313        "replace": {
314            "e10s": True,
315        },
316        "merge": {
317            "mozharness": {
318                "extra-options": [
319                    "--setpref=fission.autostart=true",
320                    "--enable-xorigin-tests",
321                ],
322            },
323        },
324    },
325    "fission-webgl-ipc": {
326        # TODO: After 2021-05-01, verify this variant is still needed.
327        "description": "{description} with fission and WebGL IPC process enabled",
328        "suffix": "fis-gli",
329        "replace": {
330            "e10s": True,
331        },
332        "merge": {
333            "mozharness": {
334                "extra-options": [
335                    "--setpref=fission.autostart=true",
336                    "--setpref=dom.serviceWorkers.parent_intercept=true",
337                    "--setpref=webgl.out-of-process=true",
338                ],
339            },
340        },
341    },
342    "socketprocess": {
343        "description": "{description} with socket process enabled",
344        "suffix": "spi",
345        "merge": {
346            "mozharness": {
347                "extra-options": [
348                    "--setpref=media.peerconnection.mtransport_process=true",
349                    "--setpref=network.process.enabled=true",
350                ],
351            }
352        },
353    },
354    "socketprocess_networking": {
355        "description": "{description} with networking on socket process enabled",
356        "suffix": "spi-nw",
357        "merge": {
358            "mozharness": {
359                "extra-options": [
360                    "--setpref=network.process.enabled=true",
361                    "--setpref=network.http.network_access_on_socket_process.enabled=true",
362                    "--setpref=network.ssl_tokens_cache_enabled=true",
363                ],
364            }
365        },
366    },
367    "wayland": {
368        "description": "{description} with Wayland backend enabled",
369        "suffix": "wayland",
370        "replace": {
371            "run-on-projects": [],
372        },
373        "merge": {
374            "mozharness": {
375                "extra-options": [
376                    "--setpref=widget.wayland.test-workarounds.enabled=true",
377                ],
378            }
379        },
380    },
381    "webrender": {
382        "description": "{description} with webrender enabled",
383        "suffix": "wr",
384        "merge": {
385            "webrender": True,
386        },
387    },
388    "webrender-sw": {
389        "description": "{description} with software webrender enabled",
390        "suffix": "swr",
391        "merge": {
392            "webrender": True,
393            "mozharness": {
394                "extra-options": [
395                    "--setpref=gfx.webrender.software=true",
396                ],
397            },
398        },
399    },
400    "webrender-sw-a11y-checks": {
401        "description": "{description} with software webrender and accessibility checks enabled",
402        "suffix": "swr-a11y-checks",
403        "replace": {
404            "run-on-projects": {
405                "by-test-platform": {
406                    "linux.*64(-shippable)?-qr/opt": ["trunk"],
407                    "default": [],
408                },
409            },
410            "tier": 2,
411        },
412        "merge": {
413            "webrender": True,
414            "mozharness": {
415                "extra-options": [
416                    "--setpref=gfx.webrender.software=true",
417                    "--enable-a11y-checks",
418                ],
419            },
420        },
421    },
422    "webrender-sw-fission": {
423        "description": "{description} with software webrender and fission enabled",
424        "filterfn": fission_filter,
425        "suffix": "swr-fis",
426        "replace": {
427            "e10s": True,
428        },
429        "merge": {
430            "webrender": True,
431            "mozharness": {
432                "extra-options": [
433                    "--setpref=gfx.webrender.software=true",
434                    "--setpref=fission.autostart=true",
435                ],
436            },
437        },
438    },
439    "webrender-sw-wayland": {
440        "description": "{description} with software webrender and Wayland backend enabled",
441        "suffix": "swr-wayland",
442        "replace": {
443            "run-on-projects": [],
444        },
445        "merge": {
446            "mozharness": {
447                "extra-options": [
448                    "--setpref=gfx.webrender.software=true",
449                    "--setpref=widget.wayland.test-workarounds.enabled=true",
450                ],
451            }
452        },
453    },
454    "webgl-ipc": {
455        # TODO: After 2021-05-01, verify this variant is still needed.
456        "description": "{description} with WebGL IPC process enabled",
457        "suffix": "gli",
458        "replace": {
459            "run-on-projects": {
460                "by-test-platform": {
461                    "linux.*-64.*": ["trunk"],
462                    "mac.*": ["trunk"],
463                    "win.*": ["trunk"],
464                    "default": [],
465                },
466            },
467        },
468        "merge": {
469            "mozharness": {
470                "extra-options": [
471                    "--setpref=webgl.out-of-process=true",
472                ],
473            },
474        },
475    },
476    "webgl-ipc-profiling": {
477        # TODO: After 2021-05-01, verify this variant is still needed.
478        "description": "{description} with WebGL IPC process enabled",
479        "suffix": "gli",
480        "merge": {
481            "mozharness": {
482                "extra-options": [
483                    "--setpref=webgl.out-of-process=true",
484                ],
485            },
486        },
487    },
488}
489
490
491DYNAMIC_CHUNK_DURATION = 20 * 60  # seconds
492"""The approximate time each test chunk should take to run."""
493
494
495DYNAMIC_CHUNK_MULTIPLIER = {
496    # Desktop xpcshell tests run in parallel. Reduce the total runtime to
497    # compensate.
498    "^(?!android).*-xpcshell.*": 0.2,
499}
500"""A multiplication factor to tweak the total duration per platform / suite."""
501
502
503logger = logging.getLogger(__name__)
504
505transforms = TransformSequence()
506
507# Schema for a test description
508#
509# *****WARNING*****
510#
511# This is a great place for baffling cruft to accumulate, and that makes
512# everyone move more slowly.  Be considerate of your fellow hackers!
513# See the warnings in taskcluster/docs/how-tos.rst
514#
515# *****WARNING*****
516test_description_schema = Schema(
517    {
518        # description of the suite, for the task metadata
519        Required("description"): text_type,
520        # test suite category and name
521        Optional("suite"): Any(
522            text_type,
523            {Optional("category"): text_type, Optional("name"): text_type},
524        ),
525        # base work directory used to set up the task.
526        Optional("workdir"): optionally_keyed_by(
527            "test-platform", Any(text_type, "default")
528        ),
529        # the name by which this test suite is addressed in try syntax; defaults to
530        # the test-name.  This will translate to the `unittest_try_name` or
531        # `talos_try_name` attribute.
532        Optional("try-name"): text_type,
533        # additional tags to mark up this type of test
534        Optional("tags"): {text_type: object},
535        # the symbol, or group(symbol), under which this task should appear in
536        # treeherder.
537        Required("treeherder-symbol"): text_type,
538        # the value to place in task.extra.treeherder.machine.platform; ideally
539        # this is the same as build-platform, and that is the default, but in
540        # practice it's not always a match.
541        Optional("treeherder-machine-platform"): text_type,
542        # attributes to appear in the resulting task (later transforms will add the
543        # common attributes)
544        Optional("attributes"): {text_type: object},
545        # relative path (from config.path) to the file task was defined in
546        Optional("job-from"): text_type,
547        # The `run_on_projects` attribute, defaulting to "all".  This dictates the
548        # projects on which this task should be included in the target task set.
549        # See the attributes documentation for details.
550        #
551        # Note that the special case 'built-projects', the default, uses the parent
552        # build task's run-on-projects, meaning that tests run only on platforms
553        # that are built.
554        Optional("run-on-projects"): optionally_keyed_by(
555            "app",
556            "subtest",
557            "test-platform",
558            "test-name",
559            "variant",
560            Any([text_type], "built-projects"),
561        ),
562        # When set only run on projects where the build would already be running.
563        # This ensures tasks where this is True won't be the cause of the build
564        # running on a project it otherwise wouldn't have.
565        Optional("built-projects-only"): bool,
566        # the sheriffing tier for this task (default: set based on test platform)
567        Optional("tier"): optionally_keyed_by(
568            "test-platform", "variant", "app", "subtest", Any(int, "default")
569        ),
570        # number of chunks to create for this task.  This can be keyed by test
571        # platform by passing a dictionary in the `by-test-platform` key.  If the
572        # test platform is not found, the key 'default' will be tried.
573        Required("chunks"): optionally_keyed_by("test-platform", Any(int, "dynamic")),
574        # Custom 'test_manifest_loader' to use, overriding the one configured in the
575        # parameters. When 'null', no test chunking will be performed. Can also
576        # be used to disable "manifest scheduling".
577        Optional("test-manifest-loader"): Any(None, *list(manifest_loaders)),
578        # the time (with unit) after which this task is deleted; default depends on
579        # the branch (see below)
580        Optional("expires-after"): text_type,
581        # The different configurations that should be run against this task, defined
582        # in the TEST_VARIANTS object.
583        Optional("variants"): optionally_keyed_by(
584            "test-platform", "project", Any(list(TEST_VARIANTS))
585        ),
586        # Whether to run this task with e10s.  If false, run
587        # without e10s; if true, run with e10s; if 'both', run one task with and
588        # one task without e10s.  E10s tasks have "-e10s" appended to the test name
589        # and treeherder group.
590        Required("e10s"): optionally_keyed_by(
591            "test-platform", "project", Any(bool, "both")
592        ),
593        # Whether the task should run with WebRender enabled or not.
594        Optional("webrender"): bool,
595        Optional("webrender-run-on-projects"): optionally_keyed_by(
596            "app", Any([text_type], "default")
597        ),
598        # The EC2 instance size to run these tests on.
599        Required("instance-size"): optionally_keyed_by(
600            "test-platform", Any("default", "large", "xlarge")
601        ),
602        # type of virtualization or hardware required by test.
603        Required("virtualization"): optionally_keyed_by(
604            "test-platform", Any("virtual", "virtual-with-gpu", "hardware")
605        ),
606        # Whether the task requires loopback audio or video (whatever that may mean
607        # on the platform)
608        Required("loopback-audio"): bool,
609        Required("loopback-video"): bool,
610        # Whether the test can run using a software GL implementation on Linux
611        # using the GL compositor. May not be used with "legacy" sized instances
612        # due to poor LLVMPipe performance (bug 1296086).  Defaults to true for
613        # unit tests on linux platforms and false otherwise
614        Optional("allow-software-gl-layers"): bool,
615        # For tasks that will run in docker-worker, this is the
616        # name of the docker image or in-tree docker image to run the task in.  If
617        # in-tree, then a dependency will be created automatically.  This is
618        # generally `desktop-test`, or an image that acts an awful lot like it.
619        Required("docker-image"): optionally_keyed_by(
620            "test-platform",
621            Any(
622                # a raw Docker image path (repo/image:tag)
623                text_type,
624                # an in-tree generated docker image (from `taskcluster/docker/<name>`)
625                {"in-tree": text_type},
626                # an indexed docker image
627                {"indexed": text_type},
628            ),
629        ),
630        # seconds of runtime after which the task will be killed.  Like 'chunks',
631        # this can be keyed by test pltaform.
632        Required("max-run-time"): optionally_keyed_by("test-platform", int),
633        # the exit status code that indicates the task should be retried
634        Optional("retry-exit-status"): [int],
635        # Whether to perform a gecko checkout.
636        Required("checkout"): bool,
637        # Wheter to perform a machine reboot after test is done
638        Optional("reboot"): Any(False, "always", "on-exception", "on-failure"),
639        # What to run
640        Required("mozharness"): {
641            # the mozharness script used to run this task
642            Required("script"): optionally_keyed_by("test-platform", text_type),
643            # the config files required for the task
644            Required("config"): optionally_keyed_by("test-platform", [text_type]),
645            # mochitest flavor for mochitest runs
646            Optional("mochitest-flavor"): text_type,
647            # any additional actions to pass to the mozharness command
648            Optional("actions"): [text_type],
649            # additional command-line options for mozharness, beyond those
650            # automatically added
651            Required("extra-options"): optionally_keyed_by(
652                "test-platform", [text_type]
653            ),
654            # the artifact name (including path) to test on the build task; this is
655            # generally set in a per-kind transformation
656            Optional("build-artifact-name"): text_type,
657            Optional("installer-url"): text_type,
658            # If not false, tooltool downloads will be enabled via relengAPIProxy
659            # for either just public files, or all files.  Not supported on Windows
660            Required("tooltool-downloads"): Any(
661                False,
662                "public",
663                "internal",
664            ),
665            # Add --blob-upload-branch=<project> mozharness parameter
666            Optional("include-blob-upload-branch"): bool,
667            # The setting for --download-symbols (if omitted, the option will not
668            # be passed to mozharness)
669            Optional("download-symbols"): Any(True, "ondemand"),
670            # If set, then MOZ_NODE_PATH=/usr/local/bin/node is included in the
671            # environment.  This is more than just a helpful path setting -- it
672            # causes xpcshell tests to start additional servers, and runs
673            # additional tests.
674            Required("set-moz-node-path"): bool,
675            # If true, include chunking information in the command even if the number
676            # of chunks is 1
677            Required("chunked"): optionally_keyed_by("test-platform", bool),
678            Required("requires-signed-builds"): optionally_keyed_by(
679                "test-platform", bool
680            ),
681        },
682        # The set of test manifests to run.
683        Optional("test-manifests"): Any(
684            [text_type],
685            {"active": [text_type], "skipped": [text_type]},
686        ),
687        # The current chunk (if chunking is enabled).
688        Optional("this-chunk"): int,
689        # os user groups for test task workers; required scopes, will be
690        # added automatically
691        Optional("os-groups"): optionally_keyed_by("test-platform", [text_type]),
692        Optional("run-as-administrator"): optionally_keyed_by("test-platform", bool),
693        # -- values supplied by the task-generation infrastructure
694        # the platform of the build this task is testing
695        Required("build-platform"): text_type,
696        # the label of the build task generating the materials to test
697        Required("build-label"): text_type,
698        # the label of the signing task generating the materials to test.
699        # Signed builds are used in xpcshell tests on Windows, for instance.
700        Optional("build-signing-label"): text_type,
701        # the build's attributes
702        Required("build-attributes"): {text_type: object},
703        # the platform on which the tests will run
704        Required("test-platform"): text_type,
705        # limit the test-platforms (as defined in test-platforms.yml)
706        # that the test will run on
707        Optional("limit-platforms"): optionally_keyed_by("app", [text_type]),
708        # the name of the test (the key in tests.yml)
709        Required("test-name"): text_type,
710        # the product name, defaults to firefox
711        Optional("product"): text_type,
712        # conditional files to determine when these tests should be run
713        Exclusive("when", "optimization"): {
714            Optional("files-changed"): [text_type],
715        },
716        # Optimization to perform on this task during the optimization phase.
717        # Optimizations are defined in taskcluster/taskgraph/optimize.py.
718        Exclusive("optimization", "optimization"): OptimizationSchema,
719        # The SCHEDULES component for this task; this defaults to the suite
720        # (not including the flavor) but can be overridden here.
721        Exclusive("schedules-component", "optimization"): Any(
722            text_type,
723            [text_type],
724        ),
725        Optional("worker-type"): optionally_keyed_by(
726            "test-platform",
727            Any(text_type, None),
728        ),
729        Optional(
730            "require-signed-extensions",
731            description="Whether the build being tested requires extensions be signed.",
732        ): optionally_keyed_by("release-type", "test-platform", bool),
733        # The target name, specifying the build artifact to be tested.
734        # If None or not specified, a transform sets the target based on OS:
735        # target.dmg (Mac), target.apk (Android), target.tar.bz2 (Linux),
736        # or target.zip (Windows).
737        Optional("target"): optionally_keyed_by(
738            "test-platform",
739            Any(
740                text_type,
741                None,
742                {Required("index"): text_type, Required("name"): text_type},
743            ),
744        ),
745        # A list of artifacts to install from 'fetch' tasks.
746        Optional("fetches"): {
747            text_type: optionally_keyed_by("test-platform", [text_type])
748        },
749        # Opt-in to Python 3 support
750        Optional("python-3"): bool,
751        # Raptor / browsertime specific keys that need to be here to support
752        # using `by-key` after `by-variant`. Ideally these keys should not exist
753        # in the tests.py schema and instead we'd split variants before the raptor
754        # transforms need them. See bug 1700774.
755        Optional("app"): text_type,
756        Optional("subtest"): text_type,
757        # Define if a given task supports artifact builds or not, see bug 1695325.
758        Optional("supports-artifact-builds"): bool,
759    }
760)
761
762
763@transforms.add
764def handle_keyed_by_mozharness(config, tasks):
765    """Resolve a mozharness field if it is keyed by something"""
766    fields = [
767        "mozharness",
768        "mozharness.chunked",
769        "mozharness.config",
770        "mozharness.extra-options",
771        "mozharness.requires-signed-builds",
772        "mozharness.script",
773    ]
774    for task in tasks:
775        for field in fields:
776            resolve_keyed_by(
777                task, field, item_name=task["test-name"], enforce_single_match=False
778            )
779        yield task
780
781
782@transforms.add
783def set_defaults(config, tasks):
784    for task in tasks:
785        build_platform = task["build-platform"]
786        if build_platform.startswith("android"):
787            # all Android test tasks download internal objects from tooltool
788            task["mozharness"]["tooltool-downloads"] = "internal"
789            task["mozharness"]["actions"] = ["get-secrets"]
790
791            # loopback-video is always true for Android, but false for other
792            # platform phyla
793            task["loopback-video"] = True
794        task["mozharness"]["set-moz-node-path"] = True
795
796        # software-gl-layers is only meaningful on linux unittests, where it defaults to True
797        if task["test-platform"].startswith("linux") and task["suite"] not in [
798            "talos",
799            "raptor",
800        ]:
801            task.setdefault("allow-software-gl-layers", True)
802        else:
803            task["allow-software-gl-layers"] = False
804
805        # Enable WebRender by default on the QuantumRender test platforms, since
806        # the whole point of QuantumRender is to run with WebRender enabled.
807        # This currently matches linux64-qr and windows10-64-qr; both of these
808        # have /opt and /debug variants.
809        if "-qr/" in task["test-platform"]:
810            task["webrender"] = True
811        else:
812            task.setdefault("webrender", False)
813
814        task.setdefault("e10s", True)
815        task.setdefault("try-name", task["test-name"])
816        task.setdefault("os-groups", [])
817        task.setdefault("run-as-administrator", False)
818        task.setdefault("chunks", 1)
819        task.setdefault("run-on-projects", "built-projects")
820        task.setdefault("built-projects-only", False)
821        task.setdefault("instance-size", "default")
822        task.setdefault("max-run-time", 3600)
823        task.setdefault("reboot", False)
824        task.setdefault("virtualization", "virtual")
825        task.setdefault("loopback-audio", False)
826        task.setdefault("loopback-video", False)
827        task.setdefault("limit-platforms", [])
828        task.setdefault("docker-image", {"in-tree": "ubuntu1804-test"})
829        task.setdefault("checkout", False)
830        task.setdefault("require-signed-extensions", False)
831        task.setdefault("variants", [])
832        task.setdefault("supports-artifact-builds", True)
833
834        task["mozharness"].setdefault("extra-options", [])
835        task["mozharness"].setdefault("requires-signed-builds", False)
836        task["mozharness"].setdefault("tooltool-downloads", "public")
837        task["mozharness"].setdefault("set-moz-node-path", False)
838        task["mozharness"].setdefault("chunked", False)
839        yield task
840
841
842@transforms.add
843def resolve_keys(config, tasks):
844    for task in tasks:
845        resolve_keyed_by(
846            task,
847            "require-signed-extensions",
848            item_name=task["test-name"],
849            enforce_single_match=False,
850            **{
851                "release-type": config.params["release_type"],
852            }
853        )
854        yield task
855
856
857@transforms.add
858def setup_raptor(config, tasks):
859    """Add options that are specific to raptor jobs (identified by suite=raptor)"""
860    from taskgraph.transforms.raptor import transforms as raptor_transforms
861
862    for task in tasks:
863        if task["suite"] != "raptor":
864            yield task
865            continue
866
867        for t in raptor_transforms(config, [task]):
868            yield t
869
870
871@transforms.add
872def limit_platforms(config, tasks):
873    for task in tasks:
874        if not task["limit-platforms"]:
875            yield task
876            continue
877
878        limited_platforms = {key: key for key in task["limit-platforms"]}
879        if keymatch(limited_platforms, task["test-platform"]):
880            yield task
881
882
883transforms.add_validate(test_description_schema)
884
885
886@transforms.add
887def handle_suite_category(config, tasks):
888    for task in tasks:
889        task.setdefault("suite", {})
890
891        if isinstance(task["suite"], text_type):
892            task["suite"] = {"name": task["suite"]}
893
894        suite = task["suite"].setdefault("name", task["test-name"])
895        category = task["suite"].setdefault("category", suite)
896
897        task.setdefault("attributes", {})
898        task["attributes"]["unittest_suite"] = suite
899        task["attributes"]["unittest_category"] = category
900
901        script = task["mozharness"]["script"]
902        category_arg = None
903        if suite.startswith("test-verify") or suite.startswith("test-coverage"):
904            pass
905        elif script in ("android_emulator_unittest.py", "android_hardware_unittest.py"):
906            category_arg = "--test-suite"
907        elif script == "desktop_unittest.py":
908            category_arg = "--{}-suite".format(category)
909
910        if category_arg:
911            task["mozharness"].setdefault("extra-options", [])
912            extra = task["mozharness"]["extra-options"]
913            if not any(arg.startswith(category_arg) for arg in extra):
914                extra.append("{}={}".format(category_arg, suite))
915
916        # From here on out we only use the suite name.
917        task["suite"] = suite
918        yield task
919
920
921@transforms.add
922def setup_talos(config, tasks):
923    """Add options that are specific to talos jobs (identified by suite=talos)"""
924    for task in tasks:
925        if task["suite"] != "talos":
926            yield task
927            continue
928
929        extra_options = task.setdefault("mozharness", {}).setdefault(
930            "extra-options", []
931        )
932        extra_options.append("--use-talos-json")
933
934        # win7 needs to test skip
935        if task["build-platform"].startswith("win32"):
936            extra_options.append("--add-option")
937            extra_options.append("--setpref,gfx.direct2d.disabled=true")
938
939        yield task
940
941
942@transforms.add
943def setup_browsertime_flag(config, tasks):
944    """Optionally add `--browsertime` flag to Raptor pageload tests."""
945
946    browsertime_flag = config.params["try_task_config"].get("browsertime", False)
947
948    for task in tasks:
949        if not browsertime_flag or task["suite"] != "raptor":
950            yield task
951            continue
952
953        if task["treeherder-symbol"].startswith("Rap"):
954            # The Rap group is subdivided as Rap{-fenix,-refbrow(...),
955            # so `taskgraph.util.treeherder.replace_group` isn't appropriate.
956            task["treeherder-symbol"] = task["treeherder-symbol"].replace(
957                "Rap", "Btime", 1
958            )
959
960        extra_options = task.setdefault("mozharness", {}).setdefault(
961            "extra-options", []
962        )
963        extra_options.append("--browsertime")
964
965        yield task
966
967
968@transforms.add
969def handle_artifact_prefix(config, tasks):
970    """Handle translating `artifact_prefix` appropriately"""
971    for task in tasks:
972        if task["build-attributes"].get("artifact_prefix"):
973            task.setdefault("attributes", {}).setdefault(
974                "artifact_prefix", task["build-attributes"]["artifact_prefix"]
975            )
976        yield task
977
978
979@transforms.add
980def set_target(config, tasks):
981    for task in tasks:
982        build_platform = task["build-platform"]
983        target = None
984        if "target" in task:
985            resolve_keyed_by(
986                task, "target", item_name=task["test-name"], enforce_single_match=False
987            )
988            target = task["target"]
989        if not target:
990            if build_platform.startswith("macosx"):
991                target = "target.dmg"
992            elif build_platform.startswith("android"):
993                target = "target.apk"
994            elif build_platform.startswith("win"):
995                target = "target.zip"
996            else:
997                target = "target.tar.bz2"
998
999        if isinstance(target, dict):
1000            # TODO Remove hardcoded mobile artifact prefix
1001            index_url = get_index_url(target["index"])
1002            installer_url = "{}/artifacts/public/{}".format(index_url, target["name"])
1003            task["mozharness"]["installer-url"] = installer_url
1004        else:
1005            task["mozharness"]["build-artifact-name"] = get_artifact_path(task, target)
1006
1007        yield task
1008
1009
1010@transforms.add
1011def set_treeherder_machine_platform(config, tasks):
1012    """Set the appropriate task.extra.treeherder.machine.platform"""
1013    translation = {
1014        # Linux64 build platform for asan is specified differently to
1015        # treeherder.
1016        "macosx1014-64/debug": "osx-10-14/debug",
1017        "macosx1014-64/opt": "osx-10-14/opt",
1018        "macosx1014-64-shippable/opt": "osx-10-14-shippable/opt",
1019        "macosx1100-64/opt": "osx-1100/opt",
1020        "macosx1100-64-shippable/opt": "osx-1100-shippable/opt",
1021        "win64-asan/opt": "windows10-64/asan",
1022        "win64-aarch64/opt": "windows10-aarch64/opt",
1023    }
1024    for task in tasks:
1025        # For most desktop platforms, the above table is not used for "regular"
1026        # builds, so we'll always pick the test platform here.
1027        # On macOS though, the regular builds are in the table.  This causes a
1028        # conflict in `verify_task_graph_symbol` once you add a new test
1029        # platform based on regular macOS builds, such as for QR.
1030        # Since it's unclear if the regular macOS builds can be removed from
1031        # the table, workaround the issue for QR.
1032        if "android" in task["test-platform"] and "pgo/opt" in task["test-platform"]:
1033            platform_new = task["test-platform"].replace("-pgo/opt", "/pgo")
1034            task["treeherder-machine-platform"] = platform_new
1035        elif "android-em-7.0-x86_64-qr" in task["test-platform"]:
1036            task["treeherder-machine-platform"] = task["test-platform"].replace(
1037                ".", "-"
1038            )
1039        elif "android-em-7.0-x86_64-shippable-qr" in task["test-platform"]:
1040            task["treeherder-machine-platform"] = task["test-platform"].replace(
1041                ".", "-"
1042            )
1043        elif "-qr" in task["test-platform"]:
1044            task["treeherder-machine-platform"] = task["test-platform"]
1045        elif "android-hw" in task["test-platform"]:
1046            task["treeherder-machine-platform"] = task["test-platform"]
1047        elif "android-em-7.0-x86_64" in task["test-platform"]:
1048            task["treeherder-machine-platform"] = task["test-platform"].replace(
1049                ".", "-"
1050            )
1051        elif "android-em-7.0-x86" in task["test-platform"]:
1052            task["treeherder-machine-platform"] = task["test-platform"].replace(
1053                ".", "-"
1054            )
1055        # Bug 1602863 - must separately define linux64/asan and linux1804-64/asan
1056        # otherwise causes an exception during taskgraph generation about
1057        # duplicate treeherder platform/symbol.
1058        elif "linux64-asan/opt" in task["test-platform"]:
1059            task["treeherder-machine-platform"] = "linux64/asan"
1060        elif "linux1804-asan/opt" in task["test-platform"]:
1061            task["treeherder-machine-platform"] = "linux1804-64/asan"
1062        else:
1063            task["treeherder-machine-platform"] = translation.get(
1064                task["build-platform"], task["test-platform"]
1065            )
1066        yield task
1067
1068
1069@transforms.add
1070def set_download_symbols(config, tasks):
1071    """In general, we download symbols immediately for debug builds, but only
1072    on demand for everything else. ASAN builds shouldn't download
1073    symbols since they don't product symbol zips see bug 1283879"""
1074    for task in tasks:
1075        if task["test-platform"].split("/")[-1] == "debug":
1076            task["mozharness"]["download-symbols"] = True
1077        elif (
1078            task["build-platform"] == "linux64-asan/opt"
1079            or task["build-platform"] == "linux64-asan-qr/opt"
1080            or task["build-platform"] == "windows10-64-asan-qr/opt"
1081        ):
1082            if "download-symbols" in task["mozharness"]:
1083                del task["mozharness"]["download-symbols"]
1084        else:
1085            task["mozharness"]["download-symbols"] = "ondemand"
1086        yield task
1087
1088
1089@transforms.add
1090def handle_keyed_by(config, tasks):
1091    """Resolve fields that can be keyed by platform, etc."""
1092    fields = [
1093        "instance-size",
1094        "docker-image",
1095        "max-run-time",
1096        "chunks",
1097        "variants",
1098        "e10s",
1099        "suite",
1100        "run-on-projects",
1101        "os-groups",
1102        "run-as-administrator",
1103        "workdir",
1104        "worker-type",
1105        "virtualization",
1106        "fetches.fetch",
1107        "fetches.toolchain",
1108        "target",
1109        "webrender-run-on-projects",
1110    ]
1111    for task in tasks:
1112        for field in fields:
1113            resolve_keyed_by(
1114                task,
1115                field,
1116                item_name=task["test-name"],
1117                defer=["variant"],
1118                enforce_single_match=False,
1119                project=config.params["project"],
1120            )
1121        yield task
1122
1123
1124@transforms.add
1125def setup_browsertime(config, tasks):
1126    """Configure browsertime dependencies for Raptor pageload tests that have
1127    `--browsertime` extra option."""
1128
1129    for task in tasks:
1130        # We need to make non-trivial changes to various fetches, and our
1131        # `by-test-platform` may not be "compatible" with existing
1132        # `by-test-platform` filters.  Therefore we do everything after
1133        # `handle_keyed_by` so that existing fields have been resolved down to
1134        # simple lists.  But we use the `by-test-platform` machinery to express
1135        # filters so that when the time comes to move browsertime into YAML
1136        # files, the transition is straight-forward.
1137        extra_options = task.get("mozharness", {}).get("extra-options", [])
1138
1139        if task["suite"] != "raptor" or "--browsertime" not in extra_options:
1140            yield task
1141            continue
1142
1143        ts = {
1144            "by-test-platform": {
1145                "android.*": ["browsertime", "linux64-geckodriver", "linux64-node"],
1146                "linux.*": ["browsertime", "linux64-geckodriver", "linux64-node"],
1147                "macosx.*": ["browsertime", "macosx64-geckodriver", "macosx64-node"],
1148                "windows.*aarch64.*": [
1149                    "browsertime",
1150                    "win32-geckodriver",
1151                    "win32-node",
1152                ],
1153                "windows.*-32.*": ["browsertime", "win32-geckodriver", "win32-node"],
1154                "windows.*-64.*": ["browsertime", "win64-geckodriver", "win64-node"],
1155            },
1156        }
1157
1158        task.setdefault("fetches", {}).setdefault("toolchain", []).extend(
1159            evaluate_keyed_by(ts, "fetches.toolchain", task)
1160        )
1161
1162        fs = {
1163            "by-test-platform": {
1164                "android.*": ["linux64-ffmpeg-4.1.4"],
1165                "linux.*": ["linux64-ffmpeg-4.1.4"],
1166                "macosx.*": ["mac64-ffmpeg-4.1.1"],
1167                "windows.*aarch64.*": ["win64-ffmpeg-4.1.1"],
1168                "windows.*-32.*": ["win64-ffmpeg-4.1.1"],
1169                "windows.*-64.*": ["win64-ffmpeg-4.1.1"],
1170            },
1171        }
1172
1173        cd_fetches = {
1174            "android.*": [
1175                "linux64-chromedriver-87",
1176                "linux64-chromedriver-89",
1177                "linux64-chromedriver-90",
1178                "linux64-chromedriver-91",
1179            ],
1180            "linux.*": [
1181                "linux64-chromedriver-87",
1182                "linux64-chromedriver-89",
1183                "linux64-chromedriver-90",
1184                "linux64-chromedriver-91",
1185            ],
1186            "macosx.*": [
1187                "mac64-chromedriver-87",
1188                "mac64-chromedriver-89",
1189                "mac64-chromedriver-90",
1190                "mac64-chromedriver-91",
1191            ],
1192            "windows.*aarch64.*": [
1193                "win32-chromedriver-87",
1194                "win32-chromedriver-89",
1195                "win32-chromedriver-90",
1196                "win32-chromedriver-91",
1197            ],
1198            "windows.*-32.*": [
1199                "win32-chromedriver-87",
1200                "win32-chromedriver-89",
1201                "win32-chromedriver-90",
1202                "win32-chromedriver-91",
1203            ],
1204            "windows.*-64.*": [
1205                "win32-chromedriver-87",
1206                "win32-chromedriver-89",
1207                "win32-chromedriver-90",
1208                "win32-chromedriver-91",
1209            ],
1210        }
1211
1212        chromium_fetches = {
1213            "linux.*": ["linux64-chromium"],
1214            "macosx.*": ["mac-chromium"],
1215            "windows.*aarch64.*": ["win32-chromium"],
1216            "windows.*-32.*": ["win32-chromium"],
1217            "windows.*-64.*": ["win64-chromium"],
1218        }
1219
1220        cd_extracted_name = {
1221            "windows": "{}chromedriver.exe",
1222            "mac": "{}chromedriver",
1223            "default": "{}chromedriver",
1224        }
1225
1226        if "--app=chrome" in extra_options or "--app=chrome-m" in extra_options:
1227            # Only add the chromedriver fetches when chrome is running
1228            for platform in cd_fetches:
1229                fs["by-test-platform"][platform].extend(cd_fetches[platform])
1230        if "--app=chromium" in extra_options:
1231            for platform in chromium_fetches:
1232                fs["by-test-platform"][platform].extend(chromium_fetches[platform])
1233
1234            # The chromedrivers for chromium are repackaged into the archives
1235            # that we get the chromium binary from so we always have a compatible
1236            # version.
1237            cd_extracted_name = {
1238                "windows": "chrome-win/chromedriver.exe",
1239                "mac": "chrome-mac/chromedriver",
1240                "default": "chrome-linux/chromedriver",
1241            }
1242
1243        # Disable the Raptor install step
1244        if "--app=chrome-m" in extra_options:
1245            extra_options.append("--noinstall")
1246
1247        task.setdefault("fetches", {}).setdefault("fetch", []).extend(
1248            evaluate_keyed_by(fs, "fetches.fetch", task)
1249        )
1250
1251        extra_options.extend(
1252            (
1253                "--browsertime-browsertimejs",
1254                "$MOZ_FETCHES_DIR/browsertime/node_modules/browsertime/bin/browsertime.js",
1255            )
1256        )  # noqa: E501
1257
1258        eos = {
1259            "by-test-platform": {
1260                "windows.*": [
1261                    "--browsertime-node",
1262                    "$MOZ_FETCHES_DIR/node/node.exe",
1263                    "--browsertime-geckodriver",
1264                    "$MOZ_FETCHES_DIR/geckodriver.exe",
1265                    "--browsertime-chromedriver",
1266                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["windows"],
1267                    "--browsertime-ffmpeg",
1268                    "$MOZ_FETCHES_DIR/ffmpeg-4.1.1-win64-static/bin/ffmpeg.exe",
1269                ],
1270                "macosx.*": [
1271                    "--browsertime-node",
1272                    "$MOZ_FETCHES_DIR/node/bin/node",
1273                    "--browsertime-geckodriver",
1274                    "$MOZ_FETCHES_DIR/geckodriver",
1275                    "--browsertime-chromedriver",
1276                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["mac"],
1277                    "--browsertime-ffmpeg",
1278                    "$MOZ_FETCHES_DIR/ffmpeg-4.1.1-macos64-static/bin/ffmpeg",
1279                ],
1280                "default": [
1281                    "--browsertime-node",
1282                    "$MOZ_FETCHES_DIR/node/bin/node",
1283                    "--browsertime-geckodriver",
1284                    "$MOZ_FETCHES_DIR/geckodriver",
1285                    "--browsertime-chromedriver",
1286                    "$MOZ_FETCHES_DIR/" + cd_extracted_name["default"],
1287                    "--browsertime-ffmpeg",
1288                    "$MOZ_FETCHES_DIR/ffmpeg-4.1.4-i686-static/ffmpeg",
1289                ],
1290            }
1291        }
1292
1293        extra_options.extend(evaluate_keyed_by(eos, "mozharness.extra-options", task))
1294
1295        yield task
1296
1297
1298def get_mobile_project(task):
1299    """Returns the mobile project of the specified task or None."""
1300
1301    if not task["build-platform"].startswith("android"):
1302        return
1303
1304    mobile_projects = ("fenix", "geckoview", "refbrow", "chrome-m")
1305
1306    for name in mobile_projects:
1307        if name in task["test-name"]:
1308            return name
1309
1310    target = task.get("target")
1311    if target:
1312        if isinstance(target, dict):
1313            target = target["name"]
1314
1315        for name in mobile_projects:
1316            if name in target:
1317                return name
1318
1319    return None
1320
1321
1322@transforms.add
1323def adjust_mobile_e10s(config, tasks):
1324    for task in tasks:
1325        project = get_mobile_project(task)
1326        if project == "geckoview":
1327            # Geckoview is always-e10s
1328            task["e10s"] = True
1329        yield task
1330
1331
1332@transforms.add
1333def disable_wpt_timeouts_on_autoland(config, tasks):
1334    """do not run web-platform-tests that are expected TIMEOUT on autoland"""
1335    for task in tasks:
1336        if (
1337            "web-platform-tests" in task["test-name"]
1338            and config.params["project"] == "autoland"
1339        ):
1340            task["mozharness"].setdefault("extra-options", []).append("--skip-timeout")
1341        yield task
1342
1343
1344@transforms.add
1345def split_variants(config, tasks):
1346    for task in tasks:
1347        variants = task.pop("variants", [])
1348
1349        yield copy.deepcopy(task)
1350
1351        for name in variants:
1352            variant = TEST_VARIANTS[name]
1353
1354            if "filterfn" in variant and not variant["filterfn"](task):
1355                continue
1356
1357            taskv = copy.deepcopy(task)
1358            taskv["attributes"]["unittest_variant"] = name
1359            taskv["description"] = variant["description"].format(**taskv)
1360
1361            suffix = "-" + variant["suffix"]
1362            taskv["test-name"] += suffix
1363            taskv["try-name"] += suffix
1364
1365            group, symbol = split_symbol(taskv["treeherder-symbol"])
1366            if group != "?":
1367                group += suffix
1368            else:
1369                symbol += suffix
1370            taskv["treeherder-symbol"] = join_symbol(group, symbol)
1371
1372            taskv.update(variant.get("replace", {}))
1373            yield merge(taskv, variant.get("merge", {}))
1374
1375
1376@transforms.add
1377def handle_keyed_by_variant(config, tasks):
1378    """Resolve fields that can be keyed by platform, etc."""
1379    fields = [
1380        "run-on-projects",
1381        "tier",
1382    ]
1383    for task in tasks:
1384        for field in fields:
1385            resolve_keyed_by(
1386                task,
1387                field,
1388                item_name=task["test-name"],
1389                enforce_single_match=False,
1390                variant=task["attributes"].get("unittest_variant"),
1391            )
1392        yield task
1393
1394
1395@transforms.add
1396def enable_code_coverage(config, tasks):
1397    """Enable code coverage for the ccov build-platforms"""
1398    for task in tasks:
1399        if "ccov" in task["build-platform"]:
1400            # Do not run tests on fuzzing builds
1401            if "fuzzing" in task["build-platform"]:
1402                task["run-on-projects"] = []
1403                continue
1404
1405            # Skip this transform for android code coverage builds.
1406            if "android" in task["build-platform"]:
1407                task.setdefault("fetches", {}).setdefault("toolchain", []).append(
1408                    "linux64-grcov"
1409                )
1410                task["mozharness"].setdefault("extra-options", []).append(
1411                    "--java-code-coverage"
1412                )
1413                yield task
1414                continue
1415            task["mozharness"].setdefault("extra-options", []).append("--code-coverage")
1416            task["instance-size"] = "xlarge"
1417
1418            # Temporarily disable Mac tests on mozilla-central
1419            if "mac" in task["build-platform"]:
1420                task["run-on-projects"] = []
1421
1422            # Ensure we always run on the projects defined by the build, unless the test
1423            # is try only or shouldn't run at all.
1424            if task["run-on-projects"] not in [[]]:
1425                task["run-on-projects"] = "built-projects"
1426
1427            # Ensure we don't optimize test suites out.
1428            # We always want to run all test suites for coverage purposes.
1429            task.pop("schedules-component", None)
1430            task.pop("when", None)
1431            task["optimization"] = None
1432
1433            # Add a toolchain and a fetch task for the grcov binary.
1434            if any(p in task["build-platform"] for p in ("linux", "osx", "win")):
1435                task.setdefault("fetches", {})
1436                task["fetches"].setdefault("fetch", [])
1437                task["fetches"].setdefault("toolchain", [])
1438
1439            if "linux" in task["build-platform"]:
1440                task["fetches"]["toolchain"].append("linux64-grcov")
1441            elif "osx" in task["build-platform"]:
1442                task["fetches"]["fetch"].append("grcov-osx-x86_64")
1443            elif "win" in task["build-platform"]:
1444                task["fetches"]["toolchain"].append("win64-grcov")
1445
1446            if "talos" in task["test-name"]:
1447                task["max-run-time"] = 7200
1448                if "linux" in task["build-platform"]:
1449                    task["docker-image"] = {"in-tree": "ubuntu1804-test"}
1450                task["mozharness"]["extra-options"].append("--add-option")
1451                task["mozharness"]["extra-options"].append("--cycles,1")
1452                task["mozharness"]["extra-options"].append("--add-option")
1453                task["mozharness"]["extra-options"].append("--tppagecycles,1")
1454                task["mozharness"]["extra-options"].append("--add-option")
1455                task["mozharness"]["extra-options"].append("--no-upload-results")
1456                task["mozharness"]["extra-options"].append("--add-option")
1457                task["mozharness"]["extra-options"].append("--tptimeout,15000")
1458            if "raptor" in task["test-name"]:
1459                task["max-run-time"] = 1800
1460        yield task
1461
1462
1463@transforms.add
1464def handle_run_on_projects(config, tasks):
1465    """Handle translating `built-projects` appropriately"""
1466    for task in tasks:
1467        if task["run-on-projects"] == "built-projects":
1468            task["run-on-projects"] = task["build-attributes"].get(
1469                "run_on_projects", ["all"]
1470            )
1471
1472        if task.pop("built-projects-only", False):
1473            built_projects = set(
1474                task["build-attributes"].get("run_on_projects", {"all"})
1475            )
1476            run_on_projects = set(task.get("run-on-projects", set()))
1477
1478            # If 'all' exists in run-on-projects, then the intersection of both
1479            # is built-projects. Similarly if 'all' exists in built-projects,
1480            # the intersection is run-on-projects (so do nothing). When neither
1481            # contains 'all', take the actual set intersection.
1482            if "all" in run_on_projects:
1483                task["run-on-projects"] = sorted(built_projects)
1484            elif "all" not in built_projects:
1485                task["run-on-projects"] = sorted(run_on_projects & built_projects)
1486        yield task
1487
1488
1489@transforms.add
1490def handle_tier(config, tasks):
1491    """Set the tier based on policy for all test descriptions that do not
1492    specify a tier otherwise."""
1493    for task in tasks:
1494        if "tier" in task:
1495            resolve_keyed_by(
1496                task, "tier", item_name=task["test-name"], enforce_single_match=False
1497            )
1498
1499        # only override if not set for the test
1500        if "tier" not in task or task["tier"] == "default":
1501            if task["test-platform"] in [
1502                "linux64/opt",
1503                "linux64/debug",
1504                "linux64-shippable/opt",
1505                "linux64-devedition/opt",
1506                "linux64-asan/opt",
1507                "linux64-qr/opt",
1508                "linux64-qr/debug",
1509                "linux64-shippable-qr/opt",
1510                "linux1804-64/opt",
1511                "linux1804-64/debug",
1512                "linux1804-64-shippable/opt",
1513                "linux1804-64-devedition/opt",
1514                "linux1804-64-qr/opt",
1515                "linux1804-64-qr/debug",
1516                "linux1804-64-shippable-qr/opt",
1517                "linux1804-64-asan-qr/opt",
1518                "linux1804-64-tsan-qr/opt",
1519                "windows7-32-qr/debug",
1520                "windows7-32-qr/opt",
1521                "windows7-32-devedition-qr/opt",
1522                "windows7-32-shippable-qr/opt",
1523                "windows10-32-qr/debug",
1524                "windows10-32-qr/opt",
1525                "windows10-32-shippable-qr/opt",
1526                "windows10-32-2004-qr/debug",
1527                "windows10-32-2004-qr/opt",
1528                "windows10-32-2004-shippable-qr/opt",
1529                "windows10-aarch64-qr/opt",
1530                "windows10-64/debug",
1531                "windows10-64/opt",
1532                "windows10-64-shippable/opt",
1533                "windows10-64-devedition/opt",
1534                "windows10-64-qr/opt",
1535                "windows10-64-qr/debug",
1536                "windows10-64-shippable-qr/opt",
1537                "windows10-64-devedition-qr/opt",
1538                "windows10-64-asan-qr/opt",
1539                "windows10-64-2004-qr/opt",
1540                "windows10-64-2004-qr/debug",
1541                "windows10-64-2004-shippable-qr/opt",
1542                "windows10-64-2004-devedition-qr/opt",
1543                "windows10-64-2004-asan-qr/opt",
1544                "macosx1014-64/opt",
1545                "macosx1014-64/debug",
1546                "macosx1014-64-shippable/opt",
1547                "macosx1014-64-devedition/opt",
1548                "macosx1014-64-devedition-qr/opt",
1549                "macosx1014-64-qr/opt",
1550                "macosx1014-64-shippable-qr/opt",
1551                "macosx1014-64-qr/debug",
1552                "macosx1015-64/opt",
1553                "macosx1015-64/debug",
1554                "macosx1015-64-shippable/opt",
1555                "macosx1015-64-devedition/opt",
1556                "macosx1015-64-devedition-qr/opt",
1557                "macosx1015-64-qr/opt",
1558                "macosx1015-64-shippable-qr/opt",
1559                "macosx1015-64-qr/debug",
1560                "android-em-7.0-x86_64-shippable/opt",
1561                "android-em-7.0-x86_64/debug",
1562                "android-em-7.0-x86_64/debug-isolated-process",
1563                "android-em-7.0-x86_64/opt",
1564                "android-em-7.0-x86-shippable/opt",
1565                "android-em-7.0-x86_64-shippable-qr/opt",
1566                "android-em-7.0-x86_64-qr/debug",
1567                "android-em-7.0-x86_64-qr/opt",
1568            ]:
1569                task["tier"] = 1
1570            else:
1571                task["tier"] = 2
1572
1573        yield task
1574
1575
1576@transforms.add
1577def apply_raptor_tier_optimization(config, tasks):
1578    for task in tasks:
1579        if task["suite"] != "raptor":
1580            yield task
1581            continue
1582
1583        if not task["test-platform"].startswith("android-hw"):
1584            task["optimization"] = {"skip-unless-expanded": None}
1585            if task["tier"] > 1:
1586                task["optimization"] = {"skip-unless-backstop": None}
1587
1588        if task["attributes"].get("unittest_variant"):
1589            task["tier"] = max(task["tier"], 2)
1590        yield task
1591
1592
1593@transforms.add
1594def disable_try_only_platforms(config, tasks):
1595    """Turns off platforms that should only run on try."""
1596    try_only_platforms = ()
1597    for task in tasks:
1598        if any(re.match(k + "$", task["test-platform"]) for k in try_only_platforms):
1599            task["run-on-projects"] = []
1600        yield task
1601
1602
1603@transforms.add
1604def ensure_spi_disabled_on_all_but_spi(config, tasks):
1605    for task in tasks:
1606        variant = task["attributes"].get("unittest_variant", "")
1607        has_setpref = (
1608            "gtest" not in task["suite"]
1609            and "cppunit" not in task["suite"]
1610            and "jittest" not in task["suite"]
1611            and "junit" not in task["suite"]
1612            and "raptor" not in task["suite"]
1613        )
1614
1615        if (
1616            has_setpref
1617            and variant != "socketprocess"
1618            and variant != "socketprocess_networking"
1619        ):
1620            task["mozharness"]["extra-options"].append(
1621                "--setpref=media.peerconnection.mtransport_process=false"
1622            )
1623            task["mozharness"]["extra-options"].append(
1624                "--setpref=network.process.enabled=false"
1625            )
1626
1627        yield task
1628
1629
1630@transforms.add
1631def split_e10s(config, tasks):
1632    for task in tasks:
1633        e10s = task["e10s"]
1634
1635        if e10s:
1636            task_copy = copy.deepcopy(task)
1637            task_copy["test-name"] += "-e10s"
1638            task_copy["e10s"] = True
1639            task_copy["attributes"]["e10s"] = True
1640            yield task_copy
1641
1642        if not e10s or e10s == "both":
1643            task["test-name"] += "-1proc"
1644            task["try-name"] += "-1proc"
1645            task["e10s"] = False
1646            task["attributes"]["e10s"] = False
1647            group, symbol = split_symbol(task["treeherder-symbol"])
1648            if group != "?":
1649                group += "-1proc"
1650            task["treeherder-symbol"] = join_symbol(group, symbol)
1651            task["mozharness"]["extra-options"].append("--disable-e10s")
1652            yield task
1653
1654
1655@transforms.add
1656def set_test_verify_chunks(config, tasks):
1657    """Set the number of chunks we use for test-verify."""
1658    for task in tasks:
1659        if any(task["suite"].startswith(s) for s in ("test-verify", "test-coverage")):
1660            env = config.params.get("try_task_config", {}) or {}
1661            env = env.get("templates", {}).get("env", {})
1662            task["chunks"] = perfile_number_of_chunks(
1663                config.params.is_try(),
1664                env.get("MOZHARNESS_TEST_PATHS", ""),
1665                config.params.get("head_repository", ""),
1666                config.params.get("head_rev", ""),
1667                task["test-name"],
1668            )
1669
1670            # limit the number of chunks we run for test-verify mode because
1671            # test-verify is comprehensive and takes a lot of time, if we have
1672            # >30 tests changed, this is probably an import of external tests,
1673            # or a patch renaming/moving files in bulk
1674            maximum_number_verify_chunks = 3
1675            if task["chunks"] > maximum_number_verify_chunks:
1676                task["chunks"] = maximum_number_verify_chunks
1677
1678        yield task
1679
1680
1681@transforms.add
1682def set_test_manifests(config, tasks):
1683    """Determine the set of test manifests that should run in this task."""
1684
1685    for task in tasks:
1686        # When a task explicitly requests no 'test_manifest_loader', test
1687        # resolving will happen at test runtime rather than in the taskgraph.
1688        if "test-manifest-loader" in task and task["test-manifest-loader"] is None:
1689            yield task
1690            continue
1691
1692        # Set 'tests_grouped' to "1", so we can differentiate between suites that are
1693        # chunked at the test runtime and those that are chunked in the taskgraph.
1694        task.setdefault("tags", {})["tests_grouped"] = "1"
1695
1696        if taskgraph.fast:
1697            # We want to avoid evaluating manifests when taskgraph.fast is set. But
1698            # manifests are required for dynamic chunking. Just set the number of
1699            # chunks to one in this case.
1700            if task["chunks"] == "dynamic":
1701                task["chunks"] = 1
1702            yield task
1703            continue
1704
1705        manifests = task.get("test-manifests")
1706        if manifests:
1707            if isinstance(manifests, list):
1708                task["test-manifests"] = {"active": manifests, "skipped": []}
1709            yield task
1710            continue
1711
1712        mozinfo = guess_mozinfo_from_task(task)
1713
1714        loader_name = task.pop(
1715            "test-manifest-loader", config.params["test_manifest_loader"]
1716        )
1717        loader = get_manifest_loader(loader_name, config.params)
1718
1719        task["test-manifests"] = loader.get_manifests(
1720            task["suite"],
1721            frozenset(mozinfo.items()),
1722        )
1723
1724        # The default loader loads all manifests. If we use a non-default
1725        # loader, we'll only run some subset of manifests and the hardcoded
1726        # chunk numbers will no longer be valid. Dynamic chunking should yield
1727        # better results.
1728        if not isinstance(loader, DefaultLoader):
1729            task["chunks"] = "dynamic"
1730
1731        yield task
1732
1733
1734@transforms.add
1735def resolve_dynamic_chunks(config, tasks):
1736    """Determine how many chunks are needed to handle the given set of manifests."""
1737
1738    for task in tasks:
1739        if task["chunks"] != "dynamic":
1740            yield task
1741            continue
1742
1743        if not task.get("test-manifests"):
1744            raise Exception(
1745                "{} must define 'test-manifests' to use dynamic chunking!".format(
1746                    task["test-name"]
1747                )
1748            )
1749
1750        runtimes = {
1751            m: r
1752            for m, r in get_runtimes(task["test-platform"], task["suite"]).items()
1753            if m in task["test-manifests"]["active"]
1754        }
1755
1756        # Truncate runtimes that are above the desired chunk duration. They
1757        # will be assigned to a chunk on their own and the excess duration
1758        # shouldn't cause additional chunks to be needed.
1759        times = [min(DYNAMIC_CHUNK_DURATION, r) for r in runtimes.values()]
1760        avg = round(sum(times) / len(times), 2) if times else 0
1761        total = sum(times)
1762
1763        # If there are manifests missing from the runtimes data, fill them in
1764        # with the average of all present manifests.
1765        missing = [m for m in task["test-manifests"]["active"] if m not in runtimes]
1766        total += avg * len(missing)
1767
1768        # Apply any chunk multipliers if found.
1769        key = "{}-{}".format(task["test-platform"], task["test-name"])
1770        matches = keymatch(DYNAMIC_CHUNK_MULTIPLIER, key)
1771        if len(matches) > 1:
1772            raise Exception(
1773                "Multiple matching values for {} found while "
1774                "determining dynamic chunk multiplier!".format(key)
1775            )
1776        elif matches:
1777            total = total * matches[0]
1778
1779        chunks = int(round(total / DYNAMIC_CHUNK_DURATION))
1780
1781        # Make sure we never exceed the number of manifests, nor have a chunk
1782        # length of 0.
1783        task["chunks"] = min(chunks, len(task["test-manifests"]["active"])) or 1
1784        yield task
1785
1786
1787@transforms.add
1788def split_chunks(config, tasks):
1789    """Based on the 'chunks' key, split tests up into chunks by duplicating
1790    them and assigning 'this-chunk' appropriately and updating the treeherder
1791    symbol.
1792    """
1793
1794    for task in tasks:
1795        # If test-manifests are set, chunk them ahead of time to avoid running
1796        # the algorithm more than once.
1797        chunked_manifests = None
1798        if "test-manifests" in task:
1799            manifests = task["test-manifests"]
1800            chunked_manifests = chunk_manifests(
1801                task["suite"],
1802                task["test-platform"],
1803                task["chunks"],
1804                manifests["active"],
1805            )
1806
1807            # Add all skipped manifests to the first chunk of backstop pushes
1808            # so they still show up in the logs. They won't impact runtime much
1809            # and this way tools like ActiveData are still aware that they
1810            # exist.
1811            if config.params["backstop"] and manifests["active"]:
1812                chunked_manifests[0].extend(manifests["skipped"])
1813
1814        for i in range(task["chunks"]):
1815            this_chunk = i + 1
1816
1817            # copy the test and update with the chunk number
1818            chunked = copy.deepcopy(task)
1819            chunked["this-chunk"] = this_chunk
1820
1821            if chunked_manifests is not None:
1822                chunked["test-manifests"] = sorted(chunked_manifests[i])
1823
1824            group, symbol = split_symbol(chunked["treeherder-symbol"])
1825            if task["chunks"] > 1 or not symbol:
1826                # add the chunk number to the TH symbol
1827                symbol += str(this_chunk)
1828                chunked["treeherder-symbol"] = join_symbol(group, symbol)
1829
1830            yield chunked
1831
1832
1833@transforms.add
1834def allow_software_gl_layers(config, tasks):
1835    """
1836    Handle the "allow-software-gl-layers" property for platforms where it
1837    applies.
1838    """
1839    for task in tasks:
1840        if task.get("allow-software-gl-layers"):
1841            # This should be set always once bug 1296086 is resolved.
1842            task["mozharness"].setdefault("extra-options", []).append(
1843                "--allow-software-gl-layers"
1844            )
1845
1846        yield task
1847
1848
1849@transforms.add
1850def enable_webrender(config, tasks):
1851    """
1852    Handle the "webrender" property by passing a flag to mozharness if it is
1853    enabled.
1854    """
1855    for task in tasks:
1856        if task.get("webrender"):
1857            extra_options = task["mozharness"].setdefault("extra-options", [])
1858            extra_options.append("--enable-webrender")
1859            # We only want to 'setpref' on tests that have a profile
1860            if not task["attributes"]["unittest_category"] in [
1861                "cppunittest",
1862                "geckoview-junit",
1863                "gtest",
1864                "jittest",
1865                "raptor",
1866            ]:
1867                extra_options.append("--setpref=layers.d3d11.enable-blacklist=false")
1868
1869            # run webrender variants on the projects specified on webrender-run-on-projects
1870            if task.get("webrender-run-on-projects") is not None:
1871                task["run-on-projects"] = task["webrender-run-on-projects"]
1872
1873        yield task
1874
1875
1876@transforms.add
1877def set_schedules_for_webrender_android(config, tasks):
1878    """android-hw has limited resources, we need webrender on phones"""
1879    for task in tasks:
1880        if task["suite"] in ["crashtest", "reftest"] and task[
1881            "test-platform"
1882        ].startswith("android-hw"):
1883            task["schedules-component"] = "android-hw-gfx"
1884        yield task
1885
1886
1887@transforms.add
1888def set_retry_exit_status(config, tasks):
1889    """Set the retry exit status to TBPL_RETRY, the value returned by mozharness
1890    scripts to indicate a transient failure that should be retried."""
1891    for task in tasks:
1892        task["retry-exit-status"] = [4]
1893        yield task
1894
1895
1896@transforms.add
1897def set_profile(config, tasks):
1898    """Set profiling mode for tests."""
1899    ttconfig = config.params["try_task_config"]
1900    profile = ttconfig.get("gecko-profile", False)
1901    settings = (
1902        "gecko-profile-interval",
1903        "gecko-profile-entries",
1904        "gecko-profile-threads",
1905        "gecko-profile-features",
1906    )
1907
1908    for task in tasks:
1909        if profile and task["suite"] in ["talos", "raptor"]:
1910            extras = task["mozharness"]["extra-options"]
1911            extras.append("--gecko-profile")
1912            for setting in settings:
1913                value = ttconfig.get(setting)
1914                if value is not None:
1915                    # These values can contain spaces (eg the "DOM Worker"
1916                    # thread) and the command is constructed in different,
1917                    # incompatible ways on different platforms.
1918
1919                    if task["test-platform"].startswith("win"):
1920                        # Double quotes for Windows (single won't work).
1921                        extras.append("--" + setting + '="' + str(value) + '"')
1922                    else:
1923                        # Other platforms keep things as separate values,
1924                        # rather than joining with spaces.
1925                        extras.append("--" + setting + "=" + str(value))
1926
1927        yield task
1928
1929
1930@transforms.add
1931def set_tag(config, tasks):
1932    """Set test for a specific tag."""
1933    tag = None
1934    if config.params["try_mode"] == "try_option_syntax":
1935        tag = config.params["try_options"]["tag"]
1936    for task in tasks:
1937        if tag:
1938            task["mozharness"]["extra-options"].extend(["--tag", tag])
1939        yield task
1940
1941
1942@transforms.add
1943def set_test_type(config, tasks):
1944    types = ["mochitest", "reftest", "talos", "raptor", "geckoview-junit", "gtest"]
1945    for task in tasks:
1946        for test_type in types:
1947            if test_type in task["suite"] and "web-platform" not in task["suite"]:
1948                task.setdefault("tags", {})["test-type"] = test_type
1949        yield task
1950
1951
1952@transforms.add
1953def set_worker_type(config, tasks):
1954    """Set the worker type based on the test platform."""
1955    for task in tasks:
1956        # during the taskcluster migration, this is a bit tortured, but it
1957        # will get simpler eventually!
1958        test_platform = task["test-platform"]
1959        if task.get("worker-type"):
1960            # This test already has its worker type defined, so just use that (yields below)
1961            pass
1962        elif test_platform.startswith("macosx1014-64"):
1963            task["worker-type"] = MACOSX_WORKER_TYPES["macosx1014-64"]
1964        elif test_platform.startswith("macosx1015-64"):
1965            if "--power-test" in task["mozharness"]["extra-options"]:
1966                task["worker-type"] = MACOSX_WORKER_TYPES["macosx1014-64-power"]
1967            else:
1968                task["worker-type"] = MACOSX_WORKER_TYPES["macosx1015-64"]
1969        elif test_platform.startswith("macosx1100-64"):
1970            task["worker-type"] = MACOSX_WORKER_TYPES["macosx1100-64"]
1971        elif test_platform.startswith("win"):
1972            # figure out what platform the job needs to run on
1973            if task["virtualization"] == "hardware":
1974                # some jobs like talos and reftest run on real h/w - those are all win10
1975                if test_platform.startswith("windows10-64-ref-hw-2017"):
1976                    win_worker_type_platform = WINDOWS_WORKER_TYPES[
1977                        "windows10-64-ref-hw-2017"
1978                    ]
1979                elif test_platform.startswith("windows10-aarch64-qr"):
1980                    win_worker_type_platform = WINDOWS_WORKER_TYPES[
1981                        "windows10-aarch64-qr"
1982                    ]
1983                else:
1984                    win_worker_type_platform = WINDOWS_WORKER_TYPES["windows10-64"]
1985            else:
1986                # the other jobs run on a vm which may or may not be a win10 vm
1987                win_worker_type_platform = WINDOWS_WORKER_TYPES[
1988                    test_platform.split("/")[0]
1989                ]
1990            # now we have the right platform set the worker type accordingly
1991            task["worker-type"] = win_worker_type_platform[task["virtualization"]]
1992        elif test_platform.startswith("android-hw-g5"):
1993            if task["suite"] != "raptor":
1994                task["worker-type"] = "t-bitbar-gw-unit-g5"
1995            else:
1996                task["worker-type"] = "t-bitbar-gw-perf-g5"
1997        elif test_platform.startswith("android-hw-p2"):
1998            if task["suite"] != "raptor":
1999                task["worker-type"] = "t-bitbar-gw-unit-p2"
2000            else:
2001                task["worker-type"] = "t-bitbar-gw-perf-p2"
2002        elif test_platform.startswith("android-hw-s7"):
2003            if task["suite"] != "raptor":
2004                task["worker-type"] = "t-bitbar-gw-unit-s7"
2005            else:
2006                task["worker-type"] = "t-bitbar-gw-perf-s7"
2007        elif test_platform.startswith("android-em-7.0-x86"):
2008            task["worker-type"] = "t-linux-metal"
2009        elif test_platform.startswith("linux") or test_platform.startswith("android"):
2010            if task.get("suite", "") in ["talos", "raptor"] and not task[
2011                "build-platform"
2012            ].startswith("linux64-ccov"):
2013                task["worker-type"] = "t-linux-talos-1804"
2014            else:
2015                task["worker-type"] = LINUX_WORKER_TYPES[task["instance-size"]]
2016        else:
2017            raise Exception("unknown test_platform {}".format(test_platform))
2018
2019        yield task
2020
2021
2022@transforms.add
2023def set_schedules_components(config, tasks):
2024    for task in tasks:
2025        if "optimization" in task or "when" in task:
2026            yield task
2027            continue
2028
2029        category = task["attributes"]["unittest_category"]
2030        schedules = task.get("schedules-component", category)
2031        if isinstance(schedules, string_types):
2032            schedules = [schedules]
2033
2034        schedules = set(schedules)
2035        if schedules & set(INCLUSIVE_COMPONENTS):
2036            # if this is an "inclusive" test, then all files which might
2037            # cause it to run are annotated with SCHEDULES in moz.build,
2038            # so do not include the platform or any other components here
2039            task["schedules-component"] = sorted(schedules)
2040            yield task
2041            continue
2042
2043        schedules.add(category)
2044        schedules.add(platform_family(task["build-platform"]))
2045
2046        if task["webrender"]:
2047            schedules.add("webrender")
2048
2049        task["schedules-component"] = sorted(schedules)
2050        yield task
2051
2052
2053@transforms.add
2054def make_job_description(config, tasks):
2055    """Convert *test* descriptions to *job* descriptions (input to
2056    taskgraph.transforms.job)"""
2057
2058    for task in tasks:
2059        mobile = get_mobile_project(task)
2060        if mobile and (mobile not in task["test-name"]):
2061            label = "{}-{}-{}-{}".format(
2062                config.kind, task["test-platform"], mobile, task["test-name"]
2063            )
2064        else:
2065            label = "{}-{}-{}".format(
2066                config.kind, task["test-platform"], task["test-name"]
2067            )
2068        if task["chunks"] > 1:
2069            label += "-{}".format(task["this-chunk"])
2070
2071        build_label = task["build-label"]
2072
2073        try_name = task["try-name"]
2074        if task["suite"] == "talos":
2075            attr_try_name = "talos_try_name"
2076        elif task["suite"] == "raptor":
2077            attr_try_name = "raptor_try_name"
2078        else:
2079            attr_try_name = "unittest_try_name"
2080
2081        attr_build_platform, attr_build_type = task["build-platform"].split("/", 1)
2082
2083        attributes = task.get("attributes", {})
2084        attributes.update(
2085            {
2086                "build_platform": attr_build_platform,
2087                "build_type": attr_build_type,
2088                "test_platform": task["test-platform"],
2089                "test_chunk": str(task["this-chunk"]),
2090                "supports-artifact-builds": task["supports-artifact-builds"],
2091                attr_try_name: try_name,
2092            }
2093        )
2094
2095        if "test-manifests" in task:
2096            attributes["test_manifests"] = task["test-manifests"]
2097
2098        jobdesc = {}
2099        name = "{}-{}".format(task["test-platform"], task["test-name"])
2100        jobdesc["name"] = name
2101        jobdesc["label"] = label
2102        jobdesc["description"] = task["description"]
2103        jobdesc["attributes"] = attributes
2104        jobdesc["dependencies"] = {"build": build_label}
2105        jobdesc["job-from"] = task["job-from"]
2106
2107        if task.get("fetches"):
2108            jobdesc["fetches"] = task["fetches"]
2109
2110        if task["mozharness"]["requires-signed-builds"] is True:
2111            jobdesc["dependencies"]["build-signing"] = task["build-signing-label"]
2112
2113        if "expires-after" in task:
2114            jobdesc["expires-after"] = task["expires-after"]
2115
2116        jobdesc["routes"] = []
2117        jobdesc["run-on-projects"] = sorted(task["run-on-projects"])
2118        jobdesc["scopes"] = []
2119        jobdesc["tags"] = task.get("tags", {})
2120        jobdesc["extra"] = {
2121            "chunks": {
2122                "current": task["this-chunk"],
2123                "total": task["chunks"],
2124            },
2125            "suite": attributes["unittest_suite"],
2126        }
2127        jobdesc["treeherder"] = {
2128            "symbol": task["treeherder-symbol"],
2129            "kind": "test",
2130            "tier": task["tier"],
2131            "platform": task.get("treeherder-machine-platform", task["build-platform"]),
2132        }
2133
2134        schedules = task.get("schedules-component", [])
2135        if task.get("when"):
2136            # This may still be used by comm-central.
2137            jobdesc["when"] = task["when"]
2138        elif "optimization" in task:
2139            jobdesc["optimization"] = task["optimization"]
2140        elif set(schedules) & set(INCLUSIVE_COMPONENTS):
2141            jobdesc["optimization"] = {"test-inclusive": schedules}
2142        else:
2143            jobdesc["optimization"] = {"test": schedules}
2144
2145        run = jobdesc["run"] = {}
2146        run["using"] = "mozharness-test"
2147        run["test"] = task
2148
2149        if "workdir" in task:
2150            run["workdir"] = task.pop("workdir")
2151
2152        jobdesc["worker-type"] = task.pop("worker-type")
2153        if task.get("fetches"):
2154            jobdesc["fetches"] = task.pop("fetches")
2155
2156        yield jobdesc
2157
2158
2159def normpath(path):
2160    return path.replace("/", "\\")
2161
2162
2163def get_firefox_version():
2164    with open("browser/config/version.txt", "r") as f:
2165        return f.readline().strip()
2166