java-topology/defects/opentofu/unit/OpenTofuSnapshotManifestAlgorithm.java

162 lines
6.3 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.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* opentofu-0002: readConfigSnapshot manifest validation O(M²) linear scan
*
* Slow: for k := range snap.Modules { for _, record := range manifest { if record.Key == k — O(M×R)
* Fast: pre-build manifestKeys map[string]bool, then if !manifestKeys[k] — O(M+R)
*
* Verifies: slow_ops >= M*(M-1)/2, fast_ops == M+R, ratio >= 10x at M=200
*/
public class OpenTofuSnapshotManifestAlgorithm {
static long slowOps = 0;
static long fastOps = 0;
static class ManifestRecord {
final String key;
ManifestRecord(String key) { this.key = key; }
}
/**
* Slow version: mirrors the original Go validation loop
* for k := range snap.Modules { found := false; for _, record := range manifest { if record.Key == k ... } }
*/
static boolean slowValidate(Map<String, Object> snapModules, List<ManifestRecord> manifest) {
slowOps = 0;
for (String k : snapModules.keySet()) {
boolean found = false;
for (ManifestRecord record : manifest) {
slowOps++;
if (record.key.equals(k)) {
found = true;
break;
}
}
if (!found) {
return false; // module not in manifest
}
}
return true;
}
/**
* Fast version: pre-build a set of manifest keys
* manifestKeys := make(map[string]bool); for _, record := range manifest { manifestKeys[record.Key] = true }
* then: for k := range snap.Modules { if !manifestKeys[k] ... }
*/
static boolean fastValidate(Map<String, Object> snapModules, List<ManifestRecord> manifest) {
fastOps = 0;
Map<String, Boolean> manifestKeys = new HashMap<>(manifest.size() * 2);
for (ManifestRecord record : manifest) {
fastOps++;
manifestKeys.put(record.key, true);
}
for (String k : snapModules.keySet()) {
fastOps++;
if (!manifestKeys.containsKey(k)) {
return false;
}
}
return true;
}
/** Build snap.Modules and manifest with M entries each */
static Object[] buildData(int M) {
Map<String, Object> snapModules = new LinkedHashMap<>();
List<ManifestRecord> manifest = new ArrayList<>();
for (int i = 0; i < M; i++) {
String key = "module_" + i;
snapModules.put(key, new Object());
manifest.add(new ManifestRecord(key));
}
return new Object[]{snapModules, manifest};
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
int passed = 0;
int total = 0;
// Test 1: correctness — both return true when all modules are in manifest
{
total++;
Object[] data = buildData(20);
Map<String, Object> modules = (Map<String, Object>) data[0];
List<ManifestRecord> manifest = (List<ManifestRecord>) data[1];
boolean slowResult = slowValidate(modules, manifest);
boolean fastResult = fastValidate(modules, manifest);
boolean ok = slowResult && fastResult;
System.out.println((ok ? "PASS" : "FAIL") + " [correctness M=20]: slow=" + slowResult + " fast=" + fastResult);
if (ok) passed++;
}
// Test 2: both detect missing module
{
total++;
Object[] data = buildData(10);
Map<String, Object> modules = (Map<String, Object>) data[0];
List<ManifestRecord> manifest = (List<ManifestRecord>) data[1];
modules.put("module_EXTRA", new Object()); // not in manifest
boolean slowResult = slowValidate(modules, manifest);
boolean fastResult = fastValidate(modules, manifest);
boolean ok = !slowResult && !fastResult;
System.out.println((ok ? "PASS" : "FAIL") + " [missing module detection]: slow=" + slowResult + " fast=" + fastResult);
if (ok) passed++;
}
// Test 3: slow op count is O(M²)
{
total++;
int M = 200;
Object[] data = buildData(M);
slowValidate((Map<String, Object>) data[0], (List<ManifestRecord>) data[1]);
// Worst case: every module scans full manifest before finding a match
// Average case: M * M/2 comparisons. At minimum, M*(M-1)/2 for insertion-order mismatch.
// We verify at least M comparisons (trivially), but also that it's superlinear.
// Since keys match in order but manifest may be searched fully before finding, min = sum(1..M) = M*(M+1)/2
long minExpected = (long) M * (M - 1) / 2;
boolean ok = slowOps >= minExpected;
System.out.println((ok ? "PASS" : "FAIL") + " [slow O(M²) M=" + M + "]: ops=" + slowOps + " >= " + minExpected);
if (ok) passed++;
}
// Test 4: fast op count is O(M+R)
{
total++;
int M = 200;
Object[] data = buildData(M);
fastValidate((Map<String, Object>) data[0], (List<ManifestRecord>) data[1]);
// fastOps = R (manifest scan) + M (module validation) = 2M since R=M
boolean ok = fastOps <= 2L * M + 1;
System.out.println((ok ? "PASS" : "FAIL") + " [fast O(M+R) M=" + M + "]: ops=" + fastOps + " <= " + (2 * M + 1));
if (ok) passed++;
}
// Test 5: ratio >= 10x at M=200
{
total++;
int M = 200;
Object[] data = buildData(M);
Map<String, Object> modules = (Map<String, Object>) data[0];
List<ManifestRecord> manifest = (List<ManifestRecord>) data[1];
slowValidate(modules, manifest);
long slowCount = slowOps;
fastValidate(modules, manifest);
long fastCount = fastOps;
double ratio = (double) slowCount / fastCount;
boolean ok = ratio >= 10.0;
System.out.printf((ok ? "PASS" : "FAIL") + " [ratio M=%d]: slowOps=%d fastOps=%d ratio=%.1fx%n", M, slowCount, fastCount, ratio);
if (ok) passed++;
}
System.out.println(passed + "/" + total + " PASS");
if (passed != total) System.exit(1);
}
}