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.
Files changed (32) hide show
  1. pyvbaharness-1.0.0/LICENSE +21 -0
  2. pyvbaharness-1.0.0/PKG-INFO +508 -0
  3. pyvbaharness-1.0.0/README.md +469 -0
  4. pyvbaharness-1.0.0/pyproject.toml +77 -0
  5. pyvbaharness-1.0.0/setup.cfg +4 -0
  6. pyvbaharness-1.0.0/src/pyvbaharness/__init__.py +66 -0
  7. pyvbaharness-1.0.0/src/pyvbaharness/__main__.py +258 -0
  8. pyvbaharness-1.0.0/src/pyvbaharness/codegen.py +576 -0
  9. pyvbaharness-1.0.0/src/pyvbaharness/dialog_policy.py +149 -0
  10. pyvbaharness-1.0.0/src/pyvbaharness/lock.py +76 -0
  11. pyvbaharness-1.0.0/src/pyvbaharness/numbering.py +283 -0
  12. pyvbaharness-1.0.0/src/pyvbaharness/oracle.py +144 -0
  13. pyvbaharness-1.0.0/src/pyvbaharness/pool.py +181 -0
  14. pyvbaharness-1.0.0/src/pyvbaharness/process_control.py +300 -0
  15. pyvbaharness-1.0.0/src/pyvbaharness/properties.py +129 -0
  16. pyvbaharness-1.0.0/src/pyvbaharness/protocol.py +105 -0
  17. pyvbaharness-1.0.0/src/pyvbaharness/pytest_plugin.py +138 -0
  18. pyvbaharness-1.0.0/src/pyvbaharness/ranges.py +53 -0
  19. pyvbaharness-1.0.0/src/pyvbaharness/results.py +177 -0
  20. pyvbaharness-1.0.0/src/pyvbaharness/screenshot.py +193 -0
  21. pyvbaharness-1.0.0/src/pyvbaharness/session.py +1132 -0
  22. pyvbaharness-1.0.0/src/pyvbaharness/vbasig.py +247 -0
  23. pyvbaharness-1.0.0/src/pyvbaharness/worker/__init__.py +5 -0
  24. pyvbaharness-1.0.0/src/pyvbaharness/worker/__main__.py +448 -0
  25. pyvbaharness-1.0.0/src/pyvbaharness/worker/excel_host.py +796 -0
  26. pyvbaharness-1.0.0/src/pyvbaharness/worker/watcher.py +361 -0
  27. pyvbaharness-1.0.0/src/pyvbaharness.egg-info/PKG-INFO +508 -0
  28. pyvbaharness-1.0.0/src/pyvbaharness.egg-info/SOURCES.txt +30 -0
  29. pyvbaharness-1.0.0/src/pyvbaharness.egg-info/dependency_links.txt +1 -0
  30. pyvbaharness-1.0.0/src/pyvbaharness.egg-info/entry_points.txt +5 -0
  31. pyvbaharness-1.0.0/src/pyvbaharness.egg-info/requires.txt +15 -0
  32. 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).