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, dirName, mkdirRecurse;
48 import std.path : buildNormalizedPath, buildPath, baseName, relativePath, dirSeparator;
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 options.files.
123             filter!(a => a != options.fileName).
124             map!(a => buildNormalizedPath(a)).
125             map!(a => DirEntry(a))
126             .array;
127     }
128 
129     DirEntry[] modules;
130     foreach (dir; options.dirs) {
131         enforce(isDir(dir), dir ~ " is not a directory name");
132         auto entries = dirEntries(dir, "*.d", SpanMode.depth);
133         auto normalised = entries.map!(a => buildNormalizedPath(a.name));
134 
135         bool isHiddenDir(string p) { return p.startsWith("."); }
136         bool anyHiddenDir(string p) { return p.splitter(dirSeparator).canFind!isHiddenDir; }
137 
138         modules ~= normalised.
139             filter!(a => !anyHiddenDir(a)).
140             map!(a => DirEntry(a)).array;
141     }
142 
143     return modules;
144 }
145 
146 
147 string[] findModuleNames(in Options options) {
148     import std.path : dirSeparator, stripExtension, absolutePath;
149 
150     // if a user passes -Isrc and a file is called src/foo/bar.d,
151     // the module name should be foo.bar, not src.foo.bar,
152     // so this function subtracts import path options
153     string relativeToImportDirs(string path) {
154         foreach(string importPath; options.includes) {
155             importPath = relativePath(importPath);
156             if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator;
157             if(path.startsWith(importPath)) {
158                 return path.replace(importPath, "");
159             }
160         }
161 
162         return path;
163     }
164 
165     return findModuleEntries(options).
166         filter!(a => a.baseName != "package.d" && a.baseName != "reggaefile.d").
167         filter!(a => a.absolutePath != options.fileName.absolutePath).
168         map!(a => relativeToImportDirs(a.name)).
169         map!(a => replace(a.stripExtension, dirSeparator, ".")).
170         array;
171 }
172 
173 string writeUtMainFile(string[] args) {
174     auto options = getGenUtOptions(args);
175     return writeUtMainFile(options);
176 }
177 
178 string writeUtMainFile(Options options) {
179     if (options.earlyReturn) {
180         return options.fileName;
181     }
182 
183     return writeUtMainFile(options, findModuleNames(options));
184 }
185 
186 private string writeUtMainFile(Options options, in string[] modules) {
187     if (!options.fileName) {
188         options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d");
189     }
190 
191     if(!haveToUpdate(options, modules)) {
192         if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected");
193         return options.fileName;
194     } else {
195         if(options.verbose) writeln("Writing to unit test main file ", options.fileName);
196     }
197 
198     const dirName = options.fileName.dirName;
199     dirName.exists || mkdirRecurse(dirName);
200 
201 
202     auto wfile = File(options.fileName, "w");
203     wfile.write(modulesDbList(modules));
204     wfile.writeln(q{
205 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand.
206 import std.stdio;
207 import unit_threaded;
208 });
209 
210     wfile.writeln("int main(string[] args)");
211     wfile.writeln("{");
212     wfile.writeln(`    writeln("\nAutomatically generated file ` ~
213                   options.fileName.replace("\\", "\\\\") ~ `");`);
214     wfile.writeln("    writeln(`Running unit tests from dirs " ~ options.dirs.to!string ~ "`);");
215 
216     immutable indent = "                     ";
217     wfile.writeln("    return runTests!(\n" ~
218                   modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~
219                   "\n" ~ indent ~ ")\n" ~ indent ~ "(args);");
220     wfile.writeln("}");
221     wfile.close();
222 
223     return options.fileName;
224 }
225 
226 
227 private bool haveToUpdate(in Options options, in string[] modules) {
228     if (!options.fileName.exists) {
229         return true;
230     }
231 
232     auto file = File(options.fileName);
233     return file.readln.strip != modulesDbList(modules);
234 }
235 
236 
237 //used to not update the file if the file list hasn't changed
238 private string modulesDbList(in string[] modules) @safe pure nothrow {
239     return "//" ~ modules.join(",");
240 }