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 }