kimera 0.1.2 → 0.1.4

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +18 -1
  4. data/lib/kimera/cli/report_file.rb +1 -1
  5. data/lib/kimera/cli/run/pass.rb +9 -2
  6. data/lib/kimera/execution/baseline_failure/breakdown.rb +35 -0
  7. data/lib/kimera/execution/baseline_failure.rb +18 -8
  8. data/lib/kimera/execution/baseline_pass.rb +11 -3
  9. data/lib/kimera/execution/harness.rb +2 -2
  10. data/lib/kimera/execution/isolated.rb +8 -0
  11. data/lib/kimera/execution/isolated_child.rb +7 -3
  12. data/lib/kimera/execution/isolated_child_command.rb +11 -4
  13. data/lib/kimera/execution/isolated_child_minitest.rb +11 -5
  14. data/lib/kimera/execution/isolated_outcome.rb +33 -6
  15. data/lib/kimera/execution/isolated_plan.rb +13 -5
  16. data/lib/kimera/execution/isolated_verdict.rb +14 -5
  17. data/lib/kimera/execution/kinship.rb +27 -0
  18. data/lib/kimera/execution/overlay_guard_modules.rb +17 -0
  19. data/lib/kimera/execution/overlay_guards.rb +6 -0
  20. data/lib/kimera/execution/parallel_test_databases.rb +11 -3
  21. data/lib/kimera/execution/pool_driver.rb +3 -3
  22. data/lib/kimera/execution/reflections.rb +35 -0
  23. data/lib/kimera/execution/reload.rb +33 -39
  24. data/lib/kimera/execution/reload_errand.rb +39 -0
  25. data/lib/kimera/execution/schedule.rb +17 -9
  26. data/lib/kimera/execution/worker_pool/worker.rb +5 -0
  27. data/lib/kimera/execution/worker_pool.rb +4 -2
  28. data/lib/kimera/frameworks/adapter.rb +5 -1
  29. data/lib/kimera/frameworks/minitest_adapter.rb +19 -0
  30. data/lib/kimera/incremental/session.rb +1 -1
  31. data/lib/kimera/memo_guard.rb +66 -0
  32. data/lib/kimera/registry/source_file.rb +6 -1
  33. data/lib/kimera/support/version.rb +1 -1
  34. data/skills/kimera/SKILL.md +3 -1
  35. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 042a730e48edfef4744060425cd10f04e34006025af3f36db3ab17f0be59e0bc
4
- data.tar.gz: 97a3914ca3f803f01e68564af89ee07667703aadfa6ec74269bbf37470add0fb
3
+ metadata.gz: 05af564bc168e88e752fd28c2c623d8fe03b3d02402472819930015ceaff5492
4
+ data.tar.gz: 1708453bd3e4fb16b5eb054be04e94ff4ab8dc06a2c6297adc257c8e23d5fb9e
5
5
  SHA512:
6
- metadata.gz: 2c2d774a35384621bca26d92d5c9e77550f3b08ecf1304019b37e695eeacbbc5566622de50fbae65077f61035fa25e241817aa31e917c0455e8ad23b26c00f9b
7
- data.tar.gz: 7ebd3017fd0edbd7a2abb3c585ee347dfe0b66a03111ffa9efb322b835b6661d29bd2554f863bc075035f242da3bf66f514f098ebaf2b64339b39b90f8dde558
6
+ metadata.gz: 98620d29d7d69bdca3fc0bd51b51ee040e7a033627841e41e42860623cfd006d617805b817d428a2d688ca5df4c24ffde65aca0337985d7898ffae03b2f816c0
7
+ data.tar.gz: e509c0ff5416eb1cb41d8d933190d8e9d63249e010791e07ba5b0c0b0678d7e7d00aef6d2bee28dac103cfa4d8ffe6aea22b8515549b79a0bfe299f58df30d58
data/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 (2026-09-27)
4
+
5
+ - Guard-style memoization is no longer mutated, just as `@x ||=` never was.
6
+ In a method that opens with `return @x if @x` (and assigns `@x` later) or
7
+ `return if @x` then `@x = true`, the guard gets no `condition` mutant and
8
+ the flag no `true => false`. Removing a memo only recomputes an equal value,
9
+ so those mutants always survived. A re-entrancy guard of the same shape is
10
+ skipped too.
11
+ - `--isolated` first runs the unmutated suite once in a fresh mirror. If it
12
+ isn't green there (for example, a test helper that won't load), the run
13
+ stops with exit 1 and the error, as a red warm baseline does. Before, every
14
+ mutant it covered was scored `killed` with no failing test named.
15
+ - An isolated mirror symlinks `vendor/`, `node_modules/` and `.bundle/`
16
+ instead of leaving them out. It creates `tmp/` and `log/` empty, and still
17
+ leaves out `.git/` and `coverage/`. A boot that needs frontend tooling or a
18
+ vendored bundle now loads in the mirror.
19
+ - An isolated kill caused by a test file failing to load now puts the load
20
+ error (Minitest: message and first backtrace lines; RSpec: its load-error
21
+ report) in the result's `detail`.
22
+ - Parallel warm workers follow Rails' fork contract more closely. Each sets
23
+ `ActiveSupport::TestCase.parallel_worker_id` to its slot, so per-worker
24
+ resources keyed on it (a Redis db, lease or key namespace, a port) no
25
+ longer collide between workers. `parallelize_before_fork` hooks run once per
26
+ fleet, not again before every spawn (including replacement workers forked
27
+ while other workers are mid-test). `parallelize_teardown` hooks get the
28
+ worker number.
29
+ - A red parallel baseline lists, for each worker that ran a failing test,
30
+ where each failure fell in that worker's run and which tests ran just
31
+ before it. A test lost when its worker hit the hard timeout or crashed now
32
+ says so, instead of showing up as an unexplained failure.
33
+
34
+ ## 0.1.3 (2026-09-27)
35
+
36
+ - `--isolated` no longer scores a child that crashes before reporting results
37
+ as a kill. It is `harness_error`, and its `detail` gives the exit status and
38
+ the ends of the child's stderr. A Minitest child that fails to load a test
39
+ file still reports that as a kill, as the RSpec child already did.
40
+ - `--isolated` passes test ids and test files to the child in a request file
41
+ instead of argv, so a mutant covered by thousands of tests no longer fails
42
+ with `Errno::E2BIG: Argument list too long`.
43
+ - Warm reload-path mutants (for example, statements in a memoized method) run
44
+ one test at a time and stop at the first failure. Tests covering the
45
+ mutant's method run first. The hard timeout now applies to each test, not
46
+ the whole suite, so a mutant a test kills is no longer reported as
47
+ "reload worker produced no result". A test that overruns the timeout is a
48
+ `timeout`, and a worker that dies names its exit status or signal.
49
+ - A scope or association declared in a concern's `included do` block is now
50
+ instrumented in the classes that already included the concern. It used to be
51
+ reported `no_coverage`, or `survived` when it was covered through another
52
+ path.
53
+ - An association overlaid on a parent model now also reaches subclasses that
54
+ declared associations of their own (such as STI children). They used to keep
55
+ the unguarded reflection, so mutants in the association's scope lambda
56
+ survived.
57
+ - The red-baseline hint prints a plain Minitest command for a Minitest suite
58
+ instead of `rspec …`.
59
+ - `kimera report`/`kimera mutant` and incremental sessions read report JSON as
60
+ UTF-8, so they no longer crash under a non-UTF-8 locale when a report
61
+ contains non-ASCII text.
62
+
3
63
  ## 0.1.2 (2026-09-26)
4
64
 
5
65
  - Kimera sets `KIMERA=1` in its own process before loading the suite, and in
data/README.md CHANGED
@@ -220,7 +220,10 @@ selector and Kimera's own harness-critical files (see below).
220
220
 
221
221
  An isolated child is judged on its failed tests, not its exit status. A child
222
222
  that exits non-zero with no failing test is `harness_error` (unjudged, gated
223
- by `--max-errors`), not a kill. The usual cause is a coverage floor such as
223
+ by `--max-errors`), not a kill. So is a child that dies before reporting any
224
+ results (a boot error, an exit hook, a crash); its `detail` carries the
225
+ child's exit status and the ends of its stderr. The usual cause of a non-zero
226
+ exit with no failures is a coverage floor such as
224
227
  SimpleCov's `minimum_coverage`, which a partial run always trips. Kimera sets
225
228
  `KIMERA=1` wherever it loads your suite (the warm process, `--isolated`
226
229
  children, and `kimera doctor`'s test commands), so skip the floor there:
@@ -231,6 +234,13 @@ SimpleCov.start { minimum_coverage(line: 100, branch: 100) unless ENV["KIMERA"]
231
234
 
232
235
  `kimera doctor` warns when it finds an ungated `minimum_coverage`.
233
236
 
237
+ Each isolated mirror is a copy of the project root with some exceptions:
238
+ `.git/` and `coverage/` are left out, `tmp/` and `log/` start empty, and
239
+ `vendor/`, `node_modules/` and `.bundle/` are symlinked to the originals. The
240
+ exception is a directory that holds a mutated file, which is always copied.
241
+ Before judging any mutant, `--isolated` runs the unmutated suite once in a
242
+ mirror. If it isn't green there, the run stops with exit 1 and the error.
243
+
234
244
  ### Progress output
235
245
 
236
246
  During a run, a progress bar tracks each phase on stderr. It shows only when
@@ -303,6 +313,13 @@ The *default* set is small and low-noise:
303
313
  - `!x => x`
304
314
  - forced `if` conditions
305
315
 
316
+ Memoization is not a mutation target. Dropping a memo only recomputes an equal
317
+ value, so that mutant would always survive. `@x ||= …` has no such mutant, and
318
+ neither does a guard that opens a method: `return @x if defined?(@x)`,
319
+ `return @x if @x` (with `@x` assigned later in the method), or `return if @x`
320
+ followed by `@x = true`. Everything the method computes is still mutated. A
321
+ re-entrancy guard with the run-once shape is skipped too.
322
+
306
323
  The extended families are one flag away. Use `--operators all`, or pick from:
307
324
 
308
325
  | family | mutation |
@@ -13,7 +13,7 @@ module Kimera::CLI::ReportFile
13
13
  module_function
14
14
 
15
15
  def parse(path)
16
- JSON.parse(File.read(path))
16
+ JSON.parse(File.read(path, encoding: Encoding::UTF_8))
17
17
  rescue JSON::ParserError => error
18
18
  raise(Kimera::UsageError, "unreadable report #{path}: #{error.message}")
19
19
  end
@@ -63,7 +63,7 @@ class Kimera::CLI::Run::Pass
63
63
  end
64
64
 
65
65
  def run(remaining)
66
- isolated = ->(ids) { isolation.run(ids: ids, label: "mutants (isolated)") }
66
+ isolated = ->(ids) { judge(ids) }
67
67
  return isolated.call(remaining) if @options[:isolated]
68
68
  pooled(remaining, isolated)
69
69
  end
@@ -74,8 +74,13 @@ class Kimera::CLI::Run::Pass
74
74
  harness.run(ids: together, label: "mutants (warm)").merge(isolated.call(apart))
75
75
  end
76
76
 
77
+ def judge(ids)
78
+ isolation.verify!
79
+ isolation.run(ids: ids, label: "mutants (isolated)")
80
+ end
81
+
77
82
  def isolation
78
- Kimera::Execution::IsolatedExecution.new(**core, **{ hard_timeout: @options[:hard_timeout] }.compact)
83
+ @_isolation ||= Kimera::Execution::IsolatedExecution.new(**core, **limits)
79
84
  end
80
85
 
81
86
  def core
@@ -84,6 +89,8 @@ class Kimera::CLI::Run::Pass
84
89
  .merge(framework: @options[:framework], test_files: files)
85
90
  end
86
91
 
92
+ def limits = { hard_timeout: @options[:hard_timeout] }.compact
93
+
87
94
  def split(ids)
88
95
  patterns = Array(@options[:isolate_when_covered_by])
89
96
  ids.partition do |id|
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::BaselineFailure::Breakdown
4
+ MAX_FAILURES = 5
5
+ PRECEDING = 3
6
+
7
+ def initialize(failed, workers)
8
+ @failed = failed
9
+ @workers = workers
10
+ end
11
+
12
+ def to_s
13
+ lines = @workers.sort_by(&:first).filter_map { |slot, ids| worker(slot, ids) }
14
+ lines.empty? ? "" : "\n per worker (tests in the order it ran them):\n#{lines.join("\n")}"
15
+ end
16
+
17
+ private
18
+
19
+ def worker(slot, ids)
20
+ hits = ids.each_index.select { |index| @failed.include?(ids[index]) }
21
+ return if hits.empty?
22
+ " worker #{slot}: ran #{ids.size}; failed #{failures(ids, hits)}#{preceding(ids, hits.first)}"
23
+ end
24
+
25
+ def failures(ids, hits)
26
+ shown = hits.first(MAX_FAILURES).map { |index| "##{index + 1} #{ids[index]}" }
27
+ extra = hits.size - shown.size
28
+ shown.join(", ") + (extra.positive? ? " (+#{extra} more)" : "")
29
+ end
30
+
31
+ def preceding(ids, first)
32
+ before = ids[[first - PRECEDING, 0].max...first]
33
+ before.empty? ? " (first test on this worker)" : "; just before: #{before.join(", ")}"
34
+ end
35
+ end
@@ -10,16 +10,28 @@ end
10
10
  class Kimera::Execution::BaselineFailure < Kimera::Error
11
11
  MAX_DETAILS = 3
12
12
  MAX_MESSAGE = 300
13
+ LOSSES = { timeout: "its worker was killed at the hard timeout (--hard-timeout) before reporting a result" }.freeze
14
+ CRASH = "its worker died before reporting a result"
13
15
 
14
16
  class << self
15
- def build(failed, messages)
16
- new(summary(failed, messages))
17
+ def build(failed, messages, command, workers: {})
18
+ new(summary(failed, messages, command, workers: workers))
17
19
  end
18
20
 
19
- def summary(failed, messages)
21
+ def lost(reason) = LOSSES.fetch(reason, CRASH)
22
+
23
+ def mirrored(reason, hint)
24
+ new(
25
+ "isolated baseline is not green: the unmutated suite fails in a mirror of the project\n " \
26
+ "#{reason.gsub("\n", "\n ")}\n#{hint}"
27
+ )
28
+ end
29
+
30
+ def summary(failed, messages, command, workers: {})
20
31
  text = "baseline suite is not green: #{failed.join(", ")}"
21
32
  text += appendix(failed, messages)
22
- "#{text}\n#{reproduce(failed)}"
33
+ text += Kimera::Execution::BaselineFailure::Breakdown.new(failed, workers).to_s
34
+ "#{text}\n reproduce without kimera: #{command}"
23
35
  end
24
36
 
25
37
  def appendix(failed, messages)
@@ -39,9 +51,7 @@ class Kimera::Execution::BaselineFailure < Kimera::Error
39
51
  def truncate(text)
40
52
  text.length > MAX_MESSAGE ? "#{text[0, MAX_MESSAGE]}…" : text
41
53
  end
42
-
43
- def reproduce(failed)
44
- " reproduce without kimera: rspec #{failed.first(MAX_DETAILS).join(" ")} --order defined"
45
- end
46
54
  end
47
55
  end
56
+
57
+ require_relative "baseline_failure/breakdown"
@@ -71,7 +71,13 @@ class Kimera::Execution::BaselinePass
71
71
  end
72
72
 
73
73
  def dispatch
74
- yield(resolve: resolve, lost: loss)
74
+ yield(resolve: resolve, lost: loss, trace: tracer)
75
+ end
76
+
77
+ def journal = @_journal ||= Hash.new { |workers, slot| workers[slot] = [] }
78
+
79
+ def tracer
80
+ ->(slot, test_id) { journal[slot] << test_id }
75
81
  end
76
82
 
77
83
  def resolve
@@ -90,7 +96,8 @@ class Kimera::Execution::BaselinePass
90
96
  method(:lost)
91
97
  end
92
98
 
93
- def lost(test_id, _reason)
99
+ def lost(test_id, reason)
100
+ @messages[test_id] = Kimera::Execution::BaselineFailure.lost(reason)
94
101
  tally.failures << test_id
95
102
  @progress.tick
96
103
  end
@@ -118,6 +125,7 @@ class Kimera::Execution::BaselinePass
118
125
  end
119
126
 
120
127
  def failure!(failed)
121
- raise(Kimera::Execution::BaselineFailure.build(failed, @messages))
128
+ command = @adapter.reproduce(failed.first(Kimera::Execution::BaselineFailure::MAX_DETAILS))
129
+ raise(Kimera::Execution::BaselineFailure.build(failed, @messages, command, workers: journal))
122
130
  end
123
131
  end
@@ -126,8 +126,8 @@ class Kimera::Execution::Harness
126
126
  end
127
127
 
128
128
  def parallel(pass)
129
- pass.parallel! do |resolve:, lost:|
130
- driver.drive(adapter.test_ids, driver.method(:channel), resolve: resolve, lost: lost)
129
+ pass.parallel! do |resolve:, lost:, trace:|
130
+ driver.drive(adapter.test_ids, driver.method(:channel), resolve: resolve, lost: lost, trace: trace)
131
131
  end
132
132
  end
133
133
 
@@ -5,6 +5,7 @@ require "tmpdir"
5
5
  require_relative "../results/result"
6
6
  require_relative "../results/run_report"
7
7
  require_relative "../synthesis/overlay"
8
+ require_relative "baseline_failure"
8
9
  require_relative "isolated_plan"
9
10
  require_relative "isolated_scheduling"
10
11
  require_relative "isolated_verdict"
@@ -30,6 +31,13 @@ class Kimera::Execution::IsolatedExecution
30
31
  @options = options
31
32
  end
32
33
 
34
+ def verify!
35
+ errors.puts("kimera: isolated baseline (unmutated mirror)") unless progress.enabled?
36
+ outcome = with_mirror { |mirror| verdict(mirror, plan.suite) }
37
+ return if outcome.status == :survived
38
+ raise(Kimera::Execution::BaselineFailure.mirrored(outcome.explain(limit), Kimera::Execution::IsolatedPlan::MIRROR_HINT))
39
+ end
40
+
33
41
  def run(ids: self.ids, label: "mutants")
34
42
  progress.start(ids.size, label)
35
43
  report(ids)
@@ -9,15 +9,19 @@ begin
9
9
  rescue StandardError, ScriptError
10
10
  nil
11
11
  end
12
- RSpec.configuration.output_stream = StringIO.new
12
+ output = StringIO.new
13
+ RSpec.configuration.output_stream = output
13
14
  RSpec.configuration.error_stream = StringIO.new
14
15
  RSpec.configuration.deprecation_stream = StringIO.new
15
16
 
16
- ledger, *locations = ARGV
17
+ ledger, request = ARGV
18
+ locations = JSON.parse(File.read(request, encoding: Encoding::UTF_8)).fetch("tests")
17
19
  code = RSpec::Core::Runner.run(locations)
18
20
 
19
21
  failed = RSpec.world.all_examples.select { |example| example.execution_result.status == :failed }
20
22
  outside = RSpec.world.non_example_failure ? 1 : 0
21
- File.write(ledger, JSON.generate(failures: failed.size + outside, failing: failed.map(&:id)))
23
+ report = { failures: failed.size + outside, failing: failed.map(&:id) }
24
+ report[:load_error] = output.string.strip[0, 4000] if outside.positive?
25
+ File.write(ledger, JSON.generate(report))
22
26
 
23
27
  exit code
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require_relative "suite_env"
4
5
 
5
6
  module Kimera
@@ -19,19 +20,25 @@ class Kimera::Execution::IsolatedPlan::ChildCommand
19
20
  end
20
21
 
21
22
  def command(mirror, locations, ledger:, paths:)
23
+ File.write(request_path(ledger), JSON.generate(request(locations)))
22
24
  includes = paths.flat_map { |path| ["-I", path] }
23
- bundled(mirror, Kimera::Execution::SUITE_ENV.dup, ["ruby", *includes, "-I", helpers, *argv(locations, ledger)])
25
+ bundled(mirror, Kimera::Execution::SUITE_ENV.dup, ["ruby", *includes, "-I", helpers, *argv(ledger)])
24
26
  end
25
27
 
26
- def argv(locations, ledger)
27
- return [CHILD, ledger, *locations] unless minitest?
28
- [MINITEST_CHILD, ledger, *@test_files, "--", *locations]
28
+ def argv(ledger)
29
+ [minitest? ? MINITEST_CHILD : CHILD, ledger, request_path(ledger)]
30
+ end
31
+
32
+ def request(locations)
33
+ { "tests" => locations, "files" => @test_files }
29
34
  end
30
35
 
31
36
  def helpers
32
37
  minitest? ? "test" : "spec"
33
38
  end
34
39
 
40
+ def request_path(ledger) = "#{ledger}.request"
41
+
35
42
  private
36
43
 
37
44
  def bundled(mirror, env, argv)
@@ -12,13 +12,19 @@ rescue NameError, ArgumentError
12
12
  end
13
13
  end
14
14
 
15
- ledger, *rest = ARGV
16
- split = rest.index("--") or abort("usage: <ledger> <test files...> -- <ids...>")
17
- files = rest[0...split]
18
- ids = rest[(split + 1)..]
15
+ ledger, request = ARGV
16
+ request = JSON.parse(File.read(request, encoding: Encoding::UTF_8))
17
+ files = request.fetch("files")
18
+ ids = request.fetch("tests")
19
19
  ARGV.clear
20
20
 
21
- files.each { |f| require File.expand_path(f) }
21
+ begin
22
+ files.each { |f| require File.expand_path(f) }
23
+ rescue StandardError, ScriptError => error
24
+ cause = ["#{error.class}: #{error.message}", *Array(error.backtrace).first(5).map { |line| " #{line}" }]
25
+ File.write(ledger, JSON.generate(failures: 1, failing: [], load_error: cause.join("\n")))
26
+ exit(1)
27
+ end
22
28
 
23
29
  methods = {}
24
30
  Minitest::Runnable.runnables.each do |runnable|
@@ -8,6 +8,9 @@ module Kimera
8
8
  end
9
9
 
10
10
  class Kimera::Execution::IsolatedOutcome
11
+ HEAD_LINES = 5
12
+ TAIL_LINES = 15
13
+
11
14
  attr_reader :status, :failing, :detail
12
15
 
13
16
  def initialize(status, failing = nil, detail = nil)
@@ -16,32 +19,56 @@ class Kimera::Execution::IsolatedOutcome
16
19
  @detail = detail
17
20
  end
18
21
 
22
+ def explain(limit)
23
+ return "timed out after #{limit}s" if status == :timeout
24
+ named = Array(failing)
25
+ return "failing: #{named.join(", ")}" if named.any?
26
+ detail || "the test child failed without naming a test"
27
+ end
28
+
19
29
  class << self
20
- def judge(exit, path)
30
+ def judge(exit, path, stderr = "")
21
31
  return new(:timeout) if exit == :timeout
22
32
  ledger = read(path)
23
- return unreported(exit) unless ledger
33
+ return unreported(exit, stderr) unless ledger
24
34
  tally(exit, ledger)
25
35
  end
26
36
 
27
37
  private
28
38
 
29
- def unreported(exit) = new(exit.success? ? :survived : :killed)
39
+ def unreported(exit, stderr)
40
+ new(:harness_error, nil, "test child #{status_of(exit)} without reporting results#{tail(stderr)}")
41
+ end
42
+
43
+ def tail(stderr)
44
+ lines = stderr.to_s.strip.lines(chomp: true)
45
+ lines.empty? ? "" : ":\n#{ends(lines).join("\n")}"
46
+ end
47
+
48
+ def ends(lines)
49
+ return lines if lines.size <= HEAD_LINES + TAIL_LINES
50
+ [*lines.first(HEAD_LINES), "…", *lines.last(TAIL_LINES)]
51
+ end
52
+
53
+ def status_of(exit)
54
+ code = exit.exitstatus
55
+ code ? "exited #{code}" : "died on signal #{exit.termsig}"
56
+ end
30
57
 
31
58
  def tally(exit, ledger)
32
59
  failing = Array(ledger["failing"])
33
- return new(:killed, failing) if ledger["failures"].positive?
60
+ return new(:killed, failing, ledger["load_error"]) if ledger["failures"].positive?
34
61
  return new(:survived) if exit.success?
35
62
  new(:harness_error, nil, unexplained(exit))
36
63
  end
37
64
 
38
65
  def unexplained(exit)
39
- "suite exited #{exit.exitstatus || "on signal #{exit.termsig}"} with 0 failures " \
66
+ "suite #{status_of(exit)} with 0 failures " \
40
67
  "(a coverage floor or exit hook? skip it when ENV[\"KIMERA\"] is set)"
41
68
  end
42
69
 
43
70
  def read(path)
44
- ledger = JSON.parse(File.read(path))
71
+ ledger = JSON.parse(File.read(path, encoding: Encoding::UTF_8))
45
72
  ledger if ledger.is_a?(Hash) && ledger["failures"].is_a?(Integer)
46
73
  rescue Errno::ENOENT, JSON::ParserError
47
74
  nil
@@ -10,7 +10,12 @@ module Kimera
10
10
  end
11
11
 
12
12
  class Kimera::Execution::IsolatedPlan
13
- MIRROR_SKIP = %w[.git tmp vendor node_modules coverage log .bundle].freeze
13
+ MIRROR_SKIP = %w[.git coverage].freeze
14
+ MIRROR_EMPTY = %w[tmp log].freeze
15
+ MIRROR_LINK = %w[vendor node_modules .bundle].freeze
16
+ MIRROR_HINT =
17
+ " the mirror copies the project except #{MIRROR_SKIP.join(", ")} " \
18
+ "(#{MIRROR_EMPTY.join(", ")} start empty; #{MIRROR_LINK.join(", ")} are symlinked)".freeze
14
19
 
15
20
  attr_reader :registry
16
21
 
@@ -34,14 +39,17 @@ class Kimera::Execution::IsolatedPlan
34
39
  child.command(mirror, locations, ledger: ledger, paths: mutables)
35
40
  end
36
41
 
42
+ def suite = selection.all
43
+
37
44
  def mutables
38
45
  @registry.files.map { |f| f.split(File::SEPARATOR).first }.uniq
39
46
  end
40
47
 
41
- class << self
42
- def mirrors(entries)
43
- entries.reject { |entry| MIRROR_SKIP.include?(entry) }
44
- end
48
+ def placement(entry)
49
+ return :copy if mutables.include?(entry)
50
+ return :skip if MIRROR_SKIP.include?(entry)
51
+ return :empty if MIRROR_EMPTY.include?(entry)
52
+ MIRROR_LINK.include?(entry) ? :link : :copy
45
53
  end
46
54
 
47
55
  private
@@ -38,13 +38,16 @@ module Kimera
38
38
  def verdict(mirror, locations)
39
39
  Dir.mktmpdir("kimera-ledger") do |dir|
40
40
  ledger = File.join(dir, "ledger.json")
41
- IsolatedOutcome.judge(launch(mirror, locations, ledger), ledger)
41
+ stderr = File.join(dir, "stderr.log")
42
+ IsolatedOutcome.judge(launch(mirror, locations, ledger, stderr), ledger, captured(stderr))
42
43
  end
43
44
  end
44
45
 
45
- def launch(mirror, locations, ledger)
46
+ def captured(path) = File.read(path, encoding: Encoding::UTF_8).scrub
47
+
48
+ def launch(mirror, locations, ledger, stderr)
46
49
  env, cmd = plan.command(mirror, locations, ledger)
47
- waitfor(Process.spawn(env, *cmd, chdir: mirror, pgroup: true, out: File::NULL, err: File::NULL))
50
+ waitfor(Process.spawn(env, *cmd, chdir: mirror, pgroup: true, out: File::NULL, err: stderr))
48
51
  end
49
52
 
50
53
  def with_mirror
@@ -56,8 +59,14 @@ module Kimera
56
59
 
57
60
  def populate(mirror)
58
61
  root = plan.root
59
- IsolatedPlan.mirrors(Dir.children(root)).each do |entry|
60
- FileUtils.cp_r(File.join(root, entry), File.join(mirror, entry))
62
+ Dir.children(root).each { |entry| place(File.join(root, entry), File.join(mirror, entry), entry) }
63
+ end
64
+
65
+ def place(source, target, entry)
66
+ case plan.placement(entry)
67
+ when :copy then FileUtils.cp_r(source, target)
68
+ when :link then File.symlink(source, target)
69
+ when :empty then FileUtils.mkdir_p(target) if File.directory?(source)
61
70
  end
62
71
  end
63
72
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../registry/mutation_point"
4
+
5
+ module Kimera
6
+ module Execution
7
+ end
8
+ end
9
+
10
+ class Kimera::Execution::Kinship
11
+ def initialize(registry, coverage)
12
+ @registry = registry
13
+ @coverage = coverage
14
+ end
15
+
16
+ def order(id, tests)
17
+ (related(@registry.index.fetch(id, Kimera::MutationPoint::NONE)) & tests) | tests
18
+ end
19
+
20
+ private
21
+
22
+ def related(point)
23
+ siblings = @registry.at(point.file).sort_by { |other| other.method_name == point.method_name ? 0 : 1 }
24
+ known = @coverage.to_h
25
+ siblings.flat_map { |other| other.ids.flat_map { |mutant| Array(known[mutant]) } }
26
+ end
27
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "callback"
4
+ require_relative "reflections"
4
5
 
5
6
  module Kimera
6
7
  module Execution
@@ -56,14 +57,30 @@ module Kimera
56
57
  define_method(hook) do |base = nil, &block|
57
58
  next super(base, &block) unless block && OverlayGuardModules.replacing?(self, stored, base)
58
59
  instance_variable_set(stored, block)
60
+ OverlayGuardModules.reapply(self, block)
59
61
  end
60
62
  end
61
63
  end
62
64
 
63
65
  def serialization = SERIALIZEGUARD
64
66
 
67
+ def reflection = Reflections::GUARD
68
+
69
+ MIXES = Module.instance_method(:include?)
70
+
65
71
  def concern = CONCERNGUARD
66
72
 
73
+ def reapply(concern, block)
74
+ ObjectSpace.each_object(Class).each { |klass| klass.class_eval(&block) if owner?(klass, concern) }
75
+ end
76
+
77
+ def owner?(klass, concern)
78
+ return false if klass.singleton_class? || !mixes?(klass, concern)
79
+ [klass.superclass].compact.none? { |parent| mixes?(parent, concern) }
80
+ end
81
+
82
+ def mixes?(klass, concern) = MIXES.bind_call(klass, concern)
83
+
67
84
  def replacing?(target, stored, base)
68
85
  Kimera::Execution::OverlayGuards.overlaying? && !base && target.instance_variable_defined?(stored)
69
86
  end
@@ -26,6 +26,7 @@ module Kimera
26
26
  callbacks!
27
27
  concern!
28
28
  serialize!
29
+ reflections!
29
30
  end
30
31
 
31
32
  def enum!
@@ -43,6 +44,11 @@ module Kimera
43
44
  ActiveRecord::Base.singleton_class.prepend(OverlayGuardModules.serialization)
44
45
  end
45
46
 
47
+ def reflections!
48
+ return unless defined?(ActiveRecord::Reflection)
49
+ ActiveRecord::Reflection.singleton_class.prepend(OverlayGuardModules.reflection)
50
+ end
51
+
46
52
  def concern!
47
53
  return unless defined?(ActiveSupport::Concern)
48
54
  ActiveSupport::Concern.prepend(OverlayGuardModules.concern)
@@ -19,23 +19,31 @@ class Kimera::Execution::ParallelTestDatabases
19
19
  end
20
20
 
21
21
  def after_fork(index)
22
+ identify(index)
22
23
  return unless index && active?
23
24
  ActiveSupport::Testing::Parallelization.after_fork_hooks.each { |hook| hook.call(index) }
24
25
  @adapter.start
25
26
  end
26
27
 
27
28
  def before_exit(index)
28
- Array(index).each { cleanup }
29
+ Array(index).each { |worker| cleanup(worker) }
29
30
  rescue StandardError => error
30
31
  errors.puts("kimera: parallelize_teardown failed (#{error.class}: #{error.message})")
31
32
  end
32
33
 
33
34
  private
34
35
 
35
- def cleanup
36
+ def cleanup(index)
36
37
  return unless active?
37
38
  scrub
38
- ActiveSupport::Testing::Parallelization.run_cleanup_hooks.each(&:call)
39
+ ActiveSupport::Testing::Parallelization.run_cleanup_hooks.each { |hook| hook.call(index) }
40
+ end
41
+
42
+ def identify(index)
43
+ return unless index && @jobs > 1 && defined?(ActiveSupport::TestCase)
44
+ ActiveSupport::TestCase.parallel_worker_id = index
45
+ rescue NoMethodError
46
+ nil
39
47
  end
40
48
 
41
49
  def scrub
@@ -46,10 +46,11 @@ class Kimera::Execution::Pool
46
46
  @options[:coverage] = measured
47
47
  end
48
48
 
49
- def drive(queue, spawner, resolve:, lost:)
49
+ def drive(queue, spawner, resolve:, lost:, trace: nil)
50
+ database.before_fork
50
51
  Kimera::Execution::WorkerPool.new(
51
52
  queue: queue, spawner: spawner, jobs: @options.fetch(:jobs),
52
- hard_timeout: @options.fetch(:hard), resolve: resolve, lost: lost
53
+ hard_timeout: @options.fetch(:hard), resolve: resolve, lost: lost, trace: trace
53
54
  ).run
54
55
  end
55
56
 
@@ -66,7 +67,6 @@ class Kimera::Execution::Pool
66
67
  def database = @options.fetch(:paralleldb)
67
68
 
68
69
  def launch(entry, index)
69
- database.before_fork
70
70
  duty = Duty.new(entry, index, channels)
71
71
  [spawn(duty), *duty.pipes.parent!]
72
72
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ module Reflections
6
+ module_function
7
+
8
+ GUARD =
9
+ Module.new do
10
+ def add_reflection(owner, name, reflection)
11
+ key = Reflections.key(owner, name)
12
+ stale = OverlayGuards.overlaying? && owner._reflections[key]
13
+ super
14
+ Reflections.inherit(owner, key, stale, reflection) if stale
15
+ end
16
+ end
17
+
18
+ def key(owner, name)
19
+ text = name.to_s
20
+ owner._reflections.key?(text) ? text : name.to_sym
21
+ end
22
+
23
+ def inherit(owner, key, stale, reflection)
24
+ owner.descendants.each { |heir| replace(heir, key, stale, reflection) }
25
+ end
26
+
27
+ def replace(heir, key, stale, reflection)
28
+ held = heir._reflections
29
+ return unless held[key].equal?(stale)
30
+ heir.clear_reflections_cache
31
+ heir._reflections = held.merge(key => reflection)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -5,38 +5,16 @@ require_relative "../runtime"
5
5
  require_relative "../synthesis/overlay"
6
6
  require_relative "child_process"
7
7
  require_relative "overlay_guards"
8
+ require_relative "reload_errand"
8
9
  require_relative "verdicts"
9
10
 
10
11
  class Kimera::Execution::Reload
12
+ include Kimera::Execution::ChildProcess
13
+
11
14
  Failure =
12
15
  Data.define(:exception) do
13
16
  def message = "#{exception.class}: #{exception.message}"
14
17
  end
15
- include Kimera::Execution::ChildProcess
16
-
17
- Errand =
18
- Struct.new(:id, :reader, :writer) do
19
- def parent!
20
- writer.close
21
- end
22
-
23
- def child!
24
- reader.close
25
- end
26
-
27
- def emit(payload)
28
- writer.puts(payload)
29
- writer.close
30
- end
31
-
32
- def await(deadline, &)
33
- return reader.gets if reader.wait_readable(deadline)
34
- yield
35
- nil
36
- ensure
37
- reader.close
38
- end
39
- end
40
18
 
41
19
  def initialize(registry:, adapter:, isolation:, root:)
42
20
  @registry = registry
@@ -45,8 +23,8 @@ class Kimera::Execution::Reload
45
23
  @root = root
46
24
  end
47
25
 
48
- def run(id, deadline:)
49
- errand = Errand.new(id, *IO.pipe)
26
+ def run(id, deadline:, tests: @adapter.test_ids)
27
+ errand = Errand.new(id, tests, *IO.pipe)
50
28
  collect(errand, child(errand), deadline)
51
29
  end
52
30
 
@@ -59,32 +37,42 @@ class Kimera::Execution::Reload
59
37
  def work(errand)
60
38
  errand.child!
61
39
  silence!
62
- errand.emit(report(errand.id))
40
+ errand.emit(report(errand))
63
41
  exit!(0)
64
42
  end
65
43
 
66
- def report(id)
67
- status, fails = evaluate(id)
68
- JSON.generate(id: id, status: status.to_s, fails: fails)
44
+ def report(errand)
45
+ status, fails = evaluate(errand)
46
+ JSON.generate(id: errand.id, status: status.to_s, fails: Array(fails).map { |fail| fail.to_s.scrub })
69
47
  end
70
48
 
71
49
  def collect(errand, pid, deadline)
72
50
  line = errand.await(deadline) { kill(pid) }
73
- reap(pid)
74
- verdict(errand.id, line)
51
+ verdict(errand.id, line, reap(pid), deadline)
75
52
  end
76
53
 
77
- def evaluate(id)
78
- mutate(id).verdict
54
+ def evaluate(errand)
55
+ failed = mutate(errand)
56
+ failed ? failed.verdict : [:survived, []]
79
57
  rescue StandardError, ScriptError => error
80
58
  [:error, [Failure.new(error).message]]
81
59
  end
82
60
 
83
- def mutate(id)
61
+ def mutate(errand)
62
+ id = errand.id
84
63
  file = verdicts.point(id).file
85
64
  overlay(file, File.join(File.expand_path(@root), file), id)
86
65
  Kimera::Runtime.active = nil
87
- @isolation.around { @adapter.run(@adapter.test_ids) }
66
+ @isolation.around { hunt(errand) }
67
+ end
68
+
69
+ def hunt(errand)
70
+ errand.tests.lazy.map { |test| trial(test, errand) }.reject(&:passed?).first
71
+ end
72
+
73
+ def trial(test, errand)
74
+ errand.tick
75
+ @adapter.run([test])
88
76
  end
89
77
 
90
78
  def overlay(file, path, id)
@@ -92,12 +80,18 @@ class Kimera::Execution::Reload
92
80
  Kimera::Execution::OverlayGuards.overlay { Kimera::Overlay.evaluate(baked, path) }
93
81
  end
94
82
 
95
- def verdict(id, line)
96
- return verdicts.unjudged(id, "reload worker produced no result") unless line
83
+ def verdict(id, line, status = nil, deadline = nil)
84
+ return verdicts.timeout(id, deadline) if line == :timeout
85
+ return verdicts.unjudged(id, "reload worker produced no result#{exited(status)}") unless line
97
86
  message = parse(line)
98
87
  message ? verdicts.parse(message) : verdicts.unjudged(id, "reload worker output unparseable")
99
88
  end
100
89
 
90
+ def exited(status)
91
+ return "" unless status
92
+ status.signaled? ? " (died on signal #{status.termsig})" : " (exited #{status.exitstatus})"
93
+ end
94
+
101
95
  def verdicts
102
96
  Kimera::Execution::Verdicts.new(@registry)
103
97
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ end
6
+ end
7
+
8
+ class Kimera::Execution::Reload; end
9
+
10
+ Kimera::Execution::Reload::Errand =
11
+ Struct.new(:id, :tests, :reader, :writer) do
12
+ tick = "tick\n"
13
+ def parent!
14
+ writer.close
15
+ end
16
+
17
+ def child!
18
+ reader.close
19
+ writer.sync = true
20
+ end
21
+ define_method(:tick) { writer.write(tick) }
22
+ def emit(payload)
23
+ writer.puts(payload)
24
+ writer.close
25
+ end
26
+
27
+ def await(deadline)
28
+ heard(deadline).tap { |line| yield if line == :timeout }
29
+ ensure
30
+ reader.close
31
+ end
32
+ define_method(:heard) do |deadline|
33
+ while reader.wait_readable(deadline)
34
+ line = reader.gets
35
+ return line unless line == tick
36
+ end
37
+ :timeout
38
+ end
39
+ end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "priority"
4
+ require_relative "kinship"
4
5
  require_relative "reload"
5
6
  require_relative "verdicts"
6
7
 
7
8
  class Kimera::Execution::Schedule
8
9
  Ledger = Struct.new(:results, :leaks)
9
- Batch = Struct.new(:safe, :reloadable, :deferred, :ledger)
10
+ Batch = Struct.new(:safe, :reloadable, :deferred, :ledger, :coverage)
10
11
 
11
12
  LOSSES = { timeout: :expired }.freeze
12
13
 
@@ -37,14 +38,16 @@ class Kimera::Execution::Schedule
37
38
  safe, unsafe = ids.partition { |id| verdicts.safe?(id) }
38
39
  reloadable, deferred = unsafe.partition { |id| verdicts.reloadable?(id) }
39
40
  progress.start(safe.size + unsafe.size, label)
40
- Batch.new(Kimera::Execution::Priority.new(coverage).order(safe), reloadable, deferred, Ledger.new({}, []))
41
+ Batch.new(prioritized(safe, coverage), reloadable, deferred, Ledger.new({}, []), coverage)
41
42
  end
42
43
 
44
+ def prioritized(safe, coverage) = Kimera::Execution::Priority.new(coverage).order(safe)
45
+
43
46
  def process(batch)
44
- safe, reloadable, deferred, ledger = batch.to_a
45
- pool(safe, ledger)
46
- reload(reloadable, ledger)
47
- defer(deferred, ledger)
47
+ ledger = batch.ledger
48
+ pool(batch.safe, ledger)
49
+ reload(batch)
50
+ defer(batch.deferred, ledger)
48
51
  end
49
52
 
50
53
  def finish(ledger)
@@ -58,8 +61,13 @@ class Kimera::Execution::Schedule
58
61
  progress.tick(result.status)
59
62
  end
60
63
 
61
- def reload(ids, ledger)
62
- ids.each { |id| record(ledger, id, reloader.run(id, deadline: hard)) }
64
+ def reload(batch)
65
+ kinship = Kimera::Execution::Kinship.new(@registry, batch.coverage)
66
+ batch.reloadable.each { |id| record(batch.ledger, id, rerun(id, kinship)) }
67
+ end
68
+
69
+ def rerun(id, kinship)
70
+ reloader.run(id, deadline: hard, tests: kinship.order(id, @options.fetch(:adapter).test_ids))
63
71
  end
64
72
 
65
73
  def defer(ids, ledger)
@@ -97,7 +105,7 @@ class Kimera::Execution::Schedule
97
105
  def crashed(id) = verdicts.unjudged(id, "worker crashed before result")
98
106
 
99
107
  def reloader
100
- Kimera::Execution::Reload.new(
108
+ @_reloader ||= Kimera::Execution::Reload.new(
101
109
  registry: @registry, adapter: @options.fetch(:adapter),
102
110
  isolation: @options.fetch(:isolation), root: @options.fetch(:root)
103
111
  )
@@ -10,6 +10,11 @@ Kimera::Execution::WorkerPool::Worker =
10
10
  self.inflight = id
11
11
  end
12
12
 
13
+ def claim(id, limit)
14
+ busy!(id)
15
+ renew(limit)
16
+ end
17
+
13
18
  def renew(limit)
14
19
  self.deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + limit
15
20
  end
@@ -29,8 +29,8 @@ class Kimera::Execution::WorkerPool
29
29
  def assign(worker)
30
30
  id = fleet.take
31
31
  return close(worker) unless id
32
- worker.busy!(id)
33
- worker.renew(limit)
32
+ worker.claim(id, limit)
33
+ trace(worker.slot, id)
34
34
  worker.offer(id)
35
35
  end
36
36
 
@@ -83,6 +83,8 @@ class Kimera::Execution::WorkerPool
83
83
  worker.renew(limit)
84
84
  end
85
85
 
86
+ def trace(slot, id) = context.options[:trace]&.call(slot, id)
87
+
86
88
  def close(worker) = shut(worker.retire)
87
89
 
88
90
  def watch
@@ -42,7 +42,11 @@ class Kimera::Frameworks::Adapter
42
42
  id
43
43
  end
44
44
 
45
- @registry = {}
45
+ def reproduce(ids)
46
+ "rspec #{(ids & test_ids).join(" ")} --order defined"
47
+ end
48
+
49
+ @registry ||= {}
46
50
 
47
51
  NO_LOADER = -> {}
48
52
  ADAPTER_LOADERS = {
@@ -51,8 +51,27 @@ class Kimera::Frameworks::MinitestAdapter < Kimera::Frameworks::Adapter
51
51
  Kimera::Frameworks::RunOutcome.new(passed: failed.empty?, failed_ids: failed, failures: failures)
52
52
  end
53
53
 
54
+ def reproduce(ids)
55
+ "bundle exec ruby -Itest -rminitest/autorun -e #{quote(loads(ids))} -- -n #{quote(filter(ids))} --seed 1"
56
+ end
57
+
54
58
  private
55
59
 
60
+ def loads(ids)
61
+ ids.filter_map { |id| origin(id) }.uniq.map { |file| "require File.expand_path(#{file.dump})" }.join("; ")
62
+ end
63
+
64
+ def filter(ids) = "/^(?:#{ids.map { |id| Regexp.escape(id) }.join("|")})$/"
65
+
66
+ def origin(id)
67
+ klass, name = @methods[id]
68
+ return unless klass
69
+ path, = klass.instance_method(name).source_location
70
+ path.delete_prefix("#{Dir.pwd}#{File::SEPARATOR}")
71
+ end
72
+
73
+ def quote(text) = "'#{text.gsub("'") { "'\\''" }}'"
74
+
56
75
  def record(id, failed, failures)
57
76
  klass, name = @methods[id]
58
77
  return unless klass
@@ -24,7 +24,7 @@ class Kimera::Incremental::Session
24
24
  class << self
25
25
  def load(path, registry: nil)
26
26
  return new unless path && File.exist?(path)
27
- session = decode(JSON.parse(File.read(path)))
27
+ session = decode(JSON.parse(File.read(path, encoding: Encoding::UTF_8)))
28
28
  session.prune!(registry) if registry
29
29
  session
30
30
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/syntax"
4
+ require_relative "support/syntax_types"
5
+
6
+ class Kimera::MemoGuard
7
+ CONDITIONAL = "conditional"
8
+ BOOLEAN_LITERAL = "boolean_literal"
9
+ READ = Kimera::SyntaxTypes::InstanceVariableReadNode
10
+ WRITE = Kimera::SyntaxTypes::InstanceVariableWriteNode
11
+
12
+ Exemption = Data.define(:location, :operator)
13
+
14
+ class << self
15
+ def exemptions(source)
16
+ result = Kimera::Syntax.parse(source)
17
+ return [] if result.failure?
18
+ defs(result.value).flat_map { new(statements(it.body)).exemptions }
19
+ end
20
+
21
+ def statements(body) = body.is_a?(Kimera::SyntaxTypes::StatementsNode) ? body.body : []
22
+
23
+ def defs(node, found = [])
24
+ found << node if node.is_a?(Kimera::SyntaxTypes::DefNode)
25
+ node.compact_child_nodes.each { defs(it, found) }
26
+ found
27
+ end
28
+ end
29
+
30
+ def initialize(statements)
31
+ @guard, @flag, *@rest = statements
32
+ end
33
+
34
+ def exemptions
35
+ return [] unless name && (value? || flag?)
36
+ [Exemption.new(@guard.location, CONDITIONAL), *raised]
37
+ end
38
+
39
+ private
40
+
41
+ def name
42
+ predicate = @guard.predicate if guard?
43
+ predicate.name if predicate.is_a?(READ)
44
+ end
45
+
46
+ def guard? = @guard.is_a?(Kimera::SyntaxTypes::IfNode) && @guard.compact_child_nodes.size == 2 && returns?
47
+
48
+ def returns? = @guard.statements.body.size == 1 && returned.is_a?(Kimera::SyntaxTypes::ReturnNode)
49
+
50
+ def returned = @guard.statements.body.first
51
+
52
+ def value? = caches? && [@flag, *@rest].any? { named?(it, WRITE) }
53
+
54
+ def caches?
55
+ values = returned.arguments&.arguments
56
+ values&.size == 1 && named?(values.first, READ)
57
+ end
58
+
59
+ def flag? = returned.compact_child_nodes.empty? && raises?
60
+
61
+ def raises? = named?(@flag, WRITE) && @flag.value.is_a?(Kimera::SyntaxTypes::TrueNode)
62
+
63
+ def raised = flag? ? [Exemption.new(@flag.value.location, BOOLEAN_LITERAL)] : []
64
+
65
+ def named?(node, kind) = node.is_a?(kind) && node.name == name
66
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prism"
4
+ require_relative "../memo_guard"
4
5
  require_relative "../memoization"
5
6
  require_relative "mutation_point"
6
7
  require_relative "walking"
@@ -25,6 +26,8 @@ class Kimera::RegistryScan::SourceFile
25
26
 
26
27
  def tainted = @_tainted ||= Kimera::Memoization.ranges(@source)
27
28
 
29
+ def guards = @_guards ||= Kimera::MemoGuard.exemptions(@source)
30
+
28
31
  def mined(root)
29
32
  found = []
30
33
  walk(root, origin) { |node, cursor| bank(found, node, cursor) }
@@ -36,7 +39,7 @@ class Kimera::RegistryScan::SourceFile
36
39
  end
37
40
 
38
41
  def bank(found, node, cursor)
39
- pairs = harvest(applicable(cursor), node, cursor.position)
42
+ pairs = harvest(applicable(cursor), node, cursor.position).reject { |key, _| guarded?(node, key) }
40
43
  return if pairs.empty?
41
44
  found << assemble(node, pairs, cursor).taint!(tainted)
42
45
  end
@@ -46,6 +49,8 @@ class Kimera::RegistryScan::SourceFile
46
49
  cursor.position ? chosen : chosen.reject(&:statement?)
47
50
  end
48
51
 
52
+ def guarded?(node, key) = guards.include?(Kimera::MemoGuard::Exemption.new(node.location, key))
53
+
49
54
  def harvest(operators, node, position)
50
55
  operators.flat_map { |operator| operator.pairs(node, position: position) }
51
56
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kimera
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -119,7 +119,9 @@ bundle exec kimera run --isolated --jobs 4 # oracle mode (see Strengthen)
119
119
  suppress the warm path.
120
120
  - `--isolated` judges on failed tests, not the exit status. A child that
121
121
  exits non-zero with zero failures (typically a SimpleCov `minimum_coverage`
122
- floor tripped by the partial run) is `harness_error`, not a kill. Kimera
122
+ floor tripped by the partial run) is `harness_error`, not a kill, and so is
123
+ a child that crashes before reporting (read its `detail` for the stderr).
124
+ Kimera
123
125
  sets `KIMERA=1` wherever it loads the suite (warm, `--isolated`, doctor), so
124
126
  gate the floor on it (`minimum_coverage ... unless ENV["KIMERA"]`); `kimera
125
127
  doctor` warns when it isn't.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kimera
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giacomo GK
@@ -99,6 +99,7 @@ files:
99
99
  - lib/kimera/cli/workflows.rb
100
100
  - lib/kimera/error.rb
101
101
  - lib/kimera/execution/baseline_failure.rb
102
+ - lib/kimera/execution/baseline_failure/breakdown.rb
102
103
  - lib/kimera/execution/baseline_pass.rb
103
104
  - lib/kimera/execution/boot.rb
104
105
  - lib/kimera/execution/callback.rb
@@ -116,13 +117,16 @@ files:
116
117
  - lib/kimera/execution/isolated_verdict.rb
117
118
  - lib/kimera/execution/isolated_watchdog.rb
118
119
  - lib/kimera/execution/isolation.rb
120
+ - lib/kimera/execution/kinship.rb
119
121
  - lib/kimera/execution/null_progress.rb
120
122
  - lib/kimera/execution/overlay_guard_modules.rb
121
123
  - lib/kimera/execution/overlay_guards.rb
122
124
  - lib/kimera/execution/parallel_test_databases.rb
123
125
  - lib/kimera/execution/pool_driver.rb
124
126
  - lib/kimera/execution/priority.rb
127
+ - lib/kimera/execution/reflections.rb
125
128
  - lib/kimera/execution/reload.rb
129
+ - lib/kimera/execution/reload_errand.rb
126
130
  - lib/kimera/execution/rig.rb
127
131
  - lib/kimera/execution/schedule.rb
128
132
  - lib/kimera/execution/schemata.rb
@@ -147,6 +151,7 @@ files:
147
151
  - lib/kimera/incremental/git_diff.rb
148
152
  - lib/kimera/incremental/selection.rb
149
153
  - lib/kimera/incremental/session.rb
154
+ - lib/kimera/memo_guard.rb
150
155
  - lib/kimera/memoization.rb
151
156
  - lib/kimera/operators.rb
152
157
  - lib/kimera/operators/argument_drop.rb