1 module dshould.ShouldType; 2 3 import core.exception : AssertError; 4 import std.algorithm : map; 5 import std.format : format; 6 import std.meta : allSatisfy; 7 import std.range : iota; 8 import std.string : empty, join; 9 import std.traits : TemplateArgsOf; 10 public import std.traits : isInstanceOf; 11 import std.typecons : Tuple; 12 13 // prevent default arguments from being accidentally filled by regular parameters 14 // void foo(..., Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 15 public struct Fence 16 { 17 } 18 19 /** 20 * .should begins every fluent assertion in dshould. It takes no parameters on its own. 21 * Note that leaving a .should phrase unfinished will error at runtime. 22 */ 23 public auto should(T)(lazy T got) pure 24 { 25 T get() pure @safe 26 { 27 return got; 28 } 29 30 return ShouldType!(typeof(&get))(&get); 31 } 32 33 /** 34 * ShouldType is the base type passed between UFCS words in fluent assertions. 35 * It stores the left-hand side expression of the phrase, called "got" in errors, 36 * as well as the words making up the assertion phrase as template arguments. 37 */ 38 public struct ShouldType(G, string[] phrase = []) 39 { 40 import std.algorithm : canFind; 41 42 private G got_; 43 44 private int* refCount_ = null; 45 46 /** 47 * Add a word to the phrase. Can be chained. 48 */ 49 public auto addWord(string word)() 50 { 51 return ShouldType!(G, phrase ~ word)(this.got_, this.refCount); 52 } 53 54 // Ensure that ShouldType constness is applied to lhs value 55 public auto got() 56 { 57 scope(failure) 58 { 59 // prevent exceptions in got_() from setting off the unterminated-chain error 60 terminateChain; 61 } 62 63 return this.got_(); 64 } 65 66 public const(typeof(this.got_())) got() const 67 { 68 scope(failure) 69 { 70 // we know refCount is nonconst 71 (cast() this).terminateChain; 72 } 73 return this.got_(); 74 } 75 76 private this(G got) { this.got_ = got; this.refCount = 1; } 77 78 /** 79 * Manually initialize a new ShouldType value from an existing one's ref count. 80 * All ShouldTypes of one phrase must use the same reference counter. 81 */ 82 public this(G got, ref int refCount) @trusted 83 in 84 { 85 assert(refCount != CHAIN_TERMINATED, "don't copy Should that's been terminated"); 86 } 87 do 88 { 89 this.got_ = got; 90 this.refCount_ = &refCount; 91 this.refCount++; 92 } 93 94 this(this) @trusted 95 in 96 { 97 assert(this.refCount != CHAIN_TERMINATED); 98 } 99 do 100 { 101 this.refCount++; 102 } 103 104 ~this() @trusted 105 { 106 import std.exception : enforce; 107 108 this.refCount--; 109 // NOT an assert! 110 // this ensures that if we fail as a side effect of a test failing, we don't override its exception 111 enforce!Exception(this.refCount > 0, "unterminated should-chain!"); 112 } 113 114 /** 115 * Checks a boolean condition for truth, throwing an exception when it fails. 116 * The components making up the exception string are passed lazily. 117 * The message has the form: "Test failed: expected {expected}[ because reason], but got {butGot}" 118 * For instance: "Test failed: expected got.empty() because there should be nothing in there, but got [5]." 119 * In that case, `expected` is "got.empty()" and `butGot` is "[5]". 120 */ 121 public void check(bool condition, lazy string expected, lazy string butGot, string file, size_t line) pure @safe 122 { 123 terminateChain; 124 125 if (!condition) 126 { 127 throw new FluentError(expected, butGot, file, line); 128 } 129 } 130 131 /** 132 * Mark that the semantic end of this phrase has been reached. 133 * If this is not called, the phrase will error on scope exit. 134 */ 135 public void terminateChain() 136 { 137 this.refCount = CHAIN_TERMINATED; // terminate chain, safe ref checker 138 } 139 140 private static enum isStringLiteral(T...) = T.length == 1 && is(typeof(T[0]) == string); 141 142 /** 143 * Allows to check that only a select list of words are permitted before the current word. 144 * On failure, an informative error is printed. 145 * Usage: should.allowOnlyWords!("word1", "word2").before!"newWord"; 146 */ 147 public template allowOnlyWords(allowedWords...) 148 if (allSatisfy!(isStringLiteral, allowedWords)) 149 { 150 void before(string newWord)() 151 { 152 static foreach (word; phrase) 153 { 154 static assert([allowedWords].canFind(word), `bad grammar: "` ~ word ~ ` ` ~ newWord ~ `"`); 155 } 156 } 157 } 158 159 /** 160 * Allows to check that a specified word appeared in the phrase before the current word. 161 * On failure, an informative error is printed. 162 * Usage: should.requireWord!"word".before!"newWord"; 163 */ 164 public template requireWord(string requiredWord) 165 { 166 void before(string newWord)() 167 { 168 static assert( 169 hasWord!requiredWord, 170 `bad grammar: expected "` ~ requiredWord ~ `" before "` ~ newWord ~ `"` 171 ); 172 } 173 } 174 175 /** 176 * Evaluates to true if the given word exists in the current phrase. 177 */ 178 public enum hasWord(string word) = phrase.canFind(word); 179 180 /** 181 * Evaluates to true if no words exist in the current phrase. 182 */ 183 public enum hasNoWords = phrase.empty; 184 185 // work around https://issues.dlang.org/show_bug.cgi?id=18839 186 public auto empty()(Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 187 { 188 return this.empty_(Fence(), file, line); 189 } 190 191 private enum CHAIN_TERMINATED = int.max; 192 193 private @property ref int refCount() 194 { 195 if (this.refCount_ is null) 196 { 197 this.refCount_ = new int; 198 *this.refCount_ = 0; 199 } 200 return *this.refCount_; 201 } 202 } 203 204 /** 205 * Ensures that the given range is empty. 206 * Specified here due to https://issues.dlang.org/show_bug.cgi?id=18839 207 */ 208 public void empty_(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 209 if (isInstanceOf!(ShouldType, Should)) 210 { 211 import std.range : empty; 212 213 with (should) 214 { 215 allowOnlyWords!("be", "not").before!"empty"; 216 requireWord!"be".before!"empty"; 217 218 terminateChain; 219 220 auto got = should.got(); 221 222 static if (hasWord!"not") 223 { 224 check(!got.empty, "nonempty range", format("%s", got), file, line); 225 } 226 else 227 { 228 check(got.empty, "empty range", format("%s", got), file, line); 229 } 230 } 231 } 232 233 /// 234 unittest 235 { 236 import dshould.basic; 237 238 (int[]).init.should.be.empty; 239 [5].should.not.be.empty; 240 } 241 242 private class FluentErrorImpl(T = AssertError) : T 243 { 244 private const string expectedPart = null; // before reason 245 public const string reason = null; 246 private const string butGotPart = null; // after reason 247 248 invariant 249 { 250 assert(!this.expectedPart.empty); 251 } 252 253 public this(string expectedPart, string butGotPart, string file, size_t line) pure @safe 254 in 255 { 256 assert(!expectedPart.empty); 257 assert(!butGotPart.empty); 258 } 259 do 260 { 261 this.expectedPart = expectedPart; 262 this.butGotPart = butGotPart; 263 264 super(combinedMessage, file, line); 265 } 266 267 public this(string expectedPart, string reason, string butGotPart, string file, size_t line) pure @safe 268 in 269 { 270 assert(!expectedPart.empty); 271 assert(!butGotPart.empty); 272 } 273 do 274 { 275 this.expectedPart = expectedPart; 276 this.reason = reason; 277 this.butGotPart = butGotPart; 278 279 super(combinedMessage, file, line); 280 } 281 282 public this(string msg, string file, size_t line) pure @safe 283 in 284 { 285 assert(!msg.empty); 286 } 287 do 288 { 289 this.expectedPart = msg; 290 291 super(combinedMessage, file, line); 292 } 293 294 public FluentError because(string reason) pure @safe 295 { 296 return new FluentError(this.expectedPart, reason, this.butGotPart, this.file, this.line); 297 } 298 299 private @property string combinedMessage() pure @safe 300 { 301 string message = format!`Test failed: expected %s`(this.expectedPart); 302 303 if (!this.reason.empty) 304 { 305 message ~= format!` because %s`(this.reason); 306 } 307 308 if (!this.butGotPart.empty) 309 { 310 message ~= format!`, but got %s`(this.butGotPart); 311 } 312 313 return message; 314 } 315 } 316 317 /** 318 * When a fluent exception is thrown during the evaluation of the left-hand side of this word, 319 * then the reason for the test is set to the `reason` parameter. 320 * 321 * Usage: 2.should.be(2).because("math is sane"); 322 */ 323 public T because(T)(lazy T value, string reason) 324 { 325 try 326 { 327 return value; 328 } 329 catch (FluentError fluentError) 330 { 331 throw fluentError.because(reason); 332 } 333 } 334 335 /** 336 * Indicates a fluent assert has failed, as well as what was tested, why it was tested, and what the outcome was. 337 * When unit_threaded is provided, FluentError is a unit_threaded test error. 338 */ 339 static if (__traits(compiles, { import unit_threaded.exception : UnitTestError; })) 340 { 341 import unit_threaded.exception : UnitTestError; 342 343 public alias FluentError = FluentErrorImpl!UnitTestError; 344 } 345 else 346 { 347 public alias FluentError = FluentErrorImpl!AssertError; 348 } 349 350 deprecated("replace FluentException with FluentError") 351 public alias FluentException = FluentError;