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 void dirContentCopy(Path src, Path dst) {
109     import std.algorithm;
110     import std.file;
111     import std.path;
112     import my.file;
113 
114     assert(src.isDir);
115     assert(dst.isDir);
116 
117     foreach (f; dirEntries(src, SpanMode.shallow).filter!"a.isFile") {
118         auto dst_f = buildPath(dst, f.name.baseName).Path;
119         copy(f.name, dst_f);
120         if (isExecutable(Path(f.name)))
121             setExecutable(dst_f);
122     }
123 }
124 
125 auto regexIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
126     import std.regex : regex, matchFirst;
127 
128     auto r = regex(rawRegex);
129 
130     foreach (v; array) {
131         if (!matchFirst(v, r).empty)
132             return;
133     }
134 
135     import unit_threaded.exception : fail;
136 
137     fail(formatValueInItsOwnLine("Value ",
138             rawRegex) ~ formatValueInItsOwnLine("not in ", array), file, line);
139 }
140 
141 auto regexNotIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
142     import std.regex : regex, matchFirst;
143     import unit_threaded.exception : fail;
144 
145     auto r = regex(rawRegex);
146 
147     foreach (v; array) {
148         if (!matchFirst(v, r).empty) {
149             fail(formatValueInItsOwnLine("Value ",
150                     rawRegex) ~ formatValueInItsOwnLine("in ", array), file, line);
151             return;
152         }
153     }
154 }
155 
156 string[] formatValueInItsOwnLine(T)(in string prefix, scope auto ref T value) {
157     import std.conv : to;
158     import std.traits : isSomeString;
159     import std.range.primitives : isInputRange;
160     import std.traits; // too many to list
161     import std.range; // also
162 
163     static if (isSomeString!T) {
164         // isSomeString is true for wstring and dstring,
165         // so call .to!string anyway
166         return [prefix ~ `"` ~ value.to!string ~ `"`];
167     } else static if (isInputRange!T) {
168         return formatRange(prefix, value);
169     } else {
170         return [prefix ~ convertToString(value)];
171     }
172 }
173 
174 string[] formatRange(T)(in string prefix, scope auto ref T value) {
175     import std.conv : text;
176     import std.range : ElementType;
177     import std.algorithm : map, reduce, max;
178 
179     //some versions of `text` are @system
180     auto defaultLines = () @trusted { return [prefix ~ value.text]; }();
181 
182     static if (!isInputRange!(ElementType!T))
183         return defaultLines;
184     else {
185         import std.array : array;
186 
187         const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length)
188             .reduce!max;
189         const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5)
190             || maxElementSize > 10;
191         if (!tooBigForOneLine)
192             return defaultLines;
193         return [prefix ~ "["] ~ value.map!(a => formatValueInItsOwnLine("              ",
194                 a).join("") ~ ",").array ~ "          ]";
195     }
196 }