Files
rea/tests/fixtures/hopperBookmarkMutationProbe.py
morluto fd7da77fc0 fix(hopper): verify bookmark names and clear existing labels
Use the public rename API for an explicit empty existing name, retain omitted names and raw listing values, and verify fresh unnamed creation and explicit mutations by readback. Preserve independent verifier restorations and their primary failures.

Refs #5475. Focused bridge regressions and the complete real-Hopper suite passed on macOS arm64 with Hopper 6.6.0-demo 42.6316032. The verifier used a temporary unavailable Ghidra override to meet its sole-candidate prerequisite; reporter Hopper 6.1 remains unverified.
2026-10-20 22:39:23 +08:00

185 lines
6.5 KiB
Python

"""Exercise bookmark mutation and exact readback at the Hopper bridge boundary."""
import json
from pathlib import Path
import sys
class FakeSegment:
pass
class FakeDocument:
def __init__(self):
self.names = {}
self.setter_calls = []
self.rename_calls = []
self.ignore_next_set = False
self.ignore_next_rename = False
self.default_name_on_omitted = False
def getDocumentName(self):
return "bookmark-fixture"
def getExecutableFilePath(self):
return "/tmp/rea-hopper-bookmark-fixture"
def getDatabaseFilePath(self):
return None
def backgroundProcessActive(self):
return False
def getSegmentAtAddress(self, address):
return FakeSegment() if address == 0x401000 else None
def setBookmarkAtAddress(self, address, name=None):
self.setter_calls.append((address, name))
if self.ignore_next_set:
self.ignore_next_set = False
return True
if address not in self.names:
if name is None and self.default_name_on_omitted:
self.names[address] = "provider-default"
else:
self.names[address] = "" if name == "" else name
elif name is not None and name != "":
self.names[address] = name
# The native API return is not authoritative; the bridge reads state back.
return False
def renameBookmarkAtAddress(self, address, name):
self.rename_calls.append((address, name))
if self.ignore_next_rename:
self.ignore_next_rename = False
return False
self.names[address] = None if name == "" else name
return False
def hasBookmarkAtAddress(self, address):
return address in self.names
def getBookmarkName(self, address):
return self.names[address]
def getBookmarks(self):
return list(self.names)
def removeBookmarkAtAddress(self, address):
self.names.pop(address, None)
return False
class FakeDocumentProvider:
document = FakeDocument()
@classmethod
def getAllDocuments(cls):
return [cls.document]
@classmethod
def getCurrentDocument(cls):
return cls.document
def main():
bridge_path = sys.argv[1]
namespace = {"__file__": bridge_path, "__name__": "rea_hopper_bridge"}
source = Path(bridge_path).read_text(encoding="utf-8")
exec(compile(source, bridge_path, "exec"), namespace)
namespace["REA_TARGET_PATH"] = FakeDocumentProvider.document.getExecutableFilePath()
namespace["_configure_hopper_api"](FakeDocumentProvider)
namespace["_bind_session_document"]()
dispatch = namespace["_dispatch"]
address = "0x401000"
document = FakeDocumentProvider.document
fresh_empty_result = dispatch("set_bookmark", {"address": address, "name": ""})
fresh_empty_listing = dispatch("list_bookmarks", {})
fresh_empty_raw_name = document.getBookmarkName(0x401000)
dispatch("unset_bookmark", {"address": address})
document.default_name_on_omitted = True
default_named_omitted_result = dispatch("set_bookmark", {"address": address})
default_named_omitted_listing = dispatch("list_bookmarks", {})
dispatch("unset_bookmark", {"address": address})
document.default_name_on_omitted = False
fresh_omitted_result = dispatch("set_bookmark", {"address": address})
fresh_omitted_listing = dispatch("list_bookmarks", {})
dispatch("unset_bookmark", {"address": address})
named_creation_result = dispatch(
"set_bookmark", {"address": address, "name": "initial-name"}
)
clear_result = dispatch("set_bookmark", {"address": address, "name": ""})
clear_listing = dispatch("list_bookmarks", {})
renamed_result = dispatch(
"set_bookmark", {"address": address, "name": "replacement-name"}
)
replacement_listing = dispatch("list_bookmarks", {})
setter_calls_before_omitted = len(document.setter_calls)
omitted_result = dispatch("set_bookmark", {"address": address})
setter_calls_after_omitted = len(document.setter_calls)
omitted_listing = dispatch("list_bookmarks", {})
document.ignore_next_set = True
ignored_set_result = dispatch(
"set_bookmark", {"address": address, "name": "ignored-name"}
)
ignored_set_listing = dispatch("list_bookmarks", {})
document.ignore_next_rename = True
failed_clear_result = dispatch("set_bookmark", {"address": address, "name": ""})
failed_clear_listing = dispatch("list_bookmarks", {})
rename_method = document.renameBookmarkAtAddress
document.renameBookmarkAtAddress = None
missing_rename = None
try:
dispatch("set_bookmark", {"address": address, "name": ""})
except Exception as error:
missing_rename = {
"type": type(error).__name__,
"diagnostic_type": namespace["_diagnostic_type"](error),
}
document.renameBookmarkAtAddress = rename_method
preserved_after_missing_rename = dispatch("list_bookmarks", {})
unset_result = dispatch("unset_bookmark", {"address": address})
after_unset = dispatch("list_bookmarks", {})
print(json.dumps({
"fresh_empty": {
"result": fresh_empty_result,
"listing": fresh_empty_listing,
"raw_name": fresh_empty_raw_name,
},
"fresh_omitted": {
"result": fresh_omitted_result,
"listing": fresh_omitted_listing,
},
"default_named_omitted": {
"result": default_named_omitted_result,
"listing": default_named_omitted_listing,
},
"named_creation_result": named_creation_result,
"empty_clear": {"result": clear_result, "listing": clear_listing},
"nonempty_replacement": {"result": renamed_result, "listing": replacement_listing},
"omitted_existing": {
"result": omitted_result,
"setter_calls_before": setter_calls_before_omitted,
"setter_calls_after": setter_calls_after_omitted,
"listing": omitted_listing,
},
"ignored_setter": {"result": ignored_set_result, "listing": ignored_set_listing},
"failed_clear": {"result": failed_clear_result, "listing": failed_clear_listing},
"missing_rename": missing_rename,
"preserved_after_missing_rename": preserved_after_missing_rename,
"unset": {"result": unset_result, "listing": after_unset},
"rename_calls": [[hex(item[0]), item[1]] for item in document.rename_calls],
}, sort_keys=True))
if __name__ == "__main__":
main()