active_mutator 0.6.0 → 0.7.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/README.md +45 -13
- data/lib/active_mutator/abort_flag.rb +53 -0
- data/lib/active_mutator/baseline.rb +132 -58
- data/lib/active_mutator/baseline_hooks.rb +24 -2
- data/lib/active_mutator/cli.rb +18 -3
- data/lib/active_mutator/config.rb +8 -1
- data/lib/active_mutator/config_file.rb +17 -1
- data/lib/active_mutator/coverage_map.rb +4 -1
- data/lib/active_mutator/diagnostics/ndjson.rb +25 -0
- data/lib/active_mutator/diagnostics/text.rb +110 -0
- data/lib/active_mutator/events.rb +55 -0
- data/lib/active_mutator/memory_ceiling.rb +59 -0
- data/lib/active_mutator/memory_probe.rb +82 -0
- data/lib/active_mutator/reporter/github.rb +13 -2
- data/lib/active_mutator/reporter/json.rb +19 -5
- data/lib/active_mutator/reporter/stryker_json.rb +7 -1
- data/lib/active_mutator/reporter/terminal.rb +31 -2
- data/lib/active_mutator/result.rb +7 -1
- data/lib/active_mutator/runner.rb +209 -65
- data/lib/active_mutator/sampler.rb +69 -0
- data/lib/active_mutator/scheduler.rb +105 -61
- data/lib/active_mutator/since_filter.rb +68 -7
- data/lib/active_mutator/version.rb +1 -1
- data/lib/active_mutator.rb +7 -0
- metadata +8 -1
|
@@ -8,43 +8,63 @@ module ActiveMutator
|
|
|
8
8
|
class Scheduler
|
|
9
9
|
OrphanedError = Class.new(Error)
|
|
10
10
|
|
|
11
|
+
# first_seq: phase-2 escalation runs on its own scheduler; starting its
|
|
12
|
+
# sequence after phase 1's keeps mutant numbers unique across the run.
|
|
11
13
|
def initialize(jobs:, worker: Worker.method(:run), on_result: nil,
|
|
12
|
-
calibrators: nil, orphaned: -> { Process.ppid == 1 }
|
|
14
|
+
calibrators: nil, orphaned: -> { Process.ppid == 1 },
|
|
15
|
+
events: Events.new, first_seq: 1, abort: AbortFlag.new)
|
|
16
|
+
@abort = abort
|
|
13
17
|
@jobs = jobs
|
|
14
18
|
@worker = worker
|
|
15
19
|
@on_result = on_result
|
|
16
20
|
@calibrators = calibrators
|
|
17
21
|
@orphaned = orphaned
|
|
22
|
+
@events = events
|
|
23
|
+
@next_seq = first_seq
|
|
18
24
|
@last_logged_scale = {} # lane => last scale logged for that lane
|
|
19
25
|
end
|
|
20
26
|
|
|
27
|
+
# Signals are the Runner's: its AbortFlag trips, and the poll loop here
|
|
28
|
+
# kills every worker and raises Aborted with what finished.
|
|
21
29
|
def run(items)
|
|
22
|
-
previous_traps = nil
|
|
23
30
|
running = {}
|
|
24
|
-
previous_traps = install_signal_handlers(running)
|
|
25
31
|
results = []
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
@abort.deferred do
|
|
33
|
+
# Browser-covered mutants each boot Chrome + an app server; running
|
|
34
|
+
# them concurrently melts CPUs and manufactures false timeouts.
|
|
35
|
+
# Parallel lane first at full width, then the serial lane one at a time.
|
|
36
|
+
run_pool(items.select { |i| i.lane == :parallel }, @jobs, running, results)
|
|
37
|
+
run_pool(items.select { |i| i.lane == :serial }, 1, running, results)
|
|
38
|
+
end
|
|
31
39
|
results
|
|
32
40
|
ensure
|
|
33
|
-
|
|
41
|
+
cleanup(running)
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
private
|
|
37
45
|
|
|
38
|
-
def run_pool(items, width, running)
|
|
46
|
+
def run_pool(items, width, running, results)
|
|
39
47
|
queue = items.dup
|
|
40
|
-
results = []
|
|
41
48
|
until queue.empty? && running.empty?
|
|
42
49
|
abort_if_orphaned!(running)
|
|
43
|
-
|
|
50
|
+
abort!(running, results) if @abort.tripped?
|
|
51
|
+
# A trip mid-fill stops the forking; the next pass kills what started.
|
|
52
|
+
spawn(queue.shift, running) while running.size < width && !queue.empty? && !@abort.tripped?
|
|
44
53
|
reap(running, results)
|
|
45
54
|
sleep 0.02 unless running.empty?
|
|
46
55
|
end
|
|
47
|
-
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# CI allows only seconds between SIGTERM and SIGKILL, so kill every
|
|
59
|
+
# worker group and move on without waiting for any of them.
|
|
60
|
+
def abort!(running, results)
|
|
61
|
+
in_flight = running.map do |pid, entry|
|
|
62
|
+
m = entry[:item].mutation
|
|
63
|
+
{ seq: entry[:seq], pid: pid, subject: m.subject.name, file: m.subject.file, line: m.line,
|
|
64
|
+
description: m.description }
|
|
65
|
+
end
|
|
66
|
+
cleanup(running, wait: false)
|
|
67
|
+
raise Aborted.new(@abort.reason, results: results, in_flight: in_flight)
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
# SIGKILL on the parent (or a closed terminal, or CI teardown) cannot be
|
|
@@ -54,18 +74,27 @@ module ActiveMutator
|
|
|
54
74
|
def abort_if_orphaned!(running)
|
|
55
75
|
return unless @orphaned.call
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
raise OrphanedError, "parent process died; aborting mutation run"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cleanup(running, wait: true)
|
|
81
|
+
running.each_key { |pid| signal_group(pid) }
|
|
82
|
+
running.each do |pid, entry|
|
|
83
|
+
entry[:reader].close
|
|
84
|
+
entry[:stderr_file].close!
|
|
85
|
+
Process.waitpid(pid) if wait
|
|
86
|
+
rescue Errno::ECHILD
|
|
60
87
|
nil
|
|
61
88
|
end
|
|
62
89
|
running.clear
|
|
63
|
-
raise OrphanedError, "parent process died; aborting mutation run"
|
|
64
90
|
end
|
|
65
91
|
|
|
66
92
|
STDERR_TAIL_LINES = 20
|
|
67
93
|
|
|
68
94
|
def spawn(item, running)
|
|
95
|
+
calibrator = calibrator_for(item)
|
|
96
|
+
budget = calibrator ? calibrator.budget_for(item) : item.timeout
|
|
97
|
+
log_scale(calibrator, item.lane)
|
|
69
98
|
reader, writer = IO.pipe
|
|
70
99
|
stderr_file = Tempfile.new("active_mutator-worker")
|
|
71
100
|
pid = fork do
|
|
@@ -78,55 +107,86 @@ module ActiveMutator
|
|
|
78
107
|
# harmless everywhere else.
|
|
79
108
|
ENV["PGGSSENCMODE"] ||= "disable"
|
|
80
109
|
@worker.call(item.mutation, item.example_ids, writer)
|
|
110
|
+
# A second line, after the worker's own report: the worker can exit
|
|
111
|
+
# between two memory samples, so it reports its peak itself.
|
|
112
|
+
writer.puts("", JSON.generate("peak_rss_kb" => MemoryProbe.peak_rss_kb))
|
|
81
113
|
writer.close
|
|
82
114
|
Process.exit!(0)
|
|
83
115
|
end
|
|
84
116
|
writer.close
|
|
85
|
-
calibrator = calibrator_for(item)
|
|
86
|
-
budget = calibrator ? calibrator.budget_for(item) : item.timeout
|
|
87
|
-
log_scale(calibrator, item.lane)
|
|
88
117
|
started = now
|
|
118
|
+
seq = @next_seq
|
|
119
|
+
@next_seq += 1
|
|
89
120
|
running[pid] = { reader: reader, item: item, started: started, stderr_file: stderr_file,
|
|
90
|
-
budget: budget, deadline: started + budget }
|
|
121
|
+
budget: budget, deadline: started + budget, seq: seq, payload: +"" }
|
|
122
|
+
emit_start(item, pid, seq, budget) if @events.listening?
|
|
123
|
+
ensure
|
|
124
|
+
unless pid
|
|
125
|
+
reader&.close
|
|
126
|
+
writer&.close
|
|
127
|
+
stderr_file&.close!
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def emit_start(item, pid, seq, budget)
|
|
132
|
+
m = item.mutation
|
|
133
|
+
@events.emit(:mutant_start, seq: seq, pid: pid, lane: item.lane, budget: budget,
|
|
134
|
+
examples: item.example_ids.size, subject: m.subject.name,
|
|
135
|
+
file: m.subject.file, line: m.line, description: m.description)
|
|
91
136
|
end
|
|
92
137
|
|
|
93
138
|
def reap(running, results)
|
|
94
139
|
running.to_a.each do |pid, entry|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
# Read while the worker runs so a full pipe cannot prevent its exit.
|
|
141
|
+
# A descendant may retain the writer after that exit; keep the group
|
|
142
|
+
# tracked until EOF so aborts and the deadline still apply to it.
|
|
143
|
+
chunk = entry[:reader].read_nonblock(65_536, exception: false)
|
|
144
|
+
entry[:payload] << chunk if chunk.is_a?(String)
|
|
145
|
+
entry[:eof] = true if chunk.nil?
|
|
146
|
+
entry[:exited] ||= Process.waitpid(pid, Process::WNOHANG)
|
|
147
|
+
if entry[:exited] && entry[:eof]
|
|
98
148
|
result = finish(entry)
|
|
99
|
-
if result.status == :killed
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
results << result
|
|
149
|
+
calibrator_for(entry[:item])&.record(result.seconds, entry[:budget]) if result.status == :killed
|
|
150
|
+
results << complete(pid, entry, result)
|
|
151
|
+
running.delete(pid)
|
|
103
152
|
elsif now > entry[:deadline]
|
|
104
153
|
kill(pid)
|
|
105
|
-
running.delete(pid)
|
|
106
154
|
entry[:reader].close
|
|
107
155
|
entry[:stderr_file].close!
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
seconds = now - entry[:started]
|
|
157
|
+
details = format("timed out after %.1fs (budget %.1fs)", seconds, entry[:budget])
|
|
158
|
+
results << complete(pid, entry, Result.new(mutation: entry[:item].mutation, status: :timeout,
|
|
159
|
+
details: details, seconds: seconds))
|
|
160
|
+
running.delete(pid)
|
|
110
161
|
end
|
|
111
162
|
end
|
|
112
163
|
end
|
|
113
164
|
|
|
114
165
|
def finish(entry)
|
|
115
|
-
|
|
166
|
+
seconds = now - entry[:started]
|
|
167
|
+
report_line, stats_line = entry[:payload].lines.map(&:strip).reject(&:empty?)
|
|
116
168
|
entry[:reader].close
|
|
117
169
|
stderr_tail = stderr_tail(entry[:stderr_file])
|
|
118
|
-
data =
|
|
170
|
+
data = report_line && JSON.parse(report_line)
|
|
119
171
|
# A self-mutation of Worker#emit can produce well-formed JSON without a
|
|
120
172
|
# "status" key (or with a non-Hash root); treat any unusable payload as
|
|
121
173
|
# a worker error instead of crashing the whole run.
|
|
122
174
|
reported = data.is_a?(Hash) && data.key?("status")
|
|
123
175
|
status = reported ? data["status"].to_sym : :error
|
|
124
176
|
details = reported ? data["details"] : unreported_details(stderr_tail)
|
|
177
|
+
peak = JSON.parse(stats_line)["peak_rss_kb"] if stats_line
|
|
125
178
|
rescue JSON::ParserError
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
Result.new(mutation: entry[:item].mutation, status: :error,
|
|
180
|
+
details: "worker emitted unparseable payload", seconds: seconds)
|
|
128
181
|
else
|
|
129
|
-
|
|
182
|
+
Result.new(mutation: entry[:item].mutation, status: status, details: details,
|
|
183
|
+
seconds: seconds, peak_rss_kb: peak)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def complete(pid, entry, result)
|
|
187
|
+
@events.emit(:mutant_end, seq: entry[:seq], pid: pid, status: result.status,
|
|
188
|
+
seconds: result.seconds, peak_rss_kb: result.peak_rss_kb)
|
|
189
|
+
report(result)
|
|
130
190
|
end
|
|
131
191
|
|
|
132
192
|
def report(result)
|
|
@@ -149,14 +209,7 @@ module ActiveMutator
|
|
|
149
209
|
end
|
|
150
210
|
|
|
151
211
|
def kill(pid)
|
|
152
|
-
|
|
153
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
154
|
-
# Group not established yet (setpgid race) or already gone: direct kill.
|
|
155
|
-
begin
|
|
156
|
-
Process.kill("KILL", pid)
|
|
157
|
-
rescue Errno::ESRCH
|
|
158
|
-
nil
|
|
159
|
-
end
|
|
212
|
+
signal_group(pid)
|
|
160
213
|
ensure
|
|
161
214
|
begin
|
|
162
215
|
Process.waitpid(pid)
|
|
@@ -165,26 +218,17 @@ module ActiveMutator
|
|
|
165
218
|
end
|
|
166
219
|
end
|
|
167
220
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
nil
|
|
177
|
-
end
|
|
178
|
-
exit(130)
|
|
179
|
-
end
|
|
180
|
-
[sig, previous]
|
|
221
|
+
def signal_group(pid)
|
|
222
|
+
Process.kill("KILL", -pid) # negative pid = whole process group
|
|
223
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
224
|
+
# Group not established yet (setpgid race) or already gone: direct kill.
|
|
225
|
+
begin
|
|
226
|
+
Process.kill("KILL", pid)
|
|
227
|
+
rescue Errno::ESRCH
|
|
228
|
+
nil
|
|
181
229
|
end
|
|
182
230
|
end
|
|
183
231
|
|
|
184
|
-
def restore_traps(previous)
|
|
185
|
-
previous.each { |sig, handler| trap(sig, handler || "DEFAULT") }
|
|
186
|
-
end
|
|
187
|
-
|
|
188
232
|
def calibrator_for(item)
|
|
189
233
|
@calibrators && @calibrators[item.lane]
|
|
190
234
|
end
|
|
@@ -7,17 +7,32 @@ module ActiveMutator
|
|
|
7
7
|
|
|
8
8
|
def self.parse(diff_text)
|
|
9
9
|
changed = Hash.new { |h, k| h[k] = [] }
|
|
10
|
+
each_hunk(diff_text) do |file, start, count|
|
|
11
|
+
count.times { |i| changed[file] << start + i }
|
|
12
|
+
end
|
|
13
|
+
changed.reject { |_, lines| lines.empty? }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# New-side line each pure deletion follows (0 = top of file), per file.
|
|
17
|
+
# The deletion sits between that line and the next, so it is inside a
|
|
18
|
+
# subject only when the subject spans both.
|
|
19
|
+
def self.deletion_gaps(diff_text)
|
|
20
|
+
gaps = Hash.new { |h, k| h[k] = [] }
|
|
21
|
+
each_hunk(diff_text) do |file, start, count|
|
|
22
|
+
gaps[file] << start if count.zero?
|
|
23
|
+
end
|
|
24
|
+
gaps
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.each_hunk(diff_text)
|
|
10
28
|
current = nil
|
|
11
29
|
diff_text.each_line do |line|
|
|
12
30
|
if line.start_with?("+++ b/")
|
|
13
31
|
current = line.delete_prefix("+++ b/").strip
|
|
14
32
|
elsif current && (match = HUNK.match(line))
|
|
15
|
-
|
|
16
|
-
count = (match[2] || "1").to_i
|
|
17
|
-
count.times { |i| changed[current] << start + i }
|
|
33
|
+
yield current, match[1].to_i, (match[2] || "1").to_i
|
|
18
34
|
end
|
|
19
35
|
end
|
|
20
|
-
changed.reject { |_, lines| lines.empty? }
|
|
21
36
|
end
|
|
22
37
|
|
|
23
38
|
# Every file the diff touched on the new side, including deletion-only
|
|
@@ -29,7 +44,36 @@ module ActiveMutator
|
|
|
29
44
|
end.uniq
|
|
30
45
|
end
|
|
31
46
|
|
|
47
|
+
# Tokens that never change what the code does.
|
|
48
|
+
NOISE = %i[IGNORED_NEWLINE EMBDOC_BEGIN EMBDOC_LINE EMBDOC_END EOF].freeze
|
|
49
|
+
|
|
50
|
+
# True when two sources differ only in comments and blank lines. Magic
|
|
51
|
+
# comments (`# frozen_string_literal: true`) change behavior, so they
|
|
52
|
+
# count as code; so does anything that fails to parse.
|
|
53
|
+
def self.same_code?(old_source, new_source)
|
|
54
|
+
old_code = code_of(old_source)
|
|
55
|
+
!old_code.nil? && old_code == code_of(new_source)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.code_of(source)
|
|
59
|
+
result = Prism.parse_lex(source)
|
|
60
|
+
return nil if result.failure?
|
|
61
|
+
|
|
62
|
+
# A trailing comment swallows its line's newline, so comments become a
|
|
63
|
+
# newline instead of vanishing; runs of newlines collapse to one.
|
|
64
|
+
tokens = result.value.last.filter_map do |token, _state|
|
|
65
|
+
next if NOISE.include?(token.type)
|
|
66
|
+
|
|
67
|
+
%i[COMMENT NEWLINE].include?(token.type) ? :NEWLINE : [token.type, token.value]
|
|
68
|
+
end
|
|
69
|
+
tokens = tokens.chunk_while { |a, b| a == :NEWLINE && b == :NEWLINE }.map(&:first)
|
|
70
|
+
tokens.shift if tokens.first == :NEWLINE
|
|
71
|
+
tokens.pop if tokens.last == :NEWLINE
|
|
72
|
+
[tokens, result.magic_comments.map { |c| [c.key, c.value] }]
|
|
73
|
+
end
|
|
74
|
+
|
|
32
75
|
def initialize(ref:, root:)
|
|
76
|
+
@ref = ref
|
|
33
77
|
@root = root
|
|
34
78
|
diff = IO.popen(
|
|
35
79
|
["git", "-C", root, "diff", "--unified=0", ref, "--", "*.rb"], &:read
|
|
@@ -37,6 +81,7 @@ module ActiveMutator
|
|
|
37
81
|
raise Error, "git diff #{ref} failed" unless $?.success?
|
|
38
82
|
|
|
39
83
|
@changed = self.class.parse(diff)
|
|
84
|
+
@gaps = self.class.deletion_gaps(diff)
|
|
40
85
|
@touched = self.class.touched_files(diff)
|
|
41
86
|
untracked = IO.popen(
|
|
42
87
|
["git", "-C", root, "ls-files", "--others", "--exclude-standard", "--", "*.rb"], &:read
|
|
@@ -53,12 +98,28 @@ module ActiveMutator
|
|
|
53
98
|
# further git calls.
|
|
54
99
|
def changed_files = @touched
|
|
55
100
|
|
|
101
|
+
# Whether a changed file's edits since the ref are only comments. Reads
|
|
102
|
+
# the old side with `git show`; a file missing there (untracked, renamed)
|
|
103
|
+
# prints nothing, so it compares against an empty source and any code in
|
|
104
|
+
# it counts.
|
|
105
|
+
def comment_only?(path)
|
|
106
|
+
old_source = IO.popen(["git", "-C", @root, "show", "#{@ref}:#{path}"], err: File::NULL, &:read)
|
|
107
|
+
self.class.same_code?(old_source, File.read(File.join(@root, path)))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Whether the file only lost lines since the ref: touched, but with no
|
|
111
|
+
# added or changed lines (untracked files count as all added).
|
|
112
|
+
def deletion_only?(path) = !@changed.key?(path)
|
|
113
|
+
|
|
56
114
|
def cover?(subject)
|
|
57
|
-
|
|
58
|
-
|
|
115
|
+
rel = subject.file.delete_prefix("#{@root}/")
|
|
116
|
+
lines = @changed[rel]
|
|
59
117
|
return true if lines == :all
|
|
60
118
|
|
|
61
|
-
|
|
119
|
+
range = subject.line_range
|
|
120
|
+
return true if lines&.any? { |line| range.cover?(line) }
|
|
121
|
+
|
|
122
|
+
@gaps.fetch(rel, []).any? { |gap| range.cover?(gap) && range.cover?(gap + 1) }
|
|
62
123
|
end
|
|
63
124
|
end
|
|
64
125
|
end
|
data/lib/active_mutator.rb
CHANGED
|
@@ -25,6 +25,11 @@ require_relative "active_mutator/operators/negation_removal"
|
|
|
25
25
|
require_relative "active_mutator/mutation"
|
|
26
26
|
require_relative "active_mutator/analysis"
|
|
27
27
|
require_relative "active_mutator/engine"
|
|
28
|
+
require_relative "active_mutator/abort_flag"
|
|
29
|
+
require_relative "active_mutator/memory_ceiling"
|
|
30
|
+
require_relative "active_mutator/events"
|
|
31
|
+
require_relative "active_mutator/diagnostics/text"
|
|
32
|
+
require_relative "active_mutator/diagnostics/ndjson"
|
|
28
33
|
require_relative "active_mutator/atomic_file"
|
|
29
34
|
require_relative "active_mutator/coverage_map"
|
|
30
35
|
require_relative "active_mutator/baseline"
|
|
@@ -32,6 +37,8 @@ require_relative "active_mutator/baseline_delta"
|
|
|
32
37
|
require_relative "active_mutator/defined_constants"
|
|
33
38
|
require_relative "active_mutator/inserter"
|
|
34
39
|
require_relative "active_mutator/closure_reload"
|
|
40
|
+
require_relative "active_mutator/memory_probe"
|
|
41
|
+
require_relative "active_mutator/sampler"
|
|
35
42
|
require_relative "active_mutator/worker"
|
|
36
43
|
require_relative "active_mutator/result"
|
|
37
44
|
require_relative "active_mutator/work_item"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_mutator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel John
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- README.md
|
|
71
71
|
- exe/active_mutator
|
|
72
72
|
- lib/active_mutator.rb
|
|
73
|
+
- lib/active_mutator/abort_flag.rb
|
|
73
74
|
- lib/active_mutator/accepted_ledger.rb
|
|
74
75
|
- lib/active_mutator/analysis.rb
|
|
75
76
|
- lib/active_mutator/atomic_file.rb
|
|
@@ -83,10 +84,15 @@ files:
|
|
|
83
84
|
- lib/active_mutator/config_file.rb
|
|
84
85
|
- lib/active_mutator/coverage_map.rb
|
|
85
86
|
- lib/active_mutator/defined_constants.rb
|
|
87
|
+
- lib/active_mutator/diagnostics/ndjson.rb
|
|
88
|
+
- lib/active_mutator/diagnostics/text.rb
|
|
86
89
|
- lib/active_mutator/edit.rb
|
|
87
90
|
- lib/active_mutator/engine.rb
|
|
91
|
+
- lib/active_mutator/events.rb
|
|
88
92
|
- lib/active_mutator/fingerprint.rb
|
|
89
93
|
- lib/active_mutator/inserter.rb
|
|
94
|
+
- lib/active_mutator/memory_ceiling.rb
|
|
95
|
+
- lib/active_mutator/memory_probe.rb
|
|
90
96
|
- lib/active_mutator/mutation.rb
|
|
91
97
|
- lib/active_mutator/operators/base.rb
|
|
92
98
|
- lib/active_mutator/operators/call_swap.rb
|
|
@@ -104,6 +110,7 @@ files:
|
|
|
104
110
|
- lib/active_mutator/reporter/terminal.rb
|
|
105
111
|
- lib/active_mutator/result.rb
|
|
106
112
|
- lib/active_mutator/runner.rb
|
|
113
|
+
- lib/active_mutator/sampler.rb
|
|
107
114
|
- lib/active_mutator/scheduler.rb
|
|
108
115
|
- lib/active_mutator/since_filter.rb
|
|
109
116
|
- lib/active_mutator/source_location.rb
|