import java.util.*; /** * Unit test simulating Forgejo forgejo-0003: * models/asymkey/ssh_key.go synchronizePublicKeys() has three O(N*M) scans: * * 1. Building providedKeys with dedup: O(K^2) where K = LDAP-provided key count * for _, v := range sshPublicKeys { * key = ... * if !util.SliceContainsString(providedKeys, key) { // O(|providedKeys|) each time * * 2. Finding new keys: O(P * G) where P = provided, G = DB keys * for _, key := range providedKeys { * if !util.SliceContainsString(giteaKeys, key) { // O(G) each * * 3. Finding deleted keys: O(G * P) * for _, giteaKey := range giteaKeys { * if !util.SliceContainsString(providedKeys, giteaKey) { // O(P) each * * This runs per user per LDAP sync cycle. With many users or large key sets it accumulates. * Fix: build maps for O(1) lookup before each loop. */ public class ForgejoSSHKeySyncTest { // Defective: O(K^2) dedup + O(P*G) + O(G*P) static long countDefectiveOps(int rawKeyCount, int giteaKeyCount) { // Phase 1: dedup raw keys into providedKeys // Assume half are unique: K/2 unique keys, K/2 duplicates // Each insertion checks against growing list long ops = 0; int provided = 0; for (int i = 0; i < rawKeyCount; i++) { ops += provided; // SliceContainsString cost if (i % 2 == 0) provided++; // ~half unique } // Phase 2: find new keys (provided not in gitea) ops += (long) provided * giteaKeyCount; // Phase 3: find keys to delete (gitea not in provided) ops += (long) giteaKeyCount * provided; return ops; } static long countFixedOps(int rawKeyCount, int giteaKeyCount) { // Phase 1: dedup with set — O(K) total long ops = rawKeyCount; int provided = rawKeyCount / 2; // ~half unique // Build giteaKeysSet: O(G) ops += giteaKeyCount; // Phase 2: find new keys — O(P) ops += provided; // Phase 3: find keys to delete — O(G) ops += giteaKeyCount; return ops; } // Simulate the sync logic for correctness static Map> syncKeysDefective( List rawKeys, List giteaKeys) { // Dedup rawKeys List provided = new ArrayList<>(); for (String k : rawKeys) { if (!provided.contains(k)) { provided.add(k); } } // Find new keys List toAdd = new ArrayList<>(); for (String k : provided) { if (!giteaKeys.contains(k)) { toAdd.add(k); } } // Find keys to delete List toDelete = new ArrayList<>(); for (String k : giteaKeys) { if (!provided.contains(k)) { toDelete.add(k); } } Map> result = new HashMap<>(); result.put("toAdd", toAdd); result.put("toDelete", toDelete); return result; } static Map> syncKeysFixed( List rawKeys, List giteaKeys) { // Dedup rawKeys with set Set providedSet = new LinkedHashSet<>(); List provided = new ArrayList<>(); for (String k : rawKeys) { if (providedSet.add(k)) { provided.add(k); } } // Build giteaKeys set Set giteaSet = new HashSet<>(giteaKeys); // Find new keys List toAdd = new ArrayList<>(); for (String k : provided) { if (!giteaSet.contains(k)) { toAdd.add(k); } } // Find keys to delete List toDelete = new ArrayList<>(); for (String k : giteaKeys) { if (!providedSet.contains(k)) { toDelete.add(k); } } Map> result = new HashMap<>(); result.put("toAdd", toAdd); result.put("toDelete", toDelete); return result; } public static void main(String[] args) { System.out.println("forgejo-0003: synchronizePublicKeys() O(K^2+P*G) -> O(K+P+G)"); System.out.println("=".repeat(60)); // Correctness test List rawKeys = Arrays.asList( "ssh-rsa AAAA1", "ssh-rsa AAAA2", "ssh-rsa AAAA1", // dup "ssh-rsa AAAA3", "ssh-rsa AAAA2" // more dups ); List giteaKeys = Arrays.asList( "ssh-rsa AAAA2", "ssh-rsa AAAA4" ); Map> def = syncKeysDefective(rawKeys, giteaKeys); Map> fix = syncKeysFixed(rawKeys, giteaKeys); // Expected: add AAAA1 and AAAA3 (new from LDAP), delete AAAA4 (no longer in LDAP) List expectedAdd = Arrays.asList("ssh-rsa AAAA1", "ssh-rsa AAAA3"); List expectedDelete = Arrays.asList("ssh-rsa AAAA4"); if (!def.get("toAdd").equals(expectedAdd)) { throw new AssertionError("Defective toAdd wrong: " + def.get("toAdd")); } if (!def.get("toDelete").equals(expectedDelete)) { throw new AssertionError("Defective toDelete wrong: " + def.get("toDelete")); } if (!fix.get("toAdd").equals(expectedAdd)) { throw new AssertionError("Fixed toAdd wrong: " + fix.get("toAdd")); } if (!fix.get("toDelete").equals(expectedDelete)) { throw new AssertionError("Fixed toDelete wrong: " + fix.get("toDelete")); } System.out.println("PASS: correctness verified"); // Performance comparison System.out.printf("%n%-20s %15s %12s %10s%n", "scenario", "defective_ops", "fixed_ops", "ratio"); System.out.println("-".repeat(58)); int[][] scenarios = { {10, 10}, {50, 50}, {100, 100}, {500, 500}, {1000, 1000} }; for (int[] sc : scenarios) { int K = sc[0], G = sc[1]; long dOps = countDefectiveOps(K, G); long fOps = countFixedOps(K, G); double ratio = (double) dOps / fOps; System.out.printf("K=%-4d G=%-4d %15d %12d %9.1fx%n", K, G, dOps, fOps, ratio); } long dOps = countDefectiveOps(500, 500); long fOps = countFixedOps(500, 500); double ratio = (double) dOps / fOps; if (ratio < 10) { throw new AssertionError("Expected >10x ratio at K=500 G=500, got " + ratio); } System.out.println("\nAll assertions PASS"); System.out.println("Fix: use providedKeysSet(map) for O(1) dedup; giteaKeysSet for O(1) diff"); } }