From 03817d9a963f5806ce795609acdd642b89d40e8e Mon Sep 17 00:00:00 2001 From: Giles Knap Date: Fri, 7 Feb 2025 06:49:32 +0000 Subject: [PATCH] Better error reporting on bad ioc.yaml (#276) * restore tests * restore CRs * remove trailing CR * ignore extra line at end of outputs * raise ValueError for Jinja failures * add better error handling on referenced objects * include jinja error text in value error --- src/ibek/utils.py | 7 +- tests/samples/iocs/DLS8515.ibek.ioc.yaml | 2 +- tests/test_cli.py | 238 +++++++++++++++++++++++ tests/test_unit.py | 5 +- 4 files changed, 245 insertions(+), 7 deletions(-) diff --git a/src/ibek/utils.py b/src/ibek/utils.py index a474132a0..572f70dd9 100644 --- a/src/ibek/utils.py +++ b/src/ibek/utils.py @@ -104,9 +104,10 @@ def render(self, context: Any, template_text: Any) -> str: ioc_yaml_file_name=self.file_name, ioc_name=self.ioc_name, ) - except Exception: - print(f"ERROR RENDERING TEMPLATE:\n{template_text}") - raise + except Exception as e: + raise ValueError( + f"Error rendering template:\n{template_text}\nError:{e}" + ) from e def render_map(self, context: Any, map: Mapping[str, str | None]) -> dict[str, str]: """ diff --git a/tests/samples/iocs/DLS8515.ibek.ioc.yaml b/tests/samples/iocs/DLS8515.ibek.ioc.yaml index 2074cab29..b996849e7 100644 --- a/tests/samples/iocs/DLS8515.ibek.ioc.yaml +++ b/tests/samples/iocs/DLS8515.ibek.ioc.yaml @@ -11,7 +11,7 @@ entities: - type: ipac.Hy8002 name: BLX4I-VA-IOC-01-Slot4 - # interrupt_vector: Vec1 + interrupt_vector: Vec1 slot: 4 - type: DLS8515.DLS8515 diff --git a/tests/test_cli.py b/tests/test_cli.py index beab7e4e3..ce81f6f07 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,247 @@ +""" +System tests that run the CLI commands and compare the output to expected +results. +""" + +import json import subprocess import sys +from pathlib import Path + +import pytest +from pytest_mock import MockerFixture +import ibek.utils as utils from ibek import __version__ +from ibek.globals import ( + GLOBALS, + PVI_YAML_PATTERN, + SUPPORT_YAML_PATTERN, +) +from ibek.runtime_cmds.commands import generate +from ibek.support_cmds.commands import generate_links +from tests.conftest import run_cli def test_cli_version(): cmd = [sys.executable, "-m", "ibek", "--version"] assert subprocess.check_output(cmd).decode().strip() == __version__ + + +def test_ibek_schema(tmp_path: Path, samples: Path): + """generate the global ibek schema""" + schema_path = tmp_path / "schema.json" + run_cli("support", "generate-schema", "--output", schema_path) + expected = json.loads( + (samples / "schemas" / "ibek.support.schema.json").read_text() + ) + + actual = json.loads((schema_path).read_text()) + assert expected == actual + + +def test_single_schema(tmp_path: Path, samples: Path): + """generate schema from a support module definition yaml""" + + schema_path = tmp_path / "single.ibek.support.schema.json" + yaml_path = samples / "support" / "motorSim.ibek.support.yaml" + run_cli( + "ioc", "generate-schema", "--no-ibek-defs", yaml_path, "--output", schema_path + ) + + expected = json.loads( + (samples / "schemas" / "single.ibek.ioc.schema.json").read_text() + ) + + actual = json.loads((schema_path).read_text()) + assert expected == actual + + +def test_motor_sim_schema(tmp_path: Path, samples: Path): + """generate schema for a container with two support modules""" + + schema_combined = tmp_path / "motorSim.ibek.ioc.schema.json" + yaml_path1 = samples / "support" / "asyn.ibek.support.yaml" + yaml_path2 = samples / "support" / "motorSim.ibek.support.yaml" + run_cli( + "ioc", + "generate-schema", + "--no-ibek-defs", + yaml_path1, + yaml_path2, + "--output", + schema_combined, + ) + + expected = json.loads( + (samples / "schemas" / "motorSim.ibek.ioc.schema.json").read_text() + ) + + actual = json.loads((schema_combined).read_text()) + assert expected == actual + + +def test_build_runtime_motorSim(tmp_epics_root: Path, samples: Path): + """ + build an ioc runtime script from an IOC instance entity file + and multiple support module definition files + + Also verifies database subst file generation for multiple + entity instantiations. + """ + + generic_generate( + tmp_epics_root, + samples, + "motorSim", + ["motorSim", "asyn"], + ) + + +def test_build_utils_features(tmp_epics_root: Path, samples: Path): + """ + build an ioc runtime script to verify utils features + """ + generic_generate( + tmp_epics_root, + samples, + "utils", + ["utils"], + ) + + +def test_generate_links_ibek(samples: Path, mocker: MockerFixture): + symlink_mock = mocker.patch("ibek.support_cmds.commands.symlink_files") + + generate_links(samples / "support") + + symlink_mock.assert_any_call( + samples / "support", PVI_YAML_PATTERN, GLOBALS.PVI_DEFS + ) + symlink_mock.assert_any_call( + samples / "support", SUPPORT_YAML_PATTERN, GLOBALS.IBEK_DEFS + ) + + +def test_ipac(tmp_epics_root: Path, samples: Path): + """ + Tests that an id argument can include another argument in its default value + """ + + # reset the InterruptVector counter to its initial state (if already used) + if "InterruptVector" in utils.UTILS.variables: + utils.UTILS.variables["InterruptVector"] = 191 + + generic_generate( + tmp_epics_root, + samples, + "ipac-test", + ["ipac", "epics"], + ) + + +def test_gauges(tmp_epics_root: Path, samples: Path): + """ + Tests that an id argument can include another argument in its default value + """ + generic_generate( + tmp_epics_root, + samples, + "gauges", + ["asyn", "gauges"], + ) + + +def test_quadem(tmp_epics_root: Path, samples: Path): + """ + Tests the use of CollectionDefinitions in an IOC instance + this example uses the tetramm beam position monitor module + """ + generic_generate( + tmp_epics_root, + samples, + "quadem", + ["ADCore", "quadem"], + ) + + +def generic_generate( + epics_root: Path, samples: Path, ioc_name: str, support_names: list[str] +): + ioc_yaml = samples / "iocs" / f"{ioc_name}.ibek.ioc.yaml" + support_yamls = [ + samples / "support" / f"{name}.ibek.support.yaml" for name in support_names + ] + expected_outputs = samples / "outputs" / ioc_name + + generate(ioc_yaml, support_yamls) + + for output in expected_outputs.glob("*"): + actual = epics_root / "runtime" / output.name + if not actual.exists(): + actual = epics_root / "opi" / output.name + assert actual.exists(), "Missing output file" + assert output.read_text().strip() == actual.read_text().strip() + + +def test_andreas_motors(tmp_epics_root: Path, samples: Path): + """ + Motor and axis example + """ + generic_generate( + tmp_epics_root, + samples, + "technosoft", + ["asyn", "technosoft"], + ) + + +def test_list(tmp_epics_root: Path, samples: Path): + """ + Motor and axis example + """ + generic_generate( + tmp_epics_root, + samples, + "listarg", + ["listarg"], + ) + + +def test_fast_vacuum(tmp_epics_root: Path, samples: Path): + """ + Cut down copy of dlsPLC containing fast vacuum master and channel + """ + generic_generate( + tmp_epics_root, + samples, + "fastVacuum", + ["fastVacuum"], + ) + + +def test_dls_plc(tmp_epics_root: Path, samples: Path): + """ + Cut down copy of dlsPLC containing fast vacuum master and channel + """ + generic_generate( + tmp_epics_root, + samples, + "dlsPLC", + ["dlsPLC", "asyn"], + ) + + +def test_dls8515(tmp_epics_root: Path, samples: Path): + """ + Cut down copy of dlsPLC containing fast vacuum master and channel + """ + with pytest.raises(ValueError) as ctx: + generic_generate( + tmp_epics_root, + samples, + "DLS8515", + ["DLS8515", "ipac", "epics"], + ) + + assert "`ipac.Hy8002`.interrupt_vector" in str(ctx.value) diff --git a/tests/test_unit.py b/tests/test_unit.py index 5698aa951..a5d5caf8e 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -6,7 +6,6 @@ import shutil from pathlib import Path -import jinja2 import pytest from ibek.commands import semver_compare @@ -87,11 +86,11 @@ def test_strict(): assert text == "giles of age 59" my_template = "{{ person.name ~ ' of age ' ~ height }}" - with pytest.raises(jinja2.exceptions.UndefinedError): + with pytest.raises(ValueError): text = UTILS.render({"person": p}, my_template) my_template = "{{ person.name ~ ' of age ' ~ person.height }}" - with pytest.raises(jinja2.exceptions.UndefinedError): + with pytest.raises(ValueError): text = UTILS.render({"person": p}, my_template)