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 }