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 }