scientific-method-engine 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/PKG-INFO +6 -1
  2. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/README.md +5 -0
  3. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/pyproject.toml +1 -1
  4. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/cli.py +1 -0
  5. scientific_method_engine-0.4.0/src/scientific_method_engine/x86/effect_order.py +91 -0
  6. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/reports.py +3 -0
  7. scientific_method_engine-0.4.0/tests/test_effect_order.py +114 -0
  8. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/.gitignore +0 -0
  9. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/LICENSE +0 -0
  10. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/__init__.py +0 -0
  11. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/__main__.py +0 -0
  12. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ClearNoReturnFunctions.java +0 -0
  13. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/CreateFunctions.java +0 -0
  14. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ExportBoundedFlow.java +0 -0
  15. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ExportFunctionFingerprints.java +0 -0
  16. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ExportFunctionInventory.java +0 -0
  17. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/MergeFallThroughFragment.java +0 -0
  18. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/RecoverCitedFunctions.java +0 -0
  19. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/RepairReturningCallers.java +0 -0
  20. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportCallArguments.java +0 -0
  21. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportCallPaths.java +0 -0
  22. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +0 -0
  23. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportCallsToRange.java +0 -0
  24. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +0 -0
  25. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportDataBytes.java +0 -0
  26. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportDecompileMatches.java +0 -0
  27. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportDecompileWindow.java +0 -0
  28. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportFilePatternInMemory.java +0 -0
  29. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +0 -0
  30. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +0 -0
  31. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportFunctionSummary.java +0 -0
  32. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportInstructionContext.java +0 -0
  33. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportInstructionWindow.java +0 -0
  34. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +0 -0
  35. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportMemoryBlocks.java +0 -0
  36. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportRandomnessCandidates.java +0 -0
  37. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportReferences.java +0 -0
  38. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportScalarConstants.java +0 -0
  39. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportStringReferences.java +0 -0
  40. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/ghidra/ReportSymbolReferences.java +0 -0
  41. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/__init__.py +0 -0
  42. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/dispatch.py +0 -0
  43. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/image.py +0 -0
  44. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/machine.py +0 -0
  45. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/pe.py +0 -0
  46. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/result_flow.py +0 -0
  47. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/trace.py +0 -0
  48. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/src/scientific_method_engine/x86/values.py +0 -0
  49. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/tests/test_dispatch.py +0 -0
  50. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/tests/test_pe.py +0 -0
  51. {scientific_method_engine-0.3.0 → scientific_method_engine-0.4.0}/tests/test_x86.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scientific-method-engine
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code.
5
5
  Project-URL: Source, https://github.com/kibertoad/refurbished-dinosaurs-toolkit/tree/main/packages/scientific-method-engine
6
6
  Author: kibertoad
@@ -108,3 +108,8 @@ Commands, inputs, limits and acceptance rules are in
108
108
  [the bounded evidence reporter guide](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md).
109
109
  The reader and engine check that they speak the same prepared-config protocol and refuse to run
110
110
  otherwise.
111
+
112
+ The `effects` command additionally emits path-local `effectOrdering` timelines,
113
+ pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
114
+ or nested service effects stay separate; no return code establishes rollback or
115
+ transactionality. See [the full contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#ordered-effect-path-summaries).
@@ -96,3 +96,8 @@ Commands, inputs, limits and acceptance rules are in
96
96
  [the bounded evidence reporter guide](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md).
97
97
  The reader and engine check that they speak the same prepared-config protocol and refuse to run
98
98
  otherwise.
99
+
100
+ The `effects` command additionally emits path-local `effectOrdering` timelines,
101
+ pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
102
+ or nested service effects stay separate; no return code establishes rollback or
103
+ transactionality. See [the full contract](https://github.com/kibertoad/refurbished-dinosaurs-toolkit/blob/main/docs/bounded-evidence-reporters.md#ordered-effect-path-summaries).
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "scientific-method-engine"
7
7
  # The release workflow writes the published version from the package's release tag.
8
- version = "0.3.0"
8
+ version = "0.4.0"
9
9
  description = "Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -9,6 +9,7 @@ CONFIG_LIMIT = 1024 * 1024
9
9
  PREPARED_CONFIG_LIMIT = 16 * 1024 * 1024
10
10
  USAGE = ("Usage: scientific-method-engine <operand|operand-candidates|target|bounds|owner|callees|trace|uses|arguments|"
11
11
  "effects|returns|memory|incoming|call-order|guards|allocation|dispatch> <config.json|->\n"
12
+ "effects includes ordered path writes/calls and local restoration witnesses; transactionality remains unestablished.\n"
12
13
  " scientific-method-engine ghidra-scripts")
13
14
 
14
15
 
@@ -0,0 +1,91 @@
1
+ """Ordered, path-local effect evidence; no transactional or native-execution claims."""
2
+ from bisect import bisect_right
3
+
4
+
5
+ KINDS = {"read", "write", "call", "call-return", "return", "branch", "compare", "flag-assumption",
6
+ "arithmetic", "value-transfer", "conversion", "flag-write", "flags-save", "flags-restore", "local-iret", "string-operation"}
7
+
8
+
9
+ def _storage(event):
10
+ # Equal offsets alone cannot identify storage. Retain the segment/base expression
11
+ # and the complete access width; a narrower restore is a separate access.
12
+ if not all(k in event for k in ("segment", "offset", "width")):
13
+ return None
14
+ # Terms are hashable tuples; compare them directly instead of serializing each access.
15
+ return (event["segment"].get("expression"), event["offset"].get("expression"), event["width"],
16
+ event.get("segmentInterpretation"))
17
+
18
+
19
+ def effect_ordering(report):
20
+ """Add bounded path timelines and explicit local restoration witnesses.
21
+
22
+ The existing tracer bounds every event. Prefix counts index a path's write
23
+ list instead of copying that list for every call (linear report size). A
24
+ witness restores one read value only; it says nothing about other storage,
25
+ aliases, external effects, resources or transactionality.
26
+ """
27
+ summaries = []
28
+ for index, path in enumerate(report["paths"]):
29
+ timeline, writes, calls, witnesses = [], [], [], []
30
+ snapshots = {}
31
+ pending = {}
32
+ unknown_orders = []
33
+ last_write = {}
34
+ for event in path["events"]:
35
+ kind = event["kind"]
36
+ if kind in KINDS:
37
+ timeline.append(event)
38
+ if kind == "read":
39
+ key = _storage(event)
40
+ if key is not None:
41
+ snapshots.setdefault(key, {})[event["value"].get("expression")] = event
42
+ elif kind == "write":
43
+ key = _storage(event)
44
+ original = snapshots.get(key, {}).get(event["value"].get("expression"))
45
+ # An equal expression alone is not a restore: an independent constant store or a
46
+ # re-pushed return address can match it. The written value must derive from the read.
47
+ if (original is not None and last_write.get(key, -1) > original["order"]
48
+ and original["site"] in event["value"].get("producers", ())):
49
+ witnesses.append({"readOrder": original["order"], "restoreOrder": event["order"],
50
+ "entry": event["entry"], "width": event["width"],
51
+ "pathReturned": path["returned"],
52
+ "unknownEffectsBetweenCount": len(unknown_orders) - bisect_right(unknown_orders, original["order"]),
53
+ "meaning": "same complete storage/value expression restored locally; not transactionality"})
54
+ writes.append(event)
55
+ last_write[key] = event["order"]
56
+ elif kind == "call":
57
+ call = {"order": event["order"], "site": event["site"], "entry": event["entry"],
58
+ "depth": event["depth"], "target": event.get("target"),
59
+ "writesBeforeCount": len(writes), "status": "unresolved-or-stopped",
60
+ "unknownEffects": True, "continuation": None, "_unknownStart": len(unknown_orders)}
61
+ calls.append(call)
62
+ pending.setdefault((event["site"], event["depth"]), []).append(call)
63
+ elif kind == "call-return":
64
+ stack = pending.get((event["callSite"], event["depth"]), [])
65
+ if stack:
66
+ call = stack.pop()
67
+ modeled = event.get("modeled", False)
68
+ call.update(status="modeled-return" if modeled else "traced-return",
69
+ unknownEffects=bool(modeled or event.get("unknownMemoryEffects") or len(unknown_orders) > call.pop("_unknownStart")),
70
+ returnOrder=event["order"], writesAfterCount=len(writes),
71
+ continuation="assumes balanced returning service; its memory/flag effects are unknown" if modeled
72
+ else "local callee return reached within the instruction model")
73
+ if event.get("modeled") or event.get("unknownMemoryEffects"):
74
+ unknown_orders.append(event["order"])
75
+ for call in calls:
76
+ call.pop("_unknownStart", None)
77
+ boundary = None
78
+ if path.get("stop"):
79
+ boundary = {"site": path.get("stopSite"), "reason": path["stop"],
80
+ "writesBeforeCount": len(writes), "meaning": "later effects are not read"}
81
+ summaries.append({"path": index, "returned": path["returned"], "stop": boundary,
82
+ "guards": path["guards"], "timeline": timeline,
83
+ "writeOrders": [w["order"] for w in writes], "calls": calls,
84
+ "localRestorationWitnesses": witnesses,
85
+ "effectCompleteWithinModel": bool(path["returned"] and not unknown_orders),
86
+ "transactionality": "not established; local writes and result codes cannot prove external rollback"})
87
+ report["effectOrdering"] = {"paths": summaries,
88
+ "allPathsRead": report["completeWithinModel"],
89
+ "nativeReachability": "unconfirmed",
90
+ "meaning": "separate conditional paths; write prefixes index each path's writeOrders"}
91
+ return report
@@ -5,6 +5,7 @@ from capstone import CS_AC_READ, CS_AC_WRITE
5
5
  from capstone.x86 import X86_OP_IMM, X86_OP_MEM, X86_OP_REG
6
6
  from .machine import State, StopPath, REGISTERS, ALIASES, segment_register
7
7
  from .values import unknown
8
+ from .effect_order import effect_ordering
8
9
  from .result_flow import return_flows
9
10
  from .image import Image, integer
10
11
  from .trace import (trace, walk, call_target, unsupported_transfer, uncovered, base_mnemonic, OVERLAP_REASON, CONTESTED_REASON,
@@ -1379,6 +1380,8 @@ def _run_report(image, config, command):
1379
1380
  report = near_pointer_provenance(report, config)
1380
1381
  if command == "allocation":
1381
1382
  return allocations(report, config)
1383
+ if command == "effects":
1384
+ report = effect_ordering(report)
1382
1385
  if command == "returns":
1383
1386
  report = return_flows(report, config)
1384
1387
  if command != "trace":
@@ -0,0 +1,114 @@
1
+ """Synthetic path evidence; no original bytes or claims."""
2
+ import unittest
3
+ from test_x86 import Code, report
4
+
5
+
6
+ class EffectOrderTests(unittest.TestCase):
7
+ def paths(self, result):
8
+ return result["effectOrdering"]["paths"]
9
+
10
+ def test_early_exit_has_distinct_write_contract_at_common_return(self):
11
+ c = Code().emit("85 c0").branch("74", "return").emit("c7 06 20 00 01 00").label("return").emit("c3")
12
+ r = report(c, "effects")
13
+ self.assertTrue(r["effectOrdering"]["allPathsRead"])
14
+ self.assertEqual(sorted(len(p["writeOrders"]) for p in self.paths(r)), [0, 1])
15
+ self.assertTrue(all(p["guards"] for p in self.paths(r)))
16
+ self.assertTrue(all(p["transactionality"].startswith("not established") for p in self.paths(r)))
17
+
18
+ def test_writes_before_modeled_failure_and_later_predicate_remain_separate(self):
19
+ c = Code().emit("c7 06 20 00 01 00").label("service").branch("e8", "serviceBody")
20
+ c.emit("83 3e 22 00 00").branch("74", "return").emit("c7 06 24 00 02 00").label("return").emit("c3")
21
+ c.label("serviceBody").emit("c3")
22
+ r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "synthetic conditional failure",
23
+ "cases": [{"registers": {"ax": 1}}]}])
24
+ for p in self.paths(r):
25
+ call = p["calls"][0]
26
+ self.assertEqual(call["writesBeforeCount"], 1)
27
+ self.assertEqual(call["status"], "modeled-return")
28
+ self.assertTrue(call["unknownEffects"])
29
+ self.assertFalse(p["effectCompleteWithinModel"])
30
+ branch = next(e for e in p["timeline"] if e["kind"] == "branch")
31
+ compare = next(e for e in p["timeline"] if e["kind"] == "compare")
32
+ self.assertGreater(compare["order"], call["returnOrder"])
33
+ self.assertGreater(branch["order"], compare["order"])
34
+ self.assertEqual(branch["flagProducer"], compare["site"])
35
+
36
+ def test_child_writes_are_not_lost_in_caller_bracket(self):
37
+ c = Code().emit("c7 06 20 00 01 00").branch("e8", "child").emit("c7 06 20 00 00 00 c3")
38
+ c.label("child").emit("c7 06 20 00 03 00 c3")
39
+ p = self.paths(report(c, "effects", registers={"ds": 0x2000, "ss": 0x3000, "sp": 0xff00}))[0]
40
+ writes = [e for e in p["timeline"] if e["kind"] == "write" and e.get("role") != "stack"]
41
+ self.assertTrue(any(e["entry"] == c.labels["child"] and e["value"]["value"] == 3 for e in writes))
42
+ self.assertEqual(p["calls"][0]["status"], "traced-return")
43
+ self.assertFalse(p["calls"][0]["unknownEffects"])
44
+
45
+ def test_unknown_nested_service_propagates_to_outer_call(self):
46
+ c = Code().branch("e8", "child").emit("c3").label("child").label("service").branch("e8", "external").emit("c3")
47
+ c.label("external").emit("c3")
48
+ r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "unknown child service", "cases": [{}]}])
49
+ p = self.paths(r)[0]
50
+ self.assertTrue(all(call["unknownEffects"] for call in p["calls"]))
51
+ self.assertFalse(p["effectCompleteWithinModel"])
52
+
53
+ def test_snapshot_restore_witness_does_not_cover_bypass_or_other_storage(self):
54
+ c = Code().emit("a1 20 00 c7 06 20 00 02 00 85 db").branch("74", "return")
55
+ c.emit("a3 20 00").label("return").emit("c3")
56
+ r = report(c, "effects")
57
+ self.assertEqual(sorted(len(p["localRestorationWitnesses"]) for p in self.paths(r)), [0, 1])
58
+ w = next(w for p in self.paths(r) for w in p["localRestorationWitnesses"])
59
+ self.assertEqual(w["width"], 2)
60
+ self.assertTrue(w["pathReturned"])
61
+ self.assertEqual(w["unknownEffectsBetweenCount"], 0)
62
+ for code in ["a1 20 00 c7 06 20 00 02 00 a3 22 00 c3", # other storage
63
+ "a1 20 00 c7 06 20 00 02 00 a2 20 00 c3", # partial width
64
+ "a1 20 00 c7 06 20 00 02 00 b8 03 00 a3 20 00 c3", # changed value
65
+ "a1 20 00 c7 06 20 00 02 00 26 a3 20 00 c3"]: # other segment
66
+ self.assertFalse(self.paths(report(code, "effects"))[0]["localRestorationWitnesses"])
67
+
68
+ def test_equal_value_without_read_provenance_is_not_a_restore(self):
69
+ # An independent immediate store equals the value read earlier.
70
+ p = self.paths(report("c7 06 20 00 00 00 83 3e 20 00 00 c7 06 20 00 05 00 c7 06 20 00 00 00 c3", "effects"))[0]
71
+ self.assertFalse(p["localRestorationWitnesses"])
72
+ # A loop re-pushes the same return address into a stack slot another call overwrote.
73
+ c = Code().emit("b9 02 00").label("top").branch("e8", "a").branch("e8", "b").emit("49").branch("75", "top").emit("c3")
74
+ c.label("a").emit("c3").label("b").emit("c3")
75
+ r = report(c, "effects", registers={"ss": 0x3000, "sp": 0xff00})
76
+ self.assertTrue(any(p["returned"] for p in self.paths(r)))
77
+ self.assertFalse([w for p in self.paths(r) for w in p["localRestorationWitnesses"]])
78
+
79
+ def test_timeline_keeps_flag_assumptions_that_split_paths(self):
80
+ r = report("b9 02 00 f3 a4 c3", "effects")
81
+ self.assertEqual(len(self.paths(r)), 2)
82
+ values = sorted(next(e for e in p["timeline"] if e["kind"] == "flag-assumption")["value"]["value"] for p in self.paths(r))
83
+ self.assertEqual(values, [0, 1])
84
+
85
+ def test_restore_prefix_is_not_a_completed_return(self):
86
+ p = self.paths(report("a1 20 00 c7 06 20 00 02 00 a3 20 00 ee", "effects"))[0]
87
+ self.assertTrue(p["localRestorationWitnesses"])
88
+ self.assertFalse(p["localRestorationWitnesses"][0]["pathReturned"])
89
+ self.assertFalse(p["effectCompleteWithinModel"])
90
+
91
+ def test_unknown_service_between_snapshot_and_restore_remains_visible(self):
92
+ c = Code().emit("a1 20 00 50 c7 06 20 00 02 00").label("service").branch("e8", "child")
93
+ c.emit("58 a3 20 00 c3").label("child").emit("c3")
94
+ r = report(c, "effects", callModels=[{"site": c.labels["service"], "evidence": "unknown external effects", "cases": [{}]}])
95
+ p = self.paths(r)[0]
96
+ self.assertFalse(p["effectCompleteWithinModel"])
97
+ # The model invalidates stack memory too; no surviving snapshot value is guessed.
98
+ self.assertFalse(p["localRestorationWitnesses"])
99
+
100
+ def test_port_stop_and_path_caps_cannot_prove_absent_later_effects(self):
101
+ p = self.paths(report("c7 06 20 00 01 00 ee c7 06 22 00 02 00 c3", "effects"))[0]
102
+ self.assertFalse(p["returned"])
103
+ self.assertEqual(p["stop"]["writesBeforeCount"], 1)
104
+ self.assertIn("port", p["stop"]["reason"].lower())
105
+ r = report("85 c0 74 06 c7 06 20 00 01 00 c3", "effects", maxPaths=1)
106
+ self.assertFalse(r["effectOrdering"]["allPathsRead"])
107
+ self.assertTrue(r["gaps"])
108
+ r = report("c7 06 20 00 01 00 c3", "effects", maxSteps=1)
109
+ self.assertFalse(self.paths(r)[0]["returned"])
110
+ self.assertEqual(self.paths(r)[0]["stop"]["writesBeforeCount"], 1)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ unittest.main()