Skip to content

Commit

Permalink
Tests: Use TestDub FSEntry constructor for more accurate tests
Browse files Browse the repository at this point in the history
The previous approach added Packages in the PackageManager directly,
completely sidestepping the scanning the PackageManager does,
which is a behavior we absolutely want to test.

By setting up the filesystem before we instantiate our `TestDub` instance,
we can remove a bunch of exceptions / overrides, and get a much closer
behavior to the final product.
  • Loading branch information
Geod24 committed Feb 15, 2024
1 parent 5dbdd3e commit 323eef4
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 149 deletions.
190 changes: 104 additions & 86 deletions source/dub/test/base.d
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,31 @@ import dub.recipe.io : parsePackageRecipe;
/// Example of a simple unittest for a project with a single dependency
unittest
{
// `a` will be loaded as the project while `b` will be loaded
// as a simple package. The recipe files can be in JSON or SDL format,
// here we use both to demonstrate this.
const a = `{ "name": "a", "dependencies": { "b": "~>1.0" } }`;
const b = `name "b"`;

// Enabling this would provide some more verbose output, which makes
// debugging a failing unittest much easier.
version (none) {
enableLogging();
scope(exit) disableLogging();
}

scope dub = new TestDub();
// Let the `PackageManager` know about the `b` package
dub.addTestPackage("b", Version("1.0.0"), b, PackageFormat.sdl);
// And about our main package
auto mainPackage = dub.addTestPackage("a", Version("1.0.0"), a);
// Initialization is best done as a delegate passed to `TestDub` constructor,
// which receives an `FSEntry` representing the root of the filesystem.
// Various low-level functions are exposed (mkdir, writeFile, ...),
// as well as higher-level functions (`writePackageFile`).
scope dub = new TestDub((scope FSEntry root) {
// `a` will be loaded as the project while `b` will be loaded
// as a simple package. The recipe files can be in JSON or SDL format,
// here we use both to demonstrate this.
root.writeFile(TestDub.ProjectPath ~ "dub.json",
`{ "name": "a", "dependencies": { "b": "~>1.0" } }`);
// Note that you currently need to add the `version` to the package
root.writePackageFile("b", "1.0.0", `name "b"
version "1.0.0"`, PackageFormat.sdl);
});

// `Dub.loadPackage` will set this package as the project
// While not required, it follows the common Dub use case.
dub.loadPackage(mainPackage);
dub.loadPackage();
// This triggers the dependency resolution process that happens
// when one does not have a selection file in the project.
// Dub will resolve dependencies and generate the selection file
Expand Down Expand Up @@ -231,46 +235,6 @@ public class TestDub : Dub
{
return cast(inout(TestPackageManager)) this.m_packageManager;
}

/**
* 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)
{
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 name, Version vers, string content,
PackageFormat fmt = PackageFormat.json)
{
return this.packageManager.add(PackageName(name), vers, content, fmt);
}
}

/**
Expand Down Expand Up @@ -436,40 +400,6 @@ package class TestPackageManager : PackageManager
return false;
}

/**
* 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.
*/
public Package add(in PackageName pkg, in Version vers, string content,
PackageFormat fmt, PlacementLocation loc = PlacementLocation.user)
{
import dub.recipe.io : serializePackageRecipe;

auto path = this.getPackagePath(loc, pkg, vers.toString());
this.fs.mkdir(path);

final switch (fmt) {
case PackageFormat.json:
path ~= "dub.json";
break;
case PackageFormat.sdl:
path ~= "dub.sdl";
break;
}

auto recipe = parsePackageRecipe(content, path.toNativeString());
recipe.version_ = vers.toString();
auto app = appender!string();
serializePackageRecipe(app, recipe, path.toNativeString());
this.fs.writeFile(path, app.data());

this.refresh();
return this.getPackage(pkg, vers, loc);
}

/// Add a reachable SCM package to this `PackageManager`
public void addTestSCMPackage(in Repository repo, string dub_json)
{
Expand Down Expand Up @@ -645,6 +575,94 @@ public class FSEntry
return null;
}

/*+*************************************************************************
Utility function
Below this banners are functions that are provided for the convenience
of writing tests for `Dub`.
***************************************************************************/

/// Prints a visual representation of the filesystem to stdout for debugging
public void print(bool content = false)
{
import std.range : repeat;
static import std.stdio;

size_t indent;
for (FSEntry p = this.parent; p !is null; p = p.parent)
indent++;
// Don't print anything (even a newline) for root
if (this.parent is null)
std.stdio.write('/');
else
std.stdio.write('|', '-'.repeat(indent), ' ', this.name, ' ');

final switch (this.type) {
case Type.Directory:
std.stdio.writeln('(', this.children.length, " entries):");
foreach (c; this.children)
c.print(content);
break;
case Type.File:
if (!content)
std.stdio.writeln('(', this.content.length, " bytes)");
else if (this.name.endsWith(".json") || this.name.endsWith(".sdl"))
std.stdio.writeln('(', this.content.length, " bytes): ",
cast(string) this.content);
else
std.stdio.writeln('(', this.content.length, " bytes): ",
this.content);
break;
}
}

/// Writes a package file for package `name` of version `vers` at `loc`.
public void writePackageFile (in string name, in string vers, in string recipe,
in PackageFormat fmt = PackageFormat.json,
in PlacementLocation location = PlacementLocation.user)
{
const path = FSEntry.getPackagePath(name, vers, location);
this.mkdir(path).writeFile(
NativePath(fmt == PackageFormat.json ? "dub.json" : "dub.sdl"),
recipe);
}

/// Returns: The final destination a specific package needs to be stored in
public static NativePath getPackagePath(in string name_, string vers,
PlacementLocation location = PlacementLocation.user)
{
PackageName name = PackageName(name_);
// Keep in sync with `dub.packagemanager: PackageManager.getPackagePath`
// and `Location.getPackagePath`
NativePath result (in NativePath base)
{
NativePath res = base ~ name.main.toString() ~ vers ~
name.main.toString();
res.endsWithSlash = true;
return res;
}

final switch (location) {
case PlacementLocation.user:
return result(TestDub.Paths.userSettings ~ "packages/");
case PlacementLocation.system:
return result(TestDub.Paths.systemSettings ~ "packages/");
case PlacementLocation.local:
return result(TestDub.ProjectPath ~ "/.dub/packages/");
}
}

/*+*************************************************************************
Public filesystem functions
Below this banners are functions which mimic the behavior of a file
system.
***************************************************************************/

/// Returns: The `path` of this FSEntry
public NativePath path() const
{
Expand Down
88 changes: 43 additions & 45 deletions source/dub/test/dependencies.d
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,18 @@ import dub.test.base;
// Ensure that simple dependencies get resolved correctly
unittest
{
const a = `name "a"
scope dub = new TestDub((scope FSEntry root) {
root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
version "1.0.0"
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"), c, PackageFormat.sdl);
dub.addTestPackage(`b`, Version("1.0.0"), b, PackageFormat.sdl);
dub.loadPackage(dub.addTestPackage(`a`, Version("1.0.0"), a, PackageFormat.sdl));
`);
root.writePackageFile("b", "1.0.0", `name "b"
version "1.0.0"`, PackageFormat.sdl);
root.writePackageFile("c", "1.0.0", `name "c"
version "1.0.0"`, PackageFormat.sdl);
});
dub.loadPackage();

dub.upgrade(UpgradeOptions.select);

Expand All @@ -54,18 +55,16 @@ dependency "c" version="*"
// 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"), c, PackageFormat.sdl);
dub.addTestPackage(`b`, Version("1.0.0"), b, PackageFormat.sdl);
dub.loadPackage(dub.addTestPackage(`a`, Version("1.0.0"), a, PackageFormat.sdl));
scope dub = new TestDub((scope FSEntry root) {
root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
dependency "b" version="*"`);
root.writePackageFile("b", "1.0.0", `name "b"
version "1.0.0"
dependency "c" version="*"`, PackageFormat.sdl);
root.writePackageFile("c", "1.0.0", `name "c"
version "1.0.0"`, PackageFormat.sdl);
});
dub.loadPackage();

dub.upgrade(UpgradeOptions.select);

Expand All @@ -78,23 +77,21 @@ dependency "c" version="*"
// Simple diamond dependency
unittest
{
const a = `name "a"
scope dub = new TestDub((scope FSEntry root) {
root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `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"), d, PackageFormat.sdl);
dub.addTestPackage(`c`, Version("1.0.0"), c, PackageFormat.sdl);
dub.addTestPackage(`b`, Version("1.0.0"), b, PackageFormat.sdl);
dub.loadPackage(dub.addTestPackage(`a`, Version("1.0.0"), a, PackageFormat.sdl));
dependency "c" version="*"`);
root.writePackageFile("b", "1.0.0", `name "b"
version "1.0.0"
dependency "d" version="*"`, PackageFormat.sdl);
root.writePackageFile("c", "1.0.0", `name "c"
version "1.0.0"
dependency "d" version="*"`, PackageFormat.sdl);
root.writePackageFile("d", "1.0.0", `name "d"
version "1.0.0"`, PackageFormat.sdl);

});
dub.loadPackage();

dub.upgrade(UpgradeOptions.select);

Expand All @@ -108,24 +105,25 @@ dependency "d" version="*"
// 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"), a, PackageFormat.sdl));
scope dub = new TestDub((scope FSEntry root) {
root.writeFile(TestDub.ProjectPath ~ "dub.sdl", `name "a"
dependency "b" version="*"`);
});
dub.loadPackage();

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(exc.message() == `Failed to find any versions for package b, referenced by a ~master`);

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(`b`, Version("1.0.0"), `name "b"`, PackageFormat.sdl);
dub.fs.writePackageFile(`b`, "1.0.0", `name "b"
version "1.0.0"`, PackageFormat.sdl);
dub.packageManager.refresh();
dub.upgrade(UpgradeOptions.select);
assert(dub.project.hasAllDependencies(), "project have missing dependencies");
assert(dub.project.getDependency("b", true), "Missing 'b' dependency");
Expand Down
25 changes: 13 additions & 12 deletions source/dub/test/other.d
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@ unittest
const ValidURL = `git+https://example.com/dlang/dub`;
// Taken from a commit in the dub repository
const ValidHash = "54339dff7ce9ec24eda550f8055354f712f15800";
const Template = `{"name": "%s", "dependencies": {
const Template = `{"name": "%s", "version": "1.0.0", "dependencies": {
"dep1": { "repository": "%s", "version": "%s" }}}`;

scope dub = new TestDub();
scope dub = new TestDub((scope FSEntry fs) {
// Invalid URL, valid hash
fs.writePackageFile("a", "1.0.0", Template.format("a", "git+https://nope.nope", ValidHash));
// Valid URL, invalid hash
fs.writePackageFile("b", "1.0.0", Template.format("b", ValidURL, "invalid"));
// Valid URL, valid hash
fs.writePackageFile("c", "1.0.0", Template.format("c", ValidURL, ValidHash));
});
dub.packageManager.addTestSCMPackage(
Repository(ValidURL, ValidHash), `{ "name": "dep1" }`);

// Invalid URL, valid hash
const a = Template.format("a", "git+https://nope.nope", ValidHash);
try
dub.loadPackage(dub.addTestPackage(`a`, Version("1.0.0"), a));
dub.loadPackage(dub.packageManager.getPackage(PackageName("a"), Version("1.0.0")));
catch (Exception exc)
assert(exc.message.canFind("Unable to fetch"));
assert(exc.message.canFind("Unable to fetch"));

// Valid URL, invalid hash
const b = Template.format("b", ValidURL, "invalid");
try
dub.loadPackage(dub.addTestPackage(`b`, Version("1.0.0"), b));
dub.loadPackage(dub.packageManager.getPackage(PackageName("b"), Version("1.0.0")));
catch (Exception exc)
assert(exc.message.canFind("Unable to fetch"));

// Valid URL, valid hash
const c = Template.format("c", ValidURL, ValidHash);
dub.loadPackage(dub.addTestPackage(`c`, Version("1.0.0"), c));
dub.loadPackage(dub.packageManager.getPackage(PackageName("c"), Version("1.0.0")));
assert(dub.project.hasAllDependencies());
assert(dub.project.getDependency("dep1", true), "Missing 'dep1' dependency");
}
Loading

0 comments on commit 323eef4

Please sign in to comment.