rails-profiler 0.34.5 → 0.34.6

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: 0b498ec1274af5787fe96b70e64613b009298b578a7b084d65d450ec86a431bc
4
- data.tar.gz: b575d9afef090eee5539cfca2ffd536887d90b4c08a3b5099504a8e31ca69eab
3
+ metadata.gz: 0637e842e7e8cce996b450d94201b5a117bc6797fe095a488ea9f0b9b54c504f
4
+ data.tar.gz: 5949fcd0e376266ebfff2cc698b4964c4af39f85ae83832e905c0909354844dc
5
5
  SHA512:
6
- metadata.gz: f7da413fad8fd79420dddc04242b1ce30c05aac1106073219b7c0dcda26dada8c59a1b8e0d6a6c3c403d92ee6cd519271285d5f99fe6670b0e970179f79be119
7
- data.tar.gz: d36516568229d7fa228dad51b70a30e8ca2753cc07fa114279f322200d32bd9a4f5e02891ff0ea96b0c904dc1594d528f677129b840fe0acadd2f106f29ea9cc
6
+ metadata.gz: a5834f8092fa5c9346fb578444a32e43ffb1ebe669ece132b8214d1400257a6dabcea588b3a81125924c40f64fd98aa410e92523500c829b7af06f128be42997
7
+ data.tar.gz: 4728ac7e13f397cf2439a079f63430d74056a9bf126c7cfb29001fcde35003b076b76426b600465a246c2a177f660600f9ae4e2deef445273cfb43464d09b398
data/CHANGELOG.md CHANGED
@@ -19,6 +19,37 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.34.6] - 2026-10-16
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Added
27
+
28
+ - **Test runner:** `config.test_runner_kill_grace_seconds` (5 by default),
29
+ `config.test_runner_max_output_bytes` (5 MB by default) and `config.test_runner_max_active_runs`
30
+ (2 by default), described under the fixes below. `nil` lifts either limit.
31
+
32
+ ### Fixed
33
+
34
+ - **Test runner:** A test process ended by a signal (killed for lack of memory, crashed, stopped by
35
+ another program) is no longer reported `passed` with exit code 0. The run ends `signaled`, with no
36
+ exit code, the name of the signal in a new `signal` field of the API, a `Signal` line in the MCP
37
+ tool `run_tests`, a `[Profiler]` line at the end of its output, and "Ended by a signal" in the page.
38
+ - **Test runner:** **Stop** now stops the whole test run. The test process runs in a process group
39
+ of its own: `TERM` goes to the whole group, the processes rspec or rails started included, and
40
+ `KILL` follows after `config.test_runner_kill_grace_seconds` for whatever ignored it. A stopped
41
+ run used to leave those processes, and a process ignoring `TERM`, running.
42
+ - **Test runner:** The output a run keeps is limited to `config.test_runner_max_output_bytes`: its
43
+ beginning, a `[Profiler] Output truncated` line, and its last quarter once the run ends, with the
44
+ number of bytes left out. It used to be kept whole, however large. At most 20 finished runs are
45
+ kept, on top of the one hour they were already kept.
46
+ - **Test runner:** Starting a run while `config.test_runner_max_active_runs` runs still have their
47
+ test process is refused, with a `429` from the API, the reason in the page, and a tool error from
48
+ `run_tests`. Any number of runs could be started at once.
49
+ - **Test runner:** A run is only known to the server process that started it. With Puma in cluster
50
+ mode, a request reaching another worker gets a `404` that now gives this reason; the
51
+ [test runner documentation](docs/testing.md#several-server-processes) describes the limit.
52
+
22
53
  ## [0.34.5] - 2026-10-16
23
54
 
24
55
  <!-- stamped -->
@@ -5311,7 +5311,7 @@
5311
5311
  if (status === "passed") return "var(--profiler-success, #10b981)";
5312
5312
  if (status === "failed") return "var(--profiler-error, #ef4444)";
5313
5313
  if (status === "running") return "var(--profiler-accent, #06b6d4)";
5314
- if (status === "killed" || status === "error") return "var(--profiler-warning, #f59e0b)";
5314
+ if (status === "killed" || status === "signaled" || status === "error") return "var(--profiler-warning, #f59e0b)";
5315
5315
  return "var(--profiler-text-muted, #888)";
5316
5316
  }
5317
5317
  function statusLabel(status) {
@@ -5321,6 +5321,7 @@
5321
5321
  passed: "\u2713 Passed",
5322
5322
  failed: "\u2717 Failed",
5323
5323
  killed: "\u25A0 Killed",
5324
+ signaled: "\u26A1 Ended by a signal",
5324
5325
  error: "\u26A0 Error"
5325
5326
  };
5326
5327
  return map[status] || status;
@@ -5430,7 +5431,7 @@
5430
5431
  }, [refetchFiles]);
5431
5432
  y2(() => {
5432
5433
  if (!currentRun || !isRunning) return;
5433
- if (["passed", "failed", "killed", "error"].includes(currentRun.status)) {
5434
+ if (["passed", "failed", "killed", "signaled", "error"].includes(currentRun.status)) {
5434
5435
  setIsRunning(false);
5435
5436
  return;
5436
5437
  }
@@ -5495,8 +5496,8 @@
5495
5496
  const data = await startTestRun({ data: { files: Array.from(selected), framework } });
5496
5497
  setCurrentRun(data);
5497
5498
  setIsRunning(true);
5498
- } catch {
5499
- setError("Failed to start test run");
5499
+ } catch (e3) {
5500
+ setError(e3 instanceof Error && e3.message ? e3.message : "Failed to start test run");
5500
5501
  }
5501
5502
  };
5502
5503
  const stopRun = async () => {
@@ -10,6 +10,10 @@ module Profiler
10
10
  # How long the page waits before asking for the output that came since.
11
11
  STREAM_RETRY_MS = 1000
12
12
 
13
+ # Runs live in the memory of the server process that started them.
14
+ RUN_NOT_FOUND = "Run not found. Runs are kept by the server process that started them: " \
15
+ "with several processes (Puma in cluster mode), follow a run from that process."
16
+
13
17
  def files
14
18
  framework = params[:framework]
15
19
  tree = Profiler::TestRunner::Discovery.files(framework: framework)
@@ -34,11 +38,13 @@ module Profiler
34
38
  render json: run.to_h, status: :created
35
39
  rescue Profiler::TestRunner::InvalidFileError => e
36
40
  render json: { error: e.message }, status: :unprocessable_entity
41
+ rescue Profiler::TestRunner::TooManyRunsError => e
42
+ render json: { error: e.message }, status: :too_many_requests
37
43
  end
38
44
 
39
45
  def show
40
46
  run = Profiler::TestRunner.run_store.find(params[:id])
41
- return render json: { error: "Run not found" }, status: :not_found unless run
47
+ return render json: { error: RUN_NOT_FOUND }, status: :not_found unless run
42
48
 
43
49
  render json: run.to_h
44
50
  end
@@ -51,7 +57,7 @@ module Profiler
51
57
  position = [(request.headers["Last-Event-ID"].presence || params[:position]).to_i, 0].max
52
58
  result = Profiler::TestRunner.run_store.read_output(params[:id], position: position)
53
59
  if result[:status] == "not_found"
54
- render json: { error: "Run not found" }, status: :not_found
60
+ render json: { error: RUN_NOT_FOUND }, status: :not_found
55
61
  return
56
62
  end
57
63
 
@@ -75,7 +81,8 @@ module Profiler
75
81
  if killed
76
82
  head :no_content
77
83
  else
78
- render json: { error: "Run not found or not running" }, status: :not_found
84
+ known = Profiler::TestRunner.run_store.find(params[:id])
85
+ render json: { error: known ? "Run not running" : RUN_NOT_FOUND }, status: :not_found
79
86
  end
80
87
  end
81
88
 
@@ -49,6 +49,8 @@ module Profiler
49
49
  :track_console,
50
50
  :apply_env_overrides_when_disabled, :restrict_storage_permissions,
51
51
  :test_runner_allow_undiscovered_files, :allow_explain_outside_development,
52
+ :test_runner_kill_grace_seconds,
53
+ :test_runner_max_output_bytes, :test_runner_max_active_runs,
52
54
  :track_mailers, :capture_mail_body, :sanitize_mailer_recipients, :mailer_skip_actions,
53
55
  :compress_bodies, :compress_body_threshold, :max_captured_body_bytes, :max_captured_log_bytes,
54
56
  :redact_sensitive_data, :filter_parameters, :env_allowlist,
@@ -113,6 +115,14 @@ module Profiler
113
115
  # only runs in the development and test environments, even with enabled = true elsewhere:
114
116
  # PostgreSQL's EXPLAIN ANALYZE runs the query. true allows it in every environment.
115
117
  @allow_explain_outside_development = false
118
+ # Stopping a run sends TERM to its whole process group, then KILL after this many seconds
119
+ # if any process of the group is still there.
120
+ @test_runner_kill_grace_seconds = 5
121
+ # What a run keeps of its output, in bytes: the beginning, then the last quarter of this
122
+ # once it is exceeded, with a marker in between. nil keeps everything.
123
+ @test_runner_max_output_bytes = 5 * 1024 * 1024
124
+ # Runs whose test process has not ended yet; another one is refused. nil for no limit.
125
+ @test_runner_max_active_runs = 2
116
126
  @track_mailers = true
117
127
  @capture_mail_body = false
118
128
  @sanitize_mailer_recipients = false
@@ -45,7 +45,7 @@ module Profiler
45
45
  run_started_at = Time.now
46
46
  begin
47
47
  run = Profiler::TestRunner::Runner.start(files: files, framework: framework)
48
- rescue Profiler::TestRunner::InvalidFileError => e
48
+ rescue Profiler::TestRunner::InvalidFileError, Profiler::TestRunner::TooManyRunsError => e
49
49
  return ::MCP::Tool::Response.new([{ type: "text", text: e.message }], error: true)
50
50
  end
51
51
 
@@ -108,7 +108,9 @@ module Profiler
108
108
  text += "| Field | Value |\n|-------|-------|\n"
109
109
  text += "| Run ID | `#{run_id}` |\n"
110
110
  text += "| Status | **#{run_data["status"]}** |\n"
111
- text += "| Exit code | #{run_data["exit_code"].inspect} |\n\n"
111
+ text += "| Exit code | #{run_data["exit_code"].inspect} |\n"
112
+ text += "| Signal | #{run_data["signal"]} |\n" if run_data["signal"]
113
+ text += "\n"
112
114
  text += "## Output\n```\n#{output.strip}\n```"
113
115
  [{ type: "text", text: text }]
114
116
  end
@@ -136,6 +138,7 @@ module Profiler
136
138
  lines << "| Files | #{files.size} |"
137
139
  lines << "| Status | **#{run.status}** |"
138
140
  lines << "| Exit code | #{run.exit_code.inspect} |"
141
+ lines << "| Signal | #{run.signal} |" if run.signal
139
142
  lines << "| Duration | #{run.to_h[:duration]&.round(0)}ms |"
140
143
 
141
144
  if timed_out
@@ -6,10 +6,18 @@ require_relative "../redaction"
6
6
 
7
7
  module Profiler
8
8
  module TestRunner
9
+ # Raised by RunStore#create when as many runs as allowed still have their test process.
10
+ class TooManyRunsError < StandardError; end
11
+
12
+ # The runs live in the memory of the process that started them: under a server running
13
+ # several processes (Puma in cluster mode), a run is only known to the worker that started it.
9
14
  class RunStore
10
15
  TTL = 3600 # 1 hour
16
+ # Finished runs kept at most, the oldest dropped first, on top of the TTL.
17
+ MAX_FINISHED_RUNS = 20
11
18
 
12
- Run = Struct.new(:id, :status, :pid, :started_at, :finished_at, :output_lines, :exit_code, :files, :framework, keyword_init: true) do
19
+ # signal: the name of the signal that ended the process ("SIGKILL"), nil when it exited.
20
+ Run = Struct.new(:id, :status, :pid, :started_at, :finished_at, :output_lines, :exit_code, :signal, :files, :framework, keyword_init: true) do
13
21
  def to_h
14
22
  super.merge(
15
23
  started_at: started_at&.iso8601,
@@ -20,7 +28,9 @@ module Profiler
20
28
  end
21
29
  end
22
30
 
23
- TERMINAL_STATUSES = %w[passed failed killed error].freeze
31
+ # signaled: the process was ended by a signal that did not come from the profiler (out of
32
+ # memory, a crash, another program), so the tests neither passed nor failed.
33
+ TERMINAL_STATUSES = %w[passed failed killed signaled error].freeze
24
34
  NOT_FOUND = { chunks: [].freeze, status: "not_found", position: 0, finished: true }.freeze
25
35
 
26
36
  def initialize
@@ -29,25 +39,51 @@ module Profiler
29
39
  @held = Concurrent::Hash.new
30
40
  @output_finished = Concurrent::Hash.new
31
41
  @held_lock = Mutex.new
42
+ @output_limits = Concurrent::Hash.new
43
+ @active = Concurrent::Hash.new
44
+ @create_lock = Mutex.new
32
45
  end
33
46
 
34
- def create(files:, framework:)
35
- id = SecureRandom.hex(8)
36
- run = Run.new(
37
- id: id,
38
- status: "pending",
39
- pid: nil,
40
- started_at: Time.now,
41
- finished_at: nil,
42
- output_lines: Concurrent::Array.new,
43
- exit_code: nil,
44
- files: files,
45
- framework: framework.to_s
46
- )
47
- @runs[id] = run
48
- @locks[id] = { mutex: Mutex.new, cond: ConditionVariable.new }
49
- cleanup_old_runs
50
- run
47
+ # max_active: the run is refused when that many runs still have their test process; a run
48
+ # counts from here until #release. max_output_bytes: what the run keeps of its output (see
49
+ # #keep_output). nil for no limit.
50
+ def create(files:, framework:, max_active: nil, max_output_bytes: nil)
51
+ @create_lock.synchronize do
52
+ if max_active && @active.size >= max_active
53
+ raise TooManyRunsError,
54
+ "#{@active.size} test runs are already running, the limit is #{max_active} " \
55
+ "(config.test_runner_max_active_runs). Wait for one to end, or stop it."
56
+ end
57
+
58
+ id = SecureRandom.hex(8)
59
+ run = Run.new(
60
+ id: id,
61
+ status: "pending",
62
+ pid: nil,
63
+ started_at: Time.now,
64
+ finished_at: nil,
65
+ output_lines: Concurrent::Array.new,
66
+ exit_code: nil,
67
+ signal: nil,
68
+ files: files,
69
+ framework: framework.to_s
70
+ )
71
+ cleanup_old_runs
72
+ @runs[id] = run
73
+ @locks[id] = { mutex: Mutex.new, cond: ConditionVariable.new }
74
+ @active[id] = true
75
+ if max_output_bytes
76
+ tail = max_output_bytes / 4
77
+ @output_limits[id] = { head: max_output_bytes - tail, tail: tail, kept: 0, after_head: 0, end: nil, end_bytes: 0 }
78
+ end
79
+ run
80
+ end
81
+ end
82
+
83
+ # The test process of the run ended: it no longer counts against max_active.
84
+ def release(id)
85
+ @active.delete(id)
86
+ nil
51
87
  end
52
88
 
53
89
  def find(id)
@@ -88,7 +124,7 @@ module Profiler
88
124
  cut = text.bytesize - holdback
89
125
  @held[id] = text.byteslice(cut, text.bytesize - cut)
90
126
  shown = as_text(text.byteslice(0, cut))
91
- run.output_lines.push(shown) unless shown.empty?
127
+ keep_output(id, run, shown) unless shown.empty?
92
128
  end
93
129
  signal(id)
94
130
  end
@@ -102,7 +138,8 @@ module Profiler
102
138
  @held_lock.synchronize do
103
139
  @output_finished[id] = true
104
140
  rest = @held.delete(id)
105
- run.output_lines.push(as_text(rest)) if rest && !rest.empty?
141
+ keep_output(id, run, as_text(rest)) if rest && !rest.empty?
142
+ release_tail(id, run)
106
143
  end
107
144
  signal(id)
108
145
  end
@@ -171,10 +208,64 @@ module Profiler
171
208
  bytes.dup.force_encoding(Encoding::UTF_8).scrub
172
209
  end
173
210
 
174
- def forget_output(id)
211
+ # Under a limit, the output is kept as it comes up to the head (the limit less a quarter),
212
+ # then a marker line is shown at once and only the last quarter is kept, in a buffer
213
+ # shown by #release_tail once the process printed everything: the summary of a test run is
214
+ # at its end. Lines only ever get added, so the positions the pages hold stay right.
215
+ # Called under @held_lock.
216
+ def keep_output(id, run, text)
217
+ limit = @output_limits[id]
218
+ return run.output_lines.push(text) unless limit
219
+
220
+ unless limit[:end]
221
+ room = limit[:head] - limit[:kept]
222
+ if text.bytesize <= room
223
+ limit[:kept] += text.bytesize
224
+ return run.output_lines.push(text)
225
+ end
226
+
227
+ # What fits, without the first bytes of a character the limit cuts in two.
228
+ head = text.byteslice(0, room).scrub("")
229
+ run.output_lines.push(head) unless head.empty?
230
+ run.output_lines.push("\n[Profiler] Output truncated: the run printed more than " \
231
+ "#{limit[:head]} bytes. The rest is not kept; its last " \
232
+ "#{limit[:tail]} bytes are shown once the run ends.\n")
233
+ limit[:kept] += head.bytesize
234
+ limit[:end] = []
235
+ text = text.byteslice(head.bytesize, text.bytesize - head.bytesize)
236
+ end
237
+
238
+ # The pieces of the end, the oldest dropped while the others hold enough without it.
239
+ limit[:after_head] += text.bytesize
240
+ limit[:end] << text
241
+ limit[:end_bytes] += text.bytesize
242
+ while limit[:end_bytes] - limit[:end].first.bytesize >= limit[:tail]
243
+ limit[:end_bytes] -= limit[:end].shift.bytesize
244
+ end
245
+ end
246
+
247
+ # Shows the end kept past the limit, and lets anything appended later (the profiler's own
248
+ # lines about the end of the process) through.
249
+ def release_tail(id, run)
250
+ limit = @output_limits.delete(id)
251
+ return unless limit && limit[:end]
252
+
253
+ tail = limit[:end].join
254
+ tail = tail.byteslice(tail.bytesize - limit[:tail], limit[:tail]) if tail.bytesize > limit[:tail]
255
+ # Dropping from the front can leave the last bytes of a character first.
256
+ tail = tail.byteslice(1, tail.bytesize - 1) while !tail.empty? && (tail.getbyte(0) & 0xC0) == 0x80
257
+ left_out = limit[:after_head] - tail.bytesize
258
+ run.output_lines.push("[Profiler] #{left_out} bytes left out. The last #{tail.bytesize} bytes " \
259
+ "of the output:\n")
260
+ run.output_lines.push(tail.scrub) unless tail.empty?
261
+ end
262
+
263
+ def forget(id)
264
+ @runs.delete(id)
265
+ @locks.delete(id)
175
266
  @held.delete(id)
176
267
  @output_finished.delete(id)
177
- true
268
+ @output_limits.delete(id)
178
269
  end
179
270
 
180
271
  def snapshot(run, position)
@@ -187,9 +278,13 @@ module Profiler
187
278
  }
188
279
  end
189
280
 
281
+ # Finished runs older than the TTL, and the oldest past MAX_FINISHED_RUNS, are dropped; a
282
+ # run whose process is still there (killed, still printing) is kept whatever its age.
190
283
  def cleanup_old_runs
191
284
  cutoff = Time.now - TTL
192
- @runs.delete_if { |id, r| r.finished_at && r.finished_at < cutoff && @locks.delete(id) && forget_output(id) }
285
+ finished = @runs.values.select { |r| r.finished_at && !@active.key?(r.id) }.sort_by(&:finished_at)
286
+ expired, recent = finished.partition { |r| r.finished_at < cutoff }
287
+ (expired + recent.first([recent.size - MAX_FINISHED_RUNS, 0].max)).each { |r| forget(r.id) }
193
288
  end
194
289
  end
195
290
 
@@ -21,24 +21,49 @@ module Profiler
21
21
 
22
22
  # Every caller (the HTTP controller, the MCP run_tests tool) goes through here, so the
23
23
  # selection is checked once, before any run is recorded or any process is started.
24
+ # Raises TooManyRunsError when config.test_runner_max_active_runs runs still have their
25
+ # test process.
24
26
  def self.start(files:, framework:)
25
27
  validate_files!(files, framework)
26
- run = Profiler::TestRunner.run_store.create(files: files, framework: framework)
28
+ config = Profiler.configuration
29
+ run = Profiler::TestRunner.run_store.create(
30
+ files: files, framework: framework,
31
+ max_active: config.test_runner_max_active_runs,
32
+ max_output_bytes: config.test_runner_max_output_bytes
33
+ )
27
34
  spawn_async(run)
28
35
  run
29
36
  end
30
37
 
38
+ # The test process leads its own process group (see run_process): TERM goes to the whole
39
+ # group, the processes rspec or rails started included, and KILL follows after
40
+ # config.test_runner_kill_grace_seconds for whatever is still there.
31
41
  def self.kill(run_id)
32
42
  run = Profiler::TestRunner.run_store.find(run_id)
33
43
  return false unless run && run.pid && run.status == "running"
34
44
 
35
45
  begin
36
- Process.kill("TERM", run.pid)
37
- Profiler::TestRunner.run_store.update(run_id, status: "killed", finished_at: Time.now)
38
- true
46
+ Process.kill("TERM", -run.pid)
39
47
  rescue Errno::ESRCH
40
48
  # Process already exited
41
- false
49
+ return false
50
+ end
51
+ Profiler::TestRunner.run_store.update(run_id, status: "killed", finished_at: Time.now)
52
+ kill_group_later(run.pid)
53
+ true
54
+ end
55
+
56
+ # The group exists as long as one of its processes does. Its id is the pid of the test
57
+ # process, which the system can give to a new process once the whole group is gone; that
58
+ # takes the pids to wrap around within the delay, and the new process would have to lead a
59
+ # group of its own.
60
+ def self.kill_group_later(pgid)
61
+ delay = Profiler.configuration.test_runner_kill_grace_seconds.to_f
62
+ Thread.new do
63
+ sleep delay
64
+ Process.kill("KILL", -pgid)
65
+ rescue Errno::ESRCH, Errno::EPERM
66
+ nil # the group ended on TERM
42
67
  end
43
68
  end
44
69
 
@@ -123,6 +148,8 @@ module Profiler
123
148
  status: "error",
124
149
  finished_at: Time.now
125
150
  )
151
+ ensure
152
+ Profiler::TestRunner.run_store.release(run.id)
126
153
  end
127
154
  end
128
155
 
@@ -132,8 +159,9 @@ module Profiler
132
159
 
133
160
  Profiler::TestRunner.run_store.update(run.id, status: "running", started_at: Time.now)
134
161
 
135
- # unsetenv_others: the child gets exactly env, not the ENV of this process
136
- IO.popen([env, *cmd, unsetenv_others: true, err: [:child, :out]], "r") do |io|
162
+ # unsetenv_others: the child gets exactly env, not the ENV of this process. pgroup: the
163
+ # child leads a process group of its own, which kill stops as a whole.
164
+ IO.popen([env, *cmd, unsetenv_others: true, pgroup: true, err: [:child, :out]], "r") do |io|
137
165
  Profiler::TestRunner.run_store.update(run.id, pid: io.pid)
138
166
 
139
167
  begin
@@ -148,21 +176,38 @@ module Profiler
148
176
  end
149
177
  end
150
178
 
151
- exit_code = $?.exitstatus || 0
179
+ process_status = $?
180
+ exit_code = process_status.exitstatus # nil when a signal ended the process
181
+ signal = signal_name(process_status.termsig) if process_status.signaled?
152
182
  # A run killed from the profiler stays killed, whatever the process answered to TERM.
153
183
  status = if run.status == "killed" then "killed"
184
+ elsif signal then "signaled"
154
185
  elsif exit_code == 0 then "passed"
155
186
  else "failed"
156
187
  end
157
188
 
189
+ if status == "signaled"
190
+ # Before the status: a page following the run stops at its terminal status.
191
+ Profiler::TestRunner.run_store.append_output(
192
+ run.id, "\n[Profiler] The test process was ended by #{signal} before it finished: " \
193
+ "the result of the tests is unknown.\n"
194
+ )
195
+ end
196
+
158
197
  Profiler::TestRunner.run_store.update(
159
198
  run.id,
160
199
  status: status,
161
200
  finished_at: Time.now,
162
- exit_code: exit_code
201
+ exit_code: exit_code,
202
+ signal: signal
163
203
  )
164
204
  end
165
205
 
206
+ def self.signal_name(number)
207
+ name = Signal.signame(number)
208
+ name ? "SIG#{name}" : "signal #{number}"
209
+ end
210
+
166
211
  def self.build_command(files, framework)
167
212
  root = project_root
168
213
  absolute_files = files.map { |f| File.join(root, f) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.34.5"
4
+ VERSION = "0.34.6"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.5
4
+ version: 0.34.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy