diff --git a/cylc/flow/config.py b/cylc/flow/config.py index b536f52c702..93a49a435c3 100644 --- a/cylc/flow/config.py +++ b/cylc/flow/config.py @@ -2142,28 +2142,26 @@ def set_required_outputs( continue taskdef.set_required_output(output, not optional) - # Expire is dangerous, it must be visible and optional in the graph - bad_exp = set() - good_exp = set() - for (task, output), (opt, _, _) in task_output_opt.items(): + # Add expired outputs to taskdefs if flagged in the graph. + graph_exp = set() + for (task, output) in task_output_opt.keys(): if output == TASK_OUTPUT_EXPIRED: - if not opt: - bad_exp.add(task) - continue - good_exp.add(task) + graph_exp.add(task) self.taskdefs[task].add_output( TASK_OUTPUT_EXPIRED, TASK_OUTPUT_EXPIRED ) - # likewise clock-expiry is only legal if flagged in the graph + # clock-expire must be flagged in the graph for visibility + bad_exp = set() for task in self.expiration_offsets: - if task not in good_exp: + if task not in graph_exp: bad_exp.add(task) if bad_exp: - msg = '\n '.join([t + ":expired?" for t in bad_exp]) + msg = '\n '.join( + [t + f":{TASK_OUTPUT_EXPIRED}?" for t in bad_exp]) raise WorkflowConfigError( - f"Expiring tasks must be optional in the graph as:\n {msg}" + f"Clock-expire must be visible in the graph:\n {msg}" ) def find_taskdefs(self, name: str) -> Set[TaskDef]: diff --git a/cylc/flow/graph_parser.py b/cylc/flow/graph_parser.py index ab3890961f9..090ebae1c5a 100644 --- a/cylc/flow/graph_parser.py +++ b/cylc/flow/graph_parser.py @@ -61,6 +61,7 @@ class Replacement: """A class to remember match group information in re.sub() calls""" + def __init__(self, replacement): self.replacement = replacement self.substitutions = [] @@ -745,6 +746,10 @@ def _set_output_opt( if suicide: return + if output == TASK_OUTPUT_EXPIRED and not optional: + raise GraphParseError( + f"Output {name}:{output} must be optional (append '?')") + if output == TASK_OUTPUT_FINISHED: # Interpret :finish pseudo-output if optional: diff --git a/cylc/flow/task_job_mgr.py b/cylc/flow/task_job_mgr.py index 3b5b1bcd1a1..49590245e38 100644 --- a/cylc/flow/task_job_mgr.py +++ b/cylc/flow/task_job_mgr.py @@ -104,7 +104,6 @@ TASK_STATUS_SUBMITTED, TASK_STATUS_RUNNING, TASK_STATUS_WAITING, - TASK_STATUS_EXPIRED, TASK_STATUSES_ACTIVE ) from cylc.flow.wallclock import ( @@ -183,9 +182,9 @@ def kill_task_jobs(self, itasks, expire=False): if expire: expire tasks (in the callback) after killing them. """ + ok = True if expire: # Check these tasks are allowed to expire. - ok = True output = TASK_OUTPUT_EXPIRED for itask in itasks: msg = itask.state.outputs.get_msg(output) diff --git a/tests/integration/scripts/test_completion_server.py b/tests/integration/scripts/test_completion_server.py index 24375c31f27..db0fb2a57d6 100644 --- a/tests/integration/scripts/test_completion_server.py +++ b/tests/integration/scripts/test_completion_server.py @@ -91,7 +91,6 @@ async def test_list_prereqs_and_outputs(flow, scheduler, start): # list outputs (b1) assert await _complete_cylc('cylc', 'set', b1.id, '--out', '') == { # regular task outputs - 'expired', 'failed', 'started', 'submit-failed', diff --git a/tests/integration/test_config.py b/tests/integration/test_config.py index acf24d17eaf..dd8da5edb24 100644 --- a/tests/integration/test_config.py +++ b/tests/integration/test_config.py @@ -263,6 +263,13 @@ def test_parse_special_tasks_families(flow, scheduler, validate, section): with pytest.raises(WorkflowConfigError) as exc_ctx: config = validate(id_) assert 'external triggers must be used only once' in str(exc_ctx.value) + + elif section == 'clock-expire': + with pytest.raises(WorkflowConfigError) as exc_ctx: + config = validate(id_) + assert ( + 'Clock-expire must be visible in the graph' in str(exc_ctx.value) + ) else: config = validate(id_) assert set(config.cfg['scheduling']['special tasks'][section]) == { diff --git a/tests/integration/test_task_job_mgr.py b/tests/integration/test_task_job_mgr.py index b085162a1da..e1338a7ae77 100644 --- a/tests/integration/test_task_job_mgr.py +++ b/tests/integration/test_task_job_mgr.py @@ -99,7 +99,6 @@ async def test_run_job_cmd_no_hosts_error( # killing the task should not result in an error... schd.task_job_mgr.kill_task_jobs( - schd.workflow, schd.pool.get_tasks() ) diff --git a/tests/integration/tui/screenshots/test_show.success.html b/tests/integration/tui/screenshots/test_show.success.html index afdcd1a73b4..fe016f285df 100644 --- a/tests/integration/tui/screenshots/test_show.success.html +++ b/tests/integration/tui/screenshots/test_show.success.html @@ -17,7 +17,6 @@ state: waiting prerequisites: (None) outputs: ('-': not completed) - - 1/foo expired - 1/foo submitted - 1/foo submit-failed - 1/foo started @@ -36,6 +35,7 @@ + quit: q help: h context: enter tree: - ← + → navigation: ↑ ↓ ↥ ↧ Home End filter tasks: T f s r R filter workflows: W E p \ No newline at end of file