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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kimera::Execution::Shift::CoverageChannel
|
|
4
|
+
def initialize(adapter)
|
|
5
|
+
@adapter = adapter
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def serve(request, response)
|
|
9
|
+
ledger = Kimera::Runtime.start!
|
|
10
|
+
drain(request, response, ledger)
|
|
11
|
+
emit(response, t: "done")
|
|
12
|
+
ensure
|
|
13
|
+
stop(ledger)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def drain(request, response, ledger)
|
|
19
|
+
while (line = request.gets)
|
|
20
|
+
step(response, JSON.parse(line)["id"], ledger)
|
|
21
|
+
emit(response, t: "ready")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def step(response, testid, ledger)
|
|
26
|
+
Kimera::Runtime.active = nil
|
|
27
|
+
emit(response, **message(testid, ledger, @adapter.run([testid])))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def message(testid, ledger, outcome)
|
|
31
|
+
{ t: "result", id: testid, passed: outcome.passed? }
|
|
32
|
+
.merge(touched: Kimera::Runtime.drain!(ledger), failure: outcome.failures[testid])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stop(ledger)
|
|
36
|
+
Kimera::Runtime.stop!(ledger)
|
|
37
|
+
Kimera::Runtime.active = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def emit(io, **msg)
|
|
41
|
+
io.puts(JSON.generate(msg))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kimera::Execution::Shift::KillerMemory
|
|
4
|
+
LIMIT = 16
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@recent = []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def order(tests)
|
|
11
|
+
likely = @recent & tests
|
|
12
|
+
likely + (tests - likely)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def remember(testid)
|
|
16
|
+
@recent.delete(testid)
|
|
17
|
+
@recent.unshift(testid)
|
|
18
|
+
@recent.pop while @recent.size > LIMIT
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Kimera::Execution::Shift::LeakGuard
|
|
4
|
+
def initialize(every, &evaluator)
|
|
5
|
+
@every = every
|
|
6
|
+
@evaluator = evaluator
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def check(io, history, index)
|
|
10
|
+
return unless due?(index)
|
|
11
|
+
return if history.empty?
|
|
12
|
+
recheck(io, history.last)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def due?(index)
|
|
18
|
+
return false unless @every&.positive?
|
|
19
|
+
((index + 1) % @every).zero?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def recheck(io, id)
|
|
23
|
+
return if @evaluator.call(id).killed?
|
|
24
|
+
emit(io, t: "leak", id: id, detail: detail(id))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def detail(id)
|
|
28
|
+
"mutant #{id} killed earlier but survived re-run (state leakage suspected)"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def emit(io, **msg)
|
|
32
|
+
io.puts(JSON.generate(msg))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "../results/result"
|
|
6
|
+
require_relative "../runtime"
|
|
7
|
+
require_relative "isolation"
|
|
8
|
+
|
|
9
|
+
class Kimera::Execution::Shift
|
|
10
|
+
Subject = Struct.new(:id, :file)
|
|
11
|
+
Channel =
|
|
12
|
+
Data.define(:requests, :responses) do
|
|
13
|
+
def each_request
|
|
14
|
+
while (line = requests.gets)
|
|
15
|
+
yield(line, responses)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(adapter:, registry:, coverage: nil, **options)
|
|
21
|
+
@adapter = adapter
|
|
22
|
+
@registry = registry
|
|
23
|
+
@coverage = coverage
|
|
24
|
+
@options = options
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def serve(requests, responses)
|
|
28
|
+
listen(Channel.new(requests, responses))
|
|
29
|
+
emit(responses, t: "done")
|
|
30
|
+
ensure
|
|
31
|
+
Kimera::Runtime.active = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def coverage(requests, responses) = channel.serve(requests, responses)
|
|
35
|
+
|
|
36
|
+
def run(ids, io)
|
|
37
|
+
history = []
|
|
38
|
+
ids.each_with_index { |id, index| process(id, io, history, index) }
|
|
39
|
+
emit(io, t: "done")
|
|
40
|
+
ensure
|
|
41
|
+
Kimera::Runtime.active = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def evaluate(id)
|
|
45
|
+
safely(Subject.new(id, @registry.index[id]&.file))
|
|
46
|
+
ensure
|
|
47
|
+
Kimera::Runtime.active = nil
|
|
48
|
+
isolation.reset!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def isolation = @_isolation ||= @options.fetch(:isolation) { Kimera::Execution::Isolation.new }
|
|
54
|
+
|
|
55
|
+
def timeout = @options.fetch(:soft_timeout, 5.0)
|
|
56
|
+
|
|
57
|
+
def attempt
|
|
58
|
+
kind = self.class
|
|
59
|
+
@_attempt ||= kind::Attempt.new(
|
|
60
|
+
adapter: @adapter, isolation: isolation, killers: kind::KillerMemory.new, timeout: timeout
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def leaks = @_leaks ||= self.class::LeakGuard.new(@options.fetch(:leak_every, 10)) { |id| evaluate(id) }
|
|
65
|
+
|
|
66
|
+
def channel = @_channel ||= self.class::CoverageChannel.new(@adapter)
|
|
67
|
+
|
|
68
|
+
def listen(connection)
|
|
69
|
+
history = []
|
|
70
|
+
index = 0
|
|
71
|
+
connection.each_request { |line, responses| index = step(line, responses, history, index) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def step(line, responses, history, index)
|
|
75
|
+
process(JSON.parse(line)["id"], responses, history, index)
|
|
76
|
+
emit(responses, t: "ready")
|
|
77
|
+
index + 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def safely(subject)
|
|
81
|
+
tests = available(subject.id)
|
|
82
|
+
return result(subject, :no_coverage) if tests.empty?
|
|
83
|
+
outcome(subject, tests)
|
|
84
|
+
rescue StandardError, ScriptError => error
|
|
85
|
+
error(subject, error)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def error(subject, error)
|
|
89
|
+
return result(subject, :timeout, timeout) if error.is_a?(Timeout::Error)
|
|
90
|
+
result(subject, :error, detail: "#{error.class}: #{error.message}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def outcome(subject, tests)
|
|
94
|
+
started = monotonic
|
|
95
|
+
outcome = attempt.run(subject.id, tests)
|
|
96
|
+
result(subject, classify(outcome), monotonic - started, failing(outcome), tests)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def process(id, io, history, index)
|
|
100
|
+
result = evaluate(id)
|
|
101
|
+
emit(io, **result.message)
|
|
102
|
+
history << id if result.killed?
|
|
103
|
+
leaks.check(io, history, index)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def classify(outcome)
|
|
107
|
+
!outcome || outcome.passed? ? :survived : :killed
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def failing(outcome) = outcome&.failed_ids
|
|
111
|
+
|
|
112
|
+
def available(id)
|
|
113
|
+
return @adapter.test_ids unless @coverage
|
|
114
|
+
@coverage.fetch(id) { @coverage.fetch(id.to_s, []) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def result(subject, status, duration = nil, fails = nil, cover = nil, detail: nil)
|
|
118
|
+
Kimera::MutantResult.new(
|
|
119
|
+
mutant_id: subject.id, status: status, file: subject.file,
|
|
120
|
+
duration: duration, failing_tests: fails,
|
|
121
|
+
covering_tests: cover, detail: detail
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def emit(io, **message)
|
|
126
|
+
io.puts(JSON.generate(message))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def monotonic
|
|
130
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
require_relative "shift/attempt"
|
|
135
|
+
require_relative "shift/coverage_channel"
|
|
136
|
+
require_relative "shift/killer_memory"
|
|
137
|
+
require_relative "shift/leak_guard"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::Sweep
|
|
9
|
+
def initialize(ids, lock)
|
|
10
|
+
@ids = ids
|
|
11
|
+
@lock = lock
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def total = @ids.size
|
|
15
|
+
|
|
16
|
+
def results = @_results ||= []
|
|
17
|
+
|
|
18
|
+
def pop = queue.pop(timeout: 0)
|
|
19
|
+
|
|
20
|
+
def each(&) = @ids.each(&)
|
|
21
|
+
|
|
22
|
+
def record(result)
|
|
23
|
+
@lock.synchronize { results << result }
|
|
24
|
+
results.size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def queue = @_queue ||= Queue.new.tap { |pending| @ids.each { |id| pending << id } }
|
|
30
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "isolation"
|
|
4
|
+
|
|
5
|
+
class Kimera::Execution::TransactionIsolation < Kimera::Execution::Isolation; end
|
|
6
|
+
class Kimera::Execution::TransactionIsolation::Rollback < StandardError; end
|
|
7
|
+
|
|
8
|
+
class Kimera::Execution::TransactionIsolation
|
|
9
|
+
def initialize(owner)
|
|
10
|
+
super()
|
|
11
|
+
@owner = owner
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def around
|
|
15
|
+
result = nil
|
|
16
|
+
rollback { result = yield }
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def rollback(&)
|
|
23
|
+
@owner.transaction(requires_new: true) { unwind(&) }
|
|
24
|
+
rescue Rollback
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def unwind
|
|
29
|
+
yield
|
|
30
|
+
raise(Rollback)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Execution
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
Kimera::Execution::Trial =
|
|
9
|
+
Struct.new(:id, :mirror, :synth, :file) do
|
|
10
|
+
def stage(root)
|
|
11
|
+
source = File.read(File.join(root, file), encoding: Encoding::UTF_8)
|
|
12
|
+
target = File.join(mirror, file)
|
|
13
|
+
File.write(target, synth.bake(file, source, id))
|
|
14
|
+
[source, target]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../results/result"
|
|
4
|
+
require_relative "../self_protection"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Execution
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Kimera::Execution::Verdicts
|
|
12
|
+
def initialize(registry)
|
|
13
|
+
@registry = registry
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def safe?(id)
|
|
17
|
+
point(id).safe?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reloadable?(id)
|
|
21
|
+
!point(id).body?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reclassify!(results)
|
|
25
|
+
results.each { |id, result| reclassify(results, id, result) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse(message)
|
|
29
|
+
id = message["id"]
|
|
30
|
+
Kimera::MutantResult.new(
|
|
31
|
+
mutant_id: id, status: message["status"].to_sym, file: path(id),
|
|
32
|
+
duration: message["ms"], failing_tests: message["fails"], covering_tests: message["cover"]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def timeout(id, duration)
|
|
37
|
+
Kimera::MutantResult.new(
|
|
38
|
+
mutant_id: id, status: :timeout, file: path(id),
|
|
39
|
+
duration: duration, detail: "hard watchdog timeout"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unjudged(id, detail)
|
|
44
|
+
Kimera::MutantResult.new(mutant_id: id, status: :harness_error, file: path(id), detail: detail)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def quarantine(id)
|
|
48
|
+
Kimera::MutantResult.new(mutant_id: id, status: :isolated_only, file: path(id), detail: point(id).unsafe_reason)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def point(id)
|
|
52
|
+
@registry.index[id] || Kimera::MutationPoint::NONE
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def path(id)
|
|
56
|
+
point(id).file
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def reclassify(results, id, result)
|
|
62
|
+
return unless suspect?(result)
|
|
63
|
+
results[id] = result.isolated(detail(result.status))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def suspect?(result)
|
|
67
|
+
Kimera::Status::SELF_SUSPECT.include?(result.status) && harness?(result)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def harness?(result)
|
|
71
|
+
Kimera::SelfProtection.critical?(result.file)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def detail(status)
|
|
75
|
+
"#{status == :survived ? "unfalsifiable warm survivor" : "self-crash"} in the in-process runner " \
|
|
76
|
+
"(warm #{status}); evaluate with --isolated"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "stillborn_guard"
|
|
4
|
+
|
|
5
|
+
class Kimera::Execution::WorkerPool::Fleet
|
|
6
|
+
include Kimera::Execution::ChildProcess
|
|
7
|
+
|
|
8
|
+
def initialize(queue:, spawner:, jobs:, pool:)
|
|
9
|
+
@source = queue
|
|
10
|
+
@spawner = spawner
|
|
11
|
+
@jobs = jobs
|
|
12
|
+
@pool = pool
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def bootstrap
|
|
16
|
+
[@jobs, queue.size].min.times { spawn }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&)
|
|
20
|
+
workers.each_value(&)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def pairs(&)
|
|
24
|
+
workers.to_a.each(&)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def fetch(pipe)
|
|
28
|
+
workers.fetch(pipe)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def keys
|
|
32
|
+
workers.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def any?
|
|
36
|
+
workers.any?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def take
|
|
40
|
+
until queue.empty?
|
|
41
|
+
id = queue.shift
|
|
42
|
+
return id unless done[id]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def done!(id)
|
|
47
|
+
done[id] = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def progress!
|
|
51
|
+
guard.progress!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def remove(pipe, reason: :crash)
|
|
55
|
+
worker = workers.delete(pipe)
|
|
56
|
+
slots.push(worker.slot)
|
|
57
|
+
close(worker)
|
|
58
|
+
charge(worker.inflight, reason, reap(worker.pid))
|
|
59
|
+
refill
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def queue = @_queue ||= @source.dup
|
|
65
|
+
|
|
66
|
+
def done = @_done ||= {}
|
|
67
|
+
|
|
68
|
+
def workers = @_workers ||= {}
|
|
69
|
+
|
|
70
|
+
def slots = @_slots ||= (0...@jobs).to_a
|
|
71
|
+
|
|
72
|
+
def guard = @_guard ||= Kimera::Execution::WorkerPool::StillbornGuard.new(@jobs)
|
|
73
|
+
|
|
74
|
+
def spawn
|
|
75
|
+
slot = slots.shift
|
|
76
|
+
pid, request, pipe = @spawner.call(slot)
|
|
77
|
+
workers[pipe] = Kimera::Execution::WorkerPool::Worker.new(pid, request, pipe, nil, nil, slot)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def close(worker)
|
|
81
|
+
shut(worker.response)
|
|
82
|
+
shut(worker.request)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def charge(id, reason, status)
|
|
86
|
+
return unless id
|
|
87
|
+
guard.track(status) if reason == :crash
|
|
88
|
+
done!(id)
|
|
89
|
+
@pool.lost(id, reason)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def refill
|
|
93
|
+
missing = @jobs - workers.size
|
|
94
|
+
missing.times do
|
|
95
|
+
break unless queue.any? { |id| !done[id] }
|
|
96
|
+
replace
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def replace
|
|
101
|
+
spawn
|
|
102
|
+
@pool.assign(workers.values.last)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../error"
|
|
4
|
+
|
|
5
|
+
class Kimera::Execution::WorkerPool::StillbornGuard
|
|
6
|
+
def initialize(jobs)
|
|
7
|
+
@jobs = jobs
|
|
8
|
+
@stillborn = 0
|
|
9
|
+
@progressed = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def progress!
|
|
13
|
+
@progressed = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def track(status)
|
|
17
|
+
return if @progressed
|
|
18
|
+
@stillborn += 1
|
|
19
|
+
abort!(status) if @stillborn > @jobs
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def abort!(status)
|
|
25
|
+
raise(Kimera::Error, message(status))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def message(status)
|
|
29
|
+
"#{@stillborn} warm workers died before reporting a result " \
|
|
30
|
+
"(last exit: #{describe(status)}). The app raises during " \
|
|
31
|
+
"fork or the suite aborts on load; re-run with --jobs 1 to see " \
|
|
32
|
+
"the error."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def describe(status)
|
|
36
|
+
[status].compact.map do |current|
|
|
37
|
+
current.signaled? ? "signal #{current.termsig}" : "status #{current.exitstatus}"
|
|
38
|
+
end.fetch(0, "unknown")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Kimera::Execution::WorkerPool::Worker =
|
|
4
|
+
Struct.new(:pid, :request, :response, :inflight, :deadline, :slot) do
|
|
5
|
+
def idle!
|
|
6
|
+
self.inflight = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def busy!(id)
|
|
10
|
+
self.inflight = id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def renew(limit)
|
|
14
|
+
self.deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + limit
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def expired?(now)
|
|
18
|
+
deadline && !(deadline - now).positive?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def retire
|
|
22
|
+
self.inflight = nil
|
|
23
|
+
self.deadline = nil
|
|
24
|
+
request
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def offer(id)
|
|
28
|
+
request.puts(JSON.generate(id: id))
|
|
29
|
+
rescue Errno::EPIPE, IOError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
require_relative "child_process"
|
|
6
|
+
|
|
7
|
+
class Kimera::Execution::WorkerPool
|
|
8
|
+
include Kimera::Execution::ChildProcess
|
|
9
|
+
|
|
10
|
+
Context = Data.define(:queue, :spawner, :resolve, :lost, :options)
|
|
11
|
+
|
|
12
|
+
POLL_INTERVAL = 0.2
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def new(queue:, spawner:, resolve:, lost:, **options)
|
|
16
|
+
super(Context.new(queue, spawner, resolve, lost, options))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(context)
|
|
21
|
+
@context = context
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run
|
|
25
|
+
bootstrap
|
|
26
|
+
poll while fleet.any?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def assign(worker)
|
|
30
|
+
id = fleet.take
|
|
31
|
+
return close(worker) unless id
|
|
32
|
+
worker.busy!(id)
|
|
33
|
+
worker.renew(limit)
|
|
34
|
+
worker.offer(id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lost(id, reason) = context.lost.call(id, reason)
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :context
|
|
42
|
+
|
|
43
|
+
def limit = context.options.fetch(:hard_timeout)
|
|
44
|
+
|
|
45
|
+
def fleet
|
|
46
|
+
@_fleet ||= Kimera::Execution::WorkerPool::Fleet.new(
|
|
47
|
+
queue: context.queue, spawner: context.spawner, jobs: context.options.fetch(:jobs), pool: self
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bootstrap
|
|
52
|
+
fleet.bootstrap
|
|
53
|
+
fleet.each { |worker| assign(worker) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def poll
|
|
57
|
+
ready, = IO.select(fleet.keys, nil, nil, POLL_INTERVAL)
|
|
58
|
+
Array(ready).each { |pipe| service(pipe) }
|
|
59
|
+
watch
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def service(pipe)
|
|
63
|
+
worker = fleet.fetch(pipe)
|
|
64
|
+
line = pipe.gets
|
|
65
|
+
return fleet.remove(pipe) unless line
|
|
66
|
+
message = parse(line)
|
|
67
|
+
dispatch(worker, message) if message
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def dispatch(worker, message)
|
|
71
|
+
case message["t"]
|
|
72
|
+
when "result" then finish(worker, message)
|
|
73
|
+
when "leak" then context.resolve.call(message)
|
|
74
|
+
when "ready" then assign(worker)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def finish(worker, message)
|
|
79
|
+
fleet.done!(message["id"])
|
|
80
|
+
fleet.progress!
|
|
81
|
+
context.resolve.call(message)
|
|
82
|
+
worker.idle!
|
|
83
|
+
worker.renew(limit)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def close(worker) = shut(worker.retire)
|
|
87
|
+
|
|
88
|
+
def watch
|
|
89
|
+
current = now
|
|
90
|
+
fleet.pairs { |pipe, worker| expire(pipe, worker, current) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expire(pipe, worker, current)
|
|
94
|
+
return unless worker.expired?(current)
|
|
95
|
+
kill(worker.pid)
|
|
96
|
+
fleet.remove(pipe, reason: :timeout)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
require_relative "worker_pool/fleet"
|
|
101
|
+
require_relative "worker_pool/worker"
|