Pass over a declared source root that is not there - #220
Open
MarkusPaulsen wants to merge 1 commit into
Open
Conversation
Discovery refused any declared source root that was not an existing directory. That is right where the path is there and is not a directory, which rejectsSourceRootThatIsNotADirectory pins down. It also fired where the path was simply absent, which neither Gradle nor Maven treats as an error: both collect nothing from such a directory and carry on. Nothing catches the refusal, so JUnit recorded it against every policy-protected test. In SCOREReproducibilityPackage, whose build file names assignment/src, a path present only once a student repository is checked out there, 220 of 294 tests in one run failed with the same IllegalStateException. An absent root is now passed over, named in a warning together with the descriptor that declared it, and the source set is marked as no longer known to be the whole of the project. That is the mechanism this class already used for a declaration it could not resolve, and it keeps the supervised package from being counted across what may be part of a project. Two faults on the Maven side would have made this worse rather than better. A declared sourceDirectory that was passed over emptied the list, which the reader took for "nothing was declared" and answered with src/main/java, the very substitution the refusal existed to prevent; whether something was declared is now tracked apart from what it resolved to. And the broad catch around Maven reading rewrote every refusal as "Cannot parse Maven source roots", the SecurityException for an escaping root included; it now covers reading the file alone. Whether the roots were there is folded in before completeness is written, so a path that reads cleanly but is absent cannot leave a replacement looking complete. This makes one thing quieter, knowingly. A masking defect, an example inside a comment read as a declaration, used to end the run. It now empties the roots and marks them incomplete. The two are indistinguishable once the reader has produced the same declaration, so what defends against a decoy is correct masking and the tests for it, which assert on the roots and still pass.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A source root that a build file names but the project does not contain ended every test in the run. Gradle and Maven both treat such a directory as legal and simply take nothing from it, so Ares now passes it over, says which one it passed over, and records that the source set is no longer known to be the whole project. A path that exists without being a directory, and one outside the project, are refused exactly as before.
Linked issues
No linked issues.
1. Problem
Discovery refused any declared source root that was not an existing directory. That is right for a path that exists and is not a directory, and
ProjectSourcesFinderEdgeCaseTestpins that case down. It also fired for a root that is simply not there, which no build tool treats as an error.ProjectSourcesFinderis at fault, invalidateSourceRootand its callers. Nothing catches the refusal, so JUnit records it against every policy-protected test.Observed in
ls1intum/SCOREReproducibilityPackageon JDK 17 and Gradle 9.7.1, upgrading 2.1.2 to 2.1.3: its build file declaresassignment/src, a path that exists only once a student repository is checked out there. 220 of 294 tests in one run failed with the sameIllegalStateException, all eight container images and every mode job red.Nothing was recorded wrongly, since the run ends before any attack executes. What was lost is the run itself, and a diagnosis a reader had to dig a stack trace out of a test report to find.
2. Improvement from the user's perspective
An exercise whose build file names a directory that is optional, generated later, or checked out only in some runs now works instead of failing wholesale. That covers a test repository exercised on its own and any build declaring a generated source directory whose producer legitimately writes nothing.
Where a root is passed over, the log names it and the build file it came from, so the fix is one line in that file rather than a stack trace to interpret.
Enforcement does not become more permissive. The roots that are there still scope what Ares supervises, and because the source set is marked as not known to be whole, the supervised package is taken from the compiled output instead of being counted across what may be part of a project.
3. Improvement from the maintainer's perspective
One rule now says what it means.
Files.isDirectorywas false both for a path that is absent and for one occupied by a file, and only the second is a configuration error. Those are separate conditions in separate methods, so each can be read and tested on its own.Two latent faults on the Maven side go with it. A declared
<sourceDirectory>that was passed over would have emptied the list and silently reactivatedsrc/main/java, which is the substitution the old refusal existed to prevent; the reader now tracks that something was declared, apart from what it resolved to. And the broadcatcharound the whole of Maven reading rewrote every refusal asCannot parse Maven source roots, including theSecurityExceptionfor a root outside the project; it now covers reading the file alone.Maven can also report incomplete roots now, where it previously hard-coded completeness.
4. Testing manual
Build tool independent: the change is in discovery, which runs before any mode is chosen. Gradle and Maven are both exercised below.
Prerequisites
Steps
mvn test -Dtest=ProjectSourcesFinderAbsentRootTest.mvn test -Dtest=ProjectSourcesFinderEdgeCaseTest,ProjectSourcesFinderTest.mvn test -Dtest=JavaProjectScannerPackageFallbackTest.Expected result
Step 1: 14 tests pass. Read their names in the output rather than the count: they state the contract, including that a replacement naming one present and one absent root keeps the present one, that a replacement naming only an absent root leaves the set empty rather than falling back to
src/main/java, that a root outside the project is still refused, and that a link pointing at nothing is passed over while one pointing at a directory is followed and checked on what it resolves to.Step 2: for each passed-over root, a line naming that root and the build file that declared it, then one line per source set saying its roots are no longer known to be the whole of it. A run that passes a root over silently would be wrong, and so would one that names no descriptor.
Step 3: 52 tests pass. Two matter most, and both assert on a refusal that must survive:
rejectsSourceRootThatIsNotADirectorywrites a file and declares it as a root, andrejectsGradleRootEscapingTheProjectdeclares one outside the project.Step 4: 10 tests pass, including
ignoresAPresentSourceRootReportedAsIncomplete. It puts a real source root holding packagesomewhere.other.entirelynext to compiled output holdingde.tum.cit.aet, marks the roots incomplete, and requires the compiled output to win. Flipping that flag totruemakes it fail withsomewhere.other.entirely, which is what shows the test is about the flag rather than about the fixture.Negative case (what must still be rejected)
Three refusals must survive, and steps 1 and 3 each check some of them: a declared root that exists and is not a directory, a declared root resolving outside the project, and a Maven root outside the project, which must arrive as
SecurityExceptionrather than as a parsing failure.ProjectSourcesFinderAbsentRootTestcovers the Maven half,ProjectSourcesFinderEdgeCaseTestthe Gradle half.Ares has become more permissive in exactly one respect, deliberately, and it is worth a reviewer's attention: a masking defect, where an example inside a comment is read as a declaration, used to end the run loudly. It now empties the roots and marks them incomplete. The two are indistinguishable once the reader has produced the same declaration, so what defends against a decoy is correct masking and the tests for it,
ignoresSourceDirectoriesInsideCommentsAndStringsandignoresSourceDirectoriesInsideASlashyString, both of which assert on the roots and both of which still pass.Modes exercised
No mode-specific behaviour changed.
5. Test case coverage regarding this PR
Figures from a local
mvn test -Punit-core-tests,coverageon this branch, since the aggregated CI report is not available before the run.BuildToolConfigurationandJavaProjectScannercarry Javadoc corrections only and have no row.ProjectSourcesFinderEvery new test asserts on the discovered roots and the completeness flag, not on a run finishing. The scanner test was confirmed to fail when the flag it is about is flipped.
Breaking changes and migration
No breaking changes or migration. The public API under
de.tum.cit.ase.ares.apiis unchanged, and so are the policy file format, the generated security test code and the minimum JDK, Maven and Gradle versions.The behaviour change goes in the permissive direction only: an exercise that failed on 2.1.3 because a declared source root was absent now runs. An exercise that worked before works unchanged, since a root that was there was never passed over. Nothing has to be done to upgrade.
Checklist
docs/,README.adoc, Javadoc) was updated where the change is user-facing.Review progress