Skip to content

Commit d4edafa

Browse files
hjoliverwxtim
andauthored
Fix output consistency check when there are offsets. (#6526)
* Fix output consistency check when there are offsets. * Update change log. * Add graph parser unit tests. Co-authored-by: Tim Pillinger <[email protected]> * Deflake a test. * Sort pairs to make errors deterministic. * dict(was hiding stuff) * tweak variable name (review suggestion) --------- Co-authored-by: Tim Pillinger <[email protected]> Co-authored-by: Tim Pillinger <[email protected]>
1 parent d934619 commit d4edafa

File tree

6 files changed

+62
-28
lines changed

6 files changed

+62
-28
lines changed

changes.d/6526.fix.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Output optionality validation now checks tasks with cycle offsets.

cylc/flow/graph_parser.py

+17-18
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ class GraphParser:
251251
rf"""
252252
(!)? # suicide mark
253253
({_RE_NODE}) # node name
254+
({_RE_OFFSET})? # cycle point offset
254255
({_RE_QUAL})? # trigger qualifier
255256
({_RE_OPT})? # optional output indicator
256257
""",
@@ -469,10 +470,9 @@ def parse_graph(self, graph_string: str) -> None:
469470
pairs.add((chain[i], chain[i + 1]))
470471

471472
# Get a set of RH nodes which are not at the LH of another pair:
472-
pairs_dict = dict(pairs)
473-
terminals = set(pairs_dict.values()).difference(pairs_dict.keys())
473+
terminals = {p[1] for p in pairs}.difference({p[0] for p in pairs})
474474

475-
for pair in pairs:
475+
for pair in sorted(pairs, key=lambda p: str(p[0])):
476476
self._proc_dep_pair(pair, terminals)
477477

478478
@classmethod
@@ -540,16 +540,12 @@ def _proc_dep_pair(
540540
raise GraphParseError(mismatch_msg.format(right))
541541

542542
# Raise error for cycle point offsets at the end of chains
543-
if '[' in right:
544-
if left and (right in terminals):
545-
# This right hand side is at the end of a chain:
546-
raise GraphParseError(
547-
'Invalid cycle point offsets only on right hand '
548-
'side of a dependency (must be on left hand side):'
549-
f' {left} => {right}')
550-
else:
551-
# This RHS is also a LHS in a chain:
552-
return
543+
if '[' in right and left and (right in terminals):
544+
# This right hand side is at the end of a chain:
545+
raise GraphParseError(
546+
'Invalid cycle point offsets only on right hand '
547+
'side of a dependency (must be on left hand side):'
548+
f' {left} => {right}')
553549

554550
# Split right side on AND.
555551
rights = right.split(self.__class__.OP_AND)
@@ -887,15 +883,15 @@ def _compute_triggers(
887883
raise ValueError( # pragma: no cover
888884
f"Unexpected graph expression: '{right}'"
889885
)
890-
suicide_char, name, output, opt_char = m.groups()
886+
suicide_char, name, offset, output, opt_char = m.groups()
891887
suicide = (suicide_char == self.__class__.SUICIDE)
892888
optional = (opt_char == self.__class__.OPTIONAL)
893889
if output:
894890
output = output.strip(self.__class__.QUALIFIER)
895891

896892
if name in self.family_map:
897893
fam = True
898-
mems = self.family_map[name]
894+
rhs_members = self.family_map[name]
899895
if not output:
900896
# (Plain family name on RHS).
901897
# Make implicit success explicit.
@@ -922,10 +918,13 @@ def _compute_triggers(
922918
else:
923919
# Convert to standard output names if necessary.
924920
output = TaskTrigger.standardise_name(output)
925-
mems = [name]
921+
rhs_members = [name]
926922
outputs = [output]
927923

928-
for mem in mems:
929-
self._set_triggers(mem, suicide, trigs, expr, orig_expr)
924+
for mem in rhs_members:
925+
if not offset:
926+
# Nodes with offsets on the RHS do not define triggers.
927+
self._set_triggers(mem, suicide, trigs, expr, orig_expr)
930928
for output in outputs:
929+
# But they must be consistent with output optionality.
931930
self._set_output_opt(mem, output, optional, suicide, fam)

tests/flakyfunctional/cylc-show/04-multi.t

+12-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ outputs: ('⨯': not completed)
4444
⨯ 2016/t1 succeeded
4545
⨯ 2016/t1 failed
4646
output completion: incomplete
47-
⨯ ┆ succeeded
47+
┆ (
48+
✓ ┆ started
49+
⨯ ┆ and succeeded
50+
┆ )
4851
4952
Task ID: 2017/t1
5053
title: (not given)
@@ -61,7 +64,10 @@ outputs: ('⨯': not completed)
6164
⨯ 2017/t1 succeeded
6265
⨯ 2017/t1 failed
6366
output completion: incomplete
64-
⨯ ┆ succeeded
67+
┆ (
68+
✓ ┆ started
69+
⨯ ┆ and succeeded
70+
┆ )
6571
6672
Task ID: 2018/t1
6773
title: (not given)
@@ -78,7 +84,10 @@ outputs: ('⨯': not completed)
7884
⨯ 2018/t1 succeeded
7985
⨯ 2018/t1 failed
8086
output completion: incomplete
81-
⨯ ┆ succeeded
87+
┆ (
88+
✓ ┆ started
89+
⨯ ┆ and succeeded
90+
┆ )
8291
__TXT__
8392

8493
contains_ok "${RUND}/show2.txt" <<'__TXT__'

tests/functional/graphing/06-family-or.t

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ cat >'flow.cylc' <<'__FLOW_CONFIG__'
2626
initial cycle point = 2000
2727
[[graph]]
2828
T00 = """
29-
A
30-
B
31-
A[-PT24H]:fail-any | B[-PT24H]:fail-any => c
29+
A?
30+
B?
31+
A[-PT24H]:fail-any? | B[-PT24H]:fail-any? => c
3232
"""
3333
[runtime]
3434
[[A]]

tests/integration/reftests/test_pre_initial.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ async def test_over_bracketed(flow, scheduler, reftest):
102102
'final cycle point': '2013-12-25T12:00Z',
103103
'graph': {
104104
'T12': '''
105-
(a[-P1D]:fail | b[-P1D]:fail | c[-P1D]:fail) => d
106-
a & b & c # Implied by implicit cycling now...
105+
(a[-P1D]:fail? | b[-P1D]:fail? | c[-P1D]:fail?) => d
106+
a? & b? & c? # Implied by implicit cycling now...
107107
''',
108108
},
109109
},

tests/unit/test_graph_parser.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,31 @@ def test_graph_syntax_errors_2(seq, graph, expected_err):
139139
'Invalid cycle point offsets only on right',
140140
id='no-cycle-point-RHS'
141141
),
142+
# See https://github.com/cylc/cylc-flow/issues/6523
143+
# For the next 4 tests:
144+
param(
145+
# Yes I know it's circular, but it's here to
146+
# demonstrate that the test below is broken:
147+
"foo:finished => foo",
148+
'Output foo:succeeded can\'t be both required and optional',
149+
id='finish-implies-success-optional'
150+
),
151+
param(
152+
"foo[-P1]:finish => foo",
153+
'Output foo:succeeded can\'t be both required and optional',
154+
id='finish-implies-success-optional-offset'
155+
),
156+
param(
157+
"foo[-P1]:succeeded | foo[-P1]:failed => bar",
158+
# order of outputs varies in the error message
159+
'must both be optional if both are used',
160+
id='succeed-or-failed-mustbe-optional'
161+
),
162+
param(
163+
"foo[-P1]:succeeded? | foo[-P1]:failed? => foo",
164+
'Output foo:succeeded can\'t be both required and optional',
165+
id='succeed-or-failed-implies-success-optional'
166+
),
142167
]
143168
)
144169
def test_graph_syntax_errors(graph, expected_err):
@@ -948,8 +973,8 @@ def test_RHS_AND(graph: str, expected_triggers: Dict[str, List[str]]):
948973
param((('a', 'b[-P42M]'), {'b[-P42M]'}), 'Invalid cycle point offset'),
949974
# No error if offset in NON-terminal RHS:
950975
param((('a', 'b[-P42M]'), {}), None),
951-
# Don't check the left hand side if this has a non-terminal RHS:
952-
param((('a &', 'b[-P42M]'), {}), None),
976+
# Check the left hand side if this has a non-terminal RHS:
977+
param((('a &', 'b[-P42M]'), {}), 'Null task name in graph'),
953978
)
954979
)
955980
def test_proc_dep_pair(args, err):

0 commit comments

Comments
 (0)