"""Synthetic, network-free tests for scientific-visualization helpers.""" from __future__ import annotations import json import importlib.util import stat import sys import tempfile import unittest from pathlib import Path SKILL_ROOT = Path(__file__).resolve().parents[2] / "skills" / "scientific-visualization" SCRIPTS = SKILL_ROOT / "scripts" ASSETS = SKILL_ROOT / "assets" sys.path.insert(0, str(SCRIPTS)) sys.path.insert(0, str(ASSETS)) import _common # noqa: E402 import color_palettes # noqa: E402 import export_plan # noqa: E402 import figure_export # noqa: E402 import image_metadata # noqa: E402 import palette_audit # noqa: E402 import style_presets # noqa: E402 import skill_contract class CommonSafetyTests(unittest.TestCase): def test_json_output_is_private_and_refuses_overwrite(self) -> None: with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "report.json" _common.emit_json({"ok": True}, output=output) self.assertEqual(json.loads(output.read_text()), {"ok": True}) self.assertEqual(stat.S_IMODE(output.stat().st_mode), 0o600) with self.assertRaises(_common.CliError): _common.emit_json({"ok": False}, output=output) def test_input_and_output_symlinks_are_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) real = root / "real.txt" real.write_text("data", encoding="utf-8") link = root / "link.txt" try: link.symlink_to(real) except OSError: self.skipTest("symlinks unavailable") with self.assertRaises(_common.CliError): _common.checked_input_file(link) with self.assertRaises(_common.CliError): _common.checked_output_file(link, force=True) class PaletteTests(unittest.TestCase): def test_wcag_contrast_reference_values(self) -> None: black = palette_audit.parse_hex_color("#000000") white = palette_audit.parse_hex_color("#FFFFFF") self.assertAlmostEqual(palette_audit.contrast_ratio(black, white), 21.0) self.assertAlmostEqual(palette_audit.cie_lstar(black), 0.0) self.assertAlmostEqual(palette_audit.cie_lstar(white), 100.0) def test_on_white_subset_meets_graphical_threshold(self) -> None: report = palette_audit.audit_palette( color_palettes.OKABE_ITO_ON_WHITE, background="#FFFFFF", role="graphical", ) self.assertEqual(report["contrast_screen"]["review_count"], 0) self.assertEqual(report["palette"]["color_count"], 5) def test_tol_palettes_match_current_issue(self) -> None: self.assertEqual( color_palettes.TOL_HIGH_CONTRAST, ["#004488", "#DDAA33", "#BB5566"], ) self.assertEqual(len(color_palettes.TOL_MUTED), 9) self.assertEqual(color_palettes.TOL_MUTED[0], "#CC6677") def test_grayscale_screen_is_explicitly_heuristic(self) -> None: report = palette_audit.audit_palette( ["#FF0000", "#00FF00"], grayscale_min_delta=90, ) self.assertEqual(report["grayscale_screen"]["review_count"], 1) self.assertIn("Heuristic", report["grayscale_screen"]["basis"]) class MetadataTests(unittest.TestCase): def test_svg_dimensions_and_external_resource_count(self) -> None: with tempfile.TemporaryDirectory() as directory: svg = Path(directory) / "figure.svg" svg.write_text( '' 'Label' '' "", encoding="utf-8", ) report = image_metadata.inspect_file(svg) metadata = report["metadata"] self.assertEqual(metadata["format"], "SVG") self.assertAlmostEqual(metadata["width_mm"], 25.4) self.assertAlmostEqual(metadata["height_mm"], 25.4) self.assertEqual(metadata["text_element_count"], 1) self.assertEqual(metadata["external_image_count"], 1) def test_eps_bounding_box(self) -> None: with tempfile.TemporaryDirectory() as directory: eps = Path(directory) / "figure.eps" eps.write_text( "%!PS-Adobe-3.0 EPSF-3.0\n" "%%BoundingBox: 0 0 144 72\n" "%%EOF\n", encoding="latin-1", ) report = image_metadata.inspect_file(eps) self.assertAlmostEqual(report["metadata"]["width_mm"], 50.8) self.assertAlmostEqual(report["metadata"]["height_mm"], 25.4) def test_svg_dtd_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: svg = Path(directory) / "unsafe.svg" svg.write_text( ']>' '&x;', encoding="utf-8", ) with self.assertRaises(_common.CliError): image_metadata.inspect_file(svg) def test_raster_dimensions_dpi_mode_and_screen(self) -> None: try: from PIL import Image except ImportError: self.skipTest("Pillow not installed") with tempfile.TemporaryDirectory() as directory: image_path = Path(directory) / "synthetic.tiff" Image.new("RGB", (300, 150), "white").save( image_path, dpi=(300, 300), compression="tiff_lzw", ) inspected = image_metadata.inspect_file(image_path) screened = image_metadata.screen_metadata( inspected, expected_formats=["tiff"], expected_modes=["RGB"], min_dpi=300, target_width_mm=25.4, alpha_policy="forbid", ) self.assertEqual(inspected["metadata"]["mode"], "RGB") self.assertEqual(inspected["metadata"]["width_px"], 300) self.assertEqual(screened["screening_summary"]["fail"], 0) def test_effective_dpi_tolerates_binary_float_roundoff(self) -> None: report = { "schema_version": "1.0", "input": {"size_bytes": 1}, "metadata": { "format": "PNG", "kind": "raster", "width_px": 2100, "height_px": 1500, "dpi_x": None, "dpi_y": None, "mode": "RGB", }, } screened = image_metadata.screen_metadata( report, min_dpi=300, target_width_mm=177.8, ) self.assertEqual(screened["screening_summary"]["fail"], 0) class PublisherProfileTests(unittest.TestCase): def test_profiles_capture_phase_and_current_dimensions(self) -> None: nature = export_plan.build_plan( "nature", figure_type="combination", width="single", phase="final", ) science = export_plan.build_plan( "science", figure_type="line-art", width="full", phase="revised", ) cell = export_plan.build_plan( "cell", figure_type="line-art", width="full", phase="final", ) self.assertEqual(nature["width"]["millimeters"], 89.0) self.assertEqual(science["width"]["millimeters"], 184.0) self.assertEqual(cell["width"]["millimeters"], 174.0) self.assertTrue(nature["phase_matches_snapshot"]) self.assertIn("do not establish", nature["notice"]) def test_phase_mismatch_is_not_silenced(self) -> None: plan = export_plan.build_plan( "plos", figure_type="photo", phase="initial", ) self.assertFalse(plan["phase_matches_snapshot"]) self.assertEqual(plan["profile_phase"], "post-provisional-accept") def test_acs_profile_does_not_invent_formats_or_dpi(self) -> None: plan = export_plan.build_plan( "acs", figure_type="combination", width="single", ) self.assertIsNone(plan["formats"]) self.assertIsNone(plan["raster_dpi"]) def test_plos_synthetic_tiff_matches_recorded_properties(self) -> None: try: from PIL import Image except ImportError: self.skipTest("Pillow not installed") plan = export_plan.build_plan( "plos", figure_type="photo", width="full", phase="post-provisional-accept", ) with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "Fig1.tiff" Image.new("RGB", (2250, 1000), "white").save( path, dpi=(300, 300), compression="tiff_lzw", ) result = export_plan.validate_against_plan(plan, path) self.assertEqual(result["screening_summary"]["fail"], 0) class StyleTests(unittest.TestCase): def test_plain_style_has_dimension_preserving_export_default(self) -> None: style = style_presets.get_style("default") self.assertEqual(style["savefig.bbox"], "standard") self.assertEqual(style["pdf.fonttype"], 42) self.assertEqual(len(style["_palette_colors"]), 5) def test_all_mplstyle_assets_parse_when_matplotlib_available(self) -> None: try: import matplotlib as mpl except ImportError: self.skipTest("Matplotlib not installed") for path in sorted(ASSETS.glob("*.mplstyle")): with self.subTest(path=path.name): parsed = mpl.rc_params_from_file( path, fail_on_error=True, use_default_template=False, ) self.assertIn("axes.prop_cycle", parsed) def test_generated_style_parses_when_matplotlib_available(self) -> None: try: import matplotlib as mpl except ImportError: self.skipTest("Matplotlib not installed") with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "generated.mplstyle" style_presets.create_style_template(output) parsed = mpl.rc_params_from_file( output, fail_on_error=True, use_default_template=False, ) self.assertEqual(parsed["pdf.fonttype"], 42) class ExportTests(unittest.TestCase): def test_profile_export_requires_explicit_confirmation(self) -> None: with self.assertRaises(_common.CliError): figure_export.save_for_journal( object(), "unused", "nature", ) with self.assertRaises(_common.CliError): figure_export.save_for_journal( object(), "unused", "nature", confirm_profile=True, ) def test_atomic_export_and_manifest(self) -> None: try: import matplotlib matplotlib.use("Agg") import matplotlib as mpl import matplotlib.pyplot as plt from PIL import Image except ImportError: self.skipTest("Matplotlib not installed") with tempfile.TemporaryDirectory() as directory: base = Path(directory) / "figure" fig, ax = plt.subplots(figsize=(2, 1)) ax.plot([0, 1], [0, 1]) try: with mpl.rc_context({"savefig.bbox": "tight"}): report = figure_export.export_figure( fig, base, formats=["png", "pdf"], dpi=150, provenance={"raw_data": "synthetic"}, write_manifest=True, ) self.assertEqual(len(report["outputs"]), 2) self.assertTrue((Path(directory) / "figure.export.json").exists()) with Image.open(Path(directory) / "figure.png") as image: self.assertEqual(image.size, (300, 150)) if importlib.util.find_spec("pypdf") is not None: font_report = figure_export.verify_font_embedding( Path(directory) / "figure.pdf" ) resources = font_report["font_resources"] self.assertGreater(resources["resource_count"], 0) self.assertTrue(resources["all_embedded"]) with self.assertRaises(_common.CliError): figure_export.export_figure( fig, base, formats=["png"], dpi=150, ) finally: plt.close(fig) # The shared --help contract: every argparse CLI this skill ships answers --help # without doing any work. It skips when the skill's packages are absent and runs # for real under `python tests/run_all.py --isolated`. CliHelpTests = skill_contract.cli.help_test_case(SKILL_ROOT) if __name__ == "__main__": unittest.main()