import java.util.*; /** * Unit test simulating Forgejo forgejo-0002: * modules/repository/init.go LoadRepoConfig() license sort uses O(P*L) scans: * * for _, name := range setting.Repository.PreferredLicenses { * if util.SliceContainsString(Licenses, name, true) { // O(L) scan * sortedLicenses = append(sortedLicenses, name) * } * } * for _, name := range Licenses { * if !util.SliceContainsString(setting.Repository.PreferredLicenses, name, true) { // O(P) scan * sortedLicenses = append(sortedLicenses, name) * } * } * * Forgejo ships 776 licenses. Any preferred list of P entries costs O(P*776 + 776*P). * Fix: build sets for O(1) lookup before the loops. */ public class ForgejoLicenseSortTest { // Defective: O(P*L + L*P) static List sortLicensesDefective( List allLicenses, List preferred) { List sorted = new ArrayList<>(allLicenses.size()); for (String name : preferred) { if (containsIgnoreCase(allLicenses, name)) { sorted.add(name); } } for (String name : allLicenses) { if (!containsIgnoreCase(preferred, name)) { sorted.add(name); } } return sorted; } static boolean containsIgnoreCase(List list, String target) { String t = target.toLowerCase(Locale.ROOT); for (String s : list) { if (s.toLowerCase(Locale.ROOT).equals(t)) return true; } return false; } // Fixed: build sets first, then O(P + L) static List sortLicensesFixed( List allLicenses, List preferred) { Set licenseSet = new HashSet<>(allLicenses.size() * 2); for (String s : allLicenses) licenseSet.add(s.toLowerCase(Locale.ROOT)); Set preferredSet = new HashSet<>(preferred.size() * 2); for (String s : preferred) preferredSet.add(s.toLowerCase(Locale.ROOT)); List sorted = new ArrayList<>(allLicenses.size()); for (String name : preferred) { if (licenseSet.contains(name.toLowerCase(Locale.ROOT))) { sorted.add(name); } } for (String name : allLicenses) { if (!preferredSet.contains(name.toLowerCase(Locale.ROOT))) { sorted.add(name); } } return sorted; } static long countDefectiveOps(List allLicenses, List preferred) { long ops = 0; // First loop: P * L for (String name : preferred) { ops += allLicenses.size(); } // Second loop: L * P for (String name : allLicenses) { ops += preferred.size(); } return ops; } static long countFixedOps(List allLicenses, List preferred) { // Build sets: O(L + P) long ops = allLicenses.size() + preferred.size(); // Two loops: O(P + L) ops += preferred.size() + allLicenses.size(); return ops; } // Build a realistic license list like Forgejo's 776 licenses static List makeLicenses(int count) { List licenses = new ArrayList<>(count); String[] templates = { "MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "LGPL-2.1", "BSD-2-Clause", "BSD-3-Clause", "ISC", "MPL-2.0", "CDDL-1.0" }; for (int i = 0; i < count; i++) { licenses.add(templates[i % templates.length] + "-variant-" + i); } return licenses; } public static void main(String[] args) { System.out.println("forgejo-0002: LoadRepoConfig license sort O(P*L) -> O(P+L)"); System.out.println("=".repeat(60)); // Correctness test with small data List licenses = Arrays.asList( "MIT", "Apache-2.0", "GPL-2.0", "GPL-3.0", "BSD-2-Clause" ); List preferred = Arrays.asList("GPL-3.0", "MIT"); List defective = sortLicensesDefective(licenses, preferred); List fixed = sortLicensesFixed(licenses, preferred); // Both should start with preferred items (in preferred order), rest follow if (!defective.get(0).equals("GPL-3.0") || !defective.get(1).equals("MIT")) { throw new AssertionError("Defective sort wrong ordering: " + defective); } if (!fixed.get(0).equals("GPL-3.0") || !fixed.get(1).equals("MIT")) { throw new AssertionError("Fixed sort wrong ordering: " + fixed); } if (defective.size() != fixed.size()) { throw new AssertionError("Size mismatch: " + defective.size() + " vs " + fixed.size()); } System.out.println("PASS: correctness verified"); // Benchmark: 776 licenses (realistic), various preferred counts System.out.printf("%n%-10s %15s %12s %10s%n", "licenses", "defective_ops", "fixed_ops", "ratio"); System.out.println("-".repeat(48)); int L = 776; List allLicenses = makeLicenses(L); int[] prefCounts = {1, 5, 10, 20}; for (int P : prefCounts) { List pref = allLicenses.subList(0, P); long def = countDefectiveOps(allLicenses, pref); long fix = countFixedOps(allLicenses, pref); double ratio = (double) def / fix; System.out.printf("L=%-4d P=%-4d %15d %12d %9.1fx%n", L, P, def, fix, ratio); } long def776 = countDefectiveOps(allLicenses, allLicenses.subList(0, 5)); long fix776 = countFixedOps(allLicenses, allLicenses.subList(0, 5)); double ratio776 = (double) def776 / fix776; if (ratio776 < 4) { throw new AssertionError("Expected >4x ratio at L=776, P=5, got " + ratio776); } System.out.println("\nAll assertions PASS"); System.out.println("Fix: build licensesSet and preferredSet before loops for O(1) lookup"); } }