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