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 @DontTest
125 Options testOptions(string[] args) {
126     import reggae.config: setOptions;
127     auto options = getOptions(["reggae", "-C", newTestDir] ~ args);
128     setOptions(options);
129     return options;
130 }
131 
132 Options _testProjectOptions(in string backend, in string projectName) {
133     return testOptions(["-b", backend, projectPath(projectName)]);
134 }
135 
136 Options _testProjectOptions(in string projectName) {
137     return testOptions(["-b", getValue!string, projectPath(projectName)]);
138 }
139 
140 Options _testProjectOptions(string module_)() {
141     import std.string;
142     return _testProjectOptions(module_.split(".")[0]);
143 }
144 
145 Options _testProjectOptions(string module_)(string backend) {
146     import std.string;
147     return _testProjectOptions(backend, module_.split(".")[0]);
148 }
149 
150 
151 // used to change files and cause a rebuild
152 void overwrite(in Options options, in string fileName, in string newContents) {
153     import core.thread;
154     import std.stdio;
155     import std.path;
156 
157     // ninja has problems with timestamp differences that are less than a second apart
158     if(options.backend == Backend.ninja) {
159         Thread.sleep(1.seconds);
160     }
161 
162     auto file = File(buildPath(options.workingDir, fileName), "w");
163     file.writeln(newContents);
164 }
165 
166 // used to change files and cause a rebuild
167 void overwrite(in string fileName, in string newContents) {
168     import reggae.config;
169     overwrite(options, fileName, newContents);
170 }
171 
172 
173 string[] ninja(string[] args = []) {
174     return ["ninja", "-j1"] ~ args;
175 }
176 
177 string[] make(string[] args = []) {
178     return ["make"] ~ args;
179 }
180 
181 string[] tup(string[] args = []) {
182     return ["tup"] ~ args;
183 }
184 
185 string[] binary(string path, string[] args = []) {
186     import std.path;
187     return [buildPath(path, "build"), "--norerun", "--single"] ~ args;
188 }
189 
190 string[] buildCmd(in Options options, string[] args = []) {
191     return buildCmd(options.backend, options.workingDir, args);
192 }
193 
194 string[] buildCmd(Backend backend, string path, string[] args = []) {
195     final switch(backend) {
196     case Backend.ninja:
197         return ninja(args);
198     case Backend.make:
199         return make(args);
200     case Backend.tup:
201         return tup(args);
202     case Backend.binary:
203         return binary(path, args);
204     case Backend.none:
205         return [];
206     }
207 }
208 
209 // do a build in the integration test context
210 // this uses the build description to generate the build
211 // then runs the build command
212 void doTestBuildFor(string module_ = __MODULE__)(ref Options options, string[] args = []) {
213     prepareTestBuild!module_(options);
214     justDoTestBuild!module_(options, args);
215 }
216 
217 void prepareTestBuild(string module_ = __MODULE__)(ref Options options) {
218     import std.file;
219     import std.string;
220     import std.path;
221     import std.algorithm: canFind;
222     import reggae.config;
223 
224     mkdirRecurse(buildPath(options.workingDir, ".reggae"));
225     symlink(buildPath(testsPath, "dcompile"), buildPath(options.workingDir, ".reggae", "dcompile"));
226 
227     // copy the project files over, that way the tests can modify them
228     immutable projectsPath = buildPath(origPath, "tests", "projects");
229     immutable projectName = module_.split(".")[0];
230     immutable projectPath = buildPath(projectsPath, projectName);
231 
232     // change the directory of the project to be where the build dir is
233     options.projectPath = buildPath(origPath, (options.workingDir).relativePath(origPath));
234     auto modulePath = buildPath(projectsPath, module_.split(".").join(dirSeparator));
235 
236     // copy all project files over to the build directory
237     if(module_.canFind("reggaefile")) {
238         copyProjectFiles(projectPath, options.workingDir);
239         options.projectPath = options.workingDir;
240     }
241 
242     setOptions(options);
243 }
244 
245 void justDoTestBuild(string module_ = __MODULE__)(in Options options, string[] args = []) {
246     import tests.utils;
247 
248     auto cmdArgs = buildCmd(options, args);
249     doBuildFor!module_(options, cmdArgs); // generate build
250     if(options.backend != Backend.binary && options.backend != Backend.none)
251         cmdArgs.shouldExecuteOk(WorkDir(options.workingDir));
252 }
253 
254 string[] buildCmdShouldRunOk(alias module_ = __MODULE__)(in Options options,
255                                                          string[] args = [],
256                                                          string file = __FILE__,
257                                                          ulong line = __LINE__ ) {
258     import tests.utils;
259     auto cmdArgs = buildCmd(options, args);
260 
261     string[] doTheBuild() {
262         doBuildFor!module_(options, cmdArgs);
263         return [];
264     }
265 
266     // the binary backend in the tests isn't a separate executable, but make, ninja and tup are
267     return options.backend == Backend.binary
268         ? doTheBuild
269         : cmdArgs.shouldExecuteOk(WorkDir(options.workingDir), file, line);
270 }
271 
272 // copy one of the test projects to a temporary test directory
273 void copyProjectFiles(in string projectPath, in string testPath) {
274     import std.file;
275     import std.path;
276     foreach(entry; dirEntries(projectPath, SpanMode.depth)) {
277         if(entry.isDir) continue;
278         auto tgtName = buildPath(testPath, entry.relativePath(projectPath));
279         auto dir = dirName(tgtName);
280         if(!dir.exists) mkdirRecurse(dir);
281         copy(entry, buildPath(testPath, tgtName));
282     }
283 }
284 
285 // whether a file exists in the test sandbox
286 void shouldNotExist(string fileName, string file = __FILE__, size_t line = __LINE__) {
287     import reggae.config;
288     import std.file;
289 
290     fileName = inPath(options, fileName);
291     if(fileName.exists) {
292         throw new UnitTestException(["File " ~ fileName ~ " was not expected to exist but does"]);
293     }
294 }