package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; /** * NetworkXCyclesTest * * Models the CWE-407 defects across three projects: * * NX-001 (MEDIUM): networkx recursive_simple_cycles — B[nextnode] dedup * Defective: B[nextnode] is an ArrayList; `not in` is O(|B[nextnode]|) per edge. * Fixed: B[nextnode] is a HashSet; `not in` (contains) is O(1). * * rubocop-0001 (MEDIUM): RuboCop IgnoredNode — @ignored_nodes dedup * Defective: @ignored_nodes is an ArrayList; identity scan O(N) per on_str call. * Fixed: @ignored_nodes is a HashSet with identity-based hashing; O(1). * * solargraph-0001 (MEDIUM): Solargraph Chain — @@inference_stack dedup * Defective: @@inference_stack is a shared ArrayList; O(D) include? per pin * AND shared across simulated concurrent "threads" (no isolation). * Fixed: Per-thread HashSet; O(1) contains, isolated per thread. * * All tests instrument operation counts explicitly — no wall-clock timing. */ public class NetworkXCyclesTest { // ----------------------------------------------------------------------- // NX-001: B[nextnode] deduplication model // Simulates the inner loop of circuit() across E edges and growing B sets. // ----------------------------------------------------------------------- /** * Defective: B[nextnode] is ArrayList; `not in` is O(|B[nextnode]|). * * Edge pattern: all edges target the same nextnode=0, with thisnode running * from 0..numUniqueThisNodes-1. This maximises B[0] growth: after k distinct * thisnode values have been added, the next novel thisnode scans all k entries * before discovering it is absent. Total comparisons = 0+1+2+...+(k-1) = k*(k-1)/2. * Subsequent repeats of already-seen thisnode values each scan until they hit, * averaging k/2 comparisons per repeat. */ static long nx001_defectiveBDedup(int numDistinctSources, int numRepeats) { ArrayList B0 = new ArrayList<>(); // B[nextnode=0] long comparisons = 0; // Phase 1: insert all distinct thisnode values 0..numDistinctSources-1 for (int thisnode = 0; thisnode < numDistinctSources; thisnode++) { // Scan full list before each new insert — O(size) miss for (Integer existing : B0) { comparisons++; // won't match — thisnode not yet in list } B0.add(thisnode); } // Phase 2: repeat lookups for already-present nodes — O(position) hit for (int rep = 0; rep < numRepeats; rep++) { int thisnode = rep % numDistinctSources; for (Integer existing : B0) { comparisons++; if (existing.equals(thisnode)) break; // hit at position thisnode } } return comparisons; } /** * Fixed: B[nextnode] is HashSet; `not in` (contains) is O(1). * Same edge pattern — all operations are one hash probe each. */ static long nx001_fixedBDedup(int numDistinctSources, int numRepeats) { HashSet B0 = new HashSet<>(); long lookups = 0; // Phase 1: insert distinct sources — O(1) contains check each for (int thisnode = 0; thisnode < numDistinctSources; thisnode++) { lookups++; // one O(1) hash probe — CWE-407 fix B0.add(thisnode); } // Phase 2: repeat lookups — O(1) each for (int rep = 0; rep < numRepeats; rep++) { lookups++; // one O(1) hash probe — CWE-407 fix } return lookups; } // ----------------------------------------------------------------------- // rubocop-0001: @ignored_nodes identity dedup model // Simulates on_str firing R times with S ignored nodes accumulated. // Nodes are modelled as Long object IDs (identity comparison via ==). // ----------------------------------------------------------------------- /** Defective: ignored_nodes is ArrayList; identity scan O(S) per on_str call. */ static long rubocop0001_defectiveIgnoredNodes(int numStringNodes, int numIgnoredNodes) { // Build ignored_nodes list (S entries) ArrayList ignoredNodes = new ArrayList<>(); Long[] nodeObjects = new Long[numIgnoredNodes]; for (int i = 0; i < numIgnoredNodes; i++) { nodeObjects[i] = (long) (i + 1_000_000); // distinct Long objects ignoredNodes.add(nodeObjects[i]); } long comparisons = 0; // Simulate on_str called R times — each call checks ignored_node?(node) for (int r = 0; r < numStringNodes; r++) { Long queryNode = nodeObjects[r % numIgnoredNodes]; // always a hit // `ignored_nodes.any? { |n| n.equal?(node) }` — O(S) scan for (Long ignored : ignoredNodes) { comparisons++; if (ignored == queryNode) { // identity comparison break; } } } return comparisons; } /** * Fixed: ignored_nodes is an identity-based HashSet; include?(node) is O(1). * Java models identity-based hashing via IdentityHashMap used as a Set. */ static long rubocop0001_fixedIgnoredNodes(int numStringNodes, int numIgnoredNodes) { // IdentityHashMap with dummy values models Set.new.compare_by_identity IdentityHashMap ignoredNodes = new IdentityHashMap<>(); Long[] nodeObjects = new Long[numIgnoredNodes]; for (int i = 0; i < numIgnoredNodes; i++) { nodeObjects[i] = (long) (i + 1_000_000); ignoredNodes.put(nodeObjects[i], Boolean.TRUE); } long lookups = 0; for (int r = 0; r < numStringNodes; r++) { Long queryNode = nodeObjects[r % numIgnoredNodes]; lookups++; // one O(1) identity hash lookup — CWE-407 fix ignoredNodes.containsKey(queryNode); } return lookups; } // ----------------------------------------------------------------------- // solargraph-0001: @@inference_stack isolation + dedup model // Simulates infer_from_definitions across T concurrent "threads", // each with D pins to process at inference depth D. // ----------------------------------------------------------------------- /** Shared mutable state modelling @@inference_stack = [] (the defect). */ static class DefectiveInferenceStack { final ArrayList stack = new ArrayList<>(); long comparisons = 0; boolean include(long pinId) { for (Long existing : stack) { comparisons++; if (existing.equals(pinId)) return true; } return false; } void push(long pinId) { stack.add(pinId); } void pop() { if (!stack.isEmpty()) stack.remove(stack.size() - 1); } } /** Per-thread state modelling Thread.current[:solargraph_inference_stack] (the fix). */ static class FixedInferenceStack { // Each "thread" gets its own HashSet — thread-local isolation final HashMap> threadStacks = new HashMap<>(); long lookups = 0; private HashSet stackFor(int threadId) { return threadStacks.computeIfAbsent(threadId, k -> new HashSet<>()); } boolean include(int threadId, long pinId) { lookups++; // O(1) hash lookup — CWE-407 fix return stackFor(threadId).contains(pinId); } void add(int threadId, long pinId) { stackFor(threadId).add(pinId); } void delete(int threadId, long pinId) { stackFor(threadId).remove(pinId); } } /** * Simulate T threads each processing D pins through the defective shared stack. * Returns total comparisons across all threads. */ static long solargraph0001_defectiveStack(int numThreads, int pinsPerThread) { DefectiveInferenceStack shared = new DefectiveInferenceStack(); // Sequential simulation: each thread pushes its pins, checks, pops for (int t = 0; t < numThreads; t++) { for (int p = 0; p < pinsPerThread; p++) { long pinId = (long) t * pinsPerThread + p; // `next if @@inference_stack.include?(pin)` — O(D) scan shared.include(pinId); shared.push(pinId); } // pop all pins for this "thread" (in defective impl they share the stack) for (int p = 0; p < pinsPerThread; p++) { shared.pop(); } } return shared.comparisons; } /** * Simulate T threads each processing D pins through the fixed per-thread Set. * Returns total lookups across all threads. */ static long solargraph0001_fixedStack(int numThreads, int pinsPerThread) { FixedInferenceStack fixed = new FixedInferenceStack(); for (int t = 0; t < numThreads; t++) { for (int p = 0; p < pinsPerThread; p++) { long pinId = (long) t * pinsPerThread + p; // O(1) set lookup — CWE-407 fix fixed.include(t, pinId); fixed.add(t, pinId); } for (int p = 0; p < pinsPerThread; p++) { long pinId = (long) t * pinsPerThread + p; fixed.delete(t, pinId); } } return fixed.lookups; } // ----------------------------------------------------------------------- // Test 1 — NX-001: defective B-list scan > fixed B-set lookup at E=200, N=20 // ----------------------------------------------------------------------- static void test1_nx001_BSetVsList() { int numDistinctSources = 50; int numRepeats = 100; long defectOps = nx001_defectiveBDedup(numDistinctSources, numRepeats); long fixedOps = nx001_fixedBDedup(numDistinctSources, numRepeats); // Phase-1 defect cost: triangular 0+1+...+(k-1) = k*(k-1)/2 long expectedPhase1Defect = (long) numDistinctSources * (numDistinctSources - 1) / 2; System.out.printf( "test1 NX-001: distinct_sources=%d repeats=%d defect_comparisons=%d (expect phase1>=%d) fixed_lookups=%d%n", numDistinctSources, numRepeats, defectOps, expectedPhase1Defect, fixedOps); assert defectOps > fixedOps : "NX-001: defective list scan must do more comparisons than set lookup; defect=" + defectOps + " fixed=" + fixedOps; assert defectOps >= expectedPhase1Defect : "NX-001: defect comparisons=" + defectOps + " must be at least triangular=" + expectedPhase1Defect; } // ----------------------------------------------------------------------- // Test 2 — NX-001: scaling — doubling edges grows defect super-linearly // ----------------------------------------------------------------------- static void test2_nx001_quadraticScaling() { // Double the number of distinct sources; repeats held constant. // Defect phase-1 cost is k*(k-1)/2 — quadratic in k. // Fixed cost is k + repeats — linear in k. int repeats = 50; int k1 = 40; int k2 = 80; // 2x k long d1 = nx001_defectiveBDedup(k1, repeats); long d2 = nx001_defectiveBDedup(k2, repeats); long f1 = nx001_fixedBDedup(k1, repeats); long f2 = nx001_fixedBDedup(k2, repeats); double defectGrowth = (double) d2 / Math.max(1, d1); double fixedGrowth = (double) f2 / Math.max(1, f1); System.out.printf( "test2 NX-001 scaling: k1=%d k2=%d defect_growth=%.2fx fixed_growth=%.2fx%n", k1, k2, defectGrowth, fixedGrowth); assert defectGrowth > fixedGrowth : "NX-001: defect should grow faster than fix when k doubles; got defect=" + defectGrowth + " fixed=" + fixedGrowth; assert defectGrowth > 2.0 : "NX-001: defect should grow super-linearly (quadratic), got " + defectGrowth; assert fixedGrowth <= 2.5 : "NX-001: fixed set lookup should grow at most linearly; got " + fixedGrowth; } // ----------------------------------------------------------------------- // Test 3 — rubocop-0001: identity-list scan > identity-set lookup at R=500, S=50 // ----------------------------------------------------------------------- static void test3_rubocop0001_identitySetVsList() { int numStringNodes = 500; int numIgnoredNodes = 50; long defectOps = rubocop0001_defectiveIgnoredNodes(numStringNodes, numIgnoredNodes); long fixedOps = rubocop0001_fixedIgnoredNodes(numStringNodes, numIgnoredNodes); System.out.printf( "test3 rubocop-0001: on_str=%d ignored=%d defect_comparisons=%d fixed_lookups=%d%n", numStringNodes, numIgnoredNodes, defectOps, fixedOps); assert defectOps > fixedOps : "rubocop-0001: identity list scan must cost more than identity set lookup"; // Worst case for defect: query always hits last element → S comparisons each // With hits cycling through all S nodes (always hits at position r%S+1 on average), // total should be at least R comparisons. assert defectOps >= numStringNodes : "rubocop-0001: expected at least R=" + numStringNodes + " comparisons, got " + defectOps; } // ----------------------------------------------------------------------- // Test 4 — solargraph-0001: shared-stack list scan > per-thread set lookup // ----------------------------------------------------------------------- static void test4_solargraph0001_threadLocalSetVsSharedList() { int numThreads = 10; int pinsPerThread = 30; long defectOps = solargraph0001_defectiveStack(numThreads, pinsPerThread); long fixedOps = solargraph0001_fixedStack(numThreads, pinsPerThread); System.out.printf( "test4 solargraph-0001: threads=%d pins_per_thread=%d defect_comparisons=%d fixed_lookups=%d%n", numThreads, pinsPerThread, defectOps, fixedOps); assert defectOps > fixedOps : "solargraph-0001: shared-list scan must do more work than per-thread set"; assert defectOps > 0 : "solargraph-0001: defect must perform at least one comparison"; } // ----------------------------------------------------------------------- // Test 5 — solargraph-0001: thread isolation — per-thread set never sees // another thread's pins (no cross-contamination in fixed impl) // ----------------------------------------------------------------------- static void test5_solargraph0001_perThreadIsolation() { FixedInferenceStack fixed = new FixedInferenceStack(); int numThreads = 5; int pinsPerThread = 20; // Each thread adds its own pins for (int t = 0; t < numThreads; t++) { for (int p = 0; p < pinsPerThread; p++) { long pinId = (long) t * pinsPerThread + p; fixed.add(t, pinId); } } // Verify thread T cannot see thread T+1's pins (isolation invariant) for (int t = 0; t < numThreads - 1; t++) { long otherThreadPin = (long) (t + 1) * pinsPerThread; // first pin of next thread boolean crossVisible = fixed.stackFor(t).contains(otherThreadPin); assert !crossVisible : "solargraph-0001: thread " + t + " must not see pin from thread " + (t+1) + " (pin=" + otherThreadPin + ")"; } // Verify each thread can see its own pins for (int t = 0; t < numThreads; t++) { long ownPin = (long) t * pinsPerThread; // first pin of this thread boolean selfVisible = fixed.stackFor(t).contains(ownPin); assert selfVisible : "solargraph-0001: thread " + t + " must be able to see its own pin (pin=" + ownPin + ")"; } System.out.printf( "test5 solargraph-0001 isolation: %d threads x %d pins — no cross-contamination confirmed%n", numThreads, pinsPerThread); } // ----------------------------------------------------------------------- // Main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== NetworkXCyclesTest ==="); System.out.println("Modelling CWE-407: NX-001 + rubocop-0001 + solargraph-0001"); System.out.println(); test1_nx001_BSetVsList(); System.out.println(" PASS test1_nx001_BSetVsList"); test2_nx001_quadraticScaling(); System.out.println(" PASS test2_nx001_quadraticScaling"); test3_rubocop0001_identitySetVsList(); System.out.println(" PASS test3_rubocop0001_identitySetVsList"); test4_solargraph0001_threadLocalSetVsSharedList(); System.out.println(" PASS test4_solargraph0001_threadLocalSetVsSharedList"); test5_solargraph0001_perThreadIsolation(); System.out.println(" PASS test5_solargraph0001_perThreadIsolation"); System.out.println(); System.out.println("All 5 tests PASSED."); } }