kimera 0.1.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 (171) hide show
  1. checksums.yaml +7 -0
  2. data/.kimera.yml.example +59 -0
  3. data/CHANGELOG.md +36 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +528 -0
  6. data/exe/kimera +22 -0
  7. data/lib/kimera/audit/operator_audit.rb +71 -0
  8. data/lib/kimera/cli/argv.rb +15 -0
  9. data/lib/kimera/cli/baseline.rb +87 -0
  10. data/lib/kimera/cli/changed.rb +27 -0
  11. data/lib/kimera/cli/ci.rb +27 -0
  12. data/lib/kimera/cli/completion.rb +37 -0
  13. data/lib/kimera/cli/doctor.rb +115 -0
  14. data/lib/kimera/cli/flag.rb +55 -0
  15. data/lib/kimera/cli/help_text.rb +45 -0
  16. data/lib/kimera/cli/init.rb +105 -0
  17. data/lib/kimera/cli/mutant.rb +82 -0
  18. data/lib/kimera/cli/report_file.rb +20 -0
  19. data/lib/kimera/cli/run/arguments.rb +58 -0
  20. data/lib/kimera/cli/run/cycle.rb +78 -0
  21. data/lib/kimera/cli/run/digest.rb +49 -0
  22. data/lib/kimera/cli/run/emission.rb +45 -0
  23. data/lib/kimera/cli/run/gate.rb +43 -0
  24. data/lib/kimera/cli/run/options.rb +85 -0
  25. data/lib/kimera/cli/run/pass.rb +100 -0
  26. data/lib/kimera/cli/run/sources.rb +56 -0
  27. data/lib/kimera/cli/run.rb +52 -0
  28. data/lib/kimera/cli/skill.rb +23 -0
  29. data/lib/kimera/cli/suggestion.rb +36 -0
  30. data/lib/kimera/cli/survivors/panel.rb +81 -0
  31. data/lib/kimera/cli/survivors.rb +89 -0
  32. data/lib/kimera/cli/synthesize.rb +66 -0
  33. data/lib/kimera/cli/workflows.rb +8 -0
  34. data/lib/kimera/cli.rb +132 -0
  35. data/lib/kimera/error.rb +3 -0
  36. data/lib/kimera/execution/baseline_failure.rb +47 -0
  37. data/lib/kimera/execution/baseline_pass.rb +123 -0
  38. data/lib/kimera/execution/boot.rb +50 -0
  39. data/lib/kimera/execution/callback.rb +33 -0
  40. data/lib/kimera/execution/child_process.rb +48 -0
  41. data/lib/kimera/execution/composite_isolation.rb +24 -0
  42. data/lib/kimera/execution/harness.rb +150 -0
  43. data/lib/kimera/execution/isolated.rb +64 -0
  44. data/lib/kimera/execution/isolated_child.rb +15 -0
  45. data/lib/kimera/execution/isolated_child_command.rb +44 -0
  46. data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
  47. data/lib/kimera/execution/isolated_plan.rb +54 -0
  48. data/lib/kimera/execution/isolated_scheduling.rb +54 -0
  49. data/lib/kimera/execution/isolated_test_selection.rb +22 -0
  50. data/lib/kimera/execution/isolated_verdict.rb +72 -0
  51. data/lib/kimera/execution/isolated_watchdog.rb +59 -0
  52. data/lib/kimera/execution/isolation.rb +17 -0
  53. data/lib/kimera/execution/null_progress.rb +17 -0
  54. data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
  55. data/lib/kimera/execution/overlay_guards.rb +56 -0
  56. data/lib/kimera/execution/parallel_test_databases.rb +64 -0
  57. data/lib/kimera/execution/pool_driver.rb +101 -0
  58. data/lib/kimera/execution/priority.rb +23 -0
  59. data/lib/kimera/execution/reload.rb +104 -0
  60. data/lib/kimera/execution/rig.rb +41 -0
  61. data/lib/kimera/execution/schedule.rb +105 -0
  62. data/lib/kimera/execution/schemata.rb +88 -0
  63. data/lib/kimera/execution/shift/attempt.rb +34 -0
  64. data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
  65. data/lib/kimera/execution/shift/killer_memory.rb +20 -0
  66. data/lib/kimera/execution/shift/leak_guard.rb +34 -0
  67. data/lib/kimera/execution/shift.rb +137 -0
  68. data/lib/kimera/execution/sweep.rb +30 -0
  69. data/lib/kimera/execution/transaction_isolation.rb +32 -0
  70. data/lib/kimera/execution/trial.rb +16 -0
  71. data/lib/kimera/execution/verdicts.rb +78 -0
  72. data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
  73. data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
  74. data/lib/kimera/execution/worker_pool/worker.rb +32 -0
  75. data/lib/kimera/execution/worker_pool.rb +101 -0
  76. data/lib/kimera/frameworks/adapter.rb +82 -0
  77. data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
  78. data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
  79. data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
  80. data/lib/kimera/incremental/git_diff.rb +98 -0
  81. data/lib/kimera/incremental/selection.rb +27 -0
  82. data/lib/kimera/incremental/session.rb +109 -0
  83. data/lib/kimera/memoization.rb +104 -0
  84. data/lib/kimera/operators/argument_drop.rb +19 -0
  85. data/lib/kimera/operators/arithmetic.rb +12 -0
  86. data/lib/kimera/operators/audit.rb +3 -0
  87. data/lib/kimera/operators/base.rb +40 -0
  88. data/lib/kimera/operators/binary_swap.rb +23 -0
  89. data/lib/kimera/operators/boolean.rb +26 -0
  90. data/lib/kimera/operators/chain_link_deletion.rb +25 -0
  91. data/lib/kimera/operators/collection_literal.rb +18 -0
  92. data/lib/kimera/operators/comparison.rb +13 -0
  93. data/lib/kimera/operators/conditional.rb +36 -0
  94. data/lib/kimera/operators/default_argument.rb +17 -0
  95. data/lib/kimera/operators/element_drop.rb +31 -0
  96. data/lib/kimera/operators/index_fetch.rb +15 -0
  97. data/lib/kimera/operators/kernel_coercion.rb +17 -0
  98. data/lib/kimera/operators/method_unwrap.rb +20 -0
  99. data/lib/kimera/operators/negation.rb +15 -0
  100. data/lib/kimera/operators/node_swap.rb +20 -0
  101. data/lib/kimera/operators/numeric_literal.rb +39 -0
  102. data/lib/kimera/operators/op_assign.rb +18 -0
  103. data/lib/kimera/operators/plugins.rb +27 -0
  104. data/lib/kimera/operators/rails_association.rb +24 -0
  105. data/lib/kimera/operators/rails_callback.rb +49 -0
  106. data/lib/kimera/operators/rails_declaration.rb +16 -0
  107. data/lib/kimera/operators/rails_permit.rb +34 -0
  108. data/lib/kimera/operators/rails_validation.rb +17 -0
  109. data/lib/kimera/operators/range.rb +15 -0
  110. data/lib/kimera/operators/regexp_literal.rb +16 -0
  111. data/lib/kimera/operators/respond_to_guard.rb +24 -0
  112. data/lib/kimera/operators/return_value.rb +36 -0
  113. data/lib/kimera/operators/safe_navigation.rb +15 -0
  114. data/lib/kimera/operators/selector_swap.rb +24 -0
  115. data/lib/kimera/operators/statement_deletion.rb +26 -0
  116. data/lib/kimera/operators/string_literal.rb +22 -0
  117. data/lib/kimera/operators/symbol_literal.rb +26 -0
  118. data/lib/kimera/operators/vocabulary.rb +82 -0
  119. data/lib/kimera/operators.rb +141 -0
  120. data/lib/kimera/plugins.rb +3 -0
  121. data/lib/kimera/registry/builder.rb +59 -0
  122. data/lib/kimera/registry/mutation_point.rb +146 -0
  123. data/lib/kimera/registry/numbering.rb +15 -0
  124. data/lib/kimera/registry/registry.rb +120 -0
  125. data/lib/kimera/registry/source_file.rb +71 -0
  126. data/lib/kimera/registry/tally.rb +11 -0
  127. data/lib/kimera/registry/walking.rb +55 -0
  128. data/lib/kimera/report/actions.rb +38 -0
  129. data/lib/kimera/report/coloring.rb +25 -0
  130. data/lib/kimera/report/formats.rb +78 -0
  131. data/lib/kimera/report/live.rb +56 -0
  132. data/lib/kimera/report/log.rb +46 -0
  133. data/lib/kimera/report/progress.rb +55 -0
  134. data/lib/kimera/report/screen.rb +44 -0
  135. data/lib/kimera/report/sections.rb +50 -0
  136. data/lib/kimera/report/tally.rb +84 -0
  137. data/lib/kimera/report/text.rb +104 -0
  138. data/lib/kimera/results/result.rb +72 -0
  139. data/lib/kimera/results/run_report.rb +114 -0
  140. data/lib/kimera/rewrite/ast_walk.rb +20 -0
  141. data/lib/kimera/rewrite/directive.rb +61 -0
  142. data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
  143. data/lib/kimera/rewrite/directive_handlers.rb +138 -0
  144. data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
  145. data/lib/kimera/rewrite/numbered_params.rb +50 -0
  146. data/lib/kimera/runtime.rb +51 -0
  147. data/lib/kimera/scope/config.rb +90 -0
  148. data/lib/kimera/scope/file_set.rb +51 -0
  149. data/lib/kimera/scope/ignore_list.rb +55 -0
  150. data/lib/kimera/self_protection.rb +56 -0
  151. data/lib/kimera/support/duration.rb +13 -0
  152. data/lib/kimera/support/errors.rb +8 -0
  153. data/lib/kimera/support/operator_protocol.rb +10 -0
  154. data/lib/kimera/support/syntax.rb +68 -0
  155. data/lib/kimera/support/syntax_types.rb +20 -0
  156. data/lib/kimera/support/unparse.rb +15 -0
  157. data/lib/kimera/support/version.rb +5 -0
  158. data/lib/kimera/support/warnings.rb +15 -0
  159. data/lib/kimera/synthesis/file_weave.rb +78 -0
  160. data/lib/kimera/synthesis/guard_weaver.rb +77 -0
  161. data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
  162. data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
  163. data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
  164. data/lib/kimera/synthesis/overlay.rb +52 -0
  165. data/lib/kimera/synthesis/overlay_splice.rb +101 -0
  166. data/lib/kimera/synthesis/project.rb +58 -0
  167. data/lib/kimera/synthesis/source_map.rb +42 -0
  168. data/lib/kimera/version.rb +3 -0
  169. data/lib/kimera.rb +13 -0
  170. data/skills/kimera/SKILL.md +118 -0
  171. metadata +259 -0
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "callback"
4
+
5
+ module Kimera
6
+ module Execution
7
+ module OverlayGuardModules
8
+ module_function
9
+
10
+ ENUMGUARD =
11
+ Module.new do
12
+ def enum(*args, **kwargs, &)
13
+ name = args.find { |a| a.is_a?(Symbol) || a.is_a?(String) } || kwargs.keys.first
14
+ return if name && respond_to?(:defined_enums) && defined_enums.key?(name.to_s)
15
+ super
16
+ end
17
+ end
18
+
19
+ CALLBACKGUARD =
20
+ Module.new do
21
+ def set_callback(name, *filter_list, &block)
22
+ return super unless Kimera::Execution::OverlayGuards.overlaying?
23
+ type, filters, options = normalize_callback_params(filter_list.dup, block)
24
+ fresh = Kimera::Execution::OverlayGuardModules.fresh(__send__(:get_callbacks, name), type, filters)
25
+ return if fresh.empty?
26
+ super(name, type, *fresh, options)
27
+ end
28
+ end
29
+
30
+ def enum = ENUMGUARD
31
+
32
+ def callback = CALLBACKGUARD
33
+
34
+ def fresh(chain, type, filters)
35
+ running = chain.select { |callback| callback.kind == type }
36
+ filters.reject { |filter| duplicate?(running, filter) }
37
+ end
38
+
39
+ def duplicate?(callbacks, filter)
40
+ match = Kimera::Execution::Callback.new(filter)
41
+ callbacks.any? { |callback| match.duplicate?(callback.filter) }
42
+ end
43
+
44
+ SERIALIZEGUARD =
45
+ Module.new do
46
+ def serialize(attribute, *args, **options, &)
47
+ return if OverlayGuards.overlaying? && OverlayGuardModules.serialized?(type_for_attribute(attribute))
48
+ super
49
+ end
50
+ end
51
+
52
+ CONCERNGUARD =
53
+ Module.new do
54
+ %i[included prepended].each do |hook|
55
+ stored = :"@_#{hook}_block"
56
+ define_method(hook) do |base = nil, &block|
57
+ next super(base, &block) unless block && OverlayGuardModules.replacing?(self, stored, base)
58
+ instance_variable_set(stored, block)
59
+ end
60
+ end
61
+ end
62
+
63
+ def serialization = SERIALIZEGUARD
64
+
65
+ def concern = CONCERNGUARD
66
+
67
+ def replacing?(target, stored, base)
68
+ Kimera::Execution::OverlayGuards.overlaying? && !base && target.instance_variable_defined?(stored)
69
+ end
70
+
71
+ def serialized?(type, depth = 10)
72
+ return false unless depth.positive?
73
+ return true if leaf?(type)
74
+ serialized?(unwrap(type), depth - 1)
75
+ end
76
+
77
+ def leaf?(type)
78
+ defined?(ActiveRecord::Type::Serialized) &&
79
+ type.is_a?(ActiveRecord::Type::Serialized)
80
+ end
81
+
82
+ def unwrap(type)
83
+ type.cast_type
84
+ rescue NoMethodError
85
+ subtype(type)
86
+ end
87
+
88
+ def subtype(type)
89
+ type.subtype
90
+ rescue NoMethodError
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "overlay_guard_modules"
4
+
5
+ module Kimera
6
+ module Execution
7
+ module OverlayGuards
8
+ class << self
9
+ attr_accessor :overlaying
10
+
11
+ alias overlaying? overlaying
12
+ end
13
+
14
+ module_function
15
+
16
+ def overlay
17
+ install!
18
+ self.overlaying = true
19
+ yield
20
+ ensure
21
+ self.overlaying = false
22
+ end
23
+
24
+ def install!
25
+ enum!
26
+ callbacks!
27
+ concern!
28
+ serialize!
29
+ end
30
+
31
+ def enum!
32
+ return unless defined?(ActiveRecord::Base)
33
+ ActiveRecord::Base.singleton_class.prepend(OverlayGuardModules.enum)
34
+ end
35
+
36
+ def callbacks!
37
+ return unless defined?(ActiveSupport::Callbacks::ClassMethods)
38
+ ActiveSupport::Callbacks::ClassMethods.prepend(OverlayGuardModules.callback)
39
+ end
40
+
41
+ def serialize!
42
+ return unless defined?(ActiveRecord::Base)
43
+ ActiveRecord::Base.singleton_class.prepend(OverlayGuardModules.serialization)
44
+ end
45
+
46
+ def concern!
47
+ return unless defined?(ActiveSupport::Concern)
48
+ ActiveSupport::Concern.prepend(OverlayGuardModules.concern)
49
+ end
50
+
51
+ def serialized?(type)
52
+ OverlayGuardModules.serialized?(type)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Kimera::Execution::ParallelTestDatabases
4
+ def initialize(adapter:, jobs:, errors: nil)
5
+ @adapter = adapter
6
+ @jobs = jobs
7
+ @errors = errors
8
+ end
9
+
10
+ def active?
11
+ return false unless eligible?
12
+ register!
13
+ ActiveSupport::Testing::Parallelization.after_fork_hooks.any?
14
+ end
15
+
16
+ def before_fork
17
+ return unless active?
18
+ ActiveSupport::Testing::Parallelization.before_fork_hooks.each(&:call)
19
+ end
20
+
21
+ def after_fork(index)
22
+ return unless index && active?
23
+ ActiveSupport::Testing::Parallelization.after_fork_hooks.each { |hook| hook.call(index) }
24
+ @adapter.start
25
+ end
26
+
27
+ def before_exit(index)
28
+ Array(index).each { cleanup }
29
+ rescue StandardError => error
30
+ errors.puts("kimera: parallelize_teardown failed (#{error.class}: #{error.message})")
31
+ end
32
+
33
+ private
34
+
35
+ def cleanup
36
+ return unless active?
37
+ ActiveSupport::Testing::Parallelization.run_cleanup_hooks.each(&:call)
38
+ end
39
+
40
+ def errors = @errors || $stderr
41
+
42
+ def eligible?
43
+ return false unless @jobs > 1
44
+ return false unless defined?(ActiveSupport::Testing::Parallelization)
45
+ configured?
46
+ end
47
+
48
+ def configured?
49
+ ActiveSupport.parallelize_test_databases
50
+ rescue NoMethodError
51
+ false
52
+ end
53
+
54
+ def register!
55
+ return unless defined?(ActiveRecord::Base)
56
+ require("active_record/test_databases")
57
+ rescue LoadError, StandardError => error
58
+ errors.puts(notice(error))
59
+ end
60
+
61
+ def notice(error)
62
+ "kimera: per-worker test databases unavailable (#{error.class}: #{error.message}); workers share one database"
63
+ end
64
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "child_process"
4
+ require_relative "shift"
5
+ require_relative "worker_pool"
6
+
7
+ class Kimera::Execution::Pool
8
+ include Kimera::Execution::ChildProcess
9
+
10
+ Channels =
11
+ Struct.new(:req_r, :req_w, :res_r, :res_w) do
12
+ def parent!
13
+ req_r.close
14
+ res_w.close
15
+ [req_w, res_r]
16
+ end
17
+
18
+ def child!
19
+ req_w.close
20
+ res_r.close
21
+ end
22
+ end
23
+
24
+ Duty =
25
+ Struct.new(:entry, :index, :pipes) do
26
+ def run(shift)
27
+ shift.public_send(entry, pipes.req_r, pipes.res_w)
28
+ end
29
+
30
+ def finish(database)
31
+ database.before_exit(index)
32
+ pipes.res_w.close
33
+ end
34
+ end
35
+
36
+ def initialize(adapter:, registry:, isolation:, **options)
37
+ @adapter = adapter
38
+ @registry = registry
39
+ @isolation = isolation
40
+ @options = options
41
+ end
42
+
43
+ def coverage = @options[:coverage]
44
+
45
+ def coverage=(measured)
46
+ @options[:coverage] = measured
47
+ end
48
+
49
+ def drive(queue, spawner, resolve:, lost:)
50
+ Kimera::Execution::WorkerPool.new(
51
+ queue: queue, spawner: spawner, jobs: @options.fetch(:jobs),
52
+ hard_timeout: @options.fetch(:hard), resolve: resolve, lost: lost
53
+ ).run
54
+ end
55
+
56
+ def worker(index = nil)
57
+ launch(:serve, index)
58
+ end
59
+
60
+ def channel(index = nil)
61
+ launch(:coverage, index)
62
+ end
63
+
64
+ private
65
+
66
+ def database = @options.fetch(:paralleldb)
67
+
68
+ def launch(entry, index)
69
+ database.before_fork
70
+ duty = Duty.new(entry, index, channels)
71
+ [spawn(duty), *duty.pipes.parent!]
72
+ end
73
+
74
+ def channels
75
+ request, response = Array.new(2) { IO.pipe }
76
+ Channels.new(*request, *response)
77
+ end
78
+
79
+ def spawn(duty) = fork { boot(duty) }
80
+
81
+ def boot(duty)
82
+ duty.pipes.child!
83
+ silence!
84
+ database.after_fork(duty.index)
85
+ serve(duty)
86
+ exit!(0)
87
+ end
88
+
89
+ def serve(duty)
90
+ duty.run(build)
91
+ ensure
92
+ duty.finish(database)
93
+ end
94
+
95
+ def build
96
+ Kimera::Execution::Shift.new(
97
+ adapter: @adapter, registry: @registry, coverage: coverage,
98
+ isolation: @isolation, soft_timeout: @options.fetch(:soft), leak_every: @options.fetch(:leak)
99
+ )
100
+ end
101
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kimera
4
+ module Execution
5
+ end
6
+ end
7
+
8
+ class Kimera::Execution::Priority
9
+ def initialize(coverage)
10
+ @coverage = coverage
11
+ end
12
+
13
+ def order(ids)
14
+ return ids unless @coverage
15
+ ids.sort_by { |id| -count(id) }
16
+ end
17
+
18
+ private
19
+
20
+ def count(id)
21
+ @coverage.fetch(id) { @coverage.fetch(id.to_s, []) }.size
22
+ end
23
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../runtime"
5
+ require_relative "../synthesis/overlay"
6
+ require_relative "child_process"
7
+ require_relative "overlay_guards"
8
+ require_relative "verdicts"
9
+
10
+ class Kimera::Execution::Reload
11
+ Failure =
12
+ Data.define(:exception) do
13
+ def message = "#{exception.class}: #{exception.message}"
14
+ 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
+
41
+ def initialize(registry:, adapter:, isolation:, root:)
42
+ @registry = registry
43
+ @adapter = adapter
44
+ @isolation = isolation
45
+ @root = root
46
+ end
47
+
48
+ def run(id, deadline:)
49
+ errand = Errand.new(id, *IO.pipe)
50
+ collect(errand, child(errand), deadline)
51
+ end
52
+
53
+ private
54
+
55
+ def child(errand)
56
+ fork { work(errand) }.tap { errand.parent! }
57
+ end
58
+
59
+ def work(errand)
60
+ errand.child!
61
+ silence!
62
+ errand.emit(report(errand.id))
63
+ exit!(0)
64
+ end
65
+
66
+ def report(id)
67
+ status, fails = evaluate(id)
68
+ JSON.generate(id: id, status: status.to_s, fails: fails)
69
+ end
70
+
71
+ def collect(errand, pid, deadline)
72
+ line = errand.await(deadline) { kill(pid) }
73
+ reap(pid)
74
+ verdict(errand.id, line)
75
+ end
76
+
77
+ def evaluate(id)
78
+ mutate(id).verdict
79
+ rescue StandardError, ScriptError => error
80
+ [:error, [Failure.new(error).message]]
81
+ end
82
+
83
+ def mutate(id)
84
+ file = verdicts.point(id).file
85
+ overlay(file, File.join(File.expand_path(@root), file), id)
86
+ Kimera::Runtime.active = nil
87
+ @isolation.around { @adapter.run(@adapter.test_ids) }
88
+ end
89
+
90
+ def overlay(file, path, id)
91
+ baked = Kimera::Overlay.new(@registry).bake(file, File.read(path, encoding: Encoding::UTF_8), id)
92
+ Kimera::Execution::OverlayGuards.overlay { Kimera::Overlay.evaluate(baked, path) }
93
+ end
94
+
95
+ def verdict(id, line)
96
+ return verdicts.unjudged(id, "reload worker produced no result") unless line
97
+ message = parse(line)
98
+ message ? verdicts.parse(message) : verdicts.unjudged(id, "reload worker output unparseable")
99
+ end
100
+
101
+ def verdicts
102
+ Kimera::Execution::Verdicts.new(@registry)
103
+ end
104
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "parallel_test_databases"
4
+ require_relative "pool_driver"
5
+ require_relative "schedule"
6
+
7
+ class Kimera::Execution::Rig
8
+ def initialize(**options)
9
+ @options = options
10
+ end
11
+
12
+ def driver = @_driver ||= pool
13
+
14
+ def spawner = @_spawner ||= @options[:spawner] || driver.method(:worker)
15
+
16
+ def schedule = @_schedule ||= plan
17
+
18
+ private
19
+
20
+ def parallel
21
+ @_parallel ||= Kimera::Execution::ParallelTestDatabases.new(
22
+ adapter: @options.fetch(:adapter), jobs: @options.fetch(:jobs)
23
+ )
24
+ end
25
+
26
+ def pool
27
+ Kimera::Execution::Pool.new(
28
+ adapter: @options.fetch(:adapter), registry: @options.fetch(:registry),
29
+ isolation: @options.fetch(:isolation), jobs: @options.fetch(:jobs), hard: @options.fetch(:hard),
30
+ soft: @options.fetch(:soft), leak: @options.fetch(:leak), paralleldb: parallel, coverage: @options[:coverage]
31
+ )
32
+ end
33
+
34
+ def plan
35
+ Kimera::Execution::Schedule.new(
36
+ registry: @options.fetch(:registry), driver: driver, spawner: spawner,
37
+ adapter: @options.fetch(:adapter), isolation: @options.fetch(:isolation),
38
+ root: @options.fetch(:root), progress: @options.fetch(:progress), hard: @options.fetch(:hard)
39
+ )
40
+ end
41
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "priority"
4
+ require_relative "reload"
5
+ require_relative "verdicts"
6
+
7
+ class Kimera::Execution::Schedule
8
+ Ledger = Struct.new(:results, :leaks)
9
+ Batch = Struct.new(:safe, :reloadable, :deferred, :ledger)
10
+
11
+ LOSSES = { timeout: :expired }.freeze
12
+
13
+ def initialize(registry:, driver:, spawner:, **options)
14
+ @registry = registry
15
+ @driver = driver
16
+ @spawner = spawner
17
+ @options = options
18
+ end
19
+
20
+ def run(ids, coverage:, label: "mutants")
21
+ batch = plan(ids, coverage, label)
22
+ process(batch)
23
+ finish(batch.ledger)
24
+ ensure
25
+ progress.finish
26
+ end
27
+
28
+ private
29
+
30
+ def progress = @options.fetch(:progress)
31
+
32
+ def hard = @options[:hard]
33
+
34
+ def verdicts = @_verdicts ||= Kimera::Execution::Verdicts.new(@registry)
35
+
36
+ def plan(ids, coverage, label)
37
+ safe, unsafe = ids.partition { |id| verdicts.safe?(id) }
38
+ reloadable, deferred = unsafe.partition { |id| verdicts.reloadable?(id) }
39
+ progress.start(safe.size + unsafe.size, label)
40
+ Batch.new(Kimera::Execution::Priority.new(coverage).order(safe), reloadable, deferred, Ledger.new({}, []))
41
+ end
42
+
43
+ def process(batch)
44
+ safe, reloadable, deferred, ledger = batch.to_a
45
+ pool(safe, ledger)
46
+ reload(reloadable, ledger)
47
+ defer(deferred, ledger)
48
+ end
49
+
50
+ def finish(ledger)
51
+ results = ledger.results
52
+ verdicts.reclassify!(results)
53
+ Kimera::RunReport.new(results: results.values, leaks: ledger.leaks, registry: @registry)
54
+ end
55
+
56
+ def record(ledger, id, result)
57
+ ledger.results[id] = result
58
+ progress.tick(result.status)
59
+ end
60
+
61
+ def reload(ids, ledger)
62
+ ids.each { |id| record(ledger, id, reloader.run(id, deadline: hard)) }
63
+ end
64
+
65
+ def defer(ids, ledger)
66
+ ids.each { |id| record(ledger, id, verdicts.quarantine(id)) }
67
+ end
68
+
69
+ def pool(queue, ledger)
70
+ @driver.drive(queue, @spawner, resolve: resolver(ledger), lost: loser(ledger))
71
+ end
72
+
73
+ def resolver(ledger)
74
+ ->(message) { resolve(message, ledger) }
75
+ end
76
+
77
+ def loser(ledger)
78
+ ->(id, reason) { record(ledger, id, lost(id, reason)) }
79
+ end
80
+
81
+ def resolve(message, ledger)
82
+ case message["t"]
83
+ when "result" then parse(message, ledger)
84
+ when "leak" then ledger.leaks << Kimera::LeakReport.new(mutant_id: message["id"], detail: message["detail"])
85
+ end
86
+ end
87
+
88
+ def parse(message, ledger)
89
+ result = verdicts.parse(message)
90
+ record(ledger, result.mutant_id, result)
91
+ end
92
+
93
+ def lost(id, reason) = __send__(LOSSES.fetch(reason, :crashed), id)
94
+
95
+ def expired(id) = verdicts.timeout(id, hard)
96
+
97
+ def crashed(id) = verdicts.unjudged(id, "worker crashed before result")
98
+
99
+ def reloader
100
+ Kimera::Execution::Reload.new(
101
+ registry: @registry, adapter: @options.fetch(:adapter),
102
+ isolation: @options.fetch(:isolation), root: @options.fetch(:root)
103
+ )
104
+ end
105
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require_relative "../self_protection"
5
+ require_relative "../synthesis/overlay"
6
+ require_relative "overlay_guards"
7
+
8
+ class Kimera::Execution::Schemata
9
+ attr_reader :skipped
10
+
11
+ def initialize(registry, root: ".", errors: nil)
12
+ @registry = registry
13
+ @root = root
14
+ @errors = errors
15
+ @skipped = {}
16
+ end
17
+
18
+ class << self
19
+ def install!
20
+ Kimera::Execution::OverlayGuards.install!
21
+ end
22
+
23
+ def with_guards(&)
24
+ Kimera::Execution::OverlayGuards.overlay(&)
25
+ end
26
+ end
27
+
28
+ def overlay!
29
+ Kimera::Execution::OverlayGuards.install!
30
+ @registry.files.flat_map { |path| load(path) }
31
+ end
32
+
33
+ private
34
+
35
+ def load(path)
36
+ file = File.join(root, path)
37
+ return [] unless File.file?(file)
38
+ return [] if Kimera::SelfProtection.protected?(file)
39
+ overlay(path, file)
40
+ end
41
+
42
+ def overlay(path, file)
43
+ result = synthesize(path, file)
44
+ return [] if result.mutant_ids.empty?
45
+ apply(result, file)
46
+ rescue StandardError, ScriptError, SystemExit => error
47
+ skip(path, error)
48
+ end
49
+
50
+ def synthesize(path, file)
51
+ synth.synthesize(path, File.read(file, encoding: Encoding::UTF_8))
52
+ end
53
+
54
+ def apply(result, file)
55
+ silence { result.install(file) }
56
+ end
57
+
58
+ def skip(path, error)
59
+ reason = "#{error.class}: #{error.message}"
60
+ @skipped[path] = reason
61
+ notice(path, reason)
62
+ []
63
+ end
64
+
65
+ def notice(path, reason)
66
+ errors.puts(
67
+ "kimera: #{path} cannot run in warm workers — schemata setup failed " \
68
+ "(#{reason}); its mutants are reported no_coverage"
69
+ )
70
+ end
71
+
72
+ def errors = @errors || $stderr
73
+
74
+ def root = @_root ||= File.expand_path(@root)
75
+
76
+ def synth = @_synth ||= Kimera::Overlay.new(@registry)
77
+
78
+ def silence(&)
79
+ original, captured = capture
80
+ Kimera::Execution::OverlayGuards.overlay(&).tap { original.write(captured.string) }
81
+ ensure
82
+ $stderr = original
83
+ end
84
+
85
+ def capture
86
+ [$stderr, $stderr = StringIO.new]
87
+ end
88
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ class Kimera::Execution::Shift::Attempt
6
+ def initialize(adapter:, isolation:, killers:, timeout:)
7
+ @adapter = adapter
8
+ @isolation = isolation
9
+ @killers = killers
10
+ @timeout = timeout
11
+ end
12
+
13
+ def run(id, tests)
14
+ timeout { @isolation.around { attempt(id, tests) } }
15
+ end
16
+
17
+ private
18
+
19
+ def attempt(id, tests)
20
+ @killers.order(tests).lazy.map { |testid| kill(id, testid) }.find { |outcome| outcome }
21
+ end
22
+
23
+ def kill(id, testid)
24
+ Kimera::Runtime.active = id
25
+ outcome = @adapter.run([testid])
26
+ return if outcome.passed?
27
+ @killers.remember(testid)
28
+ outcome
29
+ end
30
+
31
+ def timeout(&)
32
+ Timeout.timeout(@timeout, &)
33
+ end
34
+ end