java-topology/defects/numpy/unit/NumpyTest.java

252 lines
9.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.

package unit;
import java.util.*;
/**
* Standalone unit tests for NumPy CWE-407 defects.
*
* numpy-0001: f2py crackfortran _get_depend_dict — O(W²) list dedup in dep resolution
* Simulates the "if w not in words: words.append(w)" inner loop.
* slow(): counts ops using list membership (words.contains(w)) — O(W) per insert
* fast(): counts ops using a parallel HashSet for O(1) membership
* Assert: slowOps >= fastOps * 10 for W=500 deps per variable
*
* numpy-0002: recfunctions.stack_arrays — seen=[] list dedup O(A×F²) field-name tracking
* Real code (numpy/lib/recfunctions.py:1396-1405):
* seen = []
* if name not in seen: seen.append(name) # O(F) per insert
* Fix: seen = set()
*
* numpy-0003: recfunctions.join_by — names list rebuilt inside loop + .index() O(F²)
* Real code (numpy/lib/recfunctions.py:1610-1612):
* names = [name for name, dtype in ndtype] # O(F) rebuild per iteration
* nameidx = names.index(fname) # O(F) linear search
* Fix: name_to_idx dict for O(1) lookup
*/
public class NumpyTest {
// ── numpy-0001 ─────────────────────────────────────────────────────────────
/**
* Simulates _get_depend_dict slow path.
*
* For each variable, we have a list of direct deps (depSources).
* Each dep expands into more deps — all land in 'words' (a list).
* Every insertion does: if w not in words → O(|words|) scan.
*
* @param depSources transitive dependencies to merge into words (simulates the expansion)
* @return op count (each list.contains() call = 1 op)
*/
static long slowDependDict(List<String> initial, List<String> depSources) {
long ops = 0;
List<String> words = new ArrayList<>(initial);
// Simulate: for word in words[:]: for w in deps[word]: if w not in words: words.append(w)
// We flatten: just insert all depSources into words using linear contains()
for (String w : depSources) {
// O(|words|) scan per candidate
for (int i = 0; i < words.size(); i++) {
ops++; // linear scan cost
if (words.get(i).equals(w)) {
break; // already present, skip
}
if (i == words.size() - 1) {
// not found — append (words grows, next iterations cost more)
words.add(w);
break;
}
}
}
return ops;
}
/**
* Simulates _get_depend_dict fast path.
*
* Parallel HashSet for O(1) membership; list kept for ordering.
*
* @return op count (each HashSet.contains() = 1 op)
*/
static long fastDependDict(List<String> initial, List<String> depSources) {
long ops = 0;
List<String> words = new ArrayList<>(initial);
Set<String> wordsSet = new HashSet<>(initial);
for (String w : depSources) {
ops++; // O(1) set contains
if (!wordsSet.contains(w)) {
words.add(w);
wordsSet.add(w);
}
}
return ops;
}
static void testDependDict() {
int numVars = 1; // single variable with many deps (worst case per variable)
int W = 500; // transitive dep count
// Build unique dep names
List<String> initial = new ArrayList<>();
for (int i = 0; i < 10; i++) initial.add("init_dep_" + i);
// depSources includes duplicates (realistic — many vars share deps)
List<String> depSources = new ArrayList<>();
for (int i = 0; i < W; i++) depSources.add("var_" + i);
// Add duplicates to simulate repeated merges
for (int i = 0; i < W / 2; i++) depSources.add("var_" + i);
long slowOps = slowDependDict(initial, depSources);
long fastOps = fastDependDict(initial, depSources);
System.out.printf(
"numpy-0001 W=%-4d slowOps=%-8d fastOps=%-6d ratio=%.1fx%n",
W, slowOps, fastOps, (double) slowOps / fastOps
);
assert slowOps > fastOps * 10 :
"numpy-0001 FAIL: expected slowOps > 10×fastOps, got " + slowOps + " vs " + fastOps;
System.out.println("numpy-0001 PASS");
}
// ── numpy-0002: stack_arrays seen=[] list dedup ───────────────────────────
/** Simulates stack_arrays seen=[] field dedup — returns total list comparisons */
static long slowStackArrays(int numArrays, int fieldsPerArray) {
List<String> seen = new ArrayList<>();
long ops = 0;
for (int a = 0; a < numArrays; a++) {
for (int f = 0; f < fieldsPerArray; f++) {
String name = "field_" + f; // shared field names across arrays
boolean found = false;
for (int k = 0; k < seen.size(); k++) {
ops++;
if (seen.get(k).equals(name)) { found = true; break; }
}
if (!found) seen.add(name);
}
}
return ops;
}
/** Simulates fixed stack_arrays with seen=set() — returns total ops */
static long fastStackArrays(int numArrays, int fieldsPerArray) {
Set<String> seen = new HashSet<>();
long ops = 0;
for (int a = 0; a < numArrays; a++) {
for (int f = 0; f < fieldsPerArray; f++) {
String name = "field_" + f;
ops++; // O(1) set probe
seen.add(name);
}
}
return ops;
}
static void testStackArrays() {
int A = 100, F = 200; // 100 arrays, 200 fields each
long slowOps = slowStackArrays(A, F);
long fastOps = fastStackArrays(A, F);
System.out.printf(
"numpy-0002 A=%-4d F=%-4d slowOps=%-10d fastOps=%-8d ratio=%.1fx%n",
A, F, slowOps, fastOps, (double) slowOps / fastOps
);
assert slowOps > fastOps * 30 :
"numpy-0002 FAIL: expected >30x ratio; slow=" + slowOps + " fast=" + fastOps;
System.out.println("numpy-0002 PASS");
}
// ── numpy-0003: join_by names-list rebuild + .index() ─────────────────────
/** Simulates join_by ndtype names-list rebuild + .index() — returns total comparisons */
static long slowJoinBy(int f1, int f2) {
List<String> ndtype = new ArrayList<>();
long ops = 0;
for (int i = 0; i < f1; i++) ndtype.add("r1_field_" + i);
for (int i = 0; i < f2; i++) {
String fname = "r2_field_" + i;
// Rebuild list (simulates list comprehension O(F) per iteration)
List<String> names = new ArrayList<>(ndtype);
ops += names.size();
// .index() linear search
int idx = -1;
for (int k = 0; k < names.size(); k++) {
ops++;
if (names.get(k).equals(fname)) { idx = k; break; }
}
if (idx == -1) ndtype.add(fname);
}
// Site 2: output dtype names tuple membership check
List<String> outputNames = new ArrayList<>(ndtype);
for (int i = 0; i < f1; i++) {
String f = "r1_field_" + i;
for (int k = 0; k < outputNames.size(); k++) {
ops++;
if (outputNames.get(k).equals(f)) break;
}
}
for (int i = 0; i < f2; i++) {
String f = "r2_field_" + i;
for (int k = 0; k < outputNames.size(); k++) {
ops++;
if (outputNames.get(k).equals(f)) break;
}
}
return ops;
}
/** Simulates fixed join_by with name_to_idx dict — returns total ops */
static long fastJoinBy(int f1, int f2) {
Map<String, Integer> nameToIdx = new HashMap<>();
List<String> ndtype = new ArrayList<>();
long ops = 0;
for (int i = 0; i < f1; i++) {
nameToIdx.put("r1_field_" + i, i);
ndtype.add("r1_field_" + i);
}
for (int i = 0; i < f2; i++) {
String fname = "r2_field_" + i;
ops++; // O(1) dict probe
if (!nameToIdx.containsKey(fname)) {
nameToIdx.put(fname, ndtype.size());
ndtype.add(fname);
}
}
// Site 2: set membership
Set<String> outputNamesSet = new HashSet<>(ndtype);
for (int i = 0; i < f1 + f2; i++) ops++;
return ops;
}
static void testJoinBy() {
int F1 = 200, F2 = 200;
long slowOps = slowJoinBy(F1, F2);
long fastOps = fastJoinBy(F1, F2);
System.out.printf(
"numpy-0003 F1=%-4d F2=%-4d slowOps=%-10d fastOps=%-8d ratio=%.1fx%n",
F1, F2, slowOps, fastOps, (double) slowOps / fastOps
);
assert slowOps > fastOps * 50 :
"numpy-0003 FAIL: expected >50x ratio; slow=" + slowOps + " fast=" + fastOps;
System.out.println("numpy-0003 PASS");
}
// ── main ──────────────────────────────────────────────────────────────────
public static void main(String[] args) {
int pass = 0, total = 3;
try { testDependDict(); pass++; } catch (AssertionError e) { System.err.println(e.getMessage()); }
try { testStackArrays(); pass++; } catch (AssertionError e) { System.err.println(e.getMessage()); }
try { testJoinBy(); pass++; } catch (AssertionError e) { System.err.println(e.getMessage()); }
System.out.printf("%n%d/%d PASS%n", pass, total);
if (pass != total) System.exit(1);
}
}