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, mkdirRecurse; 48 import std.path : buildNormalizedPath, buildPath, baseName, relativePath, dirSeparator, dirName; 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 dubFilesToAbsPaths(options.fileName, options.files) 123 .map!toDirEntry 124 .array; 125 } 126 127 DirEntry[] modules; 128 foreach (dir; options.dirs) { 129 enforce(isDir(dir), dir ~ " is not a directory name"); 130 auto entries = dirEntries(dir, "*.d", SpanMode.depth); 131 auto normalised = entries.map!(a => buildNormalizedPath(a.name)); 132 133 bool isHiddenDir(string p) { return p.startsWith("."); } 134 bool anyHiddenDir(string p) { return p.splitter(dirSeparator).canFind!isHiddenDir; } 135 136 modules ~= normalised. 137 filter!(a => !anyHiddenDir(a)). 138 map!toDirEntry.array; 139 } 140 141 return modules; 142 } 143 144 auto toDirEntry(string a) { 145 return DirEntry(removePackage(a)); 146 } 147 148 // package.d files will show up as foo.bar.package 149 // remove .package from the end 150 string removePackage(string name) { 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 160 // dub list of files, don't bother reading the filesystem since 161 // dub has done it already 162 return files 163 .filter!(a => a != fileName) 164 .map!(a => removePackage(a)) 165 .map!(a => buildNormalizedPath(a)) 166 .array; 167 } 168 169 @("issue 40") 170 unittest { 171 import unit_threaded.should; 172 import std.path; 173 dubFilesToAbsPaths("", ["foo/bar/package.d"]).shouldEqual( 174 [buildPath("foo", "bar")]); 175 } 176 177 178 string[] findModuleNames(in Options options) { 179 import std.path : dirSeparator, stripExtension, absolutePath; 180 181 // if a user passes -Isrc and a file is called src/foo/bar.d, 182 // the module name should be foo.bar, not src.foo.bar, 183 // so this function subtracts import path options 184 string relativeToImportDirs(string path) { 185 foreach(string importPath; options.includes) { 186 importPath = relativePath(importPath); 187 if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator; 188 if(path.startsWith(importPath)) { 189 return path.replace(importPath, ""); 190 } 191 } 192 193 return path; 194 } 195 196 return findModuleEntries(options). 197 filter!(a => a.baseName != "reggaefile.d"). 198 filter!(a => a.absolutePath != options.fileName.absolutePath). 199 map!(a => relativeToImportDirs(a.name)). 200 map!(a => replace(a.stripExtension, dirSeparator, ".")). 201 array; 202 } 203 204 string writeUtMainFile(string[] args) { 205 auto options = getGenUtOptions(args); 206 return writeUtMainFile(options); 207 } 208 209 string writeUtMainFile(Options options) { 210 if (options.earlyReturn) { 211 return options.fileName; 212 } 213 214 return writeUtMainFile(options, findModuleNames(options)); 215 } 216 217 private string writeUtMainFile(Options options, in string[] modules) { 218 if (!options.fileName) { 219 options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d"); 220 } 221 222 if(!haveToUpdate(options, modules)) { 223 if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected"); 224 return options.fileName; 225 } else { 226 if(options.verbose) writeln("Writing to unit test main file ", options.fileName); 227 } 228 229 const dirName = options.fileName.dirName; 230 dirName.exists || mkdirRecurse(dirName); 231 232 233 auto wfile = File(options.fileName, "w"); 234 wfile.write(modulesDbList(modules)); 235 wfile.writeln(q{ 236 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 237 import unit_threaded; 238 }); 239 240 wfile.writeln("int main(string[] args)"); 241 wfile.writeln("{"); 242 243 immutable indent = " "; 244 wfile.writeln(" return args.runTests!(\n" ~ 245 modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~ 246 "\n" ~ indent ~ ");"); 247 wfile.writeln("}"); 248 wfile.close(); 249 250 return options.fileName; 251 } 252 253 254 private bool haveToUpdate(in Options options, in string[] modules) { 255 if (!options.fileName.exists) { 256 return true; 257 } 258 259 auto file = File(options.fileName); 260 return file.readln.strip != modulesDbList(modules); 261 } 262 263 264 //used to not update the file if the file list hasn't changed 265 private string modulesDbList(in string[] modules) @safe pure nothrow { 266 return "//" ~ modules.join(","); 267 }