mutineer 1.3.0 → 1.5.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/CHANGELOG.md +317 -0
- data/README.md +200 -28
- data/lib/mutineer/changed_lines.rb +57 -2
- data/lib/mutineer/cli.rb +142 -92
- data/lib/mutineer/config.rb +215 -56
- data/lib/mutineer/coverage_map.rb +161 -14
- data/lib/mutineer/daemon_backend.rb +3 -0
- data/lib/mutineer/daemon_client.rb +170 -13
- data/lib/mutineer/daemon_server.rb +2 -1
- data/lib/mutineer/external_backend.rb +2 -2
- data/lib/mutineer/isolation.rb +91 -6
- 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 +14 -68
- data/lib/mutineer/minitest_integration.rb +27 -10
- data/lib/mutineer/mutant_id.rb +5 -4
- data/lib/mutineer/mutator_registry.rb +12 -3
- data/lib/mutineer/mutators/array_literal.rb +0 -7
- data/lib/mutineer/mutators/base.rb +63 -4
- data/lib/mutineer/mutators/chain_link.rb +0 -7
- data/lib/mutineer/mutators/collection_method.rb +1 -1
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/operand_removal.rb +0 -7
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/return_nil.rb +5 -11
- data/lib/mutineer/mutators/string_literal.rb +1 -1
- data/lib/mutineer/pairing.rb +222 -15
- data/lib/mutineer/parser.rb +12 -3
- data/lib/mutineer/project.rb +7 -0
- data/lib/mutineer/rails_worker_db.rb +1 -1
- data/lib/mutineer/reporter.rb +288 -52
- data/lib/mutineer/result.rb +37 -1
- data/lib/mutineer/runner.rb +133 -16
- data/lib/mutineer/statement_lines.rb +143 -0
- data/lib/mutineer/test_runners/minitest.rb +4 -2
- data/lib/mutineer/test_runners/rspec.rb +243 -3
- data/lib/mutineer/test_runners.rb +5 -3
- data/lib/mutineer/version.rb +1 -1
- data/lib/mutineer/worker_pool.rb +4 -2
- data/lib/mutineer.rb +3 -0
- metadata +10 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
module Mutators
|
|
7
|
+
# Operator-assignment mutator (Tier-2).
|
|
8
|
+
#
|
|
9
|
+
# Swaps the operator of a compound assignment, one mutation per
|
|
10
|
+
# occurrence: `t += x` -> `t -= x`. Prism parses `t += x` as its own
|
|
11
|
+
# node, not as a CallNode, so the arithmetic mutator never sees it.
|
|
12
|
+
#
|
|
13
|
+
# The mapping is the same as for arithmetic. Other compound operators
|
|
14
|
+
# (`||=`, `&&=`, `|=`, `<<=` and the rest) are not changed.
|
|
15
|
+
class OperatorAssignment < Base
|
|
16
|
+
# Token replacements for compound arithmetic operators.
|
|
17
|
+
REPLACEMENTS = {
|
|
18
|
+
:+ => "-=", :- => "+=", :* => "/=", :/ => "*=", :% => "*=", :** => "*="
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Visits `t += x`.
|
|
22
|
+
#
|
|
23
|
+
# @param node [Prism::LocalVariableOperatorWriteNode] node to inspect.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def visit_local_variable_operator_write_node(node)
|
|
26
|
+
emit(node)
|
|
27
|
+
super # the value can hold its own compound assignment
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Visits `@t += x`.
|
|
31
|
+
#
|
|
32
|
+
# @param node [Prism::InstanceVariableOperatorWriteNode] node to inspect.
|
|
33
|
+
# @return [void]
|
|
34
|
+
def visit_instance_variable_operator_write_node(node)
|
|
35
|
+
emit(node)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Visits `@@t += x`.
|
|
40
|
+
#
|
|
41
|
+
# @param node [Prism::ClassVariableOperatorWriteNode] node to inspect.
|
|
42
|
+
# @return [void]
|
|
43
|
+
def visit_class_variable_operator_write_node(node)
|
|
44
|
+
emit(node)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Visits `$t += x`.
|
|
49
|
+
#
|
|
50
|
+
# @param node [Prism::GlobalVariableOperatorWriteNode] node to inspect.
|
|
51
|
+
# @return [void]
|
|
52
|
+
def visit_global_variable_operator_write_node(node)
|
|
53
|
+
emit(node)
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Visits `T += x`.
|
|
58
|
+
#
|
|
59
|
+
# @param node [Prism::ConstantOperatorWriteNode] node to inspect.
|
|
60
|
+
# @return [void]
|
|
61
|
+
def visit_constant_operator_write_node(node)
|
|
62
|
+
emit(node)
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Visits `A::T += x` and `::T += x`.
|
|
67
|
+
#
|
|
68
|
+
# @param node [Prism::ConstantPathOperatorWriteNode] node to inspect.
|
|
69
|
+
# @return [void]
|
|
70
|
+
def visit_constant_path_operator_write_node(node)
|
|
71
|
+
emit(node)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Visits `a.b += x` and `a&.b += x`.
|
|
76
|
+
#
|
|
77
|
+
# @param node [Prism::CallOperatorWriteNode] node to inspect.
|
|
78
|
+
# @return [void]
|
|
79
|
+
def visit_call_operator_write_node(node)
|
|
80
|
+
emit(node)
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Visits `a[i] += x`.
|
|
85
|
+
#
|
|
86
|
+
# @param node [Prism::IndexOperatorWriteNode] node to inspect.
|
|
87
|
+
# @return [void]
|
|
88
|
+
def visit_index_operator_write_node(node)
|
|
89
|
+
emit(node)
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# Emits a mutation when the node's operator has a replacement.
|
|
96
|
+
#
|
|
97
|
+
# @param node [Prism::Node] a node with `binary_operator` and `binary_operator_loc`.
|
|
98
|
+
# @return [void]
|
|
99
|
+
def emit(node)
|
|
100
|
+
replacement = REPLACEMENTS[node.binary_operator]
|
|
101
|
+
return unless replacement
|
|
102
|
+
|
|
103
|
+
loc = node.binary_operator_loc
|
|
104
|
+
@mutations << Mutation.new(
|
|
105
|
+
start_offset: loc.start_offset,
|
|
106
|
+
end_offset: loc.end_offset,
|
|
107
|
+
replacement: replacement,
|
|
108
|
+
operator: :operator_assignment
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -16,11 +16,12 @@ module Mutineer
|
|
|
16
16
|
def mutations_for(subject, source)
|
|
17
17
|
@source = source
|
|
18
18
|
@mutations = []
|
|
19
|
-
body = subject.def_node.body
|
|
20
|
-
if body
|
|
21
|
-
body.accept(self) # rule 1 (explicit return nodes in this body)
|
|
22
|
-
final_expression_nil(body) # rule 2 (method's final expression)
|
|
19
|
+
@body = subject.def_node.body
|
|
20
|
+
if @body
|
|
21
|
+
@body.accept(self) # rule 1 (explicit return nodes in this body)
|
|
22
|
+
final_expression_nil(@body) # rule 2 (method's final expression)
|
|
23
23
|
end
|
|
24
|
+
drop_dangling_heredocs
|
|
24
25
|
@mutations
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -39,13 +40,6 @@ module Mutineer
|
|
|
39
40
|
super
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
# Nested method definitions are discovered as their own subjects; do not
|
|
43
|
-
# recurse into them (prevents double-counting their statements).
|
|
44
|
-
#
|
|
45
|
-
# @param node [Prism::DefNode] nested definition node.
|
|
46
|
-
# @return [void]
|
|
47
|
-
def visit_def_node(node); end
|
|
48
|
-
|
|
49
43
|
private
|
|
50
44
|
|
|
51
45
|
# Mutates a method's final expression to nil when eligible.
|
|
@@ -15,7 +15,7 @@ module Mutineer
|
|
|
15
15
|
# @param node [Prism::StringNode] node to inspect.
|
|
16
16
|
# @return [void]
|
|
17
17
|
def visit_string_node(node)
|
|
18
|
-
#
|
|
18
|
+
# Only plain "..." / '...' quotes. opening is nil for
|
|
19
19
|
# interpolation parts and %w[] elements; heredocs/%-literals use a
|
|
20
20
|
# different opening token. Skipping them keeps mutants re-parseable.
|
|
21
21
|
if %w[" '].include?(node.opening)
|
data/lib/mutineer/pairing.rb
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "project_path"
|
|
4
|
+
|
|
3
5
|
module Mutineer
|
|
4
6
|
# Source -> test pairing by path convention (#11). Pure stdlib path logic:
|
|
5
7
|
# no Rails, no class loading, no process. Two jobs:
|
|
6
|
-
# * expand_sources — a directory argument becomes its sorted **/*.rb files
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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.
|
|
11
16
|
#
|
|
12
17
|
# Independently unit-testable: every method is pure in/out over the
|
|
13
18
|
# filesystem, so the pairing contract is exercised with plain fixtures, no
|
|
@@ -17,37 +22,88 @@ module Mutineer
|
|
|
17
22
|
|
|
18
23
|
# Expand each positional source: a directory -> its sorted **/*.rb files
|
|
19
24
|
# (relative to project_root); a file (or glob, or anything non-directory)
|
|
20
|
-
# ->
|
|
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.
|
|
21
28
|
#
|
|
22
29
|
# @param args [Array<String>] source paths or directories.
|
|
23
30
|
# @param project_root [String] repository root for relative expansion.
|
|
24
31
|
# @return [Array<String>] flattened, deduped source file list.
|
|
25
32
|
def expand_sources(args, project_root:)
|
|
26
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
|
|
27
37
|
Array(args).flat_map do |arg|
|
|
28
38
|
abs = File.expand_path(arg, root)
|
|
29
39
|
if File.directory?(abs)
|
|
30
|
-
Dir.glob(File.join(abs, "**", "*.rb")).sort.map { |f| f
|
|
40
|
+
Dir.glob(File.join(abs, "**", "*.rb")).sort.map { |f| existing_relative(f, roots) || f }
|
|
31
41
|
else
|
|
32
|
-
|
|
42
|
+
# `..` resolves as text, as FileSwap and the daemon already read it.
|
|
43
|
+
[(existing_relative(abs, roots) if File.exist?(abs)) || arg]
|
|
33
44
|
end
|
|
34
45
|
end.uniq
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
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.
|
|
41
81
|
#
|
|
42
82
|
# @param source_rel [String] relative source path.
|
|
43
83
|
# @param project_root [String] repository root for existence checks.
|
|
44
84
|
# @param prefer [String] preferred framework name.
|
|
45
85
|
# @return [String, nil] existing test path or nil.
|
|
46
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")
|
|
47
101
|
base, lib = logical_path(source_rel)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
107
|
end
|
|
52
108
|
|
|
53
109
|
# Strip the source root to a logical path (no ".rb") and flag lib/
|
|
@@ -87,5 +143,156 @@ module Mutineer
|
|
|
87
143
|
rspec << "spec/lib/#{base}_spec.rb" if lib
|
|
88
144
|
prefer == "rspec" ? rspec + minitest : minitest + rspec
|
|
89
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
|
|
90
297
|
end
|
|
91
298
|
end
|
data/lib/mutineer/parser.rb
CHANGED
|
@@ -10,9 +10,9 @@ module Mutineer
|
|
|
10
10
|
|
|
11
11
|
# Thin boundary around Prism.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
# result.source.source (raw bytes), and result.errors
|
|
15
|
-
# struct.
|
|
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
16
|
class Parser
|
|
17
17
|
# Parses a file with Prism.
|
|
18
18
|
#
|
|
@@ -32,5 +32,14 @@ module Mutineer
|
|
|
32
32
|
def self.parse_string(source)
|
|
33
33
|
Prism.parse(source)
|
|
34
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
|
|
35
44
|
end
|
|
36
45
|
end
|
data/lib/mutineer/project.rb
CHANGED
|
@@ -82,6 +82,9 @@ module Mutineer
|
|
|
82
82
|
# @param node [Prism::CallNode] call node.
|
|
83
83
|
# @return [void]
|
|
84
84
|
def visit_call_node(node)
|
|
85
|
+
if %i[public private protected].include?(node.name) && node.receiver.nil? && node.arguments.nil?
|
|
86
|
+
@module_function_active = false # without arguments, Ruby goes back to instance methods
|
|
87
|
+
end
|
|
85
88
|
if node.name == :module_function && node.receiver.nil?
|
|
86
89
|
args = node.arguments&.arguments || []
|
|
87
90
|
if args.empty?
|
|
@@ -109,7 +112,9 @@ module Mutineer
|
|
|
109
112
|
return unless node.expression.is_a?(Prism::SelfNode)
|
|
110
113
|
|
|
111
114
|
@singleton_depth += 1
|
|
115
|
+
saved_active = @module_function_active
|
|
112
116
|
super
|
|
117
|
+
@module_function_active = saved_active # a visibility call in here is not the module body's
|
|
113
118
|
@singleton_depth -= 1
|
|
114
119
|
end
|
|
115
120
|
|
|
@@ -126,7 +131,9 @@ module Mutineer
|
|
|
126
131
|
singleton: !node.receiver.nil? || @singleton_depth.positive? || @module_function_active,
|
|
127
132
|
def_node: node
|
|
128
133
|
)
|
|
134
|
+
saved_active = @module_function_active
|
|
129
135
|
super
|
|
136
|
+
@module_function_active = saved_active # a visibility call in a method body runs only when it is called
|
|
130
137
|
end
|
|
131
138
|
|
|
132
139
|
private
|
|
@@ -65,7 +65,7 @@ module Mutineer
|
|
|
65
65
|
unless adapter.start_with?("sqlite")
|
|
66
66
|
raise NotImplementedError,
|
|
67
67
|
"worker-DB isolation currently provisions SQLite only (got adapter #{adapter.inspect}); " \
|
|
68
|
-
"Postgres per-worker provisioning is not yet supported. Use a SQLite test DB, or drop --
|
|
68
|
+
"Postgres per-worker provisioning is not yet supported. Use a SQLite test DB with --daemon, or drop --daemon to run serially."
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
per_worker_config(hash, worker)
|