Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ HELM_READER_VERSION ?= 0.1.2
# classifier depends on this tool's observed failure semantics (a
# cancelled run exits 0 and writes no report, and the report is a single
# write at the end of the run), so the version must not float on
# `@latest`. Override per-build with GREMLINS_VERSION=<ver>.
# `@latest`. The pin also holds still the meaning of --exclude-files,
# which the script relies on to keep a run inside the package it names: a
# Go regex, matched unanchored against slash-separated paths relative to
# the invoked directory. v0.6.0 is the newest release, so the pin cannot
# be lifted by bumping — and it carries a known defect worth knowing
# about before reading a crashed job: a second signal during shutdown
# panics with `send on closed channel` in the tool's own signal
# handling, taking the report with it.
# Override per-build with GREMLINS_VERSION=<ver>.
GREMLINS_VERSION ?= v0.6.0

clean:
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/tui/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package tui

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/cli/tui/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package tui

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/auto_reconciler_rename_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/extract_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/metastructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/resource_summaries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/synchronizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/metastructure/translate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package metastructure

import (
Expand Down
2 changes: 2 additions & 0 deletions internal/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//
// SPDX-License-Identifier: FSL-1.1-ALv2

//go:build unit

package schema

import (
Expand Down
62 changes: 50 additions & 12 deletions scripts/mutation-test-changed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,28 @@ set -euo pipefail
# is a single write after the run — so a version bump means re-checking them.

# ── 1. Classify one package's result ────────────────────────────────────────
# classify_result <report-path>
# classify_result <report-path> <exit-status>
# Prints "status|reason|score|killed|lived|timed_out" for a single package.
#
# Only the report decides the verdict; gremlins' exit status is a diagnostic the
# caller logs. A report that is missing, unreadable, or not shaped like a
# gremlins report is a classification outcome, never an abort.
# Only the report decides the verdict. With no report, gremlins' exit status
# names the reason, because it is all there is to tell a cancelled run — which
# exits 0 and writes nothing — from one that ended before it could write.
# Nothing is read from gremlins' output: it carries the coverage run's own
# output when coverage fails, so a unit test that panics puts panic text there
# without gremlins having died. A report that is missing, unreadable, or not
# shaped like a gremlins report is a classification outcome, never an abort.
classify_result() {
local report_path="$1" result
local report_path="$1" status="$2" result

if [[ ! -f "$report_path" ]]; then
# A gremlins that is not installed leaves the shell exiting 127, so the tool
# is checked before the status of the run that never happened.
if ! command -v gremlins > /dev/null 2>&1; then
echo "failed|gremlins not found|n/a|0|0|0"
else
elif [[ "$status" == 0 ]]; then
echo "failed|no output|n/a|0|0|0"
else
echo "failed|gremlins exited $status without a report|n/a|0|0|0"
fi
return 0
fi
Expand Down Expand Up @@ -140,7 +148,9 @@ PYEOF
echo "$result"
}

# ── 2. Helper: find nearest go.mod by walking up from a directory ───────────
# ── 2. Helpers ──────────────────────────────────────────────────────────────
# find_module_root <pkg>: finds the nearest go.mod by walking up from a
# directory.
find_module_root() {
local dir="$REPO_ROOT/$1"
while [[ "$dir" != "$REPO_ROOT" && "$dir" != "/" ]]; do
Expand All @@ -154,6 +164,23 @@ find_module_root() {
echo "$REPO_ROOT"
}

# has_own_unit_tests <dir>: true when a test file directly in <dir> carries the
# unit build tag. The directory's own test files are listed and handed to grep
# by name rather than letting grep walk the tree, because tests under a
# sub-directory belong to another package and cannot make this one mutable. A
# directory with no test files of its own — or no directory at all — is answered
# before grep is reached, since a grep given a pattern and no file to read would
# take the run's stdin. The glob runs in a subshell, the way the gremlins run
# scopes its own cd, so nullglob stays out of the rest of the script.
has_own_unit_tests() {
(
shopt -s nullglob
local test_files=("$1"/*_test.go)
[[ ${#test_files[@]} -gt 0 ]] \
&& grep -q '//go:build unit' "${test_files[@]}" 2>/dev/null
)
}

# ── 3. Summary plumbing ─────────────────────────────────────────────────────
# On a runner, rows are appended as they are produced so a run that dies
# mid-way still shows what it managed to do. Locally they are buffered and the
Expand Down Expand Up @@ -274,11 +301,10 @@ main() {
# Map to unique package directories
changed_packages=$(echo "$changed_files" | xargs -I{} dirname {} | sort -u)

# Filter to packages that have //go:build unit test files
# Filter to packages that have //go:build unit test files of their own
testable_packages=()
while IFS= read -r pkg; do
pkg_abs="$REPO_ROOT/$pkg"
if [[ -d "$pkg_abs" ]] && grep -qlr '//go:build unit' --include='*_test.go' "$pkg_abs" 2>/dev/null; then
if has_own_unit_tests "$REPO_ROOT/$pkg"; then
testable_packages+=("$pkg")
fi
done <<< "$changed_packages"
Expand Down Expand Up @@ -335,18 +361,30 @@ main() {
in_flight_pkg="$pkg"
in_flight_log="$log_file"

# gremlins mutates the whole directory subtree below the package it is
# invoked on, so a package with sub-packages under it is charged with
# mutants from code the run was never asked about. --exclude-files takes a
# regex matched unanchored against every path gremlins walks below the
# invoked directory, and those paths are always slash-separated whatever the
# platform — so a path holding a separator is by construction in a
# sub-directory, and excluding '/' leaves exactly the invoked package. This
# narrows what is mutated, not what is covered: coverage is still gathered
# over the whole subtree, and gremlins has no flag to narrow that.
#
# No pipeline, so the exit status is gremlins' own and set -e cannot end the
# run here. gremlins may exit non-zero when mutants survive — that's expected
# and not a failure; the status is a diagnostic, never the verdict.
# and not a failure; the status never overrules a report, it only says how a
# run that wrote none ended.
status=0
(cd "$module_root" && gremlins unleash \
--tags unit \
--timeout-coefficient 10 \
--workers 4 \
--exclude-files '/' \
-o "$report_file" \
"./$rel_pkg") > "$log_file" 2>&1 || status=$?

result=$(classify_result "$report_file")
result=$(classify_result "$report_file" "$status")
IFS='|' read -r result_status reason score killed lived timed_out <<< "$result"

if [[ "$result_status" == "ok" ]]; then
Expand Down
Loading
Loading