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 * each invocation and List.contains() is O(V). Called once per argument per * request → O(A×V) per call. * Fix: pre-compute Set 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> 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 argNames, List uriVars) { int hits = 0; for (String arg : argNames) { // Defect: getVariableNames() re-invoked inside lambda → new List each time List freshList = new ArrayList<>(uriVars); // simulate getVariableNames() if (freshList.contains(arg)) { // O(V) scan hits++; } } return hits > 0; } /** Fixed version: pre-compute Set once, O(1) lookup. */ static boolean uriVarLookupFixed(List argNames, List uriVars) { Set 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 args = IntStream.range(0, argCount).mapToObj(i -> "arg" + i).collect(Collectors.toList()); List 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 topoSortDefect(List beans) { // beans[i] = {beanId, requiredBeanId_or_-1_if_none} List unsorted = new ArrayList<>(); List sorted = new ArrayList<>(); for (Integer[] b : beans) { if (b[1] < 0) sorted.add(0, b[0]); else unsorted.add(b[0]); } Map required = new HashMap<>(); for (Integer[] b : beans) { if (b[1] >= 0) required.put(b[0], b[1]); } while (!unsorted.isEmpty()) { boolean acyclic = false; Iterator 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 unsortedSet (O(1) lookup), * use ArrayDeque for O(1) prepend. */ static List topoSortFixed(List beans) { List unsorted = new ArrayList<>(); ArrayDeque sortedDeque = new ArrayDeque<>(); for (Integer[] b : beans) { if (b[1] < 0) sortedDeque.addFirst(b[0]); else unsorted.add(b[0]); } Map 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 unsortedSet = new HashSet<>(unsorted); while (!unsorted.isEmpty()) { boolean acyclic = false; Iterator 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 buildBeanChain(int n) { List 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 beans = buildBeanChain(n); int REPS = 100; long start = System.nanoTime(); List 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"); } }