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 @@
+