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;
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;
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 
82     bool earlyReturn() @safe pure nothrow const {
83         return help || showVersion;
84     }
85 }
86 
87 
88 Options getGenUtOptions(string[] args) {
89     import std.getopt;
90 
91     Options options;
92     auto getOptRes = getopt(
93         args,
94         "verbose|v", "Verbose mode.", &options.verbose,
95         "file|f", "The filename to write. Will use a temporary if not set.", &options.fileName,
96         "version", "Show version.", &options.showVersion,
97         );
98 
99     if (getOptRes.helpWanted) {
100         defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", getOptRes.options);
101         options.help = true;
102         return options;
103     }
104 
105     if (options.showVersion) {
106         writeln("unit_threaded.runtime version v0.0.1");
107         return options;
108     }
109 
110     options.dirs = args.length <= 1 ? ["."] : args[1 .. $];
111 
112     if (options.verbose) {
113         writeln(__FILE__, ": finding all test cases in ", options.dirs);
114     }
115 
116     return options;
117 }
118 
119 
120 DirEntry[] findModuleEntries(in string[] dirs) {
121 
122     DirEntry[] modules;
123     foreach (dir; dirs) {
124         enforce(isDir(dir), dir ~ " is not a directory name");
125         auto entries = dirEntries(dir, "*.d", SpanMode.depth);
126         auto normalised = entries.map!(a => DirEntry(buildNormalizedPath(a.name)));
127         modules ~= normalised.array;
128     }
129 
130     return modules;
131 }
132 
133 string[] findModuleNames(in string[] dirs) {
134     import std.path : dirSeparator;
135 
136     //cut off extension
137     return findModuleEntries(dirs).
138         map!(a => replace(a.name[0 .. $ - 2], dirSeparator, ".")).
139         array;
140 }
141 
142 string writeUtMainFile(string[] args) {
143     auto options = getGenUtOptions(args);
144     return writeUtMainFile(options);
145 }
146 
147 string writeUtMainFile(Options options) {
148     if (options.earlyReturn) {
149         return options.fileName;
150     }
151 
152     return writeUtMainFile(options, findModuleNames(options.dirs));
153 }
154 
155 private string writeUtMainFile(Options options, in string[] modules) {
156     if (!options.fileName) {
157         options.fileName = buildPath(tempDir, getcwd[1..$], "ut.d");
158     }
159 
160     if(!haveToUpdate(options, modules)) {
161         if(options.verbose) writeln("Not writing to ", options.fileName, ": no changes detected");
162         return options.fileName;
163     } else {
164         if(options.verbose) writeln("Writing to unit test main file ", options.fileName);
165     }
166 
167     const dirName = options.fileName.dirName;
168     dirName.exists || mkdirRecurse(dirName);
169 
170 
171     auto wfile = File(options.fileName, "w");
172     wfile.write(modulesDbList(modules));
173     wfile.writeln(q{
174 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand.
175 import std.stdio;
176 import unit_threaded;
177 });
178 
179     wfile.writeln("int main(string[] args)");
180     wfile.writeln("{");
181     wfile.writeln(`    writeln("\nAutomatically generated file ` ~
182                   options.fileName.replace("\\", "\\\\") ~ `");`);
183     wfile.writeln("    writeln(`Running unit tests from dirs " ~ options.dirs.to!string ~ "`);");
184 
185     immutable indent = "                     ";
186     wfile.writeln("    return runTests!(\n" ~
187                   modules.map!(a => indent ~ `"` ~ a ~ `"`).join(",\n") ~
188                   "\n" ~ indent ~ ")\n" ~ indent ~ "(args);");
189     wfile.writeln("}");
190     wfile.close();
191 
192     return options.fileName;
193 }
194 
195 
196 private bool haveToUpdate(in Options options, in string[] modules) {
197     if (!options.fileName.exists) {
198         return true;
199     }
200 
201     auto file = File(options.fileName);
202     return file.readln.strip != modulesDbList(modules);
203 }
204 
205 
206 //used to not update the file if the file list hasn't changed
207 private string modulesDbList(in string[] modules) @safe pure nothrow {
208     return "//" ~ modules.join(",");
209 }