Files
musrfit/tests/musrview_check/musrview_check.py
Andreas Suter ba939574a4
All checks were successful
Build and Deploy Documentation / build-and-deploy (push) Successful in 27s
added musrview png ref test via ctest
2026-02-18 17:07:20 +01:00

145 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""
Runs musrview --png on a given msr-file, then compares the generated PNGs
against reference images using pixel-level comparison (Pillow).
Usage:
musrview_check.py <musrview> <msr-file> <ref-dir> <test-name>
[--tol T] [--generate] [musrview-opts...]
Modes:
default Compare generated PNGs against references in <ref-dir>/<test-name>/
--generate Generate reference PNGs into <ref-dir>/<test-name>/ (no comparison)
Tolerance metric: mean absolute pixel difference normalised to [0, 1].
0.0 = identical, 1.0 = maximally different. Default tolerance: 0.01 (~1%).
"""
import argparse
import glob
import os
import shutil
import subprocess
import sys
import tempfile
def pixel_diff(img_a_path, img_b_path):
"""Return the mean absolute pixel difference normalised to [0, 1]."""
from PIL import Image
import numpy as np
a = np.asarray(Image.open(img_a_path).convert("RGBA"), dtype=np.float64)
b = np.asarray(Image.open(img_b_path).convert("RGBA"), dtype=np.float64)
if a.shape != b.shape:
return 1.0 # completely different dimensions
return np.mean(np.abs(a - b)) / 255.0
def main():
# ---- argument parsing ----------------------------------------------------
parser = argparse.ArgumentParser(description="musrview PNG integration test")
parser.add_argument("musrview", help="path to musrview executable")
parser.add_argument("msr_file", help="path to msr input file")
parser.add_argument("ref_dir", help="root reference directory")
parser.add_argument("test_name", help="test name (subdirectory in ref_dir)")
parser.add_argument("--tol", type=float, default=0.01,
help="tolerance for pixel comparison (default 0.01)")
parser.add_argument("--generate", action="store_true",
help="generate reference PNGs instead of comparing")
# everything after the known args is forwarded to musrview
args, musrview_opts = parser.parse_known_args()
msr_basename = os.path.splitext(os.path.basename(args.msr_file))[0]
ref_subdir = os.path.join(args.ref_dir, args.test_name)
# ---- run musrview in a temporary directory -------------------------------
work_dir = os.path.dirname(os.path.abspath(args.msr_file))
with tempfile.TemporaryDirectory() as tmp_dir:
# snapshot existing PNGs so we only pick up newly created ones
pre_existing = set(glob.glob(os.path.join(work_dir, f"{msr_basename}_*.png")))
cmd = [args.musrview, args.msr_file, "--png"] + musrview_opts
print(f"running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True,
cwd=work_dir,
env={**os.environ, "MUSRVIEW_PNG_DIR": tmp_dir})
if result.returncode != 0:
print(f"**ERROR** musrview returned exit code {result.returncode}")
print(result.stdout + result.stderr)
return 1
# collect only newly created PNGs (exclude pre-existing ones)
generated = sorted(
p for p in glob.glob(os.path.join(work_dir, f"{msr_basename}_*.png"))
if p not in pre_existing
)
if not generated:
generated = sorted(glob.glob(os.path.join(tmp_dir, f"{msr_basename}_*.png")))
if not generated:
print(f"**ERROR** no PNGs matching '{msr_basename}_*.png' found")
print(f" checked: {work_dir}")
if result.stdout:
print(result.stdout)
return 1
# ---- generate mode ---------------------------------------------------
if args.generate:
os.makedirs(ref_subdir, exist_ok=True)
for png in generated:
dst = os.path.join(ref_subdir, os.path.basename(png))
shutil.copy2(png, dst)
print(f" saved reference: {dst}")
# clean up generated PNGs from work_dir
for png in generated:
if os.path.dirname(os.path.abspath(png)) == os.path.abspath(work_dir):
os.remove(png)
print(f"GENERATE: {len(generated)} reference PNG(s) written to {ref_subdir}")
return 0
# ---- compare mode ----------------------------------------------------
if not os.path.isdir(ref_subdir):
print(f"**ERROR** reference directory not found: {ref_subdir}")
return 1
failures = 0
compared = 0
for png_path in generated:
name = os.path.basename(png_path)
ref_path = os.path.join(ref_subdir, name)
if not os.path.isfile(ref_path):
print(f"FAIL: no reference PNG for {name}")
failures += 1
continue
diff = pixel_diff(png_path, ref_path)
compared += 1
if diff > args.tol:
print(f"FAIL: {name} diff={diff:.6f} > tol={args.tol:.6f}")
failures += 1
else:
print(f"PASS: {name} diff={diff:.6f} <= tol={args.tol:.6f}")
# clean up generated PNGs from work_dir
for png in generated:
if os.path.dirname(os.path.abspath(png)) == os.path.abspath(work_dir):
os.remove(png)
if compared == 0:
print("**ERROR** no PNGs were compared")
return 1
if failures:
print(f"\n{failures} of {len(generated)} PNG(s) FAILED")
return 1
print(f"\nAll {compared} PNG(s) PASSED (tol={args.tol})")
return 0
if __name__ == "__main__":
sys.exit(main())