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