diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 91102fc..dd959e3 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -603,6 +603,7 @@ BuildPlatform m_buildPlatform; BuildSettings m_buildSettings; string m_defaultConfig; + bool m_allowNonLibraryConfigs = true; } override void prepare(scope CommandArgs args) @@ -674,7 +675,8 @@ if (pack) dub.loadPackage(pack); else dub.loadPackageFromCwd(); - m_defaultConfig = dub.getDefaultConfiguration(m_buildPlatform); + m_defaultConfig = dub.getDefaultConfiguration(m_buildPlatform, m_allowNonLibraryConfigs); + logInfo("Got %s for %s", m_defaultConfig, m_allowNonLibraryConfigs); return true; } @@ -829,6 +831,10 @@ } class TestCommand : PackageBuildCommand { + private { + string m_mainFile; + } + this() { this.name = "test"; @@ -838,10 +844,14 @@ "Builds a library configuration of the selected package and executes all contained unit tests." ]; this.acceptsAppArgs = true; + m_allowNonLibraryConfigs = false; } override void prepare(scope CommandArgs args) { + args.getopt("main-file", &m_mainFile, [ + "Specifies a custom file containing the main() function to use for running the tests." + ]); super.prepare(args); } @@ -853,7 +863,14 @@ setupPackage(dub, package_name); - enforce(false, "not implemented"); + //if (!m_nodeps) { + logInfo("Checking dependencies in '%s'", dub.projectPath.toNativeString()); + dub.update(UpdateOptions.none); + //} + + logInfo("Running unit tests for package %s, configuration '%s'.", dub.project.mainPackage.name, m_build_config); + + dub.testProject(m_buildSettings, m_buildPlatform, m_build_config, Path(m_mainFile), app_args); return 0; } } diff --git a/source/dub/dub.d b/source/dub/dub.d index 2002d6c..e06b1c0 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -111,6 +111,8 @@ @property inout(PackageManager) packageManager() inout { return m_packageManager; } + @property inout(Project) project() inout { return m_project; } + /// Loads the package from the current working directory as the main /// project package. void loadPackageFromCwd() @@ -134,7 +136,7 @@ m_project = new Project(m_packageManager, pack); } - string getDefaultConfiguration(BuildPlatform platform) const { return m_project.getDefaultConfiguration(platform); } + string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { return m_project.getDefaultConfiguration(platform, allow_non_library_configs); } /// Performs retrieval and removal as necessary for /// the application. @@ -188,6 +190,83 @@ generator.generateProject(settings); } + void testProject(BuildSettings build_settings, BuildPlatform platform, string config, Path custom_main_file, string[] run_args) + { + if (custom_main_file.length && !custom_main_file.absolute) custom_main_file = getWorkingDirectory() ~ custom_main_file; + + auto test_config = format("__test__%s__", config); + + BuildSettings lbuildsettings = build_settings; + m_project.addBuildSettings(lbuildsettings, platform, config); + enforce(lbuildsettings.targetType != TargetType.executable && lbuildsettings.targetType != TargetType.none, + "Can only test configurations with a library target type."); + enforce(lbuildsettings.mainSourceFile.length, `A "mainSourceFile" is required for testing, but none was set or inferred.`); + + BuildSettingsTemplate tcinfo = m_project.mainPackage.info.getConfiguration(config).buildSettings; + tcinfo.targetType = TargetType.executable; + tcinfo.targetName = test_config; + tcinfo.versions[""] ~= "VibeCustomMain"; // HACK for vibe.d's legacy main() behavior + string custommodname; + if (custom_main_file.length) { + import std.path; + tcinfo.sourceFiles[""] ~= custom_main_file.relativeTo(m_project.mainPackage.path).toNativeString(); + tcinfo.importPaths[""] ~= custom_main_file.parentPath.toNativeString(); + custommodname = custom_main_file.head.toString().baseName(".d"); + } + + auto mainmodname = lbuildsettings.determineModuleName(Path(lbuildsettings.mainSourceFile), m_project.mainPackage.path); + + // generate main file + Path mainfile = getTempDir() ~ "test_main.d"; + tcinfo.sourceFiles[""] ~= mainfile.toNativeString(); + tcinfo.mainSourceFile = mainfile.toNativeString(); + if (!m_dryRun) { + auto fil = openFile(mainfile, FileMode.CreateTrunc); + scope(exit) fil.close(); + if (custommodname.length) { + fil.write(format(q{ + module test_main; + import %s; + import %s; + }, mainmodname, custommodname)); + } else { + fil.write(format(q{ + module test_main; + import %s; + import std.stdio; + import core.runtime; + + void main() { writeln("All unit tests were successful."); } + shared static this() { + version (Have_tested) { + import core.runtime; + Runtime.moduleUnitTester = () => true; + //runUnitTests!app(new JsonTestResultWriter("results.json")); + assert(runUnitTests!%s(new ConsoleTestResultWriter), "Unit tests failed."); + } + } + }, mainmodname, mainmodname)); + } + } + m_project.mainPackage.info.configurations ~= ConfigurationInfo(test_config, tcinfo); + m_project = new Project(m_packageManager, m_project.mainPackage); + + BuildSettings tbuildsettings = build_settings; + m_project.addBuildSettings(tbuildsettings, platform, test_config); + + auto generator = createProjectGenerator("build", m_project, m_packageManager); + GeneratorSettings settings; + settings.platform = platform; + settings.compiler = getCompiler(platform.compilerBinary); + settings.config = test_config; + settings.buildType = "unittest"; + settings.buildSettings = tbuildsettings; + settings.run = true; + settings.runArgs = run_args; + if (m_dryRun) return; + generator.generateProject(settings); + } + /// Outputs a JSON description of the project, including its dependencies. void describeProject(BuildPlatform platform, string config) { @@ -443,3 +522,26 @@ private Path makeAbsolute(Path p) const { return p.absolute ? p : m_rootPath ~ p; } private Path makeAbsolute(string p) const { return makeAbsolute(Path(p)); } } + +string determineModuleName(BuildSettings settings, Path file, Path base_path) +{ + assert(base_path.absolute); + if (!file.absolute) file = base_path ~ file; + foreach (ipath; settings.importPaths.map!(p => Path(p))) { + if (!ipath.absolute) ipath = base_path ~ ipath; + assert(!ipath.empty); + if (file.startsWith(ipath)) { + auto mpath = file[ipath.length .. file.length]; + auto ret = appender!string; + foreach (i; 0 .. mpath.length) { + import std.path; + auto p = mpath[i].toString(); + if (i > 0) ret ~= "."; + if (i+1 < mpath.length) ret ~= p; + else ret ~= p.baseName(".d"); + } + return ret.data; + } + } + throw new Exception("Main source file not found in any import path."); +} diff --git a/source/dub/package_.d b/source/dub/package_.d index 8ceda86..deac7d2 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -146,7 +146,7 @@ } @property string vers() const { return m_parentPackage ? m_parentPackage.vers : m_info.version_; } @property Version ver() const { return Version(this.vers); } - @property const(PackageInfo) info() const { return m_info; } + @property ref inout(PackageInfo) info() inout { return m_info; } @property Path path() const { return m_path; } @property Path packageInfoFile() const { return m_path ~ "package.json"; } @property const(Dependency[string]) dependencies() const { return m_info.dependencies; } @@ -244,11 +244,11 @@ } /// Returns the default configuration to build for the given platform - string getDefaultConfiguration(in BuildPlatform platform, bool is_main_package = false) + string getDefaultConfiguration(in BuildPlatform platform, bool allow_non_library = false) const { - foreach(ref conf; m_info.configurations){ - if( !conf.matchesPlatform(platform) ) continue; - if( !is_main_package && conf.buildSettings.targetType == TargetType.executable ) continue; + foreach (ref conf; m_info.configurations) { + if (!conf.matchesPlatform(platform)) continue; + if (!allow_non_library && conf.buildSettings.targetType == TargetType.executable) continue; return conf.name; } return null; @@ -382,6 +382,14 @@ return ret; } + inout(ConfigurationInfo) getConfiguration(string name) + inout { + foreach (c; configurations) + if (c.name == name) + return c; + throw new Exception("Unknown configuration: "~name); + } + void parseJson(Json json) { foreach( string field, value; json ){ diff --git a/source/dub/project.d b/source/dub/project.d index 21da918..7eebe04 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -90,7 +90,7 @@ @property const(Package[]) dependencies() const { return m_dependencies; } /// Main package. - @property const (Package) mainPackage() const { return m_main; } + @property inout(Package) mainPackage() inout { return m_main; } /** Allows iteration of the dependency tree in topological order */ @@ -142,9 +142,9 @@ else return null; } - string getDefaultConfiguration(BuildPlatform platform) + string getDefaultConfiguration(BuildPlatform platform, bool allow_non_library_configs = true) const { - return m_main.getDefaultConfiguration(platform, true); + return m_main.getDefaultConfiguration(platform, allow_non_library_configs); } /// Rereads the applications state. @@ -354,7 +354,7 @@ // check for conflicts (packages missing in the final configuration graph) foreach (p; getTopologicalPackageList()) - enforce(p.name in ret, "Conflicting configurations for package "~p.name); + enforce(p.name in ret, "Could not resolve configuration for package "~p.name); return ret; } @@ -376,6 +376,7 @@ auto configs = getPackageConfigs(platform, config); foreach (pkg; this.getTopologicalPackageList(false, root_package, configs)) { + auto pkg_path = pkg.path.toNativeString(); dst.addVersions(["Have_" ~ stripDlangSpecialChars(pkg.name)]); assert(pkg.name in configs, "Missing configuration for "~pkg.name); @@ -385,7 +386,7 @@ if (psettings.targetType != TargetType.none) { if (shallow && pkg !is m_main) psettings.sourceFiles = null; - processVars(dst, pkg.path.toNativeString(), psettings); + processVars(dst, pkg_path, psettings); if (psettings.importPaths.empty) logWarn(`Package %s (configuration "%s") defines no import paths, use {"importPaths": [...]} or the default package directory structure to fix this.`, pkg.name, configs[pkg.name]); if (psettings.mainSourceFile.empty && pkg is m_main) @@ -397,7 +398,8 @@ dst.targetType = psettings.targetType; dst.targetPath = psettings.targetPath; dst.targetName = psettings.targetName; - dst.workingDirectory = psettings.workingDirectory; + dst.mainSourceFile = processVars(psettings.mainSourceFile, pkg_path, true); + dst.workingDirectory = processVars(psettings.workingDirectory, pkg_path, true); } } @@ -779,43 +781,46 @@ } private void processVars(ref Appender!(string[]) dst, string project_path, string[] vars, bool are_paths = false) { - foreach( var; vars ){ - auto idx = std.string.indexOf(var, '$'); - if( idx >= 0 ){ - auto vres = appender!string(); - while( idx >= 0 ){ - if( idx+1 >= var.length ) break; - if( var[idx+1] == '$' ){ - vres.put(var[0 .. idx+1]); - var = var[idx+2 .. $]; - } else { - vres.put(var[0 .. idx]); - var = var[idx+1 .. $]; + foreach (var; vars) dst.put(processVars(var, project_path, are_paths)); +} - size_t idx2 = 0; - while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++; - auto varname = var[0 .. idx2]; - var = var[idx2 .. $]; +private string processVars(string var, string project_path, bool is_path) +{ + auto idx = std.string.indexOf(var, '$'); + if (idx >= 0) { + auto vres = appender!string(); + while (idx >= 0) { + if (idx+1 >= var.length) break; + if (var[idx+1] == '$') { + vres.put(var[0 .. idx+1]); + var = var[idx+2 .. $]; + } else { + vres.put(var[0 .. idx]); + var = var[idx+1 .. $]; - string env_variable; - if( varname == "PACKAGE_DIR" ) vres.put(project_path); - else if( (env_variable = environment.get(varname)) != null) vres.put(env_variable); - else enforce(false, "Invalid variable: "~varname); - } - idx = std.string.indexOf(var, '$'); + size_t idx2 = 0; + while( idx2 < var.length && isIdentChar(var[idx2]) ) idx2++; + auto varname = var[0 .. idx2]; + var = var[idx2 .. $]; + + string env_variable; + if( varname == "PACKAGE_DIR" ) vres.put(project_path); + else if( (env_variable = environment.get(varname)) != null) vres.put(env_variable); + else enforce(false, "Invalid variable: "~varname); } - vres.put(var); - var = vres.data; + idx = std.string.indexOf(var, '$'); } - if( are_paths ){ - auto p = Path(var); - if( !p.absolute ){ - logDebug("Fixing relative path: %s ~ %s", project_path, p.toNativeString()); - p = Path(project_path) ~ p; - } - dst.put(p.toNativeString()); - } else dst.put(var); + vres.put(var); + var = vres.data; } + if (is_path) { + auto p = Path(var); + if (!p.absolute) { + logDebug("Fixing relative path: %s ~ %s", project_path, p.toNativeString()); + p = Path(project_path) ~ p; + } + return p.toNativeString(); + } else return var; } private bool isIdentChar(dchar ch)