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