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 return this.got_(); 68 } 69 70 private this(G got) { this.got_ = got; this.refCount = 1; } 71 72 /** 73 * Manually initialize a new ShouldType value from an existing one's ref count. 74 * All ShouldTypes of one phrase must use the same reference counter. 75 */ 76 public this(G got, ref int refCount) @trusted 77 in 78 { 79 assert(refCount != CHAIN_TERMINATED, "don't copy Should that's been terminated"); 80 } 81 do 82 { 83 this.got_ = got; 84 this.refCount_ = &refCount; 85 this.refCount++; 86 } 87 88 this(this) @trusted 89 in 90 { 91 assert(this.refCount != CHAIN_TERMINATED); 92 } 93 do 94 { 95 this.refCount++; 96 } 97 98 ~this() @trusted 99 { 100 this.refCount--; 101 assert(this.refCount > 0, "unterminated should-chain!"); 102 } 103 104 /** 105 * Checks a boolean condition for truth, throwing an exception when it fails. 106 * The components making up the exception string are passed lazily. 107 * The message has the form: "Test failed: expected {expected}[ because reason], but got {butGot}" 108 * For instance: "Test failed: expected got.empty() because there should be nothing in there, but got [5]." 109 * In that case, `expected` is "got.empty()" and `butGot` is "[5]". 110 */ 111 public void check(bool condition, lazy string expected, lazy string butGot, string file, size_t line) pure @safe 112 { 113 terminateChain; 114 115 if (!condition) 116 { 117 throw new FluentException(expected, butGot, file, line); 118 } 119 } 120 121 /** 122 * Mark that the semantic end of this phrase has been reached. 123 * If this is not called, the phrase will error on scope exit. 124 */ 125 public void terminateChain() 126 { 127 this.refCount = CHAIN_TERMINATED; // terminate chain, safe ref checker 128 } 129 130 private static enum isStringLiteral(T...) = T.length == 1 && is(typeof(T[0]) == string); 131 132 /** 133 * Allows to check that only a select list of words are permitted before the current word. 134 * On failure, an informative error is printed. 135 * Usage: should.allowOnlyWords!("word1", "word2").before!"newWord"; 136 */ 137 public template allowOnlyWords(allowedWords...) 138 if (allSatisfy!(isStringLiteral, allowedWords)) 139 { 140 void before(string newWord)() 141 { 142 static foreach (word; phrase) 143 { 144 static assert([allowedWords].canFind(word), `bad grammar: "` ~ word ~ ` ` ~ newWord ~ `"`); 145 } 146 } 147 } 148 149 /** 150 * Allows to check that a specified word appeared in the phrase before the current word. 151 * On failure, an informative error is printed. 152 * Usage: should.requireWord!"word".before!"newWord"; 153 */ 154 public template requireWord(string requiredWord) 155 { 156 void before(string newWord)() 157 { 158 static assert( 159 hasWord!requiredWord, 160 `bad grammar: expected "` ~ requiredWord ~ `" before "` ~ newWord ~ `"` 161 ); 162 } 163 } 164 165 /** 166 * Evaluates to true if the given word exists in the current phrase. 167 */ 168 public enum hasWord(string word) = phrase.canFind(word); 169 170 // work around https://issues.dlang.org/show_bug.cgi?id=18839 171 public auto empty()(Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 172 { 173 return this.empty_(Fence(), file, line); 174 } 175 176 private enum CHAIN_TERMINATED = int.max; 177 178 private @property ref int refCount() 179 { 180 if (this.refCount_ is null) 181 { 182 this.refCount_ = new int; 183 *this.refCount_ = 0; 184 } 185 return *this.refCount_; 186 } 187 } 188 189 /** 190 * Ensures that the given range is empty. 191 * Specified here due to https://issues.dlang.org/show_bug.cgi?id=18839 192 */ 193 public void empty_(Should)(Should should, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 194 if (isInstanceOf!(ShouldType, Should)) 195 { 196 import std.range : empty; 197 198 with (should) 199 { 200 allowOnlyWords!("be", "not").before!"empty"; 201 requireWord!"be".before!"empty"; 202 203 terminateChain; 204 205 auto got = should.got(); 206 207 static if (hasWord!"not") 208 { 209 check(!got.empty, "nonempty range", format("%s", got), file, line); 210 } 211 else 212 { 213 check(got.empty, "empty range", format("%s", got), file, line); 214 } 215 } 216 } 217 218 /// 219 unittest 220 { 221 import dshould.basic; 222 223 [].should.be.empty; 224 [5].should.not.be.empty; 225 } 226 227 private class FluentExceptionImpl(T : Exception) : T 228 { 229 private const string expectedPart = null; // before reason 230 public const string reason = null; 231 private const string butGotPart = null; // after reason 232 233 invariant 234 { 235 assert(!this.expectedPart.empty); 236 } 237 238 public this(string expectedPart, string butGotPart, string file, size_t line) pure @safe 239 in 240 { 241 assert(!expectedPart.empty); 242 assert(!butGotPart.empty); 243 } 244 do 245 { 246 this.expectedPart = expectedPart; 247 this.butGotPart = butGotPart; 248 249 super(combinedMessage, file, line); 250 } 251 252 public this(string expectedPart, string reason, 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.reason = reason; 262 this.butGotPart = butGotPart; 263 264 super(combinedMessage, file, line); 265 } 266 267 public this(string msg, string file, size_t line) pure @safe 268 in 269 { 270 assert(!msg.empty); 271 } 272 do 273 { 274 this.expectedPart = msg; 275 276 super(combinedMessage, file, line); 277 } 278 279 public FluentException because(string reason) pure @safe 280 { 281 return new FluentException(this.expectedPart, reason, this.butGotPart, this.file, this.line); 282 } 283 284 private @property string combinedMessage() pure @safe 285 { 286 string message = format!`Test failed: expected %s`(this.expectedPart); 287 288 if (!this.reason.empty) 289 { 290 message ~= format!` because %s`(this.reason); 291 } 292 293 if (!this.butGotPart.empty) 294 { 295 message ~= format!`, but got %s`(this.butGotPart); 296 } 297 298 return message; 299 } 300 } 301 302 /** 303 * When a fluent exception is thrown during the evaluation of the left-hand side of this word, 304 * then the reason for the test is set to the `reason` parameter. 305 * 306 * Usage: 2.should.be(2).because("math is sane"); 307 */ 308 public T because(T)(lazy T value, string reason) 309 { 310 try 311 { 312 return value; 313 } 314 catch (FluentException fluentException) 315 { 316 throw fluentException.because(reason); 317 } 318 } 319 320 static if (__traits(compiles, { import unit_threaded.should : UnitTestException; })) 321 { 322 import unit_threaded.should : UnitTestException; 323 324 /** 325 * Indicates a fluent assert has failed, as well as what was tested, why it was tested, and what the outcome was. 326 * When unit_threaded is provided, FluentException is a unit_threaded test exception. 327 */ 328 public alias FluentException = FluentExceptionImpl!UnitTestException; 329 } 330 else 331 { 332 public alias FluentException = FluentExceptionImpl!Exception; 333 }