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.process: ProcessException;
186         import std.conv: text;
187         import std.array: join;
188 
189         try {
190             const res = executeInSandbox(args);
191             if(res.status != 0)
192                throw new UnitTestException(text("Could not execute `", args.join(" "), "`:\n", res.output),
193                                            file, line);
194         } catch (ProcessException e) {
195             throw new UnitTestException(text("Could not execute `", args.join(" "), "`:\n", e.msg),
196                                         file, line);
197         }
198     }
199 
200     alias shouldExecuteOk = shouldSucceed;
201 
202     /**
203        Executing `args` should fail
204      */
205     void shouldFail(string file = __FILE__, size_t line = __LINE__)
206                    (in string[] args...)
207         @safe const
208     {
209         import unit_threaded.exception: UnitTestException;
210         import std.process: ProcessException;
211         import std.conv: text;
212         import std.array: join;
213 
214         try {
215             const res = executeInSandbox(args);
216             if(res.status == 0)
217                 throw new UnitTestException(
218                     text("`", args.join(" "), "` should have failed but didn't:\n", res.output),
219                     file,
220                     line);
221         } catch (ProcessException) {} // Allow failure by ProcessException.
222     }
223 
224 
225 private:
226 
227     auto executeInSandbox(in string[] args) @safe const {
228         import std.process: execute, Config;
229         import std.algorithm: startsWith;
230         import std.array: replace;
231 
232         const string[string] env = null;
233         const config = Config.none;
234         const maxOutput = size_t.max;
235         const workDir = testPath;
236 
237         const executable = args[0].startsWith("./")
238             ? inSandboxPath(args[0].replace("./", ""))
239             : args[0];
240 
241         return execute(executable ~ args[1..$], env, config, maxOutput, workDir);
242     }
243 
244     static string newTestDir() {
245         import std.file: exists, mkdirRecurse;
246 
247         if(!sandboxesPath.exists) {
248             () @trusted { mkdirRecurse(sandboxesPath); }();
249         }
250 
251         return makeTempDir();
252     }
253 
254     static string makeTempDir() {
255         import std.algorithm: copy;
256         import std.exception: enforce;
257         import std.conv: to;
258         import std..string: fromStringz;
259         import core.stdc..string: strerror;
260         import core.stdc.errno: errno;
261 
262         char[2048] template_;
263         // the YYYYYY is to give some randomness on systems
264         // where it is necessary to have more than 26 items
265         // harmless elsewhere; the name is random stuff anyway
266         copy(buildPath(sandboxesPath, "YYYYYYXXXXXX") ~ '\0', template_[]);
267 
268         auto path = () @trusted { return mkdtemp(&template_[0]).to!string; }();
269 
270         enforce(path != "",
271                 "\n" ~
272                 "Failed to create temporary directory name using template '" ~
273                 () @trusted { return fromStringz(&template_[0]); }() ~ "': " ~
274                 () @trusted { return strerror(errno).to!string; }());
275 
276         return path.absolutePath;
277     }
278 }