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