-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
2384 lines (2181 loc) · 122 KB
/
Copy pathMakefile
File metadata and controls
2384 lines (2181 loc) · 122 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# BenchBox Makefile
# This makefile provides commands for building, testing and development
PR_FANOUT_JOBS ?= 4
PR_REVIEW_BASE ?= develop
PR_REVIEW_PR_LIMIT ?= 1000
PR_REVIEW_MAX_COMMENTS ?= 0
PR_REVIEW_EXECUTOR_SANDBOX ?= workspace-write
PR_REVIEW_EXECUTOR_APPROVAL ?= never
POOL_SIZE ?= 10
WORKTREE_POOL_PARENT ?= ..
DEV_LOOP_METRICS_DAYS ?= 30
DEV_LOOP_METRICS_LIMIT ?= 100
AUDIT_SHA_TARGET_REF ?= origin/develop
AUDIT_SHA_REQUIRE_CURRENT ?=
# Minimum free disk space (in 1K-blocks) required on the pool parent
# directory before `make worktree-claim` will allocate a slot. Default
# 5 GB. Override to 0 to bypass the check (e.g. during low-space CI).
POOL_MIN_FREE_KB ?= 5000000
JOINORDER_BUILD_DIR ?= $(HOME)/Developer/benchmark_runs/joinorder/build/joinorder-imdb-2013-v1
JOINORDER_POSTGRES_DB ?= imdb
JOINORDER_POSTGRES_USER ?= postgres
JOINORDER_QUERIES ?= _project/joinorder/build-inputs/queries
JOINORDER_REFERENCE ?= _project/joinorder/reference_cardinalities.json
# Age threshold (in seconds) before a `.benchbox/claim_in_progress` marker
# is treated as evidence of an aborted claim by `worktree-pool-check`.
# Fresh markers indicate an in-flight `worktree-claim` and must not be
# reported as aborted: `worktree-pool-check` is documented as safe for
# periodic / cron use, and `worktree-claim` writes the marker at the
# start of a normal claim and only removes it at the end. Default 600s
# (10 min); concurrent claim runs typically finish in seconds.
POOL_CLAIM_MARKER_STALE_SECONDS ?= 600
# Shell command snippet that resolves the main clone's directory name
# (e.g. "BenchBox"). Pool worktree paths derive from it as
# $(WORKTREE_POOL_PARENT)/$(POOL_REPO_CMD).pool-NN. Inlined as a Make
# variable so the four pool-management targets share a single source of
# truth instead of repeating the four-deep nested expansion.
POOL_REPO_CMD = basename "$$(dirname "$$(realpath "$$(git rev-parse --git-common-dir)")")"
.PHONY: test test-unit test-integration test-tpch test-all test-fast test-unlock test-medium test-slow test-stress test-pytest clean lint lint-markers lint-imports lint-explorer-tokens lint-site-theme-tokens artifact-hygiene audit-sha-check agent-write-preflight install develop coverage coverage-fast coverage-all coverage-html coverage-report coverage-check test-duckdb test-sqlite test-read-primitives test-benchmarks test-ci typecheck validate-imports catalog-schema-check format dependency-check docs-build docs-serve docs-clean docs-linkcheck docs-validate docs-check docs-images test-pyspark ci-lint ci-test ci-docs ci-local security-audit spellcheck docstring-coverage test-package test-integration-smoke test-correctness-gate plan-capture-gate correctness-gate-digests-regen test-local-matrix joinorder-verify-reference-results complexity-check complexity-report duplicate-check duplicate-check-verbose duplicate-check-json skill-sync skill-sync-check skill-sync-verify skill-sync-lock-audit mutation-test tpchavoc-equivalence-report tpchavoc-equivalence-report-postgres tpchavoc-equivalence-report-datafusion tpchavoc-equivalence-report-clickhouse tpchavoc-dataframe-equivalence-report ssb-cross-surface-equivalence-report amplab-cross-surface-equivalence-report coffeeshop-cross-surface-equivalence-report clickbench-cross-surface-equivalence-report joinorder-synthetic-cross-surface-equivalence-report h2odb-cross-surface-equivalence-report read-primitives-cross-surface-equivalence-report cross-surface-update-baseline oracle-coverage-map oracle-coverage-map-check cross-surface-applicability-report compile-tpcds-binaries parity-fixtures parity-check compat-docs compat-docs-check pr-preflight pr-preflight-fast-tests pr-content-guard pr-open pr-status pr-review-followups pr-review-followups-list dev-loop-metrics shrink-rollup worktree-pool-init worktree-pool-status worktree-pool-check worktree-claim worktree-claim-locked worktree-claim-attempt worktree-release worktree-pool-reset worktree-pool-sweep-stale worktree-pool-disk-clean worktree-list worktree-prune todo-reindex
# Primary test commands using pytest marker system
test: test-fast
@echo "Default test run completed. Use 'make help' to see all test options."
test-all:
@echo "Running non-resource-heavy tests in parallel..."
uv run -- python -m pytest -m "not (slow or stress or resource_heavy or live_integration)"
@echo "Running slow and resource-heavy tests serially..."
uv run -- python -m pytest -m "(slow or resource_heavy) and not (stress or live_integration)" -n 0
test-unit:
uv run -- python -m pytest -m "unit" --tb=short
test-integration:
uv run -- python -m pytest -m "integration and not live_integration and not stress" --tb=short
test-tpch:
uv run -- python -m pytest -m "tpch" --tb=short
# Curated lightweight smoke lane
test-quick:
uv run -- python -m pytest -m "fast and not (slow or stress or resource_heavy or live_integration)" --tb=short --maxfail=5
# Verbose test output for all tests
test-verbose:
uv run -- python -m pytest -v
# Enhanced pytest commands using comprehensive marker system
test-pytest:
uv run -- python -m pytest -m "not stress"
# Speed-based testing
test-fast:
uv run -- python -m pytest -m "fast and not (slow or stress or resource_heavy or live_integration)" --tb=short
test-unlock:
@LOCK_DIR="$${BENCHBOX_TEST_LOCK_DIR:-$$HOME/.benchbox}"; \
case "$$LOCK_DIR" in \
"~") LOCK_DIR="$$HOME" ;; \
"~/"*) LOCK_DIR="$$HOME/$${LOCK_DIR#\~/}" ;; \
esac; \
LOCK_PATH="$$LOCK_DIR/test.lock"; \
echo "Removing stale BenchBox test lock at $$LOCK_PATH..."; \
rm -f "$$LOCK_PATH"
@echo "Lock cleared."
test-medium:
uv run -- python -m pytest -m "medium and not (slow or stress or resource_heavy or live_integration)" --tb=short --timeout=60 -n 5
test-slow:
uv run -- python -m pytest -m "slow and not (stress or live_integration)" -n 0 --tb=short -v
test-stress:
uv run -- python -m pytest -m "stress" -n 0 --tb=short -v
# Development cycle testing using the curated fast unit subset
test-dev:
uv run -- python -m pytest -m "fast and unit and not (slow or stress or resource_heavy or live_integration)" --tb=short --maxfail=3
# Smoke tests (alias for test-quick)
test-smoke: test-quick
# Bounded real-result correctness gate for develop PRs: one local benchmark case
# (DuckDB x TPC-H, SF=1, reference qgen seed) through generate/load/execute with
# phase, cardinality, EXACT stored answer-set row-count checks, AND stored VALUE
# digests. The value oracle (BENCHBOX_EMIT_RESULT_DIGEST=1) makes the runner emit an
# order-normalized digest of each stream-0 query's full result set, which the gate
# asserts against a stored reference digest -- so a wrong-but-same-cardinality answer
# (e.g. a perturbed Q1 aggregate, a swapped column) is caught, not just a wrong row
# count. The proven guarantee is value+cardinality at SF=1/pinned-seed only; values
# above SF=1 are UNGUARDED (no stored answers exist there). The query subset is the
# 18 TPC-H queries whose answer-set cardinalities are stable across dbgen builds;
# Q11/Q16/Q18/Q20 are excluded because their HAVING/threshold boundaries make the
# stored row count vary with the generated data (see tests/README.md).
#
# WHAT THE VALUE ORACLE IS (and is NOT): the stored reference digests are a frozen
# benchbox-on-DuckDB snapshot, so the value check is a REGRESSION SNAPSHOT vs a
# DuckDB-pinned baseline (it detects CHANGE from the frozen answer), NOT an
# independent correctness oracle -- a conceptual value bug present at freeze time is
# enshrined, not caught. Regenerate the reference with `make correctness-gate-digests-regen`.
#
# The JUnit-report guard fails the target if the selected node SKIPs instead of
# running: pytest exits 0 on a selected skip, which would otherwise pass the gate
# without executing the benchmark. The report goes to a private temp file (not the
# repo-root test-results.xml that pytest-ci.ini also writes) and is removed after
# parsing; both the pytest status and the guard status are propagated.
#
# CORRECTNESS_GATE_QUERY_IDS is the ONE source of the gated query-id set, shared by
# both this gate and `correctness-gate-digests-regen` (the reference is regenerated
# from the same query set it is asserted against -- never re-hardcode the list).
CORRECTNESS_GATE_QUERY_IDS := 1,2,3,4,5,6,7,8,9,10,12,13,14,15,17,19,21,22
test-correctness-gate:
@REPORT="$$(mktemp)"; \
BENCHBOX_STRICT_EXPECTED_RESULTS=1 BENCHBOX_EMIT_RESULT_DIGEST=1 BENCHBOX_CORRECTNESS_GATE_QUERY_IDS=$(CORRECTNESS_GATE_QUERY_IDS) uv run -- python -m pytest -m stress "tests/integration/test_local_platform_benchmark_matrix.py::test_local_platform_benchmark_matrix[tpch-duckdb]" -n 0 --tb=short --timeout=1200 -v --junitxml="$$REPORT"; \
PYTEST_STATUS=$$?; \
uv run -- python -c "import sys, xml.etree.ElementTree as ET; root = ET.parse(sys.argv[1]).getroot(); suites = [root] if root.tag == 'testsuite' else root.findall('testsuite'); tests = sum(int(s.get('tests') or 0) for s in suites); skipped = sum(int(s.get('skipped') or 0) for s in suites); errors = sum(int(s.get('errors') or 0) for s in suites); failures = sum(int(s.get('failures') or 0) for s in suites); print('correctness gate guard: ran=%d skipped=%d failures=%d errors=%d' % (tests, skipped, failures, errors)); sys.exit(0 if (tests == 1 and skipped == 0 and errors == 0 and failures == 0) else 'correctness gate: expected exactly 1 node to run with 0 skipped/failed/errored; a selected skip means duckdb/tpch dropped from the stable matrix or the node-id drifted')" "$$REPORT"; \
GUARD_STATUS=$$?; \
rm -f "$$REPORT"; \
test $$PYTEST_STATUS -eq 0 && test $$GUARD_STATUS -eq 0
plan-capture-gate:
uv run -- python -m pytest tests/integration/test_plan_capture_gate.py -q -n 0 --tb=short
# Regenerate the bounded correctness-gate VALUE-digest reference
# (benchbox/core/expected_results/reference_digests/tpch_value_digests_sf1.json)
# from a live gate run. Replaces the historical hand-copy: this runs the SAME gate
# configuration (DuckDB x TPC-H, SF=1, reference qgen seed, the shared
# CORRECTNESS_GATE_QUERY_IDS set) with BENCHBOX_EMIT_RESULT_DIGEST=1 and WRITES the
# stream-0 per-query digests + provenance back to the JSON. Idempotent on a clean
# tree: `make correctness-gate-digests-regen && git diff --exit-code <json>` is clean.
# The reference is a regression SNAPSHOT vs a DuckDB-pinned baseline, tied to the
# DuckDB build in uv.lock (stamped into the file's provenance). Run this whenever the
# digest normalization changes or DuckDB is bumped, in the SAME change, or
# `make test-correctness-gate` goes RED on a correct tree.
correctness-gate-digests-regen:
BENCHBOX_CORRECTNESS_GATE_QUERY_IDS=$(CORRECTNESS_GATE_QUERY_IDS) uv run -- python _project/scripts/regenerate_correctness_gate_digests.py
# Gate: compare every TPC-Havoc SQL variant to canonical TPC-H on real SF=0.1
# DuckDB data; exits non-zero on any divergence beyond KNOWN_DIVERGENCES
# (see benchbox/core/tpchavoc/equivalence.py).
tpchavoc-equivalence-report:
uv run -- python -m benchbox.core.tpchavoc.equivalence
# Second-engine SAMPLE: compare every Postgres-executable TPC-Havoc SQL variant
# to canonical TPC-H on the SAME PostgreSQL instance (both translated to the
# postgres dialect). Excludes POSTGRES_TPCHAVOC_SKIPS (un-executable variants).
# Needs a reachable Postgres (defaults match the postgres-integration CI service
# container; override via PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE). Skips
# cleanly (exit 0) if no server is reachable. The DuckDB gate above stays the
# hard blocker; this is a non-blocking sample (see equivalence.py).
tpchavoc-equivalence-report-postgres:
uv run -- python -m benchbox.core.tpchavoc.equivalence --engine postgres
# Third-engine SAMPLE: compare every DataFusion-executable TPC-Havoc SQL variant
# to canonical TPC-H on the SAME in-process DataFusion session (both translated
# to the datafusion dialect, which the seam normalizes to postgres). Excludes
# DATAFUSION_TPCHAVOC_SKIPS (variants DataFusion cannot plan). DataFusion is
# in-process (no service container); skips cleanly (exit 0) only if DataFusion is
# not installed. The DuckDB gate above stays the hard blocker; this is a
# non-blocking sample with a DIFFERENT gap profile from Postgres (see equivalence.py).
tpchavoc-equivalence-report-datafusion:
uv run -- python -m benchbox.core.tpchavoc.equivalence --engine datafusion
# Fourth-engine SAMPLE: compare every ClickHouse-executable TPC-Havoc SQL variant
# to canonical TPC-H on the SAME in-process ClickHouse instance (chDB /
# clickhouse-local), both translated to the NATIVE clickhouse dialect (NOT
# normalized to postgres - this is the first sampled engine on a non-Postgres
# dialect). Excludes CLICKHOUSE_TPCHAVOC_SKIPS (variants ClickHouse cannot execute)
# and runs with SQL-standard NULL semantics (join_use_nulls=1). Unlike the other
# three engines this is NOT systematic-zero: CLICKHOUSE_KNOWN_DIVERGENCES records
# the irreducible engine-semantic result differences (Decimal-vs-Float division,
# SUM of an empty group = 0, partial correlated-subquery decorrelation). ClickHouse
# is in-process (no service container); skips cleanly (exit 0) only if chDB is not
# installed. The DuckDB gate above stays the hard blocker; this is a non-blocking
# sample (see equivalence.py).
tpchavoc-equivalence-report-clickhouse:
uv run -- python -m benchbox.core.tpchavoc.equivalence --engine clickhouse
# Gate: compare every TPC-Havoc DataFrame variant (both backends) to canonical
# TPC-H on real SF=0.1 DuckDB-backed data; exits non-zero on any divergence
# beyond KNOWN_DIVERGENCES (see benchbox/core/tpchavoc/dataframe_equivalence.py).
tpchavoc-dataframe-equivalence-report:
uv run -- python -m benchbox.core.tpchavoc.dataframe_equivalence
# Gate: compare a dual-surface benchmark's DataFrame surface (both backends) to
# its OWN SQL surface on real SF=0.1 DuckDB-backed data; exits non-zero on any
# divergence beyond the benchmark's baseline. SQL is the reference for its own
# DataFrame surface, so no hand-curated answer key is needed (see
# benchbox/core/equivalence/cross_surface.py). Currently gates ssb, amplab, and coffeeshop.
ssb-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark ssb
amplab-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark amplab
coffeeshop-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark coffeeshop
# Enforced gate: clickbench SQL<->DataFrame equivalence on a bounded DuckDB cell.
# In GATES (STAGED_GATES is empty) and run in the blocking correctness-gate (pr.yml);
# exits non-zero on any unclassified divergence. Q18's order-less LIMIT is the one
# classified exception (see _project/analysis/clickbench-cross-surface-divergences.md).
clickbench-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark clickbench
# Enforced gate: joinorder_synthetic SQL<->DataFrame equivalence on a bounded DuckDB
# cell. In GATES (STAGED_GATES is empty) and run in the blocking correctness-gate
# (pr.yml); exits non-zero on any unclassified divergence (see
# _project/analysis/joinorder-synthetic-cross-surface-divergences.md).
joinorder-synthetic-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark joinorder_synthetic
# Enforced gate: H2O-DB DataFrame surface vs its own SQL surface on a bounded
# ~100k-row DuckDB cell (SF=0.01: its generator base is the 10M-row small tier, so
# the shared SF=0.1 would emit ~1M rows). Q9's PERCENTILE_CONT carries one
# classified DECIMAL(8,2)-scale exception (see cross_surface.py); every other cell
# must match. Exits non-zero on any unclassified divergence.
h2odb-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark h2odb
# Enforced gate: Read Primitives DataFrame surface vs its DuckDB-dialect SQL
# surface on a bounded ~300k-row TPC-H cell (SF=0.05). 148 gateable queries (the 4
# fulltext/json ids DuckDB cannot transpile are excluded). Every compared cell
# matches; the classified cells are irreducible engine differences (HLL/T-Digest
# approximation, DECIMAL-scale percentile/ROUND, ARG_MIN ties, JSON text, Polars
# Map-dtype gap) plus legitimately-empty selective/no-JSON filters. Exits non-zero
# on any unclassified divergence.
read-primitives-cross-surface-equivalence-report:
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark read_primitives
# Maintenance writer (#903 follow-up): drop known-divergence baseline entries that
# no longer reproduce for ONE gate, in a reviewed change. Explicit/operator-driven -
# the blocking gate run never prunes; only writes when the run is otherwise fully
# clean, and is idempotent on a second run. Usage:
# make cross-surface-update-baseline BENCHMARK=h2odb
cross-surface-update-baseline:
@test -n "$(BENCHMARK)" || { echo "Usage: make cross-surface-update-baseline BENCHMARK=<ssb|amplab|coffeeshop|clickbench|joinorder_synthetic|h2odb|read_primitives>"; exit 1; }
uv run -- python -m benchbox.core.equivalence.cross_surface --benchmark $(BENCHMARK) --update-baseline
# Regenerate the benchmark correctness-oracle coverage map (which oracle, if any,
# guards each shipped benchmark). Derived from the registry + provider/gate
# registries; commit the refreshed _project/analysis/ artifacts.
oracle-coverage-map:
uv run -- python _project/scripts/generate_oracle_coverage_map.py
# Advisory CI guard: fail if the checked-in coverage map is stale (e.g. a new
# benchmark was added without regenerating, hiding an UNGUARDED surface).
oracle-coverage-map-check:
uv run -- python _project/scripts/generate_oracle_coverage_map.py --check
# Cross-surface applicability drill-down (report mode): which dual-surface
# unguarded benchmarks actually ship comparable DataFrame queries (cross-surface
# gateable) vs which only support DataFrame loading (need a w2 fallback oracle).
cross-surface-applicability-report:
uv run -- python _project/scripts/cross_surface_applicability_sweep.py
# Real benchmark matrix across local SQL platforms x all benchmarks (heavy, opt-in)
test-local-matrix:
uv run -- python -m pytest tests/integration/test_local_platform_benchmark_matrix.py -m stress -n 0 --tb=short -v
@echo "Tip: set BENCHBOX_SERVICE_LOCAL_MATRIX=1 to include Trino/Presto/Firebolt/PostgreSQL/TimescaleDB service-backed locals."
joinorder-verify-reference-results:
@[ -n "$(JOINORDER_POSTGRES_CONTAINER)" ] || { echo "JOINORDER_POSTGRES_CONTAINER is required"; exit 2; }
uv run -- python _project/scripts/build_joinorder_data.py verify-reference-results \
--work-dir "$(JOINORDER_BUILD_DIR)" \
--container-name "$(JOINORDER_POSTGRES_CONTAINER)" \
--database "$(JOINORDER_POSTGRES_DB)" \
--user "$(JOINORDER_POSTGRES_USER)" \
--queries "$(JOINORDER_QUERIES)" \
--reference "$(JOINORDER_REFERENCE)"
# Database-specific testing
test-duckdb:
uv run -- python -m pytest -m "duckdb" --tb=short
test-sqlite:
uv run -- python -m pytest -m "sqlite" --tb=short
test-pyspark:
./scripts/run_pyspark_tests.sh
# Benchmark-specific testing
test-read-primitives:
uv run -- python -m pytest -m "primitives" --tb=short
test-benchmarks:
uv run -- python -m pytest -m "tpch or tpcds or ssb or amplab or clickbench or h2odb or merge" --tb=short
test-tpcds:
uv run -- python -m pytest -m "tpcds" --tb=short
# Feature-specific testing
test-olap:
uv run -- python -m pytest -m "olap" --tb=short
test-window:
uv run -- python -m pytest -m "window_functions" --tb=short
# CI/CD testing
test-ci:
uv run -- python -m pytest -c pytest-ci.ini -m "not (slow or flaky or local_only)" --cov=benchbox --cov-report=term-missing:skip-covered --cov-report=xml:coverage.xml
# Fast CI feedback (excludes cloud platform tests for speed)
test-no-cloud:
uv run -- python -m pytest -m "not (slow or cloud_import)" --ignore=tests/unit/platforms/databricks --ignore=tests/unit/platforms/snowflake --ignore=tests/unit/platforms/bigquery --ignore=tests/unit/platforms/redshift --tb=short
# Complete test suite (nightly/full validation)
test-full:
uv run -- python -m pytest -m "not stress" --tb=short -v
# Parallel testing
test-parallel:
uv run -- python -m pytest -n auto --tb=short
test-parallel-fast:
uv run -- python -m pytest -n auto -m "fast" --tb=short
# Live integration tests (require cloud credentials)
test-live:
@echo "Running live integration tests (requires cloud credentials)"
@echo "See .env.example for credential setup"
uv run -- python -m pytest -m "live_integration" --tb=short -v
test-live-databricks:
@echo "Running Databricks live tests (requires DATABRICKS_TOKEN)"
uv run -- python -m pytest -m "live_databricks" --tb=short -v
test-live-snowflake:
@echo "Running Snowflake live tests (requires SNOWFLAKE_PASSWORD)"
uv run -- python -m pytest -m "live_snowflake" --tb=short -v
test-live-bigquery:
@echo "Running BigQuery live tests (requires BIGQUERY_PROJECT)"
uv run -- python -m pytest -m "live_bigquery" --tb=short -v
test-live-all:
@echo "Running all live integration tests (requires credentials for all platforms)"
uv run -- python -m pytest -m "live_integration" --tb=short -v
test-live-redshift:
@echo "Running Redshift live tests (requires REDSHIFT_HOST)"
uv run -- python -m pytest -m "live_redshift" --tb=short -v
test-live-athena:
@echo "Running Athena live tests (requires ATHENA_REGION)"
uv run -- python -m pytest -m "live_athena" --tb=short -v
test-live-firebolt:
@echo "Running Firebolt live tests (requires FIREBOLT_CLIENT_ID)"
uv run -- python -m pytest -m "live_firebolt" --tb=short -v
test-live-firebolt-core:
@echo "Running Firebolt Core live tests (requires Docker - make test-docker-up-firebolt)"
uv run -- python -m pytest -m "live_firebolt_core" --tb=short -v -n 0
test-live-starburst:
@echo "Running Starburst Galaxy live tests (requires STARBURST_HOST)"
uv run -- python -m pytest -m "live_starburst" --tb=short -v
test-live-motherduck:
@echo "Running MotherDuck live tests (requires MOTHERDUCK_TOKEN)"
uv run -- python -m pytest -m "live_motherduck" --tb=short -v
test-live-pg-duckdb:
@echo "Running pg_duckdb live tests (requires Docker PostgreSQL with pg_duckdb)"
uv run -- python -m pytest -m "live_pg_duckdb" --tb=short -v
test-live-pg-mooncake:
@echo "Running pg_mooncake live tests (requires Docker PostgreSQL with pg_mooncake)"
uv run -- python -m pytest -m "live_pg_mooncake" --tb=short -v
test-live-cedardb:
@echo "Running CedarDB live tests (requires Docker CedarDB - make test-docker-up-cedardb)"
uv run -- python -m pytest -m "live_cedardb" --tb=short -v -n 0
test-docker-up-pg-extensions:
@echo "Starting pg_duckdb (port 5432) and pg_mooncake (port 5433)..."
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
project_file="$$state_dir/pg-extensions.project"; \
mkdir -p "$$state_dir"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
project_name="benchbox-pg-extensions-test-$$(date +%s)-$$RANDOM"; \
fi; \
status=1; \
cleanup() { \
if [ $$status -ne 0 ]; then \
docker compose -p "$$project_name" -f docker/postgres-extensions/docker-compose.yml down -v >/dev/null 2>&1 || true; \
rm -f "$$project_file"; \
fi; \
}; \
trap cleanup EXIT INT TERM; \
docker compose -p "$$project_name" -f docker/postgres-extensions/docker-compose.yml up -d --wait; \
printf '%s\n' "$$project_name" > "$$project_file"; \
status=0
test-docker-down-pg-extensions:
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
project_file="$$state_dir/pg-extensions.project"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
echo "No tracked Docker test stack for pg-extensions"; \
exit 0; \
fi; \
docker compose -p "$$project_name" -f docker/postgres-extensions/docker-compose.yml down -v; \
rm -f "$$project_file"
test-docker-pg-extensions:
@echo "Running pg_duckdb and pg_mooncake Docker integration tests"
@set -e; \
project_name="benchbox-pg-extensions-test-$$(date +%s)-$$RANDOM"; \
cleanup() { docker compose -p "$$project_name" -f docker/postgres-extensions/docker-compose.yml down -v || true; }; \
trap cleanup EXIT INT TERM; \
docker compose -p "$$project_name" -f docker/postgres-extensions/docker-compose.yml up -d --wait; \
uv run -- python -m pytest -m "live_pg_duckdb or live_pg_mooncake" --tb=short -v -n 0
# Docker-based integration tests (requires Docker and docker compose)
DOCKER_PLATFORMS := clickhouse trino presto postgresql starrocks doris databend influxdb cedardb firebolt questdb singlestore
DOCKER_TEST_STATE_DIR ?= /tmp/benchbox-docker-projects
# Container engine for local test-docker-* compose stacks: `docker` (default,
# and the ONLY engine CI uses) or `mocker` (Docker-compatible CLI over Apple
# `container`; Apple-silicon/macOS-26 LOCAL DEV ONLY, MUST NOT run in CI). The
# docker/*/docker-compose.yml files stay unmodified; only the driver swaps.
# See AGENTS.md "Mocker as a local test-docker engine".
CONTAINER_ENGINE ?= docker
COMPOSE := $(CONTAINER_ENGINE) compose
# `compose down -v` extended to also remove leaked named volumes on a SUCCESSFUL
# down. mocker 0.5.4's `compose down -v` removes containers but LEAKS named
# volumes (a stale-data risk across runs); this prunes any volume with the
# project prefix afterward. A no-op beyond `down -v` on docker (which already
# removes them). Scoped to the project so it never touches unrelated volumes.
# Grepping the project-prefixed name directly avoids assuming `volume ls` emits a
# header or a fixed column layout. $(1)=project $(2)=compose file.
define compose_down_fresh
$(COMPOSE) -p "$(1)" -f "$(2)" down -v; if [ "$(CONTAINER_ENGINE)" = "mocker" ]; then mocker volume ls 2>/dev/null | grep -oE "$(1)[-_][A-Za-z0-9._-]+" | while read -r _v; do mocker volume rm "$$_v" >/dev/null 2>&1 || true; done; fi
endef
test-docker-up-%:
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
project_file="$$state_dir/$*.project"; \
mkdir -p "$$state_dir"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
project_name="benchbox-$*-test-$$(date +%s)-$$RANDOM"; \
fi; \
status=1; \
cleanup() { \
if [ $$status -ne 0 ]; then \
{ $(call compose_down_fresh,$$project_name,docker/$*/docker-compose.yml) ; } >/dev/null 2>&1 || true; \
rm -f "$$project_file"; \
fi; \
}; \
trap cleanup EXIT INT TERM; \
$(COMPOSE) -p "$$project_name" -f docker/$*/docker-compose.yml up -d --wait; \
printf '%s\n' "$$project_name" > "$$project_file"; \
status=0
test-docker-down-%:
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
project_file="$$state_dir/$*.project"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
echo "No tracked Docker test stack for $*"; \
exit 0; \
fi; \
$(call compose_down_fresh,$$project_name,docker/$*/docker-compose.yml); \
rm -f "$$project_file"
# Explicit override: generic test-docker-% expands to -m "live_firebolt" (cloud tests).
# Firebolt Core Docker tests use the separate live_firebolt_core marker.
test-docker-firebolt:
@echo "Running Firebolt Core Docker integration tests"
@set -e; \
project_name="benchbox-firebolt-test-$$(date +%s)-$$RANDOM"; \
cleanup() { { $(call compose_down_fresh,$$project_name,docker/firebolt/docker-compose.yml) ; } || true; }; \
trap cleanup EXIT INT TERM; \
$(COMPOSE) -p "$$project_name" -f docker/firebolt/docker-compose.yml up -d --wait; \
uv run -- python -m pytest -m "live_firebolt_core" --tb=short -v -n 0
test-docker-%:
@echo "Running $* Docker integration tests"
@set -e; \
project_name="benchbox-$*-test-$$(date +%s)-$$RANDOM"; \
cleanup() { { $(call compose_down_fresh,$$project_name,docker/$*/docker-compose.yml) ; } || true; }; \
trap cleanup EXIT INT TERM; \
$(COMPOSE) -p "$$project_name" -f docker/$*/docker-compose.yml up -d --wait; \
uv run -- python -m pytest -m "live_$*" --tb=short -v -n 0
test-docker-up-all:
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
mkdir -p "$$state_dir"; \
run_id="$$(date +%s)-$$RANDOM"; \
status=1; \
cleanup() { \
if [ $$status -ne 0 ]; then \
echo "Cleaning up partially started Docker services..."; \
for p in $(DOCKER_PLATFORMS); do \
project_file="$$state_dir/$$p.project"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -n "$$project_name" ]; then \
{ $(call compose_down_fresh,$$project_name,docker/$$p/docker-compose.yml) ; } >/dev/null 2>&1 || true; \
rm -f "$$project_file"; \
fi; \
done; \
fi; \
}; \
trap cleanup EXIT INT TERM; \
for p in $(DOCKER_PLATFORMS); do \
project_file="$$state_dir/$$p.project"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
project_name="benchbox-$$p-test-$$run_id"; \
printf '%s\n' "$$project_name" > "$$project_file"; \
fi; \
echo "Starting $$p..."; \
$(COMPOSE) -p "$$project_name" -f docker/$$p/docker-compose.yml up -d --wait; \
done; \
status=0
test-docker-down-all:
@set -e; \
state_dir="$(DOCKER_TEST_STATE_DIR)"; \
for p in $(DOCKER_PLATFORMS); do \
project_file="$$state_dir/$$p.project"; \
project_name="$$(cat "$$project_file" 2>/dev/null || true)"; \
if [ -z "$$project_name" ]; then \
echo "Skipping $$p (no tracked Docker test stack)"; \
continue; \
fi; \
echo "Stopping $$p..."; \
$(call compose_down_fresh,$$project_name,docker/$$p/docker-compose.yml); \
rm -f "$$project_file"; \
done
test-docker-all:
@echo "Running all Docker integration tests (requires Docker)"
@for p in $(DOCKER_PLATFORMS); do \
echo "=== Testing $$p ==="; \
$(MAKE) test-docker-$$p || exit 1; \
done
# Compose-lifecycle parity acceptance test: asserts up --wait health-gating,
# published-port reachability, and the down -v fresh-state guarantee (no leaked
# container/volume) for the selected engine. Same asserts on docker and mocker so
# parity is measured. Usage:
# make test-docker-parity # docker (default)
# make test-docker-parity CONTAINER_ENGINE=mocker # Apple container backend
# make test-docker-parity PARITY_PLATFORMS="questdb postgresql doris"
PARITY_PLATFORMS ?= questdb postgresql
.PHONY: test-docker-parity
test-docker-parity:
@bash _project/scripts/mocker_compose_parity.sh $(CONTAINER_ENGINE) $(PARITY_PLATFORMS)
# Coverage commands using pytest
coverage-fast:
uv run -- python -m pytest -c pytest-ci.ini -m "fast and not (slow or stress or resource_heavy or live_integration or cloud_import)" --cov=benchbox --cov-report=term-missing:skip-covered
coverage-all:
uv run -- python -m pytest -c pytest-ci.ini --cov=benchbox --cov-branch --cov-report=term-missing:skip-covered --cov-report=html:htmlcov --cov-report=xml:coverage.xml
coverage: coverage-all
coverage-html:
uv run -- python -m pytest -c pytest-ci.ini --cov=benchbox --cov-report=html:htmlcov
coverage-report:
uv run -- python -m pytest -c pytest-ci.ini --cov=benchbox --cov-report=xml:coverage.xml --cov-report=term-missing
# Cyclomatic complexity checks
complexity-check:
uv run -- python scripts/check_complexity.py
complexity-report:
uv run -- python scripts/check_complexity.py --no-fail --top 30
# Install and development
install:
uv sync
develop:
uv sync --group dev
# Clean build artifacts
clean:
rm -rf build/
rm -rf dist/
rm -rf *.egg-info/
rm -rf __pycache__/
rm -rf .pytest_cache/
rm -rf .coverage
rm -rf htmlcov/
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
find . -name '*.pyo' -delete
find . -name '.DS_Store' -delete
# Linting (ruff + explorer token scan)
lint:
uv run ruff check .
$(MAKE) lint-explorer-tokens
$(MAKE) lint-site-theme-tokens
# Dependency audit - checks that every declared dep has an import site or is allowlisted.
# Fails if an unused dep is introduced. See _project/scripts/dependency_audit/.
audit-deps:
uv run -- python _project/scripts/dependency_audit/check_deps.py
# Regenerate the raw declared-dependency inventory from pyproject.toml.
audit-raw:
uv run -- python _project/scripts/dependency_audit/parse_deps.py
# Verify the committed raw inventory matches pyproject.toml. Fails on drift.
audit-raw-check:
uv run -- python _project/scripts/dependency_audit/parse_deps.py --check
# Validate that an audit report records the develop SHA it describes.
audit-sha-check:
@test -n "$(FILE)" || { echo "Usage: make audit-sha-check FILE=<audit.md>"; exit 1; }
uv run --no-project -- python _project/scripts/audit_sha_check.py \
--target-ref "$(AUDIT_SHA_TARGET_REF)" \
$(if $(AUDIT_SHA_REQUIRE_CURRENT),--require-current $(AUDIT_SHA_REQUIRE_CURRENT),) \
"$(FILE)"
# Validate marker registration and the explicit marker-strategy policy.
lint-markers:
uv run -- python -m pytest --collect-only -q -p no:warnings
uv run -- python -m pytest tests/unit/test_marker_strategy.py -q
# Enforce the utils < core < platforms < cli import-layering convention and
# the experimental/mcp isolation contracts (see .importlinter). Fails on any
# newly introduced layering violation that isn't already an explicitly
# named, justified exception in .importlinter's ignore_imports lists.
lint-imports:
uv run -- lint-imports
# Token-scan gate for the Results Explorer retheme: fails when raw Tailwind
# palette literals (text-/bg-/border-/...-{slate|gray|...}-{50..950}) appear
# under results-explorer/src outside an explicit allowlist marker. Stdlib-only
# so no dependency sync is required before the gate runs.
lint-explorer-tokens:
python3 _project/scripts/scan_explorer_tokens.py
lint-site-theme-tokens:
python3 _project/scripts/scan_explorer_tokens.py landing/shared landing/index.html landing/style.css landing/prompts/index.html landing/prompts/prompts.css docs/_templates/page.html docs/_static/custom.css results-explorer/index.html results-explorer/src/components/Layout.tsx
# Stale-theme gate: fails when active Results Explorer source/tests or
# unsuperseded analysis files revive the retired mixed-theme contract phrases.
# Allowlist: inline `allow-stale-theme: <reason>` marker, or a supersession
# header (Superseded / supersedes / supersession) in `_project/analysis/*`
# files. `_project/DONE/*` is excluded by design.
lint-explorer-stale-theme:
python3 _project/scripts/scan_explorer_stale_theme.py
# Validate a built Results Explorer read model against the snapshot invariants
# (required columns, ranking/compare eligibility). Point SNAPSHOT at the built
# .duckdb; defaults to the path docs.yml's pipeline writes. CI runs the same
# script in .github/workflows/docs.yml after the explorer data build.
SNAPSHOT ?= results-explorer/public/data/results.duckdb
.PHONY: explorer-snapshot-check
explorer-snapshot-check:
uv run -- python _project/scripts/results_explorer_snapshot_invariants.py "$(SNAPSHOT)"
artifact-hygiene:
uv run -- python _project/scripts/artifact_hygiene_check.py --all-tracked
# skill-sync — materialize project-local skills from ~/.skill-sync/skills.
# Manifest is tracked (skill-sync.yaml/skill-sync.lock). The `claude` target
# (.claude/skills) is a TRACKED snapshot committed for cloud/CI parity — verify
# it with `make skill-sync-verify`. The codex/gemini/antigravity mirrors stay
# gitignored and are regenerated locally per developer. Override SKILL_SYNC to
# point at a different install (e.g. an npm-installed copy).
SKILL_SYNC ?= /Users/joe/Developer/skill-sync/dist/cli/index.js
skill-sync:
@if [ -f "$(SKILL_SYNC)" ]; then \
$(MAKE) -s agent-write-preflight; \
node "$(SKILL_SYNC)" sync; \
else \
echo "skill-sync not installed at $(SKILL_SYNC); skipping (override with SKILL_SYNC=path/to/dist/cli/index.js)"; \
fi
skill-sync-check:
@if [ -f "$(SKILL_SYNC)" ]; then \
node "$(SKILL_SYNC)" doctor; \
else \
echo "skill-sync not installed at $(SKILL_SYNC); skipping (override with SKILL_SYNC=path/to/dist/cli/index.js)"; \
fi
# skill-sync-verify — offline integrity gate for the tracked snapshot (CI/local).
# Proves .claude/skills matches skill-sync.lock + the regenerated config; exits
# non-zero on any hand-edit, stray file, or stale config. Skips when the CLI is
# absent (e.g. cloud/CI without skill-sync) — the committed snapshot is plain
# files and needs skill-sync only to be *verified*, not to be consumed.
skill-sync-verify:
@if [ -f "$(SKILL_SYNC)" ]; then \
node "$(SKILL_SYNC)" verify; \
else \
echo "skill-sync not installed at $(SKILL_SYNC); skipping verify (override with SKILL_SYNC=path/to/dist/cli/index.js)"; \
fi
# Review helper for PRs that modify skill-sync.lock while .claude/skills is gitignored.
# Usage: make skill-sync-lock-audit [BASE=origin/develop] [TODO=_project/.../item.yaml] [CHECK=1]
skill-sync-lock-audit:
uv run --project _project/scripts -- python _project/scripts/skill_sync_lock_audit.py \
--base $${BASE:-origin/develop} \
$${TODO:+--todo $$TODO} \
$${CHECK:+--check}
# Duplicate code detection (AST structural clone detection)
duplicate-check:
uv run -- python scripts/check_duplicate_code.py
duplicate-check-verbose:
uv run -- python scripts/check_duplicate_code.py --verbose --top-n 30
duplicate-check-json:
uv run -- python scripts/check_duplicate_code.py --json
mutation-test:
@echo "Running mutation tests on critical modules..."
uv run -- mutmut run
@echo "--- Mutation test results ---"
uv run -- mutmut results
##@ CI Local Equivalents
# These targets mirror GitHub Actions workflows for local validation
# CI lint check - exact match for lint.yml workflow
ci-lint:
@echo "Running CI lint checks..."
uv run ruff check .
uv run ruff format --check .
uv run ty check
$(MAKE) lint-markers
$(MAKE) lint-explorer-tokens
$(MAKE) lint-site-theme-tokens
$(MAKE) artifact-hygiene
$(MAKE) skill-sync-check
uv run -- python _project/scripts/timing_policy_check.py --strict
$(MAKE) compat-docs-check
$(MAKE) oracle-coverage-map-check
$(MAKE) audit-deps
@echo "✅ CI lint checks passed"
# CI test check - exact match for test.yml workflow (fast tests with coverage)
# Note: -p pytest_cov re-enables pytest-cov which is disabled by default in pytest.ini
# Suite-wide coverage threshold set to 70%. tests/conftest.py emits a separate
# non-failing advisory warning below 80%; 70 is the blocking CI floor.
ci-test:
@echo "Running CI test suite..."
uv run -- python -m pytest tests -m "fast and not (slow or stress or resource_heavy or live_integration)" --tb=short -p pytest_cov --cov=benchbox --cov-report=xml:coverage.xml --cov-report=term-missing --cov-fail-under=70
@echo "✅ CI test suite passed"
# CI docs build - exact match for docs.yml workflow
ci-docs:
@echo "Running CI docs checks..."
@$(MAKE) docs-validate
@cd docs && uv run sphinx-build -b html --keep-going . _build/html
@echo "✅ CI docs build passed"
# Security audit - exact match for test.yml security job
security-audit:
@echo "Running security audit..."
@if [ -n "$(PIP_AUDIT_IGNORE_VULNS)" ]; then \
IGNORE_ARGS=$$(printf '%s' "$(PIP_AUDIT_IGNORE_VULNS)" | tr ',' '\n' | sed '/^$$/d;s/^/--ignore-vuln /' | tr '\n' ' '); \
uvx pip-audit $$IGNORE_ARGS; \
else \
uvx pip-audit; \
fi
@echo "✅ Security audit passed"
# Spellcheck - exact match for docs.yml spellcheck job
spellcheck:
@echo "Running spellcheck..."
uvx codespell --ignore-words=.codespell-ignore.txt --skip="*.pyc,_build,*.json,*.lock,*.svg,*.min.js,*.min.css,_binaries,*.tpl,*.dst,*.tbl,_sources,benchmark_runs,*.dat,*.pdf,_project,_blog,htmlcov,.venv"
@echo "✅ Spellcheck passed"
# Linkcheck - exact match for docs.yml linkcheck job
ci-linkcheck:
@echo "Running documentation link check..."
@cd docs && uv run sphinx-build -b linkcheck . _build/linkcheck
@echo "Link check results:"
@cat docs/_build/linkcheck/output.txt 2>/dev/null || echo "No output file generated"
@echo "✅ Linkcheck passed"
# Docstring coverage - exact match for docs.yml docstring-coverage job
docstring-coverage:
@echo "Running docstring coverage check..."
uvx interrogate -c pyproject.toml --fail-under 90 benchbox/
@echo "✅ Docstring coverage passed"
# Package build and install test - exact match for test.yml test-package job
test-package:
@echo "Building and testing package installation..."
rm -rf dist/
uv build
uvx twine check dist/*
@echo "Testing package installation..."
@wheel_count=$$(find "$$PWD/dist" -maxdepth 1 -type f -name '*.whl' | wc -l | tr -d '[:space:]'); \
if [ "$$wheel_count" != "1" ]; then \
echo "Expected exactly one wheel, found $$wheel_count"; \
find "$$PWD/dist" -maxdepth 1 -type f -print; \
exit 1; \
fi; \
wheel=$$(find "$$PWD/dist" -maxdepth 1 -type f -name '*.whl' -print -quit); \
tmpdir=$$(mktemp -d); \
trap 'rm -rf "$$tmpdir"' EXIT; \
cd "$$tmpdir"; \
uv run --isolated --no-project --with "$$wheel" -- python -c "import benchbox; print('Package installation successful')"; \
uv run --isolated --no-project --with "$$wheel" -- benchbox --help > /dev/null
@echo "✅ Package test passed"
# Integration smoke tests - exact match for test.yml integration-smoke job
test-integration-smoke:
@echo "Running integration smoke tests..."
uv run -- python -m pytest tests/integration -m "platform_smoke or (integration and fast)" --tb=short
@echo "✅ Integration smoke tests passed"
# Run all CI checks locally - ensures CI will pass before push
ci-local:
@echo "========================================"
@echo "Running all CI checks locally..."
@echo "========================================"
@echo ""
@echo "Step 1/5: Lint checks..."
@$(MAKE) ci-lint
@echo ""
@echo "Step 2/5: Fast tests with coverage..."
@$(MAKE) ci-test
@echo ""
@echo "Step 3/5: Integration smoke tests..."
@$(MAKE) test-integration-smoke
@echo ""
@echo "Step 4/5: Documentation build..."
@$(MAKE) ci-docs
@echo ""
@echo "Step 5/5: Package build..."
@$(MAKE) test-package
@echo ""
@echo "========================================"
@echo "✅ All CI checks passed!"
@echo "========================================"
# --- Apple container Linux CI-parity sandbox (opt-in; Apple silicon + macOS 26) ---
# Reproduce the Linux pr.yml gate locally inside a `container machine`. Motivated by a
# MEASURED macOS<->Linux divergence: on identical DuckDB 1.3.2/arm64, TPC-H Q2/Q10/Q15
# value digests differ, so `make test-correctness-gate` FAILS on a correct tree on Apple
# silicon (the pinned digest references are Linux-generated). This wrapper is the only way
# to validate that soundness gate pre-PR from a Mac. Purely additive and opt-in: a no-op on
# non-Apple-silicon / non-macOS-26 hosts, and NEVER a CI or pr-open dependency. One-time
# setup + usage: AGENTS.md "Apple container Linux CI parity". Override the gate with
# `make ci-linux CI_LINUX_CMD='make ci-local'`.
CI_LINUX_MACHINE ?= benchbox-agent
CI_LINUX_CMD ?= make test-correctness-gate
.PHONY: ci-linux
ci-linux:
@if [ "$$(uname -s)" != "Darwin" ] || [ "$$(uname -m)" != "arm64" ]; then \
echo "ci-linux: skipped -- Apple silicon macOS only (host $$(uname -s)/$$(uname -m)); no-op."; \
exit 0; \
fi; \
if [ "$$(sw_vers -productVersion | cut -d. -f1)" -lt 26 ]; then \
echo "ci-linux: skipped -- needs macOS 26+ (host $$(sw_vers -productVersion)); no-op."; \
exit 0; \
fi; \
if ! command -v container >/dev/null 2>&1; then \
echo "ci-linux: Apple 'container' not installed -> 'brew install container' (see AGENTS.md)."; \
exit 1; \
fi; \
if ! container machine list 2>/dev/null | awk 'NR>1{print $$1}' | grep -Fqx "$(CI_LINUX_MACHINE)"; then \
echo "ci-linux: machine '$(CI_LINUX_MACHINE)' not found. One-time setup (see AGENTS.md):"; \
echo " container system start"; \
echo " container build --arch arm64 --tag local/benchbox-agent docker/benchbox-agent"; \
echo " container machine create local/benchbox-agent --name $(CI_LINUX_MACHINE) --home-mount rw --cpus 4 --memory 8G"; \
exit 1; \
fi; \
echo "==> ci-linux: '$(CI_LINUX_CMD)' inside container machine '$(CI_LINUX_MACHINE)'"; \
container machine run -n $(CI_LINUX_MACHINE) -- bash -lc 'cd "$(CURDIR)" && $(CI_LINUX_CMD)'
# Type checking
typecheck:
uv run ty check
# Backward-compatible alias for older local notes/scripts.
typecheck-uv: typecheck
# Import validation. Alias for the import-linter gate (lint-imports); the
# previously-referenced scripts/validate_imports.py never existed, so the bare
# target failed with file-not-found. CI uses lint-imports directly.
validate-imports: lint-imports
# Field-level schema validation for migrated YAML catalogs (see benchbox/core/catalog_schema.py).
catalog-schema-check:
uv run -- python -m benchbox.core.catalog_schema
# Dependency matrix / validation
dependency-check:
uv run -- python -m benchbox.utils.dependency_validation $(ARGS)
# Format code (ruff formatter)
format:
uv run ruff format .
##@ Documentation
# Build Sphinx documentation locally
docs-build:
@echo "Building documentation..."
@cd docs && uv run sphinx-build -b html --keep-going . _build/html
@echo "✅ Docs built: docs/_build/html/index.html"
# Build and serve documentation on http://localhost:8000
docs-serve: docs-build
@echo "Serving docs at http://localhost:8000"
@echo "Press Ctrl+C to stop"
@cd docs/_build/html && uv run -- python -m http.server 8000
# Clean documentation build artifacts
docs-clean:
@echo "Cleaning documentation build artifacts..."
@rm -rf docs/_build
@echo "✅ Documentation artifacts cleaned"
# Check for broken links in documentation
docs-linkcheck:
@echo "Checking documentation for broken links..."
@cd docs && uv run sphinx-build -b linkcheck . _build/linkcheck
@echo ""
@echo "Link check results:"
@cat docs/_build/linkcheck/output.txt || echo "No broken links found!"
# Validate example file references
docs-validate:
@echo "Validating example file references..."
@uv run -- python scripts/validate_example_references.py
@echo ""
@echo "Checking example file syntax..."
@uv run -- python scripts/check_example_syntax.py
@echo ""
@echo "Validating visualization screenshot sync..."
@uv run -- python scripts/validate_visualization_images.py
@echo ""
@echo "Checking repo-local doc relative links..."
@uv run -- python scripts/check_doc_relative_links.py
# Refresh generated visualization screenshots and sync shared docs/blog copies
docs-images:
@echo "Capturing visualization screenshots..."
@uv run -- python scripts/capture_chart_images.py
# Regenerate the /prompts/ landing route catalog include from catalog.yaml.
prompt-quickstarts-write:
@uv run -- python scripts/generate_landing_quickstarts.py --write
# Fail if landing/prompts/catalog.generated.js is stale or invalid.
# Wired into docs CI by `landing-prompts-launch-gates`.
prompt-quickstarts-check:
@uv run -- python scripts/generate_landing_quickstarts.py --check
# Run all documentation checks (build, linkcheck, validate)
docs-check: docs-validate docs-linkcheck docs-build
@echo ""
@echo "✅ All documentation checks passed!"
# Compile TPC-DS (and TPC-H) binaries from patched sources for the current
# platform and deploy them into benchbox/_binaries/ so they are used at runtime.
# No Docker required - builds natively on macOS ARM64/x86_64.
# Run this whenever _sources/tpc-ds/tools/ patches change.
compile-tpcds-binaries:
bash _sources/compilation/scripts/compile-all-platforms.sh --native