Skip to content

Tests: Mock IO in the Dub class too #2941

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 50 additions & 38 deletions source/dub/dub.d
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import dub.compilers.compiler;
import dub.data.settings : SPS = SkipPackageSuppliers, Settings;
import dub.dependency;
import dub.dependencyresolver;
import dub.internal.io.realfs;
import dub.internal.utils;
import dub.internal.vibecompat.core.file;
import dub.internal.vibecompat.data.json;
import dub.internal.vibecompat.inet.url;
import dub.internal.logging;
Expand All @@ -29,7 +29,7 @@ import std.array : array, replace;
import std.conv : text, to;
import std.encoding : sanitize;
import std.exception : enforce;
import std.file;
import std.file : tempDir, thisExePath;
import std.process : environment;
import std.range : assumeSorted, empty;
import std.string;
Expand Down Expand Up @@ -119,6 +119,7 @@ deprecated unittest
*/
class Dub {
protected {
Filesystem fs;
bool m_dryRun = false;
PackageManager m_packageManager;
PackageSupplier[] m_packageSuppliers;
Expand Down Expand Up @@ -156,8 +157,15 @@ class Dub {
this(string root_path = ".", PackageSupplier[] base = null,
SkipPackageSuppliers skip = SkipPackageSuppliers.none)
{
this(new RealFS(), root_path, base, skip);
}

package this (Filesystem fs, string root_path, PackageSupplier[] base = null,
SkipPackageSuppliers skip = SkipPackageSuppliers.none)
{
this.fs = fs;
m_rootPath = NativePath(root_path);
if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath;
if (!m_rootPath.absolute) m_rootPath = fs.getcwd() ~ m_rootPath;

init();

Expand Down Expand Up @@ -202,9 +210,10 @@ class Dub {
{
// Note: We're doing `init()` before setting the `rootPath`,
// to prevent `init` from reading the project's settings.
this.fs = new RealFS();
init();
this.m_rootPath = root;
m_packageManager = new PackageManager(pkg_root);
m_packageManager = new PackageManager(pkg_root, this.fs);
}

deprecated("Use the overload that takes `(NativePath pkg_root, NativePath root)`")
Expand All @@ -223,12 +232,15 @@ class Dub {
*/
protected PackageManager makePackageManager()
{
return new PackageManager(m_rootPath, m_dirs.userPackages, m_dirs.systemSettings, false);
const local = this.m_rootPath ~ ".dub/packages/";
const user = m_dirs.userPackages ~ "packages/";
const system = m_dirs.systemSettings ~ "packages/";
return new PackageManager(this.fs, local, user, system);
}

protected void init()
{
this.m_dirs = SpecialDirs.make();
this.m_dirs = SpecialDirs.make(this.fs);
this.m_config = this.loadConfig(this.m_dirs);
this.m_defaultCompiler = this.determineDefaultCompiler();
}
Expand All @@ -254,13 +266,13 @@ class Dub {
{
import dub.internal.configy.easy;

static void readSettingsFile (NativePath path_, ref Settings current)
static void readSettingsFile (in Filesystem fs, NativePath path_, ref Settings current)
{
// TODO: Remove `StrictMode.Warn` after v1.40 release
// The default is to error, but as the previous parser wasn't
// complaining, we should first warn the user.
const path = path_.toNativeString();
if (path.exists) {
if (fs.existsFile(path_)) {
auto newConf = parseConfigFileSimple!Settings(path, StrictMode.Warn);
if (!newConf.isNull())
current = current.merge(newConf.get());
Expand Down Expand Up @@ -290,11 +302,11 @@ class Dub {
}
}

readSettingsFile(dirs.systemSettings ~ "settings.json", result);
readSettingsFile(dubFolderPath ~ "../etc/dub/settings.json", result);
readSettingsFile(this.fs, dirs.systemSettings ~ "settings.json", result);
readSettingsFile(this.fs, dubFolderPath ~ "../etc/dub/settings.json", result);
version (Posix) {
if (dubFolderPath.absolute && dubFolderPath.startsWith(NativePath("usr")))
readSettingsFile(NativePath("/etc/dub/settings.json"), result);
readSettingsFile(this.fs, NativePath("/etc/dub/settings.json"), result);
}

// Override user + local package path from system / binary settings
Expand All @@ -308,11 +320,11 @@ class Dub {
}

// load user config:
readSettingsFile(dirs.userSettings ~ "settings.json", result);
readSettingsFile(this.fs, dirs.userSettings ~ "settings.json", result);

// load per-package config:
if (!this.m_rootPath.empty)
readSettingsFile(this.m_rootPath ~ "dub.settings.json", result);
readSettingsFile(this.fs, this.m_rootPath ~ "dub.settings.json", result);

// same as userSettings above, but taking into account the
// config loaded from user settings and per-package config as well.
Expand Down Expand Up @@ -445,7 +457,7 @@ class Dub {
@property void rootPath(NativePath root_path)
{
m_rootPath = root_path;
if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath;
if (!m_rootPath.absolute) m_rootPath = this.fs.getcwd() ~ m_rootPath;
}

/// Returns the name listed in the dub.json of the current
Expand Down Expand Up @@ -560,12 +572,11 @@ class Dub {
void loadSingleFilePackage(NativePath path)
{
import dub.recipe.io : parsePackageRecipe;
import std.file : readText;
import std.path : baseName, stripExtension;

path = makeAbsolute(path);

string file_content = readText(path.toNativeString());
string file_content = this.fs.readText(path);

if (file_content.startsWith("#!")) {
auto idx = file_content.indexOf('\n');
Expand Down Expand Up @@ -834,9 +845,9 @@ class Dub {
}
}

string configFilePath = (m_project.rootPackage.path ~ "dscanner.ini").toNativeString();
if (!args.canFind("--config") && exists(configFilePath)) {
settings.runArgs ~= ["--config", configFilePath];
const configFilePath = (m_project.rootPackage.path ~ "dscanner.ini");
if (!args.canFind("--config") && this.fs.existsFile(configFilePath)) {
settings.runArgs ~= ["--config", configFilePath.toNativeString()];
}

settings.runArgs ~= args ~ [m_project.rootPackage.path.toNativeString()];
Expand Down Expand Up @@ -887,8 +898,7 @@ class Dub {
const cache = this.m_dirs.cache;
logInfo("Cleaning", Color.green, "all artifacts at %s",
cache.toNativeString().color(Mode.bold));
if (existsFile(cache))
rmdirRecurse(cache.toNativeString());
this.fs.removeDir(cache, true);
}

/// Ditto
Expand All @@ -898,10 +908,8 @@ class Dub {
logInfo("Cleaning", Color.green, "artifacts for package %s at %s",
pack.name.color(Mode.bold),
cache.toNativeString().color(Mode.bold));

// TODO: clear target files and copy files
if (existsFile(cache))
rmdirRecurse(cache.toNativeString());
this.fs.removeDir(cache, true);
}

deprecated("Use the overload that accepts either a `Version` or a `VersionRange` as second argument")
Expand Down Expand Up @@ -1501,7 +1509,7 @@ class Dub {
}

writePackageRecipe(srcfile.parentPath ~ ("dub."~destination_file_ext), m_project.rootPackage.rawRecipe);
removeFile(srcfile);
this.fs.removeFile(srcfile);
}

/** Runs DDOX to generate or serve documentation.
Expand Down Expand Up @@ -1631,7 +1639,6 @@ class Dub {
*/
protected string determineDefaultCompiler() const
{
import std.file : thisExePath;
import std.path : buildPath, dirName, expandTilde, isAbsolute, isDirSeparator;
import std.range : front;

Expand Down Expand Up @@ -1661,23 +1668,24 @@ class Dub {
if (result.length)
{
string compilerPath = buildPath(thisExePath().dirName(), result ~ exe);
if (existsFile(compilerPath))
if (this.fs.existsFile(NativePath(compilerPath)))
return compilerPath;
}
else
{
auto nextFound = compilers.find!(bin => existsFile(buildPath(thisExePath().dirName(), bin ~ exe)));
auto nextFound = compilers.find!(
bin => this.fs.existsFile(NativePath(buildPath(thisExePath().dirName(), bin ~ exe))));
if (!nextFound.empty)
return buildPath(thisExePath().dirName(), nextFound.front ~ exe);
}

// If nothing found next to dub, search the user's PATH, starting
// with the compiler name from their DUB config file, if specified.
auto paths = environment.get("PATH", "").splitter(sep).map!NativePath;
if (result.length && paths.canFind!(p => existsFile(p ~ (result ~ exe))))
if (result.length && paths.canFind!(p => this.fs.existsFile(p ~ (result ~ exe))))
return result;
foreach (p; paths) {
auto res = compilers.find!(bin => existsFile(p ~ (bin~exe)));
auto res = compilers.find!(bin => this.fs.existsFile(p ~ (bin~exe)));
if (!res.empty)
return res.front;
}
Expand All @@ -1691,7 +1699,7 @@ class Dub {
import dub.test.base : TestDub;

auto dub = new TestDub(null, ".", null, SkipPackageSuppliers.configured);
immutable testdir = getWorkingDirectory() ~ "test-determineDefaultCompiler";
immutable testdir = dub.fs.getcwd() ~ "test-determineDefaultCompiler";

immutable olddc = environment.get("DC", null);
immutable oldpath = environment.get("PATH", null);
Expand All @@ -1704,19 +1712,18 @@ class Dub {
}
scope (exit) repairenv("DC", olddc);
scope (exit) repairenv("PATH", oldpath);
scope (exit) std.file.rmdirRecurse(testdir.toNativeString());

version (Windows) enum sep = ";", exe = ".exe";
version (Posix) enum sep = ":", exe = "";

immutable dmdpath = testdir ~ "dmd" ~ "bin";
immutable ldcpath = testdir ~ "ldc" ~ "bin";
ensureDirectory(dmdpath);
ensureDirectory(ldcpath);
dub.fs.mkdir(dmdpath);
dub.fs.mkdir(ldcpath);
immutable dmdbin = dmdpath ~ ("dmd" ~ exe);
immutable ldcbin = ldcpath ~ ("ldc2" ~ exe);
writeFile(dmdbin, null);
writeFile(ldcbin, null);
dub.fs.writeFile(dmdbin, "dmd");
dub.fs.writeFile(ldcbin, "ldc");

environment["DC"] = dmdbin.toNativeString();
assert(dub.determineDefaultCompiler() == dmdbin.toNativeString());
Expand Down Expand Up @@ -2093,9 +2100,14 @@ package struct SpecialDirs {
NativePath cache;

/// Returns: An instance of `SpecialDirs` initialized from the environment
deprecated("Use the overload that accepts a `Filesystem`")
public static SpecialDirs make () {
import std.file : tempDir;
scope fs = new RealFS();
return SpecialDirs.make(fs);
}

/// Ditto
public static SpecialDirs make (scope Filesystem fs) {
SpecialDirs result;
result.temp = NativePath(tempDir);

Expand All @@ -2109,7 +2121,7 @@ package struct SpecialDirs {
result.systemSettings = NativePath("/var/lib/dub/");
result.userSettings = NativePath(environment.get("HOME")) ~ ".dub/";
if (!result.userSettings.absolute)
result.userSettings = getWorkingDirectory() ~ result.userSettings;
result.userSettings = fs.getcwd() ~ result.userSettings;
result.userPackages = result.userSettings;
}
result.cache = result.userPackages ~ "cache";
Expand Down
40 changes: 32 additions & 8 deletions source/dub/internal/io/mockfs.d
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ public final class MockFS : Filesystem {

const abs = path.absolute();
auto segments = this.adaptPath(path);
// A path such as `C:\foo` gets turned into `[ "C:", "foo" ]`,
// so check if the root (drive) matches before we do comparison
version (Windows) if (abs && !segments.empty) {
enforce(this.root.name == segments.front.name,
"Cannot mkdir new drive '" ~ segments.front.name ~ `'`);
segments.popFront();
}
reduce!((FSEntry dir, segment) => dir.mkdir(segment.name))(
(abs ? this.root : this.cwd), segments);
}
Expand Down Expand Up @@ -315,28 +322,37 @@ public final class MockFS : Filesystem {

const abs = path.absolute();
auto segments = this.adaptPath(path);
// A path such as `C:\foo` gets turned into `[ "C:", "foo" ]`.
// We need to do the root comparison before comparing the paths
version (Windows) if (abs && !segments.empty) {
if (this.root.name != segments.front.name)
return null;
segments.popFront();
}
// Casting away constness because no good way to do this with `inout`,
// but `FSEntry.lookup` is `inout` too.
return cast(inout(FSEntry)) reduce!(
(FSEntry dir, segment) => dir ? dir.lookup(segment.name) : null)
(cast() (abs ? this.root : this.cwd), segments);
}

/// helper function for code common between `mkdir` and `lookup`
/**
* Adapt a path to work around various platform & library issues
*
* This helper function is for code common between `mkdir` and `lookup`,
* and need to be read in the context of those. The various adaptations
* done to the path to make it uniform are described within the function.
*
* Params:
* path = The path to adapt
*/
private auto adaptPath (in NativePath path) const scope {
if (!path.absolute()) return path.bySegment;
auto segments = path.bySegment;
// `library-nonet` (using vibe.d) has an empty front for absolute path,
// while our built-in module (in vibecompat) does not.
if (segments.front.name.length == 0)
segments.popFront();
// A path such as `C:\foo` gets turned into `[ "", "C:", "foo" ]`,
// so after dropping the empty segment we need to drop the drive
version (Windows) if (!segments.empty) {
enforce(this.root.name == segments.front.name,
"Cannot mkdir new drive '" ~ segments.front.name ~ '"');
segments.popFront();
}
return segments;
}
}
Expand Down Expand Up @@ -559,3 +575,11 @@ unittest {
assert(fs.getcwd == P("/foo/"));
}
}

version (Windows) unittest {
scope fs = new MockFS('X');
assert(fs.existsDirectory(NativePath(`X:\`)));
assert(!fs.existsDirectory(NativePath(`C:\`)));
assert(!fs.existsDirectory(NativePath(`C:\foo\bar`)));
assert(!fs.existsFile(NativePath(`C:/foo/bar.exe`)));
}
6 changes: 4 additions & 2 deletions source/dub/internal/io/realfs.d
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,12 @@ public final class RealFS : Filesystem {
///
public override void removeDir (in NativePath path, bool force = false)
{
const str = path.toNativeString();
if (!std.file.exists(str)) return;
if (force)
std.file.rmdirRecurse(path.toNativeString());
std.file.rmdirRecurse(str);
else
std.file.rmdir(path.toNativeString());
std.file.rmdir(str);
}

/// Ditto
Expand Down
5 changes: 3 additions & 2 deletions source/dub/packagemanager.d
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,15 @@ class PackageManager {
Params:
path = Path of the single repository
*/
this(NativePath path)
this(NativePath path, Filesystem fs = null)
{
import dub.internal.io.realfs;
this.fs = new RealFS();
this.fs = fs !is null ? fs : new RealFS();
this.m_internal.searchPath = [ path ];
this.refresh();
}

deprecated("Use the overload that accepts a `Filesystem`")
this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true)
{
import dub.internal.io.realfs;
Expand Down
Loading