Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
daed7fe
Detect the supervised package instead of defaulting to it
Aug 4, 2026
7e5e4bf
Cover the discovery and detection paths that decide the supervised scope
Aug 6, 2026
9732c4f
Abort the permission fixtures when the environment cannot deny access
Aug 6, 2026
626e685
Point the TUM default package at de.tum.cit.aet
Aug 9, 2026
6321ab7
Stop the self-tests inheriting their supervised package from a default
Aug 10, 2026
f2b6d7b
Document what the supervised-package fallback does not guarantee
Aug 10, 2026
0136141
Merge branch 'main' into fix/detect-supervised-package-without-policy
Aug 11, 2026
7186ada
Say what the derived supervised package does not establish
Aug 12, 2026
c7380ac
Read the Gradle descriptor as structure rather than as lines
Aug 12, 2026
4baf86e
Satisfy the quality gates the new scan tripped
Aug 12, 2026
18ce34c
Refuse a derived supervised scope the compiled project contradicts
Aug 13, 2026
3a44573
Say when the discovered source roots are only part of a project
Aug 17, 2026
6757b42
Permit the packages the project has, not the one above them
Aug 17, 2026
e3ebe85
Keep the descriptor reader inside the quality gate
Aug 17, 2026
75887cd
Refuse an output whose classes no scope could be checked against
Aug 17, 2026
88d8613
Put the scanner field and the compiled fixtures where they belong
Aug 17, 2026
560fb44
Describe the scanner the manual actually documents
Aug 17, 2026
20a37dc
Report a wide test-class package instead of refusing it
Aug 17, 2026
137b901
Apply the formatter and drop the imports the shared fixture freed
Aug 17, 2026
74361eb
Cover the string forms the descriptor masker had never been run against
Aug 18, 2026
543b5ab
Make a generated test ask which classes to analyse instead of carryin…
Aug 18, 2026
0274781
Assert what a generated test now asks rather than what it used to carry
Aug 18, 2026
d837178
Ask at runtime in the WALA generation too, and stop a derived fall-ba…
Aug 18, 2026
8e039eb
Make the generated inventory answerable for, not merely present
Aug 18, 2026
c7e2d53
Put back the two things removing every filter threw out
Aug 18, 2026
9308702
Say what the timeout exemption decides, and stop overstating it
Aug 19, 2026
a6bc1f9
Ask for the supervised scope where a test case is made
Aug 19, 2026
cbf88ff
Reserve only what Ares itself must not be confused with
Aug 21, 2026
171ba75
Let an empty supervised package be an empty package
Aug 21, 2026
50b0a21
Say it once, in the place a reader looks
Aug 21, 2026
c96ba7a
Spell the hand-kept copies alike
Aug 21, 2026
8ac170c
Ask a permission both questions, and keep the released signatures
Aug 21, 2026
8e82643
Refuse a sink no supervised frame can be blamed for
Aug 21, 2026
4cdba5f
Let each branch answer for itself
Aug 21, 2026
f4537fd
Say what the empty case actually does
Aug 21, 2026
3be27a0
Leave the released Creator method the one to implement
Aug 21, 2026
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
2 changes: 1 addition & 1 deletion docs/securitytest/TestCaseFactoryAndBuilderManual.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ ProjectSourcesFinder.findProjectSourcesPath()

| Override | Default in `JavaProjectScanner` | Override in `JavaProgrammingExerciseProjectScanner` |
|---|---|---|
| Default package | `""` (empty string) | `"de.tum.cit.ase"` |
| Default package | `""` (empty string) | `"de.tum.cit.aet"` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| Default main class | `"Main"` | `"Main"` (unchanged) |

When the base scanner finds no package or main class, these TUM-specific defaults ensure reasonable behaviour for Artemis-hosted exercises.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,9 @@ public aspect JavaAspectJThreadSystemAdviceDefinitions extends JavaAspectJAbstra
}
// Ares's own infrastructure frames (this advice, internals) are never student
// code, even when restrictedPackage is a broad prefix that nominally covers them
// (e.g. the self-test fallback "de.tum.cit.ase"). Skipping them lets the walk
// reach the TimeoutUtils frame that legitimately owns this @StrictTimeout worker.
// (e.g. the broad "de.tum.cit.ase" scope Ares' own self-tests configure).
// Skipping them lets the walk reach the TimeoutUtils frame that legitimately
// owns this @StrictTimeout worker.
if (className.startsWith("de.tum.cit.ase.ares.api.")) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ private static boolean isThreadCreationFromAresTimeout(@Nullable String restrict
// Ares's own infrastructure frames (this advice, internals) are never student
// code, even when restrictedPackage is a broad prefix that nominally covers
// them
// (e.g. the self-test fallback "de.tum.cit.ase"). Skipping them lets the walk
// reach the TimeoutUtils frame that legitimately owns this @StrictTimeout
// worker.
// (e.g. the broad "de.tum.cit.ase" scope Ares' own self-tests configure).
// Skipping them lets the walk reach the TimeoutUtils frame that legitimately
// owns this @StrictTimeout worker.
if (className.startsWith("de.tum.cit.ase.ares.api.")) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public JavaProgrammingExerciseProjectScanner(BuildToolConfiguration buildConfigu
* declares no package. Overriding the {@code protected} default (rather than
* re-implementing {@code scanForPackageName}) lets the parent's polymorphic
* fallback pick this up.
* <p>
* This is the root package the Artemis exercise templates at TUM are generated
* with. It is deliberately not Ares' own {@code de.tum.cit.ase} namespace: that
* one identifies this library, whereas this value is a guess about the
* supervised project, and the two looking alike is what makes them easy to
* conflate.
*
* @since 2.0.0
* @author Markus Paulsen
Expand All @@ -29,7 +35,7 @@ public JavaProgrammingExerciseProjectScanner(BuildToolConfiguration buildConfigu
@Override
@Nonnull
protected String getDefaultPackage() {
return "de.tum.cit.ase";
return "de.tum.cit.aet";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.type.ArrayType;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ClassFileImporter;

import de.tum.cit.ase.ares.api.buildtoolconfiguration.BuildMode;
import de.tum.cit.ase.ares.api.buildtoolconfiguration.BuildToolConfiguration;
Expand Down Expand Up @@ -78,6 +80,18 @@ public JavaProjectScanner(BuildToolConfiguration buildConfiguration) {
this.buildConfiguration = Objects.requireNonNull(buildConfiguration, "buildConfiguration must not be null");
}

/**
* The last-resort supervised package, used only when neither the production
* sources nor the compiled production output declares one.
* <p>
* Prefer detection over this hook. A default that the project does not contain
* mis-scopes enforcement silently: the analysis path resolves to a directory
* that does not exist, no class is imported, and no resource domain is
* enforced, while nothing fails. {@link #scanForPackageName()} therefore
* reaches this value only when the project offers nothing to detect.
*
* @return the default package name
*/
@Nonnull
protected String getDefaultPackage() {
return "";
Expand Down Expand Up @@ -415,6 +429,31 @@ private static String qualifiedTypeName(String packageName, TypeDeclaration<?> t
return packageName.isEmpty() ? nestedName : packageName + "." + nestedName;
}

/**
* Derives the supervised package from the project, never from a default that
* the project does not contain.
* <p>
* Resolution runs in three steps, and each falls through only when it finds
* nothing at all:
* <ol>
* <li>the most frequent non-reserved package declared by the production
* <em>sources</em>;</li>
* <li>otherwise the most frequent non-reserved package declared by the compiled
* production <em>output</em>. This covers every project whose build descriptor
* the source-root discovery cannot parse, because the compiled output sits at
* the build tool's own location whatever the descriptor says;</li>
* <li>otherwise {@link #getDefaultPackage()}, with a warning naming the roots
* that were searched. Reaching this step means the project declared nothing to
* detect, and a default the project does not contain mis-scopes enforcement
* silently, so the warning is the only signal a reader gets.</li>
* </ol>
* Step 2 is what keeps step 3 out of reach for a real project. The compiled
* output is authoritative whatever the build descriptor says, because the build
* tool writes it to its own location, so a descriptor that source-root
* discovery cannot parse no longer costs the supervised scope.
*
* @return the supervised package name, possibly empty; never null
*/
@Override
@Nonnull
public String scanForPackageName() {
Expand All @@ -425,10 +464,69 @@ public String scanForPackageName() {
counts.merge(name, 1L, Long::sum);
}
}
return counts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry::getKey))
.map(Map.Entry::getKey).findFirst().orElse(getDefaultPackage());
if (counts.isEmpty()) {
Comment thread
Claudia-Anthropica marked this conversation as resolved.
counts = compiledPackageCounts();
}
if (counts.isEmpty()) {
String defaultPackage = getDefaultPackage();
LOG.warn(
"No supervised package could be detected: no production source declared one under {} and no compiled class declared one under {}. "
+ "Falling back to the configured default \"{}\", which enforces nothing if the project does not contain it.",
productionRoots(), productionOutputRoot(), defaultPackage);
return defaultPackage;
}
return counts.entrySet().stream().sorted(
Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder()).thenComparing(Map.Entry::getKey))
.map(Map.Entry::getKey).findFirst().orElseThrow();
}

/**
* Counts the non-reserved packages declared by the compiled production classes.
* <p>
* Only top-level classes are counted: a nested or anonymous class produces its
* own class file, so counting every file would weight a package by how many
* inner classes it happens to contain. Blank package names are skipped, which
* also disposes of {@code module-info.class} at the root of the output tree.
*
* @return the package counts, empty when nothing is compiled or readable
*/
@Nonnull
private Map<String, Long> compiledPackageCounts() {
Path outputRoot = productionOutputRoot();
if (!Files.isDirectory(outputRoot) || !Files.isReadable(outputRoot)) {
return Map.of();
}
Map<String, Long> counts = new HashMap<>();
for (JavaClass javaClass : new ClassFileImporter().importPath(outputRoot)) {
// The binary name carries the '$', so it is what distinguishes a nested or
// anonymous class here. getSimpleName() does not: it answers "Inner" for
// Busy$Inner and the empty string for Busy$1.
if (javaClass.getName().contains("$")) {
continue;
}
String name = javaClass.getPackageName();
if (!name.isBlank() && ReservedPackageGuard.reservedPrefixOf(name) == null) {
counts.merge(name, 1L, Long::sum);
}
}
return counts;
}

/**
* Resolves the compiled production output root for the discovered build tool.
* <p>
* Without a build configuration this mirrors
* {@link BuildMode#getClasspath(Path, String)}, which resolves the build-tool
* directory against the working directory of the test run.
*
* @return the production output root; never null
*/
@Nonnull
private Path productionOutputRoot() {
if (buildConfiguration != null) {
return buildConfiguration.productionOutputRoot();
}
return Path.of(scanForBuildMode().getBuildDirectory()).toAbsolutePath();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,21 @@ public final class ProjectSourcesFinder {
private static final String DEFAULT_TEST_SOURCE = "src/test/java";
private static final Pattern PROPERTY_ASSIGNMENT = Pattern
.compile("(?m)^\\s*(?:def|val|var)?\\s*([A-Za-z_][A-Za-z0-9_.-]*)\\s*=\\s*['\"]([^'\"]+)['\"]");
// The plural alternative must come first, and the singular one must refuse a
// following 's'. Matched the other way round, 'srcDir' consumes the prefix of
// 'srcDirs' and the capture starts at the leftover "s = [", which
// resolveGradlePath cannot resolve: every srcDirs declaration was then dropped
// without a trace, and a project declaring its main sources that way looked to
// Ares like a project with no production sources at all.
//
// Known gaps, deliberately not covered here: Kotlin's setSrcDirs(...) and
// srcDirs.set(...)/from(...), and lists spread over several lines. A
// line-oriented regex cannot follow the Gradle DSL, and pretending otherwise
// trades one silent failure for another. They are non-fatal instead: when the
// descriptor cannot be parsed, JavaProjectScanner.scanForPackageName falls back
// to the compiled output, which is authoritative whatever the build file says.
private static final Pattern SOURCE_DIRECTORY = Pattern
.compile("(?:srcDir\\s*(?:\\(\\s*)?|srcDirs\\s*(?:=|\\()\\s*)([^)\\]\\n}]+)");
.compile("(?:srcDirs\\s*(?:\\+?=|\\()\\s*|srcDir(?!s)\\s*(?:\\(\\s*)?)([^)\\]\\n}]+)");
Comment thread
MarkusPaulsen marked this conversation as resolved.
Outdated
private static String pomXmlPath = "pom.xml";
private static String buildGradlePath = "build.gradle";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void testDefaultPackage_TumSpecific() {

// Then
assertNotNull(result);
assertEquals("de.tum.cit.ase", result); // TUM-specific default package
assertEquals("de.tum.cit.aet", result); // TUM-specific default package
}
}

Expand Down Expand Up @@ -266,7 +266,7 @@ void testTumDefaultPackage() {
String result = scanner.scanForPackageName();

// Then
assertEquals("de.tum.cit.ase", result);
assertEquals("de.tum.cit.aet", result);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,27 @@ class QualifiedLegacy extends junit.framework.TestCase { public void testReal()
class TestCase {}
class SpoofedLegacy extends TestCase { public void testLooksLegacy() {} }
""");
// The same spoof with the declaration in a second file of that package: the
// name then resolves through the package rather than through the compilation
// unit, which is a separate resolution step and must reject it just as firmly.
Files.writeString(tests.resolve("SplitTestCase.java"), """
package split;
class TestCase {}
""");
Files.writeString(tests.resolve("SplitSpoof.java"), """
package split;
class SplitSpoofedLegacy extends TestCase { public void testLooksLegacy() {} }
""");
// A wildcard import is the remaining way a bare TestCase can name the JUnit 3
// class: nothing is imported by name and the package declares no type of that
// name, so resolution falls through to the wildcarded candidates.
Files.writeString(tests.resolve("WildcardLegacy.java"), """
package wildcard;
import junit.framework.*;
class WildcardLegacy extends TestCase { public void testWildcarded() {} }
""");
JavaProjectScanner scanner = new JavaProjectScanner(configuration(production, tests));
assertArrayEquals(new String[] { "legacy.ImportedLegacy", "legacy.QualifiedLegacy" },
assertArrayEquals(new String[] { "legacy.ImportedLegacy", "legacy.QualifiedLegacy", "wildcard.WildcardLegacy" },
scanner.scanForTestClasses());
}

Expand Down
Loading
Loading