mutineer-mongoid 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- metadata +143 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_path"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# Source -> test pairing by path convention (#11). Pure stdlib path logic:
|
|
7
|
+
# no Rails, no class loading, no process. Two jobs:
|
|
8
|
+
# * expand_sources — a directory argument becomes its sorted **/*.rb files;
|
|
9
|
+
# a file inside the root becomes its root-relative path.
|
|
10
|
+
# * infer_tests — a source's test files by convention (app/ and lib/
|
|
11
|
+
# sources map to test/.../_test.rb, test/.../<name>_*_test.rb,
|
|
12
|
+
# test/.../test_*.rb or spec/.../_spec.rb), preserving
|
|
13
|
+
# namespaced subdirectories. The first EXISTING exact
|
|
14
|
+
# candidate stays first. Split Minitest files join a
|
|
15
|
+
# Minitest match. A matched spec is left as that one file.
|
|
16
|
+
#
|
|
17
|
+
# Independently unit-testable: every method is pure in/out over the
|
|
18
|
+
# filesystem, so the pairing contract is exercised with plain fixtures, no
|
|
19
|
+
# Rails, no fork.
|
|
20
|
+
module Pairing
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Expand each positional source: a directory -> its sorted **/*.rb files
|
|
24
|
+
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
25
|
+
# -> its path relative to project_root, so `./lib/x.rb` and an absolute
|
|
26
|
+
# path pair like `lib/x.rb` (#104). A missing file, or one outside the
|
|
27
|
+
# root, stays as typed. Flattened, deduped, order-stable.
|
|
28
|
+
#
|
|
29
|
+
# @param args [Array<String>] source paths or directories.
|
|
30
|
+
# @param project_root [String] repository root for relative expansion.
|
|
31
|
+
# @return [Array<String>] flattened, deduped source file list.
|
|
32
|
+
def expand_sources(args, project_root:)
|
|
33
|
+
root = File.expand_path(project_root)
|
|
34
|
+
# The root as typed and as its real path (macOS `/var` vs `/private/var`),
|
|
35
|
+
# resolved once for every file below.
|
|
36
|
+
roots = [root, ProjectPath.root_real(root)].uniq
|
|
37
|
+
Array(args).flat_map do |arg|
|
|
38
|
+
abs = File.expand_path(arg, root)
|
|
39
|
+
if File.directory?(abs)
|
|
40
|
+
Dir.glob(File.join(abs, "**", "*.rb")).sort.map { |f| existing_relative(f, roots) || f }
|
|
41
|
+
else
|
|
42
|
+
# `..` resolves as text, as FileSwap and the daemon already read it.
|
|
43
|
+
[(existing_relative(abs, roots) if File.exist?(abs)) || arg]
|
|
44
|
+
end
|
|
45
|
+
end.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# An existing file's root-relative path, or nil outside the root. Tries the
|
|
49
|
+
# path as given, then with its directory resolved, so a path typed through
|
|
50
|
+
# a symlinked parent (or macOS `/var` under a `/private/var` root) matches.
|
|
51
|
+
# The file's own name is kept, so a symlinked file is not renamed.
|
|
52
|
+
#
|
|
53
|
+
# @param abs [String] expanded absolute path of an existing file.
|
|
54
|
+
# @param roots [Array<String>] the project root, expanded and real.
|
|
55
|
+
# @return [String, nil] root-relative path, or nil outside the root.
|
|
56
|
+
def existing_relative(abs, roots)
|
|
57
|
+
root_relative(abs, roots) ||
|
|
58
|
+
root_relative(File.join(File.realpath(File.dirname(abs)), File.basename(abs)), roots)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# `abs` relative to the first of `roots` it is under, or nil. Text only: a
|
|
62
|
+
# symlinked source keeps its own name (realpath would rename it to its
|
|
63
|
+
# target and pair the wrong test). `roots` holds the root's real path too,
|
|
64
|
+
# so the macOS `/var` vs `/private/var` alias still matches.
|
|
65
|
+
#
|
|
66
|
+
# @param abs [String] expanded absolute path.
|
|
67
|
+
# @param roots [Array<String>] the project root, expanded and real.
|
|
68
|
+
# @return [String, nil] root-relative path, or nil outside the root.
|
|
69
|
+
def root_relative(abs, roots)
|
|
70
|
+
roots.each do |r|
|
|
71
|
+
prefix = r.end_with?("/") ? r : "#{r}/"
|
|
72
|
+
return abs.delete_prefix(prefix) if abs.start_with?(prefix)
|
|
73
|
+
end
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The first test path {#infer_tests} would run for a source, or nil.
|
|
78
|
+
# `prefer` is the resolved framework ("minitest" | "rspec"): its candidates
|
|
79
|
+
# are tried first, the other framework's as fallback, so a minitest default
|
|
80
|
+
# still finds a spec and vice-versa.
|
|
81
|
+
#
|
|
82
|
+
# @param source_rel [String] relative source path.
|
|
83
|
+
# @param project_root [String] repository root for existence checks.
|
|
84
|
+
# @param prefer [String] preferred framework name.
|
|
85
|
+
# @return [String, nil] existing test path or nil.
|
|
86
|
+
def infer_test(source_rel, project_root:, prefer: "minitest")
|
|
87
|
+
infer_tests(source_rel, project_root: project_root, prefer: prefer).first
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Every test file convention pairs with a source. The first existing exact
|
|
91
|
+
# candidate from {#candidates} stays first. Each `<basename>_*_test.rb` in
|
|
92
|
+
# the mirrored test directory is added after it (#87). A spec that the exact
|
|
93
|
+
# rules already found is left alone: split Minitest names do not join it
|
|
94
|
+
# and do not replace it.
|
|
95
|
+
#
|
|
96
|
+
# @param source_rel [String] relative source path.
|
|
97
|
+
# @param project_root [String] repository root for existence checks.
|
|
98
|
+
# @param prefer [String] preferred framework name.
|
|
99
|
+
# @return [Array<String>] existing test paths, exact candidate first.
|
|
100
|
+
def infer_tests(source_rel, project_root:, prefer: "minitest")
|
|
101
|
+
base, lib = logical_path(source_rel)
|
|
102
|
+
root = File.expand_path(project_root)
|
|
103
|
+
exact = candidates(base, lib, prefer).find { |rel| File.exist?(File.expand_path(rel, root)) }
|
|
104
|
+
return [exact] if exact&.end_with?("_spec.rb")
|
|
105
|
+
|
|
106
|
+
([exact] + split_test_files(base, lib, root)).compact.uniq
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
110
|
+
# sources. app/foo/bar.rb and lib/foo/bar.rb both -> "foo/bar"; anything
|
|
111
|
+
# else -> the path minus ".rb" (still attempted). Namespaced subdirs are
|
|
112
|
+
# preserved verbatim — structural, never constant resolution.
|
|
113
|
+
#
|
|
114
|
+
# @param source_rel [String] relative source path.
|
|
115
|
+
# @return [Array(String, Boolean)] logical base path and whether it came from lib/.
|
|
116
|
+
def logical_path(source_rel)
|
|
117
|
+
no_ext = source_rel.sub(/\.rb\z/, "")
|
|
118
|
+
if no_ext.start_with?("app/")
|
|
119
|
+
[no_ext.sub(%r{\Aapp/}, ""), false]
|
|
120
|
+
elsif no_ext.start_with?("lib/")
|
|
121
|
+
[no_ext.sub(%r{\Alib/}, ""), true]
|
|
122
|
+
else
|
|
123
|
+
[no_ext, false]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Ordered candidate test paths: _test.rb, then Minitest's test_*.rb, then
|
|
128
|
+
# _spec.rb. lib/ sources also get the test/lib/... and spec/lib/... layouts.
|
|
129
|
+
#
|
|
130
|
+
# @param base [String] logical source path without extension.
|
|
131
|
+
# @param lib [Boolean] whether the source originated from lib/.
|
|
132
|
+
# @param prefer [String] preferred framework name.
|
|
133
|
+
# @return [Array<String>] candidate test paths in preferred order.
|
|
134
|
+
def candidates(base, lib, prefer)
|
|
135
|
+
minitest = ["test/#{base}_test.rb"]
|
|
136
|
+
minitest << "test/lib/#{base}_test.rb" if lib
|
|
137
|
+
unless File.basename(base) == "helper" # test/test_helper.rb is Minitest's support file, not a test
|
|
138
|
+
prefixed = base.sub(%r{[^/]+\z}) { |name| "test_#{name}" }
|
|
139
|
+
minitest << "test/#{prefixed}.rb"
|
|
140
|
+
minitest << "test/lib/#{prefixed}.rb" if lib
|
|
141
|
+
end
|
|
142
|
+
rspec = ["spec/#{base}_spec.rb"]
|
|
143
|
+
rspec << "spec/lib/#{base}_spec.rb" if lib
|
|
144
|
+
prefer == "rspec" ? rspec + minitest : minitest + rspec
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `<basename>_*_test.rb` files in each mirrored test directory (#87). The
|
|
148
|
+
# exact `<basename>_test.rb` name is not included. `lib/` sources also look
|
|
149
|
+
# under `test/lib/`. Names are matched as text, not as a glob, so a basename
|
|
150
|
+
# that contains `*` stays literal. `test/` comes before `test/lib/`, and
|
|
151
|
+
# each directory is sorted.
|
|
152
|
+
#
|
|
153
|
+
# @param base [String] logical source path without extension.
|
|
154
|
+
# @param lib [Boolean] whether the source originated from lib/.
|
|
155
|
+
# @param root [String] expanded project root.
|
|
156
|
+
# @return [Array<String>] relative split-test paths.
|
|
157
|
+
def split_test_files(base, lib, root)
|
|
158
|
+
name = File.basename(base)
|
|
159
|
+
dirs = [mirror_dir("test", base)]
|
|
160
|
+
dirs << mirror_dir("test/lib", base) if lib
|
|
161
|
+
dirs.flat_map { |dir| split_tests_in(root, dir, name, base) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Relative test directory for a logical source path. A source with no
|
|
165
|
+
# subdirectory (`calc`) maps to `prefix` itself, not `prefix/.`.
|
|
166
|
+
#
|
|
167
|
+
# @param prefix [String] `test` or `test/lib`.
|
|
168
|
+
# @param base [String] logical source path without extension.
|
|
169
|
+
# @return [String] relative directory.
|
|
170
|
+
def mirror_dir(prefix, base)
|
|
171
|
+
dir = File.dirname(base)
|
|
172
|
+
dir == "." ? prefix : File.join(prefix, dir)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Split test files directly inside `dir_rel` (not in subdirectories).
|
|
176
|
+
#
|
|
177
|
+
# @param root [String] expanded project root.
|
|
178
|
+
# @param dir_rel [String] relative directory.
|
|
179
|
+
# @param name [String] source basename without extension.
|
|
180
|
+
# @param base [String] logical source path without extension.
|
|
181
|
+
# @return [Array<String>] sorted relative paths.
|
|
182
|
+
def split_tests_in(root, dir_rel, name, base)
|
|
183
|
+
root_abs = File.expand_path(root)
|
|
184
|
+
dir_abs = File.expand_path(dir_rel, root_abs)
|
|
185
|
+
return [] unless inside_project?(dir_abs, root_abs)
|
|
186
|
+
return [] unless File.directory?(dir_abs)
|
|
187
|
+
|
|
188
|
+
Dir.children(dir_abs).filter_map do |entry|
|
|
189
|
+
next unless split_entry?(entry, name)
|
|
190
|
+
next unless File.file?(File.join(dir_abs, entry))
|
|
191
|
+
next if claimed_by_longer_source?(root, base, entry, dir_rel)
|
|
192
|
+
|
|
193
|
+
File.join(dir_rel, entry)
|
|
194
|
+
end.sort
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# True when `dir_abs` is the project root or a directory inside it.
|
|
198
|
+
# A source path such as `../../evil.rb` must not list tests outside the project.
|
|
199
|
+
#
|
|
200
|
+
# @param dir_abs [String] expanded directory.
|
|
201
|
+
# @param root_abs [String] expanded project root.
|
|
202
|
+
# @return [Boolean]
|
|
203
|
+
def inside_project?(dir_abs, root_abs)
|
|
204
|
+
prefix = root_abs == File::SEPARATOR ? File::SEPARATOR : "#{root_abs}#{File::SEPARATOR}"
|
|
205
|
+
dir_abs == root_abs || dir_abs.start_with?(prefix)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# True when `entry` is `<name>_<piece>_test.rb` and not the exact
|
|
209
|
+
# `<name>_test.rb` file.
|
|
210
|
+
#
|
|
211
|
+
# @param entry [String] test file basename.
|
|
212
|
+
# @param name [String] source basename without extension.
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
def split_entry?(entry, name)
|
|
215
|
+
return false unless entry.end_with?("_test.rb")
|
|
216
|
+
|
|
217
|
+
rest = entry.delete_prefix("#{name}_")
|
|
218
|
+
rest != entry && rest.match?(/\A.+_test\.rb\z/)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# True when a longer source file on disk owns this split test. `user.rb`
|
|
222
|
+
# does not take `user_session_test.rb` when `user_session.rb` exists in the
|
|
223
|
+
# same directory, under `app/`, or under `lib/`. An intermediate file
|
|
224
|
+
# counts: `bar_upsert.rb` owns `bar_upsert_guards_test.rb`, so `bar.rb`
|
|
225
|
+
# does not.
|
|
226
|
+
#
|
|
227
|
+
# @param root [String] expanded project root.
|
|
228
|
+
# @param base [String] logical source path without extension.
|
|
229
|
+
# @param entry [String] test file basename.
|
|
230
|
+
# @param scanned_dir [String, nil] relative test directory being scanned.
|
|
231
|
+
# When set, a longer source claims the file only if that source searches
|
|
232
|
+
# this directory. An `app/` source does not take a `test/lib/` file.
|
|
233
|
+
# @return [Boolean]
|
|
234
|
+
def claimed_by_longer_source?(root, base, entry, scanned_dir = nil)
|
|
235
|
+
name = File.basename(base)
|
|
236
|
+
stem = entry.sub(/_test\.rb\z/, "")
|
|
237
|
+
return false unless stem.start_with?("#{name}_")
|
|
238
|
+
|
|
239
|
+
rest = stem.delete_prefix("#{name}_")
|
|
240
|
+
return false if rest.empty?
|
|
241
|
+
|
|
242
|
+
dir = File.dirname(base)
|
|
243
|
+
parts = rest.split("_", -1)
|
|
244
|
+
(1..parts.length).any? do |i|
|
|
245
|
+
longer = "#{name}_#{parts.first(i).join("_")}"
|
|
246
|
+
next false unless longer_owns_entry?(entry, longer)
|
|
247
|
+
|
|
248
|
+
longer_source_exists?(root, dir, longer, scanned_dir)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# True when `entry` is the exact test, or a split test with a non-empty
|
|
253
|
+
# piece, for `stem`. `user_session__test.rb` is neither for
|
|
254
|
+
# `user_session`, so that source does not take it from `user.rb`.
|
|
255
|
+
#
|
|
256
|
+
# @param entry [String] test file basename.
|
|
257
|
+
# @param stem [String] longer source basename without extension.
|
|
258
|
+
# @return [Boolean]
|
|
259
|
+
def longer_owns_entry?(entry, stem)
|
|
260
|
+
entry == "#{stem}_test.rb" || split_entry?(entry, stem)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# True when a longer source file exists beside this logical path.
|
|
264
|
+
# The logical directory itself is checked, so `src/user_session.rb` owns
|
|
265
|
+
# `test/src/user_session_test.rb`. `app/` and `lib/` are checked too, so an
|
|
266
|
+
# `app/` source still yields the name to a `lib/` sibling and the reverse.
|
|
267
|
+
#
|
|
268
|
+
# @param root [String] expanded project root.
|
|
269
|
+
# @param dir [String] logical directory, or `.` when the source has none.
|
|
270
|
+
# @param stem [String] longer source basename without extension.
|
|
271
|
+
# @param scanned_dir [String, nil] relative test directory being scanned.
|
|
272
|
+
# @return [Boolean]
|
|
273
|
+
def longer_source_exists?(root, dir, stem, scanned_dir = nil)
|
|
274
|
+
file = "#{stem}.rb"
|
|
275
|
+
folders = [dir == "." ? nil : dir]
|
|
276
|
+
%w[app lib].each { |prefix| folders << (dir == "." ? prefix : File.join(prefix, dir)) }
|
|
277
|
+
folders.uniq.any? do |folder|
|
|
278
|
+
rel = folder ? File.join(folder, file) : file
|
|
279
|
+
next false unless File.file?(File.join(root, rel))
|
|
280
|
+
next false if scanned_dir && !source_searches_dir?(rel, scanned_dir)
|
|
281
|
+
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# True when pairing this source looks in `scanned_dir`.
|
|
287
|
+
#
|
|
288
|
+
# @param source_rel [String] project-relative source path.
|
|
289
|
+
# @param scanned_dir [String] relative test directory.
|
|
290
|
+
# @return [Boolean]
|
|
291
|
+
def source_searches_dir?(source_rel, scanned_dir)
|
|
292
|
+
base, lib = logical_path(source_rel)
|
|
293
|
+
dirs = [mirror_dir("test", base)]
|
|
294
|
+
dirs << mirror_dir("test/lib", base) if lib
|
|
295
|
+
dirs.include?(scanned_dir)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# Raised only for I/O failures while reading a source file.
|
|
7
|
+
#
|
|
8
|
+
# Prism syntax errors are NOT raised — they are in-band via ParseResult#errors.
|
|
9
|
+
class ParseError < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Thin boundary around Prism.
|
|
12
|
+
#
|
|
13
|
+
# The parse methods return a Prism::ParseResult so all callers use
|
|
14
|
+
# result.value, result.source.source (raw bytes), and result.errors
|
|
15
|
+
# uniformly. No wrapping struct.
|
|
16
|
+
class Parser
|
|
17
|
+
# Parses a file with Prism.
|
|
18
|
+
#
|
|
19
|
+
# @param path [String] source file path.
|
|
20
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
21
|
+
# @raise [Mutineer::ParseError] when file I/O fails.
|
|
22
|
+
def self.parse_file(path)
|
|
23
|
+
Prism.parse_file(path)
|
|
24
|
+
rescue SystemCallError => e
|
|
25
|
+
raise ParseError, e.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Parses source text with Prism.
|
|
29
|
+
#
|
|
30
|
+
# @param source [String] source text.
|
|
31
|
+
# @return [Prism::ParseResult] Prism parse result.
|
|
32
|
+
def self.parse_string(source)
|
|
33
|
+
Prism.parse(source)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The comments of source text, without building its syntax tree: a few
|
|
37
|
+
# times cheaper than {.parse_string} for a caller that needs only these.
|
|
38
|
+
#
|
|
39
|
+
# @param source [String] source text.
|
|
40
|
+
# @return [Array<Prism::Comment>] its comments, in source order.
|
|
41
|
+
def self.comments(source)
|
|
42
|
+
Prism.parse_comments(source)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutineer
|
|
4
|
+
# Coarse run progress on stderr: one line per 10% step of completed mutants
|
|
5
|
+
# (which means every completion on a run smaller than ten). Shared by every
|
|
6
|
+
# backend so a long run is never silent between config resolution and the
|
|
7
|
+
# report. Thread-safe so the daemon's parallel workers can share one instance.
|
|
8
|
+
# Writes only to stderr, keeping `--format json` stdout and `--output` files
|
|
9
|
+
# byte-exact per the Reporter's stream contract.
|
|
10
|
+
class Progress
|
|
11
|
+
# Builds a progress counter for a run of `total` mutants.
|
|
12
|
+
#
|
|
13
|
+
# @param total [Integer] number of mutants that will run.
|
|
14
|
+
# @param io [IO] destination stream (defaults to stderr; injectable for tests).
|
|
15
|
+
def initialize(total, io: $stderr)
|
|
16
|
+
@total = total
|
|
17
|
+
@done = 0
|
|
18
|
+
@last_step = 0
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@io = io
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Records one completed mutant and prints when a 10% boundary is crossed.
|
|
24
|
+
# A zero-total counter never prints (guards the division, and an empty run
|
|
25
|
+
# has nothing to report).
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def tick
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
return if @total.zero?
|
|
31
|
+
|
|
32
|
+
@done += 1
|
|
33
|
+
step = (@done * 10) / @total
|
|
34
|
+
return if step <= @last_step
|
|
35
|
+
|
|
36
|
+
@last_step = step
|
|
37
|
+
@io.puts "[mutineer] #{@done}/#{@total} mutants (#{(@done * 100) / @total}%)"
|
|
38
|
+
end
|
|
39
|
+
rescue IOError, Errno::EPIPE
|
|
40
|
+
# Progress is best-effort: a closed stderr (piped consumer went away) must
|
|
41
|
+
# never kill the run — in the daemon's worker threads a raise here would
|
|
42
|
+
# propagate through Thread#join and abort scoring.
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|