106 lines
4.2 KiB
Java
106 lines
4.2 KiB
Java
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);
|
||
}
|
||
}
|