package unit; import java.util.*; /** * Erlang/OTP CWE-407 unit tests — standalone, no JUnit. * * erlang-0001 digraph.erl one_path() — lists:member on growing visited-list * lib/stdlib/src/digraph.erl (patch: erlang-0001-one-path-sets.patch) * * erlang-0003 code_server.erl merge_path1() — lists:member(P, Acc) in recursive loop * lib/kernel/src/code_server.erl lines 600-609 * Acc grows as each unique path is prepended; lists:member is O(|Acc|) * per call → O(N²) total for N unique paths. * Fix: track seen paths in a HashSet alongside the accumulator list. */ public class ErlangAlgorithm { // ----------------------------------------------------------------------- // erlang-0001: digraph one_path — lists:member on visited list // // In DFS graph path-finding, each node visited checks membership against // the accumulated visited list (Xs in the Erlang code). // Slow: ArrayList.contains() — O(depth) per node // Fast: HashSet.contains() — O(1) per node // ----------------------------------------------------------------------- /** * Simulate one_path DFS traversal with a linear visited-list (Erlang original). * Graph: a long linear chain 0→1→2→...→N-1 * Returns total comparison operations performed. */ static long onePathSlowOps(int n) { // visited list, mirrors Erlang's Xs accumulator (a plain list) List visited = new ArrayList<>(n); long ops = 0; // Simulate DFS: at each step, check if next node is already visited for (int node = 0; node < n; node++) { // lists:member(node, visited) — linear scan boolean found = false; for (int i = 0; i < visited.size(); i++) { ops++; if (visited.get(i).equals(node)) { found = true; break; } } if (!found) { visited.add(node); } } return ops; } /** * Simulate one_path DFS traversal with a HashSet visited-set (patched). * Returns total comparison operations performed. */ static long onePathFastOps(int n) { Set visited = new HashSet<>(n); long ops = 0; for (int node = 0; node < n; node++) { ops++; // O(1) hash lookup — count as single op if (!visited.contains(node)) { visited.add(node); } } return ops; } // ----------------------------------------------------------------------- // erlang-0003: code_server merge_path1 — lists:member(P, Acc) per unique path // // merge_path1/3 recursively processes a list of N directory paths. // For each new path P, it checks whether P is already in Acc (the output // accumulator) using lists:member/2, which is O(|Acc|). // Since Acc grows by 1 for each unique P, total cost = 1+2+...+N = O(N²). // // This runs at VM startup (add_loader_path) and on every code:add_paths/1 call. // Large Elixir/OTP deployments routinely have N=500-2000 library directories. // // File: lib/kernel/src/code_server.erl lines 600-609 // // Slow: ArrayList.contains() — O(|Acc|) per path → O(N²) total // Fast: HashSet.contains() — O(1) per path → O(N) total // ----------------------------------------------------------------------- /** * Simulate merge_path1 with the original List accumulator. * Input: N unique directory paths. * Returns total comparison operations performed. */ static long mergePathSlowOps(int n) { // Acc: the accumulator list (paths seen so far, prepended) List acc = new ArrayList<>(n); long ops = 0; // Simulate the IPath list (empty — all paths are new) // merge_path1([P|Path], IPath, Acc) → member check on Acc for (int i = 0; i < n; i++) { String path = "/usr/lib/erlang/lib/module-" + i + "/ebin"; // lists:member(P, Acc) — O(|Acc|) linear scan boolean found = false; for (int j = 0; j < acc.size(); j++) { ops++; if (acc.get(j).equals(path)) { found = true; break; } } if (!found) { // IPath1 = exclude(P, IPath) — for empty IPath this is a no-op acc.add(path); // [P|Acc] — prepend (modelled as add at end) } } return ops; } /** * Simulate merge_path1 with the patched HashSet seen-set alongside the accumulator. * Returns total comparison operations performed. */ static long mergePathFastOps(int n) { List acc = new ArrayList<>(n); Set seen = new HashSet<>(n); long ops = 0; for (int i = 0; i < n; i++) { String path = "/usr/lib/erlang/lib/module-" + i + "/ebin"; ops++; // sets:is_element — O(1) if (!seen.contains(path)) { seen.add(path); acc.add(path); } } return ops; } // ----------------------------------------------------------------------- // Test runner // ----------------------------------------------------------------------- public static void main(String[] args) { int passed = 0; int failed = 0; // erlang-0001: digraph one_path int[] graphSizes = {100, 300, 500, 1000}; for (int n : graphSizes) { long slow = onePathSlowOps(n); long fast = onePathFastOps(n); boolean pass = slow >= fast * 5; System.out.printf( "erlang-0001 N=%-5d slow=%8d fast=%8d ratio=%.1fx %s%n", n, slow, fast, (double) slow / fast, pass ? "PASS" : "FAIL"); if (pass) passed++; else failed++; } // erlang-0003: code_server merge_path1 // N represents number of unique library directories (typical: 200-2000) int[] pathCounts = {200, 500, 1000, 2000}; for (int n : pathCounts) { long slow = mergePathSlowOps(n); long fast = mergePathFastOps(n); // Expected ratio: ~N/2 (triangular sum / N = N/2) boolean pass = slow >= fast * 5; System.out.printf( "erlang-0003 N=%-5d slow=%8d fast=%8d ratio=%.1fx %s%n", n, slow, fast, (double) slow / fast, pass ? "PASS" : "FAIL"); if (pass) passed++; else failed++; } System.out.printf("%nTotal: %d/%d PASS%n", passed, passed + failed); if (failed > 0) System.exit(1); } }