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