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