1 module tests.it;
2 
3 public import reggae;
4 public import unit_threaded;
5 
6 immutable string origPath;
7 
8 shared static this() {
9     import std.file: mkdirRecurse, rmdirRecurse, getcwd, dirEntries, SpanMode, exists, isDir;
10     import std.path: buildNormalizedPath, absolutePath;
11     import std.algorithm: map, find;
12 
13     auto paths = [".", ".."].map!(a => buildNormalizedPath(getcwd, a))
14         .find!(a => buildNormalizedPath(a, "dub.json").exists);
15     assert(!paths.empty, "Error: Cannot find reggae top dir using dub.json");
16     origPath = paths.front.absolutePath;
17 
18     if(testsPath.exists) {
19         writelnUt("[IT] Removing old test path ", testsPath);
20         foreach(entry; dirEntries(testsPath, SpanMode.shallow)) {
21             if(isDir(entry.name)) {
22                 rmdirRecurse(entry);
23             }
24         }
25     }
26 
27     writelnUt("[IT] Creating new test path ", testsPath);
28     mkdirRecurse(testsPath);
29 
30     buildDCompile();
31 }
32 
33 private void buildDCompile() {
34     import std.meta: aliasSeqOf;
35     import std.exception: enforce;
36     import std.conv: text;
37     import std.stdio: writeln;
38     import std.algorithm: any;
39     import std.file: exists;
40     import std.path: buildPath;
41     import std.process: execute, Config;
42     import std.array: join;
43     import reggae.file;
44 
45     enum fileNames = ["dcompile.d", "dependencies.d"];
46 
47     const exeName = buildPath("tmp", "dcompile");
48     immutable needToRecompile =
49         !exeName.exists ||
50         fileNames.
51         any!(a => buildPath(origPath, "payload", "reggae", a).
52         newerThan(buildPath(testsPath, a)));
53 
54     if(!needToRecompile)
55         return;
56 
57     writeln("[IT] Building dcompile");
58 
59     foreach(fileName; aliasSeqOf!fileNames) {
60         writeFile!fileName;
61     }
62 
63     enum args = ["dmd", "-ofdcompile"] ~ fileNames;
64     const string[string] env = null;
65     Config config = Config.none;
66     size_t maxOutput = size_t.max;
67     const workDir = testsPath;
68 
69     immutable res = execute(args, env, config, maxOutput, workDir);
70     enforce(res.status == 0, text("Could not execute '", args.join(" "), "':\n", res.output));
71 }
72 
73 private void writeFile(string fileName)() {
74     import std.stdio;
75     import std.path;
76     auto file = File(buildPath(testsPath, fileName), "w");
77     file.write(import(fileName));
78 }
79 
80 
81 string testsPath() @safe {
82     import std.file;
83     import std.path;
84     return buildNormalizedPath(origPath, "tmp");
85 }
86 
87 
88 string inOrigPath(T...)(T parts) {
89     return inPath(origPath, parts);
90 }
91 
92 string inPath(T...)(in string path, T parts) {
93     import std.path;
94     return buildPath(path, parts).absolutePath;
95 }
96 
97 string inPath(T...)(in Options options, T parts) {
98     return inPath(options.workingDir, parts);
99 }
100 
101 
102 string projectPath(in string name) {
103     import std.path;
104     return inOrigPath("tests", "projects", name);
105 }
106 
107 version(Posix)
108     extern(C) char* mkdtemp(char*);
109 else
110     char* mkdtemp(char*) {
111         throw new Exception("No mkdtemp function on Windows");
112     }
113 
114 string newTestDir() {
115     import std.conv;
116     import std.path;
117     import std.algorithm;
118 
119     char[100] template_;
120     std.algorithm.copy(buildPath(testsPath, "XXXXXX") ~ '\0', template_[]);
121     auto ret = mkdtemp(&template_[0]).to!string;
122 
123     return ret.absolutePath;
124 }
125 
126 @DontTest
127 Options testOptions(string[] args) {
128     import reggae.config: setOptions;
129     auto options = getOptions(["reggae", "-C", newTestDir] ~ args);
130     setOptions(options);
131     return options;
132 }
133 
134 Options _testProjectOptions(in string backend, in string projectName) {
135     return testOptions(["-b", backend, projectPath(projectName)]);
136 }
137 
138 Options _testProjectOptions(in string projectName) {
139     return testOptions(["-b", getValue!string, projectPath(projectName)]);
140 }
141 
142 Options _testProjectOptions(string module_)() {
143     import std.string;
144     return _testProjectOptions(module_.split(".")[0]);
145 }
146 
147 Options _testProjectOptions(string module_)(string backend) {
148     import std.string;
149     return _testProjectOptions(backend, module_.split(".")[0]);
150 }
151 
152 
153 // used to change files and cause a rebuild
154 void overwrite(in Options options, in string fileName, in string newContents) {
155     import core.thread;
156     import std.stdio;
157     import std.path;
158 
159     // ninja has problems with timestamp differences that are less than a second apart
160     if(options.backend == Backend.ninja) {
161         Thread.sleep(1.seconds);
162     }
163 
164     auto file = File(buildPath(options.workingDir, fileName), "w");
165     file.writeln(newContents);
166 }
167 
168 // used to change files and cause a rebuild
169 void overwrite(in string fileName, in string newContents) {
170     import reggae.config;
171     overwrite(options, fileName, newContents);
172 }
173 
174 
175 string[] ninja(string[] args = []) {
176     return ["ninja", "-j1"] ~ args;
177 }
178 
179 string[] make(string[] args = []) {
180     return ["make"] ~ args;
181 }
182 
183 string[] tup(string[] args = []) {
184     return ["tup"] ~ args;
185 }
186 
187 string[] binary(string path, string[] args = []) {
188     import std.path;
189     return [buildPath(path, "build"), "--norerun", "--single"] ~ args;
190 }
191 
192 string[] buildCmd(in Options options, string[] args = []) {
193     return buildCmd(options.backend, options.workingDir, args);
194 }
195 
196 string[] buildCmd(Backend backend, string path, string[] args = []) {
197     final switch(backend) {
198     case Backend.ninja:
199         return ninja(args);
200     case Backend.make:
201         return make(args);
202     case Backend.tup:
203         return tup(args);
204     case Backend.binary:
205         return binary(path, args);
206     case Backend.none:
207         return [];
208     }
209 }
210 
211 // do a build in the integration test context
212 // this uses the build description to generate the build
213 // then runs the build command
214 void doTestBuildFor(string module_ = __MODULE__)(ref Options options, string[] args = []) {
215     prepareTestBuild!module_(options);
216     justDoTestBuild!module_(options, args);
217 }
218 
219 void prepareTestBuild(string module_ = __MODULE__)(ref Options options) {
220     version(Windows) {
221         assert(false);
222     } else {
223         import std.file: symlink, mkdirRecurse;
224         import std.string;
225         import std.path;
226         import std.algorithm: canFind;
227         import reggae.config;
228 
229         mkdirRecurse(buildPath(options.workingDir, ".reggae"));
230         symlink(buildPath(testsPath, "dcompile"), buildPath(options.workingDir, ".reggae", "dcompile"));
231 
232         // copy the project files over, that way the tests can modify them
233         immutable projectsPath = buildPath(origPath, "tests", "projects");
234         immutable projectName = module_.split(".")[0];
235         immutable projectPath = buildPath(projectsPath, projectName);
236 
237         // change the directory of the project to be where the build dir is
238         options.projectPath = buildPath(origPath, (options.workingDir).relativePath(origPath));
239         auto modulePath = buildPath(projectsPath, module_.split(".").join(dirSeparator));
240 
241         // copy all project files over to the build directory
242         if(module_.canFind("reggaefile")) {
243             copyProjectFiles(projectPath, options.workingDir);
244             options.projectPath = options.workingDir;
245         }
246 
247         setOptions(options);
248     }
249 }
250 
251 void justDoTestBuild(string module_ = __MODULE__)(in Options options, string[] args = []) {
252     import tests.utils;
253 
254     auto cmdArgs = buildCmd(options, args);
255     doBuildFor!module_(options, cmdArgs); // generate build
256     if(options.backend != Backend.binary && options.backend != Backend.none)
257         cmdArgs.shouldExecuteOk(WorkDir(options.workingDir));
258 }
259 
260 string[] buildCmdShouldRunOk(alias module_ = __MODULE__)(in Options options,
261                                                          string[] args = [],
262                                                          string file = __FILE__,
263                                                          size_t line = __LINE__ ) {
264     import tests.utils;
265     auto cmdArgs = buildCmd(options, args);
266 
267     string[] doTheBuild() {
268         doBuildFor!module_(options, cmdArgs);
269         return [];
270     }
271 
272     // the binary backend in the tests isn't a separate executable, but make, ninja and tup are
273     return options.backend == Backend.binary
274         ? doTheBuild
275         : cmdArgs.shouldExecuteOk(WorkDir(options.workingDir), file, line);
276 }
277 
278 // copy one of the test projects to a temporary test directory
279 void copyProjectFiles(in string projectPath, in string testPath) {
280     import std.file;
281     import std.path;
282     foreach(entry; dirEntries(projectPath, SpanMode.depth)) {
283         if(entry.isDir) continue;
284         auto tgtName = buildPath(testPath, entry.relativePath(projectPath));
285         auto dir = dirName(tgtName);
286         if(!dir.exists) mkdirRecurse(dir);
287         copy(entry, buildPath(testPath, tgtName));
288     }
289 }
290 
291 // whether a file exists in the test sandbox
292 void shouldNotExist(string fileName, string file = __FILE__, size_t line = __LINE__) {
293     import reggae.config;
294     import std.file;
295 
296     fileName = inPath(options, fileName);
297     if(fileName.exists) {
298         throw new UnitTestException(["File " ~ fileName ~ " was not expected to exist but does"]);
299     }
300 }