polyrun 1.0.0

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +31 -0
  3. data/CONTRIBUTING.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +140 -0
  6. data/SECURITY.md +27 -0
  7. data/bin/polyrun +6 -0
  8. data/docs/SETUP_PROFILE.md +106 -0
  9. data/lib/polyrun/cli/coverage_commands.rb +150 -0
  10. data/lib/polyrun/cli/coverage_merge_io.rb +124 -0
  11. data/lib/polyrun/cli/database_commands.rb +149 -0
  12. data/lib/polyrun/cli/env_commands.rb +43 -0
  13. data/lib/polyrun/cli/helpers.rb +113 -0
  14. data/lib/polyrun/cli/init_command.rb +99 -0
  15. data/lib/polyrun/cli/plan_command.rb +134 -0
  16. data/lib/polyrun/cli/prepare_command.rb +71 -0
  17. data/lib/polyrun/cli/prepare_recipe.rb +77 -0
  18. data/lib/polyrun/cli/queue_command.rb +101 -0
  19. data/lib/polyrun/cli/quick_command.rb +13 -0
  20. data/lib/polyrun/cli/report_commands.rb +94 -0
  21. data/lib/polyrun/cli/run_shards_command.rb +88 -0
  22. data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +91 -0
  23. data/lib/polyrun/cli/run_shards_plan_options.rb +45 -0
  24. data/lib/polyrun/cli/run_shards_planning.rb +124 -0
  25. data/lib/polyrun/cli/run_shards_run.rb +168 -0
  26. data/lib/polyrun/cli/start_bootstrap.rb +99 -0
  27. data/lib/polyrun/cli/timing_command.rb +31 -0
  28. data/lib/polyrun/cli.rb +184 -0
  29. data/lib/polyrun/config.rb +61 -0
  30. data/lib/polyrun/coverage/cobertura_zero_lines.rb +32 -0
  31. data/lib/polyrun/coverage/collector.rb +184 -0
  32. data/lib/polyrun/coverage/collector_finish.rb +95 -0
  33. data/lib/polyrun/coverage/filter.rb +22 -0
  34. data/lib/polyrun/coverage/formatter.rb +115 -0
  35. data/lib/polyrun/coverage/merge/formatters.rb +181 -0
  36. data/lib/polyrun/coverage/merge/formatters_html.rb +55 -0
  37. data/lib/polyrun/coverage/merge.rb +127 -0
  38. data/lib/polyrun/coverage/merge_fragment_meta.rb +47 -0
  39. data/lib/polyrun/coverage/merge_merge_two.rb +117 -0
  40. data/lib/polyrun/coverage/rails.rb +128 -0
  41. data/lib/polyrun/coverage/reporting.rb +41 -0
  42. data/lib/polyrun/coverage/result.rb +18 -0
  43. data/lib/polyrun/coverage/track_files.rb +141 -0
  44. data/lib/polyrun/data/cached_fixtures.rb +122 -0
  45. data/lib/polyrun/data/factory_counts.rb +35 -0
  46. data/lib/polyrun/data/factory_instrumentation.rb +50 -0
  47. data/lib/polyrun/data/fixtures.rb +68 -0
  48. data/lib/polyrun/data/parallel_provisioning.rb +93 -0
  49. data/lib/polyrun/data/snapshot.rb +84 -0
  50. data/lib/polyrun/database/clone_shards.rb +81 -0
  51. data/lib/polyrun/database/provision.rb +72 -0
  52. data/lib/polyrun/database/shard.rb +63 -0
  53. data/lib/polyrun/database/url_builder/connection/infer.rb +49 -0
  54. data/lib/polyrun/database/url_builder/connection/url_builders.rb +43 -0
  55. data/lib/polyrun/database/url_builder/connection.rb +191 -0
  56. data/lib/polyrun/database/url_builder/template_prepare.rb +21 -0
  57. data/lib/polyrun/database/url_builder.rb +160 -0
  58. data/lib/polyrun/debug.rb +81 -0
  59. data/lib/polyrun/env/ci.rb +65 -0
  60. data/lib/polyrun/log.rb +70 -0
  61. data/lib/polyrun/minitest.rb +17 -0
  62. data/lib/polyrun/partition/constraints.rb +69 -0
  63. data/lib/polyrun/partition/hrw.rb +33 -0
  64. data/lib/polyrun/partition/min_heap.rb +64 -0
  65. data/lib/polyrun/partition/paths.rb +28 -0
  66. data/lib/polyrun/partition/paths_build.rb +128 -0
  67. data/lib/polyrun/partition/plan.rb +189 -0
  68. data/lib/polyrun/partition/plan_lpt.rb +49 -0
  69. data/lib/polyrun/partition/plan_sharding.rb +48 -0
  70. data/lib/polyrun/partition/stable_shuffle.rb +18 -0
  71. data/lib/polyrun/prepare/artifacts.rb +40 -0
  72. data/lib/polyrun/prepare/assets.rb +57 -0
  73. data/lib/polyrun/queue/file_store.rb +199 -0
  74. data/lib/polyrun/queue/file_store_pending.rb +48 -0
  75. data/lib/polyrun/quick/assertions.rb +32 -0
  76. data/lib/polyrun/quick/errors.rb +6 -0
  77. data/lib/polyrun/quick/example_group.rb +66 -0
  78. data/lib/polyrun/quick/example_runner.rb +93 -0
  79. data/lib/polyrun/quick/matchers.rb +156 -0
  80. data/lib/polyrun/quick/reporter.rb +42 -0
  81. data/lib/polyrun/quick/runner.rb +180 -0
  82. data/lib/polyrun/quick.rb +1 -0
  83. data/lib/polyrun/railtie.rb +7 -0
  84. data/lib/polyrun/reporting/junit.rb +125 -0
  85. data/lib/polyrun/reporting/junit_emit.rb +58 -0
  86. data/lib/polyrun/reporting/rspec_junit.rb +39 -0
  87. data/lib/polyrun/rspec.rb +15 -0
  88. data/lib/polyrun/templates/POLYRUN.md +45 -0
  89. data/lib/polyrun/templates/ci_matrix.polyrun.yml +14 -0
  90. data/lib/polyrun/templates/minimal_gem.polyrun.yml +13 -0
  91. data/lib/polyrun/templates/rails_prepare.polyrun.yml +31 -0
  92. data/lib/polyrun/timing/merge.rb +35 -0
  93. data/lib/polyrun/timing/summary.rb +25 -0
  94. data/lib/polyrun/version.rb +3 -0
  95. data/lib/polyrun.rb +58 -0
  96. data/polyrun.gemspec +37 -0
  97. data/sig/polyrun/cli.rbs +6 -0
  98. data/sig/polyrun/config.rbs +20 -0
  99. data/sig/polyrun/debug.rbs +12 -0
  100. data/sig/polyrun/log.rbs +12 -0
  101. data/sig/polyrun/minitest.rbs +5 -0
  102. data/sig/polyrun/quick.rbs +19 -0
  103. data/sig/polyrun/rspec.rbs +5 -0
  104. data/sig/polyrun.rbs +11 -0
  105. metadata +288 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca0c4afee7136f1cdf3ffe319d55c08457323119102a0a4d01478b02df91ce31
4
+ data.tar.gz: d468805765ee230bed530fdce182f189c0521e3dff7516be5c75bc620c362fc8
5
+ SHA512:
6
+ metadata.gz: 98e04c5388e51da737bb73407497830d82bf61daabdfd77e9024fd2d973663dfed934c87691c6f2e864d7642e457a1eaaff27e3cf3f79bfa102a92ec9a0034aa
7
+ data.tar.gz: b6c407039df97914ed8e07a04bdb0dc4c8e044d9b0ffd3802e44e600a6c4002fe22b8673266263c5981bb4bde90246c992defb589eb3b785952b0c56f67c263e
@@ -0,0 +1,31 @@
1
+ # Code of Conduct
2
+
3
+ ## Our pledge
4
+
5
+ We pledge to make participation in the Polyrun community a harassment-free experience for everyone. We operate on principles of mutual respect, privacy, and authentic engagement. We value substantive contributions and clarity on intentions.
6
+
7
+ ## Our standards
8
+
9
+ Examples of behavior that contributes to a positive environment include:
10
+
11
+ - Authenticity: engaging with genuine curiosity and admitting uncertainty rather than feigning knowledge.
12
+ - Responsible innovation: taking full responsibility for any content or code contributed, whether manually written or generated by automation tools.
13
+ - Gentle correction: responding politely to errors. We view mistakes as opportunities for learning, provided they are addressed with humility.
14
+ - Inclusive language: using language that welcomes diverse perspectives and respects the privacy and identity of all participants.
15
+
16
+ Examples of unacceptable behavior include:
17
+
18
+ - Harassment: public or private harassment, trolling, or insulting comments.
19
+ - Weaponized complexity: using jargon or overwhelming volume (including automated spam) to silence others.
20
+ - Publishing private information: sharing others' data or personal context without explicit permission.
21
+
22
+ ## Artificial intelligence and automation
23
+
24
+ In accordance with our commitment to collective awareness:
25
+
26
+ - Contributors are responsible for the accuracy and security of any AI-generated artifacts they submit.
27
+ - "The AI wrote it" is not a valid excuse for introducing bugs, security vulnerabilities, or bias.
28
+
29
+ ## Enforcement
30
+
31
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@kiskolabs.com. All complaints will be reviewed and investigated promptly and fairly.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,84 @@
1
+ # Contributing to Polyrun
2
+
3
+ ## Development setup
4
+
5
+ ```bash
6
+ bundle install
7
+ bundle exec appraisal install
8
+ ```
9
+
10
+ ## Tests
11
+
12
+ ```bash
13
+ bundle exec rspec
14
+ # or
15
+ bundle exec rake spec
16
+ ```
17
+
18
+ Coverage merge performance (large synthetic payloads):
19
+
20
+ ```bash
21
+ bundle exec rake bench_merge
22
+ # or: ruby benchmark/merge_coverage.rb
23
+ ```
24
+
25
+ Run the suite under alternate Ruby constraints (see `Appraisals` and `gemfiles/`):
26
+
27
+ ```bash
28
+ bundle exec appraisal ruby32 rspec
29
+ bundle exec appraisal ruby34 rspec
30
+ ```
31
+
32
+ ## Linting
33
+
34
+ [RuboCop](https://rubocop.org/) with [Standard](https://github.com/standardrb/standard) style, plus `rubocop-rspec` and `rubocop-thread_safety`. Project-specific cop tweaks and metric `Exclude` lists live in `.rubocop.yml`.
35
+
36
+ ```bash
37
+ bundle exec rubocop
38
+ bundle exec rubocop -a # safe autocorrect
39
+ bundle exec rake rubocop
40
+ ```
41
+
42
+ ## RBS
43
+
44
+ Type signatures live under `sig/` and ship with the gem. Validate them after changes:
45
+
46
+ ```bash
47
+ bundle exec rake rbs
48
+ # equivalent: bundle exec rbs -I sig validate
49
+ ```
50
+
51
+ Keep `require "polyrun"` free of RSpec/Minitest: optional wiring stays in `polyrun/rspec`, `polyrun/minitest`, and `polyrun/reporting/rspec_junit` (see README).
52
+
53
+ [Trunk](https://trunk.io/) aggregates RuboCop, YAML, Markdown, shellcheck, and more (see `.trunk/trunk.yaml`):
54
+
55
+ ```bash
56
+ trunk check
57
+ trunk fmt
58
+ ```
59
+
60
+ CI runs `rake ci` (RSpec + RuboCop). Optional Trunk workflow: `.github/workflows/trunk.yml`.
61
+
62
+ ## Adopting Polyrun in other repos
63
+
64
+ - [docs/SETUP_PROFILE.md](docs/SETUP_PROFILE.md) — agent or human checklist (CI model A vs B, database, prepare).
65
+ - `bundle exec polyrun init --list` — lists starter `polyrun.yml` and `POLYRUN.md` templates (see `examples/templates/README.md`).
66
+
67
+ ## Examples
68
+
69
+ Runnable demos live under `examples/`. After changing the gem, smoke them:
70
+
71
+ ```bash
72
+ cd examples && ./bin/ci_prepare
73
+ cd examples/simple/simple_demo && RAILS_ENV=test bundle exec rspec
74
+ # optional: complex polyrepo demo (multi-DB + three Vite clients + RSpec E2E)
75
+ # cd examples/complex/polyrepo_demo && RAILS_ENV=test bin/rails db:prepare && bundle exec rspec
76
+ ```
77
+
78
+ See `examples/README.md`.
79
+
80
+ ## Pull requests
81
+
82
+ - One logical change per PR when possible.
83
+ - Add or update specs for behavior changes.
84
+ - Run `bundle exec rake ci` before pushing.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrei Makarov
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.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Polyrun
2
+
3
+ Ruby gem for parallel test runs, merged coverage (SimpleCov-compatible JSON to JSON, LCOV, Cobertura, or console output), CI reporting (JUnit, timing), and parallel-test hygiene (fixtures, snapshots, per-shard databases, asset preparation). Ship it as one development dependency: no runtime gem dependencies beyond the standard library and vendored code.
4
+
5
+ ## Why?
6
+
7
+ Running tests in parallel across processes still requires a single merged coverage report, stable shard assignment, isolated databases per worker so rows are not shared, and reliable timing data for cost-based splits—without wiring together many small tools and shell scripts.
8
+
9
+ Polyrun provides:
10
+
11
+ - Orchestration: `plan`, `run-shards`, and `parallel-rspec` (run-shards plus merge-coverage), with an optional on-disk queue and constraints for file lists and load balancing.
12
+ - Coverage: merge SimpleCov-compatible JSON fragments; emit JSON, LCOV, Cobertura, or console summaries (you can drop separate SimpleCov merge plugins for this path).
13
+ - CI reporting: JUnit XML from RSpec JSON; slow-file reports from merged timing JSON.
14
+ - Parallel hygiene: asset digest markers, SQL snapshots, YAML fixture batches, and DB URL or shard helpers aligned with `POLYRUN_SHARD_*`.
15
+ - Optional **`polyrun quick`**: `Polyrun::Quick` — nested `describe`, `it` / `test`, `before` / `after`, `let` / `let!`, `expect(x).to …` matchers, `assert_*` (Minitest-like), and optional **`Polyrun::Quick.capybara!`** (extends `Capybara::DSL` when the **capybara** gem is loaded). Stdlib-only in Polyrun itself. Coverage uses the same `Polyrun::Coverage::Collector` path as RSpec when `POLYRUN_COVERAGE=1` or `config/polyrun_coverage.yml` is present (not when `POLYRUN_COVERAGE_DISABLE=1`).
16
+ - No runtime gems in the gemspec: stdlib and vendored pieces only.
17
+
18
+ Capybara and Playwright stay in your application; Polyrun does not replace browser drivers.
19
+
20
+ ## How?
21
+
22
+ 1. Add the gem (path or RubyGems) and `require "polyrun"` where you integrate—for example coverage merge in CI or prepare hooks.
23
+ 2. Add a `polyrun.yml` beside the app, or pass `-c` to point at one. Configure `partition` (paths, shard index and total, strategy), and optionally `databases` (Postgres template and `shard_db_pattern`), `prepare`, and `coverage`. If you use `partition.paths_build`, Polyrun can write `partition.paths_file` (for example `spec/spec_paths.txt`) from globs and ordered stages—substring priorities for integration specs, or a regex stage for “Rails-heavy files first”—without a per-project Ruby script. That step runs before `plan` and `run-shards`. Use `bin/polyrun build-paths` to refresh the paths file only.
24
+ 3. Run prepare once before fan-out—for example `script/ci_prepare` for Vite or webpack builds, and `Polyrun::Prepare::Assets` digest markers. See `examples/TESTING_REQUIREMENTS.md`.
25
+ 4. Run workers with `bin/polyrun run-shards --workers N -- bundle exec rspec`: N separate OS processes, each running RSpec with its own file list from `partition.paths_file`, or `spec/spec_paths.txt`, or else `spec/**/*_spec.rb`. Stderr shows where paths came from; after a successful multi-worker run it reminds you to run merge-coverage unless you use `parallel-rspec` or `run-shards --merge-coverage`.
26
+ 5. Merge artifacts with `bin/polyrun merge-coverage` on `coverage/polyrun-fragment-*.json` (one fragment per `POLYRUN_SHARD_INDEX` when coverage is on), or use `bin/polyrun parallel-rspec` or `run-shards --merge-coverage` so Polyrun runs merge for you. Optional: `merge-timing`, `report-timing`, `report-junit`.
27
+
28
+ Quick CLI samples:
29
+
30
+ If the current directory already has `polyrun.yml` or `config/polyrun.yml`, you can omit `-c` (same as `Config.load` default discovery). Pass `-c PATH` or set `POLYRUN_CONFIG` when the file lives elsewhere or uses another name.
31
+
32
+ ```bash
33
+ bin/polyrun version
34
+ bin/polyrun build-paths # write spec/spec_paths.txt from partition.paths_build (uses polyrun.yml in cwd)
35
+ bin/polyrun parallel-rspec --workers 5 # run-shards + merge-coverage (default: bundle exec rspec)
36
+ bin/polyrun run-shards --workers 5 --merge-coverage -- bundle exec rspec
37
+ bin/polyrun merge-coverage -i cov1.json -i cov2.json -o merged.json --format json,lcov,cobertura,console
38
+ bin/polyrun env --shard 0 --total 4 # print DATABASE_URL exports from polyrun.yml in cwd
39
+ bin/polyrun init --list
40
+ bin/polyrun init --profile gem -o polyrun.yml # starter YAML; see docs/SETUP_PROFILE.md
41
+ bin/polyrun quick # Polyrun::Quick examples under spec/polyrun_quick/ or test/polyrun_quick/
42
+ ```
43
+
44
+ ### Adopting Polyrun (setup profile and scaffolds)
45
+
46
+ - [docs/SETUP_PROFILE.md](docs/SETUP_PROFILE.md) — Checklist for project type (gem, Rails, Appraisal), parallelism target (one CI job with N workers, matrix shards, or a single non-matrix runner), database layout, prepare, spec order, coverage, and CI model A (single runner with `parallel-rspec`) versus model B (matrix plus a merge-coverage job). Treat `polyrun.yml` as the contract; bin scripts and `database.yml` are adapters.
47
+ - `polyrun init` writes a starter `polyrun.yml` or `POLYRUN.md` from built-in templates (`--profile gem`, `rails`, `ci-matrix`, `doc`). Profiles are listed in `examples/templates/README.md`.
48
+
49
+ Runnable Rails demos (multi-DB, Vite, Capybara, Docker, Postgres sketches) live in `examples/README.md`. For development (tests, RuboCop, Appraisal), see [CONTRIBUTING.md](CONTRIBUTING.md).
50
+
51
+ ---
52
+
53
+ ## Library (after `require "polyrun"`)
54
+
55
+ That single require loads the CLI and core library **without** loading RSpec or Minitest. Optional integrations (use only what your app needs):
56
+
57
+ - `require "polyrun/rspec"` — `Polyrun::RSpec` registers `ParallelProvisioning` in `before(:suite)` (your app must use RSpec).
58
+ - `require "polyrun/minitest"` — `Polyrun::Minitest` is a thin alias for `ParallelProvisioning.run_suite_hooks!` (does not `require "minitest"`).
59
+ - `require "polyrun/reporting/rspec_junit"` — `Polyrun::Reporting::RspecJunit` adds RSpec’s JSON formatter and writes JUnit on exit; RSpec is loaded only inside `RspecJunit.install!`.
60
+
61
+ | API | Purpose |
62
+ |-----|---------|
63
+ | `Polyrun::Quick` (`require "polyrun/quick"`) | Nested `describe`; `it` / `test`; `before` / `after`; `let` / `let!`; `expect(x).to eq` / `be_truthy` / `be_falsey` / `match` / `include`; `assert_*`. Call `Polyrun::Quick.capybara!` after `require "capybara"` (and configure `Capybara.app` in your app) to use `visit`, `page`, etc. Run: `polyrun quick` (defaults: `spec/polyrun_quick/**/*.rb`, `test/polyrun_quick/**/*.rb`). |
64
+ | `Polyrun::Log` | Swappable stderr/stdout for all CLI and library messages. Set `Polyrun.stderr` / `Polyrun.stdout` (or `Polyrun::Log.stderr` / `stdout`) to an `IO`, `StringIO`, or Ruby `Logger`. `Polyrun::Log.reset_io!` clears custom sinks. |
65
+ | `Polyrun::Coverage::Merge` | Merge fragments; formatters for JSON, LCOV, Cobertura, console summary. |
66
+ | `Polyrun::Coverage::Collector` | Stdlib `Coverage` → JSON fragment (`POLYRUN_COVERAGE_DISABLE` to skip); `track_under: %w[lib app]`, filters, optional % gate. |
67
+ | `Polyrun::Coverage::Reporting` | Write all formats to a directory from a blob or merged JSON file. |
68
+ | `Polyrun::Reporting::JUnit` | RSpec `--format json` output or Polyrun testcase JSON → JUnit XML (CI). For RSpec formatter wiring see `Polyrun::Reporting::RspecJunit` (`require "polyrun/reporting/rspec_junit"`). |
69
+ | `Polyrun::Timing::Summary` | Text report of slowest files from merged `polyrun_timing.json`. |
70
+ | `Polyrun::Data::Fixtures` | YAML table batches (`each_table`, `load_directory`, optional `apply_insert_all!` with ActiveRecord). |
71
+ | `Polyrun::Data::CachedFixtures` | Process-local memoized fixture blocks (`register` / `fetch`, stats, `reset!`). |
72
+ | `Polyrun::Data::ParallelProvisioning` | Serial vs parallel-worker suite hooks from `POLYRUN_SHARD_*` / `TEST_ENV_NUMBER`. |
73
+ | `Polyrun::Data::FactoryInstrumentation` | Opt-in FactoryBot patch → `FactoryCounts` (after `require "factory_bot"`). |
74
+ | `Polyrun::Data::SqlSnapshot` | PostgreSQL `pg_dump` / `psql` snapshots under `spec/fixtures/sql_snapshots/`. |
75
+ | `Polyrun::Data::FactoryCounts` | Factory/build counters + summary text. |
76
+ | `Polyrun::RSpec` (`require "polyrun/rspec"`) | `install_parallel_provisioning!` → `before(:suite)` hooks. |
77
+ | `Polyrun::Minitest` (`require "polyrun/minitest"`) | `install_parallel_provisioning!` → same as `ParallelProvisioning.run_suite_hooks!` (no Minitest gem dependency). |
78
+ | `Polyrun::Reporting::RspecJunit` (`require "polyrun/reporting/rspec_junit"`) | CI: RSpec JSON formatter + JUnit from `install!` (RSpec loaded only there). |
79
+ | `Polyrun::Prepare::Assets` | Digest trees, marker file, `assets:precompile`. |
80
+ | `Polyrun::Database::Shard` | Shard env map, `%{shard}` DB names, URL path suffix for `postgres://`, `mysql2://`, `mongodb://`, etc. |
81
+ | `Polyrun::Database::UrlBuilder` | URLs from `polyrun.yml` `databases:` — nested blocks or `adapter:` for common Rails stacks (`postgresql`, `mysql`/`mysql2`, `trilogy`, `sqlserver`/`mssql`, `sqlite3`/`sqlite`, `mongodb`/`mongo`). |
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ bundle install
87
+ bundle exec rake spec
88
+ bundle exec rake rbs # optional: validate RBS in sig/
89
+ bundle exec rake ci # RSpec + RuboCop
90
+ ```
91
+
92
+ Tests include subprocess CLI coverage (`spec/polyrun/cli_spec.rb`: merge-coverage across formats, merge-timing, report-junit, report-timing, plan, env with `databases:`, prepare, report-coverage, db:* dry-run and errors) and unit specs for `Coverage::Merge`, `Timing::Merge`, `Timing::Summary`, `Reporting::JUnit`, `Partition::Plan`, `Database::UrlBuilder` and `Shard`, `Prepare::Artifacts`, `Data::*`, `Env::Ci`, `Queue::FileStore`, and `SqlSnapshot` (with `Open3` stubbed for `pg_dump`).
93
+
94
+ Merge performance defaults align with `spec/polyrun/coverage/merge_scale_spec.rb` (~110 files × 310 lines per fragment):
95
+
96
+ ```bash
97
+ ruby benchmark/merge_coverage.rb
98
+ bundle exec rake bench_merge
99
+ # MERGE_FRAGMENTS=16 MERGE_REPS=2 ruby benchmark/merge_coverage.rb
100
+ ```
101
+
102
+ The script benchmarks `merge_two`, balanced `merge_blob_tree` (same reduction as `merge-coverage` / `merge_files`), a naive left-fold, JSON on disk via `merge_files`, and `merge_fragments` with `meta` and `polyrun_coverage_groups` (group recomputation).
103
+
104
+ Merge is JSON aggregation over coverage fragments; for typical apps it stays well under one second. If merge exceeds ten seconds, Polyrun prints a warning on stderr (override with `POLYRUN_MERGE_SLOW_WARN_SECONDS`; set to `0` to disable).
105
+
106
+ ## CLI (reference)
107
+
108
+ ```bash
109
+ bin/polyrun plan --total 2 --shard 0 a.rb b.rb c.rb
110
+ bin/polyrun plan --total 3 --shard 0 --timing polyrun_timing.json --paths-file spec/spec_paths.txt
111
+ bin/polyrun plan --strategy hrw --total 4 --shard 0 --paths-file spec/spec_paths.txt
112
+ bin/polyrun queue init --paths-file spec/spec_paths.txt --timing polyrun_timing.json --dir .polyrun-queue
113
+ bin/polyrun report-coverage -i merged.json -o coverage/out --format json,lcov,cobertura,console
114
+ bin/polyrun report-junit -i rspec.json -o junit.xml
115
+ bin/polyrun report-timing -i polyrun_timing.json --top 20
116
+ bin/polyrun plan --shard 0 --total 4 # polyrun.yml in cwd
117
+ bin/polyrun prepare --recipe assets --dry-run
118
+ bin/polyrun parallel-rspec --workers 4
119
+ bin/polyrun quick spec/polyrun_quick/smoke.rb
120
+ ```
121
+
122
+ `polyrun.yml` can set `partition.*`, `prepare.recipe` (`default` or `assets`), `prepare.rails_root`, and related keys. `POLYRUN_SHARD_*` overrides configuration where documented; CLI flags override environment variables.
123
+
124
+ Shard index and total in CI (`Polyrun::Env::Ci`): when set, `POLYRUN_SHARD_INDEX` and `POLYRUN_SHARD_TOTAL` take precedence. When `CI` is truthy, `CI_NODE_INDEX` / `CI_NODE_TOTAL` and other parallel-job environment variables are read if present. If your runner does not export those, set `POLYRUN_SHARD_*` from the job matrix.
125
+
126
+ File queue (`polyrun queue …`): batches live on disk under a lock file; paths move from `pending` to `leases` on claim and to `done` on ack. There is no lease TTL: if a worker dies after claiming, paths remain in `leases` until you recover them (manually or with a future reclaim command).
127
+
128
+ ## Examples
129
+
130
+ See [`examples/README.md`](examples/README.md) for Rails apps (Capybara, Playwright, Vite, multi-database, Docker Compose, polyrepo). Parallel CI practices: [`examples/TESTING_REQUIREMENTS.md`](examples/TESTING_REQUIREMENTS.md). Behavioral contracts: `spec/polyrun/mandatory_parallel_support_spec.rb`.
131
+
132
+ You can replace SimpleCov and simplecov plugins, parallel_tests, and rspec_junit_formatter with Polyrun for those roles. Use `merge-timing`, `report-timing`, and `Data::FactoryCounts` (optionally with `Data::FactoryInstrumentation`) for slow-file and factory metrics. YAML fixture batches and bulk inserts can use `Data::Fixtures` and `ParallelProvisioning` for shard-aware seeding; wire your own `truncate` and `load_seed` in hooks.
133
+
134
+ ---
135
+
136
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
137
+
138
+ <a href="https://www.kiskolabs.com">
139
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
140
+ </a>
data/SECURITY.md ADDED
@@ -0,0 +1,27 @@
1
+ # SECURITY
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Do not open a public GitHub issue for security vulnerabilities.
6
+
7
+ Email security details to security@kiskolabs.com.
8
+
9
+ Include: description, steps to reproduce, potential impact, and suggested fix (if available).
10
+
11
+ ### Response Timeline
12
+
13
+ - We will acknowledge receipt of your report
14
+ - We will provide an initial assessment
15
+ - We will keep you informed of our progress and resolution timeline
16
+
17
+ ### Disclosure Policy
18
+
19
+ - We will work with you to understand and resolve the issue
20
+ - We will credit you for the discovery (unless you prefer to remain anonymous)
21
+ - We will publish a security advisory after the vulnerability is patched
22
+ - We will coordinate public disclosure with you
23
+
24
+ ## Automation security
25
+
26
+ - Context isolation: do not include production credentials, API keys, or personally identifiable information in prompts sent to third-party LLMs or automation services.
27
+ - Supply chain: verify automated dependencies.
data/bin/polyrun ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
3
+ require "polyrun"
4
+
5
+ status = Polyrun::CLI.run(ARGV)
6
+ exit(status.nil? ? 0 : status)
@@ -0,0 +1,106 @@
1
+ # Polyrun setup profile (agent / human checklist)
2
+
3
+ Use this as a fill-in worksheet before editing a host project. `polyrun.yml` is the contract; everything else is an adapter that must stay aligned with it.
4
+
5
+ ## 1. Project shape
6
+
7
+ | Field | Options / notes |
8
+ |--------|------------------|
9
+ | Project type | Gem (library, no Rails app), Rails (full app), or multi-gemfile (Appraisal / multiple `gemfiles/*.gemfile`) |
10
+ | Gemfile path to polyrun | For example `gem "polyrun", path: "../polyrun.rb"` or `gem "polyrun"` from RubyGems—note the path relative to each gemfile |
11
+ | Appraisal / Docker | If Appraisal: polyrun must appear in `Appraisals` and each generated gemfile. If Docker: document working directory and whether prepare is one-shot or repeated with tests |
12
+
13
+ ## 2. Parallelism target (pick one primary story)
14
+
15
+ | Target | Typical CLI / CI |
16
+ |--------|-------------------|
17
+ | Single CI job, N workers on one runner | One workflow job runs `polyrun parallel-rspec --workers N` (or `start`); merge coverage in the same job or a small follow-up step; upload artifacts |
18
+ | Matrix: one shard per CI job | Matrix sets `POLYRUN_SHARD_INDEX` / `POLYRUN_SHARD_TOTAL` (or CI vendor equivalents—see `Polyrun::Env::Ci` and the README); each job runs `plan` plus the test command for its shard; upload `coverage/polyrun-fragment-<i>.json`; a merge job runs `merge-coverage` |
19
+
20
+ Do not mix “fan out N workers inside one job” with “matrix shard index” in the same workflow without a clear story for which process writes which fragment and where merge runs.
21
+
22
+ ## 3. Database
23
+
24
+ | Field | Options |
25
+ |--------|---------|
26
+ | Single test DB | Omitting `databases:` in `polyrun.yml` may be enough; ensure parallel workers do not share one DB if they mutate data |
27
+ | Multi-DB or shard suffixes | `databases:` in `polyrun.yml` (`shard_db_pattern`, `template_db`, optional `connections` and `env_key`). `database.yml` or `DATABASE_URL` may use `%{shard}` or `POLYRUN_SHARD_INDEX` suffixes—the same convention as `polyrun env` |
28
+ | External provisioning | Sometimes `db:prepare` plus shell clone scripts (multi-DB apps)—document ordering: prepare databases before `run-shards` |
29
+
30
+ ## 4. Prepare (run once before workers)
31
+
32
+ | Field | Options |
33
+ |--------|---------|
34
+ | None | Typical for gems without assets |
35
+ | Assets | `Polyrun::Prepare::Assets`, `prepare.recipe: assets` or `default`, digest markers |
36
+ | Playwright / browsers | Install once in prepare; workers skip reinstall (`SKIP_*` env flags in app code if needed) |
37
+ | Custom shell | `prepare.recipe: shell` with `prepare.command:`—must not repeat heavy work inside each worker |
38
+
39
+ Rule: anything expensive (compile, `yarn`, Playwright download) belongs in prepare or a CI cache step, not in `before(:suite)` per worker unless gated by `POLYRUN_SHARD_TOTAL` or similar env.
40
+
41
+ ## 5. Spec list and ordering
42
+
43
+ | Field | Options |
44
+ |--------|---------|
45
+ | Plain glob | `partition.paths_build.all_glob: spec/**/*_spec.rb` and empty or minimal `stages` |
46
+ | Ordered stages | `partition.paths_build.stages`: regex (e.g. slow integration first) or `sort_by_substring_order` for stable ordering |
47
+
48
+ Refresh list: `polyrun -c polyrun.yml build-paths` (also runs automatically before `plan` / `run-shards` when configured).
49
+
50
+ ## 6. Coverage and CI reports
51
+
52
+ | Field | Options |
53
+ |--------|---------|
54
+ | Collector | `require "polyrun"` plus `Polyrun::Coverage::Collector.start!` in `spec_helper` (non-Rails gems) |
55
+ | Rails | `require "polyrun/coverage/rails"` (or documented Rails integration) in `spec_helper` / `test_helper` |
56
+ | Fragments | Per shard: `coverage/polyrun-fragment-<shard>.json` |
57
+ | Merge | `polyrun merge-coverage` on fragments → merged JSON; then `polyrun report-coverage` (formats: json, lcov, cobertura, console, html, …) |
58
+ | JUnit | `polyrun report-junit` from RSpec JSON if needed |
59
+
60
+ ## 7. `polyrun.yml` as contract — adapters
61
+
62
+ After `polyrun.yml` is fixed, add or adjust adapters (same shard semantics):
63
+
64
+ | Adapter | Role |
65
+ |---------|------|
66
+ | `bin/rspec_parallel` / `bin/rspec_ci_shard` | Thin wrappers: prepare, start, or plan + rspec |
67
+ | `bin/polyrun-rspec` | Single shard (`env` + plan + rspec), e.g. one matrix cell or ad-hoc run |
68
+ | `config/database.yml` | ERB / suffixes matching `databases.shard_db_pattern` |
69
+ | Prepare script | Referenced from `prepare.command` |
70
+ | `POLYRUN.md` (or `spec/README`) | Canonical commands for this repo; CI model (below) |
71
+
72
+ Bot workflow: read and write `polyrun.yml` first, then generate or patch wrappers and docs to match.
73
+
74
+ ## 8. CI models (document one in `POLYRUN.md`)
75
+
76
+ Model A — one job, N worker processes
77
+
78
+ - Command: `polyrun -c polyrun.yml parallel-rspec --workers N` (or `polyrun start`). Global `-c` / `-v` / `-h` must appear before the subcommand.
79
+ - Coverage fragments appear on the same runner; `merge-coverage` can run in the same job after workers finish.
80
+
81
+ Model B — matrix of jobs (one shard per job)
82
+
83
+ - Each job sets `POLYRUN_SHARD_INDEX` / `POLYRUN_SHARD_TOTAL` (and DB URLs per shard if needed).
84
+ - Run `polyrun build-paths`, `polyrun plan`, then `bundle exec rspec` (or `bin/rspec_ci_shard`) for that shard only.
85
+ - Upload `coverage/polyrun-fragment-*.json` (or named per shard).
86
+ - A final `merge-coverage` job downloads artifacts and merges.
87
+
88
+ GitHub Actions does not set `CI_NODE_INDEX` / `CI_NODE_TOTAL` by default—set `POLYRUN_*` explicitly in the matrix.
89
+
90
+ ## 9. Chatbot / agent context blocks (high signal)
91
+
92
+ 1. Upstream README sections: partition, prepare, databases, merge-coverage, `Env::Ci` (shard detection).
93
+ 2. Project `polyrun.yml` (filled in) plus one of: `POLYRUN.md` or `spec/README.md` with the canonical test command and CI model (A or B).
94
+ 3. Exact `Gemfile` / Appraisal lines for `polyrun` and any Docker or path notes.
95
+
96
+ ## 10. Scaffold from this repo
97
+
98
+ ```bash
99
+ polyrun init --list
100
+ polyrun init --profile gem -o polyrun.yml
101
+ polyrun init --profile rails -o polyrun.yml
102
+ polyrun init --profile ci-matrix -o polyrun.yml
103
+ polyrun init --profile doc -o POLYRUN.md # host-project doc template
104
+ ```
105
+
106
+ Templates live under `lib/polyrun/templates/` in the gem. See `examples/templates/README.md` for profile descriptions.
@@ -0,0 +1,150 @@
1
+ require "json"
2
+ require "fileutils"
3
+ require "optparse"
4
+
5
+ require_relative "coverage_merge_io"
6
+
7
+ module Polyrun
8
+ class CLI
9
+ module CoverageCommands
10
+ include CoverageMergeIo
11
+
12
+ private
13
+
14
+ def cmd_merge_coverage(argv, _config_path)
15
+ inputs, output, formats = merge_coverage_parse_argv(argv)
16
+ if inputs.empty?
17
+ Polyrun::Log.warn "merge-coverage: need at least one existing -i FILE (after glob expansion)"
18
+ return 2
19
+ end
20
+
21
+ Polyrun::Log.warn "merge-coverage: merging #{inputs.size} fragment(s)" if @verbose
22
+ Polyrun::Debug.log_kv(
23
+ merge_coverage: "start",
24
+ output: output,
25
+ formats: formats,
26
+ input_paths: inputs
27
+ )
28
+ input_bytes = inputs.sum { |p| File.size(p) }
29
+ Polyrun::Debug.log("merge-coverage: input_bytes=#{input_bytes}")
30
+
31
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ r = merge_coverage_merge_fragments(inputs)
33
+ merged = r[:blob]
34
+ Polyrun::Debug.log("merge-coverage: merged_blob file_count=#{merged.size}")
35
+
36
+ payload = Polyrun::Coverage::Merge.to_simplecov_json(merged, meta: r[:meta], groups: r[:groups])
37
+ out_abs = File.expand_path(output)
38
+ merge_coverage_write_json_payload(out_abs, payload)
39
+ merge_coverage_write_format_outputs(merged, r, out_abs, formats)
40
+
41
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
42
+ merge_coverage_log_finish(elapsed, inputs)
43
+ 0
44
+ end
45
+
46
+ # Warn when wall time exceeds this many seconds (default 10). Set POLYRUN_MERGE_SLOW_WARN_SECONDS=0 to disable.
47
+ def merge_slow_warn_threshold_seconds
48
+ v = ENV["POLYRUN_MERGE_SLOW_WARN_SECONDS"]
49
+ return 10.0 if v.nil? || v.to_s.strip.empty?
50
+
51
+ s = v.to_s.strip.downcase
52
+ return nil if %w[0 false no].include?(s)
53
+
54
+ Float(v)
55
+ rescue ArgumentError
56
+ 10.0
57
+ end
58
+
59
+ def cmd_report_coverage(argv)
60
+ input = nil
61
+ output_dir = "coverage/polyrun"
62
+ basename = "polyrun-coverage"
63
+ formats = Polyrun::Coverage::Reporting::DEFAULT_FORMATS.dup
64
+
65
+ parser = OptionParser.new do |opts|
66
+ opts.banner = "usage: polyrun report-coverage -i FILE [-o DIR] [--basename NAME] [--format json,lcov,cobertura,console,html]"
67
+ opts.on("-i", "--input PATH", "Merged or raw SimpleCov JSON") { |v| input = v }
68
+ opts.on("-o", "--output DIR", "Output directory") { |v| output_dir = v }
69
+ opts.on("--basename NAME", "File name prefix") { |v| basename = v }
70
+ opts.on("--format LIST", String) { |v| formats = v.split(",").map(&:strip) }
71
+ end
72
+ parser.parse!(argv)
73
+ input ||= argv.first
74
+
75
+ unless input && File.file?(input)
76
+ Polyrun::Log.warn "report-coverage: need -i FILE or a path argument"
77
+ return 2
78
+ end
79
+
80
+ paths = Polyrun::Debug.time("report-coverage: write_from_json_file") do
81
+ Polyrun::Coverage::Reporting.write_from_json_file(
82
+ File.expand_path(input),
83
+ output_dir: File.expand_path(output_dir),
84
+ basename: basename,
85
+ formats: formats
86
+ )
87
+ end
88
+ Polyrun::Log.puts JSON.generate(paths.transform_keys(&:to_s))
89
+ 0
90
+ end
91
+
92
+ def merge_coverage_after_shards(output:, format_list:, config_path:)
93
+ files = merge_coverage_fragment_json_files
94
+ if files.empty?
95
+ Polyrun::Log.warn "polyrun run-shards: --merge-coverage: no coverage/polyrun-fragment-*.json found (enable Polyrun coverage in spec_helper?)"
96
+ return 0
97
+ end
98
+
99
+ merge_coverage_after_shards_log_start(files, output, format_list)
100
+ code = merge_coverage_after_shards_run_merge(files, output, format_list, config_path)
101
+ return code unless code == 0
102
+
103
+ merge_coverage_after_shards_strict_gate(output, code)
104
+ rescue JSON::ParserError => e
105
+ Polyrun::Log.warn "polyrun run-shards: merged coverage JSON parse failed: #{e.message}"
106
+ 1
107
+ end
108
+
109
+ def merge_coverage_fragment_json_files
110
+ pattern = File.join(Dir.pwd, "coverage", "polyrun-fragment-*.json")
111
+ Dir.glob(pattern).sort
112
+ end
113
+
114
+ def merge_coverage_after_shards_log_start(files, output, format_list)
115
+ Polyrun::Log.warn "polyrun run-shards: merging #{files.size} coverage fragment(s) → #{output}"
116
+ Polyrun::Debug.log("merge-coverage-after-shards: #{files.size} fragment(s) → #{output} format=#{format_list}")
117
+ Polyrun::Debug.log("merge-coverage-after-shards: fragments=#{files.join(", ")}")
118
+ end
119
+
120
+ def merge_coverage_after_shards_run_merge(files, output, format_list, config_path)
121
+ merge_argv = []
122
+ files.each { |f| merge_argv.push("-i", f) }
123
+ merge_argv += ["-o", output, "--format", format_list]
124
+ Polyrun::Debug.time("merge-coverage (parent after workers)") do
125
+ cmd_merge_coverage(merge_argv, config_path)
126
+ end
127
+ end
128
+
129
+ def merge_coverage_after_shards_strict_gate(output, code)
130
+ gate = coverage_minimum_line_gate_from_polyrun_coverage_yml
131
+ Polyrun::Debug.log_kv(
132
+ coverage_gate_config: "polyrun_coverage.yml",
133
+ gate: gate.inspect
134
+ )
135
+ return code if gate.nil? || !gate[:strict]
136
+
137
+ merged_path = File.expand_path(output)
138
+ unless File.file?(merged_path)
139
+ Polyrun::Log.warn "polyrun run-shards: --merge-coverage: expected merged JSON at #{merged_path} missing"
140
+ return 1
141
+ end
142
+
143
+ below = merge_coverage_min_line_gate_below?(merged_path, gate)
144
+ return 1 if below
145
+
146
+ code
147
+ end
148
+ end
149
+ end
150
+ end