1 /** 2 * This module implements functionality helpful for writing integration tests 3 * as opposed to the unit variety where unit-tests are defined as not 4 * having global side-effects. In constrast, this module implements 5 * assertions that check for global side-effects such as writing to the 6 * file system. 7 */ 8 9 module unit_threaded.integration; 10 11 version(Windows) { 12 extern(C) char* mktemp(char* template_); 13 14 char* mkdtemp(char* t) { 15 version(unitUnthreaded) 16 return mkdtempImpl(t); 17 else { 18 synchronized { 19 return mkdtempImpl(t); 20 } 21 } 22 } 23 24 char* mkdtempImpl(char* t) { 25 // mktemp can actually only return up to 26 unique names per 26 // the documentation at MSDN. To hack around this, I am just 27 // putting in some random letters at some YYYYYY defined below 28 // to make conflicts less likely. 29 30 import core.stdc..string : strstr; 31 import core.stdc.stdlib : rand; 32 import std.file: mkdirRecurse; 33 import std..string: fromStringz; 34 35 char* where = strstr(t, "YYYYYY"); 36 assert(where !is null); 37 while(*where == 'Y') { 38 *where++ = rand() % 26 + 'A'; 39 } 40 41 char* result = mktemp(t); 42 43 if(result is null) return null; 44 mkdirRecurse(result.fromStringz); 45 46 return result; 47 } 48 49 } else { 50 extern(C) char* mkdtemp(char* template_); 51 } 52 53 54 shared static this() { 55 import std.file: exists, rmdirRecurse; 56 57 if(Sandbox.sandboxesPath.exists) 58 rmdirRecurse(Sandbox.sandboxesPath); 59 } 60 61 62 @safe: 63 64 /** 65 Responsible for creating a temporary directory to serve as a sandbox where 66 files can be created, written to or deleted. 67 */ 68 struct Sandbox { 69 import std.path; 70 71 enum defaultSandboxesPath = buildPath("tmp", "unit-threaded"); 72 static string sandboxesPath = defaultSandboxesPath; 73 string testPath; 74 75 /// Instantiate a Sandbox object 76 static Sandbox opCall() { 77 Sandbox ret; 78 ret.testPath = newTestDir; 79 return ret; 80 } 81 82 83 static void setPath(string path) { 84 import std.file: exists, mkdirRecurse; 85 sandboxesPath = path; 86 if(!sandboxesPath.exists) () @trusted { mkdirRecurse(sandboxesPath); }(); 87 } 88 89 90 static void resetPath() { 91 sandboxesPath = defaultSandboxesPath; 92 } 93 94 /// Write a file to the sandbox 95 void writeFile(in string fileName, in string output = "") const { 96 import std.stdio: File; 97 import std.path: buildPath, dirName; 98 import std.file: mkdirRecurse; 99 100 () @trusted { mkdirRecurse(buildPath(testPath, fileName.dirName)); }(); 101 File(buildPath(testPath, fileName), "wb").writeln(output); 102 } 103 104 /// Write a file to the sandbox 105 void writeFile(in string fileName, in string[] lines) const { 106 import std.array; 107 writeFile(fileName, lines.join("\n")); 108 } 109 110 111 /// Assert that a file exists in the sandbox 112 void shouldExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const { 113 import std.file: exists; 114 import std.path: buildPath; 115 import unit_threaded.exception: fail; 116 117 fileName = buildPath(testPath, fileName); 118 if(!fileName.exists) 119 fail("Expected " ~ fileName ~ " to exist but it didn't", file, line); 120 } 121 122 /// Assert that a file does not exist in the sandbox 123 void shouldNotExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const { 124 import std.file: exists; 125 import std.path: buildPath; 126 import unit_threaded.exception: fail; 127 128 fileName = buildPath(testPath, fileName); 129 if(fileName.exists) 130 fail("Expected " ~ fileName ~ " to not exist but it did", file, line); 131 } 132 133 /// read a file in the test sandbox and verify its contents 134 void shouldEqualContent(in string fileName, in string content, 135 in string file = __FILE__, in size_t line = __LINE__) 136 const 137 { 138 import std.file: readText; 139 import std..string: chomp, splitLines; 140 import unit_threaded.assertions: shouldEqual; 141 142 readText(buildPath(testPath, fileName)).shouldEqual(content, file, line); 143 } 144 145 /// read a file in the test sandbox and verify its contents 146 void shouldEqualLines(in string fileName, in string[] lines, 147 string file = __FILE__, size_t line = __LINE__) 148 const 149 { 150 import std.file: readText; 151 import std..string: chomp, splitLines; 152 import unit_threaded.assertions: shouldEqual; 153 154 readText(buildPath(testPath, fileName)).chomp.splitLines 155 .shouldEqual(lines, file, line); 156 } 157 158 // `fileName` should contain `needle` 159 void fileShouldContain(in string fileName, 160 in string needle, 161 in string file = __FILE__, 162 in size_t line = __LINE__) 163 { 164 import std.file: readText; 165 import unit_threaded.assertions: shouldBeIn; 166 needle.shouldBeIn(readText(inSandboxPath(fileName)), file, line); 167 } 168 169 string sandboxPath() @safe @nogc pure nothrow const { 170 return testPath; 171 } 172 173 string inSandboxPath(in string fileName) @safe pure nothrow const { 174 import std.path: buildPath; 175 return buildPath(sandboxPath, fileName); 176 } 177 178 /** 179 Executing `args` should succeed 180 */ 181 void shouldSucceed(string file = __FILE__, size_t line = __LINE__) 182 (in string[] args...) 183 @safe const 184 { 185 import unit_threaded.exception: UnitTestException; 186 import std.process: ProcessException; 187 import std.conv: text; 188 import std.array: join; 189 190 try { 191 const res = executeInSandbox(args); 192 if(res.status != 0) 193 throw new UnitTestException(text("Could not execute `", args.join(" "), "`:\n", res.output), 194 file, line); 195 } catch (ProcessException e) { 196 throw new UnitTestException(text("Could not execute `", args.join(" "), "`:\n", e.msg), 197 file, line); 198 } 199 } 200 201 alias shouldExecuteOk = shouldSucceed; 202 203 /** 204 Executing `args` should fail 205 */ 206 void shouldFail(string file = __FILE__, size_t line = __LINE__) 207 (in string[] args...) 208 @safe const 209 { 210 import unit_threaded.exception: UnitTestException; 211 import std.process: ProcessException; 212 import std.conv: text; 213 import std.array: join; 214 215 try { 216 const res = executeInSandbox(args); 217 if(res.status == 0) 218 throw new UnitTestException( 219 text("`", args.join(" "), "` should have failed but didn't:\n", res.output), 220 file, 221 line); 222 } catch (ProcessException) {} // Allow failure by ProcessException. 223 } 224 225 226 private: 227 228 auto executeInSandbox(in string[] args) @safe const { 229 import std.process: execute, Config; 230 import std.algorithm: startsWith; 231 import std.array: replace; 232 233 const string[string] env = null; 234 const config = Config.none; 235 const maxOutput = size_t.max; 236 const workDir = testPath; 237 238 const executable = args[0].startsWith("./") 239 ? inSandboxPath(args[0].replace("./", "")) 240 : args[0]; 241 242 return execute(executable ~ args[1..$], env, config, maxOutput, workDir); 243 } 244 245 static string newTestDir() { 246 import std.file: exists, mkdirRecurse; 247 248 if(!sandboxesPath.exists) { 249 () @trusted { mkdirRecurse(sandboxesPath); }(); 250 } 251 252 return makeTempDir(); 253 } 254 255 static string makeTempDir() { 256 import std.algorithm: copy; 257 import std.exception: enforce; 258 import std.conv: to; 259 import std..string: fromStringz; 260 import core.stdc..string: strerror; 261 import core.stdc.errno: errno; 262 263 char[2048] template_; 264 // the YYYYYY is to give some randomness on systems 265 // where it is necessary to have more than 26 items 266 // harmless elsewhere; the name is random stuff anyway 267 copy(buildPath(sandboxesPath, "YYYYYYXXXXXX") ~ '\0', template_[]); 268 269 auto path = () @trusted { return mkdtemp(&template_[0]).to!string; }(); 270 271 enforce(path != "", 272 "\n" ~ 273 "Failed to create temporary directory name using template '" ~ 274 () @trusted { return fromStringz(&template_[0]); }() ~ "': " ~ 275 () @trusted { return strerror(errno).to!string; }()); 276 277 return path.absolutePath; 278 } 279 }