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