1 /** 2 This module implements a $(LINK2 http://dlang.org/template-mixin.html, 3 template mixin) containing a program to search a list of directories 4 for all .d files therein, then writes a D program to run all unit 5 tests in those files using unit_threaded. The program 6 implemented by this mixin only writes out a D file that itself must be 7 compiled and run. 8 9 To use this as a runnable program, simply mix in and compile: 10 ----- 11 #!/usr/bin/rdmd 12 import unit_threaded; 13 mixin genUtMain; 14 ----- 15 16 Or just use rdmd with the included gen_ut_main, 17 which does the above. The examples below use the second option. 18 19 By default, genUtMain will look for unit tests in a $(D tests) 20 folder and write a program out to a file named $(D ut.d). To change 21 the file to write to, use the $(D -f) option. To change what 22 directories to look in, simply pass them in as the remaining 23 command-line arguments. 24 25 Examples: 26 ----- 27 # write ut.d that finds unit tests from files in the tests directory 28 rdmd $PHOBOS/std/experimental/testing/gen_ut_main.d 29 30 # write foo.d that finds unit tests from the src and other directories 31 rdmd $PHOBOS/std/experimental/testing/gen_ut_main.d -f foo.d src other 32 ----- 33 34 The resulting $(D ut.d) file (or as named by the $(D -f) option) is 35 also a program that must be compiled and, when run, will run the unit 36 tests found. By default, it will run all tests. To run one test or 37 all tests in a particular package, pass them in as command-line arguments. 38 The $(D -h) option will list all command-line options. 39 40 Examples (assuming the generated file is called $(D ut.d)): 41 ----- 42 rdmd -unittest ut.d # run all tests 43 rdmd -unittest ut.d tests.foo tests.bar # run all tests from these packages 44 rdmd ut.d -h # list command-line options 45 ----- 46 */ 47 48 module unit_threaded.runtime; 49 50 import std.stdio; 51 import std.array : replace, array, join; 52 import std.conv : to; 53 import std.algorithm : map, filter, startsWith, endsWith, remove; 54 import std.string: strip; 55 import std.exception : enforce; 56 import std.file : exists, DirEntry, dirEntries, isDir, SpanMode, tempDir, getcwd, dirName, mkdirRecurse; 57 import std.path : buildNormalizedPath, buildPath, baseName, relativePath, dirSeparator; 58 59 60 mixin template genUtMain() { 61 62 int main(string[] args) { 63 try { 64 writeUtMainFile(args); 65 return 0; 66 } catch(Exception ex) { 67 import std.stdio: stderr; 68 stderr.writeln(ex.msg); 69 return 1; 70 } 71 } 72 } 73 74 75 struct Options { 76 bool verbose; 77 string fileName; 78 string[] dirs; 79 bool help; 80 bool showVersion; 81 string[] includes; 82 83 bool earlyReturn() @safe pure nothrow const { 84 return help || showVersion; 85 } 86 } 87 88 89 Options getGenUtOptions(string[] args) { 90 import std.getopt; 91 92 Options options; 93 auto getOptRes = getopt( 94 args, 95 "verbose|v", "Verbose mode.", &options.verbose, 96 "file|f", "The filename to write. Will use a temporary if not set.", &options.fileName, 97 "I", "Import paths as would be passed to the compiler", &options.includes, 98 "version", "Show version.", &options.showVersion, 99 ); 100 101 if (getOptRes.helpWanted) { 102 defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", getOptRes.options); 103 options.help = true; 104 return options; 105 } 106 107 if (options.showVersion) { 108 writeln("unit_threaded.runtime version v0.5.7"); 109 return options; 110 } 111 112 options.dirs = args.length <= 1 ? ["."] : args[1 .. $]; 113 114 if (options.verbose) { 115 writeln(__FILE__, ": finding all test cases in ", options.dirs); 116 } 117 118 return options; 119 } 120 121 122 DirEntry[] findModuleEntries(in Options options) { 123 124 DirEntry[] modules; 125 foreach (dir; options.dirs) { 126 enforce(isDir(dir), dir ~ " is not a directory name"); 127 auto entries = dirEntries(dir, "*.d", SpanMode.depth); 128 auto normalised = entries.map!(a => buildNormalizedPath(a.name)); 129 130 modules ~= normalised. 131 map!(a => DirEntry(a)).array; 132 } 133 134 return modules; 135 } 136 137 138 string[] findModuleNames(in Options options) { 139 import std.path : dirSeparator, stripExtension; 140 141 // if a user passes -Isrc and a file is called src/foo/bar.d, 142 // the module name should be foo.bar, not src.foo.bar, 143 // so this function subtracts import path options 144 string relativeToImportDirs(string path) { 145 foreach(string importPath; options.includes) { 146 if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator; 147 if(path.startsWith(importPath)) { 148 return path.replace(importPath, ""); 149 } 150 } 151 152 return path; 153 } 154 155 return findModuleEntries(options). 156 filter!(a => a.baseName != "package.d" && a.baseName != "reggaefile.d"). 157 map!(a => relativeToImportDirs(a.name)). 158 map!(a => replace(a.stripExtension, dirSeparator, ".")). 159 array; 160 } 161 162 string writeUtMainFile(string[] args) { 163 auto options = getGenUtOptions(args); 164 return writeUtMainFile(options); 165 } 166 167 string writeUtMainFile(Options options) { 168 if (options.earlyReturn) { 169 return options.fileName; 170 } 171 172 return writeUtMainFile(options, findModuleNames(options)); 173 } 174 175 private string writeUtMainFile(Options options, in string[] modules) { 176 if (!options.fileName) { 177 options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d"); 178 } 179 180 if(!haveToUpdate(options, modules)) { 181 if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected"); 182 return options.fileName; 183 } else { 184 if(options.verbose) writeln("Writing to unit test main file ", options.fileName); 185 } 186 187 const dirName = options.fileName.dirName; 188 dirName.exists || mkdirRecurse(dirName); 189 190 191 auto wfile = File(options.fileName, "w"); 192 wfile.write(modulesDbList(modules)); 193 wfile.writeln(q{ 194 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 195 import std.stdio; 196 import unit_threaded; 197 }); 198 199 wfile.writeln("int main(string[] args)"); 200 wfile.writeln("{"); 201 wfile.writeln(` writeln("\nAutomatically generated file ` ~ 202 options.fileName.replace("\\", "\\\\") ~ `");`); 203 wfile.writeln(" writeln(`Running unit tests from dirs " ~ options.dirs.to!string ~ "`);"); 204 205 immutable indent = " "; 206 wfile.writeln(" return runTests!(\n" ~ 207 modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~ 208 "\n" ~ indent ~ ")\n" ~ indent ~ "(args);"); 209 wfile.writeln("}"); 210 wfile.close(); 211 212 return options.fileName; 213 } 214 215 216 private bool haveToUpdate(in Options options, in string[] modules) { 217 if (!options.fileName.exists) { 218 return true; 219 } 220 221 auto file = File(options.fileName); 222 return file.readln.strip != modulesDbList(modules); 223 } 224 225 226 //used to not update the file if the file list hasn't changed 227 private string modulesDbList(in string[] modules) @safe pure nothrow { 228 return "//" ~ modules.join(","); 229 }