1 module exceeds_expectations.expect; 2 3 import colorize; 4 import exceeds_expectations.exceptions; 5 import exceeds_expectations.expect_not; 6 import exceeds_expectations.pretty_print; 7 import exceeds_expectations.utils; 8 import std.algorithm; 9 import std.conv; 10 import std.file; 11 import std.math; 12 import std.range; 13 import std.traits; 14 15 16 /** 17 * Begins an expectation. 18 */ 19 public Expect!T expect(T)(const T received, string filePath = __FILE__, size_t line = __LINE__) 20 { 21 // TODO: Try to make this function auto-ref 22 return Expect!T(received, filePath, line); 23 } 24 25 26 /** 27 * Wrapper whose methods are expectations. 28 */ 29 public struct Expect(TReceived) 30 { 31 private const(TReceived) received; 32 private immutable string filePath; 33 private immutable size_t line; 34 private bool completed = false; 35 36 private this(const(TReceived) received, string filePath, size_t line) 37 { 38 this.received = received; 39 this.filePath = filePath; 40 this.line = line; 41 } 42 43 ~this() 44 { 45 if (!completed) 46 { 47 throw new InvalidExpectationException( 48 "`expect` was called but no assertion was made at " ~ 49 filePath ~ "(" ~ line.to!string ~ "): \n\n" ~ 50 formatCode(readText(filePath), line, 2) ~ "\n", 51 filePath, line 52 ); 53 } 54 } 55 56 private void fail(string description) 57 { 58 string locationString = "Failing expectation at " ~ filePath ~ "(" ~ line.to!string ~ ")"; 59 60 throw new FailingExpectationException( 61 description, 62 locationString, 63 filePath, line 64 ); 65 } 66 67 /// Negates the expectation. Returns an [ExpectNot]. 68 public ExpectNot!TReceived not() 69 { 70 completed = true; // Because a new expectation is returned, and this one will be discarded. 71 return ExpectNot!TReceived(received, filePath, line); 72 } 73 74 /// Checks that `received == expected` and throws 75 /// [FailingExpectationException] otherwise. 76 public void toEqual(TExpected)(const auto ref TExpected expected) 77 if (canCompareForEquality!(TReceived, TExpected)) 78 { 79 completed = true; 80 81 if (received != expected) 82 { 83 fail( 84 formatDifferences(prettyPrint(expected), prettyPrint(received), false) 85 ); 86 } 87 } 88 89 /// Checks that `predicate(received)` returns true and throws a 90 /// [FailingExpectationException] otherwise. 91 public void toSatisfy(bool delegate(const(TReceived)) predicate) 92 { 93 completed = true; 94 95 if (!predicate(received)) 96 { 97 fail( 98 "Received: ".color(fg.light_red) ~ prettyPrint(received) 99 ); 100 } 101 } 102 103 /// Checks that `predicate(received)` returns true for all `predicates` and 104 /// throws a [FailingExpectationException] otherwise. 105 public void toSatisfyAll(bool delegate(const(TReceived))[] predicates...) 106 { 107 completed = true; 108 109 if (predicates.length == 0) 110 { 111 throw new InvalidExpectationException( 112 "Missing predicates at " ~ filePath ~ "(" ~ line.to!string ~ "): \n" ~ 113 "\n" ~ formatCode(readText(filePath), line, 2) ~ "\n", 114 filePath, line 115 ); 116 } 117 118 if (predicates.length == 1) 119 { 120 toSatisfy(predicates[0]); 121 return; 122 } 123 124 125 auto results = predicates.map!(p => p(received)); 126 immutable size_t numFailures = count!(e => !e)(results); 127 128 if(numFailures > 0) 129 { 130 size_t[] failingIndices = 131 results 132 .zip(iota(0, results.length)) 133 .filter!(tup => !tup[0]) 134 .map!(tup => tup[1]) 135 .array; 136 137 immutable string description = 138 numFailures == predicates.length ? 139 "Received value did not satisfy any predicates, but was expected to satisfy all." : 140 ( 141 "Received value did not satisfy " ~ 142 ( 143 ( 144 numFailures == 1 ? 145 "predicate at index " : 146 "predicates at indices " 147 ) ~ humanReadableNumbers(failingIndices) ~ ", but was expected to satisfy all." 148 ) 149 ); 150 151 fail( 152 "Received: ".color(fg.light_red) ~ prettyPrint(received) ~ "\n" ~ 153 description 154 ); 155 } 156 } 157 158 /// Checks that `predicate(received)` returns true for at least one of the 159 /// given `predicates` and throws a [FailingExpectationException] otherwise. 160 public void toSatisfyAny(bool delegate(const(TReceived))[] predicates...) 161 { 162 completed = true; 163 164 if (predicates.length == 0) 165 { 166 throw new InvalidExpectationException( 167 "Missing predicates at " ~ filePath ~ "(" ~ line.to!string ~ "): \n" ~ 168 "\n" ~ formatCode(readText(filePath), line, 2) ~ "\n", 169 filePath, line 170 ); 171 } 172 173 if (predicates.length == 1) 174 { 175 toSatisfy(predicates[0]); 176 return; 177 } 178 179 auto results = predicates.map!(p => p(received)); 180 immutable size_t numPassed = count!(e => e)(results); 181 182 if(numPassed == 0) 183 { 184 fail( 185 "Received: ".color(fg.light_red) ~ prettyPrint(received) ~ "\n" ~ 186 "Received value did not satisfy any predicates, but was expected to satisfy at least one." 187 ); 188 } 189 } 190 191 /** 192 * Checks that `received.isClose(expected, maxRelDiff, maxAbsDiff)` and 193 * throws a [FailingExpectationException] otherwise. 194 * 195 * `maxRelDiff` and `maxAbsDiff` have the same default values as in 196 * [std.math.isClose]. 197 * 198 * See_Also: [std.math.isClose] 199 */ 200 public void toApproximatelyEqual(TExpected, F : real)( 201 const auto ref TExpected expected, 202 F maxRelDiff = CommonDefaultFor!(TReceived, TExpected), 203 F maxAbsDiff = 0.0 204 ) 205 if ( 206 __traits(compiles, received.isClose(expected, maxRelDiff, maxAbsDiff)) 207 ) 208 { 209 completed = true; 210 211 if (!received.isClose(expected, maxRelDiff, maxAbsDiff)) 212 { 213 fail( 214 formatDifferences(prettyPrint(expected), prettyPrint(received), false) ~ 215 formatApproxDifferences(expected, received, maxRelDiff, maxAbsDiff) 216 ); 217 } 218 } 219 220 /// Checks that `received is expected` and throws a 221 /// [FailingExpectationException] otherwise. 222 public void toBe(TExpected)(const auto ref TExpected expected) 223 { 224 completed = true; 225 226 if (received !is expected) 227 { 228 fail( 229 "Arguments do not reference the same object (received is expected == false)." 230 ); 231 } 232 } 233 234 /// Checks that received is a `TExpected` or a sub-type of it. Throws a 235 /// [FailingExpectationException] if `received` cannot be cast to 236 /// `TExpected` or if `received is null`. 237 public void toBeOfType(TExpected)() 238 if ((is(TExpected == class) || is(TExpected == interface)) && 239 (is(TReceived == class) || is(TReceived == interface))) 240 { 241 completed = true; 242 243 if (received is null) 244 { 245 fail( 246 formatDifferences( 247 prettyPrint(typeid(TExpected)), 248 "null", 249 false 250 ) 251 ); 252 } 253 254 if (!cast(TExpected) received) 255 { 256 TypeInfo receivedTypeInfo = typeid(received); 257 258 static if (is(TReceived == interface)) 259 { 260 receivedTypeInfo = typeid(cast(Object) received); 261 } 262 263 fail( 264 formatTypeDifferences( 265 typeid(TExpected), 266 receivedTypeInfo, 267 false, 268 ) 269 ); 270 } 271 } 272 273 /** 274 * Calls `received` and catches any exceptions thrown by it. There are three 275 * possible outcomes: 276 * 277 * - `received` throws a `TExpected` or one of its sub-types. The 278 * expectation passes. 279 * 280 * - `received` doesn't throw a `TExpected`, but does throw something else. 281 * A [FailingExpectationException] is thrown. 282 * 283 * - `received` doesn't throw anything. A [FailingExpectationException] is 284 * thrown. 285 */ 286 public void toThrow(TExpected : Throwable = Throwable)() 287 if (isCallable!TReceived) 288 { 289 completed = true; 290 291 try 292 { 293 received(); 294 } 295 catch (Throwable e) // @suppress(dscanner.suspicious.catch_em_all) 296 { 297 if (cast(TExpected) e) return; 298 299 fail( 300 formatTypeDifferences( 301 typeid(TExpected), 302 typeid(e), 303 false 304 ) 305 ); 306 } 307 308 fail( 309 formatDifferences( 310 prettyPrint(typeid(TExpected)), 311 "Nothing was thrown", 312 false 313 ) 314 ); 315 } 316 }