1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This module contains tools for testing where a sandbox is needed for creating
7 temporary files.
8 */
9 module my.test;
10 
11 import std.path : buildPath, baseName;
12 import std.format : format;
13 
14 import my.path;
15 
16 private AbsolutePath tmpDir() {
17     import std.file : thisExePath;
18     import std.path : dirName;
19 
20     return buildPath(thisExePath.dirName, "test_area").AbsolutePath;
21 }
22 
23 TestArea makeTestArea(string name, string file = __FILE__) {
24     return TestArea(buildPath(file.baseName, name));
25 }
26 
27 struct ExecResult {
28     int status;
29     string output;
30 }
31 
32 struct TestArea {
33     import std.file : rmdirRecurse, mkdirRecurse, exists, readText, chdir;
34     import std.process : wait;
35     import std.stdio : File, stdin;
36     static import std.process;
37 
38     const AbsolutePath sandboxPath;
39     private int commandLogCnt;
40 
41     private AbsolutePath root;
42     private bool chdirToRoot;
43 
44     this(string name) {
45         root = AbsolutePath(".");
46         sandboxPath = buildPath(tmpDir, name).AbsolutePath;
47 
48         if (exists(sandboxPath)) {
49             rmdirRecurse(sandboxPath);
50         }
51         mkdirRecurse(sandboxPath);
52     }
53 
54     ~this() {
55         if (chdirToRoot) {
56             chdir(root);
57         }
58     }
59 
60     /// Change current working directory to the sandbox. It is reset in the dtor.
61     void chdirToSandbox() {
62         chdirToRoot = true;
63         chdir(sandboxPath);
64     }
65 
66     /// Execute a command in the sandbox.
67     ExecResult exec(Args...)(auto ref Args args_) {
68         string[] args;
69         static foreach (a; args_)
70             args ~= a;
71 
72         const log = inSandbox(format!"command%s.log"(commandLogCnt++).Path);
73 
74         int exitCode = 1;
75         try {
76             auto fout = File(log, "w");
77             fout.writefln("%-(%s %)", args);
78 
79             exitCode = std.process.spawnProcess(args, stdin, fout, fout, null,
80                     std.process.Config.none, sandboxPath).wait;
81             fout.writeln("exit code: ", exitCode);
82         } catch (Exception e) {
83         }
84         return ExecResult(exitCode, readText(log));
85     }
86 
87     ExecResult exec(string[] args, string[string] env) {
88         const log = inSandbox(format!"command%s.log"(commandLogCnt++).Path);
89 
90         int exitCode = 1;
91         try {
92             auto fout = File(log, "w");
93             fout.writefln("%-(%s %)", args);
94 
95             exitCode = std.process.spawnProcess(args, stdin, fout, fout, env,
96                     std.process.Config.none, sandboxPath).wait;
97             fout.writeln("exit code: ", exitCode);
98         } catch (Exception e) {
99         }
100         return ExecResult(exitCode, readText(log));
101     }
102 
103     Path inSandbox(string fileName) @safe pure nothrow const {
104         return sandboxPath ~ fileName;
105     }
106 }
107 
108 /** Execute all classes in the current module as unittests.
109  *
110  * Each class must have the following members and they are executed in this order:
111  *
112  * void setup()
113  * void test()
114  * void shutdown()
115  */
116 void runClassesAsUnittest(string module_ = __MODULE__)() {
117     import std.traits : hasMember;
118 
119     mixin("import module1 = " ~ module_ ~ ";");
120 
121     static foreach (member; __traits(allMembers, module1)) {
122         {
123             alias MemberT = __traits(getMember, module1, member);
124             static if (is(MemberT == class)) {
125                 auto instance = new MemberT;
126                 static if (hasMember!(MemberT, "setup")) {
127                     static if (__traits(getVisibility, instance.setup) == "public")
128                         instance.setup();
129                 }
130                 instance.test();
131                 static if (hasMember!(MemberT, "shutdown")) {
132                     static if (__traits(getVisibility, instance.shutdown) == "public")
133                         instance.shutdown();
134                 }
135             }
136         }
137     }
138 }
139 
140 @("shall execute all classes as tests")
141 unittest {
142     runClassesAsUnittest();
143 }
144 
145 private class ATestCase {
146     void test() {
147     }
148 }
149 
150 /// Copy the content of `src``to `dst`.
151 void dirContentCopy(Path src, Path dst) {
152     import std.algorithm;
153     import std.file;
154     import std.path;
155     import my.file;
156 
157     assert(src.isDir);
158     assert(dst.isDir);
159 
160     foreach (f; dirEntries(src, SpanMode.shallow).filter!"a.isFile") {
161         auto dst_f = buildPath(dst, f.name.baseName).Path;
162         copy(f.name, dst_f);
163         if (isExecutable(Path(f.name)))
164             setExecutable(dst_f);
165     }
166 }
167 
168 /// Check that `rawRegex` match at least once for the elements of `array`.
169 auto regexIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
170     import std.regex : regex, matchFirst;
171     import unit_threaded.exception : fail;
172 
173     auto r = regex(rawRegex);
174 
175     foreach (v; array) {
176         if (!matchFirst(v, r).empty)
177             return;
178     }
179 
180     fail(formatValueInItsOwnLine("Value ",
181             rawRegex) ~ formatValueInItsOwnLine("not in ", array), file, line);
182 }
183 
184 auto regexNotIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
185     import std.regex : regex, matchFirst;
186     import unit_threaded.exception : fail;
187 
188     auto r = regex(rawRegex);
189 
190     foreach (v; array) {
191         if (!matchFirst(v, r).empty) {
192             fail(formatValueInItsOwnLine("Value ",
193                     rawRegex) ~ formatValueInItsOwnLine("in ", array), file, line);
194             return;
195         }
196     }
197 }
198 
199 string[] formatValueInItsOwnLine(T)(in string prefix, scope auto ref T value) {
200     import std.conv : to;
201     import std.traits : isSomeString;
202     import std.range.primitives : isInputRange;
203     import std.traits; // too many to list
204     import std.range; // also
205 
206     static if (isSomeString!T) {
207         // isSomeString is true for wstring and dstring,
208         // so call .to!string anyway
209         return [prefix ~ `"` ~ value.to!string ~ `"`];
210     } else static if (isInputRange!T) {
211         return formatRange(prefix, value);
212     } else {
213         return [prefix ~ convertToString(value)];
214     }
215 }
216 
217 string[] formatRange(T)(in string prefix, scope auto ref T value) {
218     import std.conv : text;
219     import std.range : ElementType;
220     import std.algorithm : map, reduce, max;
221 
222     //some versions of `text` are @system
223     auto defaultLines = () @trusted { return [prefix ~ value.text]; }();
224 
225     static if (!isInputRange!(ElementType!T))
226         return defaultLines;
227     else {
228         import std.array : array;
229 
230         const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length)
231             .reduce!max;
232         const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5)
233             || maxElementSize > 10;
234         if (!tooBigForOneLine)
235             return defaultLines;
236         return [prefix ~ "["] ~ value.map!(a => formatValueInItsOwnLine("              ",
237                 a).join("") ~ ",").array ~ "          ]";
238     }
239 }