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 }