java-topology/defects/micronaut-core/unit/MicronautCoreTest.java

219 lines
8.8 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.

import java.util.*;
import java.util.stream.*;
/**
* CWE-407 unit tests for micronaut-core defects.
*
* micronaut-core-0001: HttpClientIntroductionAdvice.buildDefaultBinder
* calls uriCtx.getUriTemplate().getVariableNames().contains(argument.getName())
* inside a per-argument lambda. getVariableNames() allocates a new List<String>
* each invocation and List.contains() is O(V). Called once per argument per
* request → O(A×V) per call.
* Fix: pre-compute Set<String> from variable names once before building the binder.
*
* micronaut-core-0002: DefaultBeanContext.topologicalSort
* Two defects:
* (a) unsorted.stream().map(...).map(...).anyMatch(clazz::isAssignableFrom) is
* O(B) stream reconstruction called for each component of each bean in each
* outer while-iteration → O(B²×C) overall.
* Fix: pre-build Set<Class<?>> unsortedBeanTypes and update on removal.
* (b) sorted.add(0, bean) is O(B) ArrayList prepend → O(B²) total.
* Fix: use ArrayDeque.addFirst() O(1).
*/
public class MicronautCoreTest {
// -----------------------------------------------------------------------
// micronaut-core-0001: URI variable lookup in HTTP client binder
// -----------------------------------------------------------------------
/** Simulates defective per-argument URI variable lookup using List.contains(). */
static boolean uriVarLookupDefect(List<String> argNames, List<String> uriVars) {
int hits = 0;
for (String arg : argNames) {
// Defect: getVariableNames() re-invoked inside lambda → new List each time
List<String> freshList = new ArrayList<>(uriVars); // simulate getVariableNames()
if (freshList.contains(arg)) { // O(V) scan
hits++;
}
}
return hits > 0;
}
/** Fixed version: pre-compute Set<String> once, O(1) lookup. */
static boolean uriVarLookupFixed(List<String> argNames, List<String> uriVars) {
Set<String> varSet = new HashSet<>(uriVars); // computed once, O(V)
int hits = 0;
for (String arg : argNames) {
if (varSet.contains(arg)) { // O(1) per lookup
hits++;
}
}
return hits > 0;
}
static long timeUriVarLookup(boolean fixed, int argCount, int varCount) {
List<String> args = IntStream.range(0, argCount).mapToObj(i -> "arg" + i).collect(Collectors.toList());
List<String> vars = IntStream.range(0, varCount).mapToObj(i -> "var" + i).collect(Collectors.toList());
// Seed with one match so result is stable
if (!args.isEmpty() && !vars.isEmpty()) vars.set(0, args.get(0));
int REPS = 2000;
long start = System.nanoTime();
boolean r = false;
for (int rep = 0; rep < REPS; rep++) {
r = fixed ? uriVarLookupFixed(args, vars) : uriVarLookupDefect(args, vars);
}
long elapsed = System.nanoTime() - start;
if (!r) throw new AssertionError("should have matched");
return elapsed / REPS;
}
static void testUriVarLookup() {
int N = 100; // 100 args, 100 uri vars — amplifies the O(A*V) cost
long defectNs = timeUriVarLookup(false, N, N);
long fixedNs = timeUriVarLookup(true, N, N);
double ratio = (double) defectNs / fixedNs;
System.out.printf("micronaut-core-0001 [N=%d]: defect=%dns fixed=%dns ratio=%.1fx%n",
N, defectNs, fixedNs, ratio);
if (ratio < 2.0) {
throw new AssertionError("Expected ratio >= 2.0x, got " + ratio);
}
System.out.println("micronaut-core-0001 PASS");
}
// -----------------------------------------------------------------------
// micronaut-core-0002: topologicalSort stream scan + ArrayList prepend
// -----------------------------------------------------------------------
/**
* Simulates the topologicalSort inner stream scan:
* for each bean, for each required component, scan all remaining unsorted beans.
* Defect: O(B²×C) — stream rebuilt from List each check.
*/
static List<Integer> topoSortDefect(List<Integer[]> beans) {
// beans[i] = {beanId, requiredBeanId_or_-1_if_none}
List<Integer> unsorted = new ArrayList<>();
List<Integer> sorted = new ArrayList<>();
for (Integer[] b : beans) {
if (b[1] < 0) sorted.add(0, b[0]);
else unsorted.add(b[0]);
}
Map<Integer, Integer> required = new HashMap<>();
for (Integer[] b : beans) {
if (b[1] >= 0) required.put(b[0], b[1]);
}
while (!unsorted.isEmpty()) {
boolean acyclic = false;
Iterator<Integer> i = unsorted.iterator();
while (i.hasNext()) {
int bean = i.next();
int req = required.getOrDefault(bean, -1);
if (req < 0) {
i.remove();
sorted.add(0, bean); // O(B) prepend
acyclic = true;
} else {
// Defect: O(B) stream scan for each bean each iteration
boolean stillUnsorted = unsorted.stream().anyMatch(u -> u == req);
if (!stillUnsorted) {
i.remove();
sorted.add(0, bean); // O(B) prepend
acyclic = true;
}
}
}
if (!acyclic && !unsorted.isEmpty()) {
sorted.add(0, unsorted.remove(0));
}
}
return sorted;
}
/**
* Fixed version: pre-build Set<Integer> unsortedSet (O(1) lookup),
* use ArrayDeque for O(1) prepend.
*/
static List<Integer> topoSortFixed(List<Integer[]> beans) {
List<Integer> unsorted = new ArrayList<>();
ArrayDeque<Integer> sortedDeque = new ArrayDeque<>();
for (Integer[] b : beans) {
if (b[1] < 0) sortedDeque.addFirst(b[0]);
else unsorted.add(b[0]);
}
Map<Integer, Integer> required = new HashMap<>();
for (Integer[] b : beans) {
if (b[1] >= 0) required.put(b[0], b[1]);
}
// Pre-build set of unsorted bean IDs
Set<Integer> unsortedSet = new HashSet<>(unsorted);
while (!unsorted.isEmpty()) {
boolean acyclic = false;
Iterator<Integer> i = unsorted.iterator();
while (i.hasNext()) {
int bean = i.next();
int req = required.getOrDefault(bean, -1);
if (req < 0 || !unsortedSet.contains(req)) { // O(1)
i.remove();
sortedDeque.addFirst(bean); // O(1)
unsortedSet.remove(bean);
acyclic = true;
}
}
if (!acyclic && !unsorted.isEmpty()) {
int removed = unsorted.remove(0);
sortedDeque.addFirst(removed);
unsortedSet.remove(removed);
}
}
return new ArrayList<>(sortedDeque);
}
/** Build a chain: bean[i] requires bean[i-1], so they must sort in order. */
static List<Integer[]> buildBeanChain(int n) {
List<Integer[]> beans = new ArrayList<>();
beans.add(new Integer[]{0, -1}); // root, no dependency
for (int i = 1; i < n; i++) {
beans.add(new Integer[]{i, i - 1}); // bean i requires bean i-1
}
return beans;
}
static long timeTopoSort(boolean fixed, int n) {
List<Integer[]> beans = buildBeanChain(n);
int REPS = 100;
long start = System.nanoTime();
List<Integer> result = null;
for (int r = 0; r < REPS; r++) {
result = fixed ? topoSortFixed(new ArrayList<>(beans)) : topoSortDefect(new ArrayList<>(beans));
}
long elapsed = System.nanoTime() - start;
if (result == null || result.isEmpty()) throw new AssertionError("null result");
return elapsed / REPS;
}
static void testTopoSort() {
int N = 400; // chain of 400 beans — O(B²) vs O(B) gap widens
long defectNs = timeTopoSort(false, N);
long fixedNs = timeTopoSort(true, N);
double ratio = (double) defectNs / fixedNs;
System.out.printf("micronaut-core-0002 [N=%d]: defect=%dns fixed=%dns ratio=%.1fx%n",
N, defectNs, fixedNs, ratio);
if (ratio < 2.0) {
throw new AssertionError("Expected ratio >= 2.0x, got " + ratio);
}
System.out.println("micronaut-core-0002 PASS");
}
// -----------------------------------------------------------------------
// main
// -----------------------------------------------------------------------
public static void main(String[] args) {
testUriVarLookup();
testTopoSort();
System.out.println("ALL PASS");
}
}