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 }