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 }