|
| 1 | +import hashlib |
| 2 | +import json |
1 | 3 | import subprocess |
2 | 4 | from typing import List, Dict, Optional, Tuple |
3 | 5 | from jinja2 import Environment, FileSystemLoader |
|
23 | 25 | SHORT_TIMEOUT = 10 |
24 | 26 |
|
25 | 27 | DIFF_RESULTS_DIR = "diff-results" |
| 28 | +ITERATIONS_LOGS_DIR = "iterations-logs" |
26 | 29 |
|
27 | 30 |
|
28 | 31 | def slugify(value, allow_unicode=False): |
@@ -491,6 +494,119 @@ def __archive_fieldcompare_diffs(self) -> None: |
491 | 494 | self, |
492 | 495 | ) |
493 | 496 |
|
| 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 | + |
494 | 610 | def _build_docker(self): |
495 | 611 | """ |
496 | 612 | Builds the docker image |
@@ -664,6 +780,21 @@ def run(self, run_directory: Path): |
664 | 780 | solver_time=docker_run_result.runtime, |
665 | 781 | fieldcompare_time=0) |
666 | 782 |
|
| 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 | + |
667 | 798 | fieldcompare_result = self._run_field_compare() |
668 | 799 | std_out.extend(fieldcompare_result.stdout_data) |
669 | 800 | std_err.extend(fieldcompare_result.stderr_data) |
|
0 commit comments