11/* eslint-disable no-prototype-builtins */
22import engine from './methods.js'
33import { parse } from './parser/dsl.js'
4- import { snapshot } from './snapshot.js'
4+ import { serialize , snapshot } from './snapshot.js'
55import { hash } from './hash.js'
6- import { SpecialHoF } from './symbols.js'
6+ import { SpecialHoF , ConstantFunc } from './symbols.js'
77import { failure , parseFailure , success , testRuntimeFailure } from './outputs.js'
8+ import fc from 'fast-check'
9+ import { argumentsToArbitraries } from './utils.js'
10+ import { always } from 'ramda'
811const snap = snapshot ( )
912
1013/**
@@ -16,6 +19,32 @@ export function addMethod (name, fn) {
1619 engine . addMethod ( name , data => fn ( ...[ ] . concat ( data ) ) )
1720}
1821
22+ /**
23+ * Executes the method passed in, and adds the arbitraries to the engine.
24+ * @param {(...args: any[]) => ({ [key: string]: any }) } fn
25+ */
26+ export function addDefinitions ( fn ) {
27+ const definitions = fn ( )
28+ Object . keys ( definitions ) . forEach ( key => {
29+ if ( typeof definitions [ key ] === 'function' ) {
30+ const method = definitions [ key ]
31+ engine . addMethod ( '#' + key , ( data ) => {
32+ if ( data === undefined ) return method ( )
33+ return method ( ...[ ] . concat ( data ) )
34+ } , { sync : true } )
35+ } else {
36+ const arbitrary = definitions [ key ]
37+ if ( arbitrary instanceof fc . Arbitrary ) {
38+ engine . addMethod ( '#' + key , always ( arbitrary ) , { sync : true } )
39+ } else {
40+ const func = always ( fc . constant ( arbitrary ) )
41+ func [ ConstantFunc ] = true
42+ engine . addMethod ( '#' + key , func , { sync : true } )
43+ }
44+ }
45+ } )
46+ }
47+
1948/**
2049 * Just executes the expression, used for "before" / "beforeEach" / "after" / "afterEach".
2150 * @param {string } input
@@ -25,6 +54,16 @@ export async function execute (input) {
2554 await engine . run ( ast )
2655}
2756
57+ class FuzzError extends Error {
58+ constructor ( counterExample , seed , message , shrunk ) {
59+ super ( )
60+ this . counterExample = counterExample
61+ this . seed = seed
62+ this . shrunk = shrunk
63+ this . message = message
64+ }
65+ }
66+
2867/**
2968 * Runs the tests in the Pineapple JSON Logic Engine.
3069 * @param {string } input
@@ -46,16 +85,48 @@ export async function run (input, id, func, file) {
4685 const h = hash ( input )
4786 let result = [ func ]
4887 let lastSpecial = false
88+
4989 for ( const step of script ) {
5090 // Override to break the special "hof" class thing.
5191 if ( lastSpecial ) {
5292 if ( ! Object . values ( step ) [ 0 ] [ 0 ] . special && typeof result [ 0 ] . result === 'function' ) result [ 0 ] = result [ 0 ] . result
5393 }
5494 const [ current ] = result
5595 if ( typeof result [ 0 ] !== 'function' ) return [ result [ 0 ] , false , 'Does not return a function.' ]
56- result = await engine . run ( step , { func : current , id : ( `${ idName } (${ input } ) [${ idHash } ]` ) , snap, hash : h , rule : input , file } )
96+
97+ const key = Object . keys ( step ) [ 0 ]
98+ const [ inputs , expectation ] = step [ key ]
99+ const arbs = await argumentsToArbitraries ( ...inputs )
100+ let failed = null
101+ try {
102+ let count = 0
103+ await fc . assert ( fc . asyncProperty ( ...arbs , async ( ...args ) => {
104+ count ++
105+ const countStr = count > 1 ? `.${ count } ` : ''
106+ result = await engine . run ( {
107+ [ key ] : [ { preserve : args } , expectation ]
108+ } , { func : current , id : ( `${ idName } (${ input } ) [${ idHash } ${ countStr } ]` ) , snap, hash : h , rule : input , file, args, context : current . instance , fuzzed : ! arbs . constant } )
109+ if ( ! result [ 1 ] ) failed = result
110+ return result [ 1 ]
111+ } ) , {
112+ seed : key === 'snapshot' ? parseInt ( h . substring ( 0 , 16 ) , 16 ) : undefined ,
113+ numRuns : arbs . constant ? 1 : key === 'snapshot' ? 10 : undefined ,
114+ reporter ( out ) {
115+ if ( out . failed ) {
116+ throw new FuzzError ( out . counterexample , out . seed , out . error , out . numShrinks )
117+ }
118+ }
119+ } )
120+ } catch ( e ) {
121+ if ( e instanceof FuzzError && ! arbs . constant ) {
122+ failed [ 2 ] = ( failed [ 2 ] || '' ) + `\nFailing Example: ${ serialize ( e . counterExample ) } \nShrunk ${ e . shrunk } times.\nSeed: ${ e . seed } `
123+ }
124+ }
125+
57126 const [ data , success , message ] = result
127+ if ( failed ) return failed
58128 if ( ! success ) return [ data , false , message ]
129+
59130 // Special Override for the Class-Based HoF thing.
60131 if ( current [ SpecialHoF ] ) {
61132 if ( typeof result [ 0 ] !== 'function' ) result [ 0 ] = current
@@ -93,7 +164,7 @@ export async function run (input, id, func, file) {
93164export function hof ( ClassToUse , staticClass = false ) {
94165 return ( ...args ) => {
95166 const instance = staticClass ? ClassToUse : new ClassToUse ( ...args )
96- const f = ( [ method , ...args ] ) => {
167+ const f = ( method , ...args ) => {
97168 if ( ! ( method in instance && ( instance . hasOwnProperty ( method ) || ClassToUse . prototype . hasOwnProperty ( method ) ) ) ) { throw new Error ( `'${ method } ' is not a method of '${ ClassToUse . name } '` ) }
98169 f . result = instance [ method ] ( ...args )
99170 return f
0 commit comments