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