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 }