diff --git a/Cmdline/Action/List.cs b/Cmdline/Action/List.cs index 19153e2e84..e5cadd5bce 100644 --- a/Cmdline/Action/List.cs +++ b/Cmdline/Action/List.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Collections.Generic; +using System.Linq; using log4net; @@ -53,6 +54,11 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) if (exportFileType == null) { var installed = new SortedDictionary(registry.Installed()); + var upgradeable = registry + .CheckUpgradeable(instance.VersionCriteria(), new HashSet()) + [true] + .Select(m => m.identifier) + .ToHashSet(); foreach (KeyValuePair mod in installed) { @@ -102,7 +108,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - else if (latest.version.IsEqualTo(current_version) && !registry.HasUpdate(mod.Key, instance.VersionCriteria())) + else if (!upgradeable.Contains(mod.Key)) { // Up to date log.InfoFormat("Latest {0} is {1}", mod.Key, latest.version); @@ -119,7 +125,7 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) } } } - else if (latest.version.IsGreaterThan(mod.Value) || registry.HasUpdate(mod.Key, instance.VersionCriteria())) + else { // Upgradable bullet = "^"; diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 69848c959f..11ce3a499c 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -4,10 +4,10 @@ using System.Transactions; using Autofac; -using log4net; using CKAN.Versioning; using CKAN.Configuration; +using CKAN.Extensions; namespace CKAN.CmdLine { @@ -114,38 +114,17 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) var registry = regMgr.registry; if (options.upgrade_all) { - var to_upgrade = new List(); - - foreach (KeyValuePair mod in registry.Installed(false)) + var to_upgrade = registry + .CheckUpgradeable(instance.VersionCriteria(), new HashSet()) + [true]; + if (to_upgrade.Count == 0) { - try - { - // Check if upgrades are available - var latest = registry.LatestAvailable(mod.Key, instance.VersionCriteria()); - - // This may be an unindexed mod. If so, - // skip rather than crash. See KSP-CKAN/CKAN#841. - if (latest == null || latest.IsDLC) - { - continue; - } - - if (latest.version.IsGreaterThan(mod.Value) || registry.HasUpdate(mod.Key, instance.VersionCriteria())) - { - // Upgradable - log.InfoFormat("New version {0} found for {1}", - latest.version, latest.identifier); - to_upgrade.Add(latest); - } - - } - catch (ModuleNotFoundKraken) - { - log.InfoFormat("{0} is installed, but no longer in the registry", - mod.Key); - } + user.RaiseMessage(Properties.Resources.UpgradeAllUpToDate); + } + else + { + UpgradeModules(manager, user, instance, to_upgrade); } - UpgradeModules(manager, user, instance, true, to_upgrade); } else { @@ -196,16 +175,15 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) private void UpgradeModules(GameInstanceManager manager, IUser user, CKAN.GameInstance instance, - bool ConfirmPrompt, List modules) { UpgradeModules( manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => installer.Upgrade(modules, downloader, - ref possibleConfigOnlyDirs, regMgr, true, true, ConfirmPrompt), - m => modules.Add(m) - ); + ref possibleConfigOnlyDirs, + regMgr, true, true, true), + m => modules.Add(m)); } /// @@ -223,19 +201,72 @@ private void UpgradeModules(GameInstanceManager manager, UpgradeModules( manager, user, instance, repoData, (ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs) => - installer.Upgrade( - identsAndVersions.Select(arg => CkanModule.FromIDandVersion( - regMgr.registry, arg, - instance.VersionCriteria())) - .ToList(), - downloader, - ref possibleConfigOnlyDirs, - regMgr, - true), - m => identsAndVersions.Add(m.identifier) - ); + { + var crit = instance.VersionCriteria(); + var registry = regMgr.registry; + // Installed modules we're NOT upgrading + var heldIdents = registry.Installed(false) + .Keys + .Except(identsAndVersions.Select(arg => UpToFirst(arg, '='))) + .ToHashSet(); + // The modules we'll have after upgrading as aggressively as possible + var limiters = identsAndVersions.Select(req => CkanModule.FromIDandVersion(registry, req, crit) + ?? DefaultIfThrows( + () => registry.LatestAvailable(req, crit)) + ?? registry.GetInstalledVersion(req)) + .Concat(heldIdents.Select(ident => registry.GetInstalledVersion(ident))) + .Where(m => m != null) + .ToList(); + // Modules allowed by THOSE modules' relationships + var upgradeable = registry + .CheckUpgradeable(crit, heldIdents, limiters) + [true] + .ToDictionary(m => m.identifier, + m => m); + // Substitute back in the ident=ver requested versions + var to_upgrade = new List(); + foreach (var request in identsAndVersions) + { + var module = CkanModule.FromIDandVersion(registry, request, crit) + ?? (upgradeable.TryGetValue(request, out CkanModule m) + ? m + : null); + if (module == null) + { + user.RaiseMessage(Properties.Resources.UpgradeAlreadyUpToDate, request); + } + else + { + to_upgrade.Add(module); + } + } + if (to_upgrade.Count > 0) + { + installer.Upgrade(to_upgrade, downloader, ref possibleConfigOnlyDirs, regMgr, true); + } + }, + m => identsAndVersions.Add(m.identifier)); } + public static T DefaultIfThrows(Func func) + { + try + { + return func(); + } + catch + { + return default; + } + } + + private static string UpToFirst(string orig, char toFind) + => UpTo(orig, orig.IndexOf(toFind)); + + private static string UpTo(string orig, int pos) + => pos >= 0 && pos < orig.Length ? orig.Substring(0, pos) + : orig; + // Action isn't allowed private delegate void AttemptUpgradeAction(ModuleInstaller installer, NetAsyncModulesDownloader downloader, RegistryManager regMgr, ref HashSet possibleConfigOnlyDirs); @@ -292,7 +323,5 @@ private void UpgradeModules(GameInstanceManager manager, private readonly IUser user; private readonly GameInstanceManager manager; private readonly RepositoryDataManager repoData; - - private static readonly ILog log = LogManager.GetLogger(typeof(Upgrade)); } } diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx index 1dcf548b05..de8b154718 100644 --- a/Cmdline/Properties/Resources.resx +++ b/Cmdline/Properties/Resources.resx @@ -372,6 +372,8 @@ Try `ckan list` for a list of installed mods. You already have the latest version Upgrade aborted: {0} Module {0} not found + Module {0} is already up to date + All modules are up to date CKAN can't upgrade expansion '{0}' for you To upgrade this expansion, download any updates from the store page from which you purchased it: {0} diff --git a/ConsoleUI/InstallScreen.cs b/ConsoleUI/InstallScreen.cs index 9ad7da2284..6816819f0a 100644 --- a/ConsoleUI/InstallScreen.cs +++ b/ConsoleUI/InstallScreen.cs @@ -75,16 +75,27 @@ public override void Run(ConsoleTheme theme, Action process = null } NetAsyncModulesDownloader dl = new NetAsyncModulesDownloader(this, manager.Cache); if (plan.Install.Count > 0) { - List iList = new List(plan.Install); + var iList = plan.Install + .Select(m => registry.LatestAvailable(m.identifier, + manager.CurrentInstance.VersionCriteria(), + null, + registry.InstalledModules + .Select(im => im.Module) + .ToArray(), + plan.Install)) + .ToArray(); inst.InstallList(iList, resolvOpts, regMgr, ref possibleConfigOnlyDirs, dl); plan.Install.Clear(); } if (plan.Upgrade.Count > 0) { - inst.Upgrade(plan.Upgrade - .Select(ident => regMgr.registry.LatestAvailable( - ident, manager.CurrentInstance.VersionCriteria())) - .ToList(), - dl, ref possibleConfigOnlyDirs, regMgr); + var upgGroups = registry + .CheckUpgradeable(manager.CurrentInstance.VersionCriteria(), + // Hold identifiers not chosen for upgrading + registry.Installed(false) + .Keys + .Except(plan.Upgrade) + .ToHashSet()); + inst.Upgrade(upgGroups[true], dl, ref possibleConfigOnlyDirs, regMgr); plan.Upgrade.Clear(); } if (plan.Replace.Count > 0) { diff --git a/ConsoleUI/ModInfoScreen.cs b/ConsoleUI/ModInfoScreen.cs index 3e55db34df..de4e26f761 100644 --- a/ConsoleUI/ModInfoScreen.cs +++ b/ConsoleUI/ModInfoScreen.cs @@ -266,6 +266,9 @@ private int addDependencies(int top = 8) const int lblW = 16; int nameW = midL - 2 - lblW - 2 - 1; int depsH = (h - 2) * numDeps / (numDeps + numConfs); + var upgradeableGroups = registry + .CheckUpgradeable(manager.CurrentInstance.VersionCriteria(), + new HashSet()); AddObject(new ConsoleFrame( 1, top, midL, top + h - 1, @@ -290,7 +293,8 @@ private int addDependencies(int top = 8) foreach (RelationshipDescriptor rd in mod.depends) { tb.AddLine(ScreenObject.TruncateLength( // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString())) + ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), + upgradeableGroups[true])) + rd.ToString(), nameW )); @@ -315,7 +319,8 @@ private int addDependencies(int top = 8) foreach (RelationshipDescriptor rd in mod.conflicts) { tb.AddLine(ScreenObject.TruncateLength( // Show install status - ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString())) + ModListScreen.StatusSymbol(plan.GetModStatus(manager, registry, rd.ToString(), + upgradeableGroups[true])) + rd.ToString(), nameW )); diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 39c2708086..838da7a63f 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -82,7 +82,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re case "i": return registry.IsInstalled(m.identifier, false); case "u": - return registry.HasUpdate(m.identifier, manager.CurrentInstance.VersionCriteria()); + return upgradeableGroups?[true].Any(upg => upg.identifier == m.identifier) ?? false; case "n": // Filter new return recent.Contains(m.identifier); @@ -188,7 +188,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re ); moduleList.AddTip("+", Properties.Resources.ModListUpgradeTip, () => moduleList.Selection != null && !moduleList.Selection.IsDLC - && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) + && (upgradeableGroups?[true].Any(upg => upg.identifier == moduleList.Selection.identifier) ?? false) ); moduleList.AddTip("+", Properties.Resources.ModListReplaceTip, () => moduleList.Selection != null @@ -199,7 +199,7 @@ public ModListScreen(GameInstanceManager mgr, RepositoryDataManager repoData, Re if (!registry.IsInstalled(moduleList.Selection.identifier, false)) { plan.ToggleInstall(moduleList.Selection); } else if (registry.IsInstalled(moduleList.Selection.identifier, false) - && registry.HasUpdate(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria())) { + && (upgradeableGroups?[true].Any(upg => upg.identifier == moduleList.Selection.identifier) ?? false)) { plan.ToggleUpgrade(moduleList.Selection); } else if (registry.GetReplacement(moduleList.Selection.identifier, manager.CurrentInstance.VersionCriteria()) != null) { plan.ToggleReplace(moduleList.Selection.identifier); @@ -367,21 +367,13 @@ private bool CaptureKey(ConsoleTheme theme) private bool HasAnyUpgradeable() { - foreach (string identifier in registry.Installed(true).Select(kvp => kvp.Key)) { - if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { - return true; - } - } - return false; + return (upgradeableGroups?[true].Count ?? 0) > 0; } private bool UpgradeAll(ConsoleTheme theme) { - foreach (string identifier in registry.Installed(true).Select(kvp => kvp.Key)) { - if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { - plan.Upgrade.Add(identifier); - } - } + plan.Upgrade.UnionWith(upgradeableGroups?[true].Select(m => m.identifier) + ?? Enumerable.Empty()); return true; } @@ -564,17 +556,20 @@ private List GetAllMods(ConsoleTheme theme, bool force = false) { UpdateRegistry(theme, false); } - allMods = new List(registry.CompatibleModules(manager.CurrentInstance.VersionCriteria())); + var crit = manager.CurrentInstance.VersionCriteria(); + allMods = new List(registry.CompatibleModules(crit)); foreach (InstalledModule im in registry.InstalledModules) { CkanModule m = null; try { - m = registry.LatestAvailable(im.identifier, manager.CurrentInstance.VersionCriteria()); + m = registry.LatestAvailable(im.identifier, crit); } catch (ModuleNotFoundKraken) { } if (m == null) { // Add unavailable installed mods to the list allMods.Add(im.Module); } } + upgradeableGroups = registry + .CheckUpgradeable(crit, new HashSet()); } return allMods; } @@ -621,7 +616,8 @@ private bool ApplyChanges(ConsoleTheme theme) /// public string StatusSymbol(CkanModule m) { - return StatusSymbol(plan.GetModStatus(manager, registry, m.identifier)); + return StatusSymbol(plan.GetModStatus(manager, registry, m.identifier, + upgradeableGroups?[true] ?? new List())); } /// @@ -664,8 +660,9 @@ private long totalInstalledDownloadSize() private readonly GameInstanceManager manager; private RegistryManager regMgr; private Registry registry; - private readonly RepositoryDataManager repoData; - private readonly bool debug; + private readonly RepositoryDataManager repoData; + private Dictionary> upgradeableGroups; + private readonly bool debug; private readonly ConsoleField searchBox; private readonly ConsoleListBox moduleList; @@ -773,15 +770,19 @@ public void Reset() /// Game instance manager containing the instances /// Registry of instance being displayed /// The mod + /// List of modules that can be upgraded /// /// Status of mod /// - public InstallStatus GetModStatus(GameInstanceManager manager, IRegistryQuerier registry, string identifier) + public InstallStatus GetModStatus(GameInstanceManager manager, + IRegistryQuerier registry, + string identifier, + List upgradeable) { if (registry.IsInstalled(identifier, false)) { if (Remove.Contains(identifier)) { return InstallStatus.Removing; - } else if (registry.HasUpdate(identifier, manager.CurrentInstance.VersionCriteria())) { + } else if (upgradeable.Any(m => m.identifier == identifier)) { if (Upgrade.Contains(identifier)) { return InstallStatus.Upgrading; } else { diff --git a/Core/Registry/IRegistryQuerier.cs b/Core/Registry/IRegistryQuerier.cs index 060c216b15..7485843455 100644 --- a/Core/Registry/IRegistryQuerier.cs +++ b/Core/Registry/IRegistryQuerier.cs @@ -165,45 +165,99 @@ public static bool IsAutodetected(this IRegistryQuerier querier, string identifi && querier.InstalledVersion(identifier) is UnmanagedModuleVersion; /// - /// Is the mod installed and does it have a newer version compatible with version + /// Is the mod installed and does it have a newer version compatible with versionCrit /// - public static bool HasUpdate(this IRegistryQuerier querier, string identifier, GameVersionCriteria version) + public static bool HasUpdate(this IRegistryQuerier querier, + string identifier, + GameVersionCriteria versionCrit, + out CkanModule latestMod, + ICollection installed = null) { - CkanModule newest_version; + // Check if it's installed (including manually!) + var instVer = querier.InstalledVersion(identifier); + if (instVer == null) + { + latestMod = null; + return false; + } + // Check if it's available try { - // Check if it's both installed and available - newest_version = querier.LatestAvailable(identifier, version); - if (newest_version == null || !querier.IsInstalled(identifier, false)) + latestMod = querier.LatestAvailable(identifier, versionCrit, null, installed); + } + catch + { + latestMod = null; + } + if (latestMod == null) + { + return false; + } + // Check if the installed module is up to date + var comp = latestMod.version.CompareTo(instVer); + if (comp == -1 + || (comp == 0 && !querier.MetadataChanged(identifier))) + { + latestMod = null; + return false; + } + // Checking with a RelationshipResolver here would commit us to + // testing against the currently installed modules in the registry, + // which could block us from upgrading away from a problem. + // Trust our LatestAvailable call above. + return true; + } + + public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, + GameVersionCriteria versionCrit, + HashSet heldIdents) + { + // Get the absolute latest versions ignoring restrictions, + // to break out of mutual version-depending deadlocks + var unlimited = querier.Installed(false) + .Keys + .Select(ident => !heldIdents.Contains(ident) + && querier.HasUpdate(ident, versionCrit, + out CkanModule latest) + && !latest.IsDLC + ? latest + : querier.GetInstalledVersion(ident)) + .Where(m => m != null) + .ToList(); + return querier.CheckUpgradeable(versionCrit, heldIdents, unlimited); + } + + public static Dictionary> CheckUpgradeable(this IRegistryQuerier querier, + GameVersionCriteria versionCrit, + HashSet heldIdents, + List limiters) + { + // Use those as the installed modules + var upgradeable = new List(); + var notUpgradeable = new List(); + foreach (var ident in querier.Installed(false).Keys) + { + if (!heldIdents.Contains(ident) + && querier.HasUpdate(ident, versionCrit, + out CkanModule latest, limiters) + && !latest.IsDLC) { - return false; + upgradeable.Add(latest); } - // Check if the installed module is up to date - if (newest_version.version <= querier.InstalledVersion(identifier) - && !querier.MetadataChanged(identifier)) + else { - return false; - } - // All quick checks pass. Now check the relationships. - var instMod = querier.InstalledModule(identifier); - RelationshipResolver resolver = new RelationshipResolver( - new CkanModule[] { newest_version }, - // Remove the old module when installing the new one - instMod == null ? null : new CkanModule[] { instMod.Module }, - new RelationshipResolverOptions() + var current = querier.InstalledModule(ident); + if (current != null && !current.Module.IsDLC) { - with_recommends = false, - without_toomanyprovides_kraken = true, - }, - querier, - version - ); + notUpgradeable.Add(current.Module); + } + } } - catch (Exception) + return new Dictionary> { - return false; - } - return true; + { true, upgradeable }, + { false, notUpgradeable }, + }; } private static bool MetadataChanged(this IRegistryQuerier querier, string identifier) diff --git a/Core/Repositories/AvailableModule.cs b/Core/Repositories/AvailableModule.cs index 85c564e2bd..15d53aa593 100644 --- a/Core/Repositories/AvailableModule.cs +++ b/Core/Repositories/AvailableModule.cs @@ -171,6 +171,20 @@ private static bool DependsAndConflictsOK(CkanModule module, ICollection mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier)); + UpdateCol.Visible = mainModList.ResetHasUpdate(inst, RegistryManager.Instance(inst, repoData).registry, + ChangeSet, ModGrid.Rows); } var registry = RegistryManager.Instance(inst, repoData).registry; mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); @@ -348,9 +350,11 @@ private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e) var registry = RegistryManager.Instance(inst, repoData).registry; foreach (var module in mainModList.Modules) { - mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game, registry); + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, + inst.Name, inst.game, registry); } UpdateHiddenTagsAndLabels(); + UpdateCol.Visible = mainModList.ResetHasUpdate(inst, registry, ChangeSet, ModGrid.Rows); } #endregion @@ -1321,41 +1325,16 @@ private bool _UpdateModsList(Dictionary old_modules = null) regMgr.ScanUnmanagedFiles(); RaiseMessage?.Invoke(Properties.Resources.MainModListLoadingInstalled); - var versionCriteria = inst.VersionCriteria(); - - var installedIdents = registry.InstalledModules - .Select(im => im.identifier) - .ToHashSet(); - var gui_mods = registry.InstalledModules - .AsParallel() - .Where(instMod => !instMod.Module.IsDLC) - .Select(instMod => new GUIMod( - instMod, repoData, registry, versionCriteria, null, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV)) - .Concat(registry.CompatibleModules(versionCriteria) - .Where(m => !installedIdents.Contains(m.identifier)) - .AsParallel() - .Where(m => !m.IsDLC) - .Select(m => new GUIMod( - m, repoData, registry, versionCriteria, null, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV))) - .Concat(registry.IncompatibleModules(versionCriteria) - .Where(m => !installedIdents.Contains(m.identifier)) - .AsParallel() - .Where(m => !m.IsDLC) - .Select(m => new GUIMod( - m, repoData, registry, versionCriteria, true, - Main.Instance.configuration.HideEpochs, - Main.Instance.configuration.HideV))) - .ToHashSet(); + + var guiMods = mainModList.GetGUIMods(registry, repoData, inst, + Main.Instance.configuration) + .ToHashSet(); RaiseMessage?.Invoke(Properties.Resources.MainModListPreservingNew); var toNotify = new HashSet(); if (old_modules != null) { - foreach (GUIMod gm in gui_mods) + foreach (GUIMod gm in guiMods) { if (old_modules.TryGetValue(gm.Identifier, out bool oldIncompat)) { @@ -1376,21 +1355,18 @@ private bool _UpdateModsList(Dictionary old_modules = null) else { // Copy the new mod flag from the old list. - var old_new_mods = new HashSet( - mainModList.Modules.Where(m => m.IsNew)); - foreach (var gui_mod in gui_mods.Intersect(old_new_mods)) + var oldNewMods = mainModList.Modules.Where(m => m.IsNew) + .ToHashSet(); + foreach (var guiMod in guiMods.Intersect(oldNewMods)) { - gui_mod.IsNew = true; + guiMod.IsNew = true; } } LabelsAfterUpdate?.Invoke(toNotify); RaiseMessage?.Invoke(Properties.Resources.MainModListPopulatingList); // Update our mod listing - mainModList.ConstructModList(gui_mods, - inst.Name, - inst.game, - ChangeSet); + mainModList.ConstructModList(guiMods, inst.Name, inst.game, ChangeSet); UpdateChangeSetAndConflicts(inst, registry); @@ -1867,10 +1843,20 @@ public void UpdateChangeSetAndConflicts(GameInstance inst, IRegistryQuerier regi List full_change_set = null; Dictionary new_conflicts = null; - var user_change_set = mainModList.ComputeUserChangeSet(registry, inst.VersionCriteria()); + var gameVersion = inst.VersionCriteria(); + var user_change_set = mainModList.ComputeUserChangeSet(registry, gameVersion); try { - var gameVersion = inst.VersionCriteria(); + // Set the target versions of upgrading mods based on what's actually allowed + foreach (var ch in user_change_set.OfType()) + { + if (mainModList.full_list_of_mod_rows[ch.Mod.identifier].Tag is GUIMod gmod) + { + // This setter calls UpdateChangeSetAndConflicts, so there's a risk of + // an infinite loop here. Tread lightly! + gmod.SelectedMod = ch.targetMod; + } + } var tuple = mainModList.ComputeFullChangeSetFromUserChangeSet(registry, user_change_set, gameVersion); full_change_set = tuple.Item1.ToList(); new_conflicts = tuple.Item2.ToDictionary( diff --git a/GUI/Controls/ModInfoTabs/Versions.cs b/GUI/Controls/ModInfoTabs/Versions.cs index a253ad67ac..d10753f72a 100644 --- a/GUI/Controls/ModInfoTabs/Versions.cs +++ b/GUI/Controls/ModInfoTabs/Versions.cs @@ -82,6 +82,8 @@ private void VersionsListView_ItemCheck(object sender, ItemCheckEventArgs e) case CheckState.Checked: if (allowInstall(module)) { + // Uncheck the upgrade box if it's checked + Main.Instance.ManageMods.MarkModForUpdate(visibleGuiModule.Identifier, false); // Add this version to the change set visibleGuiModule.SelectedMod = module; } @@ -230,10 +232,13 @@ private void checkInstallable(ListViewItem[] items) Partitioner.Create(items, true) // Distribute across cores .AsParallel() - // Abort when they switch to another mod - .WithCancellation(cancelTokenSrc.Token) // Return them as they're processed .WithMergeOptions(ParallelMergeOptions.NotBuffered) + // Abort when they switch to another mod + .WithCancellation(cancelTokenSrc.Token) + // Check the important ones first + .OrderBy(item => (item.Tag as CkanModule) != visibleGuiModule.InstalledMod?.Module + && (item.Tag as CkanModule) != visibleGuiModule.SelectedMod) // Slow step to be performed across multiple cores .Where(item => installable(installer, item.Tag as CkanModule, registry)) // Jump back to GUI thread for the updates for each compatible item diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs index b99cd71b2b..2ba6a4d220 100644 --- a/GUI/Labels/ModuleLabel.cs +++ b/GUI/Labels/ModuleLabel.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.ComponentModel; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; @@ -80,6 +81,11 @@ public bool ContainsModule(IGame game, string identifier) public bool AppliesTo(string instanceName) => InstanceName == null || InstanceName == instanceName; + public IEnumerable IdentifiersFor(IGame game) + => ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet idents) + ? idents + : Enumerable.Empty(); + /// /// Add a module to this label's group /// diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs index 279e6cac6c..fb3f069d37 100644 --- a/GUI/Labels/ModuleLabelList.cs +++ b/GUI/Labels/ModuleLabelList.cs @@ -79,5 +79,10 @@ public bool Save(string path) return false; } } + + public IEnumerable HeldIdentifiers(GameInstance inst) + => LabelsFor(inst.Name).Where(l => l.HoldVersion) + .SelectMany(l => l.IdentifiersFor(inst.game)) + .Distinct(); } } diff --git a/GUI/Model/GUIMod.cs b/GUI/Model/GUIMod.cs index 37366a87dc..8a18ac7a08 100644 --- a/GUI/Model/GUIMod.cs +++ b/GUI/Model/GUIMod.cs @@ -38,16 +38,6 @@ public CkanModule SelectedMod { selectedMod = value; - if (IsInstalled && HasUpdate) - { - var isLatest = (LatestCompatibleMod?.Equals(selectedMod) ?? false); - if (IsUpgradeChecked ^ isLatest) - { - // Try upgrading if they pick the latest - Main.Instance.ManageMods.MarkModForUpdate(Identifier, isLatest); - } - - } Main.Instance.ManageMods.MarkModForInstall(Identifier, selectedMod == null); var inst = Main.Instance.CurrentInstance; @@ -75,7 +65,7 @@ private void OnPropertyChanged([CallerMemberName] string name = null) public string Name { get; private set; } public bool IsInstalled { get; private set; } public bool IsAutoInstalled { get; private set; } - public bool HasUpdate { get; private set; } + public bool HasUpdate { get; set; } public bool HasReplacement { get; private set; } public bool IsIncompatible { get; private set; } public bool IsAutodetected { get; private set; } @@ -185,7 +175,6 @@ public GUIMod(CkanModule mod, Description = mod.description?.Trim() ?? string.Empty; Abbrevation = new string(Name.Split(' ').Where(s => s.Length > 0).Select(s => s[0]).ToArray()); - HasUpdate = registry.HasUpdate(mod.identifier, current_game_version); HasReplacement = registry.GetReplacement(mod, current_game_version) != null; DownloadSize = mod.download_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.download_size); InstallSize = mod.install_size == 0 ? Properties.Resources.GUIModNSlashA : CkanModule.FmtSize(mod.install_size); diff --git a/GUI/Model/ModChange.cs b/GUI/Model/ModChange.cs index 6149de4d1e..0db9904c94 100644 --- a/GUI/Model/ModChange.cs +++ b/GUI/Model/ModChange.cs @@ -157,7 +157,7 @@ public override string Description /// /// The target version for upgrading /// - public readonly CkanModule targetMod; + public CkanModule targetMod; private bool IsReinstall => targetMod.identifier == Mod.identifier diff --git a/GUI/Model/ModList.cs b/GUI/Model/ModList.cs index 8d605dc3bd..15368877d0 100644 --- a/GUI/Model/ModList.cs +++ b/GUI/Model/ModList.cs @@ -425,7 +425,39 @@ public string StripEpoch(string version) public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVersionCriteria crit) { log.Debug("Computing user changeset"); - var modChanges = Modules.SelectMany(mod => mod.GetModChanges()); + var modChanges = Modules.SelectMany(mod => mod.GetModChanges()) + .ToList(); + + // Inter-mod dependencies can block some upgrades, which can sometimes but not always + // be overcome by upgrading both mods. Try to pick the right target versions. + if (registry != null) + { + var upgrades = modChanges.OfType() + .ToArray(); + if (upgrades.Length > 0) + { + var upgradeable = registry.CheckUpgradeable(crit, + // Hold identifiers not chosen for upgrading + registry.Installed(false) + .Select(kvp => kvp.Key) + .Except(upgrades.Select(ch => ch.Mod.identifier)) + .ToHashSet()) + [true] + .ToDictionary(m => m.identifier, + m => m); + foreach (var change in upgrades) + { + change.targetMod = upgradeable.TryGetValue(change.Mod.identifier, + out CkanModule allowedMod) + // Upgrade to the version the registry says we should + ? allowedMod + // Not upgradeable! + : change.Mod; + } + modChanges.RemoveAll(ch => ch is ModUpgrade upg && upg.Mod == upg.targetMod); + } + } + return (registry == null ? modChanges : modChanges.Union( @@ -436,6 +468,97 @@ public HashSet ComputeUserChangeSet(IRegistryQuerier registry, GameVe .ToHashSet(); } + /// + /// Check upgradeability of all rows and set GUIMod.HasUpdate appropriately + /// + /// Current game instance + /// Current instance's registry + /// Currently pending changeset + /// The grid rows in case we need to replace some + /// true if any mod can be updated, false otherwise + public bool ResetHasUpdate(GameInstance inst, + IRegistryQuerier registry, + List ChangeSet, + DataGridViewRowCollection rows) + { + var upgGroups = registry.CheckUpgradeable(inst.VersionCriteria(), + ModuleLabels.HeldIdentifiers(inst) + .ToHashSet()); + foreach ((var upgradeable, var mods) in upgGroups) + { + foreach (var ident in mods.Select(m => m.identifier)) + { + var row = full_list_of_mod_rows[ident]; + if (row.Tag is GUIMod gmod && gmod.HasUpdate != upgradeable) + { + gmod.HasUpdate = upgradeable; + if (row.Visible) + { + // Swap whether the row has an upgrade checkbox + var newRow = + full_list_of_mod_rows[ident] = + MakeRow(gmod, ChangeSet, inst.Name, inst.game); + var rowIndex = row.Index; + rows.Remove(row); + rows.Insert(rowIndex, newRow); + } + } + } + } + return upgGroups[true].Count > 0; + } + + /// + /// Get all the GUI mods for the given instance. + /// + /// Registry of the instance + /// Repo data of the instance + /// Game instance + /// GUI config to use + /// Sequence of GUIMods + public IEnumerable GetGUIMods(IRegistryQuerier registry, + RepositoryDataManager repoData, + GameInstance inst, + GUIConfiguration config) + => GetGUIMods(registry, repoData, inst, inst.VersionCriteria(), + registry.InstalledModules.Select(im => im.identifier) + .ToHashSet(), + config.HideEpochs, config.HideV); + + private IEnumerable GetGUIMods(IRegistryQuerier registry, + RepositoryDataManager repoData, + GameInstance inst, + GameVersionCriteria versionCriteria, + HashSet installedIdents, + bool hideEpochs, + bool hideV) + => registry.CheckUpgradeable(versionCriteria, + ModuleLabels.HeldIdentifiers(inst) + .ToHashSet()) + .SelectMany(kvp => kvp.Value + .Where(mod => !registry.IsAutodetected(mod.identifier)) + .Select(mod => new GUIMod(registry.InstalledModule(mod.identifier), + repoData, registry, + versionCriteria, null, + hideEpochs, hideV) + { + HasUpdate = kvp.Key, + })) + .Concat(registry.CompatibleModules(versionCriteria) + .Where(m => !installedIdents.Contains(m.identifier)) + .AsParallel() + .Where(m => !m.IsDLC) + .Select(m => new GUIMod(m, repoData, registry, + versionCriteria, null, + hideEpochs, hideV))) + .Concat(registry.IncompatibleModules(versionCriteria) + .Where(m => !installedIdents.Contains(m.identifier)) + .AsParallel() + .Where(m => !m.IsDLC) + .Select(m => new GUIMod(m, repoData, registry, + versionCriteria, true, + hideEpochs, hideV))); + private static readonly ILog log = LogManager.GetLogger(typeof(ModList)); } } diff --git a/Tests/CmdLine/UpgradeTests.cs b/Tests/CmdLine/UpgradeTests.cs index a0da76a277..7e5b8b2926 100644 --- a/Tests/CmdLine/UpgradeTests.cs +++ b/Tests/CmdLine/UpgradeTests.cs @@ -100,5 +100,538 @@ public void RunCommand_IdentifierEqualsVersionSyntax_UpgradesToCorrectVersion( }); } } + + [Test, + TestCase("No mods, do nothing without crashing", + new string[] { }, + new string[] { }, + new string[] { }, + new string[] { }), + TestCase("No mods, do nothing (--all) without crashing", + new string[] { }, + new string[] { }, + null, + new string[] { }), + TestCase("Enforce version requirements of identifier=version specified mod", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.2.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { "Depender=1.1.0", "Dependency" }, + new string[] { "1.1.0", "1.1.0" }), + // Installed and latest version of the depender has a version specific depends, + // the current installed dependency is old, and we upgrade to an intermediate version + // instead of the absolute latest + // (lamont-granquist's use case with identifiers) + TestCase("Should upgrade-with-identifiers to intermediate version when installed dependency blocks latest", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { "Dependency" }, + new string[] { "1.0.0", "1.1.0" }), + // Installed and latest version of the depender has a version specific depends, + // the current installed dependency is old, and we upgrade to an intermediate version + // instead of the absolute latest + // (lamont-granquist's use case with --all) + TestCase("Should upgrade-all to intermediate version when installed dependency blocks latest", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.1.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.2.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + null, + new string[] { "1.0.0", "1.1.0" }), + // Depender stops any upgrades at all (with identifiers) + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Depender stops any upgrades-with-identifiers at all", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { "Dependency" }, + new string[] { "1.0.0", "1.0.0" }), + // Depender stops any upgrades at all (--all) + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Depender stops any upgrades-all at all", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + null, + new string[] { "1.0.0", "1.0.0" }), + // Depender blocks latest dependency, but the latest available depender + // doesn't have that limitation, and we upgrade both to latest + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Upgrade-with-identifiers both to bypass current version limitation", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + new string[] { "Depender", "Dependency" }, + new string[] { "1.1.0", "1.1.0" }), + // Depender blocks latest dependency, but the latest available depender + // doesn't have that limitation, and we upgrade both to latest + // (could be broken by naive fix for lamont-granquist's use case) + TestCase("Upgrade-all both to bypass current version limitation", + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""depends"": [ + { + ""name"": ""Dependency"", + ""max_version"": ""1.0.0"" + } + ], + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.0.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData"" + } + ] + }", + }, + new string[] { + @"{ + ""spec_version"": 1, + ""identifier"": ""Depender"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Depender"" + } + ] + }", + @"{ + ""spec_version"": 1, + ""identifier"": ""Dependency"", + ""version"": ""1.1.0"", + ""download"": ""https://github.com/"", + ""install"": [ + { + ""find"": ""DogeCoinFlag"", + ""install_to"": ""GameData/Dependency"" + } + ] + }", + }, + null, + new string[] { "1.1.0", "1.1.0" }), + ] + public void RunCommand_VersionDependsUpgrade_UpgradesCorrectly(string description, + string[] instModules, + string[] addlModules, + string[] upgradeIdentifiers, + string[] versionsAfter) + { + // Arrange + var user = new CapturingUser(false, q => true, (msg, objs) => 0); + using (var inst = new DisposableKSP()) + using (var repo = new TemporaryRepository(instModules.Concat(addlModules) + .ToArray())) + using (var repoData = new TemporaryRepositoryData(user, repo.repo)) + using (var config = new FakeConfiguration(inst.KSP, inst.KSP.Name)) + using (var manager = new GameInstanceManager(user, config) + { + CurrentInstance = inst.KSP, + }) + { + var regMgr = RegistryManager.Instance(inst.KSP, repoData.Manager); + regMgr.registry.RepositoriesClear(); + regMgr.registry.RepositoriesAdd(repo.repo); + + // Register installed mods + var instMods = instModules.Select(CkanModule.FromJson) + .ToArray(); + foreach (var fromModule in instMods) + { + regMgr.registry.RegisterModule(fromModule, + Enumerable.Empty(), + inst.KSP, false); + } + // Pre-store mods that might be installed + foreach (var toModule in addlModules.Select(CkanModule.FromJson)) + { + manager.Cache.Store(toModule, TestData.DogeCoinFlagZip(), null); + } + // Simulate passing `--all` + var opts = upgradeIdentifiers != null + ? new UpgradeOptions() + { + modules = upgradeIdentifiers.ToList(), + } + : new UpgradeOptions() + { + modules = new List() {}, + upgrade_all = true, + }; + + // Act + ICommand cmd = new Upgrade(manager, repoData.Manager, user); + cmd.RunCommand(inst.KSP, opts); + + // Assert + CollectionAssert.AreEqual(versionsAfter, + instMods.Select(m => regMgr.registry + .GetInstalledVersion(m.identifier) + .version + .ToString()) + .ToArray(), + description); + + } + } } } diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index c012840627..56a68c1170 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -268,7 +268,7 @@ public void HasUpdate_WithUpgradeableManuallyInstalledMod_ReturnsTrue() GameVersionCriteria crit = new GameVersionCriteria(mod.ksp_version); // Act - bool has = registry.HasUpdate(mod.identifier, crit); + bool has = registry.HasUpdate(mod.identifier, crit, out _); // Assert Assert.IsTrue(has, "Can't upgrade manually installed DLL"); @@ -321,7 +321,10 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() GameVersionCriteria crit = new GameVersionCriteria(olderDepMod.ksp_version); // Act - bool has = registry.HasUpdate(olderDepMod.identifier, crit); + bool has = registry.HasUpdate(olderDepMod.identifier, crit, out _, + registry.InstalledModules + .Select(im => im.Module) + .ToList()); // Assert Assert.IsFalse(has, "Upgrade allowed that would break another mod's dependency"); diff --git a/Tests/GUI/Model/GUIMod.cs b/Tests/GUI/Model/GUIMod.cs index 28aae0ef26..e0d961e412 100644 --- a/Tests/GUI/Model/GUIMod.cs +++ b/Tests/GUI/Model/GUIMod.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; #if NET5_0_OR_GREATER using System.Runtime.Versioning; @@ -44,7 +45,7 @@ public void NewGuiModsAreNotSelectedForUpgrade() } [Test] - public void HasUpdateReturnsTrueWhenUpdateAvailable() + public void HasUpdate_UpdateAvailable_ReturnsTrue() { var user = new NullUser(); using (var tidy = new DisposableKSP()) @@ -63,9 +64,14 @@ public void HasUpdateReturnsTrueWhenUpdateAvailable() var registry = new Registry(repoData.Manager, repo.repo); registry.RegisterModule(old_version, Enumerable.Empty(), null, false); + var upgradeableGroups = registry.CheckUpgradeable(tidy.KSP.VersionCriteria(), + new HashSet()); var mod = new GUIMod(old_version, repoData.Manager, registry, tidy.KSP.VersionCriteria(), - null, false, false); + null, false, false) + { + HasUpdate = upgradeableGroups[true].Any(m => m.identifier == old_version.identifier), + }; Assert.True(mod.HasUpdate); } }