evilution 0.1.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/.beads/.gitignore +51 -0
- data/.beads/.migration-hint-ts +1 -0
- data/.beads/README.md +81 -0
- data/.beads/config.yaml +67 -0
- data/.beads/interactions.jsonl +0 -0
- data/.beads/issues.jsonl +68 -0
- data/.beads/metadata.json +4 -0
- data/.claude/prompts/architect.md +98 -0
- data/.claude/prompts/devops.md +106 -0
- data/.claude/prompts/tests.md +160 -0
- data/CHANGELOG.md +19 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +190 -0
- data/Rakefile +12 -0
- data/claude-swarm.yml +28 -0
- data/exe/evilution +6 -0
- data/lib/evilution/ast/parser.rb +83 -0
- data/lib/evilution/ast/source_surgeon.rb +13 -0
- data/lib/evilution/cli.rb +78 -0
- data/lib/evilution/config.rb +98 -0
- data/lib/evilution/coverage/collector.rb +47 -0
- data/lib/evilution/coverage/test_map.rb +25 -0
- data/lib/evilution/diff/file_filter.rb +29 -0
- data/lib/evilution/diff/parser.rb +47 -0
- data/lib/evilution/integration/base.rb +11 -0
- data/lib/evilution/integration/rspec.rb +184 -0
- data/lib/evilution/isolation/fork.rb +70 -0
- data/lib/evilution/mutation.rb +45 -0
- data/lib/evilution/mutator/base.rb +54 -0
- data/lib/evilution/mutator/operator/arithmetic_replacement.rb +37 -0
- data/lib/evilution/mutator/operator/array_literal.rb +22 -0
- data/lib/evilution/mutator/operator/boolean_literal_replacement.rb +31 -0
- data/lib/evilution/mutator/operator/boolean_operator_replacement.rb +50 -0
- data/lib/evilution/mutator/operator/collection_replacement.rb +37 -0
- data/lib/evilution/mutator/operator/comparison_replacement.rb +37 -0
- data/lib/evilution/mutator/operator/conditional_branch.rb +36 -0
- data/lib/evilution/mutator/operator/conditional_negation.rb +36 -0
- data/lib/evilution/mutator/operator/float_literal.rb +26 -0
- data/lib/evilution/mutator/operator/hash_literal.rb +22 -0
- data/lib/evilution/mutator/operator/integer_literal.rb +45 -0
- data/lib/evilution/mutator/operator/method_body_replacement.rb +22 -0
- data/lib/evilution/mutator/operator/negation_insertion.rb +22 -0
- data/lib/evilution/mutator/operator/nil_replacement.rb +20 -0
- data/lib/evilution/mutator/operator/return_value_removal.rb +22 -0
- data/lib/evilution/mutator/operator/statement_deletion.rb +24 -0
- data/lib/evilution/mutator/operator/string_literal.rb +22 -0
- data/lib/evilution/mutator/operator/symbol_literal.rb +20 -0
- data/lib/evilution/mutator/registry.rb +55 -0
- data/lib/evilution/parallel/pool.rb +98 -0
- data/lib/evilution/parallel/worker.rb +24 -0
- data/lib/evilution/reporter/cli.rb +72 -0
- data/lib/evilution/reporter/json.rb +59 -0
- data/lib/evilution/reporter/suggestion.rb +51 -0
- data/lib/evilution/result/mutation_result.rb +37 -0
- data/lib/evilution/result/summary.rb +54 -0
- data/lib/evilution/runner.rb +139 -0
- data/lib/evilution/subject.rb +20 -0
- data/lib/evilution/version.rb +5 -0
- data/lib/evilution.rb +51 -0
- data/sig/evilution.rbs +4 -0
- metadata +130 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Evilution Gem DevOps Specialist
|
|
2
|
+
|
|
3
|
+
You are a DevOps specialist for evilution — a Ruby gem for mutation testing. Your focus is CI/CD, gem publishing, and development infrastructure.
|
|
4
|
+
|
|
5
|
+
## Git Workflow
|
|
6
|
+
|
|
7
|
+
Before starting any new task:
|
|
8
|
+
1. `git checkout master && git pull`
|
|
9
|
+
2. `git checkout -b <descriptive-branch-name>`
|
|
10
|
+
3. Do all work on the feature branch
|
|
11
|
+
|
|
12
|
+
## Core Responsibilities
|
|
13
|
+
|
|
14
|
+
1. **CI/CD**: GitHub Actions for testing across Ruby versions
|
|
15
|
+
2. **Gem Publishing**: RubyGems release process
|
|
16
|
+
3. **Code Quality**: RuboCop, bundler-audit
|
|
17
|
+
4. **Versioning**: Semantic versioning for gem releases
|
|
18
|
+
|
|
19
|
+
## GitHub Actions CI
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
# .github/workflows/ci.yml
|
|
23
|
+
name: CI
|
|
24
|
+
|
|
25
|
+
on:
|
|
26
|
+
push:
|
|
27
|
+
branches: [master]
|
|
28
|
+
pull_request:
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
test:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
strategy:
|
|
34
|
+
matrix:
|
|
35
|
+
ruby: ['3.2', '3.3', '4.0']
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- name: Set up Ruby
|
|
40
|
+
uses: ruby/setup-ruby@v1
|
|
41
|
+
with:
|
|
42
|
+
ruby-version: ${{ matrix.ruby }}
|
|
43
|
+
bundler-cache: true
|
|
44
|
+
|
|
45
|
+
- name: Run specs
|
|
46
|
+
run: bundle exec rspec
|
|
47
|
+
|
|
48
|
+
- name: Run linter
|
|
49
|
+
run: bundle exec rubocop
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Gem Release Process
|
|
53
|
+
|
|
54
|
+
1. Update version in `lib/evilution/version.rb`
|
|
55
|
+
2. Update `CHANGELOG.md`
|
|
56
|
+
3. Commit: `git commit -m "Bump version to X.Y.Z"`
|
|
57
|
+
4. Tag: `git tag vX.Y.Z`
|
|
58
|
+
5. Build and push:
|
|
59
|
+
```bash
|
|
60
|
+
gem build evilution.gemspec
|
|
61
|
+
gem push evilution-X.Y.Z.gem
|
|
62
|
+
```
|
|
63
|
+
6. Or use bundler's rake task: `bundle exec rake release`
|
|
64
|
+
|
|
65
|
+
## Versioning Strategy
|
|
66
|
+
|
|
67
|
+
Follow semantic versioning:
|
|
68
|
+
- **PATCH** (0.1.x): Bug fixes, new mutation operators
|
|
69
|
+
- **MINOR** (0.x.0): New features (new integrations, new output formats)
|
|
70
|
+
- **MAJOR** (x.0.0): Breaking API changes
|
|
71
|
+
|
|
72
|
+
## Security
|
|
73
|
+
|
|
74
|
+
- No credentials in gem package — gemspec excludes sensitive files
|
|
75
|
+
- `bundler-audit` for dependency vulnerability scanning
|
|
76
|
+
- No `eval` of user-provided input (only eval of mutation-generated code in forked processes)
|
|
77
|
+
|
|
78
|
+
## Dependencies
|
|
79
|
+
|
|
80
|
+
Runtime dependencies must be minimal:
|
|
81
|
+
- `diff-lcs` — diff computation for reports
|
|
82
|
+
- Prism ships with Ruby 3.3+ (declared as gemspec dependency for 3.2)
|
|
83
|
+
|
|
84
|
+
Dev dependencies:
|
|
85
|
+
- `rspec` — testing
|
|
86
|
+
- `rubocop` — linting
|
|
87
|
+
- `rake` — task runner
|
|
88
|
+
|
|
89
|
+
## Project Structure Awareness
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
evilution.gemspec # Gem metadata and dependencies
|
|
93
|
+
Gemfile # Dev dependency management
|
|
94
|
+
Rakefile # Default task: spec + rubocop
|
|
95
|
+
exe/evilution # CLI executable
|
|
96
|
+
lib/ # Gem source code
|
|
97
|
+
spec/ # RSpec specs
|
|
98
|
+
.github/workflows/ # CI configuration
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Key Principles
|
|
102
|
+
|
|
103
|
+
- Keep CI fast — run specs and rubocop only
|
|
104
|
+
- Multi-Ruby matrix — test on 3.2, 3.3, and 4.0
|
|
105
|
+
- No Docker needed — this is a pure Ruby gem
|
|
106
|
+
- No database, no external services
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Evilution Gem Testing Specialist
|
|
2
|
+
|
|
3
|
+
You are an RSpec testing specialist ensuring comprehensive test coverage for evilution — a Ruby mutation testing gem.
|
|
4
|
+
|
|
5
|
+
## Git Workflow
|
|
6
|
+
|
|
7
|
+
Before starting any new task:
|
|
8
|
+
1. `git checkout master && git pull`
|
|
9
|
+
2. `git checkout -b <descriptive-branch-name>`
|
|
10
|
+
3. Do all work on the feature branch
|
|
11
|
+
|
|
12
|
+
## Core Responsibilities
|
|
13
|
+
|
|
14
|
+
1. **Test Coverage**: Write comprehensive specs for all gem classes
|
|
15
|
+
2. **Test Types**: Unit specs, integration specs, fixture-based specs
|
|
16
|
+
3. **Test Quality**: Tests must be meaningful — verify behavior, not implementation
|
|
17
|
+
4. **Test Performance**: Keep the suite fast (no network, no heavy I/O)
|
|
18
|
+
5. **TDD**: Write specs first, then implement
|
|
19
|
+
|
|
20
|
+
## Project Rules
|
|
21
|
+
|
|
22
|
+
- Self-documenting test names — no comments in spec files
|
|
23
|
+
- One spec file per class, mirroring the lib/ directory structure
|
|
24
|
+
- Use fixture Ruby files for parser and mutation operator tests
|
|
25
|
+
- No FactoryBot — plain Ruby objects and fixture files
|
|
26
|
+
- No database, no Rails — this is a pure Ruby gem
|
|
27
|
+
|
|
28
|
+
## Testing Framework: RSpec
|
|
29
|
+
|
|
30
|
+
### Unit Spec Example
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
RSpec.describe Evilution::AST::SourceSurgeon do
|
|
34
|
+
describe ".apply" do
|
|
35
|
+
it "replaces text at the given byte offset" do
|
|
36
|
+
source = "age >= 18"
|
|
37
|
+
result = described_class.apply(source, offset: 4, length: 2, replacement: ">")
|
|
38
|
+
|
|
39
|
+
expect(result).to eq("age > 18")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Mutation Operator Spec Example
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
RSpec.describe Evilution::Mutator::Operator::ComparisonReplacement do
|
|
49
|
+
let(:source) { "def adult?(age)\n age >= 18\nend" }
|
|
50
|
+
let(:subject_under_test) { build_subject(source: source, file: "example.rb") }
|
|
51
|
+
|
|
52
|
+
describe "#call" do
|
|
53
|
+
it "generates mutations for >= operator" do
|
|
54
|
+
mutations = described_class.new.call(subject_under_test)
|
|
55
|
+
|
|
56
|
+
mutated_sources = mutations.map(&:mutated_source)
|
|
57
|
+
expect(mutated_sources).to include(
|
|
58
|
+
"def adult?(age)\n age > 18\nend",
|
|
59
|
+
"def adult?(age)\n age == 18\nend"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Integration Spec Example
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RSpec.describe "End-to-end mutation run" do
|
|
70
|
+
it "detects surviving mutants in poorly tested code" do
|
|
71
|
+
result = Evilution::Runner.new(
|
|
72
|
+
files: [fixture_path("poorly_tested.rb")],
|
|
73
|
+
spec_files: [fixture_path("poorly_tested_spec.rb")]
|
|
74
|
+
).call
|
|
75
|
+
|
|
76
|
+
expect(result.summary.survived).to be > 0
|
|
77
|
+
expect(result.summary.score).to be < 1.0
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## TDD Cycle
|
|
83
|
+
|
|
84
|
+
1. **Write a spec** for the class/method being built
|
|
85
|
+
2. **Watch it fail** — confirm it fails for the right reason
|
|
86
|
+
3. **Write minimal implementation** to make the spec pass
|
|
87
|
+
4. **Refactor** while keeping specs green
|
|
88
|
+
5. **Repeat** for the next behavior
|
|
89
|
+
|
|
90
|
+
## Test Organization
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
spec/
|
|
94
|
+
├── spec_helper.rb
|
|
95
|
+
├── support/
|
|
96
|
+
│ ├── helpers.rb # Shared test helpers (build_subject, fixture_path)
|
|
97
|
+
│ └── fixtures/
|
|
98
|
+
│ ├── simple_class.rb # Fixture Ruby files for parsing
|
|
99
|
+
│ ├── comparison.rb # Fixture with comparison operators
|
|
100
|
+
│ ├── arithmetic.rb # Fixture with arithmetic operators
|
|
101
|
+
│ └── conditionals.rb # Fixture with if/else
|
|
102
|
+
├── evilution/
|
|
103
|
+
│ ├── config_spec.rb
|
|
104
|
+
│ ├── runner_spec.rb
|
|
105
|
+
│ ├── subject_spec.rb
|
|
106
|
+
│ ├── mutation_spec.rb
|
|
107
|
+
│ ├── cli_spec.rb
|
|
108
|
+
│ ├── ast/
|
|
109
|
+
│ │ ├── parser_spec.rb
|
|
110
|
+
│ │ └── source_surgeon_spec.rb
|
|
111
|
+
│ ├── mutator/
|
|
112
|
+
│ │ ├── base_spec.rb
|
|
113
|
+
│ │ ├── registry_spec.rb
|
|
114
|
+
│ │ └── operator/
|
|
115
|
+
│ │ ├── comparison_replacement_spec.rb
|
|
116
|
+
│ │ ├── arithmetic_replacement_spec.rb
|
|
117
|
+
│ │ └── ...
|
|
118
|
+
│ ├── isolation/
|
|
119
|
+
│ │ └── fork_spec.rb
|
|
120
|
+
│ ├── integration/
|
|
121
|
+
│ │ └── rspec_spec.rb
|
|
122
|
+
│ ├── result/
|
|
123
|
+
│ │ ├── mutation_result_spec.rb
|
|
124
|
+
│ │ └── summary_spec.rb
|
|
125
|
+
│ └── reporter/
|
|
126
|
+
│ ├── json_spec.rb
|
|
127
|
+
│ └── cli_spec.rb
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Testing Patterns
|
|
131
|
+
|
|
132
|
+
### Arrange-Act-Assert
|
|
133
|
+
1. **Arrange**: Set up fixture files, build Subject objects, configure as needed
|
|
134
|
+
2. **Act**: Call the method under test
|
|
135
|
+
3. **Assert**: Verify the expected output
|
|
136
|
+
|
|
137
|
+
### Fixture Files
|
|
138
|
+
- Store fixture Ruby files in `spec/support/fixtures/`
|
|
139
|
+
- Each fixture exercises a specific category of mutations
|
|
140
|
+
- Keep fixtures minimal — only the code needed for the test
|
|
141
|
+
|
|
142
|
+
### Testing Mutation Operators
|
|
143
|
+
For each operator, verify:
|
|
144
|
+
- Correct mutations are generated for target node types
|
|
145
|
+
- Non-target nodes are left alone
|
|
146
|
+
- Edge cases: empty bodies, nested nodes, multiple occurrences
|
|
147
|
+
- Generated mutations are valid Ruby (parseable by Prism)
|
|
148
|
+
|
|
149
|
+
### Testing Isolation::Fork
|
|
150
|
+
- Verify child process gets clean state
|
|
151
|
+
- Verify timeout kills the child
|
|
152
|
+
- Verify results are marshalled correctly via pipe
|
|
153
|
+
- Use simple test commands (not full RSpec) in specs
|
|
154
|
+
|
|
155
|
+
### Edge Cases
|
|
156
|
+
Always test:
|
|
157
|
+
- Empty/nil inputs
|
|
158
|
+
- Boundary conditions (single-statement bodies, nested methods)
|
|
159
|
+
- Invalid source code (parser errors)
|
|
160
|
+
- Files with no mutable nodes
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **18 mutation operators**: ArithmeticReplacement, ComparisonReplacement, BooleanOperatorReplacement, BooleanLiteralReplacement, NilReplacement, IntegerLiteral, FloatLiteral, StringLiteral, ArrayLiteral, HashLiteral, SymbolLiteral, ConditionalNegation, ConditionalBranch, StatementDeletion, MethodBodyReplacement, NegationInsertion, ReturnValueRemoval, CollectionReplacement
|
|
8
|
+
- **Prism-based AST parsing** with source-level surgery via byte offsets
|
|
9
|
+
- **Fork-based isolation** for safe mutation execution
|
|
10
|
+
- **RSpec integration** for test execution
|
|
11
|
+
- **Parallel execution** with configurable worker count (`--jobs`)
|
|
12
|
+
- **Diff-based targeting** to mutate only changed code (`--diff HEAD~1`)
|
|
13
|
+
- **Coverage-based filtering** — skip mutations on lines no test exercises
|
|
14
|
+
- **JSON output** for AI agents and CI pipelines (`--format json`)
|
|
15
|
+
- **CLI output** with human-readable mutation testing results
|
|
16
|
+
- **Actionable suggestions** for surviving mutants
|
|
17
|
+
- **Configuration file** support (`.evilution.yml`)
|
|
18
|
+
- **`evilution init`** command to generate default config
|
|
19
|
+
- **Error handling** with clean exit codes (0=pass, 1=fail, 2=error)
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"evilution" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["denis.kiselyov@gmail.com"](mailto:"denis.kiselyov@gmail.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Kiselev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Evilution — Mutation Testing for Ruby
|
|
2
|
+
|
|
3
|
+
> **Purpose**: Validate test suite quality by injecting small code changes (mutations) and checking whether tests detect them. Surviving mutations indicate gaps in test coverage.
|
|
4
|
+
|
|
5
|
+
**License**: MIT (free, no commercial restrictions)
|
|
6
|
+
**Language**: Ruby >= 3.3
|
|
7
|
+
**Parser**: Prism (Ruby's official AST parser, ships with Ruby 3.3+)
|
|
8
|
+
**Test framework**: RSpec (currently the only supported integration)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add to `Gemfile`:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "evilution", group: :test
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then: `bundle install`
|
|
19
|
+
|
|
20
|
+
Or standalone: `gem install evilution`
|
|
21
|
+
|
|
22
|
+
## Command Reference
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
evilution [command] [options] [files...]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Commands
|
|
29
|
+
|
|
30
|
+
| Command | Description | Default |
|
|
31
|
+
|-----------|------------------------------------------|---------|
|
|
32
|
+
| `run` | Execute mutation testing against files | Yes |
|
|
33
|
+
| `init` | Generate `.evilution.yml` config file | |
|
|
34
|
+
| `version` | Print version string | |
|
|
35
|
+
|
|
36
|
+
### Options (for `run` command)
|
|
37
|
+
|
|
38
|
+
| Flag | Type | Default | Description |
|
|
39
|
+
|-------------------------|---------|--------------|---------------------------------------------------|
|
|
40
|
+
| `-j`, `--jobs N` | Integer | CPU cores | Parallel worker count. Use `1` for sequential. |
|
|
41
|
+
| `-t`, `--timeout N` | Integer | 10 | Per-mutation timeout in seconds. |
|
|
42
|
+
| `-f`, `--format FORMAT` | String | `text` | Output format: `text` or `json`. |
|
|
43
|
+
| `--diff BASE` | String | _(none)_ | Git ref. Only mutate methods whose definition line changed since BASE. |
|
|
44
|
+
| `--min-score FLOAT` | Float | 0.0 | Minimum mutation score (0.0–1.0) to pass. |
|
|
45
|
+
| `--no-coverage` | Boolean | false | Reserved; currently has no effect. |
|
|
46
|
+
| `-v`, `--verbose` | Boolean | false | Verbose output. |
|
|
47
|
+
| `-q`, `--quiet` | Boolean | false | Suppress output. |
|
|
48
|
+
|
|
49
|
+
### Exit Codes
|
|
50
|
+
|
|
51
|
+
| Code | Meaning | Agent action |
|
|
52
|
+
|------|-----------------------------------------------|---------------------------------------|
|
|
53
|
+
| 0 | Mutation score meets or exceeds `--min-score` | Success. No action needed. |
|
|
54
|
+
| 1 | Mutation score below `--min-score` | Parse output, fix surviving mutants. |
|
|
55
|
+
| 2 | Tool error (bad config, parse failure, etc.) | Check stderr, fix invocation. |
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
Generate default config: `bundle exec evilution init`
|
|
60
|
+
|
|
61
|
+
Creates `.evilution.yml`:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# jobs: 4 # parallel workers
|
|
65
|
+
# timeout: 10 # seconds per mutation
|
|
66
|
+
# format: text # text | json
|
|
67
|
+
# min_score: 0.0 # 0.0–1.0
|
|
68
|
+
# integration: rspec # test framework
|
|
69
|
+
# coverage: true # skip mutations on uncovered lines
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Precedence**: CLI flags override `.evilution.yml` values.
|
|
73
|
+
|
|
74
|
+
## JSON Output Schema
|
|
75
|
+
|
|
76
|
+
Use `--format json` for machine-readable output. Schema:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"version": "string — gem version",
|
|
81
|
+
"timestamp": "string — ISO 8601 timestamp of the report",
|
|
82
|
+
"summary": {
|
|
83
|
+
"total": "integer — total mutations generated",
|
|
84
|
+
"killed": "integer — mutations detected by tests (test failed = good)",
|
|
85
|
+
"survived": "integer — mutations NOT detected (test passed = gap in coverage)",
|
|
86
|
+
"timed_out": "integer — mutations that exceeded timeout",
|
|
87
|
+
"errors": "integer — mutations that caused unexpected errors",
|
|
88
|
+
"score": "float — killed / (total - errors), range 0.0-1.0, rounded to 4 decimals",
|
|
89
|
+
"duration": "float — total wall-clock seconds, rounded to 4 decimals"
|
|
90
|
+
},
|
|
91
|
+
"survived": [
|
|
92
|
+
{
|
|
93
|
+
"operator": "string — mutation operator name (see Operators table)",
|
|
94
|
+
"file": "string — relative path to mutated file",
|
|
95
|
+
"line": "integer — line number of the mutation",
|
|
96
|
+
"status": "string — result status: 'survived', 'killed', 'timeout', or 'error'",
|
|
97
|
+
"duration": "float — seconds this mutation took, rounded to 4 decimals",
|
|
98
|
+
"diff": "string — unified diff snippet",
|
|
99
|
+
"suggestion": "string — actionable hint for surviving mutants (survived only)"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"killed": ["... same shape as survived entries ..."],
|
|
103
|
+
"timed_out": ["... same shape as survived entries ..."],
|
|
104
|
+
"errors": ["... same shape as survived entries ..."]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Key metric**: `summary.score` — the mutation score. Higher is better. 1.0 means all mutations were caught.
|
|
109
|
+
|
|
110
|
+
## Mutation Operators (18 total)
|
|
111
|
+
|
|
112
|
+
Each operator name is stable and appears in JSON output under `survived[].operator`.
|
|
113
|
+
|
|
114
|
+
| Operator | What it does | Example |
|
|
115
|
+
|---------------------------|-------------------------------------------|------------------------------------|
|
|
116
|
+
| `arithmetic_replacement` | Swap arithmetic operators | `a + b` -> `a - b` |
|
|
117
|
+
| `comparison_replacement` | Swap comparison operators | `a >= b` -> `a > b` |
|
|
118
|
+
| `boolean_operator_replacement` | Swap `&&` / `\|\|` | `a && b` -> `a \|\| b` |
|
|
119
|
+
| `boolean_literal_replacement` | Flip boolean literals | `true` -> `false` |
|
|
120
|
+
| `nil_replacement` | Replace expression with `nil` | `expr` -> `nil` |
|
|
121
|
+
| `integer_literal` | Boundary-value integer mutations | `n` -> `0`, `1`, `n+1`, `n-1` |
|
|
122
|
+
| `float_literal` | Boundary-value float mutations | `f` -> `0.0`, `1.0` |
|
|
123
|
+
| `string_literal` | Empty the string | `"str"` -> `""` |
|
|
124
|
+
| `array_literal` | Empty the array | `[a, b]` -> `[]` |
|
|
125
|
+
| `hash_literal` | Empty the hash | `{k: v}` -> `{}` |
|
|
126
|
+
| `symbol_literal` | Replace with sentinel symbol | `:foo` -> `:__evilution_mutated__` |
|
|
127
|
+
| `conditional_negation` | Replace condition with `true`/`false` | `if cond` -> `if true` |
|
|
128
|
+
| `conditional_branch` | Remove if/else branch | Deletes branch body |
|
|
129
|
+
| `statement_deletion` | Remove statements from method bodies | Deletes a statement |
|
|
130
|
+
| `method_body_replacement` | Replace entire method body with `nil` | Method body -> `nil` |
|
|
131
|
+
| `negation_insertion` | Negate predicate methods | `x.empty?` -> `!x.empty?` |
|
|
132
|
+
| `return_value_removal` | Strip return values | `return x` -> `return` |
|
|
133
|
+
| `collection_replacement` | Swap collection methods | `map` -> `each`, `select` <-> `reject` |
|
|
134
|
+
|
|
135
|
+
## Recommended Workflows for AI Agents
|
|
136
|
+
|
|
137
|
+
### 1. Full project scan
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
bundle exec evilution run lib/ --format json --jobs 4 --min-score 0.8
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Parse JSON output. Exit code 0 = pass, 1 = surviving mutants to address.
|
|
144
|
+
|
|
145
|
+
### 2. PR / diff-only scan (fast feedback)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
bundle exec evilution run lib/ --format json --diff main --min-score 0.9
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Mutates methods whose definition (starting) line is changed compared to `main` (the diff filter is based on the method’s first line, not any line in its body). Use this for incremental checks — it's fast and focused on newly added or moved methods and changed signatures.
|
|
152
|
+
|
|
153
|
+
### 3. Single-file targeted scan
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bundle exec evilution run lib/specific_file.rb --format json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Use when you know which file was modified and want to verify its test coverage.
|
|
160
|
+
|
|
161
|
+
### 4. Fixing surviving mutants
|
|
162
|
+
|
|
163
|
+
For each entry in `survived[]`:
|
|
164
|
+
1. Read `file` at `line` to understand the code context
|
|
165
|
+
2. Read `operator` to understand what was changed
|
|
166
|
+
3. Read `suggestion` for a hint on what test to write
|
|
167
|
+
4. Write a test that would fail if the mutation were applied
|
|
168
|
+
5. Re-run evilution on just that file to verify the mutant is now killed
|
|
169
|
+
|
|
170
|
+
### 5. CI gate
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bundle exec evilution run lib/ --format json --min-score 0.8 --quiet
|
|
174
|
+
# Exit code 0 = pass, 1 = fail, 2 = error
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Note: `--quiet` suppresses all stdout output (including JSON). Use it in CI only when you care about the exit code and do not need JSON output.
|
|
178
|
+
|
|
179
|
+
## Internals (for context, not for direct use)
|
|
180
|
+
|
|
181
|
+
1. **Parse** — Prism parses Ruby files into ASTs with exact byte offsets
|
|
182
|
+
2. **Extract** — Methods are identified as mutation subjects
|
|
183
|
+
3. **Mutate** — Operators produce text replacements at precise byte offsets (source-level surgery, no AST unparsing)
|
|
184
|
+
4. **Isolate** — Each mutation runs in a `fork()`-ed child process (no test pollution)
|
|
185
|
+
5. **Test** — RSpec executes against the mutated source
|
|
186
|
+
6. **Report** — Results aggregated into text or JSON
|
|
187
|
+
|
|
188
|
+
## Repository
|
|
189
|
+
|
|
190
|
+
https://github.com/marinazzio/evilution
|
data/Rakefile
ADDED
data/claude-swarm.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
swarm:
|
|
3
|
+
name: "Evilution Ruby gem Development Team"
|
|
4
|
+
main: architect
|
|
5
|
+
|
|
6
|
+
instances:
|
|
7
|
+
architect:
|
|
8
|
+
description: "Ruby architect coordinating full-stack development for Evilution Ruby gem"
|
|
9
|
+
directory: .
|
|
10
|
+
model: opus
|
|
11
|
+
connections: [tests, devops]
|
|
12
|
+
prompt_file: .claude/prompts/architect.md
|
|
13
|
+
vibe: true
|
|
14
|
+
|
|
15
|
+
tests:
|
|
16
|
+
description: "RSpec testing, factories, and test coverage specialist"
|
|
17
|
+
directory: ./spec
|
|
18
|
+
model: opus
|
|
19
|
+
allowed_tools: [Read, Edit, Write, Bash, Grep, Glob, LS]
|
|
20
|
+
prompt_file: .claude/prompts/tests.md
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
devops:
|
|
24
|
+
description: "Deployment, Docker, CI/CD, and production configuration specialist"
|
|
25
|
+
directory: ./.github
|
|
26
|
+
model: opus
|
|
27
|
+
allowed_tools: [Read, Edit, Write, Bash, Grep, Glob, LS]
|
|
28
|
+
prompt_file: .claude/prompts/devops.md
|
data/exe/evilution
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Evilution
|
|
6
|
+
module AST
|
|
7
|
+
class Parser
|
|
8
|
+
def call(file_path)
|
|
9
|
+
raise ParseError, "file not found: #{file_path}" unless File.exist?(file_path)
|
|
10
|
+
|
|
11
|
+
source = File.read(file_path)
|
|
12
|
+
result = Prism.parse(source)
|
|
13
|
+
|
|
14
|
+
raise ParseError, "failed to parse #{file_path}: #{result.errors.map(&:message).join(", ")}" if result.failure?
|
|
15
|
+
|
|
16
|
+
extract_subjects(result.value, source, file_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def extract_subjects(tree, source, file_path)
|
|
22
|
+
finder = SubjectFinder.new(source, file_path)
|
|
23
|
+
finder.visit(tree)
|
|
24
|
+
finder.subjects
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class SubjectFinder < Prism::Visitor
|
|
29
|
+
attr_reader :subjects
|
|
30
|
+
|
|
31
|
+
def initialize(source, file_path)
|
|
32
|
+
@source = source
|
|
33
|
+
@file_path = file_path
|
|
34
|
+
@subjects = []
|
|
35
|
+
@context = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def visit_module_node(node)
|
|
39
|
+
@context.push(constant_name(node.constant_path))
|
|
40
|
+
super
|
|
41
|
+
@context.pop
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def visit_class_node(node)
|
|
45
|
+
@context.push(constant_name(node.constant_path))
|
|
46
|
+
super
|
|
47
|
+
@context.pop
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def visit_def_node(node)
|
|
51
|
+
scope = @context.join("::")
|
|
52
|
+
name = if scope.empty?
|
|
53
|
+
"##{node.name}"
|
|
54
|
+
else
|
|
55
|
+
"#{scope}##{node.name}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
loc = node.location
|
|
59
|
+
method_source = @source[loc.start_offset...loc.end_offset]
|
|
60
|
+
|
|
61
|
+
@subjects << Subject.new(
|
|
62
|
+
name: name,
|
|
63
|
+
file_path: @file_path,
|
|
64
|
+
line_number: loc.start_line,
|
|
65
|
+
source: method_source,
|
|
66
|
+
node: node
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def constant_name(node)
|
|
75
|
+
if node.respond_to?(:full_name)
|
|
76
|
+
node.full_name
|
|
77
|
+
else
|
|
78
|
+
node.name.to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|