Skip to content

Commit 035afd7

Browse files
committed
Ensure that undefined optional outputs raise an error everywhere in graph.
1 parent ee842bf commit 035afd7

File tree

7 files changed

+43
-11
lines changed

7 files changed

+43
-11
lines changed

changes.d/6583.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix bug where graph items with undefined outputs were missed at validation if the graph item was not an upstream dependency of another graph item.

cylc/flow/config.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,6 +1844,7 @@ def generate_triggers(self, lexpression, left_nodes, right, seq,
18441844

18451845
triggers = {}
18461846
xtrig_labels = set()
1847+
18471848
for left in left_nodes:
18481849
if left.startswith('@'):
18491850
xtrig_labels.add(left[1:])
@@ -2265,6 +2266,17 @@ def load_graph(self):
22652266
self.workflow_polling_tasks.update(
22662267
parser.workflow_state_polling_tasks)
22672268
self._proc_triggers(parser, seq, task_triggers)
2269+
self.check_outputs(
2270+
[
2271+
task_output
2272+
for task_output in parser.task_output_opt
2273+
if task_output[0]
2274+
in [
2275+
task_output.split(':')[0]
2276+
for task_output in parser.terminals
2277+
]
2278+
]
2279+
)
22682280

22692281
self.set_required_outputs(task_output_opt)
22702282

@@ -2278,6 +2290,26 @@ def load_graph(self):
22782290
for tdef in self.taskdefs.values():
22792291
tdef.tweak_outputs()
22802292

2293+
def check_outputs(
2294+
self, tasks_and_outputs: Iterable[Tuple[str, str]]
2295+
) -> None:
2296+
"""Check that task outputs have been registered with tasks.
2297+
2298+
Args: tasks_and_outputs: ((task, output), ...)
2299+
2300+
Raises: WorkflowConfigError is a user has defined a task with a
2301+
custom output, but has not registered a custom output.
2302+
"""
2303+
for task, output in tasks_and_outputs:
2304+
registered_outputs = self.cfg['runtime'][task]['outputs']
2305+
if (
2306+
not TaskOutputs.is_valid_std_name(output)
2307+
and output not in registered_outputs
2308+
):
2309+
raise WorkflowConfigError(
2310+
f"Undefined custom output: {task}:{output}"
2311+
)
2312+
22812313
def _proc_triggers(self, parser, seq, task_triggers):
22822314
"""Define graph edges, taskdefs, and triggers, from graph sections."""
22832315
suicides = 0

cylc/flow/graph_parser.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,10 +470,12 @@ def parse_graph(self, graph_string: str) -> None:
470470
pairs.add((chain[i], chain[i + 1]))
471471

472472
# Get a set of RH nodes which are not at the LH of another pair:
473-
terminals = {p[1] for p in pairs}.difference({p[0] for p in pairs})
473+
self.terminals = {p[1] for p in pairs}.difference(
474+
{p[0] for p in pairs}
475+
)
474476

475477
for pair in sorted(pairs, key=lambda p: str(p[0])):
476-
self._proc_dep_pair(pair, terminals)
478+
self._proc_dep_pair(pair, self.terminals)
477479

478480
@classmethod
479481
def _report_invalid_lines(cls, lines: List[str]) -> None:

cylc/flow/task_outputs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def color_wrap(string, is_complete):
601601
@staticmethod
602602
def is_valid_std_name(name: str) -> bool:
603603
"""Check name is a valid standard output name."""
604-
return name in SORT_ORDERS
604+
return name in TASK_OUTPUTS
605605

606606
@staticmethod
607607
def output_sort_key(item: Iterable[str]) -> float:

tests/integration/test_dbstatecheck.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async def checker(
4545
},
4646
'runtime': {
4747
'bad': {'simulation': {'fail cycle points': '1000'}},
48-
'output': {'outputs': {'trigger': 'message'}}
48+
'output': {'outputs': {'trigger': 'message', 'custom_output': 'foo'}}
4949
}
5050
})
5151
schd: Scheduler = mod_scheduler(wid, paused_start=False)
@@ -119,13 +119,13 @@ def test_output(checker):
119119
'output',
120120
'10000101T0000Z',
121121
"{'submitted': 'submitted', 'started': 'started', 'succeeded': "
122-
"'succeeded', 'trigger': 'message'}",
122+
"'succeeded', 'trigger': 'message', 'custom_output': 'foo'}",
123123
],
124124
[
125125
'output',
126126
'10010101T0000Z',
127127
"{'submitted': 'submitted', 'started': 'started', 'succeeded': "
128-
"'succeeded', 'trigger': 'message'}",
128+
"'succeeded', 'trigger': 'message', 'custom_output': 'foo'}",
129129
],
130130
]
131131
assert result == expect

tests/integration/test_optional_outputs.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,7 @@ def implicit_completion_config(mod_flow, mod_validate):
288288
},
289289
'runtime': {
290290
'root': {
291-
'outputs': {
292-
'x': 'xxx',
293-
'y': 'yyy',
294-
'z': 'zzz',
295-
}
291+
'outputs': {x: f'{x * 3}' for x in 'abcdefghijklxyz'}
296292
}
297293
}
298294
})

tests/integration/validate/test_outputs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def test_completion_expression_invalid(
276276
'outputs': {
277277
'x': 'xxx',
278278
'y': 'yyy',
279+
'file-1': 'asdf'
279280
},
280281
},
281282
},

0 commit comments

Comments
 (0)