178 lines
6.7 KiB
Java
178 lines
6.7 KiB
Java
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<Integer> 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<Integer> 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<String> 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<String> acc = new ArrayList<>(n);
|
|
Set<String> 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);
|
|
}
|
|
}
|