Wolfgang Thallergcc 15.1.0ada185d1 year agoHistory
224 lines · 6.5 KB · D
RawDownload
#!/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);    }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t