1 | /* @flow weak */
|
2 | /**
|
3 | # JSVerify
|
4 |
|
5 | <img src="https://raw.githubusercontent.com/jsverify/jsverify/master/jsverify-300.png" align="right" height="100" />
|
6 |
|
7 | > Property-based checking. Like QuickCheck.
|
8 |
|
9 | [![Build Status](https://secure.travis-ci.org/jsverify/jsverify.svg?branch=master)](http://travis-ci.org/jsverify/jsverify)
|
10 | [![NPM version](https://badge.fury.io/js/jsverify.svg)](http://badge.fury.io/js/jsverify)
|
11 | [![Dependency Status](https://david-dm.org/jsverify/jsverify.svg)](https://david-dm.org/jsverify/jsverify)
|
12 | [![devDependency Status](https://david-dm.org/jsverify/jsverify/dev-status.svg)](https://david-dm.org/jsverify/jsverify#info=devDependencies)
|
13 | [![Code Climate](https://img.shields.io/codeclimate/github/jsverify/jsverify.svg)](https://codeclimate.com/github/jsverify/jsverify)
|
14 |
|
15 | ## Getting Started
|
16 |
|
17 | Install the module with: `npm install jsverify`
|
18 |
|
19 | ## Synopsis
|
20 |
|
21 | ```js
|
22 | var jsc = require("jsverify");
|
23 |
|
24 | // forall (f : bool -> bool) (b : bool), f (f (f b)) = f(b).
|
25 | var boolFnAppliedThrice =
|
26 | jsc.forall("bool -> bool", "bool", function (f, b) {
|
27 | return f(f(f(b))) === f(b);
|
28 | });
|
29 |
|
30 | jsc.assert(boolFnAppliedThrice);
|
31 | // OK, passed 100 tests
|
32 | ```
|
33 | */
|
34 | ;
|
35 |
|
36 | /**
|
37 | ## Documentation
|
38 |
|
39 | ### Usage with [mocha](http://mochajs.org/)
|
40 |
|
41 | Using jsverify with mocha is easy, just define the properties and use `jsverify.assert`.
|
42 |
|
43 | Starting from version 0.4.3 you can write your specs without any boilerplate:
|
44 |
|
45 | ```js
|
46 | describe("sort", function () {
|
47 | jsc.property("idempotent", "array nat", function (arr) {
|
48 | return _.isEqual(sort(sort(arr)), sort(arr));
|
49 | });
|
50 | });
|
51 | ```
|
52 |
|
53 | Starting from version 0.8.0 you can write the specs in TypeScript. There are
|
54 | typings provided. The drawback is that you cannot use type DSL:
|
55 |
|
56 | ```typescript
|
57 | describe("basic jsverify usage", () => {
|
58 | jsc.property("(b && b) === b", jsc.bool, b => (b && b) === b);
|
59 |
|
60 | jsc.property("boolean fn thrice", jsc.fn(jsc.bool), jsc.bool, (f, b) =>
|
61 | f(f(f(b))) === f(b)
|
62 | );
|
63 | });
|
64 | ```
|
65 |
|
66 | You can also provide `--jsverifyRngState state` command line argument, to run tests with particular random generator state.
|
67 |
|
68 | ```
|
69 | $ mocha examples/nat.js
|
70 |
|
71 | 1) natural numbers are less than 90:
|
72 | Error: Failed after 49 tests and 1 shrinks. rngState: 074e9b5f037a8c21d6; Counterexample: 90;
|
73 |
|
74 | $ mocha examples/nat.js --grep 'are less than' --jsverifyRngState 074e9b5f037a8c21d6
|
75 |
|
76 | 1) natural numbers are less than 90:
|
77 | Error: Failed after 1 tests and 1 shrinks. rngState: 074e9b5f037a8c21d6; Counterexample: 90;
|
78 | ```
|
79 |
|
80 | Erroneous case is found with first try.
|
81 |
|
82 | ### Usage with [jasmine](https://jasmine.github.io/)
|
83 |
|
84 | Check [jasmineHelpers.js](helpers/jasmineHelpers.js) and [jasmineHelpers2.js](helpers/jasmineHelpers2.js) for jasmine 1.3 and 2.0 respectively.
|
85 |
|
86 | ## API Reference
|
87 |
|
88 | > _Testing shows the presence, not the absence of bugs._
|
89 | >
|
90 | > Edsger W. Dijkstra
|
91 |
|
92 | To show that propositions hold, we need to construct proofs.
|
93 | There are two extremes: proof by example (unit tests) and formal (machine-checked) proof.
|
94 | Property-based testing is somewhere in between.
|
95 | We formulate propositions, invariants or other properties we believe to hold, but
|
96 | only test it to hold for numerous (randomly generated) values.
|
97 |
|
98 | Types and function signatures are written in [Coq](http://coq.inria.fr/)/[Haskell](http://www.haskell.org/haskellwiki/Haskell)-influenced style:
|
99 | C# -style `List<T> filter(List<T> v, Func<T, bool> predicate)` is represented by
|
100 | `filter(v: array T, predicate: T -> bool): array T` in our style.
|
101 |
|
102 | Methods and objects live in `jsc` object, e.g. `shrink.bless` method is used by
|
103 | ```js
|
104 | var jsc = require("jsverify");
|
105 | var foo = jsc.shrink.bless(...);
|
106 | ```
|
107 |
|
108 | Methods starting with `.dot` are prototype methods:
|
109 | ```js
|
110 | var arb = jsc.nat;
|
111 | var arb2 = jsc.nat.smap(f, g);
|
112 | ```
|
113 |
|
114 | `jsverify` can operate with both synchronous and asynchronous-promise properties.
|
115 | Generally every property can be wrapped inside [functor](http://learnyouahaskell.com/functors-applicative-functors-and-monoids),
|
116 | for now in either identity or promise functor, for synchronous and promise properties respectively.
|
117 | */
|
118 |
|
119 | var assert = require("assert");
|
120 | var lazyseq = require("lazy-seq");
|
121 |
|
122 | var api = require("./api.js");
|
123 | var either = require("./either.js");
|
124 | var environment = require("./environment.js");
|
125 | var FMap = require("./finitemap.js");
|
126 | var fn = require("./fn.js");
|
127 | var functor = require("./functor.js");
|
128 | var random = require("./random.js");
|
129 | var show = require("./show.js");
|
130 | var shrink = require("./shrink.js");
|
131 | var suchthat = require("./suchthat.js");
|
132 | var sum = require("./sum.js");
|
133 | var typify = require("./typify.js");
|
134 | var utils = require("./utils.js");
|
135 |
|
136 | /**
|
137 | ### Properties
|
138 | */
|
139 |
|
140 | function shrinkResult(arbs, x, test, size, shrinksN, exc, transform) {
|
141 | assert(arbs.length === x.length, "shrinkResult: arbs and x has to be of same size");
|
142 | assert(typeof size === "number", "shrinkResult: size should be number");
|
143 | assert(typeof shrinksN === "number", "shrinkResult: shrinkN should be number");
|
144 |
|
145 | var shrinks = utils.pluck(arbs, "shrink");
|
146 | var shows = utils.pluck(arbs, "show");
|
147 |
|
148 | var shrinked = shrink.tuple(shrinks, x);
|
149 |
|
150 | var shrinkP = lazyseq.fold(shrinked, true, function (y, rest) {
|
151 | var t = test(size, y, shrinksN + 1);
|
152 | return functor.map(t, function (tprime) {
|
153 | return tprime !== true ? tprime : rest();
|
154 | });
|
155 | });
|
156 |
|
157 | return functor.map(shrinkP, function (shrinkPPrime) {
|
158 | if (shrinkPPrime === true) {
|
159 | var res = {
|
160 | counterexample: x,
|
161 | counterexamplestr: show.tuple(shows, x),
|
162 | shrinks: shrinksN,
|
163 | exc: exc,
|
164 | };
|
165 | return transform(res);
|
166 | } else {
|
167 | return shrinkPPrime;
|
168 | }
|
169 | });
|
170 | }
|
171 |
|
172 | function isArbitrary(arb) {
|
173 | return (typeof arb === "object" || typeof arb === "function") &&
|
174 | typeof arb.generator === "function" &&
|
175 | typeof arb.shrink === "function" &&
|
176 | typeof arb.show === "function";
|
177 | }
|
178 |
|
179 | /**
|
180 | - `forall(arbs: arbitrary a ..., userenv: (map arbitrary)?, prop : a -> property): property`
|
181 |
|
182 | Property constructor
|
183 | */
|
184 | function forall() {
|
185 | var args = Array.prototype.slice.call(arguments);
|
186 | var gens = args.slice(0, -1);
|
187 | var property = args[args.length - 1];
|
188 | var env;
|
189 |
|
190 | var lastgen = gens[gens.length - 1];
|
191 |
|
192 | if (!isArbitrary(lastgen) && typeof lastgen !== "string") {
|
193 | env = utils.merge(environment, lastgen);
|
194 | gens = gens.slice(0, -1);
|
195 | } else {
|
196 | env = environment;
|
197 | }
|
198 |
|
199 | assert(gens.length > 0, "forall requires at least single generator");
|
200 |
|
201 | // Map typify-dsl to hard generators
|
202 | gens = gens.map(function (g) {
|
203 | g = typeof g === "string" ? typify.parseTypify(env, g) : g;
|
204 | return utils.force(g);
|
205 | });
|
206 |
|
207 | assert(typeof property === "function", "property should be a function");
|
208 |
|
209 | function test(size, x, shrinks) {
|
210 | assert(Array.isArray(x), "generators results should be always tuple");
|
211 |
|
212 | return functor.bind(property, x, function (r, exc) {
|
213 | if (r === true) {
|
214 | return true;
|
215 | } else if (typeof r === "function") {
|
216 | var rRec = r(size);
|
217 |
|
218 | return functor.map(rRec, function (rRecPrime) {
|
219 | if (rRecPrime === true) {
|
220 | return true;
|
221 | } else {
|
222 | return shrinkResult(gens, x, test, size, shrinks, exc, function (rr) {
|
223 | return {
|
224 | counterexample: rr.counterexample.concat(rRecPrime.counterexample),
|
225 | counterexamplestr: rr.counterexamplestr, // + "; " + rRec.counterexamplestr,
|
226 | shrinks: rr.shrinks,
|
227 | exc: rr.exc || rRecPrime.exc,
|
228 | };
|
229 | });
|
230 | }
|
231 | });
|
232 | } else {
|
233 | return shrinkResult(gens, x, test, size, shrinks, exc || r, utils.identity);
|
234 | }
|
235 | });
|
236 | }
|
237 |
|
238 | return function (size) {
|
239 | var x = gens.map(function (arb) { return arb.generator(size); });
|
240 | var r = test(size, x, 0);
|
241 | return r;
|
242 | };
|
243 | }
|
244 |
|
245 | function formatFailedCase(r, state, includeStack) {
|
246 | var msg = "Failed after " + r.tests + " tests and " + r.shrinks + " shrinks. ";
|
247 | msg += "rngState: " + (r.rngState || state) + "; ";
|
248 | msg += "Counterexample: " + r.counterexamplestr + "; ";
|
249 | if (r.exc) {
|
250 | if (r.exc instanceof Error) {
|
251 | msg += "Exception: " + r.exc.message;
|
252 | if (includeStack) {
|
253 | msg += "\nStack trace: " + r.exc.stack;
|
254 | }
|
255 | } else {
|
256 | msg += "Error: " + r.exc;
|
257 | }
|
258 | }
|
259 | return msg;
|
260 | }
|
261 |
|
262 | function findRngState(argv) { // eslint-disable-line consistent-return
|
263 | for (var i = 0; i < argv.length - 1; i++) {
|
264 | if (argv[i] === "--jsverifyRngState") {
|
265 | return argv[i + 1];
|
266 | }
|
267 | }
|
268 | }
|
269 |
|
270 | /**
|
271 | - `check (prop: property, opts: checkoptions?): result`
|
272 |
|
273 | Run random checks for given `prop`. If `prop` is promise based, result is also wrapped in promise.
|
274 |
|
275 | Options:
|
276 | - `opts.tests` - test count to run, default 100
|
277 | - `opts.size` - maximum size of generated values, default 50
|
278 | - `opts.quiet` - do not `console.log`
|
279 | - `opts.rngState` - state string for the rng
|
280 |
|
281 | The `result` is `true` if check succeeds, otherwise it's an object with various fields:
|
282 | - `counterexample` - an input for which property fails.
|
283 | - `tests` - number of tests run before failing case is found
|
284 | - `shrinks` - number of shrinks performed
|
285 | - `exc` - an optional exception thrown by property function
|
286 | - `rngState` - random number generator's state before execution of the property
|
287 | */
|
288 | function check(property, opts) {
|
289 | opts = opts || {};
|
290 | opts.size = opts.size || 50;
|
291 | opts.tests = opts.tests || 100;
|
292 | opts.quiet = opts.quiet || false;
|
293 |
|
294 | assert(typeof property === "function", "property should be a function");
|
295 |
|
296 | var state;
|
297 |
|
298 | if (opts.rngState) {
|
299 | random.setStateString(opts.rngState);
|
300 | } else if (typeof process !== "undefined") {
|
301 | var argvState = findRngState(process.argv);
|
302 | if (argvState) {
|
303 | random.setStateString(argvState);
|
304 | }
|
305 | }
|
306 |
|
307 | function loop(i) {
|
308 | state = random.currentStateString();
|
309 | if (i > opts.tests) {
|
310 | return true;
|
311 | }
|
312 |
|
313 | var size = random(0, opts.size);
|
314 |
|
315 | // wrap non-promises in trampoline
|
316 | var r = functor.pure(property(size));
|
317 |
|
318 | return functor.map(r, function (rPrime) {
|
319 | if (rPrime === true) {
|
320 | return loop(i + 1);
|
321 | } else {
|
322 | rPrime.tests = i;
|
323 | /* global console */
|
324 | if (!opts.quiet) {
|
325 | console.error(formatFailedCase(rPrime, state, true), rPrime.counterexample);
|
326 | }
|
327 | return rPrime;
|
328 | }
|
329 | });
|
330 | }
|
331 |
|
332 | return functor.run(functor.map(loop(1), function (r) {
|
333 | if (r === true) {
|
334 | if (!opts.quiet) { console.info("OK, passed " + opts.tests + " tests"); }
|
335 | } else {
|
336 | r.rngState = state;
|
337 | }
|
338 |
|
339 | return r;
|
340 | }));
|
341 | }
|
342 |
|
343 | /**
|
344 | - `assert(prop: property, opts: checkoptions?) : void`
|
345 |
|
346 | Same as `check`, but throw exception if property doesn't hold.
|
347 | */
|
348 | function checkThrow(property, opts) {
|
349 | opts = opts || {};
|
350 | if (opts.quiet === undefined) {
|
351 | opts.quiet = true;
|
352 | }
|
353 |
|
354 | return functor.run(functor.map(check(property, opts), function (r) {
|
355 | if (r !== true) {
|
356 | if (r.exc instanceof Error) {
|
357 | r.exc.message = formatFailedCase(r);
|
358 | throw r.exc;
|
359 | } else {
|
360 | throw new Error(formatFailedCase(r));
|
361 | }
|
362 | }
|
363 | }));
|
364 | }
|
365 |
|
366 | /**
|
367 | - `property(name: string, ...)`
|
368 |
|
369 | Assuming there is globally defined `it`, the same as:
|
370 |
|
371 | ```js
|
372 | it(name, function () {
|
373 | jsc.assert(jsc.forall(...));
|
374 | }
|
375 | ```
|
376 |
|
377 | You can use `property` to write facts too:
|
378 | ```js
|
379 | jsc.property("+0 === -0", function () {
|
380 | return +0 === -0;
|
381 | });
|
382 | ```
|
383 | */
|
384 | function bddProperty(name) {
|
385 | /* global it: true */
|
386 | var args = Array.prototype.slice.call(arguments, 1);
|
387 | if (args.length === 1) {
|
388 | it(name, function () {
|
389 | return functor.run(functor.map(args[0](), function (result) { // eslint-disable-line consistent-return
|
390 | if (typeof result === "function") {
|
391 | return checkThrow(result);
|
392 | } else if (result !== true) {
|
393 | throw new Error(name + " doesn't hold");
|
394 | }
|
395 | }));
|
396 | });
|
397 | } else {
|
398 | var prop = forall.apply(undefined, args);
|
399 | it(name, function () {
|
400 | return checkThrow(prop);
|
401 | });
|
402 | }
|
403 | /* global it: false */
|
404 | }
|
405 |
|
406 | /**
|
407 | - `compile(desc: string, env: typeEnv?): arbitrary a`
|
408 |
|
409 | Compile the type description in provided type environment, or default one.
|
410 | */
|
411 | function compile(str, env) {
|
412 | env = env ? utils.merge(environment, env) : environment;
|
413 | return typify.parseTypify(env, str);
|
414 | }
|
415 |
|
416 | /**
|
417 | - `sampler(arb: arbitrary a, genSize: nat = 10): (sampleSize: nat?) -> a`
|
418 |
|
419 | Create a sampler for a given arbitrary with an optional size. Handy when used in
|
420 | a REPL:
|
421 | ```
|
422 | > jsc = require('jsverify') // or require('./lib/jsverify') w/in the project
|
423 | ...
|
424 | > jsonSampler = jsc.sampler(jsc.json, 4)
|
425 | [Function]
|
426 | > jsonSampler()
|
427 | 0.08467432763427496
|
428 | > jsonSampler()
|
429 | [ [ [] ] ]
|
430 | > jsonSampler()
|
431 | ''
|
432 | > sampledJson(2)
|
433 | [-0.4199344692751765, false]
|
434 | ```
|
435 | */
|
436 | function sampler(arb, size) {
|
437 | size = typeof size === "number" ? Math.abs(size) : 10;
|
438 | return function (count) {
|
439 | if (typeof count === "number") {
|
440 | var acc = [];
|
441 | count = Math.abs(count);
|
442 | for (var i = 0; i < count; i++) {
|
443 | acc.push(arb.generator(size));
|
444 | }
|
445 | return acc;
|
446 | } else {
|
447 | return arb.generator(size);
|
448 | }
|
449 | };
|
450 | }
|
451 |
|
452 | /**
|
453 | - `throws(block: () -> a, error: class?, message: string?): bool`
|
454 |
|
455 | Executes nullary function `block`. Returns `true` if `block` throws. See [assert.throws](https://nodejs.org/api/assert.html#assert_assert_throws_block_error_message)
|
456 | */
|
457 | function throws(block, error, message) {
|
458 | assert(error === undefined || typeof error === "function", "throws: error parameter must be a constructor");
|
459 | assert(message === undefined || typeof message === "string", "throws: message parameter must be a string");
|
460 |
|
461 | try {
|
462 | block();
|
463 | return false;
|
464 | } catch (e) {
|
465 | if (error !== undefined) {
|
466 | if (e instanceof error) {
|
467 | return message === undefined || e.message === message;
|
468 | } else {
|
469 | return false;
|
470 | }
|
471 | } else {
|
472 | return true;
|
473 | }
|
474 | }
|
475 | }
|
476 |
|
477 | /**
|
478 | - `assertForall(arbs: arbitrary a ..., userenv: (map arbitrary)?, prop : a -> property): void`
|
479 |
|
480 | Combines 'assert' and 'forall'.
|
481 | Constructs a property with forall from arguments, then throws an exception if the property doesn't hold.
|
482 | Options for 'assert' cannot be set here - use assert(forall(...)) if you need that.
|
483 | */
|
484 | function assertForall() {
|
485 | return checkThrow(forall.apply(null, arguments));
|
486 | }
|
487 |
|
488 | /**
|
489 | - `checkForall(arbs: arbitrary a ..., userenv: (map arbitrary)?, prop : a -> property): result`
|
490 |
|
491 | Combines 'check' and 'forall'.
|
492 | Constructs a property with forall from arguments, and returns a value based on if the property holds or not.
|
493 | See 'check' for description of return value.
|
494 |
|
495 | Options for 'check' cannot be set here - use check(forall(...)) if you need that.
|
496 | */
|
497 | function checkForall() {
|
498 | return check(forall.apply(null, arguments));
|
499 | }
|
500 |
|
501 | /**
|
502 | ### Types
|
503 |
|
504 | - `generator a` is a function `(size: nat) -> a`.
|
505 | - `show` is a function `a -> string`.
|
506 | - `shrink` is a function `a -> [a]`, returning *smaller* values.
|
507 | - `arbitrary a` is a triple of generator, shrink and show functions.
|
508 | - `{ generator: nat -> a, shrink : a -> array a, show: a -> string }`
|
509 |
|
510 | ### Blessing
|
511 |
|
512 | We chose to represent generators and shrinks by functions, yet we would
|
513 | like to have additional methods on them. Thus we *bless* objects with
|
514 | additional properties.
|
515 |
|
516 | Usually you don't need to bless anything explicitly, as all combinators
|
517 | return blessed values.
|
518 |
|
519 | See [perldoc for bless](http://perldoc.perl.org/functions/bless.html).
|
520 | */
|
521 |
|
522 | /// include ./typify.js
|
523 | /// include ./arbitraryBless.js
|
524 | /// include ./bless.js
|
525 | /// include ./primitive.js
|
526 | /// include ./arbitrary.js
|
527 | /// include ./recordWithEnv.js
|
528 | /// include ./record.js
|
529 | /// include ./string.js
|
530 | /// include ./fn.js
|
531 | /// include ./small.js
|
532 | /// include ./generator.js
|
533 | /// include ./shrink.js
|
534 | /// include ./show.js
|
535 | /// include ./random.js
|
536 | /// include ./either.js
|
537 | /// include ./utils.js
|
538 |
|
539 | // Export
|
540 | var jsc = {
|
541 | forall: forall,
|
542 | check: check,
|
543 | assert: checkThrow,
|
544 | assertForall: assertForall,
|
545 | checkForall: checkForall,
|
546 | property: bddProperty,
|
547 | sampler: sampler,
|
548 | throws: throws,
|
549 |
|
550 | // generators
|
551 | fn: fn.fn,
|
552 | fun: fn.fn,
|
553 | suchthat: suchthat.suchthat,
|
554 |
|
555 | // either
|
556 | left: either.left,
|
557 | right: either.right,
|
558 |
|
559 | // sum
|
560 | addend: sum.addend,
|
561 |
|
562 | // compile
|
563 | compile: compile,
|
564 |
|
565 | generator: api.generator,
|
566 | shrink: api.shrink,
|
567 |
|
568 | // internal utility lib
|
569 | random: random,
|
570 |
|
571 | show: show,
|
572 | utils: utils,
|
573 | _: {
|
574 | FMap: FMap,
|
575 | },
|
576 | };
|
577 |
|
578 | /* primitives */
|
579 | /* eslint-disable guard-for-in */
|
580 | for (var k in api.arbitrary) {
|
581 | jsc[k] = api.arbitrary[k];
|
582 | }
|
583 | /* eslint-enable guard-for-in */
|
584 |
|
585 | module.exports = jsc;
|
586 |
|
587 | /// plain ../FAQ.md
|
588 | /// plain ../CONTRIBUTING.md
|
589 | /// plain ../CHANGELOG.md
|
590 | /// plain ../related-work.md
|
591 | /// plain ../LICENSE
|