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 }