diff options
author | Iain Buclaw <ibuclaw@gdcproject.org> | 2025-02-25 19:47:06 +0100 |
---|---|---|
committer | Iain Buclaw <ibuclaw@gdcproject.org> | 2025-02-25 22:31:51 +0100 |
commit | df4565eaa9b02906a8fa6bb37845c0b4fdedaa20 (patch) | |
tree | 6d02a40a46ebdd1674b6ffe3bbe2181201751f36 /libphobos/scripts | |
parent | a407eada0173455d267ba403e9e0fe54f0f5dd51 (diff) | |
download | gcc-df4565eaa9b02906a8fa6bb37845c0b4fdedaa20.zip gcc-df4565eaa9b02906a8fa6bb37845c0b4fdedaa20.tar.gz gcc-df4565eaa9b02906a8fa6bb37845c0b4fdedaa20.tar.bz2 |
libphobos: Add script for extracting unittests from phobos
This script parses all unittests annotated with three slashes (`///')
and extracts them into a standalone test case. The intended use is for
generating inexpensive tests to be ran for the phobos testsuite.
libphobos/ChangeLog:
* scripts/.gitignore: Add tests_extractor.
* scripts/README: Document tests_extractor.d.
* scripts/tests_extractor.d: New file.
Diffstat (limited to 'libphobos/scripts')
-rw-r--r-- | libphobos/scripts/.gitignore | 1 | ||||
-rw-r--r-- | libphobos/scripts/README | 11 | ||||
-rw-r--r-- | libphobos/scripts/tests_extractor.d | 224 |
3 files changed, 236 insertions, 0 deletions
diff --git a/libphobos/scripts/.gitignore b/libphobos/scripts/.gitignore index a5d300b..ddbaf41 100644 --- a/libphobos/scripts/.gitignore +++ b/libphobos/scripts/.gitignore @@ -1,3 +1,4 @@ # Dub leaves built programs in this directory. gen_druntime_sources gen_phobos_sources +tests_extractor diff --git a/libphobos/scripts/README b/libphobos/scripts/README index 248324d..5444b71 100644 --- a/libphobos/scripts/README +++ b/libphobos/scripts/README @@ -26,3 +26,14 @@ gen_phobos_sources.d Example: cd src && ../scripts/gen_phobos_sources >> Makefile.am + +tests_extractor.d + + Searches the given input directory recursively for public unittest blocks + (annotated with three slashes). The tests will be extracted as one file for + each source file to the output directory. Used to regenerate all tests + cases in testsuite/libphobos.phobos. + + Example: + + ./tests_extractor -i ../libphobos/src -o ../testsuite/libphobos.phobos diff --git a/libphobos/scripts/tests_extractor.d b/libphobos/scripts/tests_extractor.d new file mode 100644 index 0000000..bc861f5 --- /dev/null +++ b/libphobos/scripts/tests_extractor.d @@ -0,0 +1,224 @@ +#!/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); + } +} |