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