mutaruby 0.1.0 → 0.1.1
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 +23 -0
- data/README.md +13 -4
- data/lib/mutaruby/cli.rb +75 -67
- data/lib/mutaruby/command.rb +40 -31
- data/lib/mutaruby/error.rb +6 -0
- data/lib/mutaruby/generator.rb +59 -74
- data/lib/mutaruby/ignore_file.rb +26 -30
- data/lib/mutaruby/mutation.rb +16 -13
- data/lib/mutaruby/parsing.rb +15 -19
- data/lib/mutaruby/reporters.rb +32 -27
- data/lib/mutaruby/runner.rb +67 -72
- data/lib/mutaruby/sandbox.rb +18 -16
- data/lib/mutaruby/selection.rb +21 -30
- data/lib/mutaruby/version.rb +1 -1
- data/lib/mutaruby.rb +0 -15
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7881b725c1ef86850ff3a13370a7f3217bfd3a943f152d26465ff62439559ef
|
|
4
|
+
data.tar.gz: 55071854a05f0a10e85c749eb3a2984e04591ab6dcca7d69c5cc674675b01796
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab4e2654379b04db8da0e6ef63fedcf918f9eb2fc8632d3632ea1412b86cd43e68e0a3f85ca998b424d48d8da4bd1bc0099d19af0369a124d1687ed49cfa2db1
|
|
7
|
+
data.tar.gz: 2e26578a0581acde2d08c3ab7876a2d5868a05c91af2e1805f0609727582fdf32c2847be1571d42aa64158e0b5e5a0d4e4d5c78548e6b44a41f3513b388f453e
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.1.1] - 2026-10-12
|
|
7
|
+
|
|
8
|
+
mutaruby now scores 100% on its own code: every mutant is killed by a test or listed, with a checkable reason, in
|
|
9
|
+
`.mutaruby-ignore.yml`. Getting there found the bugs below.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Processes that a test run leaves behind (a server, a forked worker) are now killed when the run ends, not only on
|
|
14
|
+
timeout, so they cannot interfere with the next mutant.
|
|
15
|
+
- A project root given through a symlink (for example under `/tmp` on macOS) was copied into the sandboxes as an
|
|
16
|
+
empty directory, so the baseline ran without the project.
|
|
17
|
+
- An ignore entry for a removed sole argument (`mutation: "x =>"`) never matched: the mutant's change text had a
|
|
18
|
+
trailing space. Change texts are now whitespace-normalised the same way on both sides.
|
|
19
|
+
- Captured baseline output is decoded as UTF-8 even when Ruby's default external encoding is something else.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `literal-string` also mutates `%q()` and `%Q[]` strings.
|
|
24
|
+
- `argument-removal` no longer skips calls on the right-hand side of a multiple assignment.
|
|
25
|
+
- Option parsing is available as `Mutaruby::CLI#parse`; `Runner::Options.build` supplies the defaults.
|
|
26
|
+
- Each library file requires what it uses and loads on its own.
|
|
27
|
+
|
|
6
28
|
## [0.1.0] - 2026-10-11
|
|
7
29
|
|
|
8
30
|
First release.
|
|
@@ -27,4 +49,5 @@ First release.
|
|
|
27
49
|
reason, ambiguity and staleness checks, `--strict-ignore` and `--suggest-ignore`.
|
|
28
50
|
- Human, JSON and JUnit XML reports, a mutation score and `--fail-under` for CI.
|
|
29
51
|
|
|
52
|
+
[0.1.1]: https://github.com/Atlas-Authorization/mutaruby/releases/tag/v0.1.1
|
|
30
53
|
[0.1.0]: https://github.com/Atlas-Authorization/mutaruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
# mutaruby
|
|
2
2
|
|
|
3
|
+
[](https://github.com/Atlas-Authorization/mutaruby/actions/workflows/ci.yml)
|
|
4
|
+

|
|
5
|
+
|
|
3
6
|
Free, MIT-licensed mutation testing for Ruby.
|
|
4
7
|
|
|
8
|
+
mutaruby is mutation-tested with itself. In version 0.1.1, all 3,619 mutants of its own code are accounted for:
|
|
9
|
+
3,613 are killed by its tests (499 of them by timeout), and the other 6 are true equivalents, each listed in
|
|
10
|
+
[`.mutaruby-ignore.yml`](.mutaruby-ignore.yml) with a reason you can check. `bundle exec rake mutate` reproduces the
|
|
11
|
+
run; it fails on any surviving mutant or stale ignore entry.
|
|
12
|
+
|
|
5
13
|
mutaruby makes small, deliberate changes to your code, one at a time, and runs your test command against each change.
|
|
6
14
|
If your tests still pass, the change (the *mutant*) **survived**, and that points to code your tests do not really check.
|
|
7
15
|
|
|
@@ -207,7 +215,7 @@ mutant's, are dropped before anything runs. `--list-operators` prints this list.
|
|
|
207
215
|
| `loop-condition-false` | a `while` loop never runs | `while i < n` to `while false` |
|
|
208
216
|
| `literal-int` | an integer becomes n+1, n-1, 0 and 1 (each distinct value once) | `retries = 3` to `4`, `2`, `0`, `1` |
|
|
209
217
|
| `literal-float` | a float becomes 0.0 and x+1.0 | `0.5` to `0.0`, `1.5` |
|
|
210
|
-
| `literal-string` | a quoted string
|
|
218
|
+
| `literal-string` | a quoted string (`'...'`, `"..."`, `%q()`, `%Q[]`) becomes empty; an empty one becomes `"mutaruby"` | `'admin'` to `''` |
|
|
211
219
|
| `literal-symbol` | a `:symbol` becomes `:__mutaruby__` | `:read` to `:__mutaruby__` |
|
|
212
220
|
| `literal-boolean` | `true` and `false` | `strict = true` to `strict = false` |
|
|
213
221
|
| `literal-nil` | `nil` becomes `true` | `cache = nil` to `cache = true` |
|
|
@@ -376,9 +384,10 @@ text. If it is not, the run aborts rather than testing a mutant in the wrong pla
|
|
|
376
384
|
mutant would be "killed" and the score would mean nothing, so mutaruby stops with exit status 2 and shows the tail of
|
|
377
385
|
the output. The baseline's duration also sets the default timeout.
|
|
378
386
|
|
|
379
|
-
**
|
|
380
|
-
|
|
381
|
-
|
|
387
|
+
**No run outlives its mutant.** Each run is the leader of its own process group. When the run times out, and also
|
|
388
|
+
when it finishes normally, whatever is left of the group (a hung test, a forked server, a parallel test worker) gets
|
|
389
|
+
`SIGTERM`, then `SIGKILL` if it is still there two seconds later. Nothing from one mutant's run can interfere with
|
|
390
|
+
the next one.
|
|
382
391
|
|
|
383
392
|
**Site keys instead of line numbers** for the ignore file; see [above](#equivalent-mutants-the-ignore-file).
|
|
384
393
|
|
data/lib/mutaruby/cli.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require 'etc'
|
|
5
|
+
require_relative 'runner'
|
|
6
|
+
require_relative 'reporters'
|
|
5
7
|
|
|
6
8
|
module Mutaruby
|
|
7
9
|
# Command line interface. Exit status: 0 success, 1 score below --fail-under (or stale ignore entries with
|
|
@@ -10,13 +12,16 @@ module Mutaruby
|
|
|
10
12
|
EXIT_OK = 0
|
|
11
13
|
EXIT_FAIL = 1
|
|
12
14
|
EXIT_ERROR = 2
|
|
15
|
+
EXIT_INTERRUPTED = 130
|
|
16
|
+
|
|
17
|
+
attr_reader :options, :flags
|
|
13
18
|
|
|
14
19
|
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
15
20
|
@argv = argv.dup
|
|
16
21
|
@stdout = stdout
|
|
17
22
|
@stderr = stderr
|
|
18
|
-
@
|
|
19
|
-
|
|
23
|
+
@options = Runner::Options.build(jobs: Etc.nprocessors, progress: true)
|
|
24
|
+
@flags = {}
|
|
20
25
|
@include_ops = []
|
|
21
26
|
@exclude_ops = []
|
|
22
27
|
end
|
|
@@ -26,78 +31,83 @@ module Mutaruby
|
|
|
26
31
|
EXIT_OK # --help / --version
|
|
27
32
|
end
|
|
28
33
|
|
|
34
|
+
# Parses the arguments into `options` (and `flags`); the remaining arguments are the file patterns.
|
|
35
|
+
def parse
|
|
36
|
+
parser.parse!(@argv)
|
|
37
|
+
@options.patterns = @argv
|
|
38
|
+
@options.operators = operators
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
29
42
|
private
|
|
30
43
|
|
|
31
44
|
def execute
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return list_operators if @list_operators
|
|
35
|
-
|
|
36
|
-
@o.operators = operators
|
|
37
|
-
raise Error, 'missing --test COMMAND (the command that runs your tests; exit 0 = pass)' if @o.command.nil? && !@list
|
|
45
|
+
parse
|
|
46
|
+
return list_operators if @flags[:list_operators]
|
|
38
47
|
|
|
39
|
-
runner = Runner.new(@
|
|
40
|
-
return list(runner.plan) if @list
|
|
48
|
+
runner = Runner.new(@options, out: @stderr)
|
|
49
|
+
return list(runner.plan) if @flags[:list]
|
|
50
|
+
raise Error, 'missing --test COMMAND (the command that runs your tests; exit 0 = pass)' unless @options.command
|
|
41
51
|
|
|
42
52
|
report = runner.run
|
|
43
53
|
emit(report)
|
|
44
54
|
status(report)
|
|
45
55
|
rescue OptionParser::ParseError, Error, IgnoreFile::Invalid, SpliceError => e
|
|
46
|
-
@stderr.puts "mutaruby: #{e
|
|
56
|
+
@stderr.puts "mutaruby: #{e}"
|
|
47
57
|
EXIT_ERROR
|
|
48
58
|
rescue Interrupt
|
|
49
59
|
@stderr.puts "\nmutaruby: interrupted"
|
|
50
|
-
|
|
60
|
+
EXIT_INTERRUPTED
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
def parser
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
64
|
+
o = @options
|
|
65
|
+
OptionParser.new do |op|
|
|
66
|
+
op.banner = 'Usage: mutaruby --test COMMAND [options] [FILE|GLOB ...] (default: lib/**/*.rb)'
|
|
67
|
+
op.separator ''
|
|
68
|
+
op.separator 'Test oracle:'
|
|
69
|
+
op.on('-t', '--test COMMAND', 'Shell command that runs the tests; exit 0 = mutant survived, non-zero = killed') { |v| o.command = v }
|
|
70
|
+
op.on('-C', '--root DIR', 'Project root (copied for every worker; default: current directory)') { |v| o.root = v }
|
|
71
|
+
op.on('-j', '--jobs N', Integer, "Parallel workers (default: #{Etc.nprocessors})") { |v| o.jobs = v }
|
|
72
|
+
op.on('--timeout SECONDS', Float, 'Per-mutant timeout (default: baseline time x factor + 5s)') { |v| o.timeout = v }
|
|
73
|
+
op.on('--timeout-factor F', Float, 'Multiplier on the baseline time for the default timeout (default: 3)') { |v| o.timeout_factor = v }
|
|
74
|
+
op.on('--copy-exclude GLOB', 'Do not copy matching paths into the sandboxes (repeatable; .git and coverage are always skipped)') { |v| o.copy_excludes += [v] }
|
|
75
|
+
op.separator ''
|
|
76
|
+
op.separator 'Selection:'
|
|
77
|
+
op.on('--since REF', 'Only files changed since a git ref (committed, staged, unstaged, untracked)') { |v| o.since = v }
|
|
78
|
+
op.on('--method PATTERN', 'Only mutants inside matching methods ("Calc#add", "Calc#*", "add"; repeatable)') { |v| o.methods += [v] }
|
|
79
|
+
op.on('--exclude-method PATTERN', 'Skip mutants inside matching methods (repeatable)') { |v| o.exclude_methods += [v] }
|
|
80
|
+
op.on('--line SPEC', 'Only mutants on these lines: [FILE:]LINE or [FILE:]FROM-TO (repeatable)') { |v| o.lines += [Selection.parse_line_spec(v)] }
|
|
81
|
+
op.on('--exclude-line SPEC', 'Skip mutants on these lines (repeatable)') { |v| o.exclude_lines += [Selection.parse_line_spec(v)] }
|
|
82
|
+
op.on('--operators LIST', Array, 'Only these operators (comma-separated; globs allowed, e.g. condition-*)') { |v| @include_ops += v }
|
|
83
|
+
op.on('--exclude-operators LIST', Array, 'Skip these operators (comma-separated; globs allowed)') { |v| @exclude_ops += v }
|
|
84
|
+
op.on('--shard I/N', 'Run only shard I of N (1-based) of the selected mutants') { |v| o.shard = Selection.parse_shard(v) }
|
|
85
|
+
op.separator ''
|
|
86
|
+
op.separator 'Equivalent mutants:'
|
|
87
|
+
op.on('--ignore-file PATH', 'Ignore file (default: .mutaruby-ignore.yml in the root, if present)') { |v| o.ignore_file = v }
|
|
88
|
+
op.on('--strict-ignore', 'Fail (exit 1) when an ignore entry matches no surviving mutant') { @flags[:strict] = true }
|
|
89
|
+
op.on('--suggest-ignore', 'Print ignore-file stubs for the surviving mutants (fill in each reason)') { @flags[:suggest] = true }
|
|
90
|
+
op.separator ''
|
|
91
|
+
op.separator 'Output:'
|
|
92
|
+
op.on('--json PATH', 'Write a JSON report ("-" = stdout)') { |v| @flags[:json] = v }
|
|
93
|
+
op.on('--junit PATH', 'Write a JUnit-style XML report ("-" = stdout)') { |v| @flags[:junit] = v }
|
|
94
|
+
op.on('--fail-under PERCENT', Float, 'Exit 1 when the mutation score is below PERCENT') { |v| @flags[:fail_under] = v }
|
|
95
|
+
op.on('--list', 'List the selected mutants without running anything') { @flags[:list] = true }
|
|
96
|
+
op.on('--list-operators', 'List the mutation operators') { @flags[:list_operators] = true }
|
|
97
|
+
op.on('-q', '--quiet', 'No progress marks') { o.progress = false }
|
|
98
|
+
op.on('-v', '--verbose', 'Also list timeouts and equivalent mutants') { @flags[:verbose] = true }
|
|
99
|
+
op.on('--version', 'Print the version') do
|
|
89
100
|
@stdout.puts "mutaruby #{VERSION}"
|
|
90
101
|
throw :done
|
|
91
102
|
end
|
|
92
|
-
|
|
93
|
-
@stdout.puts
|
|
103
|
+
op.on('-h', '--help', 'Show this help') do
|
|
104
|
+
@stdout.puts op
|
|
94
105
|
throw :done
|
|
95
106
|
end
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
# nil (every operator) unless --operators / --exclude-operators narrowed the set.
|
|
101
111
|
def operators
|
|
102
112
|
return nil if @include_ops.empty? && @exclude_ops.empty?
|
|
103
113
|
|
|
@@ -124,33 +134,31 @@ module Mutaruby
|
|
|
124
134
|
end
|
|
125
135
|
|
|
126
136
|
def emit(report)
|
|
127
|
-
human_io = [@json, @junit].include?('-') ? @stderr : @stdout
|
|
128
|
-
Reporters.human(report, human_io, verbose: @verbose)
|
|
129
|
-
write(@json) { |io| Reporters.json(report, io) } if @json
|
|
130
|
-
write(@junit) { |io| Reporters.junit(report, io) } if @junit
|
|
131
|
-
return unless @suggest
|
|
132
|
-
|
|
137
|
+
human_io = [@flags[:json], @flags[:junit]].include?('-') ? @stderr : @stdout
|
|
138
|
+
Reporters.human(report, human_io, verbose: @flags[:verbose])
|
|
139
|
+
write(@flags[:json]) { |io| Reporters.json(report, io) } if @flags[:json]
|
|
140
|
+
write(@flags[:junit]) { |io| Reporters.junit(report, io) } if @flags[:junit]
|
|
133
141
|
survivors = report.outcomes.select { |o| o.status == :survived }.map(&:mutation)
|
|
142
|
+
return unless @flags[:suggest] && survivors.any?
|
|
143
|
+
|
|
134
144
|
@stderr.puts '# ignore-file stubs (add a reason to each; keep only true equivalents):'
|
|
135
|
-
@stderr.puts IgnoreFile.stub_for(survivors, report.sources)
|
|
145
|
+
@stderr.puts IgnoreFile.stub_for(survivors, report.sources)
|
|
136
146
|
end
|
|
137
147
|
|
|
138
148
|
def write(path, &block)
|
|
139
|
-
|
|
140
|
-
block.call(@stdout)
|
|
141
|
-
else
|
|
142
|
-
File.open(path, 'w', &block)
|
|
143
|
-
end
|
|
149
|
+
path == '-' ? block.call(@stdout) : File.open(path, 'w', &block)
|
|
144
150
|
end
|
|
145
151
|
|
|
146
152
|
def status(report)
|
|
147
153
|
code = EXIT_OK
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
score = report.score
|
|
155
|
+
if @flags[:fail_under] && score && score < @flags[:fail_under]
|
|
156
|
+
@stderr.puts format('mutaruby: score %s is below --fail-under %.2f%%', Reporters.score_text(report), @flags[:fail_under])
|
|
150
157
|
code = EXIT_FAIL
|
|
151
158
|
end
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
n = report.stale.size
|
|
160
|
+
if @flags[:strict] && n.positive?
|
|
161
|
+
@stderr.puts "mutaruby: #{n} stale ignore entr#{n == 1 ? 'y' : 'ies'} (--strict-ignore)"
|
|
154
162
|
code = EXIT_FAIL
|
|
155
163
|
end
|
|
156
164
|
code
|
data/lib/mutaruby/command.rb
CHANGED
|
@@ -12,55 +12,61 @@ module Mutaruby
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
OUTPUT_TAIL = 4000
|
|
15
|
+
# After SIGTERM, a process group gets this many seconds to exit before SIGKILL.
|
|
16
|
+
KILL_GRACE = 2
|
|
17
|
+
POLL = 0.05
|
|
15
18
|
|
|
16
|
-
def initialize(command, root:,
|
|
19
|
+
def initialize(command, root:, kill_grace: KILL_GRACE)
|
|
17
20
|
@command = command
|
|
21
|
+
@kill_grace = kill_grace
|
|
18
22
|
@root = File.expand_path(root)
|
|
19
|
-
@
|
|
20
|
-
@live = {}
|
|
23
|
+
@live = []
|
|
21
24
|
@lock = Mutex.new
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Kills every run still in progress (used when mutaruby itself is interrupted: the runs live in their own process
|
|
25
28
|
# groups, so a Ctrl-C in the terminal does not reach them).
|
|
26
29
|
def kill_all
|
|
27
|
-
|
|
28
|
-
pids.each { |pid| kill_group(pid) }
|
|
30
|
+
@lock.synchronize { @live.each { |pid| kill_group(pid) } }
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# dir: sandbox project dir. timeout: seconds (nil = none). capture: keep an output tail (for the baseline report).
|
|
32
34
|
def run(dir, timeout: nil, env: {}, capture: false)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
return execute(dir, timeout, env, File::NULL) unless capture
|
|
36
|
+
|
|
37
|
+
Tempfile.create do |log|
|
|
38
|
+
result = execute(dir, timeout, env, log)
|
|
39
|
+
log.rewind
|
|
40
|
+
result.output = tail(log.read)
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def execute(dir, timeout, env, out)
|
|
35
48
|
started = monotonic
|
|
36
|
-
pid = Process.spawn(child_env(dir).merge(
|
|
49
|
+
pid = Process.spawn(child_env(dir).merge(env), '/bin/sh', '-c', @command,
|
|
37
50
|
chdir: dir, pgroup: true, in: File::NULL, out: out, err: out)
|
|
38
|
-
@lock.synchronize { @live
|
|
51
|
+
@lock.synchronize { @live << pid }
|
|
39
52
|
waiter = Process.detach(pid)
|
|
40
|
-
timed_out = waiter.join(timeout).nil?
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
waiter.join
|
|
44
|
-
end
|
|
53
|
+
timed_out = waiter.join(timeout).nil?
|
|
54
|
+
# A timed-out run, and anything a finished run left behind (a server, a forked worker), must not outlive it.
|
|
55
|
+
kill_group(pid)
|
|
45
56
|
st = waiter.value
|
|
46
57
|
@lock.synchronize { @live.delete(pid) }
|
|
47
58
|
status = if timed_out then :timeout
|
|
48
59
|
elsif st.success? then :passed
|
|
49
60
|
else :failed
|
|
50
61
|
end
|
|
51
|
-
Result.new(status: status, exitstatus: st.exitstatus, seconds: monotonic - started
|
|
52
|
-
output: log ? tail(log.path) : nil)
|
|
53
|
-
ensure
|
|
54
|
-
log&.close!
|
|
62
|
+
Result.new(status: status, exitstatus: st.exitstatus, seconds: monotonic - started)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
private
|
|
58
|
-
|
|
59
65
|
# Bundler exports the ORIGINAL Gemfile path to child processes. Left as is, `bundle exec` in the sandbox would
|
|
60
66
|
# resolve the Gemfile (and a `gemspec` line's lib/ directory) from the user's checkout and test the unmutated code.
|
|
67
|
+
# The per-mutant variables are always set or cleared explicitly, so a mutaruby run nested inside another one's
|
|
68
|
+
# test command does not inherit the outer run's values.
|
|
61
69
|
def child_env(dir)
|
|
62
|
-
# The per-mutant variables are always set or cleared explicitly, so a mutaruby run nested inside another one's
|
|
63
|
-
# test command does not inherit the outer run's mutant id.
|
|
64
70
|
env = { 'MUTARUBY' => '1', 'MUTARUBY_MUTANT_ID' => nil, 'MUTARUBY_FILE' => nil }
|
|
65
71
|
%w[BUNDLE_GEMFILE BUNDLE_APP_CONFIG].each do |k|
|
|
66
72
|
v = ENV.fetch(k, nil)
|
|
@@ -72,13 +78,18 @@ module Mutaruby
|
|
|
72
78
|
env
|
|
73
79
|
end
|
|
74
80
|
|
|
81
|
+
# SIGTERM to the whole group, up to kill_grace seconds to exit, then SIGKILL.
|
|
75
82
|
def kill_group(pid)
|
|
76
|
-
|
|
77
|
-
deadline = monotonic +
|
|
78
|
-
sleep
|
|
79
|
-
|
|
83
|
+
signal_group('TERM', pid)
|
|
84
|
+
deadline = monotonic + @kill_grace
|
|
85
|
+
sleep POLL while monotonic < deadline && alive?(pid)
|
|
86
|
+
signal_group('KILL', pid)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def signal_group(sig, pid)
|
|
90
|
+
Process.kill(sig, -pid)
|
|
80
91
|
rescue Errno::ESRCH, Errno::EPERM
|
|
81
|
-
|
|
92
|
+
# the group is already gone
|
|
82
93
|
end
|
|
83
94
|
|
|
84
95
|
def alive?(pid)
|
|
@@ -88,10 +99,8 @@ module Mutaruby
|
|
|
88
99
|
false
|
|
89
100
|
end
|
|
90
101
|
|
|
91
|
-
def tail(
|
|
92
|
-
|
|
93
|
-
s = s[-OUTPUT_TAIL..] if s.bytesize > OUTPUT_TAIL
|
|
94
|
-
s.force_encoding(Encoding::UTF_8).scrub
|
|
102
|
+
def tail(bytes)
|
|
103
|
+
bytes.byteslice([bytes.bytesize - OUTPUT_TAIL, 0].max..).force_encoding(Encoding::UTF_8).scrub
|
|
95
104
|
end
|
|
96
105
|
|
|
97
106
|
def monotonic
|