1 /* 2 * Copyright Andrej Mitrovic 2013. 3 * Distributed under the Boost Software License, Version 1.0. 4 * (See accompanying file LICENSE_1_0.txt or copy at 5 * http://www.boost.org/LICENSE_1_0.txt) 6 */ 7 module dchip.util; 8 9 import core.exception; 10 import std.traits; 11 import std.typetuple; 12 13 /** 14 See: https://github.com/slembcke/Chipmunk2D/issues/56 15 16 For two function pointers to be safely casted: 17 Either return type and parameters must match perfectly, 18 or they have to be pointers. 19 */ 20 T safeCast(T, S)(S s) 21 { 22 static if (is(Unqual!T == Unqual!S)) 23 return cast(T)s; 24 else 25 static if (isSomeFunction!T && isSomeFunction!S) 26 { 27 alias TParams = ParameterTypeTuple!T; 28 alias SParams = ParameterTypeTuple!S; 29 30 alias TReturn = ReturnType!T; 31 alias SReturn = ReturnType!S; 32 33 static assert(is(TReturn == SReturn) || isPointer!TReturn && isPointer!SReturn); 34 35 static assert(TParams.length == SParams.length); 36 37 static if (is(TParams == SParams)) 38 { 39 return cast(T)s; 40 } 41 else 42 { 43 foreach (IDX, _; SParams) 44 { 45 static assert(is(SParams[IDX] == TParams[IDX]) 46 || isPointer!(SParams[IDX]) && isPointer!(TParams[IDX])); 47 } 48 49 return cast(T)s; 50 } 51 } 52 else 53 static if (isFloatingPoint!T && isFloatingPoint!S) 54 return cast(T)s; 55 else 56 static assert(0); 57 } 58 59 /** 60 Return the exception of type $(D Exc) that is 61 expected to be thrown when $(D expr) is evaluated. 62 63 This is useful to verify the custom exception type 64 holds some interesting state. 65 66 If no exception is thrown, then a new exception 67 is thrown to notify the user of the missing exception. 68 */ 69 Exc getException(Exc, E)(lazy E expr, string file = __FILE__, size_t line = __LINE__) 70 { 71 try 72 { 73 expr(); 74 throw new Exception("Error: No exception was thrown.", file, line); 75 } 76 catch (Exc e) 77 { 78 return e; 79 } 80 } 81 82 /// 83 version (CHIP_ENABLE_UNITTESTS) 84 unittest 85 { 86 assert({ throw new Exception("my message"); }().getException!Exception.msg == "my message"); 87 88 static class MyExc : Exception 89 { 90 this(string file) 91 { 92 this.file = file; 93 super(""); 94 } 95 96 string file; 97 } 98 99 assert({ throw new MyExc("file.txt"); }().getException!MyExc.file == "file.txt"); 100 101 try 102 { 103 assert(getException!MyExc({ }()).file == "file.txt"); 104 } 105 catch (Exception exc) 106 { 107 assert(exc.msg == "Error: No exception was thrown."); 108 } 109 } 110 111 /** 112 Return the exception message of an exception. 113 If no exception was thrown, then a new exception 114 is thrown to notify the user of the missing exception. 115 */ 116 string getExceptionMsg(E)(lazy E expr, string file = __FILE__, size_t line = __LINE__) 117 { 118 import std.exception : collectExceptionMsg; 119 120 auto result = collectExceptionMsg!Throwable(expr); 121 122 if (result is null) 123 throw new Exception("Error: No exception was thrown.", file, line); 124 125 return result; 126 } 127 128 /// 129 version (CHIP_ENABLE_UNITTESTS) 130 unittest 131 { 132 assert(getExceptionMsg({ throw new Exception("my message"); }()) == "my message"); 133 assert(getExceptionMsg({ }()).getExceptionMsg == "Error: No exception was thrown."); 134 } 135 136 /** Verify that calling $(D expr) throws and contains the exception message $(D msg). */ 137 void assertErrorsWith(E)(lazy E expr, string msg, string file = __FILE__, size_t line = __LINE__) 138 { 139 try 140 { 141 expr.getExceptionMsg.assertEqual(msg); 142 } 143 catch (AssertError ae) 144 { 145 ae.file = file; 146 ae.line = line; 147 throw ae; 148 } 149 } 150 151 /// 152 version (CHIP_ENABLE_UNITTESTS) 153 unittest 154 { 155 require(1 == 2).assertErrorsWith("requirement failed."); 156 require(1 == 2, "%s is not true").assertErrorsWith("%s is not true"); 157 require(1 == 2, "%s is not true", "1 == 2").assertErrorsWith("1 == 2 is not true"); 158 159 require(1 == 1).assertErrorsWith("requirement failed.") 160 .assertErrorsWith("Error: No exception was thrown."); 161 } 162 163 /** 164 Similar to $(D enforce), except it can take a formatting string as the second argument. 165 $(B Note:) Until Issue 8687 is fixed, $(D file) and $(D line) have to be compile-time 166 arguments, which might create template bloat. 167 */ 168 T require(string file = __FILE__, size_t line = __LINE__, T, Args...) 169 (T value, Args args) 170 { 171 if (value) 172 return value; 173 174 static if (Args.length) 175 { 176 static if (Args.length > 1) 177 { 178 import std..string : format; 179 string msg = format(args[0], args[1 .. $]); 180 } 181 else 182 { 183 import std.conv : text; 184 string msg = text(args); 185 } 186 } 187 else 188 enum msg = "requirement failed."; 189 190 throw new Exception(msg, file, line); 191 } 192 193 /// 194 version (CHIP_ENABLE_UNITTESTS) 195 unittest 196 { 197 require(1 == 2).getExceptionMsg.assertEqual("requirement failed."); 198 require(1 == 2, "%s is not true").getExceptionMsg.assertEqual("%s is not true"); 199 require(1 == 2, "%s is not true", "1 == 2").getExceptionMsg.assertEqual("1 == 2 is not true"); 200 } 201 202 template assertEquality(bool checkEqual) 203 { 204 import std..string : format; 205 206 void assertEquality(T1, T2)(T1 lhs, T2 rhs, string file = __FILE__, size_t line = __LINE__) 207 //~ if (is(typeof(lhs == rhs) : bool)) // note: errors are better without this 208 { 209 static if (is(typeof(lhs == rhs) : bool)) 210 enum string compare = "lhs == rhs"; 211 else 212 static if (is(typeof(equal(lhs, rhs)) : bool)) 213 enum string compare = "equal(lhs, rhs)"; // std.algorithm for ranges 214 else 215 static assert(0, format("lhs type '%s' cannot be compared against rhs type '%s'", 216 __traits(identifier, T1), __traits(identifier, T2))); 217 218 mixin(format(q{ 219 if (%s(%s)) 220 throw new AssertError( 221 format("(%%s %%s %%s) failed.", lhs.enquote, checkEqual ? "==" : "!=", rhs.enquote), 222 file, line); 223 }, checkEqual ? "!" : "", compare)); 224 } 225 } 226 227 /** 228 An overload of $(D enforceEx) which allows constructing the exception with the arguments its ctor supports. 229 The ctor's last parameters must be a string (file) and size_t (line). 230 */ 231 template enforceEx(E) 232 { 233 T enforceEx(T, string file = __FILE__, size_t line = __LINE__, Args...)(T value, Args args) 234 if (is(typeof(new E(args, file, line)))) 235 { 236 if (!value) throw new E(args, file, line); 237 return value; 238 } 239 } 240 241 /// 242 version (CHIP_ENABLE_UNITTESTS) 243 unittest 244 { 245 static class Exc : Exception 246 { 247 this(int x, string file, int line) 248 { 249 super("", file, line); 250 this.x = x; 251 } 252 253 int x; 254 } 255 256 try 257 { 258 enforceEx!Exc(false, 1); 259 assert(0); 260 } 261 catch (Exc ex) 262 { 263 assert(ex.x == 1); 264 } 265 } 266 267 /** Unittest functions which give out a message with the failing expression. */ 268 alias assertEqual = assertEquality!true; 269 270 /** Common mispelling. */ 271 alias assertEquals = assertEqual; 272 273 /// Ditto 274 alias assertNotEqual = assertEquality!false; 275 276 /// 277 version (CHIP_ENABLE_UNITTESTS) 278 unittest 279 { 280 assertEqual(1, 1); 281 assertNotEqual(1, 2); 282 283 assert(assertEqual("foo", "bar").getExceptionMsg == `("foo" == "bar") failed.`); 284 assert(assertNotEqual(1, 1).getExceptionMsg == "(1 != 1) failed."); 285 286 int x; 287 int[] y; 288 static assert(!__traits(compiles, x.assertEqual(y))); 289 } 290 291 template assertProp(string prop, bool state) 292 { 293 void assertProp(T)(T arg, string file = __FILE__, size_t line = __LINE__) 294 { 295 import std..string : format; 296 297 mixin(format(q{ 298 if (%sarg.%s) 299 { 300 throw new AssertError( 301 format(".%%s is %%s : %%s", prop, !state, arg), file, line); 302 } 303 }, state ? "!" : "", prop)); 304 } 305 } 306 307 /// Assert range isn't empty. 308 alias assertEmpty = assertProp!("empty", true); 309 310 /// Ditto 311 alias assertNotEmpty = assertProp!("empty", false); 312 313 /// 314 version (CHIP_ENABLE_UNITTESTS) 315 unittest 316 { 317 // Issue 9588 - format prints context pointer for struct 318 static struct S { int x; bool empty() { return x == 0; } } 319 320 S s = S(1); 321 assert(assertEmpty(s).getExceptionMsg == ".empty is false : S(1)"); 322 323 s.x = 0; 324 assertEmpty(s); 325 326 assert(assertNotEmpty(s).getExceptionMsg == ".empty is true : S(0)"); 327 s.x = 1; 328 assertNotEmpty(s); 329 } 330 331 /// Useful template to generate an assert check function 332 template assertOp(string op) 333 { 334 void assertOp(T1, T2)(T1 lhs, T2 rhs, 335 string file = __FILE__, 336 size_t line = __LINE__) 337 { 338 import std..string : format; 339 340 string msg = format("(%s %s %s) failed.", lhs, op, rhs); 341 342 mixin(format(q{ 343 if (!(lhs %s rhs)) throw new AssertError(msg, file, line); 344 }, op)); 345 } 346 } 347 348 /// 349 version (CHIP_ENABLE_UNITTESTS) 350 unittest 351 { 352 alias assertEqual = assertOp!"=="; 353 alias assertNotEqual = assertOp!"!="; 354 alias assertGreaterThan = assertOp!">"; 355 alias assertGreaterThanOrEqual = assertOp!">="; 356 357 assertEqual(1, 1); 358 assertNotEqual(1, 2); 359 assertGreaterThan(2, 1); 360 assertGreaterThanOrEqual(2, 2); 361 } 362 363 /** 364 Return string representation of argument. 365 If argument is already a string or a 366 character, enquote it to make it more readable. 367 */ 368 string enquote(T)(T arg) 369 { 370 import std.conv : to; 371 import std.range : isInputRange, ElementEncodingType; 372 import std..string : format; 373 import std.traits : isSomeString, isSomeChar; 374 375 static if (isSomeString!T) 376 return format(`"%s"`, arg); 377 else 378 static if (isSomeChar!T) 379 return format("'%s'", arg); 380 else 381 static if (isInputRange!T && is(ElementEncodingType!T == dchar)) 382 return format(`"%s"`, to!string(arg)); 383 else 384 return to!string(arg); 385 } 386 387 /// 388 version (CHIP_ENABLE_UNITTESTS) 389 unittest 390 { 391 assert(enquote(0) == "0"); 392 assert(enquote(enquote(0)) == `"0"`); 393 assert(enquote("foo") == `"foo"`); 394 assert(enquote('a') == "'a'"); 395 } 396 397 /** 398 Export all enum members as aliases. This allows enums to be used as types 399 and allows its members to be used as if they're defined in module scope. 400 */ 401 package mixin template _ExportEnumMembers(E) if (is(E == enum)) 402 { 403 mixin(_makeEnumAliases!(E)()); 404 } 405 406 /// ditto 407 package string _makeEnumAliases(E)() if (is(E == enum)) 408 { 409 import std.array; 410 import std..string; 411 412 enum enumName = __traits(identifier, E); 413 Appender!(string[]) result; 414 415 foreach (string member; __traits(allMembers, E)) 416 result ~= format("alias %s = %s.%s;", member, enumName, member); 417 418 return result.data.join("\n"); 419 } 420 421 /// 422 version (CHIP_ENABLE_UNITTESTS) 423 unittest 424 { 425 enum enum_type_t 426 { 427 foo, 428 bar, 429 } 430 431 mixin _ExportEnumMembers!enum_type_t; 432 433 enum_type_t e1 = enum_type_t.foo; // ok 434 enum_type_t e2 = bar; // ok 435 }