Skip to content

CI: Delphi tests run through one entry point, in parallel - #3036

Merged
colinmegill merged 3 commits into
edgefrom
delphi-tests-parallel
Oct 12, 2026
Merged

colinmegill merged 3 commits into
edgefrom
delphi-tests-parallel

Conversation

@colinmegill

@colinmegill colinmegill commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

What changes

Observable changes: none to product behaviour. The Delphi workflow's jobs become test shard 1/3, 2/3 and 3/3 plus test. test keeps the required-check name, posts the same coverage comment and fails if any shard fails.

  • One entry point. ci/local/delphi-python.sh holds every step the Delphi test job ran inline (stack, migrations, copy-in, projection-gate wiring, pytest with coverage, coverage comment). The workflow calls the script and nothing else; the same command runs on any Docker host. It removes published host ports, so several runs can share a host.
  • Parallel pytest. A parallel pass (pytest-xdist, one worker per CPU in the delphi container, --dist loadgroup; each dataset's tests stay on one worker so its conversation is computed once), then a sequential pass of the tests marked serial. A plain pytest still runs sequentially (-n0).
  • Shards on hosted CI. Three jobs run --shard 1/3, 2/3, 3/3, split by expected seconds per file or dataset group. The test job (same check name as before) checks that every collected test ran in exactly one shard, adds up the counts, combines coverage and posts the same coverage comment.
  • Tests marked serial (marker only; no test thinned or skipped). Each collided when run concurrently:
    • poller/test_capacity_queue_postgres.py: creates the cluster-wide role math_queue_test; parallel run: "role already exists".
    • poller/test_backfill_postgres.py: same role, plus fixed zids 13500...; parallel run: duplicate keys, foreign-key failures.
    • test_certify_bundle_integration.py: seeds fixed zid 1990000001...; parallel run: duplicate key on conversations_zid_key.
    • test_engine_rebuild.py: seeds fixed zids 9101/9102; parallel run: duplicate keys on participants/comments.
    • test_generator_vote_copy.py: seeds and deletes fixed zids per test; parallel run: assert 7 == 12 (another worker's rows).
    • test_prodclone_extract.py::test_survey_and_extract_round_trip: leaves zids 101001-101005 in the shared database; when it ran first, the certify-bundle survey picked zid 101005 instead of its planted conversation.

Evidence

Counts are identical everywhere: 4116 passed, 228 skipped, 44 xfailed, 2 xpassed (4390 collected), the same as the latest edge run. Per-test outcomes were compared test by test with the serial run.

Run Where pytest time Whole script Result
serial (DELPHI_PYTEST_WORKERS=0), final code work box A, 12 CPUs 403 s 472 s 4116/228/44/2
parallel, 12 workers work box B 102 s + 84 s serial pass 258 s identical per test
parallel, 4 workers (hosted runner size) work box A 152 s + 78 s serial pass 308 s identical per test
3 shards x 4 workers, concurrently (final plan) work box B 184 / 183 / 241 s per shard (whole script) counts add up; identical per test

Coverage: the combined shard coverage and the 12-worker run match the serial run line for line (25412 statements, 7950 missed). One 4-worker run differed by one line in polismath/poller/service.py. That line depends on timing in the live poller.

Hosted CI wall-clock (Delphi Python Tests):

Run Wall-clock pytest
before: latest edge push, job test 38212555754 24 m 01 s (run 24 m 05 s) 754 s, one process
after: this PR, 3 shards + test 38225722361 17 m 20 s (slowest shard 17 m 01 s, combine 14 s) 248 / 251 / 291 s per shard (parallel + serial pass)

Hosted counts: shards 1228+1474+1414 passed, 21+154+53 skipped, 22+0+22 xfailed, 2+0+0 xpassed = 4116 / 228 / 44 / 2, 4390 of 4390 collected tests, each in exactly one shard (checked by the test job). Coverage: 25412 statements, 7951 missed (edge: 7950). The one-line difference is the timing-dependent line in polismath/poller/service.py described above.

Each shard still spends about 6 minutes building every image in docker-compose.test.yml, plus disk cleanup and Buildx teardown. Most of the remaining time is that fixed cost. Layer caching and building only the services this job uses are separate changes.

One test fixture changed (the test's assertions did not). test_engine_rebuild.py generated its conversations with vote timestamps at 2026-10-03. That is inside the 10-day start window (POLL_FROM_DAYS_AGO) of the stack's live math-python poller, which runs during pytest. The poller could compute those conversations under its own label. test_dry_writes_nothing_and_digests_match_the_published_rows then saw rows outside the rebuild label (assert 2 == 0). Whether that happened depended on which tests ran first: it failed in shard 3 twice (once on a work box, once in hosted run 38220973217) and passed in every full run. The fixture now uses 2020 timestamps, which the poller never reads. Shard 3 then passed in every later run.

Run it locally: bash ci/local/delphi-python.sh (whole suite), --shard 2/3 (one shard), combine DIR... (merge shard results). DELPHI_PYTEST_WORKERS=0 gives the old single sequential pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s

colinmegill and others added 3 commits October 12, 2026 00:59
ci/local/delphi-python.sh now holds every step of the Delphi pytest job
(stack, migrations, copy-in, projection-gate wiring, pytest, coverage), and
the workflow calls it and nothing else, so the same check runs on any Docker
host. It removes published host ports so several runs can share one host.

pytest runs in two passes: a parallel pass (pytest-xdist, one worker per CPU,
--dist loadgroup, each dataset's tests on one worker so its conversation is
computed once), then the tests marked `serial` sequentially. The serial marker
is on the tests that collide when run concurrently: fixed ids or cluster-wide
roles in the shared test database, each with the observed failure in a comment.

Hosted CI splits the suite into three shard jobs (--shard I/3, by expected
seconds per file or dataset group) and the `test` job combines them: it checks
that every collected test ran in exactly one shard, adds up the counts and
combines coverage into the same PR comment. A plain `pytest` still runs
sequentially (-n0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
…indow

The shard plan now weighs each unit by expected wall seconds: parallel-pass
seconds shared by four workers, plus serial-pass seconds at full weight. The
durations file holds [parallel, serial] seconds per unit, measured on a hosted
run. The plan uses a fixed worker count, so every machine computes the same
plan.

test_engine_rebuild's generated conversations had vote timestamps at
2026-10-03, inside the 10-day start window of the stack's live math-python
poller. The poller could compute them under its own label, and the assertion
that only the rebuild label has rows then failed (assert 2 == 0). Whether it
did depended on which tests ran first, so a shard hit it. The fixture now
uses 2020 timestamps. The assertion is unchanged.

Shard results no longer include the compose inputs (stack/).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018NVGBuYCk4EZmiz4csUv9s
@github-actions

Copy link
Copy Markdown

Delphi Coverage Report

File Stmts Miss Cover
polismath/init.py 2 0 100%
polismath/benchmarks/bench_pca.py 128 107 16%
polismath/benchmarks/bench_repness.py 81 65 20%
polismath/benchmarks/bench_update_votes.py 38 28 26%
polismath/benchmarks/benchmark_utils.py 34 0 100%
polismath/components/init.py 1 0 100%
polismath/components/config.py 165 70 58%
polismath/conversation/init.py 2 0 100%
polismath/conversation/conversation.py 1291 264 80%
polismath/conversation/group_vote_counts.py 38 2 95%
polismath/conversation/manager.py 131 42 68%
polismath/database/init.py 1 0 100%
polismath/database/dynamodb.py 392 176 55%
polismath/database/postgres.py 414 95 77%
polismath/delphi_storage/init.py 1 0 100%
polismath/delphi_storage/codec.py 295 22 93%
polismath/delphi_storage/golden_corpus.py 60 9 85%
polismath/delphi_storage/import_report.py 51 3 94%
polismath/delphi_storage/legacy_import.py 171 16 91%
polismath/delphi_storage/postgres.py 49 22 55%
polismath/delphi_storage/production_import.py 419 96 77%
polismath/delphi_storage/resource.py 179 32 82%
polismath/delphi_storage/scan_pacing.py 77 0 100%
polismath/delphi_storage/writer.py 158 17 89%
polismath/empty_output.py 29 2 93%
polismath/engine_adapter.py 317 265 16%
polismath/job_child/init.py 331 50 85%
polismath/job_child/census.py 101 8 92%
polismath/pca_kmeans_rep/init.py 5 0 100%
polismath/pca_kmeans_rep/clusters.py 257 38 85%
polismath/pca_kmeans_rep/corr.py 98 17 83%
polismath/pca_kmeans_rep/group_k_smoother.py 28 0 100%
polismath/pca_kmeans_rep/legacy_kmeans.py 197 8 96%
polismath/pca_kmeans_rep/pca.py 162 19 88%
polismath/pca_kmeans_rep/repness.py 226 5 98%
polismath/poller/init.py 4 0 100%
polismath/poller/admission.py 318 15 95%
polismath/poller/backfill.py 936 32 97%
polismath/poller/capacity.py 485 27 94%
polismath/poller/capacity_queue.py 248 13 95%
polismath/poller/coordinator_bridge.py 317 317 0%
polismath/poller/math_writer.py 113 3 97%
polismath/poller/promotion.py 112 4 96%
polismath/poller/readiness.py 368 13 96%
polismath/poller/rebuild_child.py 133 5 96%
polismath/poller/service.py 665 52 92%
polismath/poller/worker_pool.py 140 0 100%
polismath/queue/executor.py 223 111 50%
polismath/regression/init.py 4 0 100%
polismath/regression/clojure_comparer.py 188 20 89%
polismath/regression/comparer.py 891 640 28%
polismath/regression/datasets.py 135 27 80%
polismath/regression/recorder.py 36 27 25%
polismath/regression/utils.py 130 85 35%
polismath/replay/init.py 2 0 100%
polismath/replay/attribution_capture.py 49 0 100%
polismath/replay/axis_continuity.py 347 24 93%
polismath/replay/certify.py 999 59 94%
polismath/replay/crosslang.py 128 2 98%
polismath/replay/decision_ties.py 55 8 85%
polismath/replay/diagnostics.py 50 7 86%
polismath/replay/driver.py 73 1 99%
polismath/replay/dsn_admission.py 36 1 97%
polismath/replay/equiv_query.py 10 0 100%
polismath/replay/event_ingress.py 95 1 99%
polismath/replay/fixture_bundle.py 815 62 92%
polismath/replay/fixture_config.py 266 11 96%
polismath/replay/fixture_extract.py 285 12 96%
polismath/replay/fixture_generate.py 103 1 99%
polismath/replay/fixture_samples.py 157 14 91%
polismath/replay/fixture_selection.py 119 0 100%
polismath/replay/fixture_survey.py 147 11 93%
polismath/replay/legacy_pca.py 29 17 41%
polismath/replay/polarity.py 319 25 92%
polismath/replay/poller_equiv.py 776 99 87%
polismath/replay/prodclone.py 193 2 99%
polismath/replay/real_data.py 146 10 93%
polismath/replay/schedule.py 205 10 95%
polismath/replay/served_math.py 159 4 97%
polismath/replay/shard_bench.py 189 65 66%
polismath/replay/stagecompare.py 890 161 82%
polismath/replay/stages.py 256 40 84%
polismath/replay/stepcompare.py 86 1 99%
polismath/replay/store.py 120 11 91%
polismath/replay/tie_capture.py 107 19 82%
polismath/replay/types.py 71 6 92%
polismath/run_math_pipeline.py 284 93 67%
polismath/tools/engine_rebuild.py 230 11 95%
polismath/types.py 22 0 100%
polismath/utils/cli_flags.py 15 0 100%
polismath/utils/clj_hash.py 44 0 100%
polismath/utils/env_flags.py 13 0 100%
polismath/utils/general.py 63 39 38%
polismath/utils/output_profile.py 66 2 97%
polismath/utils/serialization.py 10 5 50%
polismath/utils/vote_convention.py 159 5 97%
umap_narrative/501_calculate_comment_extremity.py 113 38 66%
umap_narrative/502_calculate_priorities.py 136 103 24%
umap_narrative/700_datamapplot_for_layer.py 502 502 0%
umap_narrative/701_static_datamapplot_for_layer.py 311 311 0%
umap_narrative/702_consensus_divisive_datamapplot.py 435 275 37%
umap_narrative/801_narrative_report_batch.py 666 455 32%
umap_narrative/803_check_batch_status.py 268 148 45%
umap_narrative/llm_factory_constructor/init.py 2 0 100%
umap_narrative/llm_factory_constructor/model_provider.py 234 107 54%
umap_narrative/narrative_data.py 178 65 63%
umap_narrative/numerical_stages.py 54 25 54%
umap_narrative/polismath_commentgraph/init.py 1 0 100%
umap_narrative/polismath_commentgraph/cli.py 268 268 0%
umap_narrative/polismath_commentgraph/core/init.py 3 3 0%
umap_narrative/polismath_commentgraph/core/clustering.py 108 108 0%
umap_narrative/polismath_commentgraph/core/embedding.py 104 104 0%
umap_narrative/polismath_commentgraph/lambda_handler.py 219 219 0%
umap_narrative/polismath_commentgraph/schemas/init.py 2 0 100%
umap_narrative/polismath_commentgraph/schemas/dynamo_models.py 160 9 94%
umap_narrative/polismath_commentgraph/tests/conftest.py 17 17 0%
umap_narrative/polismath_commentgraph/tests/test_clustering.py 74 74 0%
umap_narrative/polismath_commentgraph/tests/test_embedding.py 55 55 0%
umap_narrative/polismath_commentgraph/tests/test_storage.py 87 87 0%
umap_narrative/polismath_commentgraph/utils/init.py 9 7 22%
umap_narrative/polismath_commentgraph/utils/converter.py 283 237 16%
umap_narrative/polismath_commentgraph/utils/group_data.py 332 289 13%
umap_narrative/polismath_commentgraph/utils/storage.py 581 466 20%
umap_narrative/reset_conversation.py 162 53 67%
umap_narrative/run_pipeline.py 337 207 39%
umap_narrative/topic_naming.py 148 11 93%
Total 25412 7951 69%

@colinmegill
colinmegill merged commit fd33de4 into edge Oct 12, 2026
16 of 17 checks passed
@colinmegill
colinmegill deleted the delphi-tests-parallel branch October 12, 2026 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant