Skip to content

[Bug]: Findings from first pass at mutation testing #245

@dmcilvaney

Description

@dmcilvaney

Mutation Testing Triage — azldev

Generated from two full-repo gremlins passes (merged):

  • Default mutators (5 types: CONDITIONALS_BOUNDARY, CONDITIONALS_NEGATION, ARITHMETIC_BASE, INVERT_NEGATIVES, INCREMENT_DECREMENT) — 87.27% efficacy, 238 LIVED survivors.
  • Inverted config (.gremlins.yaml, 6 less-common types: INVERT_LOGICAL, INVERT_LOOPCTRL, INVERT_ASSIGNMENTS, INVERT_BITWISE, REMOVE_SELF_ASSIGNMENTS) — 85.01% efficacy, 97 LIVED survivors.

The two mutant sets are disjoint, so this is a clean union of 335 survivors.

How to read this

Each LIVED survivor (a mutation no test caught) is classified on three axes:

Axis Values Meaning
Verdict fix / nofix Is this a real assertion gap worth closing? nofix = equivalent mutant, log/error-wording only, cosmetic, or unreachable/defensive.
Pri hi / med / lo / Severity if the underlying behavior actually broke (only for fix).
Prag y / n Pragmatic to test? Is the test worth the effort? n even for real gaps when catching it needs disproportionate scaffolding (containers, real binaries, network, root) or is inherently brittle (timing/jitter, map-iteration order). All nofix are n.

The actionable set is fix + Prag=y: real gaps that a cheap, focused test would close.

Mutator abbreviations: CB=CONDITIONALS_BOUNDARY, CN=CONDITIONALS_NEGATION, AB=ARITHMETIC_BASE, IN=INVERT_NEGATIVES, ID=INCREMENT_DECREMENT, IL=INVERT_LOGICAL, ILC=INVERT_LOOPCTRL, IA=INVERT_ASSIGNMENTS, IB=INVERT_BITWISE, RSA=REMOVE_SELF_ASSIGNMENTS.

Summary

Default New Combined
LIVED survivors 238 97 335
fix 109 72 181
nofix 129 25 154
fix · hi 16 16 32
fix · med 59 36 95
fix · lo 34 20 54
fix · Prag=y (actionable) ~170
fix · Prag=n (real but not worth it) ~11

Headline: the tests are genuinely solid (154/335 survivors are equivalent/cosmetic), and of the 181 real gaps, the large majority are cheap, focused unit tests — only a handful (timing/jitter, map-order, integration-only) aren't worth the cost.

High-priority shortlist (the 32 hi gaps, all Prag=y unless noted)

Security / data integrity

Location Mut Why it matters
cmds/component/changed.go:517 IL path-traversal escape guard never fires (silently disabled)
utils/fileutils/copy.go:194 ILC recursive copy stops after first subdir, dropping siblings
utils/archive/archive.go:286 IB extracted file perms forced to 0777
projectconfig/resources.go:108 CN+IL merge wipes earlier-layer RpmRepos
projectconfig/resources.go:116 CN+IL merge wipes earlier-layer RpmRepoSetTemplates
projectconfig/resources.go:124 CN+IL merge wipes earlier-layer RpmRepoSets
projectconfig/resources.go:929 CN GPG-enabled set without key passes validation

Correctness

Location Mut Why it matters
core/components/resolver.go:470 CN spec-path conflict detection inverted (wrong spec accepted)
core/components/resolver.go:145 ILC break drops remaining loose components after dedup hit
core/components/resolver.go:318 ILC break drops later matches after excluded spec
core/components/resolver.go:406 ILC break drops remaining group members after duplicate
core/sources/synthistory.go:682 IL upstream-commit boundary defeated, collects from HEAD
cmds/image/boot.go:347 IL valid image/iso-only boot wrongly rejected
cmds/component/update.go:179 IL false "no components matched" on a matched filter
rpm/spec/edit.go:346 IL InsertTag lands tag in wrong section/package
rpm/spec/edit.go:395 RSA InsertTag can place tag inside %if (conditional, changes build)
rpm/spec/edit.go:777 AB+IN GetHighestPatchTagNumber off-by-one → patch number reuse
utils/dirdiff/dirdiff.go:247 IL every changed file emits "special" message, not a real diff
utils/dirdiff/dirdiff.go:274 CB phantom blank line in added-file diff
utils/dirdiff/dirdiff.go:278 CB phantom blank line in removed-file diff
utils/retry/retry.go:106 CB MaxAttempts==0 → operation never runs
lockfile/store.go:107 CN uncached Get makes every fresh-process lock load fail
rpm/repoquery.go:175 CN wrong/missing --releasever queries wrong distro
rpm/extractor.go:220 CN symlinks silently dropped during RPM extraction
projectconfig/loader.go:108 CN Project-merge early-return skips all later merges
projectconfig/project.go:90 CN valid rpm-repos skip downstream validators
cmds/advanced/mock.go:205 CN explicit --mock-config path silently ignored

Convergence: resources.go:108/116/124 and rpm/extractor.go:220-225 were each flagged by both runs — strong signals. One MergeUpdatesFrom test (non-empty receiver, distinct keys) kills ~6 mutants.


Full triage by package

A. internal/rpm/spec — 66 (43 default + 23 new)

Location Mut Verdict Pri Prag Notes
edit.go:191 CB fix lo y tagFamily all-digit tag returns it, no panic
edit.go:191 IL fix med y tagFamily("Source9999")=="source"; no direct coverage
edit.go:289 CB fix lo y family-tag insert position when tag at line 0
edit.go:293 CB fix lo y insert when only tag is line 0
edit.go:330 CN nofix n sectionFound still set; equivalent
edit.go:331 IL fix med y InsertTag to missing sub-package → ErrSectionNotFound
edit.go:336 CN nofix n scan bound only; unobservable
edit.go:337 CN nofix n sectionEnd bound; unobservable
edit.go:337 IL fix med y sectionEnd from correct package (multi-package spec)
edit.go:346 IL fix hi y InsertTag ignores foreign-section tags
edit.go:385 CB fix lo y inserted tag lands after %endif
edit.go:385 CB nofix n defensive len() bound; panic-only
edit.go:394 CB nofix n marginal scan bound / defensive
edit.go:394 ILC/ID fix lo/med y skip past multi-line conditional stops at sectionEnd
edit.go:395 RSA fix hi y InsertTag past nested %if/%endif stays unconditional
edit.go:433 IL fix med y PrependLinesToSection inserts after header
edit.go:604 IL nofix n Atoi("") fails regardless; equivalent
edit.go:622 CN nofix n HasSection still true via body; equivalent
edit.go:668 RSA nofix n totalRemoved 0 before first accumulation
edit.go:681 RSA fix lo y RemovePatchEntry tag-only (not patchlist) match
edit.go:765 CB nofix n running-max >/>= identical
edit.go:777 AB+IN fix hi y bare Patch:+Patch1 returns 1 (dup-number bug)
edit.go:777 CB nofix n inner guard makes >=0 equivalent
edit.go:896 CB/IL nofix n removable section never at line 0; equivalent
edit.go:909 CB nofix n documented-unreachable fallback
edit.go:999 CB nofix n %if never equals section bounds; equivalent
edit.go:1000 CB nofix n directive line never equals section index
edit.go:1004 ILC fix med y RemoveSection balances multiple conditional pairs
edit.go:1015 CB nofix n running-min identical
edit.go:1027/1050/1063/1093 AB nofix n error-message line number only
edit.go:1046 AB+IN fix med y removal errors when trim-boundary line has content
edit.go:1085 CB nofix n %else never coincides; equivalent
edit.go:1085 IL fix lo y %else/%elif spanning-section removal errors
edit.go:1086 CB nofix n %if/%endif never equals section index
edit.go:1086 CN fix med y RemoveSection with internal %if/%else/%endif
edit.go:1086 IL fix lo y straddling pair not treated as fully-inside
spec.go:258 AB nofix n internal parse counter; output unaffected
spec.go:259 IA fix med y no duplicate insertion to non-final section
spec.go:259 RSA fix med y visitor cursor correct after InsertLinesAfter
spec.go:440 CN fix med y Visit propagates section-end error
spec.go:449 CN fix lo y Visit propagates SpecEnd callback error
spec.go:559 CB+CN fix lo y trailing "-n" empty; -n overrides positional
spec.go:571 ID nofix n loop self-corrects via default case
specquery.go:63/144 IL nofix n logging/diagnostic only
specquery.go:139 ILC fix lo y parse robust to blank line mid-output (continue)
spectool.go:24 IL fix/nofix lo y/n scheme-only "mailto:" non-URL (one of two is equivalent)
spectool.go:50 ILC fix lo y blank line skipped, not truncating list
spectool.go:56 ILC fix lo y malformed line doesn't drop later sources

B. internal/projectconfig — 44 (35 default + 9 new)

Location Mut Verdict Pri Prag Notes
component.go:419 CB nofix n leading-hyphen pkg name unrealistic
component.go:423 RSA nofix n accumulation masked by delimiter scan
component.go:472 ILC nofix n at-most-one-group invariant; break==continue
distro.go:210 CN fix med y amd64 host selects X86_64 mock override
distro.go:210 IL fix med y amd64 + empty X86_64 leaves path unchanged
distro.go:212 CN fix/nofix lo y/n Aarch64-only def must not select arm path
distro.go:212 IL fix med y amd64 + set Aarch64 leaves path unchanged
loader.go:108 CN fix hi y sections after Project still merge (no early return)
loader.go:149 CN nofix n mergeResources never errors; equivalent
loader.go:161 CN fix med y Resources actually merged when present
overlay.go:258 CN fix med y invalid regex in search-replace-in-spec must error
overlay.go:278 CN fix med y invalid regex in search-replace-in-file must error
package.go:121 ILC nofix n at-most-one-group invariant
project.go:90 CN fix hi y valid rpm-repos must not skip validators
project.go:94/98/105/109 CN fix med y invalid sets/distro/collision after valid still rejected
resources.go:108 CN+IL fix hi y merge preserves earlier-only RpmRepos (no wipe)
resources.go:108 CN fix med y merge into nil RpmRepos allocates
resources.go:108/116/124 CB nofix n empty→non-nil map; loop no-ops
resources.go:116 CN+IL fix hi y merge preserves earlier-only templates
resources.go:116 CN fix med y merge into nil templates allocates
resources.go:124 CN+IL fix hi y merge preserves earlier-only sets
resources.go:124 CN fix med y merge into nil sets allocates
resources.go:546 CB×4+CN fix lo y URI scheme first-char letter detection (table test)
resources.go:596 CN fix lo y Default(): ""→binary, others pass through
resources.go:816 CN nofix n diagnostic description text only
resources.go:921 CN fix med y invalid base-uri / skipped GPG must error
resources.go:929 CN fix hi y GPG-enabled set without key must error (inverted)
resources.go:937 CN fix med y malformed gpg-key must be rejected
resources.go:1049 AB nofix n 1-based index in error message
resources.go:558 ILC nofix n break exits switch only (last stmt); equivalent

C. internal/app/azldev/cmds — 71 (47 default + 24 new)

Location Mut Verdict Pri Prag Notes
advanced/mock.go:179 CB/CN nofix n empty-list guard / interactive RunShell
advanced/mock.go:205 CN fix hi y explicit MockConfigPath returns NewRunner(path)
component/build.go:412 ILC fix med y RPMs after none-channel RPM still moved
component/changed.go:467 IL fix med y no fingerprint-match when in only one lock map
component/changed.go:517 IL fix hi y repoRelPath rejects ".."/escaping paths
component/diffsources.go:70 CN fix med y error on >1 components, success on one
component/preparesources.go:90 CN fix med y no spurious error when resolve succeeds
component/render.go:1002/1119 CN nofix n guards slog.Debug on error only
component/render.go:1094 ILC fix med y failure marker written for errored after non-errored
component/render.go:1163 IL fix hi y --clean-stale -a without --output-dir/--force accepted
component/render.go:1254 AB nofix n map capacity hint
component/render.go:1258 CN fix med y aliased component dir not reported orphan
component/render.go:1281/1296 ILC fix lo n orphan detection after sibling — map-iteration-order brittle
component/update.go:179 CN+IL fix hi/med y "no components matched" only when zero matched
component/update.go:263/290/322/327/352 CB/CN nofix n log/error-wording guards
component/update.go:359 ILC fix med y savable lock written after skipped/upToDate
component/update.go:394 CN+IL fix med y local clears / upstream keeps lock.ImportCommit
component/update.go:401 CN+IL fix lo/med y seed ImportCommit only for upstream first update
component/update.go:402 CN fix med y seed only for upstream source type
component/update.go:651/653/711 ID/AB nofix n log-only summary/progress counters
component/update.go:700 CB/CN nofix n progress-display guard
component/update.go:771/784 ID nofix n progress sync-count only
component/update.go:773/786 ILC fix med y components after up-to-date/reuse-locked still processed
downloadsources.go:138 CN fix med y error when all lookaside URIs fail
downloadsources.go:141 ILC fix med y download stops after first successful URI
downloadsources.go:156/161 CN nofix n logged display path only
image/boot.go:288 CN fix med y needEmptyDisk/validation for ISO-only boot
image/boot.go:347 IL fix hi y --image-path-only boot accepted
image/pytestrunner.go:296/335 CN/IL fix med y pytest args use absolute image/glob path
pkg/list.go:222/223/226/227 CN fix lo y sort order by PackageName then Type
pkg/list.go:223/227/319 CB nofix n <→<= equivalent inside distinct-key guard
pkg/list.go:589 CN nofix n guards conflicting-mapping slog.Warn
pkg/list.go:599 ILC fix med y srpm entries after duplicate packageName still mapped
pkg/list.go:647 ILC fix med y -debuginfo for packages after debug-named entry
pkg/list.go:652/679 ILC fix lo n -debuginfo/-debugsource after collision — map-order brittle
repo/query.go:368/525 AB nofix n slice capacity hints
repo/query.go:389/399 ILC fix med y --no-srpms skips only source; later arch repo emitted

D. internal/app/azldev (+ core/components, core/sources) — 64 (44 default + 20 new)

Location Mut Verdict Pri Prag Notes
app.go:88/123/195/294/332/378 CN/RSA/IL nofix n version string / root guard / usage tmpl / log gates
app.go:239 CN fix lo y empty GroupID gets default, explicit preserved
app.go:637 CN fix med y failing post-init callback propagates (injectable)
app.go:648 CN fix med y runnable leaf command survives removeEmptyCommands
command.go:107 IL fix med y error when projectDir set but config nil
command.go:183 IL fix lo y nil / bool result yields no formatted output
command.go:255 CB nofix n MarshalIndent never empty; equivalent
core/components/resolver.go:145 ILC fix hi y break drops remaining loose components
core/components/resolver.go:318 ILC fix hi y break drops later matches after excluded spec
core/components/resolver.go:406 ILC fix hi y break drops remaining group members
core/components/resolver.go:470 CN fix hi y conflicting spec path errors; matching doesn't
core/components/resolver.go:764-770 CN/ILC nofix n lock-drift slog.Warn scan
core/components/resolver.go:837/839 CB/CN nofix/fix lo n/y fix-suggestion text (mostly cosmetic)
core/sources/mockprocessor.go:272/278 ILC fix med y later components processed after missing/errored result
core/sources/sourceprep.go:233 IL fix med y .git preserved only when overlays AND withGitRepo
core/sources/sourceprep.go:541/564 CN/AB/IN nofix n removal-failure warn / count in slog.Info
core/sources/sourceprep.go:1001 CN fix lo y macro filename with '_'/'+' accepted
core/sources/synthistory.go:315/335/371 CN/CB nofix n updateHead error path / debug-gate / log value
core/sources/synthistory.go:434 CN fix med y branch ref (not detached HEAD) updated after rewrite
core/sources/synthistory.go:682 IL fix hi y commits newer than upstreamCommit excluded (boundary)
env.go:135 CN fix lo y classicToolkitDir derived only when ProjectDir set
env.go:226 CB nofix n retries==1 clamps either way; equivalent
env.go:341/345/352/357 CN/CB/AB/IA/RSA nofix n console suggestion-box sizing; cosmetic
env.go:395 CN×4 / IL×2 fix med y newLockStore nil/non-nil per input guards
env.go:461/465 CN fix med y Distro() resolves valid config; errors on empty name

E. internal/utils — 56 (41 default + 15 new)

Location Mut Verdict Pri Prag Notes
archive/archive.go:286 CB fix med y entry sized exactly maxEntryBytes accepted
archive/archive.go:286 IB fix hi y extracted file mode preserved, not forced 0777
dirdiff/dirdiff.go:247 IL fix hi y modified text file yields unified diff, not special-msg
dirdiff/dirdiff.go:274/278 CB fix hi y no phantom blank line in added/removed-file diff
dirdiff/dirdiff.go:406 CB fix med y leading NUL byte detected as binary
dirdiff/json.go:88/96/99 ID fix med y line numbers across sequential add/remove/context
dirdiff/json.go:115 CB+CN fix med y 4-field hunk header parses real start lines
dirdiff/render.go:82 ILC nofix n empty line trailing only; equivalent
downloader/downloader.go:180 CB/CN nofix n progress telemetry only
externalcmd/externalcmd.go:268 CB/CN nofix/fix med y configured file listeners tail and receive lines
externalcmd/externalcmd.go:407 IL fix med y RunAndGetOutput errors w/ only file/stdout listener
externalcmd/externalcmd.go:437 CN nofix n gates stderr warning log
fileutils/aferocustom/.../allowedroots.go:189 ILC nofix n parents exist; pathsToCreate unchanged
fileutils/copy.go:194 ILC fix hi y siblings after a subdirectory entry still copied
fileutils/embedfs.go:142 CB fix lo y Readdir(0) reads all entries
fileutils/file.go:102 CB nofix n DEL(0x7f) boundary unreachable in RPM names
iso/iso.go:78 CN nofix n default description label; cosmetic
kiwi/kiwi.go:359/363 CN fix med y --target-arch/--profile appended iff set
kiwi/kiwi.go:424 CN nofix n SELinux error-message wording
parmap/parmap.go:76 CB nofix n clamping limit 1→1; no-op
parmap/parmap.go:122 ILC nofix n identical Cancelled results; redundant notifications
prereqs/prereqs.go:48/79/88 CN fix med/lo y prompt + auto-install + proceed on success
prereqs/prereqs.go:128 CN/IL fix med/lo y fallback to /usr/lib/os-release; surface other errors
reflectable/prettywriter.go:56/70 CN/IL nofix n table styling/header-color; cosmetic
reflectable/tablewriter.go:223/224 IL/ILC fix med y OmitEmpty rendering; fields after omitted still rendered
retry/retry.go:106 CB fix hi y MaxAttempts==0 defaults so operation still runs
retry/retry.go:110/114/118/129/132/138/142 CN/AB/IN/CB nofix n backoff/jitter timing magnitude; non-deterministic
retry/retry.go:82 ILC nofix n loop exits next iteration anyway; equivalent
retry/retry.go:139 IA/RSA fix/nofix lo n delay within jitter band — brittle/statistical

F. internal/providers, rpm (non-spec), lockfile, repolayout, projectgen, fingerprint — 34 (28 default + 6 new)

Location Mut Verdict Pri Prag Notes
fingerprint/fingerprint.go:154 CB nofix n len()>0 before sorted-key loop; equivalent
lockfile/lockfile.go:223 ILC fix lo n remaining-component validation — map-order flaky
lockfile/lockfile.go:287 IL fix med y non-lock/hidden files not reported as orphans
lockfile/store.go:107 CN fix hi y fresh (uncached) Get of on-disk lock succeeds
lockfile/store.go:162 CN fix lo y Save propagates write failure
lockfile/store.go:194 CN fix med y Exists false after Remove (cache eviction)
projectgen/projectgen.go:66 CN fix lo y error returned when .gitignore write fails
providers/rpmprovider/rpmprovider.go:66 CN nofix n telemetry/event args only
providers/sourceproviders/fedorasource.go:265 AB nofix n sourceIndex+1 in progress log
providers/sourceproviders/fedorasourceprovider.go:298 CN nofix n deferred cleanup logs Debug only
providers/sourceproviders/sourcemanager.go:209 CB/CN nofix n retry config; no accessor, brittle
providers/sourceproviders/sourcemanager.go:228/236/237 CN/IL nofix n Warn/Debug log gates
providers/sourceproviders/sourcemanager.go:322 CN×2/IL fix med/lo y lookaside tried when hash/hashType present
providers/sourceproviders/sourcemanager.go:356/364/425/451 CN fix med y lookaside attempted/built; origin success; UpstreamName
repo/repolayout/layout.go:80 AB nofix n slice capacity hint
rpm/extractor.go:220 CN fix hi y symlink extracted when fsLinker present
rpm/extractor.go:221 CN/IB fix med y symlink header detected as link
rpm/extractor.go:225 IL fix med y symlink unsupported when fsLinker nil
rpm/mock/mock.go:568/662/666/693 CB/CN fix lo y config-opts only emitted when corresponding value set
rpm/mock/mock.go:671 CB nofix n len()>0 before sorted-key loop; equivalent
rpm/repoquery.go:175 CN fix hi y --releasever appended with value only when set

Suggested order of attack (Prag=y, hi first)

  1. One test, many mutants: projectconfig/resources.go MergeUpdatesFrom (non-empty receiver, distinct keys) — kills the 108/116/124 wipe mutants from both runs.
  2. Security: changed.go:517 path-traversal guard; archive.go:286 perms; copy.go:194 recursive-copy break.
  3. Resolver break-drops: resolver.go:145/318/406 — multi-component fixtures asserting all survive.
  4. Validation gates: resources.go:929 GPG; loader.go:108 / project.go:90 early-return/skip.
  5. The remaining hi, then med. Skip all Prag=n (real but brittle/expensive: retry jitter, map-order-dependent orphan/debuginfo ordering, no-accessor retry config).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: testingRelates to automated testing of tools (unit testing, scenario testing)bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions