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,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module Frameworks
|
|
7
|
+
RunOutcome =
|
|
8
|
+
Struct.new(:passed, :failed_ids, :failures, keyword_init: true) do
|
|
9
|
+
def passed?
|
|
10
|
+
passed
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def failures
|
|
14
|
+
self[:failures] || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def verdict
|
|
18
|
+
passed? ? [:survived, []] : [:killed, failed_ids]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Kimera::Frameworks::Adapter
|
|
25
|
+
def source(files)
|
|
26
|
+
raise(NotImplementedError)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_ids
|
|
30
|
+
raise(NotImplementedError)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start; end
|
|
34
|
+
|
|
35
|
+
def finish; end
|
|
36
|
+
|
|
37
|
+
def run(ids)
|
|
38
|
+
raise(NotImplementedError)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def describe(id)
|
|
42
|
+
id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@registry = {}
|
|
46
|
+
|
|
47
|
+
NO_LOADER = -> {}
|
|
48
|
+
ADAPTER_LOADERS = {
|
|
49
|
+
"rspec" => lambda do
|
|
50
|
+
require_relative "rspec_adapter"
|
|
51
|
+
end,
|
|
52
|
+
"minitest" => lambda do
|
|
53
|
+
require_relative "minitest_adapter"
|
|
54
|
+
end
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
attr_reader :registry
|
|
59
|
+
|
|
60
|
+
def load(name)
|
|
61
|
+
ADAPTER_LOADERS.fetch(name.to_s, NO_LOADER).call
|
|
62
|
+
fetch(name).build
|
|
63
|
+
rescue LoadError => error
|
|
64
|
+
raise(Kimera::Error, unloadable(name, error))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def unloadable(name, error)
|
|
68
|
+
"framework #{name} is configured but cannot be loaded (#{error.message}); " \
|
|
69
|
+
"set framework: rspec or minitest in .kimera.yml, or add it to the Gemfile"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def register(name, klass)
|
|
73
|
+
registry[name.to_s] = klass
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fetch(name)
|
|
77
|
+
registry.fetch(name.to_s) do
|
|
78
|
+
raise(Kimera::Error, "unknown test framework adapter: #{name}")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "adapter"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module Frameworks
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Kimera::Frameworks::MinitestAdapter < Kimera::Frameworks::Adapter
|
|
11
|
+
TESTROOTS = %w[test spec].freeze
|
|
12
|
+
INSTALLED = true
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def build
|
|
16
|
+
require("minitest")
|
|
17
|
+
Minitest.seed ||= 1
|
|
18
|
+
disable
|
|
19
|
+
new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def disable
|
|
25
|
+
Minitest.class_variable_set(:@@installed_at_exit, INSTALLED)
|
|
26
|
+
rescue NameError, ArgumentError
|
|
27
|
+
Minitest.define_singleton_method(:run) { |*| true }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
super
|
|
33
|
+
@methods = {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def source(files)
|
|
37
|
+
paths(files)
|
|
38
|
+
Array(files).each { |f| load(File.expand_path(f)) }
|
|
39
|
+
reindex
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_ids
|
|
44
|
+
@methods.keys
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def run(ids)
|
|
48
|
+
failed = []
|
|
49
|
+
failures = {}
|
|
50
|
+
ids.each { |id| record(id, failed, failures) }
|
|
51
|
+
Kimera::Frameworks::RunOutcome.new(passed: failed.empty?, failed_ids: failed, failures: failures)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def record(id, failed, failures)
|
|
57
|
+
klass, name = @methods[id]
|
|
58
|
+
return unless klass
|
|
59
|
+
result = klass.new(name).run
|
|
60
|
+
return unless killing?(result)
|
|
61
|
+
failure(id, result, failed, failures)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def failure(id, result, failed, failures)
|
|
65
|
+
failed << id
|
|
66
|
+
failures[id] = message(result)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def paths(files)
|
|
70
|
+
Array(files).each do |file|
|
|
71
|
+
parts = File.expand_path(file).split(File::SEPARATOR)
|
|
72
|
+
TESTROOTS.each { |root| prepend(parts, root) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def prepend(parts, root)
|
|
77
|
+
index = parts.rindex(root)
|
|
78
|
+
return unless index
|
|
79
|
+
dir = parts[0..index].join(File::SEPARATOR)
|
|
80
|
+
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reindex
|
|
84
|
+
@methods = {}
|
|
85
|
+
Minitest::Runnable.runnables.each { |runnable| index(runnable) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def index(runnable)
|
|
89
|
+
runnable.runnable_methods.each { |name| @methods["#{runnable}##{name}"] = [runnable, name] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def killing?(result)
|
|
93
|
+
!result.passed? && !result.skipped?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def message(result)
|
|
97
|
+
assertion = result.failures.first
|
|
98
|
+
assertion && "#{assertion.class.name.split("::").last}: #{assertion.message}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Kimera::Frameworks::Adapter.register(:minitest, Kimera::Frameworks::MinitestAdapter)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rspec/core"
|
|
4
|
+
require_relative "adapter"
|
|
5
|
+
require_relative "rspec_group_index"
|
|
6
|
+
|
|
7
|
+
module Kimera
|
|
8
|
+
module Frameworks
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Kimera::Frameworks::RSpecAdapter < Kimera::Frameworks::Adapter
|
|
13
|
+
class << self
|
|
14
|
+
def build
|
|
15
|
+
configure
|
|
16
|
+
new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def configure
|
|
22
|
+
RSpec::Core::ConfigurationOptions.new([]).configure(RSpec.configuration)
|
|
23
|
+
rescue StandardError, ScriptError
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
super
|
|
30
|
+
@examples = {}
|
|
31
|
+
@suite = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def source(files)
|
|
35
|
+
Array(files).each { |file| load(file) }
|
|
36
|
+
reindex
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_ids
|
|
41
|
+
@examples.keys
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def describe(id)
|
|
45
|
+
ex = @examples[id]
|
|
46
|
+
ex ? ex.full_description : id
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def start
|
|
50
|
+
pid = Process.pid
|
|
51
|
+
return if @suite == pid
|
|
52
|
+
@suite = pid
|
|
53
|
+
hooks(:@before_suite_hooks)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def finish
|
|
57
|
+
return unless @suite == Process.pid
|
|
58
|
+
hooks(:@after_suite_hooks)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run(ids)
|
|
62
|
+
selected = ids.filter_map { |id| @examples[id] }
|
|
63
|
+
reset
|
|
64
|
+
clear(selected)
|
|
65
|
+
execute(selected)
|
|
66
|
+
outcome(selected)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def outcome(selected)
|
|
72
|
+
failed = selected.select { |ex| ex.execution_result.status == :failed }
|
|
73
|
+
Kimera::Frameworks::RunOutcome.new(
|
|
74
|
+
passed: failed.empty?, failed_ids: failed.map(&:id),
|
|
75
|
+
failures: failed.to_h { |example| [example.id, failure(example)] }
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def hooks(store)
|
|
80
|
+
config = RSpec.configuration
|
|
81
|
+
context = RSpec::Core::SuiteHookContext.new(store.to_s, config.reporter)
|
|
82
|
+
config.instance_variable_get(store).each { |hook| hook.run(context) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def failure(example)
|
|
86
|
+
exception = example.execution_result.exception
|
|
87
|
+
exception && "#{exception.class}: #{exception.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reset
|
|
91
|
+
world = RSpec.world
|
|
92
|
+
world.wants_to_quit = false
|
|
93
|
+
world.non_example_failure = false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def load(file) = Kernel.load(File.expand_path(file))
|
|
97
|
+
|
|
98
|
+
def reindex
|
|
99
|
+
@examples = {}
|
|
100
|
+
RSpec.world.example_groups.each { |top| index(top) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def index(top)
|
|
104
|
+
Kimera::Frameworks::RSpecGroupIndex.examples(top).each { |example| @examples[example.id] = example }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def clear(examples)
|
|
108
|
+
examples.each { |ex| ex.instance_variable_set(:@exception, nil) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def execute(selected)
|
|
112
|
+
reporter = RSpec::Core::Reporter.new(RSpec.configuration)
|
|
113
|
+
filtered = RSpec.world.filtered_examples
|
|
114
|
+
wanted = selected.to_a
|
|
115
|
+
Kimera::Frameworks::RSpecGroupIndex.top(selected).each { |top| perform(top, reporter, filtered, wanted) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def perform(top, reporter, filtered, wanted)
|
|
119
|
+
Kimera::Frameworks::RSpecGroupIndex.narrow(filtered, top, wanted) { top.run(reporter) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Kimera::Frameworks::Adapter.register(:rspec, Kimera::Frameworks::RSpecAdapter)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module Frameworks
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class Kimera::Frameworks::RSpecGroupIndex
|
|
9
|
+
class << self
|
|
10
|
+
def top(examples)
|
|
11
|
+
examples.map { |example| group(example) }.uniq
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def tree(group, collected = [])
|
|
15
|
+
collected << group
|
|
16
|
+
group.children.each { |child| tree(child, collected) }
|
|
17
|
+
collected
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def filtered(group)
|
|
21
|
+
RSpec.configuration.filter_manager.prune(group.examples)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def examples(top)
|
|
25
|
+
tree(top).flat_map { |group| filtered(group) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def narrow(filtered, top, wanted, &)
|
|
29
|
+
swap(filtered, tree(top).to_h { |group| [group, group.examples & wanted] }, &)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def swap(filtered, scoped)
|
|
35
|
+
filtered.merge!(scoped)
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
scoped.each_key { |group| filtered.delete(group) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def group(example)
|
|
42
|
+
example.example_group.parent_groups.last
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Incremental
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class Kimera::Incremental::DiffError < Kimera::Error; end
|
|
12
|
+
|
|
13
|
+
class Kimera::Incremental::GitDiff
|
|
14
|
+
FILE_HEADER = %r{\A\+\+\+ b/(.*)\n?\z}
|
|
15
|
+
HUNK_HEADER = /\A@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/
|
|
16
|
+
BASEFLAGS = %w[diff --relative --unified=0 --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def lines(**scope)
|
|
20
|
+
new(**scope).lines
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def files(**scope)
|
|
24
|
+
new(**scope).files
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse(text)
|
|
28
|
+
files = {}
|
|
29
|
+
current = nil
|
|
30
|
+
text.each_line { |line| current = advance(files, current, line) }
|
|
31
|
+
files
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def advance(files, current, line)
|
|
37
|
+
header = line.match(FILE_HEADER)
|
|
38
|
+
return named(header) if header
|
|
39
|
+
hunk = line.match(HUNK_HEADER)
|
|
40
|
+
collect(files, current, hunk) if current && hunk
|
|
41
|
+
current
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def named(header)
|
|
45
|
+
path = header[1]
|
|
46
|
+
path unless path == File::NULL
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def collect(files, file, match)
|
|
50
|
+
span = match[2]
|
|
51
|
+
count = span ? Integer(span, 10) : 1
|
|
52
|
+
return if count.zero?
|
|
53
|
+
range(files, file, Integer(match[1], 10), count)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def range(files, file, start, count)
|
|
57
|
+
set = (files[file] ||= Set.new)
|
|
58
|
+
(start...(start + count)).each { |line| set << line }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(since:, root: ".", paths: nil)
|
|
63
|
+
@since = since
|
|
64
|
+
@root = root
|
|
65
|
+
@paths = paths
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def lines
|
|
69
|
+
out, ok = read
|
|
70
|
+
raise(failure) unless ok
|
|
71
|
+
self.class.parse(out)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def files
|
|
75
|
+
lines.keys
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def failure
|
|
81
|
+
Kimera::Incremental::DiffError.new(
|
|
82
|
+
"git diff against #{@since.inspect} failed (unknown ref, not a git repository, or git missing)"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def read
|
|
87
|
+
out, _error, status = Open3.capture3(*command)
|
|
88
|
+
[out, status.success?]
|
|
89
|
+
rescue Errno::ENOENT
|
|
90
|
+
["", false]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def command
|
|
94
|
+
cmd = ["git", "-C", @root.to_s, *BASEFLAGS, @since.to_s]
|
|
95
|
+
cmd += ["--", *Array(@paths)] if @paths
|
|
96
|
+
cmd
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_diff"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module Incremental
|
|
7
|
+
module Selection
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def select(registry, changes)
|
|
11
|
+
ids = []
|
|
12
|
+
registry.points.each { |point| ids.concat(ids(point, changes)) }
|
|
13
|
+
ids
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ids(point, changes)
|
|
17
|
+
lines = changes[point.file]
|
|
18
|
+
return [] unless lines && point.location.range.any? { |line| lines.include?(line) }
|
|
19
|
+
point.ids
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def changed(registry, since:, root: ".")
|
|
23
|
+
select(registry, GitDiff.lines(since: since, root: root))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../results/result"
|
|
5
|
+
require_relative "../results/run_report"
|
|
6
|
+
|
|
7
|
+
module Kimera
|
|
8
|
+
module Incremental
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Kimera::Incremental::Session
|
|
13
|
+
VERSION = 2
|
|
14
|
+
|
|
15
|
+
attr_reader :results, :leaks
|
|
16
|
+
|
|
17
|
+
def initialize(results: {}, leaks: [], meta: {}, fingerprints: {})
|
|
18
|
+
@results = results
|
|
19
|
+
@leaks = leaks
|
|
20
|
+
@meta = meta
|
|
21
|
+
@fingerprints = fingerprints
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def load(path, registry: nil)
|
|
26
|
+
return new unless path && File.exist?(path)
|
|
27
|
+
session = decode(JSON.parse(File.read(path)))
|
|
28
|
+
session.prune!(registry) if registry
|
|
29
|
+
session
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def decode(data)
|
|
33
|
+
new(**attributes(data))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attributes(data)
|
|
37
|
+
attrs = { leaks: leaks(data["leaks"]), meta: data["meta"] || {} }
|
|
38
|
+
attrs[:results] = keys(data["results"]) { |hash| Kimera::MutantResult.from_h(hash) }
|
|
39
|
+
attrs[:fingerprints] = keys(data["fingerprints"]) { |fingerprint| fingerprint }
|
|
40
|
+
attrs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def leaks(raw)
|
|
44
|
+
Array(raw).map { |hash| Kimera::LeakReport.new(mutant_id: hash["mutant_id"], detail: hash["detail"]) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def keys(hash)
|
|
48
|
+
Hash(hash).to_h { |id, value| [Integer(id, 10), yield(value)] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fingerprint(registry, id)
|
|
52
|
+
pair = registry.point(id)
|
|
53
|
+
return unless pair
|
|
54
|
+
mutant, point = pair
|
|
55
|
+
[point.file, point.location.start_line, point.original_source, mutant.label].join(" ")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prune!(registry)
|
|
60
|
+
@results.reject! { |id, _| stale?(id, registry) }
|
|
61
|
+
@leaks.select! { |leak| @results.key?(leak.mutant_id) }
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def done?(id)
|
|
66
|
+
@results.key?(id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report(evaluated, waived, registry)
|
|
70
|
+
results = evaluated.filter_map { |id| @results[id] }
|
|
71
|
+
results += waived.map { |id| Kimera::MutantResult.waived(id, registry.index[id]&.file) }
|
|
72
|
+
Kimera::RunReport.new(results: results, leaks: @leaks, registry: registry)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def merge!(report)
|
|
76
|
+
report.results.each { |r| @results[r.mutant_id] = r }
|
|
77
|
+
@leaks.concat(report.leaks)
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def save(path, registry: nil, meta: {})
|
|
82
|
+
@meta = @meta.merge(meta)
|
|
83
|
+
@fingerprints = fingerprints(registry) if registry
|
|
84
|
+
File.write(path, JSON.pretty_generate(payload))
|
|
85
|
+
path
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def stale?(id, registry)
|
|
91
|
+
stored = @fingerprints[id]
|
|
92
|
+
!stored || stored != self.class.fingerprint(registry, id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def payload
|
|
96
|
+
base.merge("fingerprints" => @fingerprints.transform_keys(&:to_s), "leaks" => @leaks.map(&:to_h))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def base
|
|
100
|
+
{ "version" => VERSION, "meta" => @meta, "results" => @results.transform_keys(&:to_s).transform_values(&:to_h) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fingerprints(registry)
|
|
104
|
+
@results.keys.each_with_object({}) do |id, hash|
|
|
105
|
+
fingerprint = self.class.fingerprint(registry, id)
|
|
106
|
+
hash[id] = fingerprint if fingerprint
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/syntax"
|
|
4
|
+
require_relative "support/syntax_types"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Memoization
|
|
8
|
+
OR_WRITE_NODES = [
|
|
9
|
+
SyntaxTypes::InstanceVariableOrWriteNode,
|
|
10
|
+
SyntaxTypes::ClassVariableOrWriteNode,
|
|
11
|
+
SyntaxTypes::GlobalVariableOrWriteNode
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
CONST_WRITE_NODES = [SyntaxTypes::ConstantWriteNode, SyntaxTypes::ConstantPathWriteNode].freeze
|
|
15
|
+
|
|
16
|
+
SELF_OR_WRITE = {
|
|
17
|
+
SyntaxTypes::InstanceVariableWriteNode => SyntaxTypes::InstanceVariableReadNode,
|
|
18
|
+
SyntaxTypes::ClassVariableWriteNode => SyntaxTypes::ClassVariableReadNode,
|
|
19
|
+
SyntaxTypes::GlobalVariableWriteNode => SyntaxTypes::GlobalVariableReadNode
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
Range =
|
|
23
|
+
Struct.new(:start_offset, :end_offset, :reason, :holes) do
|
|
24
|
+
def contains?(start, finish)
|
|
25
|
+
return false unless start >= start_offset && finish <= end_offset
|
|
26
|
+
Array(holes).none? { |first, last| start >= first && finish <= last }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def ranges(source)
|
|
33
|
+
result = Kimera::Syntax.parse(source)
|
|
34
|
+
return [] if result.failure?
|
|
35
|
+
ranges = []
|
|
36
|
+
visit(result.value, ranges)
|
|
37
|
+
ranges
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def visit(node, ranges)
|
|
41
|
+
return unless node.is_a?(SyntaxTypes::Node)
|
|
42
|
+
taint = taint(node)
|
|
43
|
+
ranges << taint if taint
|
|
44
|
+
node.compact_child_nodes.each { |child| visit(child, ranges) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def taint(node)
|
|
48
|
+
case node
|
|
49
|
+
when *OR_WRITE_NODES then value(node, "memoized via ||=")
|
|
50
|
+
when *CONST_WRITE_NODES then value(node, "constant assignment (computed once)")
|
|
51
|
+
else cached(node)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def value(node, reason)
|
|
56
|
+
expression = node.value
|
|
57
|
+
range(expression, reason) if expression
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cached(node)
|
|
61
|
+
reader = SELF_OR_WRITE[node.class]
|
|
62
|
+
return unless reader
|
|
63
|
+
memoized(node, reader)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def memoized(node, reader)
|
|
67
|
+
value = node.value
|
|
68
|
+
return unless value.is_a?(SyntaxTypes::OrNode)
|
|
69
|
+
left = value.left
|
|
70
|
+
return unless left.is_a?(reader) && same?(node, left)
|
|
71
|
+
range(value, "memoized via `x = x || ...`")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def same?(write, read)
|
|
75
|
+
write.name == read.name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def range(node, reason)
|
|
79
|
+
start, finish = span(node)
|
|
80
|
+
Range.new(start, finish, reason, holes(node))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
HOLE_NODES = [SyntaxTypes::DefNode, SyntaxTypes::LambdaNode].freeze
|
|
84
|
+
|
|
85
|
+
def holes(node, holes = [])
|
|
86
|
+
return holes unless node.is_a?(SyntaxTypes::Node)
|
|
87
|
+
return body(node, holes) if HOLE_NODES.any? { |kind| node.is_a?(kind) }
|
|
88
|
+
node.compact_child_nodes.each { |child| holes(child, holes) }
|
|
89
|
+
holes
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def body(node, holes)
|
|
93
|
+
value = node.body
|
|
94
|
+
holes << span(value) if value
|
|
95
|
+
holes
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def span(node)
|
|
99
|
+
location = node.location
|
|
100
|
+
start = location.start_offset
|
|
101
|
+
[start, start + location.length]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
class Kimera::Operators::ArgumentDrop < Kimera::Operators::Base
|
|
7
|
+
SKIP_KINDS = [SplatNode, KeywordHashNode, BlockArgumentNode, ForwardingArgumentsNode].freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def key = "argument_drop"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def variants(node, **)
|
|
14
|
+
return unless identifier?(node)
|
|
15
|
+
args = arguments(node)
|
|
16
|
+
return if args.any? { |a| SKIP_KINDS.any? { |k| a.is_a?(k) } }
|
|
17
|
+
drops(args, "drop_argument", label: "drop arg")
|
|
18
|
+
end
|
|
19
|
+
end
|