polyrun 2.1.0 → 2.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ab4d8c8553db3e8e62e83967c9090fc7e788c994d450febb0dc64d39d946f3b
4
- data.tar.gz: 3edbc34c324880653c8fd6d7675a31193adfebb0f0a4439d33f74e4ab9d1c327
3
+ metadata.gz: 756cf8e1b8e2176c4097520752dfdd4b856d03b134a999462641f90a9f217e71
4
+ data.tar.gz: 284e536d886b4bfc913415b0e3301a31ca98ef3446da1b24163bcc6af485e642
5
5
  SHA512:
6
- metadata.gz: 1e372f737bf9d8cf76d39979f87dec6acd33e325ef1b5355acdbdb588d9542bc94d0fa0236eecb6632152c0b148560e489a9e26132bede885ddbb73628fa3830
7
- data.tar.gz: c61bedba0ea3f6399ffee3d217d719182de6c3e945fbbb14aea9c811f0b75e7e968b7e30a30162fb944635a949629e0102976d0f01e2851411b7694649ac8c5f
6
+ metadata.gz: 445f64e0e62be50ab6c63026f27ad8f5f093521bbeed4a05b0ce7adc6a13bcb0496f1489d8243b403f859826e599194c3399a761a9210cbbf5d8027f3fdba52e
7
+ data.tar.gz: 554a292c2c7ac40e3df11d886ca7a71c10b81f1fa144af00c5c1d20b950ff6d999306be1e3fa75a9982450b2d202bc2bee34d5201ab39dbb386b1b73b42f1403
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.1.2 (2026-07-05)
6
+
7
+ - Fix per-example timing JSON when RSpec supplies `absolute_file_path` metadata; fix formatter registration when `install_example_timing!` uses a custom output path.
8
+
9
+ ## 2.1.1 (2026-07-05)
10
+
11
+ - Fix HTML coverage report `Encoding::CompatibilityError` when source files contain UTF-8; read templates, assets, and source lines as UTF-8 in `formatters_html.rb`.
12
+
5
13
  ## 2.1.0 (2026-07-03)
6
14
 
7
15
  - Add experimental per-example spec quality (`Polyrun::SpecQuality`): `POLYRUN_SPEC_QUALITY=1`, worker JSONL fragments, `merge-spec-quality`, `report-spec-quality`, and `run-shards --merge-spec-quality`.
@@ -92,7 +92,7 @@ module Polyrun
92
92
  def merge_coverage_after_shards(output:, format_list:, config_path:)
93
93
  files = merge_coverage_fragment_json_files
94
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?)"
95
+ Polyrun::Log.warn "polyrun run-shards: --merge-coverage: no coverage fragments found under coverage (enable coverage collection in your test setup)"
96
96
  return 0
97
97
  end
98
98
 
@@ -57,7 +57,7 @@ module Polyrun
57
57
  pattern = Polyrun::Reporting::FailureMerge.default_fragment_glob
58
58
  files = Dir.glob(pattern).sort
59
59
  if files.empty?
60
- Polyrun::Log.warn "polyrun run-shards: --merge-failures: no #{Polyrun::Reporting::FailureMerge::FRAGMENT_GLOB} under fragment dir (enable Polyrun::RSpec.install_failure_fragments! in spec_helper?)"
60
+ Polyrun::Log.warn "polyrun run-shards: --merge-failures: no failure fragments found under tmp/polyrun_failures (enable failure fragments in your test setup)"
61
61
  return nil
62
62
  end
63
63
 
@@ -13,21 +13,19 @@ module Polyrun
13
13
  -h, --help
14
14
 
15
15
  Trace timing (stderr): DEBUG=1 or POLYRUN_DEBUG=1
16
- Branch coverage in JSON fragments: POLYRUN_COVERAGE_BRANCHES=1 (stdlib Coverage; merge-coverage merges branches)
17
- polyrun quick coverage: POLYRUN_COVERAGE=1 or (config/polyrun_coverage.yml + POLYRUN_QUICK_COVERAGE=1); POLYRUN_COVERAGE_DISABLE=1 skips
18
- Merge wall time (stderr): POLYRUN_PROFILE_MERGE=1 (or verbose / DEBUG)
16
+ Coverage: POLYRUN_COVERAGE=1 (or config/polyrun_coverage.yml + POLYRUN_QUICK_COVERAGE=1); POLYRUN_COVERAGE_DISABLE=1 skips; POLYRUN_COVERAGE_BRANCHES=1 for branch data in fragments
17
+ Merge profiling (stderr): POLYRUN_PROFILE_MERGE=1 (or verbose / DEBUG)
19
18
  Post-merge formats (run-shards): POLYRUN_MERGE_FORMATS (default: json,lcov,cobertura,console,html)
20
- Skip optional script/build_spec_paths.rb before start: POLYRUN_SKIP_BUILD_SPEC_PATHS=1
21
- Skip start auto-prepare / auto DB provision: POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
22
- Skip writing paths_file from partition.paths_build: POLYRUN_SKIP_PATHS_BUILD=1
23
- Warn if merge-coverage wall time exceeds N seconds (default 10): POLYRUN_MERGE_SLOW_WARN_SECONDS (0 disables)
24
- Failure fragments (run-shards --merge-failures): POLYRUN_MERGE_FAILURES=1; parent sets POLYRUN_FAILURE_FRAGMENTS=1 in workers; POLYRUN_FAILURE_FRAGMENT_DIR, POLYRUN_MERGED_FAILURES_OUT, POLYRUN_MERGED_FAILURES_FORMAT; after_suite sets POLYRUN_MERGED_FAILURES_PATH when merge ran
25
- Parallel RSpec workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start); distinct from POLYRUN_SHARD_PROCESSES / ci-shard --shard-processes (local processes per CI matrix job)
26
- Per-worker wall timeout: run-shards --worker-timeout SEC or POLYRUN_WORKER_TIMEOUT_SEC (max time since each worker spawn). Parent polls all live workers together. Exit 124; remaining workers stopped.
27
- Per-worker idle timeout: --worker-idle-timeout SEC or POLYRUN_WORKER_IDLE_TIMEOUT_SEC counts only after a successful ping timestamp (positive float in POLYRUN_WORKER_PING_FILE); empty or unreadable pings do not satisfy idle enforcement—use wall timeout until the first ping. RSpec/Minitest/Quick installers call Polyrun::WorkerPing.ping! per example/suite. Ping files live under tmp/polyrun/ (gitignored via tmp/); parent unlinks each after its worker exits. Exit 125. Optional outer cap: --worker-timeout (exit 124). Optional periodic pings: POLYRUN_WORKER_PING_THREAD=1 (POLYRUN_WORKER_PING_INTERVAL_SEC); WorkerPing.ensure_interval_ping_thread! (installers invoke it—call yourself if wiring workers without install_worker_ping!).
28
- If Polyrun::Log.stderr is null or redirected away, set POLYRUN_ORCHESTRATION_STDERR=1 to also print timeout/SIGINT summary lines to process stderr.
29
- Spec quality (opt-in): POLYRUN_SPEC_QUALITY=1; per-example JSONL fragments; POLYRUN_SPEC_QUALITY_SAMPLE=0.0-1.0 (default 1.0); POLYRUN_SPEC_QUALITY_STRICT=1; run-shards --merge-spec-quality sets POLYRUN_SPEC_QUALITY_FRAGMENTS=1 in workers; merge-spec-quality / report-spec-quality
30
- Partition timing granularity (default file): POLYRUN_TIMING_GRANULARITY=file|example (experimental per-example; see partition.timing_granularity)
19
+ Start skips: POLYRUN_SKIP_BUILD_SPEC_PATHS=1, POLYRUN_START_SKIP_PREPARE=1, POLYRUN_START_SKIP_DATABASES=1
20
+ Paths build skip: POLYRUN_SKIP_PATHS_BUILD=1
21
+ Slow merge warning (seconds, default 10; 0 disables): POLYRUN_MERGE_SLOW_WARN_SECONDS
22
+ Failure merge: run-shards --merge-failures (enable failure fragments in test setup); POLYRUN_MERGE_FAILURES=1, POLYRUN_FAILURE_FRAGMENT_DIR, POLYRUN_MERGED_FAILURES_OUT
23
+ Parallel workers: POLYRUN_WORKERS default 5, max 10 (run-shards / parallel-rspec / start). CI local processes per job: POLYRUN_SHARD_PROCESSES or ci-shard --shard-processes (not POLYRUN_WORKERS)
24
+ Per-worker wall timeout: --worker-timeout SEC or POLYRUN_WORKER_TIMEOUT_SEC. Exit 124; parent stops remaining workers.
25
+ Per-worker idle timeout: --worker-idle-timeout SEC or POLYRUN_WORKER_IDLE_TIMEOUT_SEC after a progress ping (POLYRUN_WORKER_PING_FILE). Enable pings in test setup. Exit 125. Optional periodic pings: POLYRUN_WORKER_PING_THREAD=1 (POLYRUN_WORKER_PING_INTERVAL_SEC).
26
+ Orchestration warnings on process stderr: POLYRUN_ORCHESTRATION_STDERR=1
27
+ Spec quality (opt-in): POLYRUN_SPEC_QUALITY=1; run-shards --merge-spec-quality; merge-spec-quality / report-spec-quality
28
+ Partition timing granularity (default file): POLYRUN_TIMING_GRANULARITY=file|example (experimental; see partition.timing_granularity)
31
29
  Partition strategies: round_robin (default, sorted), preserve_order_round_robin (paths-file order), lazy_robin (sorted RR + timing diagnostics), cost_binpack (LPT), hrw. partition.timing_file without strategy implies cost_binpack.
32
30
 
33
31
  commands:
@@ -45,7 +43,7 @@ module Polyrun
45
43
  init write a starter polyrun.yml or POLYRUN.md from built-in templates (see docs/SETUP_PROFILE.md)
46
44
  queue file-backed batch queue: init (optional --shard/--total etc. as plan, then claim/ack/reclaim/status --json)
47
45
  run-queue init queue and run N workers that claim batches until drained
48
- quick run Polyrun::Quick (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
46
+ quick quick test runner (describe/it, before/after, let, expect…to, assert_*; optional capybara!)
49
47
  hook run <phase> run one shell hook from polyrun.yml hooks: (e.g. before_suite); optional --shard/--total
50
48
  report-coverage write all coverage formats from one JSON file
51
49
  report-junit RSpec JSON or Polyrun testcase JSON → JUnit XML (CI)
@@ -53,7 +51,7 @@ module Polyrun
53
51
  merge-timing merge polyrun_timing_*.json shards
54
52
  merge-spec-quality merge polyrun-spec-quality-fragment-*.jsonl shards
55
53
  report-spec-quality spec quality report from merged JSON (zero-hit, hot lines, churn)
56
- config print effective config by dotted path (see Polyrun::Config::Effective; same tree as YAML plus merged prepare.env, resolved partition shard fields, workers)
54
+ config print effective config by dotted path (loaded YAML plus merged prepare.env, resolved partition shard fields, workers)
57
55
  env print shard + database env (see polyrun.yml databases)
58
56
  db:setup-template migrate template DB (PostgreSQL)
59
57
  db:setup-shard CREATE DATABASE shard FROM template (one POLYRUN_SHARD_INDEX)
@@ -2,6 +2,10 @@
2
2
  module Polyrun
3
3
  class CLI
4
4
  # Wait, wall/idle timeout, and +after_shard+ hooks for parallel workers (+run-shards+ / +ci-shard-*+).
5
+ #
6
+ # Per-worker wait states: running → normal_exit | wall_timeout (124) | idle_timeout (125).
7
+ # When wall or idle caps are set, the multiplex loop polls every live PID each tick so timeouts
8
+ # apply fairly across shards (not only the worker currently in Process.wait).
5
9
  module RunShardsParallelWait
6
10
  WORKER_TIMEOUT_EXIT_STATUS = 124
7
11
  WORKER_IDLE_TIMEOUT_EXIT_STATUS = 125
@@ -215,7 +219,7 @@ module Polyrun
215
219
  ping_suffix = (loc && !loc.to_s.strip.empty?) ? "; last ping #{loc.to_s.strip}" : ""
216
220
  Polyrun::Log.orchestration_warn "polyrun run-shards: WORKER IDLE TIMEOUT after #{idle_sec}s since last per-example progress ping — shard #{h[:shard]} pid #{h[:pid]}#{ping_suffix}."
217
221
  Polyrun::Log.warn "polyrun run-shards: idle shard file sample: #{sample}#{suffix}"
218
- Polyrun::Log.warn "polyrun run-shards: use Polyrun::RSpec.install_worker_ping! / Polyrun::Minitest.install_worker_ping! (Polyrun Quick calls ping! each example); exit #{WORKER_IDLE_TIMEOUT_EXIT_STATUS}."
222
+ Polyrun::Log.warn "polyrun run-shards: enable per-example worker progress pings in your test setup so idle timeouts reflect real work; exit #{WORKER_IDLE_TIMEOUT_EXIT_STATUS}."
219
223
  end
220
224
 
221
225
  def run_shards_wait_or_force_stop_status(pid)
@@ -48,7 +48,7 @@ module Polyrun
48
48
  opts.banner = "usage: polyrun run-shards [--workers N] [--worker-timeout SEC] [--worker-idle-timeout SEC] [--strategy NAME] [--paths-file P] [--timing P] [--timing-granularity VAL] [--constraints P] [--seed S] [--merge-coverage] [--merge-output P] [--merge-format LIST] [--merge-failures] [--merge-failures-output P] [--merge-failures-format jsonl|json] [--merge-spec-quality] [--merge-spec-quality-output P] [--no-report-spec-quality] [--] <command> [args...]"
49
49
  opts.on("--workers N", Integer) { |v| st[:workers] = v }
50
50
  opts.on("--worker-timeout SEC", Float, "Max seconds per worker since spawn (also POLYRUN_WORKER_TIMEOUT_SEC); kills stuck workers (exit 124)") { |v| st[:worker_timeout_sec] = v }
51
- opts.on("--worker-idle-timeout SEC", Float, "Max seconds since last valid WorkerPing timestamp in POLYRUN_WORKER_PING_FILE (needs prior ping); RSpec/Minitest: install_worker_ping!; Quick: automatic; exit 125") { |v| st[:worker_idle_timeout_sec] = v }
51
+ opts.on("--worker-idle-timeout SEC", Float, "Max seconds since last worker progress ping (POLYRUN_WORKER_PING_FILE); enable pings in test setup; exit 125") { |v| st[:worker_idle_timeout_sec] = v }
52
52
  opts.on("--strategy NAME", String) do |v|
53
53
  st[:strategy] = v
54
54
  st[:strategy_explicit] = true
@@ -61,10 +61,10 @@ module Polyrun
61
61
  opts.on("--merge-coverage", "After success, merge coverage/polyrun-fragment-*.json (Polyrun coverage must be enabled)") { st[:merge_coverage] = true }
62
62
  opts.on("--merge-output PATH", String) { |v| st[:merge_output] = v }
63
63
  opts.on("--merge-format LIST", String) { |v| st[:merge_format] = v }
64
- opts.on("--merge-failures", "After all workers exit, merge tmp/polyrun_failures/polyrun-failure-fragment-*.jsonl (use Polyrun::RSpec.install_failure_fragments!)") { st[:merge_failures] = true }
64
+ opts.on("--merge-failures", "After all workers exit, merge failure fragments from tmp/polyrun_failures (requires failure fragments in test setup)") { st[:merge_failures] = true }
65
65
  opts.on("--merge-failures-output PATH", String) { |v| st[:merge_failures_output] = v }
66
66
  opts.on("--merge-failures-format VAL", "jsonl (default) or json") { |v| st[:merge_failures_format] = v }
67
- opts.on("--merge-spec-quality", "After workers exit, merge coverage/polyrun-spec-quality-fragment-*.jsonl (POLYRUN_SPEC_QUALITY in workers)") { st[:merge_spec_quality] = true }
67
+ opts.on("--merge-spec-quality", "After workers exit, merge spec-quality fragments from coverage (enable spec-quality collection in test setup)") { st[:merge_spec_quality] = true }
68
68
  opts.on("--merge-spec-quality-output PATH", String) { |v| st[:merge_spec_quality_output] = v }
69
69
  opts.on("--no-report-spec-quality", "Skip printing spec-quality report after merge") { st[:report_spec_quality] = false }
70
70
  end
@@ -162,7 +162,7 @@ module Polyrun
162
162
  Polyrun::Log.warn "polyrun run-shards: shard #{s} re-run (same spec list, no interleave): #{rerun}"
163
163
  end
164
164
  unless merge_failures
165
- Polyrun::Log.warn "polyrun run-shards: one merged failure report use run-shards --merge-failures with Polyrun::RSpec.install_failure_fragments!; POLYRUN_MERGED_FAILURES_PATH is set on after_suite when merge runs."
165
+ Polyrun::Log.warn "polyrun run-shards: for one combined failure report, add --merge-failures (requires failure fragments enabled in your test setup)"
166
166
  end
167
167
  end
168
168
  end
@@ -96,7 +96,7 @@ module Polyrun
96
96
  def merge_spec_quality_after_shards(ctx)
97
97
  files = merge_spec_quality_fragment_files
98
98
  if files.empty?
99
- Polyrun::Log.warn "polyrun run-shards: --merge-spec-quality: no coverage/polyrun-spec-quality-fragment-*.jsonl found (enable POLYRUN_SPEC_QUALITY in spec_helper?)"
99
+ Polyrun::Log.warn "polyrun run-shards: --merge-spec-quality: no spec-quality fragments found under coverage (enable spec-quality collection in your test setup)"
100
100
  return nil
101
101
  end
102
102
 
@@ -23,14 +23,14 @@ module Polyrun
23
23
  )
24
24
  file_list_html = render_html_partial("file_list", file_rows_html: files.map { |file| html_file_list_row(file) }.join("\n"))
25
25
  file_sections_html = files.map { |file| render_html_partial("file_section", file: file) }.join("\n")
26
- ERB.new(File.read(html_template_path), trim_mode: "-").result_with_hash(
26
+ ERB.new(File.read(html_template_path, encoding: Encoding::UTF_8), trim_mode: "-").result_with_hash(
27
27
  title: CGI.escapeHTML(title.to_s),
28
28
  generated_label: html_generated_label(generated_at),
29
29
  overview_html: overview_html,
30
30
  file_list_html: file_list_html,
31
31
  file_sections_html: file_sections_html,
32
- stylesheet: File.read(html_stylesheet_path),
33
- javascript: File.read(html_javascript_path)
32
+ stylesheet: File.read(html_stylesheet_path, encoding: Encoding::UTF_8),
33
+ javascript: File.read(html_javascript_path, encoding: Encoding::UTF_8)
34
34
  )
35
35
  end
36
36
  # rubocop:enable Metrics/AbcSize
@@ -56,7 +56,7 @@ module Polyrun
56
56
  end
57
57
 
58
58
  def render_html_partial(name, locals = {})
59
- ERB.new(File.read(html_partial_path(name)), trim_mode: "-").result_with_hash(locals)
59
+ ERB.new(File.read(html_partial_path(name), encoding: Encoding::UTF_8), trim_mode: "-").result_with_hash(locals)
60
60
  end
61
61
 
62
62
  def html_file_payload(path, file, root)
@@ -152,7 +152,7 @@ module Polyrun
152
152
  def html_source_lines(path, fallback_length)
153
153
  return Array.new(fallback_length, "") unless File.file?(path.to_s)
154
154
 
155
- File.readlines(path.to_s, chomp: true)
155
+ File.readlines(path.to_s, chomp: true, encoding: Encoding::UTF_8)
156
156
  rescue Errno::ENOENT, Errno::EACCES, ArgumentError
157
157
  Array.new(fallback_length, "")
158
158
  end
@@ -6,6 +6,13 @@ require "time"
6
6
  module Polyrun
7
7
  module Queue
8
8
  # File-backed queue (spec_queue.md): +queue.json+, +pending/*.json+ chunks, +done.jsonl+, +leases.json+ (OS flock).
9
+ #
10
+ # Path lifecycle (lease transitions):
11
+ # init! → paths in pending chunks only
12
+ # claim! → pending −batch → active lease in leases.json
13
+ # ack! → lease removed; paths appended to done.jsonl
14
+ # reclaim! → stale or matching lease → paths returned to pending
15
+ # reclaim_lease! → one lease by id → paths returned to pending
9
16
  class FileStore
10
17
  CHUNK_SIZE = 500
11
18
 
data/lib/polyrun/rspec.rb CHANGED
@@ -21,6 +21,8 @@ module Polyrun
21
21
  if output_path
22
22
  op = output_path
23
23
  Class.new(Polyrun::Timing::RSpecExampleFormatter) do
24
+ ::RSpec::Core::Formatters.register self, :example_finished, :close
25
+
24
26
  define_method(:timing_output_path) { op }
25
27
  end
26
28
  else
@@ -1,6 +1,6 @@
1
1
  require "json"
2
2
 
3
- require "rspec/core/formatters/base_formatter"
3
+ require "rspec/core/formatters"
4
4
 
5
5
  module Polyrun
6
6
  module Timing
@@ -13,23 +13,23 @@ module Polyrun
13
13
  # Or {Polyrun::RSpec.install_example_timing!} (+output_path:+ avoids touching +ENV+).
14
14
  #
15
15
  # Default output path: +ENV["POLYRUN_EXAMPLE_TIMING_OUT"]+ if set, else +polyrun_timing_examples.json+.
16
- class RSpecExampleFormatter < RSpec::Core::Formatters::BaseFormatter
17
- RSpec::Core::Formatters.register self, :example_finished, :close
16
+ class RSpecExampleFormatter
17
+ ::RSpec::Core::Formatters.register self, :example_finished, :close
18
18
 
19
19
  def initialize(output)
20
- super
20
+ @output = output
21
21
  @times = {}
22
22
  end
23
23
 
24
24
  def example_finished(notification)
25
25
  ex = notification.example
26
- result = ex.execution_result
27
- return if result.pending?
26
+ return if ex.pending?
28
27
 
28
+ result = ex.execution_result
29
29
  t = result.run_time
30
30
  return unless t
31
31
 
32
- path = ex.metadata[:absolute_path]
32
+ path = example_absolute_path(ex)
33
33
  return unless path
34
34
 
35
35
  line = ex.metadata[:line_number]
@@ -48,6 +48,13 @@ module Polyrun
48
48
  def timing_output_path
49
49
  ENV["POLYRUN_EXAMPLE_TIMING_OUT"] || "polyrun_timing_examples.json"
50
50
  end
51
+
52
+ private
53
+
54
+ def example_absolute_path(example)
55
+ metadata = example.metadata
56
+ metadata[:absolute_file_path] || metadata[:file_path] || metadata[:absolute_path]
57
+ end
51
58
  end
52
59
  end
53
60
  end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "2.1.0"
2
+ VERSION = "2.1.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov