evilution 0.23.0 → 0.25.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 +210 -0
- data/CHANGELOG.md +51 -0
- data/README.md +81 -4
- data/exe/evil +6 -0
- data/lib/evilution/ast/source_surgeon.rb +15 -1
- data/lib/evilution/cli/commands/compare.rb +68 -0
- data/lib/evilution/cli/parser/command_extractor.rb +78 -0
- data/lib/evilution/cli/parser/file_args.rb +41 -0
- data/lib/evilution/cli/parser/options_builder.rb +123 -0
- data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
- data/lib/evilution/cli/parser.rb +27 -196
- data/lib/evilution/cli/printers/compare.rb +159 -0
- data/lib/evilution/cli.rb +1 -0
- data/lib/evilution/compare/categorizer.rb +109 -0
- data/lib/evilution/compare/detector.rb +21 -0
- data/lib/evilution/compare/fingerprint.rb +83 -0
- data/lib/evilution/compare/normalizer.rb +106 -0
- data/lib/evilution/compare/record.rb +16 -0
- data/lib/evilution/compare.rb +15 -0
- data/lib/evilution/config.rb +178 -3
- data/lib/evilution/example_filter.rb +143 -0
- data/lib/evilution/integration/base.rb +11 -57
- data/lib/evilution/integration/crash_detector.rb +5 -2
- data/lib/evilution/integration/minitest.rb +25 -7
- data/lib/evilution/integration/minitest_crash_detector.rb +5 -2
- data/lib/evilution/integration/rspec.rb +99 -12
- data/lib/evilution/isolation/fork.rb +26 -0
- data/lib/evilution/isolation/in_process.rb +1 -0
- data/lib/evilution/mcp/info_tool.rb +77 -5
- data/lib/evilution/mcp/mutate_tool/config_builder.rb +20 -0
- data/lib/evilution/mcp/mutate_tool/error_payload.rb +17 -0
- data/lib/evilution/mcp/mutate_tool/option_parser.rb +54 -0
- data/lib/evilution/mcp/mutate_tool/progress_streamer.rb +37 -0
- data/lib/evilution/mcp/mutate_tool/report_trimmer.rb +31 -0
- data/lib/evilution/mcp/mutate_tool/survived_enricher.rb +52 -0
- data/lib/evilution/mcp/mutate_tool.rb +34 -186
- data/lib/evilution/mutation.rb +43 -3
- data/lib/evilution/mutator/base.rb +39 -1
- data/lib/evilution/mutator/operator/argument_nil_substitution.rb +5 -1
- data/lib/evilution/mutator/operator/argument_removal.rb +5 -1
- data/lib/evilution/parallel/work_queue.rb +149 -31
- data/lib/evilution/parallel_db_warning.rb +68 -0
- data/lib/evilution/reporter/cli.rb +38 -11
- data/lib/evilution/reporter/html/assets/style.css +85 -0
- data/lib/evilution/reporter/html/baseline_keys.rb +28 -0
- data/lib/evilution/reporter/html/diff_formatter.rb +27 -0
- data/lib/evilution/reporter/html/escape.rb +12 -0
- data/lib/evilution/reporter/html/namespace.rb +11 -0
- data/lib/evilution/reporter/html/report.rb +68 -0
- data/lib/evilution/reporter/html/section.rb +21 -0
- data/lib/evilution/reporter/html/sections/baseline_comparison.rb +46 -0
- data/lib/evilution/reporter/html/sections/error_details.rb +30 -0
- data/lib/evilution/reporter/html/sections/error_entry.rb +22 -0
- data/lib/evilution/reporter/html/sections/file_section.rb +62 -0
- data/lib/evilution/reporter/html/sections/header.rb +29 -0
- data/lib/evilution/reporter/html/sections/mutation_map.rb +32 -0
- data/lib/evilution/reporter/html/sections/neutral_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/summary_cards.rb +11 -0
- data/lib/evilution/reporter/html/sections/survived_details.rb +35 -0
- data/lib/evilution/reporter/html/sections/survived_entry.rb +36 -0
- data/lib/evilution/reporter/html/sections/truncation_notice.rb +17 -0
- data/lib/evilution/reporter/html/sections/unparseable_details.rb +25 -0
- data/lib/evilution/reporter/html/sections/unresolved_details.rb +25 -0
- data/lib/evilution/reporter/html/sections.rb +4 -0
- data/lib/evilution/reporter/html/stylesheet.rb +14 -0
- data/lib/evilution/reporter/html/templates/baseline_comparison.html.erb +8 -0
- data/lib/evilution/reporter/html/templates/error_details.html.erb +6 -0
- data/lib/evilution/reporter/html/templates/error_entry.html.erb +10 -0
- data/lib/evilution/reporter/html/templates/file_section.html.erb +12 -0
- data/lib/evilution/reporter/html/templates/header.html.erb +4 -0
- data/lib/evilution/reporter/html/templates/mutation_map.html.erb +6 -0
- data/lib/evilution/reporter/html/templates/neutral_details.html.erb +14 -0
- data/lib/evilution/reporter/html/templates/report.html.erb +17 -0
- data/lib/evilution/reporter/html/templates/summary_cards.html.erb +26 -0
- data/lib/evilution/reporter/html/templates/survived_details.html.erb +21 -0
- data/lib/evilution/reporter/html/templates/survived_entry.html.erb +8 -0
- data/lib/evilution/reporter/html/templates/truncation_notice.html.erb +1 -0
- data/lib/evilution/reporter/html/templates/unparseable_details.html.erb +11 -0
- data/lib/evilution/reporter/html/templates/unresolved_details.html.erb +11 -0
- data/lib/evilution/reporter/html.rb +11 -390
- data/lib/evilution/reporter/json.rb +19 -9
- data/lib/evilution/reporter/suggestion/diff_helpers.rb +28 -0
- data/lib/evilution/reporter/suggestion/registry.rb +64 -0
- data/lib/evilution/reporter/suggestion/templates/generic.rb +55 -0
- data/lib/evilution/reporter/suggestion/templates/minitest.rb +659 -0
- data/lib/evilution/reporter/suggestion/templates/rspec.rb +613 -0
- data/lib/evilution/reporter/suggestion.rb +8 -1327
- data/lib/evilution/result/mutation_result.rb +9 -1
- data/lib/evilution/result/summary.rb +21 -1
- data/lib/evilution/runner/baseline_runner.rb +92 -0
- data/lib/evilution/runner/diagnostics.rb +105 -0
- data/lib/evilution/runner/isolation_resolver.rb +134 -0
- data/lib/evilution/runner/mutation_executor.rb +325 -0
- data/lib/evilution/runner/mutation_planner.rb +126 -0
- data/lib/evilution/runner/report_publisher.rb +60 -0
- data/lib/evilution/runner/subject_pipeline.rb +121 -0
- data/lib/evilution/runner.rb +61 -692
- data/lib/evilution/source_ast_cache.rb +39 -0
- data/lib/evilution/spec_ast_cache.rb +166 -0
- data/lib/evilution/spec_resolver.rb +6 -1
- data/lib/evilution/spec_selector.rb +39 -0
- data/lib/evilution/temp_dir_tracker.rb +23 -3
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +7 -5
- metadata +75 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../mutate_tool"
|
|
4
|
+
require_relative "../../runner"
|
|
5
|
+
require_relative "../../spec_resolver"
|
|
6
|
+
|
|
7
|
+
module Evilution::MCP::MutateTool::SurvivedEnricher
|
|
8
|
+
def self.call(data, survived_results, config)
|
|
9
|
+
entries = data["survived"]
|
|
10
|
+
return unless entries.is_a?(Array)
|
|
11
|
+
|
|
12
|
+
explicit_spec = explicit_spec_override(config)
|
|
13
|
+
resolver = explicit_spec ? nil : resolver_for_integration(config.integration)
|
|
14
|
+
cache = {}
|
|
15
|
+
|
|
16
|
+
entries.each_with_index do |entry, index|
|
|
17
|
+
result = survived_results[index]
|
|
18
|
+
next unless result
|
|
19
|
+
|
|
20
|
+
mutation = result.mutation
|
|
21
|
+
entry["subject"] = mutation.subject.name
|
|
22
|
+
spec_file = explicit_spec || cache.fetch(mutation.file_path) do
|
|
23
|
+
cache[mutation.file_path] = resolver.call(mutation.file_path)
|
|
24
|
+
end
|
|
25
|
+
entry["spec_file"] = spec_file if spec_file
|
|
26
|
+
entry["next_step"] = build_next_step(mutation, spec_file)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.explicit_spec_override(config)
|
|
31
|
+
return nil unless config.respond_to?(:spec_files)
|
|
32
|
+
|
|
33
|
+
files = Array(config.spec_files).compact.map(&:to_s).reject(&:empty?)
|
|
34
|
+
files.first
|
|
35
|
+
end
|
|
36
|
+
private_class_method :explicit_spec_override
|
|
37
|
+
|
|
38
|
+
def self.resolver_for_integration(integration)
|
|
39
|
+
integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
|
|
40
|
+
return Evilution::SpecResolver.new unless integration_class
|
|
41
|
+
|
|
42
|
+
integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
|
|
43
|
+
end
|
|
44
|
+
private_class_method :resolver_for_integration
|
|
45
|
+
|
|
46
|
+
def self.build_next_step(mutation, spec_file)
|
|
47
|
+
target = spec_file || "the covering test file"
|
|
48
|
+
"Add a test in #{target} that fails against this mutation at #{mutation.file_path}:#{mutation.line} " \
|
|
49
|
+
"(#{mutation.subject.name}, #{mutation.operator_name})."
|
|
50
|
+
end
|
|
51
|
+
private_class_method :build_next_step
|
|
52
|
+
end
|
|
@@ -5,7 +5,6 @@ require "mcp"
|
|
|
5
5
|
require_relative "../config"
|
|
6
6
|
require_relative "../runner"
|
|
7
7
|
require_relative "../reporter/json"
|
|
8
|
-
require_relative "../reporter/suggestion"
|
|
9
8
|
require_relative "../spec_resolver"
|
|
10
9
|
|
|
11
10
|
require_relative "../mcp"
|
|
@@ -104,194 +103,43 @@ class Evilution::MCP::MutateTool < MCP::Tool
|
|
|
104
103
|
|
|
105
104
|
class << self
|
|
106
105
|
def call(server_context:, files: [], verbosity: nil, **opts)
|
|
107
|
-
|
|
108
|
-
parsed_files, line_ranges = parse_files(Array(files))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
Evilution::MCP::MutateTool::OptionParser.validate!(opts)
|
|
107
|
+
parsed_files, line_ranges = Evilution::MCP::MutateTool::OptionParser.parse_files(Array(files))
|
|
108
|
+
config = Evilution::MCP::MutateTool::ConfigBuilder.build(
|
|
109
|
+
files: parsed_files,
|
|
110
|
+
line_ranges: line_ranges,
|
|
111
|
+
params: opts
|
|
112
|
+
)
|
|
113
|
+
on_result = Evilution::MCP::MutateTool::ProgressStreamer.build(
|
|
114
|
+
server_context: server_context,
|
|
115
|
+
suggest_tests: opts[:suggest_tests],
|
|
116
|
+
integration: config.integration
|
|
117
|
+
)
|
|
118
|
+
summary = Evilution::Runner.new(config: config, on_result: on_result).call
|
|
119
|
+
report = Evilution::Reporter::JSON.new(
|
|
120
|
+
suggest_tests: opts[:suggest_tests] == true,
|
|
121
|
+
integration: config.integration
|
|
122
|
+
).call(summary)
|
|
123
|
+
normalized_verbosity = Evilution::MCP::MutateTool::OptionParser.normalize_verbosity(verbosity)
|
|
124
|
+
compact = Evilution::MCP::MutateTool::ReportTrimmer.call(
|
|
125
|
+
report,
|
|
126
|
+
verbosity: normalized_verbosity,
|
|
127
|
+
survived_results: summary.survived_results,
|
|
128
|
+
config: config,
|
|
129
|
+
enricher: Evilution::MCP::MutateTool::SurvivedEnricher
|
|
130
|
+
)
|
|
117
131
|
|
|
118
132
|
::MCP::Tool::Response.new([{ type: "text", text: compact }])
|
|
119
133
|
rescue Evilution::Error => e
|
|
120
|
-
|
|
121
|
-
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
VALID_VERBOSITIES = %w[full summary minimal].freeze
|
|
125
|
-
PASSTHROUGH_KEYS = %i[target timeout jobs fail_fast suggest_tests incremental integration
|
|
126
|
-
isolation baseline save_session].freeze
|
|
127
|
-
ALLOWED_OPT_KEYS = (PASSTHROUGH_KEYS + %i[spec skip_config]).freeze
|
|
128
|
-
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
def parse_files(raw_files)
|
|
132
|
-
files = []
|
|
133
|
-
ranges = {}
|
|
134
|
-
|
|
135
|
-
raw_files.each do |arg|
|
|
136
|
-
file, range_str = arg.split(":", 2)
|
|
137
|
-
files << file
|
|
138
|
-
next unless range_str
|
|
139
|
-
|
|
140
|
-
ranges[file] = parse_line_range(range_str)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
[files, ranges]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def parse_line_range(str)
|
|
147
|
-
if str.include?("-")
|
|
148
|
-
start_str, end_str = str.split("-", 2)
|
|
149
|
-
start_line = Integer(start_str)
|
|
150
|
-
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
151
|
-
start_line..end_line
|
|
152
|
-
else
|
|
153
|
-
line = Integer(str)
|
|
154
|
-
line..line
|
|
155
|
-
end
|
|
156
|
-
rescue ArgumentError, TypeError
|
|
157
|
-
raise Evilution::ParseError, "invalid line range: #{str.inspect}"
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def validate_opts!(opts)
|
|
161
|
-
unknown = opts.keys - ALLOWED_OPT_KEYS
|
|
162
|
-
return if unknown.empty?
|
|
163
|
-
|
|
164
|
-
raise Evilution::ParseError, "unknown parameters: #{unknown.join(", ")}"
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def build_config_opts(files, line_ranges, params)
|
|
168
|
-
# Preload is disabled for MCP invocations: `require`-ing Rails into the
|
|
169
|
-
# long-lived MCP server would poison subsequent runs against other
|
|
170
|
-
# projects. MCP users who want the speedup should use the CLI.
|
|
171
|
-
opts = { target_files: files, line_ranges: line_ranges, format: :json, quiet: true, preload: false }
|
|
172
|
-
opts[:skip_config_file] = true if params[:skip_config]
|
|
173
|
-
opts[:spec_files] = params[:spec] if params[:spec]
|
|
174
|
-
PASSTHROUGH_KEYS.each { |key| opts[key] = params[key] unless params[key].nil? }
|
|
175
|
-
opts
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def normalize_verbosity(value)
|
|
179
|
-
normalized = value.to_s.strip.downcase
|
|
180
|
-
normalized = "summary" if normalized.empty?
|
|
181
|
-
return normalized if VALID_VERBOSITIES.include?(normalized)
|
|
182
|
-
|
|
183
|
-
raise Evilution::ParseError, "invalid verbosity: #{value.inspect} (must be full, summary, or minimal)"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def trim_report(json_string, verbosity, survived_results, config)
|
|
187
|
-
data = ::JSON.parse(json_string)
|
|
188
|
-
case verbosity
|
|
189
|
-
when "full"
|
|
190
|
-
strip_diffs(data, "killed")
|
|
191
|
-
strip_diffs(data, "neutral")
|
|
192
|
-
strip_diffs(data, "equivalent")
|
|
193
|
-
when "summary"
|
|
194
|
-
data.delete("killed")
|
|
195
|
-
data.delete("neutral")
|
|
196
|
-
data.delete("equivalent")
|
|
197
|
-
when "minimal"
|
|
198
|
-
data.delete("killed")
|
|
199
|
-
data.delete("neutral")
|
|
200
|
-
data.delete("equivalent")
|
|
201
|
-
data.delete("timed_out")
|
|
202
|
-
data.delete("errors")
|
|
203
|
-
end
|
|
204
|
-
enrich_survived(data, survived_results, config)
|
|
205
|
-
::JSON.generate(data)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def enrich_survived(data, survived_results, config)
|
|
209
|
-
entries = data["survived"]
|
|
210
|
-
return unless entries.is_a?(Array)
|
|
211
|
-
|
|
212
|
-
explicit_spec = explicit_spec_override(config)
|
|
213
|
-
resolver = explicit_spec ? nil : resolver_for_integration(config.integration)
|
|
214
|
-
cache = {}
|
|
215
|
-
|
|
216
|
-
entries.each_with_index do |entry, index|
|
|
217
|
-
result = survived_results[index]
|
|
218
|
-
next unless result
|
|
219
|
-
|
|
220
|
-
mutation = result.mutation
|
|
221
|
-
entry["subject"] = mutation.subject.name
|
|
222
|
-
spec_file = explicit_spec || cache.fetch(mutation.file_path) do
|
|
223
|
-
cache[mutation.file_path] = resolver.call(mutation.file_path)
|
|
224
|
-
end
|
|
225
|
-
entry["spec_file"] = spec_file if spec_file
|
|
226
|
-
entry["next_step"] = build_next_step(mutation, spec_file)
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def explicit_spec_override(config)
|
|
231
|
-
return nil unless config.respond_to?(:spec_files)
|
|
232
|
-
|
|
233
|
-
files = Array(config.spec_files).compact.map(&:to_s).reject(&:empty?)
|
|
234
|
-
files.first
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def resolver_for_integration(integration)
|
|
238
|
-
integration_class = Evilution::Runner::INTEGRATIONS[integration.to_sym]
|
|
239
|
-
return Evilution::SpecResolver.new unless integration_class
|
|
240
|
-
|
|
241
|
-
integration_class.baseline_options[:spec_resolver] || Evilution::SpecResolver.new
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def build_next_step(mutation, spec_file)
|
|
245
|
-
target = spec_file || "the covering test file"
|
|
246
|
-
"Add a test in #{target} that fails against this mutation at #{mutation.file_path}:#{mutation.line} " \
|
|
247
|
-
"(#{mutation.subject.name}, #{mutation.operator_name})."
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def strip_diffs(data, key)
|
|
251
|
-
return unless data[key]
|
|
252
|
-
|
|
253
|
-
data[key].each { |entry| entry.delete("diff") }
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def build_streaming_callback(server_context, suggest_tests, integration)
|
|
257
|
-
return nil unless suggest_tests && server_context.respond_to?(:report_progress)
|
|
258
|
-
|
|
259
|
-
suggestion = Evilution::Reporter::Suggestion.new(suggest_tests: true, integration: integration)
|
|
260
|
-
survivor_index = 0
|
|
261
|
-
|
|
262
|
-
proc do |result|
|
|
263
|
-
next unless result.survived?
|
|
264
|
-
|
|
265
|
-
begin
|
|
266
|
-
survivor_index += 1
|
|
267
|
-
detail = build_suggestion_detail(result.mutation, suggestion)
|
|
268
|
-
server_context.report_progress(survivor_index, message: ::JSON.generate(detail))
|
|
269
|
-
rescue StandardError # rubocop:disable Lint/SuppressedException
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def build_suggestion_detail(mutation, suggestion)
|
|
275
|
-
{
|
|
276
|
-
operator: mutation.operator_name,
|
|
277
|
-
file: mutation.file_path,
|
|
278
|
-
line: mutation.line,
|
|
279
|
-
subject: mutation.subject.name,
|
|
280
|
-
diff: mutation.diff,
|
|
281
|
-
suggestion: suggestion.suggestion_for(mutation)
|
|
282
|
-
}
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def build_error_payload(error)
|
|
286
|
-
error_type = case error
|
|
287
|
-
when Evilution::ConfigError then "config_error"
|
|
288
|
-
when Evilution::ParseError then "parse_error"
|
|
289
|
-
else "runtime_error"
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
payload = { type: error_type, message: error.message }
|
|
293
|
-
payload[:file] = error.file if error.file
|
|
294
|
-
{ error: payload }
|
|
134
|
+
payload = Evilution::MCP::MutateTool::ErrorPayload.build(e)
|
|
135
|
+
::MCP::Tool::Response.new([{ type: "text", text: ::JSON.generate(payload) }], error: true)
|
|
295
136
|
end
|
|
296
137
|
end
|
|
297
138
|
end
|
|
139
|
+
|
|
140
|
+
require_relative "mutate_tool/error_payload"
|
|
141
|
+
require_relative "mutate_tool/option_parser"
|
|
142
|
+
require_relative "mutate_tool/config_builder"
|
|
143
|
+
require_relative "mutate_tool/report_trimmer"
|
|
144
|
+
require_relative "mutate_tool/survived_enricher"
|
|
145
|
+
require_relative "mutate_tool/progress_streamer"
|
data/lib/evilution/mutation.rb
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "diff/lcs"
|
|
4
|
-
require "diff/lcs/hunk"
|
|
5
4
|
|
|
6
5
|
class Evilution::Mutation
|
|
7
6
|
attr_reader :subject, :operator_name, :original_source,
|
|
8
|
-
:mutated_source, :
|
|
7
|
+
:mutated_source, :original_slice, :mutated_slice,
|
|
8
|
+
:file_path, :line, :column, :parse_status
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# rubocop:disable Metrics/ParameterLists
|
|
11
|
+
def initialize(subject:, operator_name:, original_source:, mutated_source:,
|
|
12
|
+
file_path:, line:, column: 0, original_slice: nil, mutated_slice: nil,
|
|
13
|
+
parse_status: :ok)
|
|
14
|
+
# rubocop:enable Metrics/ParameterLists
|
|
11
15
|
@subject = subject
|
|
12
16
|
@operator_name = operator_name
|
|
13
17
|
@original_source = original_source
|
|
14
18
|
@mutated_source = mutated_source
|
|
19
|
+
@original_slice = original_slice
|
|
20
|
+
@mutated_slice = mutated_slice
|
|
15
21
|
@file_path = file_path
|
|
16
22
|
@line = line
|
|
17
23
|
@column = column
|
|
24
|
+
@parse_status = parse_status
|
|
18
25
|
@diff = nil
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
def unparseable?
|
|
29
|
+
@parse_status == :unparseable
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
def diff
|
|
22
33
|
@diff ||= compute_diff
|
|
23
34
|
end
|
|
24
35
|
|
|
36
|
+
def unified_diff
|
|
37
|
+
return @unified_diff if defined?(@unified_diff)
|
|
38
|
+
|
|
39
|
+
@unified_diff = compute_unified_diff
|
|
40
|
+
end
|
|
41
|
+
|
|
25
42
|
def strip_sources!
|
|
26
43
|
diff # ensure diff is cached before clearing sources
|
|
27
44
|
@original_source = nil
|
|
@@ -49,6 +66,29 @@ class Evilution::Mutation
|
|
|
49
66
|
result.join("\n")
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
def compute_unified_diff
|
|
70
|
+
return nil if @original_slice.nil? || @mutated_slice.nil?
|
|
71
|
+
|
|
72
|
+
original_lines = @original_slice.lines
|
|
73
|
+
mutated_lines = @mutated_slice.lines
|
|
74
|
+
body = ::Diff::LCS.sdiff(original_lines, mutated_lines).map { |c| format_sdiff_change(c) }.join("\n")
|
|
75
|
+
[
|
|
76
|
+
"--- a/#{file_path}",
|
|
77
|
+
"+++ b/#{file_path}",
|
|
78
|
+
"@@ -#{line},#{original_lines.length} +#{line},#{mutated_lines.length} @@",
|
|
79
|
+
body
|
|
80
|
+
].reject(&:empty?).join("\n")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_sdiff_change(change)
|
|
84
|
+
case change.action
|
|
85
|
+
when "=" then " #{change.old_element.chomp}"
|
|
86
|
+
when "-" then "-#{change.old_element.chomp}"
|
|
87
|
+
when "+" then "+#{change.new_element.chomp}"
|
|
88
|
+
when "!" then "-#{change.old_element.chomp}\n+#{change.new_element.chomp}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
52
92
|
public
|
|
53
93
|
|
|
54
94
|
def to_s
|
|
@@ -27,24 +27,62 @@ class Evilution::Mutator::Base < Prism::Visitor
|
|
|
27
27
|
def add_mutation(offset:, length:, replacement:, node:)
|
|
28
28
|
return if @filter && @filter.skip?(node)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
surgery = Evilution::AST::SourceSurgeon.apply(
|
|
31
31
|
@file_source,
|
|
32
32
|
offset: offset,
|
|
33
33
|
length: length,
|
|
34
34
|
replacement: replacement
|
|
35
35
|
)
|
|
36
|
+
mutated_source = surgery.source
|
|
37
|
+
|
|
38
|
+
original_slice, mutated_slice = slice_affected_lines(
|
|
39
|
+
mutated_source: mutated_source,
|
|
40
|
+
offset: offset,
|
|
41
|
+
length: length,
|
|
42
|
+
replacement_bytesize: replacement.bytesize
|
|
43
|
+
)
|
|
36
44
|
|
|
37
45
|
@mutations << Evilution::Mutation.new(
|
|
38
46
|
subject: @subject,
|
|
39
47
|
operator_name: self.class.operator_name,
|
|
40
48
|
original_source: @file_source,
|
|
41
49
|
mutated_source: mutated_source,
|
|
50
|
+
original_slice: original_slice,
|
|
51
|
+
mutated_slice: mutated_slice,
|
|
52
|
+
parse_status: surgery.status,
|
|
42
53
|
file_path: @subject.file_path,
|
|
43
54
|
line: node.location.start_line,
|
|
44
55
|
column: node.location.start_column
|
|
45
56
|
)
|
|
46
57
|
end
|
|
47
58
|
|
|
59
|
+
NEWLINE_BYTE = 10
|
|
60
|
+
private_constant :NEWLINE_BYTE
|
|
61
|
+
|
|
62
|
+
def slice_affected_lines(mutated_source:, offset:, length:, replacement_bytesize:)
|
|
63
|
+
line_start = line_start_byte(@file_source, offset)
|
|
64
|
+
orig_line_end = line_end_byte(@file_source, [offset + length - 1, line_start].max)
|
|
65
|
+
mut_line_end = line_end_byte(mutated_source, [offset + replacement_bytesize - 1, line_start].max)
|
|
66
|
+
|
|
67
|
+
[
|
|
68
|
+
@file_source.byteslice(line_start, orig_line_end - line_start),
|
|
69
|
+
mutated_source.byteslice(line_start, mut_line_end - line_start)
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def line_start_byte(source, offset)
|
|
74
|
+
i = offset - 1
|
|
75
|
+
i -= 1 while i >= 0 && source.getbyte(i) != NEWLINE_BYTE
|
|
76
|
+
i + 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def line_end_byte(source, from)
|
|
80
|
+
limit = source.bytesize
|
|
81
|
+
i = from
|
|
82
|
+
i += 1 while i < limit && source.getbyte(i) != NEWLINE_BYTE
|
|
83
|
+
i < limit ? i + 1 : limit
|
|
84
|
+
end
|
|
85
|
+
|
|
48
86
|
def byteslice_source(offset, length)
|
|
49
87
|
@file_source.byteslice(offset, length).force_encoding(@file_source.encoding)
|
|
50
88
|
end
|
|
@@ -13,7 +13,7 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
|
|
|
13
13
|
def visit_call_node(node)
|
|
14
14
|
args = node.arguments&.arguments
|
|
15
15
|
|
|
16
|
-
if
|
|
16
|
+
if mutable?(node, args)
|
|
17
17
|
args.each_index do |i|
|
|
18
18
|
parts = args.each_with_index.map { |a, j| j == i ? "nil" : a.slice }
|
|
19
19
|
replacement = parts.join(", ")
|
|
@@ -32,6 +32,10 @@ class Evilution::Mutator::Operator::ArgumentNilSubstitution < Evilution::Mutator
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
def mutable?(node, args)
|
|
36
|
+
args && args.length >= 1 && positional_only?(args) && node.name != :[]=
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
def positional_only?(args)
|
|
36
40
|
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
37
41
|
end
|
|
@@ -13,7 +13,7 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
13
13
|
def visit_call_node(node)
|
|
14
14
|
args = node.arguments&.arguments
|
|
15
15
|
|
|
16
|
-
if
|
|
16
|
+
if mutable?(node, args)
|
|
17
17
|
args.each_index do |i|
|
|
18
18
|
remaining = args.each_with_index.filter_map { |a, j| a.slice if j != i }
|
|
19
19
|
replacement = remaining.join(", ")
|
|
@@ -32,6 +32,10 @@ class Evilution::Mutator::Operator::ArgumentRemoval < Evilution::Mutator::Base
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
def mutable?(node, args)
|
|
36
|
+
args && args.length >= 2 && positional_only?(args) && node.name != :[]=
|
|
37
|
+
end
|
|
38
|
+
|
|
35
39
|
def positional_only?(args)
|
|
36
40
|
args.none? { |arg| SKIP_TYPES.any? { |type| arg.is_a?(type) } }
|
|
37
41
|
end
|