Skip to content

Commit 903a16c

Browse files
committed
Release 0.0.3
1 parent 96d95e9 commit 903a16c

5 files changed

Lines changed: 74 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.0.3] - 2026-04-10
6+
7+
### Fixed
8+
9+
- Fixed Revit auto-launch dialog handling by starting the startup dialog resolver immediately after spawning Revit, before waiting for the named pipe.
10+
- Fixed startup dialog button selection to prefer safe allow-list actions such as `Always Load` and `Load Once`.
11+
- Prevented accidental clicks on destructive dialog actions such as `Do Not Load`, `Cancel`, and `No`.
12+
13+
### Notes
14+
15+
- This release focuses on reliable auto-launch behavior when Revit shows unsigned add-in security dialogs during startup.
16+
517
## [0.0.2] - 2026-04-10
618

719
### Fixed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "revitdevtool_pytest"
7-
version = "0.0.2"
7+
version = "0.0.3"
88
description = "pytest plugin for testing Revit API code via RevitDevTool Named Pipe bridge"
99
readme = "README.md"
1010
license = "MIT"

src/revitdevtool_pytest/dialog_resolver.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ class DialogResolverOptions:
5959
"Autodesk", "Revit", "Load", "Security", "Warning", "Add-in", "Addin",
6060
])
6161
preferred_button_keywords: list[str] = field(default_factory=lambda: [
62-
"Always Load", "Load", "OK", "Yes", "Accept", "Continue", "Close",
62+
"Always Load", "Load Once", "Load", "OK", "Yes", "Accept", "Continue", "Close",
63+
])
64+
blocked_button_keywords: list[str] = field(default_factory=lambda: [
65+
"Do Not Load", "Cancel", "No",
6366
])
6467

6568

@@ -106,17 +109,25 @@ def _is_whitelisted(self, title: str) -> bool:
106109
return any(kw.lower() in lower for kw in self._opts.dialog_title_keywords)
107110

108111
def _find_button(self, parent: int) -> int | None:
109-
result: list[int] = []
112+
result: list[tuple[int, int]] = []
110113

111114
@_EnumWindowsProc
112115
def _cb(child: int, _: int) -> bool:
113-
if _is_preferred_button(child, self._opts.preferred_button_keywords):
114-
result.append(child)
115-
return False
116+
score = _get_button_score(
117+
child,
118+
self._opts.preferred_button_keywords,
119+
self._opts.blocked_button_keywords,
120+
)
121+
if score is not None:
122+
result.append((score, child))
116123
return True
117124

118125
_user32.EnumChildWindows(parent, _cb, 0)
119-
return result[0] if result else None
126+
if not result:
127+
return None
128+
129+
result.sort(key=lambda item: item[0])
130+
return result[0][1]
120131

121132
def _enum_dialog_windows(self) -> list[int]:
122133
windows: list[int] = []
@@ -131,14 +142,30 @@ def _cb(hwnd: int, _: int) -> bool:
131142
return windows
132143

133144

134-
def _is_preferred_button(hwnd: int, keywords: list[str]) -> bool:
145+
def _get_button_score(hwnd: int, keywords: list[str], blocked_keywords: list[str]) -> int | None:
135146
if _get_class_name(hwnd).lower() != _BUTTON_CLASS:
136-
return False
147+
return None
148+
137149
text = _get_window_text(hwnd)
138150
if not text:
139-
return False
151+
return None
152+
140153
text_lower = text.lower()
141-
return any(kw.lower() in text_lower for kw in keywords)
154+
155+
if any(kw.lower() in text_lower for kw in blocked_keywords):
156+
return None
157+
158+
for index, keyword in enumerate(keywords):
159+
keyword_lower = keyword.lower()
160+
if text_lower == keyword_lower:
161+
return index
162+
163+
for index, keyword in enumerate(keywords):
164+
keyword_lower = keyword.lower()
165+
if keyword_lower in text_lower:
166+
return index + len(keywords)
167+
168+
return None
142169

143170

144171
def _is_target_dialog(hwnd: int, target_pid: int) -> bool:

src/revitdevtool_pytest/discovery.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ def launch_revit(
8787
return wait_for_revit_pipe(version, timeout_s=wait_timeout_s)
8888

8989

90+
def start_revit(version: int) -> int:
91+
"""Start Revit with ``/nosplash`` and return the spawned process id."""
92+
exe_path = find_revit_path(version)
93+
if exe_path is None:
94+
raise FileNotFoundError(f"Revit {version} installation not found.")
95+
96+
process = subprocess.Popen( # noqa: S603
97+
[exe_path, REVIT_NOSPLASH],
98+
creationflags=subprocess.DETACHED_PROCESS,
99+
)
100+
return int(process.pid)
101+
102+
90103
def wait_for_revit_pipe(
91104
version: int | None = None,
92105
timeout_s: float = DEFAULT_LAUNCH_TIMEOUT_S,

src/revitdevtool_pytest/plugin.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OPT_VERSION,
2323
PLUGIN_NAME,
2424
)
25-
from .discovery import find_revit_path, find_revit_pipes, launch_revit, select_instance
25+
from .discovery import find_revit_path, find_revit_pipes, select_instance, start_revit, wait_for_revit_pipe
2626
from .models import TestOutcome
2727
from .serializer import serialize_test
2828

@@ -152,22 +152,25 @@ def _auto_launch(version: int, config: pytest.Config) -> RevitInstance:
152152
)
153153

154154
print(f"{PLUGIN_NAME}: Launching Revit {version}...")
155-
instance = launch_revit(version, wait_timeout_s=timeout)
156155

157-
if instance is None:
158-
pytest.exit(
159-
f"{PLUGIN_NAME}: Revit {version} launched but Named Pipe did not appear within {timeout}s.",
160-
returncode=EXIT_CODE_CONFIG_ERROR,
161-
)
156+
process_id = start_revit(version)
162157

163158
try:
164159
from .dialog_resolver import StartupDialogResolver as _Resolver
165160

166-
_dialog_resolver = _Resolver(instance.process_id)
161+
_dialog_resolver = _Resolver(process_id)
167162
_dialog_resolver.start()
168163
except ImportError:
169164
pass
170165

166+
instance = wait_for_revit_pipe(version, timeout_s=timeout)
167+
168+
if instance is None:
169+
pytest.exit(
170+
f"{PLUGIN_NAME}: Revit {version} launched but Named Pipe did not appear within {timeout}s.",
171+
returncode=EXIT_CODE_CONFIG_ERROR,
172+
)
173+
171174
print(f"{PLUGIN_NAME}: Connected to Revit {instance.version} (pid={instance.process_id})")
172175
return instance
173176

0 commit comments

Comments
 (0)