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