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