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;
27     if(!Sandbox.sandboxPath.exists) return;
28 
29     foreach(entry; dirEntries(Sandbox.sandboxPath, SpanMode.shallow)) {
30         if(isDir(entry.name)) {
31             rmdirRecurse(entry);
32         }
33     }
34 }
35 
36 
37 @safe:
38 
39 /**
40  Responsible for creating a temporary directory to serve as a sandbox where
41  files can be created, written to or deleted.
42  */
43 struct Sandbox {
44     import std.path;
45 
46     enum defaultSandboxPath = buildPath("tmp", "unit-threaded");
47     static string sandboxPath = defaultSandboxPath;
48     string testPath;
49 
50     /// Instantiate a Sandbox object
51     static Sandbox opCall() {
52         Sandbox ret;
53         ret.testPath = newTestDir;
54         return ret;
55     }
56 
57     ///
58     @safe unittest {
59         auto sb = Sandbox();
60         assert(sb.testPath != "");
61     }
62 
63     static void setPath(string path) {
64         import std.file;
65         sandboxPath = path;
66         if(!sandboxPath.exists) () @trusted { mkdirRecurse(sandboxPath); }();
67     }
68 
69     ///
70     @safe unittest {
71         import std.file;
72         import std.path;
73         import unit_threaded.should;
74 
75         Sandbox.sandboxPath.shouldEqual(defaultSandboxPath);
76 
77         immutable newPath = buildPath("foo", "bar", "baz");
78         assert(!newPath.exists);
79         Sandbox.setPath(newPath);
80         assert(newPath.exists);
81         scope(exit) () @trusted { rmdirRecurse("foo"); }();
82         Sandbox.sandboxPath.shouldEqual(newPath);
83 
84         with(immutable Sandbox()) {
85             writeFile("newPath.txt");
86             assert(buildPath(newPath, testPath, "newPath.txt").exists);
87         }
88 
89         Sandbox.resetPath;
90         Sandbox.sandboxPath.shouldEqual(defaultSandboxPath);
91     }
92 
93     static void resetPath() {
94         sandboxPath = defaultSandboxPath;
95     }
96 
97     /// Write a file to the sandbox
98     void writeFile(in string fileName, in string output = "") const {
99         import std.stdio: File;
100         import std.path: buildPath, dirName;
101         import std.file: mkdirRecurse;
102 
103         () @trusted { mkdirRecurse(buildPath(testPath, fileName.dirName)); }();
104         File(buildPath(testPath, fileName), "w").writeln(output);
105     }
106 
107     /// Write a file to the sanbox
108     void writeFile(in string fileName, in string[] lines) const {
109         import std.array;
110         writeFile(fileName, lines.join("\n"));
111     }
112 
113     ///
114     @safe unittest {
115         import std.file;
116         import std.path;
117 
118         with(immutable Sandbox()) {
119             assert(!buildPath(testPath, "foo.txt").exists);
120             writeFile("foo.txt");
121             assert(buildPath(testPath, "foo.txt").exists);
122         }
123     }
124 
125     @safe unittest {
126         import std.file: exists;
127         import std.path: buildPath;
128 
129         with(immutable Sandbox()) {
130             writeFile("foo/bar.txt");
131             assert(buildPath(testPath, "foo", "bar.txt").exists);
132         }
133     }
134 
135     /// Assert that a file exists in the sandbox
136     void shouldExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
137         import std.file;
138         import std.path;
139         import unit_threaded.should: fail;
140 
141         fileName = buildPath(testPath, fileName);
142         if(!fileName.exists)
143             fail("Expected " ~ fileName ~ " to exist but it didn't", file, line);
144     }
145 
146     ///
147     @safe unittest {
148         with(immutable Sandbox()) {
149             import unit_threaded.should;
150 
151             shouldExist("bar.txt").shouldThrow;
152             writeFile("bar.txt");
153             shouldExist("bar.txt");
154         }
155     }
156 
157     /// Assert that a file does not exist in the sandbox
158     void shouldNotExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
159         import std.file;
160         import std.path;
161         import unit_threaded.should;
162 
163         fileName = buildPath(testPath, fileName);
164         if(fileName.exists)
165             fail("Expected " ~ fileName ~ " to not exist but it did", file, line);
166     }
167 
168     ///
169     @safe unittest {
170         with(immutable Sandbox()) {
171             import unit_threaded.should;
172 
173             shouldNotExist("baz.txt");
174             writeFile("baz.txt");
175             shouldNotExist("baz.txt").shouldThrow;
176         }
177     }
178 
179     /// read a file in the test sandbox and verify its contents
180     void shouldEqualLines(in string fileName, in string[] lines,
181                           string file = __FILE__, size_t line = __LINE__) const @trusted {
182         import std.file;
183         import std.string;
184         import unit_threaded.should;
185 
186         readText(buildPath(testPath, fileName)).chomp.splitLines
187             .shouldEqual(lines, file, line);
188     }
189 
190     ///
191     @safe unittest {
192         with(immutable Sandbox()) {
193             import unit_threaded.should;
194 
195             writeFile("lines.txt", ["foo", "toto"]);
196             shouldEqualLines("lines.txt", ["foo", "bar"]).shouldThrow;
197             shouldEqualLines("lines.txt", ["foo", "toto"]);
198         }
199     }
200 
201 private:
202 
203     static string newTestDir() {
204         import std.file: exists, mkdirRecurse;
205 
206         if(!sandboxPath.exists) {
207             () @trusted { mkdirRecurse(sandboxPath); }();
208         }
209 
210         return makeTempDir();
211     }
212 
213     static string makeTempDir() {
214         import std.algorithm: copy;
215         import std.exception: enforce;
216         import std.conv: to;
217         import core.stdc.string: strerror;
218         import core.stdc.errno: errno;
219 
220         char[100] template_;
221         copy(buildPath(sandboxPath, "XXXXXX") ~ '\0', template_[]);
222 
223         auto ret = () @trusted { return mkdtemp(&template_[0]).to!string; }();
224 
225         enforce(ret != "", "Failed to create temporary directory name: " ~
226                 () @trusted { return strerror(errno).to!string; }());
227 
228         return ret.absolutePath;
229     }
230 }