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 }