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 }