// Here's the plan: // This class will take an instance of ModList and manipulate it in various ways // What we want to achieve is two things: // A) a binary search / bisect algorithm to find the minimum set of mods // that exhibit a bug // B) a linear search / batching algorithm for the same purpose // Why both? I think B will be most useful most often but A theoretically // should be faster // Why I think A might not always be faster is because it takes us a very long // time to load a lot of mods // So say it takes us 30 minutes to load 300 mods // Via bisect we would be loading 30 + 15 + 7.5 + ... = some 50 minutes // Via linear search we would be loading say 30 mods at a time // Which would be 3 minutes per batch for 10 batches // ie. 30 minutes // Reality is a little bit more complicated than that but that is the theory // Now - how should this class do what I detailed it to do // Keep the original ModList and copy it for every iteration // Whether that be an iteration of bisect or a batch of linear search // For every new batch make sure all its dependencies are loaded (ModList.loadRequired()) // Then try run game and proceed to next batch (or don't) // Progressively our ModList will shrink (or not, regardless) // And we should keep a registry of tested (say Good) mods and ones we haven't gotten to yet // Maybe even make sure each batch contains N untested mods // And that we don't test the same mod twice (unless it's a library) import 'package:flutter_test/flutter_test.dart'; import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; import 'package:rimworld_modman/mod_list_troubleshooter.dart'; Mod makeDummy() { return Mod( name: 'Dummy Mod', id: 'dummy', path: '', versions: ["1.5"], description: '', dependencies: [], loadAfter: [], loadBefore: [], incompatibilities: [], size: 0, isBaseGame: false, isExpansion: false, enabled: false, ); } void main() { group('Bisect Tests', () { late ModList modList = ModList(); setUp(() { modList = ModList(); // Add some base mods for (int i = 0; i < 20; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith(name: 'Test Mod $i', id: modId); modList.mods[modId] = mod; } // Add some mods with dependencies for (int i = 20; i < 30; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith( name: 'Test Mod $i', id: modId, dependencies: ['test.mod${i - 20}'], // Depend on earlier mods ); modList.mods[modId] = mod; } modList.enableAll(); }); test( 'Should end up with half the mods every forward iteration until 1', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.binaryForward(); // Half of our initial 30 expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.first, equals('test.mod15')); result = troubleshooter.binaryForward(); // Half of our previous result expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod22')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(4)); expect(result.activeMods.keys.first, equals('test.mod26')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.first, equals('test.mod28')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod29')); }, ); test( 'Should end up with half the mods every backward iteration until 1', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.binaryBackward(); // Half of our initial 30 expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.last, equals('test.mod14')); result = troubleshooter.binaryBackward(); // Half of our previous result expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.last, equals('test.mod7')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(4)); expect(result.activeMods.keys.last, equals('test.mod3')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.last, equals('test.mod1')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.last, equals('test.mod0')); }, ); test('Should end up with half the mods every iteration until 1', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.binaryBackward(); // Half of our initial 30 expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.last, equals('test.mod14')); result = troubleshooter.binaryForward(); // Half of our previous result expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod7')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(4)); expect(result.activeMods.keys.last, equals('test.mod10')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.last, equals('test.mod9')); }); test('Should handle abuse gracefully', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(15)); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(8)); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(4)); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(2)); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod9')); }); }); group('Linear Tests', () { late ModList modList = ModList(); setUp(() { modList = ModList(); // Add some base mods for (int i = 0; i < 20; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith(name: 'Test Mod $i', id: modId); modList.mods[modId] = mod; } // Add some mods with dependencies for (int i = 20; i < 30; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith( name: 'Test Mod $i', id: modId, dependencies: ['test.mod${i - 20}'], // Depend on earlier mods ); modList.mods[modId] = mod; } modList.enableAll(); }); test('Should end up with 10 mods every forward iteration', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); }); test('Should end up with 10 mods every backward iteration', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); }); test('Should end up with 10 mods every iteration', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); }); test('Should handle abuse gracefully', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); }); test('Should handle different step sizes', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); }); test('Cannot return more items than there are', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10000); expect(result.activeMods.length, equals(30)); result = troubleshooter.linearForward(stepSize: 10000); expect(result.activeMods.length, equals(30)); }); }); group('Navigation tests', () { late ModList modList = ModList(); setUp(() { modList = ModList(); // Add some base mods for (int i = 0; i < 20; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith(name: 'Test Mod $i', id: modId); modList.mods[modId] = mod; } // Add some mods with dependencies for (int i = 20; i < 30; i++) { final modId = 'test.mod$i'; final mod = makeDummy().copyWith( name: 'Test Mod $i', id: modId, dependencies: ['test.mod${i - 20}'], // Depend on earlier mods ); modList.mods[modId] = mod; } modList.enableAll(); }); test('Mixed navigation should work', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod5')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod5')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod5')); expect(result.activeMods.keys.last, equals('test.mod14')); }); test('Complex navigation sequence should work correctly', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.first, equals('test.mod15')); result = troubleshooter.linearBackward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod25')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(3)); expect(result.activeMods.keys.first, equals('test.mod27')); result = troubleshooter.linearForward(stepSize: 2); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.first, equals('test.mod27')); expect(result.activeMods.keys.last, equals('test.mod28')); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(1)); expect(result.activeMods.keys.first, equals('test.mod27')); }); test('Varying step sizes in linear navigation', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearForward(stepSize: 15); expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod14')); result = troubleshooter.linearForward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod4')); result = troubleshooter.linearForward(stepSize: 2); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod1')); result = troubleshooter.linearBackward(stepSize: 3); expect(result.activeMods.length, equals(3)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod2')); result = troubleshooter.linearBackward(stepSize: 7); expect(result.activeMods.length, equals(7)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod6')); }); test('Edge case - switching approaches at the boundary', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod5')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.linearBackward(stepSize: 2); expect(result.activeMods.length, equals(2)); expect(result.activeMods.keys.first, equals('test.mod8')); expect(result.activeMods.keys.last, equals('test.mod9')); }); test('Testing reset/restart behavior', () { final troubleshooter = ModListTroubleshooter(modList); // Do some navigation first var result = troubleshooter.linearForward(stepSize: 5); expect(result.activeMods.length, equals(5)); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(3)); // Create a new troubleshooter with the same mod list (simulating reset) final newTroubleshooter = ModListTroubleshooter(modList); // First operation should work as if we're starting fresh result = newTroubleshooter.binaryForward(); expect(result.activeMods.length, equals(15)); expect(result.activeMods.keys.first, equals('test.mod15')); // Original troubleshooter should still be in its own state result = troubleshooter.linearForward(stepSize: 1); expect(result.activeMods.length, equals(1)); }); test('Alternate between multiple approaches repeatedly', () { final troubleshooter = ModListTroubleshooter(modList); // Alternate between binary and linear several times var result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(15)); result = troubleshooter.linearBackward(stepSize: 5); expect(result.activeMods.length, equals(5)); result = troubleshooter.binaryForward(); expect(result.activeMods.length, equals(3)); result = troubleshooter.linearBackward(stepSize: 1); expect(result.activeMods.length, equals(1)); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); result = troubleshooter.binaryBackward(); expect(result.activeMods.length, equals(5)); // Final set of mods should be consistent with the operations performed expect(result.activeMods.length, equals(5)); }); // These tests specifically examine the nuances of linear navigation test('Linear navigation window adjustment - forward', () { final troubleshooter = ModListTroubleshooter(modList); // First linearForward with a specific step size var result = troubleshooter.linearForward(stepSize: 8); expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod7')); // Second call should move forward since current selection size matches step size result = troubleshooter.linearForward(stepSize: 8); expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod8')); expect(result.activeMods.keys.last, equals('test.mod15')); // Change step size - should adapt the window size without moving position result = troubleshooter.linearForward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod8')); expect(result.activeMods.keys.last, equals('test.mod12')); // Move forward with new step size result = troubleshooter.linearForward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod13')); expect(result.activeMods.keys.last, equals('test.mod17')); }); test('Linear navigation window adjustment - backward', () { final troubleshooter = ModListTroubleshooter(modList); // Move to the end first troubleshooter.linearBackward(stepSize: 30); // First linearBackward with a specific step size var result = troubleshooter.linearBackward(stepSize: 8); expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod22')); expect(result.activeMods.keys.last, equals('test.mod29')); // Second call should move backward since current selection size matches step size result = troubleshooter.linearBackward(stepSize: 8); expect(result.activeMods.length, equals(8)); expect(result.activeMods.keys.first, equals('test.mod14')); expect(result.activeMods.keys.last, equals('test.mod21')); // Change step size - should adapt the window size without moving position result = troubleshooter.linearBackward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod17')); expect(result.activeMods.keys.last, equals('test.mod21')); // Move backward with new step size result = troubleshooter.linearBackward(stepSize: 5); expect(result.activeMods.length, equals(5)); expect(result.activeMods.keys.first, equals('test.mod12')); expect(result.activeMods.keys.last, equals('test.mod16')); }); test('Linear navigation boundary handling - forward', () { final troubleshooter = ModListTroubleshooter(modList); var result = troubleshooter.linearForward(stepSize: 25); expect(result.activeMods.length, equals(25)); expect(result.activeMods.keys.first, equals('test.mod0')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod9')); result = troubleshooter.linearForward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); result = troubleshooter.linearForward(stepSize: 3); expect(result.activeMods.length, equals(3)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod12')); }); test('Linear navigation boundary handling - backward', () { final troubleshooter = ModListTroubleshooter(modList); troubleshooter.linearForward(stepSize: 30); var result = troubleshooter.linearBackward(stepSize: 25); expect(result.activeMods.length, equals(25)); expect(result.activeMods.keys.first, equals('test.mod5')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod20')); expect(result.activeMods.keys.last, equals('test.mod29')); result = troubleshooter.linearBackward(stepSize: 10); expect(result.activeMods.length, equals(10)); expect(result.activeMods.keys.first, equals('test.mod10')); expect(result.activeMods.keys.last, equals('test.mod19')); result = troubleshooter.linearBackward(stepSize: 3); expect(result.activeMods.length, equals(3)); expect(result.activeMods.keys.first, equals('test.mod17')); expect(result.activeMods.keys.last, equals('test.mod19')); }); // Test to verify we always get the requested number of mods at boundaries test( 'Linear navigation always returns exactly stepSize mods when possible', () { final troubleshooter = ModListTroubleshooter(modList); troubleshooter.linearForward(stepSize: 23); var result = troubleshooter.linearForward(stepSize: 7); expect(result.activeMods.length, equals(7)); result = troubleshooter.linearForward(stepSize: 7); expect(result.activeMods.length, equals(7)); result = troubleshooter.linearBackward(stepSize: 23); expect(result.activeMods.length, equals(23)); result = troubleshooter.linearBackward(stepSize: 8); expect(result.activeMods.length, equals(8)); result = troubleshooter.linearBackward(stepSize: 8); expect(result.activeMods.length, equals(8)); }, ); test('Linear navigation with oversized steps', () { final troubleshooter = ModListTroubleshooter(modList); // Step size larger than total mods var result = troubleshooter.linearForward(stepSize: 50); expect(result.activeMods.length, equals(30)); // All 30 mods expect(result.activeMods.keys.first, equals('test.mod0')); expect(result.activeMods.keys.last, equals('test.mod29')); // Forward with oversized step should still return all mods result = troubleshooter.linearForward(stepSize: 50); expect(result.activeMods.length, equals(30)); // Still all 30 mods // Now with backward result = troubleshooter.linearBackward(stepSize: 50); expect(result.activeMods.length, equals(30)); // All 30 mods // Another backward with oversized step result = troubleshooter.linearBackward(stepSize: 50); expect(result.activeMods.length, equals(30)); // Still all 30 mods }); }); group('Loading dependencies', () { late ModList modList = ModList(); setUp(() { modList = ModList(); for (int i = 0; i < 20; i++) { final modId = 'test.mod$i'; var mod = makeDummy().copyWith(name: 'Test Mod $i', id: modId); if (i % 3 == 0) { mod = mod.copyWith(dependencies: ['test.mod${i + 1}']); } modList.mods[modId] = mod; } // Dependencies are: // 0 -> 1 // 3 -> 4 // 6 -> 7 // 9 -> 10 // 12 -> 13 // 15 -> 16 // 18 -> 19 modList.enableAll(); }); // Not that it has any reason to since they're completely detached... test('Should not fuck up troubleshooter', () { final troubleshooter = ModListTroubleshooter(modList); final expectedFirst = [ 'test.mod10', 'test.mod9', 'test.mod8', 'test.mod2', 'test.mod4', 'test.mod3', 'test.mod5', 'test.mod7', 'test.mod6', 'test.mod1', 'test.mod0', ]; var result = troubleshooter.linearForward(stepSize: 10); var loadOrder = result.loadRequired(); expect(loadOrder.loadOrder.length, equals(11)); expect(loadOrder.loadOrder, equals(expectedFirst)); final expectedSecond = [ 'test.mod19', 'test.mod18', 'test.mod17', 'test.mod11', 'test.mod13', 'test.mod12', 'test.mod14', 'test.mod16', 'test.mod15', 'test.mod10', ]; result = troubleshooter.linearForward(stepSize: 10); loadOrder = result.loadRequired(); expect(loadOrder.loadOrder.length, equals(10)); expect(loadOrder.loadOrder, equals(expectedSecond)); }); }); }