Skip to content

Commit 6f62691

Browse files
authored
Merge pull request #5826 from oliver-sanders/reflog
tests/i: add reflog fixture
2 parents fe43c7c + fa340fa commit 6f62691

File tree

9 files changed

+226
-13
lines changed

9 files changed

+226
-13
lines changed

.github/workflows/test_fast.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,22 @@ jobs:
2020
fail-fast: false # don't stop on first failure
2121
matrix:
2222
os: ['ubuntu-latest']
23-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3']
23+
python-version: ['3.7', '3.8', '3.10', '3.11', '3']
2424
include:
25+
# mac os test
2526
- os: 'macos-latest'
26-
python-version: '3.7'
27+
python-version: '3.7' # oldest supported version
28+
29+
# non-utc timezone test
30+
- os: 'ubuntu-latest'
31+
python-version: '3.9' # not the oldest, not the most recent version
32+
time-zone: 'XXX-09:35'
33+
2734
env:
35+
# Use non-UTC time zone
36+
TZ: ${{ matrix.time-zone }}
2837
PYTEST_ADDOPTS: --cov --cov-append -n 5 --color=yes
38+
2939
steps:
3040
- name: Checkout
3141
uses: actions/checkout@v4

cylc/flow/task_pool.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,9 +1400,11 @@ def remove_if_complete(self, itask):
14001400
- retain and recompute runahead
14011401
(C7 failed tasks don't count toward runahead limit)
14021402
"""
1403+
ret = False
14031404
if cylc.flow.flags.cylc7_back_compat:
14041405
if not itask.state(TASK_STATUS_FAILED, TASK_OUTPUT_SUBMIT_FAILED):
14051406
self.remove(itask, 'finished')
1407+
ret = True
14061408
if self.compute_runahead():
14071409
self.release_runahead_tasks()
14081410
else:
@@ -1416,11 +1418,14 @@ def remove_if_complete(self, itask):
14161418
else:
14171419
# Remove as completed.
14181420
self.remove(itask, 'finished')
1421+
ret = True
14191422
if itask.identity == self.stop_task_id:
14201423
self.stop_task_finished = True
14211424
if self.compute_runahead():
14221425
self.release_runahead_tasks()
14231426

1427+
return ret
1428+
14241429
def spawn_on_all_outputs(
14251430
self, itask: TaskProxy, completed_only: bool = False
14261431
) -> None:

pytest.ini

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,10 @@ testpaths =
3232
cylc/flow/
3333
tests/unit/
3434
tests/integration/
35-
env =
36-
# a weird timezone to check that tests aren't assuming the local timezone
37-
TZ=XXX-09:35
3835
doctest_optionflags =
3936
NORMALIZE_WHITESPACE
4037
IGNORE_EXCEPTION_DETAIL
4138
ELLIPSIS
4239
asyncio_mode = auto
4340
markers=
44-
linkcheck: Test links
41+
linkcheck: Test links

setup.cfg

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ tests =
121121
pytest-asyncio>=0.17,!=0.23.*
122122
pytest-cov>=2.8.0
123123
pytest-xdist>=2
124-
pytest-env>=0.6.2
125124
pytest>=6
126125
testfixtures>=6.11.0
127126
towncrier>=23

tests/integration/conftest.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from pathlib import Path
2121
import pytest
2222
from shutil import rmtree
23+
from time import time
2324
from typing import List, TYPE_CHECKING, Set, Tuple, Union
2425

2526
from cylc.flow.config import WorkflowConfig
@@ -32,8 +33,10 @@
3233
install as cylc_install,
3334
get_option_parser as install_gop
3435
)
36+
from cylc.flow.util import serialise
3537
from cylc.flow.wallclock import get_current_time_string
3638
from cylc.flow.workflow_files import infer_latest_run_from_id
39+
from cylc.flow.workflow_status import StopMode
3740

3841
from .utils import _rm_if_empty
3942
from .utils.flow_tools import (
@@ -473,3 +476,140 @@ def _inner(source, **kwargs):
473476
workflow_id = infer_latest_run_from_id(workflow_id)
474477
return workflow_id
475478
yield _inner
479+
480+
481+
@pytest.fixture
482+
def reflog():
483+
"""Integration test version of the --reflog CLI option.
484+
485+
This returns a set which captures task triggers.
486+
487+
Note, you'll need to call this on the scheduler *after* you have started
488+
it.
489+
490+
Args:
491+
schd:
492+
The scheduler to capture triggering information for.
493+
flow_nums:
494+
If True, the flow numbers of the task being triggered will be added
495+
to the end of each entry.
496+
497+
Returns:
498+
tuple
499+
500+
(task, triggers):
501+
If flow_nums == False
502+
(task, flow_nums, triggers):
503+
If flow_nums == True
504+
505+
task:
506+
The [relative] task ID e.g. "1/a".
507+
flow_nums:
508+
The serialised flow nums e.g. ["1"].
509+
triggers:
510+
Sorted tuple of the trigger IDs, e.g. ("1/a", "2/b").
511+
512+
"""
513+
514+
def _reflog(schd, flow_nums=False):
515+
submit_task_jobs = schd.task_job_mgr.submit_task_jobs
516+
triggers = set()
517+
518+
def _submit_task_jobs(*args, **kwargs):
519+
nonlocal submit_task_jobs, triggers, flow_nums
520+
itasks = submit_task_jobs(*args, **kwargs)
521+
for itask in itasks:
522+
deps = tuple(sorted(itask.state.get_resolved_dependencies()))
523+
if flow_nums:
524+
triggers.add(
525+
(itask.identity, serialise(itask.flow_nums), deps or None)
526+
)
527+
else:
528+
triggers.add((itask.identity, deps or None))
529+
return itasks
530+
531+
schd.task_job_mgr.submit_task_jobs = _submit_task_jobs
532+
533+
return triggers
534+
535+
return _reflog
536+
537+
538+
@pytest.fixture
539+
def complete():
540+
"""Wait for the workflow, or tasks within it to complete.
541+
542+
Args:
543+
schd:
544+
The scheduler to await.
545+
tokens_list:
546+
If specified, this will wait for the tasks represented by these
547+
tokens to be marked as completed by the task pool.
548+
stop_mode:
549+
If tokens_list is not provided, this will wait for the scheduler
550+
to be shutdown with the specified mode (default = AUTO, i.e.
551+
workflow completed normally).
552+
timeout:
553+
Max time to wait for the condition to be met.
554+
555+
Note, if you need to increase this, you might want to rethink your
556+
test.
557+
558+
Note, use this timeout rather than wrapping the complete call with
559+
async_timeout (handles shutdown logic more cleanly).
560+
561+
"""
562+
async def _complete(
563+
schd,
564+
*tokens_list,
565+
stop_mode=StopMode.AUTO,
566+
timeout=60,
567+
):
568+
start_time = time()
569+
tokens_list = [tokens.task for tokens in tokens_list]
570+
571+
# capture task completion
572+
remove_if_complete = schd.pool.remove_if_complete
573+
574+
def _remove_if_complete(itask):
575+
ret = remove_if_complete(itask)
576+
if ret and itask.tokens.task in tokens_list:
577+
tokens_list.remove(itask.tokens.task)
578+
return ret
579+
580+
schd.pool.remove_if_complete = _remove_if_complete
581+
582+
# capture workflow shutdown
583+
set_stop = schd._set_stop
584+
has_shutdown = False
585+
586+
def _set_stop(mode=None):
587+
nonlocal has_shutdown, stop_mode
588+
if mode == stop_mode:
589+
has_shutdown = True
590+
return set_stop(mode)
591+
else:
592+
set_stop(mode)
593+
raise Exception(f'Workflow bailed with stop mode = {mode}')
594+
595+
schd._set_stop = _set_stop
596+
597+
# determine the completion condition
598+
if tokens_list:
599+
condition = lambda: bool(tokens_list)
600+
else:
601+
condition = lambda: bool(not has_shutdown)
602+
603+
# wait for the condition to be met
604+
while condition():
605+
# allow the main loop to advance
606+
await asyncio.sleep(0)
607+
if time() - start_time > timeout:
608+
raise Exception(
609+
f'Timeout waiting for {", ".join(map(str, tokens_list))}'
610+
)
611+
612+
# restore regular shutdown logic
613+
schd._set_stop = set_stop
614+
615+
return _complete

tests/integration/test_examples.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,45 @@ async def test_db_select(one, start, db_select):
229229
results = db_select(
230230
schd, False, 'task_states', name='one', status='waiting')
231231
assert len(results) == 1
232+
233+
234+
async def test_reflog(flow, scheduler, run, reflog, complete):
235+
"""Test the triggering of tasks.
236+
237+
This is the integration test version of "reftest" in the funtional tests.
238+
239+
It works by capturing the triggers which caused each submission so that
240+
they can be compared with the expected outcome.
241+
"""
242+
id_ = flow({
243+
'scheduler': {
244+
'allow implicit tasks': 'True',
245+
},
246+
'scheduling': {
247+
'initial cycle point': '1',
248+
'final cycle point': '1',
249+
'cycling mode': 'integer',
250+
'graph': {
251+
'P1': '''
252+
a => b => c
253+
x => b => z
254+
b[-P1] => b
255+
'''
256+
}
257+
}
258+
})
259+
schd = scheduler(id_, paused_start=False)
260+
261+
async with run(schd):
262+
triggers = reflog(schd) # Note: add flow_nums=True to capture flows
263+
await complete(schd)
264+
265+
assert triggers == {
266+
# 1/a was triggered by nothing (i.e. it's parentless)
267+
('1/a', None),
268+
# 1/b was triggered by three tasks (note the pre-initial dependency)
269+
('1/b', ('0/b', '1/a', '1/x')),
270+
('1/c', ('1/b',)),
271+
('1/x', None),
272+
('1/z', ('1/b',)),
273+
}

tests/integration/tui/screenshots/test_scheduler_logs.workflow-configuration-file.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">Path: mypath </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
44
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">&lt;</span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#e5e5e5;background:#000000">S</span><span style="color:#000000;background:#e5e5e5">elect File </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">&gt;</span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
55
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
6+
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">[runtime] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
7+
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> [[a]] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
68
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">[scheduling] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
79
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> [[graph]] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
810
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> R1 = a </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
9-
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5">[runtime] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
10-
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> [[a]] </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
1111
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
1212
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>
1313
<span style="color:#000000;background:#e5e5e5"></span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"> </span><span style="color:#000000;background:#e5e5e5"></span>

tests/integration/tui/test_updater.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,16 @@ async def test_filters(one_conf, flow, scheduler, run, updater):
147147
'graph': {
148148
'R1': 'a & b & c',
149149
}
150-
}
150+
},
151+
'runtime': {
152+
# TODO: remove this runtime section in
153+
# https://github.com/cylc/cylc-flow/pull/5721
154+
'root': {
155+
'simulation': {
156+
'default run length': 'PT1M',
157+
},
158+
},
159+
},
151160
}, name='one'), paused_start=True)
152161
two = scheduler(flow(one_conf, name='two'))
153162
tre = scheduler(flow(one_conf, name='tre'))

tests/integration/utils/flow_tools.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def _make_src_flow(src_path, conf):
5353
def _make_flow(
5454
cylc_run_dir: Union[Path, str],
5555
test_dir: Path,
56-
conf: Union[dict, str],
56+
conf: dict,
5757
name: Optional[str] = None,
5858
id_: Optional[str] = None,
5959
) -> str:
@@ -66,8 +66,19 @@ def _make_flow(
6666
flow_run_dir = (test_dir / name)
6767
flow_run_dir.mkdir(parents=True, exist_ok=True)
6868
id_ = str(flow_run_dir.relative_to(cylc_run_dir))
69-
if isinstance(conf, dict):
70-
conf = flow_config_str(conf)
69+
conf = flow_config_str({
70+
# override the default simulation runtime logic to make
71+
# tasks execute instantly
72+
# NOTE: this is prepended so it can be overwritten
73+
'runtime': {
74+
'root': {
75+
'simulation': {
76+
'default run length': 'PT0S',
77+
},
78+
},
79+
},
80+
**conf,
81+
})
7182
with open((flow_run_dir / WorkflowFiles.FLOW_FILE), 'w+') as flow_file:
7283
flow_file.write(conf)
7384
return id_

0 commit comments

Comments
 (0)