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