polyrun 1.4.1 → 1.5.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/polyrun/cli/ci_shard_hooks.rb +12 -4
  4. data/lib/polyrun/cli/ci_shard_run_command.rb +3 -1
  5. data/lib/polyrun/cli/help.rb +3 -0
  6. data/lib/polyrun/cli/helpers.rb +22 -0
  7. data/lib/polyrun/cli/run_shards_parallel_children.rb +26 -34
  8. data/lib/polyrun/cli/run_shards_parallel_wait.rb +267 -0
  9. data/lib/polyrun/cli/run_shards_plan_boot_phases.rb +34 -1
  10. data/lib/polyrun/cli/run_shards_plan_options.rb +6 -2
  11. data/lib/polyrun/cli/run_shards_run.rb +7 -33
  12. data/lib/polyrun/cli/run_shards_worker_interrupt.rb +75 -0
  13. data/lib/polyrun/coverage/collector_finish.rb +3 -2
  14. data/lib/polyrun/coverage/formatter.rb +2 -1
  15. data/lib/polyrun/coverage/merge/formatters_html.rb +191 -43
  16. data/lib/polyrun/coverage/merge/html/_file_list.html.erb +21 -0
  17. data/lib/polyrun/coverage/merge/html/_file_section.html.erb +26 -0
  18. data/lib/polyrun/coverage/merge/html/_groups_table.html.erb +18 -0
  19. data/lib/polyrun/coverage/merge/html/_overview.html.erb +47 -0
  20. data/lib/polyrun/coverage/merge/html/report.css +147 -0
  21. data/lib/polyrun/coverage/merge/html/report.js +48 -0
  22. data/lib/polyrun/coverage/merge/html/template.html.erb +30 -0
  23. data/lib/polyrun/coverage/track_files.rb +9 -0
  24. data/lib/polyrun/hooks.rb +9 -1
  25. data/lib/polyrun/log.rb +16 -0
  26. data/lib/polyrun/minitest.rb +34 -0
  27. data/lib/polyrun/quick/example_runner.rb +11 -0
  28. data/lib/polyrun/rspec.rb +18 -0
  29. data/lib/polyrun/version.rb +1 -1
  30. data/lib/polyrun/worker_ping.rb +74 -0
  31. data/sig/polyrun/minitest.rbs +2 -0
  32. data/sig/polyrun/rspec.rbs +4 -0
  33. data/sig/polyrun/worker_ping.rbs +10 -0
  34. metadata +12 -1
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <title><%= title %></title>
6
+ <style>
7
+ <%= stylesheet %>
8
+ </style>
9
+ </head>
10
+ <body>
11
+ <div id="wrapper">
12
+ <div class="topbar">
13
+ <h1><%= title %></h1>
14
+ <div class="timestamp">Generated <%= generated_label %></div>
15
+ </div>
16
+
17
+ <%= overview_html %>
18
+
19
+ <%= file_list_html %>
20
+
21
+ <div id="source-files">
22
+ <%= file_sections_html %>
23
+ </div>
24
+ </div>
25
+
26
+ <script>
27
+ <%= javascript %>
28
+ </script>
29
+ </body>
30
+ </html>
@@ -22,6 +22,15 @@ module Polyrun
22
22
  end.map { |rel| File.expand_path(rel, root) }.uniq
23
23
  end
24
24
 
25
+ # Keeps only loaded files that match configured +track_files+ globs.
26
+ def keep_tracked_files(blob, root, track_files)
27
+ tracked = expand_globs(root, track_files).each_with_object({}) { |abs, acc| acc[abs] = true }
28
+ blob.each_with_object({}) do |(path, entry), out|
29
+ abs = File.expand_path(path.to_s)
30
+ out[abs] = entry if tracked[abs]
31
+ end
32
+ end
33
+
25
34
  # Adds tracked files that were never required, with simulated line arrays (blank/comment => nil, else 0).
26
35
  # Matches SimpleCov +add_not_loaded_files+ behavior for coverage completeness.
27
36
  def merge_untracked_into_blob(blob, root, track_files)
data/lib/polyrun/hooks.rb CHANGED
@@ -118,6 +118,9 @@ module Polyrun
118
118
  if reg&.any?(phase)
119
119
  begin
120
120
  reg.run(phase, merged)
121
+ rescue Interrupt
122
+ Polyrun::Log.warn "polyrun hooks: #{phase} ruby hook interrupted"
123
+ return 130
121
124
  rescue => e
122
125
  Polyrun::Log.warn "polyrun hooks: #{phase} ruby hook failed: #{e.class}: #{e.message}"
123
126
  return 1
@@ -125,7 +128,12 @@ module Polyrun
125
128
  end
126
129
 
127
130
  commands_for(phase).each do |cmd|
128
- ok = system(merged, "sh", "-c", cmd)
131
+ ok = begin
132
+ system(merged, "sh", "-c", cmd)
133
+ rescue Interrupt
134
+ Polyrun::Log.warn "polyrun hooks: #{phase} shell hook interrupted"
135
+ return 130
136
+ end
129
137
  return $?.exitstatus unless ok
130
138
  end
131
139
  0
data/lib/polyrun/log.rb CHANGED
@@ -6,6 +6,9 @@ module Polyrun
6
6
  #
7
7
  # Polyrun::Log.stderr = Logger.new($stderr)
8
8
  # Polyrun::Log.stdout = StringIO.new
9
+ #
10
+ # Orchestration (+orchestration_warn+): worker timeout and SIGINT lines use the same sink as +warn+ unless
11
+ # +POLYRUN_ORCHESTRATION_STDERR=1+ and stderr is not process +$stderr+ (then the summary is copied to +$stderr+).
9
12
  module Log
10
13
  class << self
11
14
  attr_writer :stderr
@@ -25,6 +28,19 @@ module Polyrun
25
28
  emit_line(stderr, msg)
26
29
  end
27
30
 
31
+ # Like {#warn}, and when +POLYRUN_ORCHESTRATION_STDERR=1+ and {#stderr} is not the process +$stderr+,
32
+ # also writes one line to +$stderr+ so timeout/interrupt attribution survives custom/null Log sinks.
33
+ def orchestration_warn(msg)
34
+ warn(msg)
35
+ return unless %w[1 true yes].include?(ENV["POLYRUN_ORCHESTRATION_STDERR"]&.downcase)
36
+ return if stderr.equal?($stderr)
37
+
38
+ # Intentionally the real stderr stream (+Kernel#warn+ routes through +Log.stderr+).
39
+ # rubocop:disable Style/StderrPuts
40
+ $stderr.puts(msg.to_s.chomp)
41
+ # rubocop:enable Style/StderrPuts
42
+ end
43
+
28
44
  def puts(msg = "")
29
45
  if msg.nil?
30
46
  stdout.write("\n")
@@ -7,11 +7,45 @@ module Polyrun
7
7
  # after Rails / DB configuration (same timing as a direct call to
8
8
  # {Data::ParallelProvisioning.run_suite_hooks!}).
9
9
  module Minitest
10
+ module WorkerPingTestHook
11
+ def setup
12
+ Polyrun::WorkerPing.ping!(location: polyrun_minitest_location)
13
+ super
14
+ end
15
+
16
+ def teardown
17
+ super
18
+ Polyrun::WorkerPing.ping!(location: polyrun_minitest_location)
19
+ end
20
+
21
+ private
22
+
23
+ def polyrun_minitest_location
24
+ file, line = method(name).source_location
25
+ (file && line) ? "#{file}:#{line}" : nil
26
+ rescue NameError
27
+ nil
28
+ end
29
+ end
30
+
10
31
  module_function
11
32
 
12
33
  # Runs {Data::ParallelProvisioning.run_suite_hooks!} (serial vs shard worker hooks).
13
34
  def install_parallel_provisioning!
14
35
  Polyrun::Data::ParallelProvisioning.run_suite_hooks!
15
36
  end
37
+
38
+ # Same ping semantics as {RSpec.install_worker_ping!}: +ping!+ at test +setup+ and +teardown+.
39
+ # Requires +minitest+ to be loaded first (+Minitest::Test+ defined).
40
+ def install_worker_ping!
41
+ require_relative "worker_ping"
42
+ unless defined?(::Minitest::Test)
43
+ Polyrun::Log.warn "polyrun minitest: install_worker_ping! skipped (load minitest/autorun or minitest/test first)"
44
+ return
45
+ end
46
+
47
+ ::Minitest::Test.send(:prepend, WorkerPingTestHook)
48
+ Polyrun::WorkerPing.ensure_interval_ping_thread!
49
+ end
16
50
  end
17
51
  end
@@ -1,7 +1,10 @@
1
+ require_relative "../worker_ping"
1
2
  require_relative "assertions"
2
3
  require_relative "errors"
3
4
  require_relative "matchers"
4
5
 
6
+ Polyrun::WorkerPing.ensure_interval_ping_thread!
7
+
5
8
  module Polyrun
6
9
  module Quick
7
10
  # Per-example execution: merged lets, hooks, assertions, optional Capybara::DSL.
@@ -20,6 +23,8 @@ module Polyrun
20
23
  define_let_methods!
21
24
  run_let_bangs_from_chain
22
25
  extend_capybara_if_enabled!
26
+ qloc = quick_example_location(block)
27
+ Polyrun::WorkerPing.ping!(location: qloc)
23
28
  begin
24
29
  run_before_hooks_from_chain(ancestor_chain)
25
30
  instance_eval(&block)
@@ -32,11 +37,17 @@ module Polyrun
32
37
  run_after_hooks_from_chain(ancestor_chain)
33
38
  reset_capybara_if_enabled!
34
39
  @_let_cache = {}
40
+ Polyrun::WorkerPing.ping!(location: qloc)
35
41
  end
36
42
  end
37
43
 
38
44
  private
39
45
 
46
+ def quick_example_location(block)
47
+ loc = block&.source_location
48
+ loc ? "#{loc[0]}:#{loc[1]}" : nil
49
+ end
50
+
40
51
  def merge_lets_from_chain(ancestor_chain)
41
52
  @merged_lets = {}
42
53
  ancestor_chain.each do |g|
data/lib/polyrun/rspec.rb CHANGED
@@ -44,5 +44,23 @@ module Polyrun
44
44
  config.add_formatter Polyrun::Reporting::RspecFailureFragmentFormatter
45
45
  end
46
46
  end
47
+
48
+ # Writes {WorkerPing} after suite start, before/after each example (+location+ is file:line from metadata).
49
+ # Keeps +--worker-idle-timeout+ sensitive to example progress (not only a background thread).
50
+ def install_worker_ping!
51
+ require "rspec/core"
52
+ require_relative "worker_ping"
53
+ ::RSpec.configure do |config|
54
+ config.before(:suite) { Polyrun::WorkerPing.ping! }
55
+ config.before(:each) do |example|
56
+ Polyrun::WorkerPing.ping!(location: example.metadata[:location] || example.location)
57
+ end
58
+ config.after(:each) do |example|
59
+ Polyrun::WorkerPing.ping!(location: example.metadata[:location] || example.location)
60
+ end
61
+ end
62
+
63
+ Polyrun::WorkerPing.ensure_interval_ping_thread!
64
+ end
47
65
  end
48
66
  end
@@ -1,3 +1,3 @@
1
1
  module Polyrun
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  end
@@ -0,0 +1,74 @@
1
+ module Polyrun
2
+ # Writes a monotonic timestamp to +POLYRUN_WORKER_PING_FILE+ when the test process advances
3
+ # (typically once per example). When +location:+ is passed (path:line of the example), the file
4
+ # is two lines: timestamp, then that string. Parents use +--worker-idle-timeout+ to detect a worker with no
5
+ # progress *inside* a single example—unlike a background thread, +ping!+ does not run while Ruby
6
+ # is busy on the main thread, so a tight CPU loop or stuck native code leaves the timestamp stale.
7
+ #
8
+ # Prefer framework installs (call from helpers *after* loading the runner):
9
+ #
10
+ # require "polyrun/rspec"
11
+ # Polyrun::RSpec.install_worker_ping!
12
+ #
13
+ # require "polyrun/minitest"
14
+ # Polyrun::Minitest.install_worker_ping!
15
+ #
16
+ # Polyrun Quick runs +ping!+ automatically when requiring the Quick stack.
17
+ #
18
+ # Optional interval thread (+POLYRUN_WORKER_PING_THREAD=1+, +POLYRUN_WORKER_PING_INTERVAL_SEC+): call
19
+ # {#ensure_interval_ping_thread!} once at worker startup if you rely on periodic pings without per-example {#ping!};
20
+ # installers call this so the env toggle works out of the box.
21
+ module WorkerPing
22
+ class << self
23
+ def ping!(location: nil)
24
+ path = ping_file_path
25
+ return if path.empty?
26
+
27
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_s
28
+ loc = location.to_s.strip
29
+ payload = loc.empty? ? t : "#{t}\n#{loc}"
30
+ File.binwrite(path, payload)
31
+ rescue SystemCallError
32
+ # best-effort
33
+ end
34
+
35
+ def ping_file_path
36
+ ENV["POLYRUN_WORKER_PING_FILE"].to_s.strip
37
+ end
38
+
39
+ # Starts a periodic +ping!+ thread when +POLYRUN_WORKER_PING_THREAD+ is truthy and +POLYRUN_WORKER_PING_FILE+ is set.
40
+ # Prefer per-example {#ping!}; safe to call more than once (idempotent).
41
+ # rubocop:disable ThreadSafety/ClassInstanceVariable -- idempotent once-per-process latch
42
+ def ensure_interval_ping_thread!
43
+ thread_flag = ENV["POLYRUN_WORKER_PING_THREAD"]
44
+ return unless %w[1 true yes].include?(thread_flag&.downcase)
45
+
46
+ path = ping_file_path
47
+ return if path.empty?
48
+
49
+ @interval_ping_mx ||= Mutex.new
50
+ @interval_ping_mx.synchronize do
51
+ return if @interval_ping_started
52
+
53
+ raw = ENV["POLYRUN_WORKER_PING_INTERVAL_SEC"].to_s.strip
54
+ interval = Float(raw.empty? ? "15" : raw, exception: false) || 15.0
55
+ interval = 1.0 if interval < 1.0
56
+
57
+ ping!
58
+ # rubocop:disable ThreadSafety/NewThread -- optional periodic ping alongside per-example ping!
59
+ Thread.new do
60
+ loop do
61
+ sleep(interval)
62
+ ping!
63
+ rescue SystemCallError, Interrupt
64
+ break
65
+ end
66
+ end
67
+ # rubocop:enable ThreadSafety/NewThread
68
+ @interval_ping_started = true
69
+ end
70
+ end
71
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
72
+ end
73
+ end
74
+ end
@@ -1,5 +1,7 @@
1
1
  module Polyrun
2
2
  module Minitest
3
3
  def self.install_parallel_provisioning!: () -> void
4
+
5
+ def self.install_worker_ping!: () -> void
4
6
  end
5
7
  end
@@ -3,5 +3,9 @@ module Polyrun
3
3
  def self.install_parallel_provisioning!: (untyped rspec_config) -> void
4
4
 
5
5
  def self.install_example_timing!: (?output_path: String? ) -> void
6
+
7
+ def self.install_failure_fragments!: (?only_if: untyped?) -> void
8
+
9
+ def self.install_worker_ping!: () -> void
6
10
  end
7
11
  end
@@ -0,0 +1,10 @@
1
+ module Polyrun
2
+ module WorkerPing
3
+ def self.ping!: (?location: String?) -> void
4
+
5
+ def self.ping_file_path: () -> String
6
+
7
+ # Idempotent — installers call once; POLYRUN_WORKER_PING_THREAD gates the periodic thread.
8
+ def self.ensure_interval_ping_thread!: () -> void
9
+ end
10
+ 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: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -188,10 +188,12 @@ files:
188
188
  - lib/polyrun/cli/report_commands.rb
189
189
  - lib/polyrun/cli/run_shards_command.rb
190
190
  - lib/polyrun/cli/run_shards_parallel_children.rb
191
+ - lib/polyrun/cli/run_shards_parallel_wait.rb
191
192
  - lib/polyrun/cli/run_shards_plan_boot_phases.rb
192
193
  - lib/polyrun/cli/run_shards_plan_options.rb
193
194
  - lib/polyrun/cli/run_shards_planning.rb
194
195
  - lib/polyrun/cli/run_shards_run.rb
196
+ - lib/polyrun/cli/run_shards_worker_interrupt.rb
195
197
  - lib/polyrun/cli/start_bootstrap.rb
196
198
  - lib/polyrun/cli/timing_command.rb
197
199
  - lib/polyrun/config.rb
@@ -207,6 +209,13 @@ files:
207
209
  - lib/polyrun/coverage/merge.rb
208
210
  - lib/polyrun/coverage/merge/formatters.rb
209
211
  - lib/polyrun/coverage/merge/formatters_html.rb
212
+ - lib/polyrun/coverage/merge/html/_file_list.html.erb
213
+ - lib/polyrun/coverage/merge/html/_file_section.html.erb
214
+ - lib/polyrun/coverage/merge/html/_groups_table.html.erb
215
+ - lib/polyrun/coverage/merge/html/_overview.html.erb
216
+ - lib/polyrun/coverage/merge/html/report.css
217
+ - lib/polyrun/coverage/merge/html/report.js
218
+ - lib/polyrun/coverage/merge/html/template.html.erb
210
219
  - lib/polyrun/coverage/merge_fragment_meta.rb
211
220
  - lib/polyrun/coverage/merge_merge_two.rb
212
221
  - lib/polyrun/coverage/rails.rb
@@ -273,6 +282,7 @@ files:
273
282
  - lib/polyrun/timing/rspec_example_formatter.rb
274
283
  - lib/polyrun/timing/summary.rb
275
284
  - lib/polyrun/version.rb
285
+ - lib/polyrun/worker_ping.rb
276
286
  - polyrun.gemspec
277
287
  - sig/polyrun.rbs
278
288
  - sig/polyrun/cli.rbs
@@ -282,6 +292,7 @@ files:
282
292
  - sig/polyrun/minitest.rbs
283
293
  - sig/polyrun/quick.rbs
284
294
  - sig/polyrun/rspec.rbs
295
+ - sig/polyrun/worker_ping.rbs
285
296
  homepage: https://github.com/amkisko/polyrun.rb
286
297
  licenses:
287
298
  - MIT