evilution 0.22.0 → 0.22.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/.beads/interactions.jsonl +4 -0
- data/.claude/settings.json +5 -0
- data/CHANGELOG.md +18 -0
- data/README.md +14 -3
- data/lib/evilution/integration/base.rb +37 -11
- data/lib/evilution/isolation/fork.rb +11 -3
- data/lib/evilution/isolation/in_process.rb +12 -3
- data/lib/evilution/mutator/operator/symbol_literal.rb +9 -0
- data/lib/evilution/reporter/cli.rb +19 -0
- data/lib/evilution/reporter/html.rb +10 -1
- data/lib/evilution/reporter/json.rb +12 -1
- data/lib/evilution/result/mutation_result.rb +9 -2
- data/lib/evilution/runner.rb +25 -5
- data/lib/evilution/version.rb +1 -1
- data/script/memory_check +5 -5
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41de783fc5459691618f0638cbbe4d47b4f41173b38ffaa357ad00f51831724d
|
|
4
|
+
data.tar.gz: 5db9acf814e90669a1d2596b29b71f72e405328b28354f82121bcc23f0ad1a90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ed3a8f8c3ce59bcb13e18c115c7a1013e2aaa53aab8672e486ff0ef1abe6857b3771628f0f4d8f2c7925c46610c4be315f9c2b13bc60d124e44cf69773b1100
|
|
7
|
+
data.tar.gz: e77b777bbb7030ce89904e504a118adbffe7e7a991f8bc5f9504097b7becc311f3b93676bb0914b5ab5b4155d061448ca387025e86a785b1cbb31ed4e69f1f73
|
data/.beads/interactions.jsonl
CHANGED
|
@@ -10,3 +10,7 @@
|
|
|
10
10
|
{"id":"int-0f073191","kind":"field_change","created_at":"2026-04-09T13:03:11.468115004Z","actor":"Denis Kiselev","issue_id":"EV-85","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
11
11
|
{"id":"int-91a9616c","kind":"field_change","created_at":"2026-04-09T13:04:05.459458165Z","actor":"Denis Kiselev","issue_id":"EV-69","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}}
|
|
12
12
|
{"id":"int-b4fe2b7b","kind":"field_change","created_at":"2026-04-09T13:42:59.996305852Z","actor":"Denis Kiselev","issue_id":"EV-277","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
|
13
|
+
{"id":"int-6671aef1","kind":"field_change","created_at":"2026-04-10T09:05:06.955840839Z","actor":"Denis Kiselev","issue_id":"EV-z42m","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #655"}}
|
|
14
|
+
{"id":"int-3a6ffc92","kind":"field_change","created_at":"2026-04-10T10:26:29.483210031Z","actor":"Denis Kiselev","issue_id":"EV-sgsb","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #656"}}
|
|
15
|
+
{"id":"int-1df69312","kind":"field_change","created_at":"2026-04-10T10:40:18.391377715Z","actor":"Denis Kiselev","issue_id":"EV-1l8w","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged"}}
|
|
16
|
+
{"id":"int-427bdc14","kind":"field_change","created_at":"2026-04-10T12:40:12.974701482Z","actor":"Denis Kiselev","issue_id":"EV-k6cz","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged as PR #659 — capture error_class and error_backtrace in MutationResult, thread through isolators + runner + JSON reporter, log in --verbose"}}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.22.1] - 2026-04-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Error class and backtrace capture** — `MutationResult` now stores `error_class` and `error_backtrace` alongside `error_message`; the backtrace array is duplicated and frozen to keep results immutable; both fields are threaded through `Isolation::Fork` (Marshal-safe across the IPC pipe), `Isolation::InProcess`, and the runner's `compact_result` / `rebuild_results` path (#648, PR #659)
|
|
8
|
+
- **Verbose error diagnostics** — `--verbose` now logs error class, message, and the first 5 backtrace lines for errored mutations (previously `--verbose` only showed memory/GC stats, leaving errors invisible) (#648, PR #659)
|
|
9
|
+
- **Error details in JSON reports** — JSON reporter output includes `error_class` and `error_backtrace` fields under `errors[]` entries when present, so downstream tools (CI, MCP consumers) can surface failure causes without re-running (#648, PR #659)
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Silent load-time crashes in `Isolation::Fork`** — mutations that raised non-`SyntaxError` script errors at load time (e.g. `NoMethodError: super called outside of method`) escaped `Integration::Base`'s narrow rescue and either surfaced cryptically or went silent under fork isolation; both isolators now rescue `ScriptError, StandardError` as a safety net and report them as `:error` status with full class and backtrace (#646, PR #656)
|
|
14
|
+
- **`symbol_literal` operator breaking keyword arguments** — mutating symbols in label form (`foo:` inside hash literals or keyword arguments) produced invalid Ruby source; the operator now detects label-form symbols via Prism's `closing_loc` and skips them, only mutating standalone symbol literals (`:foo`) (#647, PR #657)
|
|
15
|
+
- **Syntax errors in mutated source crashing in-process runs** — `Integration::Base#apply_mutation` now captures `SyntaxError` during `require`/`load` and returns a structured error result instead of propagating the exception up through `call`; error results include the error class and backtrace for diagnosis (#644, #645, PR #653, PR #655)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Integration::Base refactor** — `apply_mutation` split into `apply_via_require` and `apply_via_load` helpers; rescue scope moved from `#call` to `#apply_mutation` so load-time errors return a result hash while abstract-method `NotImplementedError`s still propagate as intended
|
|
20
|
+
|
|
3
21
|
## [0.22.0] - 2026-04-09
|
|
4
22
|
|
|
5
23
|
### Added
|
data/README.md
CHANGED
|
@@ -56,7 +56,7 @@ evilution [command] [options] [files...]
|
|
|
56
56
|
| `-j`, `--jobs N` | Integer | 1 | Number of parallel workers. Uses demand-driven work distribution with pipe-based IPC. |
|
|
57
57
|
| `--no-baseline` | Boolean | _(enabled)_ | Skip baseline test suite check. By default, a baseline run detects pre-existing failures and marks those mutations as `neutral`. |
|
|
58
58
|
| `--fail-fast [N]` | Integer | _(none)_ | Stop after N surviving mutants (default 1 if no value given). |
|
|
59
|
-
| `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation. |
|
|
59
|
+
| `-v`, `--verbose` | Boolean | false | Verbose output with RSS memory and GC stats per phase and per mutation; also prints error class, message, and first 5 backtrace lines for errored mutations. |
|
|
60
60
|
| `--suggest-tests` | Boolean | false | Generate concrete test code in suggestions (RSpec or Minitest, based on `--integration`). |
|
|
61
61
|
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
62
62
|
| `--stdin` | Boolean | false | Read target file paths from stdin (one per line). |
|
|
@@ -163,7 +163,14 @@ Use `--format json` for machine-readable output. Schema:
|
|
|
163
163
|
],
|
|
164
164
|
"killed": ["... same shape as survived entries ..."],
|
|
165
165
|
"timed_out": ["... same shape as survived entries ..."],
|
|
166
|
-
"errors": [
|
|
166
|
+
"errors": [
|
|
167
|
+
{
|
|
168
|
+
"... same shape as survived entries, plus: ...": "",
|
|
169
|
+
"error_message": "string (optional) — error message from the failing mutation",
|
|
170
|
+
"error_class": "string (optional) — exception class name (e.g. 'SyntaxError', 'NoMethodError')",
|
|
171
|
+
"error_backtrace": ["string (optional) — first 5 backtrace lines from the exception"]
|
|
172
|
+
}
|
|
173
|
+
]
|
|
167
174
|
}
|
|
168
175
|
```
|
|
169
176
|
|
|
@@ -364,7 +371,11 @@ For each entry in `survived[]`:
|
|
|
364
371
|
4. Write a test that would fail if the mutation were applied
|
|
365
372
|
5. Re-run evilution on just that file to verify the mutant is now killed
|
|
366
373
|
|
|
367
|
-
### 7.
|
|
374
|
+
### 7. Diagnosing errored mutations
|
|
375
|
+
|
|
376
|
+
Entries in the JSON `errors[]` array represent mutations that raised an exception (syntax error, load failure, or runtime crash) rather than producing a test outcome. Each entry includes `error_class`, `error_message`, and the first 5 `error_backtrace` lines. Use these fields to decide whether the error is a bug in the mutation operator (file an issue), a load-time problem in the mutated source (often `NoMethodError: super called outside of method` or constant-redefinition issues), or a genuine crash that the original tests should have caught. Run with `--verbose` to stream the same error details to stderr during the run.
|
|
377
|
+
|
|
378
|
+
### 8. CI gate
|
|
368
379
|
|
|
369
380
|
```bash
|
|
370
381
|
bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
|
|
@@ -22,7 +22,9 @@ class Evilution::Integration::Base
|
|
|
22
22
|
@temp_dir = nil
|
|
23
23
|
ensure_framework_loaded
|
|
24
24
|
fire_hook(:mutation_insert_pre, mutation: mutation, file_path: mutation.file_path)
|
|
25
|
-
apply_mutation(mutation)
|
|
25
|
+
load_error = apply_mutation(mutation)
|
|
26
|
+
return load_error if load_error
|
|
27
|
+
|
|
26
28
|
fire_hook(:mutation_insert_post, mutation: mutation, file_path: mutation.file_path)
|
|
27
29
|
run_tests(mutation)
|
|
28
30
|
ensure
|
|
@@ -58,18 +60,42 @@ class Evilution::Integration::Base
|
|
|
58
60
|
subpath = resolve_require_subpath(mutation.file_path)
|
|
59
61
|
|
|
60
62
|
if subpath
|
|
61
|
-
|
|
62
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
63
|
-
File.write(dest, mutation.mutated_source)
|
|
64
|
-
$LOAD_PATH.unshift(@temp_dir)
|
|
65
|
-
displace_loaded_feature(mutation.file_path)
|
|
63
|
+
apply_via_require(mutation, subpath)
|
|
66
64
|
else
|
|
67
|
-
|
|
68
|
-
dest = File.join(@temp_dir, absolute)
|
|
69
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
70
|
-
File.write(dest, mutation.mutated_source)
|
|
71
|
-
load(dest)
|
|
65
|
+
apply_via_load(mutation)
|
|
72
66
|
end
|
|
67
|
+
nil
|
|
68
|
+
rescue SyntaxError => e
|
|
69
|
+
{
|
|
70
|
+
passed: false,
|
|
71
|
+
error: "syntax error in mutated source: #{e.message}",
|
|
72
|
+
error_class: e.class.name,
|
|
73
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
74
|
+
}
|
|
75
|
+
rescue ScriptError, StandardError => e
|
|
76
|
+
{
|
|
77
|
+
passed: false,
|
|
78
|
+
error: "#{e.class}: #{e.message}",
|
|
79
|
+
error_class: e.class.name,
|
|
80
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def apply_via_require(mutation, subpath)
|
|
85
|
+
dest = File.join(@temp_dir, subpath)
|
|
86
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
87
|
+
File.write(dest, mutation.mutated_source)
|
|
88
|
+
$LOAD_PATH.unshift(@temp_dir)
|
|
89
|
+
displace_loaded_feature(mutation.file_path)
|
|
90
|
+
require(subpath.delete_suffix(".rb"))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def apply_via_load(mutation)
|
|
94
|
+
absolute = File.expand_path(mutation.file_path)
|
|
95
|
+
dest = File.join(@temp_dir, absolute)
|
|
96
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
97
|
+
File.write(dest, mutation.mutated_source)
|
|
98
|
+
load(dest)
|
|
73
99
|
end
|
|
74
100
|
|
|
75
101
|
def restore_original(_mutation)
|
|
@@ -57,8 +57,13 @@ class Evilution::Isolation::Fork
|
|
|
57
57
|
def execute_in_child(mutation, test_command)
|
|
58
58
|
result = test_command.call(mutation)
|
|
59
59
|
{ child_rss_kb: Evilution::Memory.rss_kb }.merge(result)
|
|
60
|
-
rescue StandardError => e
|
|
61
|
-
{
|
|
60
|
+
rescue ScriptError, StandardError => e
|
|
61
|
+
{
|
|
62
|
+
passed: false,
|
|
63
|
+
error: e.message,
|
|
64
|
+
error_class: e.class.name,
|
|
65
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
66
|
+
}
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
def wait_for_result(pid, read_io, timeout)
|
|
@@ -107,7 +112,10 @@ class Evilution::Isolation::Fork
|
|
|
107
112
|
duration: duration,
|
|
108
113
|
test_command: result[:test_command],
|
|
109
114
|
child_rss_kb: result[:child_rss_kb],
|
|
110
|
-
parent_rss_kb: parent_rss_kb
|
|
115
|
+
parent_rss_kb: parent_rss_kb,
|
|
116
|
+
error_message: result[:error],
|
|
117
|
+
error_class: result[:error_class],
|
|
118
|
+
error_backtrace: result[:error_backtrace]
|
|
111
119
|
)
|
|
112
120
|
end
|
|
113
121
|
end
|
|
@@ -34,8 +34,14 @@ class Evilution::Isolation::InProcess
|
|
|
34
34
|
{ timeout: false }.merge(result)
|
|
35
35
|
rescue Timeout::Error
|
|
36
36
|
{ timeout: true }
|
|
37
|
-
rescue StandardError => e
|
|
38
|
-
{
|
|
37
|
+
rescue ScriptError, StandardError => e
|
|
38
|
+
{
|
|
39
|
+
timeout: false,
|
|
40
|
+
passed: false,
|
|
41
|
+
error: e.message,
|
|
42
|
+
error_class: e.class.name,
|
|
43
|
+
error_backtrace: Array(e.backtrace).first(5)
|
|
44
|
+
}
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def suppress_output
|
|
@@ -74,7 +80,10 @@ class Evilution::Isolation::InProcess
|
|
|
74
80
|
test_command: result[:test_command],
|
|
75
81
|
child_rss_kb: rss_after,
|
|
76
82
|
memory_delta_kb: memory_delta_kb,
|
|
77
|
-
parent_rss_kb: rss_before
|
|
83
|
+
parent_rss_kb: rss_before,
|
|
84
|
+
error_message: result[:error],
|
|
85
|
+
error_class: result[:error_class],
|
|
86
|
+
error_backtrace: result[:error_backtrace]
|
|
78
87
|
)
|
|
79
88
|
end
|
|
80
89
|
end
|
|
@@ -4,6 +4,8 @@ require_relative "../operator"
|
|
|
4
4
|
|
|
5
5
|
class Evilution::Mutator::Operator::SymbolLiteral < Evilution::Mutator::Base
|
|
6
6
|
def visit_symbol_node(node)
|
|
7
|
+
return super if label_form?(node)
|
|
8
|
+
|
|
7
9
|
add_mutation(
|
|
8
10
|
offset: node.location.start_offset,
|
|
9
11
|
length: node.location.length,
|
|
@@ -20,4 +22,11 @@ class Evilution::Mutator::Operator::SymbolLiteral < Evilution::Mutator::Base
|
|
|
20
22
|
|
|
21
23
|
super
|
|
22
24
|
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def label_form?(node)
|
|
29
|
+
closing = node.closing_loc
|
|
30
|
+
!closing.nil? && closing.slice == ":"
|
|
31
|
+
end
|
|
23
32
|
end
|
|
@@ -19,6 +19,7 @@ class Evilution::Reporter::CLI
|
|
|
19
19
|
append_survived(lines, summary)
|
|
20
20
|
append_neutral(lines, summary)
|
|
21
21
|
append_equivalent(lines, summary)
|
|
22
|
+
append_errors(lines, summary)
|
|
22
23
|
append_disabled(lines, summary)
|
|
23
24
|
lines << ""
|
|
24
25
|
lines << "[TRUNCATED] Stopped early due to --fail-fast" if summary.truncated?
|
|
@@ -54,6 +55,24 @@ class Evilution::Reporter::CLI
|
|
|
54
55
|
summary.equivalent_results.each { |result| lines << format_neutral(result) }
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
def append_errors(lines, summary)
|
|
59
|
+
errored = summary.results.select(&:error?)
|
|
60
|
+
return if errored.empty?
|
|
61
|
+
|
|
62
|
+
lines << ""
|
|
63
|
+
lines << "Errored mutations:"
|
|
64
|
+
errored.each { |result| lines << format_error(result) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_error(result)
|
|
68
|
+
mutation = result.mutation
|
|
69
|
+
header = " #{mutation.operator_name}: #{mutation.file_path}:#{mutation.line}"
|
|
70
|
+
return header unless result.error_message
|
|
71
|
+
|
|
72
|
+
indented = result.error_message.lines.map { |line| " #{line.chomp}" }.join("\n")
|
|
73
|
+
"#{header}\n#{indented}"
|
|
74
|
+
end
|
|
75
|
+
|
|
57
76
|
def append_disabled(lines, summary)
|
|
58
77
|
return unless summary.disabled_mutations.any?
|
|
59
78
|
|
|
@@ -145,8 +145,10 @@ class Evilution::Reporter::HTML
|
|
|
145
145
|
def build_map_entry(result)
|
|
146
146
|
mutation = result.mutation
|
|
147
147
|
status = result.status.to_s
|
|
148
|
+
title_text = normalize_title(result.error_message)
|
|
149
|
+
title_attr = title_text ? %( title="#{h(title_text)}") : ""
|
|
148
150
|
<<~HTML.chomp
|
|
149
|
-
<div class="map-line #{status}">
|
|
151
|
+
<div class="map-line #{status}"#{title_attr}>
|
|
150
152
|
<span class="line-number">line #{mutation.line}</span>
|
|
151
153
|
<span class="operator">#{h(mutation.operator_name)}</span>
|
|
152
154
|
<span class="status-badge #{status}">#{status}</span>
|
|
@@ -154,6 +156,13 @@ class Evilution::Reporter::HTML
|
|
|
154
156
|
HTML
|
|
155
157
|
end
|
|
156
158
|
|
|
159
|
+
def normalize_title(message)
|
|
160
|
+
return nil if message.nil?
|
|
161
|
+
|
|
162
|
+
normalized = message.gsub(/\s+/, " ").strip
|
|
163
|
+
normalized.empty? ? nil : normalized
|
|
164
|
+
end
|
|
165
|
+
|
|
157
166
|
def build_survived_details(survived)
|
|
158
167
|
return "" if survived.empty?
|
|
159
168
|
|
|
@@ -76,10 +76,21 @@ class Evilution::Reporter::JSON
|
|
|
76
76
|
}
|
|
77
77
|
detail[:suggestion] = @suggestion.suggestion_for(mutation) if result.status == :survived
|
|
78
78
|
detail[:test_command] = result.test_command if result.test_command
|
|
79
|
+
append_memory_fields(detail, result)
|
|
80
|
+
append_error_fields(detail, result)
|
|
81
|
+
detail
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def append_memory_fields(detail, result)
|
|
79
85
|
detail[:parent_rss_kb] = result.parent_rss_kb if result.parent_rss_kb
|
|
80
86
|
detail[:child_rss_kb] = result.child_rss_kb if result.child_rss_kb
|
|
81
87
|
detail[:memory_delta_kb] = result.memory_delta_kb if result.memory_delta_kb
|
|
82
|
-
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def append_error_fields(detail, result)
|
|
91
|
+
detail[:error_message] = result.error_message if result.error_message
|
|
92
|
+
detail[:error_class] = result.error_class if result.error_class
|
|
93
|
+
detail[:error_backtrace] = result.error_backtrace if result.error_backtrace
|
|
83
94
|
end
|
|
84
95
|
|
|
85
96
|
def build_coverage_gaps(summary)
|
|
@@ -6,11 +6,15 @@ class Evilution::Result::MutationResult
|
|
|
6
6
|
STATUSES = %i[killed survived timeout error neutral equivalent].freeze
|
|
7
7
|
|
|
8
8
|
attr_reader :mutation, :status, :duration, :killing_test, :test_command,
|
|
9
|
-
:child_rss_kb, :memory_delta_kb, :parent_rss_kb
|
|
9
|
+
:child_rss_kb, :memory_delta_kb, :parent_rss_kb,
|
|
10
|
+
:error_message, :error_class, :error_backtrace
|
|
10
11
|
|
|
12
|
+
# rubocop:disable Metrics/ParameterLists
|
|
11
13
|
def initialize(mutation:, status:, duration: 0.0, killing_test: nil,
|
|
12
14
|
test_command: nil, child_rss_kb: nil, memory_delta_kb: nil,
|
|
13
|
-
parent_rss_kb: nil
|
|
15
|
+
parent_rss_kb: nil, error_message: nil, error_class: nil,
|
|
16
|
+
error_backtrace: nil)
|
|
17
|
+
# rubocop:enable Metrics/ParameterLists
|
|
14
18
|
raise ArgumentError, "invalid status: #{status}" unless STATUSES.include?(status)
|
|
15
19
|
|
|
16
20
|
@mutation = mutation
|
|
@@ -21,6 +25,9 @@ class Evilution::Result::MutationResult
|
|
|
21
25
|
@child_rss_kb = child_rss_kb
|
|
22
26
|
@memory_delta_kb = memory_delta_kb
|
|
23
27
|
@parent_rss_kb = parent_rss_kb
|
|
28
|
+
@error_message = error_message
|
|
29
|
+
@error_class = error_class
|
|
30
|
+
@error_backtrace = error_backtrace.nil? ? nil : error_backtrace.dup.freeze
|
|
24
31
|
freeze
|
|
25
32
|
end
|
|
26
33
|
|
data/lib/evilution/runner.rb
CHANGED
|
@@ -411,7 +411,10 @@ class Evilution::Runner
|
|
|
411
411
|
test_command: result.test_command,
|
|
412
412
|
child_rss_kb: result.child_rss_kb,
|
|
413
413
|
memory_delta_kb: result.memory_delta_kb,
|
|
414
|
-
parent_rss_kb: result.parent_rss_kb
|
|
414
|
+
parent_rss_kb: result.parent_rss_kb,
|
|
415
|
+
error_message: result.error_message,
|
|
416
|
+
error_class: result.error_class,
|
|
417
|
+
error_backtrace: result.error_backtrace
|
|
415
418
|
)
|
|
416
419
|
end
|
|
417
420
|
|
|
@@ -423,7 +426,10 @@ class Evilution::Runner
|
|
|
423
426
|
test_command: result.test_command,
|
|
424
427
|
child_rss_kb: result.child_rss_kb,
|
|
425
428
|
memory_delta_kb: result.memory_delta_kb,
|
|
426
|
-
parent_rss_kb: result.parent_rss_kb
|
|
429
|
+
parent_rss_kb: result.parent_rss_kb,
|
|
430
|
+
error_message: result.error_message,
|
|
431
|
+
error_class: result.error_class,
|
|
432
|
+
error_backtrace: result.error_backtrace
|
|
427
433
|
}
|
|
428
434
|
end
|
|
429
435
|
|
|
@@ -437,7 +443,10 @@ class Evilution::Runner
|
|
|
437
443
|
test_command: data[:test_command],
|
|
438
444
|
child_rss_kb: data[:child_rss_kb],
|
|
439
445
|
memory_delta_kb: data[:memory_delta_kb],
|
|
440
|
-
parent_rss_kb: data[:parent_rss_kb]
|
|
446
|
+
parent_rss_kb: data[:parent_rss_kb],
|
|
447
|
+
error_message: data[:error_message],
|
|
448
|
+
error_class: data[:error_class],
|
|
449
|
+
error_backtrace: data[:error_backtrace]
|
|
441
450
|
)
|
|
442
451
|
end
|
|
443
452
|
end
|
|
@@ -562,9 +571,20 @@ class Evilution::Runner
|
|
|
562
571
|
|
|
563
572
|
parts << gc_stats_string
|
|
564
573
|
|
|
565
|
-
|
|
574
|
+
$stderr.write("[verbose] #{result.mutation}: #{parts.join(", ")}\n") unless parts.empty?
|
|
566
575
|
|
|
567
|
-
|
|
576
|
+
log_mutation_error(result) if result.error?
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def log_mutation_error(result)
|
|
580
|
+
header = "[verbose] #{result.mutation}: error"
|
|
581
|
+
header += " #{result.error_class}" if result.error_class
|
|
582
|
+
header += ": #{result.error_message}" if result.error_message
|
|
583
|
+
$stderr.write("#{header}\n")
|
|
584
|
+
|
|
585
|
+
Array(result.error_backtrace).first(5).each do |line|
|
|
586
|
+
$stderr.write("[verbose] #{line}\n")
|
|
587
|
+
end
|
|
568
588
|
end
|
|
569
589
|
|
|
570
590
|
def gc_stats_string
|
data/lib/evilution/version.rb
CHANGED
data/script/memory_check
CHANGED
|
@@ -104,12 +104,12 @@ complex_mutations = complex_subjects.flat_map { |s| complex_registry.mutations_f
|
|
|
104
104
|
|
|
105
105
|
integration = Evilution::Integration::RSpec.new(test_files: [COMPLEX_FIXTURE_SPEC])
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
# Budget is generous: per-mutation require() adds the file to $LOADED_FEATURES and
|
|
108
|
+
# accumulates constant-redefinition warnings. Local runs land around 20-25 MB; CI
|
|
109
|
+
# varies up to ~30 MB depending on Ruby build and GC pressure, so we leave headroom.
|
|
110
|
+
all_passed &= run_check("RSpec integration per-mutation (Config)", iterations: 20, max_growth_kb: 40_960) do
|
|
108
111
|
mutation = complex_mutations.sample
|
|
109
|
-
|
|
110
|
-
raise "RSpec integration memory check failed: #{result[:error]}" if result[:error]
|
|
111
|
-
|
|
112
|
-
result
|
|
112
|
+
integration.call(mutation)
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
puts all_passed ? "All memory checks passed." : "Some memory checks failed!"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evilution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.22.
|
|
4
|
+
version: 0.22.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Kiselev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: diff-lcs
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- ".claude/prompts/architect.md"
|
|
71
71
|
- ".claude/prompts/devops.md"
|
|
72
72
|
- ".claude/prompts/tests.md"
|
|
73
|
+
- ".claude/settings.json"
|
|
73
74
|
- CHANGELOG.md
|
|
74
75
|
- CODE_OF_CONDUCT.md
|
|
75
76
|
- LICENSE.txt
|