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 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 
173 
174 string[] findModuleNames(in Options options) {
175     import std.path : dirSeparator, stripExtension, absolutePath, relativePath;
176     import std.algorithm: endsWith, startsWith, filter, map;
177     import std.array: replace, array;
178     import std.path: baseName, absolutePath;
179 
180     // if a user passes -Isrc and a file is called src/foo/bar.d,
181     // the module name should be foo.bar, not src.foo.bar,
182     // so this function subtracts import path options
183     string relativeToImportDirs(string path) {
184         foreach(string importPath; options.includes) {
185             importPath = relativePath(importPath);
186             if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator;
187             if(path.startsWith(importPath)) {
188                 return path.replace(importPath, "");
189             }
190         }
191 
192         return path;
193     }
194 
195     return findModuleEntries(options).
196         filter!(a => a.baseName != "reggaefile.d").
197         filter!(a => a.absolutePath != options.fileName.absolutePath).
198         map!(a => relativeToImportDirs(a.name)).
199         map!(a => replace(a.stripExtension, dirSeparator, ".")).
200         array;
201 }
202 
203 string writeUtMainFile(string[] args) {
204     auto options = getGenUtOptions(args);
205     return writeUtMainFile(options);
206 }
207 
208 string writeUtMainFile(Options options) {
209     if (options.earlyReturn) {
210         return options.fileName;
211     }
212 
213     return writeUtMainFile(options, findModuleNames(options));
214 }
215 
216 private string writeUtMainFile(Options options, in string[] modules) {
217     import std.path: buildPath, dName = dirName;
218     import std.stdio: writeln, File;
219     import std.file: tempDir, getcwd, mkdirRecurse, exists;
220     import std.algorithm: map;
221     import std.array: join;
222 
223     if (!options.fileName) {
224         options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d");
225     }
226 
227     if(!haveToUpdate(options, modules)) {
228         if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected");
229         return options.fileName;
230     } else {
231         if(options.verbose) writeln("Writing to unit test main file ", options.fileName);
232     }
233 
234     const dirName = options.fileName.dName;
235     dirName.exists || mkdirRecurse(dirName);
236 
237 
238     auto wfile = File(options.fileName, "w");
239     wfile.write(modulesDbList(modules));
240     wfile.writeln(q{
241 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand.
242 import unit_threaded;
243 });
244 
245     wfile.writeln("int main(string[] args)");
246     wfile.writeln("{");
247 
248     immutable indent = "                          ";
249     wfile.writeln("    return args.runTests!(\n" ~
250                   modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~
251                   "\n" ~ indent ~ ");");
252     wfile.writeln("}");
253     wfile.close();
254 
255     return options.fileName;
256 }
257 
258 
259 private bool haveToUpdate(in Options options, in string[] modules) {
260     import std.file: exists;
261     import std.stdio: File;
262     import std.array: join;
263     import std.string: strip;
264 
265     if (!options.fileName.exists) {
266         return true;
267     }
268 
269     auto file = File(options.fileName);
270     return file.readln.strip != modulesDbList(modules);
271 }
272 
273 
274 //used to not update the file if the file list hasn't changed
275 private string modulesDbList(in string[] modules) @safe pure nothrow {
276     import std.array: join;
277     return "//" ~ modules.join(",");
278 }