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