1 module unit_threaded.randomized.gen;
2 
3 import std.traits : isSomeString, isNumeric, isFloatingPoint, isIntegral, isSomeChar, isAggregateType;
4 import std.random : uniform, Random;
5 import std.range: isInputRange, ElementType;
6 import std.algorithm: filter, map;
7 import std.array: array;
8 
9 import unit_threaded;
10 
11 /* Return $(D true) if the passed $(D T) is a $(D Gen) struct.
12 
13 A $(D Gen!T) is something that implicitly converts to $(D T), has a method
14 called $(D gen) that is accepting a $(D ref Random).
15 
16 This module already brings Gens for numeric types, strings and ascii strings.
17 
18 If a function needs to be benchmarked that has a parameter of custom type a
19 custom $(D Gen) is required.
20 */
21 template isGen(T)
22 {
23     static if (is(T : Gen!(S), S...))
24         enum isGen = true;
25     else
26         enum isGen = false;
27 }
28 
29 ///
30 @UnitTest
31 unittest
32 {
33     static assert(!isGen!int);
34     static assert(isGen!(Gen!(int, 0, 10)));
35 }
36 
37 private template minimum(T) {
38     import std.traits: isIntegral, isFloatingPoint;
39     static if(isIntegral!T || isSomeChar!T)
40         enum minimum = T.min;
41     else static if (isFloatingPoint!T)
42         enum mininum = T.min_normal;
43     else
44         enum minimum = T.init;
45 }
46 
47 private template maximum(T) {
48     static if(isNumeric!T)
49         enum maximum = T.max;
50     else
51         enum maximum = T.init;
52 }
53 
54 /** A $(D Gen) type that generates numeric values between the values of the
55 template parameter $(D low) and $(D high).
56 */
57 mixin template GenNumeric(T, T low, T high) {
58 
59     static assert(is(typeof(() {
60         T[] res = frontLoaded();
61     })), "GenNumeric needs a function frontLoaded returning " ~ T.stringof ~ "[]");
62 
63     alias Value = T;
64 
65     T value;
66 
67     T gen(ref Random gen)
68     {
69         static assert(low <= high);
70 
71         this.value = _index < frontLoaded.length
72             ? frontLoaded[_index++]
73             : uniform!("[]")(low, high, gen);
74 
75         return this.value;
76     }
77 
78     ref T opCall()
79     {
80         return this.value;
81     }
82 
83     void toString(scope void delegate(const(char)[]) sink) @trusted
84     {
85         import std.format : formattedWrite;
86 
87         static if (isFloatingPoint!T)
88         {
89             static if (low == T.min_normal && high == T.max)
90             {
91                 formattedWrite(sink, "'%s'", this.value);
92             }
93         }
94         else static if (low == T.min && high == T.max)
95         {
96             formattedWrite(sink, "'%s'", this.value);
97         }
98         else
99         {
100             formattedWrite(sink, "'%s' low = '%s' high = '%s'", this.value,
101                 low, high);
102         }
103     }
104 
105     alias opCall this;
106 
107 
108     private int _index;
109 }
110 
111 /** A $(D Gen) type that generates numeric values between the values of the
112 template parameter $(D low) and $(D high).
113 */
114 struct Gen(T, T low = minimum!T, T high = maximum!T) if (isIntegral!T)
115 {
116     private static T[] frontLoaded() @safe pure nothrow {
117         T[] values = [0, 1, T.min, T.max];
118         return values.filter!(a => a >= low && a <= high).array;
119     }
120 
121     mixin GenNumeric!(T, low, high);
122 }
123 
124 struct Gen(T, T low = 0, T high = 6.022E23) if(isFloatingPoint!T) {
125      private static T[] frontLoaded() @safe pure nothrow {
126          T[] values = [0, T.epsilon, T.min_normal, high];
127          return values.filter!(a => a >= low && a <= high).array;
128     }
129 
130     mixin GenNumeric!(T, low, high);
131 }
132 
133 @safe pure unittest {
134     import unit_threaded.asserts: assertEqual;
135 
136     auto rnd = Random(1337);
137     Gen!int gen;
138     assertEqual(gen.gen(rnd), 0);
139     assertEqual(gen.gen(rnd), 1);
140     assertEqual(gen.gen(rnd), int.min);
141     assertEqual(gen.gen(rnd), int.max);
142     assertEqual(gen.gen(rnd), 1125387415); //1st non front-loaded value
143 }
144 
145 @safe unittest {
146     // not pure because of floating point flags
147     import unit_threaded.asserts: assertEqual;
148     import std.math: approxEqual;
149     import std.conv: to;
150 
151     auto rnd = Random(1337);
152     Gen!float gen;
153     assertEqual(gen.gen(rnd), 0);
154     assertEqual(gen.gen(rnd), float.epsilon);
155     assertEqual(gen.gen(rnd), float.min_normal);
156     assert(approxEqual(gen.gen(rnd), 6.022E23), gen.value.to!string);
157     assert(approxEqual(gen.gen(rnd), 1.57791E23), gen.value.to!string);
158 }
159 
160 
161 @safe unittest {
162     // not pure because of floating point flags
163     import unit_threaded.asserts: assertEqual;
164     import std.math: approxEqual;
165     import std.conv: to;
166 
167     auto rnd = Random(1337);
168     Gen!(float, 0, 5) gen;
169     assertEqual(gen.gen(rnd), 0);
170     assertEqual(gen.gen(rnd), float.epsilon);
171     assertEqual(gen.gen(rnd), float.min_normal);
172     assertEqual(gen.gen(rnd), 5);
173     assert(approxEqual(gen.gen(rnd), 1.31012), gen.value.to!string);
174 }
175 
176 /** A $(D Gen) type that generates unicode strings with a number of
177 charatacters that is between template parameter $(D low) and $(D high).
178 */
179 struct Gen(T, size_t low = 0, size_t high = 32) if (isSomeString!T)
180 {
181     static immutable T charSet;
182     static immutable size_t numCharsInCharSet;
183     alias Value = T;
184 
185     T value;
186     static this()
187     {
188         import std.array : array;
189         import std.uni : unicode;
190         import std.format : format;
191         import std.range : chain, iota;
192         import std.algorithm : map, joiner;
193         import std.conv : to;
194         import std.utf : count;
195 
196         Gen!(T, low, high).charSet = chain(
197             iota(0x21, 0x7E).map!(a => to!T(cast(dchar) a)),
198             iota(0xA1, 0x1EF).map!(a => to!T(cast(dchar) a)))
199             .joiner.array.to!T;
200         Gen!(T, low, high).numCharsInCharSet = count(charSet);
201     }
202 
203     T gen(ref Random gen)
204     {
205         static assert(low <= high);
206         import std.range : drop;
207         import std.array : appender, front;
208         import std.utf : byDchar;
209 
210         if(_index < frontLoaded.length) {
211             value = frontLoaded[_index++];
212             return value;
213         }
214 
215         auto app = appender!T();
216         app.reserve(high);
217         size_t numElems = uniform!("[]")(low, high, gen);
218 
219         for (size_t i = 0; i < numElems; ++i)
220         {
221             size_t toSelect = uniform!("[)")(0, numCharsInCharSet, gen);
222             app.put(charSet.byDchar().drop(toSelect).front);
223         }
224 
225         this.value = app.data;
226         return this.value;
227     }
228 
229     ref T opCall()
230     {
231         return this.value;
232     }
233 
234     void toString(scope void delegate(const(char)[]) sink)
235     {
236         import std.format : formattedWrite;
237 
238         static if (low == 0 && high == 32)
239         {
240             formattedWrite(sink, "'%s'", this.value);
241         }
242         else
243         {
244             formattedWrite(sink, "'%s' low = '%s' high = '%s'", this.value,
245                            low, high);
246         }
247     }
248 
249     alias opCall this;
250 
251 private:
252 
253     int _index;
254 
255     T[] frontLoaded() @safe pure nothrow const {
256         import std.algorithm: filter;
257         import std.array: array;
258         T[] values = ["", "a", "é"];
259         return values.filter!(a => a.length >= low && a.length <= high).array;
260     }
261 }
262 
263 unittest
264 {
265     import std.meta : AliasSeq, aliasSeqOf;
266     import std.range : iota;
267     import std.array : empty;
268     import unit_threaded.asserts;
269 
270     foreach (index, T; AliasSeq!(string, wstring, dstring)) {
271         auto r = Random(1337);
272         Gen!T a;
273         T expected = "";
274         assertEqual(a.gen(r), expected);
275         expected = "a";
276         assertEqual(a.gen(r), expected);
277         expected = "é";
278         assertEqual(a.gen(r), expected);
279         expected = "¥ǫƔSūOēLJĂ¹ũ/ŇQĚćzĬůƫË­ÔRĎƕƙĹÒ";
280         assertEqual(a.gen(r), expected);
281     }
282 }
283 
284 /// DITTO This random $(D string)s only consisting of ASCII character
285 struct GenASCIIString(size_t low = 1, size_t high = 32)
286 {
287     static string charSet;
288     static immutable size_t numCharsInCharSet;
289 
290     string value;
291 
292     static this()
293     {
294         import std.array : array;
295         import std.uni : unicode;
296         import std.format : format;
297         import std.range : chain, iota;
298         import std.algorithm : map, joiner;
299                 import std.conv : to;
300                 import std.utf : byDchar, count;
301 
302         GenASCIIString!(low, high).charSet = to!string(chain(iota(0x21,
303             0x7E).map!(a => to!char(cast(dchar) a)).array));
304 
305         GenASCIIString!(low, high).numCharsInCharSet = count(charSet);
306     }
307 
308     string gen(ref Random gen)
309     {
310         import std.array : appender;
311 
312         if(_index < frontLoaded.length) {
313             value = frontLoaded[_index++];
314             return value;
315         }
316 
317         auto app = appender!string();
318         app.reserve(high);
319         size_t numElems = uniform!("[]")(low, high, gen);
320 
321         for (size_t i = 0; i < numElems; ++i)
322         {
323             size_t toSelect = uniform!("[)")(0, numCharsInCharSet, gen);
324             app.put(charSet[toSelect]);
325         }
326 
327         this.value = app.data;
328         return this.value;
329     }
330 
331     ref string opCall()
332     {
333         return this.value;
334     }
335 
336     void toString(scope void delegate(const(char)[]) sink)
337     {
338         import std.format : formattedWrite;
339 
340         static if (low == 0 && high == 32)
341         {
342             formattedWrite(sink, "'%s'", this.value);
343         }
344         else
345         {
346             formattedWrite(sink, "'%s' low = '%s' high = '%s'", this.value,
347                 low, high);
348         }
349     }
350 
351     alias opCall this;
352 
353 private:
354 
355     int _index;
356 
357     string[] frontLoaded() @safe pure nothrow const {
358         return ["", "a"];
359     }
360 }
361 
362 
363 @safe unittest {
364     import unit_threaded.asserts;
365     auto rnd = Random(1337);
366     GenASCIIString!() gen;
367     assertEqual(gen.gen(rnd), "");
368     assertEqual(gen.gen(rnd), "a");
369     assertEqual(gen.gen(rnd), "i<pDqp7-LV;W`d)w/}VXi}TR=8CO|m");
370 }
371 
372 struct Gen(T, size_t low = 1, size_t high = 1024) if(isInputRange!T && isNumeric!(ElementType!T)) {
373 
374     import std.traits: Unqual, isIntegral, isFloatingPoint;
375     alias Value = T;
376     alias E = Unqual!(ElementType!T);
377 
378     T value;
379 
380     T gen(ref Random rnd) {
381         value = _index < frontLoaded.length
382             ? frontLoaded[_index++]
383             : genArray(rnd);
384         return value;
385     }
386 
387     alias value this;
388 
389 private:
390 
391     size_t _index;
392      //these values are always generated
393     T[] frontLoaded() @safe pure nothrow {
394         T[] ret = [[], [ElementType!T(0)], [ElementType!T(1)]];
395         return ret;
396     }
397 
398     T genArray(ref Random rnd) {
399         import std.array: appender;
400         immutable length = uniform(low, high, rnd);
401         auto app = appender!T;
402         app.reserve(length);
403         foreach(i; 0 .. length) {
404             static if(isIntegral!E)
405                 app.put(uniform(E.min, E.max, rnd));
406             else static if(isFloatingPoint!E)
407                 app.put(uniform(-1e12, 1e12, rnd));
408             else
409                 static assert("Cannot generage elements of type ", E.stringof);
410         }
411 
412         return app.data;
413     }
414 }
415 
416 static assert(isGen!(Gen!(int[])));
417 
418 
419 @("Gen!int[] generates random arrays of int")
420 @safe pure unittest {
421     import unit_threaded.asserts: assertEqual;
422 
423     auto rnd = Random(1337);
424     auto gen = Gen!(int[], 1, 10)();
425 
426     // first the front-loaded values
427     assertEqual(gen.gen(rnd), []);
428     assertEqual(gen.gen(rnd), [0]);
429     assertEqual(gen.gen(rnd), [1]);
430     // then the first pseudo-random one
431     assertEqual(gen.gen(rnd),
432                 [-1465941156, -1234426648, -952939353, 185030105,
433                  -174732633, -2001577638, -768796814, -1136496558, 78996564]);
434 }
435 
436 @("Gen!ubyte[] generates random arrays of ubyte")
437 @safe pure unittest {
438     import unit_threaded.asserts: assertEqual;
439     auto rnd = Random(1337);
440     auto gen = Gen!(ubyte[], 1, 10)();
441     assertEqual(gen.gen(rnd), []);
442 }
443 
444 
445 @("Gen!double[] generates random arrays of double")
446 @safe unittest {
447     import unit_threaded.asserts: assertEqual;
448 
449     auto rnd = Random(1337);
450     auto gen = Gen!(double[], 1, 10)();
451 
452     // first the front-loaded values
453     assertEqual(gen.gen(rnd), []);
454     assertEqual(gen.gen(rnd), [0]);
455     assertEqual(gen.gen(rnd), [1]);
456     assertEqual(gen.gen(rnd).length, 9);
457 }
458 
459 struct Gen(T) if(is(T == bool)) {
460     bool value;
461     alias value this;
462     bool gen(ref Random rnd) @safe {
463         value = [false, true][uniform(0, 2, rnd)];
464         return value;
465     }
466 }
467 
468 @("Gen!bool generates random booleans")
469 @safe unittest {
470     import unit_threaded.asserts: assertEqual;
471 
472     auto rnd = Random(1337);
473     auto gen = Gen!bool();
474 
475     assertEqual(gen.gen(rnd), true);
476     assertEqual(gen.gen(rnd), true);
477     assertEqual(gen.gen(rnd), false);
478     assertEqual(gen.gen(rnd), false);
479 }
480 
481 
482 struct Gen(T, T low = minimum!T, T high = maximum!T) if (isSomeChar!T)
483 {
484     private static T[] frontLoaded() @safe pure nothrow { return []; }
485     mixin GenNumeric!(T, low, high);
486 }
487 
488 
489 @("Gen char, wchar, dchar")
490 @safe unittest {
491     import unit_threaded.asserts: assertEqual;
492     {
493         auto rnd = Random(1337);
494         Gen!char gen;
495         assertEqual(cast(int)gen.gen(rnd), 151);
496     }
497     {
498         auto rnd = Random(1337);
499         Gen!wchar gen;
500         assertEqual(cast(int)gen.gen(rnd), 3223);
501     }
502     {
503         auto rnd = Random(1337);
504         Gen!dchar gen;
505         assertEqual(cast(int)gen.gen(rnd), 3223);
506     }
507 }
508 
509 private template AggregateTuple(T...) {
510     import unit_threaded.randomized.random: ParameterToGen;
511     import std.meta: staticMap;
512     alias AggregateTuple = staticMap!(ParameterToGen, T);
513 }
514 
515 struct Gen(T) if(isAggregateType!T) {
516 
517     import std.traits: Fields;
518     AggregateTuple!(Fields!T) generators;
519 
520     alias Value = T;
521 
522     T gen(ref Random rnd) @safe {
523         static if(is(T == class))
524             auto ret = new T;
525         else
526             T ret;
527 
528         foreach(i, ref g; generators) {
529             ret.tupleof[i] = g.gen(rnd);
530         }
531 
532         return ret;
533     }
534 }
535 
536 @("struct")
537 @safe unittest {
538     import unit_threaded.asserts: assertEqual;
539 
540     struct Foo {
541         int i;
542         string s;
543     }
544 
545     auto rnd = Random(1337);
546     Gen!Foo gen;
547     assertEqual(gen.gen(rnd), Foo(0, ""));
548     assertEqual(gen.gen(rnd), Foo(1, "a"));
549     assertEqual(gen.gen(rnd), Foo(int.min, "é"));
550 }
551 
552 @("class")
553 @safe unittest {
554     import unit_threaded.asserts: assertEqual;
555 
556     static class Foo {
557         this() {}
558         this(int i, string s) { this.i = i; this.s = s; }
559         override string toString() @safe const pure nothrow {
560             import std.conv;
561             return text(`Foo(`, i, `, "`, s, `")`);
562         }
563         override bool opEquals(Object _rhs) @safe const pure nothrow {
564             auto rhs = cast(Foo)_rhs;
565             return i == rhs.i && s == rhs.s;
566         }
567         int i;
568         string s;
569     }
570 
571     auto rnd = Random(1337);
572     Gen!Foo gen;
573     assertEqual(gen.gen(rnd), new Foo(0, ""));
574     assertEqual(gen.gen(rnd), new Foo(1, "a"));
575     assertEqual(gen.gen(rnd), new Foo(int.min, "é"));
576 }