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 }