mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
qet-mcp: tests for what a second mutation audit found untested
tools/qet-mcp-audit/mutate.py over the code changed since #1095 (_build_script, the diff helpers, _parse_script_output, tool_items and the wire-end keys of #1118), tests needing no QElectroTech only: master f530b7636's suite noticed 531 of 578 planted bugs (92 %). With these tests, 568 (98 %). The 10 left are equivalent: rsplit("/", 1) vs 2, a 1e-6 tolerance compared with < or <=, a one-letter orientation sliced [:1] or [:2], branches that only touch parts without terminals, and the placeholder terminal 0 a conductor uuid overwrites. - Every argument kind refuses what it cannot take, before any launch: malformed indices, bool, points and nodes, search_and_replace with an unknown kind or conductor field or an empty element_info field, "conductor" given with "element" or "terminal" alone, an op that is not an object -- and the same kinds accept what they should. - _parse_script_output reports capabilities (none logged = unknown, not "nothing missing"), notes on the right op, save and stopped_early; ignores a line without the marker even when it would parse, and a line of a kind it does not know. - A uuid wire end in a column of terminals (same x) keys like the numbered one: the match is on x, y and orientation together. Tests only; 264/264. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -458,6 +458,80 @@ class EditValidation(unittest.TestCase):
|
||||
need = json.loads(re.search(r"var need = (\[.*?\]);", script).group(1))
|
||||
self.assertFalse({"shapeIndex", "textIndex", "imageIndex"} & set(need))
|
||||
|
||||
def test_malformed_arguments_are_refused(self):
|
||||
"""Each argument kind rejects what it cannot take, before any launch.
|
||||
Found by the mutation audit: forcing any of these checks off went
|
||||
unnoticed."""
|
||||
f = {"op": "add_folio", "id": "f"}
|
||||
cases = [
|
||||
("indices", {"op": "group_terminals", "strip": 0, "indices": []}),
|
||||
("indices", {"op": "group_terminals", "strip": 0, "indices": [1, "2"]}),
|
||||
("indices", {"op": "group_terminals", "strip": 0, "indices": [True]}),
|
||||
("indices", {"op": "bridge_terminals", "strip": 0, "indices": 3}),
|
||||
("true or false", {"op": "add_polygon", "folio": 0, "closed": 1,
|
||||
"points": [{"x": 0, "y": 0}, {"x": 1, "y": 1}]}),
|
||||
("at least 2", {"op": "add_polygon", "folio": 0, "closed": True,
|
||||
"points": [{"x": 0, "y": 0}]}),
|
||||
("at least 2", {"op": "add_polygon", "folio": 0, "closed": True, "points": "xy"}),
|
||||
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
|
||||
"points": [{"x": 0, "y": 0}, {"x": "1", "y": 1}]}),
|
||||
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
|
||||
"points": [{"x": 0, "y": 0}, {"x": True, "y": 1}]}),
|
||||
("entries must be", {"op": "add_polygon", "folio": 0, "closed": True,
|
||||
"points": [{"x": 0, "y": 0}, [1, 1]]}),
|
||||
("at least 2 nodes", {"op": "add_path", "folio": 0, "closed": False,
|
||||
"nodes": [{"x": 0, "y": 0}]}),
|
||||
("corner, smooth or symmetric", {"op": "add_path", "folio": 0, "closed": False,
|
||||
"nodes": [{"x": 0, "y": 0},
|
||||
{"x": 1, "y": 1, "kind": "sharp"}]}),
|
||||
("inHandle", {"op": "add_path", "folio": 0, "closed": False,
|
||||
"nodes": [{"x": 0, "y": 0}, {"x": 1, "y": 1, "inHandle": [0, 0]}]}),
|
||||
("outHandle", {"op": "add_path", "folio": 0, "closed": False,
|
||||
"nodes": [{"x": 0, "y": 0}, {"x": 1, "y": 1, "outHandle": {"x": 0}}]}),
|
||||
("unknown kind", {"op": "search_and_replace", "kind": "folio", "field": "x",
|
||||
"pattern": "a", "replacement": "b", "regex": False,
|
||||
"case_sensitive": False}),
|
||||
("unknown conductor field", {"op": "search_and_replace", "kind": "conductor",
|
||||
"field": "label", "pattern": "a", "replacement": "b",
|
||||
"regex": False, "case_sensitive": False}),
|
||||
("non-empty", {"op": "search_and_replace", "kind": "element_info", "field": "",
|
||||
"pattern": "a", "replacement": "b", "regex": False,
|
||||
"case_sensitive": False}),
|
||||
("not both", {"op": "delete_conductor", "folio": 0,
|
||||
"conductor": "{11111111-2222-4333-8444-555555555555}",
|
||||
"element": "{11111111-2222-4333-8444-555555555555}"}),
|
||||
("not both", {"op": "delete_conductor", "folio": 0,
|
||||
"conductor": "{11111111-2222-4333-8444-555555555555}", "terminal": 0}),
|
||||
("not an object", "add_folio"),
|
||||
]
|
||||
for message, op in cases:
|
||||
with self.subTest(op=op):
|
||||
with self.assertRaisesRegex(ValueError, message):
|
||||
self.build([f, op])
|
||||
|
||||
def test_valid_shapes_of_those_arguments_pass(self):
|
||||
"""The other side: the same kinds accept what they should, so the
|
||||
refusals above are not everything failing."""
|
||||
s = self.build([
|
||||
{"op": "add_polygon", "folio": 0, "closed": False,
|
||||
"points": [{"x": 0, "y": 0}, {"x": 1.5, "y": -2}]},
|
||||
{"op": "add_path", "folio": 0, "closed": True,
|
||||
"nodes": [{"x": 0, "y": 0, "kind": "smooth", "inHandle": {"x": 1, "y": 1}},
|
||||
{"x": 5, "y": 5, "outHandle": {"x": 2, "y": 2}}]},
|
||||
{"op": "add_polygon", "folio": 0, "closed": True,
|
||||
"points": [{"x": 0, "y": 0, "kind": "anything"}, {"x": 1, "y": 1}]},
|
||||
{"op": "search_and_replace", "kind": "conductor", "field": "num", "pattern": "a",
|
||||
"replacement": "b", "regex": True, "case_sensitive": False},
|
||||
{"op": "search_and_replace", "kind": "text", "field": "", "pattern": "a",
|
||||
"replacement": "b", "regex": False, "case_sensitive": True}])
|
||||
self.assertIn("qet.addPolygon(0, [{", s)
|
||||
self.assertIn("qet.searchAndReplace(\"conductor\", \"num\", \"a\", \"b\", true, false)", s)
|
||||
self.assertIn("qet.searchAndReplace(\"text\", \"\", \"a\", \"b\", false, true)", s)
|
||||
|
||||
def test_no_id_stores_nothing(self):
|
||||
s = self.build([{"op": "add_folio"}])
|
||||
self.assertNotIn("R[", s.split("if (missing.length === 0) {")[1])
|
||||
|
||||
def test_a_string_index_that_is_not_a_uuid_is_refused(self):
|
||||
for bad in ("3", "{nope}", "11111111-1111"):
|
||||
with self.subTest(index=bad):
|
||||
@@ -566,6 +640,37 @@ class ResultParsing(unittest.TestCase):
|
||||
self.assertEqual(parsed["operations"], [])
|
||||
|
||||
|
||||
def test_capabilities_notes_and_save(self):
|
||||
"""Every field the script reports reaches the result, attached where
|
||||
it belongs. Found by the mutation audit: only the binary tests read
|
||||
these back."""
|
||||
text = "\n".join([
|
||||
self.line(kind="capabilities", missing=["addFolio"]),
|
||||
self.line(kind="op", index=0, op="a", id=None, result=True),
|
||||
self.line(kind="op", index=1, op="b", id=None, result=False),
|
||||
self.line(kind="op_note", index=1, note="why"),
|
||||
self.line(kind="save", result=True, stopped_early=True),
|
||||
self.line(kind="something_newer", result=False)]) # not a save
|
||||
parsed = m._parse_script_output(text)
|
||||
self.assertEqual(set(parsed), {"missing_methods", "operations", "saved", "stopped_early"})
|
||||
self.assertEqual(parsed["missing_methods"], ["addFolio"])
|
||||
self.assertTrue(parsed["saved"])
|
||||
self.assertTrue(parsed["stopped_early"])
|
||||
self.assertNotIn("note", parsed["operations"][0])
|
||||
self.assertEqual(parsed["operations"][1]["note"], "why")
|
||||
# no capabilities line: unknown, not "nothing missing"
|
||||
self.assertIsNone(m._parse_script_output(self.line(kind="save", result=False))["missing_methods"])
|
||||
self.assertEqual(m._parse_script_output(self.line(kind="capabilities", missing=None))
|
||||
["missing_methods"], [])
|
||||
|
||||
def test_a_line_without_the_marker_is_ignored(self):
|
||||
"""Even one that would parse if read from where the marker would be."""
|
||||
almost = "x" * (len(m._MARKER) - 1) + json.dumps({"kind": "save", "result": True})
|
||||
parsed = m._parse_script_output(almost)
|
||||
self.assertIsNone(parsed["saved"])
|
||||
self.assertFalse(parsed["stopped_early"])
|
||||
|
||||
|
||||
class TerminalOrder(unittest.TestCase):
|
||||
"""QElectroTech indexes terminals top-to-bottom then left-to-right, not in
|
||||
file order. Getting it wrong wires the wrong end of a coil with no error;
|
||||
@@ -962,6 +1067,32 @@ class ReadToolContracts(unittest.TestCase):
|
||||
self.assertEqual(numbered, "1:{E}@0,-4,2--{E}@6,0,1")
|
||||
self.assertEqual(by_uuid, numbered)
|
||||
|
||||
def test_conductor_key_same_in_both_forms_in_a_column(self):
|
||||
"""Terminals one above the other share their x: a uuid end must be
|
||||
matched on x, y and orientation together, not on any one of them.
|
||||
Found by the mutation audit: an "or" in place of "and" went
|
||||
unnoticed while every test's terminals differed in every coordinate."""
|
||||
root = ET.fromstring(
|
||||
'<project><collection><category name="import"><category name="x">'
|
||||
'<element name="C.elmt"><definition><description>'
|
||||
'<terminal uuid="{T1}" x="0" y="0" orientation="n"/>'
|
||||
'<terminal uuid="{T2}" x="0" y="20" orientation="n"/>'
|
||||
'<terminal uuid="{T3}" x="30" y="20" orientation="n"/>'
|
||||
'</description></definition></element>'
|
||||
'</category></category></collection>'
|
||||
'<diagram><elements><element uuid="{E}" type="embed://import/x/C.elmt">'
|
||||
'<terminals><terminal id="1" x="0" y="4" orientation="0"/>'
|
||||
'<terminal id="2" x="0" y="24" orientation="0"/>'
|
||||
'<terminal id="3" x="30" y="24" orientation="0"/></terminals>'
|
||||
'</element></elements><conductors>'
|
||||
'<conductor terminal1="2" terminal2="3"/>'
|
||||
'<conductor element1="{E}" terminal1="{T2}" element2="{E}" terminal2="{T3}"/>'
|
||||
'</conductors></diagram></project>')
|
||||
numbered, by_uuid = [m._conductor_row(i, c, ix)["key"]
|
||||
for i, c, ix in m._conductors(root)]
|
||||
self.assertEqual(numbered, "1:{E}@0,24,0--{E}@30,24,0")
|
||||
self.assertEqual(by_uuid, numbered)
|
||||
|
||||
def test_conductor_row_without_an_index(self):
|
||||
c = ET.fromstring('<conductor terminal1="7" terminal2="8"/>')
|
||||
self.assertEqual(m._conductor_row(3, c)["key"], "3:#7--#8")
|
||||
|
||||
Reference in New Issue
Block a user