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