@@ -204,6 +204,30 @@ def registry_key_exists() -> bool:
204204 return False
205205
206206
207+ def read_registry_value (sub_key : str , value_name : str ) -> str | None :
208+ import winreg # Windows-only; imported lazily so this module loads on the Linux CI runner.
209+
210+ try :
211+ with winreg .OpenKey (winreg .HKEY_CURRENT_USER , sub_key , 0 , winreg .KEY_READ ) as key :
212+ value , _ = winreg .QueryValueEx (key , value_name )
213+ return str (value )
214+ except FileNotFoundError :
215+ return None
216+
217+
218+ # (label, sub_key, value_name) for the context-menu entries the app populates on first launch.
219+ REGISTRY_VALUE_PROBES = (
220+ ("Subsearch" , r"Software\Classes\*\shell\Subsearch" , "" ),
221+ ("Icon" , r"Software\Classes\*\shell\Subsearch" , "Icon" ),
222+ ("AppliesTo" , r"Software\Classes\*\shell\Subsearch" , "AppliesTo" ),
223+ ("command" , r"Software\Classes\*\shell\Subsearch\command" , "" ),
224+ )
225+
226+
227+ def read_registry_values () -> list [tuple [str , str | None ]]:
228+ return [(label , read_registry_value (sub_key , value_name )) for label , sub_key , value_name in REGISTRY_VALUE_PROBES ]
229+
230+
207231class BinaryTester :
208232 def msi_artifact_path (self ) -> Path :
209233 candidates = sorted (Paths .artifacts .glob ("*.msi" ))
@@ -368,46 +392,96 @@ def test_executable(self, test_length: int = 30) -> None:
368392 self .end_process (EXE_NAME )
369393
370394
371- class BinaryTestReport :
372- _PROBES = ("Exe" , "Log" , "Config" , "Registry key" )
395+ class Check :
396+ def __init__ (self , label : str , expected : bool , actual : bool , detail : str ) -> None :
397+ self .label = label
398+ self .expected = expected
399+ self .actual = actual
400+ self .detail = detail
373401
374- # Per stage, the expected presence of (exe, log, config, registry key).
375- _EXPECTED_STATE = {
376- "install" : (True , False , False , True ),
377- "executable" : (True , True , True , True ),
378- "uninstall" : (False , True , True , False ),
402+ @property
403+ def passed (self ) -> bool :
404+ return self .actual == self .expected
405+
406+
407+ class BinaryTestReport :
408+ _TITLES = {
409+ "install" : "MSI install" ,
410+ "executable" : "Subsearch ran" ,
411+ "uninstall" : "MSI uninstall" ,
379412 }
380413
381414 def __init__ (self , step_summary : StepSummary ) -> None :
382415 self ._step_summary = step_summary
383416
384- def _current_state (self ) -> tuple [bool , bool , bool , bool ]:
385- return (
386- Paths .installed_executable .is_file (),
387- Paths .log_file .is_file (),
388- Paths .config_file .is_file (),
389- registry_key_exists (),
390- )
417+ def _path_check (self , label : str , path : Path , expected : bool ) -> Check :
418+ present = path .is_file ()
419+ detail = path .as_posix () if present else f"missing ({ path .as_posix ()} )"
420+ return Check (label , expected , present , detail )
421+
422+ def _registry_value_checks (self , expected_populated : bool ) -> list [Check ]:
423+ checks = []
424+ for label , value in read_registry_values ():
425+ populated = value not in (None , "" )
426+ if value is None :
427+ detail = "key absent"
428+ elif value == "" :
429+ detail = "empty placeholder"
430+ else :
431+ detail = f"`{ value } `"
432+ checks .append (Check (f"Registry { label } " , expected_populated , populated , detail ))
433+ return checks
434+
435+ def _checks_for_install (self ) -> list [Check ]:
436+ return [
437+ self ._path_check ("Executable" , Paths .installed_executable , expected = True ),
438+ self ._path_check ("Start Menu shortcut" , Paths .start_menu_shortcut , expected = True ),
439+ * self ._registry_value_checks (expected_populated = False ),
440+ ]
441+
442+ def _checks_for_executable (self ) -> list [Check ]:
443+ return [
444+ self ._path_check ("Executable" , Paths .installed_executable , expected = True ),
445+ self ._path_check ("Log" , Paths .log_file , expected = True ),
446+ self ._path_check ("Config" , Paths .config_file , expected = True ),
447+ * self ._registry_value_checks (expected_populated = True ),
448+ ]
449+
450+ def _checks_for_uninstall (self ) -> list [Check ]:
451+ return [
452+ self ._path_check ("Executable removed" , Paths .installed_executable , expected = False ),
453+ self ._path_check ("Start Menu shortcut removed" , Paths .start_menu_shortcut , expected = False ),
454+ Check ("Registry key removed" , expected = False , actual = registry_key_exists (), detail = "HKCU context-menu key" ),
455+ ]
456+
457+ def _checks_for_stage (self , name : str ) -> list [Check ]:
458+ return {
459+ "install" : self ._checks_for_install ,
460+ "executable" : self ._checks_for_executable ,
461+ "uninstall" : self ._checks_for_uninstall ,
462+ }[name ]()
463+
464+ def add_stage_card (self , name : str ) -> None :
465+ checks = self ._checks_for_stage (name )
466+ passed = all (check .passed for check in checks )
467+ self ._step_summary .card (f"{ self ._TITLES [name ]} : { self ._step_summary .result (passed )} " , passed = passed )
468+ self ._step_summary .add_summary ("Reason:" )
469+ for check in checks :
470+ mark = self ._step_summary .result (check .passed )
471+ self ._step_summary .add_summary (f"- { check .label } : { mark } - { check .detail } " )
472+ self ._log_stage (name , checks , passed )
473+ self ._assert_stage_passed (name , passed )
391474
392475 def _assert_stage_passed (self , name : str , passed : bool ) -> None :
393476 if not passed :
394477 list_files_in_directory (Paths .installed_executable .parent )
395478 list_files_in_directory (Paths .log_file .parent )
396479 raise RuntimeError (f"{ name } test failed" )
397480
398- def add_stage_card (self , name : str ) -> None :
399- actual = self ._current_state ()
400- expected = self ._EXPECTED_STATE [name ]
401- passed = actual == expected
402- self ._step_summary .card (f"{ name .capitalize ()} test" , passed = passed )
403- for probe , probe_found , probe_expected in zip (self ._PROBES , actual , expected ):
404- self ._step_summary .add_summary (f"- { probe } : { self ._step_summary .result (probe_found == probe_expected )} " )
405- self ._log_stage (name , actual , passed )
406- self ._assert_stage_passed (name , passed )
407-
408- def _log_stage (self , name : str , actual : tuple [bool , bool , bool , bool ], passed : bool ) -> None :
409- log (", " .join (f"{ probe } : { present } " for probe , present in zip (self ._PROBES , actual )))
410- log (f"{ name .capitalize ()} test { 'passed' if passed else 'failed' } " , level = "PASS" if passed else "FAIL" )
481+ def _log_stage (self , name : str , checks : list [Check ], passed : bool ) -> None :
482+ for check in checks :
483+ log (f"{ check .label } : { 'OK' if check .passed else 'BAD' } ({ check .detail } )" )
484+ log (f"{ self ._TITLES [name ]} { 'passed' if passed else 'failed' } " , level = "PASS" if passed else "FAIL" )
411485 print (STYLE_SEPARATOR )
412486
413487
0 commit comments