henitai 0.1.10 → 0.2.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 +94 -1
- data/README.md +33 -7
- data/assets/schema/henitai.schema.json +6 -0
- data/lib/henitai/cli/clean_command.rb +48 -0
- data/lib/henitai/cli/command_support.rb +51 -0
- data/lib/henitai/cli/init_command.rb +64 -0
- data/lib/henitai/cli/operator_command.rb +95 -0
- data/lib/henitai/cli/options.rb +120 -0
- data/lib/henitai/cli/run_command.rb +103 -0
- data/lib/henitai/cli.rb +17 -327
- data/lib/henitai/configuration.rb +26 -12
- data/lib/henitai/configuration_validator/rules.rb +143 -0
- data/lib/henitai/configuration_validator/scalars.rb +123 -0
- data/lib/henitai/configuration_validator.rb +12 -239
- data/lib/henitai/coverage_bootstrapper.rb +24 -24
- data/lib/henitai/eager_load.rb +36 -5
- data/lib/henitai/execution_engine.rb +6 -11
- data/lib/henitai/git_diff_analyzer.rb +34 -0
- data/lib/henitai/integration/base.rb +171 -0
- data/lib/henitai/integration/child_debug_support.rb +115 -0
- data/lib/henitai/integration/child_runtime_control.rb +50 -0
- data/lib/henitai/integration/coverage_suppression.rb +43 -0
- data/lib/henitai/integration/minitest.rb +133 -0
- data/lib/henitai/integration/mutant_run_support.rb +77 -0
- data/lib/henitai/integration/rspec_child_runner.rb +61 -0
- data/lib/henitai/integration/rspec_process_runner.rb +66 -13
- data/lib/henitai/integration/rspec_test_selection.rb +135 -0
- data/lib/henitai/integration/scenario_log_support.rb +116 -0
- data/lib/henitai/integration.rb +43 -519
- data/lib/henitai/mutant/activator.rb +13 -79
- data/lib/henitai/mutant/parameter_source.rb +98 -0
- data/lib/henitai/mutant.rb +14 -2
- data/lib/henitai/mutant_generator.rb +21 -2
- data/lib/henitai/mutant_history_store/sql.rb +72 -0
- data/lib/henitai/mutant_history_store.rb +12 -91
- data/lib/henitai/mutant_identity.rb +34 -0
- data/lib/henitai/parallel_execution_runner.rb +29 -11
- data/lib/henitai/per_test_coverage_collector.rb +3 -1
- data/lib/henitai/process_wakeup.rb +49 -0
- data/lib/henitai/process_worker_runner.rb +148 -0
- data/lib/henitai/reporter.rb +96 -11
- data/lib/henitai/result.rb +49 -16
- data/lib/henitai/runner.rb +96 -30
- data/lib/henitai/scenario_execution_result.rb +16 -3
- data/lib/henitai/slot_scheduler/draining.rb +140 -0
- data/lib/henitai/slot_scheduler/process_control.rb +43 -0
- data/lib/henitai/slot_scheduler.rb +214 -0
- data/lib/henitai/static_filter.rb +10 -3
- data/lib/henitai/survivor_activation_cache.rb +81 -0
- data/lib/henitai/survivor_loader.rb +140 -0
- data/lib/henitai/survivor_rerun_strategy.rb +195 -0
- data/lib/henitai/survivor_selector.rb +36 -0
- data/lib/henitai/survivor_test_filter.rb +72 -0
- data/lib/henitai/unparse_helper.rb +5 -2
- data/lib/henitai/version.rb +1 -1
- data/lib/henitai.rb +10 -0
- data/sig/configuration_validator.rbs +46 -22
- data/sig/henitai.rbs +329 -53
- metadata +46 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cda194514bf677d142e24c97c1589714041d2c88c4d2b33ffaf642e79c7ebe5d
|
|
4
|
+
data.tar.gz: 8505d5d6add5b850c5accb4904f43351e129b9261699c58051c35faa1a4b3758
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b17e21c51f0d4aac03154d8b782d4e361ea8fa792c03a984b9d5b94deccd2411ffb3fe8a7e6fe096b76b33f2bc211a813618c059642b4dec3e54b8fab4d01d8a
|
|
7
|
+
data.tar.gz: 109527f73b390086f03b5249dd90e97bf57ad3e34551f94d1f80b8f1e5968699e8d6cefe394ebf87bc10465b2a759de737d5f54257807e654a92cd040fa73f59
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `henitai run` now exits 0 when a run evaluates no valid mutants, instead of
|
|
14
|
+
failing a CI gate on a vacuous result
|
|
15
|
+
- Flaky-retry counts are now recorded on the parallel execution path (they were
|
|
16
|
+
always reported as 0 regardless of actual retries)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `Result` and `Reporter::Json` take their IO as an injected dependency,
|
|
20
|
+
restoring the domain/infrastructure boundary (no public API change)
|
|
21
|
+
- Decomposed the monolithic `integration.rb` into single-responsibility files
|
|
22
|
+
and reparented `Integration::Minitest` off `Rspec` onto a shared
|
|
23
|
+
`MutantRunSupport` mixin; restored class-size discipline across the codebase
|
|
24
|
+
- Narrowed broad rescues in `safe_unparse` and per-test coverage snapshotting
|
|
25
|
+
|
|
26
|
+
### Internal
|
|
27
|
+
- De-mocked runner specs, added `process_wakeup` and helper coverage, and
|
|
28
|
+
removed sleep- and chdir-based flakiness from the suite
|
|
29
|
+
- Enabled full-operator dogfooding configuration and isolated the
|
|
30
|
+
`minitest_simplecov` spec
|
|
31
|
+
- Reconciled README and consolidated plan-tree documentation
|
|
32
|
+
|
|
33
|
+
## [0.2.0] - 2026-04-30
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- `--survivors-from <path>` flag on `henitai run` for survivor-only reruns:
|
|
37
|
+
re-execute only the mutants that survived a prior full run without re-running
|
|
38
|
+
the whole suite
|
|
39
|
+
- `SurvivorLoader` reads a Stryker-compatible JSON report and extracts survivor
|
|
40
|
+
IDs, per-survivor coverage maps (`coveredBy`), and the anchoring git SHA
|
|
41
|
+
- `SurvivorSelector` filters the current mutant set to the survivor subset; emits
|
|
42
|
+
a drift warning when more than 50% of loaded survivors are unmatched (source
|
|
43
|
+
changed significantly since the prior run)
|
|
44
|
+
- `SurvivorTestFilter` skips survivors whose covering tests are unchanged since
|
|
45
|
+
the prior report's git SHA, marking them `:survived` immediately without
|
|
46
|
+
execution — same safety logic as StrykerJS incremental mode
|
|
47
|
+
- `MutantIdentity` module: stable SHA256 identity computed from expression,
|
|
48
|
+
operator, description, location, and mutation signature; shared by
|
|
49
|
+
`MutantHistoryStore` and `Mutant#stable_id`
|
|
50
|
+
- `Mutant#stable_id` exposes the stable identity; emitted as `stableId` in the
|
|
51
|
+
JSON report so survivor reports remain useful across commits
|
|
52
|
+
- `Result` carries `session_id` (UUID) and optional `git_sha` (HEAD SHA at
|
|
53
|
+
report time); both emitted in the Stryker-compatible JSON as `sessionId` /
|
|
54
|
+
`gitSha`
|
|
55
|
+
- `Reporter::Json` writes an immutable per-session snapshot alongside the
|
|
56
|
+
canonical report at `reports/sessions/<session_id>/mutation-report.json`,
|
|
57
|
+
giving `--survivors-from` a stable reference path across runs
|
|
58
|
+
- `GitDiffAnalyzer#head_sha` — returns the current HEAD SHA; `nil` when git is
|
|
59
|
+
unavailable (conservative fallback: all survivors are executed)
|
|
60
|
+
- `ProcessWorkerRunner` — flat process-slot scheduler for parallel mutant
|
|
61
|
+
execution: each slot owns one OS process, slots are refilled as children
|
|
62
|
+
finish, no thread per child
|
|
63
|
+
- Interrupt handling, spawn failure isolation, and in-slot retry added to
|
|
64
|
+
`ProcessWorkerRunner` (PR 6)
|
|
65
|
+
- Timeout precision and two-phase process-group cleanup in
|
|
66
|
+
`ProcessWorkerRunner` (PR 5)
|
|
67
|
+
- x86_64 platform added to gem platform list
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- JSON mutation report vendor extension now always includes `sessionId`
|
|
71
|
+
(and `gitSha` when available) to support survivor-only reruns
|
|
72
|
+
- Terminal reporter labels partial reruns as "Partial survivor rerun" and
|
|
73
|
+
shows matched / unmatched / skipped-by-diff counts
|
|
74
|
+
- History store skips `runs` row insertion for partial reruns to avoid
|
|
75
|
+
distorting trend analytics; per-mutant `current_status` upsert still runs
|
|
76
|
+
- CLI exits 0 for partial reruns with a printed warning; threshold comparison
|
|
77
|
+
is skipped (applying a partial score to a CI gate is misleading)
|
|
78
|
+
- `StaticFilter` merges per-test coverage into standard coverage so
|
|
79
|
+
`coveredBy` data is available to both RSpec and Minitest survivor reports
|
|
80
|
+
- `.henitai.yml` default: operators set to `light`, timeout lowered to 10 s,
|
|
81
|
+
`max_flaky_retries: 3` added
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
- Minitest autorun hook suppressed in mutation child processes to prevent
|
|
85
|
+
spurious re-runs of the full suite inside each fork
|
|
86
|
+
- Coverage bootstrap RSpec subprocess ARGV leakage resolved — child processes
|
|
87
|
+
no longer inherit the parent's `--format` / file arguments
|
|
88
|
+
- Survivor rerun state preserved across RSpec execution (was reset on each
|
|
89
|
+
subprocess boot)
|
|
90
|
+
- `--survivors-from` now respects dirty worktrees: coverage and source file
|
|
91
|
+
state are read from the working tree, not the index
|
|
92
|
+
- Recipe fast path skipped when source files changed since the cached run,
|
|
93
|
+
preventing stale cache hits after edits
|
|
94
|
+
|
|
95
|
+
### Performance
|
|
96
|
+
- File discovery cached in the integration layer; repeated calls within one
|
|
97
|
+
run no longer re-scan the filesystem
|
|
98
|
+
- Polling sleep removed from the scheduler hot loop; slots are refilled
|
|
99
|
+
event-driven on child exit
|
|
100
|
+
|
|
10
101
|
## [0.1.10] - 2026-04-16
|
|
11
102
|
|
|
12
103
|
### Fixed
|
|
@@ -211,7 +302,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
211
302
|
- CLI critical path: `henitai run` now executes the full pipeline, supports `--since`, returns CI-friendly exit codes, and `henitai version` prints `Henitai::VERSION`
|
|
212
303
|
- RSpec per-test coverage output: `henitai/coverage_formatter` now writes `coverage/henitai_per_test.json`
|
|
213
304
|
|
|
214
|
-
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.1
|
|
305
|
+
[Unreleased]: https://github.com/martinotten/henitai/compare/v0.2.1...HEAD
|
|
306
|
+
[0.2.1]: https://github.com/martinotten/henitai/compare/v0.2.0...v0.2.1
|
|
307
|
+
[0.2.0]: https://github.com/martinotten/henitai/compare/v0.1.10...v0.2.0
|
|
215
308
|
[0.1.10]: https://github.com/martinotten/henitai/compare/v0.1.9...v0.1.10
|
|
216
309
|
[0.1.9]: https://github.com/martinotten/henitai/compare/v0.1.8...v0.1.9
|
|
217
310
|
[0.1.8]: https://github.com/martinotten/henitai/compare/v0.1.7...v0.1.8
|
data/README.md
CHANGED
|
@@ -15,10 +15,9 @@ A Ruby mutation testing framework
|
|
|
15
15
|
## Maturaty
|
|
16
16
|
|
|
17
17
|
- This is alpha software, there will be bugs
|
|
18
|
-
- Henitai tests itself
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- Lots of code has been carefully crafted by AI agents, not everything has been reviewed by humans (yet)
|
|
18
|
+
- Henitai tests itself and other projects
|
|
19
|
+
- It might break with a release
|
|
20
|
+
- If you need a mature solution for mutation testing in ruby, take a look at the [https://github.com/mbj/mutant](Mutant gem)
|
|
22
21
|
|
|
23
22
|
## What is mutation testing?
|
|
24
23
|
|
|
@@ -28,6 +27,8 @@ A mutation testing tool makes small, systematic changes — *mutants* — to you
|
|
|
28
27
|
|
|
29
28
|
The ratio of killed mutants to total mutants is the **Mutation Score** (MS). A high mutation score is a strong quality signal.
|
|
30
29
|
|
|
30
|
+
As mutation testing modifies your code you make sure that your tests cannot have any unintended side-effects.
|
|
31
|
+
|
|
31
32
|
## Installation
|
|
32
33
|
|
|
33
34
|
Add to your `Gemfile`:
|
|
@@ -42,7 +43,7 @@ Or install globally:
|
|
|
42
43
|
gem install henitai
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
**Requires Ruby 4.0.
|
|
46
|
+
**Requires Ruby 4.0.0+**
|
|
46
47
|
|
|
47
48
|
## Quick start
|
|
48
49
|
|
|
@@ -56,6 +57,9 @@ bundle exec henitai run --since origin/main
|
|
|
56
57
|
# Run on a specific subject pattern
|
|
57
58
|
bundle exec henitai run 'MyClass#my_method'
|
|
58
59
|
bundle exec henitai run 'MyNamespace*'
|
|
60
|
+
|
|
61
|
+
# Re-run only survivors from a prior mutation report
|
|
62
|
+
bundle exec henitai run --survivors-from reports/mutation-report.json
|
|
59
63
|
```
|
|
60
64
|
|
|
61
65
|
Configuration lives in `.henitai.yml`:
|
|
@@ -68,6 +72,9 @@ integration:
|
|
|
68
72
|
includes:
|
|
69
73
|
- lib
|
|
70
74
|
|
|
75
|
+
excludes:
|
|
76
|
+
- lib/henitai/eager_load.rb # standalone entry points with no in-process coverage
|
|
77
|
+
|
|
71
78
|
mutation:
|
|
72
79
|
operators: light # light | full
|
|
73
80
|
timeout: 10.0
|
|
@@ -112,6 +119,12 @@ and the terminal only shows progress plus a concise summary. Pass
|
|
|
112
119
|
when the mutation score meets the low threshold, `1` when it does not, and `2`
|
|
113
120
|
for framework errors.
|
|
114
121
|
|
|
122
|
+
`henitai run --survivors-from ...` performs a partial rerun: it reports only
|
|
123
|
+
the selected survivors, skips threshold-based exit checks, and does not update
|
|
124
|
+
the run trend history. Dirty worktree changes are included, so you can edit
|
|
125
|
+
tests locally without committing first; if source files under `includes` are
|
|
126
|
+
dirty, Henitai reruns the matched survivors conservatively.
|
|
127
|
+
|
|
115
128
|
The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/workspaces/henitai/assets/schema/henitai.schema.json) for editor autocompletion.
|
|
116
129
|
|
|
117
130
|
## Operator sets
|
|
@@ -129,13 +142,17 @@ The repository ships a JSON Schema at [`assets/schema/henitai.schema.json`](/wor
|
|
|
129
142
|
**Full** — adds lower-signal operators:
|
|
130
143
|
|
|
131
144
|
- `ArrayDeclaration`, `HashLiteral`, `RangeLiteral`
|
|
145
|
+
- `MethodChainUnwrap` — remove a link from a method chain
|
|
146
|
+
- `RegexMutator` — mutate regexp quantifiers, anchors, char-class negation
|
|
132
147
|
- `SafeNavigation` — `&.` → `.`
|
|
133
148
|
- `PatternMatch` — case/in arm removal
|
|
134
149
|
- `BlockStatement` — remove blocks
|
|
135
150
|
- `MethodExpression` — remove calls
|
|
136
151
|
- `AssignmentExpression` — mutate compound assignment
|
|
152
|
+
- `UnaryOperator` — remove unary `-` and `~`
|
|
153
|
+
- `UpdateOperator` — swap compound assignments (`+=`↔`-=`, `*=`↔`/=`, `||=`↔`&&=`)
|
|
137
154
|
|
|
138
|
-
## Stryker Dashboard integration
|
|
155
|
+
## Stryker Dashboard integration
|
|
139
156
|
|
|
140
157
|
```yaml
|
|
141
158
|
# .henitai.yml
|
|
@@ -150,7 +167,12 @@ dashboard:
|
|
|
150
167
|
base_url: "https://dashboard.stryker-mutator.io"
|
|
151
168
|
```
|
|
152
169
|
|
|
153
|
-
Set `STRYKER_DASHBOARD_API_KEY` in your CI environment to publish reports.
|
|
170
|
+
Set `STRYKER_DASHBOARD_API_KEY` in your CI environment to publish reports. When
|
|
171
|
+
the key, project, and version are present, the `dashboard` reporter uploads the
|
|
172
|
+
Stryker-schema report to the dashboard REST API (default
|
|
173
|
+
`https://dashboard.stryker-mutator.io`); otherwise it is skipped silently. The
|
|
174
|
+
project defaults to the git remote and the version to the current branch or CI
|
|
175
|
+
ref (`GITHUB_REF_NAME`/`GITHUB_REF`/`GITHUB_SHA`).
|
|
154
176
|
|
|
155
177
|
JSON reports are written to `reports/mutation-report.json` by default. Set
|
|
156
178
|
`reports_dir` to change the output directory.
|
|
@@ -172,6 +194,10 @@ Framework integration smoke projects live under `spec/fixtures/integration_smoke
|
|
|
172
194
|
and exercise `henitai` against small RSpec and Minitest apps via the local path
|
|
173
195
|
dependency.
|
|
174
196
|
|
|
197
|
+
Git hook support is tracked in [`.githooks/pre-commit`](/Users/martinotten/projects/mo/henitai/.githooks/pre-commit).
|
|
198
|
+
Enable it with `git config core.hooksPath .githooks` so commits run RuboCop,
|
|
199
|
+
RSpec, and the integration smoke suite before they are created.
|
|
200
|
+
|
|
175
201
|
A Dev Container configuration is included (`.devcontainer/`) for VS Code with the official `ruby:4`
|
|
176
202
|
image, the Codex CLI, and RTK preinstalled. Codex support is bootstrapped with
|
|
177
203
|
`rtk init -g --codex --auto-patch` during container creation.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
class CLI
|
|
7
|
+
# Implements `henitai clean`: removes generated report artifacts listed in
|
|
8
|
+
# {CLI::REPORT_CLEANUP_PATHS} and prints a deletion summary. Mixed into
|
|
9
|
+
# {CLI}.
|
|
10
|
+
module CleanCommand
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def clean_command
|
|
14
|
+
@command_halted = false
|
|
15
|
+
options = parse_clean_options
|
|
16
|
+
return if @command_halted
|
|
17
|
+
|
|
18
|
+
config = load_config(options)
|
|
19
|
+
removed_paths = cleanup_report_artifacts(config)
|
|
20
|
+
puts clean_summary(removed_paths)
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
handle_run_error(e)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cleanup_report_artifacts(config)
|
|
26
|
+
removed_paths = report_cleanup_paths(config).select { |path| File.exist?(path) }
|
|
27
|
+
removed_paths.each { |path| FileUtils.rm_f(path) }
|
|
28
|
+
removed_paths
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def report_cleanup_paths(config)
|
|
32
|
+
REPORT_CLEANUP_PATHS.map do |relative_path|
|
|
33
|
+
File.join(config.reports_dir, *relative_path)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clean_summary(removed_paths)
|
|
38
|
+
return "No generated report artifacts to clean" if removed_paths.empty?
|
|
39
|
+
|
|
40
|
+
format(
|
|
41
|
+
"Removed %<count>s generated report artifact%<plural>s",
|
|
42
|
+
count: removed_paths.length,
|
|
43
|
+
plural: removed_paths.length == 1 ? "" : "s"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class CLI
|
|
5
|
+
# Shared helpers for CLI command handlers: configuration loading (applying
|
|
6
|
+
# CLI overrides on top of the config file) and uniform error handling.
|
|
7
|
+
module CommandSupport
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def load_config(options)
|
|
11
|
+
Configuration.load(
|
|
12
|
+
path: options.fetch(:config, Configuration::CONFIG_FILE),
|
|
13
|
+
overrides: configuration_overrides(options)
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configuration_overrides(options)
|
|
18
|
+
deep_compact(
|
|
19
|
+
{
|
|
20
|
+
integration: options[:integration],
|
|
21
|
+
all_logs: options[:all_logs],
|
|
22
|
+
mutation: {
|
|
23
|
+
operators: options[:operators],
|
|
24
|
+
timeout: options[:timeout]
|
|
25
|
+
},
|
|
26
|
+
jobs: options[:jobs]
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def deep_compact(value)
|
|
32
|
+
case value
|
|
33
|
+
when Hash
|
|
34
|
+
value.each_with_object({}) do |(key, nested_value), result|
|
|
35
|
+
compacted = deep_compact(nested_value)
|
|
36
|
+
result[key] = compacted unless compacted.nil?
|
|
37
|
+
end
|
|
38
|
+
when Array
|
|
39
|
+
value.map { |item| deep_compact(item) }.compact
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_run_error(error)
|
|
46
|
+
warn "#{error.class}: #{error.message}"
|
|
47
|
+
exit 2
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class CLI
|
|
5
|
+
# Implements `henitai init`: writes a starter `.henitai.yml`, optionally
|
|
6
|
+
# prompting for the default RSpec integration when stdin is a TTY. Mixed
|
|
7
|
+
# into {CLI}.
|
|
8
|
+
module InitCommand
|
|
9
|
+
INIT_TEMPLATE_LINES = [
|
|
10
|
+
"# yaml-language-server: $schema=./assets/schema/henitai.schema.json",
|
|
11
|
+
"includes:",
|
|
12
|
+
" - lib",
|
|
13
|
+
"mutation:",
|
|
14
|
+
" operators: light",
|
|
15
|
+
" timeout: 10.0",
|
|
16
|
+
" max_flaky_retries: 3",
|
|
17
|
+
" sampling:",
|
|
18
|
+
" ratio: 0.05",
|
|
19
|
+
" strategy: stratified",
|
|
20
|
+
"reports_dir: reports",
|
|
21
|
+
"thresholds:",
|
|
22
|
+
" high: 80",
|
|
23
|
+
" low: 60"
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def init_command
|
|
29
|
+
path = @argv.shift || Configuration::CONFIG_FILE
|
|
30
|
+
unexpected_arguments = @argv.dup
|
|
31
|
+
warn "Unexpected arguments: #{unexpected_arguments.join(' ')}" unless unexpected_arguments.empty?
|
|
32
|
+
exit 1 unless unexpected_arguments.empty?
|
|
33
|
+
|
|
34
|
+
File.write(path, init_template)
|
|
35
|
+
puts "Created #{path}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def init_template
|
|
39
|
+
template = init_template_lines
|
|
40
|
+
template << integration_block if include_default_integration?
|
|
41
|
+
"#{template.join("\n")}\n"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def init_template_lines
|
|
45
|
+
INIT_TEMPLATE_LINES.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def include_default_integration?
|
|
49
|
+
return true unless $stdin.tty?
|
|
50
|
+
|
|
51
|
+
print "Use the default RSpec integration? [Y/n] "
|
|
52
|
+
response = $stdin.gets&.strip&.downcase
|
|
53
|
+
response.nil? || response.empty? || !%w[n no].include?(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def integration_block
|
|
57
|
+
<<~YAML.chomp
|
|
58
|
+
integration:
|
|
59
|
+
name: rspec
|
|
60
|
+
YAML
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Henitai
|
|
4
|
+
class CLI
|
|
5
|
+
# Implements `henitai operator`: lists the built-in operators with their
|
|
6
|
+
# human-readable descriptions and examples. Mixed into {CLI}.
|
|
7
|
+
module OperatorCommand
|
|
8
|
+
OPERATOR_METADATA = {
|
|
9
|
+
"ArithmeticOperator" => ["Arithmetic operators", "a + b -> a - b"],
|
|
10
|
+
"EqualityOperator" => ["Comparison operators", "a == b -> a != b"],
|
|
11
|
+
"LogicalOperator" => ["Boolean operators", "a && b -> a || b"],
|
|
12
|
+
"BooleanLiteral" => ["Boolean literals", "true -> false"],
|
|
13
|
+
"ConditionalExpression" => ["Conditional branches", "if cond then ... end"],
|
|
14
|
+
"StringLiteral" => ["String literals", '"foo" -> ""'],
|
|
15
|
+
"ReturnValue" => ["Return expressions", "return x -> return nil"],
|
|
16
|
+
"ArrayDeclaration" => ["Array literals", "[1, 2] -> []"],
|
|
17
|
+
"HashLiteral" => ["Hash literals", "{ a: 1 } -> {}"],
|
|
18
|
+
"RangeLiteral" => ["Range literals", "1..5 -> 1...5"],
|
|
19
|
+
"SafeNavigation" => ["Safe navigation", "user&.name -> user.name"],
|
|
20
|
+
"PatternMatch" => ["Pattern matching", "in { x: Integer } -> in { x: String }"],
|
|
21
|
+
"BlockStatement" => ["Block statements", "{ do_work } -> {}"],
|
|
22
|
+
"MethodExpression" => ["Method calls", "call_service -> nil"],
|
|
23
|
+
"AssignmentExpression" => ["Assignment expressions", "x += 1 -> x -= 1"],
|
|
24
|
+
"MethodChainUnwrap" => ["Method chain unwrap", "a.b.c -> a.b"],
|
|
25
|
+
"RegexMutator" => ["Regex literals", "/foo+/ -> /foo*/"],
|
|
26
|
+
"UnaryOperator" => ["Unary operators", "-x -> x"],
|
|
27
|
+
"UpdateOperator" => ["Compound assignment", "x += 1 -> x -= 1"]
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def operator_command
|
|
33
|
+
subcommand = @argv.shift
|
|
34
|
+
case subcommand
|
|
35
|
+
when "list" then puts operator_list_text
|
|
36
|
+
when nil, "-h", "--help" then puts operator_help_text
|
|
37
|
+
else
|
|
38
|
+
warn "Unknown operator command: #{subcommand}"
|
|
39
|
+
warn operator_help_text
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
rescue ArgumentError => e
|
|
43
|
+
warn e.message
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def operator_help_text
|
|
48
|
+
<<~HELP
|
|
49
|
+
Hen'i-tai operator commands
|
|
50
|
+
|
|
51
|
+
Usage:
|
|
52
|
+
henitai operator list
|
|
53
|
+
|
|
54
|
+
Run `henitai operator list` to see all built-in operators.
|
|
55
|
+
HELP
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def operator_list_text
|
|
59
|
+
validate_operator_metadata!
|
|
60
|
+
sections = [
|
|
61
|
+
operator_list_section("Light set", Operator::LIGHT_SET),
|
|
62
|
+
operator_list_section("Full set", Operator::FULL_SET)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
["Available operators", *sections].join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def operator_list_section(title, names)
|
|
69
|
+
rows = names.map { |name| operator_description_row(name) }
|
|
70
|
+
([title] + rows).join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def operator_description_row(name)
|
|
74
|
+
description, example = operator_metadata[name] || fallback_operator_metadata
|
|
75
|
+
|
|
76
|
+
format("- %<name>s: %<description>s (%<example>s)", name:, description:, example:)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def operator_metadata
|
|
80
|
+
OPERATOR_METADATA
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fallback_operator_metadata
|
|
84
|
+
["No metadata available", "n/a"]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def validate_operator_metadata!
|
|
88
|
+
missing = Operator::FULL_SET - operator_metadata.keys
|
|
89
|
+
return if missing.empty?
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, "Missing operator metadata for: #{missing.join(', ')}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Henitai
|
|
6
|
+
class CLI
|
|
7
|
+
# Builds the OptionParser instances for the `run` and `clean` commands and
|
|
8
|
+
# parses argv into an options Hash. Help/version options set
|
|
9
|
+
# +@command_halted+ so the caller can skip the rest of the command.
|
|
10
|
+
module Options
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def parse_run_options
|
|
14
|
+
options = {}
|
|
15
|
+
build_run_option_parser(options).parse!(@argv)
|
|
16
|
+
options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_clean_options
|
|
20
|
+
options = {}
|
|
21
|
+
build_clean_option_parser(options).parse!(@argv)
|
|
22
|
+
options
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_run_option_parser(options)
|
|
26
|
+
OptionParser.new do |opts|
|
|
27
|
+
opts.banner = "Usage: henitai run [options] [SUBJECT_PATTERN...]"
|
|
28
|
+
add_since_option(opts, options)
|
|
29
|
+
add_integration_option(opts, options)
|
|
30
|
+
add_config_option(opts, options)
|
|
31
|
+
add_operator_option(opts, options)
|
|
32
|
+
add_jobs_option(opts, options)
|
|
33
|
+
add_output_option(opts, options)
|
|
34
|
+
add_survivors_from_option(opts, options)
|
|
35
|
+
add_fail_on_survivors_option(opts, options)
|
|
36
|
+
add_help_option(opts)
|
|
37
|
+
add_version_option(opts)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_clean_option_parser(options)
|
|
42
|
+
OptionParser.new do |opts|
|
|
43
|
+
opts.banner = "Usage: henitai clean [options]"
|
|
44
|
+
add_config_option(opts, options)
|
|
45
|
+
add_help_option(opts)
|
|
46
|
+
add_version_option(opts)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def add_since_option(opts, options)
|
|
51
|
+
opts.on("--since GIT_REF", "Only mutate subjects changed since GIT_REF") do |ref|
|
|
52
|
+
options[:since] = ref
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_integration_option(opts, options)
|
|
57
|
+
opts.on("--use INTEGRATION", "Test framework integration (rspec)") do |name|
|
|
58
|
+
options[:integration] = name
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_config_option(opts, options)
|
|
63
|
+
opts.on("--config PATH", "Path to .henitai.yml") do |path|
|
|
64
|
+
options[:config] = path
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add_operator_option(opts, options)
|
|
69
|
+
opts.on("--operators SET", "Operator set: light | full") do |set|
|
|
70
|
+
options[:operators] = set
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_jobs_option(opts, options)
|
|
75
|
+
opts.on("--jobs N", Integer, "Number of parallel workers (default: 1)") do |n|
|
|
76
|
+
options[:jobs] = n
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_output_option(opts, options)
|
|
81
|
+
opts.on("--all-logs", "--verbose", "Print all captured child logs") do
|
|
82
|
+
options[:all_logs] = true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def add_survivors_from_option(opts, options)
|
|
87
|
+
opts.on(
|
|
88
|
+
"--survivors-from PATH",
|
|
89
|
+
"Re-run only survivors from a prior report " \
|
|
90
|
+
"(partial rerun; threshold checks are skipped; dirty worktrees are included)"
|
|
91
|
+
) do |path|
|
|
92
|
+
options[:survivors_from] = path
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_fail_on_survivors_option(opts, options)
|
|
97
|
+
opts.on(
|
|
98
|
+
"--fail-on-survivors",
|
|
99
|
+
"Exit 1 for partial reruns when any survivors remain (otherwise exits 0)"
|
|
100
|
+
) do
|
|
101
|
+
options[:fail_on_survivors] = true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_help_option(opts)
|
|
106
|
+
opts.on("-h", "--help", "Show this help") do
|
|
107
|
+
puts opts
|
|
108
|
+
@command_halted = true
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add_version_option(opts)
|
|
113
|
+
opts.on("-v", "--version", "Show version") do
|
|
114
|
+
puts Henitai::VERSION
|
|
115
|
+
@command_halted = true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|