├── .prettierignore ├── .gitignore ├── .npmignore ├── .prettierrc ├── examples ├── square.ts ├── array.ts ├── index.ts ├── number.ts └── matrix.ts ├── tsconfig.json ├── src ├── internal │ ├── bi-multi-map.ts │ ├── combination-checker.ts │ ├── bi-multi-map.spec.ts │ ├── combination-checker.spec.ts │ ├── union-graph.ts │ └── union-graph.spec.ts └── fast-spec.ts ├── LICENSE ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | src/ 3 | lib/tsconfig.tsbuildinfo 4 | lib/**/*.js.map 5 | lib/**/*.spec.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | 4 | "printWidth": 120, 5 | "tabWidth": 2, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /examples/square.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { funcDef, varDef, findSpecs, FindSpecSettings } from '../src/fast-spec'; 3 | // replace by: from 'fast-spec' 4 | 5 | export function squareSpecs(settings: FindSpecSettings) { 6 | return findSpecs( 7 | [ 8 | funcDef('mul', 2, (a: number, b: number) => a * b), 9 | funcDef('square', 1, (a: number) => a * a), 10 | varDef('x', fc.integer()) 11 | ], 12 | settings 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/array.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { funcDef, instDef, varDef, findSpecs, FindSpecSettings } from '../src/fast-spec'; 3 | // replace by: from 'fast-spec' 4 | 5 | export function arraySpecs(settings: FindSpecSettings) { 6 | return findSpecs( 7 | [ 8 | funcDef('concat', 2, (a: any[], b: any[]) => [...a, ...b]), 9 | funcDef('reverse', 1, (a: any[]) => [...a].reverse()), 10 | instDef('[]', []), 11 | varDef('x', fc.array(fc.char())) 12 | ], 13 | settings 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import { FindSpecSettings } from '../src/fast-spec'; 2 | import { matrixSpecs } from './matrix'; 3 | import { numberSpecs } from './number'; 4 | import { arraySpecs } from './array'; 5 | import { squareSpecs } from './square'; 6 | 7 | const settings: FindSpecSettings = { numSamples: 10000 }; 8 | const exec = (label: string, extractSpecs: (settings: FindSpecSettings) => string[]) => { 9 | console.log(`>>> ${label} <<<`); 10 | console.log(extractSpecs(settings)); 11 | }; 12 | 13 | exec('array', arraySpecs); 14 | exec('matrix', matrixSpecs); 15 | exec('number', numberSpecs); 16 | exec('square', squareSpecs); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "alwaysStrict": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "noUnusedLocals": true, 10 | "removeComments": false, 11 | "preserveConstEnums": true, 12 | "strictNullChecks": true, 13 | "downlevelIteration": true, 14 | "importHelpers": true, 15 | "esModuleInterop": true, 16 | "module": "commonjs", 17 | "target": "es6", 18 | "lib": ["es6"], 19 | "outDir": "lib/" 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["src/**/*.spec.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/number.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { funcDef, instDef, varDef, findSpecs, FindSpecSettings } from '../src/fast-spec'; 3 | // replace by: from 'fast-spec' 4 | 5 | export function numberSpecs(settings: FindSpecSettings) { 6 | return findSpecs( 7 | [ 8 | funcDef('mul', 2, (a: number, b: number) => a * b), 9 | funcDef('div', 2, (a: number, b: number) => a / b), 10 | funcDef('plus', 2, (a: number, b: number) => a + b), 11 | funcDef('minus', 2, (a: number, b: number) => a - b), 12 | instDef('1', 1), 13 | instDef('0', 0), 14 | varDef('x', fc.integer()) 15 | ], 16 | settings 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/internal/bi-multi-map.ts: -------------------------------------------------------------------------------- 1 | export class BiMultiMap { 2 | /** All kwown links */ 3 | private readonly links = new Map>(); 4 | 5 | /** 6 | * Check if there is a direct link between nodeA and nodeB 7 | */ 8 | has(nodeA: string, nodeB: string): boolean { 9 | return ( 10 | (this.links.has(nodeA) && this.links.get(nodeA)!.has(nodeB)) || 11 | (this.links.has(nodeB) && this.links.get(nodeB)!.has(nodeA)) 12 | ); 13 | } 14 | 15 | /** 16 | * Add a direct link between nodeA and nodeB 17 | */ 18 | add(nodeA: string, nodeB: string): void { 19 | if (!this.links.has(nodeA)) { 20 | this.links.set(nodeA, new Set()); 21 | } 22 | this.links.get(nodeA)!.add(nodeB); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/internal/combination-checker.ts: -------------------------------------------------------------------------------- 1 | import { Arbitrary, check, property, genericTuple } from 'fast-check'; 2 | import isEqual = require('lodash.isequal'); 3 | 4 | export function combinationCheck( 5 | arbs: Arbitrary[], 6 | builder1: (d: any[]) => any, 7 | builder2: (d: any[]) => any, 8 | numFuzz?: number 9 | ): boolean { 10 | if (arbs.length === 0) { 11 | // Combination only rely on constants 12 | try { 13 | return isEqual(builder1([]), builder2([])); 14 | } catch (_err) { 15 | return false; 16 | } 17 | } else { 18 | // Combination rely on non-constant values 19 | const out = check( 20 | property(genericTuple(arbs), t => { 21 | return isEqual(builder1(t), builder2(t)); 22 | }), 23 | { numRuns: numFuzz, endOnFailure: true } 24 | ); 25 | return !out.failed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nicolas DUBIEN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/internal/bi-multi-map.spec.ts: -------------------------------------------------------------------------------- 1 | import { BiMultiMap } from './bi-multi-map'; 2 | 3 | test('should not detect links between non existing nodes', () => { 4 | // Arrange 5 | const biMulti = new BiMultiMap(); 6 | 7 | // Act 8 | biMulti.add('a', 'b'); 9 | 10 | // Assert 11 | expect(biMulti.has('c', 'd')).toBe(false); 12 | }); 13 | 14 | test('should not detect links between unrelated nodes', () => { 15 | // Arrange 16 | const biMulti = new BiMultiMap(); 17 | 18 | // Act 19 | biMulti.add('a', 'b'); 20 | biMulti.add('c', 'd'); 21 | 22 | // Assert 23 | expect(biMulti.has('a', 'd')).toBe(false); 24 | }); 25 | 26 | test('should detect links between related nodes', () => { 27 | // Arrange 28 | const biMulti = new BiMultiMap(); 29 | 30 | // Act 31 | biMulti.add('a', 'b'); 32 | 33 | // Assert 34 | expect(biMulti.has('a', 'b')).toBe(true); 35 | }); 36 | 37 | test('should detect links between related nodes declared in the opposite order', () => { 38 | // Arrange 39 | const biMulti = new BiMultiMap(); 40 | 41 | // Act 42 | biMulti.add('a', 'b'); 43 | 44 | // Assert 45 | expect(biMulti.has('b', 'a')).toBe(true); 46 | }); 47 | -------------------------------------------------------------------------------- /src/internal/combination-checker.spec.ts: -------------------------------------------------------------------------------- 1 | import { combinationCheck } from './combination-checker'; 2 | import { nat } from 'fast-check'; 3 | 4 | test('should detect valid static combinations', () => { 5 | // Arrange 6 | const builder1 = () => [1, 2, 3]; 7 | const builder2 = () => [1, 2, 3]; 8 | 9 | // Act 10 | const out = combinationCheck([], builder1, builder2); 11 | 12 | // Act 13 | expect(out).toBe(true); 14 | }); 15 | 16 | test('should detect invalid static combinations', () => { 17 | // Arrange 18 | const builder1 = () => [1, 2, 3]; 19 | const builder2 = () => [1, 2, 4]; 20 | 21 | // Act 22 | const out = combinationCheck([], builder1, builder2); 23 | 24 | // Act 25 | expect(out).toBe(false); 26 | }); 27 | 28 | test('should detect valid static combinations', () => { 29 | // Arrange 30 | const arbs = [nat(), nat()]; 31 | const builder1 = ([a, b]: [number, number]) => a * b; 32 | const builder2 = ([a, b]: [number, number]) => b * a; 33 | 34 | // Act 35 | const out = combinationCheck(arbs, builder1, builder2); 36 | 37 | // Act 38 | expect(out).toBe(true); 39 | }); 40 | 41 | test('should detect invalid static combinations', () => { 42 | // Arrange 43 | const arbs = [nat(), nat()]; 44 | const builder1 = ([a, b]: [number, number]) => a * b; 45 | const builder2 = ([a, b]: [number, number]) => a + b; 46 | 47 | // Act 48 | const out = combinationCheck(arbs, builder1, builder2); 49 | 50 | // Act 51 | expect(out).toBe(false); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-spec", 3 | "version": "0.0.6", 4 | "description": "Discover laws in your code like with QuickSpec", 5 | "main": "lib/fast-spec.js", 6 | "types": "lib/fast-check.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "jest", 11 | "example": "ts-node examples/index.ts", 12 | "format": "prettier --write \"**/*.{js,ts}\"", 13 | "format:check": "prettier --list-different \"**/*.{js,ts}\"" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/dubzzz/fast-spec.git" 18 | }, 19 | "keywords": [ 20 | "quickspec", 21 | "fast-check", 22 | "laws" 23 | ], 24 | "author": "Nicolas DUBIEN ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/dubzzz/fast-spec/issues" 28 | }, 29 | "homepage": "https://github.com/dubzzz/fast-spec#readme", 30 | "dependencies": { 31 | "fast-check": "^1.17.0", 32 | "lodash.isequal": "^4.5.0", 33 | "tslib": "^1.10.0" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^24.0.19", 37 | "@types/lodash.isequal": "^4.5.5", 38 | "@types/node": "^12.11.1", 39 | "jest": "^24.9.0", 40 | "prettier": "^1.18.2", 41 | "ts-jest": "^24.1.0", 42 | "ts-node": "^8.4.1", 43 | "typescript": "^3.6.4" 44 | }, 45 | "jest": { 46 | "globals": { 47 | "ts-jest": { 48 | "tsConfig": "tsconfig.json" 49 | } 50 | }, 51 | "transform": { 52 | "^.+\\.ts$": "ts-jest" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-spec 2 | 3 | Discover laws in your code like with QuickSpec 4 | 5 | ## In a nutshell 6 | 7 | Have you ever wondered what could be the laws that rule your code? The complex relations there might be between two distinct functions? 8 | 9 | `fast-spec` is able to help you discover them. 10 | 11 | ## Example 12 | 13 | Let's find the laws that link `concat`, `reverse` and `[]` together. 14 | 15 | ```js 16 | import * as fc from "fast-check"; 17 | import { funcDef, instDef, varDef, findSpecs } from "fast-spec"; 18 | // const fc = require("fast-check"); 19 | // const { funcDef, instDef, varDef, findSpecs } = require("fast-spec"); 20 | 21 | findSpecs([ 22 | // declare functions to be considered 23 | funcDef("concat", 2, (a, b) => [...a, ...b]), 24 | funcDef("reverse", 1, (a) => [...a].reverse()), 25 | // declare basic root values (of special interest) 26 | instDef("[]", []), 27 | // declare complex values that can be consumed by your functions 28 | varDef("x", fc.array(fc.char())) 29 | ], { // optional settings 30 | // number of combinations to try - default: 100 31 | numSamples: 100, 32 | // complexity of the combinations - default: 2 33 | complexity: 2, 34 | // number of inputs to try to confirm a combination - default: 100 35 | numFuzz: 100 36 | }) 37 | ``` 38 | 39 | `fast-spec` will be able to find relationships like: 40 | - concat([], []) = [] 41 | - concat([], x0) == x0 42 | - concat(x0, []) == x0 43 | - concat(concat(x0, x1), x2) == concat(x0, concat(x1, x2)) 44 | - concat(reverse(x0), reverse(x1)) == reverse(concat(x1, x0)) 45 | - reverse([]) = [] 46 | - reverse(reverse(x0)) == x0 47 | - ... 48 | 49 | *Live example available at https://runkit.com/dubzzz/hello-world-fast-spec-v2* 50 | -------------------------------------------------------------------------------- /examples/matrix.ts: -------------------------------------------------------------------------------- 1 | import * as fc from 'fast-check'; 2 | import { funcDef, instDef, varDef, findSpecs, FindSpecSettings } from '../src/fast-spec'; 3 | // replace by: from 'fast-spec' 4 | 5 | export function matrixSpecs(settings: FindSpecSettings) { 6 | return findSpecs( 7 | [ 8 | funcDef('add', 2, add), 9 | funcDef('mul', 2, mul), 10 | funcDef('transpose', 1, transpose), 11 | funcDef('neg', 1, neg), 12 | funcDef('diag', 1, diag), 13 | funcDef('com', 1, com), 14 | funcDef('inv', 1, inv), 15 | funcDef('det', 1, det), 16 | instDef('Id', [[1, 0], [0, 1]]), 17 | instDef('0', 0), 18 | varDef('mi', invertibleMatrixArb), 19 | varDef('mn', nonInvertibleMatrixArb) 20 | ], 21 | settings 22 | ); 23 | } 24 | 25 | // Matrix internal 26 | 27 | // [ 28 | // [ m[0][0], m[0][1] ], 29 | // [ m[1][0], m[1][1] ], 30 | // ] 31 | type Matrix = [[number, number], [number, number]]; 32 | 33 | const matrixArb = fc.array(fc.integer(-100, 100), 4, 4).map(vs => [[vs[0], vs[1]], [vs[2], vs[3]]]); 34 | const invertibleMatrixArb = matrixArb.filter((m: Matrix) => det(m) !== 0); 35 | const nonInvertibleMatrixArb = fc 36 | .tuple(fc.array(fc.integer(-100, 100), 2, 2), fc.integer(-10, 10)) 37 | .map(([vs, f]) => [[vs[0], vs[1]], [f * vs[0], f * vs[1]]]); 38 | 39 | function add(ma: Matrix, mb: Matrix): Matrix { 40 | // prettier-ignore 41 | return [ 42 | [ ma[0][0] + mb[0][0], ma[0][1] + mb[0][1] ], 43 | [ ma[1][0] + mb[1][0], ma[1][1] + mb[1][1] ] 44 | ]; 45 | } 46 | function mul(ma: Matrix, mb: Matrix): Matrix { 47 | // prettier-ignore 48 | return [ 49 | [ ma[0][0] * mb[0][0] + ma[0][1] * mb[1][0], ma[0][0] * mb[0][1] + ma[0][1] * mb[1][1] ], 50 | [ ma[1][0] * mb[0][0] + ma[1][1] * mb[1][0], ma[1][0] * mb[0][1] + ma[1][1] * mb[1][1] ], 51 | ]; 52 | } 53 | function transpose(m: Matrix): Matrix { 54 | // prettier-ignore 55 | return [ 56 | [ m[0][0], m[1][0] ], 57 | [ m[0][1], m[1][1] ] 58 | ]; 59 | } 60 | function neg(m: Matrix): Matrix { 61 | // prettier-ignore 62 | return [ 63 | [ -m[0][0], -m[0][1] ], 64 | [ -m[1][0], -m[1][1] ] 65 | ]; 66 | } 67 | function diag(m: Matrix): Matrix { 68 | // prettier-ignore 69 | return [ 70 | [ m[0][0], 0 ], 71 | [ 0 , m[1][1] ] 72 | ]; 73 | } 74 | function com(m: Matrix): Matrix { 75 | // prettier-ignore 76 | return [ 77 | [ m[1][1], -m[0][1] ], 78 | [ -m[1][0], m[0][0] ] 79 | ]; 80 | } 81 | function inv(m: Matrix): Matrix { 82 | const d = m[0][0] * m[1][1] - m[0][1] * m[1][0]; 83 | if (d === 0) { 84 | throw new Error(`This matrix is not inversible`); 85 | } 86 | // prettier-ignore 87 | return [ 88 | [ m[1][1] / d, -m[0][1] / d ], 89 | [ -m[1][0] / d, m[0][0] / d ] 90 | ]; 91 | } 92 | function det(m: Matrix): number { 93 | return m[0][0] * m[1][1] - m[0][1] * m[1][0]; 94 | } 95 | -------------------------------------------------------------------------------- /src/internal/union-graph.ts: -------------------------------------------------------------------------------- 1 | export class UnionGraph { 2 | /** 3 | * Store the rootNode corresponding to a given node 4 | * 5 | * `{ [node]: rootNode }` 6 | */ 7 | private readonly parentNode = new Map(); 8 | 9 | /** 10 | * Store all the nodes linked to a given rootNode (including itself) 11 | * 12 | * `{ [ rootNode ]: Set }` 13 | */ 14 | private readonly links = new Map>(); 15 | 16 | /** 17 | * Check a link between two nodes 18 | */ 19 | hasLink(nodeA: string, nodeB: string): boolean { 20 | if (nodeA === nodeB) { 21 | // stop: no need for a link 22 | return true; 23 | } 24 | 25 | const parentNodeA = this.parentNode.get(nodeA); 26 | const parentNodeB = this.parentNode.get(nodeB); 27 | return parentNodeA !== undefined && parentNodeA === parentNodeB; 28 | } 29 | 30 | /** 31 | * Create a link between two nodes (must have different names) 32 | * 33 | * Unknown nodes will be created 34 | */ 35 | addLink(nodeA: string, nodeB: string): void { 36 | if (nodeA === nodeB) { 37 | // stop: no need for a link 38 | return; 39 | } 40 | 41 | const parentNodeA = this.parentNode.get(nodeA); 42 | const parentNodeB = this.parentNode.get(nodeB); 43 | 44 | if (parentNodeA && parentNodeB) { 45 | if (parentNodeA === parentNodeB) { 46 | // stop: there is already a link between nodeA and nodeB 47 | // as they reference the same parent 48 | return; 49 | } 50 | // merge A and B 51 | const linksForParentA = this.links.get(parentNodeA)!; 52 | const linksForParentB = this.links.get(parentNodeB)!; 53 | for (const lnkB of linksForParentB) linksForParentA.add(lnkB); 54 | // update the parent for B and its brothers to be the same as the one for A 55 | for (const lnkB of linksForParentB) this.parentNode.set(lnkB, parentNodeA); 56 | // remove B 57 | this.links.delete(parentNodeB); 58 | } else if (parentNodeA) { 59 | // append nodeB into already known this.links for nodeA 60 | this.links.get(parentNodeA)!.add(nodeB); 61 | // declare the parent for B to be the same as the one for A 62 | this.parentNode.set(nodeB, parentNodeA); 63 | } else if (parentNodeB) { 64 | // append nodeA into already known this.links for nodeB 65 | this.links.get(parentNodeB)!.add(nodeA); 66 | // declare the parent for A to be the same as the one for B 67 | this.parentNode.set(nodeA, parentNodeB); 68 | } else { 69 | // create a new equlity relation 70 | this.links.set(nodeA, new Set([nodeA, nodeB])); 71 | // declare the parent of A and B to be A 72 | this.parentNode.set(nodeA, nodeA); 73 | this.parentNode.set(nodeB, nodeA); 74 | } 75 | } 76 | 77 | /** 78 | * Extract all the relationships declared so far 79 | */ 80 | values(): string[][] { 81 | return Array.from(this.links.values()).map(vs => Array.from(vs)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/internal/union-graph.spec.ts: -------------------------------------------------------------------------------- 1 | import { UnionGraph } from './union-graph'; 2 | 3 | test('should be able to declare an empty graph', () => { 4 | // Arrange / Act 5 | const union = new UnionGraph(); 6 | 7 | // Act 8 | expect(reorder(union.values())).toEqual([]); 9 | }); 10 | 11 | test('should be able to declare a first link', () => { 12 | // Arrange 13 | const union = new UnionGraph(); 14 | 15 | // Act 16 | union.addLink('a', 'b'); 17 | 18 | // Assert 19 | expect(reorder(union.values())).toEqual([['a', 'b']]); 20 | }); 21 | 22 | test('should be able to add nodes without any of them from the tree', () => { 23 | // Arrange 24 | const union = new UnionGraph(); 25 | 26 | // Act 27 | union.addLink('a', 'b'); 28 | union.addLink('c', 'd'); 29 | 30 | // Assert 31 | expect(reorder(union.values())).toEqual([['a', 'b'], ['c', 'd']]); 32 | }); 33 | 34 | test('should be able to add a new node to an existing tree', () => { 35 | // Arrange 36 | const union = new UnionGraph(); 37 | 38 | // Act 39 | union.addLink('a', 'b'); 40 | union.addLink('a', 'c'); 41 | 42 | // Assert 43 | expect(reorder(union.values())).toEqual([['a', 'b', 'c']]); 44 | }); 45 | 46 | test('should be able to add a new node to an existing tree by referencing the other node', () => { 47 | // Arrange 48 | const union = new UnionGraph(); 49 | 50 | // Act 51 | union.addLink('a', 'b'); 52 | union.addLink('b', 'c'); 53 | 54 | // Assert 55 | expect(reorder(union.values())).toEqual([['a', 'b', 'c']]); 56 | }); 57 | 58 | test('should be able to merge two trees together', () => { 59 | // Arrange 60 | const union = new UnionGraph(); 61 | 62 | // Act 63 | union.addLink('a', 'b'); 64 | union.addLink('c', 'd'); 65 | union.addLink('b', 'd'); 66 | 67 | // Assert 68 | expect(reorder(union.values())).toEqual([['a', 'b', 'c', 'd']]); 69 | }); 70 | 71 | test('should be able to add new nodes on merged trees', () => { 72 | // Arrange 73 | const union = new UnionGraph(); 74 | 75 | // Act 76 | union.addLink('a', 'b'); 77 | union.addLink('c', 'd'); 78 | union.addLink('b', 'd'); 79 | union.addLink('b', 'e'); 80 | union.addLink('c', 'f'); 81 | 82 | // Assert 83 | expect(reorder(union.values())).toEqual([['a', 'b', 'c', 'd', 'e', 'f']]); 84 | }); 85 | 86 | test('should not find a link between non existing nodes', () => { 87 | // Arrange 88 | const union = new UnionGraph(); 89 | 90 | // Act 91 | union.addLink('a', 'b'); 92 | 93 | // Assert 94 | expect(union.hasLink('c', 'd')).toBe(false); 95 | }); 96 | 97 | test('should not find a link if one of the node is missing', () => { 98 | // Arrange 99 | const union = new UnionGraph(); 100 | 101 | // Act 102 | union.addLink('a', 'b'); 103 | 104 | // Assert 105 | expect(union.hasLink('b', 'c')).toBe(false); 106 | expect(union.hasLink('c', 'b')).toBe(false); 107 | }); 108 | 109 | test('should not find a link between unrelated nodes', () => { 110 | // Arrange 111 | const union = new UnionGraph(); 112 | 113 | // Act 114 | union.addLink('a', 'b'); 115 | union.addLink('c', 'd'); 116 | 117 | // Assert 118 | expect(union.hasLink('b', 'c')).toBe(false); 119 | }); 120 | 121 | test('should find a link between directly linked nodes', () => { 122 | // Arrange 123 | const union = new UnionGraph(); 124 | 125 | // Act 126 | union.addLink('a', 'b'); 127 | 128 | // Assert 129 | expect(union.hasLink('a', 'b')).toBe(true); 130 | expect(union.hasLink('b', 'a')).toBe(true); 131 | }); 132 | 133 | test('should find a link between indirectly linked nodes', () => { 134 | // Arrange 135 | const union = new UnionGraph(); 136 | 137 | // Act 138 | union.addLink('a', 'b'); 139 | union.addLink('b', 'c'); 140 | 141 | // Assert 142 | expect(union.hasLink('a', 'c')).toBe(true); 143 | expect(union.hasLink('c', 'a')).toBe(true); 144 | }); 145 | 146 | // Helper 147 | 148 | function reorder(values: string[][]): string[][] { 149 | return values.map(vs => vs.sort()).sort((vsA, vsB) => vsA[0].localeCompare(vsB[0])); 150 | } 151 | -------------------------------------------------------------------------------- /src/fast-spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | array, 3 | memo, 4 | constant, 5 | constantFrom, 6 | genericTuple, 7 | shuffledSubarray, 8 | oneof, 9 | tuple, 10 | sample, 11 | Arbitrary 12 | } from 'fast-check'; 13 | import { UnionGraph } from './internal/union-graph'; 14 | import { combinationCheck } from './internal/combination-checker'; 15 | import { BiMultiMap } from './internal/bi-multi-map'; 16 | 17 | export enum SpecDefType { 18 | Function = 'function', 19 | Instance = 'instance', 20 | Variable = 'variable' 21 | } 22 | 23 | export type SpecDefFunction = { 24 | type: SpecDefType.Function; 25 | name: string; 26 | numParameters: number; 27 | value: (...args: any[]) => any; 28 | }; 29 | export function funcDef(name: string, numParameters: number, value: (...args: any[]) => any): SpecDefFunction { 30 | return { name, type: SpecDefType.Function, value, numParameters }; 31 | } 32 | 33 | export type SpecDefInstance = { 34 | type: SpecDefType.Instance; 35 | name: string; 36 | value: any; 37 | }; 38 | export function instDef(name: string, value: any): SpecDefInstance { 39 | return { name, type: SpecDefType.Instance, value }; 40 | } 41 | 42 | export type SpecDefVariable = { 43 | type: SpecDefType.Variable; 44 | name: string; 45 | value: Arbitrary; 46 | }; 47 | export function varDef(name: string, value: Arbitrary): SpecDefVariable { 48 | return { name, type: SpecDefType.Variable, value }; 49 | } 50 | 51 | export type FindSpecElement = SpecDefFunction | SpecDefInstance | SpecDefVariable; 52 | 53 | export type FindSpecSettings = { 54 | /** 55 | * Number of combinations to try - default: 100 56 | */ 57 | numSamples?: number; 58 | /** 59 | * Complexity of the combinations - default: 2 60 | * 61 | * Higher complexity will produce combinations with more nested calls. 62 | */ 63 | complexity?: number; 64 | /** 65 | * Number of inputs to try to confirm a combination - default: 100 66 | */ 67 | numFuzz?: number; 68 | }; 69 | 70 | type FindSpecsInternal = { 71 | numArbs: number; 72 | build: (ins: any[], offset: number) => { value: any; nextOffset: number }; 73 | repr: (ins: string[], offset: number) => { value: string; nextOffset: number }; 74 | }; 75 | type FindSpecsInternalBuilder = (n?: number) => Arbitrary; 76 | 77 | export function findSpecs(def: FindSpecElement[], settings?: FindSpecSettings): string[] { 78 | const baseArbs: { name: string; arb: Arbitrary }[] = []; 79 | const specTermArbBuilder: FindSpecsInternalBuilder[] = []; 80 | 81 | for (const el of def) { 82 | switch (el.type) { 83 | case SpecDefType.Variable: { 84 | baseArbs.push({ name: el.name, arb: el.value }); 85 | specTermArbBuilder.push( 86 | memo(() => 87 | constant({ 88 | numArbs: 1, 89 | build: (vs: any[], offset: number) => { 90 | return { value: vs[offset], nextOffset: offset + 1 }; 91 | }, 92 | repr: (xn: string[], offset: number) => { 93 | return { value: xn[offset], nextOffset: offset + 1 }; 94 | } 95 | }) 96 | ) 97 | ); 98 | break; 99 | } 100 | case SpecDefType.Instance: { 101 | specTermArbBuilder.push( 102 | memo(() => 103 | constant({ 104 | numArbs: 0, 105 | build: (vs: any[], offset: number) => { 106 | return { value: el.value, nextOffset: offset }; 107 | }, 108 | repr: (xn: string[], offset: number) => { 109 | return { value: el.name, nextOffset: offset }; 110 | } 111 | }) 112 | ) 113 | ); 114 | break; 115 | } 116 | case SpecDefType.Function: { 117 | const elArb = memo(n => 118 | n <= 1 || el.numParameters === 0 119 | ? constant({ 120 | numArbs: el.numParameters, 121 | build: (vs: any[], offset: number) => { 122 | const nextOffset = offset + el.numParameters; 123 | return { 124 | value: el.value(...vs.slice(offset, nextOffset)), 125 | nextOffset 126 | }; 127 | }, 128 | repr: (xn: string[], offset: number) => { 129 | const nextOffset = offset + el.numParameters; 130 | return { 131 | value: `${el.name}(${xn.slice(offset, nextOffset).join(', ')})`, 132 | nextOffset 133 | }; 134 | } 135 | }) 136 | : genericTuple([...Array(el.numParameters)].map(() => oneof(...specTermArbBuilder.map(a => a())))).map( 137 | t => { 138 | return { 139 | numArbs: t.reduce((acc, cur) => acc + cur.numArbs, 0), 140 | build: (ins: any[], offset: number) => { 141 | let nextOffset = offset; 142 | const vs: any = []; 143 | for (let idx = 0; idx !== el.numParameters; ++idx) { 144 | const tmp = t[idx].build(ins, nextOffset); 145 | nextOffset = tmp.nextOffset; 146 | vs.push(tmp.value); 147 | } 148 | return { 149 | value: el.value(...vs), 150 | nextOffset 151 | }; 152 | }, 153 | repr: (xn: string[], offset: number) => { 154 | let nextOffset = offset; 155 | const vs: string[] = []; 156 | for (let idx = 0; idx !== el.numParameters; ++idx) { 157 | const tmp = t[idx].repr(xn, nextOffset); 158 | nextOffset = tmp.nextOffset; 159 | vs.push(tmp.value); 160 | } 161 | return { 162 | value: `${el.name}(${vs.join(', ')})`, 163 | nextOffset 164 | }; 165 | } 166 | }; 167 | } 168 | ) 169 | ); 170 | specTermArbBuilder.push(elArb); 171 | break; 172 | } 173 | } 174 | } 175 | const maxDepth = settings && settings.complexity !== undefined ? settings.complexity : 2; 176 | const specTermArb = oneof(...specTermArbBuilder.map(a => a(maxDepth))); 177 | const specArb = tuple(specTermArb, specTermArb) 178 | .chain(([t1, t2]) => { 179 | const numArbs = t1.numArbs > t2.numArbs ? t1.numArbs : t2.numArbs; 180 | const variableIndexes = [...Array(numArbs)].map((_, i) => i); 181 | 182 | return tuple( 183 | oneof(constant(variableIndexes), shuffledSubarray(variableIndexes, numArbs, numArbs)), 184 | numArbs > 0 185 | ? array(constantFrom(...baseArbs), numArbs, numArbs) // throw if no baseArbs 186 | : constant([]) 187 | ).map(([reindex, inputsDef]) => { 188 | const applyReindex = (ins: any[]) => { 189 | return reindex.map(ri => ins[ri]); 190 | }; 191 | const variableNames = inputsDef.map((inputDef, i) => `${inputDef.name}${i}`); 192 | return { 193 | inputArbs: inputsDef.map(inputDef => inputDef.arb), 194 | build1: (ins: any[]) => t1.build(ins, 0).value, 195 | build2: (ins: any[]) => t2.build(applyReindex(ins), 0).value, 196 | spec1: t1.repr(variableNames, 0).value, 197 | spec2: t2.repr(applyReindex(variableNames), 0).value 198 | }; 199 | }); 200 | }) 201 | .filter(d => d.spec1 !== d.spec2) 202 | .noShrink(); 203 | 204 | const union = new UnionGraph(); 205 | const biMulti = new BiMultiMap(); 206 | for (const spec of sample(specArb, settings && settings.numSamples)) { 207 | // Skip already covered combinations 208 | if (biMulti.has(spec.spec1, spec.spec2) || union.hasLink(spec.spec1, spec.spec2)) { 209 | continue; 210 | } 211 | biMulti.add(spec.spec1, spec.spec2); 212 | // Check if the combination holds 213 | if (combinationCheck(spec.inputArbs, spec.build1, spec.build2, settings && settings.numFuzz)) { 214 | union.addLink(spec.spec1, spec.spec2); 215 | } 216 | } 217 | return union.values().map(vs => vs.sort().join(' == ')); 218 | } 219 | --------------------------------------------------------------------------------