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