diff --git a/test/mod_list_test.dart b/test/mod_list_test.dart index ed39ef8..bf03379 100644 --- a/test/mod_list_test.dart +++ b/test/mod_list_test.dart @@ -81,6 +81,7 @@ void main() { final rimworldIndex = sortedMods.indexOf('ludeon.rimworld'); expect(harmonyIndex, lessThan(rimworldIndex)); }); + test('Prepatcher should load after Harmony and RimWorld', () { final prepatcherIndex = sortedMods.indexOf('prepatcher'); final harmonyIndex = sortedMods.indexOf('harmony'); @@ -88,21 +89,25 @@ void main() { expect(prepatcherIndex, greaterThan(harmonyIndex)); expect(prepatcherIndex, greaterThan(rimworldIndex)); }); + test('RimWorld should load before Anomaly', () { final rimworldIndex = sortedMods.indexOf('ludeon.rimworld'); final anomalyIndex = sortedMods.indexOf('ludeon.rimworld.anomaly'); expect(rimworldIndex, lessThan(anomalyIndex)); }); + test('Disabled dummy mod should not be loaded', () { final disabledIndex = sortedMods.indexOf('disabledDummy'); expect(disabledIndex, isNegative); }); - test("yuuuge mod should load before smol", () { + + test('Larger mods should load before smaller ones', () { final smolIndex = sortedMods.indexOf('smol'); final yuuugeIndex = sortedMods.indexOf('yuuuge'); expect(yuuugeIndex, lessThan(smolIndex)); }); - test('Error generating load order with incompatible mods', () { + + test('Incompatible mods should throw exception', () { dummyList.setEnabled('incompatible', true); expect(() => dummyList.generateLoadOrder(), throwsException); }); @@ -114,20 +119,242 @@ void main() { dummyList2.disableAll(); dummyList2.setEnabled('prepatcher', true); final sortedMods2 = dummyList2.loadRequired(); + group('Test loadRequired', () { - test( - 'Harmony should get enabled by loadRequired as a dependency of prepatcher', - () { - final harmonyIndex = sortedMods2.indexOf('harmony'); - expect(harmonyIndex, isNot(-1)); - }, - ); - test('No other mods should get enabled', () { + test('Dependencies should be automatically enabled', () { + final harmonyIndex = sortedMods2.indexOf('harmony'); + expect(harmonyIndex, isNot(-1)); + }); + + test('Only required mods should be enabled', () { for (final mod in dummyMods2.keys) { if (mod != 'prepatcher' && mod != 'harmony') { expect(sortedMods2.indexOf(mod), isNegative); } } }); + + test('Incompatible mods should throw exception', () { + dummyList2.setEnabled('incompatible', true); + expect(() => dummyList2.loadRequired(), throwsException); + }); + }); + + group('Test cyclic dependencies', () { + test('Cyclic dependencies should throw exception', () { + final cyclicMods = generateDummyMods(); + // Create a cyclic dependency: A -> B -> C -> A + cyclicMods['modA'] = cyclicMods['smol']!.copyWith( + name: 'Mod A', + id: 'modA', + dependencies: ['modB'], + ); + cyclicMods['modB'] = cyclicMods['smol']!.copyWith( + name: 'Mod B', + id: 'modB', + dependencies: ['modC'], + ); + cyclicMods['modC'] = cyclicMods['smol']!.copyWith( + name: 'Mod C', + id: 'modC', + dependencies: ['modA'], + ); + + final list = ModList(); + list.mods = cyclicMods; + list.enableAll(); + + expect(() => list.generateLoadOrder(), throwsException); + }); + }); + + group('Test soft constraints', () { + test('Load preferences should be respected when possible', () { + final softConstraintMods = generateDummyMods(); + softConstraintMods['modA'] = softConstraintMods['smol']!.copyWith( + name: 'Mod A', + id: 'modA', + loadAfter: ['modB'], + loadBefore: ['modC'], + ); + softConstraintMods['modB'] = softConstraintMods['smol']!.copyWith( + name: 'Mod B', + id: 'modB', + ); + softConstraintMods['modC'] = softConstraintMods['smol']!.copyWith( + name: 'Mod C', + id: 'modC', + ); + + final list = ModList(); + list.mods = softConstraintMods; + list.enableAll(); + final order = list.generateLoadOrder(); + + final aIndex = order.indexOf('modA'); + final bIndex = order.indexOf('modB'); + final cIndex = order.indexOf('modC'); + + expect(aIndex, greaterThan(bIndex)); + expect(aIndex, lessThan(cIndex)); + }); + }); + + group('Test conflict detection', () { + test('All conflicts should be correctly identified', () { + final incompatibleMods = generateDummyMods(); + incompatibleMods['modA'] = incompatibleMods['smol']!.copyWith( + name: 'Mod A', + id: 'modA', + incompatibilities: ['modB', 'modC'], + ); + incompatibleMods['modB'] = incompatibleMods['smol']!.copyWith( + name: 'Mod B', + id: 'modB', + ); + incompatibleMods['modC'] = incompatibleMods['smol']!.copyWith( + name: 'Mod C', + id: 'modC', + ); + + final list = ModList(); + list.mods = incompatibleMods; + list.enableAll(); + + final conflicts = list.checkIncompatibilities(); + expect(conflicts.length, equals(2)); + + // Check if conflicts contain these pairs (order doesn't matter) + expect( + conflicts.any( + (c) => + (c[0] == 'modA' && c[1] == 'modB') || + (c[0] == 'modB' && c[1] == 'modA'), + ), + isTrue, + ); + expect( + conflicts.any( + (c) => + (c[0] == 'modA' && c[1] == 'modC') || + (c[0] == 'modC' && c[1] == 'modA'), + ), + isTrue, + ); + }); + }); + + group('Test enable/disable functionality', () { + test('Enable and disable methods should work correctly', () { + final testMods = generateDummyMods(); + final list = ModList(); + list.mods = testMods; + + list.enableAll(); + for (final mod in list.mods.values) { + expect(mod.enabled, isTrue); + } + + list.disableAll(); + for (final mod in list.mods.values) { + expect(mod.enabled, isFalse); + } + }); + }); + + group('Test base game and expansion handling', () { + test('Base game and expansions should be correctly ordered', () { + final gameMods = generateDummyMods(); + final list = ModList(); + list.mods = gameMods; + list.enableAll(); + list.setEnabled('incompatible', false); // Avoid exception + + final order = list.generateLoadOrder(); + + // Base game should load before any expansions + final baseGameIndex = order.indexOf('ludeon.rimworld'); + final expansionIndex = order.indexOf('ludeon.rimworld.anomaly'); + expect(baseGameIndex, lessThan(expansionIndex)); + + // Normal mods should come after base game (except harmony) + for (final modId in order) { + if (modId != 'ludeon.rimworld' && + modId != 'harmony' && + !modId.startsWith('ludeon.rimworld.')) { + expect(order.indexOf(modId), greaterThan(baseGameIndex)); + } + } + }); + }); + + group('Test complex dependency chains', () { + test('Complex dependency chains should resolve correctly', () { + final complexMods = generateDummyMods(); + // Create a chain: A -> B -> C -> D + complexMods['modA'] = complexMods['smol']!.copyWith( + name: 'Mod A', + id: 'modA', + dependencies: ['modB'], + ); + complexMods['modB'] = complexMods['smol']!.copyWith( + name: 'Mod B', + id: 'modB', + dependencies: ['modC'], + ); + complexMods['modC'] = complexMods['smol']!.copyWith( + name: 'Mod C', + id: 'modC', + dependencies: ['modD'], + ); + complexMods['modD'] = complexMods['smol']!.copyWith( + name: 'Mod D', + id: 'modD', + ); + + final list = ModList(); + list.mods = complexMods; + list.disableAll(); + list.setEnabled('modA', true); + + final result = list.loadRequired(); + + // All mods in the chain should be enabled + expect(result.contains('modA'), isTrue); + expect(result.contains('modB'), isTrue); + expect(result.contains('modC'), isTrue); + expect(result.contains('modD'), isTrue); + + // The order should be D -> C -> B -> A + expect(result.indexOf('modD'), lessThan(result.indexOf('modC'))); + expect(result.indexOf('modC'), lessThan(result.indexOf('modB'))); + expect(result.indexOf('modB'), lessThan(result.indexOf('modA'))); + }); + }); + + group('Test constraint prioritization', () { + test('Hard dependencies should override soft constraints', () { + final constraintMods = generateDummyMods(); + // A depends on B but wants to load before it (impossible) + constraintMods['modA'] = constraintMods['smol']!.copyWith( + name: 'Mod A', + id: 'modA', + dependencies: ['modB'], + loadBefore: ['modB'], + ); + constraintMods['modB'] = constraintMods['smol']!.copyWith( + name: 'Mod B', + id: 'modB', + ); + + final list = ModList(); + list.mods = constraintMods; + list.enableAll(); + + final order = list.generateLoadOrder(); + + // Hard dependency must win + expect(order.indexOf('modB'), lessThan(order.indexOf('modA'))); + }); }); }