Skip to content

Commit 4a1885d

Browse files
Archive and compare iterations.log for implicit coupling
Archive precice-*-iterations.log files during system tests and compare them against reference copies for implicit-coupling regression checks.
1 parent 1edddb9 commit 4a1885d

4 files changed

Lines changed: 149 additions & 0 deletions

File tree

changelog-entries/743.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Archived `precice-*-iterations.log` files into `iterations-logs/` during system tests and compared them against reference copies for implicit-coupling regression checks ([#743](https://github.com/precice/tutorials/pull/743)).

tools/tests/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ The easiest way to debug a systemtest run is first to have a look at the output
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.
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+
For implicit-coupling runs, `precice-*-iterations.log` files are collected into `iterations-logs/` and compared by hash against archived reference copies (stored next to each reference `.tar.gz` in a `*.iterations-logs/` directory, or legacy `.iterations-hashes.json` sidecars). A mismatch fails the test.
111+
110112
## Adding new tests
111113

112114
### Adding tutorials

tools/tests/generate_reference_results.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from systemtests.Systemtest import Systemtest, GLOBAL_TIMEOUT
66
from pathlib import Path
77
from typing import List
8+
import shutil
89
from paths import PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR
910
import hashlib
1011
from jinja2 import Environment, FileSystemLoader
@@ -179,6 +180,20 @@ def main():
179180
raise RuntimeError(
180181
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")
181182

183+
collected = systemtest._collect_iterations_logs(systemtest.get_system_test_dir())
184+
if collected:
185+
ref_logs_dir = systemtest._iterations_logs_reference_dir()
186+
ref_logs_dir.mkdir(parents=True, exist_ok=True)
187+
for rel, src in collected:
188+
dest = ref_logs_dir / rel
189+
dest.parent.mkdir(parents=True, exist_ok=True)
190+
shutil.copy2(src, dest)
191+
logging.info(
192+
"Wrote iterations logs for %s to %s",
193+
systemtest.reference_result.path.name,
194+
ref_logs_dir,
195+
)
196+
182197
# write readme
183198
for tutorial in reference_result_per_tutorial.keys():
184199
reference_results_dir = tutorial.path / "reference-results"

tools/tests/systemtests/Systemtest.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import hashlib
2+
import json
13
import subprocess
24
from typing import List, Dict, Optional, Tuple
35
from jinja2 import Environment, FileSystemLoader
@@ -23,6 +25,7 @@
2325
SHORT_TIMEOUT = 10
2426

2527
DIFF_RESULTS_DIR = "diff-results"
28+
ITERATIONS_LOGS_DIR = "iterations-logs"
2629

2730

2831
def slugify(value, allow_unicode=False):
@@ -491,6 +494,119 @@ def __archive_fieldcompare_diffs(self) -> None:
491494
self,
492495
)
493496

497+
@staticmethod
498+
def _sha256_file(path: Path) -> str:
499+
"""Compute SHA-256 hex digest of a file."""
500+
h = hashlib.sha256()
501+
mv = memoryview(bytearray(128 * 1024))
502+
with open(path, 'rb', buffering=0) as f:
503+
while n := f.readinto(mv):
504+
h.update(mv[:n])
505+
return h.hexdigest()
506+
507+
def _iterations_logs_reference_dir(self) -> Path:
508+
"""Directory next to the reference tar storing archived iterations.log files."""
509+
stem = self.reference_result.path.name.replace(".tar.gz", "")
510+
return self.reference_result.path.parent / f"{stem}.iterations-logs"
511+
512+
def _collect_iterations_logs(
513+
self, system_test_dir: Path
514+
) -> List[Tuple[str, Path]]:
515+
"""
516+
Collect precice-*-iterations.log files from case dirs.
517+
Returns list of (relative_path, absolute_path) e.g. ("solid-fenics/precice-Solid-iterations.log", path).
518+
"""
519+
collected = []
520+
for case in self.case_combination.cases:
521+
case_dir = system_test_dir / Path(case.path).name
522+
if not case_dir.exists():
523+
continue
524+
for log_file in case_dir.glob("precice-*-iterations.log"):
525+
if log_file.is_file():
526+
rel = f"{Path(case.path).name}/{log_file.name}"
527+
collected.append((rel, log_file))
528+
return collected
529+
530+
def _reference_iterations_hashes(self) -> Optional[Dict[str, str]]:
531+
"""
532+
Load expected iterations.log hashes from archived reference files or a legacy sidecar.
533+
Returns None if no reference data is available.
534+
"""
535+
ref_dir = self._iterations_logs_reference_dir()
536+
if ref_dir.is_dir():
537+
ref_hashes = {}
538+
for log_file in ref_dir.rglob("precice-*-iterations.log"):
539+
if log_file.is_file():
540+
rel = log_file.relative_to(ref_dir).as_posix()
541+
ref_hashes[rel] = self._sha256_file(log_file)
542+
if ref_hashes:
543+
return ref_hashes
544+
545+
sidecar = self.reference_result.path.with_suffix(".iterations-hashes.json")
546+
if not sidecar.exists():
547+
return None
548+
try:
549+
ref_hashes = json.loads(sidecar.read_text())
550+
except (json.JSONDecodeError, OSError) as e:
551+
logging.warning(
552+
"Could not read iterations hashes from %s: %s", sidecar, e
553+
)
554+
return None
555+
return ref_hashes if ref_hashes else None
556+
557+
def __archive_iterations_logs(self) -> None:
558+
"""Copy precice-*-iterations.log from case dirs into iterations-logs/ for CI artifacts."""
559+
collected = self._collect_iterations_logs(self.system_test_dir)
560+
if not collected:
561+
return
562+
dest_dir = self.system_test_dir / ITERATIONS_LOGS_DIR
563+
dest_dir.mkdir(exist_ok=True)
564+
for rel, src in collected:
565+
dest_name = Path(rel).name
566+
if len(collected) > 1:
567+
prefix = Path(rel).parent.name + "_"
568+
dest_name = prefix + dest_name
569+
shutil.copy2(src, dest_dir / dest_name)
570+
logging.debug(
571+
"Archived %d iterations log(s) to %s for %s",
572+
len(collected),
573+
dest_dir,
574+
self,
575+
)
576+
577+
def __compare_iterations_hashes(self) -> bool:
578+
"""
579+
Compare current iterations.log hashes against reference data.
580+
Returns True if comparison passes (or is skipped). Returns False if hashes differ.
581+
"""
582+
ref_hashes = self._reference_iterations_hashes()
583+
if ref_hashes is None:
584+
return True
585+
collected = self._collect_iterations_logs(self.system_test_dir)
586+
current = {rel: self._sha256_file(p) for rel, p in collected}
587+
for rel, expected in ref_hashes.items():
588+
if rel not in current:
589+
logging.critical(
590+
"Missing iterations log %s (expected from reference); %s fails",
591+
rel,
592+
self,
593+
)
594+
return False
595+
if current[rel] != expected:
596+
logging.critical(
597+
"Hash mismatch for %s (iterations.log regression); %s fails",
598+
rel,
599+
self,
600+
)
601+
return False
602+
if len(current) != len(ref_hashes):
603+
extra = set(current) - set(ref_hashes)
604+
logging.critical(
605+
"Unexpected iterations log(s) %s; %s fails", extra, self
606+
)
607+
return False
608+
return True
609+
494610
def _build_docker(self):
495611
"""
496612
Builds the docker image
@@ -664,6 +780,21 @@ def run(self, run_directory: Path):
664780
solver_time=docker_run_result.runtime,
665781
fieldcompare_time=0)
666782

783+
self.__archive_iterations_logs()
784+
if not self.__compare_iterations_hashes():
785+
self.__write_logs(std_out, std_err)
786+
logging.critical(
787+
f"Iterations.log hash comparison failed (regression), {self} failed"
788+
)
789+
return SystemtestResult(
790+
False,
791+
std_out,
792+
std_err,
793+
self,
794+
build_time=docker_build_result.runtime,
795+
solver_time=docker_run_result.runtime,
796+
fieldcompare_time=0)
797+
667798
fieldcompare_result = self._run_field_compare()
668799
std_out.extend(fieldcompare_result.stdout_data)
669800
std_err.extend(fieldcompare_result.stderr_data)

0 commit comments

Comments
 (0)