diff --git a/tests/data/isa51_tags.json b/tests/data/isa51_tags.json index 843dc8a..ce1eef7 100644 --- a/tests/data/isa51_tags.json +++ b/tests/data/isa51_tags.json @@ -58,17 +58,18 @@ ["CIC-903", "Indicating Controller", "903"] ], "invalid": [ - ["FCI-101", "I"], - ["FIH-102", "H"], - ["F-103", "function"], - ["FICC-104", "repeated"], - ["PSVX-105", "X"], - ["ABCDEF-106", "D"], - ["FQQ-107", "Q"], - ["1FC", "format"], - ["FIC", "format"], - ["fic-101-", "format"], - ["LSO-108", "O"] + ["FCI-101", "unexpected letter I at position 3"], + ["FIH-102", "trailing H needs an alarm (A) or switch (S) before it"], + ["F-103", "needs at least one function letter after F"], + ["FICC-104", "repeated letter C at position 4"], + ["PSVX-105", "unexpected letter X at position 4 after valve V"], + ["ABCDE-106", "unexpected letter D at position 4"], + ["ABCDEF-106", "not in [area-]LETTERS[-]loop[suffix] format"], + ["FQQ-107", "unexpected letter Q at position 3"], + ["1FC", "not in [area-]LETTERS[-]loop[suffix] format"], + ["FIC", "not in [area-]LETTERS[-]loop[suffix] format"], + ["fic-101-", "not in [area-]LETTERS[-]loop[suffix] format"], + ["LSO-108", "trailing O (open/closed) is only valid after a switch on a position (Z) tag"] ], "formats": [ ["10-FIC-101A", "10", "FIC", "101", "A"], diff --git a/tests/test_pid_tags.py b/tests/test_pid_tags.py index 1d80954..0e17177 100644 --- a/tests/test_pid_tags.py +++ b/tests/test_pid_tags.py @@ -39,7 +39,23 @@ def test_invalid_tags_name_the_offending_letter(tag, fragment): out = parse_tag(tag, kind="instrument") assert out["valid"] is False assert out["tag"] == tag - assert any(fragment in err for err in out["errors"]), out["errors"] + # The grammar stops at the first offending letter, so the fixture pins + # exactly one error and the fragment must sit in *that* one. + assert len(out["errors"]) == 1, out["errors"] + assert fragment in out["errors"][0], out["errors"] + + +@pytest.mark.parametrize("tag, fragment", FIXTURE["invalid"]) +def test_invalid_fixture_fragments_bind_the_letter_and_position(tag, fragment): + # Every grammar error is prefixed with the tag's own letters, so a fragment + # that is a substring of the tag ("I" for FCI) matches whatever the grammar + # says and pins nothing. A row must quote the rule text, and a grammar row + # must quote the reported position as well as the letter. + assert fragment not in tag.upper(), f"{tag!r}: fragment {fragment!r} is part of the tag" + if "format" not in fragment: + assert "position" in fragment or "letter" in fragment or "trailing" in fragment, ( + f"{tag!r}: fragment {fragment!r} names neither a letter nor a position" + ) @pytest.mark.parametrize("tag, area, letters, loop, suffix", FIXTURE["formats"])