Skip to content

fix/review-findings-batch-1: Batch-1 review fixes — glob alignment, DELETE diffs, locale, shallow-clone msg, DX#27

Merged
vedanthvdev merged 1 commit intomasterfrom
fix/review-findings-batch-1
Apr 22, 2026
Merged

fix/review-findings-batch-1: Batch-1 review fixes — glob alignment, DELETE diffs, locale, shallow-clone msg, DX#27
vedanthvdev merged 1 commit intomasterfrom
fix/review-findings-batch-1

Conversation

@vedanthvdev
Copy link
Copy Markdown
Owner

Context

Post-v1.9.15 in-depth review across four lenses (correctness,
reliability, security, adversarial) surfaced a batch of issues. This
PR applies every tier-1 finding with a targeted test so the regression
cannot re-enter silently. The tier-2 findings (mostly wording and log
tone) will land in a follow-up.

The two correctness fixes that could silently reduce coverage

outOfScope*Dirs glob alignment

PathToClassMapper (diff-side) honoured "api-test/**" but
ProjectIndex (on-disk) treated the same string as a literal prefix.
A mixed diff — one production file + a refactor under api-test/
bucketed the api-test file correctly yet still dispatched tests
discovered under api-test/src/test/java. This is the exact hole the
v1.9.14 --explain hint was introduced to warn about, but the fix
itself never landed.

Both sides now delegate to a shared OutOfScopeMatchers utility,
with a regression test that exercises literal and glob shapes against
identical on-disk layouts and asserts they produce the same dispatch
map.

git rm-only MRs were silently skipping all tests

DiffEntry.DELETE's newPath is /dev/null and the detector
only recorded newPath, so a pure-rm MR produced an empty diff and
routed the whole change through EMPTY_DIFF → SKIPPED under
local/ci mode. Now DELETE entries surface through their old
path, the ignore/out-of-scope rules apply, and deleted production
classes reach the transitive strategy like any other change. The
engine's existing missing-file filter still drops FQNs whose backing
test file is gone, so surfacing deletions never asks Gradle to run a
missing test.

Reliability hardening

  • InvalidPathException (NUL bytes, Linux-committed foo:bar.md
    on Windows, reserved device names) now caught in isIgnored and
    the glob matcher. Before, the unhandled exception killed the whole
    affectedTest task with a stack trace; now the offending file
    falls through to the unmapped bucket and the safety net escalates
    normally.
  • GitChangeDetector translates JGit's MissingObjectException
    into a targeted message naming the likely cause (shallow clone in
    CI that doesn't know the base ref).
  • Malformed globs now fail at config-time with an
    IllegalStateException naming the config key, list index, and
    offending pattern, instead of surfacing a raw
    PatternSyntaxException from several engine frames deep.
  • parseMode / parseAction / isWindows pinned to
    Locale.ROOT so Turkish-locale JVMs don't turn mode = "ci"
    into Unknown affectedTests.mode 'ci'.

DX / defense-in-depth

  • FQN shape validated before passing to Gradle's --tests argv.
    Discovered strings that aren't Java-shaped identifiers are logged
    as warnings and skipped.
  • ImplementationStrategy now recognises the DefaultFooService
    prefix shape, not only the FooServiceImpl suffix — matching the
    shipped implementationNaming = ["Impl", "Default"].
  • Per-FQN dispatch lines demoted from lifecycle to info. Each
    MR now gets one lifecycle summary per Gradle task
    (:api:test (3 test classes)); individual FQNs print only under
    --info. A 200-class MR no longer buries the outcome line under
    200 dispatch lines.

Documentation

  • Mode Javadoc re-aligned with the mode-defaults table in the
    design doc.
  • Situation Javadoc cross-reference for
    ALL_FILES_OUT_OF_SCOPE fixed.
  • TransitiveStrategy and AffectedTestsExtension now document
    the actual transitiveDepth default of 4.

Test plan

  • ./gradlew check green locally (core + gradle plugin +
    functional tests).
  • New OutOfScopeMatchersTest covers literal/glob/mixed
    entries, blank-and-null quietly dropped, malformed glob fails with
    config-key and index, and NUL-byte path fails closed.
  • New ProjectIndexTest asserts literal-form and glob-form
    produce the same dispatch map for identical on-disk layouts.
  • GitChangeDetectorTest extended with
    detectsDeletedFilesByOldPath exercising a real git rm +
    commit cycle.
  • ImplementationStrategyTest extended with
    findsTestForDefaultPrefixedImplementation forcing the naming-
    convention branch to do the work alone (no implements clause
    to let the AST branch rescue it).
  • New AffectedTestTaskFqnValidationTest covers accept-paths
    and the shell-metachar / whitespace / hyphen / leading-and-trailing-
    dot reject paths.
  • New AffectedTestTaskLocaleTest reproduces the original
    JDK-locale bug by flipping Locale.setDefault to Turkish before
    asserting parse correctness.
  • Post-merge verification on security-service (the
    pilot adopter) once the resulting release is on the Gradle Plugin
    Portal. No behaviour change for config that already worked; the
    fixes only close silent-coverage-loss paths.

…X fixes from the post-v1.9.15 review

Consolidates the tier-1 findings from the four parallel review passes
(correctness, security, reliability, adversarial) run against the v2
codebase. Every fix below landed with a targeted test so the
regression cannot re-enter silently.

Correctness — the two bugs that could silently reduce coverage:

- out-of-scope dir globs now align on both sides of the pipeline.
  The diff-side classifier in PathToClassMapper honoured
  "api-test/**" but the on-disk classifier in ProjectIndex treated
  the same string as a literal prefix, so a mixed diff (one
  production file + a refactor under api-test/) bucketed the
  api-test file correctly yet still dispatched tests discovered
  under api-test/src/test/java. Both sides now delegate to a
  shared OutOfScopeMatchers utility, with a regression test that
  exercises literal and glob shapes against identical on-disk
  layouts.
- `git rm`-only MRs no longer skip all tests. DiffEntry.DELETE
  entries now surface through their old path so ignore/out-of-scope
  rules apply normally; deleted production classes reach the
  transitive strategy instead of routing the whole MR through
  EMPTY_DIFF → SKIPPED under local/ci mode. The existing engine
  filter still drops FQNs whose backing file is gone, so surfacing
  deletions never asks Gradle to run a missing test.
- ImplementationStrategy now recognises the DefaultFooService prefix
  shape, not only the FooServiceImpl suffix. The plugin ships with
  implementationNaming = ["Impl", "Default"] and the Javadoc
  documents both shapes, but the naming-convention loop used to
  append both tokens as suffixes (FooServiceDefault), matching
  nothing real.

Reliability — hardening against inputs the v2 engine didn't handle:

- InvalidPathException (NUL bytes, Linux-committed colon paths on
  Windows, reserved device names) is now caught in both
  PathToClassMapper.isIgnored and the out-of-scope glob matcher.
  Before, the unhandled exception killed the whole affectedTest
  task with a stack trace; now the offending file falls through to
  the unmapped bucket and the safety net escalates normally.
- GitChangeDetector translates JGit's MissingObjectException into a
  targeted message naming the likely cause (shallow clone in CI
  that doesn't know the base ref). Before, users saw the raw JGit
  exception and had to guess whether the problem was the ref, the
  clone depth, or a corrupt repo.
- Malformed globs in outOfScope*Dirs and ignorePaths now fail at
  config-time with an IllegalStateException naming the config key,
  list index, and offending pattern, instead of surfacing a raw
  PatternSyntaxException from several engine frames deep.
- parseMode / parseAction / isWindows pinned to Locale.ROOT so
  Turkish-locale JVMs don't turn `mode = "ci"` into `Unknown
  affectedTests.mode 'ci'`. Matches the Locale.ROOT pin
  AffectedTestsConfig already applied to legacy parsing.

DX and defense-in-depth:

- FQN shape validated before passing to Gradle's --tests argv.
  Discovered strings that aren't Java-shaped identifiers are logged
  as warnings and skipped — a last line of defence against a buggy
  custom strategy or parser anomaly injecting a shell metacharacter
  into the test-filter flag.
- Per-FQN dispatch lines demoted from lifecycle to info. Each MR
  now gets one lifecycle summary per Gradle task
  (":api:test (3 test classes)"); individual FQNs print only under
  --info. Before, a 200-class MR buried the outcome/situation line
  the user actually cared about under 200 dispatch lines.

Documentation:

- Mode Javadoc re-aligned with the mode-defaults table in the
  design doc (LOCAL is the pre-v2 baseline; CI adds the
  DISCOVERY_EMPTY = FULL_SUITE safety net on top of that).
- Situation Javadoc cross-reference for ALL_FILES_OUT_OF_SCOPE
  fixed — the old reference pointed at a since-renamed constant.
- TransitiveStrategy and AffectedTestsExtension now document the
  actual transitiveDepth default of 4, not the pre-v2 value of 2.
@vedanthvdev vedanthvdev merged commit ca505b1 into master Apr 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant