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,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../error"
|
|
5
|
+
|
|
6
|
+
module Kimera
|
|
7
|
+
module Config
|
|
8
|
+
DEFAULT_PATH = ".kimera.yml"
|
|
9
|
+
|
|
10
|
+
SCALAR_KEYS = %w[framework source_root soft_timeout hard_timeout leak_every
|
|
11
|
+
jobs max_survivors max_ignored max_errors coverage isolate_db
|
|
12
|
+
fail_on_no_coverage progress color quiet verbose log format report baseline].freeze
|
|
13
|
+
LIST_KEYS = %w[paths tests operators exclude exclude_tests isolate_when_covered_by require].freeze
|
|
14
|
+
|
|
15
|
+
SCOPE_KEYS = %i[paths operators exclude].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def parse(argv)
|
|
20
|
+
index = argv.index("--config")
|
|
21
|
+
return root unless index
|
|
22
|
+
file = argv[index + 1]
|
|
23
|
+
raise(UsageError, "no such config file: #{file}") unless file && File.exist?(file)
|
|
24
|
+
load(file)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def prefer(from_cli, from_config, default)
|
|
28
|
+
cli = Array(from_cli)
|
|
29
|
+
chosen = cli.empty? ? Array(from_config) : cli
|
|
30
|
+
chosen.empty? ? default.dup : chosen
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def document(**options) = options.transform_keys(&:to_s)
|
|
34
|
+
|
|
35
|
+
def scope(defaults, argv)
|
|
36
|
+
defaults.merge(parse(argv).slice(*SCOPE_KEYS))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load(file)
|
|
40
|
+
return {} unless File.exist?(file)
|
|
41
|
+
options = normalize(YAML.safe_load_file(file) || {})
|
|
42
|
+
baseline = options[:baseline]
|
|
43
|
+
baseline ? options.merge(ignore: Array(options[:ignore]) + ignores_from(baseline, file)) : options
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def root(root: ".")
|
|
47
|
+
load(File.join(root, DEFAULT_PATH))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def normalize(data)
|
|
51
|
+
options = scalars(data)
|
|
52
|
+
options[:ignore] = Array(data["ignore"]).map { |r| validate(symbolize(r)) } if data.key?("ignore")
|
|
53
|
+
options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ignores_from(baseline, file)
|
|
57
|
+
anchored = File.expand_path(baseline, File.dirname(file))
|
|
58
|
+
raise(UsageError, "no such baseline file: #{anchored}") unless File.file?(anchored)
|
|
59
|
+
Array((YAML.safe_load_file(anchored) || {})["ignore"]).map { |rule| validate(symbolize(rule)) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def scalars(data)
|
|
63
|
+
(SCALAR_KEYS + LIST_KEYS).each_with_object({}) do |key, options|
|
|
64
|
+
next unless data.key?(key)
|
|
65
|
+
value = data[key]
|
|
66
|
+
options[key.to_sym] = LIST_KEYS.include?(key) ? Array(value) : value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def symbolize(rule)
|
|
71
|
+
rule.transform_keys(&:to_sym)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate(rule)
|
|
75
|
+
file = rule[:file]
|
|
76
|
+
raise(UsageError, "ignore entry missing file: #{rule.inspect}") unless file
|
|
77
|
+
reason(rule, file)
|
|
78
|
+
rule
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def reason(rule, file)
|
|
82
|
+
return unless reasonless?(rule)
|
|
83
|
+
raise(UsageError, "ignore entry for #{file} needs a reason: (why is this mutant equivalent?)")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reasonless?(rule)
|
|
87
|
+
rule[:reason].to_s.strip.empty?
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module FileSet
|
|
7
|
+
DEFAULT_GLOBS = ["app/**/*.rb", "lib/**/*.rb"].freeze
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def globs(from_cli, options)
|
|
12
|
+
Config.prefer(from_cli, options[:paths], DEFAULT_GLOBS)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def scoped(from_cli, options)
|
|
16
|
+
expand(globs(from_cli, options), exclude: options[:exclude])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def expand(patterns, include: [], exclude: [])
|
|
20
|
+
exclude((Array(patterns) + Array(include)).flat_map { |pattern| resolve(pattern) }.uniq, exclude).sort
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def relative(path, root)
|
|
24
|
+
absolute = File.expand_path(path)
|
|
25
|
+
absolute.start_with?("#{root}/") ? absolute[(root.length + 1)..] : path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def resolve(pattern)
|
|
29
|
+
return Dir.glob(File.join(pattern, "**", "*.rb")) if File.directory?(pattern)
|
|
30
|
+
Dir.glob(pattern).select { |f| File.file?(f) && f.end_with?(".rb") }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unused(patterns, exclude)
|
|
34
|
+
files = expand(patterns)
|
|
35
|
+
Array(exclude).reject do |pattern|
|
|
36
|
+
resolved = resolve(pattern).to_set
|
|
37
|
+
files.any? { |file| resolved.include?(file) || matches?(file, [pattern]) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def exclude(files, exclude)
|
|
42
|
+
patterns = Array(exclude)
|
|
43
|
+
excluded = patterns.flat_map { |p| resolve(p) }.to_set
|
|
44
|
+
files.reject { |file| excluded.include?(file) || matches?(file, patterns) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def matches?(file, patterns)
|
|
48
|
+
Array(patterns).any? { |p| File.fnmatch?(p, file, File::FNM_PATHNAME) }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module IgnoreList
|
|
5
|
+
Match =
|
|
6
|
+
Data.define(:point, :mutant) do
|
|
7
|
+
def rule?(rule)
|
|
8
|
+
glob = rule[:file]
|
|
9
|
+
glob && File.fnmatch?(glob, point.file, File::FNM_PATHNAME) && anchors?(rule)
|
|
10
|
+
end
|
|
11
|
+
private
|
|
12
|
+
def line?(value) = point.location.range.include?(Integer(value))
|
|
13
|
+
def column?(value) = point.location.start_column == Integer(value)
|
|
14
|
+
def label?(value) = value == mutant.label
|
|
15
|
+
def method?(value) = value.to_s == point.method_name
|
|
16
|
+
def original?(value) = squeeze(value) == squeeze(point.original_source)
|
|
17
|
+
def squeeze(source) = source.to_s.split.join(" ")
|
|
18
|
+
|
|
19
|
+
def anchors?(rule)
|
|
20
|
+
ANCHORS.all? do |field, matcher|
|
|
21
|
+
value = rule[field]
|
|
22
|
+
!value || __send__(matcher, value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def ids(registry, rules)
|
|
30
|
+
rules = Array(rules)
|
|
31
|
+
registry.each.filter_map { |mutant, point| mutant.id if rules.any? { |rule| match?(rule, point, mutant) } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def stale(registry, rules)
|
|
35
|
+
Array(rules).select do |rule|
|
|
36
|
+
relevant?(rule, registry) && !any?(rule, registry)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def relevant?(rule, registry)
|
|
41
|
+
glob = rule[:file]
|
|
42
|
+
glob && registry.files.any? { |file| File.fnmatch?(glob, file, File::FNM_PATHNAME) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def any?(rule, registry)
|
|
46
|
+
registry.each.any? { |mutant, point| match?(rule, point, mutant) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
ANCHORS = { line: :line?, column: :column?, label: :label? }.merge(method: :method?, original: :original?).freeze
|
|
50
|
+
|
|
51
|
+
def match?(rule, point, mutant)
|
|
52
|
+
Match.new(point, mutant).rule?(rule)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runtime"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module SelfProtection
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
OUT_OF_PROCESS_ENTRYPOINTS = %w[
|
|
10
|
+
execution/isolated_child.rb
|
|
11
|
+
execution/isolated_child_minitest.rb
|
|
12
|
+
].map { |path| File.expand_path(path, __dir__) }.freeze
|
|
13
|
+
|
|
14
|
+
def paths
|
|
15
|
+
[Runtime::SOURCE_PATH, *OUT_OF_PROCESS_ENTRYPOINTS]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
HARNESS_CRITICAL_SUFFIXES = %w[
|
|
19
|
+
kimera/frameworks/adapter.rb
|
|
20
|
+
kimera/frameworks/rspec_adapter.rb
|
|
21
|
+
kimera/frameworks/rspec_group_index.rb
|
|
22
|
+
kimera/frameworks/minitest_adapter.rb
|
|
23
|
+
kimera/execution/shift.rb
|
|
24
|
+
kimera/execution/shift/attempt.rb
|
|
25
|
+
kimera/execution/shift/coverage_channel.rb
|
|
26
|
+
kimera/execution/shift/killer_memory.rb
|
|
27
|
+
kimera/execution/shift/leak_guard.rb
|
|
28
|
+
kimera/execution/harness.rb
|
|
29
|
+
kimera/execution/baseline_pass.rb
|
|
30
|
+
kimera/execution/reload.rb
|
|
31
|
+
kimera/execution/parallel_test_databases.rb
|
|
32
|
+
kimera/execution/verdicts.rb
|
|
33
|
+
kimera/execution/worker_pool.rb
|
|
34
|
+
kimera/execution/worker_pool/fleet.rb
|
|
35
|
+
kimera/execution/child_process.rb
|
|
36
|
+
kimera/execution/rig.rb
|
|
37
|
+
kimera/registry/builder.rb
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
def critical?(path)
|
|
41
|
+
text = path.to_s
|
|
42
|
+
HARNESS_CRITICAL_SUFFIXES.any? { |suffix| text.end_with?(suffix) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def protected?(path)
|
|
46
|
+
target = canonical(path)
|
|
47
|
+
paths.any? { |own| canonical(own) == target }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def canonical(path)
|
|
51
|
+
File.realpath(path)
|
|
52
|
+
rescue SystemCallError
|
|
53
|
+
File.expand_path(path)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
Duration =
|
|
5
|
+
Data.define(:seconds) do
|
|
6
|
+
def clock = format("%d:%02d", seconds / 60, seconds % 60)
|
|
7
|
+
|
|
8
|
+
def brief
|
|
9
|
+
return format("%.1fs", seconds) if seconds >= 1
|
|
10
|
+
format(seconds >= 0.01 ? "%.0fms" : "%.1fms", seconds * 1000)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module Syntax
|
|
7
|
+
%i[
|
|
8
|
+
AndNode ArrayNode AssocNode AssocSplatNode BlockArgumentNode BlockNode
|
|
9
|
+
CallNode ClassVariableOrWriteNode ClassVariableReadNode ClassVariableWriteNode
|
|
10
|
+
CallOperatorWriteNode ClassVariableOperatorWriteNode ConstantOperatorWriteNode
|
|
11
|
+
ConstantPathOperatorWriteNode GlobalVariableOperatorWriteNode
|
|
12
|
+
InstanceVariableOperatorWriteNode LocalVariableOperatorWriteNode
|
|
13
|
+
ConstantPathWriteNode ConstantWriteNode DefNode DefinedNode FalseNode FloatNode
|
|
14
|
+
ForwardingArgumentsNode GlobalVariableOrWriteNode GlobalVariableReadNode
|
|
15
|
+
GlobalVariableWriteNode HashNode IfNode InNode InstanceVariableOrWriteNode
|
|
16
|
+
InstanceVariableReadNode InstanceVariableWriteNode IntegerNode KeywordHashNode
|
|
17
|
+
LambdaNode MatchPredicateNode MatchRequiredNode NilNode Node
|
|
18
|
+
OptionalKeywordParameterNode OptionalParameterNode OrNode ParenthesesNode
|
|
19
|
+
RangeNode RegularExpressionNode ReturnNode SplatNode StatementsNode StringNode
|
|
20
|
+
SymbolNode TrueNode UnlessNode
|
|
21
|
+
].each { |name| const_set(name, Prism.const_get(name)) }
|
|
22
|
+
|
|
23
|
+
OPERATOR_WRITES = [
|
|
24
|
+
CallOperatorWriteNode, ClassVariableOperatorWriteNode, ConstantOperatorWriteNode,
|
|
25
|
+
ConstantPathOperatorWriteNode, GlobalVariableOperatorWriteNode,
|
|
26
|
+
InstanceVariableOperatorWriteNode, LocalVariableOperatorWriteNode
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
View =
|
|
30
|
+
Data.define(:node) do
|
|
31
|
+
def collection(splats)
|
|
32
|
+
[node.elements, splats[node.class]] if splats.key?(node.class)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unwrap(names)
|
|
36
|
+
return unless node.is_a?(CallNode) && node.receiver && names.include?(node.name)
|
|
37
|
+
node.name unless node.arguments || node.block
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def operator
|
|
41
|
+
node.binary_operator if OPERATOR_WRITES.any? { |kind| node.is_a?(kind) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def diagnostic?(names, loggers)
|
|
45
|
+
receiver = node.receiver
|
|
46
|
+
return names.include?(node.name) unless receiver
|
|
47
|
+
logger?(receiver, loggers)
|
|
48
|
+
end
|
|
49
|
+
private
|
|
50
|
+
def logger?(receiver, loggers)
|
|
51
|
+
while receiver
|
|
52
|
+
return true if named?(receiver, loggers)
|
|
53
|
+
receiver = receiver.is_a?(CallNode) ? receiver.receiver : nil
|
|
54
|
+
end
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def named?(receiver, names)
|
|
59
|
+
receiver.class.method_defined?(:name) && names.include?(receiver.name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
def parse(source) = Prism.parse(source)
|
|
66
|
+
def view(node) = View.new(node)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "syntax"
|
|
4
|
+
|
|
5
|
+
module Kimera
|
|
6
|
+
module SyntaxTypes
|
|
7
|
+
Kimera::Syntax.constants.grep(/Node\z/).each do |name|
|
|
8
|
+
const_set(name, Kimera::Syntax.const_get(name))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Installer =
|
|
12
|
+
Data.define(:target) do
|
|
13
|
+
def call
|
|
14
|
+
SyntaxTypes.constants.grep(/Node\z/).each do |name|
|
|
15
|
+
target.const_set(name, SyntaxTypes.const_get(name)) unless target.constants.include?(name)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "warnings"
|
|
4
|
+
|
|
5
|
+
Kimera::Warnings.silence { require "unparser" }
|
|
6
|
+
|
|
7
|
+
module Kimera
|
|
8
|
+
module Unparse
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def parse(source) = Kimera::Warnings.silence { Unparser.parse(source) }
|
|
12
|
+
|
|
13
|
+
def unparse(node) = Kimera::Warnings.silence { Unparser.unparse(node) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/unparse"
|
|
4
|
+
require_relative "overlay_splice"
|
|
5
|
+
|
|
6
|
+
class Kimera::Overlay::FileWeave
|
|
7
|
+
include Kimera::OverlaySplice
|
|
8
|
+
|
|
9
|
+
def initialize(file, map, safe, unsafe)
|
|
10
|
+
@file = file
|
|
11
|
+
@map = map
|
|
12
|
+
@safe = safe
|
|
13
|
+
@unsafe = unsafe
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def result
|
|
17
|
+
attempt(@safe) || fallback
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def io = @_io ||= $stderr
|
|
23
|
+
|
|
24
|
+
def attempt(points)
|
|
25
|
+
render(Kimera::Guardrail.new(@map, points))
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render(weaver)
|
|
31
|
+
Kimera::Overlay::Result.new(
|
|
32
|
+
file: @file, source: @map.restore(Kimera::Unparse.unparse(weaver.tree)), mutant_ids: weaver.applied,
|
|
33
|
+
skipped_unsafe: @unsafe.flat_map(&:ids)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fallback
|
|
38
|
+
viable = bisect(@safe)
|
|
39
|
+
return recover if viable.empty?
|
|
40
|
+
finish(viable)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def finish(viable)
|
|
44
|
+
dropped = @safe - viable
|
|
45
|
+
notice(dropped)
|
|
46
|
+
result = attempt(viable)
|
|
47
|
+
result.skipped_unsafe.concat(dropped.flat_map(&:ids))
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notice(dropped)
|
|
52
|
+
io.puts(
|
|
53
|
+
"kimera: #{@file}: #{dropped.size} mutation point(s) at " \
|
|
54
|
+
"#{dropped.map { |p| "line #{p.location.start_line}" }.uniq.join(", ")} " \
|
|
55
|
+
"are unguardable (unparser round-trip); their mutants are reported no_coverage"
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def recover
|
|
60
|
+
spliced = splice
|
|
61
|
+
return spliced if spliced
|
|
62
|
+
@map.restore(Kimera::Unparse.unparse(Kimera::Guardrail.new(@map, @safe).tree))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bisect(points)
|
|
66
|
+
return points if attempt(points)
|
|
67
|
+
count = points.size
|
|
68
|
+
return [] if count <= 1
|
|
69
|
+
merge(points, count)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def merge(points, count)
|
|
73
|
+
half = count / 2
|
|
74
|
+
merged = bisect(points[0...half]) + bisect(points[half..])
|
|
75
|
+
merged.pop while merged.any? && !attempt(merged)
|
|
76
|
+
merged
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/unparse"
|
|
4
|
+
require_relative "../rewrite/ast_walk"
|
|
5
|
+
require_relative "../rewrite/directive"
|
|
6
|
+
require_relative "guardrail_dispatch"
|
|
7
|
+
require_relative "guardrail_interpolation"
|
|
8
|
+
require_relative "guardrail_value_objects"
|
|
9
|
+
require_relative "source_map"
|
|
10
|
+
|
|
11
|
+
class Kimera::Guardrail
|
|
12
|
+
include Kimera::GuardrailDispatch
|
|
13
|
+
include Kimera::GuardrailInterpolation
|
|
14
|
+
include Kimera::GuardrailValueObjects
|
|
15
|
+
|
|
16
|
+
attr_reader :applied
|
|
17
|
+
|
|
18
|
+
Patch =
|
|
19
|
+
Data.define(:map, :span, :directive) do
|
|
20
|
+
def apply(rebuilt, original)
|
|
21
|
+
expression = original.location&.expression
|
|
22
|
+
return rebuilt unless expression && map.span(expression) == span
|
|
23
|
+
Kimera::Rewrite::Directive.apply(rebuilt, directive)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(map, points)
|
|
28
|
+
@map = map
|
|
29
|
+
@points = points
|
|
30
|
+
@applied = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def tree
|
|
34
|
+
transform(@map.ast)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rewrite(node)
|
|
38
|
+
[Kimera::Unparse.unparse(transform(node)), applied]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def transform(node)
|
|
42
|
+
Kimera::Rewrite::AstWalk.rebuild(node) { |rebuilt, original| rebuild(rebuilt, original) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def bake(location, directive)
|
|
46
|
+
prune(overlay([location.start_offset, location.finish], directive))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def rebuild(rebuilt, original)
|
|
52
|
+
rebuilt = flatten(rebuilt) if rebuilt.type == :dstr
|
|
53
|
+
rebuilt = dispatch(rebuilt, points(original))
|
|
54
|
+
reopen(rebuilt)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def overlay(target, directive)
|
|
58
|
+
patch = Patch.new(@map, target, directive)
|
|
59
|
+
Kimera::Rewrite::AstWalk.rebuild(@map.ast) do |rebuilt, original|
|
|
60
|
+
reopen(patch.apply(rebuilt, original))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def points(node)
|
|
65
|
+
expr = node.location&.expression
|
|
66
|
+
return [] unless expr
|
|
67
|
+
ranges.fetch(@map.span(expr), [])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ranges
|
|
71
|
+
@_ranges ||= group(@points)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ast(type, *children)
|
|
75
|
+
Parser::AST::Node.new(type, children)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kimera
|
|
4
|
+
module GuardrailDispatch
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def group(points)
|
|
8
|
+
points.group_by do |point|
|
|
9
|
+
location = point.location
|
|
10
|
+
[location.start_offset, location.finish]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def dispatch(original, points)
|
|
15
|
+
mutants = points.flat_map(&:mutants).reverse
|
|
16
|
+
return parameter(original, mutants) if %i[optarg kwoptarg].include?(original.type)
|
|
17
|
+
guarded = guard(mutants, original) { |mutant| variant(original, mutant) }
|
|
18
|
+
guarded.equal?(original) ? guarded : ast(:begin, guarded)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def branch(mutant, truthy, dispatch)
|
|
22
|
+
id = mutant.id
|
|
23
|
+
@applied << id
|
|
24
|
+
conditional(active(id), truthy, dispatch)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def variant(original, mutant)
|
|
28
|
+
Kimera::Rewrite::Directive.apply(original, mutant.directive, unguard: method(:unwrap))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parameter(original, mutants)
|
|
32
|
+
name, default = original.children
|
|
33
|
+
original.updated(nil, [name, ast(:begin, guard(mutants, default) { missing(original, name) })])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def guard(mutants, seed) = mutants.reduce(seed) { |chain, mutant| branch(mutant, yield(mutant), chain) }
|
|
37
|
+
|
|
38
|
+
def missing(param, name)
|
|
39
|
+
ast(
|
|
40
|
+
:send, nil, :raise, ast(:const, nil, :ArgumentError),
|
|
41
|
+
ast(:str, "missing #{param.type == :kwoptarg ? "keyword" : "argument"}: #{name}")
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def unwrap(node)
|
|
46
|
+
return node unless type?(node, :begin)
|
|
47
|
+
inner, extra = node.children
|
|
48
|
+
return node unless guarded?(inner, extra)
|
|
49
|
+
unwrap(inner.children[2])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def guarded?(inner, extra)
|
|
53
|
+
!extra && type?(inner, :if) && active?(inner.children[0])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def active?(node)
|
|
57
|
+
type?(node, :send) && node.children[1] == :active?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def active(id)
|
|
61
|
+
ast(:send, ast(:const, ast(:cbase), :MutantRuntime), :active?, ast(:int, id))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def conditional(condition, truthy, fallback)
|
|
65
|
+
ast(:if, condition, truthy, fallback)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|