Skip to content

Commit 7fd5fef

Browse files
Add run-before / run-after hooks for system tests (#849)
1 parent aeeb662 commit 7fd5fef

7 files changed

Lines changed: 126 additions & 8 deletions

File tree

changelog-entries/849.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added optional `run-before` and `run-after` hooks in `tests.yaml` for system test setup ([#849](https://github.com/precice/tutorials/pull/849)).

tools/tests/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ The available cases are listed in the `metadata.yaml` of each tutorial. To add a
125125

126126
Use the `max_time` or `max_time_windows` parameters to restrict the runtime of the test to the first few coupling time windows, to save time. Aim for a runtime of less than a minute (assuming cached components), if possible.
127127

128+
Some tutorials require setup before the simulation (e.g. switching configuration files). Use optional `run-before` and `run-after` fields in `tests.yaml` to run shell commands in the copied tutorial directory after copying and before Docker build (`run-before`), or after the simulation and before field comparison (`run-after`). Example:
129+
130+
```yaml
131+
run-before: ./set-case.sh 1d3d
132+
```
133+
128134
You will need to define a reference results file. The reference results can and should be generated on GitHub using the [Generate reference results (manual)](https://github.com/precice/tutorials/actions/workflows/generate-reference-results-manual.yml) workflow for the respective test suite. You might want to temporarily set the `selected` test suite for requesting results only for a subset of test cases.
129135

130136
Note that you will need to define the `TUTORIALS_REF` in the file [`reference_versions.yaml`](https://github.com/precice/tutorials/actions/workflows/generate-reference-results-manual.yml) to match the respective branch. Restore that to `develop` after that. See a [related issue](https://github.com/precice/tutorials/issues/844).

tools/tests/generate_reference_results.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,20 @@ def main():
172172
max_times = test_suite.max_times.get(tutorial, [])
173173
mtw_list = test_suite.max_time_windows.get(tutorial, [])
174174
timeouts = test_suite.timeouts.get(tutorial, [])
175+
run_befores = test_suite.run_befores.get(tutorial, [])
176+
run_afters = test_suite.run_afters.get(tutorial, [])
175177
for i, (case, reference_result) in enumerate(zip(
176178
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial])):
177179
max_time = max_times[i] if i < len(max_times) else None
178180
max_time_windows = mtw_list[i] if i < len(mtw_list) else None
179181
timeout = timeouts[i] if i < len(timeouts) and timeouts[i] is not None else GLOBAL_TIMEOUT
182+
run_before = run_befores[i] if i < len(run_befores) else None
183+
run_after = run_afters[i] if i < len(run_afters) else None
180184
systemtests_to_run.add(
181-
Systemtest(tutorial, build_args, case, reference_result, max_time=max_time, max_time_windows=max_time_windows, timeout=timeout))
185+
Systemtest(
186+
tutorial, build_args, case, reference_result,
187+
max_time=max_time, max_time_windows=max_time_windows, timeout=timeout,
188+
run_before=run_before, run_after=run_after))
182189

183190
reference_result_per_tutorial = {}
184191
current_time_string = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

tools/tests/systemtests.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ def _group_end() -> None:
9191
timeouts = test_suite.timeouts.get(tutorial, [])
9292
tolerances = test_suite.tolerances.get(tutorial, [])
9393
skip_compares = test_suite.skip_compares.get(tutorial, [])
94+
run_befores = test_suite.run_befores.get(tutorial, [])
95+
run_afters = test_suite.run_afters.get(tutorial, [])
9496
for i, (case, reference_result) in enumerate(zip(
9597
test_suite.cases_of_tutorial[tutorial], test_suite.reference_results[tutorial])):
9698
max_time = max_times[i] if i < len(max_times) else None
@@ -100,8 +102,14 @@ def _group_end() -> None:
100102
tolerances) and tolerances[i] is not None else DEFAULT_FIELDCOMPARE_RTOL
101103
skip_compare = skip_compares[i] if i < len(
102104
skip_compares) and skip_compares[i] is not None else False
105+
run_before = run_befores[i] if i < len(run_befores) else None
106+
run_after = run_afters[i] if i < len(run_afters) else None
103107
systemtests_to_run.append(
104-
Systemtest(tutorial, build_args, case, reference_result, max_time=max_time, max_time_windows=max_time_windows, timeout=timeout, tolerance=tolerance, skip_compare=skip_compare))
108+
Systemtest(
109+
tutorial, build_args, case, reference_result,
110+
max_time=max_time, max_time_windows=max_time_windows, timeout=timeout,
111+
tolerance=tolerance, skip_compare=skip_compare,
112+
run_before=run_before, run_after=run_after))
105113

106114
if not systemtests_to_run:
107115
raise RuntimeError("Did not find any Systemtests to execute.")

tools/tests/systemtests/Systemtest.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ class Systemtest:
226226
timeout: int = GLOBAL_TIMEOUT
227227
tolerance: float = DEFAULT_FIELDCOMPARE_RTOL
228228
skip_compare: bool = False
229+
run_before: str | None = None
230+
run_after: str | None = None
229231
params_to_use: Dict[str, str] = field(init=False)
230232
env: Dict[str, str] = field(init=False)
231233

@@ -945,11 +947,41 @@ def __apply_max_time_override(self):
945947
logging.info(f"Overwrote <max-time-windows> to {self.max_time_windows} in {config_path}")
946948
config_path.write_text(new_text)
947949

950+
def _run_hook(self, stage: str, command: str | None) -> bool:
951+
"""
952+
Run a shell command in the copied tutorial directory (e.g. run-before / run-after).
953+
"""
954+
if not command:
955+
return True
956+
logging.info(f"Running {stage} for {self}: {command}")
957+
try:
958+
result = subprocess.run(
959+
command,
960+
shell=True,
961+
cwd=self.system_test_dir,
962+
capture_output=True,
963+
text=True,
964+
start_new_session=True,
965+
)
966+
except Exception as e:
967+
logging.critical(f"Failed to start {stage} for {self}: {e}")
968+
return False
969+
hook_output = (result.stdout or '') + (result.stderr or '')
970+
if hook_output.strip():
971+
logging.debug(f"{stage} output for {self}:\n{hook_output.rstrip()}")
972+
if result.returncode != 0:
973+
logging.critical(
974+
f"{stage} for {self} failed with exit code {result.returncode}: {command}")
975+
return False
976+
return True
977+
948978
def __prepare_for_run(self, run_directory: Path):
949979
"""
950980
Prepares the run_directory with folders and datastructures needed for every systemtest execution
951981
"""
952982
self.__copy_tutorial_into_directory(run_directory)
983+
if not self._run_hook('run-before', self.run_before):
984+
raise RuntimeError(f"run-before hook failed for {self}")
953985
self.__apply_max_time_override()
954986
self.__copy_tools(run_directory)
955987
self.__put_gitignore(run_directory)
@@ -961,7 +993,12 @@ def run(self, run_directory: Path):
961993
"""
962994
Runs the system test by generating the Docker Compose file, copying everything into a run folder, and executing docker-compose up.
963995
"""
964-
self.__prepare_for_run(run_directory)
996+
try:
997+
self.__prepare_for_run(run_directory)
998+
except RuntimeError as e:
999+
logging.critical(str(e))
1000+
return SystemtestResult(False, [], [str(e)], self, build_time=0, solver_time=0, fieldcompare_time=0)
1001+
9651002
self.__init_run_logs()
9661003
std_out: List[str] = []
9671004
std_err: List[str] = []
@@ -995,6 +1032,17 @@ def run(self, run_directory: Path):
9951032
solver_time=docker_run_result.runtime,
9961033
fieldcompare_time=0)
9971034

1035+
if not self._run_hook('run-after', self.run_after):
1036+
logging.critical(f"run-after hook failed for {self}")
1037+
return SystemtestResult(
1038+
False,
1039+
std_out,
1040+
std_err,
1041+
self,
1042+
build_time=docker_build_result.runtime,
1043+
solver_time=docker_run_result.runtime,
1044+
fieldcompare_time=0)
1045+
9981046
if self.skip_compare:
9991047
logging.info(f"Skipping fieldcompare for {self} (skip_compare=true)")
10001048
fieldcompare_time = 0.0
@@ -1027,7 +1075,7 @@ def run(self, run_directory: Path):
10271075
self,
10281076
build_time=docker_build_result.runtime,
10291077
solver_time=docker_run_result.runtime,
1030-
fieldcompare_time=fieldcompare_result.runtime)
1078+
fieldcompare_time=fieldcompare_time)
10311079

10321080
# self.__cleanup()
10331081
self._cleanup_docker_networks()
@@ -1044,7 +1092,12 @@ def run_for_reference_results(self, run_directory: Path):
10441092
"""
10451093
Runs the system test by generating the Docker Compose files to generate the reference results
10461094
"""
1047-
self.__prepare_for_run(run_directory)
1095+
try:
1096+
self.__prepare_for_run(run_directory)
1097+
except RuntimeError as e:
1098+
logging.critical(str(e))
1099+
return SystemtestResult(False, [], [str(e)], self, build_time=0, solver_time=0, fieldcompare_time=0)
1100+
10481101
self.__init_run_logs()
10491102
std_out: List[str] = []
10501103
std_err: List[str] = []
@@ -1077,6 +1130,17 @@ def run_for_reference_results(self, run_directory: Path):
10771130
solver_time=docker_run_result.runtime,
10781131
fieldcompare_time=0)
10791132

1133+
if not self._run_hook('run-after', self.run_after):
1134+
logging.critical(f"run-after hook failed for {self}")
1135+
return SystemtestResult(
1136+
False,
1137+
std_out,
1138+
std_err,
1139+
self,
1140+
build_time=docker_build_result.runtime,
1141+
solver_time=docker_run_result.runtime,
1142+
fieldcompare_time=0)
1143+
10801144
self._cleanup_docker_networks()
10811145
return SystemtestResult(
10821146
True,
@@ -1091,7 +1155,12 @@ def run_only_build(self, run_directory: Path):
10911155
"""
10921156
Runs only the build commmand, for example to preheat the caches of the docker builder.
10931157
"""
1094-
self.__prepare_for_run(run_directory)
1158+
try:
1159+
self.__prepare_for_run(run_directory)
1160+
except RuntimeError as e:
1161+
logging.critical(str(e))
1162+
return SystemtestResult(False, [], [str(e)], self, build_time=0, solver_time=0, fieldcompare_time=0)
1163+
10951164
self.__init_run_logs()
10961165
std_out: List[str] = []
10971166
std_err: List[str] = []

tools/tests/systemtests/TestSuite.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class TestSuite:
1515
timeouts: Dict[Tutorial, List] = field(default_factory=dict)
1616
tolerances: Dict[Tutorial, list] = field(default_factory=dict)
1717
skip_compares: Dict[Tutorial, list] = field(default_factory=dict)
18+
run_befores: Dict[Tutorial, List] = field(default_factory=dict)
19+
run_afters: Dict[Tutorial, List] = field(default_factory=dict)
1820

1921
def __repr__(self) -> str:
2022
return_string = f"Test suite: {self.name} contains:"
@@ -58,6 +60,8 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
5860
timeouts_of_tutorial = {}
5961
tolerances_of_tutorial = {}
6062
skip_compares_of_tutorial = {}
63+
run_befores_of_tutorial = {}
64+
run_afters_of_tutorial = {}
6165
# iterate over tutorials:
6266
for tutorial_case in test_suites_raw[test_suite_name]['tutorials']:
6367
tutorial = parsed_tutorials.get_by_path(tutorial_case['path'])
@@ -72,6 +76,8 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
7276
timeouts_of_tutorial[tutorial] = []
7377
tolerances_of_tutorial[tutorial] = []
7478
skip_compares_of_tutorial[tutorial] = []
79+
run_befores_of_tutorial[tutorial] = []
80+
run_afters_of_tutorial[tutorial] = []
7581

7682
all_case_combinations = tutorial.case_combinations
7783
case_combination_requested = CaseCombination.from_string_list(
@@ -119,12 +125,31 @@ def from_yaml(cls, path, parsed_tutorials: Tutorials):
119125
f"in tutorial '{tutorial}'."
120126
)
121127
skip_compares_of_tutorial[tutorial].append(skip_compare_value)
128+
129+
run_before_raw = tutorial_case.get('run-before', None)
130+
run_after_raw = tutorial_case.get('run-after', None)
131+
run_befores_of_tutorial[tutorial].append(
132+
run_before_raw.strip()
133+
if isinstance(run_before_raw, str) and run_before_raw.strip() else None)
134+
run_afters_of_tutorial[tutorial].append(
135+
run_after_raw.strip()
136+
if isinstance(run_after_raw, str) and run_after_raw.strip() else None)
122137
else:
123138
raise Exception(
124139
f"Could not find the case combination {tutorial_case['case_combination']} in the current metadata of tutorial {tutorial.name}, or it does not define all necessary participants.")
125140

126-
testsuites.append(TestSuite(test_suite_name, case_combinations_of_tutorial,
127-
reference_results_of_tutorial, max_times_of_tutorial, max_time_windows_of_tutorial, timeouts_of_tutorial, tolerances_of_tutorial, skip_compares_of_tutorial))
141+
testsuites.append(TestSuite(
142+
test_suite_name,
143+
case_combinations_of_tutorial,
144+
reference_results_of_tutorial,
145+
max_times_of_tutorial,
146+
max_time_windows_of_tutorial,
147+
timeouts_of_tutorial,
148+
tolerances_of_tutorial,
149+
skip_compares_of_tutorial,
150+
run_befores_of_tutorial,
151+
run_afters_of_tutorial,
152+
))
128153

129154
return cls(testsuites)
130155

tools/tests/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ test_suites:
396396
case_combination:
397397
- fluid1d-left-nutils
398398
- fluid3d-right-openfoam
399+
run-before: ./set-case.sh 1d3d
399400
max_time: 0.05
400401
reference_result: ./partitioned-pipe-multiscale/reference-results/fluid1d-left-nutils_fluid3d-right-openfoam.tar.gz
401402
# More case combinations are possible, but they requite calling set-case.sh
@@ -549,6 +550,7 @@ test_suites:
549550
case_combination:
550551
- fluid1d-left-nutils
551552
- fluid3d-right-openfoam
553+
run-before: ./set-case.sh 1d3d
552554
max_time: 0.05
553555
reference_result: ./water-hammer/reference-results/fluid1d-left-nutils_fluid3d-right-openfoam.tar.gz
554556
# More case combinations are possible, but they requite calling set-case.sh

0 commit comments

Comments
 (0)