Skip to content

Commit 558edba

Browse files
Make system test run directories portable for CI artifact replay
Use relative Docker Compose paths, generate rerun_systemtest.sh in each run folder, default TUTORIALS_REF to develop, and document replay from system_tests_run_*_full artifacts. Closes #387.
1 parent 0f34006 commit 558edba

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

changelog-entries/724.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- System test run directories use relative Docker Compose paths and include a `rerun_systemtest.sh` script so CI artifacts can be downloaded and replayed locally (Closes #387).

tools/tests/README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,58 @@ In this case, building and running seems to work out, but the tests fail because
104104
## Understanding what went wrong
105105

106106
The easiest way to debug a systemtest run is first to have a look at the output written into the action on GitHub.
107-
If this does not provide enough hints, the next step is to download the generated `system_tests_run_<run_id>_<run_attempt>` artifact. Note that by default this will only be generated if the systemtests fail.
107+
If this does not provide enough hints, the next step is to download the generated `system_tests_run_<run_id>_<run_attempt>_full` artifact (a smaller `_logs` archive contains only log files). Note that by default this will only be generated if the systemtests fail.
108108
Inside the archive, a test-specific subfolder like `flow-over-heated-plate_fluid-openfoam-solid-fenics_2023-11-19-211723` contains two log files: `system-tests-stderr.log` and `system-tests-stdout.log`. This can be a starting point for a further investigation. When fieldcompare runs with `--diff`, it writes VTK diff files under `precice-exports/`; if the comparison fails, those files are copied into a `diff-results/` subfolder in the same run directory (mirroring any subpaths under `precice-exports/`) so you can open them (e.g. in ParaView) to see where results differ from the reference. On successful comparisons, `diff-results/` is therefore absent.
109109

110+
### Re-running system tests from CI artifacts
111+
112+
Download the **full** artifact from a failed (or manually uploaded) workflow run:
113+
114+
`system_tests_run_<run_id>_<run_attempt>_full`
115+
116+
The archive contains a `runs/` directory shared by all system tests from that run:
117+
118+
```text
119+
runs/
120+
├── tools/ # Dockerfiles and helpers (shared)
121+
└── <tutorial>_<cases>_<timestamp>/ # one folder per system test
122+
├── docker-compose.tutorial.yaml
123+
├── docker-compose.field_compare.yaml # if fieldcompare ran
124+
├── rerun_systemtest.sh
125+
└── …
126+
```
127+
128+
To re-run one test locally:
129+
130+
1. Download and extract `system_tests_run_<run_id>_<run_attempt>_full.zip`.
131+
2. Keep the `runs/` layout intact (the tutorial folder needs the sibling `tools/` directory):
132+
133+
```bash
134+
unzip system_tests_run_<run_id>_<run_attempt>_full.zip
135+
cd system_tests_run_<run_id>_<run_attempt>_full/runs
136+
ls
137+
cd <tutorial>_<cases>_<timestamp>
138+
```
139+
140+
3. In the tutorial folder you will find the copied tutorial, generated Docker Compose
141+
files, and `rerun_systemtest.sh`. The shared `tools/` tree lives one level up in
142+
`runs/tools/`.
143+
144+
4. Re-run with Docker:
145+
146+
```bash
147+
./rerun_systemtest.sh # or: sh rerun_systemtest.sh
148+
```
149+
150+
The script rebuilds images, runs the tutorial containers, and (if
151+
`docker-compose.field_compare.yaml` exists) runs fieldcompare with the same
152+
`--exit-code-from` behavior as the CI runner. Compose paths are relative to the
153+
tutorial folder (`..` points at the parent `runs/` directory), so you can relocate the
154+
entire extracted `runs/` tree on any Linux host with Docker.
155+
156+
Fieldcompare requires reference results in the artifact (unpacked by CI during the
157+
original run) or you must unpack them manually before that step.
158+
110159
## Adding new tests
111160

112161
### Adding tutorials

tools/tests/systemtests/Systemtest.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,37 @@ def __get_docker_services(self) -> Dict[str, str]:
192192
except Exception as exc:
193193
raise KeyError("Please specify a PLATFORM argument") from exc
194194

195+
# Use an absolute path here only for validation that the requested
196+
# dockerfile context exists on the machine running the system tests.
195197
self.dockerfile_context = PRECICE_TESTS_DIR / "dockerfiles" / Path(plaform_requested)
196198
if not self.dockerfile_context.exists():
197199
raise ValueError(
198200
f"The path {self.dockerfile_context.resolve()} resulting from argument PLATFORM={plaform_requested} could not be found in the system")
199201

200202
def render_service_template_per_case(case: Case, params_to_use: Dict[str, str]) -> str:
203+
# Inside the individual system test directory (`self.system_test_dir`)
204+
# we copy a full `tools/` tree into the parent run directory
205+
# (see __copy_tools). From the point of view of the system test
206+
# directory we therefore need to go one level up to reach the
207+
# shared `tools/` folder:
208+
# <run_directory>/tools/tests/dockerfiles/<PLATFORM>
209+
# ^-------------^ parent of self.system_test_dir
210+
dockerfile_context_relative = (
211+
Path("..") / "tools" / "tests" / "dockerfiles" / Path(plaform_requested)
212+
)
213+
201214
render_dict = {
202-
'run_directory': self.run_directory.resolve(),
215+
# Use a relative path to the *parent* run directory so that
216+
# containers still see /runs/<tutorial_folder> like before,
217+
# while keeping the compose file independent of the CI
218+
# runner's absolute paths.
219+
'run_directory': "..",
203220
'tutorial_folder': self.tutorial_folder,
204221
'build_arguments': params_to_use,
205222
'params': params_to_use,
206223
'case_folder': case.path,
207224
'run': case.run_cmd,
208-
'dockerfile_context': self.dockerfile_context,
225+
'dockerfile_context': dockerfile_context_relative,
209226
}
210227
jinja_env = Environment(loader=FileSystemLoader(PRECICE_TESTS_DIR))
211228
template = jinja_env.get_template(case.component.template)
@@ -220,12 +237,20 @@ def render_service_template_per_case(case: Case, params_to_use: Dict[str, str])
220237
def __get_docker_compose_file(self):
221238
rendered_services = self.__get_docker_services()
222239
render_dict = {
223-
'run_directory': self.run_directory.resolve(),
240+
# See __get_docker_services: keep the docker-compose file
241+
# portable by referring to the parent run directory only.
242+
'run_directory': "..",
224243
'tutorial_folder': self.tutorial_folder,
225244
'tutorial': self.tutorial.path.name,
226245
'services': rendered_services,
227246
'build_arguments': self.params_to_use,
228-
'dockerfile_context': self.dockerfile_context,
247+
# The dockerfile_context value inside the templates is only
248+
# used as a build context path and does not need to be
249+
# absolute – it will be resolved relative to the system test
250+
# directory.
251+
'dockerfile_context': (
252+
Path("..") / "tools" / "tests" / "dockerfiles" / Path(self.params_to_use.get("PLATFORM"))
253+
),
229254
'precice_output_folder': PRECICE_REL_OUTPUT_DIR,
230255
}
231256
jinja_env = Environment(loader=FileSystemLoader(PRECICE_TESTS_DIR))
@@ -234,7 +259,10 @@ def __get_docker_compose_file(self):
234259

235260
def __get_field_compare_compose_file(self):
236261
render_dict = {
237-
'run_directory': self.run_directory.resolve(),
262+
# Fieldcompare should also use only relative paths from inside
263+
# the system test directory so that the run directory can be
264+
# moved and re-executed elsewhere.
265+
'run_directory': "..",
238266
'tutorial_folder': self.tutorial_folder,
239267
'precice_output_folder': PRECICE_REL_OUTPUT_DIR,
240268
'reference_output_folder': PRECICE_REL_REFERENCE_DIR + "/" + self.reference_result.path.name.replace(".tar.gz", ""),
@@ -498,9 +526,41 @@ def _build_docker(self):
498526
logging.debug(f"Building docker image for {self}")
499527
time_start = time.perf_counter()
500528
docker_compose_content = self.__get_docker_compose_file()
501-
with open(self.system_test_dir / "docker-compose.tutorial.yaml", 'w') as file:
529+
docker_compose_path = self.system_test_dir / "docker-compose.tutorial.yaml"
530+
with open(docker_compose_path, 'w') as file:
502531
file.write(docker_compose_content)
503532

533+
# Provide a small helper script inside the system test directory so
534+
# that a user downloading the corresponding `runs/` artifact can
535+
# re-run the exact docker-compose setup locally without having to
536+
# reconstruct the commands by hand.
537+
rerun_script_path = self.system_test_dir / "rerun_systemtest.sh"
538+
rerun_script_path.write_text(
539+
"#!/usr/bin/env sh\n"
540+
"set -e -u\n"
541+
"\n"
542+
"cd \"$(dirname \"$0\")\"\n"
543+
"\n"
544+
"echo \"[systemtests] Building tutorial images...\"\n"
545+
"docker compose --file docker-compose.tutorial.yaml build\n"
546+
"\n"
547+
"echo \"[systemtests] Running tutorial containers...\"\n"
548+
"docker compose --file docker-compose.tutorial.yaml up\n"
549+
"\n"
550+
"if [ -f docker-compose.field_compare.yaml ]; then\n"
551+
" echo \"[systemtests] Running fieldcompare...\"\n"
552+
" docker compose --file docker-compose.field_compare.yaml up --exit-code-from field-compare\n"
553+
"fi\n"
554+
)
555+
# Make the script executable for convenience; even if this bit
556+
# does not survive archiving, users can still run it via
557+
# `sh rerun_systemtest.sh`.
558+
try:
559+
rerun_script_path.chmod(rerun_script_path.stat().st_mode | 0o111)
560+
except Exception:
561+
logging.debug(
562+
f"Could not mark {rerun_script_path} as executable; continuing anyway.")
563+
504564
stdout_data = []
505565
stderr_data = []
506566

0 commit comments

Comments
 (0)