diff --git a/dstep/Configuration.d b/dstep/Configuration.d index 3720e54d..d46d2512 100644 --- a/dstep/Configuration.d +++ b/dstep/Configuration.d @@ -37,6 +37,9 @@ struct Configuration /// output file name or folder (in case there are many input files) string output; + /// when true then output to a directory + bool useDirOutput = false; + /// package name @("package", "Use as package name.") string packageName; diff --git a/dstep/driver/Application.d b/dstep/driver/Application.d index da1c98f7..eb0bc1da 100644 --- a/dstep/driver/Application.d +++ b/dstep/driver/Application.d @@ -23,6 +23,7 @@ import dstep.translator.Options; import dstep.core.Exceptions; import dstep.translator.Options; import dstep.translator.Translator; +import dstep.driver.Util : findBasePath, findFiles; class Application { @@ -41,11 +42,11 @@ class Application import std.exception : enforce; import std.range; + this.config = prepareConfig(config); + enforce!DStepException(config.inputFiles.length > 0, "dstep: error: must supply at least one input file\n"); - enforceInputFilesExist(config); - auto translationUnits = makeTranslationUnits(config); enforceTranslationUnitsCompiled(translationUnits); @@ -67,21 +68,45 @@ class Application } } - static void enforceInputFilesExist(const Configuration config) + static Configuration prepareConfig(Configuration config) { + import std.file : exists, isDir, isFile; + import std.path : baseName, asAbsolutePath; import std.exception : enforce; import std.format : format; + import std.array : array; + + Configuration result = config; + string[] files; + + // when only one input file is supplied, -o argument is + // interpreted as file path, otherwise as base directory path + result.useDirOutput = config.inputFiles.length != 1; - foreach (inputFile; config.inputFiles) + foreach (input; config.inputFiles) { enforce!DStepException( - exists(inputFile), - format("dstep: error: file '%s' doesn't exist\n", inputFile)); + exists(input), + format("dstep: error: input '%s' doesn't exist\n", input)); - enforce!DStepException( - isFile(inputFile), - format("dstep: error: '%s' is not a file\n", inputFile)); + if (isDir(input)) + { + files ~= findFiles(input); + result.useDirOutput = true; + } + else + { + enforce!DStepException( + isFile(input), + format("dstep: error: '%s' is not a file\n", input)); + + files ~= input.asAbsolutePath.array; + } } + + result.inputFiles = files; + + return result; } void enforceTranslationUnitsCompiled(TranslationUnit[] translationUnits) @@ -115,14 +140,13 @@ class Application import std.algorithm; import std.array; import std.range; + import std.path; - import dstep.driver.Util : makeDefaultOutputFile; + import dstep.driver.Util : makeDefaultOutputFile, findBasePath; auto inputFiles = config.inputFiles; - // when only one input file is supplied, -o argument is - // interpreted as file path, otherwise as base directory path - if (inputFiles.length == 1) + if (!config.useDirOutput) { return [config.output.empty ? makeDefaultOutputFile(inputFiles.front, false) @@ -130,11 +154,19 @@ class Application } else { - alias fmap = file => Path.buildPath( - config.output, - makeDefaultOutputFile(file, false)); - - return inputFiles.map!fmap.array; + if (config.output.empty) + { + alias fmap = file => makeDefaultOutputFile(file, false); + return inputFiles.map!fmap.array; + } + else + { + auto basePath = findBasePath(inputFiles); + alias fmap = file => Path.buildPath( + config.output, + makeDefaultOutputFile(relativePath(file, basePath), false)); + return inputFiles.map!fmap.array; + } } } diff --git a/dstep/driver/Util.d b/dstep/driver/Util.d index 90e4d3d6..49e7371f 100644 --- a/dstep/driver/Util.d +++ b/dstep/driver/Util.d @@ -15,3 +15,108 @@ string makeDefaultOutputFile(string inputFile, bool useBaseName = true) return setExtension(inputFile, "d"); } + +string findBasePath(string[] paths) +{ + import std.algorithm.iteration : filter, map, reduce; + import std.algorithm.searching : commonPrefix; + import std.array : array; + import std.path : isAbsolute, dirName, pathSplitter, buildPath; + import std.file : exists, isDir; + + auto absolutePaths = paths.filter!isAbsolute.array; + if (absolutePaths.length == 0) + return "."; + + version (Windows) + { + import std.string : replace; + absolutePaths = absolutePaths.map!(p => p.replace("/", "\\")).array; + } + + auto getDirName = (string path) => (exists(path) && isDir(path)) ? path : dirName(path); + + if (absolutePaths.length == 1) + return getDirName(absolutePaths[0]); + + auto parts = absolutePaths + .map!(p => pathSplitter(getDirName(p)).array) + .array; + + version (Windows) + { + import std.uni : icmp; + auto common = parts.reduce!((a, b) => commonPrefix!((x, y) => icmp(x, y) == 0)(a, b).array); + } + else + { + auto common = parts.reduce!((a, b) => commonPrefix(a, b).array); + } + + if (common.length == 0) + return "."; + + return buildPath(common); +} + +string[] findFiles(string dir, string[] extensions = [".h", ".hpp"]) +{ + import std.file : dirEntries, SpanMode; + import std.path : extension, asAbsolutePath; + import std.algorithm : filter, map, canFind; + import std.array : array; + import std.conv : to; + + return dirEntries(dir, SpanMode.breadth) + .filter!(f => f.isFile && extensions.canFind(extension(f.name))) + .map!(f => f.name.asAbsolutePath.to!string) + .array; +} + +unittest +{ + assert(findBasePath([]) == "."); + assert(findBasePath(["foo/bar.h", "a/b.h", "c.h"]) == "."); +} + +version (Posix) +{ + unittest + { + assert(findBasePath(["/usr/include/stdio.h"]) == "/usr/include"); + assert(findBasePath(["foo.h", "/usr/include/sys/stat.h"]) == "/usr/include/sys"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/lib/foo.h"]) == "/usr"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/string.h", "/usr/include/stdlib.h"]) == "/usr/include"); + assert(findBasePath(["usr.h", "/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/include/string.h", "include.h"]) == "/usr/include"); + assert(findBasePath(["/usr/include/stdio.h", "/usr/Include/stdio.h", "/usR/include/stdio.h"]) == "/"); + assert(findBasePath(["/usr"]) == "/usr"); + assert(findBasePath(["/usr", "/usr/lib"]) == "/usr"); + } +} + +version (Windows) +{ + unittest + { + assert(findBasePath(["/usr/include/stdio.h"]) == "."); + assert(findBasePath(["foo.h", "/usr/include/sys/stat.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/lib/foo.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/include/string.h", "/usr/include/stdlib.h"]) == "."); + assert(findBasePath(["usr.h", "/usr/include/stdio.h", "/usr/include/sys/stat.h", "/usr/include/string.h", "include.h"]) == "."); + assert(findBasePath(["/usr/include/stdio.h", "/usr/Include/stdio.h", "/usR/include/stdio.h"]) == "."); + + assert(findBasePath(["C:/usr/include/stdio.h"]) == "C:\\usr\\include"); + assert(findBasePath(["foo.h", "C:/usr/include/sys/stat.h"]) == "C:\\usr\\include\\sys"); + assert(findBasePath(["C:/usr/include/stdio.h", "C:/usr/include/sys/stat.h", "C:/usr/lib/foo.h"]) == "C:\\usr"); + assert(findBasePath(["C:/usr/include/stdio.h", "C:/usr/include/string.h", "C:/usr/include/stdlib.h"]) == "C:\\usr\\include"); + assert(findBasePath(["usr.h", "D:/usr/include/stdio.h", "D:/usr/include/sys/stat.h", "D:/usr/include/string.h", "include.h"]) == "D:\\usr\\include"); + assert(findBasePath(["E:/usr/include/stdio.h", "E:/usr/Include/stdio.h", "E:/usR/include/stdio.h"]) == "E:\\usr\\include"); + + assert(findBasePath(["D:\\test.h"]) == "D:\\"); + assert(findBasePath(["D:\\a\\b.h", "D:\\a\\c.h"]) == "D:\\a"); + assert(findBasePath(["D:\\a\\b.h", "E:\\b.h"]) == "."); + assert(findBasePath(["D:\\B\\a.h", "D:\\b\\c\\D.h", "D:\\B\\c.h"]) == "D:\\B"); + assert(findBasePath(["D:/a\\b/d.h", "D:\\a/B\\c.h"]) == "D:\\a\\b"); + assert(findBasePath(["C:\\a.h", "D:\\b.h"]) == "."); + } +} diff --git a/tests/functional/Tests.d b/tests/functional/Tests.d index 7eebf707..0a8b30b3 100644 --- a/tests/functional/Tests.d +++ b/tests/functional/Tests.d @@ -9,6 +9,7 @@ import tests.support.Assertions; import std.algorithm : canFind; import std.process : executeShell; import std.typecons; +import std.path : absolutePath; void printClangVersion() { @@ -205,6 +206,29 @@ unittest ); } +unittest +{ + assertRunsDStepCDir(["tests/functional/module/include.d", + "tests/functional/module/main0.d", + "IGNORE", + "IGNORE", + "tests/functional/module/unused.d"], + "tests/functional/module", + ["--package", "modules", "-Iresources"] + ); +} + +unittest +{ + assertRunsDStepCDir(["tests/functional/module/include.d", + "IGNORE", + "tests/functional/module/main0NotNormalized.d", + "tests/functional/module/main0_public.d"], + "tests/functional/module", + ["--public-submodules", "--package", "modules", "-Iresources", "--unspecified-output"] + ); +} + unittest { assertRunsDStepCFiles([ @@ -294,6 +318,44 @@ unittest assertIssuesWarning("tests/functional/collision.h"); } +// DStep should output to correct directory when using absolute paths. +unittest +{ + assertRunsDStepCFiles([ + TestFile("tests/functional/graph/subfile1.d", absolutePath("tests/functional/graph/subfile1.h")), + TestFile("tests/functional/include/subfile3.d", absolutePath("tests/functional/include/subfile3.h"))] + ); +} + +unittest +{ + assertRunsDStepCFile( + "tests/functional/functions.d", + absolutePath("tests/functional/functions.h") + ); +} + +// Output in same directory when single file given and no output specified +unittest +{ + assertRunsDStepCFiles( + [TestFile("tests/functional/functions.d", absolutePath("tests/functional/functions.h"))], + ["--unspecified-output"], + false); +} + +// Output in same directory when using multiple files with no output folder specified +unittest +{ + assertRunsDStepCFiles( + [TestFile("tests/functional/aggregate.d", "tests/functional/aggregate.h"), + TestFile("tests/functional/functions.d", "tests/functional/functions.h"), + TestFile("tests/functional/graph/subfile1.d", absolutePath("tests/functional/graph/subfile1.h")), + TestFile("tests/functional/include/subfile3.d", absolutePath("tests/functional/include/subfile3.h"))], + ["--unspecified-output"] + ); +} + version (OSX): // Objective-C tests diff --git a/tests/functional/graph/subfile1.d b/tests/functional/graph/subfile1.d new file mode 100644 index 00000000..673ce4f1 --- /dev/null +++ b/tests/functional/graph/subfile1.d @@ -0,0 +1,3 @@ +extern (C): + +extern __gshared int content; diff --git a/tests/functional/include/subfile3.d b/tests/functional/include/subfile3.d new file mode 100644 index 00000000..ad13ba87 --- /dev/null +++ b/tests/functional/include/subfile3.d @@ -0,0 +1,3 @@ +extern (C): + +void core (int); diff --git a/tests/support/Assertions.d b/tests/support/Assertions.d index c038c216..6a9aa0b4 100644 --- a/tests/support/Assertions.d +++ b/tests/support/Assertions.d @@ -1,6 +1,7 @@ module tests.support.Assertions; import core.exception : AssertError; +import std.sumtype : SumType, match; import tests.support.DStepRunner; @@ -10,6 +11,14 @@ struct TestFile string actual; } +struct TestDir +{ + string[] expected; + string actual; +} + +alias TestInput = SumType!(TestFile[], TestDir[]); + void assertRunsDStepCFile( string expectedPath, string cPath, @@ -30,7 +39,28 @@ void assertRunsDStepCFile( } void assertRunsDStepCFiles( - TestFile[] filesPaths, + TestFile[] input, + string[] arguments = [], + bool strict = false, + string file = __FILE__, + size_t line = __LINE__) +{ + assertRunsDStepCInput(TestInput(input), arguments, strict, file, line); +} + +void assertRunsDStepCDir( + string[] expectedFiles, + string actualDir, + string[] arguments = [], + bool strict = false, + string file = __FILE__, + size_t line = __LINE__) +{ + assertRunsDStepCInput(TestInput([TestDir(expectedFiles, actualDir)]), arguments, strict, file, line); +} + +void assertRunsDStepCInput( + TestInput input, string[] arguments = [], bool strict = false, string file = __FILE__, @@ -39,7 +69,7 @@ void assertRunsDStepCFiles( string[] extended = arguments ~ ["-Iresources"]; assertRunsDStep( - filesPaths, + input, extended, strict, file, @@ -101,13 +131,23 @@ void assertIssuesWarning( } void assertRunsDStep( - TestFile[] testFiles, + TestFile[] input, + string[] arguments, + bool strict, + string file = __FILE__, + size_t line = __LINE__) +{ + assertRunsDStep(TestInput(input), arguments, strict, file, line); +} + +void assertRunsDStep( + TestInput input, string[] arguments, bool strict, string file = __FILE__, size_t line = __LINE__) { - import std.algorithm : map; + import std.algorithm : map, joiner; import std.array : array; import std.file : readText, write; import std.format : format; @@ -122,10 +162,15 @@ void assertRunsDStep( ReturnType!testRunDStep result; + string[] actualInputs = input.match!( + (TestFile[] files) => files.map!(x => x.actual).array, + (TestDir[] dirs) => dirs.map!(x => x.actual).array + ); + try { result = testRunDStep( - testFiles.map!(x => x.actual).array, + actualInputs, arguments, &outputContents, &command, @@ -179,11 +224,19 @@ DStep output: throw new AssertError(message, file, line); } - foreach (index, testFile; testFiles) + string[] expectedOutputs = input.match!( + (TestFile[] files) => files.map!(x => x.expected).array, + (TestDir[] dirs) => dirs.map!(x => x.expected).joiner.array + ); + + foreach (index, expectedPath; expectedOutputs) { - if (fileExists(testFile.expected)) + if (expectedPath == "IGNORE") + continue; + + if (fileExists(expectedPath)) { - string expected = readText(testFile.expected); + string expected = readText(expectedPath); string actual = outputContents[index]; auto mismatch = mismatchRegionTranslated(actual, expected, 8, strict); @@ -211,7 +264,7 @@ DStep output: } else { - write(testFile.expected, outputContents[index]); + write(expectedPath, outputContents[index]); } } } diff --git a/tests/support/DStepRunner.d b/tests/support/DStepRunner.d index 2974a408..841b8a43 100644 --- a/tests/support/DStepRunner.d +++ b/tests/support/DStepRunner.d @@ -11,6 +11,36 @@ version (Windows) private alias TestRunDStepResult = ReturnType!execute; + +void copy(string source, string targetDir) +{ + import std.file : isDir, dirEntries, SpanMode, copy, mkdirRecurse; + import std.path : buildPath, asAbsolutePath, asRelativePath, baseName; + import std.array; + + mkdirRecurse(targetDir); + + if (source.isDir) + { + foreach (entry; dirEntries(source, SpanMode.breadth)) + { + string target = buildPath(targetDir, entry.name.asAbsolutePath.asRelativePath(source.asAbsolutePath).array); + if (entry.isDir) + { + mkdirRecurse(target); + } + else + { + copy(entry.name, target); + } + } + } + else + { + copy(source, buildPath(targetDir, source.baseName)); + } +} + auto testRunDStep( string[] sourcePaths, string[] arguments, @@ -21,13 +51,13 @@ auto testRunDStep( { import core.exception : AssertError; - import std.algorithm : canFind, map; - import std.file : exists, isFile, readText, rmdirRecurse; - import std.path : buildPath; + import std.algorithm : canFind, map, remove, sort; + import std.file : exists, isFile, isDir, readText, rmdirRecurse, getcwd; + import std.path : buildPath, isAbsolute, relativePath, dirName, absolutePath; import std.range : join; - import std.array : empty; + import std.array : empty, array; - import dstep.driver.Util : makeDefaultOutputFile; + import dstep.driver.Util : makeDefaultOutputFile, findBasePath, findFiles; version (OptionalGNUStep) { @@ -43,36 +73,64 @@ auto testRunDStep( } foreach (sourcePath; sourcePaths) - assertFileExists(sourcePath, file, line); + assertInputExists(sourcePath, file, line); string outputDir = namedTempDir("dstepUnitTest"); scope(exit) rmdirRecurse(outputDir); + auto argumentsLength = arguments.length; + arguments = arguments.remove!(x => x == "--unspecified-output"); + bool unspecifiedOutput = arguments.length < argumentsLength; + string[] outputPaths; + string workDir = getcwd(); - if (sourcePaths.length == 1) - { - outputPaths ~= buildPath(outputDir, - makeDefaultOutputFile(sourcePaths[0], false)); - } - else + auto sourceBasePath = findBasePath(sourcePaths.map!(file => file.absolutePath).array); + bool dirInput = false; + auto toRelative = (string path) => relativePath(path, unspecifiedOutput ? workDir : sourceBasePath); + + foreach (ref sourcePath; sourcePaths) { - foreach (sourcePath; sourcePaths) - outputPaths ~= buildPath(outputDir, - makeDefaultOutputFile(sourcePath, false)); + string path = toRelative(sourcePath.absolutePath); + bool dirPath = isDir(sourcePath); + if (unspecifiedOutput) + { + auto updatedSourcePath = buildPath(outputDir, path); + copy(sourcePath, dirPath ? updatedSourcePath : dirName(updatedSourcePath)); + if (isAbsolute(sourcePath)) + { + sourcePath = updatedSourcePath; + } + } + if (dirPath) + { + dirInput = true; + outputPaths ~= findFiles(sourcePath).map!(file => buildPath(outputDir, makeDefaultOutputFile(toRelative(file), false))).array.sort().array; + } else + { + outputPaths ~= buildPath(outputDir, makeDefaultOutputFile(path, false)); + } } - auto localCommand = ["./bin/dstep"] ~ sourcePaths ~ arguments; + auto dstepPath = buildPath(workDir, "bin", "dstep"); + auto localCommand = [dstepPath] ~ sourcePaths ~ arguments; - if (outputPaths.length == 1) - localCommand ~= ["-o", outputPaths[0]]; + if (unspecifiedOutput) + { + workDir = outputDir; + } else - localCommand ~= ["-o", outputDir]; + { + if (outputPaths.length == 1 && !dirInput) + localCommand ~= ["-o", outputPaths[0]]; + else + localCommand ~= ["-o", outputDir]; + } if (command) *command = join(localCommand, " "); - auto result = execute(localCommand); + auto result = execute(localCommand, workDir: workDir); if (outputContents) outputContents.length = outputPaths.length; @@ -130,7 +188,7 @@ class NamedTempDirException : object.Exception } } -void assertFileExists( +void assertInputExists( string expected, string file = __FILE__, size_t line = __LINE__) @@ -139,11 +197,11 @@ void assertFileExists( import std.format : format; - import tests.support.Util : fileExists; + import tests.support.Util : inputExists; - if (!fileExists(expected)) + if (!inputExists(expected)) { - auto message = format("File %s doesn't exist.", expected); + auto message = format("Input %s doesn't exist.", expected); throw new AssertError(message, file, line); } } diff --git a/tests/support/Util.d b/tests/support/Util.d index 4c29fc17..ec62faa3 100644 --- a/tests/support/Util.d +++ b/tests/support/Util.d @@ -6,6 +6,12 @@ bool fileExists(string path) return exists(path) && isFile(path); } +bool inputExists(string path) +{ + import std.file : exists, isFile, isDir; + return exists(path) && (isFile(path) || isDir(path)); +} + string mismatchRegion( string expected, string actual,