1 /** 2 * Exception formatting 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.utils.exception; 15 16 import std.algorithm; 17 import std.string; 18 19 /// Stringify an exception chain. 20 string formatException(Throwable e) 21 { 22 string[] descriptions; 23 while (e) 24 descriptions ~= e.toString(), 25 e = e.next; 26 return descriptions.join("\n===================================\n"); 27 } 28 29 // -------------------------------------------------------------------------- 30 31 import ae.utils.meta; 32 33 /// Mixin to declare a new exception type with the given name. 34 /// Automatically generates common constructors. 35 mixin template DeclareException(string NAME, BASE = Exception) 36 { 37 import ae.utils.meta.x; 38 39 mixin(mixin(X!q{ 40 class @(NAME) : BASE 41 { 42 this(string s, string fn = __FILE__, size_t ln = __LINE__) 43 { 44 super(s, fn, ln); 45 } 46 47 this(string s, Throwable next, string fn = __FILE__, size_t ln = __LINE__) 48 { 49 super(s, next, fn, ln); 50 } 51 } 52 })); 53 } 54 55 unittest 56 { 57 mixin DeclareException!q{OutOfCheeseException}; 58 try 59 throw new OutOfCheeseException("*** OUT OF CHEESE ***"); 60 catch (Exception e) 61 assert(e.classinfo.name.indexOf("Cheese") > 0); 62 } 63 64 // -------------------------------------------------------------------------- 65 66 /// This exception can never be thrown. 67 /// Useful for a temporary or aliased catch block exception type. 68 class NoException : Exception 69 { 70 @disable this() 71 { 72 super(null); 73 } /// 74 } 75 76 /// Allows toggling catch blocks with -debug=NO_CATCH. 77 /// To use, catch `CaughtException` instead of `Exception` in catch blocks. 78 debug(NO_CATCH) 79 alias CaughtException = NoException; 80 else 81 alias CaughtException = Exception; 82 83 // -------------------------------------------------------------------------- 84 85 import std.conv; 86 87 /// Returns string mixin for adding a chained exception 88 string exceptionContext(string messageExpr, string name = text(__LINE__)) 89 { 90 name = "exceptionContext_" ~ name; 91 return mixin(X!q{ 92 bool @(name); 93 scope(exit) if (@(name)) throw new Exception(@(messageExpr)); 94 scope(failure) @(name) = true; 95 }); 96 } 97 98 unittest 99 { 100 try 101 { 102 mixin(exceptionContext(q{"Second"})); 103 throw new Exception("First"); 104 } 105 catch (Exception e) 106 { 107 assert(e.msg == "First"); 108 assert(e.next.msg == "Second"); 109 } 110 } 111 112 // -------------------------------------------------------------------------- 113 114 /// Extracts the stack trace from an exception's `toString`, as an array of lines. 115 string[] getStackTrace(string until = __FUNCTION__, string since = "_d_run_main") 116 { 117 string[] lines; 118 try 119 throw new Exception(null); 120 catch (Exception e) 121 lines = e.toString().splitLines()[1..$]; 122 123 auto start = lines.countUntil!(line => line.canFind(until)); 124 auto end = lines.countUntil!(line => line.canFind(since)); 125 if (start < 0) start = -1; 126 if (end < 0) end = lines.length; 127 return lines[start+1..end]; 128 } 129 130 // -------------------------------------------------------------------------- 131 132 import core.exception; 133 import std.exception; 134 135 /// Test helper. Asserts that `a` `op` `b`, and includes the values in the error message. 136 template assertOp(string op) 137 { 138 void assertOp(A, B)(auto ref A a, auto ref B b, string file=__FILE__, int line=__LINE__) 139 { 140 if (!(mixin("a " ~ op ~ " b"))) 141 throw new AssertError("Assertion failed: %s %s %s".format(a, op, b), file, line); 142 } 143 } 144 alias assertEqual = assertOp!"=="; /// 145 146 unittest 147 { 148 assertThrown!AssertError(assertEqual(1, 2)); 149 }