diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/unit_tests/known_issues/test_convert.py b/unit_tests/known_issues/test_convert.py new file mode 100644 index 0000000000..0952f499fd --- /dev/null +++ b/unit_tests/known_issues/test_convert.py @@ -0,0 +1,170 @@ +import os +import sys +import types +import unittest +import importlib.util +from unittest.mock import mock_open, patch +from xml.etree import ElementTree as ET + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src")) + + +def _load_convert(): + sys.modules["PyQt6"] = types.ModuleType("PyQt6") + qtw = types.ModuleType("PyQt6.QtWidgets") + qtw.QErrorMessage = type("QErrorMessage", (), {}) + qtw.QWidget = type("QWidget", (), {}) + sys.modules["PyQt6.QtWidgets"] = qtw + tw = types.ModuleType("kicadtoNgspice.TrackWidget") + tw.TrackWidget = type("TrackWidget", (), {}) + sys.modules["kicadtoNgspice.TrackWidget"] = tw + path = os.path.join( + os.path.dirname(__file__), "..", "..", "src", "kicadtoNgspice", "Convert.py" + ) + spec = importlib.util.spec_from_file_location("kicadtoNgspice.Convert", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules["kicadtoNgspice.Convert"] = module + return module + + +_convert = _load_convert() +Convert = _convert.Convert + + +class _MockEntry: + def __init__(self, text): + self._text = text + + def text(self): + return self._text + + +def _entries(pairs): + return dict((index, _MockEntry(text)) for index, text in pairs) + + +class KnownIssueConverttosciform(unittest.TestCase): + """ + Characterisation tests for converttosciform input sanitation. + + Each test pins the CURRENT (buggy) behaviour. When a dev fixes the + underlying issue the assertion will fail, signalling that the test + should move into unit_tests/test_convert.py with the corrected + expectation. + """ + + def setUp(self): + self.convert = Convert([], {}, [], "/f/c.cir") + + def test_mega_misread_as_milli(self): + self.assertEqual(self.convert.converttosciform("meg"), "e-03") + + def test_kilo_unsupported(self): + self.assertEqual(self.convert.converttosciform("k"), "e-00") + + def test_capital_mega_unsupported(self): + self.assertEqual(self.convert.converttosciform("M"), "e-00") + + def test_none_falls_back_to_default(self): + self.assertEqual(self.convert.converttosciform(None), "e-00") + + def test_non_string_raises_type_error(self): + with self.assertRaises(TypeError): + self.convert.converttosciform(123) + + +class KnownIssueDefaultvalue(unittest.TestCase): + def setUp(self): + self.convert = Convert([], {}, [], "/f/c.cir") + + def test_none_passes_through(self): + self.assertEqual(self.convert.defaultvalue(None), None) + + +class KnownIssueAddSourceParameter(unittest.TestCase): + def test_ac_substring_in_node_name_garbles_line(self): + convert = Convert( + [[0, "ac", 0, 1]], + _entries([(0, "1"), (1, "0")]), + ["vac1 1 0 ac"], + "/f/c.cir", + ) + self.assertEqual(convert.addSourceParameter(), ["v ac 1 0"]) + + def test_out_of_range_index_raises(self): + convert = Convert( + [[5, "dc", 0, 0]], + _entries([(0, "5")]), + ["v1 1 0 dc 0"], + "/f/c.cir", + ) + with self.assertRaises(IndexError): + convert.addSourceParameter() + + def test_missing_entry_key_is_silently_ignored(self): + convert = Convert( + [[0, "dc", 0, 0]], + {}, + ["v1 1 0 dc 0"], + "/f/c.cir", + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 dc 0"]) + + +class KnownIssueAnalysisInsertor(unittest.TestCase): + def _content(self, variable, acv=None, dcv=None, trv=None, acp=None, + dcp=None, trp=None, ac_type="", op=None): + convert = Convert([], {}, [], "/fake/circuit.cir") + m = mock_open() + with patch("builtins.open", m): + convert.analysisInsertor( + acv or {}, + dcv or {}, + trv or {}, + variable, + acp or {}, + dcp or {}, + trp or {}, + ac_type, + op or [0], + ) + return "".join(call.args[0] for call in m().write.call_args_list) + + def test_dc_empty_source_name_not_defaulted(self): + self.assertEqual( + self._content( + "DC", + dcv=_entries( + [(0, ""), (1, "0"), (2, "0.1"), (3, "5"), (4, ""), + (5, ""), (6, ""), (7, "")] + ), + dcp={0: "V", 1: "V", 2: "V"}, + ), + ".dc 0e-00 5e-00 0.1e-00", + ) + + def test_ac_empty_type_double_space(self): + self.assertEqual( + self._content( + "AC", + acv=_entries([(0, "1"), (1, "1e6"), (2, "100")]), + acp={0: "Hz", 1: "Meg"}, + ac_type="", + ), + ".ac 100 1Hz 1e6Meg", + ) + + +class KnownIssueGetReferenceName(unittest.TestCase): + def test_missing_ref_model_raises_unbound_local(self): + tree = ET.fromstring("X") + with patch("kicadtoNgspice.Convert.ET.parse", return_value=tree): + with self.assertRaises(UnboundLocalError): + Convert([], {}, [], "/f/c.cir").getReferenceName( + "2n2222.lib", "/libs" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/test_convert.py b/unit_tests/test_convert.py new file mode 100644 index 0000000000..54cefc46cc --- /dev/null +++ b/unit_tests/test_convert.py @@ -0,0 +1,283 @@ +import os +import sys +import types +import unittest +import importlib.util +from unittest.mock import mock_open, patch +from xml.etree import ElementTree as ET + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) + + +def _load_convert(): + sys.modules["PyQt6"] = types.ModuleType("PyQt6") + qtw = types.ModuleType("PyQt6.QtWidgets") + qtw.QErrorMessage = type("QErrorMessage", (), {}) + qtw.QWidget = type("QWidget", (), {}) + sys.modules["PyQt6.QtWidgets"] = qtw + tw = types.ModuleType("kicadtoNgspice.TrackWidget") + tw.TrackWidget = type("TrackWidget", (), {}) + sys.modules["kicadtoNgspice.TrackWidget"] = tw + path = os.path.join( + os.path.dirname(__file__), "..", "src", "kicadtoNgspice", "Convert.py" + ) + spec = importlib.util.spec_from_file_location("kicadtoNgspice.Convert", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules["kicadtoNgspice.Convert"] = module + return module + + +_convert = _load_convert() +Convert = _convert.Convert + + +class _MockEntry: + def __init__(self, text): + self._text = text + + def text(self): + return self._text + + +def _entries(pairs): + return dict((index, _MockEntry(text)) for index, text in pairs) + + +class TestConverttosciform(unittest.TestCase): + def setUp(self): + self.convert = Convert([], {}, [], "/f/c.cir") + + def test_milli(self): + self.assertEqual(self.convert.converttosciform("m"), "e-03") + + def test_micro(self): + self.assertEqual(self.convert.converttosciform("u"), "e-06") + + def test_nano(self): + self.assertEqual(self.convert.converttosciform("n"), "e-09") + + def test_pico(self): + self.assertEqual(self.convert.converttosciform("p"), "e-12") + + def test_empty(self): + self.assertEqual(self.convert.converttosciform(""), "e-00") + + def test_unrecognised_unit_uses_default(self): + self.assertEqual(self.convert.converttosciform("V"), "e-00") + + +class TestDefaultvalue(unittest.TestCase): + def setUp(self): + self.convert = Convert([], {}, [], "/f/c.cir") + + def test_empty_string_defaults_to_zero(self): + self.assertEqual(self.convert.defaultvalue(""), 0) + + def test_value_passthrough(self): + self.assertEqual(self.convert.defaultvalue("100"), "100") + + def test_zero_passthrough(self): + self.assertEqual(self.convert.defaultvalue(0), 0) + + +class TestAddSourceParameter(unittest.TestCase): + def _convert(self, track, entries, schematic): + return Convert(track, entries, schematic, "/f/c.cir") + + def test_sine(self): + convert = self._convert( + [[0, "sine", 0, 4]], + _entries([(0, "1"), (1, "5"), (2, "1k"), (3, "0"), (4, "0")]), + ["v1 1 0 sine(0 0 0 0 0)"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 sine(1 5 1k 0 0)"]) + + def test_sine_empty_entries_use_zero(self): + convert = self._convert( + [[0, "sine", 0, 4]], + _entries([(0, ""), (1, ""), (2, ""), (3, ""), (4, "")]), + ["v1 1 0 sine(0 0 0 0 0)"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 sine(0 0 0 0 0)"]) + + def test_pulse(self): + convert = self._convert( + [[0, "pulse", 0, 6]], + _entries( + [(0, "0"), (1, "5"), (2, "1m"), (3, "1u"), (4, "1u"), (5, "5m"), + (6, "10m")] + ), + ["v1 1 0 pulse(0 0 0 0 0 0 0)"], + ) + self.assertEqual( + convert.addSourceParameter(), ["v1 1 0 pulse(0 5 1m 1u 1u 5m 10m)"] + ) + + def test_pwl(self): + convert = self._convert( + [[0, "pwl", 0, 0]], + _entries([(0, "0 0 1m 5 2m 0")]), + ["v1 1 0 pwl(0 0)"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 pwl(0 0 1m 5 2m 0)"]) + + def test_pwl_empty_uses_default(self): + convert = self._convert( + [[0, "pwl", 0, 0]], + _entries([(0, "")]), + ["v1 1 0 pwl(0 0)"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 pwl(0 0)"]) + + def test_ac(self): + convert = self._convert( + [[0, "ac", 0, 1]], + _entries([(0, "1"), (1, "0")]), + ["v1 1 0 ac"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 ac 1 0"]) + + def test_dc(self): + convert = self._convert( + [[0, "dc", 0, 0]], + _entries([(0, "5")]), + ["v1 1 0 dc 0"], + ) + self.assertEqual(convert.addSourceParameter(), ["v1 1 0 dc 5"]) + + def test_exp(self): + convert = self._convert( + [[0, "exp", 0, 5]], + _entries([(0, "0"), (1, "5"), (2, "1m"), (3, "1u"), (4, "1m"), (5, "1u")]), + ["v1 1 0 exp(0 0 0 0 0 0)"], + ) + self.assertEqual( + convert.addSourceParameter(), ["v1 1 0 exp(0 5 1m 1u 1m 1u)"] + ) + + def test_multiple_sources_replaced_in_order(self): + convert = self._convert( + [[0, "sine", 0, 4], [1, "dc", 5, 5]], + _entries([(0, "1"), (1, "0"), (2, "1k"), (3, "0"), (4, "0"), (5, "5")]), + ["v1 1 0 sine(0 0 0 0 0)", "v2 2 0 dc 0"], + ) + self.assertEqual( + convert.addSourceParameter(), + ["v1 1 0 sine(1 0 1k 0 0)", "v2 2 0 dc 5"], + ) + + +class TestAnalysisInsertor(unittest.TestCase): + def _content(self, variable, acv=None, dcv=None, trv=None, acp=None, + dcp=None, trp=None, ac_type="", op=None): + convert = Convert([], {}, [], "/fake/circuit.cir") + m = mock_open() + with patch("builtins.open", m): + convert.analysisInsertor( + acv or {}, + dcv or {}, + trv or {}, + variable, + acp or {}, + dcp or {}, + trp or {}, + ac_type, + op or [0], + ) + return "".join(call.args[0] for call in m().write.call_args_list) + + def test_ac_dec(self): + self.assertEqual( + self._content( + "AC", + acv=_entries([(0, "1"), (1, "1e6"), (2, "100")]), + acp={0: "Hz", 1: "Meg"}, + ac_type="dec", + ), + ".ac dec 100 1Hz 1e6Meg", + ) + + def test_ac_lin(self): + self.assertEqual( + self._content( + "AC", + acv=_entries([(0, "1"), (1, "1e6"), (2, "1")]), + acp={0: "Hz", 1: "Meg"}, + ac_type="lin", + ), + ".ac lin 1 1Hz 1e6Meg", + ) + + def test_ac_empty_entries_default_to_zero(self): + self.assertEqual( + self._content( + "AC", + acv=_entries([(0, ""), (1, ""), (2, "")]), + acp={0: "Hz", 1: "Meg"}, + ac_type="dec", + ), + ".ac dec 0 0Hz 0Meg", + ) + + def test_tran(self): + self.assertEqual( + self._content( + "TRAN", + trv=_entries([(0, "0"), (1, "0.01"), (2, "1")]), + trp={0: "sec", 1: "sec", 2: "sec"}, + ), + ".tran 0.01e-00 1e-00 0e-00", + ) + + def test_dc(self): + self.assertEqual( + self._content( + "DC", + dcv=_entries( + [(0, "V1"), (1, "0"), (2, "0.1"), (3, "5"), (4, ""), + (5, ""), (6, ""), (7, "")] + ), + dcp={0: "V", 1: "V", 2: "V"}, + ), + ".dc V1 0e-00 5e-00 0.1e-00", + ) + + def test_dc_two_sources(self): + self.assertEqual( + self._content( + "DC", + dcv=_entries( + [(0, "V1"), (1, "0"), (2, "0.1"), (3, "5"), (4, "V2"), + (5, "0"), (6, "1"), (7, "10")] + ), + dcp={0: "V", 1: "V", 2: "V", 3: "V", 4: "V", 5: "V"}, + ), + ".dc V1 0e-00 5e-00 0.1e-00 V2 0e-00 10e-00 1e-00", + ) + + def test_op(self): + self.assertEqual( + self._content("DC", dcv=_entries([(0, "V1"), (1, ""), (2, ""), + (3, ""), (4, ""), (5, ""), + (6, ""), (7, "")]), op=[1]), + ".op", + ) + + def test_unknown_variable_writes_nothing(self): + self.assertEqual(self._content("NONE"), "") + + +class TestGetReferenceName(unittest.TestCase): + def test_returns_ref_model_text(self): + tree = ET.fromstring("2N2222") + with patch("kicadtoNgspice.Convert.ET.parse", return_value=tree) as parse: + result = Convert([], {}, [], "/f/c.cir").getReferenceName( + "2n2222.lib", "/libs" + ) + self.assertEqual(result, "2N2222") + parse.assert_called_once_with(os.path.join("/libs", "2n2222.xml")) + + +if __name__ == "__main__": + unittest.main()