java-topology/defects/sklearn/unit/SklearnTest.java

106 lines
4.2 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 scikit-learn CWE-407 defects.
*
* sklearn-0001: HistGradientBoosting _check_categories — O(C×F) feature_names.index in loop
* Simulates: for feature_name in categorical_features: feature_names.index(feature_name)
* slow(): O(F) linear scan per categorical feature via indexOf — total O(C×F)
* fast(): build HashMap[name→idx] once O(F), then O(1) per lookup — total O(F+C)
* Assert: slowOps >= fastOps * 20 for F=1000 features, C=200 categorical
*/
public class SklearnTest {
// ── sklearn-0001 ──────────────────────────────────────────────────────────
/**
* Slow path: List.indexOf(featureName) per categorical feature.
* Each .indexOf() scans up to F entries.
*
* @param featureNames all feature names (length F)
* @param categoricalNames names to look up (length C)
* @return op count (each element comparison during indexOf = 1 op)
*/
static long slowCheckCategories(List<String> featureNames, List<String> categoricalNames) {
long ops = 0;
boolean[] isCategorical = new boolean[featureNames.size()];
for (String catName : categoricalNames) {
// Linear scan from index 0 — mirrors Python list.index()
for (int i = 0; i < featureNames.size(); i++) {
ops++;
if (featureNames.get(i).equals(catName)) {
isCategorical[i] = true;
break;
}
}
}
return ops;
}
/**
* Fast path: build HashMap[name→index] once, then O(1) per lookup.
*
* @param featureNames all feature names (length F)
* @param categoricalNames names to look up (length C)
* @return op count (1 op per entry in build + 1 op per lookup)
*/
static long fastCheckCategories(List<String> featureNames, List<String> categoricalNames) {
long ops = 0;
boolean[] isCategorical = new boolean[featureNames.size()];
// Build index: O(F)
Map<String, Integer> nameToIdx = new HashMap<>(featureNames.size() * 2);
for (int i = 0; i < featureNames.size(); i++) {
nameToIdx.put(featureNames.get(i), i);
ops++;
}
// Lookup: O(1) per categorical feature
for (String catName : categoricalNames) {
Integer idx = nameToIdx.get(catName);
ops++;
if (idx != null) {
isCategorical[idx] = true;
}
}
return ops;
}
static void testCheckCategories() {
int F = 1000; // total features
int C = 200; // categorical features (worst-case: all near the end of the list)
List<String> featureNames = new ArrayList<>(F);
for (int i = 0; i < F; i++) featureNames.add("feature_" + i);
// Categorical features chosen from the second half — maximizes scan depth
List<String> categoricalNames = new ArrayList<>(C);
for (int i = F / 2; i < F / 2 + C; i++) categoricalNames.add("feature_" + i);
long slowOps = slowCheckCategories(featureNames, categoricalNames);
long fastOps = fastCheckCategories(featureNames, categoricalNames);
System.out.printf(
"sklearn-0001 F=%-5d C=%-4d slowOps=%-8d fastOps=%-6d ratio=%.1fx%n",
F, C, slowOps, fastOps, (double) slowOps / fastOps
);
assert slowOps > fastOps * 20 :
"sklearn-0001 FAIL: expected slowOps > 20×fastOps, got " + slowOps + " vs " + fastOps;
System.out.println("sklearn-0001 PASS");
}
// ── main ──────────────────────────────────────────────────────────────────
public static void main(String[] args) {
int pass = 0, total = 1;
try { testCheckCategories(); 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);
}
}