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;
100         import std.path;
101         File(buildPath(testPath, fileName), "w").writeln(output);
102     }
103 
104     /// Write a file to the sanbox
105     void writeFile(in string fileName, in string[] lines) const {
106         import std.array;
107         writeFile(fileName, lines.join("\n"));
108     }
109 
110     ///
111     @safe unittest {
112         import std.file;
113         import std.path;
114 
115         with(immutable Sandbox()) {
116             assert(!buildPath(testPath, "foo.txt").exists);
117             writeFile("foo.txt");
118             assert(buildPath(testPath, "foo.txt").exists);
119         }
120     }
121 
122     /// Assert that a file exists in the sandbox
123     void shouldExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
124         import std.file;
125         import std.path;
126         import unit_threaded.should: fail;
127 
128         fileName = buildPath(testPath, fileName);
129         if(!fileName.exists)
130             fail("Expected " ~ fileName ~ " to exist but it didn't", file, line);
131     }
132 
133     ///
134     @safe unittest {
135         with(immutable Sandbox()) {
136             import unit_threaded.should;
137 
138             shouldExist("bar.txt").shouldThrow;
139             writeFile("bar.txt");
140             shouldExist("bar.txt");
141         }
142     }
143 
144     /// Assert that a file does not exist in the sandbox
145     void shouldNotExist(string fileName, in string file = __FILE__, in size_t line = __LINE__) const {
146         import std.file;
147         import std.path;
148         import unit_threaded.should;
149 
150         fileName = buildPath(testPath, fileName);
151         if(fileName.exists)
152             fail("Expected " ~ fileName ~ " to not exist but it did", file, line);
153     }
154 
155     ///
156     @safe unittest {
157         with(immutable Sandbox()) {
158             import unit_threaded.should;
159 
160             shouldNotExist("baz.txt");
161             writeFile("baz.txt");
162             shouldNotExist("baz.txt").shouldThrow;
163         }
164     }
165 
166     /// read a file in the test sandbox and verify its contents
167     void shouldEqualLines(in string fileName, in string[] lines,
168                           string file = __FILE__, size_t line = __LINE__) const @trusted {
169         import std.file;
170         import std..string;
171         import unit_threaded.should;
172 
173         readText(buildPath(testPath, fileName)).chomp.splitLines
174             .shouldEqual(lines, file, line);
175     }
176 
177     ///
178     @safe unittest {
179         with(immutable Sandbox()) {
180             import unit_threaded.should;
181 
182             writeFile("lines.txt", ["foo", "toto"]);
183             shouldEqualLines("lines.txt", ["foo", "bar"]).shouldThrow;
184             shouldEqualLines("lines.txt", ["foo", "toto"]);
185         }
186     }
187 
188 private:
189 
190     static string newTestDir() {
191         import std.file: exists, mkdirRecurse;
192 
193         if(!sandboxPath.exists) {
194             () @trusted { mkdirRecurse(sandboxPath); }();
195         }
196 
197         return makeTempDir();
198     }
199 
200     static string makeTempDir() {
201         import std.algorithm: copy;
202         import std.exception: enforce;
203         import std.conv: to;
204         import core.stdc..string: strerror;
205         import core.stdc.errno: errno;
206 
207         char[100] template_;
208         copy(buildPath(sandboxPath, "XXXXXX") ~ '\0', template_[]);
209 
210         auto ret = () @trusted { return mkdtemp(&template_[0]).to!string; }();
211 
212         enforce(ret != "", "Failed to create temporary directory name: " ~
213                 () @trusted { return strerror(errno).to!string; }());
214 
215         return ret.absolutePath;
216     }
217 }