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 }