|
| 1 | +import hashlib |
| 2 | +import json |
1 | 3 | import subprocess |
2 | | -from typing import List, Dict, Optional |
| 4 | +from typing import List, Dict, Optional, Tuple |
3 | 5 | from jinja2 import Environment, FileSystemLoader |
4 | 6 | from dataclasses import dataclass, field |
5 | 7 | import shutil |
6 | 8 | from pathlib import Path |
7 | 9 | from paths import PRECICE_REL_OUTPUT_DIR, PRECICE_TOOLS_DIR, PRECICE_REL_REFERENCE_DIR, PRECICE_TESTS_DIR, PRECICE_TUTORIAL_DIR |
8 | 10 |
|
| 11 | +ITERATIONS_LOGS_DIR = "iterations-logs" |
| 12 | + |
9 | 13 | from metadata_parser.metdata import Tutorial, CaseCombination, Case, ReferenceResult |
10 | 14 | from .SystemtestArguments import SystemtestArguments |
11 | 15 |
|
@@ -413,6 +417,88 @@ def _run_field_compare(self): |
413 | 417 | elapsed_time = time.perf_counter() - time_start |
414 | 418 | return FieldCompareResult(1, stdout_data, stderr_data, self, elapsed_time) |
415 | 419 |
|
| 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 | + |
416 | 502 | def _build_docker(self): |
417 | 503 | """ |
418 | 504 | Builds the docker image |
@@ -562,6 +648,21 @@ def run(self, run_directory: Path): |
562 | 648 | solver_time=docker_run_result.runtime, |
563 | 649 | fieldcompare_time=0) |
564 | 650 |
|
| 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 | + |
565 | 666 | fieldcompare_result = self._run_field_compare() |
566 | 667 | std_out.extend(fieldcompare_result.stdout_data) |
567 | 668 | std_err.extend(fieldcompare_result.stderr_data) |
|
0 commit comments