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,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "isolation"
|
|
4
|
+
|
|
5
|
+
class Kimera::Execution::CompositeIsolation < Kimera::Execution::Isolation
|
|
6
|
+
def initialize(strategies)
|
|
7
|
+
super()
|
|
8
|
+
@given = strategies
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def around(&block)
|
|
12
|
+
strategies.reverse.reduce(block) do |inner, strategy|
|
|
13
|
+
-> { strategy.around(&inner) }
|
|
14
|
+
end.call
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reset!
|
|
18
|
+
strategies.each(&:reset!)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def strategies = @_strategies ||= Array(@given)
|
|
24
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "english"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../error"
|
|
6
|
+
require_relative "../results/result"
|
|
7
|
+
require_relative "../results/run_report"
|
|
8
|
+
require_relative "../runtime"
|
|
9
|
+
require_relative "../self_protection"
|
|
10
|
+
require_relative "baseline_pass"
|
|
11
|
+
require_relative "boot"
|
|
12
|
+
require_relative "child_process"
|
|
13
|
+
require_relative "isolation"
|
|
14
|
+
require_relative "null_progress"
|
|
15
|
+
require_relative "parallel_test_databases"
|
|
16
|
+
require_relative "pool_driver"
|
|
17
|
+
require_relative "reload"
|
|
18
|
+
require_relative "rig"
|
|
19
|
+
require_relative "schedule"
|
|
20
|
+
require_relative "schemata"
|
|
21
|
+
require_relative "verdicts"
|
|
22
|
+
require_relative "shift"
|
|
23
|
+
require_relative "worker_pool"
|
|
24
|
+
|
|
25
|
+
class Kimera::Execution::Harness
|
|
26
|
+
include Kimera::Execution::ChildProcess
|
|
27
|
+
|
|
28
|
+
Context = Data.define(:registry, :adapter, :options)
|
|
29
|
+
State = Struct.new(:skipped, :irrelevant, :isolation)
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def new(registry:, adapter:, **options)
|
|
33
|
+
super(Context.new(registry, adapter, options), State.new({}, [], nil))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(context, state)
|
|
38
|
+
@context = context
|
|
39
|
+
@state = state
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def warm!(test_files)
|
|
43
|
+
overlay!(test_files).tap do
|
|
44
|
+
measure!
|
|
45
|
+
notice
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def without_coverage!(test_files)
|
|
50
|
+
overlay!(test_files).tap do
|
|
51
|
+
verify!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def overlay!(test_files)
|
|
56
|
+
boot.suite(test_files)
|
|
57
|
+
loader = Kimera::Execution::Schemata.new(registry, root: root, errors: errors)
|
|
58
|
+
loader.overlay!.tap { complete(loader) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def load! = boot.load!
|
|
62
|
+
|
|
63
|
+
def isolate! = (state.isolation = boot.isolate(isolation))
|
|
64
|
+
|
|
65
|
+
def run(ids: mutants, label: "mutants")
|
|
66
|
+
schedule.run(ids, coverage: coverage, label: label)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def coverage = options[:coverage]
|
|
70
|
+
|
|
71
|
+
def skipped = state.skipped
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :context, :state
|
|
76
|
+
|
|
77
|
+
def registry = context.registry
|
|
78
|
+
def adapter = context.adapter
|
|
79
|
+
def options = context.options
|
|
80
|
+
|
|
81
|
+
def root = options.fetch(:source_root, ".")
|
|
82
|
+
|
|
83
|
+
def errors = options.fetch(:errio, $stderr)
|
|
84
|
+
|
|
85
|
+
def isolation = state.isolation ||= options.fetch(:isolation) { Kimera::Execution::Isolation.new }
|
|
86
|
+
|
|
87
|
+
def soft = options.fetch(:soft_timeout, 5.0)
|
|
88
|
+
|
|
89
|
+
def hard = @_hard ||= options.fetch(:hard_timeout) { soft ? (soft * 3) + 1 : 30.0 }
|
|
90
|
+
|
|
91
|
+
def jobs = @_jobs ||= [Integer(options.fetch(:jobs, 1)), 1].max
|
|
92
|
+
|
|
93
|
+
def progress = options.fetch(:progress, Kimera::Execution::NullProgress)
|
|
94
|
+
|
|
95
|
+
def driver = rig.driver
|
|
96
|
+
|
|
97
|
+
def spawner = rig.spawner
|
|
98
|
+
|
|
99
|
+
def schedule = rig.schedule
|
|
100
|
+
|
|
101
|
+
def rig
|
|
102
|
+
@_rig ||= Kimera::Execution::Rig.new(
|
|
103
|
+
registry: registry, adapter: adapter, isolation: isolation, root: root,
|
|
104
|
+
soft: soft, hard: hard, leak: options.fetch(:leak_every, 10), jobs: jobs,
|
|
105
|
+
coverage: coverage, spawner: options[:spawner], progress: progress
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def boot
|
|
110
|
+
@_boot ||= Kimera::Execution::Boot.new(adapter: adapter, isolate: options[:isolate_db], errors: errors)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def complete(loader)
|
|
114
|
+
state.skipped = loader.skipped
|
|
115
|
+
isolate!
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mutants = registry.each.map { |mutant, _path| mutant.id }
|
|
119
|
+
|
|
120
|
+
def measure!
|
|
121
|
+
pass = baseline
|
|
122
|
+
measured = jobs > 1 ? parallel(pass) : pass.measure!
|
|
123
|
+
options[:coverage] = measured.coverage
|
|
124
|
+
driver.coverage = coverage
|
|
125
|
+
state.irrelevant = measured.irrelevant
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parallel(pass)
|
|
129
|
+
pass.parallel! do |resolve:, lost:|
|
|
130
|
+
driver.drive(adapter.test_ids, driver.method(:channel), resolve: resolve, lost: lost)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def verify! = baseline.check!
|
|
135
|
+
|
|
136
|
+
def baseline
|
|
137
|
+
Kimera::Execution::BaselinePass.new(adapter: adapter, registry: registry, progress: progress)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def notice
|
|
141
|
+
count = state.irrelevant.size
|
|
142
|
+
return if count.zero?
|
|
143
|
+
errors.puts(message(count))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def message(count)
|
|
147
|
+
"kimera: #{count} failing test(s) cover no in-scope mutant and were " \
|
|
148
|
+
"excluded from the baseline (they gate nothing this run)"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require_relative "../results/result"
|
|
6
|
+
require_relative "../results/run_report"
|
|
7
|
+
require_relative "../synthesis/overlay"
|
|
8
|
+
require_relative "isolated_plan"
|
|
9
|
+
require_relative "isolated_scheduling"
|
|
10
|
+
require_relative "isolated_verdict"
|
|
11
|
+
require_relative "isolated_watchdog"
|
|
12
|
+
require_relative "null_progress"
|
|
13
|
+
|
|
14
|
+
module Kimera
|
|
15
|
+
module Execution
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Kimera::Execution::IsolatedExecution
|
|
20
|
+
include Kimera::Execution::IsolatedExecutionScheduling
|
|
21
|
+
include Kimera::Execution::IsolatedExecutionVerdict
|
|
22
|
+
include Kimera::Execution::IsolatedExecutionWatchdog
|
|
23
|
+
|
|
24
|
+
DEFAULT_HARD_TIMEOUT = 300.0
|
|
25
|
+
|
|
26
|
+
def initialize(registry:, root:, tests:, **options)
|
|
27
|
+
@registry = registry
|
|
28
|
+
@root = root
|
|
29
|
+
@tests = tests
|
|
30
|
+
@options = options
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run(ids: self.ids, label: "mutants")
|
|
34
|
+
progress.start(ids.size, label)
|
|
35
|
+
report(ids)
|
|
36
|
+
ensure
|
|
37
|
+
progress.finish
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def progress = @options.fetch(:progress, Kimera::Execution::NullProgress)
|
|
43
|
+
|
|
44
|
+
def limit = @options.fetch(:hard_timeout, DEFAULT_HARD_TIMEOUT)
|
|
45
|
+
|
|
46
|
+
def jobs = @_jobs ||= [Integer(@options.fetch(:jobs, 1)), 1].max
|
|
47
|
+
|
|
48
|
+
def errors = @options.fetch(:errio, $stderr)
|
|
49
|
+
|
|
50
|
+
def plan
|
|
51
|
+
@_plan ||= Kimera::Execution::IsolatedPlan.new(
|
|
52
|
+
registry: @registry, root: @root, tests: @tests,
|
|
53
|
+
coverage: @options.fetch(:coverage, {}), framework: @options.fetch(:framework, "rspec"),
|
|
54
|
+
test_files: @options.fetch(:test_files, [])
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report(ids)
|
|
59
|
+
Kimera::RunReport.new(
|
|
60
|
+
results: (jobs > 1 ? concurrently(ids) : serially(ids)),
|
|
61
|
+
leaks: [], registry: plan.registry
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
RSpec::Core::ConfigurationOptions.new([]).configure(RSpec.configuration)
|
|
8
|
+
rescue StandardError, ScriptError
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
RSpec.configuration.output_stream = StringIO.new
|
|
12
|
+
RSpec.configuration.error_stream = StringIO.new
|
|
13
|
+
RSpec.configuration.deprecation_stream = StringIO.new
|
|
14
|
+
|
|
15
|
+
exit RSpec::Core::Runner.run(ARGV)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::IsolatedPlan; end
|
|
9
|
+
|
|
10
|
+
class Kimera::Execution::IsolatedPlan::ChildCommand
|
|
11
|
+
CHILD = File.expand_path("isolated_child.rb", __dir__)
|
|
12
|
+
MINITEST_CHILD = File.expand_path("isolated_child_minitest.rb", __dir__)
|
|
13
|
+
|
|
14
|
+
def initialize(framework:, test_files:)
|
|
15
|
+
@framework = framework
|
|
16
|
+
@test_files = test_files
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def command(mirror, locations, paths:)
|
|
20
|
+
bundled(mirror, {}, ["ruby", *paths.flat_map { |path| ["-I", path] }, "-I", helpers, *argv(locations)])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def argv(locations)
|
|
24
|
+
return [CHILD, *locations] unless minitest?
|
|
25
|
+
[MINITEST_CHILD, *@test_files, "--", *locations]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def helpers
|
|
29
|
+
minitest? ? "test" : "spec"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def bundled(mirror, env, argv)
|
|
35
|
+
gemfile = File.join(mirror, "Gemfile")
|
|
36
|
+
return [env, argv] unless File.file?(gemfile)
|
|
37
|
+
env["BUNDLE_GEMFILE"] = gemfile
|
|
38
|
+
[env, ["bundle", "exec", *argv]]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def minitest?
|
|
42
|
+
@framework.to_s == "minitest"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "minitest"
|
|
4
|
+
|
|
5
|
+
Minitest.seed ||= 1
|
|
6
|
+
begin
|
|
7
|
+
Minitest.class_variable_set(:@@installed_at_exit, :installed)
|
|
8
|
+
rescue NameError, ArgumentError
|
|
9
|
+
class << Minitest
|
|
10
|
+
def run(*) = true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
split = ARGV.index("--") or abort("usage: <test files...> -- <ids...>")
|
|
15
|
+
files = ARGV[0...split]
|
|
16
|
+
ids = ARGV[(split + 1)..]
|
|
17
|
+
|
|
18
|
+
files.each { |f| load File.expand_path(f) }
|
|
19
|
+
|
|
20
|
+
methods = {}
|
|
21
|
+
Minitest::Runnable.runnables.each do |runnable|
|
|
22
|
+
next unless runnable.respond_to?(:runnable_methods)
|
|
23
|
+
runnable.runnable_methods.each { |name| methods["#{runnable}##{name}"] = [runnable, name] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
killed =
|
|
27
|
+
ids.any? do |id|
|
|
28
|
+
klass, name = methods[id]
|
|
29
|
+
next false unless klass
|
|
30
|
+
result = klass.new(name).run
|
|
31
|
+
!result.passed? && !result.skipped?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
exit(killed ? 1 : 0)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../self_protection"
|
|
4
|
+
require_relative "isolated_child_command"
|
|
5
|
+
require_relative "isolated_test_selection"
|
|
6
|
+
|
|
7
|
+
module Kimera
|
|
8
|
+
module Execution
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Kimera::Execution::IsolatedPlan
|
|
13
|
+
MIRROR_SKIP = %w[.git tmp vendor node_modules coverage log .bundle].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :registry
|
|
16
|
+
|
|
17
|
+
def initialize(registry:, root:, tests:, **options)
|
|
18
|
+
@registry = registry
|
|
19
|
+
@given = root
|
|
20
|
+
@tests = tests
|
|
21
|
+
@options = options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def root = @_root ||= File.expand_path(@given)
|
|
25
|
+
|
|
26
|
+
def tests(id, point)
|
|
27
|
+
all = selection.all
|
|
28
|
+
return all if Kimera::SelfProtection.protected?(File.join(root, point.file))
|
|
29
|
+
return all unless point.safe?
|
|
30
|
+
selection.recorded(id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def command(mirror, locations)
|
|
34
|
+
child.command(mirror, locations, paths: mutables)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mutables
|
|
38
|
+
@registry.files.map { |f| f.split(File::SEPARATOR).first }.uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def mirrors(entries)
|
|
43
|
+
entries.reject { |entry| MIRROR_SKIP.include?(entry) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def selection = @_selection ||= TestSelection.new(all_tests: @tests, coverage: @options.fetch(:coverage, {}))
|
|
50
|
+
|
|
51
|
+
def child
|
|
52
|
+
@_child ||= ChildCommand.new(framework: @options.fetch(:framework), test_files: @options.fetch(:test_files))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "sweep"
|
|
4
|
+
require_relative "trial"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Execution
|
|
8
|
+
module IsolatedExecutionScheduling
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def ids
|
|
12
|
+
plan.registry.each.map { |m, _p| m.id }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def serially(ids)
|
|
16
|
+
sweep = Kimera::Execution::Sweep.new(ids, Mutex.new)
|
|
17
|
+
with_mirror { |mirror| scan(sweep, mirror) }
|
|
18
|
+
sweep.results
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def scan(sweep, mirror)
|
|
22
|
+
synth = Overlay.new(plan.registry)
|
|
23
|
+
sweep.each { |id| charge(sweep, evaluate(Kimera::Execution::Trial.new(id, mirror, synth))) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def concurrently(ids)
|
|
27
|
+
sweep = Kimera::Execution::Sweep.new(ids, Mutex.new)
|
|
28
|
+
workers(sweep).each(&:join)
|
|
29
|
+
sweep.results
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def workers(sweep)
|
|
33
|
+
Array.new(jobs) { Thread.new { work(sweep) } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def work(sweep)
|
|
37
|
+
synth = Overlay.new(plan.registry)
|
|
38
|
+
with_mirror { |mirror| drain(sweep, mirror, synth) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def drain(sweep, mirror, synth)
|
|
42
|
+
while (id = sweep.pop)
|
|
43
|
+
charge(sweep, evaluate(Kimera::Execution::Trial.new(id, mirror, synth)))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def charge(sweep, result)
|
|
48
|
+
done = sweep.record(result)
|
|
49
|
+
progress.tick(result.status)
|
|
50
|
+
trace(result, done, sweep.total)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::IsolatedPlan; end
|
|
9
|
+
|
|
10
|
+
class Kimera::Execution::IsolatedPlan::TestSelection
|
|
11
|
+
attr_reader :all
|
|
12
|
+
|
|
13
|
+
def initialize(all_tests:, coverage:)
|
|
14
|
+
@all = all_tests
|
|
15
|
+
@coverage = coverage
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def recorded(id)
|
|
19
|
+
tests = @coverage[id] || @coverage[id.to_s]
|
|
20
|
+
tests unless Array(tests).empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
module IsolatedExecutionVerdict
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def evaluate(trial)
|
|
9
|
+
point = plan.registry.index[trial.id]
|
|
10
|
+
return error(trial, "unknown mutant") unless point
|
|
11
|
+
bake(trial, point)
|
|
12
|
+
rescue StandardError => error
|
|
13
|
+
self.error(trial, "#{error.class}: #{error.message}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def bake(trial, point)
|
|
17
|
+
trial.file = point.file
|
|
18
|
+
tests = plan.tests(trial.id, point)
|
|
19
|
+
return result(trial, :no_coverage, nil) unless tests
|
|
20
|
+
status, duration = measure(trial, tests)
|
|
21
|
+
result(trial, status, duration, tests)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def measure(trial, tests)
|
|
25
|
+
started = now
|
|
26
|
+
[with_mutant(trial) { verdict(trial.mirror, tests) }, now - started]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_mutant(trial)
|
|
30
|
+
original, target = trial.stage(plan.root)
|
|
31
|
+
yield
|
|
32
|
+
ensure
|
|
33
|
+
File.write(target, original) if original
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verdict(mirror, locations)
|
|
37
|
+
env, cmd = plan.command(mirror, locations)
|
|
38
|
+
status = waitfor(Process.spawn(env, *cmd, chdir: mirror, pgroup: true, out: File::NULL, err: File::NULL))
|
|
39
|
+
return :timeout if status == :timeout
|
|
40
|
+
status.success? ? :survived : :killed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def with_mirror
|
|
44
|
+
Dir.mktmpdir("kimera-isolated") do |mirror|
|
|
45
|
+
populate(mirror)
|
|
46
|
+
yield(mirror)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def populate(mirror)
|
|
51
|
+
root = plan.root
|
|
52
|
+
IsolatedPlan.mirrors(Dir.children(root)).each do |entry|
|
|
53
|
+
FileUtils.cp_r(File.join(root, entry), File.join(mirror, entry))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def result(trial, status, duration, cover = nil)
|
|
58
|
+
MutantResult.new(
|
|
59
|
+
mutant_id: trial.id, status: status, file: trial.file, duration: duration, covering_tests: cover
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def error(trial, detail)
|
|
64
|
+
MutantResult.new(mutant_id: trial.id, status: :error, file: trial.file, detail: detail)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def now
|
|
68
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
module IsolatedExecutionWatchdog
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def waitfor(pid)
|
|
9
|
+
deadline = now + limit
|
|
10
|
+
loop do
|
|
11
|
+
step = attempt(pid, deadline)
|
|
12
|
+
return step unless step == :continue
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def attempt(pid, deadline)
|
|
17
|
+
done, status = Process.waitpid2(pid, Process::WNOHANG)
|
|
18
|
+
return status if done
|
|
19
|
+
return fail!(pid) unless (deadline - now).positive?
|
|
20
|
+
pause
|
|
21
|
+
:continue
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pause
|
|
25
|
+
sleep(@options.fetch(:poll_interval, 0.05))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fail!(pid)
|
|
29
|
+
kill(pid)
|
|
30
|
+
reap(pid)
|
|
31
|
+
:timeout
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def kill(pid)
|
|
35
|
+
Process.kill("KILL", -pid)
|
|
36
|
+
rescue Errno::ESRCH
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reap(pid)
|
|
41
|
+
Process.waitpid(pid)
|
|
42
|
+
rescue Errno::ECHILD
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def trace(result, done, total)
|
|
47
|
+
return if progress.enabled?
|
|
48
|
+
errors.puts(line(result, done, total))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def line(result, done, total)
|
|
52
|
+
format(
|
|
53
|
+
"kimera: isolated %d/%d #%s %s %s (%.1fs)", done, total,
|
|
54
|
+
*result.to_h.values_at("mutant_id", "status", "file", "duration")
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::Isolation
|
|
9
|
+
def around
|
|
10
|
+
yield
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reset!; end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require_relative "composite_isolation"
|
|
17
|
+
require_relative "transaction_isolation"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
module NullProgress
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def start(_total, _label = nil); end
|
|
9
|
+
|
|
10
|
+
def tick(_status = nil); end
|
|
11
|
+
|
|
12
|
+
def finish; end
|
|
13
|
+
|
|
14
|
+
def enabled? = false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|