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 }