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 // work around https://issues.dlang.org/show_bug.cgi?id=18839 180 public auto empty()(Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 181 { 182 return this.empty_(Fence(), file, line); 183 } 184 185 private enum CHAIN_TERMINATED = int.max; 186 187 private @property ref int refCount() 188 { 189 if (this.refCount_ is null) 190 { 191 this.refCount_ = new int; 192 *this.refCount_ = 0; 193 } 194 return *this.refCount_; 195 } 196 } 197 198 /** 199 * Ensures that the given range is empty. 200 * Specified here due to https://issues.dlang.org/show_bug.cgi?id=18839 201 */ 202 public void empty_(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 203 if (isInstanceOf!(ShouldType, Should)) 204 { 205 import std.range : empty; 206 207 with (should) 208 { 209 allowOnlyWords!("be", "not").before!"empty"; 210 requireWord!"be".before!"empty"; 211 212 terminateChain; 213 214 auto got = should.got(); 215 216 static if (hasWord!"not") 217 { 218 check(!got.empty, "nonempty range", format!"%s"(got), file, line); 219 } 220 else 221 { 222 check(got.empty, "empty range", format!"%s"(got), file, line); 223 } 224 } 225 } 226 227 /// 228 unittest 229 { 230 import dshould.basic; 231 232 (int[]).init.should.be.empty; 233 [5].should.not.be.empty; 234 } 235 236 private class FluentExceptionImpl(T : Exception) : T 237 { 238 private const string expectedPart = null; // before reason 239 public const string reason = null; 240 private const string butGotPart = null; // after reason 241 242 invariant 243 { 244 assert(!this.expectedPart.empty); 245 } 246 247 public this(string expectedPart, string butGotPart, string file, size_t line) pure @safe 248 in 249 { 250 assert(!expectedPart.empty); 251 assert(!butGotPart.empty); 252 } 253 do 254 { 255 this.expectedPart = expectedPart; 256 this.butGotPart = butGotPart; 257 258 super(combinedMessage, file, line); 259 } 260 261 public this(string expectedPart, string reason, string butGotPart, string file, size_t line) pure @safe 262 in 263 { 264 assert(!expectedPart.empty); 265 assert(!butGotPart.empty); 266 } 267 do 268 { 269 this.expectedPart = expectedPart; 270 this.reason = reason; 271 this.butGotPart = butGotPart; 272 273 super(combinedMessage, file, line); 274 } 275 276 public this(string msg, string file, size_t line) pure @safe 277 in 278 { 279 assert(!msg.empty); 280 } 281 do 282 { 283 this.expectedPart = msg; 284 285 super(combinedMessage, file, line); 286 } 287 288 public FluentException because(string reason) pure @safe 289 { 290 return new FluentException(this.expectedPart, reason, this.butGotPart, this.file, this.line); 291 } 292 293 private @property string combinedMessage() pure @safe 294 { 295 string message = format!`Test failed: expected %s`(this.expectedPart); 296 297 if (!this.reason.empty) 298 { 299 message ~= format!` because %s`(this.reason); 300 } 301 302 if (!this.butGotPart.empty) 303 { 304 message ~= format!`, but got %s`(this.butGotPart); 305 } 306 307 return message; 308 } 309 } 310 311 /** 312 * When a fluent exception is thrown during the evaluation of the left-hand side of this word, 313 * then the reason for the test is set to the `reason` parameter. 314 * 315 * Usage: 2.should.be(2).because("math is sane"); 316 */ 317 public T because(T)(lazy T value, string reason) 318 { 319 try 320 { 321 return value; 322 } 323 catch (FluentException fluentException) 324 { 325 throw fluentException.because(reason); 326 } 327 } 328 329 static if (__traits(compiles, { import unit_threaded.should : UnitTestException; })) 330 { 331 import unit_threaded.should : UnitTestException; 332 333 /** 334 * Indicates a fluent assert has failed, as well as what was tested, why it was tested, and what the outcome was. 335 * When unit_threaded is provided, FluentException is a unit_threaded test exception. 336 */ 337 public alias FluentException = FluentExceptionImpl!UnitTestException; 338 } 339 else 340 { 341 public alias FluentException = FluentExceptionImpl!Exception; 342 }