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 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 private 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 @("issue 40") 173 unittest { 174 import unit_threaded.should; 175 import std.path; 176 dubFilesToAbsPaths("", ["foo/bar/package.d"]).shouldEqual( 177 [buildPath("foo", "bar")]); 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 230 if (!options.fileName) { 231 options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d"); 232 } 233 234 if(!haveToUpdate(options, modules)) { 235 if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected"); 236 return options.fileName; 237 } else { 238 if(options.verbose) writeln("Writing to unit test main file ", options.fileName); 239 } 240 241 const dirName = options.fileName.dName; 242 dirName.exists || mkdirRecurse(dirName); 243 244 245 auto wfile = File(options.fileName, "w"); 246 wfile.write(modulesDbList(modules)); 247 wfile.writeln(q{ 248 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 249 import unit_threaded; 250 }); 251 252 wfile.writeln("int main(string[] args)"); 253 wfile.writeln("{"); 254 255 immutable indent = " "; 256 wfile.writeln(" return args.runTests!(\n" ~ 257 modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~ 258 "\n" ~ indent ~ ");"); 259 wfile.writeln("}"); 260 wfile.close(); 261 262 return options.fileName; 263 } 264 265 266 private bool haveToUpdate(in Options options, in string[] modules) { 267 import std.file: exists; 268 import std.stdio: File; 269 import std.array: join; 270 import std..string: strip; 271 272 if (!options.fileName.exists) { 273 return true; 274 } 275 276 auto file = File(options.fileName); 277 return file.readln.strip != modulesDbList(modules); 278 } 279 280 281 //used to not update the file if the file list hasn't changed 282 private string modulesDbList(in string[] modules) @safe pure nothrow { 283 import std.array: join; 284 return "//" ~ modules.join(","); 285 }