evilution 0.27.0 → 0.29.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 +4 -4
- data/.beads/interactions.jsonl +65 -0
- data/.rubocop_todo.yml +0 -1
- data/CHANGELOG.md +39 -0
- data/README.md +19 -0
- data/lib/evilution/ast/constant_names.rb +28 -11
- data/lib/evilution/ast/pattern/parser.rb +29 -17
- data/lib/evilution/baseline.rb +5 -4
- data/lib/evilution/cli/commands/session_diff.rb +6 -4
- data/lib/evilution/cli/commands/subjects.rb +6 -3
- data/lib/evilution/cli/commands/util_mutation.rb +24 -19
- data/lib/evilution/cli/parser/command_extractor.rb +9 -11
- data/lib/evilution/cli/parser/file_args.rb +3 -1
- data/lib/evilution/cli/parser/options_builder.rb +36 -1
- data/lib/evilution/cli/parser/stdin_reader.rb +2 -2
- data/lib/evilution/cli/parser.rb +18 -20
- data/lib/evilution/cli/printers/environment.rb +19 -19
- data/lib/evilution/cli/printers/session_diff.rb +8 -8
- data/lib/evilution/compare/diff_extractor/evilution.rb +22 -0
- data/lib/evilution/compare/diff_extractor/mutant.rb +30 -0
- data/lib/evilution/compare/diff_extractor.rb +6 -0
- data/lib/evilution/compare/fingerprint.rb +15 -72
- data/lib/evilution/compare/line_normalizer.rb +72 -0
- data/lib/evilution/compare/normalizer.rb +27 -9
- data/lib/evilution/config/validators/profile.rb +11 -0
- data/lib/evilution/config.rb +49 -32
- data/lib/evilution/disable_comment.rb +21 -12
- data/lib/evilution/integration/crash_detector.rb +2 -2
- data/lib/evilution/integration/loading/mutation_applier.rb +17 -12
- data/lib/evilution/integration/loading/source_evaluator.rb +6 -2
- data/lib/evilution/integration/minitest.rb +25 -16
- data/lib/evilution/integration/minitest_crash_detector.rb +2 -2
- data/lib/evilution/integration/rspec/state_guard/object_space_example_groups.rb +11 -3
- data/lib/evilution/integration/rspec.rb +4 -0
- data/lib/evilution/isolation/fork.rb +43 -28
- data/lib/evilution/isolation/in_process.rb +10 -6
- data/lib/evilution/mcp/info_tool/actions/subjects.rb +32 -23
- data/lib/evilution/mcp/info_tool/actions/tests.rb +22 -12
- data/lib/evilution/mcp/info_tool/request_parser.rb +3 -1
- data/lib/evilution/mcp/info_tool.rb +7 -3
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +3 -1
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +5 -1
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +19 -9
- data/lib/evilution/mcp/mutate_tool.rb +27 -14
- data/lib/evilution/mcp/session_tool.rb +27 -20
- data/lib/evilution/mutation.rb +60 -42
- data/lib/evilution/mutator/base.rb +23 -21
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +11 -14
- data/lib/evilution/mutator/operator/argument_removal.rb +11 -14
- data/lib/evilution/mutator/operator/begin_unwrap.rb +17 -5
- data/lib/evilution/mutator/operator/bitwise_complement.rb +26 -19
- data/lib/evilution/mutator/operator/block_param_removal.rb +18 -8
- data/lib/evilution/mutator/operator/block_pass_removal.rb +19 -15
- data/lib/evilution/mutator/operator/case_when.rb +7 -5
- data/lib/evilution/mutator/operator/conditional_branch.rb +22 -22
- data/lib/evilution/mutator/operator/equality_to_identity.rb +8 -3
- data/lib/evilution/mutator/operator/explicit_super_mutation.rb +17 -13
- data/lib/evilution/mutator/operator/index_to_at.rb +5 -4
- data/lib/evilution/mutator/operator/index_to_dig.rb +12 -6
- data/lib/evilution/mutator/operator/index_to_fetch.rb +5 -4
- data/lib/evilution/mutator/operator/keyword_argument.rb +30 -25
- data/lib/evilution/mutator/operator/mixin_removal.rb +20 -14
- data/lib/evilution/mutator/operator/multiple_assignment.rb +12 -13
- data/lib/evilution/mutator/operator/predicate_to_nil.rb +20 -0
- data/lib/evilution/mutator/operator/receiver_replacement.rb +9 -6
- data/lib/evilution/mutator/operator/regex_simplification.rb +62 -67
- data/lib/evilution/mutator/operator/rescue_body_replacement.rb +9 -8
- data/lib/evilution/mutator/operator/rescue_removal.rb +4 -7
- data/lib/evilution/mutator/operator/superclass_removal.rb +21 -15
- data/lib/evilution/mutator/registry.rb +20 -0
- data/lib/evilution/parallel/work_queue/channel/frame.rb +5 -1
- data/lib/evilution/parallel/work_queue/dispatcher.rb +15 -8
- data/lib/evilution/parallel/work_queue/worker/loop.rb +1 -1
- data/lib/evilution/parallel/work_queue/worker.rb +10 -7
- data/lib/evilution/parallel/work_queue.rb +35 -18
- data/lib/evilution/process_cleanup.rb +19 -0
- data/lib/evilution/reporter/cli/item_formatters/coverage_gap.rb +13 -8
- data/lib/evilution/reporter/cli/line_formatters/mutations.rb +17 -8
- data/lib/evilution/reporter/html/baseline_keys.rb +1 -1
- data/lib/evilution/reporter/html/diff_formatter.rb +1 -1
- data/lib/evilution/reporter/html/escape.rb +1 -1
- data/lib/evilution/reporter/html/section.rb +1 -1
- data/lib/evilution/reporter/html/sections.rb +4 -2
- data/lib/evilution/reporter/html/stylesheet.rb +1 -1
- data/lib/evilution/reporter/html.rb +8 -3
- data/lib/evilution/reporter/json.rb +52 -18
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +0 -13
- data/lib/evilution/reporter/suggestion/diff_lines.rb +28 -0
- data/lib/evilution/reporter/suggestion/registry.rb +1 -5
- data/lib/evilution/reporter/suggestion/templates/generic.rb +1 -1
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +361 -649
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +362 -603
- data/lib/evilution/reporter/suggestion/templates.rb +6 -0
- data/lib/evilution/result/error_info.rb +20 -0
- data/lib/evilution/result/memory_stats.rb +20 -0
- data/lib/evilution/result/mutation_result.rb +30 -14
- data/lib/evilution/runner/baseline_runner.rb +16 -10
- data/lib/evilution/runner/diagnostics.rb +14 -11
- data/lib/evilution/runner/isolation_resolver.rb +12 -11
- data/lib/evilution/runner/mutation_executor/mutation_runner.rb +1 -3
- data/lib/evilution/runner/mutation_executor/neutralization_pipeline.rb +1 -2
- data/lib/evilution/runner/mutation_executor/neutralizer/baseline_failed.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer/infra_error.rb +3 -10
- data/lib/evilution/runner/mutation_executor/neutralizer.rb +11 -0
- data/lib/evilution/runner/mutation_executor/result_cache.rb +4 -4
- data/lib/evilution/runner/mutation_executor/result_notifier.rb +1 -3
- data/lib/evilution/runner/mutation_executor/result_packer.rb +11 -9
- data/lib/evilution/runner/mutation_executor/strategy/parallel.rb +33 -13
- data/lib/evilution/runner/mutation_executor/strategy/sequential.rb +2 -4
- data/lib/evilution/runner/mutation_executor/strategy.rb +11 -0
- data/lib/evilution/runner/mutation_executor.rb +14 -20
- data/lib/evilution/runner/mutation_planner.rb +38 -19
- data/lib/evilution/runner/report_publisher.rb +1 -2
- data/lib/evilution/runner/subject_pipeline.rb +22 -13
- data/lib/evilution/runner.rb +36 -34
- data/lib/evilution/session/diff.rb +15 -6
- data/lib/evilution/spec_ast_cache.rb +26 -12
- data/lib/evilution/version.rb +1 -1
- data/lib/evilution.rb +1 -0
- data/script/memory_check +14 -6
- data/scripts/benchmark_density +10 -9
- data/scripts/compare_mutations +38 -21
- data/scripts/mutant_json_adapter +7 -4
- metadata +15 -3
- data/lib/evilution/reporter/html/namespace.rb +0 -11
data/lib/evilution/cli/parser.rb
CHANGED
|
@@ -20,7 +20,9 @@ class Evilution::CLI::Parser
|
|
|
20
20
|
|
|
21
21
|
preprocess_flags
|
|
22
22
|
remaining = OptionsBuilder.build(@options).parse!(@argv)
|
|
23
|
-
|
|
23
|
+
parsed_paths = FileArgs.parse(remaining)
|
|
24
|
+
@files = parsed_paths.files
|
|
25
|
+
@line_ranges = parsed_paths.ranges
|
|
24
26
|
read_stdin_files if @options.delete(:stdin) && %i[run subjects].include?(@command)
|
|
25
27
|
build_parsed_args
|
|
26
28
|
end
|
|
@@ -37,27 +39,23 @@ class Evilution::CLI::Parser
|
|
|
37
39
|
def preprocess_flags
|
|
38
40
|
result = []
|
|
39
41
|
i = 0
|
|
40
|
-
while i < @argv.length
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
next_arg = @argv[i + 1]
|
|
42
|
+
i = consume_token(i, result) while i < @argv.length
|
|
43
|
+
@argv = result
|
|
44
|
+
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
end
|
|
52
|
-
elsif arg.start_with?("--fail-fast=")
|
|
53
|
-
@options[:fail_fast] = arg.delete_prefix("--fail-fast=")
|
|
54
|
-
i += 1
|
|
55
|
-
else
|
|
56
|
-
result << arg
|
|
57
|
-
i += 1
|
|
58
|
-
end
|
|
46
|
+
def consume_token(i, result)
|
|
47
|
+
arg = @argv[i]
|
|
48
|
+
next_arg = @argv[i + 1]
|
|
49
|
+
if arg == "--fail-fast" && !next_arg.nil? && next_arg.match?(/\A-?\d+\z/)
|
|
50
|
+
@options[:fail_fast] = next_arg
|
|
51
|
+
return i + 2
|
|
59
52
|
end
|
|
60
|
-
|
|
53
|
+
if arg.start_with?("--fail-fast=")
|
|
54
|
+
@options[:fail_fast] = arg.delete_prefix("--fail-fast=")
|
|
55
|
+
return i + 1
|
|
56
|
+
end
|
|
57
|
+
result << arg
|
|
58
|
+
i + 1
|
|
61
59
|
end
|
|
62
60
|
|
|
63
61
|
def read_stdin_files
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require_relative "../printers"
|
|
4
4
|
|
|
5
5
|
class Evilution::CLI::Printers::Environment
|
|
6
|
+
PLAIN_SETTINGS = %i[
|
|
7
|
+
timeout format integration jobs isolation baseline incremental
|
|
8
|
+
verbose quiet progress min_score suggest_tests save_session
|
|
9
|
+
skip_heredoc_literals
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
6
12
|
def initialize(config, config_file:)
|
|
7
13
|
@config = config
|
|
8
14
|
@config_file = config_file
|
|
@@ -30,24 +36,18 @@ class Evilution::CLI::Printers::Environment
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def settings_lines
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
" suggest_tests: #{@config.suggest_tests}",
|
|
47
|
-
" save_session: #{@config.save_session}",
|
|
48
|
-
" target: #{@config.target || "(all files)"}",
|
|
49
|
-
" skip_heredoc_literals: #{@config.skip_heredoc_literals}",
|
|
50
|
-
" ignore_patterns: #{@config.ignore_patterns.empty? ? "(none)" : @config.ignore_patterns.inspect}"
|
|
51
|
-
]
|
|
39
|
+
plain_lines = PLAIN_SETTINGS.map { |k| setting_line(k, @config.public_send(k)) }
|
|
40
|
+
plain_lines.insert(10, setting_line(:fail_fast, @config.fail_fast || "(disabled)"))
|
|
41
|
+
plain_lines.insert(14, setting_line(:target, @config.target || "(all files)"))
|
|
42
|
+
plain_lines << setting_line(:ignore_patterns, format_ignore_patterns(@config.ignore_patterns))
|
|
43
|
+
plain_lines
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def setting_line(key, value)
|
|
47
|
+
" #{key}: #{value}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def format_ignore_patterns(patterns)
|
|
51
|
+
patterns.empty? ? "(none)" : patterns.inspect
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -32,16 +32,16 @@ class Evilution::CLI::Printers::SessionDiff
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def print_summary(io, summary)
|
|
35
|
-
delta_str = format("%+.2f%%", summary.score_delta * 100)
|
|
36
35
|
io.puts("Session Diff")
|
|
37
36
|
io.puts("=" * 40)
|
|
38
|
-
io.puts(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
io.puts(score_line("Base", summary.base_score, summary.base_killed, summary.base_total))
|
|
38
|
+
io.puts(score_line("Head", summary.head_score, summary.head_killed, summary.head_total))
|
|
39
|
+
io.puts("Delta: #{format("%+.2f%%", summary.score_delta * 100)}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def score_line(label, score, killed, total)
|
|
43
|
+
format("%<label>s score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
|
|
44
|
+
label: label, score: score * 100, killed: killed, total: total)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def print_section(io, title, mutations, color)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../diff_extractor"
|
|
4
|
+
|
|
5
|
+
# Extracts {minus:, plus:} payload arrays from Evilution-format diffs.
|
|
6
|
+
# Evilution diffs use "- " / "+ " line prefixes (note the trailing space) and
|
|
7
|
+
# do not carry unified-diff headers or hunk markers.
|
|
8
|
+
class Evilution::Compare::DiffExtractor::Evilution
|
|
9
|
+
def call(diff)
|
|
10
|
+
minus = []
|
|
11
|
+
plus = []
|
|
12
|
+
diff.to_s.each_line do |line|
|
|
13
|
+
line = line.chomp
|
|
14
|
+
if line.start_with?("- ")
|
|
15
|
+
minus << line[2..]
|
|
16
|
+
elsif line.start_with?("+ ")
|
|
17
|
+
plus << line[2..]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
{ minus: minus, plus: plus }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../diff_extractor"
|
|
4
|
+
|
|
5
|
+
# Extracts {minus:, plus:} payload arrays from Mutant unified-diff format.
|
|
6
|
+
# Skips the "--- <name>", "+++ <name>", and "@@ ... @@" header lines and
|
|
7
|
+
# returns each remaining payload line with its single leading "-" or "+"
|
|
8
|
+
# marker stripped.
|
|
9
|
+
#
|
|
10
|
+
# Header detection requires a trailing space after "---"/"+++" so that a
|
|
11
|
+
# payload line whose mutated source starts with "--" (emitted as "---var")
|
|
12
|
+
# or "++" (emitted as "+++var") is preserved rather than misclassified as
|
|
13
|
+
# a header.
|
|
14
|
+
class Evilution::Compare::DiffExtractor::Mutant
|
|
15
|
+
def call(diff)
|
|
16
|
+
minus = []
|
|
17
|
+
plus = []
|
|
18
|
+
diff.to_s.each_line do |line|
|
|
19
|
+
line = line.chomp
|
|
20
|
+
next if line.start_with?("--- ", "+++ ", "@@")
|
|
21
|
+
|
|
22
|
+
if line.start_with?("-")
|
|
23
|
+
minus << line[1..]
|
|
24
|
+
elsif line.start_with?("+")
|
|
25
|
+
plus << line[1..]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
{ minus: minus, plus: plus }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -3,80 +3,23 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "../compare"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
plus << line[2..]
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
{ minus: minus, plus: plus }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def extract_from_mutant_diff(diff)
|
|
24
|
-
minus = []
|
|
25
|
-
plus = []
|
|
26
|
-
diff.to_s.each_line do |line|
|
|
27
|
-
line = line.chomp
|
|
28
|
-
next if line.start_with?("---", "+++", "@@")
|
|
29
|
-
|
|
30
|
-
if line.start_with?("-")
|
|
31
|
-
minus << line[1..]
|
|
32
|
-
elsif line.start_with?("+")
|
|
33
|
-
plus << line[1..]
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
{ minus: minus, plus: plus }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
|
|
40
|
-
# heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
|
|
41
|
-
# inside them collapse. A mutation touching whitespace inside a regex may
|
|
42
|
-
# false-match across tools.
|
|
43
|
-
# rubocop:disable Metrics/PerceivedComplexity, Style/MultipleComparison
|
|
44
|
-
def normalize_line(line)
|
|
45
|
-
out = +""
|
|
46
|
-
i = 0
|
|
47
|
-
in_literal = nil
|
|
48
|
-
last_was_space = false
|
|
49
|
-
chars = line.chars
|
|
50
|
-
while i < chars.length
|
|
51
|
-
ch = chars[i]
|
|
52
|
-
if in_literal
|
|
53
|
-
out << ch
|
|
54
|
-
if ch == "\\" && i + 1 < chars.length
|
|
55
|
-
out << chars[i + 1]
|
|
56
|
-
i += 2
|
|
57
|
-
next
|
|
58
|
-
end
|
|
59
|
-
in_literal = nil if ch == in_literal
|
|
60
|
-
elsif ch == '"' || ch == "'"
|
|
61
|
-
in_literal = ch
|
|
62
|
-
out << ch
|
|
63
|
-
last_was_space = false
|
|
64
|
-
elsif ch == " " || ch == "\t"
|
|
65
|
-
out << " " unless last_was_space || out.empty?
|
|
66
|
-
last_was_space = true
|
|
67
|
-
else
|
|
68
|
-
out << ch
|
|
69
|
-
last_was_space = false
|
|
70
|
-
end
|
|
71
|
-
i += 1
|
|
72
|
-
end
|
|
73
|
-
out.rstrip
|
|
6
|
+
# Composes a stable SHA256 fingerprint from a mutation diff for cross-tool
|
|
7
|
+
# matching (Evilution vs Mutant). Orchestrates two collaborators along
|
|
8
|
+
# distinct change axes:
|
|
9
|
+
#
|
|
10
|
+
# - extractor: parses a tool-specific diff format into {minus:, plus:}
|
|
11
|
+
# - normalizer: collapses whitespace per line so cosmetic differences
|
|
12
|
+
# don't perturb the hash
|
|
13
|
+
class Evilution::Compare::Fingerprint
|
|
14
|
+
def initialize(extractor:, normalizer:)
|
|
15
|
+
@extractor = extractor
|
|
16
|
+
@normalizer = normalizer
|
|
74
17
|
end
|
|
75
|
-
# rubocop:enable Metrics/PerceivedComplexity, Style/MultipleComparison
|
|
76
18
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
19
|
+
def call(diff:, file_path:, line:)
|
|
20
|
+
body = @extractor.call(diff)
|
|
21
|
+
minus = body[:minus].map { |l| @normalizer.call(l) }
|
|
22
|
+
plus = body[:plus].map { |l| @normalizer.call(l) }
|
|
80
23
|
payload = [file_path, line.to_s, minus.join("\n"), plus.join("\n")].join("\x00")
|
|
81
24
|
Digest::SHA256.hexdigest(payload)
|
|
82
25
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../compare"
|
|
4
|
+
|
|
5
|
+
# Collapses whitespace runs in source-code text while preserving the contents
|
|
6
|
+
# of "..." and '...' string literals. Used for fingerprinting mutation diffs
|
|
7
|
+
# so that whitespace-only differences do not cause false fingerprint mismatches
|
|
8
|
+
# across tooling (evilution vs mutant).
|
|
9
|
+
#
|
|
10
|
+
# v1 limitation: only " and ' literals are preserved. Regex literals (/.../),
|
|
11
|
+
# heredocs, %w[], %q{} forms are treated as ordinary code — whitespace runs
|
|
12
|
+
# inside them collapse. A mutation touching whitespace inside a regex may
|
|
13
|
+
# false-match across tools.
|
|
14
|
+
class Evilution::Compare::LineNormalizer
|
|
15
|
+
QUOTES = ['"', "'"].freeze
|
|
16
|
+
WHITESPACE = [" ", "\t"].freeze
|
|
17
|
+
private_constant :QUOTES, :WHITESPACE
|
|
18
|
+
|
|
19
|
+
def call(line)
|
|
20
|
+
@chars = line.chars
|
|
21
|
+
@i = 0
|
|
22
|
+
@out = +""
|
|
23
|
+
@in_literal = nil
|
|
24
|
+
@last_was_space = false
|
|
25
|
+
|
|
26
|
+
@i += step while @i < @chars.length
|
|
27
|
+
result = @out.rstrip
|
|
28
|
+
@chars = nil
|
|
29
|
+
@out = nil
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def step
|
|
36
|
+
ch = @chars[@i]
|
|
37
|
+
return step_in_literal(ch) if @in_literal
|
|
38
|
+
return step_open_quote(ch) if QUOTES.include?(ch)
|
|
39
|
+
return step_whitespace if WHITESPACE.include?(ch)
|
|
40
|
+
|
|
41
|
+
append_regular(ch)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def step_in_literal(ch)
|
|
45
|
+
@out << ch
|
|
46
|
+
if ch == "\\" && @i + 1 < @chars.length
|
|
47
|
+
@out << @chars[@i + 1]
|
|
48
|
+
return 2
|
|
49
|
+
end
|
|
50
|
+
@in_literal = nil if ch == @in_literal
|
|
51
|
+
1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def step_open_quote(ch)
|
|
55
|
+
@in_literal = ch
|
|
56
|
+
@out << ch
|
|
57
|
+
@last_was_space = false
|
|
58
|
+
1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def step_whitespace
|
|
62
|
+
@out << " " unless @last_was_space || @out.empty?
|
|
63
|
+
@last_was_space = true
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def append_regular(ch)
|
|
68
|
+
@out << ch
|
|
69
|
+
@last_was_space = false
|
|
70
|
+
1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative "../compare"
|
|
4
4
|
require_relative "record"
|
|
5
5
|
require_relative "fingerprint"
|
|
6
|
+
require_relative "line_normalizer"
|
|
7
|
+
require_relative "diff_extractor/evilution"
|
|
8
|
+
require_relative "diff_extractor/mutant"
|
|
6
9
|
|
|
7
10
|
class Evilution::Compare::Normalizer
|
|
8
11
|
EVILUTION_BUCKETS = %w[killed survived timed_out errors neutral equivalent unresolved unparseable].freeze
|
|
@@ -17,6 +20,18 @@ class Evilution::Compare::Normalizer
|
|
|
17
20
|
"unparseable" => :unparseable
|
|
18
21
|
}.freeze
|
|
19
22
|
|
|
23
|
+
def initialize
|
|
24
|
+
line_normalizer = Evilution::Compare::LineNormalizer.new
|
|
25
|
+
@evilution_fingerprint = Evilution::Compare::Fingerprint.new(
|
|
26
|
+
extractor: Evilution::Compare::DiffExtractor::Evilution.new,
|
|
27
|
+
normalizer: line_normalizer
|
|
28
|
+
)
|
|
29
|
+
@mutant_fingerprint = Evilution::Compare::Fingerprint.new(
|
|
30
|
+
extractor: Evilution::Compare::DiffExtractor::Mutant.new,
|
|
31
|
+
normalizer: line_normalizer
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
def from_evilution(json)
|
|
21
36
|
records = []
|
|
22
37
|
EVILUTION_BUCKETS.each do |bucket|
|
|
@@ -42,24 +57,28 @@ class Evilution::Compare::Normalizer
|
|
|
42
57
|
private
|
|
43
58
|
|
|
44
59
|
def build_evilution_record(entry, index:)
|
|
45
|
-
file_path
|
|
46
|
-
line = entry["line"] or raise Evilution::Compare::InvalidInput.new("missing 'line' in record", index: index)
|
|
47
|
-
diff = entry["diff"].to_s
|
|
48
|
-
status = EVILUTION_STATUS_MAP[entry["status"]] ||
|
|
49
|
-
raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
|
|
50
|
-
body = Evilution::Compare::Fingerprint.extract_from_evilution_diff(diff)
|
|
60
|
+
file_path, line, diff, status = extract_evilution_fields(entry, index)
|
|
51
61
|
Evilution::Compare::Record.new(
|
|
52
62
|
source: :evilution,
|
|
53
63
|
file_path: file_path,
|
|
54
64
|
line: line,
|
|
55
65
|
status: status,
|
|
56
|
-
fingerprint:
|
|
66
|
+
fingerprint: @evilution_fingerprint.call(diff: diff, file_path: file_path, line: line),
|
|
57
67
|
operator: entry["operator"],
|
|
58
68
|
diff_body: diff,
|
|
59
69
|
raw: entry
|
|
60
70
|
)
|
|
61
71
|
end
|
|
62
72
|
|
|
73
|
+
def extract_evilution_fields(entry, index)
|
|
74
|
+
file_path = entry["file"] or raise Evilution::Compare::InvalidInput.new("missing 'file' in record", index: index)
|
|
75
|
+
line = entry["line"] or raise Evilution::Compare::InvalidInput.new("missing 'line' in record", index: index)
|
|
76
|
+
diff = entry["diff"].to_s
|
|
77
|
+
status = EVILUTION_STATUS_MAP[entry["status"]] ||
|
|
78
|
+
raise(Evilution::Compare::InvalidInput.new("unknown status #{entry["status"].inspect}", index: index))
|
|
79
|
+
[file_path, line, diff, status]
|
|
80
|
+
end
|
|
81
|
+
|
|
63
82
|
def build_mutant_record(cov, source_path:, index:)
|
|
64
83
|
mr = cov["mutation_result"] or raise Evilution::Compare::InvalidInput.new("missing mutation_result", index: index)
|
|
65
84
|
cr = cov["criteria_result"] or raise Evilution::Compare::InvalidInput.new("missing criteria_result", index: index)
|
|
@@ -67,13 +86,12 @@ class Evilution::Compare::Normalizer
|
|
|
67
86
|
line = parse_mutant_line(ident, index)
|
|
68
87
|
diff = mr["mutation_diff"].to_s
|
|
69
88
|
status = derive_mutant_status(mr, cr, index)
|
|
70
|
-
body = Evilution::Compare::Fingerprint.extract_from_mutant_diff(diff)
|
|
71
89
|
Evilution::Compare::Record.new(
|
|
72
90
|
source: :mutant,
|
|
73
91
|
file_path: source_path,
|
|
74
92
|
line: line,
|
|
75
93
|
status: status,
|
|
76
|
-
fingerprint:
|
|
94
|
+
fingerprint: @mutant_fingerprint.call(diff: diff, file_path: source_path, line: line),
|
|
77
95
|
operator: nil,
|
|
78
96
|
diff_body: diff,
|
|
79
97
|
raw: { "mutation_result" => mr, "criteria_result" => cr, "source_path" => source_path }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
class Evilution::Config::Validators::Profile < Evilution::Config::Validators::Base
|
|
6
|
+
ALLOWED = %i[default strict].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(value)
|
|
9
|
+
coerce_symbol!(value, allowed: ALLOWED, name: "profile")
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/evilution/config.rb
CHANGED
|
@@ -17,7 +17,8 @@ class Evilution::Config
|
|
|
17
17
|
spec_mappings: {}, spec_pattern: nil, example_targeting: true,
|
|
18
18
|
example_targeting_fallback: :full_file,
|
|
19
19
|
example_targeting_cache: { max_files: 50, max_blocks: 10_000 },
|
|
20
|
-
quiet_children: false, quiet_children_dir: "tmp/evilution_children"
|
|
20
|
+
quiet_children: false, quiet_children_dir: "tmp/evilution_children",
|
|
21
|
+
profile: :default
|
|
21
22
|
}.freeze
|
|
22
23
|
|
|
23
24
|
attr_reader :target_files, :timeout, :format,
|
|
@@ -28,7 +29,7 @@ class Evilution::Config
|
|
|
28
29
|
:skip_heredoc_literals, :related_specs_heuristic,
|
|
29
30
|
:fallback_to_full_suite, :preload, :spec_mappings, :spec_pattern,
|
|
30
31
|
:example_targeting, :example_targeting_fallback, :example_targeting_cache,
|
|
31
|
-
:spec_selector, :quiet_children, :quiet_children_dir
|
|
32
|
+
:spec_selector, :quiet_children, :quiet_children_dir, :profile
|
|
32
33
|
|
|
33
34
|
def initialize(**options)
|
|
34
35
|
skip_file = options.delete(:skip_config_file) ? true : false
|
|
@@ -183,6 +184,11 @@ class Evilution::Config
|
|
|
183
184
|
# ignore_patterns:
|
|
184
185
|
# - "call{name=info, receiver=call{name=logger}}"
|
|
185
186
|
# - "call{name=debug|warn}"
|
|
187
|
+
|
|
188
|
+
# Operator profile: default or strict (default: default).
|
|
189
|
+
# strict adds aggressive truthiness mutators (e.g. replaces
|
|
190
|
+
# `x.predicate?` with `nil`) intended for pre-merge audits.
|
|
191
|
+
# profile: default
|
|
186
192
|
YAML
|
|
187
193
|
end
|
|
188
194
|
|
|
@@ -200,40 +206,50 @@ class Evilution::Config
|
|
|
200
206
|
)
|
|
201
207
|
end
|
|
202
208
|
|
|
209
|
+
SIMPLE_ATTR_TRANSFORMS = {
|
|
210
|
+
target_files: ->(v) { Array(v) },
|
|
211
|
+
timeout: nil,
|
|
212
|
+
format: :to_sym.to_proc,
|
|
213
|
+
target: nil,
|
|
214
|
+
min_score: :to_f.to_proc,
|
|
215
|
+
verbose: nil,
|
|
216
|
+
quiet: nil,
|
|
217
|
+
baseline: nil,
|
|
218
|
+
incremental: nil,
|
|
219
|
+
suggest_tests: nil,
|
|
220
|
+
progress: nil,
|
|
221
|
+
save_session: nil,
|
|
222
|
+
line_ranges: ->(v) { v || {} },
|
|
223
|
+
spec_files: ->(v) { Array(v) },
|
|
224
|
+
show_disabled: nil,
|
|
225
|
+
baseline_session: nil,
|
|
226
|
+
skip_heredoc_literals: nil,
|
|
227
|
+
related_specs_heuristic: nil,
|
|
228
|
+
fallback_to_full_suite: nil,
|
|
229
|
+
quiet_children: nil,
|
|
230
|
+
quiet_children_dir: nil
|
|
231
|
+
}.freeze
|
|
232
|
+
private_constant :SIMPLE_ATTR_TRANSFORMS
|
|
233
|
+
|
|
203
234
|
def assign_simple_attributes(merged)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
@verbose = merged[:verbose]
|
|
210
|
-
@quiet = merged[:quiet]
|
|
211
|
-
@baseline = merged[:baseline]
|
|
212
|
-
@incremental = merged[:incremental]
|
|
213
|
-
@suggest_tests = merged[:suggest_tests]
|
|
214
|
-
@progress = merged[:progress]
|
|
215
|
-
@save_session = merged[:save_session]
|
|
216
|
-
@line_ranges = merged[:line_ranges] || {}
|
|
217
|
-
@spec_files = Array(merged[:spec_files])
|
|
218
|
-
@show_disabled = merged[:show_disabled]
|
|
219
|
-
@baseline_session = merged[:baseline_session]
|
|
220
|
-
@skip_heredoc_literals = merged[:skip_heredoc_literals]
|
|
221
|
-
@related_specs_heuristic = merged[:related_specs_heuristic]
|
|
222
|
-
@fallback_to_full_suite = merged[:fallback_to_full_suite]
|
|
223
|
-
@quiet_children = merged[:quiet_children]
|
|
224
|
-
@quiet_children_dir = merged[:quiet_children_dir]
|
|
235
|
+
SIMPLE_ATTR_TRANSFORMS.each do |key, transform|
|
|
236
|
+
value = merged[key]
|
|
237
|
+
value = transform.call(value) if transform
|
|
238
|
+
instance_variable_set(:"@#{key}", value)
|
|
239
|
+
end
|
|
225
240
|
end
|
|
226
241
|
|
|
242
|
+
VALIDATED_ATTRS = %i[
|
|
243
|
+
integration jobs fail_fast isolation ignore_patterns
|
|
244
|
+
hooks preload spec_mappings spec_pattern profile
|
|
245
|
+
].freeze
|
|
246
|
+
private_constant :VALIDATED_ATTRS
|
|
247
|
+
|
|
227
248
|
def assign_validated_attributes(merged)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
@ignore_patterns = Validators::IgnorePatterns.call(merged[:ignore_patterns])
|
|
233
|
-
@hooks = Validators::Hooks.call(merged[:hooks])
|
|
234
|
-
@preload = Validators::Preload.call(merged[:preload])
|
|
235
|
-
@spec_mappings = Validators::SpecMappings.call(merged[:spec_mappings])
|
|
236
|
-
@spec_pattern = Validators::SpecPattern.call(merged[:spec_pattern])
|
|
249
|
+
VALIDATED_ATTRS.each do |key|
|
|
250
|
+
validator = Validators.const_get(key.to_s.split("_").map(&:capitalize).join)
|
|
251
|
+
instance_variable_set(:"@#{key}", validator.call(merged[key]))
|
|
252
|
+
end
|
|
237
253
|
end
|
|
238
254
|
|
|
239
255
|
def assign_example_targeting(merged)
|
|
@@ -259,6 +275,7 @@ require_relative "config/validators/spec_pattern"
|
|
|
259
275
|
require_relative "config/validators/spec_mappings"
|
|
260
276
|
require_relative "config/validators/example_targeting_fallback"
|
|
261
277
|
require_relative "config/validators/example_targeting_cache"
|
|
278
|
+
require_relative "config/validators/profile"
|
|
262
279
|
require_relative "config/builders"
|
|
263
280
|
require_relative "config/builders/spec_resolver"
|
|
264
281
|
require_relative "config/builders/spec_selector"
|
|
@@ -20,21 +20,30 @@ class Evilution::DisableComment
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
def classify_comments(parse_result, source)
|
|
23
|
-
parse_result.comments.filter_map
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
23
|
+
parse_result.comments.filter_map { |comment| classify_comment(comment, source) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def classify_comment(comment, source)
|
|
27
|
+
loc = comment.location
|
|
28
|
+
text = comment_text(loc, source)
|
|
29
|
+
|
|
30
|
+
if text.match?(DISABLE_MARKER)
|
|
31
|
+
disable_entry(loc, text, source)
|
|
32
|
+
elsif text.match?(ENABLE_MARKER)
|
|
33
|
+
{ type: :enable, line: loc.start_line }
|
|
35
34
|
end
|
|
36
35
|
end
|
|
37
36
|
|
|
37
|
+
def comment_text(loc, source)
|
|
38
|
+
source.byteslice(loc.start_offset, loc.end_offset - loc.start_offset)
|
|
39
|
+
.force_encoding(source.encoding)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def disable_entry(loc, text, source)
|
|
43
|
+
standalone = source.lines[loc.start_line - 1].strip == text.strip
|
|
44
|
+
{ type: :disable, line: loc.start_line, standalone: standalone }
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
def scan_comments(comments, method_ranges, total_lines)
|
|
39
48
|
disabled = []
|
|
40
49
|
range_start = nil
|
|
@@ -26,11 +26,11 @@ class Evilution::Integration::CrashDetector
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def assertion_failure?
|
|
30
30
|
@assertion_failures.positive?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def
|
|
33
|
+
def crashed?
|
|
34
34
|
@crashes.any?
|
|
35
35
|
end
|
|
36
36
|
|