diff --git a/src/py_dvt_ate/instruments/scpi.py b/src/py_dvt_ate/instruments/scpi.py index 89bcd82..a1f7347 100644 --- a/src/py_dvt_ate/instruments/scpi.py +++ b/src/py_dvt_ate/instruments/scpi.py @@ -67,8 +67,6 @@ class SCPIParser: if not command_string: return SCPICommand(header="", arguments=[], is_query=False) - is_query = command_string.endswith("?") - # Split into header and arguments on first whitespace parts = command_string.split(None, 1) header = parts[0] @@ -79,6 +77,9 @@ class SCPIParser: arg_string = parts[1] arguments = [arg.strip() for arg in arg_string.split(",")] + # Query is determined by whether the header ends with '?' + is_query = header.endswith("?") + return SCPICommand( header=header, arguments=arguments, diff --git a/tests/unit/test_scpi_parser.py b/tests/unit/test_scpi_parser.py new file mode 100644 index 0000000..9ad1e61 --- /dev/null +++ b/tests/unit/test_scpi_parser.py @@ -0,0 +1,203 @@ +"""Unit tests for SCPI command parsing.""" + +import pytest + +from py_dvt_ate.instruments.scpi import SCPICommand, SCPIParser + + +class TestSCPICommand: + """Tests for the SCPICommand dataclass.""" + + def test_creation(self) -> None: + """Test SCPICommand can be created with valid values.""" + cmd = SCPICommand( + header="VOLT", + arguments=["3.3"], + is_query=False, + ) + + assert cmd.header == "VOLT" + assert cmd.arguments == ["3.3"] + assert cmd.is_query is False + + def test_keyword_for_command(self) -> None: + """Test keyword property for regular command.""" + cmd = SCPICommand(header="VOLT", arguments=["3.3"], is_query=False) + + assert cmd.keyword == "VOLT" + + def test_keyword_for_query(self) -> None: + """Test keyword property strips '?' from query.""" + cmd = SCPICommand(header="VOLT?", arguments=[], is_query=True) + + assert cmd.keyword == "VOLT" + + def test_keyword_for_nested_command(self) -> None: + """Test keyword property for nested SCPI command.""" + cmd = SCPICommand(header="TEMP:SETPOINT?", arguments=[], is_query=True) + + assert cmd.keyword == "TEMP:SETPOINT" + + +class TestSCPIParser: + """Tests for the SCPIParser class.""" + + @pytest.fixture + def parser(self) -> SCPIParser: + """Create parser instance for tests.""" + return SCPIParser() + + def test_parse_simple_query(self, parser: SCPIParser) -> None: + """Test parsing simple query command.""" + cmd = parser.parse("*IDN?") + + assert cmd.header == "*IDN?" + assert cmd.arguments == [] + assert cmd.is_query is True + assert cmd.keyword == "*IDN" + + def test_parse_simple_command(self, parser: SCPIParser) -> None: + """Test parsing simple command without arguments.""" + cmd = parser.parse("*RST") + + assert cmd.header == "*RST" + assert cmd.arguments == [] + assert cmd.is_query is False + assert cmd.keyword == "*RST" + + def test_parse_command_with_single_argument(self, parser: SCPIParser) -> None: + """Test parsing command with single numeric argument.""" + cmd = parser.parse("VOLT 3.3") + + assert cmd.header == "VOLT" + assert cmd.arguments == ["3.3"] + assert cmd.is_query is False + + def test_parse_command_with_multiple_arguments(self, parser: SCPIParser) -> None: + """Test parsing command with comma-separated arguments.""" + cmd = parser.parse("CONF:VOLT:DC 10,0.001") + + assert cmd.header == "CONF:VOLT:DC" + assert cmd.arguments == ["10", "0.001"] + assert cmd.is_query is False + + def test_parse_nested_scpi_command(self, parser: SCPIParser) -> None: + """Test parsing nested SCPI command hierarchy.""" + cmd = parser.parse("TEMP:SETPOINT 85.0") + + assert cmd.header == "TEMP:SETPOINT" + assert cmd.arguments == ["85.0"] + assert cmd.is_query is False + assert cmd.keyword == "TEMP:SETPOINT" + + def test_parse_nested_scpi_query(self, parser: SCPIParser) -> None: + """Test parsing nested SCPI query.""" + cmd = parser.parse("TEMP:SETPOINT?") + + assert cmd.header == "TEMP:SETPOINT?" + assert cmd.arguments == [] + assert cmd.is_query is True + + def test_parse_ieee_common_commands(self, parser: SCPIParser) -> None: + """Test parsing IEEE 488.2 common commands.""" + # Identity query + cmd = parser.parse("*IDN?") + assert cmd.is_query is True + assert cmd.keyword == "*IDN" + + # Reset + cmd = parser.parse("*RST") + assert cmd.is_query is False + assert cmd.keyword == "*RST" + + # Clear status + cmd = parser.parse("*CLS") + assert cmd.is_query is False + assert cmd.keyword == "*CLS" + + # Operation complete query + cmd = parser.parse("*OPC?") + assert cmd.is_query is True + assert cmd.keyword == "*OPC" + + def test_parse_strips_whitespace(self, parser: SCPIParser) -> None: + """Test parser strips leading and trailing whitespace.""" + cmd = parser.parse(" VOLT 3.3 ") + + assert cmd.header == "VOLT" + assert cmd.arguments == ["3.3"] + + def test_parse_strips_argument_whitespace(self, parser: SCPIParser) -> None: + """Test parser strips whitespace from arguments.""" + cmd = parser.parse("CONF:VOLT:DC 10 , 0.001 ") + + assert cmd.arguments == ["10", "0.001"] + + def test_parse_empty_string(self, parser: SCPIParser) -> None: + """Test parsing empty string returns empty command.""" + cmd = parser.parse("") + + assert cmd.header == "" + assert cmd.arguments == [] + assert cmd.is_query is False + + def test_parse_whitespace_only(self, parser: SCPIParser) -> None: + """Test parsing whitespace-only string returns empty command.""" + cmd = parser.parse(" ") + + assert cmd.header == "" + assert cmd.arguments == [] + assert cmd.is_query is False + + def test_parse_output_on_off(self, parser: SCPIParser) -> None: + """Test parsing output enable/disable commands.""" + cmd_on = parser.parse("OUTP ON") + assert cmd_on.arguments == ["ON"] + + cmd_off = parser.parse("OUTP OFF") + assert cmd_off.arguments == ["OFF"] + + cmd_1 = parser.parse("OUTP 1") + assert cmd_1.arguments == ["1"] + + cmd_0 = parser.parse("OUTP 0") + assert cmd_0.arguments == ["0"] + + def test_parse_channel_select(self, parser: SCPIParser) -> None: + """Test parsing channel selection commands.""" + cmd = parser.parse("INST:SEL CH1") + + assert cmd.header == "INST:SEL" + assert cmd.arguments == ["CH1"] + + def test_parse_measurement_query(self, parser: SCPIParser) -> None: + """Test parsing measurement query commands.""" + cmd = parser.parse("MEAS:VOLT:DC?") + + assert cmd.header == "MEAS:VOLT:DC?" + assert cmd.is_query is True + assert cmd.keyword == "MEAS:VOLT:DC" + + def test_parse_measurement_with_range(self, parser: SCPIParser) -> None: + """Test parsing measurement query with range argument.""" + cmd = parser.parse("MEAS:VOLT:DC? AUTO") + + assert cmd.header == "MEAS:VOLT:DC?" + assert cmd.arguments == ["AUTO"] + assert cmd.is_query is True + + def test_parse_system_error_query(self, parser: SCPIParser) -> None: + """Test parsing system error query.""" + cmd = parser.parse("SYST:ERR?") + + assert cmd.header == "SYST:ERR?" + assert cmd.is_query is True + assert cmd.keyword == "SYST:ERR" + + def test_parse_nplc_setting(self, parser: SCPIParser) -> None: + """Test parsing NPLC (integration time) command.""" + cmd = parser.parse("SENS:VOLT:DC:NPLC 10") + + assert cmd.header == "SENS:VOLT:DC:NPLC" + assert cmd.arguments == ["10"] + assert cmd.is_query is False