Skip to content

Commit bb84c33

Browse files
committed
Code coverage support
1 parent 35b86af commit bb84c33

File tree

9 files changed

+149
-6
lines changed

9 files changed

+149
-6
lines changed

.coverage

-3.7 KB
Binary file not shown.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ tmpdir_*
3030

3131
# AI Coding
3232
/plan
33+
34+
# Coverage
35+
.coverage

COMPILING.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,12 @@ Windows 11 and later:
3737

3838
Linux:
3939
* Figure it out and let me know.
40+
41+
42+
Running Coverage
43+
================
44+
45+
$ coverage run -m crystal --test
46+
$ coverage report
47+
$ coverage html
48+
$ open htmlcov/index.html

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Release Notes ⋮
2626
* Manually bring app to front on macOS when run from source,
2727
to workaround a wxPython 4.2.3 bug.
2828
* Show correct Dock icon on macOS when run from source.
29+
* Code coverage statistics can now be gathered using the `coverage` tool.
2930

3031
### v2.0.0 (September 26, 2025)
3132

poetry.lock

Lines changed: 106 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ pillow = { version = ">=9.2.0", markers = "sys_platform == 'linux'" }
123123
playwright = "^1.48.0"
124124
# For serializing local function closures in Playwright tests
125125
dill = "^0.3.8"
126+
coverage = "^7.11.0"
126127

127128
[build-system]
128129
requires = ["poetry-core>=1.0.0"]

src/crystal/main.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,14 @@ def on_atexit() -> None:
323323
# Main thread did not set an exit code. Assume a bug.
324324
exit_code = 1 # default error exit code
325325

326-
# Exit process immediately, without bothering to run garbage collection
327-
# or other cleanup processes that can take a long time
328-
os._exit(exit_code)
326+
from crystal.util.xos import is_coverage
327+
if is_coverage():
328+
# Exit process normally, so that coverage results are written to disk
329+
pass
330+
else:
331+
# Exit process immediately, without bothering to run garbage collection
332+
# or other cleanup processes that can take a long time
333+
os._exit(exit_code)
329334
atexit.register(on_atexit)
330335

331336
# Set headless mode, before anybody tries to call fg_call_later
@@ -564,6 +569,8 @@ def _on_end_session(self, event: wx.CloseEvent) -> None:
564569
# before starting bg_task() on background thread
565570
from crystal.tests.index import run_tests
566571
from crystal.util.bulkheads import capture_crashes_to_stderr
572+
from crystal.util.xos import is_coverage
573+
from crystal.util.xthreading import fg_call_and_wait
567574

568575
# NOTE: Any unhandled exception will probably call os._exit(1)
569576
# before reaching this decorator.
@@ -579,7 +586,16 @@ def bg_task():
579586
is_ok = run_tests(parsed_args.test)
580587
finally:
581588
exit_code = 0 if is_ok else 1
582-
os._exit(exit_code)
589+
if is_coverage():
590+
# Exit app normally,
591+
# so that coverage results are written to disk
592+
def close_all_windows() -> None:
593+
for w in list(wx.GetTopLevelWindows()):
594+
w.Close()
595+
fg_call_and_wait(close_all_windows)
596+
else:
597+
# Exit app immediately
598+
os._exit(exit_code)
583599
bg_call_later(bg_task)
584600

585601
# 1. Run main loop

src/crystal/tests/index.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from crystal.tests.util.subtests import SubtestFailed
1919
from crystal.util.test_mode import tests_are_running
2020
from crystal.util.xcollections.dedup import dedup_list
21-
from crystal.util.xos import is_windows
21+
from crystal.util.xos import is_coverage, is_windows
2222
from crystal.util.xthreading import bg_affinity, fg_call_and_wait, has_foreground_thread, is_foreground_thread
2323
from crystal.util.xtime import sleep_profiled
2424
from crystal.util.xtraceback import _CRYSTAL_PACKAGE_PARENT_DIRPATH
@@ -235,6 +235,10 @@ def _run_tests(test_names: list[str]) -> bool:
235235
test_func_id = (test_func.__module__, test_func.__name__) # type: _TestFuncId
236236
test_name = f'{test_func_id[0]}.{test_func_id[1]}'
237237

238+
# Skip suites that cause segfaults under coverage
239+
if is_coverage() and 'test_bulkheads' in test_name:
240+
continue
241+
238242
# Only run test if it was requested (or if all tests are to be run)
239243
if len(test_names) > 0:
240244
if test_name not in test_names and test_func.__module__ not in test_names:

src/crystal/util/xos.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ def is_asan() -> bool:
9191
return os.environ.get('CRYSTAL_ADDRESS_SANITIZER') == 'True'
9292

9393

94+
def is_coverage() -> bool:
95+
return os.environ.get('COVERAGE_RUN', None) is not None
96+
97+
9498
# === Misc ===
9599

96100
def preferences_are_called_settings_in_this_os() -> bool:

0 commit comments

Comments
 (0)