1 /** 2 * Module defines routines for generating testing sets. 3 * 4 * To define Arbitrary template for particular type $(B T) (or types) you should follow 5 * compile-time interface: 6 * <ul> 7 * <li>Define $(B generate) function that takes nothing and returns range of $(B T). 8 * This function is used to generate random sets of testing data. Size of required 9 * sample isn't passed thus use lazy ranges to generate possible infinite set of 10 * data.</li> 11 * <li>Define $(B shrink) function that takes value of $(B T) and returns range of 12 * truncated variations. This function is used to reduce failing case data to 13 * minimum possible set. You can return empty array if you like to get large bunch 14 * of random data.</li> 15 * <li>Define $(B specialCases) function that takes nothing and returns range of $(B T). 16 * This function is used to test some special values for particular type like NaN or 17 * null pointer. You can return empty array if no testing on special cases is required.</li> 18 * </ul> 19 * 20 * Usually useful practice is put static assert with $(B CheckArbitrary) template into your implementation 21 * to actually get confidence that you've done it properly. 22 * 23 * Example: 24 * --------- 25 * template Arbitrary(T) 26 * if(isIntegral!T) 27 * { 28 * static assert(CheckArbitrary!T); 29 * 30 * auto generate() 31 * { 32 * return (() => Maybe!T(uniform!"[]"(T.min, T.max))).generator; 33 * } 34 * 35 * auto shrink(T val) 36 * { 37 * class Shrinker 38 * { 39 * T saved; 40 * 41 * this(T firstVal) 42 * { 43 * saved = firstVal; 44 * } 45 * 46 * Maybe!T shrink() 47 * { 48 * if(saved == 0) return Maybe!T.nothing; 49 * 50 * if(saved > 0) saved--; 51 * if(saved < 0) saved++; 52 * 53 * return Maybe!T(saved); 54 * } 55 * } 56 * 57 * return (&(new Shrinker(val)).shrink).generator; 58 * } 59 * 60 * T[] specialCases() 61 * { 62 * return [T.min, 0, T.max]; 63 * } 64 * } 65 * --------- 66 * 67 * Copyright: © 2014 Anton Gushcha 68 * License: Subject to the terms of the MIT license, as written in the included LICENSE file. 69 * Authors: NCrashed <ncrashed@gmail.com> 70 */ 71 module dcheck.arbitrary; 72 73 import std.traits; 74 import std.range; 75 import std.random; 76 import std.conv; 77 import std.math; 78 import dcheck.maybe; 79 import dcheck.generator; 80 81 /// Minimum size of generated arrays (and strings) 82 enum ArrayGenSizeMin = 1; 83 /// Maximum size of generated arrays (and strings) 84 enum ArrayGenSizeMax = 32; 85 86 /** 87 * Checks if $(B T) has Arbitrary template with 88 * $(B generate), $(B shrink) and $(B specialCases) functions. 89 */ 90 template HasArbitrary(T) 91 { 92 template HasGenerate() 93 { 94 static if(__traits(compiles, Arbitrary!T.generate)) 95 { 96 alias ParameterTypeTuple!(Arbitrary!T.generate) Params; 97 alias ReturnType!(Arbitrary!T.generate) RetType; 98 99 enum HasGenerate = 100 isInputRange!RetType && is(ElementType!RetType == T) 101 && Params.length == 0; 102 } else 103 { 104 enum HasGenerate = false; 105 } 106 } 107 108 template HasShrink() 109 { 110 static if(__traits(compiles, Arbitrary!T.shrink )) 111 { 112 alias ParameterTypeTuple!(Arbitrary!T.shrink) Params; 113 alias ReturnType!(Arbitrary!T.shrink) RetType; 114 115 enum HasShrink = 116 isInputRange!RetType 117 && (is(ElementType!RetType == T) || isSomeChar!T && isSomeChar!(ElementType!RetType)) 118 && Params.length == 1 119 && is(Params[0] == T); 120 } else 121 { 122 enum HasShrink = false; 123 } 124 } 125 126 template HasSpecialCases() 127 { 128 static if(__traits(compiles, Arbitrary!T.specialCases)) 129 { 130 alias ParameterTypeTuple!(Arbitrary!T.specialCases) Params; 131 alias ReturnType!(Arbitrary!T.specialCases) RetType; 132 133 enum HasSpecialCases = 134 isInputRange!RetType 135 && (is(ElementType!RetType == T) || isSomeChar!T && isSomeChar!(ElementType!RetType)) 136 && Params.length == 0; 137 } else 138 { 139 enum HasSpecialCases = false; 140 } 141 } 142 143 template isFullDefined() 144 { 145 enum isFullDefined = 146 __traits(compiles, Arbitrary!T) && 147 HasGenerate!() && 148 HasShrink!() && 149 HasSpecialCases!(); 150 } 151 } 152 153 /** 154 * You can use this to define default shrink implementation. 155 */ 156 mixin template DefaultShrink(T) 157 { 158 import std.range; 159 auto shrink(T val) 160 { 161 return takeNone!(T[]); 162 } 163 } 164 165 /** 166 * You can use this to define default special cases implementation. 167 */ 168 mixin template DefaultSpecialCases(T) 169 { 170 import std.range; 171 auto specialCases() 172 { 173 return takeNone!(T[]); 174 } 175 } 176 177 /** 178 * You can use this to define only generate function. 179 */ 180 mixin template DefaultShrinkAndSpecialCases(T) 181 { 182 import std.range; 183 184 auto shrink(T val) 185 { 186 return takeNone!(T[]); 187 } 188 189 auto specialCases() 190 { 191 return takeNone!(T[]); 192 } 193 } 194 195 /** 196 * Check the $(B T) type has properly defined $(B Arbitrary) template. Prints useful user-friendly messages 197 * at compile time. 198 * 199 * Good practice is put the template in static assert while defining own instances of $(B Arbitrary) to get 200 * confidence about instance correctness. 201 */ 202 template CheckArbitrary(T) 203 { 204 // dirty hack to force at least one instance of the template 205 alias t = Arbitrary!T; 206 static assert(__traits(compiles, Arbitrary!T), "Type "~T.stringof~" doesn't have Arbitrary template!"); 207 static assert(HasArbitrary!T.HasGenerate!(), "Type "~T.stringof~" doesn't have generate function in Arbitrary template!"); 208 static assert(HasArbitrary!T.HasShrink!(), "Type "~T.stringof~" doesn't have shrink function in Arbitrary template!"); 209 static assert(HasArbitrary!T.HasSpecialCases!(), "Type "~T.stringof~" doesn't have specialCases function in Arbitrary template!"); 210 enum CheckArbitrary = HasArbitrary!T.isFullDefined!(); 211 } 212 213 /** 214 * Arbitrary for ubyte, byte, ushort, short, uing, int, ulong, long. 215 */ 216 template Arbitrary(T) 217 if(isIntegral!T) 218 { 219 static assert(CheckArbitrary!T); 220 221 auto generate() 222 { 223 return (() => Maybe!T(uniform!"[]"(T.min, T.max))).generator; 224 } 225 226 auto shrink(T val) 227 { 228 class Shrinker 229 { 230 T saved; 231 232 this(T firstVal) 233 { 234 saved = firstVal; 235 } 236 237 Maybe!T shrink() 238 { 239 if(saved == 0) return Maybe!T.nothing; 240 241 saved = saved/2; 242 return Maybe!T(saved); 243 } 244 } 245 246 return (&(new Shrinker(val)).shrink).generator; 247 } 248 249 T[] specialCases() 250 { 251 return [T.min, 0, T.max]; 252 } 253 } 254 unittest 255 { 256 Arbitrary!ubyte.generate; 257 Arbitrary!ubyte.specialCases; 258 assert(Arbitrary!ubyte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 259 260 Arbitrary!byte.generate; 261 Arbitrary!byte.specialCases; 262 assert(Arbitrary!byte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 263 assert(Arbitrary!byte.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0])); 264 265 Arbitrary!ushort.generate; 266 Arbitrary!ushort.specialCases; 267 assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 268 269 Arbitrary!short.generate; 270 Arbitrary!short.specialCases; 271 assert(Arbitrary!short.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 272 assert(Arbitrary!short.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0])); 273 274 Arbitrary!uint.generate; 275 Arbitrary!uint.specialCases; 276 assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 277 278 Arbitrary!int.generate; 279 Arbitrary!int.specialCases; 280 assert(Arbitrary!int.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 281 assert(Arbitrary!int.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0])); 282 283 Arbitrary!ulong.generate; 284 Arbitrary!ulong.specialCases; 285 assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 286 287 Arbitrary!long.generate; 288 Arbitrary!long.specialCases; 289 assert(Arbitrary!long.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0])); 290 assert(Arbitrary!long.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0])); 291 } 292 293 /** 294 * Arbitrary for float, double 295 */ 296 template Arbitrary(T) 297 if(isFloatingPoint!T) 298 { 299 static assert(CheckArbitrary!T); 300 301 auto generate() 302 { 303 return (() => Maybe!T(uniform!"[]"(-T.max, T.max))).generator; 304 } 305 306 auto shrink(T val) 307 { 308 class Shrinker 309 { 310 T saved; 311 312 this(T firstVal) 313 { 314 saved = firstVal; 315 } 316 317 Maybe!T shrink() 318 { 319 if(approxEqual(saved, 0)) return Maybe!T.nothing; 320 321 saved = saved/2; 322 return Maybe!T(saved); 323 } 324 } 325 326 return (&(new Shrinker(val)).shrink).generator; 327 } 328 329 T[] specialCases() 330 { 331 return [-T.max, 0, T.max, T.nan, T.infinity, T.epsilon, T.min_normal]; 332 } 333 } 334 unittest 335 { 336 import std.math; 337 import std.algorithm; 338 339 Arbitrary!float.generate; 340 Arbitrary!float.specialCases; 341 assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(10.0).take(3).zip([5.0, 2.5, 1.25]))); 342 assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25]))); 343 344 Arbitrary!double.generate; 345 Arbitrary!double.specialCases; 346 assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(10.0).take(3).zip([5.0, 2.5, 1.25]))); 347 assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25]))); 348 } 349 350 /** 351 * Arbitrary for bool 352 */ 353 template Arbitrary(T) 354 if(is(T == bool)) 355 { 356 static assert(CheckArbitrary!T); 357 358 auto generate() 359 { 360 return [true, false]; 361 } 362 363 mixin DefaultShrinkAndSpecialCases!T; 364 } 365 unittest 366 { 367 assert(Arbitrary!bool.generate.equal([true, false])); 368 assert(Arbitrary!bool.shrink(true).empty); 369 assert(Arbitrary!bool.shrink(false).empty); 370 } 371 372 /** 373 * Arbitrary template for char, dchar, wchar 374 */ 375 template Arbitrary(T) 376 if(isSomeChar!T) 377 { 378 static assert(CheckArbitrary!T); 379 380 auto generate() 381 { 382 enum alphabet = "abcdeABCDE12345áàäéèëÁÀÄÉÈËЯВНЛАڴٸڱ☭ତ⇕"; 383 return (() => Maybe!T(cast(T)alphabet[uniform(0, alphabet.length)])).generator; 384 } 385 386 mixin DefaultShrinkAndSpecialCases!T; 387 } 388 unittest 389 { 390 Arbitrary!char.generate.take(100); 391 assert(Arbitrary!char.shrink('a').empty); 392 assert(Arbitrary!char.specialCases().empty); 393 394 Arbitrary!wchar.generate.take(100); 395 assert(Arbitrary!wchar.shrink('a').empty); 396 assert(Arbitrary!wchar.specialCases().empty); 397 398 Arbitrary!dchar.generate.take(100); 399 assert(Arbitrary!dchar.shrink('a').empty); 400 assert(Arbitrary!dchar.specialCases().empty); 401 } 402 403 /** 404 * Arbitrary template for strings 405 */ 406 template Arbitrary(T) 407 if(isSomeString!T) 408 { 409 static assert(CheckArbitrary!T); 410 411 auto generate() 412 { 413 return (() => Maybe!T(Arbitrary!(Unqual!(ElementType!T)).generate 414 .take(uniform!"[]"(ArrayGenSizeMin, ArrayGenSizeMax)).array.idup.to!T)).generator; 415 } 416 417 auto shrink(T val) 418 { 419 class Shrinker 420 { 421 T saved; 422 423 this(T startVal) 424 { 425 saved = startVal; 426 } 427 428 auto shrink() 429 { 430 if(saved.length > 0) 431 { 432 saved = saved[1..$]; 433 return Maybe!T(saved); 434 } else return Maybe!T.nothing; 435 } 436 } 437 return (&(new Shrinker(val)).shrink).generator; 438 } 439 440 T[] specialCases() 441 { 442 return [cast(T)""]; 443 } 444 } 445 unittest 446 { 447 Arbitrary!string.generate; 448 assert(Arbitrary!string.shrink("a").take(2).equal([""])); 449 assert(Arbitrary!string.shrink("abc").take(3).equal(["bc", "c", ""])); 450 assert(Arbitrary!string.specialCases().equal([""])); 451 452 Arbitrary!wstring.generate; 453 assert(Arbitrary!wstring.shrink("a"w).take(2).equal([""w])); 454 assert(Arbitrary!wstring.shrink("abc"w).take(3).equal(["bc"w, "c"w, ""w])); 455 assert(Arbitrary!wstring.specialCases().equal([""w])); 456 457 Arbitrary!dstring.generate; 458 assert(Arbitrary!dstring.shrink("a"d).take(2).equal([""d])); 459 assert(Arbitrary!dstring.shrink("abc"d).take(3).equal(["bc"d, "c"d, ""d])); 460 assert(Arbitrary!dstring.specialCases().equal([""d])); 461 } 462 463 /** 464 * Arbitrary template for arrays 465 */ 466 template Arbitrary(T) 467 if(isArray!T && HasArbitrary!(ElementType!T).HasGenerate!() && !isSomeString!T) 468 { 469 static assert(CheckArbitrary!T); 470 471 auto generate() 472 { 473 return (() => Maybe!T( Arbitrary!(ElementType!T).generate.take(uniform!"[]"(ArrayGenSizeMin,ArrayGenSizeMax)).array )).generator; 474 } 475 476 auto shrink(T val) 477 { 478 class Shrinker 479 { 480 T saved; 481 482 this(T startVal) 483 { 484 saved = startVal; 485 } 486 487 auto shrink() 488 { 489 if(saved.length > 0) 490 { 491 saved = saved[1..$]; 492 return Maybe!T(saved); 493 } else return Maybe!T.nothing; 494 } 495 } 496 return (&(new Shrinker(val)).shrink).generator; 497 } 498 499 auto specialCases() 500 { 501 return [cast(T)[]]; 502 } 503 } 504 unittest 505 { 506 Arbitrary!(uint[]).generate; 507 assert(Arbitrary!(uint[]).shrink([1u, 2u, 3u]).take(3).equal([[2u, 3u], [3u], []])); 508 assert(Arbitrary!(uint[]).specialCases().equal([cast(uint[])[]])); 509 }