Skip to content

Commit 4ed9d9f

Browse files
Archive and compare iterations.log for implicit coupling (fixes #440)
1 parent e9c2f2e commit 4ed9d9f

4 files changed

Lines changed: 115 additions & 2 deletions

File tree

changelog-entries/440.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Archive `precice-*-iterations.log` files into `iterations-logs/` and compare them by SHA-256 hash against reference data for implicit-coupling regression checks; reference hashes are stored in `.iterations-hashes.json` sidecar files (fixes [#440](https://github.com/precice/tutorials/issues/440)).

tools/tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ In this case, building and running seems to work out, but the tests fail because
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.
107107
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.
108-
Inside the archive, a test-specific subfolder like `flow-over-heated-plate_fluid-openfoam-solid-fenics_2023-11-19-211723` contains two log files: a `stderr.log` and `stdout.log`. This can be a starting point for a further investigation.
108+
Inside the archive, a test-specific subfolder like `flow-over-heated-plate_fluid-openfoam-solid-fenics_2023-11-19-211723` contains two log files: a `stderr.log` and `stdout.log`. This can be a starting point for a further investigation. For implicit-coupling runs, `precice-*-iterations.log` files are collected into `iterations-logs/` and compared by hash against reference data (when a corresponding `.iterations-hashes.json` sidecar exists); a mismatch fails the test.
109109

110110
## Adding new tests
111111

tools/tests/generate_reference_results.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from paths import PRECICE_TUTORIAL_DIR, PRECICE_TESTS_RUN_DIR, PRECICE_TESTS_DIR, PRECICE_REL_OUTPUT_DIR
1717
import time
18+
import json
1819

1920

2021
def create_tar_gz(source_folder: Path, output_filename: Path):
@@ -139,6 +140,16 @@ def main():
139140
raise RuntimeError(
140141
f"Error executing: \n {systemtest} \n Could not find result folder {reference_result_folder}\n Probably the tutorial did not run through properly. Please check corresponding logs")
141142

143+
# Write iterations.log hashes sidecar for implicit-coupling regression checks (issue #440)
144+
collected = systemtest._collect_iterations_logs(systemtest.get_system_test_dir())
145+
if collected:
146+
hashes = {
147+
rel: Systemtest._sha256_file(p) for rel, p in collected
148+
}
149+
sidecar = systemtest.reference_result.path.with_suffix(".iterations-hashes.json")
150+
sidecar.write_text(json.dumps(hashes, sort_keys=True, indent=2))
151+
logging.info(f"Wrote iterations hashes for {systemtest.reference_result.path.name}")
152+
142153
# write readme
143154
for tutorial in reference_result_per_tutorial.keys():
144155
reference_results_dir = tutorial.path / "reference-results"

tools/tests/systemtests/Systemtest.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import hashlib
2+
import json
13
import subprocess
2-
from typing import List, Dict, Optional
4+
from typing import List, Dict, Optional, Tuple
35
from jinja2 import Environment, FileSystemLoader
46
from dataclasses import dataclass, field
57
import shutil
68
from pathlib import Path
79
from paths import PRECICE_REL_OUTPUT_DIR, PRECICE_TOOLS_DIR, PRECICE_REL_REFERENCE_DIR, PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR
810

11+
ITERATIONS_LOGS_DIR = "iterations-logs"
12+
913
from metadata_parser.metdata import Tutorial, CaseCombination, Case, ReferenceResult
1014
from .SystemtestArguments import SystemtestArguments
1115

@@ -413,6 +417,88 @@ def _run_field_compare(self):
413417
elapsed_time = time.perf_counter() - time_start
414418
return FieldCompareResult(1, stdout_data, stderr_data, self, elapsed_time)
415419

420+
@staticmethod
421+
def _sha256_file(path: Path) -> str:
422+
"""Compute SHA-256 hex digest of a file."""
423+
h = hashlib.sha256()
424+
mv = memoryview(bytearray(128 * 1024))
425+
with open(path, 'rb', buffering=0) as f:
426+
while n := f.readinto(mv):
427+
h.update(mv[:n])
428+
return h.hexdigest()
429+
430+
def _collect_iterations_logs(
431+
self, system_test_dir: Path
432+
) -> List[Tuple[str, Path]]:
433+
"""
434+
Collect precice-*-iterations.log files from case dirs.
435+
Returns list of (relative_path, absolute_path) e.g. ("solid-fenics/precice-Solid-iterations.log", path).
436+
"""
437+
collected = []
438+
for case in self.case_combination.cases:
439+
case_dir = system_test_dir / Path(case.path).name
440+
if not case_dir.exists():
441+
continue
442+
for log_file in case_dir.glob("precice-*-iterations.log"):
443+
if log_file.is_file():
444+
rel = f"{Path(case.path).name}/{log_file.name}"
445+
collected.append((rel, log_file))
446+
return collected
447+
448+
def __archive_iterations_logs(self):
449+
"""
450+
Copy precice-*-iterations.log from case dirs into iterations-logs/
451+
so they are available in CI artifacts (issue #440).
452+
"""
453+
collected = self._collect_iterations_logs(self.system_test_dir)
454+
if not collected:
455+
return
456+
dest_dir = self.system_test_dir / ITERATIONS_LOGS_DIR
457+
dest_dir.mkdir(exist_ok=True)
458+
for rel, src in collected:
459+
dest_name = Path(rel).name
460+
if len(collected) > 1:
461+
prefix = Path(rel).parent.name + "_"
462+
dest_name = prefix + dest_name
463+
shutil.copy2(src, dest_dir / dest_name)
464+
logging.debug(f"Archived {len(collected)} iterations log(s) to {dest_dir} for {self}")
465+
466+
def __compare_iterations_hashes(self) -> bool:
467+
"""
468+
Compare current iterations.log hashes against reference sidecar.
469+
Returns True if comparison passes (or is skipped). Returns False if hashes differ.
470+
"""
471+
sidecar = self.reference_result.path.with_suffix(".iterations-hashes.json")
472+
if not sidecar.exists():
473+
return True
474+
try:
475+
ref_hashes = json.loads(sidecar.read_text())
476+
except (json.JSONDecodeError, OSError) as e:
477+
logging.warning(f"Could not read iterations hashes from {sidecar}: {e}")
478+
return True
479+
if not ref_hashes:
480+
return True
481+
collected = self._collect_iterations_logs(self.system_test_dir)
482+
current = {rel: self._sha256_file(p) for rel, p in collected}
483+
for rel, expected in ref_hashes.items():
484+
if rel not in current:
485+
logging.critical(
486+
f"Missing iterations log {rel} (expected from reference); {self} fails"
487+
)
488+
return False
489+
if current[rel] != expected:
490+
logging.critical(
491+
f"Hash mismatch for {rel} (iterations.log regression); {self} fails"
492+
)
493+
return False
494+
if len(current) != len(ref_hashes):
495+
extra = set(current) - set(ref_hashes)
496+
logging.critical(
497+
f"Unexpected iterations log(s) {extra}; {self} fails"
498+
)
499+
return False
500+
return True
501+
416502
def _build_docker(self):
417503
"""
418504
Builds the docker image
@@ -562,6 +648,21 @@ def run(self, run_directory: Path):
562648
solver_time=docker_run_result.runtime,
563649
fieldcompare_time=0)
564650

651+
self.__archive_iterations_logs()
652+
if not self.__compare_iterations_hashes():
653+
self.__write_logs(std_out, std_err)
654+
logging.critical(
655+
f"Iterations.log hash comparison failed (regression), {self} failed"
656+
)
657+
return SystemtestResult(
658+
False,
659+
std_out,
660+
std_err,
661+
self,
662+
build_time=docker_build_result.runtime,
663+
solver_time=docker_run_result.runtime,
664+
fieldcompare_time=0)
665+
565666
fieldcompare_result = self._run_field_compare()
566667
std_out.extend(fieldcompare_result.stdout_data)
567668
std_err.extend(fieldcompare_result.stderr_data)

0 commit comments

Comments
 (0)