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.
- checksums.yaml +7 -0
- data/.kimera.yml.example +59 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +528 -0
- data/exe/kimera +22 -0
- data/lib/kimera/audit/operator_audit.rb +71 -0
- data/lib/kimera/cli/argv.rb +15 -0
- data/lib/kimera/cli/baseline.rb +87 -0
- data/lib/kimera/cli/changed.rb +27 -0
- data/lib/kimera/cli/ci.rb +27 -0
- data/lib/kimera/cli/completion.rb +37 -0
- data/lib/kimera/cli/doctor.rb +115 -0
- data/lib/kimera/cli/flag.rb +55 -0
- data/lib/kimera/cli/help_text.rb +45 -0
- data/lib/kimera/cli/init.rb +105 -0
- data/lib/kimera/cli/mutant.rb +82 -0
- data/lib/kimera/cli/report_file.rb +20 -0
- data/lib/kimera/cli/run/arguments.rb +58 -0
- data/lib/kimera/cli/run/cycle.rb +78 -0
- data/lib/kimera/cli/run/digest.rb +49 -0
- data/lib/kimera/cli/run/emission.rb +45 -0
- data/lib/kimera/cli/run/gate.rb +43 -0
- data/lib/kimera/cli/run/options.rb +85 -0
- data/lib/kimera/cli/run/pass.rb +100 -0
- data/lib/kimera/cli/run/sources.rb +56 -0
- data/lib/kimera/cli/run.rb +52 -0
- data/lib/kimera/cli/skill.rb +23 -0
- data/lib/kimera/cli/suggestion.rb +36 -0
- data/lib/kimera/cli/survivors/panel.rb +81 -0
- data/lib/kimera/cli/survivors.rb +89 -0
- data/lib/kimera/cli/synthesize.rb +66 -0
- data/lib/kimera/cli/workflows.rb +8 -0
- data/lib/kimera/cli.rb +132 -0
- data/lib/kimera/error.rb +3 -0
- data/lib/kimera/execution/baseline_failure.rb +47 -0
- data/lib/kimera/execution/baseline_pass.rb +123 -0
- data/lib/kimera/execution/boot.rb +50 -0
- data/lib/kimera/execution/callback.rb +33 -0
- data/lib/kimera/execution/child_process.rb +48 -0
- data/lib/kimera/execution/composite_isolation.rb +24 -0
- data/lib/kimera/execution/harness.rb +150 -0
- data/lib/kimera/execution/isolated.rb +64 -0
- data/lib/kimera/execution/isolated_child.rb +15 -0
- data/lib/kimera/execution/isolated_child_command.rb +44 -0
- data/lib/kimera/execution/isolated_child_minitest.rb +34 -0
- data/lib/kimera/execution/isolated_plan.rb +54 -0
- data/lib/kimera/execution/isolated_scheduling.rb +54 -0
- data/lib/kimera/execution/isolated_test_selection.rb +22 -0
- data/lib/kimera/execution/isolated_verdict.rb +72 -0
- data/lib/kimera/execution/isolated_watchdog.rb +59 -0
- data/lib/kimera/execution/isolation.rb +17 -0
- data/lib/kimera/execution/null_progress.rb +17 -0
- data/lib/kimera/execution/overlay_guard_modules.rb +95 -0
- data/lib/kimera/execution/overlay_guards.rb +56 -0
- data/lib/kimera/execution/parallel_test_databases.rb +64 -0
- data/lib/kimera/execution/pool_driver.rb +101 -0
- data/lib/kimera/execution/priority.rb +23 -0
- data/lib/kimera/execution/reload.rb +104 -0
- data/lib/kimera/execution/rig.rb +41 -0
- data/lib/kimera/execution/schedule.rb +105 -0
- data/lib/kimera/execution/schemata.rb +88 -0
- data/lib/kimera/execution/shift/attempt.rb +34 -0
- data/lib/kimera/execution/shift/coverage_channel.rb +43 -0
- data/lib/kimera/execution/shift/killer_memory.rb +20 -0
- data/lib/kimera/execution/shift/leak_guard.rb +34 -0
- data/lib/kimera/execution/shift.rb +137 -0
- data/lib/kimera/execution/sweep.rb +30 -0
- data/lib/kimera/execution/transaction_isolation.rb +32 -0
- data/lib/kimera/execution/trial.rb +16 -0
- data/lib/kimera/execution/verdicts.rb +78 -0
- data/lib/kimera/execution/worker_pool/fleet.rb +104 -0
- data/lib/kimera/execution/worker_pool/stillborn_guard.rb +40 -0
- data/lib/kimera/execution/worker_pool/worker.rb +32 -0
- data/lib/kimera/execution/worker_pool.rb +101 -0
- data/lib/kimera/frameworks/adapter.rb +82 -0
- data/lib/kimera/frameworks/minitest_adapter.rb +102 -0
- data/lib/kimera/frameworks/rspec_adapter.rb +123 -0
- data/lib/kimera/frameworks/rspec_group_index.rb +45 -0
- data/lib/kimera/incremental/git_diff.rb +98 -0
- data/lib/kimera/incremental/selection.rb +27 -0
- data/lib/kimera/incremental/session.rb +109 -0
- data/lib/kimera/memoization.rb +104 -0
- data/lib/kimera/operators/argument_drop.rb +19 -0
- data/lib/kimera/operators/arithmetic.rb +12 -0
- data/lib/kimera/operators/audit.rb +3 -0
- data/lib/kimera/operators/base.rb +40 -0
- data/lib/kimera/operators/binary_swap.rb +23 -0
- data/lib/kimera/operators/boolean.rb +26 -0
- data/lib/kimera/operators/chain_link_deletion.rb +25 -0
- data/lib/kimera/operators/collection_literal.rb +18 -0
- data/lib/kimera/operators/comparison.rb +13 -0
- data/lib/kimera/operators/conditional.rb +36 -0
- data/lib/kimera/operators/default_argument.rb +17 -0
- data/lib/kimera/operators/element_drop.rb +31 -0
- data/lib/kimera/operators/index_fetch.rb +15 -0
- data/lib/kimera/operators/kernel_coercion.rb +17 -0
- data/lib/kimera/operators/method_unwrap.rb +20 -0
- data/lib/kimera/operators/negation.rb +15 -0
- data/lib/kimera/operators/node_swap.rb +20 -0
- data/lib/kimera/operators/numeric_literal.rb +39 -0
- data/lib/kimera/operators/op_assign.rb +18 -0
- data/lib/kimera/operators/plugins.rb +27 -0
- data/lib/kimera/operators/rails_association.rb +24 -0
- data/lib/kimera/operators/rails_callback.rb +49 -0
- data/lib/kimera/operators/rails_declaration.rb +16 -0
- data/lib/kimera/operators/rails_permit.rb +34 -0
- data/lib/kimera/operators/rails_validation.rb +17 -0
- data/lib/kimera/operators/range.rb +15 -0
- data/lib/kimera/operators/regexp_literal.rb +16 -0
- data/lib/kimera/operators/respond_to_guard.rb +24 -0
- data/lib/kimera/operators/return_value.rb +36 -0
- data/lib/kimera/operators/safe_navigation.rb +15 -0
- data/lib/kimera/operators/selector_swap.rb +24 -0
- data/lib/kimera/operators/statement_deletion.rb +26 -0
- data/lib/kimera/operators/string_literal.rb +22 -0
- data/lib/kimera/operators/symbol_literal.rb +26 -0
- data/lib/kimera/operators/vocabulary.rb +82 -0
- data/lib/kimera/operators.rb +141 -0
- data/lib/kimera/plugins.rb +3 -0
- data/lib/kimera/registry/builder.rb +59 -0
- data/lib/kimera/registry/mutation_point.rb +146 -0
- data/lib/kimera/registry/numbering.rb +15 -0
- data/lib/kimera/registry/registry.rb +120 -0
- data/lib/kimera/registry/source_file.rb +71 -0
- data/lib/kimera/registry/tally.rb +11 -0
- data/lib/kimera/registry/walking.rb +55 -0
- data/lib/kimera/report/actions.rb +38 -0
- data/lib/kimera/report/coloring.rb +25 -0
- data/lib/kimera/report/formats.rb +78 -0
- data/lib/kimera/report/live.rb +56 -0
- data/lib/kimera/report/log.rb +46 -0
- data/lib/kimera/report/progress.rb +55 -0
- data/lib/kimera/report/screen.rb +44 -0
- data/lib/kimera/report/sections.rb +50 -0
- data/lib/kimera/report/tally.rb +84 -0
- data/lib/kimera/report/text.rb +104 -0
- data/lib/kimera/results/result.rb +72 -0
- data/lib/kimera/results/run_report.rb +114 -0
- data/lib/kimera/rewrite/ast_walk.rb +20 -0
- data/lib/kimera/rewrite/directive.rb +61 -0
- data/lib/kimera/rewrite/directive_extra_handlers.rb +52 -0
- data/lib/kimera/rewrite/directive_handlers.rb +138 -0
- data/lib/kimera/rewrite/directive_kwarg_handlers.rb +44 -0
- data/lib/kimera/rewrite/numbered_params.rb +50 -0
- data/lib/kimera/runtime.rb +51 -0
- data/lib/kimera/scope/config.rb +90 -0
- data/lib/kimera/scope/file_set.rb +51 -0
- data/lib/kimera/scope/ignore_list.rb +55 -0
- data/lib/kimera/self_protection.rb +56 -0
- data/lib/kimera/support/duration.rb +13 -0
- data/lib/kimera/support/errors.rb +8 -0
- data/lib/kimera/support/operator_protocol.rb +10 -0
- data/lib/kimera/support/syntax.rb +68 -0
- data/lib/kimera/support/syntax_types.rb +20 -0
- data/lib/kimera/support/unparse.rb +15 -0
- data/lib/kimera/support/version.rb +5 -0
- data/lib/kimera/support/warnings.rb +15 -0
- data/lib/kimera/synthesis/file_weave.rb +78 -0
- data/lib/kimera/synthesis/guard_weaver.rb +77 -0
- data/lib/kimera/synthesis/guardrail_dispatch.rb +68 -0
- data/lib/kimera/synthesis/guardrail_interpolation.rb +59 -0
- data/lib/kimera/synthesis/guardrail_value_objects.rb +46 -0
- data/lib/kimera/synthesis/overlay.rb +52 -0
- data/lib/kimera/synthesis/overlay_splice.rb +101 -0
- data/lib/kimera/synthesis/project.rb +58 -0
- data/lib/kimera/synthesis/source_map.rb +42 -0
- data/lib/kimera/version.rb +3 -0
- data/lib/kimera.rb +13 -0
- data/skills/kimera/SKILL.md +118 -0
- 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
|