You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Browse filesBrowse the repository at this point in the historyBrowse files
Markus Paulsen
committed
Merge origin/main into the documentation branch
The pull request had gone conflicting against main, and a conflicting pull
request has no merge ref, so GitHub could not schedule any `pull_request`
workflow. Only the labeler ran, on `pull_request_target`. The last two pushes
therefore had no continuous integration at all, which is why this merge is here
rather than later.
One conflict, in the test-case factory manual. Main rewrote the scanning section
when the scanner stopped matching regular expressions and started reading the
syntax tree with the imports resolved. This branch still carried the regex
description, `CLASS_PATTERN` and the rest. Main's text describes what the code
does, so it wins; git had already followed the rename from
`docs/securitytest/TestCaseFactoryAndBuilderManual.md`.
The version bump survives the merge: the pom reads 2.1.3 and nothing below it
refers to Ares. The two remaining mentions of 2.1.2 are in
`ReleasedCreatorContractTest` and `ReleasedConstructorCompatibilityTest`, which
say what the 2.1.2 release published and are kept callable. Those are statements
about a release that happened, not coordinates, and 2.1.3 has published nothing.
The prose gate found one enforced violation in the text main brought over, an
"actually" the sentence does not need, now gone. Three advisory counts rose with
that text: active-voice from 760 to 772, long-sentence from 111 to 115, and
prefer-must from 150 to 151. Those numbers are recorded rather than fixed. A
merge that carries in a page written before these rules existed is the one case
where a ceiling rises honestly, and both `cli.mjs` and the writing-rules page now
say so, along with the expectation that the next pass over that page lowers them.
Verified on the merge: 77 scanner fixtures, lint:prose at 0 enforced, eslint,
tsc, a strict build, 15 browser tests, and 644 documentation structure tests
compiled against main's production code.
Copy file name to clipboardExpand all lines: documentation/docs/contributor/subsystems/securitytest/test-case-factory-and-builder.md
+47-15Lines changed: 47 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -459,7 +459,7 @@ Defines five scanning methods that auto-detect project metadata:
459
459
|---|---|---|
460
460
|`scanForBuildMode()`|`BuildMode`| Whether the project uses Maven (`pom.xml`) or Gradle (`build.gradle`) |
461
461
|`scanForTestClasses()`|`String[]`| Fully qualified names of all classes in the **test source directory** containing `@Test` or `@Property` annotations, or extending JUnit 3's `TestCase`|
462
-
|`scanForPackageName()`|`String`| The most frequently used non-reserved package declaration across all `.java` files|
462
+
|`scanForPackageName()`|`String`| The most frequently used non-reserved package: taken from the production sources, otherwise from the compiled production output, otherwise the configured default|
463
463
|`scanForMainClassInPackage()`|`String`| The class containing `public static void main(String[])`|
464
464
|`scanForTestPath()`|`Path`| The file system path to the test source directory |
465
465
@@ -468,33 +468,65 @@ Defines five scanning methods that auto-detect project metadata:
468
468
| Aspect | Detail |
469
469
|---|---|
470
470
|**Implements**|`ProjectScanner`|
471
-
|**Technique**|Regex-based source code analysis. Walks all`.java` files under the project root and applies four compiled regex patterns. |
471
+
|**Technique**|JavaParser-backed source analysis. Walks the`.java` files under the discovered source roots and reads the parsed syntax tree; the compiled output is read with ArchUnit's `ClassFileImporter` where the sources yield nothing. |
472
472
473
-
**Regex patterns:**
473
+
**What is read from the syntax tree:**
474
474
475
-
|Pattern|Matches| Used by |
475
+
|Fact|Read from| Used by |
476
476
|---|---|---|
477
-
|`CLASS_PATTERN`|`public [final\|abstract\|strictfp] class ClassName`|`extractClassName()`|
|`TEST_ANNOTATION_PATTERN`|`@Test` or `@Property`|`extractTestClass()` (which treats classes containing `extends TestCase` as test classes) |
477
+
| Package declaration | the compilation unit's `PackageDeclaration`|`scanForPackageName()`|
478
+
| Type declarations | the top-level `TypeDeclaration`s, nested types included |`scanForMainClassInPackage()`, `scanForTestClasses()`|
479
+
|`main` method | a `public static void main(String[])` declaration, varargs included |`scanForMainClassInPackage()`|
480
+
| Test classes | a `@Test` or `@Property` annotation, or a JUnit 3 `TestCase` supertype resolved through the imports of the file |`scanForTestClasses()`|
481
+
482
+
Resolving the supertype through the imports is why this is not a regex: `extends TestCase` names a type, and which type it names depends on what the file imported.
**`scanForPackageName()` algorithm:** First filters out reserved infrastructure prefixes (via `ReservedPackageGuard.reservedPrefixOf(...)`) so a student cannot flood the project with files in a trusted namespace to make it the derived enforcement scope → counts the frequency of every remaining `package` declaration across all files → returns the most common one. This heuristic works because in a typical student project, the main source package appears in the majority of files.
495
+
The sort is not cosmetic: it is what makes two runs over one project agree.
496
+
The legacy `findProjectSourcesPath()` route still exists for callers that
497
+
predate `BuildToolConfiguration`, and differs in kind: it returns the
498
+
descriptor's own string, relative and unvalidated, where `discover(...)`
499
+
canonicalises every root and refuses one that escapes the project.
500
+
501
+
**`scanForPackageName()` algorithm:** Resolution runs in three steps, each reached only when the previous one finds nothing at all.
502
+
503
+
1.**Production sources.** Reserved infrastructure prefixes are filtered out first (via `ReservedPackageGuard.reservedPrefixOf(...)`), so a package inside a trusted namespace cannot become the derived enforcement scope. The frequency of every remaining `package` declaration is counted and the most common one wins. This heuristic works because in a typical student project the main source package appears in the majority of files.
504
+
2.**Compiled production output.** Only top-level classes are counted, so a package is not weighted by how many nested or anonymous classes it happens to contain; nesting is read from the class file rather than from the `$` in the binary name, which is a legal identifier character. This step covers a project whose build descriptor the source-root discovery cannot parse, because the build tool writes its output to the conventional directory the scanner reads.
505
+
3.**The configured default** (see [Section 10.3](#103-javaprogrammingexerciseprojectscanner)), with a warning naming the roots that were searched.
506
+
507
+
Step 1 is skipped entirely when the discovered source roots are not known to be the whole of the main source set. A Gradle descriptor can declare a root this reader cannot resolve, such as a computed list, and `BuildToolConfiguration.productionRootsComplete()` reports that. Counting declarations across part of a project produces an answer indistinguishable from one taken across all of it, so a partial set is not counted at all and the compiled output is read instead.
508
+
509
+
> **The derived package is a heuristic. What turns it into a boundary is the check that follows it.**
510
+
>
511
+
> Before enforcement is armed, `requireDerivedScopeToCoverTheProject()` reads the compiled production output and refuses the run unless **every** executable top-level class declares a non-blank, non-reserved package that is the derived scope or lies below it, compared on segment boundaries so that `de.tum.cit.aet` does not swallow `de.tum.cit.aetevil`. A class the scope leaves out, a class in the default package, a class in a reserved package, and an output root that exists but cannot be read are each refused by name. This runs on the policy-free path only: a pinned policy may deliberately supervise part of the output, and narrowing it is then the instructor's decision.
512
+
>
513
+
> **An output root holding nothing passes, with a warning, and so does one holding only `package-info` or `module-info`.** There is then no supervisable class, so enforcement is vacuous rather than mis-scoped, and an exercise whose supervised package is still empty must not fail for being empty. The generated test does the same: `JavaArchunitSupervisedClasses` warns and analyses an empty set rather than refusing. In both places that log line is the only signal, and a suite that analyses nothing reports success.
514
+
>
515
+
> That closes the case where a decoy package is voted the scope while the assignment runs beside it. Three things it still does not establish.
516
+
>
517
+
> The **vote is influenceable by whoever can add files to the project**, and in an Artemis exercise that includes the student. The check above refuses a scope that leaves compiled classes out, but not one drawn *around* them: a scope that covers everything passes by construction. The package-import allow-list no longer follows the scope for that reason, and names the packages the validated output declares instead.
518
+
>
519
+
> The **output directory is assumed, not read**. Step 2 and the check both look in `target/classes` or `build/classes/java/main`, so a build that writes its output elsewhere is not followed there. Together with the vacuous pass above, that is the sharp edge of this section: a project whose output goes somewhere else looks exactly like a project that compiled nothing, and both pass with the same warning. An exercise configured with a custom output destination is therefore not enforced by the derived path at all, and nothing fails to say so. Such an exercise must declare its scope in a policy.
520
+
>
521
+
> **The last-resort default guarantees nothing by itself.** If the project does not contain it, the analysis path resolves to a directory that does not exist. Where anything at all is compiled, the check above catches it: those classes lie outside the default, so the run is refused by name. Where nothing is compiled it does not, and the warning is again all a reader gets. During generation, before anything is compiled, the same is true.
522
+
>
523
+
> An exercise that needs a scope it can rely on declares its package in the security policy. The scanner is then not consulted at all, which is the only version of this that cannot be steered from the submission.
492
524
493
525
**`scanForTestClasses()` algorithm:** Scans only the **test source directory** (see `scanForTestPath()`) and returns every class whose file contains a `@Test` / `@Property` annotation or `extends TestCase`.
494
526
495
527
**`scanForMainClassInPackage()` algorithm:** Collects all classes with a `main` method → prefers a class named `Main` or `Application` → otherwise returns the first match → defaults to `"Main"`.
496
528
497
-
**`scanForTestPath()` algorithm:**Checks for Gradle's custom `srcDir 'test'` → falls back to `src/test/java`.
529
+
**`scanForTestPath()` algorithm:**Answers the first discovered test source root; without a build configuration it accepts the conventional `src/test/java`, or a bare `test/` directory for the Artemis Gradle layout, and otherwise falls back to the literal `src/test/java`**whether or not it exists**. That fall-back is a placeholder forced by the non-null return type rather than a claim, and no production code currently consults this method.
"why": "The advisory findings each rule is allowed. These numbers may only fall; see scripts/prose/cli.mjs. Lower one by improving the prose, never by editing it here.",
2
+
"why": "The advisory findings each rule is allowed. Lower one by improving the prose, never by editing it here. A number rises only where a merge brings in text written before these rules existed, and only in the commit that brings it; see scripts/prose/cli.mjs.",
0 commit comments