Files
flutter-rimworld-modman/test/mod_list_troubleshooter_test.dart
2025-03-16 23:48:32 +01:00

391 lines
14 KiB
Dart

// 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(
'Bisect search 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(
'Bisect search 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(
'Bisect search 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('Bisect 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(
'Linear search 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(
'Linear search 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('Linear search 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.mod20'));
expect(result.activeMods.keys.last, equals('test.mod29'));
});
test('Linear search 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('Linear search 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('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('Loading dependencies should not fuck up troubleshooter', () {
final troubleshooter = ModListTroubleshooter(modList);
final expectedFirst = [
'test.mod1',
// 0 depends on 1
'test.mod0',
'test.mod2',
// 3 depends on 4
'test.mod4',
'test.mod3',
'test.mod5',
// 6 depends on 7
'test.mod7',
'test.mod6',
'test.mod8',
// 9 depends on 10
'test.mod10',
'test.mod9',
];
var result = troubleshooter.linearForward(stepSize: 10);
var loadOrder = result.loadRequired();
expect(loadOrder.length, equals(11));
for (int i = 0; i < expectedFirst.length; i++) {
expect(loadOrder[i], equals(expectedFirst[i]));
}
final expectedSecond = [
'test.mod10',
'test.mod11',
// 12 depends on 13
'test.mod13',
'test.mod12',
'test.mod14',
// 15 depends on 16
'test.mod16',
'test.mod15',
'test.mod17',
// 18 depends on 19
'test.mod19',
'test.mod18',
];
result = troubleshooter.linearForward(stepSize: 10);
loadOrder = result.loadRequired();
expect(loadOrder.length, equals(10));
for (int i = 0; i < expectedSecond.length; i++) {
expect(loadOrder[i], equals(expectedSecond[i]));
}
});
});
}