evilution 0.22.7 → 0.24.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 +8 -0
- data/CHANGELOG.md +28 -0
- data/README.md +37 -7
- data/lib/evilution/cli/command.rb +37 -0
- data/lib/evilution/cli/commands/environment_show.rb +20 -0
- data/lib/evilution/cli/commands/init.rb +24 -0
- data/lib/evilution/cli/commands/mcp.rb +19 -0
- data/lib/evilution/cli/commands/run.rb +68 -0
- data/lib/evilution/cli/commands/session_diff.rb +30 -0
- data/lib/evilution/cli/commands/session_gc.rb +46 -0
- data/lib/evilution/cli/commands/session_list.rb +51 -0
- data/lib/evilution/cli/commands/session_show.rb +27 -0
- data/lib/evilution/cli/commands/subjects.rb +50 -0
- data/lib/evilution/cli/commands/tests_list.rb +43 -0
- data/lib/evilution/cli/commands/util_mutation.rb +66 -0
- data/lib/evilution/cli/commands/version.rb +17 -0
- data/lib/evilution/cli/commands.rb +4 -0
- data/lib/evilution/cli/dispatcher.rb +23 -0
- data/lib/evilution/cli/parsed_args.rb +12 -0
- data/lib/evilution/cli/parser/command_extractor.rb +77 -0
- data/lib/evilution/cli/parser/file_args.rb +41 -0
- data/lib/evilution/cli/parser/options_builder.rb +103 -0
- data/lib/evilution/cli/parser/stdin_reader.rb +28 -0
- data/lib/evilution/cli/parser.rb +88 -0
- data/lib/evilution/cli/printers/environment.rb +53 -0
- data/lib/evilution/cli/printers/session_detail.rb +76 -0
- data/lib/evilution/cli/printers/session_diff.rb +57 -0
- data/lib/evilution/cli/printers/session_list.rb +48 -0
- data/lib/evilution/cli/printers/subjects.rb +35 -0
- data/lib/evilution/cli/printers/tests_list.rb +45 -0
- data/lib/evilution/cli/printers/util_mutation.rb +35 -0
- data/lib/evilution/cli/printers.rb +4 -0
- data/lib/evilution/cli/result.rb +9 -0
- data/lib/evilution/cli.rb +30 -850
- data/lib/evilution/config.rb +31 -3
- data/lib/evilution/integration/base.rb +23 -55
- data/lib/evilution/integration/minitest.rb +22 -4
- data/lib/evilution/integration/rspec.rb +28 -8
- data/lib/evilution/isolation/fork.rb +11 -9
- data/lib/evilution/isolation/in_process.rb +11 -9
- data/lib/evilution/mcp/info_tool.rb +261 -0
- data/lib/evilution/mcp/mutate_tool.rb +112 -19
- data/lib/evilution/mcp/server.rb +3 -4
- data/lib/evilution/mcp/session_diff_tool.rb +5 -1
- data/lib/evilution/mcp/session_list_tool.rb +5 -1
- data/lib/evilution/mcp/session_show_tool.rb +5 -1
- data/lib/evilution/mcp/session_tool.rb +157 -0
- data/lib/evilution/reporter/cli.rb +2 -1
- data/lib/evilution/reporter/html/assets/style.css +68 -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 +47 -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/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.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 +9 -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/report.html.erb +17 -0
- data/lib/evilution/reporter/html/templates/summary_cards.html.erb +23 -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.rb +11 -349
- data/lib/evilution/reporter/json.rb +12 -8
- data/lib/evilution/result/mutation_result.rb +5 -1
- data/lib/evilution/result/summary.rb +9 -1
- data/lib/evilution/runner/baseline_runner.rb +71 -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 +255 -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 +57 -692
- data/lib/evilution/version.rb +1 -1
- metadata +71 -2
data/lib/evilution/cli.rb
CHANGED
|
@@ -1,867 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
require "optparse"
|
|
5
|
-
require "tempfile"
|
|
6
3
|
require_relative "version"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "
|
|
10
|
-
require_relative "
|
|
11
|
-
require_relative "
|
|
12
|
-
require_relative "
|
|
13
|
-
require_relative "
|
|
4
|
+
require_relative "cli/parser"
|
|
5
|
+
require_relative "cli/parsed_args"
|
|
6
|
+
require_relative "cli/printers/subjects"
|
|
7
|
+
require_relative "cli/dispatcher"
|
|
8
|
+
require_relative "cli/commands/version"
|
|
9
|
+
require_relative "cli/commands/init"
|
|
10
|
+
require_relative "cli/commands/mcp"
|
|
11
|
+
require_relative "cli/commands/subjects"
|
|
12
|
+
require_relative "cli/commands/tests_list"
|
|
13
|
+
require_relative "cli/commands/environment_show"
|
|
14
|
+
require_relative "cli/commands/util_mutation"
|
|
15
|
+
require_relative "cli/commands/session_list"
|
|
16
|
+
require_relative "cli/commands/session_show"
|
|
17
|
+
require_relative "cli/commands/session_diff"
|
|
18
|
+
require_relative "cli/commands/session_gc"
|
|
19
|
+
require_relative "cli/commands/run"
|
|
14
20
|
|
|
15
21
|
class Evilution::CLI
|
|
16
22
|
def initialize(argv, stdin: $stdin)
|
|
17
|
-
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
read_stdin_files if @options.delete(:stdin) && %i[run subjects].include?(@command)
|
|
23
|
+
parsed = Parser.new(argv, stdin: stdin).parse
|
|
24
|
+
@parsed = parsed
|
|
25
|
+
@command = parsed.command
|
|
26
|
+
@options = parsed.options
|
|
27
|
+
@files = parsed.files
|
|
28
|
+
@line_ranges = parsed.line_ranges
|
|
29
|
+
@stdin_error = parsed.stdin_error
|
|
30
|
+
@parse_error = parsed.parse_error
|
|
26
31
|
end
|
|
27
32
|
|
|
28
|
-
def call
|
|
29
|
-
|
|
30
|
-
when :version then run_version
|
|
31
|
-
when :init then run_init
|
|
32
|
-
when :mcp then run_mcp
|
|
33
|
-
when :session_list then run_session_list
|
|
34
|
-
when :session_show then run_session_show
|
|
35
|
-
when :session_diff then run_session_diff
|
|
36
|
-
when :session_gc then run_session_gc
|
|
37
|
-
when :session_error then run_subcommand_error(@session_error)
|
|
38
|
-
when :subjects then run_subjects
|
|
39
|
-
when :tests_list then run_tests_list
|
|
40
|
-
when :tests_error then run_subcommand_error(@tests_error)
|
|
41
|
-
when :environment_show then run_environment_show
|
|
42
|
-
when :environment_error then run_subcommand_error(@environment_error)
|
|
43
|
-
when :util_mutation then run_util_mutation
|
|
44
|
-
when :util_error then run_subcommand_error(@util_error)
|
|
45
|
-
when :run then run_mutations
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
33
|
+
def call
|
|
34
|
+
return run_subcommand_error(@parse_error) if @command == :parse_error
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
36
|
+
result = Dispatcher.lookup(@command).new(@parsed, stdout: $stdout, stderr: $stderr).call
|
|
37
|
+
warn("Error: #{result.error.message}") if result.error && !result.error_rendered
|
|
38
|
+
result.exit_code
|
|
54
39
|
end
|
|
55
40
|
|
|
56
|
-
|
|
57
|
-
unless File.directory?(dir)
|
|
58
|
-
warn("Error: #{dir} is not a directory")
|
|
59
|
-
return
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
specs = Dir.glob(File.join(dir, "**/*_spec.rb"))
|
|
63
|
-
@options[:spec_files] = Array(@options[:spec_files]) + specs
|
|
64
|
-
end
|
|
41
|
+
private
|
|
65
42
|
|
|
66
43
|
def run_subcommand_error(message)
|
|
67
44
|
warn("Error: #{message}")
|
|
68
45
|
2
|
|
69
46
|
end
|
|
70
|
-
|
|
71
|
-
def extract_command(argv)
|
|
72
|
-
case argv.first
|
|
73
|
-
when "version"
|
|
74
|
-
@command = :version
|
|
75
|
-
argv.shift
|
|
76
|
-
when "init"
|
|
77
|
-
@command = :init
|
|
78
|
-
argv.shift
|
|
79
|
-
when "mcp"
|
|
80
|
-
@command = :mcp
|
|
81
|
-
argv.shift
|
|
82
|
-
when "session"
|
|
83
|
-
argv.shift
|
|
84
|
-
extract_session_subcommand(argv)
|
|
85
|
-
when "subjects"
|
|
86
|
-
@command = :subjects
|
|
87
|
-
argv.shift
|
|
88
|
-
when "tests"
|
|
89
|
-
argv.shift
|
|
90
|
-
extract_tests_subcommand(argv)
|
|
91
|
-
when "environment"
|
|
92
|
-
argv.shift
|
|
93
|
-
extract_environment_subcommand(argv)
|
|
94
|
-
when "util"
|
|
95
|
-
argv.shift
|
|
96
|
-
extract_util_subcommand(argv)
|
|
97
|
-
when "run"
|
|
98
|
-
argv.shift
|
|
99
|
-
end
|
|
100
|
-
argv
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def extract_session_subcommand(argv)
|
|
104
|
-
subcommand = argv.first
|
|
105
|
-
case subcommand
|
|
106
|
-
when "list"
|
|
107
|
-
@command = :session_list
|
|
108
|
-
argv.shift
|
|
109
|
-
when "show"
|
|
110
|
-
@command = :session_show
|
|
111
|
-
argv.shift
|
|
112
|
-
when "diff"
|
|
113
|
-
@command = :session_diff
|
|
114
|
-
argv.shift
|
|
115
|
-
when "gc"
|
|
116
|
-
@command = :session_gc
|
|
117
|
-
argv.shift
|
|
118
|
-
when nil
|
|
119
|
-
@command = :session_error
|
|
120
|
-
@session_error = "Missing session subcommand. Available subcommands: list, show, diff, gc"
|
|
121
|
-
else
|
|
122
|
-
@command = :session_error
|
|
123
|
-
@session_error = "Unknown session subcommand: #{subcommand}. Available subcommands: list, show, diff, gc"
|
|
124
|
-
argv.shift
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def extract_environment_subcommand(argv)
|
|
129
|
-
subcommand = argv.first
|
|
130
|
-
case subcommand
|
|
131
|
-
when "show"
|
|
132
|
-
@command = :environment_show
|
|
133
|
-
argv.shift
|
|
134
|
-
when nil
|
|
135
|
-
@command = :environment_error
|
|
136
|
-
@environment_error = "Missing environment subcommand. Available subcommands: show"
|
|
137
|
-
else
|
|
138
|
-
@command = :environment_error
|
|
139
|
-
@environment_error = "Unknown environment subcommand: #{subcommand}. Available subcommands: show"
|
|
140
|
-
argv.shift
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def extract_tests_subcommand(argv)
|
|
145
|
-
subcommand = argv.first
|
|
146
|
-
case subcommand
|
|
147
|
-
when "list"
|
|
148
|
-
@command = :tests_list
|
|
149
|
-
argv.shift
|
|
150
|
-
when nil
|
|
151
|
-
@command = :tests_error
|
|
152
|
-
@tests_error = "Missing tests subcommand. Available subcommands: list"
|
|
153
|
-
else
|
|
154
|
-
@command = :tests_error
|
|
155
|
-
@tests_error = "Unknown tests subcommand: #{subcommand}. Available subcommands: list"
|
|
156
|
-
argv.shift
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def extract_util_subcommand(argv)
|
|
161
|
-
subcommand = argv.first
|
|
162
|
-
case subcommand
|
|
163
|
-
when "mutation"
|
|
164
|
-
@command = :util_mutation
|
|
165
|
-
argv.shift
|
|
166
|
-
when nil
|
|
167
|
-
@command = :util_error
|
|
168
|
-
@util_error = "Missing util subcommand. Available subcommands: mutation"
|
|
169
|
-
else
|
|
170
|
-
@command = :util_error
|
|
171
|
-
@util_error = "Unknown util subcommand: #{subcommand}. Available subcommands: mutation"
|
|
172
|
-
argv.shift
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def preprocess_flags(argv)
|
|
177
|
-
result = []
|
|
178
|
-
i = 0
|
|
179
|
-
while i < argv.length
|
|
180
|
-
arg = argv[i]
|
|
181
|
-
if arg == "--fail-fast"
|
|
182
|
-
next_arg = argv[i + 1]
|
|
183
|
-
|
|
184
|
-
if next_arg && next_arg.match?(/\A-?\d+\z/)
|
|
185
|
-
@options[:fail_fast] = next_arg
|
|
186
|
-
i += 2
|
|
187
|
-
else
|
|
188
|
-
result << arg
|
|
189
|
-
i += 1
|
|
190
|
-
end
|
|
191
|
-
elsif arg.start_with?("--fail-fast=")
|
|
192
|
-
@options[:fail_fast] = arg.delete_prefix("--fail-fast=")
|
|
193
|
-
i += 1
|
|
194
|
-
else
|
|
195
|
-
result << arg
|
|
196
|
-
i += 1
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
result
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def build_option_parser
|
|
203
|
-
OptionParser.new do |opts|
|
|
204
|
-
opts.banner = "Usage: evilution [command] [options] [files...]"
|
|
205
|
-
opts.version = Evilution::VERSION
|
|
206
|
-
add_separators(opts)
|
|
207
|
-
add_options(opts)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def add_separators(opts)
|
|
212
|
-
opts.separator ""
|
|
213
|
-
opts.separator "Line-range targeting: lib/foo.rb:15-30, lib/foo.rb:15, lib/foo.rb:15-"
|
|
214
|
-
opts.separator ""
|
|
215
|
-
opts.separator "Commands: run (default), init, session {list,show,diff,gc}, subjects, tests {list},"
|
|
216
|
-
opts.separator " util {mutation}, environment {show}, mcp, version"
|
|
217
|
-
opts.separator ""
|
|
218
|
-
opts.separator "Options:"
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def add_options(opts)
|
|
222
|
-
add_core_options(opts)
|
|
223
|
-
add_filter_options(opts)
|
|
224
|
-
add_flag_options(opts)
|
|
225
|
-
add_session_options(opts)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def add_core_options(opts)
|
|
229
|
-
opts.on("-j", "--jobs N", Integer, "Number of parallel workers (default: 1)") { |n| @options[:jobs] = n }
|
|
230
|
-
opts.on("-t", "--timeout N", Integer, "Per-mutation timeout in seconds") { |n| @options[:timeout] = n }
|
|
231
|
-
opts.on("-f", "--format FORMAT", "Output format: text, json, html") { |f| @options[:format] = f.to_sym }
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def add_filter_options(opts)
|
|
235
|
-
opts.on("--min-score FLOAT", Float, "Minimum mutation score to pass") { |s| @options[:min_score] = s }
|
|
236
|
-
opts.on("--spec FILES", Array, "Spec files to run (comma-separated)") { |f| @options[:spec_files] = f }
|
|
237
|
-
opts.on("--spec-dir DIR", "Include all specs in DIR") { |d| expand_spec_dir(d) }
|
|
238
|
-
opts.on("--target EXPR",
|
|
239
|
-
"Filter: method (Foo#bar), type (Foo#/Foo.), namespace (Foo*),",
|
|
240
|
-
"class (Foo), glob (source:**/*.rb), hierarchy (descendants:Foo)") do |m|
|
|
241
|
-
@options[:target] = m
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def add_flag_options(opts)
|
|
246
|
-
opts.on("--fail-fast", "Stop after N surviving mutants " \
|
|
247
|
-
"(default: disabled; if provided without N, uses 1; use --fail-fast=N)") { @options[:fail_fast] ||= 1 }
|
|
248
|
-
opts.on("--no-baseline", "Skip baseline test suite check") { @options[:baseline] = false }
|
|
249
|
-
opts.on("--incremental", "Cache killed/timeout results; skip re-running them on unchanged files") { @options[:incremental] = true }
|
|
250
|
-
opts.on("--integration NAME", "Test integration: rspec, minitest (default: rspec)") { |i| @options[:integration] = i }
|
|
251
|
-
opts.on("--isolation STRATEGY", "Isolation: auto, fork, in_process (default: auto)") { |s| @options[:isolation] = s }
|
|
252
|
-
opts.on("--preload FILE", "Preload FILE in the parent process before forking " \
|
|
253
|
-
"(default: auto-detect spec/rails_helper.rb for Rails projects)") { |f| @options[:preload] = f }
|
|
254
|
-
opts.on("--no-preload", "Disable parent-process preload even for Rails projects") { @options[:preload] = false }
|
|
255
|
-
opts.on("--stdin", "Read target file paths from stdin (one per line)") { @options[:stdin] = true }
|
|
256
|
-
opts.on("--suggest-tests", "Generate concrete test code in suggestions (RSpec or Minitest)") { @options[:suggest_tests] = true }
|
|
257
|
-
opts.on("--no-progress", "Disable progress bar") { @options[:progress] = false }
|
|
258
|
-
add_extra_flag_options(opts)
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def add_extra_flag_options(opts)
|
|
262
|
-
opts.on("--skip-heredoc-literals", "Skip all string literal mutations inside heredocs") { @options[:skip_heredoc_literals] = true }
|
|
263
|
-
opts.on("--show-disabled", "Report mutations skipped by # evilution:disable") { @options[:show_disabled] = true }
|
|
264
|
-
opts.on("--baseline-session PATH", "Compare against a baseline session in HTML report") { |p| @options[:baseline_session] = p }
|
|
265
|
-
opts.on("--save-session", "Save session results to .evilution/results/") { @options[:save_session] = true }
|
|
266
|
-
opts.on("-e", "--eval CODE", "Evaluate code snippet (for util mutation)") { |c| @options[:eval] = c }
|
|
267
|
-
opts.on("-v", "--verbose", "Verbose output") { @options[:verbose] = true }
|
|
268
|
-
opts.on("-q", "--quiet", "Suppress output") { @options[:quiet] = true }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def add_session_options(opts)
|
|
272
|
-
opts.on("--results-dir DIR", "Session results directory") { |d| @options[:results_dir] = d }
|
|
273
|
-
opts.on("--limit N", Integer, "Show only the N most recent sessions") { |n| @options[:limit] = n }
|
|
274
|
-
opts.on("--since DATE", "Show sessions since DATE (YYYY-MM-DD)") { |d| @options[:since] = d }
|
|
275
|
-
opts.on("--older-than DURATION", "Delete sessions older than DURATION (e.g., 30d, 24h, 1w)") do |d|
|
|
276
|
-
@options[:older_than] = d
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def run_init
|
|
281
|
-
path = ".evilution.yml"
|
|
282
|
-
if File.exist?(path)
|
|
283
|
-
warn("#{path} already exists")
|
|
284
|
-
return 1
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
File.write(path, Evilution::Config.default_template)
|
|
288
|
-
$stdout.puts("Created #{path}")
|
|
289
|
-
0
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def run_subjects
|
|
293
|
-
raise Evilution::ConfigError, @stdin_error if @stdin_error
|
|
294
|
-
|
|
295
|
-
config = Evilution::Config.new(target_files: @files, line_ranges: @line_ranges, **@options)
|
|
296
|
-
runner = Evilution::Runner.new(config: config)
|
|
297
|
-
subjects = runner.parse_and_filter_subjects
|
|
298
|
-
|
|
299
|
-
if subjects.empty?
|
|
300
|
-
$stdout.puts("No subjects found")
|
|
301
|
-
return 0
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
registry = Evilution::Mutator::Registry.default
|
|
305
|
-
filter = build_subject_filter(config)
|
|
306
|
-
operator_options = build_operator_options(config)
|
|
307
|
-
total_mutations = 0
|
|
308
|
-
|
|
309
|
-
subjects.each do |subj|
|
|
310
|
-
count = registry.mutations_for(subj, filter: filter, operator_options: operator_options).length
|
|
311
|
-
total_mutations += count
|
|
312
|
-
label = count == 1 ? "1 mutation" : "#{count} mutations"
|
|
313
|
-
$stdout.puts(" #{subj.name} #{subj.file_path}:#{subj.line_number} (#{label})")
|
|
314
|
-
ensure
|
|
315
|
-
subj.release_node!
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
$stdout.puts("")
|
|
319
|
-
$stdout.puts("#{subjects.length} subjects, #{total_mutations} mutations")
|
|
320
|
-
0
|
|
321
|
-
rescue Evilution::Error => e
|
|
322
|
-
warn("Error: #{e.message}")
|
|
323
|
-
2
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def build_operator_options(config)
|
|
327
|
-
{ skip_heredoc_literals: config.skip_heredoc_literals? }
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
def build_subject_filter(config)
|
|
331
|
-
return nil if config.ignore_patterns.empty?
|
|
332
|
-
|
|
333
|
-
require_relative "ast/pattern/filter"
|
|
334
|
-
Evilution::AST::Pattern::Filter.new(config.ignore_patterns)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def run_tests_list
|
|
338
|
-
config = Evilution::Config.new(target_files: @files, line_ranges: @line_ranges, **@options)
|
|
339
|
-
|
|
340
|
-
if config.spec_files.any?
|
|
341
|
-
print_explicit_spec_files(config.spec_files)
|
|
342
|
-
return 0
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
source_files = resolve_source_files(config)
|
|
346
|
-
if source_files.empty?
|
|
347
|
-
$stdout.puts("No source files found")
|
|
348
|
-
return 0
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
resolver = Evilution::SpecResolver.new
|
|
352
|
-
print_resolved_specs(source_files, resolver)
|
|
353
|
-
0
|
|
354
|
-
rescue Evilution::Error => e
|
|
355
|
-
warn("Error: #{e.message}")
|
|
356
|
-
2
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
def resolve_source_files(config)
|
|
360
|
-
return config.target_files unless config.target_files.empty?
|
|
361
|
-
|
|
362
|
-
Evilution::Git::ChangedFiles.new.call
|
|
363
|
-
rescue Evilution::Error
|
|
364
|
-
[]
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def print_explicit_spec_files(spec_files)
|
|
368
|
-
spec_files.each { |f| $stdout.puts(" #{f}") }
|
|
369
|
-
label = spec_files.length == 1 ? "1 spec file" : "#{spec_files.length} spec files"
|
|
370
|
-
$stdout.puts("")
|
|
371
|
-
$stdout.puts(label)
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def print_resolved_specs(source_files, resolver)
|
|
375
|
-
unique_specs = []
|
|
376
|
-
source_files.each do |source|
|
|
377
|
-
spec = resolver.call(source)
|
|
378
|
-
if spec
|
|
379
|
-
unique_specs << spec
|
|
380
|
-
$stdout.puts(" #{spec} (#{source})")
|
|
381
|
-
else
|
|
382
|
-
$stdout.puts(" #{source} (no spec found)")
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
unique_specs.uniq!
|
|
387
|
-
$stdout.puts("")
|
|
388
|
-
spec_label = unique_specs.length == 1 ? "1 spec file" : "#{unique_specs.length} spec files"
|
|
389
|
-
$stdout.puts("#{source_files.length} source files, #{spec_label}")
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def run_environment_show
|
|
393
|
-
config = Evilution::Config.new(**@options)
|
|
394
|
-
$stdout.puts(format_environment(config))
|
|
395
|
-
0
|
|
396
|
-
rescue Evilution::ConfigError => e
|
|
397
|
-
warn("Error: #{e.message}")
|
|
398
|
-
2
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def format_environment(config)
|
|
402
|
-
config_file = Evilution::Config::CONFIG_FILES.find { |path| File.exist?(path) }
|
|
403
|
-
lines = environment_header(config_file)
|
|
404
|
-
lines.concat(environment_settings(config))
|
|
405
|
-
lines.join("\n")
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
def environment_header(config_file)
|
|
409
|
-
[
|
|
410
|
-
"Evilution Environment",
|
|
411
|
-
("=" * 30),
|
|
412
|
-
"",
|
|
413
|
-
"evilution: #{Evilution::VERSION}",
|
|
414
|
-
"ruby: #{RUBY_VERSION}",
|
|
415
|
-
"config_file: #{config_file || "(none)"}",
|
|
416
|
-
"",
|
|
417
|
-
"Settings:"
|
|
418
|
-
]
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def environment_settings(config)
|
|
422
|
-
[
|
|
423
|
-
" timeout: #{config.timeout}",
|
|
424
|
-
" format: #{config.format}",
|
|
425
|
-
" integration: #{config.integration}",
|
|
426
|
-
" jobs: #{config.jobs}",
|
|
427
|
-
" isolation: #{config.isolation}",
|
|
428
|
-
" baseline: #{config.baseline}",
|
|
429
|
-
" incremental: #{config.incremental}",
|
|
430
|
-
" verbose: #{config.verbose}",
|
|
431
|
-
" quiet: #{config.quiet}",
|
|
432
|
-
" progress: #{config.progress}",
|
|
433
|
-
" fail_fast: #{config.fail_fast || "(disabled)"}",
|
|
434
|
-
" min_score: #{config.min_score}",
|
|
435
|
-
" suggest_tests: #{config.suggest_tests}",
|
|
436
|
-
" save_session: #{config.save_session}",
|
|
437
|
-
" target: #{config.target || "(all files)"}",
|
|
438
|
-
" skip_heredoc_literals: #{config.skip_heredoc_literals}",
|
|
439
|
-
" ignore_patterns: #{config.ignore_patterns.empty? ? "(none)" : config.ignore_patterns.inspect}"
|
|
440
|
-
]
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def run_mcp
|
|
444
|
-
require_relative "mcp/server"
|
|
445
|
-
server = Evilution::MCP::Server.build
|
|
446
|
-
transport = ::MCP::Server::Transports::StdioTransport.new(server)
|
|
447
|
-
transport.open
|
|
448
|
-
0
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
def read_stdin_files
|
|
452
|
-
@stdin_error = "--stdin cannot be combined with positional file arguments" unless @files.empty?
|
|
453
|
-
return if @stdin_error
|
|
454
|
-
|
|
455
|
-
lines = []
|
|
456
|
-
@stdin.each_line do |line|
|
|
457
|
-
line = line.strip
|
|
458
|
-
lines << line unless line.empty?
|
|
459
|
-
end
|
|
460
|
-
stdin_files, stdin_ranges = parse_file_args(lines)
|
|
461
|
-
@files = stdin_files
|
|
462
|
-
@line_ranges = @line_ranges.merge(stdin_ranges)
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def parse_file_args(raw_args)
|
|
466
|
-
files = []
|
|
467
|
-
ranges = {}
|
|
468
|
-
|
|
469
|
-
raw_args.each do |arg|
|
|
470
|
-
file, range_str = arg.split(":", 2)
|
|
471
|
-
files << file
|
|
472
|
-
next unless range_str
|
|
473
|
-
|
|
474
|
-
ranges[file] = parse_line_range(range_str)
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
[files, ranges]
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
def parse_line_range(str)
|
|
481
|
-
if str.include?("-")
|
|
482
|
-
start_str, end_str = str.split("-", 2)
|
|
483
|
-
start_line = Integer(start_str)
|
|
484
|
-
end_line = end_str.empty? ? Float::INFINITY : Integer(end_str)
|
|
485
|
-
start_line..end_line
|
|
486
|
-
else
|
|
487
|
-
line = Integer(str)
|
|
488
|
-
line..line
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
def run_util_mutation
|
|
493
|
-
source, file_path = resolve_util_mutation_source
|
|
494
|
-
subjects = parse_source_to_subjects(source, file_path)
|
|
495
|
-
config = Evilution::Config.new(**@options)
|
|
496
|
-
registry = Evilution::Mutator::Registry.default
|
|
497
|
-
operator_options = build_operator_options(config)
|
|
498
|
-
mutations = subjects.flat_map { |s| registry.mutations_for(s, operator_options: operator_options) }
|
|
499
|
-
|
|
500
|
-
if mutations.empty?
|
|
501
|
-
$stdout.puts("No mutations generated")
|
|
502
|
-
return 0
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
if @options[:format] == :json
|
|
506
|
-
print_util_mutations_json(mutations)
|
|
507
|
-
else
|
|
508
|
-
print_util_mutations_text(mutations)
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
0
|
|
512
|
-
rescue Evilution::Error => e
|
|
513
|
-
warn("Error: #{e.message}")
|
|
514
|
-
2
|
|
515
|
-
ensure
|
|
516
|
-
@util_tmpfile&.close!
|
|
517
|
-
end
|
|
518
|
-
|
|
519
|
-
def resolve_util_mutation_source
|
|
520
|
-
if @options[:eval]
|
|
521
|
-
tmpfile = Tempfile.new(["evilution_eval", ".rb"])
|
|
522
|
-
tmpfile.write(@options[:eval])
|
|
523
|
-
tmpfile.flush
|
|
524
|
-
@util_tmpfile = tmpfile
|
|
525
|
-
[@options[:eval], tmpfile.path]
|
|
526
|
-
elsif @files.first
|
|
527
|
-
path = @files.first
|
|
528
|
-
raise Evilution::Error, "file not found: #{path}" unless File.exist?(path)
|
|
529
|
-
|
|
530
|
-
begin
|
|
531
|
-
[File.read(path), path]
|
|
532
|
-
rescue SystemCallError => e
|
|
533
|
-
raise Evilution::Error, e.message
|
|
534
|
-
end
|
|
535
|
-
else
|
|
536
|
-
raise Evilution::Error, "source required: use -e 'code' or provide a file path"
|
|
537
|
-
end
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def parse_source_to_subjects(source, file_label)
|
|
541
|
-
result = Prism.parse(source)
|
|
542
|
-
raise Evilution::Error, "failed to parse source: #{result.errors.map(&:message).join(", ")}" if result.failure?
|
|
543
|
-
|
|
544
|
-
finder = Evilution::AST::SubjectFinder.new(source, file_label)
|
|
545
|
-
finder.visit(result.value)
|
|
546
|
-
finder.subjects
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
def print_util_mutations_text(mutations)
|
|
550
|
-
mutations.each_with_index do |m, i|
|
|
551
|
-
$stdout.puts("#{i + 1}. #{m.operator_name} — #{m.subject.name} (line #{m.line})")
|
|
552
|
-
m.diff.each_line { |line| $stdout.puts(" #{line}") }
|
|
553
|
-
$stdout.puts("")
|
|
554
|
-
end
|
|
555
|
-
label = mutations.length == 1 ? "1 mutation" : "#{mutations.length} mutations"
|
|
556
|
-
$stdout.puts(label)
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
def print_util_mutations_json(mutations)
|
|
560
|
-
data = mutations.map do |m|
|
|
561
|
-
{ operator: m.operator_name, subject: m.subject.name,
|
|
562
|
-
file: m.file_path, line: m.line, diff: m.diff }
|
|
563
|
-
end
|
|
564
|
-
$stdout.puts(JSON.pretty_generate(data))
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
def run_session_list
|
|
568
|
-
require_relative "session/store"
|
|
569
|
-
|
|
570
|
-
store_opts = {}
|
|
571
|
-
store_opts[:results_dir] = @options[:results_dir] if @options[:results_dir]
|
|
572
|
-
store = Evilution::Session::Store.new(**store_opts)
|
|
573
|
-
sessions = store.list
|
|
574
|
-
sessions = filter_sessions(sessions)
|
|
575
|
-
|
|
576
|
-
if sessions.empty?
|
|
577
|
-
$stdout.puts("No sessions found")
|
|
578
|
-
return 0
|
|
579
|
-
end
|
|
580
|
-
|
|
581
|
-
if @options[:format] == :json
|
|
582
|
-
$stdout.puts(JSON.pretty_generate(sessions.map { |s| session_to_hash(s) }))
|
|
583
|
-
else
|
|
584
|
-
print_session_table(sessions)
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
0
|
|
588
|
-
rescue Evilution::ConfigError => e
|
|
589
|
-
warn("Error: #{e.message}")
|
|
590
|
-
2
|
|
591
|
-
end
|
|
592
|
-
|
|
593
|
-
def filter_sessions(sessions)
|
|
594
|
-
if @options[:since]
|
|
595
|
-
cutoff = parse_date(@options[:since])
|
|
596
|
-
sessions = sessions.select do |s|
|
|
597
|
-
ts = s[:timestamp]
|
|
598
|
-
next false unless ts.is_a?(String)
|
|
599
|
-
|
|
600
|
-
Time.parse(ts) >= cutoff
|
|
601
|
-
rescue ArgumentError
|
|
602
|
-
false
|
|
603
|
-
end
|
|
604
|
-
end
|
|
605
|
-
sessions = sessions.first(@options[:limit]) if @options[:limit]
|
|
606
|
-
sessions
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
def parse_date(value)
|
|
610
|
-
Time.parse(value)
|
|
611
|
-
rescue ArgumentError
|
|
612
|
-
raise Evilution::ConfigError, "invalid --since date: #{value.inspect}. Use YYYY-MM-DD format"
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
def run_session_show
|
|
616
|
-
require_relative "session/store"
|
|
617
|
-
|
|
618
|
-
path = @files.first
|
|
619
|
-
raise Evilution::ConfigError, "session file path required" unless path
|
|
620
|
-
|
|
621
|
-
store = Evilution::Session::Store.new
|
|
622
|
-
data = store.load(path)
|
|
623
|
-
|
|
624
|
-
if @options[:format] == :json
|
|
625
|
-
$stdout.puts(JSON.pretty_generate(data))
|
|
626
|
-
else
|
|
627
|
-
print_session_detail(data)
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
0
|
|
631
|
-
rescue Evilution::Error => e
|
|
632
|
-
warn("Error: #{e.message}")
|
|
633
|
-
2
|
|
634
|
-
rescue ::JSON::ParserError => e
|
|
635
|
-
warn("Error: invalid session file: #{e.message}")
|
|
636
|
-
2
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
def run_session_diff
|
|
640
|
-
require_relative "session/store"
|
|
641
|
-
require_relative "session/diff"
|
|
642
|
-
|
|
643
|
-
raise Evilution::ConfigError, "two session file paths required" unless @files.length == 2
|
|
644
|
-
|
|
645
|
-
store = Evilution::Session::Store.new
|
|
646
|
-
base_data = store.load(@files[0])
|
|
647
|
-
head_data = store.load(@files[1])
|
|
648
|
-
|
|
649
|
-
diff = Evilution::Session::Diff.new
|
|
650
|
-
result = diff.call(base_data, head_data)
|
|
651
|
-
|
|
652
|
-
if @options[:format] == :json
|
|
653
|
-
$stdout.puts(JSON.pretty_generate(result.to_h))
|
|
654
|
-
else
|
|
655
|
-
print_session_diff(result)
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
0
|
|
659
|
-
rescue Evilution::Error, SystemCallError => e
|
|
660
|
-
warn("Error: #{e.message}")
|
|
661
|
-
2
|
|
662
|
-
rescue ::JSON::ParserError => e
|
|
663
|
-
warn("Error: invalid session file: #{e.message}")
|
|
664
|
-
2
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
def print_session_diff(result)
|
|
668
|
-
print_diff_summary(result.summary)
|
|
669
|
-
print_diff_section("Fixed (survived \u2192 killed)", result.fixed, "\e[32m")
|
|
670
|
-
print_diff_section("New survivors (killed \u2192 survived)", result.new_survivors, "\e[31m")
|
|
671
|
-
print_diff_section("Persistent survivors", result.persistent, "\e[33m")
|
|
672
|
-
|
|
673
|
-
return unless result.fixed.empty? && result.new_survivors.empty? && result.persistent.empty?
|
|
674
|
-
|
|
675
|
-
$stdout.puts("")
|
|
676
|
-
$stdout.puts("No mutation changes between sessions")
|
|
677
|
-
end
|
|
678
|
-
|
|
679
|
-
def print_diff_summary(summary)
|
|
680
|
-
delta_str = format("%+.2f%%", summary.score_delta * 100)
|
|
681
|
-
$stdout.puts("Session Diff")
|
|
682
|
-
$stdout.puts("=" * 40)
|
|
683
|
-
$stdout.puts(format("Base score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
|
|
684
|
-
score: summary.base_score * 100, killed: summary.base_killed,
|
|
685
|
-
total: summary.base_total))
|
|
686
|
-
$stdout.puts(format("Head score: %<score>6.2f%% (%<killed>d/%<total>d killed)",
|
|
687
|
-
score: summary.head_score * 100, killed: summary.head_killed,
|
|
688
|
-
total: summary.head_total))
|
|
689
|
-
$stdout.puts("Delta: #{delta_str}")
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
def print_diff_section(title, mutations, color)
|
|
693
|
-
return if mutations.empty?
|
|
694
|
-
|
|
695
|
-
reset = "\e[0m"
|
|
696
|
-
$stdout.puts("")
|
|
697
|
-
$stdout.puts("#{color}#{title} (#{mutations.length}):#{reset}")
|
|
698
|
-
mutations.each do |m|
|
|
699
|
-
$stdout.puts(" #{m["operator"]} — #{m["file"]}:#{m["line"]} #{m["subject"]}")
|
|
700
|
-
end
|
|
701
|
-
end
|
|
702
|
-
|
|
703
|
-
def run_session_gc
|
|
704
|
-
require_relative "session/store"
|
|
705
|
-
|
|
706
|
-
raise Evilution::ConfigError, "--older-than is required for session gc" unless @options[:older_than]
|
|
707
|
-
|
|
708
|
-
cutoff = parse_duration(@options[:older_than])
|
|
709
|
-
store_opts = {}
|
|
710
|
-
store_opts[:results_dir] = @options[:results_dir] if @options[:results_dir]
|
|
711
|
-
store = Evilution::Session::Store.new(**store_opts)
|
|
712
|
-
deleted = store.gc(older_than: cutoff)
|
|
713
|
-
|
|
714
|
-
if deleted.empty?
|
|
715
|
-
$stdout.puts("No sessions to delete")
|
|
716
|
-
else
|
|
717
|
-
$stdout.puts("Deleted #{deleted.length} session#{"s" unless deleted.length == 1}")
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
0
|
|
721
|
-
rescue Evilution::ConfigError => e
|
|
722
|
-
warn("Error: #{e.message}")
|
|
723
|
-
2
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
def parse_duration(value)
|
|
727
|
-
match = value.match(/\A(\d+)([dhw])\z/)
|
|
728
|
-
raise Evilution::ConfigError, "invalid --older-than format: #{value.inspect}. Use Nd, Nh, or Nw (e.g., 30d)" unless match
|
|
729
|
-
|
|
730
|
-
amount = match[1].to_i
|
|
731
|
-
seconds = case match[2]
|
|
732
|
-
when "h" then amount * 3600
|
|
733
|
-
when "d" then amount * 86_400
|
|
734
|
-
when "w" then amount * 604_800
|
|
735
|
-
end
|
|
736
|
-
Time.now - seconds
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
def print_session_detail(data)
|
|
740
|
-
print_session_header(data)
|
|
741
|
-
print_session_summary(data["summary"])
|
|
742
|
-
print_survived_section(data["survived"] || [])
|
|
743
|
-
end
|
|
744
|
-
|
|
745
|
-
def print_session_header(data)
|
|
746
|
-
$stdout.puts("Session: #{data["timestamp"]}")
|
|
747
|
-
$stdout.puts("Version: #{data["version"]}")
|
|
748
|
-
print_git_context(data["git"])
|
|
749
|
-
end
|
|
750
|
-
|
|
751
|
-
def print_git_context(git)
|
|
752
|
-
return unless git.is_a?(Hash)
|
|
753
|
-
|
|
754
|
-
branch = git["branch"]
|
|
755
|
-
sha = git["sha"]
|
|
756
|
-
return if branch.to_s.empty? && sha.to_s.empty?
|
|
757
|
-
|
|
758
|
-
$stdout.puts("Git: #{branch} (#{sha})")
|
|
759
|
-
end
|
|
760
|
-
|
|
761
|
-
def print_session_summary(summary)
|
|
762
|
-
$stdout.puts("")
|
|
763
|
-
$stdout.puts(
|
|
764
|
-
format(
|
|
765
|
-
"Score: %<score>.2f%% Total: %<total>d Killed: %<killed>d Survived: %<surv>d " \
|
|
766
|
-
"Timed out: %<to>d Errors: %<err>d Duration: %<dur>.1fs",
|
|
767
|
-
score: summary["score"] * 100, total: summary["total"], killed: summary["killed"],
|
|
768
|
-
surv: summary["survived"], to: summary["timed_out"], err: summary["errors"],
|
|
769
|
-
dur: summary["duration"]
|
|
770
|
-
)
|
|
771
|
-
)
|
|
772
|
-
end
|
|
773
|
-
|
|
774
|
-
def print_survived_section(survived)
|
|
775
|
-
$stdout.puts("")
|
|
776
|
-
if survived.empty?
|
|
777
|
-
$stdout.puts("No survived mutations")
|
|
778
|
-
else
|
|
779
|
-
$stdout.puts("Survived mutations (#{survived.length}):")
|
|
780
|
-
survived.each_with_index { |m, i| print_mutation_detail(m, i + 1) }
|
|
781
|
-
end
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
def print_mutation_detail(mutation, index)
|
|
785
|
-
$stdout.puts("")
|
|
786
|
-
$stdout.puts(" #{index}. #{mutation["operator"]} — #{mutation["file"]}:#{mutation["line"]}")
|
|
787
|
-
$stdout.puts(" Subject: #{mutation["subject"]}")
|
|
788
|
-
return unless mutation["diff"]
|
|
789
|
-
|
|
790
|
-
$stdout.puts(" Diff:")
|
|
791
|
-
mutation["diff"].each_line { |line| $stdout.puts(" #{line}") }
|
|
792
|
-
end
|
|
793
|
-
|
|
794
|
-
def session_to_hash(session)
|
|
795
|
-
{
|
|
796
|
-
"timestamp" => session[:timestamp],
|
|
797
|
-
"total" => session[:total],
|
|
798
|
-
"killed" => session[:killed],
|
|
799
|
-
"survived" => session[:survived],
|
|
800
|
-
"score" => session[:score],
|
|
801
|
-
"duration" => session[:duration],
|
|
802
|
-
"file" => session[:file]
|
|
803
|
-
}
|
|
804
|
-
end
|
|
805
|
-
|
|
806
|
-
def print_session_table(sessions)
|
|
807
|
-
header = "Timestamp Total Killed Surv. Score Duration"
|
|
808
|
-
$stdout.puts(header)
|
|
809
|
-
$stdout.puts("-" * header.length)
|
|
810
|
-
sessions.each { |s| print_session_row(s) }
|
|
811
|
-
end
|
|
812
|
-
|
|
813
|
-
def print_session_row(session)
|
|
814
|
-
$stdout.puts(
|
|
815
|
-
format(
|
|
816
|
-
"%-30<ts>s %6<total>d %6<killed>d %6<surv>d %7.2<score>f%% %7.1<dur>fs",
|
|
817
|
-
ts: session[:timestamp], total: session[:total], killed: session[:killed],
|
|
818
|
-
surv: session[:survived], score: session[:score] * 100, dur: session[:duration]
|
|
819
|
-
)
|
|
820
|
-
)
|
|
821
|
-
end
|
|
822
|
-
|
|
823
|
-
def run_mutations
|
|
824
|
-
raise Evilution::ConfigError, @stdin_error if @stdin_error
|
|
825
|
-
|
|
826
|
-
file_options = Evilution::Config.file_options
|
|
827
|
-
config = Evilution::Config.new(**@options, target_files: @files, line_ranges: @line_ranges)
|
|
828
|
-
hooks = build_hooks(config)
|
|
829
|
-
runner = Evilution::Runner.new(config: config, hooks: hooks)
|
|
830
|
-
summary = runner.call
|
|
831
|
-
summary.success?(min_score: config.min_score) ? 0 : 1
|
|
832
|
-
rescue Evilution::Error => e
|
|
833
|
-
if json_format?(config, file_options)
|
|
834
|
-
$stdout.puts(JSON.generate(error_payload(e)))
|
|
835
|
-
else
|
|
836
|
-
warn("Error: #{e.message}")
|
|
837
|
-
end
|
|
838
|
-
2
|
|
839
|
-
end
|
|
840
|
-
|
|
841
|
-
def build_hooks(config)
|
|
842
|
-
return nil if config.hooks.empty?
|
|
843
|
-
|
|
844
|
-
registry = Evilution::Hooks::Registry.new
|
|
845
|
-
Evilution::Hooks::Loader.call(registry, config.hooks)
|
|
846
|
-
registry
|
|
847
|
-
end
|
|
848
|
-
|
|
849
|
-
def json_format?(config, file_options)
|
|
850
|
-
return config.json? if config
|
|
851
|
-
|
|
852
|
-
format = @options[:format] || (file_options && file_options[:format])
|
|
853
|
-
format && format.to_sym == :json
|
|
854
|
-
end
|
|
855
|
-
|
|
856
|
-
def error_payload(error)
|
|
857
|
-
error_type = case error
|
|
858
|
-
when Evilution::ConfigError then "config_error"
|
|
859
|
-
when Evilution::ParseError then "parse_error"
|
|
860
|
-
else "runtime_error"
|
|
861
|
-
end
|
|
862
|
-
|
|
863
|
-
payload = { type: error_type, message: error.message }
|
|
864
|
-
payload[:file] = error.file if error.file
|
|
865
|
-
{ error: payload }
|
|
866
|
-
end
|
|
867
47
|
end
|