1 module dshould.contain; 2 3 import dshould.ShouldType; 4 import dshould.basic : not, should; 5 import std.traits : isAssociativeArray; 6 import std.typecons : Yes; 7 8 /** 9 * The word `.contain` takes one value, expected to appear in the range on the left hand side. 10 */ 11 public void contain(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 12 if (isInstanceOf!(ShouldType, Should)) 13 { 14 should.allowOnlyWords!("not", "only").before!"contain"; 15 16 should.addWord!"contain".checkContain(expected, file, line); 17 } 18 19 /// 20 unittest 21 { 22 [2, 3, 4].should.contain(3); 23 [2, 3, 4].should.not.contain(5); 24 } 25 26 public auto contain(Should)(Should should) 27 if (isInstanceOf!(ShouldType, Should)) 28 { 29 should.allowOnlyWords!("not").before!"contain"; 30 31 return should.addWord!"contain"; 32 } 33 34 /** 35 * The phrase `.contain.only` or `.only.contain` takes a range, the elements of which are expected to be the only 36 * elements appearing in the range on the left hand side. 37 */ 38 public void only(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 39 if (isInstanceOf!(ShouldType, Should)) 40 { 41 should.requireWord!"contain".before!"only"; 42 should.allowOnlyWords!("not", "contain").before!"only"; 43 44 should.addWord!"only".checkContain(expected, file, line); 45 } 46 47 /// 48 unittest 49 { 50 [3, 4].should.only.contain([4, 3]); 51 [3, 4].should.only.contain([1, 2, 3, 4]); 52 [3, 4].should.contain.only([4, 3]); 53 [2, 3, 4].should.not.only.contain([4, 3]); 54 } 55 56 public auto only(Should)(Should should) 57 if (isInstanceOf!(ShouldType, Should)) 58 { 59 should.allowOnlyWords!("not").before!"only"; 60 61 return should.addWord!"only"; 62 } 63 64 /** 65 * The phrase `.contain.all` takes a range, all elements of which are expected to appear 66 * in the range on the left hand side. 67 */ 68 public void all(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 69 if (isInstanceOf!(ShouldType, Should)) 70 { 71 should.requireWord!"contain".before!"all"; 72 should.allowOnlyWords!("not", "contain").before!"all"; 73 74 should.addWord!"all".checkContain(expected, file, line); 75 } 76 77 /// 78 unittest 79 { 80 [2, 3, 4].should.contain.all([3]); 81 [2, 3, 4].should.contain.all([4, 3]); 82 [2, 3, 4].should.not.contain.all([3, 4, 5]); 83 } 84 85 /** 86 * The phrase `.contain.any` takes a range, at least one element of which is expected to appear 87 * in the range on the left hand side. 88 */ 89 public void any(Should, T)(Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 90 if (isInstanceOf!(ShouldType, Should)) 91 { 92 should.requireWord!"contain".before!"any"; 93 should.allowOnlyWords!("not", "contain").before!"any"; 94 95 should.addWord!"any".checkContain(expected, file, line); 96 } 97 98 /// 99 unittest 100 { 101 [2, 3, 4].should.contain.any([4, 5]); 102 [2, 3, 4].should.not.contain.any([5, 6]); 103 } 104 105 unittest 106 { 107 const int[] constArray = [2, 3, 4]; 108 109 constArray.should.contain(4); 110 } 111 112 unittest 113 { 114 string[string] assocArray = ["a": "b"]; 115 116 assocArray.should.contain.all(["a": "b"]); 117 assocArray.should.not.contain.any(["a": "y"]); 118 assocArray.should.not.contain.any(["x": "b"]); 119 } 120 121 /** 122 * The phrase `.contain.exactly` indicates that two ranges are expected to contain exactly 123 * the same elements, but possibly in a different order. 124 */ 125 public void exactly(Should, T)( 126 Should should, T expected, Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__) 127 if (isInstanceOf!(ShouldType, Should)) 128 { 129 import std.algorithm : all, sort; 130 131 should.requireWord!"contain".before!"exactly"; 132 should.allowOnlyWords!"contain".before!"exactly"; 133 134 string colorCodedDelta(LHS, RHS)(LHS lhs, RHS rhs) 135 { 136 import dshould.stringcmp : colorizedDiff, green, red; 137 import std.array : array; 138 import std.conv : to; 139 import std.algorithm : map; 140 import std.format : format; 141 142 alias removePred = lines => lines.map!(line => red("- " ~ line)); 143 alias addPred = lines => lines.map!(line => green("+ " ~ line)); 144 alias keepPred = lines => lines.map!(line => " " ~ line); 145 146 return format( 147 "\n[\n%-(%s,\n%)\n]", 148 colorizedDiff!(string[], removePred, addPred, keepPred)( 149 rhs.map!(to!string).array.sort.array, lhs.map!(to!string).array.sort.array, Yes.forceDiff)); 150 } 151 152 with (should) 153 { 154 auto got = should.got(); 155 156 check( 157 got.all!(a => expected.canFind(a)) && expected.all!(a => got.canFind(a)), 158 "exact set of values", 159 colorCodedDelta(got, expected), 160 file, line); 161 } 162 } 163 164 /// 165 unittest 166 { 167 import dshould : equal; 168 import dshould.stringcmp : green, red; 169 import dshould.thrown : throwA; 170 171 [3, 4].should.contain.exactly([3, 4]); 172 [3, 4].should.contain.exactly([4, 3]); 173 [3, 4].should.contain.exactly([3]).should.throwA!FluentError.where.msg.should.equal( 174 "Test failed: expected exact set of values, but got \n" 175 ~ "[\n" 176 ~ " 3,\n" 177 ~ green("+ 4") ~ "\n" 178 ~ "]"); 179 [3, 4].should.contain.exactly([3, 4, 5]).should.throwA!FluentError.where.msg.should.equal( 180 "Test failed: expected exact set of values, but got \n" 181 ~ "[\n" 182 ~ " 3,\n" 183 ~ " 4,\n" 184 ~ red("- 5") ~ "\n" 185 ~ "]"); 186 [3, 4].should.contain.exactly([3, 5]).should.throwA!FluentError.where.msg.should.equal( 187 "Test failed: expected exact set of values, but got \n" 188 ~ "[\n" 189 ~ " 3,\n" 190 ~ green("+ 4") ~ ",\n" 191 ~ red("- 5") ~ "\n" 192 ~ "]"); 193 } 194 195 private void checkContain(Should, T)(Should should, T expected, string file, size_t line) 196 if (isInstanceOf!(ShouldType, Should) && isAssociativeArray!T && is(const typeof(should.got()) == const T)) 197 { 198 import std.algorithm : any, all, canFind; 199 import std.format : format; 200 201 alias pairEqual = (a, b) => a.key == b.key && a.value == b.value; 202 203 with (should) 204 { 205 auto got = should.got(); 206 alias inExpected = a => expected.byKeyValue.canFind!pairEqual(a); 207 alias inGot = a => got.byKeyValue.canFind!pairEqual(a); 208 209 static if (hasWord!"only") 210 { 211 static if (hasWord!"not") 212 { 213 check( 214 !got.byKeyValue.all!inExpected, 215 format("associative array containing pairs other than %s", expected), 216 format("%s", got), 217 file, line); 218 } 219 else 220 { 221 check( 222 got.byKeyValue.all!inExpected, 223 format("associative array containing only the pairs %s", expected), 224 format("%s", got), 225 file, line); 226 } 227 } 228 else static if (hasWord!"all") 229 { 230 static if (hasWord!"not") 231 { 232 check( 233 !expected.byKeyValue.all!inGot, 234 format("associative array not containing every pair in %s", expected), 235 format("%s", got), 236 file, line); 237 } 238 else 239 { 240 check( 241 expected.byKeyValue.all!inGot, 242 format("associative array containing every pair in %s", expected), 243 format("%s", got), 244 file, line); 245 } 246 } 247 else static if (hasWord!"any") 248 { 249 static if (hasWord!"not") 250 { 251 check( 252 !expected.byKeyValue.any!inGot, 253 format("associative array not containing any pair in %s", expected), 254 format("%s", got), 255 file, line); 256 } 257 else 258 { 259 check( 260 expected.byKeyValue.any!inGot, 261 format("associative array containing any pair of %s", expected), 262 format("%s", got), 263 file, line); 264 } 265 } 266 else 267 { 268 static assert(false, 269 `bad grammar: expected "contain all", "contain any", "contain only" (or "only contain")`); 270 } 271 } 272 } 273 274 private void checkContain(Should, T)(Should should, T expected, string file, size_t line) 275 if (isInstanceOf!(ShouldType, Should) && !isAssociativeArray!T) 276 { 277 import std.algorithm : any, all, canFind; 278 import std.format : format; 279 import std.range : ElementType, save; 280 281 with (should) 282 { 283 auto got = should.got(); 284 285 enum rhsIsValue = is(const T == const ElementType!(typeof(got))); 286 287 static if (rhsIsValue) 288 { 289 allowOnlyWords!("not", "only", "contain").before!"contain"; 290 291 static if (hasWord!"only") 292 { 293 static if (hasWord!"not") 294 { 295 check( 296 got.any!(a => a != expected), 297 format("array containing values other than %s", expected), 298 format("%s", got), 299 file, line); 300 } 301 else 302 { 303 check( 304 got.all!(a => a == expected), 305 format("array containing only the value %s", expected), 306 format("%s", got), 307 file, line); 308 } 309 } 310 else 311 { 312 static if (hasWord!"not") 313 { 314 check( 315 !got.save.canFind(expected), 316 format("array not containing %s", expected), 317 format("%s", got), 318 file, line); 319 } 320 else 321 { 322 check( 323 got.save.canFind(expected), 324 format("array containing %s", expected), 325 format("%s", got), 326 file, line); 327 } 328 } 329 } 330 else 331 { 332 static if (hasWord!"only") 333 { 334 static if (hasWord!"not") 335 { 336 check( 337 !got.all!(a => expected.save.canFind(a)), 338 format("array containing values other than %s", expected), 339 format("%s", got), 340 file, line); 341 } 342 else 343 { 344 check( 345 got.all!(a => expected.save.canFind(a)), 346 format("array containing only the values %s", expected), 347 format("%s", got), 348 file, line); 349 } 350 } 351 else static if (hasWord!"all") 352 { 353 static if (hasWord!"not") 354 { 355 check( 356 !expected.all!(a => got.save.canFind(a)), 357 format("array not containing every value in %s", expected), 358 format("%s", got), 359 file, line); 360 } 361 else 362 { 363 check( 364 expected.all!(a => got.save.canFind(a)), 365 format("array containing every value in %s", expected), 366 format("%s", got), 367 file, line); 368 } 369 } 370 else static if (hasWord!"any") 371 { 372 static if (hasWord!"not") 373 { 374 check( 375 !expected.any!(a => got.save.canFind(a)), 376 format("array not containing any value in %s", expected), 377 format("%s", got), 378 file, line); 379 } 380 else 381 { 382 check( 383 expected.any!(a => got.save.canFind(a)), 384 format("array containing any value of %s", expected), 385 format("%s", got), 386 file, line); 387 } 388 } 389 else 390 { 391 static assert(false, 392 `bad grammar: expected "contain all", "contain any", "contain only" (or "only contain")`); 393 } 394 } 395 } 396 } 397 398 unittest 399 { 400 const foo = ["foo": "bar"]; 401 402 foo.byKey.should.contain("foo"); 403 foo.byValue.should.contain("bar"); 404 }