Skip to content

Commit c496207

Browse files
committed
Fix a terminal graph items bug
1 parent f125c0f commit c496207

File tree

2 files changed

+63
-5
lines changed

2 files changed

+63
-5
lines changed

cylc/flow/graph_parser.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class GraphParser:
168168
_RE_OFFSET = r'\[[\w\-\+\^:]+\]'
169169
_RE_QUAL = QUALIFIER + r'[\w\-]+' # task or fam trigger
170170
_RE_OPT = r'\??' # optional output indicator
171+
_RE_ANDOR = re.compile(r'\s*[&|]\s*')
171172

172173
REC_QUAL = re.compile(_RE_QUAL)
173174

@@ -470,13 +471,33 @@ def parse_graph(self, graph_string: str) -> None:
470471
pairs.add((chain[i], chain[i + 1]))
471472

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

477476
for pair in sorted(pairs, key=lambda p: str(p[0])):
478477
self._proc_dep_pair(pair, self.terminals)
479478

479+
@staticmethod
480+
def get_graph_terminals(pairs):
481+
"""Get terminating ends of graphs.
482+
483+
For example in `foo => bar => baz` only `baz` terminates the chain
484+
of dependencies.
485+
486+
Examples:
487+
>>> this = GraphParser.get_graph_terminals
488+
>>> this({('foo', 'bar')})
489+
{'bar'}
490+
"""
491+
lefts = []
492+
for left, _ in pairs:
493+
if left and ('&' in left or '|' in left):
494+
# RE used because don't want to have to worry about
495+
# mutiple and/or and cleaning whitespace.
496+
lefts += GraphParser._RE_ANDOR.split(left)
497+
else:
498+
lefts.append(left)
499+
return {p[1] for p in pairs}.difference(set(lefts))
500+
480501
@classmethod
481502
def _report_invalid_lines(cls, lines: List[str]) -> None:
482503
"""Raise GraphParseError in a consistent format when there are

tests/unit/test_graph_parser.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from cylc.flow.graph_parser import GraphParser
2828
from cylc.flow.task_outputs import (
2929
TASK_OUTPUT_SUBMITTED,
30-
TASK_OUTPUT_SUBMIT_FAILED,
3130
TASK_OUTPUT_STARTED,
3231
TASK_OUTPUT_SUCCEEDED,
3332
TASK_OUTPUT_FAILED
@@ -810,7 +809,6 @@ def test_cannot_be_required():
810809
gp.parse_graph('a:submit-failed => b')
811810

812811

813-
814812
@pytest.mark.parametrize(
815813
'graph, error',
816814
[
@@ -987,3 +985,42 @@ def test_proc_dep_pair(args, err):
987985
gp._proc_dep_pair(*args)
988986
else:
989987
assert gp._proc_dep_pair(*args) is None
988+
989+
990+
@pytest.mark.parametrize(
991+
'pairs, terminals',
992+
(
993+
param(
994+
{
995+
(None, 'foo[-P1D]:restart1'),
996+
(None, 'bar?'),
997+
('foo[-P1D]:restart1|bar?', 'foo')
998+
},
999+
{'foo'},
1000+
id='|'
1001+
),
1002+
param(
1003+
{
1004+
(None, 'foo[-P1D]:restart1'),
1005+
(None, 'bar?'),
1006+
('foo[-P1D]:restart1 & bar?', 'foo')
1007+
},
1008+
{'foo'},
1009+
id='&'
1010+
),
1011+
param(
1012+
{
1013+
(None, 'foo[-P1D]:restart1'),
1014+
(None, 'bar?'),
1015+
(None, 'qux'),
1016+
('foo[-P1D]:restart1 &bar? |qux', 'foo')
1017+
},
1018+
{'foo'},
1019+
id='&-and-|'
1020+
),
1021+
)
1022+
)
1023+
def test_get_graph_terminals(pairs, terminals):
1024+
"""It identifies all graph terminals, and no non terminals.
1025+
"""
1026+
assert GraphParser.get_graph_terminals(pairs) == terminals

0 commit comments

Comments
 (0)