Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
cd ../../
- name: Archive system test logs
if: ${{ always() }}
id: archive-logs
uses: actions/upload-artifact@v7
with:
name: system_tests_run_${{ github.run_id }}_${{ github.run_attempt }}_logs
Expand All @@ -91,6 +92,26 @@ jobs:
runs/*/system-tests-run.log
runs/*/system-tests-compare.log
runs/*/*/system-tests_*.log
if-no-files-found: warn
- name: Archive fieldcompare diff visualizations
if: ${{ always() }}
id: archive-diffs
uses: actions/upload-artifact@v7
with:
name: system_tests_run_${{ github.run_id }}_${{ github.run_attempt }}_diffs
path: |
runs/*/diff-results/visualizations/**/*.png
Comment thread
MakisH marked this conversation as resolved.
if-no-files-found: warn
- name: Link diff visualizations in the job summary
if: ${{ always() && steps.archive-diffs.outputs.artifact-url != '' }}
run: |
{
echo ""
echo "## Diff visualizations"
echo ""
echo "When fieldcompare fails, PNG renders of the archived diff VTK fields are included in the [\`_diffs\` artifact](${{ steps.archive-diffs.outputs.artifact-url }})."
echo "Look under \`runs/*/diff-results/visualizations/\`."
} >> "$GITHUB_STEP_SUMMARY"
- name: Archive run files
if: ${{ failure() || inputs.upload_artifacts == 'TRUE' }}
uses: actions/upload-artifact@v7
Expand Down
2 changes: 1 addition & 1 deletion changelog-entries/441.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
- Archive fieldcompare diff VTK files into a `diff-results/` folder in each systemtest run directory on failure so they are easy to find in CI artifacts when investigating comparison failures (fixes [#441](https://github.com/precice/tutorials/issues/441)). Nested paths under `precice-exports/` are preserved under `diff-results/`.
- Archive fieldcompare diff VTK files into a `diff-results/` folder in each systemtest run directory on failure so they are easy to find in CI artifacts when investigating comparison failures (fixes [#441](https://github.com/precice/tutorials/issues/441), [#740](https://github.com/precice/tutorials/pull/740), [#883](https://github.com/precice/tutorials/pull/883)).
18 changes: 15 additions & 3 deletions tools/tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ When the tests fail at the results comparison step, this typically means that th

- `precice-exports/`: The coupling meshes of the test run.
- `reference-results/`: The coupling meshes of the reference run, as stored on Git LFS, expanded into `reference-results-unpacked`. For test cases using implicit coupling, the reference `.tar.gz` also contains the reference `precice-*-iterations.log` files.
- `diff-results/`: Numerical difference of the results in the two directories (computed with `fieldcompare dir --diff precice-exports/ reference/`). These are only present on failed comparisons.
- `diff-results/`: Numerical difference of the results in the two directories (computed with `fieldcompare dir --diff precice-exports/ reference/`). These are only present on failed comparisons and accompanied by visualization in `diff-results/visualizations/`.
- `iterations-logs/`: The `precice-*-iterations.log` files of the test run. Only present in test cases using implicit coupling. The comparisons to references only take into account the file SHA-256 checksums.

To reproduce the comparison locally, use the [same fieldcompare command](https://github.com/precice/tutorials/blob/develop/tools/tests/docker-compose.field_compare.template.yaml):
Expand All @@ -118,13 +118,23 @@ The differences are only shown per file, and there is no global metric or other

Alternatively, [visualize the `precice-exports/diff_*.vtu` in ParaView](https://precice.org/configuration-export.html#visualization-with-paraview).

To regenerate the PNG visualizations locally from an archived `diff-results/` folder (for example after downloading a CI artifact):

```bash
python3 visualize_fieldcompare_diffs.py /path/to/diff-results

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am running locally:

python3 visualize_fieldcompare_diffs.py /home/gc/Downloads/system_tests_run_31705035580_1_full/perpendicular-flap_fluid-openfoam-solid-calculix_2026-08-13-152820/diff-results

but this seems to be stuck without any output for a while. Some output in the script would be good. I assume something is wrong; it cannot be that slow.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it is very slow. It took at least 30min on my system. I guess the GHA runners are faster.

There, the job timed out, I didn't notice before:

WARNING: Could not render fieldcompare diff visualizations for Perpendicular flap (fluid-openfoam, solid-calculix): Command '['docker', 'compose', '--file', 'docker-compose.diff_visualizer.yaml', 'up', '--exit-code-from', 'diff-visualizer', '--abort-on-container-exit']' timed out after 300 seconds
CRITICAL: Fieldcompare returned non zero exit code, therefore Perpendicular flap (fluid-openfoam, solid-calculix) failed

@MakisH MakisH Aug 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice also that only 0m 49s is reported as compare time.

So, overall, the logging could be improved.

```

The default image size is set by `WINDOW_SIZE` in `visualize_fieldcompare_diffs.py` (currently `1024 x 768`). Increase it for higher-resolution PNGs, e.g. `WINDOW_SIZE = (1920, 1080)`.

For PDF output instead of PNG, replace the `plotter.show(screenshot=...)` call in `render_field()` with `plotter.render()` followed by `plotter.save_graphic(str(output_file.with_suffix(".pdf")))`.

### Re-running from CI artifacts

When a system test fails in CI, download the **full** artifact:

`system_tests_run_<run_id>_<run_attempt>_full`

(a smaller `_logs` archive contains only log files). The archive contains a shared `runs/` directory:
(a smaller `_logs` archive contains the stage log files; on comparison failures, difference visualizations are in a separate `_diffs` archive. The archives contain a shared `runs/` directory:

```text
runs/
Expand Down Expand Up @@ -288,6 +298,7 @@ Metadata and workflow/script files:
- Multi-stage build Dockerfiles that define how to build each component, in a layered approach
- `docker-compose.template.yaml`: Describes how to prepare each test (Docker Compose service template)
- `docker-compose.field_compare.template.yaml`: Describes how to compare results with fieldcompare (Docker Compose service template)
- `docker-compose.diff_visualizer.template.yaml`: Describes how to render fieldcompare diff VTK files to PNG images on failure
- `components.yaml`: Declares the available components and their parameters/options
- `reference-results-metadata.txt.template`: Template for reporting the versions and machine used to generate each reference results archive
- `reference_versions.yaml`: List of arguments to use for generating the reference results
Expand All @@ -308,7 +319,8 @@ Implementation scripts:

- `tools/tests/`
- `systemtests.py`: Main entry point
- `requirements.txt`: Dependencies (jinja2, pyyaml)
- `requirements.txt`: Dependencies (jinja2, pyyaml, pyvista for optional local use of the visualizer script)
- `visualize_fieldcompare_diffs.py`: Renders archived fieldcompare diff VTK files to PNG images (normally run via the `diff_visualizer` Docker stage)
- `metadata_parser/`: Reads the YAML files into Python objects (defines the schema)
- `systemtests/`: Main implementation classes
- `Systemtest.py`
Expand Down
20 changes: 20 additions & 0 deletions tools/tests/docker-compose.diff_visualizer.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
diff-visualizer:
build:
context: {{ dockerfile_context }}
dockerfile: Dockerfile
target: diff_visualizer
args:
{% for key, value in build_arguments.items() %}
- {{ key }}={{ value }}
{% endfor %}
volumes:
- ./{{ diff_results_folder }}:/diff-results
- ../tools/tests/visualize_fieldcompare_diffs.py:/home/precice/visualize_fieldcompare_diffs.py:ro
environment:
VTK_DEFAULT_OPENGL_WINDOW: vtkOSOpenGLRenderWindow
PYVISTA_OFF_SCREEN: "true"
command:
- /home/precice/venv/bin/python
- /home/precice/visualize_fieldcompare_diffs.py
- /diff-results
15 changes: 14 additions & 1 deletion tools/tests/dockerfiles/ubuntu_2404/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ RUN apt-get -qq update && \
python3-venv \
pkg-config \
wget \
inotify-tools
inotify-tools \
libosmesa6
Comment thread
MakisH marked this conversation as resolved.
# The following are dependencies of gmsh, needed by some tutorials
RUN apt-get -qq update && \
apt-get -qq install \
Expand All @@ -73,6 +74,18 @@ USER precice
### end of precice_dependencies stage ###


FROM precice_dependencies AS diff_visualizer
# Headless PNG rendering of fieldcompare diff VTK files (CPU/OSMesa, no display server).
USER precice
WORKDIR /home/precice
ENV VTK_DEFAULT_OPENGL_WINDOW=vtkOSOpenGLRenderWindow
ENV PYVISTA_OFF_SCREEN=true
RUN python3 -m venv /home/precice/venv && \
. /home/precice/venv/bin/activate && \
pip3 install --no-cache-dir pyvista
### end of diff_visualizer stage ###


FROM precice_dependencies AS precice
# Build & install precice into /home/precice/precice
ARG PRECICE_PR
Expand Down
3 changes: 2 additions & 1 deletion tools/tests/requirements-reference.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Reference only: run scripts keep using loose requirements.txt constraints.
# Generated by tools/releasing/update-requirements-reference.py — do not edit manually.
# Source: tools/tests/requirements.txt
# Generated: 2026-07-13T16:48:40Z
# Generated: 2026-07-19T05:18:03Z

jinja2==3.1.6
pyvista==0.48.4
pyyaml==6.0.3
1 change: 1 addition & 0 deletions tools/tests/requirements.txt
Comment thread
MakisH marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
jinja2
pyvista
pyyaml
61 changes: 61 additions & 0 deletions tools/tests/systemtests/Systemtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,66 @@ def __archive_fieldcompare_diffs(self) -> None:
self,
)

def __get_diff_visualizer_compose_file(self) -> str:
platform = self.params_to_use.get("PLATFORM")
render_dict = {
'dockerfile_context': (
Path("..") / "tools" / "tests" / "dockerfiles" / Path(platform)
),
'build_arguments': self.params_to_use,
'diff_results_folder': DIFF_RESULTS_DIR,
}
jinja_env = Environment(loader=FileSystemLoader(PRECICE_TESTS_DIR))
template = jinja_env.get_template(
"docker-compose.diff_visualizer.template.yaml")
return template.render(render_dict)

def __visualize_fieldcompare_diffs(self) -> None:
"""Best-effort rendering of archived fieldcompare diff VTK files via Docker."""
diff_results_dir = self.system_test_dir / DIFF_RESULTS_DIR
if not diff_results_dir.is_dir():
return

compose_path = self.system_test_dir / "docker-compose.diff_visualizer.yaml"
try:
compose_path.write_text(
self.__get_diff_visualizer_compose_file(), encoding="utf-8")
result = subprocess.run(
[
"docker",
"compose",
"--file",
compose_path.name,
"up",
"--exit-code-from",
"diff-visualizer",
"--abort-on-container-exit",
],
cwd=self.system_test_dir,
capture_output=True,
text=True,
timeout=300,
check=False,
)
except (OSError, subprocess.TimeoutExpired) as error:
logging.warning(
"Could not render fieldcompare diff visualizations for %s: %s",
self,
error,
)
return

if result.returncode != 0:
details = result.stderr.strip() or result.stdout.strip()
logging.warning(
"Rendering fieldcompare diff visualizations failed for %s: %s",
self,
details,
)
return
if result.stdout.strip():
logging.info(result.stdout.strip())

def __copy_rerun_system_test_script(self) -> None:
"""Copy tools/tests/rerun-system-test.sh into the run directory for artifact replay."""
rerun_src = PRECICE_TESTS_DIR / "rerun-system-test.sh"
Expand Down Expand Up @@ -1131,6 +1191,7 @@ def run(self, run_directory: Path):
std_err.extend(fieldcompare_result.stderr_data)
if fieldcompare_result.exit_code != 0:
self.__archive_fieldcompare_diffs()
self.__visualize_fieldcompare_diffs()
logging.critical(f"Fieldcompare returned non zero exit code, therefore {self} failed")
return SystemtestResult(
False,
Expand Down
Loading