pyvbaharness 1.0.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.
- pyvbaharness-1.0.0/LICENSE +21 -0
- pyvbaharness-1.0.0/PKG-INFO +508 -0
- pyvbaharness-1.0.0/README.md +469 -0
- pyvbaharness-1.0.0/pyproject.toml +77 -0
- pyvbaharness-1.0.0/setup.cfg +4 -0
- pyvbaharness-1.0.0/src/pyvbaharness/__init__.py +66 -0
- pyvbaharness-1.0.0/src/pyvbaharness/__main__.py +258 -0
- pyvbaharness-1.0.0/src/pyvbaharness/codegen.py +576 -0
- pyvbaharness-1.0.0/src/pyvbaharness/dialog_policy.py +149 -0
- pyvbaharness-1.0.0/src/pyvbaharness/lock.py +76 -0
- pyvbaharness-1.0.0/src/pyvbaharness/numbering.py +283 -0
- pyvbaharness-1.0.0/src/pyvbaharness/oracle.py +144 -0
- pyvbaharness-1.0.0/src/pyvbaharness/pool.py +181 -0
- pyvbaharness-1.0.0/src/pyvbaharness/process_control.py +300 -0
- pyvbaharness-1.0.0/src/pyvbaharness/properties.py +129 -0
- pyvbaharness-1.0.0/src/pyvbaharness/protocol.py +105 -0
- pyvbaharness-1.0.0/src/pyvbaharness/pytest_plugin.py +138 -0
- pyvbaharness-1.0.0/src/pyvbaharness/ranges.py +53 -0
- pyvbaharness-1.0.0/src/pyvbaharness/results.py +177 -0
- pyvbaharness-1.0.0/src/pyvbaharness/screenshot.py +193 -0
- pyvbaharness-1.0.0/src/pyvbaharness/session.py +1132 -0
- pyvbaharness-1.0.0/src/pyvbaharness/vbasig.py +247 -0
- pyvbaharness-1.0.0/src/pyvbaharness/worker/__init__.py +5 -0
- pyvbaharness-1.0.0/src/pyvbaharness/worker/__main__.py +448 -0
- pyvbaharness-1.0.0/src/pyvbaharness/worker/excel_host.py +796 -0
- pyvbaharness-1.0.0/src/pyvbaharness/worker/watcher.py +361 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/PKG-INFO +508 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/SOURCES.txt +30 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/dependency_links.txt +1 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/entry_points.txt +5 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/requires.txt +15 -0
- pyvbaharness-1.0.0/src/pyvbaharness.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Smith Edward
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyvbaharness
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Hang-resistant Python harness for running and validating VBA in desktop Excel
|
|
5
|
+
Author: William Smith Edward
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyVBAharness
|
|
8
|
+
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyVBAharness
|
|
9
|
+
Project-URL: Issues, https://github.com/WilliamSmithEdward/pyVBAharness/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/WilliamSmithEdward/pyVBAharness#readme
|
|
11
|
+
Keywords: excel,vba,automation,com,testing,pytest,macro,xlsm,windows
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
14
|
+
Classifier: Framework :: Pytest
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
24
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
25
|
+
Classifier: Topic :: Software Development :: Testing
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: pywin32>=306; sys_platform == "win32"
|
|
30
|
+
Provides-Extra: fuzz
|
|
31
|
+
Requires-Dist: hypothesis>=6; extra == "fuzz"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: hypothesis>=6; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
37
|
+
Requires-Dist: tomli>=2; python_version < "3.11" and extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# pyVBAharness
|
|
41
|
+
|
|
42
|
+
Run VBA in desktop Excel from Python, under a supervisor that enforces a
|
|
43
|
+
deadline on every call.
|
|
44
|
+
|
|
45
|
+
Excel automation has three failure modes that ordinary error handling does
|
|
46
|
+
not cover:
|
|
47
|
+
|
|
48
|
+
- A VBA runtime error opens a modal dialog and waits indefinitely.
|
|
49
|
+
- `Application.Run` takes no timeout parameter.
|
|
50
|
+
- Excel created over COM is not a child process, so terminating the caller
|
|
51
|
+
leaves `EXCEL.EXE` running with the workbook open.
|
|
52
|
+
|
|
53
|
+
The harness addresses each one. VBA errors are trapped inside VBA and
|
|
54
|
+
returned as data. Every command is issued from a supervisor process that
|
|
55
|
+
holds no COM references, so it can always enforce a deadline. When a
|
|
56
|
+
deadline expires, the Excel process the harness owns is terminated by
|
|
57
|
+
recorded process ID.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pyvbaharness import ExcelSession
|
|
61
|
+
|
|
62
|
+
with ExcelSession() as excel:
|
|
63
|
+
result = excel.run_vba("""
|
|
64
|
+
Public Function AddNums(ByVal a As Long, ByVal b As Long) As Long
|
|
65
|
+
AddNums = a + b
|
|
66
|
+
End Function
|
|
67
|
+
""", proc="AddNums", args=(20, 22))
|
|
68
|
+
|
|
69
|
+
result.outcome # 'passed'
|
|
70
|
+
result.value # 42
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Requirements
|
|
74
|
+
|
|
75
|
+
- Windows.
|
|
76
|
+
- Desktop Excel. Tested against Microsoft 365 x64; 2016 and newer expected
|
|
77
|
+
to work.
|
|
78
|
+
- Python 3.10 or newer. Tested on 3.14.
|
|
79
|
+
- `pywin32`, installed as a dependency.
|
|
80
|
+
|
|
81
|
+
### Trust access to the VBA project object model
|
|
82
|
+
|
|
83
|
+
Module injection goes through the VBA project object model, which Excel
|
|
84
|
+
blocks by default. Enable it at:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
File > Options > Trust Center > Trust Center Settings > Macro Settings
|
|
88
|
+
[x] Trust access to the VBA project object model
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The first run fails with a message naming this setting if it is off. The
|
|
92
|
+
setting is per Office application and per Windows user; enabling it for
|
|
93
|
+
Excel does not affect Word, and enabling it for one account does not affect
|
|
94
|
+
a service account. While enabled, any code running as that user can modify
|
|
95
|
+
VBA projects, so it suits a development machine rather than a shared server.
|
|
96
|
+
|
|
97
|
+
Run `python -m pyvbaharness doctor` to verify this and the rest of the
|
|
98
|
+
environment.
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install pyvbaharness
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Optional extras: `pyvbaharness[fuzz]` adds Hypothesis for property-based
|
|
107
|
+
testing. For development, clone and `pip install -e ".[dev,fuzz]"`.
|
|
108
|
+
|
|
109
|
+
## Running VBA
|
|
110
|
+
|
|
111
|
+
`run_vba` injects source as a module and calls one procedure from it,
|
|
112
|
+
creating an unsaved in-memory workbook if none is open. `run_macro` calls a
|
|
113
|
+
procedure that already exists in the workbook.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
excel.run_vba(source, proc="Main", args=(1, 2), timeout=30)
|
|
117
|
+
excel.run_macro("Module1.Main", 1, 2, timeout=30)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Return values arrive as Python objects. Scalars, dates, and 1-D or 2-D
|
|
121
|
+
arrays convert directly; COM objects convert to a `"<object:Range>"` marker
|
|
122
|
+
so the run still completes.
|
|
123
|
+
|
|
124
|
+
`PyVbaLog` is injected into every workbook and collects output into
|
|
125
|
+
`result.output`. `Debug.Print` writes to an Immediate window nothing is
|
|
126
|
+
reading, and `MsgBox` opens a blocking dialog, so neither is usable under
|
|
127
|
+
automation.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
result = excel.run_vba("""
|
|
131
|
+
Public Sub Main()
|
|
132
|
+
Dim i As Long
|
|
133
|
+
For i = 1 To 3
|
|
134
|
+
PyVbaLog "step " & i
|
|
135
|
+
Next i
|
|
136
|
+
End Sub
|
|
137
|
+
""")
|
|
138
|
+
result.output # ['step 1', 'step 2', 'step 3']
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Single expressions can be evaluated without writing a procedure:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
excel.eval("WorksheetFunction.Sum(1, 2, 3)") # 6.0
|
|
145
|
+
excel.eval('UCase$("abc")') # 'ABC'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Outcomes
|
|
149
|
+
|
|
150
|
+
Every run reports one of five outcomes. `passed` and `vba-error` describe
|
|
151
|
+
the VBA under test; the other three describe the harness or the environment
|
|
152
|
+
and say nothing about whether the code is correct.
|
|
153
|
+
|
|
154
|
+
| Outcome | Meaning |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `passed` | The procedure ran to completion |
|
|
157
|
+
| `vba-error` | VBA raised an error, captured in `result.error` |
|
|
158
|
+
| `timeout` | The run exceeded its deadline; Excel was terminated |
|
|
159
|
+
| `modal-blocked` | A dialog requiring a human decision appeared; Excel was terminated |
|
|
160
|
+
| `runner-error` | The harness or a COM call failed around the run |
|
|
161
|
+
|
|
162
|
+
## Errors
|
|
163
|
+
|
|
164
|
+
Errors are captured inside VBA, so no dialog opens. Injected source is
|
|
165
|
+
instrumented with `Erl` line numbers and a per-procedure handler, which
|
|
166
|
+
supplies the failing line and the call stack.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
result = excel.run_vba("""
|
|
170
|
+
Public Sub Main()
|
|
171
|
+
Dim x As Long
|
|
172
|
+
x = 1
|
|
173
|
+
Err.Raise 513, "MyModule", "something went wrong"
|
|
174
|
+
End Sub
|
|
175
|
+
""")
|
|
176
|
+
|
|
177
|
+
result.outcome # 'vba-error'
|
|
178
|
+
result.error.number # 513
|
|
179
|
+
result.error.source # 'MyModule'
|
|
180
|
+
result.error.description # 'something went wrong'
|
|
181
|
+
result.error.line # 5
|
|
182
|
+
result.error.stack # [('PyVbaUserCode.Main', 5)]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
For a nested failure, `error.stack` lists every frame the error passed
|
|
186
|
+
through, deepest first:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
[('Helpers.Parse', 12), ('Model.Load', 40), ('Main.Run', 7)]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`line_numbers=False` injects source unmodified, leaving `error.line` and
|
|
193
|
+
`error.stack` empty. Instrumentation is skipped for sources that already
|
|
194
|
+
contain numeric line labels, and an explicit `On Error` statement takes
|
|
195
|
+
precedence from the point it appears.
|
|
196
|
+
|
|
197
|
+
## Timeouts
|
|
198
|
+
|
|
199
|
+
Every command carries a deadline. On expiry the harness terminates its Excel
|
|
200
|
+
process, records the outcome as `timeout`, and starts a fresh instance for
|
|
201
|
+
the next call.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
result = excel.run_vba("""
|
|
205
|
+
Public Sub Main()
|
|
206
|
+
Do
|
|
207
|
+
Loop
|
|
208
|
+
End Sub
|
|
209
|
+
""", timeout=5.0)
|
|
210
|
+
|
|
211
|
+
result.outcome # 'timeout'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
For work of unpredictable duration, report progress from VBA and set
|
|
215
|
+
`idle_timeout`. Each report extends the deadline, so the run is terminated
|
|
216
|
+
only after it stops reporting.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
result = excel.run_vba(source, proc="Recalculate",
|
|
220
|
+
idle_timeout=60,
|
|
221
|
+
on_progress=lambda fraction, message: ...)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```vba
|
|
225
|
+
Public Sub Recalculate()
|
|
226
|
+
For i = 1 To 10000
|
|
227
|
+
' ... work ...
|
|
228
|
+
PyVbaProgress i / 10000, "row " & i
|
|
229
|
+
Next i
|
|
230
|
+
End Sub
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Workbooks and ranges
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
excel.open_workbook(r"C:\reports\model.xlsm", read_only=True)
|
|
237
|
+
excel.run_macro("Analysis.Recalculate", timeout=120)
|
|
238
|
+
rows = excel.read_range("Summary", "A1:D50")
|
|
239
|
+
|
|
240
|
+
excel.new_workbook()
|
|
241
|
+
excel.write_range("Sheet1", "A1", [[1, 2], [3, 4]])
|
|
242
|
+
excel.save_as(r"C:\out\result.xlsm")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Workbooks open read-only by default and close without saving unless
|
|
246
|
+
`save_as` is called. Range reads and writes transfer whole blocks in one COM
|
|
247
|
+
call. `reset_sheets()` clears all worksheets while keeping injected modules,
|
|
248
|
+
for use between tests.
|
|
249
|
+
|
|
250
|
+
Modules can be exported to and imported from `.bas` and `.cls` files:
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
excel.export_modules("vba/") # VBIDE export, for version control
|
|
254
|
+
excel.import_modules("vba/") # document modules are skipped
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Testing
|
|
258
|
+
|
|
259
|
+
Zero-argument procedures named `Test*` are discovered and run individually.
|
|
260
|
+
`PyVbaAssert` and `PyVbaAssertEqual` produce structured failures; any other
|
|
261
|
+
error is reported with its number, line, and stack.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
results = excel.run_tests("""
|
|
265
|
+
Public Sub TestMath()
|
|
266
|
+
PyVbaAssertEqual 4, 2 + 2
|
|
267
|
+
End Sub
|
|
268
|
+
|
|
269
|
+
Public Sub TestBroken()
|
|
270
|
+
PyVbaAssertEqual 5, 2 + 2, "arithmetic is broken"
|
|
271
|
+
End Sub
|
|
272
|
+
""")
|
|
273
|
+
|
|
274
|
+
for case in results:
|
|
275
|
+
print(case.name, case.passed, case.result.error)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
A test that hangs is reported as `timeout`. The session recycles, the test
|
|
279
|
+
module is reinjected, and the remaining tests run. When recovery is not
|
|
280
|
+
possible, such as with `auto_recycle=False`, the remaining tests are
|
|
281
|
+
reported as not run.
|
|
282
|
+
|
|
283
|
+
### pytest integration
|
|
284
|
+
|
|
285
|
+
Procedures in files named `test_*.bas` are collected as pytest items:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
pytest tests/vba/
|
|
289
|
+
pytest -k Discount -v
|
|
290
|
+
pytest --junitxml=results.xml
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Failures report the assertion message, error line, VBA stack, and any
|
|
294
|
+
`PyVbaLog` output. One auto-recycling session serves the run; under
|
|
295
|
+
pytest-xdist each worker process gets its own.
|
|
296
|
+
|
|
297
|
+
### Coverage
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
excel.add_module("Model", source, coverage=True)
|
|
301
|
+
excel.run_tests(test_source)
|
|
302
|
+
|
|
303
|
+
report = excel.coverage_report()
|
|
304
|
+
report.percent # 87.5
|
|
305
|
+
report.modules["model"].missed # [42, 43, 51]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Coverage is opt-in per module because instrumented code runs slower. Hits
|
|
309
|
+
accumulate across runs until the module is replaced.
|
|
310
|
+
|
|
311
|
+
### Property-based testing
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from pyvbaharness.properties import check_vba_function
|
|
315
|
+
|
|
316
|
+
check_vba_function(excel, source, "Discount",
|
|
317
|
+
check=lambda args, value: 0 <= value <= args[0])
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Input strategies are derived from the parsed VBA signature. Hypothesis
|
|
321
|
+
shrinks failures to a minimal counterexample. Requires
|
|
322
|
+
`pip install pyvbaharness[fuzz]`.
|
|
323
|
+
|
|
324
|
+
## Batch execution
|
|
325
|
+
|
|
326
|
+
`run_batch` runs many calls in one COM round trip. Results are returned in
|
|
327
|
+
call order with the same detail as `run_macro`, including per-call errors
|
|
328
|
+
with line and stack.
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
results = excel.run_batch([
|
|
332
|
+
("Model.Score", (row, weight)) for row, weight in inputs
|
|
333
|
+
])
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Measured against equivalent serial calls: 2.7x at 50 calls, 4.7x at 200,
|
|
337
|
+
9.9x at 3000, where per-call cost reaches 0.066 ms. Arguments must be
|
|
338
|
+
scalars.
|
|
339
|
+
|
|
340
|
+
## Parallel execution
|
|
341
|
+
|
|
342
|
+
`SessionPool` distributes work across several owned Excel instances. Each
|
|
343
|
+
member is a full session with its own process, watchdogs, and recovery, so a
|
|
344
|
+
hang recycles one member while the others continue.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from pyvbaharness import SessionPool
|
|
348
|
+
|
|
349
|
+
with SessionPool(4) as pool:
|
|
350
|
+
futures = [pool.run_vba(source, proc="Crunch", args=(n,))
|
|
351
|
+
for n in range(24)]
|
|
352
|
+
results = [f.result() for f in futures]
|
|
353
|
+
|
|
354
|
+
cases = pool.run_tests(suite_source, timeout=30) # sharded across members
|
|
355
|
+
|
|
356
|
+
future = pool.submit(lambda s: ( # multi-step flow
|
|
357
|
+
s.open_workbook(r"C:\data\model.xlsm"),
|
|
358
|
+
s.run_macro("Model.Recalculate", timeout=300),
|
|
359
|
+
s.read_range("Out", "A1:C10"),
|
|
360
|
+
)[-1])
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Throughput on a 16-core machine with 120 ms tasks: 2.0x at two members, 3.6x
|
|
364
|
+
at four, 4.7x at six (`benchmarks/output/pool-baseline-1.0.0.json`). Each
|
|
365
|
+
member uses 150 to 300 MB of RAM. Compile checks remain serialized
|
|
366
|
+
machine-wide inside a pool because they drive the visible VBE, which is a
|
|
367
|
+
shared surface; hidden runs and range IO do not interfere with each other.
|
|
368
|
+
|
|
369
|
+
## Compile checking
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
result = excel.compile_project(watch_seconds=15)
|
|
373
|
+
if result.outcome == "rejected":
|
|
374
|
+
print(result.dialog.message)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Excel is made visible for the duration of the check. A hidden Excel does not
|
|
378
|
+
surface the compile-error dialog, which would report a rejection as a pass.
|
|
379
|
+
A clean project usually returns in about a second: the VBE disables its
|
|
380
|
+
Compile command once compilation succeeds, and the harness watches for that
|
|
381
|
+
signal. An `infrastructure-failure` outcome means the check could not
|
|
382
|
+
complete and the result is unknown.
|
|
383
|
+
|
|
384
|
+
Source that calls `PyVbaLog` or the assertion helpers requires
|
|
385
|
+
`compile_project(include_harness_support=True)` so those names resolve as
|
|
386
|
+
they do at run time.
|
|
387
|
+
|
|
388
|
+
## Command line
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
pyvbaharness doctor --live
|
|
392
|
+
pyvbaharness run My.bas --proc Main --arg 42
|
|
393
|
+
pyvbaharness check My.bas Other.cls
|
|
394
|
+
pyvbaharness check --workbook model.xlsm
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`python -m pyvbaharness <command>` works identically.
|
|
398
|
+
|
|
399
|
+
`doctor` checks Excel, pywin32, the VBA project trust setting, and the VBE
|
|
400
|
+
error-trapping mode; "Break on All Errors" sends handled errors to the
|
|
401
|
+
debugger and stalls automation. `--live` additionally starts an owned Excel
|
|
402
|
+
and runs a smoke test.
|
|
403
|
+
|
|
404
|
+
Exit codes: `0` pass or accepted, `1` VBA failure or rejected compile, `2`
|
|
405
|
+
infrastructure failure.
|
|
406
|
+
|
|
407
|
+
## Dialog handling
|
|
408
|
+
|
|
409
|
+
A watcher thread scans the owned Excel process for dialogs while a run is in
|
|
410
|
+
flight and applies a deliberately narrow policy:
|
|
411
|
+
|
|
412
|
+
| Dialog | Action |
|
|
413
|
+
| --- | --- |
|
|
414
|
+
| VBA runtime error | Dismissed with End |
|
|
415
|
+
| Informational (OK, or OK and Help only) | Dismissed with OK |
|
|
416
|
+
| Anything with a real choice (Yes/No, Cancel, Retry, Debug, Save) | Reported as `modal-blocked`; Excel terminated |
|
|
417
|
+
| Compile error, outside a compile check | Reported as `modal-blocked`; Excel terminated |
|
|
418
|
+
| Excel's own prompts, such as "save your changes?" | Reported as `modal-blocked`; Excel terminated |
|
|
419
|
+
|
|
420
|
+
Excel's own prompts are not classic Win32 dialogs. "Do you want to save your
|
|
421
|
+
changes?" is a `NUIDialog` whose controls are drawn inside a NetUI surface,
|
|
422
|
+
with no Win32 buttons to enumerate or click, so its text cannot be read and
|
|
423
|
+
it is never dismissed. It is detected by modality instead: a titled Excel
|
|
424
|
+
window is disabled for as long as something modal owns the application,
|
|
425
|
+
whatever class that something is. The result is `modal-blocked` rather than
|
|
426
|
+
a bare timeout.
|
|
427
|
+
|
|
428
|
+
Prompts of this kind are suppressed in the first place by `DisplayAlerts`,
|
|
429
|
+
`AskToUpdateLinks`, `UpdateLinks:=0`, `IgnoreReadOnlyRecommended`, and
|
|
430
|
+
closing without saving. Detection covers the case where VBA under test turns
|
|
431
|
+
suppression back on. Alert suppression is re-asserted before the harness
|
|
432
|
+
closes, saves, or quits, so user code cannot leave a prompt waiting for the
|
|
433
|
+
teardown path.
|
|
434
|
+
|
|
435
|
+
`result.dialogs` records what appeared and what was done about it. On a
|
|
436
|
+
blocked dialog or a timeout, a screenshot of the Excel window is captured
|
|
437
|
+
where possible and referenced from the result, which is the practical way to
|
|
438
|
+
see an Excel prompt whose text cannot be read.
|
|
439
|
+
|
|
440
|
+
## Process ownership
|
|
441
|
+
|
|
442
|
+
The harness creates its own Excel instance and never attaches to a running
|
|
443
|
+
one. It verifies this by comparing `EXCEL.EXE` process IDs before and after
|
|
444
|
+
creation, and refuses to proceed if the instance already existed, since a
|
|
445
|
+
timeout would terminate it.
|
|
446
|
+
|
|
447
|
+
The owned instance is placed in a kernel job object with kill-on-close, so
|
|
448
|
+
Windows terminates Excel if the harness process dies for any reason,
|
|
449
|
+
including a force kill. Manifest files provide a second layer: they record
|
|
450
|
+
process ID and start time together, so a later sweep cannot terminate an
|
|
451
|
+
unrelated process that reused the ID.
|
|
452
|
+
|
|
453
|
+
One session runs at a time per machine by default, preventing accidental
|
|
454
|
+
overlap. `SessionPool` is the supported route to concurrency.
|
|
455
|
+
|
|
456
|
+
Run targets must be a plain `Proc` or `Module.Proc` inside the harness
|
|
457
|
+
workbook. Workbook-qualified targets are rejected before any COM call.
|
|
458
|
+
|
|
459
|
+
## Performance
|
|
460
|
+
|
|
461
|
+
Measured on Excel 365 x64 with Python 3.14
|
|
462
|
+
(`benchmarks/output/baseline-1.0.0.json`):
|
|
463
|
+
|
|
464
|
+
| Operation | Median |
|
|
465
|
+
| --- | --- |
|
|
466
|
+
| Session startup and teardown | 0.5 s warm, ~3 s cold |
|
|
467
|
+
| Run a procedure, same target as previous call | 0.5 ms |
|
|
468
|
+
| Run a procedure with arguments | 0.7 ms |
|
|
469
|
+
| `run_vba` with unchanged source | 0.9 ms |
|
|
470
|
+
| Run a different target (dispatcher regenerated) | 76 ms |
|
|
471
|
+
| Batched calls, 1000 per batch | 0.094 ms each |
|
|
472
|
+
| Compile check, clean project | 1.0 s |
|
|
473
|
+
| Write 10,000 cells | 63 ms |
|
|
474
|
+
| Read 10,000 cells | 9 ms |
|
|
475
|
+
|
|
476
|
+
Per-run cost depends on three caches: resolved target signatures, injected
|
|
477
|
+
source, and the generated dispatcher. Repeated calls to the same target with
|
|
478
|
+
unchanged source hit all three. Changing the target regenerates the
|
|
479
|
+
dispatcher, which accounts for the 97 ms figure.
|
|
480
|
+
|
|
481
|
+
## Development
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
python -m pytest tests/unit # 147 tests, no Excel
|
|
485
|
+
python -m pytest tests/live -m live -o addopts="" # 58 tests, real Excel
|
|
486
|
+
python benchmarks/run_benchmarks.py
|
|
487
|
+
python benchmarks/run_pool_benchmarks.py
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
The unit suite covers dialog policy, trace validation, signature parsing,
|
|
491
|
+
code generation, source instrumentation, and write chunking, plus the
|
|
492
|
+
supervisor state machine driven by a fake worker that speaks the same pipe
|
|
493
|
+
protocol. The live suite covers real Excel behavior, including deliberate
|
|
494
|
+
hangs, blocking dialogs, and a worker terminated mid-run; it asserts that no
|
|
495
|
+
Excel processes survive.
|
|
496
|
+
|
|
497
|
+
## Documentation
|
|
498
|
+
|
|
499
|
+
| Document | Contents |
|
|
500
|
+
| --- | --- |
|
|
501
|
+
| [Architecture](docs/architecture.md) | Process model, hang-resistance layers, design rationale |
|
|
502
|
+
| [Troubleshooting](docs/troubleshooting.md) | What each failure means and what to do about it |
|
|
503
|
+
| [Implementation guide](docs/IMPLEMENTATION_GUIDE.md) | How to change the code; catalog of measured Excel behaviors |
|
|
504
|
+
| [Releasing](docs/RELEASING.md) | Version bump, validation, and the PyPI publishing workflow |
|
|
505
|
+
|
|
506
|
+
## License
|
|
507
|
+
|
|
508
|
+
MIT. See [LICENSE](LICENSE).
|