1 module mocked.error; 2 3 import std.algorithm; 4 import std.array; 5 import std.conv; 6 import std.format; 7 import std.traits; 8 import std.typecons; 9 10 /** 11 * Constructs an $(D_PSYMBOL UnexpectedCallError). 12 * 13 * Params: 14 * T = Object type. 15 * Args = $(D_PARAM name)'s argument types. 16 * name = Unexpected call name. 17 * arguments = $(D_PARAM name)'s arguments. 18 * file = File. 19 * line = Line number. 20 * nextInChain = The next error. 21 * 22 * Returns: $(D_PSYMBOL UnexpectedCallError). 23 */ 24 UnexpectedCallError unexpectedCallError(T, Args...)( 25 string name, Args arguments, 26 string file = __FILE__, size_t line = __LINE__, 27 Throwable nextInChain = null) 28 { 29 return new UnexpectedCallError(formatName!T(name), 30 formatArguments!Args(arguments), file, line, nextInChain); 31 } 32 33 private string[] formatArguments(Args...)(ref Args arguments) 34 { 35 string[] formattedArguments; 36 auto spec = singleSpec("%s"); 37 auto writer = appender!(char[])(); 38 39 static foreach (i, Arg; Args) 40 { 41 static if (isSomeString!Arg) 42 { 43 formattedArguments ~= format!"%(%s %)"([arguments[i]]); 44 } 45 else 46 { 47 writer.clear(); 48 writer.formatValue(arguments[i], spec); 49 formattedArguments ~= writer[].idup; 50 } 51 } 52 return formattedArguments; 53 } 54 55 private string formatName(T)(string name) 56 { 57 return format!"%s.%s"(T.stringof, name); 58 } 59 60 /** 61 * Thrown when an unexpected call occurs. 62 */ 63 final class UnexpectedCallError : Error 64 { 65 private string name; 66 private string[] arguments; 67 68 /** 69 * Constructs an $(D_PSYMBOL UnexpectedCallError). 70 * 71 * Params: 72 * name = Unexpected call name. 73 * arguments = $(D_PARAM name)'s arguments. 74 * file = File. 75 * line = Line number. 76 * nextInChain = The next error. 77 */ 78 this(string name, string[] arguments, 79 string file = __FILE__, size_t line = __LINE__, 80 Throwable nextInChain = null) pure @safe 81 { 82 this.name = name; 83 this.arguments = arguments; 84 85 const message = format!"Unexpected call: %s(%-(%s, %))"(this.name, this.arguments); 86 87 super(message, file, line, nextInChain); 88 } 89 } 90 91 /** 92 * Constructs an $(D_PSYMBOL UnexpectedArgumentError). 93 * 94 * $(D_PARAM arguments) contains both actual and expected arguments. First the 95 * actual arguments are given. The last argument in $(D_PARAM Args) is a 96 * $(D_PSYMBOL Maybe) containing the expected arguments. 97 * 98 * Params: 99 * T = Object type. 100 * Args = $(D_PARAM name)'s actual and expected arguments. 101 * name = Unexpected call name. 102 * arguments = $(D_PARAM name)'s actual and expected arguments. 103 * file = File. 104 * line = Line number. 105 * nextInChain = The next error. 106 */ 107 UnexpectedArgumentError unexpectedArgumentError(T, Args...)( 108 string name, Args arguments, 109 string file = __FILE__, size_t line = __LINE__, 110 Throwable nextInChain = null) 111 { 112 ExpectationPair[] formattedArguments; 113 auto spec = singleSpec("%s"); 114 auto writer = appender!(char[])(); 115 116 static foreach (i, Arg; Args[0 .. $ - 1]) 117 {{ 118 static if (isSomeString!Arg) 119 { 120 const expected = format!"%(%s %)"([arguments[$ - 1].get!i]); 121 const actual = format!"%(%s %)"([arguments[i]]); 122 } 123 else 124 { 125 writer.clear(); 126 writer.formatValue(arguments[$ - 1].get!i, spec); 127 const expected = writer[].idup; 128 129 writer.clear(); 130 writer.formatValue(arguments[i], spec); 131 const actual = writer[].idup; 132 } 133 134 formattedArguments ~= ExpectationPair(actual, expected); 135 }} 136 137 return new UnexpectedArgumentError(formatName!T(name), 138 formattedArguments, file, line, nextInChain); 139 } 140 141 /// Pair containing the expected argument and the argument from the actual call. 142 private alias ExpectationPair = Tuple!(string, "actual", string, "expected"); 143 144 /** 145 * Error thrown when a method has been called with arguments that don't match 146 * the expected ones. 147 */ 148 final class UnexpectedArgumentError : Error 149 { 150 private string name; 151 private ExpectationPair[] arguments; 152 153 /** 154 * Constructs an $(D_PSYMBOL UnexpectedArgumentError). 155 * 156 * Params: 157 * name = Unexpected call name. 158 * arguments = $(D_PARAM name)'s actual and expected arguments. 159 * file = File. 160 * line = Line number. 161 * nextInChain = The next error. 162 */ 163 this(string name, ExpectationPair[] arguments, 164 string file, size_t line, Throwable nextInChain) pure @safe 165 { 166 this.name = name; 167 this.arguments = arguments; 168 169 auto message = appender!string(); 170 171 message ~= "Expectation failure:\n"; 172 173 auto actual = arguments.map!(argument => argument.actual); 174 auto expected = arguments.map!(argument => argument.expected); 175 176 message ~= format!" Expected: %s(%-(%s, %))\n"(this.name, expected); 177 message ~= format!" but got: %s(%-(%s, %))"(this.name, actual); 178 179 super(message.data, file, line, nextInChain); 180 } 181 } 182 183 /** 184 * Constructs an $(D_PSYMBOL OutOfOrderCallError). 185 * 186 * Params: 187 * T = Object type. 188 * Args = $(D_PARAM name)'s arguments. 189 * name = Unexpected call name. 190 * arguments = $(D_PARAM name)'s arguments. 191 * expected = Expected position in the call queue. 192 * got = Actual position in the call queue. 193 * file = File. 194 * line = Line number. 195 * nextInChain = The next error. 196 */ 197 OutOfOrderCallError outOfOrderCallError(T, Args...)( 198 string name, Args arguments, 199 size_t expected, size_t got, 200 string file = __FILE__, size_t line = __LINE__, 201 Throwable nextInChain = null) 202 { 203 return new OutOfOrderCallError(formatName!T(name), 204 formatArguments!Args(arguments), expected, got, file, line, nextInChain); 205 } 206 207 /** 208 * `OutOfOrderCallError` is thrown only if the checking the call order among 209 * methods of the same class is enabled. The error is thrown if a method is 210 * called earlier than expected. 211 */ 212 final class OutOfOrderCallError : Error 213 { 214 private string name; 215 private string[] arguments; 216 217 /** 218 * Constructs an $(D_PSYMBOL OutOfOrderCallError). 219 * 220 * Params: 221 * name = Unexpected call name. 222 * arguments = $(D_PARAM name)'s arguments. 223 * expected = Expected position in the call queue. 224 * got = Actual position in the call queue. 225 * file = File. 226 * line = Line number. 227 * nextInChain = The next error. 228 */ 229 this(string name, string[] arguments, 230 size_t expected, size_t got, 231 string file, size_t line, Throwable nextInChain) pure @safe 232 { 233 this.name = name; 234 this.arguments = arguments; 235 236 string message = format!"%s(%-(%s, %)) called too early:\n"(this.name, this.arguments); 237 message ~= format!"Expected at position: %s, but got: %s"(expected, got); 238 239 super(message, file, line, nextInChain); 240 } 241 } 242 243 /** 244 * Constructs an $(D_PSYMBOL ExpectationViolationException). 245 * 246 * Params: 247 * T = Object type. 248 * MaybeArgs = $(D_PARAM name)'s argument types. 249 * name = Call name. 250 * arguments = $(D_PARAM name)'s arguments. 251 * file = File. 252 * line = Line number. 253 * nextInChain = The next error. 254 */ 255 ExpectationViolationException expectationViolationException(T, MaybeArgs)( 256 string name, MaybeArgs arguments, 257 string file = __FILE__, size_t line = __LINE__, 258 Throwable nextInChain = null) 259 { 260 string[] formattedArguments; 261 auto writer = appender!(char[])(); 262 auto spec = singleSpec("%s"); 263 264 if (!arguments.isNull) 265 { 266 static foreach (i; 0 .. MaybeArgs.length) 267 { 268 static if (isSomeString!(MaybeArgs.Types[i])) 269 { 270 formattedArguments ~= format!"%(%s %)"([arguments.get!i]); 271 } 272 else 273 { 274 writer.clear(); 275 writer.formatValue(arguments.get!i, spec); 276 formattedArguments ~= writer[].idup; 277 } 278 } 279 } 280 281 return new ExpectationViolationException(formatName!T(name), 282 formattedArguments, file, line, nextInChain); 283 } 284 285 /** 286 * Expected the method to be called n times, but called m times, 287 * where m < n. 288 * Same as unexpected call, but with expected arguments instead of the actual ones. 289 * Thrown if a method was expected to be called, but wasn't. 290 */ 291 final class ExpectationViolationException : Exception 292 { 293 private string name; 294 private string[] arguments; 295 296 /** 297 * Constructs an $(D_PSYMBOL ExpectationViolationException). 298 * 299 * Params: 300 * name = Call name. 301 * arguments = $(D_PARAM name)'s arguments. 302 * file = File. 303 * line = Line number. 304 * nextInChain = The next error. 305 */ 306 this(string name, string[] arguments, string file, size_t line, Throwable nextInChain) 307 { 308 this.name = name; 309 this.arguments = arguments; 310 311 const message = this.arguments is null 312 ? format!"Expected method not called: %s"(this.name) 313 : format!"Expected method not called: %s(%-(%s, %))"(this.name, this.arguments); 314 315 super(message, file, line, nextInChain); 316 } 317 }