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 Or just use rdmd with the included gen_ut_main,
17 which does the above. The examples below use the second option.
18 
19 By default, genUtMain will look for unit tests in a $(D tests)
20 folder and write a program out to a file named $(D ut.d). 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 Examples:
26 -----
27 # write ut.d that finds unit tests from files in the tests directory
28 rdmd $PHOBOS/std/experimental/testing/gen_ut_main.d
29 
30 # write foo.d that finds unit tests from the src and other directories
31 rdmd $PHOBOS/std/experimental/testing/gen_ut_main.d -f foo.d src other
32 -----
33 
34 The resulting $(D ut.d) file (or as named by the $(D -f) option) is
35 also a program that must be compiled and, when run, will run the unit
36 tests found. By default, it will run all tests. To run one test or
37 all tests in a particular package, pass them in as command-line arguments.
38 The $(D -h) option will list all command-line options.
39 
40 Examples (assuming the generated file is called $(D ut.d)):
41 -----
42 rdmd -unittest ut.d # run all tests
43 rdmd -unittest ut.d tests.foo tests.bar # run all tests from these packages
44 rdmd ut.d -h # list command-line options
45 -----
46 */
47 
48 module unit_threaded.runtime;
49 
50 import std.stdio;
51 import std.array : replace, array, join;
52 import std.conv : to;
53 import std.algorithm : map, filter, startsWith, endsWith, remove;
54 import std.string: strip;
55 import std.exception : enforce;
56 import std.file : exists, DirEntry, dirEntries, isDir, SpanMode, tempDir, getcwd, dirName, mkdirRecurse;
57 import std.path : buildNormalizedPath, buildPath, baseName, relativePath, dirSeparator;
58 
59 
60 mixin template genUtMain() {
61 
62     int main(string[] args) {
63         try {
64             writeUtMainFile(args);
65             return 0;
66         } catch(Exception ex) {
67             import std.stdio: stderr;
68             stderr.writeln(ex.msg);
69             return 1;
70         }
71     }
72 }
73 
74 
75 struct Options {
76     bool verbose;
77     string fileName;
78     string[] dirs;
79     bool help;
80     bool showVersion;
81     string[] includes;
82 
83     bool earlyReturn() @safe pure nothrow const {
84         return help || showVersion;
85     }
86 }
87 
88 
89 Options getGenUtOptions(string[] args) {
90     import std.getopt;
91 
92     Options options;
93     auto getOptRes = getopt(
94         args,
95         "verbose|v", "Verbose mode.", &options.verbose,
96         "file|f", "The filename to write. Will use a temporary if not set.", &options.fileName,
97         "I", "Import paths as would be passed to the compiler", &options.includes,
98         "version", "Show version.", &options.showVersion,
99         );
100 
101     if (getOptRes.helpWanted) {
102         defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", getOptRes.options);
103         options.help = true;
104         return options;
105     }
106 
107     if (options.showVersion) {
108         writeln("unit_threaded.runtime version v0.5.7");
109         return options;
110     }
111 
112     options.dirs = args.length <= 1 ? ["."] : args[1 .. $];
113 
114     if (options.verbose) {
115         writeln(__FILE__, ": finding all test cases in ", options.dirs);
116     }
117 
118     return options;
119 }
120 
121 
122 DirEntry[] findModuleEntries(in Options options) {
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         modules ~= normalised.
131             map!(a => DirEntry(a)).array;
132     }
133 
134     return modules;
135 }
136 
137 
138 string[] findModuleNames(in Options options) {
139     import std.path : dirSeparator, stripExtension;
140 
141     // if a user passes -Isrc and a file is called src/foo/bar.d,
142     // the module name should be foo.bar, not src.foo.bar,
143     // so this function subtracts import path options
144     string relativeToImportDirs(string path) {
145         foreach(string importPath; options.includes) {
146             if(!importPath.endsWith(dirSeparator)) importPath ~= dirSeparator;
147             if(path.startsWith(importPath)) {
148                 return path.replace(importPath, "");
149             }
150         }
151 
152         return path;
153     }
154 
155     return findModuleEntries(options).
156         filter!(a => a.baseName != "package.d" && a.baseName != "reggaefile.d").
157         map!(a => relativeToImportDirs(a.name)).
158         map!(a => replace(a.stripExtension, dirSeparator, ".")).
159         array;
160 }
161 
162 string writeUtMainFile(string[] args) {
163     auto options = getGenUtOptions(args);
164     return writeUtMainFile(options);
165 }
166 
167 string writeUtMainFile(Options options) {
168     if (options.earlyReturn) {
169         return options.fileName;
170     }
171 
172     return writeUtMainFile(options, findModuleNames(options));
173 }
174 
175 private string writeUtMainFile(Options options, in string[] modules) {
176     if (!options.fileName) {
177         options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d");
178     }
179 
180     if(!haveToUpdate(options, modules)) {
181         if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected");
182         return options.fileName;
183     } else {
184         if(options.verbose) writeln("Writing to unit test main file ", options.fileName);
185     }
186 
187     const dirName = options.fileName.dirName;
188     dirName.exists || mkdirRecurse(dirName);
189 
190 
191     auto wfile = File(options.fileName, "w");
192     wfile.write(modulesDbList(modules));
193     wfile.writeln(q{
194 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand.
195 import std.stdio;
196 import unit_threaded;
197 });
198 
199     wfile.writeln("int main(string[] args)");
200     wfile.writeln("{");
201     wfile.writeln(`    writeln("\nAutomatically generated file ` ~
202                   options.fileName.replace("\\", "\\\\") ~ `");`);
203     wfile.writeln("    writeln(`Running unit tests from dirs " ~ options.dirs.to!string ~ "`);");
204 
205     immutable indent = "                     ";
206     wfile.writeln("    return runTests!(\n" ~
207                   modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~
208                   "\n" ~ indent ~ ")\n" ~ indent ~ "(args);");
209     wfile.writeln("}");
210     wfile.close();
211 
212     return options.fileName;
213 }
214 
215 
216 private bool haveToUpdate(in Options options, in string[] modules) {
217     if (!options.fileName.exists) {
218         return true;
219     }
220 
221     auto file = File(options.fileName);
222     return file.readln.strip != modulesDbList(modules);
223 }
224 
225 
226 //used to not update the file if the file list hasn't changed
227 private string modulesDbList(in string[] modules) @safe pure nothrow {
228     return "//" ~ modules.join(",");
229 }