Skip to content

Commit a28e1c2

Browse files
committed
fix!: ensure if-dependencies apply at target phase
BREAKING CHANGE: Tasks will no longer pull `if-dependencies` in at the target phase. It's now possible for a task with multiple `if-dependencies` to run after one task, without also causing the other to run.
1 parent 6d0b1e2 commit a28e1c2

5 files changed

Lines changed: 108 additions & 2 deletions

File tree

docs/reference/migrations.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ Migration Guide
33

44
This page can help when migrating Taskgraph across major versions.
55

6+
19.x -> 20.x
7+
------------
8+
9+
* Tasks using ``if-dependencies`` will no longer pull these dependencies into
10+
the graph during the target phase. If a desired dependency is no longer
11+
showing up in the target graph, adjust your `target_tasks` logic to ensure
12+
the dependency is explicitly selected.
13+
614
18.x -> 19.x
715
------------
816

src/taskgraph/generator.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,22 @@ def load_tasks():
533533
f"Adding {len(always_target_tasks) - len(always_target_tasks & target_tasks)} tasks with `always_target` attribute" # type: ignore
534534
)
535535
requested_tasks = target_tasks | always_target_tasks # type: ignore
536-
target_graph = full_task_graph.graph.transitive_closure(requested_tasks)
536+
537+
# Exclude unrequested if-dependency edges from transitive closure.
538+
exclude_edges = set()
539+
for task in all_tasks.values():
540+
if missing_if_deps := set(task.if_dependencies) - requested_tasks:
541+
exclude_edges.update(
542+
{
543+
(task.label, label, edge)
544+
for edge, label in task.dependencies.items()
545+
if label in missing_if_deps
546+
}
547+
)
548+
549+
target_graph = full_task_graph.graph.transitive_closure(
550+
requested_tasks, exclude_edges=exclude_edges
551+
)
537552
target_task_graph = TaskGraph(
538553
{l: all_tasks[l] for l in target_graph.nodes},
539554
target_graph,

src/taskgraph/graph.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,17 @@ class Graph(_Graph):
3434
def __init__(self, nodes, edges):
3535
super().__init__(frozenset(nodes), frozenset(edges))
3636

37-
def transitive_closure(self, nodes, reverse=False):
37+
def transitive_closure(self, nodes, reverse=False, exclude_edges=None):
3838
"""Return the transitive closure of <nodes>: the graph containing all
3939
specified nodes as well as any nodes reachable from them, and any
4040
intervening edges.
4141
4242
If `reverse` is true, the "reachability" will be reversed and this
4343
will return the set of nodes that can reach the specified nodes.
4444
45+
If `exclude_edges` is provided, those edges will not be followed during
46+
traversal.
47+
4548
Example:
4649
4750
.. code-block::
@@ -61,6 +64,8 @@ def transitive_closure(self, nodes, reverse=False):
6164
f"Unknown nodes in transitive closure: {nodes - self.nodes}"
6265
)
6366

67+
exclude_edges = exclude_edges or set()
68+
6469
# generate a new graph by expanding along edges until reaching a fixed
6570
# point
6671
new_nodes, new_edges = nodes, set()
@@ -71,6 +76,7 @@ def transitive_closure(self, nodes, reverse=False):
7176
(left, right, name)
7277
for (left, right, name) in self.edges
7378
if (right if reverse else left) in nodes
79+
and (left, right, name) not in exclude_edges
7480
}
7581
add_nodes = {(left if reverse else right) for (left, right, _) in add_edges}
7682
new_nodes = nodes | add_nodes

test/test_generator.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,56 @@ def test_kind_graph_with_target_kinds(maketgg):
414414
# _fake3 and _other should not be included
415415
assert "_fake3" not in kind_graph.nodes
416416
assert "_other" not in kind_graph.nodes
417+
418+
419+
def test_if_dependencies_not_in_target_are_removed(maketgg):
420+
"If-dependencies not in requested_tasks don't pull tasks into target graph"
421+
tgg = maketgg(
422+
target_tasks=["_other-t-2"],
423+
kinds=[
424+
("_fake", {}),
425+
(
426+
"_other",
427+
{
428+
"task-defaults": {
429+
"dependencies": {"if-dep": "_fake-t-0"},
430+
"if-dependencies": ["_fake-t-0"],
431+
}
432+
},
433+
),
434+
],
435+
)
436+
437+
full_task = tgg.full_task_set.tasks["_other-t-2"]
438+
assert "_fake-t-0" in full_task.if_dependencies
439+
440+
target_graph = tgg.target_task_graph
441+
442+
assert "_other-t-2" in target_graph.tasks
443+
assert "_other-t-1" in target_graph.tasks
444+
assert "_other-t-0" in target_graph.tasks
445+
assert "_fake-t-0" not in target_graph.tasks
446+
447+
448+
def test_if_dependencies_in_target_are_kept(maketgg):
449+
"If-dependencies that are in requested_tasks are kept"
450+
tgg = maketgg(
451+
target_tasks=["_other-t-2", "_fake-t-0"],
452+
kinds=[
453+
("_fake", {}),
454+
(
455+
"_other",
456+
{
457+
"task-defaults": {
458+
"dependencies": {"if-dep": "_fake-t-0"},
459+
"if-dependencies": ["_fake-t-0"],
460+
}
461+
},
462+
),
463+
],
464+
)
465+
466+
target_graph = tgg.target_task_graph
467+
468+
assert "_other-t-2" in target_graph.tasks
469+
assert "_fake-t-0" in target_graph.tasks

test/test_graph.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,30 @@ def test_transitive_closure_loopy(self):
134134
"transitive closure of a loop is the whole loop"
135135
self.assertEqual(self.loopy.transitive_closure({"A"}), self.loopy)
136136

137+
def test_transitive_closure_exclude_edges(self):
138+
"transitive closure with excluded edges doesn't follow those edges"
139+
self.assertEqual(
140+
self.multi_edges.transitive_closure(
141+
{"3"}, exclude_edges={("3", "2", "green")}
142+
),
143+
Graph(
144+
{"1", "2", "3"},
145+
{
146+
("2", "1", "red"),
147+
("2", "1", "blue"),
148+
("3", "1", "red"),
149+
("3", "2", "blue"),
150+
},
151+
),
152+
)
153+
154+
self.assertEqual(
155+
self.multi_edges.transitive_closure(
156+
{"3"}, exclude_edges={("3", "2", "blue"), ("3", "2", "green")}
157+
),
158+
Graph({"1", "3"}, {("3", "1", "red")}),
159+
)
160+
137161
def test_visit_postorder_empty(self):
138162
"postorder visit of an empty graph is empty"
139163
self.assertEqual(list(Graph(set(), set()).visit_postorder()), [])

0 commit comments

Comments
 (0)