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.runtime; 40 41 import unit_threaded.from; 42 43 44 mixin template genUtMain() { 45 46 int main(string[] args) { 47 try { 48 writeUtMainFile(args); 49 return 0; 50 } catch(Exception ex) { 51 import std.stdio: stderr; 52 stderr.writeln(ex.msg); 53 return 1; 54 } 55 } 56 } 57 58 59 struct Options { 60 bool verbose; 61 string fileName; 62 string[] dirs; 63 bool help; 64 bool showVersion; 65 string[] includes; 66 string[] files; 67 68 bool earlyReturn() @safe pure nothrow const { 69 return help || showVersion; 70 } 71 } 72 73 74 Options getGenUtOptions(string[] args) { 75 import std.getopt; 76 import std.stdio: writeln; 77 78 Options options; 79 auto getOptRes = getopt( 80 args, 81 "verbose|v", "Verbose mode.", &options.verbose, 82 "file|f", "The filename to write. Will use a temporary if not set.", &options.fileName, 83 "I", "Import paths as would be passed to the compiler", &options.includes, 84 "version", "Show version.", &options.showVersion, 85 ); 86 87 if (getOptRes.helpWanted) { 88 defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", getOptRes.options); 89 options.help = true; 90 return options; 91 } 92 93 if (options.showVersion) { 94 writeln("unit_threaded.runtime version v0.6.1"); 95 return options; 96 } 97 98 options.dirs = args.length <= 1 ? ["."] : args[1 .. $]; 99 100 if (options.verbose) { 101 writeln(__FILE__, ": finding all test cases in ", options.dirs); 102 } 103 104 return options; 105 } 106 107 108 from!"std.file".DirEntry[] findModuleEntries(in Options options) { 109 110 import std.algorithm: splitter, canFind, map, startsWith, filter; 111 import std.array: array, empty; 112 import std.file: DirEntry, isDir, dirEntries, SpanMode; 113 import std.path: dirSeparator, buildNormalizedPath; 114 import std.exception: enforce; 115 116 // dub list of files, don't bother reading the filesystem since 117 // dub has done it already 118 if(!options.files.empty && options.dirs == ["."]) { 119 return dubFilesToAbsPaths(options.fileName, options.files) 120 .map!toDirEntry 121 .array; 122 } 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 bool isHiddenDir(string p) { return p.startsWith("."); } 131 bool anyHiddenDir(string p) { return p.splitter(dirSeparator).canFind!isHiddenDir; } 132 133 modules ~= normalised. 134 filter!(a => !anyHiddenDir(a)). 135 map!toDirEntry.array; 136 } 137 138 return modules; 139 } 140 141 auto toDirEntry(string a) { 142 import std.file: DirEntry; 143 return DirEntry(removePackage(a)); 144 } 145 146 // package.d files will show up as foo.bar.package 147 // remove .package from the end 148 string removePackage(string name) { 149 import std.algorithm: endsWith; 150 import std.array: replace; 151 enum toRemove = "/package.d"; 152 return name.endsWith(toRemove) 153 ? name.replace(toRemove, "") 154 : name; 155 } 156 157 158 string[] dubFilesToAbsPaths(in string fileName, in string[] files) { 159 import std.algorithm: filter, map; 160 import std.array: array; 161 import std.path: buildNormalizedPath; 162 163 // dub list of files, don't bother reading the filesystem since 164 // dub has done it already 165 return files 166 .filter!(a => a != fileName) 167 .map!(a => removePackage(a)) 168 .map!(a => buildNormalizedPath(a)) 169 .array; 170 } 171 172 173 174 string[] findModuleNames(in Options options) { 175 import std.path : dirSeparator, stripExtension, absolutePath, relativePath; 176 import std.algorithm: endsWith, startsWith, filter, map; 177 import std.array: replace, array; 178 import std.path: baseName, absolutePath; 179 180 // if a user passes -Isrc and a file is called src/foo/bar.d, 181 // the module name should be foo.bar, not src.foo.bar, 182 // so this function subtracts import path options 183 string relativeToImportDirs(string path) { 184 foreach(string importPath; options.includes) { 185 importPath = relativePath(importPath); 186 if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator; 187 if(path.startsWith(importPath)) { 188 return path.replace(importPath, ""); 189 } 190 } 191 192 return path; 193 } 194 195 return findModuleEntries(options). 196 filter!(a => a.baseName != "reggaefile.d"). 197 filter!(a => a.absolutePath != options.fileName.absolutePath). 198 map!(a => relativeToImportDirs(a.name)). 199 map!(a => replace(a.stripExtension, dirSeparator, ".")). 200 array; 201 } 202 203 string writeUtMainFile(string[] args) { 204 auto options = getGenUtOptions(args); 205 return writeUtMainFile(options); 206 } 207 208 string writeUtMainFile(Options options) { 209 if (options.earlyReturn) { 210 return options.fileName; 211 } 212 213 return writeUtMainFile(options, findModuleNames(options)); 214 } 215 216 private string writeUtMainFile(Options options, in string[] modules) { 217 import std.path: buildPath, dName = dirName; 218 import std.stdio: writeln, File; 219 import std.file: tempDir, getcwd, mkdirRecurse, exists; 220 import std.algorithm: map; 221 import std.array: join; 222 223 if (!options.fileName) { 224 options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d"); 225 } 226 227 if(!haveToUpdate(options, modules)) { 228 if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected"); 229 return options.fileName; 230 } else { 231 if(options.verbose) writeln("Writing to unit test main file ", options.fileName); 232 } 233 234 const dirName = options.fileName.dName; 235 dirName.exists || mkdirRecurse(dirName); 236 237 238 auto wfile = File(options.fileName, "w"); 239 wfile.write(modulesDbList(modules)); 240 wfile.writeln(q{ 241 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 242 import unit_threaded; 243 }); 244 245 wfile.writeln("int main(string[] args)"); 246 wfile.writeln("{"); 247 248 immutable indent = " "; 249 wfile.writeln(" return args.runTests!(\n" ~ 250 modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~ 251 "\n" ~ indent ~ ");"); 252 wfile.writeln("}"); 253 wfile.close(); 254 255 return options.fileName; 256 } 257 258 259 private bool haveToUpdate(in Options options, in string[] modules) { 260 import std.file: exists; 261 import std.stdio: File; 262 import std.array: join; 263 import std..string: strip; 264 265 if (!options.fileName.exists) { 266 return true; 267 } 268 269 auto file = File(options.fileName); 270 return file.readln.strip != modulesDbList(modules); 271 } 272 273 274 //used to not update the file if the file list hasn't changed 275 private string modulesDbList(in string[] modules) @safe pure nothrow { 276 import std.array: join; 277 return "//" ~ modules.join(","); 278 }