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