mirror of
https://github.com/dlang/tools.git
synced 2025-04-25 12:40:21 +03:00
359 lines
11 KiB
D
Executable file
359 lines
11 KiB
D
Executable file
#!/usr/bin/env dub
|
|
/++dub.sdl:
|
|
name "tests_extractor"
|
|
dependency "libdparse" version="~>0.24.0"
|
|
+/
|
|
/*
|
|
* Parses all public unittests that are visible on dlang.org
|
|
* (= annotated with three slashes)
|
|
*
|
|
* Copyright (C) 2018 by D Language Foundation
|
|
*
|
|
* Author: Sebastian Wilzbach
|
|
*
|
|
* Distributed under the Boost Software License, Version 1.0.
|
|
* (See accompanying file LICENSE_1_0.txt or copy at
|
|
* http://www.boost.org/LICENSE_1_0.txt)
|
|
*/
|
|
// Written in the D programming language.
|
|
|
|
import dparse.ast;
|
|
import std.algorithm;
|
|
import std.ascii : whitespace;
|
|
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;
|
|
VisitorConfig config;
|
|
|
|
this(File outFile, ubyte[] sourceCode, VisitorConfig config)
|
|
{
|
|
this.outFile = outFile;
|
|
this.sourceCode = sourceCode;
|
|
this.config = config;
|
|
}
|
|
|
|
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);
|
|
// -betterC doesn't run unittests out of the box
|
|
if (config.betterCOutput)
|
|
{
|
|
outFile.writeln(q{extern(C) void main()
|
|
{
|
|
static foreach(test; __traits(allMembers, mixin(__MODULE__)))
|
|
static if (test.length > 8 && test[0..8] == "unittest")
|
|
mixin(test)();
|
|
}});
|
|
}
|
|
}
|
|
|
|
override void visit(const Declaration decl)
|
|
{
|
|
if (decl.unittest_ !is null && shouldIncludeUnittest(decl))
|
|
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)
|
|
{
|
|
const id = vcd.token.text;
|
|
skipTrue = config.ignoredVersions.canFind(id);
|
|
}
|
|
|
|
// search if/version block
|
|
if (!skipTrue)
|
|
{
|
|
foreach (d; decl.trueDeclarations)
|
|
visit(d);
|
|
}
|
|
|
|
// Search else block
|
|
foreach (d; decl.falseDeclarations)
|
|
visit(d);
|
|
}
|
|
|
|
private:
|
|
|
|
bool shouldIncludeUnittest(const Declaration decl)
|
|
{
|
|
if (!config.attributes.empty)
|
|
return filterForUDAs(decl);
|
|
else
|
|
return decl.unittest_.comment !is null;
|
|
}
|
|
|
|
bool filterForUDAs(const Declaration decl)
|
|
{
|
|
foreach (attr; decl.attributes)
|
|
{
|
|
if (attr.atAttribute is null)
|
|
continue;
|
|
|
|
// check for @myArg
|
|
if (config.attributes.canFind(attr.atAttribute.identifier.text))
|
|
return true;
|
|
|
|
// support @("myArg") too
|
|
if (auto argList = attr.atAttribute.argumentList)
|
|
{
|
|
foreach (arg; argList.items)
|
|
{
|
|
if (auto unaryExp = cast(UnaryExpression) arg)
|
|
if (auto primaryExp = unaryExp.primaryExpression)
|
|
{
|
|
auto attribute = primaryExp.primary.text;
|
|
if (attribute.length >= 2)
|
|
{
|
|
attribute = attribute[1 .. $ - 1];
|
|
if (config.attributes.canFind(attribute))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
void print(const Unittest u, const Attribute[] attributes)
|
|
{
|
|
/*
|
|
Write the origin source code line
|
|
u.line is the first line of the unittest block, hence we need to
|
|
subtract two lines from it as we add "import <current.module>\n\n" at
|
|
the top of the unittest.
|
|
*/
|
|
const line = u.line > 2 ? u.line - 2 : 0;
|
|
outFile.writefln("# line %d", line);
|
|
|
|
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
|
|
if (config.betterCOutput)
|
|
outFile.writef("void unittest_line_%s()\n{\n", line);
|
|
else
|
|
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;
|
|
}
|
|
}
|
|
|
|
void parseFile(File inFile, File outFile, VisitorConfig visitorConfig)
|
|
{
|
|
import dparse.lexer;
|
|
import dparse.parser : parseModule;
|
|
import dparse.rollback_allocator : RollbackAllocator;
|
|
import std.array : uninitializedArray;
|
|
|
|
if (inFile.size == 0)
|
|
warningf("%s is empty", inFile.name);
|
|
|
|
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, visitorConfig);
|
|
visitor.visit(m);
|
|
}
|
|
|
|
void parseFileDir(string inputDir, string fileName, string outputDir, VisitorConfig visitorConfig)
|
|
{
|
|
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, "_");
|
|
|
|
parseFile(File(fileName), File(buildPath(outputDir, outName), "w"), visitorConfig);
|
|
}
|
|
|
|
struct VisitorConfig
|
|
{
|
|
string[] attributes; /// List of attributes to extract;
|
|
string[] ignoredVersions; /// List of disabled version conditions
|
|
bool betterCOutput; /// Add custom extern(C) main method for running D's unittests
|
|
}
|
|
|
|
void main(string[] args)
|
|
{
|
|
import std.getopt;
|
|
import std.variant : Algebraic, visit;
|
|
|
|
string inputDir;
|
|
string outputDir = "./out";
|
|
string ignoredFilesStr;
|
|
string modulePrefix;
|
|
string attributesStr;
|
|
string ignVersionsStr;
|
|
VisitorConfig visitorConfig;
|
|
|
|
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,
|
|
"ignore", "Comma-separated list of files to exclude (partial matching is supported)", &ignoredFilesStr,
|
|
"attributes|a", "Comma-separated list of UDAs that the unittest should have", &attributesStr,
|
|
"undefinedVersions", "Comma-separated list of undefined versions", &ignVersionsStr,
|
|
"betterC", "Add custom extern(C) main method for running D's unittests", &visitorConfig.betterCOutput,
|
|
);
|
|
|
|
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 in the output directory.
|
|
`, helpInfo.options);
|
|
}
|
|
|
|
inputDir = inputDir.asNormalizedPath.array;
|
|
Algebraic!(string, File) outputLocation = cast(string) outputDir.asNormalizedPath.array;
|
|
visitorConfig.attributes = attributesStr.split(",");
|
|
visitorConfig.ignoredVersions = ignVersionsStr.split(",");
|
|
|
|
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)
|
|
{
|
|
files = [DirEntry(inputDir)];
|
|
inputDir = ".";
|
|
// for single files use stdout by default
|
|
if (outputDir == "./out")
|
|
{
|
|
outputLocation = stdout;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
files = dirEntries(inputDir, SpanMode.depth).filter!(
|
|
a => a.name.endsWith(".d") && !a.name.canFind(".git")).array;
|
|
}
|
|
|
|
auto ignoringFiles = ignoredFilesStr.split(",");
|
|
|
|
foreach (file; files)
|
|
{
|
|
if (!ignoringFiles.any!(x => file.name.canFind(x)))
|
|
{
|
|
stderr.writeln("parsing ", file);
|
|
outputLocation.visit!(
|
|
(string outputFolder) => parseFileDir(inputDir, file, outputFolder, visitorConfig),
|
|
(File outputFile) => parseFile(File(file.name, "r"), outputFile, visitorConfig),
|
|
);
|
|
}
|
|
else
|
|
{
|
|
stderr.writeln("ignoring ", file);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
The location of unittest token is known, but there might be attributes preceding it.
|
|
*/
|
|
private size_t getAttributesStartLocation(const Attribute[] attrs)
|
|
{
|
|
import dparse.lexer : tok;
|
|
|
|
if (attrs.length == 0)
|
|
return size_t.max;
|
|
|
|
if (attrs[0].atAttribute !is null)
|
|
return attrs[0].atAttribute.startLocation;
|
|
|
|
if (attrs[0].attribute != tok!"")
|
|
return attrs[0].attribute.index;
|
|
|
|
return size_t.max;
|
|
}
|
|
|
|
private size_t skipPreviousWord(const(ubyte)[] sourceCode, size_t index)
|
|
{
|
|
return index - sourceCode[0 .. index]
|
|
.retro
|
|
.enumerate
|
|
.find!(c => !whitespace.canFind(c.value))
|
|
.find!(c => whitespace.canFind(c.value))
|
|
.front.index;
|
|
}
|