diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index 8b10cf998..5e05208d6 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -188,7 +188,7 @@ class PackageManager { * Returns: * A `Package` if one was found, `null` if none exists. */ - private Package lookup (string name, Version vers) { + protected Package lookup (string name, Version vers) { if (!this.m_initialized) this.refresh(); @@ -243,6 +243,21 @@ class PackageManager { return this.lookup(name, ver); } + /** + * Adds a `Package` to this `PackageManager` + * + * This is currently only available in unittests as it is a convenience + * function used by `TestDub`, but could be generalized once IO has been + * abstracted away from this class. + */ + version (unittest) Package add(Package pkg) + { + // See `PackageManager.addPackages` for inspiration. + assert(!pkg.subPackages.length, "Subpackages are not yet supported"); + this.m_internal.fromPath ~= pkg; + return pkg; + } + /// ditto deprecated("Use the overload that accepts a `Version` as second argument") Package getPackage(string name, string ver, bool enable_overrides = true) @@ -1121,7 +1136,7 @@ private enum LocalOverridesFilename = "local-overrides.json"; * Additionally, each location host a config file, * which is not managed by this module, but by dub itself. */ -private struct Location { +package struct Location { /// The absolute path to the root of the location NativePath packagePath; @@ -1364,7 +1379,7 @@ private struct Location { * Returns: * A `Package` if one was found, `null` if none exists. */ - private inout(Package) lookup(string name, Version ver, PackageManager mgr) inout { + inout(Package) lookup(string name, Version ver, PackageManager mgr) inout { foreach (pkg; this.localPackages) if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.standard)) return pkg; @@ -1391,7 +1406,7 @@ private struct Location { * Returns: * A `Package` if one was found, `null` if none exists. */ - private Package load (string name, Version vers, PackageManager mgr) + Package load (string name, Version vers, PackageManager mgr) { if (auto pkg = this.lookup(name, vers, mgr)) return pkg; @@ -1423,7 +1438,7 @@ private struct Location { * but this function returns `$BASE/$NAME-$VERSION/` * `$BASE` is `this.packagePath`. */ - private NativePath getPackagePath (string name, string vers) + NativePath getPackagePath (string name, string vers) { NativePath result = this.packagePath ~ name ~ vers; result.endsWithSlash = true; diff --git a/source/dub/project.d b/source/dub/project.d index 76cb2e627..875ad6f16 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -1734,8 +1734,8 @@ unittest This is the runtime representation of the information contained in "dub.selections.json" within a package's directory. */ -final class SelectedVersions { - private { +public class SelectedVersions { + protected { enum FileVersion = 1; Selected m_selections; bool m_dirty = false; // has changes since last save diff --git a/source/dub/test/base.d b/source/dub/test/base.d index 4e498762a..efc6478a8 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -13,10 +13,11 @@ public import std.algorithm; import dub.data.settings; public import dub.dependency; -import dub.dub; -import dub.package_; +public import dub.dub; +public import dub.package_; import dub.packagemanager; import dub.packagesuppliers.packagesupplier; +import dub.project; // TODO: Remove and handle logging the same way we handle other IO import dub.internal.logging; @@ -54,11 +55,163 @@ public class TestDub : Dub return Settings.init; } + /// + protected override PackageManager makePackageManager() const + { + return new TestPackageManager(); + } + /// See `MockPackageSupplier` documentation for this class' implementation protected override PackageSupplier makePackageSupplier(string url) const { return new MockPackageSupplier(url); } + + /// Loads the package from the specified path as the main project package. + public override void loadPackage(NativePath path) + { + assert(0, "Not implemented"); + } + + /// Loads a specific package as the main project package (can be a sub package) + public override void loadPackage(Package pack) + { + m_project = new Project(m_packageManager, pack, new TestSelectedVersions()); + } + + /// Reintroduce parent overloads + public alias loadPackage = Dub.loadPackage; + + /** + * Creates a package with the provided recipe + * + * This is a convenience function provided to create a package based on + * a given recipe. This is to allow test-cases to be written based off + * issues more easily. + * + * In order for the `Package` to be visible to `Dub`, use `addTestPackage`, + * as `makeTestPackage` simply creates the `Package` without adding it. + * + * Params: + * str = The string representation of the `PackageRecipe` + * recipe = The `PackageRecipe` to use + * vers = The version the package is at, e.g. `Version("1.0.0")` + * fmt = The format `str` is in, either JSON or SDL + * + * Returns: + * The created `Package` instance + */ + public Package makeTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) + { + import dub.recipe.io; + final switch (fmt) { + case PackageFormat.json: + auto recipe = parsePackageRecipe(str, "dub.json"); + recipe.version_ = vers.toString(); + return new Package(recipe); + case PackageFormat.sdl: + auto recipe = parsePackageRecipe(str, "dub.sdl"); + recipe.version_ = vers.toString(); + return new Package(recipe); + } + } + + /// Ditto + public Package addTestPackage(string str, Version vers, PackageFormat fmt = PackageFormat.json) + { + return this.packageManager.add(this.makeTestPackage(str, vers, fmt)); + } +} + +/** + * + */ +public class TestSelectedVersions : SelectedVersions { + import dub.recipe.selection; + + /// Forward to parent's constructor + public this(uint version_ = FileVersion) @safe pure nothrow @nogc + { + super(version_); + } + + /// Ditto + public this(Selected data) @safe pure nothrow @nogc + { + super(data); + } + + /// Do not do IO + public override void save(NativePath path) + { + // No-op + } +} + +/** + * A `PackageManager` suitable to be used in unittests + * + * This `PackageManager` does not perform any IO. It imitates the base + * `PackageManager`, exposing 3 locations, but loading of packages is not + * automatic and needs to be done by passing a `Package` instance. + */ +package class TestPackageManager : PackageManager +{ + this() + { + NativePath pkg = NativePath("/tmp/dub-testsuite-nonexistant/packages/"); + NativePath user = NativePath("/tmp/dub-testsuite-nonexistant/user/"); + NativePath system = NativePath("/tmp/dub-testsuite-nonexistant/system/"); + super(pkg, user, system, false); + } + + /// Disabled as semantic are not implementable unless a virtual FS is created + public override @property void customCachePaths(NativePath[] custom_cache_paths) + { + assert(0, "Function not implemented"); + } + + /// Ditto + public override Package store(NativePath src, PlacementLocation dest, string name, Version vers) + { + assert(0, "Function not implemented"); + } + + /** + * This function usually scans the filesystem for packages. + * + * We don't want to do IO access and rely on users adding the packages + * before the test starts instead. + * + * Note: Deprecated `refresh(bool)` does IO, but it's deprecated + */ + public override void refresh() + { + // Do nothing + } + + /** + * Looks up a specific package + * + * Unlike its parent class, no lazy loading is performed. + * Additionally, as they are already deprecated, overrides are + * disabled and not available. + */ + public override Package getPackage(string name, Version vers, bool enable_overrides = false) + { + //assert(!enable_overrides, "Overrides are not implemented for TestPackageManager"); + + // Implementation inspired from `PackageManager.lookup`, + // except we replaced `load` with `lookup`. + if (auto pkg = this.m_internal.lookup(name, vers, this)) + return pkg; + + foreach (ref location; this.m_repositories) + if (auto p = location.lookup(name, vers, this)) + return p; + + return null; + } } /** diff --git a/source/dub/test/dependencies.d b/source/dub/test/dependencies.d new file mode 100644 index 000000000..d9f78ed65 --- /dev/null +++ b/source/dub/test/dependencies.d @@ -0,0 +1,133 @@ +/******************************************************************************* + + Test for dependencies + + This module is mostly concerned with dependency resolutions and visible user + behavior. Tests that check how different recipe would interact with one + another, and how conflicts are resolved or reported, belong here. + + The project (the loaded package) is usually named 'a' and dependencies use + single-letter, increasing name, for simplicity. Version 1.0.0 is used where + versions do not matter. Packages are usually created in reverse dependency + order when possible, unless the creation order matters. + + Test that deal with dependency resolution should not concern themselves with + the registry: instead, packages are added to the `PackageManager`, as that + makes testing the core logic more robust without adding a layer + of complexity brought by the `PackageSupplier`. + + Most tests have 3 parts: First, setup the various packages. Then, run + `dub.upgrade(UpgradeOptions.select)` to create the selection. Finally, + run tests on the resulting state. + +*******************************************************************************/ + +module dub.test.dependencies; + +version (unittest): + +import dub.test.base; + +// Ensure that simple dependencies get resolved correctly +unittest +{ + const a = `name "a" +dependency "b" version="*" +dependency "c" version="*" +`; + const b = `name "b"`; + const c = `name "c"`; + + scope dub = new TestDub(); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Test that indirect dependencies get resolved correctly +unittest +{ + const a = `name "a" +dependency "b" version="*" +`; + const b = `name "b" +dependency "c" version="*" +`; + const c = `name "c"`; + + scope dub = new TestDub(); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Simple diamond dependency +unittest +{ + const a = `name "a" +dependency "b" version="*" +dependency "c" version="*" +`; + const b = `name "b" +dependency "d" version="*" +`; + const c = `name "c" +dependency "d" version="*" +`; + const d = `name "d"`; + + scope dub = new TestDub(); + dub.addTestPackage(d, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(c, Version("1.0.0"), PackageFormat.sdl); + dub.addTestPackage(b, Version("1.0.0"), PackageFormat.sdl); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + dub.upgrade(UpgradeOptions.select); + + assert(dub.project.hasAllDependencies(), "project has missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'c' dependency"); + assert(dub.project.getDependency("c", true), "Missing 'd' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +} + +// Missing dependencies trigger an error +unittest +{ + const a = `name "a" +dependency "b" version="*" +`; + + scope dub = new TestDub(); + dub.loadPackage(dub.addTestPackage(a, Version("1.0.0"), PackageFormat.sdl)); + + try + dub.upgrade(UpgradeOptions.select); + catch (Exception exc) + assert(exc.message() == `Failed to find any versions for package b, referenced by a 1.0.0`); + + assert(!dub.project.hasAllDependencies(), "project should have missing dependencies"); + assert(dub.project.getDependency("b", true) is null, "Found 'b' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); + + // Add the missing dependency to our PackageManager + dub.addTestPackage(`name "b"`, Version("1.0.0"), PackageFormat.sdl); + dub.upgrade(UpgradeOptions.select); + assert(dub.project.hasAllDependencies(), "project have missing dependencies"); + assert(dub.project.getDependency("b", true), "Missing 'b' dependency"); + assert(dub.project.getDependency("no", true) is null, "Returned unexpected dependency"); +}