Skip to content

Commit 3d9f384

Browse files
committed
Add rust wrapper basics
1 parent 20dd8da commit 3d9f384

File tree

14 files changed

+471
-202
lines changed

14 files changed

+471
-202
lines changed

.github/workflows/ci.yml

Lines changed: 82 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,21 @@ jobs:
1111
- name: Check Whitespace
1212
run: git diff --check -- HEAD~1
1313
rust-build:
14+
name: Rust Build (${{ matrix.target }})
15+
runs-on: ${{ matrix.os }}
1416
strategy:
1517
matrix:
16-
os:
17-
- runner: ubuntu-latest
18-
arch: x86_64
19-
- runner: ubuntu-24.04-arm
20-
arch: aarch64
21-
- runner: macos-15
22-
arch: aarch64
23-
name: Rust Build (${{ matrix.os.runner }}-${{ matrix.os.arch }})
24-
runs-on: ${{ matrix.os.runner }}
18+
include:
19+
- target: aarch64-apple-darwin
20+
os: macos-15
21+
- target: aarch64-unknown-linux-musl
22+
os: ubuntu-24.04-arm
23+
- target: aarch64-unknown-linux-gnu
24+
os: ubuntu-24.04-arm
25+
- target: x86_64-unknown-linux-musl
26+
os: ubuntu-24.04
27+
- target: x86_64-unknown-linux-gnu
28+
os: ubuntu-24.04
2529
steps:
2630
- uses: actions/checkout@v4
2731
- uses: actions/cache@v4
@@ -33,160 +37,107 @@ jobs:
3337
~/.cargo/registry/cache/
3438
~/.cargo/registry/index/
3539
~/.rustup
36-
key: ${{ matrix.os.runner }}-${{ matrix.os.arch }}-rust-${{ hashFiles('.github/workflows/ci.yml', 'Cargo.lock', 'rust-toolchain.yaml') }}
40+
key: ${{ matrix.target }}-rust-${{ hashFiles('.github/workflows/ci.yml', 'Cargo.lock', 'rust-toolchain.toml') }}
41+
- name: Install musl-tools
42+
if: endsWith(matrix.target, '-musl')
43+
run: sudo apt-get update && sudo apt-get install -y musl-tools
3744
- name: Run fmt
3845
run: cargo fmt --check
3946
- name: Build release
40-
run: cargo build --release
47+
run: cargo build --release --target ${{ matrix.target }}
4148
- name: Run clippy
42-
run: cargo clippy --release
43-
ruby-spec:
44-
name: Unit Specs
49+
run: cargo clippy --release --target ${{ matrix.target }}
50+
- uses: actions/upload-artifact@v4
51+
with:
52+
name: manager-${{ matrix.target }}
53+
path: target/${{ matrix.target }}/release/manager
54+
ruby-tests:
55+
name: Ruby Tests
56+
needs: rust-build
4557
runs-on: ${{ matrix.os }}
46-
timeout-minutes: 10
47-
defaults:
48-
run:
49-
working-directory: ruby
58+
timeout-minutes: 30
5059
strategy:
5160
fail-fast: false
5261
matrix:
5362
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
54-
os: [ubuntu-latest, macos-latest]
63+
target:
64+
- aarch64-apple-darwin
65+
- aarch64-unknown-linux-musl
66+
- aarch64-unknown-linux-gnu
67+
- x86_64-unknown-linux-musl
68+
- x86_64-unknown-linux-gnu
5569
execution:
56-
- bundle exec rspec spec/unit
57-
- bundle exec mutant environment test run spec/unit
70+
- rspec spec-unit
71+
- mutant test
72+
- rspec integration-misc
73+
- rspec integration-minitest
74+
- rspec integration-rspec
75+
- rspec integration-generation
76+
- rubocop
77+
include:
78+
- target: aarch64-apple-darwin
79+
os: macos-15
80+
- target: aarch64-unknown-linux-musl
81+
os: ubuntu-24.04-arm
82+
- target: aarch64-unknown-linux-gnu
83+
os: ubuntu-24.04-arm
84+
- target: x86_64-unknown-linux-musl
85+
os: ubuntu-24.04
86+
- target: x86_64-unknown-linux-gnu
87+
os: ubuntu-24.04
5888
steps:
5989
- uses: actions/checkout@v4
90+
- uses: actions/download-artifact@v4
91+
with:
92+
name: manager-${{ matrix.target }}
93+
path: ./bin
94+
- run: chmod +x ./bin/manager
95+
- run: ./bin/manager ruby prepare
6096
- uses: ruby/setup-ruby@v1
6197
with:
6298
ruby-version: ${{ matrix.ruby }}
6399
bundler-cache: true
64100
working-directory: ./ruby
65-
- run: ${{ matrix.execution }}
101+
- run: ./bin/manager ruby ${{ matrix.execution }}
66102
ruby-mutant:
67103
name: Mutation coverage
104+
needs: rust-build
68105
runs-on: ${{ matrix.os }}
69106
timeout-minutes: 30
70-
defaults:
71-
run:
72-
working-directory: ruby
73107
strategy:
74108
fail-fast: false
75109
matrix:
76110
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
77-
os: [ubuntu-latest, macos-latest]
111+
target:
112+
- aarch64-apple-darwin
113+
- aarch64-unknown-linux-musl
114+
- aarch64-unknown-linux-gnu
115+
- x86_64-unknown-linux-musl
116+
- x86_64-unknown-linux-gnu
117+
include:
118+
- target: aarch64-apple-darwin
119+
os: macos-15
120+
- target: aarch64-unknown-linux-musl
121+
os: ubuntu-24.04-arm
122+
- target: aarch64-unknown-linux-gnu
123+
os: ubuntu-24.04-arm
124+
- target: x86_64-unknown-linux-musl
125+
os: ubuntu-24.04
126+
- target: x86_64-unknown-linux-gnu
127+
os: ubuntu-24.04
78128
steps:
79129
- uses: actions/checkout@v4
80130
with:
81131
fetch-depth: 0
82-
- uses: ruby/setup-ruby@v1
83-
with:
84-
ruby-version: ${{ matrix.ruby }}
85-
bundler-cache: true
86-
working-directory: ./ruby
87-
- run: bundle exec mutant run --zombie --since HEAD~1
88-
ruby-integration-misc:
89-
name: Integration Misc
90-
runs-on: ${{ matrix.os }}
91-
timeout-minutes: 10
92-
defaults:
93-
run:
94-
working-directory: ruby
95-
strategy:
96-
fail-fast: false
97-
matrix:
98-
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
99-
os: [ubuntu-latest, macos-latest]
100-
steps:
101-
- uses: actions/checkout@v4
102-
- uses: ruby/setup-ruby@v1
103-
with:
104-
ruby-version: ${{ matrix.ruby }}
105-
bundler-cache: true
106-
working-directory: ./ruby
107-
- run: |
108-
bundle exec rspec \
109-
spec/integration/mutant/null_spec.rb \
110-
spec/integration/mutant/isolation/fork_spec.rb \
111-
spec/integration/mutant/test_mutator_handles_types_spec.rb \
112-
spec/integration/mutant/parallel_spec.rb
113-
ruby-integration-minitest:
114-
name: Integration Minitest
115-
runs-on: ${{ matrix.os }}
116-
timeout-minutes: 10
117-
defaults:
118-
run:
119-
working-directory: ruby
120-
strategy:
121-
fail-fast: false
122-
matrix:
123-
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
124-
os: [ubuntu-latest, macos-latest]
125-
steps:
126-
- uses: actions/checkout@v4
127-
- uses: ruby/setup-ruby@v1
132+
- uses: actions/download-artifact@v4
128133
with:
129-
ruby-version: ${{ matrix.ruby }}
130-
bundler-cache: true
131-
working-directory: ./ruby
132-
- run: bundle exec rspec spec/integration -e minitest
133-
ruby-integration-rspec:
134-
name: Integration RSpec
135-
runs-on: ${{ matrix.os }}
136-
timeout-minutes: 10
137-
defaults:
138-
run:
139-
working-directory: ruby
140-
strategy:
141-
fail-fast: false
142-
matrix:
143-
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
144-
os: [ubuntu-latest, macos-latest]
145-
steps:
146-
- uses: actions/checkout@v4
147-
- uses: ruby/setup-ruby@v1
148-
with:
149-
ruby-version: ${{ matrix.ruby }}
150-
bundler-cache: true
151-
working-directory: ./ruby
152-
- run: bundle exec rspec spec/integration -e rspec
153-
ruby-integration-generation:
154-
name: Integration Mutation Generation
155-
runs-on: ${{ matrix.os }}
156-
timeout-minutes: 10
157-
defaults:
158-
run:
159-
working-directory: ruby
160-
strategy:
161-
fail-fast: false
162-
matrix:
163-
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
164-
os: [ubuntu-latest, macos-latest]
165-
steps:
166-
- uses: actions/checkout@v4
167-
- uses: ruby/setup-ruby@v1
168-
with:
169-
ruby-version: ${{ matrix.ruby }}
170-
bundler-cache: true
171-
working-directory: ./ruby
172-
- run: bundle exec rspec spec/integration -e generation
173-
ruby-rubocop:
174-
name: Rubocop
175-
runs-on: ${{ matrix.os }}
176-
timeout-minutes: 10
177-
defaults:
178-
run:
179-
working-directory: ruby
180-
strategy:
181-
fail-fast: false
182-
matrix:
183-
ruby: [ruby-3.2, ruby-3.3, ruby-3.4]
184-
os: [ubuntu-latest, macos-latest]
185-
steps:
186-
- uses: actions/checkout@v4
134+
name: manager-${{ matrix.target }}
135+
path: ./bin
136+
- run: chmod +x ./bin/manager
137+
- run: ./bin/manager ruby prepare
187138
- uses: ruby/setup-ruby@v1
188139
with:
189140
ruby-version: ${{ matrix.ruby }}
190141
bundler-cache: true
191142
working-directory: ./ruby
192-
- run: bundle exec rubocop
143+
- run: ./bin/manager ruby mutant run --zombie --since HEAD~1

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
/Cargo.lock
33
/target
44
/tmp
5+
/ruby/LICENSE
6+
/ruby/VERSION

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["mutant"]
3+
members = ["mutant", "manager"]
4+
5+
[workspace.package]
6+
version = "0.13.5"

RUST.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Mutant Rust
2+
3+
## Overview
4+
5+
Large parts of Mutant are being replaced with Rust for better performance and
6+
developer ergonomics. Rust's advanced type system (ADTs) offers a much higher
7+
complexity ceiling without driving the author insane.
8+
9+
## Current State
10+
11+
By default, mutant runs entirely in Ruby. The Rust implementation is opt-in
12+
and currently wraps the Ruby CLI, delegating all commands to the Ruby
13+
implementation. This allows incremental migration while maintaining full
14+
compatibility. A Ruby interpreter is still required to run mutant.
15+
16+
## Planned Migration
17+
18+
Ruby-independent logic will be incrementally moved to Rust:
19+
20+
- Process management
21+
- IPC (inter-process communication)
22+
- Result aggregation
23+
- Rendering
24+
25+
## Future Features
26+
27+
Rust will enable features that are difficult to implement in Ruby:
28+
29+
- Distributed mutation testing
30+
- Advanced tracing
31+
32+
## FAQ (Shitstorm Prevention)
33+
34+
**Q: Why not rewrite in $LANGUAGE?**
35+
36+
A: The author has extensive experience with Rust and finds it productive for this
37+
use case. This is a pragmatic choice, not advocacy.
38+
39+
**Q: Rust is overhyped.**
40+
41+
A: Agreed. But hype doesn't make a tool useless. Rust solves real problems here:
42+
expressing complex state machines and catching bugs at compile time that would
43+
otherwise surface in production. Other languages can
44+
solve these too, but Rust is the most pragmatic choice due to the intersection
45+
of type system, ecosystem, static binary support, and easy targeting of multiple
46+
CPU architectures.
47+
48+
**Q: Ruby is fast enough.**
49+
50+
A: No. The author has extensive experience using Ruby in high concurrency and
51+
complexity environments (a main reason Mutant exists). For many Mutant users,
52+
Ruby is not even fast enough to aggregate results quickly. Mutation testing is
53+
CPU and IPC latency bound. Just because you haven't hit Ruby's limitations
54+
doesn't mean they don't exist.
55+
56+
**Q: You're abandoning Ruby.**
57+
58+
A: No, mitigating it. Mutant itself is a Ruby mitigation technique among many.
59+
Ruby remains excellent for mutation operators and AST manipulation. The goal
60+
is to use each language where it excels.
61+
62+
**Q: But Ruby has a type system too.**
63+
64+
A: Mutant's Ruby implementation, while complex, is not (yet) and hopefully never
65+
will be a black hole with enough gravity to need an incremental change to Sorbet.
66+
I can maximize value by moving to a first-class type system, not an (however
67+
awesome given the starting point) backfill.
68+
69+
**Q: This adds complexity.**
70+
71+
A: Yes. The tradeoff is worth it for the author. Your mileage may vary.
72+
73+
## Running Mutant
74+
75+
```bash
76+
bundle exec mutant run
77+
```
78+
79+
The `bin/mutant` dispatcher selects between `bin/mutant-ruby` and `bin/mutant-rust`
80+
based on the `MUTANT_RUST` environment variable.
81+
82+
### Environment Variables
83+
84+
- `MUTANT_RUST`: Set to `1` to use the Rust implementation (default: Ruby)
85+
86+
## Version Management
87+
88+
The single source of truth for the version is `Cargo.toml` at the workspace root
89+
using workspace version inheritance. Both `mutant` and `manager` crates inherit
90+
this version.
91+
92+
The Ruby implementation reads the version differently depending on how it's invoked:
93+
- With `MUTANT_RUST=1`: reads from `MUTANT_VERSION` environment variable
94+
- Without `MUTANT_RUST`: reads from `ruby/VERSION` file
95+
96+
See `ruby/lib/mutant/version.rb` for implementation details.

manager/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "manager"
3+
version.workspace = true
4+
edition = "2021"
5+
authors = ["Markus Schirp <[email protected]>"]
6+
license = "Nonstandard"
7+
description = "Mutant development manager"
8+
9+
[[bin]]
10+
name = "manager"
11+
path = "src/main.rs"
12+
13+
[dependencies]
14+
clap = { version = "4.5", features = ["derive"] }
15+
toml = "0.8"

0 commit comments

Comments
 (0)