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/.
10 from collections
import defaultdict
12 import mozpack
.path
as mozpath
14 from mach
.util
import get_state_dir
15 from mozbuild
.base
import MozbuildObject
16 from mozpack
.files
import FileFinder
17 from moztest
.resolve
import TestManifestLoader
, TestResolver
, get_suite_definition
18 from taskgraph
.generator
import TaskGraphGenerator
19 from taskgraph
.parameters
import ParameterMismatch
, parameters_loader
20 from taskgraph
.taskgraph
import TaskGraph
22 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
23 build
= MozbuildObject
.from_environment(cwd
=here
)
25 PARAMETER_MISMATCH
= """
26 ERROR - The parameters being used to generate tasks differ from those expected
31 To fix this, either rebase onto the latest mozilla-central or pass in
32 -p/--parameters. For more information on how to define parameters, see:
33 https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/mach.html#parameters
37 def invalidate(cache
):
39 cmod
= os
.path
.getmtime(cache
)
41 # File does not exist. We catch OSError rather than use `isfile`
42 # because the recommended watchman hook could possibly invalidate the
43 # cache in-between the check to `isfile` and the call to `getmtime`
49 tc_dir
= os
.path
.join(build
.topsrcdir
, "taskcluster")
50 tmod
= max(os
.path
.getmtime(os
.path
.join(tc_dir
, p
)) for p
, _
in FileFinder(tc_dir
))
56 def cache_key(attr
, params
, disable_target_task_filter
):
58 if params
and params
["project"] not in ("autoland", "mozilla-central"):
59 key
+= f
"-{params['project']}"
61 if disable_target_task_filter
and "full" not in attr
:
66 def generate_tasks(params
=None, full
=False, disable_target_task_filter
=False):
67 attr
= "full_task_set" if full
else "target_task_set"
68 target_tasks_method
= (
70 if not disable_target_task_filter
71 else "try_select_tasks_uncommon"
73 params
= parameters_loader(
77 "try_mode": "try_select",
78 "target_tasks_method": target_tasks_method
,
81 root
= os
.path
.join(build
.topsrcdir
, "taskcluster", "ci")
83 generator
= TaskGraphGenerator(root_dir
=root
, parameters
=params
)
85 def add_chunk_patterns(tg
):
86 for task_name
, task
in tg
.tasks
.items():
87 chunks
= task
.task
.get("extra", {}).get("chunks", {})
88 if isinstance(chunks
, int):
89 task
.chunk_pattern
= "{}-*/{}".format(
90 "-".join(task_name
.split("-")[:-1]), chunks
93 assert isinstance(chunks
, dict)
94 if chunks
.get("total", 1) == 1:
95 task
.chunk_pattern
= task_name
97 task
.chunk_pattern
= "{}-*".format(
98 "-".join(task_name
.split("-")[:-1])
102 cache_dir
= os
.path
.join(
103 get_state_dir(specific_to_topsrcdir
=True), "cache", "taskgraph"
105 key
= cache_key(attr
, generator
.parameters
, disable_target_task_filter
)
106 cache
= os
.path
.join(cache_dir
, key
)
109 if os
.path
.isfile(cache
):
110 with
open(cache
) as fh
:
111 return add_chunk_patterns(TaskGraph
.from_json(json
.load(fh
))[1])
113 if not os
.path
.isdir(cache_dir
):
114 os
.makedirs(cache_dir
)
116 print("Task configuration changed, generating {}".format(attr
.replace("_", " ")))
119 os
.chdir(build
.topsrcdir
)
123 tg
= getattr(generator
, attr
)
124 except ParameterMismatch
as e
:
125 print(PARAMETER_MISMATCH
.format(e
.args
[0]))
129 key
= cache_key(attr
, generator
.parameters
, disable_target_task_filter
)
130 with
open(os
.path
.join(cache_dir
, key
), "w") as fh
:
131 json
.dump(tg
.to_json(), fh
)
132 return add_chunk_patterns(tg
)
134 # Cache both full_task_set and target_task_set regardless of whether or not
135 # --full was requested. Caching is cheap and can potentially save a lot of
137 tg_full
= generate("full_task_set")
138 tg_target
= generate("target_task_set")
140 # discard results from these, we only need cache.
142 generate("full_task_graph")
143 generate("target_task_graph")
151 def filter_tasks_by_paths(tasks
, paths
):
152 resolver
= TestResolver
.from_environment(cwd
=here
, loader_cls
=TestManifestLoader
)
153 run_suites
, run_tests
= resolver
.resolve_metadata(paths
)
154 flavors
= {(t
["flavor"], t
.get("subsuite")) for t
in run_tests
}
157 for flavor
, subsuite
in flavors
:
158 _
, suite
= get_suite_definition(flavor
, subsuite
, strict
=True)
159 if "task_regex" not in suite
:
161 "warning: no tasks could be resolved from flavor '{}'{}".format(
162 flavor
, " and subsuite '{}'".format(subsuite
) if subsuite
else ""
167 task_regexes
.update(suite
["task_regex"])
169 def match_task(task
):
170 return any(re
.search(pattern
, task
) for pattern
in task_regexes
)
173 task_name
: task
for task_name
, task
in tasks
.items() if match_task(task_name
)
177 def resolve_tests_by_suite(paths
):
178 resolver
= TestResolver
.from_environment(cwd
=here
, loader_cls
=TestManifestLoader
)
179 _
, run_tests
= resolver
.resolve_metadata(paths
)
181 suite_to_tests
= defaultdict(list)
183 # A dictionary containing all the input paths that we haven't yet
184 # assigned to a specific test flavor.
185 remaining_paths_by_suite
= defaultdict(lambda: set(paths
))
187 for test
in run_tests
:
188 key
, _
= get_suite_definition(test
["flavor"], test
.get("subsuite"), strict
=True)
190 test_path
= test
.get("srcdir_relpath")
191 if test_path
is None:
194 manifest_relpath
= None
195 if "manifest_relpath" in test
:
196 manifest_relpath
= mozpath
.normpath(test
["manifest_relpath"])
197 for path
in remaining_paths_by_suite
[key
]:
198 if test_path
.startswith(path
) or manifest_relpath
== path
:
202 suite_to_tests
[key
].append(found_path
)
203 remaining_paths_by_suite
[key
].remove(found_path
)
205 return suite_to_tests