diff --git a/CKAN/CKAN/CKAN.csproj b/CKAN/CKAN/CKAN.csproj index 531fffdf64..4baa51908c 100644 --- a/CKAN/CKAN/CKAN.csproj +++ b/CKAN/CKAN/CKAN.csproj @@ -75,9 +75,13 @@ + + + + diff --git a/CKAN/CKAN/Module.cs b/CKAN/CKAN/Module.cs index ddff0c62b5..8384869f8b 100644 --- a/CKAN/CKAN/Module.cs +++ b/CKAN/CKAN/Module.cs @@ -137,8 +137,9 @@ public class Module [JsonProperty("author")] [JsonConverter(typeof (JsonSingleOrArrayConverter))] public List author; [JsonProperty("comment")] public string comment; - [JsonProperty("conflicts")] public RelationshipDescriptor[] conflicts; - [JsonProperty("depends")] public RelationshipDescriptor[] depends; + + [JsonProperty("conflicts")] public List conflicts; + [JsonProperty("depends")] public List depends; [JsonProperty("download")] public Uri download; [JsonProperty("download_size")] public long download_size; @@ -152,17 +153,37 @@ public class Module [JsonProperty("name")] public string name; + // TODO: Deprecate? [JsonProperty("pre_depends")] public RelationshipDescriptor[] pre_depends; - [JsonProperty("provides")] public string[] provides; + [JsonProperty("provides")] public List provides; - [JsonProperty("recommends")] public RelationshipDescriptor[] recommends; + [JsonProperty("recommends")] public List recommends; [JsonProperty("release_status")] public string release_status; // TODO: Strong type [JsonProperty("resources")] public ResourcesDescriptor resources; - [JsonProperty("suggests")] public RelationshipDescriptor[] suggests; + [JsonProperty("suggests")] public List suggests; [JsonProperty("version", Required = Required.Always)] public Version version; + // A list of eveything this mod provides. + public List ProvidesList + { + // TODO: Consider caching this, but not in a way that the serialiser will try and + // serialise it. + get + { + var provides = new List(); + provides.Add(this.identifier); + + if (this.provides != null) + { + provides.AddRange(this.provides); + } + + return provides; + } + } + public string serialise() { return JsonConvert.SerializeObject(this); @@ -242,6 +263,14 @@ public bool IsCompatibleKSP(KSPVersion version) return ksp_version.Targets(version); } + + /// + /// Returns true if this module provides the functionality requested. + /// + public bool DoesProvide(string identifier) + { + return this.identifier == identifier || this.provides.Contains(identifier); + } } public class CkanInvalidMetadataJson : Exception diff --git a/CKAN/CKAN/ModuleInstaller.cs b/CKAN/CKAN/ModuleInstaller.cs index 6c6620a3da..0441a8ed1b 100644 --- a/CKAN/CKAN/ModuleInstaller.cs +++ b/CKAN/CKAN/ModuleInstaller.cs @@ -189,11 +189,8 @@ public NetAsyncDownloader DownloadAsync(List modules) } /// - /// Installs all modules given a list of identifiers. Resolves dependencies. - /// The function initializes a filesystem transaction, then installs all cached mods - /// this ensures we don't waste time and bandwidth if there is an issue with any of the cached archives. - /// After this we try to download the rest of the mods (asynchronously) and install them. - /// Finally, only if everything is successful, we commit the transaction. + /// Installs all modules given a list of identifiers as a transaction. Resolves dependencies. + /// This *will* save the registry at the end of operation. /// // // TODO: Break this up into smaller pieces! It's huge! @@ -273,11 +270,12 @@ public void InstallList(List modules, RelationshipResolverOptions option Install(modsToInstall[i]); } + registry_manager.Save(); transaction.Complete(); return; } - transaction.Dispose(); // Rollback + transaction.Dispose(); // Rollback on unsuccessful download. } } @@ -352,7 +350,8 @@ public List GetModuleContentsList(CkanModule module) /// Install our mod from the filename supplied. /// If no file is supplied, we will check the cache or download it. /// Does *not* resolve dependencies; this actually does the heavy listing. - /// Use InstallList() for requests from the user. + /// Does *not* save the registry. + /// Do *not* call this directly, use InstallList() instead. /// /// // @@ -362,8 +361,6 @@ internal void Install(CkanModule module, string filename = null) { using (var transaction = new TransactionScope()) { - - User.WriteLine(module.identifier + ":\n"); Version version = registry_manager.registry.InstalledVersion(module.identifier); @@ -393,11 +390,10 @@ internal void Install(CkanModule module, string filename = null) // Register our files. registry.RegisterModule(new InstalledModule(module_files, module, DateTime.Now)); - // Done! Save our registry changes! + // Finish our transaction, but *don't* save the registry; we may be in an + // intermediate, inconsistent state. // This is fine from a transaction standpoint, as we may not have an enclosing // transaction, and if we do, they can always roll us back. - registry_manager.Save(); - transaction.Complete(); } @@ -733,43 +729,58 @@ internal static void CopyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPa return; } - public List FindReverseDependencies(string modName) + /// + /// Uninstalls all the mods provided, including things which depend upon them. + /// This *DOES* save the registry. + /// Preferred over Uninstall. + /// + public void UninstallList(IEnumerable mods) { - var reverseDependencies = new List(); - - // loop through all installed modules - foreach (var keyValue in registry_manager.registry.installed_modules) + using (var transaction = new TransactionScope()) { - Module mod = keyValue.Value.source_module; - bool isDependency = false; + // Find all the things which need uninstalling. + IEnumerable goners = registry_manager.registry.FindReverseDependencies(mods); + + User.WriteLine("About to remove:\n"); - if (mod.depends != null) + foreach (string mod in goners) { - foreach (RelationshipDescriptor dependency in mod.depends) - { - if (dependency.name == modName) - { - isDependency = true; - break; - } - } + User.WriteLine(" * {0}", mod); + } + + bool ok = User.YesNo("\nContinue?", FrontEndType.CommandLine); + + if (!ok) + { + User.WriteLine("Mod removal aborted at user request."); + return; } - if (isDependency) + foreach (string mod in goners) { - reverseDependencies.Add(mod.identifier); + Uninstall(mod); } + + registry_manager.Save(); + + transaction.Complete(); } + } - return reverseDependencies; + public void UninstallList(string mod) + { + var list = new List(); + list.Add(mod); + UninstallList(list); } /// - /// Uninstall the module provided. + /// Uninstall the module provided. For internal use only. + /// Use UninstallList for user queries, it also does dependency handling. + /// This does *NOT* save the registry. /// - // TODO: Remove second arg; shouldn't we *always* uninstall dependencies? - public void Uninstall(string modName, bool uninstallDependencies) + private void Uninstall(string modName) { using (var transaction = new TransactionScope()) { @@ -783,16 +794,6 @@ public void Uninstall(string modName, bool uninstallDependencies) return; } - // Find all mods that depend on this one - if (uninstallDependencies) - { - List reverseDependencies = FindReverseDependencies(modName); - foreach (string reverseDependency in reverseDependencies) - { - Uninstall(reverseDependency, uninstallDependencies); - } - } - // Walk our registry to find all files for this mod. Dictionary files = registry_manager.registry.installed_modules[modName].installed_files; @@ -819,7 +820,7 @@ public void Uninstall(string modName, bool uninstallDependencies) } catch (Exception ex) { - // TODO: Report why. + // XXX: This is terrible, we're catching all exceptions. log.ErrorFormat("Failure in locating file {0} : {1}", path, ex.Message); } } @@ -827,27 +828,42 @@ public void Uninstall(string modName, bool uninstallDependencies) // Remove from registry. registry_manager.registry.DeregisterModule(modName); - registry_manager.Save(); + // TODO: We need to remove from child to parent first. foreach (string directory in directoriesToDelete) { if (!Directory.GetFiles(directory).Any()) { try { - file_transaction.Delete(directory); + file_transaction.DeleteDirectory(directory); } - catch (Exception) + catch (Exception ex) { // TODO: Report why. - User.WriteLine("Couldn't delete directory {0}", directory); + User.WriteLine("Couldn't delete directory {0} : {1}", directory, ex.Message); } } + else + { + User.WriteLine("Not removing directory {0}, it's not empty", directory); + } } transaction.Complete(); } return; } + + /// + /// Don't use this. Use Registry.FindReverseDependencies instead. + /// This method may be deprecated in the future. + /// + // Here for now to keep the GUI happy. + public HashSet FindReverseDependencies(string module) + { + return registry_manager.registry.FindReverseDependencies(module); + } + } } diff --git a/CKAN/CKAN/NetAsyncDownloader.cs b/CKAN/CKAN/NetAsyncDownloader.cs index 90f773a24a..97c697c9a4 100644 --- a/CKAN/CKAN/NetAsyncDownloader.cs +++ b/CKAN/CKAN/NetAsyncDownloader.cs @@ -190,14 +190,10 @@ private void FileDownloadComplete(int index, Exception error) { log.Debug("All files finished downloading"); - // verify no errors before commit - - bool err = false; for (int i = 0; i < downloads.Length; i++) { if (downloads[i].error != null) { - err = true; // TODO: XXX: Shouldn't we throw a kraken here? log.Error("Something went wrong but I don't know what!"); } diff --git a/CKAN/CKAN/Registry/Registry.cs b/CKAN/CKAN/Registry/Registry.cs index 14783f22bd..f11d776bfa 100644 --- a/CKAN/CKAN/Registry/Registry.cs +++ b/CKAN/CKAN/Registry/Registry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using System.Linq; using log4net; namespace CKAN @@ -22,7 +23,7 @@ public class Registry // Is that something you can do in C#? In Moose we'd use a role. public Dictionary available_modules; - public Dictionary installed_dlls; + public Dictionary installed_dlls; // name => path public Dictionary installed_modules; public int registry_version; @@ -188,6 +189,7 @@ public List Incompatible(KSPVersion ksp_version = null) /// satisifes the specified version. /// Throws a ModuleNotFoundException if asked for a non-existant module. /// Returns null if there's simply no compatible version for this system. + /// If no ksp_version is provided, the latest module for *any* KSP is returned. /// // TODO: Consider making this internal, because practically everything should @@ -246,7 +248,7 @@ public List LatestAvailableWithProvides(string module, KSPVersion ks continue; } - string[] provides = pair.Value.Latest(ksp_version).provides; + List provides = pair.Value.Latest(ksp_version).provides; if (provides != null) { foreach (string provided in provides) @@ -427,5 +429,73 @@ public bool IsInstalled(string modName) } return true; } + + /// + /// Checks the sanity of the registry, to ensure that all dependencies are met, + /// and no mods conflict with each other. Throws an InconsistentKraken on failure. + /// + public void CheckSanity() + { + IEnumerable installed = from pair in installed_modules select pair.Value.source_module; + SanityChecker.EnforceConsistency(installed, installed_dlls.Keys); + } + + /// + /// Finds and returns all modules that could not exist without the listed modules installed, including themselves. + /// Acts recursively. + /// + + public static HashSet FindReverseDependencies(IEnumerable modules_to_remove, IEnumerable orig_installed, IEnumerable dlls) + { + // Make our hypothetical install, and remove the listed modules from it. + HashSet hypothetical = new HashSet (orig_installed); // Clone because we alter hypothetical. + hypothetical.RemoveWhere(mod => modules_to_remove.Contains(mod.identifier)); + + log.DebugFormat( "Started with {0}, removing {1}, and keeping {2}; our dlls are {3}", + string.Join(", ", orig_installed), + string.Join(", ", modules_to_remove), + string.Join(", ", hypothetical), + string.Join(", ", dlls) + ); + + // Find what would break with this configuration. + // The Values.SelectMany() flattens our list of broken mods. + var broken = new HashSet ( + SanityChecker + .FindUnmetDependencies(hypothetical, dlls) + .Values + .SelectMany(x => x) + .Select(x => x.identifier) + ); + + // If nothing else would break, it's just the list of modules we're removing. + HashSet to_remove = new HashSet(modules_to_remove); + if (to_remove.IsSupersetOf(broken)) + { + log.DebugFormat("{0} is a superset of {1}, work done", string.Join(", ", to_remove), string.Join(", ", broken)); + return to_remove; + } + + // Otherwise, remove our broken modules as well, and recurse. + broken.UnionWith(to_remove); + return FindReverseDependencies(broken, orig_installed, dlls); + } + + public HashSet FindReverseDependencies(IEnumerable modules_to_remove) + { + var installed = new HashSet(installed_modules.Values.Select(x => x.source_module)); + return FindReverseDependencies(modules_to_remove, installed, new HashSet(installed_dlls.Keys)); + } + + /// + /// Finds and returns all modules that could not exist without the given module installed + /// + public HashSet FindReverseDependencies(string module) + { + var set = new HashSet(); + set.Add(module); + return FindReverseDependencies(set); + } + } } \ No newline at end of file diff --git a/CKAN/CKAN/Registry/RegistryManager.cs b/CKAN/CKAN/Registry/RegistryManager.cs index 0116040d7b..789e8f50a0 100644 --- a/CKAN/CKAN/Registry/RegistryManager.cs +++ b/CKAN/CKAN/Registry/RegistryManager.cs @@ -23,6 +23,17 @@ private RegistryManager(string path) { this.path = Path.Combine(path, "registry.json"); LoadOrCreate(); + + // We don't cause an inconsistency error to stop the registry from being loaded, + // because then the user can't do anything to correct it. However we're + // sure as hell going to complain if we spot one! + try { + registry.CheckSanity(); + } + catch (InconsistentKraken kraken) + { + log.ErrorFormat("Loaded registry with inconsistencies:\n\n{0}", kraken.InconsistenciesPretty); + } } /// @@ -90,11 +101,16 @@ public string Serialize() public void Save() { log.DebugFormat("Saving CKAN registry at {0}", path); + + // No saving the registry unless it's in a sane state. + registry.CheckSanity(); + string directoryPath = Path.GetDirectoryName(path); if (directoryPath == null) { log.DebugFormat("Failed to save registry, invalid path: {0}", path); + // TODO: Throw a friggin exception! } if (!Directory.Exists(directoryPath)) diff --git a/CKAN/CKAN/RelationshipResolver.cs b/CKAN/CKAN/RelationshipResolver.cs index cdd24c8fdb..85625bd500 100644 --- a/CKAN/CKAN/RelationshipResolver.cs +++ b/CKAN/CKAN/RelationshipResolver.cs @@ -120,7 +120,7 @@ private void Resolve(CkanModule module, RelationshipResolverOptions options) /// /// Throws a TooManyModsProvideKraken if we have too many choices. /// - private void ResolveStanza(RelationshipDescriptor[] stanza, RelationshipResolverOptions options) + private void ResolveStanza(List stanza, RelationshipResolverOptions options) { if (stanza == null) { diff --git a/CKAN/CKAN/Relationships/SanityChecker.cs b/CKAN/CKAN/Relationships/SanityChecker.cs new file mode 100644 index 0000000000..41226bc25d --- /dev/null +++ b/CKAN/CKAN/Relationships/SanityChecker.cs @@ -0,0 +1,193 @@ +using System.Linq; +using System.Collections.Generic; +using log4net; + +namespace CKAN +{ + /// + /// Sanity checks on what mods we have installed, or may install. + /// + public static class SanityChecker + { + + private static readonly ILog log = LogManager.GetLogger(typeof(SanityChecker)); + + /// + /// Checks the list of modules for consistency errors, returning a list of + /// errors found. The list will be empty if everything is fine. + /// + public static List ConsistencyErrors(IEnumerable modules, IEnumerable dlls) + { + var errors = new List(); + + // If we have no modules, then everything is fine. DLLs can't depend or conflict on things. + if (modules == null) + { + return errors; + } + + Dictionary> providers = ModulesToProvides(modules, dlls); + + foreach (KeyValuePair> entry in FindUnmetDependencies(modules, dlls)) + { + foreach (Module unhappy_mod in entry.Value) + { + errors.Add(string.Format("{0} requires {1} but nothing provides it", unhappy_mod.identifier, entry.Key)); + } + } + + // Conflicts are more difficult. Mods are allowed to conflict with themselves. + // So we walk all our mod conflicts, find what (if anything) provide those + // conflicts, and return false if it's not the module we're examining. + + // TODO: This doesn't examine versions. We should! + // TODO: It would be great to factor this into its own function, too. + + foreach (Module mod in modules) + { + // If our mod doesn't conflict with anything, skip it. + if (mod.conflicts == null) + { + continue; + } + + foreach (var conflict in mod.conflicts) + { + // If nothing conflicts with us, skip. + if (! providers.ContainsKey(conflict.name)) + { + continue; + } + + // If something does conflict with us, and it's not ourselves, that's a fail. + foreach (string provider in providers[conflict.name]) + { + if (provider != mod.identifier) + { + errors.Add(string.Format("{0} conflicts with {1}.", mod.identifier, provider)); + } + } + } + } + + // Return whatever we've found, which could be empty. + return errors; + } + + /// + /// Ensures all modules in the list provided can co-exist. + /// Throws a InconsistentKraken containing a list of inconsistences if they do not. + /// Does nothing if the modules can happily co-exist. + /// + public static void EnforceConsistency(IEnumerable modules, IEnumerable dlls = null) + { + List errors = ConsistencyErrors(modules, dlls); + + if (errors.Count != 0) + { + throw new InconsistentKraken(errors); + } + } + + /// + /// Returns true if the mods supplied can co-exist. This checks depends/pre-depends/conflicts only. + /// + public static bool IsConsistent(IEnumerable modules, IEnumerable dlls = null) + { + return ConsistencyErrors(modules, dlls).Count == 0; + } + + /// + /// Maps a list of modules and dlls to a dictionary of what's provided, and a list of + /// identifiers that supply each. + /// + // + // Eg: { + // LifeSupport => [ "TACLS", "Snacks" ] + // DogeCoinFlag => [ "DogeCoinFlag" ] + // } + public static Dictionary> ModulesToProvides(IEnumerable modules, IEnumerable dlls = null) + { + var providers = new Dictionary>(); + + if (dlls == null) + { + dlls = new List(); + } + + foreach (Module mod in modules) + { + foreach (string provides in mod.ProvidesList) + { + log.DebugFormat("{0} provides {1}", mod, provides); + providers[provides] = providers.ContainsKey(provides) ? providers[provides] : new List(); + providers[provides].Add(mod.identifier); + } + } + + // Add in our DLLs as things we know exist. + foreach (string dll in dlls) + { + if (! providers.ContainsKey(dll)) + { + providers[dll] = new List(); + } + providers[dll].Add(dll); + } + + return providers; + } + + /// + /// Given a list of modules and optional dlls, returns a dictionary of dependencies which are still not met. + /// The dictionary keys are the un-met depdendencies, the values are the modules requesting them. + /// + public static Dictionary> FindUnmetDependencies(IEnumerable modules, IEnumerable dlls = null) + { + return FindUnmetDependencies(modules, ModulesToProvides(modules, dlls)); + } + + /// + /// Given a list of modules, and a dictionary of providers, returns a dictionary of depdendencies which have not been met. + /// + internal static Dictionary> FindUnmetDependencies(IEnumerable modules, Dictionary> provided) + { + return FindUnmetDependencies(modules, new HashSet (provided.Keys)); + } + + /// + /// Given a list of modules, and a set of providers, returns a dictionary of dependencies which have not been met. + /// + internal static Dictionary> FindUnmetDependencies(IEnumerable modules, HashSet provided) + { + var unmet = new Dictionary> (); + + // TODO: This doesn't examine versions, it should! + + foreach (Module mod in modules) + { + // If this module has no dependencies, we're done. + if (mod.depends == null) + { + continue; + } + + // If it does have dependencies, but we can't find anything that provides them, + // add them to our unmet list. + foreach (RelationshipDescriptor dep in mod.depends) + { + if (! provided.Contains(dep.name)) + { + if (!unmet.ContainsKey(dep.name)) + { + unmet[dep.name] = new List(); + } + unmet[dep.name].Add(mod); // mod needs dep.name, but doesn't have it. + } + } + } + + return unmet; + } + } +} \ No newline at end of file diff --git a/CKAN/CKAN/Types/Kraken.cs b/CKAN/CKAN/Types/Kraken.cs index 1f553386d2..ba70a9e129 100644 --- a/CKAN/CKAN/Types/Kraken.cs +++ b/CKAN/CKAN/Types/Kraken.cs @@ -135,4 +135,37 @@ internal static string FormatMessage(string requested, List modules) } } + /// + /// Thrown if we find ourselves in an inconsistent state, such as when we have multiple modules + /// installed which conflict with each other. + /// + public class InconsistentKraken : Kraken + { + public List inconsistencies; + + public string InconsistenciesPretty + { + get { + string message = "The following inconsistecies were found:\n\n"; + + foreach (string issue in inconsistencies) + { + message += " * " + issue + "\n"; + } + + return message; + } + } + + public InconsistentKraken(List inconsistencies, Exception inner_exception = null) + :base(null, inner_exception) + { + this.inconsistencies = inconsistencies; + } + + public override string ToString() + { + return this.InconsistenciesPretty + this.StackTrace; + } + } } diff --git a/CKAN/CmdLine/Main.cs b/CKAN/CmdLine/Main.cs index 435ac2d2f8..da411687d1 100644 --- a/CKAN/CmdLine/Main.cs +++ b/CKAN/CmdLine/Main.cs @@ -230,7 +230,7 @@ private static int List() private static int Remove(RemoveOptions options) { var installer = ModuleInstaller.Instance; - installer.Uninstall(options.Modname, true); + installer.UninstallList(options.Modname); return Exit.OK; } @@ -258,7 +258,7 @@ private static int Upgrade(UpgradeOptions options) foreach (string module in options.modules) { - installer.Uninstall(module, false); + installer.UninstallList(module); } // Prepare options. Can these all be done in the new() somehow? diff --git a/CKAN/GUI/Main.cs b/CKAN/GUI/Main.cs index 2b63351eed..1f4e556b0c 100644 --- a/CKAN/GUI/Main.cs +++ b/CKAN/GUI/Main.cs @@ -251,7 +251,7 @@ private void ModList_CellContentClick(object sender, DataGridViewCellEventArgs e else if ((bool) cell.Value && isInstalled) { var installer = ModuleInstaller.Instance; - List reverseDependencies = installer.FindReverseDependencies(mod.identifier); + HashSet reverseDependencies = installer.FindReverseDependencies(mod.identifier); foreach (string dependency in reverseDependencies) { foreach (DataGridViewRow depRow in ModList.Rows) diff --git a/CKAN/GUI/MainInstall.cs b/CKAN/GUI/MainInstall.cs index 49d13e0825..dd6ac5ffb6 100644 --- a/CKAN/GUI/MainInstall.cs +++ b/CKAN/GUI/MainInstall.cs @@ -37,11 +37,12 @@ private void InstallMods(object sender, DoWorkEventArgs e) // this probably need if (change.Value == GUIModChangeType.Remove) { m_WaitDialog.SetDescription(String.Format("Uninstalling mod \"{0}\"", change.Key.name)); - installer.Uninstall(change.Key.identifier, true); + installer.UninstallList(change.Key.identifier); } else if (change.Value == GUIModChangeType.Update) { - installer.Uninstall(change.Key.identifier, false); + // TODO: Proper upgrades when ckan.dll supports them. + installer.UninstallList(change.Key.identifier); } } diff --git a/CKAN/GUI/MainModInfo.cs b/CKAN/GUI/MainModInfo.cs index 8ff3c21bf9..171ac78442 100644 --- a/CKAN/GUI/MainModInfo.cs +++ b/CKAN/GUI/MainModInfo.cs @@ -92,7 +92,7 @@ private TreeNode UpdateModDependencyGraphRecursively(TreeNode parentNode, CkanMo node = parentNode.Nodes.Add(module.name); } - RelationshipDescriptor[] relationships = null; + IEnumerable relationships = null; switch (relationship) { case RelationshipType.Depends: diff --git a/CKAN/GUI/MainModList.cs b/CKAN/GUI/MainModList.cs index 5fd0671007..d465d44f6f 100644 --- a/CKAN/GUI/MainModList.cs +++ b/CKAN/GUI/MainModList.cs @@ -65,11 +65,9 @@ private List> ComputeChangeSetFromMod ModuleInstaller installer = ModuleInstaller.Instance; - var reverseDependencies = new List(); - foreach (string moduleName in modulesToRemove) { - reverseDependencies = installer.FindReverseDependencies(moduleName); + var reverseDependencies = installer.FindReverseDependencies(moduleName); foreach (string reverseDependency in reverseDependencies) { CkanModule mod = registry.available_modules[reverseDependency].Latest(); diff --git a/CKAN/Tests/CKAN/SanityChecker.cs b/CKAN/Tests/CKAN/SanityChecker.cs new file mode 100644 index 0000000000..358bce445d --- /dev/null +++ b/CKAN/Tests/CKAN/SanityChecker.cs @@ -0,0 +1,181 @@ +using NUnit.Framework; +using System; +using CKAN; +using System.Collections.Generic; +using System.Linq; +using Tests; + +// We're exercising FindReverseDependencies in here, because: +// - We need a registry +// - It calls the sanity checker code to do the heavy lifting. + +namespace CKANTests +{ + [TestFixture()] + public class SanityChecker + { + private CKAN.Registry registry; + + [TestFixtureSetUp] + public void Setup() + { + registry = CKAN.Registry.Empty(); + CKAN.Repo.UpdateRegistry(TestData.TestKAN(), registry); + } + + [Test] + public void Empty() + { + var list = new List(); + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(list)); + } + + [Test] + public void Void() + { + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(null)); + } + + [Test] + public void DogeCoin() + { + // Test with a module that depends and conflicts with nothing. + var mods = new List(); + mods.Add(registry.LatestAvailable("DogeCoinFlag")); + + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods), "DogeCoinFlag"); + } + + [Test] + public void CustomBiomes() + { + var mods = new List(); + + mods.Add(registry.LatestAvailable("CustomBiomes")); + Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods), "CustomBiomes without data"); + + mods.Add(registry.LatestAvailable("CustomBiomesKerbal")); + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods), "CustomBiomes with stock data"); + + mods.Add(registry.LatestAvailable("CustomBiomesRSS")); + Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods), "CustomBiomes with conflicting data"); + } + + [Test] + public void CustomBiomesWithDlls() + { + var mods = new List(); + var dlls = new List(); + + dlls.Add("CustomBiomes"); + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls), "CustomBiomes dll by itself"); + + // This would actually be a terrible thing for users to have, but it tests the + // relationship we want. + mods.Add(registry.LatestAvailable("CustomBiomesKerbal")); + Assert.IsTrue(CKAN.SanityChecker.IsConsistent(mods, dlls), "CustomBiomes DLL, with config added"); + + mods.Add(registry.LatestAvailable("CustomBiomesRSS")); + Assert.IsFalse(CKAN.SanityChecker.IsConsistent(mods, dlls), "CustomBiomes with conflicting data"); + } + + [Test] + public void ModulesToProvides() + { + var mods = new List(); + mods.Add(registry.LatestAvailable("CustomBiomes")); + mods.Add(registry.LatestAvailable("CustomBiomesKerbal")); + mods.Add(registry.LatestAvailable("DogeCoinFlag")); + + var provides = CKAN.SanityChecker.ModulesToProvides(mods); + Assert.Contains("CustomBiomes", provides.Keys); + Assert.Contains("CustomBiomesData", provides.Keys); + Assert.Contains("CustomBiomesKerbal", provides.Keys); + Assert.Contains("DogeCoinFlag", provides.Keys); + Assert.AreEqual(4, provides.Keys.Count); + } + + [Test] + public void FindUnmetDependencies() + { + var mods = new List(); + var dlls = new List(); + Assert.IsEmpty(CKAN.SanityChecker.FindUnmetDependencies(mods, dlls), "Empty list"); + + mods.Add(registry.LatestAvailable("DogeCoinFlag")); + Assert.IsEmpty(CKAN.SanityChecker.FindUnmetDependencies(mods, dlls), "DogeCoinFlag"); + + mods.Add(registry.LatestAvailable("CustomBiomes")); + Assert.Contains("CustomBiomesData", CKAN.SanityChecker.FindUnmetDependencies(mods, dlls).Keys, "Missing CustomBiomesData"); + + mods.Add(registry.LatestAvailable("CustomBiomesKerbal")); + Assert.IsEmpty(CKAN.SanityChecker.FindUnmetDependencies(mods, dlls), "CBD+CBK"); + + mods.RemoveAll(x => x.identifier == "CustomBiomes"); + Assert.AreEqual(2, mods.Count, "Checking removed CustomBiomes"); + + Assert.Contains("CustomBiomes", CKAN.SanityChecker.FindUnmetDependencies(mods, dlls).Keys, "Missing CustomBiomes"); + } + + [Test] + public void ReverseDepends() + { + var mods = new List(); + mods.Add(registry.LatestAvailable("CustomBiomes")); + mods.Add(registry.LatestAvailable("CustomBiomesKerbal")); + mods.Add(registry.LatestAvailable("DogeCoinFlag")); + + // Make sure some of our expectations regarding dependencies are correct. + Assert.Contains("CustomBiomes", registry.LatestAvailable("CustomBiomesKerbal").depends.Select(x => x.name).ToList()); + Assert.Contains("CustomBiomesData", registry.LatestAvailable("CustomBiomes").depends.Select(x => x.name).ToList()); + + // Removing DCF should only remove itself. + var to_remove = new List(); + to_remove.Add("DogeCoinFlag"); + TestDepends(to_remove, mods, null, to_remove, "DogeCoin Removal"); + + // Removing CB should remove its data, and vice-versa. + to_remove.Clear(); + to_remove.Add("CustomBiomes"); + var expected = new List(); + expected.Add("CustomBiomes"); + expected.Add("CustomBiomesKerbal"); + TestDepends(to_remove, mods, null, expected, "CustomBiomes removed"); + + // We expect the same result removing CBK + to_remove.Clear(); + to_remove.Add("CustomBiomesKerbal"); + TestDepends(to_remove, mods, null, expected, "CustomBiomesKerbal removed"); + + // And we expect the same result if we try to remove both. + to_remove.Add("CustomBiomes"); + TestDepends(to_remove, mods, null, expected, "CustomBiomesKerbal and data removed"); + + // Finally, if we try to remove nothing, we shold get back the empty set. + expected.Clear(); + to_remove.Clear(); + TestDepends(to_remove, mods, null, expected, "Removing nothing"); + + } + + private void TestDepends(List to_remove, List mods, List dlls, List expected, string message) + { + dlls = dlls ?? new List(); + + var remove_count = to_remove.Count; + var dll_count = dlls.Count; + var mods_count = mods.Count; + + var results = CKAN.Registry.FindReverseDependencies(to_remove, mods, dlls); + + // Make sure nothing changed. + Assert.AreEqual(remove_count, to_remove.Count, message + " remove count"); + Assert.AreEqual(dll_count, dlls.Count, message + " dll count"); + Assert.AreEqual(mods_count, mods.Count, message + " mods count"); + + // Check our actual results. + CollectionAssert.AreEquivalent(expected, results, message); + } + } +} + diff --git a/CKAN/Tests/Tests.csproj b/CKAN/Tests/Tests.csproj index 4de62f0043..5db8c35ecf 100644 --- a/CKAN/Tests/Tests.csproj +++ b/CKAN/Tests/Tests.csproj @@ -57,6 +57,7 @@ +