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