diff --git a/source/dub/dub.d b/source/dub/dub.d index 3faf351..2b42fb7 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -408,7 +408,7 @@ * Returns: * A new instance of a `PackageSupplier`. */ - protected PackageSupplier makePackageSupplier(string url) const + protected PackageSupplier makePackageSupplier(string url) { switch (url.startsWith("dub+", "mvn+", "file://")) { diff --git a/source/dub/test/base.d b/source/dub/test/base.d index 77cef67..f58ca7c 100644 --- a/source/dub/test/base.d +++ b/source/dub/test/base.d @@ -127,6 +127,18 @@ /// Now actually upgrade dependencies in memory dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade); assert(dub.project.getDependency("b", true).version_ == Version("1.2.0")); + + /// Adding a package to the registry require the version and at list a recipe + dub.getRegistry().add(Version("1.3.0"), (scope FSEntry pkg) { + // This is required + pkg.writeFile(NativePath(`dub.sdl`), `name "b"`); + // Any other files can be present, as a normal package + pkg.mkdir(NativePath("source/b/")).writeFile( + NativePath("main.d"), "module b.main; void main() {}"); + }); + // Fetch the package from the registry + dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade); + assert(dub.project.getDependency("b", true).version_ == Version("1.3.0")); } // TODO: Remove and handle logging the same way we handle other IO @@ -164,6 +176,16 @@ /// The virtual filesystem that this instance acts on public FSEntry fs; + /** + * Redundant reference to the registry + * + * We currently create 2 `MockPackageSupplier`s hidden behind a + * `FallbackPackageSupplier` (see base implementation). + * The fallback is never used, and we need to provide the user + * a mean to access the registry so they can add packages to it. + */ + protected MockPackageSupplier registry; + /// Convenience constants for use in unittests version (Windows) public static immutable Root = NativePath("T:\\dub\\"); @@ -277,9 +299,12 @@ } /// See `MockPackageSupplier` documentation for this class' implementation - protected override PackageSupplier makePackageSupplier(string url) const + protected override PackageSupplier makePackageSupplier(string url) { - return new MockPackageSupplier(url); + auto r = new MockPackageSupplier(url); + if (this.registry is null) + this.registry = r; + return r; } /** @@ -292,6 +317,20 @@ { return cast(inout(TestPackageManager)) this.m_packageManager; } + + /** + * Returns a fully-typed `MockPackageSupplier` + * + * This exposes the first (and usually sole) `PackageSupplier` if typed + * as `MockPackageSupplier` so that client can call convenience functions + * on it directly. + */ + public @property inout(MockPackageSupplier) getRegistry() inout + { + // This will not work with `SkipPackageSupplier`. + assert(this.registry !is null, "The registry hasn't been instantiated?"); + return this.registry; + } } /** @@ -465,8 +504,16 @@ */ public class MockPackageSupplier : PackageSupplier { - /// Mapping of package name to packages, ordered by `Version` - protected Package[Version][PackageName] pkgs; + /// Internal duplication to avoid having to deserialize the zip content + private struct PkgData { + /// + PackageRecipe recipe; + /// + ubyte[] data; + } + + /// Mapping of package name to package zip data, ordered by `Version` + protected PkgData[Version][PackageName] pkgs; /// URL this was instantiated with protected string url; @@ -477,6 +524,48 @@ this.url = url; } + /** + * Adds a package to this `PackageSupplier` + * + * The registry API bakes in Zip files / binary data. + * When adding a package here, just provide an `FSEntry` + * representing the package directory, which will be converted + * to ZipFile / `ubyte[]` and returned by `fetchPackage`. + * + * This use a delegate approach similar to `TestDub` constructor: + * a delegate must be provided to initialize the package content. + * The delegate will be called once and is expected to contain, + * at its root, the package. + * + * The name of the package will be defined from the recipe file. + * It's version, however, must be provided as parameter. + * + * Params: + * vers = The `Version` of this package. + * dg = A delegate that will populate its parameter with the + * content of the package. + */ + public void add (in Version vers, scope void delegate(scope FSEntry root) dg) + { + scope pkgRoot = new FSEntry(); + dg(pkgRoot); + + scope recipe = pkgRoot.lookup("dub.json"); + if (recipe is null) recipe = pkgRoot.lookup("dub.sdl"); + if (recipe is null) recipe = pkgRoot.lookup("package.json"); + // Note: If you want to provide an invalid package, override + // [Mock]PackageSupplier. Most tests will expect a well-behaving + // registry so this assert is here to help with writing tests. + assert(recipe !is null, + "No package recipe found: Expected dub.json or dub.sdl"); + auto pkgRecipe = parsePackageRecipe( + pkgRoot.readText(NativePath(recipe.name)), recipe.name); + pkgRecipe.version_ = vers.toString(); + const name = PackageName(pkgRecipe.name); + this.pkgs[name][vers] = PkgData( + pkgRecipe, pkgRoot.serializeToZip("%s-%s/".format(name, vers))); + } + /// public override @property string description() { @@ -495,8 +584,7 @@ public override ubyte[] fetchPackage(in PackageName name, in VersionRange dep, bool pre_release) { - assert(0, "%s - fetchPackage not implemented for: %s" - .format(this.url, name.main)); + return this.getBestMatch(name, dep, pre_release).data; } /// @@ -505,14 +593,30 @@ { import dub.recipe.json; - Package match; - if (auto ppkgs = name.main in this.pkgs) - foreach (vers, pkg; *ppkgs) - if ((!vers.isPreRelease || pre_release) && - dep.matches(vers) && - (match is null || match.version_ < vers)) - match = pkg; - return match is null ? Json.init : toJson(match.recipe); + auto match = this.getBestMatch(name, dep, pre_release); + if (!match.data.length) + return Json.init; + auto res = toJson(match.recipe); + return res; + } + + /// + protected PkgData getBestMatch ( + in PackageName name, in VersionRange dep, bool pre_release) + { + auto ppkgs = name.main in this.pkgs; + if (ppkgs is null) + return typeof(return).init; + + PkgData match; + foreach (vers, pr; *ppkgs) + if ((!vers.isPreRelease || pre_release) && + dep.matches(vers) && + (!match.data.length || Version(match.recipe.version_) < vers)) { + match.recipe = pr.recipe; + match.data = pr.data; + } + return match; } /// @@ -912,3 +1016,36 @@ NativePath(fmt == PackageFormat.json ? "dub.json" : "dub.sdl"), recipe); } + +/** + * Converts an `FSEntry` and its children to a `ZipFile` + */ +public ubyte[] serializeToZip (scope FSEntry root, string rootPath) { + import std.path; + import std.zip; + + scope z = new ZipArchive(); + void addToZip(scope string dir, scope FSEntry e) { + auto m = new ArchiveMember(); + m.name = dir.buildPath(e.name); + m.fileAttributes = e.attributes.attrs; + m.time = e.attributes.modification; + + final switch (e.attributes.type) { + case FSEntry.Type.Directory: + // We need to ensure the directory entry ends with a slash + // otherwise it will be considered as a file. + if (m.name[$-1] != '/') + m.name ~= '/'; + z.addMember(m); + foreach (c; e.children) + addToZip(m.name, c); + break; + case FSEntry.Type.File: + m.expandedData = e.content; + z.addMember(m); + } + } + addToZip(rootPath, root); + return cast(ubyte[]) z.build(); +}