java-topology/defects/quarkus/unit/QuarkusTest.java
russell@unturf.com 8a85da480d micronaut-0004 + diamond-recursion CLEAN sweep: camel/hazelcast/tomcat/undertow/vertx/quarkus-0003 unit tests
micronaut-0004: AbstractAnnotationMetadataBuilder.processAnnotation O(2^D) diamond recursion
in meta-annotation stereotype traversal. isProcessed() guard tracks only current-path ancestors,
not globally visited nodes — diamond meta-annotation hierarchies cause exponential re-visits
of shared base annotations (e.g. @Transactional + @Retryable both extend @InterceptorBinding).
13x at D=8, 41x at D=10. 10/10 unit tests PASS.

quarkus-0003 unit tests: added to QuarkusTest.java for the existing quarkus-0003
BeanDeployment.recursiveBuild diamond defect. 9/9 PASS.

CLEAN markers: camel, hazelcast, tomcat, undertow, vertx — no diamond recursion pattern found.
Hazelcast uses proper Tarjan algorithm. Tomcat uses iterative constraint propagation.
2026-03-29 17:21:51 -04:00

399 lines
17 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Unit test for Quarkus CWE-407 defects:
* quarkus-0001: BeanInfo.getBoundInterceptors — bound.contains (ArrayList) in nested loops
* quarkus-0002: ComponentsProviderGenerator.isDependency — dependants.contains (ArrayList) in loop
* quarkus-0003: BeanDeployment.recursiveBuild — O(2^D) diamond recursion in transitive interceptor-binding resolution
*
* No JUnit. No external deps. Compile and run:
* javac -d . *.java && java -ea unit.QuarkusTest
*/
public class QuarkusTest {
// ---- quarkus-0001 simulation ----
// Simulates getBoundInterceptors(): nested loops over lifecycle + intercepted methods,
// deduplicating into 'bound' list using ArrayList.contains
static long slowGetBoundInterceptors(int methodCount, int interceptorsPerMethod) {
long ops = 0;
List<Integer> bound = new ArrayList<>();
// Loop 1: lifecycleInterceptors.values()
for (int m = 0; m < methodCount / 2; m++) {
for (int i = 0; i < interceptorsPerMethod; i++) {
int interceptorId = i; // interceptors reused across methods (dedup needed)
ops += bound.size() + 1; // cost of ArrayList.contains scan
if (!bound.contains(interceptorId)) {
bound.add(interceptorId);
}
}
}
// Loop 2: interceptedMethods.values()
for (int m = methodCount / 2; m < methodCount; m++) {
for (int i = 0; i < interceptorsPerMethod; i++) {
int interceptorId = i;
ops += bound.size() + 1;
if (!bound.contains(interceptorId)) {
bound.add(interceptorId);
}
}
}
return ops;
}
static long fastGetBoundInterceptors(int methodCount, int interceptorsPerMethod) {
long ops = 0;
Set<Integer> boundSet = new LinkedHashSet<>();
for (int m = 0; m < methodCount / 2; m++) {
for (int i = 0; i < interceptorsPerMethod; i++) {
int interceptorId = i;
ops += 1; // O(1) HashSet.contains
boundSet.add(interceptorId);
}
}
for (int m = methodCount / 2; m < methodCount; m++) {
for (int i = 0; i < interceptorsPerMethod; i++) {
int interceptorId = i;
ops += 1;
boundSet.add(interceptorId);
}
}
// Convert to sorted List at end (one-time O(I log I))
List<Integer> bound = new ArrayList<>(boundSet);
return ops;
}
// ---- quarkus-0002 simulation ----
// Simulates isDependency called O(B) times, each iterating map values (O(B)) and
// calling dependants.contains (ArrayList, O(D)).
static long slowIsDependency(int beanCount, int dependantsPerBean) {
long ops = 0;
// dependencyMap: bean → list of dependants
Map<Integer, List<Integer>> dependencyMap = new TreeMap<>();
for (int b = 0; b < beanCount; b++) {
List<Integer> dependants = new ArrayList<>();
for (int d = 0; d < dependantsPerBean; d++) {
dependants.add((b + d + 1) % beanCount);
}
dependencyMap.put(b, dependants);
}
// isDependency called for each bean (O(B) calls total)
for (int queryBean = 0; queryBean < beanCount; queryBean++) {
for (List<Integer> dependants : dependencyMap.values()) { // O(B) map values
ops += dependants.size(); // ArrayList.contains scan cost
if (dependants.contains(queryBean)) {
break;
}
}
}
return ops;
}
static long fastIsDependency(int beanCount, int dependantsPerBean) {
long ops = 0;
Map<Integer, List<Integer>> dependencyMap = new TreeMap<>();
for (int b = 0; b < beanCount; b++) {
List<Integer> dependants = new ArrayList<>();
for (int d = 0; d < dependantsPerBean; d++) {
dependants.add((b + d + 1) % beanCount);
}
dependencyMap.put(b, dependants);
}
// Build inverted index once: O(B×D)
Set<Integer> allDependants = new HashSet<>();
for (List<Integer> dependants : dependencyMap.values()) {
allDependants.addAll(dependants);
}
// isDependency is now O(1) per call
for (int queryBean = 0; queryBean < beanCount; queryBean++) {
ops += 1; // O(1) HashSet.contains
allDependants.contains(queryBean);
}
return ops;
}
// ---- quarkus-0003 simulation ----
// Faithfully reproduces BeanDeployment.recursiveBuild() — no visited set.
//
// The defect: for each key in the map, recursiveBuild is called. Inside recursiveBuild,
// for every instance whose name is also a key, recursiveBuild is called AGAIN recursively.
// No visited set → diamond shapes cause O(2^D) calls.
//
// The map models: each node's Set<String> stores its direct children.
// recursiveBuild(name) expands the set to include ALL transitive children by mutation.
//
// Diamond structure built for depth D:
// nodes at each level 0..D-1 each have two children at the next level
// All nodes at level D-1 share a single leaf at level D.
// Example D=2: root->{b1,b2}, b1->{leaf}, b2->{leaf}
// Build annotation name set: node "n{id}" maps to its direct children.
// Creates a diamond graph where two branches merge at each level:
// root → {left_1, right_1}
// left_1 → {left_2, right_2}
// right_1 → {left_2, right_2}
// ...
// left_{D-1} → {leaf}
// right_{D-1} → {leaf}
// leaf → {}
// Every node visits its children; diamond convergence at every level causes
// exponential re-visitation without a visited set.
static Map<String, Set<String>> buildDiamond(int depth) {
Map<String, Set<String>> map = new HashMap<>();
String leaf = "leaf";
map.put(leaf, new HashSet<>());
// At each level, there is a "left" and "right" node (except the leaf).
// Both nodes at level L point to the same pair of nodes at level L+1.
String prevLeft = leaf, prevRight = null; // at leaf level only one node
for (int level = depth - 1; level >= 1; level--) {
String left = "L" + level + "_left";
String right = "L" + level + "_right";
Set<String> children;
if (prevRight == null) {
// previous level was single leaf; both new nodes point to leaf
children = new HashSet<>(Set.of(prevLeft));
} else {
children = new HashSet<>(Set.of(prevLeft, prevRight));
}
map.put(left, new HashSet<>(children));
map.put(right, new HashSet<>(children));
prevLeft = left;
prevRight = right;
}
// root points to both prevLeft and prevRight
Set<String> rootChildren = new HashSet<>();
rootChildren.add(prevLeft);
if (prevRight != null) rootChildren.add(prevRight);
map.put("root", rootChildren);
return map;
}
// Exact reproduction of the defect: recursiveBuild without visited set.
// Counts each invocation in callCount.
static Set<String> slowRecursiveBuild(String name,
Map<String, Set<String>> map,
AtomicLong callCount) {
callCount.incrementAndGet();
Set<String> result = map.get(name);
if (result == null) return Collections.emptySet();
// snapshot to avoid CME (defect code iterates transitiveBindingsMap.get(name) twice,
// we snapshot just as the defect's for-loop sees the set at entry time)
List<String> snapshot = new ArrayList<>(result);
for (String child : snapshot) {
if (map.containsKey(child)) {
result.addAll(slowRecursiveBuild(child, map, callCount)); // NO visited guard
}
}
return result;
}
// Fixed: recursiveBuild with visited set
static Set<String> fastRecursiveBuildWithVisited(String name,
Map<String, Set<String>> map,
Set<String> visited,
AtomicLong callCount) {
callCount.incrementAndGet();
if (!visited.add(name)) {
return map.getOrDefault(name, Collections.emptySet());
}
Set<String> result = map.get(name);
if (result == null) return Collections.emptySet();
for (String child : List.copyOf(result)) {
if (map.containsKey(child)) {
result.addAll(fastRecursiveBuildWithVisited(child, map, visited, callCount));
}
}
return result;
}
static Set<String> fastRecursiveBuildInner(String name, Map<String, Set<String>> map,
AtomicLong callCount, Set<String> visited) {
return fastRecursiveBuildWithVisited(name, map, visited, callCount);
}
public static void main(String[] args) {
int pass = 0;
int total = 0;
// --- quarkus-0001 tests ---
{
total++;
long slow = slowGetBoundInterceptors(20, 8);
long fast = fastGetBoundInterceptors(20, 8);
boolean ok = slow > fast * 3;
System.out.println("[quarkus-0001] M=20 I=8: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
long slow = slowGetBoundInterceptors(50, 15);
long fast = fastGetBoundInterceptors(50, 15);
boolean ok = slow > fast * 5;
System.out.println("[quarkus-0001] M=50 I=15: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: both paths must produce same unique interceptor count
Set<Integer> slowBound = new LinkedHashSet<>();
Set<Integer> fastBound = new LinkedHashSet<>();
int methods = 10, interceptors = 5;
// slow: uses ArrayList dedup but we track the same set for checking
List<Integer> slowList = new ArrayList<>();
for (int m = 0; m < methods; m++) {
for (int i = 0; i < interceptors; i++) {
if (!slowList.contains(i)) slowList.add(i);
}
}
Set<Integer> fastSet = new LinkedHashSet<>();
for (int m = 0; m < methods; m++) {
for (int i = 0; i < interceptors; i++) {
fastSet.add(i);
}
}
boolean ok = slowList.size() == fastSet.size();
System.out.println("[quarkus-0001] correctness: slow=" + slowList.size() +
" fast=" + fastSet.size() + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
// --- quarkus-0002 tests ---
{
total++;
long slow = slowIsDependency(100, 5);
long fast = fastIsDependency(100, 5);
boolean ok = slow > fast * 20;
System.out.println("[quarkus-0002] B=100 D=5: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
long slow = slowIsDependency(300, 10);
long fast = fastIsDependency(300, 10);
boolean ok = slow > fast * 100;
System.out.println("[quarkus-0002] B=300 D=10: slow_ops=" + slow + " fast_ops=" + fast +
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: isDependency returns same true/false for same query
Map<Integer, List<Integer>> dmap = new HashMap<>();
dmap.put(0, new ArrayList<>(List.of(1, 2, 3)));
dmap.put(1, new ArrayList<>(List.of(4, 5)));
dmap.put(2, new ArrayList<>(List.of(6)));
// slow: iterate all lists, call contains
boolean slowResult3 = false;
boolean slowResult7 = false;
for (List<Integer> deps : dmap.values()) {
if (deps.contains(3)) { slowResult3 = true; break; }
}
for (List<Integer> deps : dmap.values()) {
if (deps.contains(7)) { slowResult7 = true; break; }
}
// fast: precompute set
Set<Integer> allDeps = new HashSet<>();
for (List<Integer> deps : dmap.values()) allDeps.addAll(deps);
boolean fastResult3 = allDeps.contains(3);
boolean fastResult7 = allDeps.contains(7);
boolean ok = slowResult3 == fastResult3 && slowResult7 == fastResult7
&& slowResult3 == true && slowResult7 == false;
System.out.println("[quarkus-0002] isDependency correctness: bean3=" + fastResult3 +
" bean7=" + fastResult7 + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
// --- quarkus-0003 tests ---
{
total++;
// Diamond depth=4: slow should make far more calls than fast
Map<String, Set<String>> map1 = buildDiamond(4);
AtomicLong slowCalls = new AtomicLong(0);
slowRecursiveBuild("root", map1, slowCalls);
Map<String, Set<String>> map2 = buildDiamond(4);
AtomicLong fastCalls = new AtomicLong(0);
fastRecursiveBuildWithVisited("root", map2, new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc; // any measurable overhead; D=8 test validates exponential growth
System.out.println("[quarkus-0003] diamond D=4: slow_calls=" + sc + " fast_calls=" + fc +
" ratio=" + String.format("%.1f", (double) sc / Math.max(fc, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Diamond depth=8: exponential gap should be large
Map<String, Set<String>> map1 = buildDiamond(8);
AtomicLong slowCalls = new AtomicLong(0);
slowRecursiveBuild("root", map1, slowCalls);
Map<String, Set<String>> map2 = buildDiamond(8);
AtomicLong fastCalls = new AtomicLong(0);
fastRecursiveBuildWithVisited("root", map2, new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc * 10;
System.out.println("[quarkus-0003] diamond D=8: slow_calls=" + sc + " fast_calls=" + fc +
" ratio=" + (sc / Math.max(fc, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: both approaches should collect the same transitive set
// Simple 3-node diamond: A->{B,C}, B->{D}, C->{D}, D->{}
Map<String, Set<String>> map3 = new HashMap<>();
map3.put("A", new HashSet<>(Set.of("B", "C")));
map3.put("B", new HashSet<>(Set.of("D")));
map3.put("C", new HashSet<>(Set.of("D")));
map3.put("D", new HashSet<>());
AtomicLong sc3 = new AtomicLong(0);
slowRecursiveBuild("A", map3, sc3);
Set<String> slowResult3 = map3.get("A"); // mutated in-place to include D
Map<String, Set<String>> map4 = new HashMap<>();
map4.put("A", new HashSet<>(Set.of("B", "C")));
map4.put("B", new HashSet<>(Set.of("D")));
map4.put("C", new HashSet<>(Set.of("D")));
map4.put("D", new HashSet<>());
AtomicLong fc3 = new AtomicLong(0);
Set<String> fastResult = fastRecursiveBuildWithVisited("A", map4, new HashSet<>(), fc3);
boolean ok = fastResult.containsAll(Set.of("B", "C", "D"))
&& slowResult3.containsAll(Set.of("B", "C", "D"));
System.out.println("[quarkus-0003] correctness: slow=" + slowResult3 +
" fast=" + fastResult + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
System.out.println("\n" + pass + "/" + total + " PASS");
if (pass != total) {
System.exit(1);
}
}
}