Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions .plan/marshal.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,10 @@
"lessons_superseded_days": 0,
"temp_on_maintenance": true,
"plugin_cache_keep_versions": 5,
"plugin_cache_keep_days": 3
"plugin_cache_keep_days": 3,
"no_plan_body_days": 7
},
"provisioned_version": "0.1.1261",
"config_seed_fingerprint": "ccb21c71"
"provisioned_version": "0.1.1274",
"config_seed_fingerprint": "59c217c3"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
Expand Down Expand Up @@ -57,10 +59,21 @@ class FrameworkAgnosticArchTest {
"de.cuioss.sheriff.gateway.pipeline.."
};

/** Framework packages that an agnostic-core class must never depend on. */
/**
* Framework packages that an agnostic-core class must never depend on.
* <p>
* {@code io.smallrye..} is listed because the class Javadoc names the per-route SmallRye
* Fault-Tolerance guard as precisely the coupling that keeps {@code routing} out of the agnostic
* set — leaving it unlisted would let an agnostic-core class acquire that same coupling unchecked.
* {@code io.netty..} and {@code org.jboss..} are listed for the same reason: they arrive
* transitively with the Quarkus/Vert.x stack and are framework by any reading of ADR-0005.
*/
private static final String[] FRAMEWORK_PACKAGES = {
"io.quarkus..",
"io.vertx..",
"io.smallrye..",
"io.netty..",
"org.jboss..",
"jakarta.enterprise..",
"jakarta.inject..",
"org.eclipse.microprofile..",
Expand All @@ -78,12 +91,66 @@ void agnosticPackagesMustNotDependOnFrameworks() {
.that().resideInAnyPackage(AGNOSTIC_PACKAGES)
.should().dependOnClassesThat().resideInAnyPackage(FRAMEWORK_PACKAGES)
.because("ADR-0005 requires config.model, config.validation, events, forward, and "
+ "pipeline to remain framework-agnostic (routing is excluded by design)")
.allowEmptyShould(true);
+ "pipeline to remain framework-agnostic (routing is excluded by design)");

rule.check(PRODUCTION_CLASSES);
}

/**
* Guards the guard: every entry in {@link #AGNOSTIC_PACKAGES} must actually resolve to at least
* one production class.
* <p>
* Without this, a renamed package or a single misspelled entry silently reduces the rule above to
* a no-op — it would match zero classes, find zero violations, and stay green while protecting
* nothing. The negative control below cannot catch that: it exercises its own hardcoded package,
* so it proves the ArchUnit mechanism works while saying nothing about whether the real package
* list still resolves. This test is what makes an empty match loud, and it names the offending
* entry rather than failing generically.
*/
@Test
@DisplayName("ADR-0005 gate is non-vacuous: every protected package resolves to at least one class")
void everyAgnosticPackageResolvesToClasses() {
for (String agnosticPackage : AGNOSTIC_PACKAGES) {
long matched = countClassesIn(agnosticPackage);

assertTrue(matched > 0,
() -> "ADR-0005 protected package '" + agnosticPackage + "' resolved to NO "
+ "production classes — the framework-agnostic rule above is silently "
+ "protecting nothing. Fix the entry in AGNOSTIC_PACKAGES, or remove it "
+ "deliberately if the package was retired.");
}
}

/**
* Counts imported production classes residing in {@code packagePattern}, which uses the ArchUnit
* {@code ..} suffix to mean "this package and its subpackages".
* <p>
* Deliberately a direct count rather than an {@link ArchRule} with an always-true condition: any
* such condition risks failing for its own reason instead of for emptiness — {@code bePublic()
* .orShould().bePackagePrivate()} looks universal but rejects a private nested class — which
* would make this guard red for a reason that has nothing to do with the gap it exists to detect.
*/
private static long countClassesIn(String packagePattern) {
String base = packagePattern.endsWith("..")
? packagePattern.substring(0, packagePattern.length() - 2)
: packagePattern;
return PRODUCTION_CLASSES.stream()
.map(JavaClass::getPackageName)
.filter(name -> name.equals(base) || name.startsWith(base + "."))
.count();
}

/**
* Negative control: proves the ArchUnit mechanism actually fails on a real violation, using the
* deliberately framework-coupled {@code quarkus} package as the standing specimen.
* <p>
* <strong>{@code allowEmptyShould(true)} here is deliberate and must not be removed</strong> — the
* asymmetry with the rule above is load-bearing. If the control package ever empties out, this
* setting makes {@code check} pass, which makes {@code assertThrows} fail loudly and tells us the
* control has stopped controlling anything. Removing it would invert that: an empty package would
* make {@code check} throw on emptiness, {@code assertThrows} would be satisfied by the wrong
* exception, and the test would go green while proving nothing.
*/
@Test
@DisplayName("ADR-0005 gate detects a deliberate framework dependency (negative control)")
void gateFailsOnFrameworkDependency() {
Expand Down
Loading