mutation_tester 1.2.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/CHANGELOG.md +13 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +19 -0
- data/Rakefile +57 -0
- data/docs/ci.md +158 -0
- data/docs/execution-runners.md +163 -0
- data/docs/json-schema.md +171 -0
- data/docs/mutation-types.md +207 -0
- data/examples/calculator.rb +35 -0
- data/examples/calculator_100_perc_spec.rb +121 -0
- data/examples/calculator_minitest.rb +53 -0
- data/examples/calculator_spec.rb +105 -0
- data/examples/github_actions/ai_mutation_gate.yml +75 -0
- data/examples/github_actions/mutation_test.yml +87 -0
- data/examples/hooks/pre-push +41 -0
- data/examples/run_example.rb +31 -0
- data/examples/run_example_minitest.rb +38 -0
- data/examples/run_example_parallel_minitest.rb +48 -0
- data/examples/run_example_parallel_rspec.rb +48 -0
- data/examples/run_example_rspec.rb +38 -0
- data/examples/spec_helper.rb +21 -0
- data/exe/mutation_test +345 -0
- data/lib/mutation_tester/batch_runner.rb +286 -0
- data/lib/mutation_tester/configuration.rb +105 -0
- data/lib/mutation_tester/core.rb +193 -0
- data/lib/mutation_tester/fork_runner/worker.rb +202 -0
- data/lib/mutation_tester/fork_runner.rb +382 -0
- data/lib/mutation_tester/framework_detector.rb +25 -0
- data/lib/mutation_tester/in_memory_loader.rb +25 -0
- data/lib/mutation_tester/mutation_runner.rb +644 -0
- data/lib/mutation_tester/mutator.rb +874 -0
- data/lib/mutation_tester/progress_display.rb +130 -0
- data/lib/mutation_tester/railtie.rb +7 -0
- data/lib/mutation_tester/rake_task.rb +18 -0
- data/lib/mutation_tester/reporters/base_reporter.rb +154 -0
- data/lib/mutation_tester/reporters/batch_json_reporter.rb +72 -0
- data/lib/mutation_tester/reporters/console_reporter.rb +130 -0
- data/lib/mutation_tester/reporters/html_reporter.rb +693 -0
- data/lib/mutation_tester/reporters/json_reporter.rb +67 -0
- data/lib/mutation_tester/test_command.rb +165 -0
- data/lib/mutation_tester/version.rb +3 -0
- data/lib/mutation_tester.rb +52 -0
- data/lib/tasks/mutation_tester.rake +80 -0
- data/readme.md +925 -0
- metadata +213 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative 'calculator'
|
|
2
|
+
|
|
3
|
+
RSpec.describe Calculator do
|
|
4
|
+
subject(:calculator) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#add' do
|
|
7
|
+
it 'adds two numbers' do
|
|
8
|
+
expect(calculator.add(2, 3)).to eq(5)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'adds negative numbers' do
|
|
12
|
+
expect(calculator.add(-2, -3)).to eq(-5)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#subtract' do
|
|
17
|
+
it 'subtracts two numbers' do
|
|
18
|
+
expect(calculator.subtract(5, 3)).to eq(2)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'handles negative results' do
|
|
22
|
+
expect(calculator.subtract(3, 5)).to eq(-2)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#multiply' do
|
|
27
|
+
it 'multiplies two numbers' do
|
|
28
|
+
expect(calculator.multiply(3, 4)).to eq(12)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'multiplies by zero' do
|
|
32
|
+
expect(calculator.multiply(5, 0)).to eq(0)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#divide' do
|
|
37
|
+
it 'divides two numbers' do
|
|
38
|
+
expect(calculator.divide(10, 2)).to eq(5)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'returns 0 when dividing by zero' do
|
|
42
|
+
expect(calculator.divide(10, 0)).to eq(0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'handles integer division' do
|
|
46
|
+
expect(calculator.divide(7, 2)).to eq(3)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe '#is_positive?' do
|
|
51
|
+
it 'returns true for positive numbers' do
|
|
52
|
+
expect(calculator.is_positive?(5)).to be true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'returns false for negative numbers' do
|
|
56
|
+
expect(calculator.is_positive?(-5)).to be false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'returns false for zero' do
|
|
60
|
+
expect(calculator.is_positive?(0)).to be false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#is_even?' do
|
|
65
|
+
it 'returns true for even numbers' do
|
|
66
|
+
expect(calculator.is_even?(4)).to be true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns false for odd numbers' do
|
|
70
|
+
expect(calculator.is_even?(3)).to be false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns true for zero' do
|
|
74
|
+
expect(calculator.is_even?(0)).to be true
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#max' do
|
|
79
|
+
it 'returns the larger number when first is larger' do
|
|
80
|
+
expect(calculator.max(5, 3)).to eq(5)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'returns the larger number when second is larger' do
|
|
84
|
+
expect(calculator.max(3, 5)).to eq(5)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'returns the number when both are equal' do
|
|
88
|
+
expect(calculator.max(5, 5)).to eq(5)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#absolute' do
|
|
93
|
+
it 'returns positive number unchanged' do
|
|
94
|
+
expect(calculator.absolute(5)).to eq(5)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns absolute value of negative number' do
|
|
98
|
+
expect(calculator.absolute(-5)).to eq(5)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns zero for zero' do
|
|
102
|
+
expect(calculator.absolute(0)).to eq(0)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# AI mutation gate for GitHub Actions (mutation_tester gem). Runs mutation_test
|
|
2
|
+
# --json, publishes the SURVIVING mutants (file/line/original/mutated = test
|
|
3
|
+
# gaps) to the job summary and a survivors.json artifact for an AI agent, then
|
|
4
|
+
# re-fails on a low score. Copy to .github/workflows/, edit "EDIT:" lines. Needs
|
|
5
|
+
# jq (preinstalled on ubuntu runners); see mutation_test.yml for the plain gate.
|
|
6
|
+
|
|
7
|
+
name: AI Mutation Gate
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
mutation_gate:
|
|
15
|
+
name: AI mutation gate
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 30
|
|
18
|
+
steps:
|
|
19
|
+
- name: Checkout
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Ruby
|
|
23
|
+
uses: ruby/setup-ruby@v1
|
|
24
|
+
with:
|
|
25
|
+
ruby-version: '3.3' # EDIT: match your project's Ruby (floor is 3.0)
|
|
26
|
+
bundler-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Run mutation tests (JSON report on stdout)
|
|
29
|
+
id: mutation
|
|
30
|
+
continue-on-error: true # keep going so the worklist/artifact below still run
|
|
31
|
+
run: |
|
|
32
|
+
# EDIT: replace with YOUR source file and its test file.
|
|
33
|
+
bundle exec mutation_test --json \
|
|
34
|
+
app/models/user.rb spec/models/user_spec.rb > mutation_report.json
|
|
35
|
+
|
|
36
|
+
- name: Summarize surviving mutants (test gaps) in the job summary
|
|
37
|
+
if: always()
|
|
38
|
+
run: |
|
|
39
|
+
score=$(jq -r '.summary.mutation_score // "n/a"' mutation_report.json)
|
|
40
|
+
survivors=$(jq '[.mutations[] | select(.status == "survived")] | length' mutation_report.json)
|
|
41
|
+
{
|
|
42
|
+
echo "## Mutation gate"
|
|
43
|
+
echo "Score: **${score}%** - surviving mutants: **${survivors}**"
|
|
44
|
+
if [ "$survivors" -eq 0 ]; then
|
|
45
|
+
echo "No surviving mutants. No test gaps to close."
|
|
46
|
+
else
|
|
47
|
+
echo "| file | line | from | to |"
|
|
48
|
+
echo "|---|---|---|---|"
|
|
49
|
+
jq -r '.mutations[] | select(.status == "survived")
|
|
50
|
+
| "| \(.file_path) | \(.line) | `\(.original)` | `\(.mutated)` |"' \
|
|
51
|
+
mutation_report.json
|
|
52
|
+
fi
|
|
53
|
+
} >> "$GITHUB_STEP_SUMMARY"
|
|
54
|
+
|
|
55
|
+
- name: Extract survivors as a machine-readable worklist for an AI agent
|
|
56
|
+
if: always()
|
|
57
|
+
run: |
|
|
58
|
+
jq '[.mutations[] | select(.status == "survived")
|
|
59
|
+
| {file: .file_path, line: .line, from: .original, to: .mutated,
|
|
60
|
+
source_line: .source_line, mutated_line: .mutated_line}]' \
|
|
61
|
+
mutation_report.json > survivors.json
|
|
62
|
+
cat survivors.json # feed this worklist to your agent CLI
|
|
63
|
+
|
|
64
|
+
- name: Upload mutation report and survivor worklist
|
|
65
|
+
if: always()
|
|
66
|
+
uses: actions/upload-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: mutation-report
|
|
69
|
+
path: |
|
|
70
|
+
mutation_report.json
|
|
71
|
+
survivors.json
|
|
72
|
+
|
|
73
|
+
- name: Enforce the gate
|
|
74
|
+
if: steps.mutation.outcome == 'failure' # non-zero run = score below threshold
|
|
75
|
+
run: exit 1
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Mutation testing in CI with GitHub Actions (mutation_tester gem).
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to .github/workflows/mutation_test.yml in your project and you
|
|
4
|
+
# have mutation testing running in CI in about five minutes. The workflow:
|
|
5
|
+
# 1. checks out your code,
|
|
6
|
+
# 2. installs Ruby and your bundle (with mutation_tester in it),
|
|
7
|
+
# 3. runs mutation_test on your source file and its test file,
|
|
8
|
+
# 4. uploads the HTML/JSON reports as a build artifact (even on failure),
|
|
9
|
+
# 5. fails the job when the mutation score is below your threshold.
|
|
10
|
+
#
|
|
11
|
+
# Prerequisite: add the gem to your project's Gemfile so `bundle exec` finds it:
|
|
12
|
+
# bundle add mutation_tester --group development,test
|
|
13
|
+
# (or add `gem "mutation_tester"` to your development/test group, then bundle install).
|
|
14
|
+
#
|
|
15
|
+
# Then look for the lines marked "EDIT:" and point them at your own files.
|
|
16
|
+
|
|
17
|
+
name: Mutation Testing
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
pull_request:
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
mutation_test:
|
|
25
|
+
name: Mutation testing
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
# Mutation testing re-runs your suite once per mutant, so give the job more
|
|
28
|
+
# head-room than a normal test run. Tune this to your project's size.
|
|
29
|
+
timeout-minutes: 30
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- name: Checkout
|
|
33
|
+
uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Set up Ruby
|
|
36
|
+
uses: ruby/setup-ruby@v1
|
|
37
|
+
with:
|
|
38
|
+
# EDIT: match the Ruby version your project uses.
|
|
39
|
+
ruby-version: '3.3'
|
|
40
|
+
# Runs `bundle install` and caches the resolved gems between runs.
|
|
41
|
+
bundler-cache: true
|
|
42
|
+
|
|
43
|
+
- name: Run mutation tests
|
|
44
|
+
# EDIT: replace with YOUR source file and its test file.
|
|
45
|
+
# The CLI exits non-zero when the score is below the threshold, which
|
|
46
|
+
# fails this step (and the job). That is your quality gate: no extra
|
|
47
|
+
# configuration needed.
|
|
48
|
+
run: bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb
|
|
49
|
+
# OPTIONAL - run mutants in parallel to finish faster. This works out of
|
|
50
|
+
# the box (see MUTATION_TESTER_PARALLEL_PROCESSES). Enable it only if
|
|
51
|
+
# your tests do not share database state. Uncomment the two lines below:
|
|
52
|
+
# env:
|
|
53
|
+
# MUTATION_TESTER_PARALLEL_PROCESSES: 4
|
|
54
|
+
|
|
55
|
+
# OPTIONAL - test several source/test pairs in one job. Uncomment and give
|
|
56
|
+
# each pair its own --output-dir so their reports do not overwrite each
|
|
57
|
+
# other. The job stops at the first pair that is below its threshold.
|
|
58
|
+
# - name: Run mutation tests (multiple files)
|
|
59
|
+
# run: |
|
|
60
|
+
# bundle exec mutation_test app/models/user.rb spec/models/user_spec.rb \
|
|
61
|
+
# --output-dir tmp/mutation_reports/user
|
|
62
|
+
# bundle exec mutation_test app/models/order.rb spec/models/order_spec.rb \
|
|
63
|
+
# --output-dir tmp/mutation_reports/order
|
|
64
|
+
|
|
65
|
+
# OPTIONAL - incremental gate for pull requests: mutate only the files
|
|
66
|
+
# changed since the PR base branch and stop at the first surviving mutant.
|
|
67
|
+
# This keeps the job fast on large repos. Two edits are needed:
|
|
68
|
+
# 1. give the Checkout step the full history so git can diff against the
|
|
69
|
+
# base branch:
|
|
70
|
+
# with:
|
|
71
|
+
# fetch-depth: 0
|
|
72
|
+
# 2. replace the "Run mutation tests" step with the incremental run:
|
|
73
|
+
# - name: Run mutation tests (changed files only)
|
|
74
|
+
# if: github.event_name == 'pull_request'
|
|
75
|
+
# run: |
|
|
76
|
+
# bundle exec mutation_test --glob 'lib/**/*.rb' \
|
|
77
|
+
# --since "origin/${{ github.base_ref }}" --fail-fast
|
|
78
|
+
|
|
79
|
+
- name: Upload mutation reports
|
|
80
|
+
# always() uploads the reports even when the gate failed the job, which
|
|
81
|
+
# is exactly when you want to inspect the surviving mutants.
|
|
82
|
+
if: always()
|
|
83
|
+
uses: actions/upload-artifact@v4
|
|
84
|
+
with:
|
|
85
|
+
name: mutation-reports
|
|
86
|
+
# Default output_dir; change it if you pass a custom --output-dir above.
|
|
87
|
+
path: tmp/mutation_reports/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# mutation_tester pre-push hook - block a push when the mutation score of your
|
|
4
|
+
# target file(s) drops below THRESHOLD, so weak tests never reach the remote.
|
|
5
|
+
#
|
|
6
|
+
# Install: cp examples/hooks/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push
|
|
7
|
+
# (lefthook/overcommit users: call this script from your pre-push step)
|
|
8
|
+
# Requires: jq (https://jqlang.github.io/jq/)
|
|
9
|
+
# On a block it prints the surviving mutants (file:line, what changed) = the gaps.
|
|
10
|
+
|
|
11
|
+
set -eu
|
|
12
|
+
|
|
13
|
+
THRESHOLD=90 # EDIT: minimum mutation score (percent) required to allow the push
|
|
14
|
+
report=$(mktemp)
|
|
15
|
+
status=0
|
|
16
|
+
|
|
17
|
+
# EDIT: the "SOURCE TEST" pairs to gate on, one per line. The loop reads this
|
|
18
|
+
# heredoc (not a pipe), so it runs in the current shell and $status survives it.
|
|
19
|
+
while read -r source test; do
|
|
20
|
+
[ -z "$source" ] && continue
|
|
21
|
+
|
|
22
|
+
# --json prints ONLY the JSON to stdout; we do our own jq threshold check, so
|
|
23
|
+
# ignore the CLI's own pass/fail exit here.
|
|
24
|
+
bundle exec mutation_test --json "$source" "$test" >"$report" || true
|
|
25
|
+
|
|
26
|
+
score=$(jq -r '.summary.mutation_score // empty' "$report")
|
|
27
|
+
if [ -z "$score" ] || jq -e --argjson t "$THRESHOLD" '.summary.mutation_score < $t' "$report" >/dev/null; then
|
|
28
|
+
echo "BLOCK: $source scored ${score:-no}% (< ${THRESHOLD}%). Surviving mutants (test gaps):" >&2
|
|
29
|
+
jq -r '.mutations[] | select(.status == "survived")
|
|
30
|
+
| " \(.file_path):\(.line) \(.original) -> \(.mutated)"' "$report" >&2
|
|
31
|
+
status=1
|
|
32
|
+
else
|
|
33
|
+
echo "OK: $source scored ${score}% (>= ${THRESHOLD}%)" >&2
|
|
34
|
+
fi
|
|
35
|
+
done <<'PAIRS'
|
|
36
|
+
app/models/user.rb spec/models/user_spec.rb
|
|
37
|
+
PAIRS
|
|
38
|
+
|
|
39
|
+
rm -f "$report"
|
|
40
|
+
[ "$status" -eq 0 ] || echo "Push blocked by mutation gate. Close the gaps above or lower THRESHOLD." >&2
|
|
41
|
+
exit "$status"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require_relative '../lib/mutation_tester'
|
|
2
|
+
|
|
3
|
+
MutationTester.configure do |config|
|
|
4
|
+
config.reporters = %i[console html json]
|
|
5
|
+
config.minimum_score = 90
|
|
6
|
+
config.verbose = true
|
|
7
|
+
config.fail_on_threshold = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
source_file = File.expand_path('calculator.rb', __dir__)
|
|
11
|
+
spec_file = File.expand_path('calculator_spec.rb', __dir__)
|
|
12
|
+
|
|
13
|
+
unless File.exist?(source_file)
|
|
14
|
+
puts Rainbow("❌ Source file not found: #{source_file}").red
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless File.exist?(spec_file)
|
|
19
|
+
puts Rainbow("❌ Spec file not found: #{spec_file}").red
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
success = MutationTester.run(source_file, spec_file)
|
|
24
|
+
|
|
25
|
+
if success
|
|
26
|
+
puts "\n" + Rainbow('✅ Example completed!').green
|
|
27
|
+
puts Rainbow('Check mutation_reports/mutation_report.html for detailed results').cyan
|
|
28
|
+
else
|
|
29
|
+
puts "\n" + Rainbow('❌ Example failed!').red
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative '../lib/mutation_tester'
|
|
2
|
+
|
|
3
|
+
puts Rainbow('=' * 80).bright
|
|
4
|
+
puts Rainbow('🧬 Minitest Example (Serial Execution)').cyan.bold
|
|
5
|
+
puts Rainbow('=' * 80).bright
|
|
6
|
+
puts ''
|
|
7
|
+
|
|
8
|
+
MutationTester.configure do |config|
|
|
9
|
+
config.parallel_processes = 1
|
|
10
|
+
config.reporters = %i[console html json]
|
|
11
|
+
config.minimum_score = 90
|
|
12
|
+
config.verbose = true
|
|
13
|
+
config.fail_on_threshold = false
|
|
14
|
+
config.output_dir = 'mutation_reports'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
source_file = File.expand_path('calculator.rb', __dir__)
|
|
18
|
+
spec_file = File.expand_path('calculator_minitest.rb', __dir__)
|
|
19
|
+
|
|
20
|
+
unless File.exist?(source_file)
|
|
21
|
+
puts Rainbow("❌ Source file not found: #{source_file}").red
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless File.exist?(spec_file)
|
|
26
|
+
puts Rainbow("❌ Test file not found: #{spec_file}").red
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
success = MutationTester.run(source_file, spec_file)
|
|
31
|
+
|
|
32
|
+
puts ''
|
|
33
|
+
if success
|
|
34
|
+
puts Rainbow('✅ Minitest example completed successfully!').green.bold
|
|
35
|
+
else
|
|
36
|
+
puts Rainbow('⚠️ Minitest example completed with issues').yellow.bold
|
|
37
|
+
end
|
|
38
|
+
puts Rainbow('📊 Check mutation_reports/mutation_report.html for detailed results').cyan
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative '../lib/mutation_tester'
|
|
2
|
+
require 'parallel'
|
|
3
|
+
|
|
4
|
+
puts Rainbow('=' * 80).bright
|
|
5
|
+
puts Rainbow('🧬 Minitest Example (Parallel Execution)').cyan.bold
|
|
6
|
+
puts Rainbow('=' * 80).bright
|
|
7
|
+
puts ''
|
|
8
|
+
|
|
9
|
+
num_processors = Parallel.processor_count
|
|
10
|
+
puts Rainbow("💻 Detected #{num_processors} processor(s), using parallel execution").cyan
|
|
11
|
+
puts ''
|
|
12
|
+
|
|
13
|
+
MutationTester.configure do |config|
|
|
14
|
+
config.parallel_processes = [num_processors, 2].max
|
|
15
|
+
config.reporters = %i[console html json]
|
|
16
|
+
config.minimum_score = 90
|
|
17
|
+
config.verbose = true
|
|
18
|
+
config.fail_on_threshold = false
|
|
19
|
+
config.output_dir = 'mutation_reports'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
source_file = File.expand_path('calculator.rb', __dir__)
|
|
23
|
+
spec_file = File.expand_path('calculator_minitest.rb', __dir__)
|
|
24
|
+
|
|
25
|
+
unless File.exist?(source_file)
|
|
26
|
+
puts Rainbow("❌ Source file not found: #{source_file}").red
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless File.exist?(spec_file)
|
|
31
|
+
puts Rainbow("❌ Test file not found: #{spec_file}").red
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
start_time = Time.now
|
|
36
|
+
success = MutationTester.run(source_file, spec_file)
|
|
37
|
+
elapsed_time = Time.now - start_time
|
|
38
|
+
|
|
39
|
+
puts ''
|
|
40
|
+
puts Rainbow("⏱️ Completed in #{elapsed_time.round(2)} seconds").cyan
|
|
41
|
+
puts ''
|
|
42
|
+
|
|
43
|
+
if success
|
|
44
|
+
puts Rainbow('✅ Parallel Minitest example completed successfully!').green.bold
|
|
45
|
+
else
|
|
46
|
+
puts Rainbow('⚠️ Parallel Minitest example completed with issues').yellow.bold
|
|
47
|
+
end
|
|
48
|
+
puts Rainbow('📊 Check mutation_reports/mutation_report.html for detailed results').cyan
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require_relative '../lib/mutation_tester'
|
|
2
|
+
require 'parallel'
|
|
3
|
+
|
|
4
|
+
puts Rainbow('=' * 80).bright
|
|
5
|
+
puts Rainbow('🧬 RSpec Example (Parallel Execution)').cyan.bold
|
|
6
|
+
puts Rainbow('=' * 80).bright
|
|
7
|
+
puts ''
|
|
8
|
+
|
|
9
|
+
num_processors = Parallel.processor_count
|
|
10
|
+
puts Rainbow("💻 Detected #{num_processors} processor(s), using parallel execution").cyan
|
|
11
|
+
puts ''
|
|
12
|
+
|
|
13
|
+
MutationTester.configure do |config|
|
|
14
|
+
config.parallel_processes = [num_processors, 2].max
|
|
15
|
+
config.reporters = %i[console html json]
|
|
16
|
+
config.minimum_score = 90
|
|
17
|
+
config.verbose = true
|
|
18
|
+
config.fail_on_threshold = false
|
|
19
|
+
config.output_dir = 'mutation_reports'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
source_file = File.expand_path('calculator.rb', __dir__)
|
|
23
|
+
spec_file = File.expand_path('calculator_spec.rb', __dir__)
|
|
24
|
+
|
|
25
|
+
unless File.exist?(source_file)
|
|
26
|
+
puts Rainbow("❌ Source file not found: #{source_file}").red
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless File.exist?(spec_file)
|
|
31
|
+
puts Rainbow("❌ Spec file not found: #{spec_file}").red
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
start_time = Time.now
|
|
36
|
+
success = MutationTester.run(source_file, spec_file)
|
|
37
|
+
elapsed_time = Time.now - start_time
|
|
38
|
+
|
|
39
|
+
puts ''
|
|
40
|
+
puts Rainbow("⏱️ Completed in #{elapsed_time.round(2)} seconds").cyan
|
|
41
|
+
puts ''
|
|
42
|
+
|
|
43
|
+
if success
|
|
44
|
+
puts Rainbow('✅ Parallel RSpec example completed successfully!').green.bold
|
|
45
|
+
else
|
|
46
|
+
puts Rainbow('⚠️ Parallel RSpec example completed with issues').yellow.bold
|
|
47
|
+
end
|
|
48
|
+
puts Rainbow('📊 Check mutation_reports/mutation_report.html for detailed results').cyan
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative '../lib/mutation_tester'
|
|
2
|
+
|
|
3
|
+
puts Rainbow('=' * 80).bright
|
|
4
|
+
puts Rainbow('🧬 RSpec Example (Serial Execution)').cyan.bold
|
|
5
|
+
puts Rainbow('=' * 80).bright
|
|
6
|
+
puts ''
|
|
7
|
+
|
|
8
|
+
MutationTester.configure do |config|
|
|
9
|
+
config.parallel_processes = 1
|
|
10
|
+
config.reporters = %i[console html json]
|
|
11
|
+
config.minimum_score = 90
|
|
12
|
+
config.verbose = true
|
|
13
|
+
config.fail_on_threshold = false
|
|
14
|
+
config.output_dir = 'mutation_reports'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
source_file = File.expand_path('calculator.rb', __dir__)
|
|
18
|
+
spec_file = File.expand_path('calculator_spec.rb', __dir__)
|
|
19
|
+
|
|
20
|
+
unless File.exist?(source_file)
|
|
21
|
+
puts Rainbow("❌ Source file not found: #{source_file}").red
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless File.exist?(spec_file)
|
|
26
|
+
puts Rainbow("❌ Spec file not found: #{spec_file}").red
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
success = MutationTester.run(source_file, spec_file)
|
|
31
|
+
|
|
32
|
+
puts ''
|
|
33
|
+
if success
|
|
34
|
+
puts Rainbow('✅ RSpec example completed successfully!').green.bold
|
|
35
|
+
else
|
|
36
|
+
puts Rainbow('⚠️ RSpec example completed with issues').yellow.bold
|
|
37
|
+
end
|
|
38
|
+
puts Rainbow('📊 Check mutation_reports/mutation_report.html for detailed results').cyan
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
RSpec.configure do |config|
|
|
2
|
+
config.expect_with :rspec do |expectations|
|
|
3
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
config.mock_with(:rspec) do |mocks|
|
|
7
|
+
mocks.verify_partial_doubles = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
11
|
+
config.filter_run_when_matching(:focus)
|
|
12
|
+
config.example_status_persistence_file_path = 'spec/examples.txt'
|
|
13
|
+
config.disable_monkey_patching!
|
|
14
|
+
config.warnings = false
|
|
15
|
+
|
|
16
|
+
config.default_formatter = 'doc' if config.files_to_run.one?
|
|
17
|
+
|
|
18
|
+
config.profile_examples = 10
|
|
19
|
+
config.order = :random
|
|
20
|
+
Kernel.srand config.seed
|
|
21
|
+
end
|