diff --git a/changelog/runtests.dd b/changelog/runtests.dd new file mode 100644 index 000000000..411a5d9f4 --- /dev/null +++ b/changelog/runtests.dd @@ -0,0 +1,12 @@ +Add runtests sub command + +The `runtests` subcommand enables the execution of integration tests in a unified environment. +Users prepare integration tests by placing test scripts in either the `test` directory or a directory specified by the `--testdir` argument. +Test scripts can be in the following formats: + +- Directories: Build/test/run a directory containing either `dub.json` / `dub.sdl` / `package.json` as a DUB project. +- `*.d` : The D language source code is executed by rdmd. +- `*.script.d` : Runs the D language source code as a file as a single-file DUB project. +- `*.sh` : Shell script format runs only in POSIX environment. +- `*.bat` : Batch file format runs only in Windows environment. +- `*.ps1`, `*.rb`, `*.py`, `*.pl` : PowerShell, Ruby, Python, and Perl scripts will be executed if the corresponding interpreter is installed. diff --git a/source/dub/commandline.d b/source/dub/commandline.d index 575e1890d..355381367 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -50,6 +50,7 @@ CommandGroup[] getCommands() @safe pure nothrow new RunCommand, new BuildCommand, new TestCommand, + new RunTestsCommand, new LintCommand, new GenerateCommand, new DescribeCommand, @@ -269,7 +270,7 @@ unittest { CommandLineHandler handler; handler.commandGroups = getCommands(); - assert(handler.commandNames == ["init", "run", "build", "test", "lint", "generate", + assert(handler.commandNames == ["init", "run", "build", "test", "runtests", "lint", "generate", "describe", "clean", "dustmite", "fetch", "add", "remove", "upgrade", "add-path", "remove-path", "add-local", "remove-local", "list", "search", "add-override", "remove-override", "list-overrides", "clean-caches", "convert"]); @@ -1730,6 +1731,559 @@ class TestCommand : PackageBuildCommand { } } +class RunTestsCommand : BuildCommand { + private { + string[] m_targetTests; + string m_testDirName; + bool m_skipBuild; + bool m_tempBuild; + + static NativePath searchApp(string appname, string[] extraPaths = null) + { + import std.algorithm : filter, map, splitter; + import std.array : array; + import std.path : chainPath, dirName; + import std.range : chain, only, take; + import std.process : environment; + + version(Windows) { + immutable exeName = appname ~ ".exe"; + enum pathSep = ';'; + } + else { + immutable exeName = appname; + enum pathSep = ':'; + } + + auto appLocs = only( + std.file.getcwd().chainPath(exeName) + ) + .chain( + extraPaths.map!(p => p.chainPath(exeName)) + ) + .chain( + environment.get("PATH", "") + .splitter(pathSep) + .map!(p => p.chainPath(exeName)) + ) + .filter!(std.file.exists); + + enforce(!appLocs.empty, "Could not find executable"); + return NativePath(appLocs.front.array); + } + + static abstract class TestExecutor { + NativePath path; + GeneratorSettings baseSettings; + this(NativePath p, GeneratorSettings s) + { + path = p; + baseSettings = s; + } + abstract string getDescription(); + abstract bool isSkip(); + abstract bool execute(); + + static int frontendVerFromString(string verstr) + { + import std.range: take; + int ver; + foreach (v; verstr.splitter(".").take(2).map!(to!int)) { + ver *= 1000; + ver += v; + } + return ver; + } + + static int getMinFrontendFromTextFile(NativePath minFrontendFile) + { + import dub.semver; + if (existsFile(minFrontendFile)) { + return frontendVerFromString(readText(minFrontendFile).strip); + } else { + return 0; + } + } + } + + static abstract class ScriptTestExecutor: TestExecutor { + NativePath workDir; + string[string] env; + this(NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s); + workDir = wd; + env = e; + } + override string getDescription() + { + import std.path: baseName; + return path.head.name.baseName(); + } + + override bool isSkip() + { + // frontend version check + auto minFrontendVer = getMinFrontendFromTextFile(path.parentPath ~ (path.head.name ~ ".min_frontend")); + auto curFrontendVer = frontendVerFromString(baseSettings.platform.compilerVersion); + return curFrontendVer < minFrontendVer; + } + } + + static class DubTestExecutor: TestExecutor { + Dub m_dub; + enum TestType + { + build, + buildAndRun, + buildAndTest, + buildAndTestWithMain, + failBuild, + } + TestType m_testType; + string[string] m_env; + + + this(Dub hostDub, NativePath p, bool tempBuild, bool nodeps, GeneratorSettings s, string[string] e, + bool noRun, bool noTest) + { + super(p, s); + if (existsFile(p ~ ".fail_build")) { + m_testType = TestType.failBuild; + } else if (noRun && noTest) { + m_testType = TestType.build; + } else if (noRun) { + m_testType = TestType.buildAndTest; + } else if (noTest) { + m_testType = TestType.buildAndRun; + } else { + m_testType = TestType.buildAndTestWithMain; + } + m_env = e; + m_dub = new Dub(p.toNativeString(), + hostDub.packageSuppliers, SkipPackageSuppliers.all, + hostDub.packageManager); + m_dub.loadPackage(); + m_dub.project.reinit(); + if (!nodeps) m_dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections); + m_dub.project.validate(); + baseSettings.tempBuild = tempBuild; + } + override string getDescription() + { + import std.path: baseName; + string description = path.head.name.baseName(); + if (m_dub is null) + return description; + if (m_dub.projectName.length > 0 && description != m_dub.projectName) + description ~= " [" ~ m_dub.projectName ~ "]"; + if (m_dub.project.rootPackage.recipe.description.length > 0) + description ~= " - " ~ m_dub.project.rootPackage.recipe.description; + return description; + } + override bool isSkip() + { + // frontend version check + auto minFrontendVer = getMinFrontendFromTextFile(path ~ ".min_frontend"); + auto curFrontendVer = frontendVerFromString(baseSettings.platform.compilerVersion); + return curFrontendVer < minFrontendVer; + } + override bool execute() + { + bool ret; + auto coverage = baseSettings.buildType == "cov" || baseSettings.buildType == "unittest-cov"; + GeneratorSettings gensettings = baseSettings; + gensettings.buildSettings.workingDirectory = path.toNativeString(); + gensettings.overrideToolWorkingDirectory = path; + m_env.byKeyValue.each!(kv => gensettings.buildSettings.environments[kv.key] = kv.value); + gensettings.rdmd = false; + gensettings.single = false; + gensettings.force = true; + + final switch (m_testType) { + case TestType.build: + gensettings.buildType = coverage ? "cov" : "debug"; + ret = !m_dub.generateProject("build", gensettings).collectException(); + break; + case TestType.buildAndRun: + gensettings.buildType = coverage ? "cov" : "debug"; + gensettings.run = true; + gensettings.runCallback = (int status, string output) { + if (status != 0) logError("Test was failed: status == %s\n%s", status, output); + ret = status == 0; + }; + auto buildResult = !m_dub.generateProject("build", gensettings).collectException(); + ret &= buildResult; + break; + case TestType.buildAndTest: + gensettings.buildType = coverage ? "unittest-cov" : "unittest"; + gensettings.run = true; + gensettings.runArgs = ["--DRT-testmode=test-only"]; + gensettings.runCallback = (int status, string output) { + if (status != 0) logError("Test was failed: status == %s\n%s", status, output); + ret = status == 0; + }; + m_dub.testProject(gensettings, gensettings.config, NativePath.init).collectException(); + break; + case TestType.buildAndTestWithMain: + gensettings.buildType = coverage ? "unittest-cov" : "unittest"; + gensettings.run = true; + gensettings.runArgs = ["--DRT-testmode=run-main"]; + gensettings.runCallback = (int status, string output) { + if (status != 0) logError("Test was failed: status == %s\n%s", status, output); + ret = status == 0; + }; + m_dub.testProject(gensettings, gensettings.config, NativePath.init).collectException(); + break; + case TestType.failBuild: + gensettings.buildType = coverage ? "cov" : "debug"; + ret = m_dub.generateProject("build", gensettings).collectException() !is null; + break; + } + return ret; + } + } + + static class SingleFileDubTestExecutor: ScriptTestExecutor + { + Dub m_dub; + this(Dub hostDub, NativePath p, bool tempBuild, bool nodeps, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_dub = new Dub(path.toNativeString(), + hostDub.packageSuppliers, SkipPackageSuppliers.all, + hostDub.packageManager); + m_dub.loadSingleFilePackage(p); + m_dub.project.reinit(); + if (!nodeps) m_dub.upgrade(UpgradeOptions.select | UpgradeOptions.noSaveSelections); + m_dub.project.validate(); + baseSettings.tempBuild = tempBuild; + } + + override string getDescription() + { + import std.path: baseName; + string description = path.head.name.baseName(); + if (m_dub is null) + return description; + if (m_dub.projectName.length > 0 && description.baseName(".script.d") != m_dub.projectName) + description ~= " [" ~ m_dub.projectName ~ "]"; + if (m_dub.project.rootPackage.recipe.description.length > 0) + description ~= " - " ~ m_dub.project.rootPackage.recipe.description; + return description; + } + + override bool execute() + { + GeneratorSettings gensettings = baseSettings; + gensettings.config = m_dub.getDefaultConfiguration(baseSettings.platform); + gensettings.buildSettings.workingDirectory = workDir.toNativeString(); + gensettings.overrideToolWorkingDirectory = workDir; + gensettings.buildSettings.runEnvironments = env; + gensettings.buildType = "debug"; + gensettings.run = true; + gensettings.rdmd = false; + gensettings.single = true; + int testResult; + gensettings.runCallback = (int status, string output) { + testResult = status; + if (status != 0) + logError("Test was failed: status == %s\n%s", status, output); + }; + auto buildResult = !m_dub.generateProject("build", gensettings).collectException(); + return buildResult && testResult == 0; + } + } + + static class RdmdTestExecutor: ScriptTestExecutor + { + NativePath m_rdmdExe; + this(NativePath rdmdExe, NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_rdmdExe = rdmdExe; + } + override bool execute() + { + import std.process: Config; + auto pid = spawnProcess([m_rdmdExe.toNativeString(), path.toNativeString()], + env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class ShellTestExecutor: ScriptTestExecutor + { + this(NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + } + override bool execute() + { + import std.process: spawnShell, Config; + auto pid = spawnShell(path.toNativeString(), env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class BatchTestExecutor: ScriptTestExecutor + { + this(NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + } + override bool execute() + { + import std.process: spawnShell, Config; + auto pid = spawnShell(path.toNativeString(), env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class RubyTestExecutor: ScriptTestExecutor + { + NativePath m_rubyExe; + this(NativePath rubyExe, NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_rubyExe = rubyExe; + } + override bool execute() + { + import std.process: Config; + auto pid = spawnProcess([m_rubyExe.toNativeString(), path.toNativeString()], + env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class PythonTestExecutor: ScriptTestExecutor + { + NativePath m_pythonExe; + this(NativePath pythonExe, NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_pythonExe = pythonExe; + } + override bool execute() + { + import std.process: Config; + auto pid = spawnProcess([m_pythonExe.toNativeString(), path.toNativeString()], + env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class PowershellTestExecutor: ScriptTestExecutor + { + NativePath m_powershellExe; + this(NativePath powershellExe, NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_powershellExe = powershellExe; + } + override bool execute() + { + import std.process: Config; + auto pid = spawnProcess([m_powershellExe.toNativeString(), path.toNativeString()], + env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + + static class PerlTestExecutor: ScriptTestExecutor + { + NativePath m_perlExe; + this(NativePath perlExe, NativePath p, GeneratorSettings s, NativePath wd, string[string] e) + { + super(p, s, wd, e); + m_perlExe = perlExe; + } + override bool execute() + { + import std.process: Config; + auto pid = spawnProcess([m_perlExe.toNativeString(), path.toNativeString()], + env, Config.none, workDir.toNativeString()); + auto result = pid.wait(); + return result == 0; + } + } + } + + + this() @safe pure nothrow + { + this.name = "runtests"; + this.argumentsPattern = "[[@]]"; + this.description = "Run all tests of the selected package"; + this.helpText = [ + `Builds the package and executes all contained tests in test directory.` + ]; + this.acceptsAppArgs = true; + } + + override void prepare(scope CommandArgs args) + { + bool coverage = false; + args.getopt("coverage", &coverage, [ + "Enables code coverage statistics to be generated." + ]); + args.getopt("t|testcase", &m_targetTests, [ + "Target test case." + ]); + args.getopt("testdir", &m_testDirName, [ + "Target test directory." + ]); + args.getopt("skip-build", &m_skipBuild, [ + "Not build test target executable file." + ]); + if (coverage) baseSettings.buildType = "cov"; + if (m_testDirName.length == 0) m_testDirName = "test"; + + super.prepare(args); + + } + + override int execute(Dub dub, string[] free_args, string[] app_args) + { + import std.string: toLower, splitLines; + import std.path: extension, baseName, globMatch; + + if (m_skipBuild) { + setupVersionPackage(dub, null); + } else { + super.execute(dub, free_args, app_args); + } + + auto testpath = dub.project.rootPackage.path ~ m_testDirName; + + immutable rdmdExe = searchApp("rdmd").ifThrown(NativePath.init); + immutable pythonExe = searchApp("python").ifThrown(NativePath.init); + immutable rubyExe = searchApp("ruby").ifThrown(NativePath.init); + immutable perlExe = searchApp("perl").ifThrown(NativePath.init); + immutable pwshExe = searchApp("pwsh").ifThrown(NativePath.init); + immutable powershellExe = !pwshExe.empty ? pwshExe : searchApp("powershell").ifThrown(NativePath.init); + immutable hasRdmd = !rdmdExe.empty; + immutable hasPython = !pythonExe.empty; + immutable hasRuby = !rubyExe.empty; + immutable hasPerl = !perlExe.empty; + immutable hasPowershell = !powershellExe.empty; + auto testenv = [ + "DUB": std.file.thisExePath, + "DC": baseSettings.platform.compilerBinary, + "CURR_DIR": testpath.toNativeString()]; + + string[] ignoreTests = existsFile(testpath ~ ".testignore") + ? readText(testpath ~ ".testignore").splitLines.filter!(a => a.length != 0).array + : null; + TestExecutor[] tests; + bool[] testresults; + + NativePath[] testScriptDirectories; + NativePath[] testScriptFiles; + foreach (de; iterateDirectory(testpath)) { + // skip if ignored testcases + if (ignoreTests.canFind!(t => de.name.globMatch(t))) continue; + // skip if cannot find specified testcases + if (m_targetTests.length > 0 && !m_targetTests.canFind(de.name.baseName, de.name.baseName(".script.d"))) continue; + // skip if posix hidden files + if (de.name.startsWith(".")) continue; + (de.isDirectory ? testScriptDirectories : testScriptFiles) ~= testpath ~ de.name; + } + foreach (p; testScriptDirectories.sort!((a, b) => a.head.name < b.head.name)) { + static bool isDubPackage(NativePath path) + { + foreach (f; packageInfoFiles) { + if (existsFile(path ~ f.filename)) return true; + } + return false; + } + if (isDubPackage(p)) { + // dub pacakage + bool existsPlatformSpecificationFile(NativePath p, string name) + { + if (existsFile(p ~ name)) return true; + foreach (de; iterateDirectory(p)) + { + if (!globMatch(de.name, name ~ "_*")) continue; + auto platformSpec = de.name.chompPrefix(name ~ "_"); + if (baseSettings.platform.matchesSpecification(platformSpec)) return true; + } + return false; + } + if (existsPlatformSpecificationFile(p, ".no_build")) continue; + tests ~= new DubTestExecutor(dub, p, baseSettings.tempBuild, m_nodeps, baseSettings, testenv, + existsPlatformSpecificationFile(p, ".no_run"), + existsPlatformSpecificationFile(p, ".no_test")); + } + } + foreach (p; testScriptFiles.sort!((a, b) => a.head.name < b.head.name)) { + if (p.head.name.toLower.endsWith(".script.d")) { + // script.d + tests ~= new SingleFileDubTestExecutor(dub, p, baseSettings.tempBuild, m_nodeps, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".d") { + // d + if (hasRdmd) tests ~= new RdmdTestExecutor(rdmdExe, p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".sh") { + // shell script + // windows: skip + // posix: $SHELL + version (Posix) tests ~= new ShellTestExecutor(p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".bat") { + // windows batch file + // windows: %COMSPEC% + // posix: skip + version (Windows) tests ~= new BatchTestExecutor(p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".ps1") { + // powershell script, skip if no-installed + if (hasPowershell) tests ~= new PowershellTestExecutor(powershellExe, p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".py") { + // python script, skip if no-installed + if (hasPython) tests ~= new PythonTestExecutor(pythonExe, p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".pl") { + // perl script, skip if no-installed + if (hasPerl) tests ~= new PerlTestExecutor(perlExe, p, baseSettings, testpath, testenv); + } else if (p.head.name.extension.toLower == ".rb") { + // ruby script, skip if no-installed + if (hasRuby) tests ~= new RubyTestExecutor(rubyExe, p, baseSettings, testpath, testenv); + } else { + // do-nothing + } + } + tests = tests.filter!(t => !t.isSkip).array; + testresults.length = tests.length; + foreach (i, t; tests) { + logInfo("Script #" ~ i.to!string, Color.cyan, "%s", t.getDescription()); + testresults[i] = t.execute().ifThrown(false); + } + logInfoNoTag("###### Test summary ######".color(Color.cyan, Mode.bold)); + foreach (i, r; testresults) { + if (r) { + logInfo("[SUCCESS]", Color.green, "#%d: %s", i, tests[i].getDescription()); + } else { + logInfo("[FAILED]", Color.red, "#%d: %s", i, tests[i].getDescription()); + } + } + if (testresults.canFind(false)) + logInfo("%s/%s tests was failed.", testresults.count(false), testresults.length); + if (testresults.canFind(true)) + logInfo("%s/%s tests was succeeded.", testresults.count(true), testresults.length); + if (!testresults.canFind(false)) { + logInfo("All tests are completed!"); + } else { + logError("Test failed."); + return -1; + } + return 0; + } +} + class LintCommand : PackageBuildCommand { private { bool m_syntaxCheck = false; diff --git a/source/dub/dub.d b/source/dub/dub.d index 75f268b6c..49f94d9f6 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -153,7 +153,8 @@ class Dub { as well as the default suppliers. */ this(string root_path = ".", PackageSupplier[] base = null, - SkipPackageSuppliers skip = SkipPackageSuppliers.none) + SkipPackageSuppliers skip = SkipPackageSuppliers.none, + PackageManager package_manager = null) { m_rootPath = NativePath(root_path); if (!m_rootPath.absolute) m_rootPath = getWorkingDirectory() ~ m_rootPath; @@ -169,7 +170,7 @@ class Dub { const registry_var = environment.get("DUB_REGISTRY", null); m_packageSuppliers = this.makePackageSuppliers(base, skip, registry_var); - m_packageManager = this.makePackageManager(); + m_packageManager = package_manager ? package_manager : this.makePackageManager(); auto ccps = m_config.customCachePaths; if (ccps.length) diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index c112cc4be..6af22a848 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -160,6 +160,8 @@ class ProjectGenerator { BuildSettings bs; if (settings.buildSettings.options & BuildOption.lowmem) bs.options |= BuildOption.lowmem; + if (settings.buildSettings.workingDirectory.length > 0 && bs.workingDirectory.length == 0) + bs.workingDirectory = settings.buildSettings.workingDirectory; BuildSettings srcbs = src.dup; envs[pack.name].updateBuildSettings(srcbs); bs.processVars(m_project, pack, srcbs, settings, true); diff --git a/test/.testignore b/test/.testignore new file mode 100644 index 000000000..317c7aedb --- /dev/null +++ b/test/.testignore @@ -0,0 +1,12 @@ +common +common.sh +test_registry.d +win32_default.d +single-file-sdl-default-name.d +issue103-single-file-package-json.d +issue103-single-file-package-w-dep.d +issue103-single-file-package.d +issue1505-single-file-package-dynamic-library.d +issue2051_running_unittests_from_dub_single_file_packages_fails.d +run-unittest.d +run-unittest.sh diff --git a/test/runtests.script.d b/test/runtests.script.d new file mode 100644 index 000000000..ede66f0f5 --- /dev/null +++ b/test/runtests.script.d @@ -0,0 +1,92 @@ +/+ dub.sdl: +name "test-runtests" ++/ +module test.runtests; + +void main() +{ + import std.process, std.path, std.file, std.algorithm; + auto targetDir = buildPath(environment.get("CURR_DIR", getcwd), "runtests"); + auto runtests(string[] args, string workDir) + { + return execute([environment.get("DUB", "dub"), "runtests"] ~ args, null, Config.none, size_t.max, workDir); + } + + // runtests of default + { + auto result = runtests([], targetDir); + assert(result.status == 0, result.output); + assert(result.output.canFind("[SUCCESS] #0: test01 [runtest-testcase-01]"), result.output); + assert(result.output.canFind("[SUCCESS] #1: test03 [runtest-testcase-03] - Test case 03 of runtests command."), + result.output); + assert(result.output.canFind("[SUCCESS] #2: test04 [runtest-testcase-04]"), result.output); + assert(result.output.canFind("[SUCCESS] #3: test02.d"), result.output); + assert(result.output.canFind("[SUCCESS] #4: test05.script.d [runtest-testcase-05]"), result.output); + version (Windows) + { + // Batch files are only executed in a Windows environment. + assert(result.output.canFind("Test 06 on windows batch file"), result.output); + assert(result.output.canFind("[SUCCESS] #5: test06.bat"), result.output); + assert(!result.output.canFind("test06.sh"), result.output); + } + version (Posix) + { + // shell scripts are only executed in a Posix environment. + assert(result.output.canFind("Test 06 on posix shell script"), result.output); + assert(result.output.canFind("[SUCCESS] #5: test06.sh"), result.output); + assert(!result.output.canFind("test06.bat"), result.output); + } + assert(result.output.canFind("6/6 tests was succeeded."), result.output); + assert(result.output.canFind("All tests are completed!"), result.output); + } + + // Fail check and `--testdir` specified + { + auto result = runtests(["--testdir", "test2"], targetDir); + version (Windows) assert(result.status == -1, result.output); + version (Posix) assert(result.status == 255, result.output); + assert(result.output.canFind("[FAILED] #0: test01 [fail-check-test]")); + assert(result.output.canFind("[SUCCESS] #1: test02.d")); + assert(result.output.canFind("1/2 tests was failed."), result.output); + assert(result.output.canFind("1/2 tests was succeeded."), result.output); + assert(result.output.canFind("Error Test failed."), result.output); + } + + // Remove coverage data + if (std.file.exists(targetDir.buildPath("test/test01/src-main.lst"))) + std.file.remove(targetDir.buildPath("test/test01/src-main.lst")); + if (std.file.exists(targetDir.buildPath("test/test01/srcexe-app.lst"))) + std.file.remove(targetDir.buildPath("test/test01/srcexe-app.lst")); + if (std.file.exists(targetDir.buildPath("test/srcexe-app.lst"))) + std.file.remove(targetDir.buildPath("test/srcexe-app.lst")); + + // Check `--coverage` of test case without building test targets + { + auto result = runtests(["--skip-build", "--coverage", "-t", "test01"], targetDir); + scope (exit) if (std.file.exists(targetDir.buildPath("test/test01/src-main.lst"))) + std.file.remove(targetDir.buildPath("test/test01/src-main.lst")); + assert(result.status == 0, result.output); + assert(!result.output.canFind("Linking runtests"), result.output); + assert(result.output.canFind("[SUCCESS] #0: test01 [runtest-testcase-01]"), result.output); + assert(result.output.canFind("1/1 tests was succeeded."), result.output); + assert(result.output.canFind("All tests are completed!"), result.output); + assert(std.file.exists(targetDir.buildPath("test/test01/src-main.lst"))); + assert(std.file.readText(targetDir.buildPath("test/test01/src-main.lst")).canFind("main.d is 100% covered")); + assert(!std.file.exists(targetDir.buildPath("test/test01/srcexe-app.lst"))); + } + + // `--force` build the test target with `--coverage` enabled and check the coverage of the test target + { + auto result = runtests(["--force", "--coverage", "-t", "test02.d"], targetDir); + scope (exit) if (std.file.exists(targetDir.buildPath("test/srcexe-app.lst"))) + std.file.remove(targetDir.buildPath("test/srcexe-app.lst")); + assert(result.status == 0, result.output); + assert(result.output.canFind("Linking runtests"), result.output); + assert(result.output.canFind("[SUCCESS] #0: test02"), result.output); + assert(result.output.canFind("1/1 tests was succeeded."), result.output); + assert(result.output.canFind("All tests are completed!"), result.output); + assert(std.file.exists(targetDir.buildPath("test/srcexe-app.lst"))); + assert(std.file.readText(targetDir.buildPath("test/srcexe-app.lst")).canFind("app.d is 100% covered")); + } + +} diff --git a/test/runtests/.no_build b/test/runtests/.no_build new file mode 100644 index 000000000..e69de29bb diff --git a/test/runtests/dub.sdl b/test/runtests/dub.sdl new file mode 100644 index 000000000..c7170b40a --- /dev/null +++ b/test/runtests/dub.sdl @@ -0,0 +1,13 @@ +name "runtests" + +configuration "exe" { + targetType "executable" + importPaths "srcexe" + sourcePaths "srcexe" +} + +configuration "lib" { + targetType "library" + importPaths "srclib" + sourcePaths "srclib" +} diff --git a/test/runtests/srcexe/app.d b/test/runtests/srcexe/app.d new file mode 100644 index 000000000..df75affbe --- /dev/null +++ b/test/runtests/srcexe/app.d @@ -0,0 +1,12 @@ +module app; + +int main(string[] args) +{ + import std.conv: to; + import std.stdio: writeln; + if (args.length == 1) + return 0; + if (args.length == 3) + writeln(args[2]); + return args[1].to!int; +} diff --git a/test/runtests/srclib/lib.d b/test/runtests/srclib/lib.d new file mode 100644 index 000000000..15691c2ff --- /dev/null +++ b/test/runtests/srclib/lib.d @@ -0,0 +1,11 @@ +module lib; + +int foo(int a, int b) +{ + return a + b; +} + +unittest +{ + assert(foo(1, 2) == 3); +} diff --git a/test/runtests/test/test01/dub.sdl b/test/runtests/test/test01/dub.sdl new file mode 100644 index 000000000..c8d658430 --- /dev/null +++ b/test/runtests/test/test01/dub.sdl @@ -0,0 +1 @@ +name "runtest-testcase-01" diff --git a/test/runtests/test/test01/src/main.d b/test/runtests/test/test01/src/main.d new file mode 100644 index 000000000..31beeec76 --- /dev/null +++ b/test/runtests/test/test01/src/main.d @@ -0,0 +1,8 @@ +module main; + +import std.stdio; + +void main() +{ + writeln("Hello, runtests"); +} diff --git a/test/runtests/test/test02.d b/test/runtests/test/test02.d new file mode 100644 index 000000000..b5c4ee245 --- /dev/null +++ b/test/runtests/test/test02.d @@ -0,0 +1,14 @@ +void main() +{ + import std.process, std.path, std.file, std.string; + string binName = "../runtests"; + version (Windows) + binName ~= ".exe"; + auto res = execute([binName, "--DRT-covopt=srcpath:..", "--DRT-covopt=merge:1"]); + assert(res.status == 0); + res = execute([binName, "42", "--DRT-covopt=srcpath:..", "--DRT-covopt=merge:1"]); + assert(res.status == 42); + res = execute([binName, "123", "test", "--DRT-covopt=srcpath:..", "--DRT-covopt=merge:1"]); + assert(res.status == 123); + assert(res.output.chomp == "test"); +} diff --git a/test/runtests/test/test03/dub.sdl b/test/runtests/test/test03/dub.sdl new file mode 100644 index 000000000..99b45ed5a --- /dev/null +++ b/test/runtests/test/test03/dub.sdl @@ -0,0 +1,6 @@ +name "runtest-testcase-03" +description "Test case 03 of runtests command." + +dependency "runtests" path="../.." +subConfiguration "runtests" "lib" + diff --git a/test/runtests/test/test03/src/app.d b/test/runtests/test/test03/src/app.d new file mode 100644 index 000000000..3093ac99a --- /dev/null +++ b/test/runtests/test/test03/src/app.d @@ -0,0 +1,8 @@ +module app; + +import lib; + +void main() +{ + assert(foo(3, 4) == 7); +} diff --git a/test/runtests/test/test04/.fail_build b/test/runtests/test/test04/.fail_build new file mode 100644 index 000000000..e69de29bb diff --git a/test/runtests/test/test04/dub.sdl b/test/runtests/test/test04/dub.sdl new file mode 100644 index 000000000..859044873 --- /dev/null +++ b/test/runtests/test/test04/dub.sdl @@ -0,0 +1,4 @@ +name "runtest-testcase-04" + +dependency "runtests" path="../.." +subConfiguration "runtests" "lib" diff --git a/test/runtests/test/test04/src/app.d b/test/runtests/test/test04/src/app.d new file mode 100644 index 000000000..c304b9007 --- /dev/null +++ b/test/runtests/test/test04/src/app.d @@ -0,0 +1,6 @@ +module app; + +void main() +{ + assert(bar(10, 20)); +} diff --git a/test/runtests/test/test05.script.d b/test/runtests/test/test05.script.d new file mode 100644 index 000000000..a8d8a7051 --- /dev/null +++ b/test/runtests/test/test05.script.d @@ -0,0 +1,13 @@ +/+ dub.sdl: +name "runtest-testcase-05" +dependency "runtests" path=".." +subConfiguration "runtests" "lib" ++/ +module test05; + +import lib; + +void main() +{ + assert(foo(40, 2) == 42); +} diff --git a/test/runtests/test/test06.bat b/test/runtests/test/test06.bat new file mode 100644 index 000000000..11bacd0e0 --- /dev/null +++ b/test/runtests/test/test06.bat @@ -0,0 +1 @@ +echo Test 06 on windows batch file diff --git a/test/runtests/test/test06.sh b/test/runtests/test/test06.sh new file mode 100755 index 000000000..d6794086e --- /dev/null +++ b/test/runtests/test/test06.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "Test 06 on posix shell script" diff --git a/test/runtests/test2/test01/dub.sdl b/test/runtests/test2/test01/dub.sdl new file mode 100644 index 000000000..ba09a4796 --- /dev/null +++ b/test/runtests/test2/test01/dub.sdl @@ -0,0 +1,4 @@ +name "fail-check-test" + +dependency "runtests" path="../.." +subConfiguration "runtests" "lib" diff --git a/test/runtests/test2/test01/src/app.d b/test/runtests/test2/test01/src/app.d new file mode 100644 index 000000000..c304b9007 --- /dev/null +++ b/test/runtests/test2/test01/src/app.d @@ -0,0 +1,6 @@ +module app; + +void main() +{ + assert(bar(10, 20)); +} diff --git a/test/runtests/test2/test02.d b/test/runtests/test2/test02.d new file mode 100644 index 000000000..919810306 --- /dev/null +++ b/test/runtests/test2/test02.d @@ -0,0 +1,3 @@ +void main() +{ +}