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