#!/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_extractorSearches 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 fileto 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); }}
#!/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);
}
}