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 TestArea {
28     import std.file : rmdirRecurse, mkdirRecurse, exists, readText;
29     import std.process : wait;
30     import std.stdio : File, stdin;
31     static import std.process;
32 
33     const AbsolutePath sandboxPath;
34     private int commandLogCnt;
35 
36     this(string name) {
37         sandboxPath = buildPath(tmpDir, name).AbsolutePath;
38 
39         if (exists(sandboxPath)) {
40             rmdirRecurse(sandboxPath);
41         }
42         mkdirRecurse(sandboxPath);
43     }
44 
45     /// Execute a command in the sandbox.
46     string exec(Args...)(auto ref Args args_) {
47         string[] args;
48         static foreach (a; args_)
49             args ~= a;
50 
51         const log = inSandbox(format!"command%s.log"(commandLogCnt++).Path);
52 
53         try {
54             auto fout = File(log, "w");
55             fout.writefln("%-(%s %)", args);
56 
57             auto exitCode = std.process.spawnProcess(args, stdin, fout, fout,
58                     env, std.process.Config.none, sandboxPath).wait;
59             fout.writeln("exit code: ", exitCode);
60         } catch (Exception e) {
61         }
62         return readText(log);
63     }
64 
65     string exec(string[] args, string[string] env) {
66         const log = inSandbox(format!"command%s.log"(commandLogCnt++).Path);
67 
68         try {
69             auto fout = File(log, "w");
70             fout.writefln("%-(%s %)", args);
71 
72             auto exitCode = std.process.spawnProcess(args, stdin, fout, fout,
73                     env, std.process.Config.none, sandboxPath).wait;
74             fout.writeln("exit code: ", exitCode);
75         } catch (Exception e) {
76         }
77         return readText(log);
78     }
79 
80     Path inSandbox(string fileName) @safe pure nothrow const {
81         return sandboxPath ~ fileName;
82     }
83 }
84 
85 void dirContentCopy(Path src, Path dst) {
86     import std.algorithm;
87     import std.file;
88     import std.path;
89     import my.file;
90 
91     assert(src.isDir);
92     assert(dst.isDir);
93 
94     foreach (f; dirEntries(src, SpanMode.shallow).filter!"a.isFile") {
95         auto dst_f = buildPath(dst, f.name.baseName).Path;
96         copy(f.name, dst_f);
97         if (isExecutable(Path(f.name)))
98             setExecutable(dst_f);
99     }
100 }
101 
102 auto regexIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
103     import std.regex : regex, matchFirst;
104 
105     auto r = regex(rawRegex);
106 
107     foreach (v; array) {
108         if (!matchFirst(v, r).empty)
109             return;
110     }
111 
112     import unit_threaded.exception : fail;
113 
114     fail(formatValueInItsOwnLine("Value ",
115             rawRegex) ~ formatValueInItsOwnLine("not in ", array), file, line);
116 }
117 
118 auto regexNotIn(T)(string rawRegex, T[] array, string file = __FILE__, in size_t line = __LINE__) {
119     import std.regex : regex, matchFirst;
120     import unit_threaded.exception : fail;
121 
122     auto r = regex(rawRegex);
123 
124     foreach (v; array) {
125         if (!matchFirst(v, r).empty) {
126             fail(formatValueInItsOwnLine("Value ",
127                     rawRegex) ~ formatValueInItsOwnLine("in ", array), file, line);
128             return;
129         }
130     }
131 }
132 
133 string[] formatValueInItsOwnLine(T)(in string prefix, scope auto ref T value) {
134     import std.conv : to;
135     import std.traits : isSomeString;
136     import std.range.primitives : isInputRange;
137     import std.traits; // too many to list
138     import std.range; // also
139 
140     static if (isSomeString!T) {
141         // isSomeString is true for wstring and dstring,
142         // so call .to!string anyway
143         return [prefix ~ `"` ~ value.to!string ~ `"`];
144     } else static if (isInputRange!T) {
145         return formatRange(prefix, value);
146     } else {
147         return [prefix ~ convertToString(value)];
148     }
149 }
150 
151 string[] formatRange(T)(in string prefix, scope auto ref T value) {
152     import std.conv : text;
153     import std.range : ElementType;
154     import std.algorithm : map, reduce, max;
155 
156     //some versions of `text` are @system
157     auto defaultLines = () @trusted { return [prefix ~ value.text]; }();
158 
159     static if (!isInputRange!(ElementType!T))
160         return defaultLines;
161     else {
162         import std.array : array;
163 
164         const maxElementSize = value.empty ? 0 : value.map!(a => a.array.length)
165             .reduce!max;
166         const tooBigForOneLine = (value.array.length > 5 && maxElementSize > 5)
167             || maxElementSize > 10;
168         if (!tooBigForOneLine)
169             return defaultLines;
170         return [prefix ~ "["] ~ value.map!(a => formatValueInItsOwnLine("              ",
171                 a).join("") ~ ",").array ~ "          ]";
172     }
173 }