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; 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; 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 82 bool earlyReturn() @safe pure nothrow const { 83 return help || showVersion; 84 } 85 } 86 87 88 Options getGenUtOptions(string[] args) { 89 import std.getopt; 90 91 Options options; 92 auto getOptRes = getopt( 93 args, 94 "verbose|v", "Verbose mode.", &options.verbose, 95 "file|f", "The filename to write. Will use a temporary if not set.", &options.fileName, 96 "version", "Show version.", &options.showVersion, 97 ); 98 99 if (getOptRes.helpWanted) { 100 defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", getOptRes.options); 101 options.help = true; 102 return options; 103 } 104 105 if (options.showVersion) { 106 writeln("unit_threaded.runtime version v0.0.1"); 107 return options; 108 } 109 110 options.dirs = args.length <= 1 ? ["."] : args[1 .. $]; 111 112 if (options.verbose) { 113 writeln(__FILE__, ": finding all test cases in ", options.dirs); 114 } 115 116 return options; 117 } 118 119 120 DirEntry[] findModuleEntries(in string[] dirs) { 121 122 DirEntry[] modules; 123 foreach (dir; dirs) { 124 enforce(isDir(dir), dir ~ " is not a directory name"); 125 auto entries = dirEntries(dir, "*.d", SpanMode.depth); 126 auto normalised = entries.map!(a => DirEntry(buildNormalizedPath(a.name))); 127 modules ~= normalised.array; 128 } 129 130 return modules; 131 } 132 133 string[] findModuleNames(in string[] dirs) { 134 import std.path : dirSeparator; 135 136 //cut off extension 137 return findModuleEntries(dirs). 138 map!(a => replace(a.name[0 .. $ - 2], dirSeparator, ".")). 139 array; 140 } 141 142 string writeUtMainFile(string[] args) { 143 auto options = getGenUtOptions(args); 144 return writeUtMainFile(options); 145 } 146 147 string writeUtMainFile(Options options) { 148 if (options.earlyReturn) { 149 return options.fileName; 150 } 151 152 return writeUtMainFile(options, findModuleNames(options.dirs)); 153 } 154 155 private string writeUtMainFile(Options options, in string[] modules) { 156 if (!options.fileName) { 157 options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d"); 158 } 159 160 if(!haveToUpdate(options, modules)) { 161 if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected"); 162 return options.fileName; 163 } else { 164 if(options.verbose) writeln("Writing to unit test main file ", options.fileName); 165 } 166 167 const dirName = options.fileName.dirName; 168 dirName.exists || mkdirRecurse(dirName); 169 170 171 auto wfile = File(options.fileName, "w"); 172 wfile.write(modulesDbList(modules)); 173 wfile.writeln(q{ 174 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 175 import std.stdio; 176 import unit_threaded; 177 }); 178 179 wfile.writeln("int main(string[] args)"); 180 wfile.writeln("{"); 181 wfile.writeln(` writeln("\nAutomatically generated file ` ~ 182 options.fileName.replace("\\", "\\\\") ~ `");`); 183 wfile.writeln(" writeln(`Running unit tests from dirs " ~ options.dirs.to!string ~ "`);"); 184 185 immutable indent = " "; 186 wfile.writeln(" return runTests!(\n" ~ 187 modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~ 188 "\n" ~ indent ~ ")\n" ~ indent ~ "(args);"); 189 wfile.writeln("}"); 190 wfile.close(); 191 192 return options.fileName; 193 } 194 195 196 private bool haveToUpdate(in Options options, in string[] modules) { 197 if (!options.fileName.exists) { 198 return true; 199 } 200 201 auto file = File(options.fileName); 202 return file.readln.strip != modulesDbList(modules); 203 } 204 205 206 //used to not update the file if the file list hasn't changed 207 private string modulesDbList(in string[] modules) @safe pure nothrow { 208 return "//" ~ modules.join(","); 209 }