| #!/usr/bin/env dub |
| /++dub.sdl: |
| name "tests_extractor" |
| dependency "libdparse" version="~>0.24.0" |
| dflags "-fall-instantiations" platform="gdc" |
| +/ |
| // Written in the D programming language. |
| |
| import dparse.ast; |
| import std.algorithm; |
| import std.conv; |
| import std.exception; |
| import std.experimental.logger; |
| import std.file; |
| import std.path; |
| import std.range; |
| import std.stdio; |
| |
| class TestVisitor : ASTVisitor |
| { |
| File outFile; |
| ubyte[] sourceCode; |
| string moduleName; |
| |
| this(File outFile, ubyte[] sourceCode) |
| { |
| this.outFile = outFile; |
| this.sourceCode = sourceCode; |
| } |
| |
| alias visit = ASTVisitor.visit; |
| |
| override void visit(const Module m) |
| { |
| if (m.moduleDeclaration !is null) |
| { |
| moduleName = m.moduleDeclaration.moduleName.identifiers.map!(i => i.text).join("."); |
| } |
| else |
| { |
| // Fallback: convert the file path to its module path, e.g. std/uni.d -> std.uni |
| moduleName = outFile.name.replace(".d", "").replace(dirSeparator, ".").replace(".package", ""); |
| } |
| m.accept(this); |
| } |
| |
| override void visit(const Declaration decl) |
| { |
| if (decl.unittest_ !is null && decl.unittest_.comment !is null) |
| print(decl.unittest_, decl.attributes); |
| |
| decl.accept(this); |
| } |
| |
| override void visit(const ConditionalDeclaration decl) |
| { |
| bool skipTrue; |
| |
| // Check if it's a version that should be skipped |
| if (auto vcd = decl.compileCondition.versionCondition) |
| { |
| if (vcd.token.text == "StdDdoc") |
| skipTrue = true; |
| } |
| |
| // Search if/version block |
| if (!skipTrue) |
| { |
| foreach (d; decl.trueDeclarations) |
| visit(d); |
| } |
| |
| // Search else block |
| foreach (d; decl.falseDeclarations) |
| visit(d); |
| } |
| |
| private: |
| |
| void print(const Unittest u, const Attribute[] attributes) |
| { |
| static immutable predefinedAttributes = ["nogc", "system", "nothrow", "safe", "trusted", "pure"]; |
| |
| // Write system attributes |
| foreach (attr; attributes) |
| { |
| // pure and nothrow |
| if (attr.attribute.type != 0) |
| { |
| import dparse.lexer : str; |
| const attrText = attr.attribute.type.str; |
| outFile.write(text(attrText, " ")); |
| } |
| |
| const atAttribute = attr.atAttribute; |
| if (atAttribute is null) |
| continue; |
| |
| const atText = atAttribute.identifier.text; |
| |
| // Ignore custom attributes (@myArg) |
| if (!predefinedAttributes.canFind(atText)) |
| continue; |
| |
| outFile.write(text("@", atText, " ")); |
| } |
| |
| // Write the unittest block |
| outFile.write("unittest\n{\n"); |
| scope(exit) outFile.writeln("}\n"); |
| |
| // Add an import to the current module |
| outFile.writefln(" import %s;", moduleName); |
| |
| // Write the content of the unittest block (but skip the first brace) |
| auto k = cast(immutable(char)[]) sourceCode[u.blockStatement.startLocation .. u.blockStatement.endLocation]; |
| k.findSkip("{"); |
| outFile.write(k); |
| |
| // If the last line contains characters, we want to add an extra line |
| // for increased visual beauty |
| if (k[$ - 1] != '\n') |
| outFile.writeln; |
| } |
| } |
| |
| bool parseFile(File inFile, File outFile) |
| { |
| import dparse.lexer; |
| import dparse.parser : parseModule; |
| import dparse.rollback_allocator : RollbackAllocator; |
| import std.array : uninitializedArray; |
| |
| if (inFile.size == 0) |
| return false; |
| |
| ubyte[] sourceCode = uninitializedArray!(ubyte[])(to!size_t(inFile.size)); |
| inFile.rawRead(sourceCode); |
| LexerConfig config; |
| auto cache = StringCache(StringCache.defaultBucketCount); |
| auto tokens = getTokensForParser(sourceCode, config, &cache); |
| |
| RollbackAllocator rba; |
| auto m = parseModule(tokens.array, inFile.name, &rba); |
| auto visitor = new TestVisitor(outFile, sourceCode); |
| visitor.visit(m); |
| return visitor.outFile.size != 0; |
| } |
| |
| void parseFileDir(string inputDir, string fileName, string outputDir) |
| { |
| import std.path : buildPath, dirSeparator, buildNormalizedPath; |
| |
| // File name without its parent directory, e.g. std/uni.d |
| string fileNameNormalized = (inputDir == "." ? fileName : fileName.replace(inputDir, "")); |
| |
| // Remove leading dots or slashes |
| while (!fileNameNormalized.empty && fileNameNormalized[0] == '.') |
| fileNameNormalized = fileNameNormalized[1 .. $]; |
| if (fileNameNormalized.length >= dirSeparator.length && |
| fileNameNormalized[0 .. dirSeparator.length] == dirSeparator) |
| fileNameNormalized = fileNameNormalized[dirSeparator.length .. $]; |
| |
| // Convert the file path to a nice output file, e.g. std/uni.d -> std_uni.d |
| string outName = fileNameNormalized.replace(dirSeparator, "_"); |
| auto outFile = buildPath(outputDir, outName); |
| |
| // Removes the output file if nothing was written |
| if (!parseFile(File(fileName), File(outFile, "w"))) |
| remove(outFile); |
| } |
| |
| void main(string[] args) |
| { |
| import std.getopt; |
| |
| string inputDir; |
| string outputDir = "./out"; |
| string modulePrefix; |
| |
| auto helpInfo = getopt(args, config.required, |
| "inputdir|i", "Folder to start the recursive search for unittest blocks (can be a single file)", &inputDir, |
| "outputdir|o", "Folder to which the extracted test files should be saved (stdout for a single file)", &outputDir, |
| ); |
| |
| if (helpInfo.helpWanted) |
| { |
| return defaultGetoptPrinter(`phobos_tests_extractor |
| Searches the input directory recursively for public unittest blocks, i.e. |
| unittest blocks that are annotated with three slashes (///). |
| The tests will be extracted as one file for each source file |
| to the output directory. |
| `, helpInfo.options); |
| } |
| |
| inputDir = inputDir.asNormalizedPath.array; |
| outputDir= outputDir.asNormalizedPath.array; |
| |
| if (!exists(outputDir)) |
| mkdir(outputDir); |
| |
| // If the module prefix is std -> add a dot for the next modules to follow |
| if (!modulePrefix.empty) |
| modulePrefix ~= '.'; |
| |
| DirEntry[] files; |
| |
| if (inputDir.isFile) |
| { |
| stderr.writeln("ignoring ", inputDir); |
| return; |
| } |
| else |
| { |
| files = dirEntries(inputDir, SpanMode.depth).filter!( |
| a => a.name.endsWith(".d") && !a.name.canFind(".git")).array; |
| } |
| |
| foreach (file; files) |
| { |
| stderr.writeln("parsing ", file); |
| parseFileDir(inputDir, file, outputDir); |
| } |
| } |