merge: track-a task 6
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"valid": [
|
||||
["FIC-101", "Flow Indicating Controller", "101"],
|
||||
["FT-101", "Flow Transmitter", "101"],
|
||||
["FE-101", "Flow Element", "101"],
|
||||
["FQI-102", "Flow Totalizing Indicator", "102"],
|
||||
["FFC-103", "Flow Ratio Controller", "103"],
|
||||
["FY-104", "Flow Relay", "104"],
|
||||
["FCV-105", "Flow Control Valve", "105"],
|
||||
["FIT-106", "Flow Indicating Transmitter", "106"],
|
||||
["PT-201", "Pressure Transmitter", "201"],
|
||||
["PI-201", "Pressure Indicator", "201"],
|
||||
["PG-202", "Pressure Gauge", "202"],
|
||||
["PDT-203", "Pressure Differential Transmitter", "203"],
|
||||
["PDI-203", "Pressure Differential Indicator", "203"],
|
||||
["PDIC-204", "Pressure Differential Indicating Controller", "204"],
|
||||
["PSV-205", "Pressure Safety Valve", "205"],
|
||||
["PSL-206", "Pressure Switch Low", "206"],
|
||||
["PSHH-207", "Pressure Switch High-high", "207"],
|
||||
["PCV-208", "Pressure Control Valve", "208"],
|
||||
["PIC-209", "Pressure Indicating Controller", "209"],
|
||||
["LT-301", "Level Transmitter", "301"],
|
||||
["LG-302", "Level Gauge", "302"],
|
||||
["LAHH-303", "Level Alarm High-high", "303"],
|
||||
["LAL-304", "Level Alarm Low", "304"],
|
||||
["LSLL-305", "Level Switch Low-low", "305"],
|
||||
["LIC-306", "Level Indicating Controller", "306"],
|
||||
["LCV-307", "Level Control Valve", "307"],
|
||||
["LS-308", "Level Switch", "308"],
|
||||
["TE-401", "Temperature Element", "401"],
|
||||
["TW-402", "Temperature Well", "402"],
|
||||
["TIC-403", "Temperature Indicating Controller", "403"],
|
||||
["TSE-404", "Temperature Safety Element", "404"],
|
||||
["TAH-405", "Temperature Alarm High", "405"],
|
||||
["TT-406", "Temperature Transmitter", "406"],
|
||||
["TCV-407", "Temperature Control Valve", "407"],
|
||||
["AIT-501", "Analysis Indicating Transmitter", "501"],
|
||||
["HV-601", "Hand Valve", "601"],
|
||||
["HS-602", "Hand Switch", "602"],
|
||||
["HIC-603", "Hand Indicating Controller", "603"],
|
||||
["ZSO-701", "Position Switch Open", "701"],
|
||||
["ZSC-702", "Position Switch Closed", "702"],
|
||||
["ZT-703", "Position Transmitter", "703"],
|
||||
["SIC-801", "Speed Indicating Controller", "801"],
|
||||
["WIT-802", "Weight Indicating Transmitter", "802"],
|
||||
["VT-803", "Vibration Transmitter", "803"],
|
||||
["JI-804", "Power Indicator", "804"],
|
||||
["EI-805", "Voltage Indicator", "805"],
|
||||
["II-806", "Current Indicator", "806"],
|
||||
["BE-807", "Burner Element", "807"],
|
||||
["RE-808", "Radiation Element", "808"],
|
||||
["UY-809", "Multivariable Relay", "809"],
|
||||
["PL-810", "Pressure Light", "810"]
|
||||
],
|
||||
"user_defined": [
|
||||
["XV-901", "Unclassified Valve", "901"],
|
||||
["NV-902", "Valve", "902"],
|
||||
["CIC-903", "Indicating Controller", "903"]
|
||||
],
|
||||
"invalid": [
|
||||
["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"],
|
||||
["FIC 101", null, "FIC", "101", null],
|
||||
["FIC101", null, "FIC", "101", null],
|
||||
["fic-101b", null, "FIC", "101", "B"]
|
||||
],
|
||||
"equipment": [
|
||||
["P-101A", "pump", "101", "A"],
|
||||
["V-201", "vessel", "201", ""],
|
||||
["E-301", "exchanger", "301", ""],
|
||||
["10-K-401", "compressor", "401", ""]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"""ISA-5.1 identification-letter grammar, pinned against a reader-checkable fixture."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from engineering.pid.tags import (
|
||||
DEFAULT_EQUIPMENT_PREFIXES,
|
||||
describe_tables,
|
||||
parse_tag,
|
||||
split_instrument_tag,
|
||||
)
|
||||
|
||||
FIXTURE = json.loads((Path(__file__).parent / "data" / "isa51_tags.json").read_text("utf-8"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag, description, loop", FIXTURE["valid"])
|
||||
def test_valid_tags_parse_with_the_standard_description(tag, description, loop):
|
||||
out = parse_tag(tag)
|
||||
assert out["valid"] is True, out["errors"]
|
||||
assert out["kind"] == "instrument"
|
||||
assert out["description"] == description
|
||||
assert out["loop"] == loop
|
||||
assert out["user_defined"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag, description, loop", FIXTURE["user_defined"])
|
||||
def test_user_choice_letters_parse_as_user_defined(tag, description, loop):
|
||||
out = parse_tag(tag)
|
||||
assert out["valid"] is True and out["user_defined"] is True
|
||||
assert out["description"] == description
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag, fragment", FIXTURE["invalid"])
|
||||
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
|
||||
# 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"])
|
||||
def test_tag_formats(tag, area, letters, loop, suffix):
|
||||
out = parse_tag(tag)
|
||||
assert (out["area"], out["letters"], out["loop"], out["suffix"]) == (
|
||||
area,
|
||||
letters,
|
||||
loop,
|
||||
suffix,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag, kind, number, suffix", FIXTURE["equipment"])
|
||||
def test_equipment_tags(tag, kind, number, suffix):
|
||||
out = parse_tag(tag)
|
||||
assert out["kind"] == "equipment" and out["valid"] is True
|
||||
assert out["equipment_kind"] == kind and out["number"] == number and out["suffix"] == suffix
|
||||
|
||||
|
||||
def test_equipment_prefixes_can_be_replaced_per_call():
|
||||
out = parse_tag("Q-1", kind="equipment", equipment_prefixes={"Q": "quench tower"})
|
||||
assert out["valid"] is True and out["equipment_kind"] == "quench tower"
|
||||
default = parse_tag("Q-1", kind="equipment")
|
||||
assert default["valid"] is False and "prefix" in default["errors"][0]
|
||||
assert DEFAULT_EQUIPMENT_PREFIXES["P"] == "pump"
|
||||
|
||||
|
||||
def test_auto_kind_prefers_instrument_then_equipment_then_reports_instrument_errors():
|
||||
assert parse_tag("PT-1")["kind"] == "instrument"
|
||||
assert parse_tag("P-1")["kind"] == "equipment"
|
||||
bad = parse_tag("FCI-1")
|
||||
assert bad["kind"] == "instrument" and bad["valid"] is False
|
||||
|
||||
|
||||
def test_split_instrument_tag_feeds_func_and_loop_attributes():
|
||||
assert split_instrument_tag("10-FIC-101A") == ("FIC", "101A")
|
||||
assert split_instrument_tag("PT 7") == ("PT", "7")
|
||||
assert split_instrument_tag("garbage") == ("GARBAGE", "")
|
||||
|
||||
|
||||
def test_describe_tables_is_json_serialisable_and_names_every_first_letter():
|
||||
tables = describe_tables()
|
||||
json.dumps(tables)
|
||||
assert set(tables["first_letters"]) == set("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
Reference in New Issue
Block a user