From 14ebd79475fe5a96d4a8bcae25e176250bac2a3a Mon Sep 17 00:00:00 2001 From: Nicolas DUBIEN Date: Fri, 6 May 2022 14:27:59 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Format=20all=20the=20files=20not?= =?UTF-8?q?=20only=20TS=20ones=20(#2923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .codesandbox/ci.json | 14 +- .github/ISSUE_TEMPLATE/BugReport.md | 12 +- .github/ISSUE_TEMPLATE/RegressionReport.md | 12 +- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/actions/deploy-netlify/action.yml | 2 +- .github/stale.yml | 6 +- .github/workflows/build-status.yml | 134 +- .github/workflows/codeql-analysis.yml | 36 +- .github/workflows/generate-changelog.yml | 2 +- .github/workflows/please-actions.yml | 4 +- .prettierignore | 3 +- .prettierrc | 12 +- CHANGELOG.md | 9 +- CHANGELOG_0.X.md | 2 + CHANGELOG_1.X.md | 15 +- CODE_OF_CONDUCT.md | 26 +- CONTRIBUTING.md | 12 +- MIGRATION_1.X_TO_2.X.md | 18 +- README.md | 349 ++-- api-extractor.json | 2 +- codemods/unify-signatures/README.md | 7 +- documentation/AdvancedArbitraries.md | 368 ++-- documentation/HandsOnPropertyBased.md | 320 +-- documentation/HandsOnPropertyBasedJs.md | 8 +- documentation/HowItWorks.md | 531 +++-- documentation/IssuesDiscovered.md | 247 +-- documentation/RaceConditions.md | 75 +- documentation/Runners.md | 454 +++-- documentation/Tips.md | 1801 ++++++++--------- example/005-race/autocomplete/main.spec.tsx | 276 +-- .../autocomplete/src/AutocompleteField.tsx | 144 +- .../src/AutocompleteFieldMostRecentQuery.tsx | 124 +- .../src/AutocompleteFieldSimple.tsx | 104 +- .../src/DebouncedAutocomplete.tsx | 6 +- example/005-race/todolist/main.spec.tsx | 258 +-- example/005-race/todolist/src/TodoList.tsx | 306 +-- example/005-race/userProfile/main.spec.tsx | 186 +- .../userProfile/src/UserProfilePage.tsx | 78 +- example/README.md | 184 +- example/tsconfig.json | 39 +- jest.config.cjs | 32 +- jest.e2e.config.cjs | 14 +- jest.unit.config.cjs | 14 +- package.esm-template.json | 4 +- package.json | 4 +- prebuild/helpers.cjs | 91 +- prebuild/prebuild.cjs | 22 +- prebuild/property.cjs | 226 +-- renovate.json | 8 +- test/esm/node-extension-cjs/README.md | 2 +- test/esm/node-extension-mjs/README.md | 2 +- test/esm/node-with-import/README.md | 2 +- test/esm/node-with-require/README.md | 2 +- test/esm/rollup-with-import/README.md | 2 +- test/esm/rollup-with-require/README.md | 2 +- test/esm/webpack-with-import/README.md | 2 +- test/esm/webpack-with-require/README.md | 2 +- test/legacy/node-8/package.json | 16 +- test/type/package.json | 3 +- test/type/tsconfig.json | 14 +- tsconfig.json | 43 +- tsconfig.nospec.json | 6 +- tsconfig.publish.json | 18 +- tsconfig.publish.types.json | 20 +- 64 files changed, 3359 insertions(+), 3384 deletions(-) diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 713d7baa..c66695ca 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,9 +1,5 @@ - - -{ - "buildCommand": "build:prod", - "sandboxes": ["vanilla", "/example"], - "node": "14" - } - - +{ + "buildCommand": "build:prod", + "sandboxes": ["vanilla", "/example"], + "node": "14" +} diff --git a/.github/ISSUE_TEMPLATE/BugReport.md b/.github/ISSUE_TEMPLATE/BugReport.md index 33e10118..8cf0389f 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.md +++ b/.github/ISSUE_TEMPLATE/BugReport.md @@ -20,10 +20,10 @@ If you have one, please provide a minimal repository reproducing the issue on Gi ## Your environment -| Packages / Softwares | Version(s) | -| --------------------- | ---------- | -| fast-check | | -| node | | -| TypeScript* | | +| Packages / Softwares | Version(s) | +| -------------------- | ---------- | +| fast-check | | +| node | | +| TypeScript\* | | -*Only for TypeScript's users +\*Only for TypeScript's users diff --git a/.github/ISSUE_TEMPLATE/RegressionReport.md b/.github/ISSUE_TEMPLATE/RegressionReport.md index 63a37448..7e1f0f6b 100644 --- a/.github/ISSUE_TEMPLATE/RegressionReport.md +++ b/.github/ISSUE_TEMPLATE/RegressionReport.md @@ -26,10 +26,10 @@ If you have one, please provide a minimal repository reproducing the issue on Gi ## Your environment -| Packages / Softwares | Version(s) | -| --------------------- | ---------- | -| fast-check | | -| node | | -| TypeScript* | | +| Packages / Softwares | Version(s) | +| -------------------- | ---------- | +| fast-check | | +| node | | +| TypeScript\* | | -*Only for TypeScript's users +\*Only for TypeScript's users diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1c777577..d760c7b1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,7 @@ + **_Category:_** - [ ] ✨ Introduce new features @@ -13,8 +14,8 @@ - [ ] 🏷️ Add or update types - [ ] ⚡️ Improve performance - [ ] _Other(s):_ ... - - + + @@ -26,6 +27,7 @@ + - [ ] Generated values - [ ] Shrink values - [ ] Performance diff --git a/.github/actions/deploy-netlify/action.yml b/.github/actions/deploy-netlify/action.yml index a6ec8c36..533dc683 100644 --- a/.github/actions/deploy-netlify/action.yml +++ b/.github/actions/deploy-netlify/action.yml @@ -7,4 +7,4 @@ inputs: required: false runs: using: 'node12' - main: 'index.cjs' \ No newline at end of file + main: 'index.cjs' diff --git a/.github/stale.yml b/.github/stale.yml index f6d96a77..eff7e7bb 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -4,9 +4,9 @@ daysUntilStale: 60 daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "✔️ Feature Accepted" - - "✔️ Bug Confirmed" - - "✔️ Idea to investigate" + - '✔️ Feature Accepted' + - '✔️ Bug Confirmed' + - '✔️ Idea to investigate' # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml index 27175b49..b9e29e2c 100644 --- a/.github/workflows/build-status.yml +++ b/.github/workflows/build-status.yml @@ -2,7 +2,7 @@ name: Build Status on: push: - branches: + branches: - main - 'next-*_*_*' - 'fix-v*' @@ -81,15 +81,15 @@ jobs: run: yarn --frozen-lockfile - name: Build the library (dev version) run: | - yarn prebuild - yarn build + yarn prebuild + yarn build - name: Unit tests shell: bash -l {0} run: | - export EXPECT_DEFAULT_SEED="true" - export DEFAULT_SEED=$(node -p "Date.now() ^ (Math.random() * 0x100000000)") - echo "DEFAULT_SEED is: ${DEFAULT_SEED}" - yarn test + export EXPECT_DEFAULT_SEED="true" + export DEFAULT_SEED=$(node -p "Date.now() ^ (Math.random() * 0x100000000)") + echo "DEFAULT_SEED is: ${DEFAULT_SEED}" + yarn test - name: Codecov uses: codecov/codecov-action@v3 with: @@ -124,15 +124,15 @@ jobs: run: yarn --frozen-lockfile - name: Build the library (dev version) run: | - yarn prebuild - yarn build + yarn prebuild + yarn build - name: End-to-end tests shell: bash -l {0} run: | - export EXPECT_DEFAULT_SEED="true" - export DEFAULT_SEED=$(node -p "Date.now() ^ (Math.random() * 0x100000000)") - echo "DEFAULT_SEED is: ${DEFAULT_SEED}" - yarn e2e + export EXPECT_DEFAULT_SEED="true" + export DEFAULT_SEED=$(node -p "Date.now() ^ (Math.random() * 0x100000000)") + echo "DEFAULT_SEED is: ${DEFAULT_SEED}" + yarn e2e test_package_quality: name: 'Test package quality' runs-on: ubuntu-latest @@ -340,59 +340,59 @@ jobs: - '13.7' - '13' steps: - - uses: actions/checkout@v3 - - name: Using Node v${{matrix.node-version}} - shell: bash -l {0} - run: nvm install ${{matrix.node-version}} - - name: Download production package - uses: actions/download-artifact@v3 - with: - name: bundle - - name: Install dependencies but use current build for fast-check (node-extension-cjs) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/node-extension-cjs' - - name: Install dependencies but use current build for fast-check (node-extension-mjs) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/node-extension-mjs' - - name: Install dependencies but use current build for fast-check (node-with-import) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/node-with-import' - - name: Install dependencies but use current build for fast-check (node-with-require) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/node-with-require' - - name: Install dependencies but use current build for fast-check (rollup-with-import) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/rollup-with-import' - - name: Install dependencies but use current build for fast-check (rollup-with-require) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/rollup-with-require' - - name: Install dependencies but use current build for fast-check (webpack-with-import) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/webpack-with-import' - - name: Install dependencies but use current build for fast-check (webpack-with-require) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/webpack-with-require' - - name: Install dependencies but use current build for fast-check (esbuild-with-import) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/esbuild-with-import' - - name: Install dependencies but use current build for fast-check (esbuild-with-require) - uses: ./.github/actions/install-deps-with-current-fc - with: - path: 'test/esm/esbuild-with-require' - - name: Check compatibility - run: | - cd test/esm - node --version - sh run.sh + - uses: actions/checkout@v3 + - name: Using Node v${{matrix.node-version}} + shell: bash -l {0} + run: nvm install ${{matrix.node-version}} + - name: Download production package + uses: actions/download-artifact@v3 + with: + name: bundle + - name: Install dependencies but use current build for fast-check (node-extension-cjs) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/node-extension-cjs' + - name: Install dependencies but use current build for fast-check (node-extension-mjs) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/node-extension-mjs' + - name: Install dependencies but use current build for fast-check (node-with-import) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/node-with-import' + - name: Install dependencies but use current build for fast-check (node-with-require) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/node-with-require' + - name: Install dependencies but use current build for fast-check (rollup-with-import) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/rollup-with-import' + - name: Install dependencies but use current build for fast-check (rollup-with-require) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/rollup-with-require' + - name: Install dependencies but use current build for fast-check (webpack-with-import) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/webpack-with-import' + - name: Install dependencies but use current build for fast-check (webpack-with-require) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/webpack-with-require' + - name: Install dependencies but use current build for fast-check (esbuild-with-import) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/esbuild-with-import' + - name: Install dependencies but use current build for fast-check (esbuild-with-require) + uses: ./.github/actions/install-deps-with-current-fc + with: + path: 'test/esm/esbuild-with-require' + - name: Check compatibility + run: | + cd test/esm + node --version + sh run.sh publish_documentation: name: 'Publish documentation' needs: documentation @@ -413,7 +413,7 @@ jobs: clean: true publish_package: name: 'Publish package' - needs: + needs: - production_package - documentation - format_lint diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c7fff355..98f2219f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: "CodeQL" +name: 'CodeQL' on: push: @@ -12,23 +12,23 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 + - name: Checkout repository + uses: actions/checkout@v3 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: javascript + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: javascript - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index 65d01c6a..ad9e5ad2 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -14,7 +14,7 @@ on: jobs: generate_changelog: - name: "Generate Changelog" + name: 'Generate Changelog' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/please-actions.yml b/.github/workflows/please-actions.yml index 25c9230f..451cc7a8 100644 --- a/.github/workflows/please-actions.yml +++ b/.github/workflows/please-actions.yml @@ -52,7 +52,7 @@ jobs: NETLIFY_AUTH_TOKEN: ${{secrets.NETLIFY_AUTH_TOKEN}} please_merge: - name: "Please merge" + name: 'Please merge' runs-on: ubuntu-latest if: github.event.issue.pull_request && github.event.comment.body == 'please merge' steps: @@ -99,7 +99,7 @@ jobs: run: git push please_debug: - name: "Please debug" + name: 'Please debug' runs-on: ubuntu-latest if: github.event.issue.pull_request && github.event.comment.body == 'please debug' steps: diff --git a/.prettierignore b/.prettierignore index 9789e857..64594fb2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,8 @@ bundle/ coverage/ -dist/ docs/ +documentation/Arbitraries.md +dist/ lib/ lib-*/ *.generated.ts diff --git a/.prettierrc b/.prettierrc index 79f07ba2..5d40fd80 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,5 @@ -{ - "parser": "typescript", - - "printWidth": 120, - "tabWidth": 2, - "singleQuote": true -} \ No newline at end of file +{ + "printWidth": 120, + "tabWidth": 2, + "singleQuote": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e65d24c..17cab057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -432,7 +432,7 @@ _New logo, new way to define fully custom arbitraries using `NextArbitrary`_ - ([PR#1760](https://github.com/dubzzz/fast-check/pull/1760)) Clean: Remove duplicated tests for letrec - ([PR#1892](https://github.com/dubzzz/fast-check/pull/1892)) Clean: Remove unneeded map in record for required keys - ([PR#1917](https://github.com/dubzzz/fast-check/pull/1917)) Clean: Remove unneeded checks in map for context-less shrink -- ([PR#1664](https://github.com/dubzzz/fast-check/pull/1664)) Doc: Only mark relevant titles as H* in the Readme +- ([PR#1664](https://github.com/dubzzz/fast-check/pull/1664)) Doc: Only mark relevant titles as H\* in the Readme - ([PR#1665](https://github.com/dubzzz/fast-check/pull/1665)) Doc: Move back to H2 for titles in the Readme - ([PR#1697](https://github.com/dubzzz/fast-check/pull/1697)) Doc: Fic grammar - ([PR#1883](https://github.com/dubzzz/fast-check/pull/1883)) Doc: Rework PR template @@ -516,7 +516,7 @@ _Easier recursive strcutures and ability discard already seen runs_ - ([PR#1581](https://github.com/dubzzz/fast-check/pull/1581)) Reject invalid weights on `fc.frequency` - ([PR#1598](https://github.com/dubzzz/fast-check/pull/1598)) Add `withCrossShrink` constraint on `fc.frequency` -- ([PR#1586](https://github.com/dubzzz/fast-check/pull/1586)) Add a way to ignore already covered cases +- ([PR#1586](https://github.com/dubzzz/fast-check/pull/1586)) Add a way to ignore already covered cases - ([PR#1601](https://github.com/dubzzz/fast-check/pull/1601)) Add `maxDepth` constraint on `fc.frequency` - ([PR#1602](https://github.com/dubzzz/fast-check/pull/1602)) Stricter checks on args of `fc.frequency` - ([PR#1603](https://github.com/dubzzz/fast-check/pull/1603)) Add `depthFactor` constraint on `fc.frequency` @@ -911,7 +911,7 @@ _Towards a uniform way to constrain arbitraries - step 1: array-like arbitraries - ([PR#975](https://github.com/dubzzz/fast-check/pull/975)) Doc: Add runkit code example - ([PR#992](https://github.com/dubzzz/fast-check/pull/992)) Doc: Add automatic simplification of min and max in codemod for [#992](https://github.com/dubzzz/fast-check/issues/992) - ([PR#993](https://github.com/dubzzz/fast-check/pull/993)) Fix: Do not depreciate overloads for array-like (yet) -- ([PR#1012](https://github.com/dubzzz/fast-check/pull/1012)) Fix: Adopt a safer signature recognition on array and set +- ([PR#1012](https://github.com/dubzzz/fast-check/pull/1012)) Fix: Adopt a safer signature recognition on array and set - ([PR#1014](https://github.com/dubzzz/fast-check/pull/1014)) Test: Ensure old non-unified syntaxes still work - ([PR#991](https://github.com/dubzzz/fast-check/pull/991)) Tool: Fix .prettierignore - ([PR#976](https://github.com/dubzzz/fast-check/pull/976)) Typo: Use WebUrlConstraints instead of an inlined typing for webUrl @@ -1028,6 +1028,7 @@ _Hybrid and full support for both ES Modules and CommonJS_ [[Code](https://github.com/dubzzz/fast-check/tree/v2.0.0)][[Diff](https://github.com/dubzzz/fast-check/compare/v1.26.0...v2.0.0)] This new major of fast-check is: + - **lighter**: 906kB with 385 files to 505kB with 287 files - **faster**: takes between -15% (sync) to -40% (async) less time to run properties ([more](https://github.com/dubzzz/fast-check/pull/748)) - **es-module** compatible: can be executed with `type:module` @@ -1044,7 +1045,7 @@ This new major of fast-check is: - ([PR#752](https://github.com/dubzzz/fast-check/pull/752)) Support ES Modules and CommonJS - ([PR#756](https://github.com/dubzzz/fast-check/pull/756)) Drop browser build -*You may refer to our migration guide in case of issue: https://github.com/dubzzz/fast-check/blob/main/MIGRATION_1.X_TO_2.X.md* +_You may refer to our migration guide in case of issue: https://github.com/dubzzz/fast-check/blob/main/MIGRATION_1.X_TO_2.X.md_ ## Fixes diff --git a/CHANGELOG_0.X.md b/CHANGELOG_0.X.md index d827d32e..f7c5508e 100644 --- a/CHANGELOG_0.X.md +++ b/CHANGELOG_0.X.md @@ -31,6 +31,7 @@ _Bundled for web-browsers and node_ [[Code](https://github.com/dubzzz/fast-check/tree/v0.0.11)][[Diff](https://github.com/dubzzz/fast-check/compare/v0.0.10...v0.0.11)] ## Features: + - Add bundle for web-browsers - Add code examples in the source code - Add minimal length parameter on all strings arbitraries @@ -39,6 +40,7 @@ _Bundled for web-browsers and node_ - Add timeout parameter on asychronous properties ## Fixes: + - Fix: unicode character generators # 0.0.10 diff --git a/CHANGELOG_1.X.md b/CHANGELOG_1.X.md index 0e2e5952..bed67d97 100644 --- a/CHANGELOG_1.X.md +++ b/CHANGELOG_1.X.md @@ -73,9 +73,9 @@ _Fix `constantFrom` not compatible with older versions of node_ ## Fixes - ([PR#583](https://github.com/dubzzz/fast-check/pull/583)) Bug: `constantFrom` not compatible with old browsers -- ([PR#569](https://github.com/dubzzz/fast-check/pull/569)) Clean: Prebuild to cjs extension +- ([PR#569](https://github.com/dubzzz/fast-check/pull/569)) Clean: Prebuild to cjs extension - ([PR#568](https://github.com/dubzzz/fast-check/pull/568)) Doc: Broken links -- ([PR#575](https://github.com/dubzzz/fast-check/pull/575)) Doc: Invalid code in example of the README +- ([PR#575](https://github.com/dubzzz/fast-check/pull/575)) Doc: Invalid code in example of the README - ([PR#578](https://github.com/dubzzz/fast-check/pull/578)) Doc: Schedule native timers - ([PR#576](https://github.com/dubzzz/fast-check/pull/576)) Example: Fibonacci example - ([PR#577](https://github.com/dubzzz/fast-check/pull/577)) Example: Fix decompPrime example for CodeSandbox @@ -114,7 +114,7 @@ _Better typings for `filter` and `oneof` plus support for null prototypes_ ## Features -- ([PR#548](https://github.com/dubzzz/fast-check/pull/548)) Stringify should distinguish `{}` from `Object.create(null)` +- ([PR#548](https://github.com/dubzzz/fast-check/pull/548)) Stringify should distinguish `{}` from `Object.create(null)` - ([PR#552](https://github.com/dubzzz/fast-check/pull/552)) Add ability to generate objects without prototype - ([PR#555](https://github.com/dubzzz/fast-check/pull/555)) Support type guards while filtering :warning: - ([PR#556](https://github.com/dubzzz/fast-check/pull/556)) Better typings for oneof and frequency :warning: @@ -414,7 +414,7 @@ _Add auto-skip after time limit option for runners_ - ([PR#354](https://github.com/dubzzz/fast-check/pull/354)) Doc: Add examples of issues discovered using fast-check - ([PR#353](https://github.com/dubzzz/fast-check/pull/353)) Doc: Better logo -- ([PR#351](https://github.com/dubzzz/fast-check/pull/351)) Size: Add dependency to tslib - *should reduce size of the bundle* +- ([PR#351](https://github.com/dubzzz/fast-check/pull/351)) Size: Add dependency to tslib - _should reduce size of the bundle_ - ([PR#349](https://github.com/dubzzz/fast-check/pull/349)) Test: No regression snapshot tests --- @@ -494,7 +494,7 @@ _Lighter bundle_ ## Fixes - ([PR#327](https://github.com/dubzzz/fast-check/pull/327)) Doc: Ability to copy-paste snippets in HandsOnPropertyBased.md -- ([PR#334](https://github.com/dubzzz/fast-check/pull/334)) Size: Reduce the size of the bundle - *Potential issue if your code directly references TupleArbitrary, it should be replaced by Arbitrary<[T1,...]>* +- ([PR#334](https://github.com/dubzzz/fast-check/pull/334)) Size: Reduce the size of the bundle - _Potential issue if your code directly references TupleArbitrary, it should be replaced by Arbitrary<[T1,...]>_ # 1.12.0 @@ -711,7 +711,7 @@ _Switch to another PRNG for better performances, better fc.commands_ ## Fixes -- ([PR#220](https://github.com/dubzzz/fast-check/pull/220)) Switch to another PRNG as default random - *more performances* +- ([PR#220](https://github.com/dubzzz/fast-check/pull/220)) Switch to another PRNG as default random - _more performances_ - ([PR#217](https://github.com/dubzzz/fast-check/pull/217)) Better typings for `fc.record` --- @@ -787,7 +787,6 @@ _Addition of `subarray` and `shuffledSubarray` arbitraries_ - ([PR#157](https://github.com/dubzzz/fast-check/pull/157)) Model based testing and commands - ([PR#158](https://github.com/dubzzz/fast-check/pull/158)) Characters shrink towards printable ascii - ## Fixes - ([PR#170](https://github.com/dubzzz/fast-check/pull/170)) Fix: `fullUnicode` and `fullUnicodeString` were failing on old releases of node @@ -842,7 +841,7 @@ _Reduce package footprint and less restrictive API for `oneof`/`frequency`_ ## Fixes -- ([PR#135](https://github.com/dubzzz/fast-check/pull/135)) Do not force explicitly one parameter in `oneof`/`frequency` +- ([PR#135](https://github.com/dubzzz/fast-check/pull/135)) Do not force explicitly one parameter in `oneof`/`frequency` - ([PR#134](https://github.com/dubzzz/fast-check/pull/134)) Doc: Typos in README - ([PR#132](https://github.com/dubzzz/fast-check/pull/132)) Add missing exports for `jsonObject` and `unicodeJsonObject` - ([PR#131](https://github.com/dubzzz/fast-check/pull/131)) Reduce package size diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c7b1cea0..98153260 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 847b1e7a..e67594eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,7 @@ It ensures that the pull request follow the code style of the project and do not If you plan to update your PR with either a fix for the tests or change following code reviews please directly commit your new commit in your branch, PR will get updated automatically. Before your fix: + ``` --*---> main on dubzzz/fast-check \ @@ -64,6 +65,7 @@ Before your fix: ``` After your fix: + ``` --*---> main on dubzzz/fast-check \ @@ -78,21 +80,21 @@ Ideally to resync your branch with main prefer a merge of main branch into your #### Adding a new arbitrary -✔️ *Create a feature request* +✔️ _Create a feature request_ Before adding any new arbitrary into fast-check please make sure to fill a `Feature request` to justify the need for such arbitrary. -✔️ *Code the arbitrary* +✔️ _Code the arbitrary_ All the arbitraries defined by fast-check are available in `src/arbitrary`. Create a new file for the new one if it does not fit into the existing ones. -✔️ *Test the arbitrary* +✔️ _Test the arbitrary_ Most of the newly added arbitraries will just be a combination of existing ones (mostly mapping from one entry to another). We expect a quite minimal amount of tests to be added as most of the logic depends on the built-in blocks. -- *Unit-test* & *Integration* - in `test/unit/arbitrary` +- _Unit-test_ & _Integration_ - in `test/unit/arbitrary` ```js import * as fc from '../../../lib/fast-check'; @@ -171,7 +173,7 @@ The `legacy` spec is responsible to check that most of the arbitraries provided The `type` spec is responsible to check that the typings are correct but they also ensure that they will not break with future changes or upcoming releases of TypeScript. -✔️ *Document the arbitrary* +✔️ _Document the arbitrary_ - Provide a minimal JSDoc on top of your new arbitrary and use the `/** @internal */` tag to hide internals - otherwise they would get published into the generated documentation diff --git a/MIGRATION_1.X_TO_2.X.md b/MIGRATION_1.X_TO_2.X.md index dc8c04ba..7f1e7fa7 100644 --- a/MIGRATION_1.X_TO_2.X.md +++ b/MIGRATION_1.X_TO_2.X.md @@ -39,22 +39,22 @@ In the previous major, `fc.constantFrom` was not typing tuples properly and refu As an example, the following was not compiling: ```ts -fc.constantFrom(false, null, undefined, 0) +fc.constantFrom(false, null, undefined, 0); ``` It required the user to explicitely specify the type: ```ts /// In version 1.x.x -fc.constantFrom(false, null, 0) +fc.constantFrom(false, null, 0); /// In version 2.x.x -fc.constantFrom(false, null, 0) +fc.constantFrom(false, null, 0); // or with an explicit typing -fc.constantFrom<(boolean | null | number)[]>(false, null, 0) +fc.constantFrom<(boolean | null | number)[]>(false, null, 0); ``` -If you explicitely typed some calls, `fc.constantFrom` should be updated into `fc.constantFrom` - *without any generic* - or `fc.constantFrom`. +If you explicitely typed some calls, `fc.constantFrom` should be updated into `fc.constantFrom` - _without any generic_ - or `fc.constantFrom`. Associated Pull Requests: [#747](https://github.com/dubzzz/fast-check/pull/747) @@ -68,7 +68,7 @@ Associated Pull Requests: [#755](https://github.com/dubzzz/fast-check/pull/755) ## No more browser build -In the previous major, fast-check was building a specific bundle for browsers. This bundle was easily *fetch-able* from CDNs like unpkg. +In the previous major, fast-check was building a specific bundle for browsers. This bundle was easily _fetch-able_ from CDNs like unpkg. Example of bundled version of fast-check: https://unpkg.com/browse/fast-check@1.22.1/lib/bundle.js @@ -80,14 +80,14 @@ If the browsers you are targeting are compatible with esm-modules, you can impor ```html ``` ### Locally build the bundled version -Alternatively, you can easily build the `lib/bundle.js` file that was provided by fast-check by running the following command-line - *here we assume that you declared fast-check as a dependency of your project in the `package.json`*. +Alternatively, you can easily build the `lib/bundle.js` file that was provided by fast-check by running the following command-line - _here we assume that you declared fast-check as a dependency of your project in the `package.json`_. ```bash npx -p browserify browserify node_modules/fast-check/lib/fast-check.js --s fastcheck > node_modules/fast-check/lib/bundle.js @@ -107,7 +107,7 @@ Associated Pull Requests: [#756](https://github.com/dubzzz/fast-check/pull/756) Support for versions of ES standard below 2017 has been removed. -If you are still using - *and not transpiling towards your target* - a version of Node or of the browser that does not support ES2017, you can either keep using fast-check 1.x.x or have a look into [babel](https://github.com/babel/babel) and related projects such as [babelify](https://github.com/babel/babelify). +If you are still using - _and not transpiling towards your target_ - a version of Node or of the browser that does not support ES2017, you can either keep using fast-check 1.x.x or have a look into [babel](https://github.com/babel/babel) and related projects such as [babelify](https://github.com/babel/babelify). Associated Pull Requests: [#748](https://github.com/dubzzz/fast-check/pull/748) diff --git a/README.md b/README.md index f0a393f2..00f1673e 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,176 @@ -

- fast-check logo -

- -

-Property based testing framework for JavaScript/TypeScript -

- -

- Build Status - npm version - monthly downloads - -

-

- Coverage Status (unit tests) - Package quality - Snyk Package quality -

-

- PRs Welcome - License - Twitter -

- -## Getting started - -Hands-on tutorial and definition of Property Based Testing: [🏁 see tutorial](https://github.com/dubzzz/fast-check/blob/main/documentation/HandsOnPropertyBased.md). Or directly try it online on our pre-configured [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). - -Property based testing frameworks check the truthfulness of properties. A property is a statement like: *for all (x, y, ...) such that precondition(x, y, ...) holds predicate(x, y, ...) is true*. - -Install the module with: `yarn add fast-check --dev` or `npm install fast-check --save-dev` - -Example of integration in [mocha](http://mochajs.org/): - -```js -const fc = require('fast-check'); - -// Code under test -const contains = (text, pattern) => text.indexOf(pattern) >= 0; - -// Properties -describe('properties', () => { - // string text always contains itself - it('should always contain itself', () => { - fc.assert(fc.property(fc.string(), text => contains(text, text))); - }); - // string a + b + c always contains b, whatever the values of a, b and c - it('should always contain its substrings', () => { - fc.assert(fc.property(fc.string(), fc.string(), fc.string(), (a,b,c) => { - // Alternatively: no return statement and direct usage of expect or assert - return contains(a+b+c, b); - })); - }); -}); -``` - -In case of failure, the test raises a red flag. Its output should help you to diagnose what went wrong in your implementation. Example with a failing implementation of contain: - -``` -1) should always contain its substrings - Error: Property failed after 1 tests (seed: 1527422598337, path: 0:0): ["","",""] - Shrunk 1 time(s) - Got error: Property failed by returning false - - Hint: Enable verbose mode in order to have the list of all failing values encountered during the run -``` - -Integration with other test frameworks: [ava](https://github.com/dubzzz/fast-check-examples/blob/main/test-ava/example.spec.js), [jasmine](https://github.com/dubzzz/fast-check-examples/blob/main/test-jasmine/example.spec.js), [jest](https://github.com/dubzzz/fast-check-examples/blob/main/test-jest/example.spec.js), [mocha](https://github.com/dubzzz/fast-check-examples/blob/main/test/longest%20common%20substr/test.js) and [tape](https://github.com/dubzzz/fast-check-examples/blob/main/test-tape/example.spec.js). - -More examples: [simple examples](https://github.com/dubzzz/fast-check/tree/main/example), [fuzzing](https://github.com/dubzzz/fuzz-rest-api) and [against various algorithms](https://github.com/dubzzz/fast-check-examples). - -Useful documentations: -- [🏁 Introduction to Property Based & Hands On](https://github.com/dubzzz/fast-check/blob/main/documentation/HandsOnPropertyBased.md) -- [🐣 Built-in arbitraries](https://github.com/dubzzz/fast-check/blob/main/documentation/Arbitraries.md) -- [🔧 Custom arbitraries](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md) -- [🏃‍♂️ Property based runners](https://github.com/dubzzz/fast-check/blob/main/documentation/Runners.md) -- [💥 Tips](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md) -- [🔌 API Reference](https://dubzzz.github.io/fast-check/) -- [⭐ Awesome fast-check](https://github.com/dubzzz/awesome-fast-check) -- [🤯 How fast-check works?](https://github.com/dubzzz/fast-check/blob/main/documentation/HowItWorks.md) - -## Why should I migrate to fast-check? - -fast-check has initially been designed in an attempt to cope with limitations I encountered while using other property based testing frameworks designed for JavaScript: - -- **Types:** strong and up-to-date types - *thanks to TypeScript* -- **Extendable:** easy `map` method to derive existing arbitraries while keeping shrink \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#transform-values)\] - *some frameworks ask the user to provide both a->b and b->a mappings in order to keep a shrinker* -- **Extendable:** kind of flatMap-operation called `chain` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#transform-arbitraries)\] - *able to bind the output of an arbitrary as input of another one while keeping the shrink working* -- **Extendable:** precondition checks with `fc.pre(...)` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#filter-invalid-combinations-using-pre-conditions)\] - *filtering invalid entries can be done directly inside the check function if needed* -- **Smart:** ability to shrink on `fc.oneof` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Arbitraries.md#combinors-of-arbitraries-t)\] - *surprisingly some frameworks don't* -- **Smart:** biased by default \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#biased-arbitraries)\] - *by default it generates both small and large values, making it easier to dig into counterexamples without having to tweak a size parameter manually* -- **Debug:** verbose mode \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#opt-for-verbose-failures)\] - *easier troubleshooting with verbose mode enabled* -- **Debug:** replay directly on the minimal counterexample \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#replay-after-failure)\] - *no need to replay the whole sequence, you get directly the counterexample* -- **Debug:** custom examples in addition of generated ones \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#add-custom-examples-next-to-generated-ones)\] - *no need to duplicate the code to play the property on custom examples* -- **Debug:** logger per predicate run \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#log-within-a-predicate)\] - *simplify your troubleshoot with fc.context and its logging feature* -- **Unique:** model based approach \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#model-based-testing-or-ui-test)\]\[[article](https://medium.com/criteo-labs/detecting-the-unexpected-in-web-ui-fuzzing-1f3822c8a3a5)\] - *use the power of property based testing to test UI, APIs or state machines* -- **Unique:** detect race conditions in your code \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#detect-race-conditions)\] - *shuffle the way your promises and async calls resolve using the power of property based testing to detect races* -- **Unique:** simplify user definable corner cases \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#simplify-user-definable-corner-cases)\] - *simplify bug resolution by asking fast-check if it can find an even simpler corner case* - -For more details, refer to the documentation in the links above. - -## Compatibility - -Here are the minimal requirements to use fast-check properly without any polyfills: - -| fast-check | node | ECMAScript version | _TypeScript (optional)_ | -|------------|---------------------|--------------------|-------------------------| -| **2.x** | ≥8(1) | ES2017 | ≥3.2 | -| **1.x** | ≥0.12(1) | ES3 | ≥3.0 | - -(1) Except for features that cannot be polyfilled - such as `bigint`-related ones - all the capabilities of fast-check should be usable given you use at least the minimal recommended version of node associated to your major of fast-check. - -**ReScript bindings** - - -Bindings to use fast-check in [ReScript](https://rescript-lang.org) are available in package [rescript-fast-check](https://www.npmjs.com/rescript-fast-check). They are maintained by [@TheSpyder](https://github.com/TheSpyder) as an external project. - -## Issues found by fast-check in famous packages - -fast-check has been able to find some unexpected behaviour among famous npm packages. Here are some of the errors detected using fast-check: - -**[jest](https://github.com/facebook/jest/)** - -**Issue detected:** `toStrictEqual` fails to distinguish 0 from 5e-324 \[[more](https://github.com/facebook/jest/issues/7941)\] - -**Code example:** `expect(0).toStrictEqual(5e-324)` succeeds - -**[js-yaml](https://github.com/nodeca/js-yaml/)** - -**Issue detected:** enabling `!!int: binary` style when dumping negative integers produces invalid content \[[more](https://github.com/nodeca/js-yaml/pull/398)\] - -**Code example:** `yaml.dump({toto: -10}, {styles:{'!!int':'binary'}})` produces `toto: 0b-1010` not `toto: -0b1010` - -**[query-string](https://github.com/sindresorhus/query-string)** - -**Issue detected:** enabling the `bracket` setting when exporting arrays containing null values produces an invalid output for the parser \[[more](https://github.com/sindresorhus/query-string/pull/138)\] - -**Code example:** -```js -m.stringify({bar: ['a', null, 'b']}, {arrayFormat: 'bracket'}) //=> "bar[]=a&bar&bar[]=b" -m.parse('bar[]=a&bar&bar[]=b', {arrayFormat: 'bracket'}) //=> {bar: [null, 'b']} -``` - -**[MORE: Issues detected thanks to fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)** - -## Credits - -**Code Contributors** - -This project would not be the same without them 💖 - [Become one of them](CONTRIBUTING.md) - - - -**Backers** - -Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/fast-check/contribute)] and help us sustain our community. - - -**Sponsors** - -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/fast-check#sponsor)] - - - - - - - - - - - +

+ fast-check logo +

+ +

+Property based testing framework for JavaScript/TypeScript +

+ +

+ Build Status + npm version + monthly downloads + +

+

+ Coverage Status (unit tests) + Package quality + Snyk Package quality +

+

+ PRs Welcome + License + Twitter +

+ +## Getting started + +Hands-on tutorial and definition of Property Based Testing: [🏁 see tutorial](https://github.com/dubzzz/fast-check/blob/main/documentation/HandsOnPropertyBased.md). Or directly try it online on our pre-configured [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). + +Property based testing frameworks check the truthfulness of properties. A property is a statement like: _for all (x, y, ...) such that precondition(x, y, ...) holds predicate(x, y, ...) is true_. + +Install the module with: `yarn add fast-check --dev` or `npm install fast-check --save-dev` + +Example of integration in [mocha](http://mochajs.org/): + +```js +const fc = require('fast-check'); + +// Code under test +const contains = (text, pattern) => text.indexOf(pattern) >= 0; + +// Properties +describe('properties', () => { + // string text always contains itself + it('should always contain itself', () => { + fc.assert(fc.property(fc.string(), (text) => contains(text, text))); + }); + // string a + b + c always contains b, whatever the values of a, b and c + it('should always contain its substrings', () => { + fc.assert( + fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => { + // Alternatively: no return statement and direct usage of expect or assert + return contains(a + b + c, b); + }) + ); + }); +}); +``` + +In case of failure, the test raises a red flag. Its output should help you to diagnose what went wrong in your implementation. Example with a failing implementation of contain: + +``` +1) should always contain its substrings + Error: Property failed after 1 tests (seed: 1527422598337, path: 0:0): ["","",""] + Shrunk 1 time(s) + Got error: Property failed by returning false + + Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +Integration with other test frameworks: [ava](https://github.com/dubzzz/fast-check-examples/blob/main/test-ava/example.spec.js), [jasmine](https://github.com/dubzzz/fast-check-examples/blob/main/test-jasmine/example.spec.js), [jest](https://github.com/dubzzz/fast-check-examples/blob/main/test-jest/example.spec.js), [mocha](https://github.com/dubzzz/fast-check-examples/blob/main/test/longest%20common%20substr/test.js) and [tape](https://github.com/dubzzz/fast-check-examples/blob/main/test-tape/example.spec.js). + +More examples: [simple examples](https://github.com/dubzzz/fast-check/tree/main/example), [fuzzing](https://github.com/dubzzz/fuzz-rest-api) and [against various algorithms](https://github.com/dubzzz/fast-check-examples). + +Useful documentations: + +- [🏁 Introduction to Property Based & Hands On](https://github.com/dubzzz/fast-check/blob/main/documentation/HandsOnPropertyBased.md) +- [🐣 Built-in arbitraries](https://github.com/dubzzz/fast-check/blob/main/documentation/Arbitraries.md) +- [🔧 Custom arbitraries](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md) +- [🏃‍♂️ Property based runners](https://github.com/dubzzz/fast-check/blob/main/documentation/Runners.md) +- [💥 Tips](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md) +- [🔌 API Reference](https://dubzzz.github.io/fast-check/) +- [⭐ Awesome fast-check](https://github.com/dubzzz/awesome-fast-check) +- [🤯 How fast-check works?](https://github.com/dubzzz/fast-check/blob/main/documentation/HowItWorks.md) + +## Why should I migrate to fast-check? + +fast-check has initially been designed in an attempt to cope with limitations I encountered while using other property based testing frameworks designed for JavaScript: + +- **Types:** strong and up-to-date types - _thanks to TypeScript_ +- **Extendable:** easy `map` method to derive existing arbitraries while keeping shrink \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#transform-values)\] - _some frameworks ask the user to provide both a->b and b->a mappings in order to keep a shrinker_ +- **Extendable:** kind of flatMap-operation called `chain` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#transform-arbitraries)\] - _able to bind the output of an arbitrary as input of another one while keeping the shrink working_ +- **Extendable:** precondition checks with `fc.pre(...)` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#filter-invalid-combinations-using-pre-conditions)\] - _filtering invalid entries can be done directly inside the check function if needed_ +- **Smart:** ability to shrink on `fc.oneof` \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Arbitraries.md#combinors-of-arbitraries-t)\] - _surprisingly some frameworks don't_ +- **Smart:** biased by default \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/AdvancedArbitraries.md#biased-arbitraries)\] - _by default it generates both small and large values, making it easier to dig into counterexamples without having to tweak a size parameter manually_ +- **Debug:** verbose mode \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#opt-for-verbose-failures)\] - _easier troubleshooting with verbose mode enabled_ +- **Debug:** replay directly on the minimal counterexample \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#replay-after-failure)\] - _no need to replay the whole sequence, you get directly the counterexample_ +- **Debug:** custom examples in addition of generated ones \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#add-custom-examples-next-to-generated-ones)\] - _no need to duplicate the code to play the property on custom examples_ +- **Debug:** logger per predicate run \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#log-within-a-predicate)\] - _simplify your troubleshoot with fc.context and its logging feature_ +- **Unique:** model based approach \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#model-based-testing-or-ui-test)\]\[[article](https://medium.com/criteo-labs/detecting-the-unexpected-in-web-ui-fuzzing-1f3822c8a3a5)\] - _use the power of property based testing to test UI, APIs or state machines_ +- **Unique:** detect race conditions in your code \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#detect-race-conditions)\] - _shuffle the way your promises and async calls resolve using the power of property based testing to detect races_ +- **Unique:** simplify user definable corner cases \[[more](https://github.com/dubzzz/fast-check/blob/main/documentation/Tips.md#simplify-user-definable-corner-cases)\] - _simplify bug resolution by asking fast-check if it can find an even simpler corner case_ + +For more details, refer to the documentation in the links above. + +## Compatibility + +Here are the minimal requirements to use fast-check properly without any polyfills: + +| fast-check | node | ECMAScript version | _TypeScript (optional)_ | +| ---------- | ------------------- | ------------------ | ----------------------- | +| **2.x** | ≥8(1) | ES2017 | ≥3.2 | +| **1.x** | ≥0.12(1) | ES3 | ≥3.0 | + +(1) Except for features that cannot be polyfilled - such as `bigint`-related ones - all the capabilities of fast-check should be usable given you use at least the minimal recommended version of node associated to your major of fast-check. + +**ReScript bindings** + +Bindings to use fast-check in [ReScript](https://rescript-lang.org) are available in package [rescript-fast-check](https://www.npmjs.com/rescript-fast-check). They are maintained by [@TheSpyder](https://github.com/TheSpyder) as an external project. + +## Issues found by fast-check in famous packages + +fast-check has been able to find some unexpected behaviour among famous npm packages. Here are some of the errors detected using fast-check: + +**[jest](https://github.com/facebook/jest/)** + +**Issue detected:** `toStrictEqual` fails to distinguish 0 from 5e-324 \[[more](https://github.com/facebook/jest/issues/7941)\] + +**Code example:** `expect(0).toStrictEqual(5e-324)` succeeds + +**[js-yaml](https://github.com/nodeca/js-yaml/)** + +**Issue detected:** enabling `!!int: binary` style when dumping negative integers produces invalid content \[[more](https://github.com/nodeca/js-yaml/pull/398)\] + +**Code example:** `yaml.dump({toto: -10}, {styles:{'!!int':'binary'}})` produces `toto: 0b-1010` not `toto: -0b1010` + +**[query-string](https://github.com/sindresorhus/query-string)** + +**Issue detected:** enabling the `bracket` setting when exporting arrays containing null values produces an invalid output for the parser \[[more](https://github.com/sindresorhus/query-string/pull/138)\] + +**Code example:** + +```js +m.stringify({ bar: ['a', null, 'b'] }, { arrayFormat: 'bracket' }); //=> "bar[]=a&bar&bar[]=b" +m.parse('bar[]=a&bar&bar[]=b', { arrayFormat: 'bracket' }); //=> {bar: [null, 'b']} +``` + +**[MORE: Issues detected thanks to fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)** + +## Credits + +**Code Contributors** + +This project would not be the same without them 💖 - [Become one of them](CONTRIBUTING.md) + + + +**Backers** + +Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/fast-check/contribute)] and help us sustain our community. + + +**Sponsors** + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/fast-check#sponsor)] + + + + + + + + + + + diff --git a/api-extractor.json b/api-extractor.json index 7bd6b85d..6a0a4a2f 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} diff --git a/codemods/unify-signatures/README.md b/codemods/unify-signatures/README.md index 672b3bfe..246d9e06 100644 --- a/codemods/unify-signatures/README.md +++ b/codemods/unify-signatures/README.md @@ -1,4 +1,5 @@ # Codemod - Unify signatures across arbitraries + _A single way to customize arbitaries of fast-check_ --- @@ -6,14 +7,17 @@ _A single way to customize arbitaries of fast-check_ Before RFC [#992](https://github.com/dubzzz/fast-check/issues/992), there was no real unity between arbitraries regarding howto apply constraints on them. For arrays, we had signatures like: + - `fc.array(arb, maxLength)` - `fc.array(arb, minLength, maxLength)` While for objects but also web urls, signatures adding constraints onto the generated values were: + - `fc.object(constraints)` - `fc.webUrl(constraints)` The choice has been to favor contraints-based signatures because (more details on rfc): + 1. when seeing a call like `fc.array(arb, 10)` it was difficult to understand the meaning of the second argument: is is for the max? for the min? 2. on `fc.array` for instance: no signature to only specify a min length, specifying a min required the user to also specify a max 3. difficult to use this kind of signatures and overloads with `fc.set`, `fc.uint32array`... or even worst `fc.object` @@ -32,10 +36,11 @@ npx jscodeshift --parser=ts --extensions=ts -t https://raw.githubusercontent.com ``` You may need one of the following additional options: + - `--allowAmbiguity=true` - _enforce potentially invalid conversions, it may be used as a second step if first execution let some non-migrated calls like `fc.array(arb, myCustomMaxLength)`_ - `--debug=true` - _enable debug mode for the codemod_ - `--local=true` - _mostly when lauching the codemod against the codebase of fast-check, it considers that local imports are imports of fast-check_ --- -**Minimal version:** `>=2.4.0` \ No newline at end of file +**Minimal version:** `>=2.4.0` diff --git a/documentation/AdvancedArbitraries.md b/documentation/AdvancedArbitraries.md index f415a80a..07f36f22 100644 --- a/documentation/AdvancedArbitraries.md +++ b/documentation/AdvancedArbitraries.md @@ -1,180 +1,188 @@ -# [:house:](../README.md) Advanced arbitraries - -:warning: Before diving into the topic of *advanced arbitraries*, it is highly recommended to have in mind the [built-in arbitraries coming with fast-check](./Arbitraries.md). - -This documentation covers the definition of new arbitraries. It can be an arbitrary derived from some existing ones or a totally new one. - -Please do not hesitate to open issues to ask for new arbitraries. - -## Table of contents - -- [Derive existing arbitraries](#derive-existing-arbitraries) - - [Filter values](#filter-values) - - [Transform values](#transform-values) - - [Transform arbitraries](#transform-arbitraries) - - [Remove the shrinker](#remove-the-shrinker) -- [Build your own](#build-your-own) - - [Starting at version 2.15.0](#starting-at-version-2150) - - [Before version 2.15.0](#before-version-2150) -- [Advanced features of arbitraries](#advanced-features-of-arbitraries) - - [Biased arbitraries](#biased-arbitraries) - - [Shrinking](#shrinking) - - [Cloneable](#cloneable) - -## Derive existing arbitraries - -All generated arbitraries inherit from the same base class: [Arbitrary](https://github.com/dubzzz/fast-check/blob/main/src/check/arbitrary/definition/Arbitrary.ts). - -It comes with two useful methods: `filter(predicate: (t: T) => boolean): Arbitrary` and `map(mapper: (t: T) => U): Arbitrary`. These methods are used internally by the framework to derive some Arbitraries from existing ones. - -Additionaly it comes with `noShrink()` which derives an existing `Arbitrary` into the same `Arbitrary` without the shrink option. - -### Filter values - -`filter(predicate: (t: T) => boolean): Arbitrary` can be used to filter undesirable values from the generated ones. It can be used as some kind of pre-requisite for the parameters required for your algorithm. For instance, you might need to generate two ordered integer values. One approach can be to use filter as follow: - -```typescript -const minMax = fc.tuple(fc.integer(), fc.integer()) - .filter(t => t[0] < t[1]); -``` - -But be aware that using `filter` may highly impact the time required to generate a valid entry. In the previous example, half of the generated tuples will be rejected. It can nontheless be a very useful and powerful tool to derive your arbitraries quickly and easily. - -### Transform values - -`map(mapper: (t: T) => U): Arbitrary` in its side does not filter any of the generated entries. It take one entry (generated or shrinked) and map it to another. - -For instance the previous example could have been refactored as follow: - -```typescript -const minMax = fc.tuple(fc.integer(), fc.integer()) - .map(t => t[0] < t[1] ? [t[0], t[1]] : [t[1], t[0]]); -``` - - -Another example would be to derive `fc.integer()` and `fc.array()` to build `fc.char()` and `fc.string()`: - -```typescript -const char = () => fc.integer(0x20, 0x7e).map(String.fromCharCode); -const string = () => fc.array(fc.char()).map(arr => arr.join('')); -``` - -Most of the [built-in arbitraries](https://github.com/dubzzz/fast-check/tree/main/src/check/arbitrary) use this trick to define themselves. - -### Transform arbitraries - -`chain(fmapper: (t: T) => Arbitrary): Arbitrary` (aka flatMap) It takes one entry from an Arbitrary and uses it to create a new Arbitrary based on that value. - -:warning: Be aware that the shrinker of such construct might not be able to shrink as much as possible (more details on [this](https://github.com/dubzzz/fast-check/issues/650#issuecomment-648397230)) - -For example you can create arbitraries based on generated values: - -```typescript -// generate an array of strings, all having the same length. -const RandomFixedLengthStringArb: Arbitrary = - fc.nat(100) - .chain(length => fc.array(fc.string(length, length))); - -// generate an array of 2-element arrays containing integer pairs -const BoundedPairsArb: Arbitrary<[number, number][]> = fc.nat().chain(bound => fc.array(fc.integer().map((leftBound: number): [number, number] => [leftBound, leftBound+bound]))); - -// generate a random sized substring of a string -const StringAndSubstringArb: Arbitrary<[string, string]> = fc.string(3,100).chain(fulltext => fc.tuple(fc.nat(fulltext.length-1), fc.nat(fulltext.length-1)).map(indexes => [fulltext, fulltext.slice(indexes[0], indexes[1]) ])) -``` - -### Remove the shrinker - -Calling `noShrink()` on an `Arbitrary` just remove the shrinker of the `Arbitrary`. For instance, the following code will produce an `Arbitrary` without shrinking operation. - -```js -const intNoShrink = fc.integer().noShrink(); -``` - -## Build your own - -In general, whatever the version of fast-check you are using, it is highly recommended to have a look to how [built-in arbitraries](https://github.com/dubzzz/fast-check/tree/main/src/arbitrary) have been implementated and to the simpler [examples](https://github.com/dubzzz/fast-check/tree/main/example) provided in the repository. - -### Starting at version 2.15.0 - -**Your version is 2.15.0 or above** - -In such case, even if extending the [class `Arbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/Arbitrary.ts#L13) still works fine, it is highly recommended to extend the [class `NextArbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/NextArbitrary.ts#L12). - -An instance of `NextArbitrary` must define three methods: -- `generate(mrng: Random, biasFactor: number | undefined): NextValue`: Given a random generator and possibly a bias (≥2), it must generate a single value along with its context (if applicable). The context is an opaque value that should only be accessed by the class that produced it. This opaque value can be helpful to guide the shrinker and give it more context on the value, how it has been produced... -- `shrink(value: T, context: unknown | undefined): Stream>`: Given a value and possibly a context (produced by `generate` or `shrink` of the very same instance), it has to produce a Stream of smaller values. Please note that the function always has to be called with a context except if `canShrinkWithoutContext` tells the caller that it can be called context-less for this precise value. -- `canShrinkWithoutContext(value: unknown): value is T`: Given a value it can tells the caller whether or not `shrink` can be called on it without passing a context. If the returned value is `false` then it means that this value should not be passed to `shrink` without its context. - -But version 2.x of fast-check does not deal with instances of `NextArbitrary` from an API point-of-view so you need to convert them towards old instances using the helper `convertFromNext`. You can also convert old instances to new ones uisng `convertToNext`. - -Since 2.15.0, most of the built-ins arbitraries coming with fast-check are based `NextArbitrary` hidden by a `convertFromNext`. - -### Before version 2.15.0 - -**Your version is strictly older than 2.15.0** - -In such case, you have to extend the [class `Arbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/Arbitrary.ts#L13). - -It consists in a single method: `generate(mrng: Random): Shrinkable`. -It takes a random number generator and it generates a value and the whole shrinking process to shrink it. - -## Advanced features of arbitraries - -### Biased arbitraries - -Property based testing framework must be able to discover any kind of issues even very rare ones happening on some small values. For instance your algorithm might use magic numbers such as `-1`, `0` or others. Or fail when the input has duplicated values... - -A common way to deal with those issues is: -- Solution A: only generate small values - *Issue: it fails to build large ones* -- Solution B: generate larger and larger entries - *Issue: what if the failing case requires both large and small values* - -The choice made by fast-check is to bias the arbitrary 1 time over `freq`. - -For `fc.integer`: -- 1 over `freq`: arbitrary between smaller values -- remaining: the full range arbitrary - -For `fc.array`: -- 1 over `freq`: - - 1 over `freq`: small array with biased values - - remaining: full range array with biased values -- remaining: the full range arbitrary - -### Shrinking - -A basic way to implement a property based testing framework is to define arbitraries with the following structure: - -```typescript -interface DummyArbitrary { - generate(mrng: Random): Ts; - shrink(prev: Ts): Stream; -} -``` - -Some frameworks actually use this approach. Unfortunately using this approach makes it impossible to shrink `oneof`. Indeed as soon as you have generated your value you do not know anymore who produced you. - -Let's imagine you are using a `oneof(integer(0, 10), integer(20, 30))` relying on the `DummyArbitrary` above. As soon as you have generated a value - a `number` - you cannot call shrink anymore as you do not know if it has been produced by `integer(0, 10)` or `integer(20, 30)` - in this precise case you can easily infer the producer. - -For this reason, the `shrink` method is not part of `Arbitrary` in fast-check but is part of the values instantiated by `generate`. - -### Cloneable - -Any generated value having a key for `fc.cloneMethod` would be handled a bit differently during the execution. Indeed those values explicitly requires to be cloned before being transmitted again to the predicate. - -Cloneable values can be seen as stateful values that would be altered as soon as we use them inside the predicate. For this precise reason they have to be recreated if they need to be used inside other runs of the predicate. - -Example of usages: -- `fc.context`: is a stateful instance that gathers all the logs for a given predicate execution. In order to provide only the logs linked to the run itself it has to be cloned between all the runs -- stream structure - -Example of a stream arbitrary: - -```typescript -const streamInt = fc.nat() - .map(seed => { - return Object.assign( - new SeededRandomStream(seed), - { [fc.cloneMethod]: () => new SeededRandomStream(seed) } - ); - }); -``` +# [:house:](../README.md) Advanced arbitraries + +:warning: Before diving into the topic of _advanced arbitraries_, it is highly recommended to have in mind the [built-in arbitraries coming with fast-check](./Arbitraries.md). + +This documentation covers the definition of new arbitraries. It can be an arbitrary derived from some existing ones or a totally new one. + +Please do not hesitate to open issues to ask for new arbitraries. + +## Table of contents + +- [Derive existing arbitraries](#derive-existing-arbitraries) + - [Filter values](#filter-values) + - [Transform values](#transform-values) + - [Transform arbitraries](#transform-arbitraries) + - [Remove the shrinker](#remove-the-shrinker) +- [Build your own](#build-your-own) + - [Starting at version 2.15.0](#starting-at-version-2150) + - [Before version 2.15.0](#before-version-2150) +- [Advanced features of arbitraries](#advanced-features-of-arbitraries) + - [Biased arbitraries](#biased-arbitraries) + - [Shrinking](#shrinking) + - [Cloneable](#cloneable) + +## Derive existing arbitraries + +All generated arbitraries inherit from the same base class: [Arbitrary](https://github.com/dubzzz/fast-check/blob/main/src/check/arbitrary/definition/Arbitrary.ts). + +It comes with two useful methods: `filter(predicate: (t: T) => boolean): Arbitrary` and `map(mapper: (t: T) => U): Arbitrary`. These methods are used internally by the framework to derive some Arbitraries from existing ones. + +Additionaly it comes with `noShrink()` which derives an existing `Arbitrary` into the same `Arbitrary` without the shrink option. + +### Filter values + +`filter(predicate: (t: T) => boolean): Arbitrary` can be used to filter undesirable values from the generated ones. It can be used as some kind of pre-requisite for the parameters required for your algorithm. For instance, you might need to generate two ordered integer values. One approach can be to use filter as follow: + +```typescript +const minMax = fc.tuple(fc.integer(), fc.integer()).filter((t) => t[0] < t[1]); +``` + +But be aware that using `filter` may highly impact the time required to generate a valid entry. In the previous example, half of the generated tuples will be rejected. It can nontheless be a very useful and powerful tool to derive your arbitraries quickly and easily. + +### Transform values + +`map(mapper: (t: T) => U): Arbitrary` in its side does not filter any of the generated entries. It take one entry (generated or shrinked) and map it to another. + +For instance the previous example could have been refactored as follow: + +```typescript +const minMax = fc.tuple(fc.integer(), fc.integer()).map((t) => (t[0] < t[1] ? [t[0], t[1]] : [t[1], t[0]])); +``` + +Another example would be to derive `fc.integer()` and `fc.array()` to build `fc.char()` and `fc.string()`: + +```typescript +const char = () => fc.integer(0x20, 0x7e).map(String.fromCharCode); +const string = () => fc.array(fc.char()).map((arr) => arr.join('')); +``` + +Most of the [built-in arbitraries](https://github.com/dubzzz/fast-check/tree/main/src/check/arbitrary) use this trick to define themselves. + +### Transform arbitraries + +`chain(fmapper: (t: T) => Arbitrary): Arbitrary` (aka flatMap) It takes one entry from an Arbitrary and uses it to create a new Arbitrary based on that value. + +:warning: Be aware that the shrinker of such construct might not be able to shrink as much as possible (more details on [this](https://github.com/dubzzz/fast-check/issues/650#issuecomment-648397230)) + +For example you can create arbitraries based on generated values: + +```typescript +// generate an array of strings, all having the same length. +const RandomFixedLengthStringArb: Arbitrary = fc + .nat(100) + .chain((length) => fc.array(fc.string(length, length))); + +// generate an array of 2-element arrays containing integer pairs +const BoundedPairsArb: Arbitrary<[number, number][]> = fc + .nat() + .chain((bound) => + fc.array(fc.integer().map((leftBound: number): [number, number] => [leftBound, leftBound + bound])) + ); + +// generate a random sized substring of a string +const StringAndSubstringArb: Arbitrary<[string, string]> = fc + .string(3, 100) + .chain((fulltext) => + fc + .tuple(fc.nat(fulltext.length - 1), fc.nat(fulltext.length - 1)) + .map((indexes) => [fulltext, fulltext.slice(indexes[0], indexes[1])]) + ); +``` + +### Remove the shrinker + +Calling `noShrink()` on an `Arbitrary` just remove the shrinker of the `Arbitrary`. For instance, the following code will produce an `Arbitrary` without shrinking operation. + +```js +const intNoShrink = fc.integer().noShrink(); +``` + +## Build your own + +In general, whatever the version of fast-check you are using, it is highly recommended to have a look to how [built-in arbitraries](https://github.com/dubzzz/fast-check/tree/main/src/arbitrary) have been implementated and to the simpler [examples](https://github.com/dubzzz/fast-check/tree/main/example) provided in the repository. + +### Starting at version 2.15.0 + +**Your version is 2.15.0 or above** + +In such case, even if extending the [class `Arbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/Arbitrary.ts#L13) still works fine, it is highly recommended to extend the [class `NextArbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/NextArbitrary.ts#L12). + +An instance of `NextArbitrary` must define three methods: + +- `generate(mrng: Random, biasFactor: number | undefined): NextValue`: Given a random generator and possibly a bias (≥2), it must generate a single value along with its context (if applicable). The context is an opaque value that should only be accessed by the class that produced it. This opaque value can be helpful to guide the shrinker and give it more context on the value, how it has been produced... +- `shrink(value: T, context: unknown | undefined): Stream>`: Given a value and possibly a context (produced by `generate` or `shrink` of the very same instance), it has to produce a Stream of smaller values. Please note that the function always has to be called with a context except if `canShrinkWithoutContext` tells the caller that it can be called context-less for this precise value. +- `canShrinkWithoutContext(value: unknown): value is T`: Given a value it can tells the caller whether or not `shrink` can be called on it without passing a context. If the returned value is `false` then it means that this value should not be passed to `shrink` without its context. + +But version 2.x of fast-check does not deal with instances of `NextArbitrary` from an API point-of-view so you need to convert them towards old instances using the helper `convertFromNext`. You can also convert old instances to new ones uisng `convertToNext`. + +Since 2.15.0, most of the built-ins arbitraries coming with fast-check are based `NextArbitrary` hidden by a `convertFromNext`. + +### Before version 2.15.0 + +**Your version is strictly older than 2.15.0** + +In such case, you have to extend the [class `Arbitrary`](https://github.com/dubzzz/fast-check/blob/c96b3f49317fa588fca852b5671827fdb2fe8d11/src/check/arbitrary/definition/Arbitrary.ts#L13). + +It consists in a single method: `generate(mrng: Random): Shrinkable`. +It takes a random number generator and it generates a value and the whole shrinking process to shrink it. + +## Advanced features of arbitraries + +### Biased arbitraries + +Property based testing framework must be able to discover any kind of issues even very rare ones happening on some small values. For instance your algorithm might use magic numbers such as `-1`, `0` or others. Or fail when the input has duplicated values... + +A common way to deal with those issues is: + +- Solution A: only generate small values - _Issue: it fails to build large ones_ +- Solution B: generate larger and larger entries - _Issue: what if the failing case requires both large and small values_ + +The choice made by fast-check is to bias the arbitrary 1 time over `freq`. + +For `fc.integer`: + +- 1 over `freq`: arbitrary between smaller values +- remaining: the full range arbitrary + +For `fc.array`: + +- 1 over `freq`: + - 1 over `freq`: small array with biased values + - remaining: full range array with biased values +- remaining: the full range arbitrary + +### Shrinking + +A basic way to implement a property based testing framework is to define arbitraries with the following structure: + +```typescript +interface DummyArbitrary { + generate(mrng: Random): Ts; + shrink(prev: Ts): Stream; +} +``` + +Some frameworks actually use this approach. Unfortunately using this approach makes it impossible to shrink `oneof`. Indeed as soon as you have generated your value you do not know anymore who produced you. + +Let's imagine you are using a `oneof(integer(0, 10), integer(20, 30))` relying on the `DummyArbitrary` above. As soon as you have generated a value - a `number` - you cannot call shrink anymore as you do not know if it has been produced by `integer(0, 10)` or `integer(20, 30)` - in this precise case you can easily infer the producer. + +For this reason, the `shrink` method is not part of `Arbitrary` in fast-check but is part of the values instantiated by `generate`. + +### Cloneable + +Any generated value having a key for `fc.cloneMethod` would be handled a bit differently during the execution. Indeed those values explicitly requires to be cloned before being transmitted again to the predicate. + +Cloneable values can be seen as stateful values that would be altered as soon as we use them inside the predicate. For this precise reason they have to be recreated if they need to be used inside other runs of the predicate. + +Example of usages: + +- `fc.context`: is a stateful instance that gathers all the logs for a given predicate execution. In order to provide only the logs linked to the run itself it has to be cloned between all the runs +- stream structure + +Example of a stream arbitrary: + +```typescript +const streamInt = fc.nat().map((seed) => { + return Object.assign(new SeededRandomStream(seed), { [fc.cloneMethod]: () => new SeededRandomStream(seed) }); +}); +``` diff --git a/documentation/HandsOnPropertyBased.md b/documentation/HandsOnPropertyBased.md index 086ad470..3e9ee1a3 100644 --- a/documentation/HandsOnPropertyBased.md +++ b/documentation/HandsOnPropertyBased.md @@ -1,159 +1,161 @@ -# [:house:](../README.md) Hands on property based - -Or go to the [JavaScript version](./HandsOnPropertyBasedJs.md) of the Hands on. - -## What is property based testing? - -Property based testing has become quite famous in functional world. Mainly introduced by QuickCheck framework in Haskell, it suggests another way to test software. It targets all the scope covered by example based testing: from unit tests to integration tests. - -It checks that a function, program or whatever system under test abides by a property. Property can be seen as a trait you expect to see in your output given the inputs. It does not have to be the expected result itself and most of the time it will not be. - -A property is just something like: - -> for all (x, y, ...) -> -> such that precondition(x, y, ...) holds -> -> predicate(x, y, ...) is true - -For example, using properties you might state that: - -> for any strings `a`, `b` and `c` -> -> `b` is a substring of `a + b + c` - -Property based testing frameworks will take this spell as an input and run the check on multiple generated random entries. In case of failure, it should provide both a counterexample and the seed causing the generation. - -They have the interesting property that the suggested counterexample is the minimal failing counterexample. - -For instance: if whenever the string `a` contains `.` in it, the check above fails, then the counterexample would be `{a: '.', b: '', c: ''}` and not `{a: 'dfsdkf:!jk.fs', b: 'azda;', c: 'yyy§g'}`. - -## Setting up a sample project - -> Just wanting to see the result without installing any packages on your machine: try it online on our pre-configured [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). - -Initialize a new node project: - -```bash -mkdir sample-fast-check -cd sample-fast-check -npm init --yes -npm install typescript ts-node -echo "{}" > tsconfig.json -``` - -Create a `src` folder and put the file `sort.ts` into it: - -```typescript -const sortInternal = (tab: T[], start: number, end: number, cmp: (a: T, b: T) => boolean): T[] => { - if (end - start < 2) return tab; - - let pivot = start; - for (let idx = start + 1; idx < end; ++idx) { - if (!cmp(tab[start], tab[idx])) { - let prev = tab[++pivot]; - tab[pivot] = tab[idx]; - tab[idx] = prev; - } - } - let prev = tab[pivot]; - tab[pivot] = tab[start]; - tab[start] = prev; - - sortInternal(tab, start, pivot, cmp); - sortInternal(tab, pivot + 1, end, cmp); - return tab; -}; - -export const sort = (tab: T[]): T[] => { - return sortInternal([...tab], 0, tab.length, (a, b) => a < b); -}; -``` - -Install a test framework: - -```bash -npm install --save-dev jest ts-jest @types/jest -mkdir specs ; touch specs/sort.spec.ts -``` - -Edit `package.json` to configure the test framework: - -```json -// -- -"scripts": { - "test": "jest" -}, -// -- -"jest": { - "moduleFileExtensions": ["ts", "tsx", "js"], - "globals": {"ts-jest": {"tsconfig": "tsconfig.json"}}, - "transform": {"^.+\\.(ts|tsx)$": "ts-jest"}, - "testMatch": ["**/specs/*.+(ts|tsx|js)"] -}, -// -- -``` - -## Hands on fast-check - -Install fast-check: - -```bash -npm install --save-dev fast-check -``` - -The algorithm under test is an integer sorting algorithm. Basically here are some of the properties we might come with: -- for any array of integers `data`: `data` and sort(`data`) should contain the same items (same number of each too) -- for any array of integers `data`: two consecutive items of sort(`data`) should be ordered - -We can translate them with fast-check syntax: - -```typescript -import * as fc from 'fast-check'; -import { sort } from '../src/sort'; - -test('should contain the same items', () => { - const count = (tab, element) => tab.filter(v => v === element).length; - fc.assert( - fc.property(fc.array(fc.integer()), data => { - const sorted = sort(data); - expect(sorted.length).toEqual(data.length); - for (const item of data) { - expect(count(sorted, item)).toEqual(count(data, item)); - } - }) - ); -}); - -test('should produce ordered array', () => { - fc.assert( - fc.property(fc.array(fc.integer()), data => { - const sorted = sort(data); - for (let idx = 1; idx < sorted.length; ++idx) { - expect(sorted[idx - 1]).toBeLessThanOrEqual(sorted[idx]); - } - }) - ); -}); -``` - -Copy and paste the code above into `specs/sort.spec.ts` and run `npm run test`. - -🎉 Congrats! 🎉 You have successfully implemented your first test using fast-check. - ---- - -If you want to experiment shrinking you might change the `sort` implementation as follow: - -```diff ---- if (!cmp(tab[start], tab[idx])) { -+++ if (cmp(tab[start], tab[idx])) { -``` - -Framework should find a counterexample for the second property. - -Then you can play with settings of `fc.assert` like: -- `{ verbose: true }`: show all the counterexamples encountered along the shrinking path -- `{ seed: }`: replay the exact same set of tests -- `{ seed: , path: }`: start directly at the entry corresponding to the given `seed` and `path` -- `{ seed: , path: , endOnFailure: true }`: start directly at the entry corresponding to the given `seed`, `path` and stop at the first failure without shrinking +# [:house:](../README.md) Hands on property based + +Or go to the [JavaScript version](./HandsOnPropertyBasedJs.md) of the Hands on. + +## What is property based testing? + +Property based testing has become quite famous in functional world. Mainly introduced by QuickCheck framework in Haskell, it suggests another way to test software. It targets all the scope covered by example based testing: from unit tests to integration tests. + +It checks that a function, program or whatever system under test abides by a property. Property can be seen as a trait you expect to see in your output given the inputs. It does not have to be the expected result itself and most of the time it will not be. + +A property is just something like: + +> for all (x, y, ...) +> +> such that precondition(x, y, ...) holds +> +> predicate(x, y, ...) is true + +For example, using properties you might state that: + +> for any strings `a`, `b` and `c` +> +> `b` is a substring of `a + b + c` + +Property based testing frameworks will take this spell as an input and run the check on multiple generated random entries. In case of failure, it should provide both a counterexample and the seed causing the generation. + +They have the interesting property that the suggested counterexample is the minimal failing counterexample. + +For instance: if whenever the string `a` contains `.` in it, the check above fails, then the counterexample would be `{a: '.', b: '', c: ''}` and not `{a: 'dfsdkf:!jk.fs', b: 'azda;', c: 'yyy§g'}`. + +## Setting up a sample project + +> Just wanting to see the result without installing any packages on your machine: try it online on our pre-configured [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). + +Initialize a new node project: + +```bash +mkdir sample-fast-check +cd sample-fast-check +npm init --yes +npm install typescript ts-node +echo "{}" > tsconfig.json +``` + +Create a `src` folder and put the file `sort.ts` into it: + +```typescript +const sortInternal = (tab: T[], start: number, end: number, cmp: (a: T, b: T) => boolean): T[] => { + if (end - start < 2) return tab; + + let pivot = start; + for (let idx = start + 1; idx < end; ++idx) { + if (!cmp(tab[start], tab[idx])) { + let prev = tab[++pivot]; + tab[pivot] = tab[idx]; + tab[idx] = prev; + } + } + let prev = tab[pivot]; + tab[pivot] = tab[start]; + tab[start] = prev; + + sortInternal(tab, start, pivot, cmp); + sortInternal(tab, pivot + 1, end, cmp); + return tab; +}; + +export const sort = (tab: T[]): T[] => { + return sortInternal([...tab], 0, tab.length, (a, b) => a < b); +}; +``` + +Install a test framework: + +```bash +npm install --save-dev jest ts-jest @types/jest +mkdir specs ; touch specs/sort.spec.ts +``` + +Edit `package.json` to configure the test framework: + +```json +// -- +"scripts": { + "test": "jest" +}, +// -- +"jest": { + "moduleFileExtensions": ["ts", "tsx", "js"], + "globals": {"ts-jest": {"tsconfig": "tsconfig.json"}}, + "transform": {"^.+\\.(ts|tsx)$": "ts-jest"}, + "testMatch": ["**/specs/*.+(ts|tsx|js)"] +}, +// -- +``` + +## Hands on fast-check + +Install fast-check: + +```bash +npm install --save-dev fast-check +``` + +The algorithm under test is an integer sorting algorithm. Basically here are some of the properties we might come with: + +- for any array of integers `data`: `data` and sort(`data`) should contain the same items (same number of each too) +- for any array of integers `data`: two consecutive items of sort(`data`) should be ordered + +We can translate them with fast-check syntax: + +```typescript +import * as fc from 'fast-check'; +import { sort } from '../src/sort'; + +test('should contain the same items', () => { + const count = (tab, element) => tab.filter((v) => v === element).length; + fc.assert( + fc.property(fc.array(fc.integer()), (data) => { + const sorted = sort(data); + expect(sorted.length).toEqual(data.length); + for (const item of data) { + expect(count(sorted, item)).toEqual(count(data, item)); + } + }) + ); +}); + +test('should produce ordered array', () => { + fc.assert( + fc.property(fc.array(fc.integer()), (data) => { + const sorted = sort(data); + for (let idx = 1; idx < sorted.length; ++idx) { + expect(sorted[idx - 1]).toBeLessThanOrEqual(sorted[idx]); + } + }) + ); +}); +``` + +Copy and paste the code above into `specs/sort.spec.ts` and run `npm run test`. + +🎉 Congrats! 🎉 You have successfully implemented your first test using fast-check. + +--- + +If you want to experiment shrinking you might change the `sort` implementation as follow: + +```diff +--- if (!cmp(tab[start], tab[idx])) { ++++ if (cmp(tab[start], tab[idx])) { +``` + +Framework should find a counterexample for the second property. + +Then you can play with settings of `fc.assert` like: + +- `{ verbose: true }`: show all the counterexamples encountered along the shrinking path +- `{ seed: }`: replay the exact same set of tests +- `{ seed: , path: }`: start directly at the entry corresponding to the given `seed` and `path` +- `{ seed: , path: , endOnFailure: true }`: start directly at the entry corresponding to the given `seed`, `path` and stop at the first failure without shrinking diff --git a/documentation/HandsOnPropertyBasedJs.md b/documentation/HandsOnPropertyBasedJs.md index 546352f4..9eebaf9a 100644 --- a/documentation/HandsOnPropertyBasedJs.md +++ b/documentation/HandsOnPropertyBasedJs.md @@ -95,6 +95,7 @@ npm install --save-dev fast-check ``` The algorithm under test is an integer sorting algorithm. Basically here are some of the properties we might come with: + - for any array of integers `data`: `data` and sort(`data`) should contain the same items (same number of each too) - for any array of integers `data`: two consecutive items of sort(`data`) should be ordered @@ -105,9 +106,9 @@ const fc = require('fast-check'); const { sort } = require('../src/sort'); test('should contain the same items', () => { - const count = (tab, element) => tab.filter(v => v === element).length; + const count = (tab, element) => tab.filter((v) => v === element).length; fc.assert( - fc.property(fc.array(fc.integer()), data => { + fc.property(fc.array(fc.integer()), (data) => { const sorted = sort(data); expect(sorted.length).toEqual(data.length); for (const item of data) { @@ -119,7 +120,7 @@ test('should contain the same items', () => { test('should produce ordered array', () => { fc.assert( - fc.property(fc.array(fc.integer()), data => { + fc.property(fc.array(fc.integer()), (data) => { const sorted = sort(data); for (let idx = 1; idx < sorted.length; ++idx) { expect(sorted[idx - 1]).toBeLessThanOrEqual(sorted[idx]); @@ -145,6 +146,7 @@ If you want to experiment shrinking you might change the `sort` implementation a Framework should find a counterexample for the second property. Then you can play with settings of `fc.assert` like: + - `{ verbose: true }`: show all the counterexamples encountered along the shrinking path - `{ seed: }`: replay the exact same set of tests - `{ seed: , path: }`: start directly at the entry corresponding to the given `seed` and `path` diff --git a/documentation/HowItWorks.md b/documentation/HowItWorks.md index d06f7100..56fb679b 100644 --- a/documentation/HowItWorks.md +++ b/documentation/HowItWorks.md @@ -25,22 +25,22 @@ You can see generators as follow: ```ts type Generator = { - generate(mrng: Random): T; -} + generate(mrng: Random): T; +}; ``` In the signature above `mrng` is a mutable random generator. It is a simple wrapper around `pure-rand` that provides a usable random instance. The class `Random` can be implemented as follow: ```js class Random { - constructor(rng) { - this.rng = rng; - } - next(min, max) { - const g = prand.uniformIntDistribution(min, max, this.rng); - this.rng = g[1]; - return g[0]; - } + constructor(rng) { + this.rng = rng; + } + next(min, max) { + const g = prand.uniformIntDistribution(min, max, this.rng); + this.rng = g[1]; + return g[0]; + } } // Can be used as follow: @@ -59,12 +59,12 @@ Let's build our first generator: the one responsible to build random integers. // const miniFc = {} miniFc.integer = (min, max) => { - return { - generate(mrng) { - return mrng.next(min, max); - } - }; -} + return { + generate(mrng) { + return mrng.next(min, max); + }, + }; +}; // It can be used as follow: // > miniFc.integer(0, 50).generate(mrng) ``` @@ -88,54 +88,48 @@ It can be implemented as follow: ```js function map(g, mapper) { - return { - generate(mrng) { - const value = g.generate(mrng); - return mapper(value); - } - }; + return { + generate(mrng) { + const value = g.generate(mrng); + return mapper(value); + }, + }; } ``` Now that we have `map` we can implement some of our missing generators: ```js -miniFc.boolean = () => map( - miniFc.integer(0, 1), - Boolean -) +miniFc.boolean = () => map(miniFc.integer(0, 1), Boolean); -miniFc.character = () => map( - miniFc.integer(0, 25), - n => String.fromCharCode(97 + n) -) +miniFc.character = () => map(miniFc.integer(0, 25), (n) => String.fromCharCode(97 + n)); ``` In order to build others, we first need to implement a generator for tuples and a generator for arrays: ```js miniFc.tuple = (...itemGenerators) => { - return { - generate(mrng) { - return itemGenerators.map(g => g.generate(mrng)); - } - }; -} + return { + generate(mrng) { + return itemGenerators.map((g) => g.generate(mrng)); + }, + }; +}; // It can be used as follow: // > miniFc.tuple(miniFc.integer(0, 50), miniFc.boolean()).generate(mrng) miniFc.array = (itemGenerator) => { - return { - generate(mrng) { - const size = mrng.next(0, 10); - const content = []; - for (let index = 0 ; index !== size ; ++index) { - content.push(itemGenerator.generate(mrng)); - } - return content; - } - }; -} + return { + generate(mrng) { + const size = mrng.next(0, 10); + const content = []; + for (let index = 0; index !== size; ++index) { + content.push(itemGenerator.generate(mrng)); + } + return content; + }, + }; +}; // It can be used as follow: // > miniFc.array(miniFc.character()).generate(mrng) ``` @@ -143,20 +137,10 @@ miniFc.array = (itemGenerator) => { Now we can build our last generators: ```js -miniFc.string = () => map( - miniFc.array(miniFc.character()), - characters => characters.join('') -) - -miniFc.dictionary = (valueGenerator) => map( - miniFc.array( - miniFc.tuple( - miniFc.string(), - valueGenerator - ) - ), - Object.fromEntries -) +miniFc.string = () => map(miniFc.array(miniFc.character()), (characters) => characters.join('')); + +miniFc.dictionary = (valueGenerator) => + map(miniFc.array(miniFc.tuple(miniFc.string(), valueGenerator)), Object.fromEntries); ``` ## Runner @@ -169,39 +153,38 @@ But first let define what is a property. We will define it as a super generator ```ts type Property = { - generate(mrng: Random): T; - run(valueUnderTest: T): boolean; -} + generate(mrng: Random): T; + run(valueUnderTest: T): boolean; +}; ``` Properties will be created using the following helper: ```js miniFc.property = (generator, predicate) => { - return { - generate(mrng) { - return generator.generate(mrng); - }, - run(valueUnderTest) { - return predicate(valueUnderTest); - } - } -} + return { + generate(mrng) { + return generator.generate(mrng); + }, + run(valueUnderTest) { + return predicate(valueUnderTest); + }, + }; +}; ``` Now, let's consider a simple example to understand how we want to use our minimal version of fast-check. The code under test will be an implementation of `isSubstring` with obviously a bug into it in order to check that our framework can find it. As a user we would like to be able to write: ```js const isSubstring = (pattern, text) => { - return text.indexOf(pattern) > 0; -} + return text.indexOf(pattern) > 0; +}; miniFc.assert( - miniFc.property( - miniFc.tuple(miniFc.string(), miniFc.string(), miniFc.string()), - ([a, b, c]) => isSubstring(b, a + b + c) - ) -) + miniFc.property(miniFc.tuple(miniFc.string(), miniFc.string(), miniFc.string()), ([a, b, c]) => + isSubstring(b, a + b + c) + ) +); ``` In terms of typings we have the following signature to fulfill: @@ -215,36 +198,39 @@ By default, in most of the frameworks, runners run the property a hundred times A basic implementation for the runner can be written as follow: ```js -miniFc.assert = property => { - for (let runId = 0 ; runId !== 100 ; ++runId) { - const seed = runId; - const mrng = new Random(prand.xoroshiro128plus(seed)); - const valueUnderTest = property.generate(mrng); - if (!property.run(valueUnderTest)) { - throw new Error(`Property failed after ${runId + 1} runs with value ${JSON.stringify(valueUnderTest)}`); - } +miniFc.assert = (property) => { + for (let runId = 0; runId !== 100; ++runId) { + const seed = runId; + const mrng = new Random(prand.xoroshiro128plus(seed)); + const valueUnderTest = property.generate(mrng); + if (!property.run(valueUnderTest)) { + throw new Error(`Property failed after ${runId + 1} runs with value ${JSON.stringify(valueUnderTest)}`); } -} + } +}; ``` Additionally in property based testing, seed is supposed not to be fixed except if specified on call-site. Implementation above can be updated as follow: ```js miniFc.assert = (property, { seed = Date.now() } = {}) => { - let rng = prand.xoroshiro128plus(seed); - for (let runId = 0 ; runId !== 100 ; ++runId) { - const valueUnderTest = property.generate(new Random(rng)); - if (!property.run(valueUnderTest)) { - throw new Error(`Property failed after ${runId + 1} runs with value ${JSON.stringify(valueUnderTest)} (seed: ${seed})`); - } - rng = rng.jump(); + let rng = prand.xoroshiro128plus(seed); + for (let runId = 0; runId !== 100; ++runId) { + const valueUnderTest = property.generate(new Random(rng)); + if (!property.run(valueUnderTest)) { + throw new Error( + `Property failed after ${runId + 1} runs with value ${JSON.stringify(valueUnderTest)} (seed: ${seed})` + ); } -} + rng = rng.jump(); + } +}; ``` In previous section, we did not cover the reason why we opted for pure random generators. In property based we want properties to be reproducible no matter the seed, no matter the hardware and no matter the time... But we also want to have independent runs for each iteration in the loop. For instance, in the implementation defined above, we call generate with the following instances of `Random`: + - `runId = 0` - Call with `new Random(prand.xoroshiro128plus(seed))` - `runId = 1` - Call with `new Random(prand.xoroshiro128plus(seed)).jump()` - `runId = 2` - Call with `new Random(prand.xoroshiro128plus(seed)).jump().jump()` @@ -276,35 +262,36 @@ First we need to adapt the `Generator` type. ```ts type Generator = { - generate(mrng: Random): T; - shrink(value: T): IterableIterator; -} + generate(mrng: Random): T; + shrink(value: T): IterableIterator; +}; ``` As you can see in the snippet above, we added a method called `shrink` onto our `Generator` type. This method takes a value - _that have been generated by this `Generator`_ and builds a stream of potential shrinks for this value. In other words, if I take back our generator of integers, I'd expect the following: + ```js const arb = miniFc.integer(0, 100); -abr.shrink(64) // potential output: 32, 16, 8, 4, 2, 1, 0 +abr.shrink(64); // potential output: 32, 16, 8, 4, 2, 1, 0 ``` For integers, the implementation will use a classical technique used in programming: dichotomy. Given the technique we want to use we can adapt the code for `integers` as follow: ```js miniFc.integer = (min, max) => { - return { - generate(mrng) { - return mrng.next(min, max); - }, - *shrink(value) { - while (value !== min) { - value = min + Math.floor((value - min) / 2); - yield value; - } - } - } -} + return { + generate(mrng) { + return mrng.next(min, max); + }, + *shrink(value) { + while (value !== min) { + value = min + Math.floor((value - min) / 2); + yield value; + } + }, + }; +}; // You can check the output by calling: // > [...miniFc.integer(0, 100).shrink(64)] // > [...miniFc.integer(0, 100).shrink(48)] @@ -320,26 +307,27 @@ While it does not cover all the possible smaller combinations it should be good ```js miniFc.tuple = (...itemGenerators) => { - return { - generate(mrng) { - return itemGenerators.map(g => g.generate(mrng)); - }, - *shrink(value) { - for (let index = 0 ; index !== itemGenerators.length ; ++index) { - const currentGenerator = itemGenerators[index]; - const currentValue = value[index]; - for (const shrunkValue of currentGenerator.shrink(currentValue)) { - yield [...value.slice(0, index), shrunkValue, ...value.slice(index + 1)]; - } - } + return { + generate(mrng) { + return itemGenerators.map((g) => g.generate(mrng)); + }, + *shrink(value) { + for (let index = 0; index !== itemGenerators.length; ++index) { + const currentGenerator = itemGenerators[index]; + const currentValue = value[index]; + for (const shrunkValue of currentGenerator.shrink(currentValue)) { + yield [...value.slice(0, index), shrunkValue, ...value.slice(index + 1)]; } - } -} + } + }, + }; +}; // You can check the output by calling: // > [...miniFc.tuple(miniFc.integer(0, 100), miniFc.integer(0, 100), miniFc.integer(0, 100)).shrink([4, 3, 4])] ``` Concerning arrays, the algorithm needs to shrink on two dimensions: + - the size of the array - the content of the array @@ -354,6 +342,7 @@ Shrinking strategy on arrays is divided into three different steps. 3. We keep the first item as is and apply recursively 1. and 2. on the tail of the array. Let's apply this logic onto our example `[4, 1, 2, 1, 3]`: + - `[2, 1, 3]` (due to 1.) - `[1, 2, 1, 3]` (due to 1.) - `[2, 1, 2, 1, 3]` (due to 2.) @@ -368,37 +357,37 @@ We can adapt our implementation of array to support shrinking as follow: ```js miniFc.array = (itemGenerator) => { - return { - generate(mrng) { - const size = mrng.next(0, 10); - const content = []; - for (let index = 0 ; index !== size ; ++index) { - content.push(itemGenerator.generate(mrng)); - } - return content; - }, - *shrink(value) { - // No shrink on empty arrays - if (value.length === 0) { - return; - } - // Step 1. Shrink on size first by keeping last items - let removedSize = Math.floor(value.length / 2); - while (removedSize > 0) { - yield value.slice(removedSize); - removedSize = Math.floor(removedSize / 2); - } - // Step 2. Shrink the first item alone - for (const shrunkItemValue of itemGenerator.shrink(value[0])) { - yield [shrunkItemValue, ...value.slice(1)]; - } - // Step 3. Keep first item untouched - for (const shrunkValue of this.shrink(value.slice(1))) { - yield [value[0], ...shrunkValue]; - } - } - } -} + return { + generate(mrng) { + const size = mrng.next(0, 10); + const content = []; + for (let index = 0; index !== size; ++index) { + content.push(itemGenerator.generate(mrng)); + } + return content; + }, + *shrink(value) { + // No shrink on empty arrays + if (value.length === 0) { + return; + } + // Step 1. Shrink on size first by keeping last items + let removedSize = Math.floor(value.length / 2); + while (removedSize > 0) { + yield value.slice(removedSize); + removedSize = Math.floor(removedSize / 2); + } + // Step 2. Shrink the first item alone + for (const shrunkItemValue of itemGenerator.shrink(value[0])) { + yield [shrunkItemValue, ...value.slice(1)]; + } + // Step 3. Keep first item untouched + for (const shrunkValue of this.shrink(value.slice(1))) { + yield [value[0], ...shrunkValue]; + } + }, + }; +}; // You can check the output by calling: // > [...miniFc.array(miniFc.integer(0, 100)).shrink([4, 1, 2, 1, 3])] ``` @@ -406,16 +395,15 @@ miniFc.array = (itemGenerator) => { Now that all our root generators have been covered let's adapt the generators based on them. When defined those derived generators we introduced a `map` function to help us in our task. Generator for characters has been defined as follow: + ```js -miniFc.character = () => map( - miniFc.integer(0, 25), - n => String.fromCharCode(97 + n) -) +miniFc.character = () => map(miniFc.integer(0, 25), (n) => String.fromCharCode(97 + n)); ``` As it generates `string` (of a single character), shrinker will consume `string` and produce smaller `string`. But how can `map` know how to convert the `string` received as input of the shrinker to passe it the shrinker of mapped generator (or `miniFc.integer(0, 25)` in our case)? Actually if we wanted to define the shrinker function for `miniFc.character` we would have written something like: + ```js const shrinker = (character) => { const derivedGenerator = miniFc.integer(0, 25); @@ -427,6 +415,7 @@ const shrinker = (character) => { ``` In other words, it means that mapping a generator to derive it requires the user to declare two helper functions: + - one to map - _eg.: from integer to character in our example_ - another one to unmap - _eg.: from character to integer in our example_ @@ -434,42 +423,37 @@ With that in mind, `map` can be changed into: ```js const map = (g, mapper, unmapper) => { - return { - generate(mrng) { - return mapper(g.generate(mrng)); - }, - *shrink(value) { - for (const shrunkValue of g.shrink(unmapper(value))) { - yield mapper(shrunkValue); - } - } - }; + return { + generate(mrng) { + return mapper(g.generate(mrng)); + }, + *shrink(value) { + for (const shrunkValue of g.shrink(unmapper(value))) { + yield mapper(shrunkValue); + } + }, + }; }; ``` And all our derived generators can be adapted to add support for shrink: ```js -miniFc.boolean = () => map( - miniFc.integer(0, 1), - Boolean, - b => b ? 1 : 0, -) -miniFc.character = () => map( +miniFc.boolean = () => map(miniFc.integer(0, 1), Boolean, (b) => (b ? 1 : 0)); +miniFc.character = () => + map( miniFc.integer(0, 25), - n => String.fromCharCode(97 + n), - c => c.codePointAt(0) - 97, -) -miniFc.string = () => map( + (n) => String.fromCharCode(97 + n), + (c) => c.codePointAt(0) - 97 + ); +miniFc.string = () => + map( miniFc.array(miniFc.character()), - characters => characters.join(''), - s => s.split('') -) -miniFc.dictionary = (valueGenerator) => map( - miniFc.array(miniFc.tuple(miniFc.string(), valueGenerator)), - Object.fromEntries, - Object.entries, -) + (characters) => characters.join(''), + (s) => s.split('') + ); +miniFc.dictionary = (valueGenerator) => + map(miniFc.array(miniFc.tuple(miniFc.string(), valueGenerator)), Object.fromEntries, Object.entries); // > [...miniFc.boolean().shrink(true)] // > [...miniFc.character().shrink("h")] @@ -496,43 +480,45 @@ In terms of code, we can adapt our `miniFc.assert` as follow: ```js miniFc.property = (generator, predicate) => { - return { - generate(mrng) { - return generator.generate(mrng); - }, - shrink(value) { - return generator.shrink(value); - }, - run(valueUnderTest) { - return predicate(valueUnderTest); - } - } -} + return { + generate(mrng) { + return generator.generate(mrng); + }, + shrink(value) { + return generator.shrink(value); + }, + run(valueUnderTest) { + return predicate(valueUnderTest); + }, + }; +}; function executeAndShrink(valueUnderTest, property) { - if (!property.run(valueUnderTest)) { - for (const shrunkValue of property.shrink(valueUnderTest)) { - const shrunkResults = executeAndShrink(shrunkValue, property); - if (shrunkResults.failed) { - return shrunkResults; - } - } - return { failed: true, value: valueUnderTest }; + if (!property.run(valueUnderTest)) { + for (const shrunkValue of property.shrink(valueUnderTest)) { + const shrunkResults = executeAndShrink(shrunkValue, property); + if (shrunkResults.failed) { + return shrunkResults; + } } - return { failed: false }; + return { failed: true, value: valueUnderTest }; + } + return { failed: false }; } miniFc.assert = (property, { seed = Date.now() } = {}) => { - let rng = prand.xoroshiro128plus(seed); - for (let runId = 0 ; runId !== 100 ; ++runId) { - const valueUnderTest = property.generate(new Random(rng)); - const testResults = executeAndShrink(valueUnderTest, property); - if (testResults.failed) { - throw new Error(`Property failed after ${runId + 1} runs with value ${JSON.stringify(testResults.value)} (seed: ${seed})`); - } - rng = rng.jump(); + let rng = prand.xoroshiro128plus(seed); + for (let runId = 0; runId !== 100; ++runId) { + const valueUnderTest = property.generate(new Random(rng)); + const testResults = executeAndShrink(valueUnderTest, property); + if (testResults.failed) { + throw new Error( + `Property failed after ${runId + 1} runs with value ${JSON.stringify(testResults.value)} (seed: ${seed})` + ); } -} + rng = rng.jump(); + } +}; ``` Here we are, our home-made property based framework is now able to generate values, run tests and find failures, and in case of failure to shrink it towards something smaller to help our users. @@ -549,10 +535,7 @@ While our current shape for `Generator` seems to be able to deal with many kind Indeed, if you think a little bit about implementing `oneof` with current design while preserving shrinking capabilities you may struggle a bit. `oneof` could be used as follow by users: ```js -miniFc.oneof( - miniFc.string(), - miniFc.boolean(), -) // produces either a string or a boolean +miniFc.oneof(miniFc.string(), miniFc.boolean()); // produces either a string or a boolean ``` First, `oneof` would be at the same level as `integer`, `tuple` and `array` but it would not be enough. @@ -567,12 +550,12 @@ So fast-check introduced the notion of `Shrinkable` and `Arbitrary`. An `Arbitra ```ts type Arbitrary = { - generate(mrng: Random): Shrinkable; -} + generate(mrng: Random): Shrinkable; +}; type Shrinkable = { - value: T; - shrink(): IterableIterator>; -} + value: T; + shrink(): IterableIterator>; +}; ``` ## Bias @@ -594,24 +577,19 @@ With our fresh new framework we could write it that way: const compare = (a, b) => false; miniFc.assert( - miniFc.property( - miniFc.tuple( - miniFc.integer(-0x80000000, 0x7fffffff), - miniFc.integer(-0x80000000, 0x7fffffff), - ), - ([a, b]) => compare(a, b) === (a === b), - ) -) + miniFc.property( + miniFc.tuple(miniFc.integer(-0x80000000, 0x7fffffff), miniFc.integer(-0x80000000, 0x7fffffff)), + ([a, b]) => compare(a, b) === (a === b) + ) +); ``` Unfortunately, our framework might not find any bug - _disclaimer: **fast-check** will find the bug_. Let's zoom on the arbitrary we just defined: + ```js -miniFc.tuple( - miniFc.integer(-0x80000000, 0x7fffffff), - miniFc.integer(-0x80000000, 0x7fffffff), -) +miniFc.tuple(miniFc.integer(-0x80000000, 0x7fffffff), miniFc.integer(-0x80000000, 0x7fffffff)); ``` Given this arbitrary we have 232 equiprobable possible choices for `a` and 232 for `b`. The probability to generate `[a, b]` such that `a === b` would be 1 over 232 which is obviously very close to zero. @@ -631,12 +609,7 @@ As a consequence when using **jsverify** with its default configuration of size ```js const jsc = require('jsverify'); // 0.8.4 -jsc.assert( - jsc.forall( - jsc.integer(), - n => Math.abs(n) <= 50 - ) -) +jsc.assert(jsc.forall(jsc.integer(), (n) => Math.abs(n) <= 50)); // As a consequence, the property: // > all integers are smaller or equal to 50 in absolute value // is true for the framework. Users have to explicitely ask for large values. @@ -647,24 +620,16 @@ But let's say we know about this issue, so we artificially increase the size for ```js const jsc = require('jsverify'); // 0.8.4 -jsc.assert( - jsc.forall( - jsc.integer(-0x80000000, 0x7fffffff), - n => Math.abs(n) > 50 - ) -) +jsc.assert(jsc.forall(jsc.integer(-0x80000000, 0x7fffffff), (n) => Math.abs(n) > 50)); // We generate values from -0x80000000 to 0x7fffffff // The probability to generate one value such that abs(n) <= 50 // is: 101 / 2**32 which is close to zero // Same issue if we use size option... jsc.assert( - jsc.forall( - jsc.integer(), - n => Math.abs(n) > 50 - ), - { size: 0x7fffffff } -) + jsc.forall(jsc.integer(), (n) => Math.abs(n) > 50), + { size: 0x7fffffff } +); ``` The last issue with this approach is that you cannot really say that one arbitrary should be large while another should not. The size apply to all the arbitaries. @@ -680,6 +645,7 @@ The observation is the following: smallest values have most of the time the bene As a consequence the approach is the following: the more we play runs, the more we increase the size of the generated values. During the first runs we try with very small values because they will most of the time execute quickly and may find specific corner cases (think about bugs due to `-1`, `0` or `1`). And at the end we generate values covering the full range of allowed values; For example, when you ask for arrays - _of size 0 to 10_ - containing only natural numbers - _between 0 to 2147483647_ - here is what you may have with such approach: + - run 1/100: arrays having `0` to `floor(10 / 100) = 0` items of values between `0` and `floor(2147483647 / 100) = 21474836` - run 2/100: arrays having `0` to `floor(2 * 10 / 100) = 0` items of values between `0` and `floor(2 * 2147483647 / 100)` - ... @@ -693,28 +659,28 @@ What if our bug only occurs when we have a small array containing very large val const fc = require('fast-check'); fc.assert( - fc.property( - fc.array( - fc.record({ - x: fc.integer(), - y: fc.integer(), - }) - ), - points => { - expect( - sortWith( - sortWith([...points], (a, b) => a.y - b.y), - (a,b) => a.x - b.x - ) - ).toEqual( - sortWith([...points], (a, b) => { - if (a.x === b.x) return a.y - b.y; - else return a.x - b.x; - }) - ) - } - ) -) + fc.property( + fc.array( + fc.record({ + x: fc.integer(), + y: fc.integer(), + }) + ), + (points) => { + expect( + sortWith( + sortWith([...points], (a, b) => a.y - b.y), + (a, b) => a.x - b.x + ) + ).toEqual( + sortWith([...points], (a, b) => { + if (a.x === b.x) return a.y - b.y; + else return a.x - b.x; + }) + ); + } + ) +); // In other words when a sort is stable it means that if two items // are considered equivalent regarding the comparison operator // they we stay respectively in the same order. @@ -733,10 +699,12 @@ The approach implemented in fast-check is pretty close to the one of **RapidChec The choice is to bias the arbitrary 1 time over `freq` - _where `freq` is a value specified into the call to generate (a bit like `size` was passed to generate in **jsverify** instead that this time the value increases over the runs)_. For `fc.integer`: + - 1 over `freq`: smaller values - remaining: the full range of values For `fc.array`: + - 1 over `freq`: - 1 over `freq`: small array with smaller values - remaining: full range array with smaller values @@ -751,6 +719,7 @@ For instance, you should never alter the array produced by the generator of arra But purity comes with one major drawback in that case: How can I do arbitraries that need to know what happens to the generated value in order to shrink properly? In fast-check there are at least two arbitraries that has those needs: + - `fc.context` - it allows users to log stuff during the execution of the test, in other words the generated value is a context and you alter it during the execution - `fc.commands` - it allows users to use fast-check for model based testing diff --git a/documentation/IssuesDiscovered.md b/documentation/IssuesDiscovered.md index 4fc9bce7..3f8991b2 100644 --- a/documentation/IssuesDiscovered.md +++ b/documentation/IssuesDiscovered.md @@ -1,121 +1,126 @@ -# [:house:](../README.md) Issues discovered using fast-check - -## [trekhleb/javascript-algorithms](https://github.com/trekhleb/javascript-algorithms/) - -**Statistics:** ~40000⭐ - *Jan 2019* - -**Issue detected:** counting sort algorithm was really badly handling negative integer values \[[more](https://github.com/trekhleb/javascript-algorithms/pull/100)\] - -**Code example:** `sort([-1])` produces `[null]` - -**Issue detected:** knutt morris pratt implementation considered `""` was not a substring of `""` \[[more](https://github.com/trekhleb/javascript-algorithms/pull/101)\] - -**Code example:** - -```js -knuthMorrisPratt("", "") //=> -1 -knuthMorrisPratt("a", "a") //=> 0 -``` - -**Issue detected:** integer overflows and rounding issues in the implementation of rabin karp \[[more](https://github.com/trekhleb/javascript-algorithms/pull/102)\]\[[+](https://github.com/trekhleb/javascript-algorithms/pull/110)\] - -**Code example:** - -```js -rabinKarp("^ !/'#'pp", " !/'#'pp") //=> -1 -// expected to be 2 - -rabinKarp("a\u{10000}", "\u{10000}") //=> -1 -// After 1st fix: issues with unicode characters outside BMP plan -rabinKarp("a耀a","耀a")) //=> 1 -rabinKarp("\u0000耀\u0000","耀\u0000")) //=> -1 -// After 2nd fix -``` - -**Issue detected:** longest common substring algorithm not properly handling unicode characters outside BMP plan \[[more](https://github.com/trekhleb/javascript-algorithms/pull/129)\] - -**Code example:** - -```js -longestCommonSubstr('𐌵𐌵**ABC', '𐌵𐌵--ABC') //=> "𐌵𐌵" -// expected to be "ABC" -``` - -## [facebook/jest](https://github.com/facebook/jest/) - -**Statistics:** ~25000⭐ ~4m/wk downloads📈 - *May 2019* - -**Issue detected:** `toStrictEqual` fails to distinguish 0 from 5e-324 \[[more](https://github.com/facebook/jest/issues/7941)\] - -**Code example:** `expect(0).toStrictEqual(5e-324)` succeeds - -**Issue detected:** `toEqual` not symmetric for Set \[[more](https://github.com/facebook/jest/issues/7975)\] - -**Code example:** -```js -const s1 = new Set([false, true]); -const s2 = new Set([new Boolean(true), new Boolean(true)]); - -expect(s1).not.toEqual(s2); // success -expect(s2).not.toEqual(s1); // failure -``` - -## [nodeca/js-yaml](https://github.com/nodeca/js-yaml/) - -**Statistics:** ~3000⭐ ~13m/wk downloads📈 - *Jan 2019* - -**Issue detected:** enabling `!!int: binary` style when dumping negative integers produces invalid content \[[more](https://github.com/nodeca/js-yaml/pull/398)\] - -**Code example:** `yaml.dump({toto: -10}, {styles:{'!!int':'binary'}})` produces `toto: 0b-1010` not `toto: -0b1010` - -## [sindresorhus/query-string](https://github.com/sindresorhus/query-string) - -**Statistics:** ~3000⭐ ~5m/wk downloads📈 - *Jan 2019* - -**Issue detected:** enabling the `bracket` setting when exporting arrays containing null values produces an invalid output for the parser \[[more](https://github.com/sindresorhus/query-string/pull/138)\] - -**Code example:** -```js -m.stringify({bar: ['a', null, 'b']}, {arrayFormat: 'bracket'}) //=> "bar[]=a&bar&bar[]=b" -m.parse('bar[]=a&bar&bar[]=b', {arrayFormat: 'bracket'}) //=> {bar: [null, 'b']} -``` - -## [stevemao/left-pad](https://github.com/stevemao/left-pad) - -**Statistics:** ~1000⭐ ~2m/wk downloads📈 - *Jan 2019* - -**Issue detected:** unicode characters outside of the BMP plan are not handled consistently \[[more](https://github.com/stevemao/left-pad/issues/58)\] - -**Code example:** -```js -leftPad('a\u{1f431}b', 4, 'x') //=> 'a\u{1f431}b' -- in: 3 code points, out: 3 code points -leftPad('abc', 4, '\u{1f431}') //=> '\u{1f431}abc' -- in: 3 code points, out: 4 code points -``` - -## [eemeli/yaml](https://github.com/eemeli/yaml) - -**Statistics:** ~100⭐ ~60k/wk downloads📈 - *Jan 2019* - -**Issue detected:** unability to parse string values starting by `:,` \[[more](https://github.com/eemeli/yaml/issues/56)\] - -**Code example:** -```js -YAML.stringify([[':,']]) //=> '- - :,\n' -YAML.parse('- - :,\n') //=> YAMLSyntaxError: Document is not valid YAML (bad indentation?) -``` - -**Issue detected:** some extra spaces added or removed during the parsing \[[more](https://github.com/eemeli/yaml/issues/57)\] - -**Code example:** -```js -YAML.parse(YAML.stringify([{k: `!""""""""""""""""""""""""""""""""""#"\\ '`}])) -//=> [{k: `!""""""""""""""""""""""""""""""""""#"\\'`}] -``` - -## [blakeembrey/javascript-stringify](https://github.com/blakeembrey/javascript-stringify/) - -**Statistics:** ~50⭐ ~250k/wk downloads📈 - *Feb 2019* - -**Issue detected:** `-0` was not stringified correctly \[[more](https://github.com/blakeembrey/javascript-stringify/pull/20)\] - -**Code example:** `stringify(-0)` produces `"0"` instead of `"-0"` +# [:house:](../README.md) Issues discovered using fast-check + +## [trekhleb/javascript-algorithms](https://github.com/trekhleb/javascript-algorithms/) + +**Statistics:** ~40000⭐ - _Jan 2019_ + +**Issue detected:** counting sort algorithm was really badly handling negative integer values \[[more](https://github.com/trekhleb/javascript-algorithms/pull/100)\] + +**Code example:** `sort([-1])` produces `[null]` + +**Issue detected:** knutt morris pratt implementation considered `""` was not a substring of `""` \[[more](https://github.com/trekhleb/javascript-algorithms/pull/101)\] + +**Code example:** + +```js +knuthMorrisPratt('', ''); //=> -1 +knuthMorrisPratt('a', 'a'); //=> 0 +``` + +**Issue detected:** integer overflows and rounding issues in the implementation of rabin karp \[[more](https://github.com/trekhleb/javascript-algorithms/pull/102)\]\[[+](https://github.com/trekhleb/javascript-algorithms/pull/110)\] + +**Code example:** + +```js +rabinKarp("^ !/'#'pp", " !/'#'pp") //=> -1 +// expected to be 2 + +rabinKarp("a\u{10000}", "\u{10000}") //=> -1 +// After 1st fix: issues with unicode characters outside BMP plan +rabinKarp("a耀a","耀a")) //=> 1 +rabinKarp("\u0000耀\u0000","耀\u0000")) //=> -1 +// After 2nd fix +``` + +**Issue detected:** longest common substring algorithm not properly handling unicode characters outside BMP plan \[[more](https://github.com/trekhleb/javascript-algorithms/pull/129)\] + +**Code example:** + +```js +longestCommonSubstr('𐌵𐌵**ABC', '𐌵𐌵--ABC'); //=> "𐌵𐌵" +// expected to be "ABC" +``` + +## [facebook/jest](https://github.com/facebook/jest/) + +**Statistics:** ~25000⭐ ~4m/wk downloads📈 - _May 2019_ + +**Issue detected:** `toStrictEqual` fails to distinguish 0 from 5e-324 \[[more](https://github.com/facebook/jest/issues/7941)\] + +**Code example:** `expect(0).toStrictEqual(5e-324)` succeeds + +**Issue detected:** `toEqual` not symmetric for Set \[[more](https://github.com/facebook/jest/issues/7975)\] + +**Code example:** + +```js +const s1 = new Set([false, true]); +const s2 = new Set([new Boolean(true), new Boolean(true)]); + +expect(s1).not.toEqual(s2); // success +expect(s2).not.toEqual(s1); // failure +``` + +## [nodeca/js-yaml](https://github.com/nodeca/js-yaml/) + +**Statistics:** ~3000⭐ ~13m/wk downloads📈 - _Jan 2019_ + +**Issue detected:** enabling `!!int: binary` style when dumping negative integers produces invalid content \[[more](https://github.com/nodeca/js-yaml/pull/398)\] + +**Code example:** `yaml.dump({toto: -10}, {styles:{'!!int':'binary'}})` produces `toto: 0b-1010` not `toto: -0b1010` + +## [sindresorhus/query-string](https://github.com/sindresorhus/query-string) + +**Statistics:** ~3000⭐ ~5m/wk downloads📈 - _Jan 2019_ + +**Issue detected:** enabling the `bracket` setting when exporting arrays containing null values produces an invalid output for the parser \[[more](https://github.com/sindresorhus/query-string/pull/138)\] + +**Code example:** + +```js +m.stringify({ bar: ['a', null, 'b'] }, { arrayFormat: 'bracket' }); //=> "bar[]=a&bar&bar[]=b" +m.parse('bar[]=a&bar&bar[]=b', { arrayFormat: 'bracket' }); //=> {bar: [null, 'b']} +``` + +## [stevemao/left-pad](https://github.com/stevemao/left-pad) + +**Statistics:** ~1000⭐ ~2m/wk downloads📈 - _Jan 2019_ + +**Issue detected:** unicode characters outside of the BMP plan are not handled consistently \[[more](https://github.com/stevemao/left-pad/issues/58)\] + +**Code example:** + +```js +leftPad('a\u{1f431}b', 4, 'x'); //=> 'a\u{1f431}b' -- in: 3 code points, out: 3 code points +leftPad('abc', 4, '\u{1f431}'); //=> '\u{1f431}abc' -- in: 3 code points, out: 4 code points +``` + +## [eemeli/yaml](https://github.com/eemeli/yaml) + +**Statistics:** ~100⭐ ~60k/wk downloads📈 - _Jan 2019_ + +**Issue detected:** unability to parse string values starting by `:,` \[[more](https://github.com/eemeli/yaml/issues/56)\] + +**Code example:** + +```js +YAML.stringify([[':,']]); //=> '- - :,\n' +YAML.parse('- - :,\n'); //=> YAMLSyntaxError: Document is not valid YAML (bad indentation?) +``` + +**Issue detected:** some extra spaces added or removed during the parsing \[[more](https://github.com/eemeli/yaml/issues/57)\] + +**Code example:** + +```js +YAML.parse(YAML.stringify([{ k: `!""""""""""""""""""""""""""""""""""#"\\ '` }])); +//=> [{k: `!""""""""""""""""""""""""""""""""""#"\\'`}] +``` + +## [blakeembrey/javascript-stringify](https://github.com/blakeembrey/javascript-stringify/) + +**Statistics:** ~50⭐ ~250k/wk downloads📈 - _Feb 2019_ + +**Issue detected:** `-0` was not stringified correctly \[[more](https://github.com/blakeembrey/javascript-stringify/pull/20)\] + +**Code example:** `stringify(-0)` produces `"0"` instead of `"-0"` diff --git a/documentation/RaceConditions.md b/documentation/RaceConditions.md index fed04110..6d212e01 100644 --- a/documentation/RaceConditions.md +++ b/documentation/RaceConditions.md @@ -19,21 +19,22 @@ By doing this it can highlight potential race conditions in your code. Please re ## Overview of the API `fc.scheduler()` is just an `Arbitrary` providing a `Scheduler` instance. The generated scheduler has the following interface: + - `schedule: (task: Promise, label?: string, metadata?: TMetadata) => Promise` - Wrap an existing promise using the scheduler. The newly created promise will resolve when the scheduler decides to resolve it (see `waitOne` and `waitAll` methods). - `scheduleFunction: (asyncFunction: (...args: TArgs) => Promise) => (...args: TArgs) => Promise` - Wrap all the promise produced by an API using the scheduler. `scheduleFunction(callApi)` - `scheduleSequence(sequenceBuilders: SchedulerSequenceItem[]): { done: boolean; faulty: boolean, task: Promise<{ done: boolean; faulty: boolean }> }` - Schedule a sequence of operations. Each operation requires the previous one to be resolved before being started. Each of the operations will be executed until its end before starting any other scheduled operation. - `count(): number` - Number of pending tasks waiting to be scheduled by the scheduler. - `waitOne: () => Promise` - Wait one scheduled task to be executed. Throws if there is no more pending tasks. -- `waitAll: () => Promise` - Wait all scheduled tasks, including the ones that might be created by one of the resolved task. Do not use if `waitAll` call has to be wrapped into an helper function such as `act` that can relaunch new tasks afterwards. In this specific case use a `while` loop running while `count() !== 0` and calling `waitOne` - *see CodeSandbox example on userProfile*. +- `waitAll: () => Promise` - Wait all scheduled tasks, including the ones that might be created by one of the resolved task. Do not use if `waitAll` call has to be wrapped into an helper function such as `act` that can relaunch new tasks afterwards. In this specific case use a `while` loop running while `count() !== 0` and calling `waitOne` - _see CodeSandbox example on userProfile_. - `waitFor: (unscheduledTask: Promise) => Promise` - Wait as many scheduled tasks as need to resolve the received task. Contrary to `waitOne` or `waitAll` it can be used to wait for calls not yet scheduled when calling it (some test solutions like supertest use such trick not to run any query before the user really calls then on the request itself). Be aware that while this helper will wait eveything to be ready for `unscheduledTask` to resolve, having uncontrolled tasks triggering stuff required for `unscheduledTask` might make replay of failures harder as such asynchronous triggers stay out-of-control for fast-check. - `report: () => SchedulerReportItem[]` - Produce an array containing all the scheduled tasks so far with their execution status. If the task has been executed, it includes a string representation of the associated output or error produced by the task if any. Tasks will be returned in the order they get executed by the scheduler. With: + ```ts type SchedulerSequenceItem = - { builder: () => Promise; label: string; metadata?: TMetadata } | - (() => Promise) -; + | { builder: () => Promise; label: string; metadata?: TMetadata } + | (() => Promise); ``` You can also define an hardcoded scheduler by using `fc.schedulerFor(ordering: number[])` - _should be passed through `fc.constant` if you want to use it as an arbitrary_. For instance: `fc.schedulerFor([1,3,2])` means that the first scheduled promise will resolve first, the third one second and at the end we will resolve the second one that have been scheduled. @@ -72,16 +73,16 @@ For instance, `Promise.all` and `Promise.race` are examples of such algorithms. shortTask.then(() => { // not impacted by the scheduler // as it is directly using the original promise -}) +}); -const scheduledShortTask = s.schedule(shortTask) -const scheduledLongTask = s.schedule(longTask) +const scheduledShortTask = s.schedule(shortTask); +const scheduledLongTask = s.schedule(longTask); // Even if in practice, shortTask is quicker than longTask // If the scheduler selected longTask to end first, // it will wait longTask to end, then once ended it will resolve scheduledLongTask, // while scheduledShortTask will still be pending until scheduled. -await s.waitOne() +await s.waitOne(); ``` ### `scheduleFunction` @@ -112,21 +113,20 @@ WARNING: `scheduleFunction` is only postponing the resolution of the function. T // - s : Scheduler // - getUserDetails: (uid: string) => Promise - API call to get details for a User - -const getUserDetailsScheduled = s.scheduleFunction(getUserDetails) +const getUserDetailsScheduled = s.scheduleFunction(getUserDetails); getUserDetailsScheduled('user-001') -// What happened under the hood? -// - A call to getUserDetails('user-001') has been triggered -// - The promise returned by the call to getUserDetails('user-001') has been registered to the scheduler + // What happened under the hood? + // - A call to getUserDetails('user-001') has been triggered + // - The promise returned by the call to getUserDetails('user-001') has been registered to the scheduler .then((dataUser001) => { // This block will only be executed when the scheduler // will schedule this Promise - }) + }); // Unlock one of the scheduled Promise registered on s // Not necessarily the first one that resolves -await s.waitOne() +await s.waitOne(); ``` ### `scheduleSequence` @@ -164,16 +164,14 @@ const otherUserId2 = '003'; // render profile for user {initialUserId} // Note: api calls to get back details for one user are also scheduled -const { rerender } = render( - -) +const { rerender } = render(); s.scheduleSequence([ async () => rerender(), async () => rerender(), -]) +]); -await s.waitAll() +await s.waitAll(); // expect to see profile for user otherUserId2 ``` @@ -185,12 +183,11 @@ In some tests, we want to try cases where we launch multiple concurrent queries ```ts const scheduleCall = (s: Scheduler, f: () => Promise) => { - s.schedule(Promise.resolve("Start the call")) - .then(() => f()); -} + s.schedule(Promise.resolve('Start the call')).then(() => f()); +}; // Calling doStuff will be part of the task scheduled in s -scheduleCall(s, () => doStuff()) +scheduleCall(s, () => doStuff()); ``` **Scheduling a call to a mocked server** @@ -200,30 +197,34 @@ Contrary the behaviour of `scheduleFunction`, real calls to servers are not imme Let's imagine you are building a TODO-list app. Your users can add a TODO only if no other TODO has the same label. If you use the built-in `scheduleFunction` to test it, the mocked-server will always receive the calls in the same order as the one they were done. ```ts -const scheduleMockedServerFunction = (s: Scheduler, f: (...args: TArgs) => Promise) => { +const scheduleMockedServerFunction = ( + s: Scheduler, + f: (...args: TArgs) => Promise +) => { return (...args: TArgs) => { - return s.schedule(Promise.resolve("Server received the call")) - .then(() => f(...args)); - } -} + return s.schedule(Promise.resolve('Server received the call')).then(() => f(...args)); + }; +}; -const newAddTodo = scheduleMockedServerFunction(s, (label) => mockedApi.addTodo(label)) +const newAddTodo = scheduleMockedServerFunction(s, (label) => mockedApi.addTodo(label)); // With newAddTodo = s.scheduleFunction((label) => mockedApi.addTodo(label)) // The mockedApi would have received todo-1 first, followed by todo-2 // When each of those calls resolve would have been the responsibility of s // In the contrary, with scheduleMockedServerFunction, the mockedApi might receive todo-2 first. -newAddTodo('todo-1') // .then -newAddTodo('todo-2') // .then +newAddTodo('todo-1'); // .then +newAddTodo('todo-2'); // .then // or... -const scheduleMockedServerFunction = (s: Scheduler, f: (...args: TArgs) => Promise) => { +const scheduleMockedServerFunction = ( + s: Scheduler, + f: (...args: TArgs) => Promise +) => { const scheduledF = s.scheduleFunction(f); return (...args: TArgs) => { - return s.schedule(Promise.resolve("Server received the call")) - .then(() => scheduledF(...args)); - } -} + return s.schedule(Promise.resolve('Server received the call')).then(() => scheduledF(...args)); + }; +}; ``` **Scheduling timers like setTimeout or setInterval** diff --git a/documentation/Runners.md b/documentation/Runners.md index 22b90736..b33b6993 100644 --- a/documentation/Runners.md +++ b/documentation/Runners.md @@ -1,222 +1,232 @@ -# [:house:](../README.md) Runners - -Runners are the way to make your [arbitraries](./Arbitraries.md) live. They receive a property - *binding between arbitraries and a check function* - to verify. - -This documentation describes all the runners and properties you can use in fast-check. - -You can refer to the [API Reference](https://dubzzz.github.io/fast-check/) for more details. - -## Table of contents - -- [Properties](#properties) -- [Runners](#runners) -- [Global configuration](#global-configuration) - -## Properties - -- `fc.property`: define a new property ie. a list of arbitraries and a test function to assess the success - -The predicate would be considered falsy if it throws or if `output` evaluates to `false`. -```typescript -function property( - arb1: Arbitrary, - predicate: (t1:T1) => (boolean|void)): Property<[T1]>; -function property( - arb1: Arbitrary, arb2: Arbitrary, - predicate: (t1:T1,t2:T2) => (boolean|void)): Property<[T1,T2]>; -... -``` - -- `fc.asyncProperty`: define a new property ie. a list of arbitraries and an asynchronous test function to assess the success - -The predicate would be considered falsy if it throws or if `output` evaluates to `false` (after `await`). -```typescript -function asyncProperty( - arb1: Arbitrary, - predicate: (t1:T1) => Promise): AsyncProperty<[T1]>; -function asyncProperty( - arb1: Arbitrary, arb2: Arbitrary, - predicate: (t1:T1,t2:T2) => Promise): AsyncProperty<[T1,T2]>; -... -``` - -**TIPS 1:** - -The output of `property` and `asyncProperty` (respectively `Property` and `AsyncProperty`) accepts optional `beforeEach` and `afterEach` hooks that would be invoked before and after the execution of the predicate. - -```typescript -property(arb1, predicate) - .beforeEach(() => { /* code executed before each call to predicate */ }) - .afterEach(() => { /* code executed after each call to predicate */ }); - -asyncProperty(arb1, predicate) - .beforeEach(async () => { /* code executed before each call to predicate */ }) - .afterEach(async () => { /* code executed after each call to predicate */ }); -``` - -**TIPS 2:** - -If you want to filter invalid entries directly at predicate level, you can use `fc.pre(...)`. - -`fc.pre` is responsible for checking for preconditions within predicate scope. - -Whenever running a predicate, the framework runs the `fc.pre` instructions as they come and if one of them has a falsy value, it stops the execution flow and asks for another value to run the predicate on. - -Contrary to its alternate solution, `.filter(...)`, a run having too many failing `fc.pre(...)` will be marked as faulty. The tolerance before marking such run as faulty can be customized with `maxSkipsPerRun` but it is recommended not to increase it too much - *too many precondition failures means lots of wasted generated values and an inefficient arbitrary definition*. - -**WARNING:** - -> The predicate function must not change the inputs it received. If it needs to, it has to clone them before going on. Impacting the inputs might led to bad shrinking and wrong display on error. - -> Nonetheless a failing property will still be a failing property. - -## Runners - -- `fc.assert`: run the property and throws in case of failure - -**This function has to be awaited in case it is called on an asynchronous property.** - -This function is ideal to be called in `describe`, `it` blocks. -It does not return anything in case of success. - -It can be parametrized using its second argument. - -```typescript -export interface Parameters { - seed?: number; // optional, initial seed of the generator: Date.now() by default - numRuns?: number; // optional, number of runs before success: 100 by default - maxSkipsPerRun?: number; // optional, maximal number of skipped entries per run: 100 by default - timeout?: number; // optional, only taken into account for asynchronous runs (asyncProperty) - // specify a timeout in milliseconds, maximum time for the predicate to return its result - // only works for async code, will not interrupt a synchronous code: disabled by default - path?: string; // optional, way to replay a failing property directly with the counterexample - // it can be fed with the counterexamplePath returned by the failing test (requires seed too) - logger?: (v: string) => void; // optional, log output: console.log by default - unbiased?: boolean; // optional, force the use of unbiased arbitraries: biased by default - verbose?: boolean; // optional, enable verbose mode: false by default - // when enabling verbose mode, - // you will be provided the list of all failing entries encountered - // whenever a property fails - useful to detect patterns - examples?: T[]; // optional, custom values added to generated ones: [] by default - // when set, those examples will be run against the property first - // followed by the values generated by the framework - // they do not increase the number of times the property will be launched - endOnFailure?: boolean; // optional, stop run on failure: false by default - // it makes the run stop at the first encountered failure without shrinking - // when used in complement to seed and path - // it replays only the minimal counterexample - skipAllAfterTimeLimit?: number; // optional, skip all runs after a given time limit - // in milliseconds (relies on Date.now): disabled by default - interruptAfterTimeLimit?: number; // optional, interrupt test execution after a given time limit - // in milliseconds (relies on Date.now): disabled by default - markInterruptAsFailure?: boolean; // optional, mark interrupted runs as failure: disabled by default - skipEqualValues?: boolean; // optional, skip repeated runs: disabled by default - // If a same input is encountered multiple times only the first one will be executed, - // next ones will be skipped. Be aware that skipping runs may lead to property failure - // if the arbitrary does not have enough values. In that case use `ignoreEqualValues` instead. - ignoreEqualValues?: boolean; // optional, do not repeat runs with already covered cases: disabled by default - // Similar to `skipEqualValues` but instead of skipping runs, it just don't rerun them. - // It can be useful when arbitrary has a limited number of variants. - reporter?: (runDetails: RunDetails) => void; // optional, custom reporter replacing the default one - // reporter is responsible for throwing in case of failure, as an example default one throws - // whenever `runDetails.failed` is true but it is up to you - // it cannot be used in conjonction with asyncReporter - // it will be used by assert for both synchronous and asynchronous properties - asyncReporter?: (runDetails: RunDetails) => Promise; // optional, custom reporter replacing the default one - // reporter is responsible for throwing in case of failure, as an example default one throws - // whenever `runDetails.failed` is true but it is up to you - // it cannot be used in conjonction with reporter - // it cannot be set on synchronous properties - // it will be used by assert for asynchronous properties -} -``` - -```typescript -function assert(property: IProperty, params?: Parameters); -``` - -- `fc.check`: run the property and return an object containing the test status along with other useful details - -**This function has to be awaited in case it is called on an asynchronous property.** - -Calling this function should never throw whatever the status of the test. It can be parametrized with the same parameters as `fc.assert`. - -```typescript -function check(property: IProperty, params?: Parameters); -``` - -The details returned by `fc.check` are the following: - -```typescript -interface RunDetails { - failed: boolean; // true in case of failure or too many skips, false otherwise - interrupted: boolean; // true in case of interrupted run, false otherwise - numRuns: number; // number of runs (all runs if success, up and including the first failure if failed) - numSkips: number; // number of skipped entries due to failed pre-condition (before the first failure) - numShrinks: number; // number of shrinks (depth required to get the minimal failing example) - seed: number; // seed used for the test - counterexample: Ts|null; // failure only: shrunk conterexample causig the property to fail - counterexamplePath: string|null; // failure only: the exact path to re-run the counterexample - // In order to replay the failing case directly, - // this value as to be set as path attribute in the Parameters (with the seed) - // of assert, check, sample or even statistics - error: string|null; // failure only: stack trace and error details - failures: Ts[]; // verbose>=1 only: failures that have occurred during the run - executionSummary: ExecutionTree[]; // verbose>=1 only: traces the origin of each value - // encountered during the test and its status - runConfiguration: Parameters; // configuration of the run, it includes local and global paramaters -} -``` - -Sub-types are available in TypeScript to distinguish between the different types of failures: - -| Sub-type | When | `failed` | `interrupted` | `counterexample`/`counterexamplePath`/`error` | -|-------------------------------------|:----:|:----:|:----:|:----:| -| `RunDetailsFailureProperty` | failure of the predicate | `true` | `true`/`false` | *not null* | -| `RunDetailsFailureTooManySkips` | too many pre-conditions failures | `true` | `false` | `null` | -| `RunDetailsFailureInterrupted` | execution took too long given `interruptAfterTimeLimit` | `true` | `true` | `null` | -| `RunDetailsSuccess` | successful run | `false` | `true`/`false` | `null` | - -In case you want to base your report on what would have been the default output of fast-check, you can use `fc.defaultReportMessage(out: RunDetails): string | undefined`. It builds the string corresponding to the error message that would have been used by `fc.assert` in case of failure and returns `undefined` if there is no failure. - -- `fc.sample`: sample generated values of an `Arbitrary` or `Property` - -It builds an array containing all the values that would have been generated for the equivalent test. - -It also accept `Parameters` as configuration in order to help you diagnose the shape of the inputs that will be received by your property. - -```typescript -type Generator = Arbitrary | IProperty; - -function sample(generator: Generator): Ts[]; -function sample(generator: Generator, params: Parameters): Ts[]; -function sample(generator: Generator, numGenerated: number): Ts[]; -``` - -- `fc.statistics`: classify the values produced by an `Arbitrary` or `Property` - -It provides useful statistics concerning generated values. -In order to be able to gather those statistics it has to be provided with a classifier function that can classify the generated value in zero, one or more categories (free labels). - -It also accept `Parameters` as configuration in order to help you diagnose the shape of the inputs that will be received by your property. - -Statistics are dumped into `console.log` but can be redirected to another source by modifying the `logger` key in `Parameters`. - -```typescript -type Generator = Arbitrary | IProperty; -type Classifier = ((v: Ts) => string) | ((v: Ts) => string[]); - -function statistics(generator: Generator, classify: Classifier): void; -function statistics(generator: Generator, classify: Classifier, params: Parameters): void; -function statistics(generator: Generator, classify: Classifier, numGenerated: number): void; -``` - -## Global configuration - -In order to define the default parameters that will be used by runners you can use one of the following helpers: - -- `fc.configureGlobal(parameters: GlobalParameters)`: define the default parameters to be used by runners -- `fc.resetConfigureGlobal()`: reset the default parameters to be used by runners -- `fc.readConfigureGlobal()`: output the default parameters to be used by runners - -See [Tips / Setup global settings](./Tips.md#setup-global-settings) for more details. +# [:house:](../README.md) Runners + +Runners are the way to make your [arbitraries](./Arbitraries.md) live. They receive a property - _binding between arbitraries and a check function_ - to verify. + +This documentation describes all the runners and properties you can use in fast-check. + +You can refer to the [API Reference](https://dubzzz.github.io/fast-check/) for more details. + +## Table of contents + +- [Properties](#properties) +- [Runners](#runners) +- [Global configuration](#global-configuration) + +## Properties + +- `fc.property`: define a new property ie. a list of arbitraries and a test function to assess the success + +The predicate would be considered falsy if it throws or if `output` evaluates to `false`. + +```typescript +function property( + arb1: Arbitrary, + predicate: (t1:T1) => (boolean|void)): Property<[T1]>; +function property( + arb1: Arbitrary, arb2: Arbitrary, + predicate: (t1:T1,t2:T2) => (boolean|void)): Property<[T1,T2]>; +... +``` + +- `fc.asyncProperty`: define a new property ie. a list of arbitraries and an asynchronous test function to assess the success + +The predicate would be considered falsy if it throws or if `output` evaluates to `false` (after `await`). + +```typescript +function asyncProperty( + arb1: Arbitrary, + predicate: (t1:T1) => Promise): AsyncProperty<[T1]>; +function asyncProperty( + arb1: Arbitrary, arb2: Arbitrary, + predicate: (t1:T1,t2:T2) => Promise): AsyncProperty<[T1,T2]>; +... +``` + +**TIPS 1:** + +The output of `property` and `asyncProperty` (respectively `Property` and `AsyncProperty`) accepts optional `beforeEach` and `afterEach` hooks that would be invoked before and after the execution of the predicate. + +```typescript +property(arb1, predicate) + .beforeEach(() => { + /* code executed before each call to predicate */ + }) + .afterEach(() => { + /* code executed after each call to predicate */ + }); + +asyncProperty(arb1, predicate) + .beforeEach(async () => { + /* code executed before each call to predicate */ + }) + .afterEach(async () => { + /* code executed after each call to predicate */ + }); +``` + +**TIPS 2:** + +If you want to filter invalid entries directly at predicate level, you can use `fc.pre(...)`. + +`fc.pre` is responsible for checking for preconditions within predicate scope. + +Whenever running a predicate, the framework runs the `fc.pre` instructions as they come and if one of them has a falsy value, it stops the execution flow and asks for another value to run the predicate on. + +Contrary to its alternate solution, `.filter(...)`, a run having too many failing `fc.pre(...)` will be marked as faulty. The tolerance before marking such run as faulty can be customized with `maxSkipsPerRun` but it is recommended not to increase it too much - _too many precondition failures means lots of wasted generated values and an inefficient arbitrary definition_. + +**WARNING:** + +> The predicate function must not change the inputs it received. If it needs to, it has to clone them before going on. Impacting the inputs might led to bad shrinking and wrong display on error. + +> Nonetheless a failing property will still be a failing property. + +## Runners + +- `fc.assert`: run the property and throws in case of failure + +**This function has to be awaited in case it is called on an asynchronous property.** + +This function is ideal to be called in `describe`, `it` blocks. +It does not return anything in case of success. + +It can be parametrized using its second argument. + +```typescript +export interface Parameters { + seed?: number; // optional, initial seed of the generator: Date.now() by default + numRuns?: number; // optional, number of runs before success: 100 by default + maxSkipsPerRun?: number; // optional, maximal number of skipped entries per run: 100 by default + timeout?: number; // optional, only taken into account for asynchronous runs (asyncProperty) + // specify a timeout in milliseconds, maximum time for the predicate to return its result + // only works for async code, will not interrupt a synchronous code: disabled by default + path?: string; // optional, way to replay a failing property directly with the counterexample + // it can be fed with the counterexamplePath returned by the failing test (requires seed too) + logger?: (v: string) => void; // optional, log output: console.log by default + unbiased?: boolean; // optional, force the use of unbiased arbitraries: biased by default + verbose?: boolean; // optional, enable verbose mode: false by default + // when enabling verbose mode, + // you will be provided the list of all failing entries encountered + // whenever a property fails - useful to detect patterns + examples?: T[]; // optional, custom values added to generated ones: [] by default + // when set, those examples will be run against the property first + // followed by the values generated by the framework + // they do not increase the number of times the property will be launched + endOnFailure?: boolean; // optional, stop run on failure: false by default + // it makes the run stop at the first encountered failure without shrinking + // when used in complement to seed and path + // it replays only the minimal counterexample + skipAllAfterTimeLimit?: number; // optional, skip all runs after a given time limit + // in milliseconds (relies on Date.now): disabled by default + interruptAfterTimeLimit?: number; // optional, interrupt test execution after a given time limit + // in milliseconds (relies on Date.now): disabled by default + markInterruptAsFailure?: boolean; // optional, mark interrupted runs as failure: disabled by default + skipEqualValues?: boolean; // optional, skip repeated runs: disabled by default + // If a same input is encountered multiple times only the first one will be executed, + // next ones will be skipped. Be aware that skipping runs may lead to property failure + // if the arbitrary does not have enough values. In that case use `ignoreEqualValues` instead. + ignoreEqualValues?: boolean; // optional, do not repeat runs with already covered cases: disabled by default + // Similar to `skipEqualValues` but instead of skipping runs, it just don't rerun them. + // It can be useful when arbitrary has a limited number of variants. + reporter?: (runDetails: RunDetails) => void; // optional, custom reporter replacing the default one + // reporter is responsible for throwing in case of failure, as an example default one throws + // whenever `runDetails.failed` is true but it is up to you + // it cannot be used in conjonction with asyncReporter + // it will be used by assert for both synchronous and asynchronous properties + asyncReporter?: (runDetails: RunDetails) => Promise; // optional, custom reporter replacing the default one + // reporter is responsible for throwing in case of failure, as an example default one throws + // whenever `runDetails.failed` is true but it is up to you + // it cannot be used in conjonction with reporter + // it cannot be set on synchronous properties + // it will be used by assert for asynchronous properties +} +``` + +```typescript +function assert(property: IProperty, params?: Parameters); +``` + +- `fc.check`: run the property and return an object containing the test status along with other useful details + +**This function has to be awaited in case it is called on an asynchronous property.** + +Calling this function should never throw whatever the status of the test. It can be parametrized with the same parameters as `fc.assert`. + +```typescript +function check(property: IProperty, params?: Parameters); +``` + +The details returned by `fc.check` are the following: + +```typescript +interface RunDetails { + failed: boolean; // true in case of failure or too many skips, false otherwise + interrupted: boolean; // true in case of interrupted run, false otherwise + numRuns: number; // number of runs (all runs if success, up and including the first failure if failed) + numSkips: number; // number of skipped entries due to failed pre-condition (before the first failure) + numShrinks: number; // number of shrinks (depth required to get the minimal failing example) + seed: number; // seed used for the test + counterexample: Ts | null; // failure only: shrunk conterexample causig the property to fail + counterexamplePath: string | null; // failure only: the exact path to re-run the counterexample + // In order to replay the failing case directly, + // this value as to be set as path attribute in the Parameters (with the seed) + // of assert, check, sample or even statistics + error: string | null; // failure only: stack trace and error details + failures: Ts[]; // verbose>=1 only: failures that have occurred during the run + executionSummary: ExecutionTree[]; // verbose>=1 only: traces the origin of each value + // encountered during the test and its status + runConfiguration: Parameters; // configuration of the run, it includes local and global paramaters +} +``` + +Sub-types are available in TypeScript to distinguish between the different types of failures: + +| Sub-type | When | `failed` | `interrupted` | `counterexample`/`counterexamplePath`/`error` | +| ----------------------------------- | :-----------------------------------------------------: | :------: | :------------: | :-------------------------------------------: | +| `RunDetailsFailureProperty` | failure of the predicate | `true` | `true`/`false` | _not null_ | +| `RunDetailsFailureTooManySkips` | too many pre-conditions failures | `true` | `false` | `null` | +| `RunDetailsFailureInterrupted` | execution took too long given `interruptAfterTimeLimit` | `true` | `true` | `null` | +| `RunDetailsSuccess` | successful run | `false` | `true`/`false` | `null` | + +In case you want to base your report on what would have been the default output of fast-check, you can use `fc.defaultReportMessage(out: RunDetails): string | undefined`. It builds the string corresponding to the error message that would have been used by `fc.assert` in case of failure and returns `undefined` if there is no failure. + +- `fc.sample`: sample generated values of an `Arbitrary` or `Property` + +It builds an array containing all the values that would have been generated for the equivalent test. + +It also accept `Parameters` as configuration in order to help you diagnose the shape of the inputs that will be received by your property. + +```typescript +type Generator = Arbitrary | IProperty; + +function sample(generator: Generator): Ts[]; +function sample(generator: Generator, params: Parameters): Ts[]; +function sample(generator: Generator, numGenerated: number): Ts[]; +``` + +- `fc.statistics`: classify the values produced by an `Arbitrary` or `Property` + +It provides useful statistics concerning generated values. +In order to be able to gather those statistics it has to be provided with a classifier function that can classify the generated value in zero, one or more categories (free labels). + +It also accept `Parameters` as configuration in order to help you diagnose the shape of the inputs that will be received by your property. + +Statistics are dumped into `console.log` but can be redirected to another source by modifying the `logger` key in `Parameters`. + +```typescript +type Generator = Arbitrary | IProperty; +type Classifier = ((v: Ts) => string) | ((v: Ts) => string[]); + +function statistics(generator: Generator, classify: Classifier): void; +function statistics(generator: Generator, classify: Classifier, params: Parameters): void; +function statistics(generator: Generator, classify: Classifier, numGenerated: number): void; +``` + +## Global configuration + +In order to define the default parameters that will be used by runners you can use one of the following helpers: + +- `fc.configureGlobal(parameters: GlobalParameters)`: define the default parameters to be used by runners +- `fc.resetConfigureGlobal()`: reset the default parameters to be used by runners +- `fc.readConfigureGlobal()`: output the default parameters to be used by runners + +See [Tips / Setup global settings](./Tips.md#setup-global-settings) for more details. diff --git a/documentation/Tips.md b/documentation/Tips.md index 8b5176f1..fdbc5506 100644 --- a/documentation/Tips.md +++ b/documentation/Tips.md @@ -1,909 +1,892 @@ -# [:house:](../README.md) Tips - -Simple tips to unlock all the power of fast-check with only few changes. - -## Table of contents - -- [Filter invalid combinations using pre-conditions](#filter-invalid-combinations-using-pre-conditions) -- [Value depending on another one](#value-depending-on-another-one) -- [Model based testing or UI test](#model-based-testing-or-ui-test) -- [Detect race conditions](#detect-race-conditions) -- [Opt for verbose failures](#opt-for-verbose-failures) -- [Log within a predicate](#log-within-a-predicate) -- [Preview generated values](#preview-generated-values) -- [Replay after failure](#replay-after-failure) -- [Replay after failure for commands](#replay-after-failure-for-commands) -- [Add custom examples next to generated ones](#add-custom-examples-next-to-generated-ones) -- [Simplify user definable corner cases](#simplify-user-definable-corner-cases) -- [Combine with other faker or random generator libraries](#combine-with-other-faker-or-random-generator-libraries) -- [Setup global settings](#setup-global-settings) -- [Avoid tests to reach the timeout of your test runner](#avoid-tests-to-reach-the-timeout-of-your-test-runner) -- [Customize the reported error](#customize-the-reported-error) -- [Create a CodeSandbox link on error](#create-a-codesandbox-link-on-error) -- [Migrate from jsverify to fast-check](#migrate-from-jsverify-to-fast-check) -- [Supported targets from node to deno](#supported-targets-from-node-to-deno) -- [Override default toString for a given instance](#override-default-toString-for-a-given-instance) -- [Larger entries by default](#larger-entries-by-default) - -## Filter invalid combinations using pre-conditions - -Filtering invalid combinations of generated entries can be done in two ways in fast-check: -- at arbitrary level using `.filter(...)` -- at property level using `fc.pre(expectedToBeTrue)` - -This part describes the usage of `fc.pre(...)`. More details on `.filter(...)` in [Advanced Arbitraries](./AdvancedArbitraries.md). - -`fc.pre(...)` can be used anywhere within check functions. For instance, you might write: - -```js -fc.assert( - fc.property( - fc.nat(), fc.nat(), - (a, b) => { - // runs not having a < b will be disgarded - fc.pre(a < b); - // ... your code - // ... and possibly other preconditions using fc.pre(...) - } - ) -) -``` - -Whenever it encounters a failing precondition, the framework generates another value and forgets about this run - *neither failed nor succeeded*. - -The advantage of `fc.pre(...)` over `.filter(...)` is that runs having too many rejected values will be marked as faulty. When used in combination of `fc.check(...)` it can help to design new filtered arbitraries as the number of skipped values will be computed and available in the output. - -However when your arbitrary is safe enough, switching to `.filter(...)` might be considered for two reasons: -- easier to share the arbitrary across multiple tests -- higher performances - contrary to `fc.pre`, `fc.filter` is not exception-based making it faster - -## Value depending on another one - -A frequently asked question is: _How to build two values depending from each others with fast-check?_ - -There are multiple ways to do that and all of them have their strengths: easier to write, faster to run, more efficient for shrinking... -Let's dig into some of them through a very simple example: we want to generate `a` and `b` such that `a ≤ b`. - -**Option 1** - `.filter` - discards half of the generated values, can infinitely loop if condition is too strict - -```js -fc.assert( - fc.property( - fc.tuple(fc.nat(), fc.nat()).filter(([a, b]) => a <= b), - ([a, b]) => { - expect(a).toBeGreaterThanOrEqualTo(b); - } - ) -); -``` - -**Option 2** - `fc.pre` - discards half of the generated values, stop when too many retries - -```js -fc.assert( - fc.property(fc.nat(), fc.nat(), (a, b) => { - fc.pre(a <= b); - expect(a).toBeGreaterThanOrEqualTo(b); - }) -); -``` - -**Option 3** - `.chain` - shrinker might not shrink towards minimal cases - -```js -fc.assert( - fc.property( - fc.nat().chain((n) => fc.tuple(fc.nat({ max: n }), fc.constant(n))), - ([a, b]) => { - expect(a).toBeGreaterThanOrEqualTo(b); - } - ) -); -``` - -**Option 4** - `.map` - sometimes complex to write - -```js -fc.assert( - fc.property( - fc.tuple(fc.nat(), fc.nat()).map(([a, b]) => (a <= b ? [a, b] : [b, a])), - ([a, b]) => { - expect(a).toBeGreaterThanOrEqualTo(b); - } - ) -); -``` - -or - -```js -fc.assert( - fc.property( - fc.tuple(fc.nat(), fc.nat()).map(([a, b]) => [a, a + b]), - ([a, b]) => { - expect(a).toBeGreaterThan(b); - } - ) -); -``` - -## Model based testing or UI test - -Model based testing approach have been introduced into fast-check to ease UI testing or state machine tests. - -The idea of the approach is to define commands that could be applied to your system. The framework then picks zero, one or more commands and run them sequentially if they can be executed on the current state. - -A full example is available [here](https://github.com/dubzzz/fast-check/tree/main/example/004-stateMachine/musicPlayer). - -Let's take the case of a list class with `pop`, `push`, `size` methods as an example. - -```typescript -class List { - data: number[] = []; - push = (v: number) => this.data.push(v); - pop = () => this.data.pop()!; - size = () => this.data.length; -} -``` - -Model based testing requires a model. A model is a simplified version of the real system. In this precise case our model would contain only a single integer representing the size of the list. - -```typescript -type Model = { num: number }; -``` - -Then we have to define a command for each of the available operations on our list. Commands come with two methods: -- `check(m: Readonly): boolean`: true if the command can be executed given the current state -- `run(m: Model, r: RealSystem): void`: execute the command on the system and update the model accordingly. Check for potential problems or inconsistencies between the model and the real system - throws in such case. - -```typescript -class PushCommand implements fc.Command { - constructor(readonly value: number) {} - check = (m: Readonly) => true; - run(m: Model, r: List): void { - r.push(this.value); // impact the system - ++m.num; // impact the model - } - toString = () => `push(${this.value})`; -} -class PopCommand implements fc.Command { - check(m: Readonly): boolean { - // should not call pop on empty list - return m.num > 0; - } - run(m: Model, r: List): void { - assert.equal(typeof r.pop(), 'number'); - --m.num; - } - toString = () => 'pop'; -} -class SizeCommand implements fc.Command { - check = (m: Readonly) => true; - run(m: Model, r: List): void { - assert.equal(r.size(), m.num); - } - toString = () => 'size'; -} -``` - -Now that all or commands are ready we can run everything: - -```typescript -// define the possible commands and their inputs -const allCommands = [ - fc.integer().map(v => new PushCommand(v)), - fc.constant(new PopCommand()), - fc.constant(new SizeCommand()) -]; -// run everything -fc.assert( - fc.property(fc.commands(allCommands, { size: '+1' }), cmds => { - const s = () => ({ model: { num: 0 }, real: new List() }); - fc.modelRun(s, cmds); - }) -); -``` - -The code above can easily be applied to other state machines, APIs or UI. In the case of asynchronous operations you need to implement `AsyncCommand` and use `asyncModelRun`. - -**NOTE:** Contrary to other arbitraries, commands built using `fc.commands` requires an extra parameter for replay purposes. In addition of passing `{ seed, path }` to `fc.assert`, `fc.commands` must be called with `{ replayPath: string }`. - -## Detect race conditions - -Even if JavaScript is mostly a mono-threaded language, it is quite easy to introduce race conditions in your code. - -`fast-check` comes with a built-in feature accessible through `fc.scheduler` that will help you to detect such issues earlier during the development. It basically re-orders the execution of your promises or async tasks in order to make it crash under unexpected orderings. - -The best way to see it in action is certainly to check the snippets provided in our [CodeSandbox@005-race](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?hidenavigation=1&module=%2F005-race%2Fautocomplete%2Fmain.spec.tsx&previewwindow=tests). - -Here is a very simple React-based example that you can play with on [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?hidenavigation=1&module=%2F005-race%2FuserProfile%2Fmain.spec.tsx&previewwindow=tests): - -```jsx -/* Component */ - -import { getUserProfile } from './api.js' -function UserPageProfile(props) { - const { userId } = props; - const [userData, setUserData] = React.useState(null); - - React.useEffect(() => { - const fetchUser = async () => { - const data = await getUserProfile(props.userId); - setUserData(data); - }; - fetchUser(); - }, [userId]); - - if (userData === null) { - return
Loading...
; - } - return ( -
-
Id: {userData.id}
-
Name: {userData.name}
-
- ); -} - -/* Test with react testing library */ - -test('should not display data related to another user', () => - fc.assert( - fc.asyncProperty( - fc.uuid(), fc.uuid(), fc.scheduler(), - async (uid1, uid2, s) => { - // Arrange - getUserProfile.mockImplementation( - s.scheduleFunction(async (userId) => ({ id: userId, name: userId }))); - - // Act - const { rerender, queryByTestId } = render(); - s.scheduleSequence([ - async () => { - rerender(); - } - ]); - while (s.count() !== 0) { - await act(async () => { - await s.waitOne(); - }); - } - - // Assert - expect((await queryByTestId('user-id')).textContent).toBe(`Id: ${uid2}`); - }) - .beforeEach(async () => { - jest.resetAllMocks(); - cleanup(); - }) - )); -``` - -In case of failure, the reported error will contain the scheduler that caused the issue along with other generated values if any. -Here is what an error can look like in case we only asked for a scheduler: - -``` - Property failed after 1 tests - { seed: -22040264, path: "0", endOnFailure: true } - Counterexample: [schedulerFor()` - -> [task${1}] promise resolved with value "A" - -> [task${3}] promise resolved with value "C" - -> [task${2}] promise resolved with value "B"`] - Shrunk 0 time(s) -``` - -Given such failure you can either replay it by using the provided `{ seed, path, endOnFailure }` - _see [Replay after failure](#replay-after-failure)_ - -or put the scheduler as an example to be used for every future run - _see [Add custom examples next to generated ones](#add-custom-examples-next-to-generated-ones)_. - -If you want to add this example in your set of custom examples you have to use `fc.schedulerFor` and copy the counterexample coming from the stack trace into the `examples` given to `fc.assert` as follow: - -```js -test('should run with custom scheduler then generated ones', () => - fc.assert( - fc.property(fc.scheduler(), (s) => {/* Test */}), - { - examples: [ - [fc.schedulerFor()` - -> [task${1}] promise resolved with value "A" - -> [task${3}] promise resolved with value "C" - -> [task${2}] promise resolved with value "B"`] - ] - } - )); -``` - -## Opt for verbose failures - -By default, the failures reported by `fast-check` feature most relevant data: -- seed -- path towards the minimal counterexample -- number of tries before the first failure -- depth of the shrink -- minimal counterexample - -`fast-check` comes with a verbose mode, which can help users while trying to dig into a failure. - -For instance, let's suppose the following property failed: -```js -fc.assert( - fc.property( - fc.string(), fc.string(), fc.string(), - (a,b,c) => contains(a+b+c, b))); -``` - -The output will look something like: -``` -Error: Property failed after 1 tests (seed: 1527423434693, path: 0:0:0): ["","",""] -Shrunk 1 time(s) -Got error: Property failed by returning false - -Hint: Enable verbose mode in order to have the list of all failing values encountered during the run -``` - -In order to enable the `verbose` mode, we just need to give a second parameter to `fc.assert` as follow: -```js -fc.assert( - fc.property( - fc.string(), fc.string(), fc.string(), - (a,b,c) => contains(a+b+c, b)), - {verbose: true}); -``` - -Verbose logs give more details on the error as they will contain all the counterexamples encountered while shrinking the inputs. The example above results in: -``` -Error: Property failed after 1 tests (seed: 1527423434693, path: 0:0:0): ["","",""] -Shrunk 2 time(s) -Got error: Property failed by returning false - -Encountered failures were: -- ["","JeXPqIQ6",">q"] -- ["","",">q"] -- ["","",""] -``` - -With that output, we notice that our `contains` implementation seems to fail when the `pattern` we are looking for is the beginning of the string we are looking in. - -Verbosity can be set to produce even more verbose logs by setting `verbose` flag to: -- `0`: `None` - default, equivalent to `false` -- `1`: `Verbose` - equivalent to `true` -- `2`: `VeryVerbose` - logs all the produced values in case of failure - -Refer to `fc.VerbosityLevel` for more details. - -## Log within a predicate - -In order to ease the diagnosis of red properties, fast-check introduced an internal logger that can be used to log stuff inside the predicate itself. - -The advantage of this logger is that one logger is linked to one run so that the counterexample comes with its own logs (and not the ones of previous failures leading to this counterexample). Logs will only be shown in case of failure contrary to `console.log` that would pop everywhere. - -Usage is quite simple, logger is one of the features available inside the `Context` interface: - -```typescript -fc.assert( - fc.property( - fc.string(), - fc.string(), - fc.context(), // comes with a log method - (a: number, b: number, ctx: fc.Context): boolean => { - const intermediateResult = /* ... */; - ctx.log(`Intermediate: ${intermediateResult}`); - return check(intermediateResult); - } - ) -) -``` - -## Preview generated values - -Before writing down your test, it might be great to confirm that the arbitrary you will be using produce the values you want. - -This can be done very easily by using either `fc.sample` or `fc.statistics`. - -The following code constructs an array containing the first 10 values that would have been generated by the arbitrary `fc.anything()` if used inside a `fc.assert` or `fc.check`: - -```typescript -fc.sample( - fc.anything(), // arbitrary or property to extract the values from - 10 // number of values to extract -); -``` - -In some cases, having a sample is not enough and we want more insights about the generated data. -For instance, I might be interested by the share of even numbers generated by `fc.nat()`. -For that purpose I can use `fc.statistics` as follow: - -```typescript -fc.statistics( - fc.nat(), // arbitrary or property to extract the values from - n => n % 2 === 0 ? 'Even number' : 'Odd number', // classifier - 10000 // number of values to extract -); -// Possible output (console.log): -// Odd number...50.30% -// Even number..49.70% -``` - -## Replay after failure - -`fast-check` comes with a must-have feature: replay a failing case immediately given its seed and path (seed only to replay all). - -Whenever `fc.assert` encounters a failure, it displays an error log featuring both the seed and the path to replay it. For instance, in the output below the seed is 1525890375951 and the path 0:0. - -``` -Error: Property failed after 1 tests -{ seed: 1525890375951, path: 0:0, endOnFailure: true } -Counterexample: [0] -Shrunk 1 time(s) -Got error: Property failed by returning false -``` - -In order to replay the failure on the counterexample - `[0]`, you have to change your code as follow: - -```typescript -// Original code -fc.assert( - fc.property( - fc.nat(), - checkEverythingIsOk - ) -); - -// Replay code: straight to the minimal counterexample -// Only replay the minimal counterexample -fc.assert( - fc.property( - fc.nat(), - checkEverythingIsOk - ), - { - seed: 1525890375951, - path: "0:0", - endOnFailure: true - } -); -``` - -**NOTE:** Replaying `fc.commands` requires passing an additional flag called `replayPath` when building this arbitrary (see below). - -## Replay after failure for commands - -As any other built-in arbitrary, `fc.commands` is replayable but the process is a bit different. - -Whenever `fc.assert` encounters a failure with `fc.commands`, it displays an error log featuring both the seed, path and replayPath to replay it. For instance, in the output below the seed is 670108017, the path 96:5 and the replayPath is AAAAABAAE:VF. - -``` -Property failed after 97 tests -{ seed: 670108017, path: "96:5", endOnFailure: true } -Counterexample: [PlayToken[0],NewGame,PlayToken[1],Refresh /*replayPath="AAAAABAAE:VF"*/] -Shrunk 1 time(s) -Got error: Error: expect(received).toEqual(expected) -``` - -In order to replay the failure on the counterexample - `[PlayToken[0],NewGame,PlayToken[1],Refresh]`, you have to change your code as follow: - -```typescript -// Original code -fc.assert( - fc.property( - fc.commands(/* array of commands */), - checkEverythingIsOk - ) -); - -// Replay code: straight to the minimal counterexample -// Only replay the minimal counterexample -fc.assert( - fc.property( - fc.commands( - /* array of commands */, - { - replayPath: "AAAAABAAE:VF" - } - ), - checkEverythingIsOk - ), - { - seed: 670108017, - path: "96:5", - endOnFailure: true - } -); -``` - -**NOTE:** Why is there something specific to do for `fc.commands`? -In order to come with a more efficient and faster shrinker, `fc.commands` takes into account the commands that have really been executed. -Basically if the framework generated the following commands `[A,B,C,A,A,C]` but only executed `[A,-,C,A,-,-]` it will shrink only `[A,C,A]`. -The value stored into `replayPath` encodes the history of what was really executed in order not re-run anything on replay. - -## Add custom examples next to generated ones - -Sometimes it might be useful to run your test on some custom examples you think useful: either because they caused your code to fail in the past or because you explicitly want to confirm it succeeds on this specific example. - -Whatever the reason, the framework provides you the ability to set a custom list of examples into the settings of `fc.assert`. -Those examples will be executed first followed by the values generated by the framework. It does not impact the number of values that will be tested against your property - *meaning that if you add 5 custom examples, you remove 5 generated values from the run*. - -The syntax is the following: - -```typescript -// For a one parameter property -fc.assert( - fc.property( - fc.nat(), - myCheckFunction - ), - { - examples: [ - [0], // first example I want to test - [Number.MAX_SAFE_INTEGER] - ] - } -) - -// For a multiple parameters property -fc.assert( - fc.property( - fc.string(), fc.string(), fc.string(), - myCheckFunction - ), - { - examples: [ - ['', '', ''] - ] - } -) -``` - -Please keep in mind that property based testing frameworks are fully able to find corner-cases with no help at all. - -## Simplify user definable corner cases - -Sometimes, you may discover a bug even before you took time to write a test for it. -But, from time to time, the corner cases you might not be that easy to troubleshoot and a smaller case would clearly help you in your investigation. - -Since version 2.19.0, fast-check comes with a built-in way to shrink automatically user definable examples defined in `examples`. - -Once you have a property that fails with your case (possibly just something like: should not crash), you just have to pass the corner case within `examples` and let fast-check reduce it to something simpler to troubleshoot. - -```typescript -fc.assert( - fc.property( - fc.array(fc.string()), - myCheckFunction - ), - { examples: [ - // the user definable corner case - [ ['__', 'proto', '__'] ] - ]} -) -``` - -Please note that currently if you want fast-check to shrink values for you, you have to give `map` a way to unmap the values. -Most of the other built-in arbitraries come with built-in support, so no special treatment for `fc.record`, `fc.string` or others. -Please note that for the moment `chain` is not supported, as a consequence arbitraries defined via `chain` will not be able to shrink user definable values. - -If you want to use it with `map`, the syntax is a bit more verbose at the moment, but it will be less verbose starting at 3.x (no more need for `fc.convertFromNext` or `fc.convertToNext`). - -```typescript -fc.assert( - fc.property( - fc.convertFromNext( - fc.convertToNext(fc.array(fc.string())).map( - arr => arr.join(','), - raw => { - // unmapper is supposed to handle not supported values by throwing - if (typeof raw !== 'string') throw new Error('Unsupported'); - // remaning is supported - return raw.split(','); - } - ) - ), - myCheckFunction - ), - { examples: [ - // the user definable corner case - [ '__,proto,__' ] - ]} -) -``` - -## Combine with other faker or random generator libraries - -In order to integrate external faker or random generator libraries within fast-check, the generators have to be wrapped as arbitraries. - -The minimal requirement that needs to be fulfilled by the wrapped library is to provide a way to be seeded and reproducible. fast-check cannot offer replay capabilities if the underlying generators are not able to generate the same values from one run to another. - -Here are some examples of how external faker libraries can be wrapped within fast-check. - -With [faker](https://www.npmjs.com/package/@faker-js/faker) - seed based faker: - -```js -const fc = require('fast-check'); -const { faker } = require('@faker-js/faker'); - -const fakerToArb = (fakerGen) => { - return fc.integer() - .noBias() // same probability to generate each of the allowed integers - .noShrink() // shrink on a seed makes no sense - .map(seed => { - faker.seed(seed); // seed the generator - return fakerGen(); // call it - }); -}; - -const streetAddressArb = fakerToArb(faker.address.streetAddress); -const customArb = fakerToArb(() => faker.fake("{{name.lastName}}, {{name.firstName}} {{name.suffix}}")); -``` - -With [lorem-ipsum](https://www.npmjs.com/package/lorem-ipsum) - random generator based faker: - -```js -const fc = require('fast-check'); -const { loremIpsum } = require("lorem-ipsum"); - -const loremArb = fc.infiniteStream(fc.double().noBias()) - .noShrink() - .map(s => { - const rng = () => s.next().value; // prng like Math.random but controlled by fast-check - return loremIpsum({ random: rng }); - }); -``` - -Please note that in the two examples above, the resulting arbitraries will not have full shrinking capabilities. But they offer a full support for random value generation. - -## Setup global settings - -All the runners provided by fast-check come with an optional parameter to customize how the runner will behave (see `fc.assert`, `fc.check`, `fc.sample` or `fc.statistics`). In the past, this parameter had to be provided runner by runner otherwise user would have fallen back on the default values hardcoded in fast-check code. For instance, if one wanted to override the default number of runs of properties, it would have written: - -```typescript -test('test #1', () => { - fc.assert( - myProp1, - { numRuns: 10 } - ) -}) -test('test #2', () => { - fc.assert( - myProp2, - { numRuns: 10 } // duplicated - ) -}) -``` - -Starting at version `1.18.0`, the code above can be changed into: - -```typescript -fc.configureGlobal({ numRuns: 10 }) // see below for the recommended way (Jest/Mocha) -test('test #1', () => { - fc.assert(myProp1) -}) -test('test #2', () => { - fc.assert(myProp2) -}) -``` - -**With Mocha** - -*Create a new setup file that will be executed before executing the test code itself - use `--file=mocha.setup.js` option to reference this file* - -```js -// mocha.setup.js -const fc = require("fast-check"); -fc.configureGlobal({ numRuns: 10 }); -``` - -**With Jest** - -*Edit the configuration of Jest to add your own setup file - usually the configuration is defined in jest.config.js* -```js -// jest.config.js -module.exports = { - setupFiles: ["./jest.setup.js"] -}; -``` - -*Create a new setup file that will be executed before executing the test code itself* - -```js -// jest.setup.js -const fc = require("fast-check"); -fc.configureGlobal({ numRuns: 10 }); -``` - -## Avoid tests reaching the timeout of your test runner - -Most of the time, test runners like Jest, Mocha or even Jasmine come with default timeouts. Whenever one test takes longer than this time limit, the test runner might stop it immediately. Unfortunately whenever fast-check gets stopped at the middle of a run, it cannot give back the seed nor the path that were used during this test. - -Here are some possible reasons why you may encounter timeouts with property based testing: -- (1) an entry generated by fast-check took longer than expected -- (2) shrinking process takes longer than expected - the main target of shrinking process is to report the user with the very minimal failing case, in order to achieve that it has to try many sub-inputs - -In order to prevent your tests from timing out in your CI, you may [setup global settings](#setup-global-settings) with the following configuration: - -```js -fc.configureGlobal({ - interruptAfterTimeLimit: 4000, // Default timeout in Jest 5000ms - markInterruptAsFailure: true, // When set to true, timeout during initial cases (1) will be marked as an error - // When set to false, timeout during initial cases (1) will not be considered as a failure -}); -``` - -If you opt for `markInterruptAsFailure: true`, you can still limit the time taken by long running tests locally by tweaking the settings passed into `fc.assert` with a value of `numRuns` smaller than your default one. - -## Customize the reported error - -By default, `fc.assert` automatically handles and formats the failures that occur when running your properties. - -Nonetheless, in some cases you might be interested into customizing, extending or even changing what should be a failure or how it should be formated. -In order to customize it, you can define your own reporting strategy by passing a custom reporter to `fc.assert`: - -```javascript -fc.assert( - // You can either use it with `fc.property` - // or `fc.asyncProperty` - fc.property(...), - { - reporter(out) { - // Let's say we want to re-create the default reporter of `assert` - if (out.failed) { - // `defaultReportMessage` is an utility that make you able to have the exact - // same report as the one that would have been generated by `assert` - throw new Error(fc.defaultReportMessage(out)); - } - } - } -) -``` - -In case your reporter is relying on asynchronous code, you can specify it by setting `asyncReporter` instead of `reporter`. -Contrary to `reporter` that will be used for both synchronous and asynchronous properties, `asyncReporter` is forbidden for synchronous properties and makes them throw. - -In the past, before `reporter` or `asyncReporter`, writing your own `fc.assert` including your own reporter would have been written as follow: - -```javascript -const throwIfFailed = (out) => { - if (out.failed) { - throw new Error(fc.defaultReportMessage(out)); - } -} -const myCustomAssert = (property, parameters) => { - const out = fc.check(property, parameters); - - if (property.isAsync()) { - return out.then(runDetails => { - throwIfFailed(runDetails) - }); - } - throwIfFailed(out); -} -``` - -## Create a CodeSandbox link on error - -_If you have not read about ways to customize the reporter used by `fc.assert` please refer to the section above._ - -In some situations, it can be useful to directly publish a minimal reproduction of an issue in order to be able to play with it. -Custom reporters can be used to provide such capabilities. - -For instance, you can automatically generate CodeSandbox environments in case of failed property with the snippet below: - -```javascript -import { getParameters } from 'codesandbox/lib/api/define'; - -const buildCodeSandboxReporter = (createFiles) => { - return function reporter(runDetails) { - if (!runDetails.failed) { - return; - } - const counterexample = runDetails.counterexample; - const originalErrorMessage = fc.defaultReportMessage(runDetails); - if (counterexample === undefined) { - throw new Error(originalErrorMessage); - } - const files = { - ...createFiles(counterexample), - 'counterexample.js': { - content: `export const counterexample = ${fc.stringify(counterexample)}` - }, - 'report.txt': { - content: originalErrorMessage - } - } - const url = `https://codesandbox.io/api/v1/sandboxes/define?parameters=${getParameters({ files })}`; - throw new Error(`${originalErrorMessage}\n\nPlay with the failure here: ${url}`); - } -} - -fc.assert( - fc.property(...), - { - reporter: buildCodeSandboxReporter(counterexample => ({ - 'index.js': { - content: 'console.log("Code to reproduce the issue")' - } - })) - } -) -``` - -The official documentation explaining how to build CodeSandbox environments from an url is available here: https://codesandbox.io/docs/importing#get-request - -## Migrate from jsverify to fast-check - -The npm package [jsverify-to-fast-check](https://www.npmjs.com/package/jsverify-to-fast-check) comes with a set of tools to help users to migrate from jsverify to fast-check smoothly. - -## Supported targets from node to deno - -Here are some alternatives ways to import fast-check into your project. - -Node with CommonJS: -```js -const fc = require('fast-check'); -``` - -Node with ES Modules: -```js -import fc from 'fast-check'; -``` - -Deno: -```js -import fc from "https://cdn.skypack.dev/fast-check"; -``` - -Web Browser: -```html - -``` - -More details on [pika](https://www.pika.dev/npm/fast-check/). - -## Override default toString for a given instance - -By default `fast-check` will serialize the generated values using it's internal stringify helper: `fc.stringify`. From time to time you may be interested in having better stringified versions of your instances. In order to do that `fast-check` offers multiple solutions: - -1. If your instance defines a `toString` method then `fast-check` will use it to properly report this instance. Except if one of the following one has been defined (they have higher priorities). -2. But defining `toString` might be to intrusive. In such case `fast-check` comes with two methods: `fc.toStringMethod` and `fc.asyncToStringMethod`. - -Most of the time you may only need to define `fc.toStringMethod`. `fc.toStringMethod` is the serializer method that will be used by `fast-check` to serialize your instance whatever the context: asynchronous or synchronous properties. - -```ts -Object.defineProperties( - myInstanceWithoutCustomToString, - { [fc.toStringMethod]: { value: () => 'my-value' } } -); -// here your instance defines a custom serializer that will be used by fast-check -// whenever needed -``` - -But when working with asynchronous values, you may need an async code to get back the value. For instance: - -```ts -Object.defineProperties( - myPromisePossiblyResolved, - { [fc.asyncToStringMethod]: { value: async () => { - const resolved = await myPromisePossiblyResolved; - return `My value: ${resolved}`; - } } } -); -``` - -Please note that: -1. `fc.asyncToStringMethod` will only be used in the context of asynchronous properties -2. While `fc.asyncToStringMethod` is marked as asynchronous it should not be too long. More precisely it should resolve barely instantly. - -## Larger entries by default - -By default all arbitraries have their size defaulted to `"small"`. In other words, it means that whenever you ask the framework to generate array-like entities they will have a _small_ number of items. By _small_, we mean that when you ask for `fc.array(fc.nat())`, you will only see arrays having between `0` and `10` elements. - -There are two main ways to change this upper bound: -- at instantiation level by passing an explicit size, like in: `fc.array(fc.nat(), {size: '+1'})` -- at global level - -At global level, there are two main options: -- `baseSize` — defaulted to `"small"` — define what should be the default size when nothing has been specified at instantiation level -- `defaultSizeToMaxWhenMaxSpecified` — defaulted to `true` — when set to `true`, any arbitrary being instantiated with an upper bound (such as `maxLength`) and no size will see it's size defaulted to `max` / when set to `false`, if not defined the size will be defaulted to `baseSize` (see above) - -You may want to read more about ways to configure global settings at [Setup global settings](#setup-global-settings). +# [:house:](../README.md) Tips + +Simple tips to unlock all the power of fast-check with only few changes. + +## Table of contents + +- [Filter invalid combinations using pre-conditions](#filter-invalid-combinations-using-pre-conditions) +- [Value depending on another one](#value-depending-on-another-one) +- [Model based testing or UI test](#model-based-testing-or-ui-test) +- [Detect race conditions](#detect-race-conditions) +- [Opt for verbose failures](#opt-for-verbose-failures) +- [Log within a predicate](#log-within-a-predicate) +- [Preview generated values](#preview-generated-values) +- [Replay after failure](#replay-after-failure) +- [Replay after failure for commands](#replay-after-failure-for-commands) +- [Add custom examples next to generated ones](#add-custom-examples-next-to-generated-ones) +- [Simplify user definable corner cases](#simplify-user-definable-corner-cases) +- [Combine with other faker or random generator libraries](#combine-with-other-faker-or-random-generator-libraries) +- [Setup global settings](#setup-global-settings) +- [Avoid tests to reach the timeout of your test runner](#avoid-tests-to-reach-the-timeout-of-your-test-runner) +- [Customize the reported error](#customize-the-reported-error) +- [Create a CodeSandbox link on error](#create-a-codesandbox-link-on-error) +- [Migrate from jsverify to fast-check](#migrate-from-jsverify-to-fast-check) +- [Supported targets from node to deno](#supported-targets-from-node-to-deno) +- [Override default toString for a given instance](#override-default-toString-for-a-given-instance) +- [Larger entries by default](#larger-entries-by-default) + +## Filter invalid combinations using pre-conditions + +Filtering invalid combinations of generated entries can be done in two ways in fast-check: + +- at arbitrary level using `.filter(...)` +- at property level using `fc.pre(expectedToBeTrue)` + +This part describes the usage of `fc.pre(...)`. More details on `.filter(...)` in [Advanced Arbitraries](./AdvancedArbitraries.md). + +`fc.pre(...)` can be used anywhere within check functions. For instance, you might write: + +```js +fc.assert( + fc.property(fc.nat(), fc.nat(), (a, b) => { + // runs not having a < b will be disgarded + fc.pre(a < b); + // ... your code + // ... and possibly other preconditions using fc.pre(...) + }) +); +``` + +Whenever it encounters a failing precondition, the framework generates another value and forgets about this run - _neither failed nor succeeded_. + +The advantage of `fc.pre(...)` over `.filter(...)` is that runs having too many rejected values will be marked as faulty. When used in combination of `fc.check(...)` it can help to design new filtered arbitraries as the number of skipped values will be computed and available in the output. + +However when your arbitrary is safe enough, switching to `.filter(...)` might be considered for two reasons: + +- easier to share the arbitrary across multiple tests +- higher performances - contrary to `fc.pre`, `fc.filter` is not exception-based making it faster + +## Value depending on another one + +A frequently asked question is: _How to build two values depending from each others with fast-check?_ + +There are multiple ways to do that and all of them have their strengths: easier to write, faster to run, more efficient for shrinking... +Let's dig into some of them through a very simple example: we want to generate `a` and `b` such that `a ≤ b`. + +**Option 1** - `.filter` - discards half of the generated values, can infinitely loop if condition is too strict + +```js +fc.assert( + fc.property( + fc.tuple(fc.nat(), fc.nat()).filter(([a, b]) => a <= b), + ([a, b]) => { + expect(a).toBeGreaterThanOrEqualTo(b); + } + ) +); +``` + +**Option 2** - `fc.pre` - discards half of the generated values, stop when too many retries + +```js +fc.assert( + fc.property(fc.nat(), fc.nat(), (a, b) => { + fc.pre(a <= b); + expect(a).toBeGreaterThanOrEqualTo(b); + }) +); +``` + +**Option 3** - `.chain` - shrinker might not shrink towards minimal cases + +```js +fc.assert( + fc.property( + fc.nat().chain((n) => fc.tuple(fc.nat({ max: n }), fc.constant(n))), + ([a, b]) => { + expect(a).toBeGreaterThanOrEqualTo(b); + } + ) +); +``` + +**Option 4** - `.map` - sometimes complex to write + +```js +fc.assert( + fc.property( + fc.tuple(fc.nat(), fc.nat()).map(([a, b]) => (a <= b ? [a, b] : [b, a])), + ([a, b]) => { + expect(a).toBeGreaterThanOrEqualTo(b); + } + ) +); +``` + +or + +```js +fc.assert( + fc.property( + fc.tuple(fc.nat(), fc.nat()).map(([a, b]) => [a, a + b]), + ([a, b]) => { + expect(a).toBeGreaterThan(b); + } + ) +); +``` + +## Model based testing or UI test + +Model based testing approach have been introduced into fast-check to ease UI testing or state machine tests. + +The idea of the approach is to define commands that could be applied to your system. The framework then picks zero, one or more commands and run them sequentially if they can be executed on the current state. + +A full example is available [here](https://github.com/dubzzz/fast-check/tree/main/example/004-stateMachine/musicPlayer). + +Let's take the case of a list class with `pop`, `push`, `size` methods as an example. + +```typescript +class List { + data: number[] = []; + push = (v: number) => this.data.push(v); + pop = () => this.data.pop()!; + size = () => this.data.length; +} +``` + +Model based testing requires a model. A model is a simplified version of the real system. In this precise case our model would contain only a single integer representing the size of the list. + +```typescript +type Model = { num: number }; +``` + +Then we have to define a command for each of the available operations on our list. Commands come with two methods: + +- `check(m: Readonly): boolean`: true if the command can be executed given the current state +- `run(m: Model, r: RealSystem): void`: execute the command on the system and update the model accordingly. Check for potential problems or inconsistencies between the model and the real system - throws in such case. + +```typescript +class PushCommand implements fc.Command { + constructor(readonly value: number) {} + check = (m: Readonly) => true; + run(m: Model, r: List): void { + r.push(this.value); // impact the system + ++m.num; // impact the model + } + toString = () => `push(${this.value})`; +} +class PopCommand implements fc.Command { + check(m: Readonly): boolean { + // should not call pop on empty list + return m.num > 0; + } + run(m: Model, r: List): void { + assert.equal(typeof r.pop(), 'number'); + --m.num; + } + toString = () => 'pop'; +} +class SizeCommand implements fc.Command { + check = (m: Readonly) => true; + run(m: Model, r: List): void { + assert.equal(r.size(), m.num); + } + toString = () => 'size'; +} +``` + +Now that all or commands are ready we can run everything: + +```typescript +// define the possible commands and their inputs +const allCommands = [ + fc.integer().map((v) => new PushCommand(v)), + fc.constant(new PopCommand()), + fc.constant(new SizeCommand()), +]; +// run everything +fc.assert( + fc.property(fc.commands(allCommands, { size: '+1' }), (cmds) => { + const s = () => ({ model: { num: 0 }, real: new List() }); + fc.modelRun(s, cmds); + }) +); +``` + +The code above can easily be applied to other state machines, APIs or UI. In the case of asynchronous operations you need to implement `AsyncCommand` and use `asyncModelRun`. + +**NOTE:** Contrary to other arbitraries, commands built using `fc.commands` requires an extra parameter for replay purposes. In addition of passing `{ seed, path }` to `fc.assert`, `fc.commands` must be called with `{ replayPath: string }`. + +## Detect race conditions + +Even if JavaScript is mostly a mono-threaded language, it is quite easy to introduce race conditions in your code. + +`fast-check` comes with a built-in feature accessible through `fc.scheduler` that will help you to detect such issues earlier during the development. It basically re-orders the execution of your promises or async tasks in order to make it crash under unexpected orderings. + +The best way to see it in action is certainly to check the snippets provided in our [CodeSandbox@005-race](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?hidenavigation=1&module=%2F005-race%2Fautocomplete%2Fmain.spec.tsx&previewwindow=tests). + +Here is a very simple React-based example that you can play with on [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?hidenavigation=1&module=%2F005-race%2FuserProfile%2Fmain.spec.tsx&previewwindow=tests): + +```jsx +/* Component */ + +import { getUserProfile } from './api.js'; +function UserPageProfile(props) { + const { userId } = props; + const [userData, setUserData] = React.useState(null); + + React.useEffect(() => { + const fetchUser = async () => { + const data = await getUserProfile(props.userId); + setUserData(data); + }; + fetchUser(); + }, [userId]); + + if (userData === null) { + return
Loading...
; + } + return ( +
+
Id: {userData.id}
+
Name: {userData.name}
+
+ ); +} + +/* Test with react testing library */ + +test('should not display data related to another user', () => + fc.assert( + fc + .asyncProperty(fc.uuid(), fc.uuid(), fc.scheduler(), async (uid1, uid2, s) => { + // Arrange + getUserProfile.mockImplementation(s.scheduleFunction(async (userId) => ({ id: userId, name: userId }))); + + // Act + const { rerender, queryByTestId } = render(); + s.scheduleSequence([ + async () => { + rerender(); + }, + ]); + while (s.count() !== 0) { + await act(async () => { + await s.waitOne(); + }); + } + + // Assert + expect((await queryByTestId('user-id')).textContent).toBe(`Id: ${uid2}`); + }) + .beforeEach(async () => { + jest.resetAllMocks(); + cleanup(); + }) + )); +``` + +In case of failure, the reported error will contain the scheduler that caused the issue along with other generated values if any. +Here is what an error can look like in case we only asked for a scheduler: + +``` + Property failed after 1 tests + { seed: -22040264, path: "0", endOnFailure: true } + Counterexample: [schedulerFor()` + -> [task${1}] promise resolved with value "A" + -> [task${3}] promise resolved with value "C" + -> [task${2}] promise resolved with value "B"`] + Shrunk 0 time(s) +``` + +Given such failure you can either replay it by using the provided `{ seed, path, endOnFailure }` - _see [Replay after failure](#replay-after-failure)_ - +or put the scheduler as an example to be used for every future run - _see [Add custom examples next to generated ones](#add-custom-examples-next-to-generated-ones)_. + +If you want to add this example in your set of custom examples you have to use `fc.schedulerFor` and copy the counterexample coming from the stack trace into the `examples` given to `fc.assert` as follow: + +```js +test('should run with custom scheduler then generated ones', () => + fc.assert( + fc.property(fc.scheduler(), (s) => { + /* Test */ + }), + { + examples: [ + [ + fc.schedulerFor()` + -> [task${1}] promise resolved with value "A" + -> [task${3}] promise resolved with value "C" + -> [task${2}] promise resolved with value "B"`, + ], + ], + } + )); +``` + +## Opt for verbose failures + +By default, the failures reported by `fast-check` feature most relevant data: + +- seed +- path towards the minimal counterexample +- number of tries before the first failure +- depth of the shrink +- minimal counterexample + +`fast-check` comes with a verbose mode, which can help users while trying to dig into a failure. + +For instance, let's suppose the following property failed: + +```js +fc.assert(fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => contains(a + b + c, b))); +``` + +The output will look something like: + +``` +Error: Property failed after 1 tests (seed: 1527423434693, path: 0:0:0): ["","",""] +Shrunk 1 time(s) +Got error: Property failed by returning false + +Hint: Enable verbose mode in order to have the list of all failing values encountered during the run +``` + +In order to enable the `verbose` mode, we just need to give a second parameter to `fc.assert` as follow: + +```js +fc.assert( + fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => contains(a + b + c, b)), + { verbose: true } +); +``` + +Verbose logs give more details on the error as they will contain all the counterexamples encountered while shrinking the inputs. The example above results in: + +``` +Error: Property failed after 1 tests (seed: 1527423434693, path: 0:0:0): ["","",""] +Shrunk 2 time(s) +Got error: Property failed by returning false + +Encountered failures were: +- ["","JeXPqIQ6",">q"] +- ["","",">q"] +- ["","",""] +``` + +With that output, we notice that our `contains` implementation seems to fail when the `pattern` we are looking for is the beginning of the string we are looking in. + +Verbosity can be set to produce even more verbose logs by setting `verbose` flag to: + +- `0`: `None` - default, equivalent to `false` +- `1`: `Verbose` - equivalent to `true` +- `2`: `VeryVerbose` - logs all the produced values in case of failure + +Refer to `fc.VerbosityLevel` for more details. + +## Log within a predicate + +In order to ease the diagnosis of red properties, fast-check introduced an internal logger that can be used to log stuff inside the predicate itself. + +The advantage of this logger is that one logger is linked to one run so that the counterexample comes with its own logs (and not the ones of previous failures leading to this counterexample). Logs will only be shown in case of failure contrary to `console.log` that would pop everywhere. + +Usage is quite simple, logger is one of the features available inside the `Context` interface: + +```typescript +fc.assert( + fc.property( + fc.string(), + fc.string(), + fc.context(), // comes with a log method + (a: number, b: number, ctx: fc.Context): boolean => { + const intermediateResult = /* ... */; + ctx.log(`Intermediate: ${intermediateResult}`); + return check(intermediateResult); + } + ) +) +``` + +## Preview generated values + +Before writing down your test, it might be great to confirm that the arbitrary you will be using produce the values you want. + +This can be done very easily by using either `fc.sample` or `fc.statistics`. + +The following code constructs an array containing the first 10 values that would have been generated by the arbitrary `fc.anything()` if used inside a `fc.assert` or `fc.check`: + +```typescript +fc.sample( + fc.anything(), // arbitrary or property to extract the values from + 10 // number of values to extract +); +``` + +In some cases, having a sample is not enough and we want more insights about the generated data. +For instance, I might be interested by the share of even numbers generated by `fc.nat()`. +For that purpose I can use `fc.statistics` as follow: + +```typescript +fc.statistics( + fc.nat(), // arbitrary or property to extract the values from + (n) => (n % 2 === 0 ? 'Even number' : 'Odd number'), // classifier + 10000 // number of values to extract +); +// Possible output (console.log): +// Odd number...50.30% +// Even number..49.70% +``` + +## Replay after failure + +`fast-check` comes with a must-have feature: replay a failing case immediately given its seed and path (seed only to replay all). + +Whenever `fc.assert` encounters a failure, it displays an error log featuring both the seed and the path to replay it. For instance, in the output below the seed is 1525890375951 and the path 0:0. + +``` +Error: Property failed after 1 tests +{ seed: 1525890375951, path: 0:0, endOnFailure: true } +Counterexample: [0] +Shrunk 1 time(s) +Got error: Property failed by returning false +``` + +In order to replay the failure on the counterexample - `[0]`, you have to change your code as follow: + +```typescript +// Original code +fc.assert(fc.property(fc.nat(), checkEverythingIsOk)); + +// Replay code: straight to the minimal counterexample +// Only replay the minimal counterexample +fc.assert(fc.property(fc.nat(), checkEverythingIsOk), { + seed: 1525890375951, + path: '0:0', + endOnFailure: true, +}); +``` + +**NOTE:** Replaying `fc.commands` requires passing an additional flag called `replayPath` when building this arbitrary (see below). + +## Replay after failure for commands + +As any other built-in arbitrary, `fc.commands` is replayable but the process is a bit different. + +Whenever `fc.assert` encounters a failure with `fc.commands`, it displays an error log featuring both the seed, path and replayPath to replay it. For instance, in the output below the seed is 670108017, the path 96:5 and the replayPath is AAAAABAAE:VF. + +``` +Property failed after 97 tests +{ seed: 670108017, path: "96:5", endOnFailure: true } +Counterexample: [PlayToken[0],NewGame,PlayToken[1],Refresh /*replayPath="AAAAABAAE:VF"*/] +Shrunk 1 time(s) +Got error: Error: expect(received).toEqual(expected) +``` + +In order to replay the failure on the counterexample - `[PlayToken[0],NewGame,PlayToken[1],Refresh]`, you have to change your code as follow: + +```typescript +// Original code +fc.assert( + fc.property( + fc.commands(/* array of commands */), + checkEverythingIsOk + ) +); + +// Replay code: straight to the minimal counterexample +// Only replay the minimal counterexample +fc.assert( + fc.property( + fc.commands( + /* array of commands */, + { + replayPath: "AAAAABAAE:VF" + } + ), + checkEverythingIsOk + ), + { + seed: 670108017, + path: "96:5", + endOnFailure: true + } +); +``` + +**NOTE:** Why is there something specific to do for `fc.commands`? +In order to come with a more efficient and faster shrinker, `fc.commands` takes into account the commands that have really been executed. +Basically if the framework generated the following commands `[A,B,C,A,A,C]` but only executed `[A,-,C,A,-,-]` it will shrink only `[A,C,A]`. +The value stored into `replayPath` encodes the history of what was really executed in order not re-run anything on replay. + +## Add custom examples next to generated ones + +Sometimes it might be useful to run your test on some custom examples you think useful: either because they caused your code to fail in the past or because you explicitly want to confirm it succeeds on this specific example. + +Whatever the reason, the framework provides you the ability to set a custom list of examples into the settings of `fc.assert`. +Those examples will be executed first followed by the values generated by the framework. It does not impact the number of values that will be tested against your property - _meaning that if you add 5 custom examples, you remove 5 generated values from the run_. + +The syntax is the following: + +```typescript +// For a one parameter property +fc.assert(fc.property(fc.nat(), myCheckFunction), { + examples: [ + [0], // first example I want to test + [Number.MAX_SAFE_INTEGER], + ], +}); + +// For a multiple parameters property +fc.assert(fc.property(fc.string(), fc.string(), fc.string(), myCheckFunction), { + examples: [['', '', '']], +}); +``` + +Please keep in mind that property based testing frameworks are fully able to find corner-cases with no help at all. + +## Simplify user definable corner cases + +Sometimes, you may discover a bug even before you took time to write a test for it. +But, from time to time, the corner cases you might not be that easy to troubleshoot and a smaller case would clearly help you in your investigation. + +Since version 2.19.0, fast-check comes with a built-in way to shrink automatically user definable examples defined in `examples`. + +Once you have a property that fails with your case (possibly just something like: should not crash), you just have to pass the corner case within `examples` and let fast-check reduce it to something simpler to troubleshoot. + +```typescript +fc.assert(fc.property(fc.array(fc.string()), myCheckFunction), { + examples: [ + // the user definable corner case + [['__', 'proto', '__']], + ], +}); +``` + +Please note that currently if you want fast-check to shrink values for you, you have to give `map` a way to unmap the values. +Most of the other built-in arbitraries come with built-in support, so no special treatment for `fc.record`, `fc.string` or others. +Please note that for the moment `chain` is not supported, as a consequence arbitraries defined via `chain` will not be able to shrink user definable values. + +If you want to use it with `map`, the syntax is a bit more verbose at the moment, but it will be less verbose starting at 3.x (no more need for `fc.convertFromNext` or `fc.convertToNext`). + +```typescript +fc.assert( + fc.property( + fc.convertFromNext( + fc.convertToNext(fc.array(fc.string())).map( + (arr) => arr.join(','), + (raw) => { + // unmapper is supposed to handle not supported values by throwing + if (typeof raw !== 'string') throw new Error('Unsupported'); + // remaning is supported + return raw.split(','); + } + ) + ), + myCheckFunction + ), + { + examples: [ + // the user definable corner case + ['__,proto,__'], + ], + } +); +``` + +## Combine with other faker or random generator libraries + +In order to integrate external faker or random generator libraries within fast-check, the generators have to be wrapped as arbitraries. + +The minimal requirement that needs to be fulfilled by the wrapped library is to provide a way to be seeded and reproducible. fast-check cannot offer replay capabilities if the underlying generators are not able to generate the same values from one run to another. + +Here are some examples of how external faker libraries can be wrapped within fast-check. + +With [faker](https://www.npmjs.com/package/@faker-js/faker) - seed based faker: + +```js +const fc = require('fast-check'); +const { faker } = require('@faker-js/faker'); + +const fakerToArb = (fakerGen) => { + return fc + .integer() + .noBias() // same probability to generate each of the allowed integers + .noShrink() // shrink on a seed makes no sense + .map((seed) => { + faker.seed(seed); // seed the generator + return fakerGen(); // call it + }); +}; + +const streetAddressArb = fakerToArb(faker.address.streetAddress); +const customArb = fakerToArb(() => faker.fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}')); +``` + +With [lorem-ipsum](https://www.npmjs.com/package/lorem-ipsum) - random generator based faker: + +```js +const fc = require('fast-check'); +const { loremIpsum } = require('lorem-ipsum'); + +const loremArb = fc + .infiniteStream(fc.double().noBias()) + .noShrink() + .map((s) => { + const rng = () => s.next().value; // prng like Math.random but controlled by fast-check + return loremIpsum({ random: rng }); + }); +``` + +Please note that in the two examples above, the resulting arbitraries will not have full shrinking capabilities. But they offer a full support for random value generation. + +## Setup global settings + +All the runners provided by fast-check come with an optional parameter to customize how the runner will behave (see `fc.assert`, `fc.check`, `fc.sample` or `fc.statistics`). In the past, this parameter had to be provided runner by runner otherwise user would have fallen back on the default values hardcoded in fast-check code. For instance, if one wanted to override the default number of runs of properties, it would have written: + +```typescript +test('test #1', () => { + fc.assert(myProp1, { numRuns: 10 }); +}); +test('test #2', () => { + fc.assert( + myProp2, + { numRuns: 10 } // duplicated + ); +}); +``` + +Starting at version `1.18.0`, the code above can be changed into: + +```typescript +fc.configureGlobal({ numRuns: 10 }); // see below for the recommended way (Jest/Mocha) +test('test #1', () => { + fc.assert(myProp1); +}); +test('test #2', () => { + fc.assert(myProp2); +}); +``` + +**With Mocha** + +_Create a new setup file that will be executed before executing the test code itself - use `--file=mocha.setup.js` option to reference this file_ + +```js +// mocha.setup.js +const fc = require('fast-check'); +fc.configureGlobal({ numRuns: 10 }); +``` + +**With Jest** + +_Edit the configuration of Jest to add your own setup file - usually the configuration is defined in jest.config.js_ + +```js +// jest.config.js +module.exports = { + setupFiles: ['./jest.setup.js'], +}; +``` + +_Create a new setup file that will be executed before executing the test code itself_ + +```js +// jest.setup.js +const fc = require('fast-check'); +fc.configureGlobal({ numRuns: 10 }); +``` + +## Avoid tests reaching the timeout of your test runner + +Most of the time, test runners like Jest, Mocha or even Jasmine come with default timeouts. Whenever one test takes longer than this time limit, the test runner might stop it immediately. Unfortunately whenever fast-check gets stopped at the middle of a run, it cannot give back the seed nor the path that were used during this test. + +Here are some possible reasons why you may encounter timeouts with property based testing: + +- (1) an entry generated by fast-check took longer than expected +- (2) shrinking process takes longer than expected - the main target of shrinking process is to report the user with the very minimal failing case, in order to achieve that it has to try many sub-inputs + +In order to prevent your tests from timing out in your CI, you may [setup global settings](#setup-global-settings) with the following configuration: + +```js +fc.configureGlobal({ + interruptAfterTimeLimit: 4000, // Default timeout in Jest 5000ms + markInterruptAsFailure: true, // When set to true, timeout during initial cases (1) will be marked as an error + // When set to false, timeout during initial cases (1) will not be considered as a failure +}); +``` + +If you opt for `markInterruptAsFailure: true`, you can still limit the time taken by long running tests locally by tweaking the settings passed into `fc.assert` with a value of `numRuns` smaller than your default one. + +## Customize the reported error + +By default, `fc.assert` automatically handles and formats the failures that occur when running your properties. + +Nonetheless, in some cases you might be interested into customizing, extending or even changing what should be a failure or how it should be formated. +In order to customize it, you can define your own reporting strategy by passing a custom reporter to `fc.assert`: + +```javascript +fc.assert( + // You can either use it with `fc.property` + // or `fc.asyncProperty` + fc.property(...), + { + reporter(out) { + // Let's say we want to re-create the default reporter of `assert` + if (out.failed) { + // `defaultReportMessage` is an utility that make you able to have the exact + // same report as the one that would have been generated by `assert` + throw new Error(fc.defaultReportMessage(out)); + } + } + } +) +``` + +In case your reporter is relying on asynchronous code, you can specify it by setting `asyncReporter` instead of `reporter`. +Contrary to `reporter` that will be used for both synchronous and asynchronous properties, `asyncReporter` is forbidden for synchronous properties and makes them throw. + +In the past, before `reporter` or `asyncReporter`, writing your own `fc.assert` including your own reporter would have been written as follow: + +```javascript +const throwIfFailed = (out) => { + if (out.failed) { + throw new Error(fc.defaultReportMessage(out)); + } +}; +const myCustomAssert = (property, parameters) => { + const out = fc.check(property, parameters); + + if (property.isAsync()) { + return out.then((runDetails) => { + throwIfFailed(runDetails); + }); + } + throwIfFailed(out); +}; +``` + +## Create a CodeSandbox link on error + +_If you have not read about ways to customize the reporter used by `fc.assert` please refer to the section above._ + +In some situations, it can be useful to directly publish a minimal reproduction of an issue in order to be able to play with it. +Custom reporters can be used to provide such capabilities. + +For instance, you can automatically generate CodeSandbox environments in case of failed property with the snippet below: + +```javascript +import { getParameters } from 'codesandbox/lib/api/define'; + +const buildCodeSandboxReporter = (createFiles) => { + return function reporter(runDetails) { + if (!runDetails.failed) { + return; + } + const counterexample = runDetails.counterexample; + const originalErrorMessage = fc.defaultReportMessage(runDetails); + if (counterexample === undefined) { + throw new Error(originalErrorMessage); + } + const files = { + ...createFiles(counterexample), + 'counterexample.js': { + content: `export const counterexample = ${fc.stringify(counterexample)}` + }, + 'report.txt': { + content: originalErrorMessage + } + } + const url = `https://codesandbox.io/api/v1/sandboxes/define?parameters=${getParameters({ files })}`; + throw new Error(`${originalErrorMessage}\n\nPlay with the failure here: ${url}`); + } +} + +fc.assert( + fc.property(...), + { + reporter: buildCodeSandboxReporter(counterexample => ({ + 'index.js': { + content: 'console.log("Code to reproduce the issue")' + } + })) + } +) +``` + +The official documentation explaining how to build CodeSandbox environments from an url is available here: https://codesandbox.io/docs/importing#get-request + +## Migrate from jsverify to fast-check + +The npm package [jsverify-to-fast-check](https://www.npmjs.com/package/jsverify-to-fast-check) comes with a set of tools to help users to migrate from jsverify to fast-check smoothly. + +## Supported targets from node to deno + +Here are some alternatives ways to import fast-check into your project. + +Node with CommonJS: + +```js +const fc = require('fast-check'); +``` + +Node with ES Modules: + +```js +import fc from 'fast-check'; +``` + +Deno: + +```js +import fc from 'https://cdn.skypack.dev/fast-check'; +``` + +Web Browser: + +```html + +``` + +More details on [pika](https://www.pika.dev/npm/fast-check/). + +## Override default toString for a given instance + +By default `fast-check` will serialize the generated values using it's internal stringify helper: `fc.stringify`. From time to time you may be interested in having better stringified versions of your instances. In order to do that `fast-check` offers multiple solutions: + +1. If your instance defines a `toString` method then `fast-check` will use it to properly report this instance. Except if one of the following one has been defined (they have higher priorities). +2. But defining `toString` might be to intrusive. In such case `fast-check` comes with two methods: `fc.toStringMethod` and `fc.asyncToStringMethod`. + +Most of the time you may only need to define `fc.toStringMethod`. `fc.toStringMethod` is the serializer method that will be used by `fast-check` to serialize your instance whatever the context: asynchronous or synchronous properties. + +```ts +Object.defineProperties(myInstanceWithoutCustomToString, { [fc.toStringMethod]: { value: () => 'my-value' } }); +// here your instance defines a custom serializer that will be used by fast-check +// whenever needed +``` + +But when working with asynchronous values, you may need an async code to get back the value. For instance: + +```ts +Object.defineProperties(myPromisePossiblyResolved, { + [fc.asyncToStringMethod]: { + value: async () => { + const resolved = await myPromisePossiblyResolved; + return `My value: ${resolved}`; + }, + }, +}); +``` + +Please note that: + +1. `fc.asyncToStringMethod` will only be used in the context of asynchronous properties +2. While `fc.asyncToStringMethod` is marked as asynchronous it should not be too long. More precisely it should resolve barely instantly. + +## Larger entries by default + +By default all arbitraries have their size defaulted to `"small"`. In other words, it means that whenever you ask the framework to generate array-like entities they will have a _small_ number of items. By _small_, we mean that when you ask for `fc.array(fc.nat())`, you will only see arrays having between `0` and `10` elements. + +There are two main ways to change this upper bound: + +- at instantiation level by passing an explicit size, like in: `fc.array(fc.nat(), {size: '+1'})` +- at global level + +At global level, there are two main options: + +- `baseSize` — defaulted to `"small"` — define what should be the default size when nothing has been specified at instantiation level +- `defaultSizeToMaxWhenMaxSpecified` — defaulted to `true` — when set to `true`, any arbitrary being instantiated with an upper bound (such as `maxLength`) and no size will see it's size defaulted to `max` / when set to `false`, if not defined the size will be defaulted to `baseSize` (see above) + +You may want to read more about ways to configure global settings at [Setup global settings](#setup-global-settings). diff --git a/example/005-race/autocomplete/main.spec.tsx b/example/005-race/autocomplete/main.spec.tsx index 1535b24f..57f97dcd 100644 --- a/example/005-race/autocomplete/main.spec.tsx +++ b/example/005-race/autocomplete/main.spec.tsx @@ -1,138 +1,138 @@ -import fc from 'fast-check'; -import * as React from 'react'; - -import AutocompleteField from './src/AutocompleteField'; -//import AutocompleteField from './src/AutocompleteFieldMostRecentQuery'; -//import AutocompleteField from './src/AutocompleteFieldSimple'; - -import { render, cleanup, fireEvent, act, getNodeText, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { search } from './src/Api'; - -// If you want to test the behaviour of fast-check in case of a bug: -const bugs = { - // enableBugBetterResults: true, - // enableBugUnfilteredResults: true, - // enableBugUnrelatedResults: true, - // enableBugDoNotDiscardOldQueries: true -}; - -if (!fc.readConfigureGlobal()) { - // Global config of Jest has been ignored, we will have a timeout after 5000ms - // (CodeSandbox falls in this category) - fc.configureGlobal({ interruptAfterTimeLimit: 4000 }); -} - -describe('AutocompleteField', () => { - it('should suggest results matching the value of the autocomplete field', async () => { - await fc.assert( - fc - .asyncProperty(AllResultsArbitrary, QueriesArbitrary, fc.scheduler({ act }), async (allResults, queries, s) => { - // Arrange - const searchImplem: typeof search = s.scheduleFunction(function search(query, maxResults) { - return Promise.resolve(allResults.filter((r) => r.includes(query)).slice(0, maxResults)); - }); - - // Act - render(); - const input = screen.getByRole('textbox') as HTMLElement; - s.scheduleSequence(buildAutocompleteEvents(input, queries)); - - // Assert - while (s.count() !== 0) { - await s.waitOne(); - - const autocompletionValue = input.attributes.getNamedItem('value')!.value; - const suggestions = (screen.queryAllByRole('listitem') as HTMLElement[]).map(getNodeText); - if (!suggestions.every((suggestion) => suggestion.includes(autocompletionValue))) { - throw new Error( - `Invalid suggestions for ${JSON.stringify(autocompletionValue)}, got: ${JSON.stringify(suggestions)}` - ); - } - } - }) - .beforeEach(async () => { - jest.resetAllMocks(); - await cleanup(); - }) - ); - }); - - it('should display more and more sugestions as results come', async () => { - await fc.assert( - fc - .asyncProperty(AllResultsArbitrary, QueriesArbitrary, fc.scheduler({ act }), async (allResults, queries, s) => { - // Arrange - const query = queries[queries.length - 1]; - const searchImplem: typeof search = s.scheduleFunction(function search(query, maxResults) { - return Promise.resolve(allResults.filter((r) => r.includes(query)).slice(0, maxResults)); - }); - - // Act - render(); - const input = screen.getByRole('textbox') as HTMLElement; - for (const event of buildAutocompleteEvents(input, queries)) { - await event.builder(); - } // All the user's inputs have been fired onto the AutocompleField - - // Assert - let suggestions: string[] = []; - while (s.count() !== 0) { - // Resolving one async query in a random order - await s.waitOne(); - - // Read suggestions shown by the component - const prevSuggestions = suggestions; - suggestions = (screen.queryAllByRole('listitem') as HTMLElement[]).map(getNodeText); - - // We expect the number of suggestions to increase up to the final number - // of suggestions for or 10 (max number of suggestions) - if (suggestions.length < prevSuggestions.length) { - const got = JSON.stringify({ - prevSuggestions, - suggestions, - }); - throw new Error(`We expect to have more and more suggestions as we resolve queries, got: ${got}`); - } - } - // At the end we expect to get results matching - if (!suggestions.every((s) => s.startsWith(query))) { - throw new Error(`Must start with ${JSON.stringify(query)}, got: ${JSON.stringify(suggestions)}`); - } - }) - .beforeEach(async () => { - jest.resetAllMocks(); - await cleanup(); - }) - ); - }); -}); - -// Helpers - -const AllResultsArbitrary = fc.uniqueArray(fc.string(), { maxLength: 1000 }); -const QueriesArbitrary = fc.array(fc.string(), { minLength: 1 }); - -/** - * Generate a sequence of events that have to be fired onto the component - * in order to send it all the queries (characters are fired one by one) - */ -const buildAutocompleteEvents = (input: HTMLElement, queries: string[]) => { - const autocompleteEvents: Exclude any>[] = []; - - for (const query of queries) { - for (let numCharacters = 0; numCharacters <= query.length; ++numCharacters) { - const subQuery = query.substring(0, numCharacters); - const builder = async () => { - await act(async () => { - fireEvent.change(input, { target: { value: subQuery } }); - }); - }; - const label = `typing(${JSON.stringify(subQuery)})`; - autocompleteEvents.push({ builder, label }); - } - } - - return autocompleteEvents; -}; +import fc from 'fast-check'; +import * as React from 'react'; + +import AutocompleteField from './src/AutocompleteField'; +//import AutocompleteField from './src/AutocompleteFieldMostRecentQuery'; +//import AutocompleteField from './src/AutocompleteFieldSimple'; + +import { render, cleanup, fireEvent, act, getNodeText, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import { search } from './src/Api'; + +// If you want to test the behaviour of fast-check in case of a bug: +const bugs = { + // enableBugBetterResults: true, + // enableBugUnfilteredResults: true, + // enableBugUnrelatedResults: true, + // enableBugDoNotDiscardOldQueries: true +}; + +if (!fc.readConfigureGlobal()) { + // Global config of Jest has been ignored, we will have a timeout after 5000ms + // (CodeSandbox falls in this category) + fc.configureGlobal({ interruptAfterTimeLimit: 4000 }); +} + +describe('AutocompleteField', () => { + it('should suggest results matching the value of the autocomplete field', async () => { + await fc.assert( + fc + .asyncProperty(AllResultsArbitrary, QueriesArbitrary, fc.scheduler({ act }), async (allResults, queries, s) => { + // Arrange + const searchImplem: typeof search = s.scheduleFunction(function search(query, maxResults) { + return Promise.resolve(allResults.filter((r) => r.includes(query)).slice(0, maxResults)); + }); + + // Act + render(); + const input = screen.getByRole('textbox') as HTMLElement; + s.scheduleSequence(buildAutocompleteEvents(input, queries)); + + // Assert + while (s.count() !== 0) { + await s.waitOne(); + + const autocompletionValue = input.attributes.getNamedItem('value')!.value; + const suggestions = (screen.queryAllByRole('listitem') as HTMLElement[]).map(getNodeText); + if (!suggestions.every((suggestion) => suggestion.includes(autocompletionValue))) { + throw new Error( + `Invalid suggestions for ${JSON.stringify(autocompletionValue)}, got: ${JSON.stringify(suggestions)}` + ); + } + } + }) + .beforeEach(async () => { + jest.resetAllMocks(); + await cleanup(); + }) + ); + }); + + it('should display more and more sugestions as results come', async () => { + await fc.assert( + fc + .asyncProperty(AllResultsArbitrary, QueriesArbitrary, fc.scheduler({ act }), async (allResults, queries, s) => { + // Arrange + const query = queries[queries.length - 1]; + const searchImplem: typeof search = s.scheduleFunction(function search(query, maxResults) { + return Promise.resolve(allResults.filter((r) => r.includes(query)).slice(0, maxResults)); + }); + + // Act + render(); + const input = screen.getByRole('textbox') as HTMLElement; + for (const event of buildAutocompleteEvents(input, queries)) { + await event.builder(); + } // All the user's inputs have been fired onto the AutocompleField + + // Assert + let suggestions: string[] = []; + while (s.count() !== 0) { + // Resolving one async query in a random order + await s.waitOne(); + + // Read suggestions shown by the component + const prevSuggestions = suggestions; + suggestions = (screen.queryAllByRole('listitem') as HTMLElement[]).map(getNodeText); + + // We expect the number of suggestions to increase up to the final number + // of suggestions for or 10 (max number of suggestions) + if (suggestions.length < prevSuggestions.length) { + const got = JSON.stringify({ + prevSuggestions, + suggestions, + }); + throw new Error(`We expect to have more and more suggestions as we resolve queries, got: ${got}`); + } + } + // At the end we expect to get results matching + if (!suggestions.every((s) => s.startsWith(query))) { + throw new Error(`Must start with ${JSON.stringify(query)}, got: ${JSON.stringify(suggestions)}`); + } + }) + .beforeEach(async () => { + jest.resetAllMocks(); + await cleanup(); + }) + ); + }); +}); + +// Helpers + +const AllResultsArbitrary = fc.uniqueArray(fc.string(), { maxLength: 1000 }); +const QueriesArbitrary = fc.array(fc.string(), { minLength: 1 }); + +/** + * Generate a sequence of events that have to be fired onto the component + * in order to send it all the queries (characters are fired one by one) + */ +const buildAutocompleteEvents = (input: HTMLElement, queries: string[]) => { + const autocompleteEvents: Exclude any>[] = []; + + for (const query of queries) { + for (let numCharacters = 0; numCharacters <= query.length; ++numCharacters) { + const subQuery = query.substring(0, numCharacters); + const builder = async () => { + await act(async () => { + fireEvent.change(input, { target: { value: subQuery } }); + }); + }; + const label = `typing(${JSON.stringify(subQuery)})`; + autocompleteEvents.push({ builder, label }); + } + } + + return autocompleteEvents; +}; diff --git a/example/005-race/autocomplete/src/AutocompleteField.tsx b/example/005-race/autocomplete/src/AutocompleteField.tsx index 11509167..5c984695 100644 --- a/example/005-race/autocomplete/src/AutocompleteField.tsx +++ b/example/005-race/autocomplete/src/AutocompleteField.tsx @@ -1,72 +1,72 @@ -import React from 'react'; - -// Injected as a props because CodeSandbox fails to provide jest.mock -// So it makes such import difficult to test -//// import { search } from './Api'; - -type Props = { - enableBugUnrelatedResults?: boolean; - enableBugBetterResults?: boolean; - enableBugUnfilteredResults?: boolean; - search: (query: string, maxResults: number) => Promise; -}; - -export default function AutocompleteField(props: Props) { - const lastQueryRef = React.useRef(''); - const lastSuccessfulQueryRef = React.useRef(''); - const [query, setQuery] = React.useState(lastQueryRef.current); - const [searchResults, setSearchResults] = React.useState([] as string[]); - - React.useEffect(() => { - const runQuery = async () => { - const results = await props.search(query, 10); - - if (!lastQueryRef.current.startsWith(query) && !props.enableBugUnrelatedResults) { - // FIXED BUG: - // We show results for queries that are unrelated to the latest started query - // eg.: AZ resolves while we look for QS, we show its results even if totally unrelated - return; - } - if ( - lastQueryRef.current.startsWith(lastSuccessfulQueryRef.current) && - lastSuccessfulQueryRef.current.length > query.length && - !props.enableBugBetterResults - ) { - // FIXED BUG: - // We might update results while we already received results - // for a query less strict than the last this one - // eg.: We receice AZ while we already have results for AZE - return; - } - - lastSuccessfulQueryRef.current = query; - setSearchResults(results); - }; - - runQuery(); - }, [query, props]); - - return ( -
- { - const value = (evt.target as any).value; - lastQueryRef.current = value; - setQuery(value); - }} - /> -
    - {searchResults - // FIXED BUG: We don't filter the results we receive - // As we want to display results as soon as possible, even if our searchResults - // are related to a past query we want to use them to provide the user with some hints - .filter(r => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) - .map(r => ( -
  • {r}
  • - ))} -
-
- ); -} +import React from 'react'; + +// Injected as a props because CodeSandbox fails to provide jest.mock +// So it makes such import difficult to test +//// import { search } from './Api'; + +type Props = { + enableBugUnrelatedResults?: boolean; + enableBugBetterResults?: boolean; + enableBugUnfilteredResults?: boolean; + search: (query: string, maxResults: number) => Promise; +}; + +export default function AutocompleteField(props: Props) { + const lastQueryRef = React.useRef(''); + const lastSuccessfulQueryRef = React.useRef(''); + const [query, setQuery] = React.useState(lastQueryRef.current); + const [searchResults, setSearchResults] = React.useState([] as string[]); + + React.useEffect(() => { + const runQuery = async () => { + const results = await props.search(query, 10); + + if (!lastQueryRef.current.startsWith(query) && !props.enableBugUnrelatedResults) { + // FIXED BUG: + // We show results for queries that are unrelated to the latest started query + // eg.: AZ resolves while we look for QS, we show its results even if totally unrelated + return; + } + if ( + lastQueryRef.current.startsWith(lastSuccessfulQueryRef.current) && + lastSuccessfulQueryRef.current.length > query.length && + !props.enableBugBetterResults + ) { + // FIXED BUG: + // We might update results while we already received results + // for a query less strict than the last this one + // eg.: We receice AZ while we already have results for AZE + return; + } + + lastSuccessfulQueryRef.current = query; + setSearchResults(results); + }; + + runQuery(); + }, [query, props]); + + return ( +
+ { + const value = (evt.target as any).value; + lastQueryRef.current = value; + setQuery(value); + }} + /> +
    + {searchResults + // FIXED BUG: We don't filter the results we receive + // As we want to display results as soon as possible, even if our searchResults + // are related to a past query we want to use them to provide the user with some hints + .filter((r) => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) + .map((r) => ( +
  • {r}
  • + ))} +
+
+ ); +} diff --git a/example/005-race/autocomplete/src/AutocompleteFieldMostRecentQuery.tsx b/example/005-race/autocomplete/src/AutocompleteFieldMostRecentQuery.tsx index f557d8ae..b68f9d0c 100644 --- a/example/005-race/autocomplete/src/AutocompleteFieldMostRecentQuery.tsx +++ b/example/005-race/autocomplete/src/AutocompleteFieldMostRecentQuery.tsx @@ -1,62 +1,62 @@ -import React from 'react'; - -// Injected as a props because CodeSandbox fails to provide jest.mock -// So it makes such import difficult to test -//// import { search } from './Api'; - -type Props = { - enableBugUnrelatedResults?: boolean; - enableBugUnfilteredResults?: boolean; - search: (query: string, maxResults: number) => Promise; -}; - -export default function AutocompleteField(props: Props) { - const lastQueryRef = React.useRef(''); - const lastQueryIdRef = React.useRef(0); - const lastSuccessfulQueryIdRef = React.useRef(lastQueryIdRef.current); - const [query, setQuery] = React.useState(lastQueryRef.current); - const [searchResults, setSearchResults] = React.useState([] as string[]); - - React.useEffect(() => { - const queryId = ++lastQueryIdRef.current; - const runQuery = async () => { - const results = await props.search(query, 10); - if (lastSuccessfulQueryIdRef.current > queryId) { - return; // A more recent query already succeeded - } - if (!lastQueryRef.current.startsWith(query) && !props.enableBugUnrelatedResults) { - // FIXED BUG: - // We show results for queries that are unrelated to the latest started query - // eg.: AZ resolves while we look for QS, we show its results even if totally unrelated - return; // Current query does not start with the query that just resolved - } - lastSuccessfulQueryIdRef.current = queryId; - setSearchResults(results); - }; - runQuery(); - }, [query, props]); - - return ( -
- { - const value = (evt.target as any).value; - lastQueryRef.current = value; - setQuery(value); - }} - /> -
    - {searchResults - // FIXED BUG: We don't filter the results we receive - // As we want to display results as soon as possible, even if our searchResults - // are related to a past query we want to use them to provide the user with some hints - .filter(r => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) - .map(r => ( -
  • {r}
  • - ))} -
-
- ); -} +import React from 'react'; + +// Injected as a props because CodeSandbox fails to provide jest.mock +// So it makes such import difficult to test +//// import { search } from './Api'; + +type Props = { + enableBugUnrelatedResults?: boolean; + enableBugUnfilteredResults?: boolean; + search: (query: string, maxResults: number) => Promise; +}; + +export default function AutocompleteField(props: Props) { + const lastQueryRef = React.useRef(''); + const lastQueryIdRef = React.useRef(0); + const lastSuccessfulQueryIdRef = React.useRef(lastQueryIdRef.current); + const [query, setQuery] = React.useState(lastQueryRef.current); + const [searchResults, setSearchResults] = React.useState([] as string[]); + + React.useEffect(() => { + const queryId = ++lastQueryIdRef.current; + const runQuery = async () => { + const results = await props.search(query, 10); + if (lastSuccessfulQueryIdRef.current > queryId) { + return; // A more recent query already succeeded + } + if (!lastQueryRef.current.startsWith(query) && !props.enableBugUnrelatedResults) { + // FIXED BUG: + // We show results for queries that are unrelated to the latest started query + // eg.: AZ resolves while we look for QS, we show its results even if totally unrelated + return; // Current query does not start with the query that just resolved + } + lastSuccessfulQueryIdRef.current = queryId; + setSearchResults(results); + }; + runQuery(); + }, [query, props]); + + return ( +
+ { + const value = (evt.target as any).value; + lastQueryRef.current = value; + setQuery(value); + }} + /> +
    + {searchResults + // FIXED BUG: We don't filter the results we receive + // As we want to display results as soon as possible, even if our searchResults + // are related to a past query we want to use them to provide the user with some hints + .filter((r) => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) + .map((r) => ( +
  • {r}
  • + ))} +
+
+ ); +} diff --git a/example/005-race/autocomplete/src/AutocompleteFieldSimple.tsx b/example/005-race/autocomplete/src/AutocompleteFieldSimple.tsx index 22c49e27..0cb6ba95 100644 --- a/example/005-race/autocomplete/src/AutocompleteFieldSimple.tsx +++ b/example/005-race/autocomplete/src/AutocompleteFieldSimple.tsx @@ -1,52 +1,52 @@ -import React from 'react'; - -// Injected as a props because CodeSandbox fails to provide jest.mock -// So it makes such import difficult to test -//// import { search } from './Api'; - -type Props = { - enableBugDoNotDiscardOldQueries?: boolean; - enableBugUnfilteredResults?: boolean; - search: (query: string, maxResults: number) => Promise; -}; - -export default function AutocompleteField(props: Props) { - const [query, setQuery] = React.useState(''); - const [searchResults, setSearchResults] = React.useState([] as string[]); - - React.useEffect(() => { - let canceled = false; - const runQuery = async () => { - const results = await props.search(query, 10); - if (canceled && !props.enableBugDoNotDiscardOldQueries) return; - setSearchResults(results); - }; - runQuery(); - return () => { - canceled = true; - }; - }, [query, props]); - - return ( -
- { - const value = (evt.target as any).value; - setQuery(value); - }} - /> -
    - {searchResults - // FIXED BUG: We don't filter the results we receive - // As we want to display results as soon as possible, even if our searchResults - // are related to a past query we want to use them to provide the user with some hints - .filter(r => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) - .map(r => ( -
  • {r}
  • - ))} -
-
- ); -} +import React from 'react'; + +// Injected as a props because CodeSandbox fails to provide jest.mock +// So it makes such import difficult to test +//// import { search } from './Api'; + +type Props = { + enableBugDoNotDiscardOldQueries?: boolean; + enableBugUnfilteredResults?: boolean; + search: (query: string, maxResults: number) => Promise; +}; + +export default function AutocompleteField(props: Props) { + const [query, setQuery] = React.useState(''); + const [searchResults, setSearchResults] = React.useState([] as string[]); + + React.useEffect(() => { + let canceled = false; + const runQuery = async () => { + const results = await props.search(query, 10); + if (canceled && !props.enableBugDoNotDiscardOldQueries) return; + setSearchResults(results); + }; + runQuery(); + return () => { + canceled = true; + }; + }, [query, props]); + + return ( +
+ { + const value = (evt.target as any).value; + setQuery(value); + }} + /> +
    + {searchResults + // FIXED BUG: We don't filter the results we receive + // As we want to display results as soon as possible, even if our searchResults + // are related to a past query we want to use them to provide the user with some hints + .filter((r) => (props.enableBugUnfilteredResults ? true : r.startsWith(query))) + .map((r) => ( +
  • {r}
  • + ))} +
+
+ ); +} diff --git a/example/005-race/debounced-autocomplete/src/DebouncedAutocomplete.tsx b/example/005-race/debounced-autocomplete/src/DebouncedAutocomplete.tsx index 51a53b74..e673e947 100644 --- a/example/005-race/debounced-autocomplete/src/DebouncedAutocomplete.tsx +++ b/example/005-race/debounced-autocomplete/src/DebouncedAutocomplete.tsx @@ -18,7 +18,7 @@ export default function DebouncedAutocomplete(props: Props) { } const timer = setTimeout( () => - suggestionsFor(query).then(suggestions => { + suggestionsFor(query).then((suggestions) => { if (!canceled || bug) { setSuggestions(suggestions); } @@ -34,9 +34,9 @@ export default function DebouncedAutocomplete(props: Props) { return (
- setQuery(e.target.value)} /> + setQuery(e.target.value)} />
    - {suggestions.map(s => ( + {suggestions.map((s) => (
  • {s}
  • diff --git a/example/005-race/todolist/main.spec.tsx b/example/005-race/todolist/main.spec.tsx index 111391e5..c60de45a 100644 --- a/example/005-race/todolist/main.spec.tsx +++ b/example/005-race/todolist/main.spec.tsx @@ -1,129 +1,129 @@ -import fc from 'fast-check'; -import React from 'react'; -import TodoList from './src/TodoList'; - -import { render, act, cleanup } from '@testing-library/react'; - -import { AddItemCommand } from './model-based/AddItemCommand'; -import { ToggleItemCommand } from './model-based/ToggleItemCommand'; -import { RemoveItemCommand } from './model-based/RemoveItemCommand'; -import { listTodos, sortTodos } from './model-based/Model'; - -describe('TodoList', () => { - it('should detect potential issues with the TodoList', async () => { - await fc.assert( - fc - .asyncProperty( - fc.scheduler({ act }), - TodoListCommands, - fc.uniqueArray( - fc.record({ id: fc.hexaString({ minLength: 8, maxLength: 8 }), label: fc.string(), checked: fc.boolean() }), - { selector: (entry) => entry.id } - ), - fc.infiniteStream(fc.boolean()), - async (s, commands, initialTodos, allFailures) => { - const { mockedApi, expectedTodos } = mockApi(s, initialTodos, allFailures); - - // Execute all the commands - const wrapper = render(); - await fc.scheduledModelRun(s, () => ({ model: { todos: [], wrapper }, real: {} }), commands); - - // Check the final state (no more items should be loading) - expect( - sortTodos((await listTodos()).map((t) => ({ label: t.label, checked: t.checked, loading: t.loading }))) - ).toEqual(sortTodos(expectedTodos().map((t) => ({ label: t.label, checked: t.checked, loading: false })))); - } - ) - .beforeEach(async () => { - await cleanup(); - }) - ); - }); -}); - -// Helpers - -const TodoListCommands = fc.commands([ - fc.string().map((label) => new AddItemCommand(label)), - fc.nat().map((pos) => new ToggleItemCommand(pos)), - fc.nat().map((pos) => new RemoveItemCommand(pos)), -]); - -type ApiTodoItem = { id: string; label: string; checked: boolean }; - -const mockApi = (s: fc.Scheduler, initialTodos: ApiTodoItem[], allFailures: fc.Stream) => { - let lastIdx = 0; - let allTodos = [...initialTodos]; - - const fetchAllTodos = s.scheduleFunction(async function fetchAllTodos(): Promise<{ - status: 'success'; - response: ApiTodoItem[]; - }> { - return { status: 'success', response: allTodos.slice() }; - }); - - const addTodo = s.scheduleFunction(async function addTodo(label: string): Promise< - | { - status: 'success'; - response: ApiTodoItem; - } - | { status: 'error' } - > { - const newTodo = { - id: `${Math.random().toString(16).substring(2)}-${++lastIdx}`, - label, - checked: false, - }; - if (allFailures.next().value) { - return { status: 'error' }; - } - allTodos.push(newTodo); - return { status: 'success', response: newTodo }; - }); - - const toggleTodo = s.scheduleFunction(async function toggleTodo(id: string): Promise< - | { - status: 'success'; - response: ApiTodoItem; - } - | { status: 'error' } - > { - const foundTodo = allTodos.find((t) => t.id === id); - if (!foundTodo || allFailures.next().value) { - return { status: 'error' }; - } - allTodos = allTodos.map((t) => { - if (t.id !== id) return t; - return { id, label: t.label, checked: !t.checked }; - }); - return { status: 'success', response: { ...foundTodo, checked: !foundTodo.checked } }; - }); - - const removeTodo = s.scheduleFunction(async function removeTodo(id: string): Promise< - | { - status: 'success'; - response: ApiTodoItem; - } - | { status: 'error' } - > { - const foundTodo = allTodos.find((t) => t.id === id); - if (!foundTodo || allFailures.next().value) { - return { status: 'error' }; - } - allTodos = allTodos.filter((t) => { - if (t.id !== id) return true; - return false; - }); - return { status: 'success', response: foundTodo }; - }); - - return { - mockedApi: { - fetchAllTodos, - addTodo, - toggleTodo, - removeTodo, - }, - expectedTodos: () => allTodos.slice(), - }; -}; +import fc from 'fast-check'; +import React from 'react'; +import TodoList from './src/TodoList'; + +import { render, act, cleanup } from '@testing-library/react'; + +import { AddItemCommand } from './model-based/AddItemCommand'; +import { ToggleItemCommand } from './model-based/ToggleItemCommand'; +import { RemoveItemCommand } from './model-based/RemoveItemCommand'; +import { listTodos, sortTodos } from './model-based/Model'; + +describe('TodoList', () => { + it('should detect potential issues with the TodoList', async () => { + await fc.assert( + fc + .asyncProperty( + fc.scheduler({ act }), + TodoListCommands, + fc.uniqueArray( + fc.record({ id: fc.hexaString({ minLength: 8, maxLength: 8 }), label: fc.string(), checked: fc.boolean() }), + { selector: (entry) => entry.id } + ), + fc.infiniteStream(fc.boolean()), + async (s, commands, initialTodos, allFailures) => { + const { mockedApi, expectedTodos } = mockApi(s, initialTodos, allFailures); + + // Execute all the commands + const wrapper = render(); + await fc.scheduledModelRun(s, () => ({ model: { todos: [], wrapper }, real: {} }), commands); + + // Check the final state (no more items should be loading) + expect( + sortTodos((await listTodos()).map((t) => ({ label: t.label, checked: t.checked, loading: t.loading }))) + ).toEqual(sortTodos(expectedTodos().map((t) => ({ label: t.label, checked: t.checked, loading: false })))); + } + ) + .beforeEach(async () => { + await cleanup(); + }) + ); + }); +}); + +// Helpers + +const TodoListCommands = fc.commands([ + fc.string().map((label) => new AddItemCommand(label)), + fc.nat().map((pos) => new ToggleItemCommand(pos)), + fc.nat().map((pos) => new RemoveItemCommand(pos)), +]); + +type ApiTodoItem = { id: string; label: string; checked: boolean }; + +const mockApi = (s: fc.Scheduler, initialTodos: ApiTodoItem[], allFailures: fc.Stream) => { + let lastIdx = 0; + let allTodos = [...initialTodos]; + + const fetchAllTodos = s.scheduleFunction(async function fetchAllTodos(): Promise<{ + status: 'success'; + response: ApiTodoItem[]; + }> { + return { status: 'success', response: allTodos.slice() }; + }); + + const addTodo = s.scheduleFunction(async function addTodo(label: string): Promise< + | { + status: 'success'; + response: ApiTodoItem; + } + | { status: 'error' } + > { + const newTodo = { + id: `${Math.random().toString(16).substring(2)}-${++lastIdx}`, + label, + checked: false, + }; + if (allFailures.next().value) { + return { status: 'error' }; + } + allTodos.push(newTodo); + return { status: 'success', response: newTodo }; + }); + + const toggleTodo = s.scheduleFunction(async function toggleTodo(id: string): Promise< + | { + status: 'success'; + response: ApiTodoItem; + } + | { status: 'error' } + > { + const foundTodo = allTodos.find((t) => t.id === id); + if (!foundTodo || allFailures.next().value) { + return { status: 'error' }; + } + allTodos = allTodos.map((t) => { + if (t.id !== id) return t; + return { id, label: t.label, checked: !t.checked }; + }); + return { status: 'success', response: { ...foundTodo, checked: !foundTodo.checked } }; + }); + + const removeTodo = s.scheduleFunction(async function removeTodo(id: string): Promise< + | { + status: 'success'; + response: ApiTodoItem; + } + | { status: 'error' } + > { + const foundTodo = allTodos.find((t) => t.id === id); + if (!foundTodo || allFailures.next().value) { + return { status: 'error' }; + } + allTodos = allTodos.filter((t) => { + if (t.id !== id) return true; + return false; + }); + return { status: 'success', response: foundTodo }; + }); + + return { + mockedApi: { + fetchAllTodos, + addTodo, + toggleTodo, + removeTodo, + }, + expectedTodos: () => allTodos.slice(), + }; +}; diff --git a/example/005-race/todolist/src/TodoList.tsx b/example/005-race/todolist/src/TodoList.tsx index 94c83883..8bc03555 100644 --- a/example/005-race/todolist/src/TodoList.tsx +++ b/example/005-race/todolist/src/TodoList.tsx @@ -1,152 +1,154 @@ -import React, { useState, useEffect } from 'react'; - -type TodoItem = { id: string; label: string; checked: boolean }; -type DraftTodoItem = { id: undefined; label: string; checked: boolean }; - -type QueryAnswerSuccess = Readonly<{ - status: 'success'; - response: TResponse; -}>; -type QueryAnswerError = { - status: 'error'; -}; -type QueryAnswer = QueryAnswerSuccess | QueryAnswerError; - -type Props = { - fetchAllTodos: () => Promise>; - addTodo: (label: string) => Promise>; // return the new item - toggleTodo: (id: string) => Promise>; // return the updated item - removeTodo: (id: string) => Promise>; // return the deleted item -}; - -export default function TodoList(props: Props) { - const { fetchAllTodos, addTodo, toggleTodo, removeTodo } = props; - - const [inputValue, setInputValue] = useState(''); - const [allTodos, setAllTodos] = useState([] as ((TodoItem | DraftTodoItem) & { - loading: boolean; - })[]); - - useEffect(() => { - const runQuery = async () => { - const query = await fetchAllTodos(); - if (query.status === 'error') { - // Ignore errors - return; - } - setAllTodos(allTodos => { - // The call to fetch all the todos might be related to outdated data - // We want to preserve all our todos that are not in the result of the query - const knownTodosInQuery = new Set(query.response.map(todo => todo.id)); - return [ - ...query.response.map(todo => ({ ...todo, loading: false })), - ...allTodos.filter(todo => !knownTodosInQuery.has(todo.id)) - ]; - }); - }; - runQuery(); - }, [fetchAllTodos]); - - const addCurrentTodo = async () => { - setInputValue(''); - - // Temporary add the todo in the list as if it was already validated by the back - const draftTodo = { - id: undefined, - label: inputValue, - checked: false, - loading: true - }; - setAllTodos(allTodos => [...allTodos, draftTodo]); - - const query = await addTodo(inputValue); - if (query.status === 'error') { - // Remove draft todo on error - setAllTodos(allTodos => allTodos.filter(todo => todo !== draftTodo)); - return; - } - // Replace draft todo by the final version - setAllTodos(allTodos => - allTodos.map(todo => { - return todo !== draftTodo ? todo : { ...query.response, loading: false }; - }) - ); - }; - - const toggleById = async (id: string) => { - // Temporary toggle the todo (serevr might still reject the toggle) - setAllTodos(allTodos => - allTodos.map(todo => (todo.id !== id ? todo : { ...todo, checked: !todo.checked, loading: true })) - ); - - const query = await toggleTodo(id); - if (query.status === 'error') { - const toggledTodo = allTodos.find(todo => todo.id === id); - if (toggledTodo) { - setAllTodos(allTodos => allTodos.map(todo => (todo.id !== id ? todo : toggledTodo))); - } - return; - } - - setAllTodos(allTodos => allTodos.map(todo => (todo.id !== id ? todo : { ...query.response, loading: false }))); - }; - - const deleteById = async (id: string) => { - // Temporary delete the todo (serevr might still reject the delete) - setAllTodos(allTodos => allTodos.filter(todo => todo.id !== id)); - - const query = await removeTodo(id); - if (query.status === 'error') { - const deletedTodo = allTodos.find(todo => todo.id === id); - if (deletedTodo) { - setAllTodos(allTodos => [...allTodos, deletedTodo]); - } - return; - } - }; - - return ( -
    -

    Add your todo:

    - setInputValue(evt.target.value)} - /> - -

    Your todos:

    -
    - {allTodos.map((todoItem, idx) => { - return ( -
    - { - if (todoItem.id !== undefined) { - toggleById(todoItem.id); - } - }} - checked={todoItem.checked} - disabled={todoItem.loading} - /> - {todoItem.label} - -
    - ); - })} -
    -
    - ); -} +import React, { useState, useEffect } from 'react'; + +type TodoItem = { id: string; label: string; checked: boolean }; +type DraftTodoItem = { id: undefined; label: string; checked: boolean }; + +type QueryAnswerSuccess = Readonly<{ + status: 'success'; + response: TResponse; +}>; +type QueryAnswerError = { + status: 'error'; +}; +type QueryAnswer = QueryAnswerSuccess | QueryAnswerError; + +type Props = { + fetchAllTodos: () => Promise>; + addTodo: (label: string) => Promise>; // return the new item + toggleTodo: (id: string) => Promise>; // return the updated item + removeTodo: (id: string) => Promise>; // return the deleted item +}; + +export default function TodoList(props: Props) { + const { fetchAllTodos, addTodo, toggleTodo, removeTodo } = props; + + const [inputValue, setInputValue] = useState(''); + const [allTodos, setAllTodos] = useState( + [] as ((TodoItem | DraftTodoItem) & { + loading: boolean; + })[] + ); + + useEffect(() => { + const runQuery = async () => { + const query = await fetchAllTodos(); + if (query.status === 'error') { + // Ignore errors + return; + } + setAllTodos((allTodos) => { + // The call to fetch all the todos might be related to outdated data + // We want to preserve all our todos that are not in the result of the query + const knownTodosInQuery = new Set(query.response.map((todo) => todo.id)); + return [ + ...query.response.map((todo) => ({ ...todo, loading: false })), + ...allTodos.filter((todo) => !knownTodosInQuery.has(todo.id)), + ]; + }); + }; + runQuery(); + }, [fetchAllTodos]); + + const addCurrentTodo = async () => { + setInputValue(''); + + // Temporary add the todo in the list as if it was already validated by the back + const draftTodo = { + id: undefined, + label: inputValue, + checked: false, + loading: true, + }; + setAllTodos((allTodos) => [...allTodos, draftTodo]); + + const query = await addTodo(inputValue); + if (query.status === 'error') { + // Remove draft todo on error + setAllTodos((allTodos) => allTodos.filter((todo) => todo !== draftTodo)); + return; + } + // Replace draft todo by the final version + setAllTodos((allTodos) => + allTodos.map((todo) => { + return todo !== draftTodo ? todo : { ...query.response, loading: false }; + }) + ); + }; + + const toggleById = async (id: string) => { + // Temporary toggle the todo (serevr might still reject the toggle) + setAllTodos((allTodos) => + allTodos.map((todo) => (todo.id !== id ? todo : { ...todo, checked: !todo.checked, loading: true })) + ); + + const query = await toggleTodo(id); + if (query.status === 'error') { + const toggledTodo = allTodos.find((todo) => todo.id === id); + if (toggledTodo) { + setAllTodos((allTodos) => allTodos.map((todo) => (todo.id !== id ? todo : toggledTodo))); + } + return; + } + + setAllTodos((allTodos) => allTodos.map((todo) => (todo.id !== id ? todo : { ...query.response, loading: false }))); + }; + + const deleteById = async (id: string) => { + // Temporary delete the todo (serevr might still reject the delete) + setAllTodos((allTodos) => allTodos.filter((todo) => todo.id !== id)); + + const query = await removeTodo(id); + if (query.status === 'error') { + const deletedTodo = allTodos.find((todo) => todo.id === id); + if (deletedTodo) { + setAllTodos((allTodos) => [...allTodos, deletedTodo]); + } + return; + } + }; + + return ( +
    +

    Add your todo:

    + setInputValue(evt.target.value)} + /> + +

    Your todos:

    +
    + {allTodos.map((todoItem, idx) => { + return ( +
    + { + if (todoItem.id !== undefined) { + toggleById(todoItem.id); + } + }} + checked={todoItem.checked} + disabled={todoItem.loading} + /> + {todoItem.label} + +
    + ); + })} +
    +
    + ); +} diff --git a/example/005-race/userProfile/main.spec.tsx b/example/005-race/userProfile/main.spec.tsx index e34df0de..5618a6c6 100644 --- a/example/005-race/userProfile/main.spec.tsx +++ b/example/005-race/userProfile/main.spec.tsx @@ -1,93 +1,93 @@ -import fc from 'fast-check'; -import * as React from 'react'; - -import UserProfilePage from './src/UserProfilePage'; - -import { render, cleanup, act, screen } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -// If you want to test the behaviour of fast-check in case of a bug: -const bugId = undefined; // = 1; // to enable bug - -if (!fc.readConfigureGlobal()) { - // Global config of Jest has been ignored, we will have a timeout after 5000ms - // (CodeSandbox falls in this category) - fc.configureGlobal({ interruptAfterTimeLimit: 4000 }); -} - -describe('UserProfilePage', () => { - it('should not display data related to another user', async () => { - await fc.assert( - fc - .asyncProperty(fc.uuid(), fc.uuid(), fc.scheduler({ act }), async (uid1, uid2, s) => { - // Arrange - const getUserProfileImplem = s.scheduleFunction(function getUserProfile(userId: string) { - return Promise.resolve({ id: userId, name: userId }); - }); - - // Act - const { rerender } = render( - - ); - s.scheduleSequence([ - async () => { - rerender(); - }, - ]); - await s.waitAll(); - - // Assert - expect(await screen.queryByText('Loading...')).toBe(null); - expect((await screen.queryByTestId('user-id'))!.textContent).toBe(`Id: ${uid2}`); - }) - .beforeEach(async () => { - jest.resetAllMocks(); - await cleanup(); - }) - ); - }); - - it('should not display data related to another user (complex)', async () => { - await fc.assert( - fc - .asyncProperty(fc.array(fc.uuid(), { minLength: 1 }), fc.scheduler(), async (loadedUserIds, s) => { - // Arrange - const getUserProfileImplem = s.scheduleFunction(function getUserProfile(userId: string) { - return Promise.resolve({ id: userId, name: userId }); - }); - - // Act - let currentUid = loadedUserIds[0]; - const { rerender } = render( - - ); - s.scheduleSequence( - loadedUserIds.slice(1).map((uid) => ({ - label: `Update user id to ${uid}`, - builder: async () => { - currentUid = uid; - rerender(); - }, - })) - ); - - // Assert - while (s.count() !== 0) { - await act(async () => { - await s.waitOne(); - }); - const isLoading = (await screen.queryByText('Loading...')) !== null; - if (!isLoading) { - const idField = await screen.queryByTestId('user-id'); - expect(idField).not.toBe(null); - expect(idField!.textContent).toBe(`Id: ${currentUid}`); - } - } - }) - .beforeEach(async () => { - jest.resetAllMocks(); - await cleanup(); - }) - ); - }); -}); +import fc from 'fast-check'; +import * as React from 'react'; + +import UserProfilePage from './src/UserProfilePage'; + +import { render, cleanup, act, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +// If you want to test the behaviour of fast-check in case of a bug: +const bugId = undefined; // = 1; // to enable bug + +if (!fc.readConfigureGlobal()) { + // Global config of Jest has been ignored, we will have a timeout after 5000ms + // (CodeSandbox falls in this category) + fc.configureGlobal({ interruptAfterTimeLimit: 4000 }); +} + +describe('UserProfilePage', () => { + it('should not display data related to another user', async () => { + await fc.assert( + fc + .asyncProperty(fc.uuid(), fc.uuid(), fc.scheduler({ act }), async (uid1, uid2, s) => { + // Arrange + const getUserProfileImplem = s.scheduleFunction(function getUserProfile(userId: string) { + return Promise.resolve({ id: userId, name: userId }); + }); + + // Act + const { rerender } = render( + + ); + s.scheduleSequence([ + async () => { + rerender(); + }, + ]); + await s.waitAll(); + + // Assert + expect(await screen.queryByText('Loading...')).toBe(null); + expect((await screen.queryByTestId('user-id'))!.textContent).toBe(`Id: ${uid2}`); + }) + .beforeEach(async () => { + jest.resetAllMocks(); + await cleanup(); + }) + ); + }); + + it('should not display data related to another user (complex)', async () => { + await fc.assert( + fc + .asyncProperty(fc.array(fc.uuid(), { minLength: 1 }), fc.scheduler(), async (loadedUserIds, s) => { + // Arrange + const getUserProfileImplem = s.scheduleFunction(function getUserProfile(userId: string) { + return Promise.resolve({ id: userId, name: userId }); + }); + + // Act + let currentUid = loadedUserIds[0]; + const { rerender } = render( + + ); + s.scheduleSequence( + loadedUserIds.slice(1).map((uid) => ({ + label: `Update user id to ${uid}`, + builder: async () => { + currentUid = uid; + rerender(); + }, + })) + ); + + // Assert + while (s.count() !== 0) { + await act(async () => { + await s.waitOne(); + }); + const isLoading = (await screen.queryByText('Loading...')) !== null; + if (!isLoading) { + const idField = await screen.queryByTestId('user-id'); + expect(idField).not.toBe(null); + expect(idField!.textContent).toBe(`Id: ${currentUid}`); + } + } + }) + .beforeEach(async () => { + jest.resetAllMocks(); + await cleanup(); + }) + ); + }); +}); diff --git a/example/005-race/userProfile/src/UserProfilePage.tsx b/example/005-race/userProfile/src/UserProfilePage.tsx index a5bb7b1b..c278d3ab 100644 --- a/example/005-race/userProfile/src/UserProfilePage.tsx +++ b/example/005-race/userProfile/src/UserProfilePage.tsx @@ -1,39 +1,39 @@ -import React from 'react'; - -type UserProfile = { id: string; name: string }; - -type Props = { - userId: string; - bug?: 1; - // Injected as a props because CodeSandbox fails to provide jest.mock - // Otherwise we might have direclty imported it and mock the import - getUserProfile: (userId: string) => Promise; -}; - -export default function UserPageProfile(props: Props) { - const [userData, setUserData] = React.useState(null as UserProfile | null); - - React.useEffect(() => { - let canceled = false; - const fetchUser = async () => { - setUserData(null); // reset on fetch - const data = await props.getUserProfile(props.userId); - if (!canceled || props.bug !== undefined) setUserData(data); - }; - fetchUser(); - return () => { - canceled = true; - }; - }, [props.getUserProfile, props.userId, props.bug]); - - if (userData === null) { - return
    Loading...
    ; - } - - return ( -
    -
    Id: {userData.id}
    -
    Name: {userData.name}
    -
    - ); -} +import React from 'react'; + +type UserProfile = { id: string; name: string }; + +type Props = { + userId: string; + bug?: 1; + // Injected as a props because CodeSandbox fails to provide jest.mock + // Otherwise we might have direclty imported it and mock the import + getUserProfile: (userId: string) => Promise; +}; + +export default function UserPageProfile(props: Props) { + const [userData, setUserData] = React.useState(null as UserProfile | null); + + React.useEffect(() => { + let canceled = false; + const fetchUser = async () => { + setUserData(null); // reset on fetch + const data = await props.getUserProfile(props.userId); + if (!canceled || props.bug !== undefined) setUserData(data); + }; + fetchUser(); + return () => { + canceled = true; + }; + }, [props.getUserProfile, props.userId, props.bug]); + + if (userData === null) { + return
    Loading...
    ; + } + + return ( +
    +
    Id: {userData.id}
    +
    Name: {userData.name}
    +
    + ); +} diff --git a/example/README.md b/example/README.md index 8c8ebd95..dce19709 100644 --- a/example/README.md +++ b/example/README.md @@ -1,92 +1,92 @@ -# Examples based on `fast-check` - -This directory gathers multiple examples of properties you might come with when using `fast-check`. - -Try online with [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). - -*Teach yourself property based through examples* - -## Examples by category - -Following examples show how you could think about properties given algorithms ranging from basic pure functions returning easy to assess outputs to complex state machines. - -**Simple data structures** - -101 for property based: - -- `decompPrime` - Returns the list of prime factors corresponding to the input value -- `fibonacci` - Returns the item at a given position in the sequence of Fibonacci -- `indexOf` - Returns the position of the first occurrence of `pattern` in `text` -- `sort` - Returns a sorted copy of the input array - -**Recursive structures** - -Let's see how to generate recursive inputs using `letrec`, `memo` or even none of them: - -- `isSearchTree` - Returns `true` if the tree is a binary search tree, `false` otherwise - -**Misc** - -Various algorithms to have more random examples: - -- `knight` - Multi dimensional dichotomy given as a coding exercise -- `mazeGenerator` - Maze generator -- `roman` - Convert from and to roman notation for numbers - -**State machines to user interfaces** - -Property based testing applied to state machines or user interfaces: - -- `MusicPlayer` - Simple music player with `play`, `pause`, `addTrack` and `next` - -**Race conditions** - -Property based testing used to detect race conditions in various kind of JavaScript snippets: - -- `AutocompleteField` - An autocomplete field written in React providing suggestions as soon as possible -- `Counter` - Increment a counter stored in a DB - non atomic and atomic versions -- `DebouncedAutocomplete` - An autocomplete field written in React providing suggestions in a debounced way (uses timers) -- `dependencyTree` - Fetch recursively dependencies for a npm package -- `TodoList` - Simple todolist React app -- `UserProfilePage` - A simple React component loading user profile on mount - -## Rules of property based - -1. Properties do not replace examples, they are just an extra layer of tests -2. Properties can be used at any level: unit, integration, end-to-end - -## Tricks to find properties - -1. Characteristics independent of the inputs - -> Examples: -> -> `for any floating point number d, Math.floor(d) is an integer` -> -> `for any integer n, Math.abs(n) ≥ 0` - -2. Characteristics derived from the inputs - -> Examples: -> -> `for any a and b integers the average of a and b is between a and b` -> -> `for any array — data, sorted(data) and data contain the same elements` - -3. Restricted set of inputs with useful characteristics - -> Examples: -> -> `for any prime number p, its decomposition into prime factors is itself` -> -> `for any a, b and c strings the concatenation of a, b and c always contains b` - -4. Characteristics on combination of functions - -> Examples: -> -> `for any file f, unzip(zip(f)) is the original file` -> -> `for any a, b numbers lcm(a, b) * gcd(a, b) equals a * b` - -5. Comparison with a simpler implementation \ No newline at end of file +# Examples based on `fast-check` + +This directory gathers multiple examples of properties you might come with when using `fast-check`. + +Try online with [CodeSandbox](https://codesandbox.io/s/github/dubzzz/fast-check/tree/main/example?previewwindow=tests). + +_Teach yourself property based through examples_ + +## Examples by category + +Following examples show how you could think about properties given algorithms ranging from basic pure functions returning easy to assess outputs to complex state machines. + +**Simple data structures** + +101 for property based: + +- `decompPrime` - Returns the list of prime factors corresponding to the input value +- `fibonacci` - Returns the item at a given position in the sequence of Fibonacci +- `indexOf` - Returns the position of the first occurrence of `pattern` in `text` +- `sort` - Returns a sorted copy of the input array + +**Recursive structures** + +Let's see how to generate recursive inputs using `letrec`, `memo` or even none of them: + +- `isSearchTree` - Returns `true` if the tree is a binary search tree, `false` otherwise + +**Misc** + +Various algorithms to have more random examples: + +- `knight` - Multi dimensional dichotomy given as a coding exercise +- `mazeGenerator` - Maze generator +- `roman` - Convert from and to roman notation for numbers + +**State machines to user interfaces** + +Property based testing applied to state machines or user interfaces: + +- `MusicPlayer` - Simple music player with `play`, `pause`, `addTrack` and `next` + +**Race conditions** + +Property based testing used to detect race conditions in various kind of JavaScript snippets: + +- `AutocompleteField` - An autocomplete field written in React providing suggestions as soon as possible +- `Counter` - Increment a counter stored in a DB - non atomic and atomic versions +- `DebouncedAutocomplete` - An autocomplete field written in React providing suggestions in a debounced way (uses timers) +- `dependencyTree` - Fetch recursively dependencies for a npm package +- `TodoList` - Simple todolist React app +- `UserProfilePage` - A simple React component loading user profile on mount + +## Rules of property based + +1. Properties do not replace examples, they are just an extra layer of tests +2. Properties can be used at any level: unit, integration, end-to-end + +## Tricks to find properties + +1. Characteristics independent of the inputs + +> Examples: +> +> `for any floating point number d, Math.floor(d) is an integer` +> +> `for any integer n, Math.abs(n) ≥ 0` + +2. Characteristics derived from the inputs + +> Examples: +> +> `for any a and b integers the average of a and b is between a and b` +> +> `for any array — data, sorted(data) and data contain the same elements` + +3. Restricted set of inputs with useful characteristics + +> Examples: +> +> `for any prime number p, its decomposition into prime factors is itself` +> +> `for any a, b and c strings the concatenation of a, b and c always contains b` + +4. Characteristics on combination of functions + +> Examples: +> +> `for any file f, unzip(zip(f)) is the original file` +> +> `for any a, b numbers lcm(a, b) * gcd(a, b) equals a * b` + +5. Comparison with a simpler implementation diff --git a/example/tsconfig.json b/example/tsconfig.json index 4d239487..54879b48 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,20 +1,19 @@ -{ - "compilerOptions": { - "downlevelIteration": true, - "target": "es2020", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react" - } - } - \ No newline at end of file +{ + "compilerOptions": { + "downlevelIteration": true, + "target": "es2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + } +} diff --git a/jest.config.cjs b/jest.config.cjs index c5e9a110..5aed77b4 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,16 +1,16 @@ -// Shared Jest configuration -// Useful for Jest plugin of vscode - -module.exports = { - moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.json', - }, - }, - collectCoverageFrom: ['/src/**'], - testMatch: ['/test/**/*.spec.ts'], - setupFiles: [], - setupFilesAfterEnv: ['/jest.setup.js'], - preset: 'ts-jest', -}; +// Shared Jest configuration +// Useful for Jest plugin of vscode + +module.exports = { + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + }, + collectCoverageFrom: ['/src/**'], + testMatch: ['/test/**/*.spec.ts'], + setupFiles: [], + setupFilesAfterEnv: ['/jest.setup.js'], + preset: 'ts-jest', +}; diff --git a/jest.e2e.config.cjs b/jest.e2e.config.cjs index 88607226..6a00881e 100644 --- a/jest.e2e.config.cjs +++ b/jest.e2e.config.cjs @@ -1,7 +1,7 @@ -const conf = require('./jest.config.cjs'); - -module.exports = Object.assign(conf, { - testMatch: ['/test/e2e/**/*.spec.ts'], - testPathIgnorePatterns: - typeof BigInt === 'undefined' ? ['/NoRegressionBigInt.spec.ts', '/documentation/Docs.md.spec.ts'] : [], -}); +const conf = require('./jest.config.cjs'); + +module.exports = Object.assign(conf, { + testMatch: ['/test/e2e/**/*.spec.ts'], + testPathIgnorePatterns: + typeof BigInt === 'undefined' ? ['/NoRegressionBigInt.spec.ts', '/documentation/Docs.md.spec.ts'] : [], +}); diff --git a/jest.unit.config.cjs b/jest.unit.config.cjs index 71f7fc94..2dde7b2d 100644 --- a/jest.unit.config.cjs +++ b/jest.unit.config.cjs @@ -1,7 +1,7 @@ -const conf = require('./jest.config.cjs'); - -module.exports = Object.assign(conf, { - testMatch: ['/test/unit/**/*.spec.ts'], - coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/lib/', '/test/', '/node_modules/'] -}); +const conf = require('./jest.config.cjs'); + +module.exports = Object.assign(conf, { + testMatch: ['/test/unit/**/*.spec.ts'], + coverageDirectory: 'coverage', + coveragePathIgnorePatterns: ['/lib/', '/test/', '/node_modules/'], +}); diff --git a/package.esm-template.json b/package.esm-template.json index 96ae6e57..3dbc1ca5 100644 --- a/package.esm-template.json +++ b/package.esm-template.json @@ -1,3 +1,3 @@ { - "type": "module" -} \ No newline at end of file + "type": "module" +} diff --git a/package.json b/package.json index 0bb91e7e..ef7cf956 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "docs": "api-extractor run --local && rm docs/fast-check.api.json && typedoc --tsconfig tsconfig.nospec.json --out docs src/fast-check-default.ts && node postbuild/main.cjs", "docs-ci": "cross-env EXPECT_GITHUB_SHA=true yarn docs", "docs:serve": "npx serve docs/", - "format": "prettier --write \"**/*.{js,ts}\"", - "format:check": "prettier --list-different \"**/*.{js,ts}\"", + "format": "prettier --write .", + "format:check": "prettier --list-different .", "lint": "eslint \"**/*.{js,ts}\" --fix", "lint:check": "eslint \"**/*.{js,ts}\"" }, diff --git a/prebuild/helpers.cjs b/prebuild/helpers.cjs index 288a315f..46dc316d 100644 --- a/prebuild/helpers.cjs +++ b/prebuild/helpers.cjs @@ -1,47 +1,44 @@ -// @ts-check - -/** - * @param num {number} - */ -const iota = num => [...Array(num)].map((v, idx) => idx); - -/** - * @param num {number} - * @param fn {(v: number) => string} - * @param ch {string} - */ -const joiner = (num, fn, ch) => - iota(num) - .map(fn) - .join(ch); - -/** - * @param num {number} - * @param fn {(v: number) => string} - */ -const commas = (num, fn) => joiner(num, fn, ','); - -/** - * arb0,arb1,... - * @param num {number} - */ -const arbCommas = num => commas(num, v => `arb${v}`); - -/** - * T0,T1,... - * @param num {number} - */ -const txCommas = num => commas(num, v => `T${v}`); - -/** - * T0|T1|... - * @param num {number} - */ -const txXor = num => joiner(num, v => `T${v}`, '|'); - -exports.iota = iota; -exports.joiner = joiner; -exports.commas = commas; -exports.arbCommas = arbCommas; -exports.txCommas = txCommas; -exports.txXor = txXor; +// @ts-check + +/** + * @param num {number} + */ +const iota = (num) => [...Array(num)].map((v, idx) => idx); + +/** + * @param num {number} + * @param fn {(v: number) => string} + * @param ch {string} + */ +const joiner = (num, fn, ch) => iota(num).map(fn).join(ch); + +/** + * @param num {number} + * @param fn {(v: number) => string} + */ +const commas = (num, fn) => joiner(num, fn, ','); + +/** + * arb0,arb1,... + * @param num {number} + */ +const arbCommas = (num) => commas(num, (v) => `arb${v}`); + +/** + * T0,T1,... + * @param num {number} + */ +const txCommas = (num) => commas(num, (v) => `T${v}`); + +/** + * T0|T1|... + * @param num {number} + */ +const txXor = (num) => joiner(num, (v) => `T${v}`, '|'); + +exports.iota = iota; +exports.joiner = joiner; +exports.commas = commas; +exports.arbCommas = arbCommas; +exports.txCommas = txCommas; +exports.txXor = txXor; diff --git a/prebuild/prebuild.cjs b/prebuild/prebuild.cjs index 8c8bc605..2243219e 100644 --- a/prebuild/prebuild.cjs +++ b/prebuild/prebuild.cjs @@ -1,11 +1,11 @@ -// @ts-check -const { writeFileSync } = require('fs'); -const { generateProperty, generatePropertySpec } = require('./property.cjs'); - -const NUM_PARAMETERS = 22; - -writeFileSync('./src/check/property/Property.generated.ts', generateProperty(NUM_PARAMETERS, false)); -writeFileSync('./test/unit/check/property/Property.generated.spec.ts', generatePropertySpec(NUM_PARAMETERS, false)); - -writeFileSync('./src/check/property/AsyncProperty.generated.ts', generateProperty(NUM_PARAMETERS, true)); -writeFileSync('./test/unit/check/property/AsyncProperty.generated.spec.ts', generatePropertySpec(NUM_PARAMETERS, true)); +// @ts-check +const { writeFileSync } = require('fs'); +const { generateProperty, generatePropertySpec } = require('./property.cjs'); + +const NUM_PARAMETERS = 22; + +writeFileSync('./src/check/property/Property.generated.ts', generateProperty(NUM_PARAMETERS, false)); +writeFileSync('./test/unit/check/property/Property.generated.spec.ts', generatePropertySpec(NUM_PARAMETERS, false)); + +writeFileSync('./src/check/property/AsyncProperty.generated.ts', generateProperty(NUM_PARAMETERS, true)); +writeFileSync('./test/unit/check/property/AsyncProperty.generated.spec.ts', generatePropertySpec(NUM_PARAMETERS, true)); diff --git a/prebuild/property.cjs b/prebuild/property.cjs index 2bf1d389..c20802fa 100644 --- a/prebuild/property.cjs +++ b/prebuild/property.cjs @@ -1,113 +1,113 @@ -// @ts-check -const { commas, iota, txCommas } = require('./helpers.cjs'); - -/** - * @param num {number} - * @param isAsync {boolean} - */ -const predicateFor = function (num, isAsync) { - return isAsync - ? `(${commas(num, (v) => `t${v}:T${v}`)}) => Promise` - : `(${commas(num, (v) => `t${v}:T${v}`)}) => (boolean|void)`; -}; - -/** - * @param num {number} - * @param isAsync {boolean} - */ -const signatureFor = (num, isAsync) => { - const functionName = isAsync ? 'asyncProperty' : 'property'; - const className = isAsync ? 'AsyncProperty' : 'Property'; - return ` - /** - * Instantiate a new {@link fast-check#I${className}} - * @param predicate - Assess the success of the property. Would be considered falsy if it throws or if its output evaluates to false - * @remarks Since ${className === 'Property' ? '0.0.1' : '0.0.7'} - * @public - */ - function ${functionName}<${txCommas(num)}>( - ${commas(num, (v) => `arb${v}:Arbitrary`)}, - predicate: ${predicateFor(num, isAsync)} - ): I${className}WithHooks<[${txCommas(num)}]>;`; -}; - -/** - * @param num {number} - * @param isAsync {boolean} - */ -const generateProperty = (num, isAsync) => { - const functionName = isAsync ? 'asyncProperty' : 'property'; - const className = isAsync ? 'AsyncProperty' : 'Property'; - const converterFunction = isAsync ? 'convertFromNextAsyncPropertyWithHooks' : 'convertFromNextPropertyWithHooks'; - const blocks = [ - // imports - `import { Arbitrary } from '../arbitrary/definition/Arbitrary';`, - `import { genericTuple } from '../../arbitrary/genericTuple';`, - `import { ${converterFunction} } from './ConvertersProperty';`, - `import { ${className}, I${className}WithHooks } from './${className}.generic';`, - `import { AlwaysShrinkableArbitrary } from '../../arbitrary/_internals/AlwaysShrinkableArbitrary';`, - `import { convertFromNext, convertToNext } from '../arbitrary/definition/Converters';`, - // declare all signatures - ...iota(num).map((id) => signatureFor(id + 1, isAsync)), - // declare function - `function ${functionName}(...args: any[]): any { - if (args.length < 2) throw new Error('${functionName} expects at least two parameters'); - const arbs = args.slice(0, args.length -1); - const p = args[args.length -1]; - return ${converterFunction}(new ${className}(genericTuple(arbs.map(arb => convertFromNext(new AlwaysShrinkableArbitrary(convertToNext(arb))))), t => p(...t))); - }`, - // export - `export { ${functionName} };`, - ]; - - return blocks.join('\n'); -}; - -/** - * @param num {number} - * @param isAsync {boolean} - */ -const testBasicCall = (num, isAsync) => { - const functionName = isAsync ? 'asyncProperty' : 'property'; - const kAsync = isAsync ? 'async' : ''; - const kAwait = isAsync ? 'await' : ''; - return ` - it('Should call the underlying arbitraries in ${functionName}${num}', ${kAsync} () => { - let data = null; - const p = ${functionName}( - ${commas(num, (v) => `stubArb.single(${v * v})`)}, - ${kAsync} (${commas(num, (v) => `a${v}:number`)}) => { - data = [${commas(num, (v) => `a${v}`)}]; - return true; - }); - expect(${kAwait} p.run(p.generate(stubRng.mutable.nocall()).value)).toBe(null); - expect(data).toEqual([${commas(num, (v) => `${v * v}`)}]); - }); - `; -}; - -/** - * @param num {number} - * @param isAsync {boolean} - */ -const generatePropertySpec = (num, isAsync) => { - const functionName = isAsync ? 'asyncProperty' : 'property'; - const className = isAsync ? 'AsyncProperty' : 'Property'; - const blocks = [ - // imports - `import * as stubArb from '../../stubs/arbitraries';`, - `import * as stubRng from '../../stubs/generators';`, - `import { ${functionName} } from '../../../../src/check/property/${className}';`, - // start blocks - `describe('${className}', () => {`, - // tests - ...iota(num).map((id) => testBasicCall(id + 1, isAsync)), - // end blocks - `});`, - ]; - - return blocks.join('\n'); -}; - -exports.generateProperty = generateProperty; -exports.generatePropertySpec = generatePropertySpec; +// @ts-check +const { commas, iota, txCommas } = require('./helpers.cjs'); + +/** + * @param num {number} + * @param isAsync {boolean} + */ +const predicateFor = function (num, isAsync) { + return isAsync + ? `(${commas(num, (v) => `t${v}:T${v}`)}) => Promise` + : `(${commas(num, (v) => `t${v}:T${v}`)}) => (boolean|void)`; +}; + +/** + * @param num {number} + * @param isAsync {boolean} + */ +const signatureFor = (num, isAsync) => { + const functionName = isAsync ? 'asyncProperty' : 'property'; + const className = isAsync ? 'AsyncProperty' : 'Property'; + return ` + /** + * Instantiate a new {@link fast-check#I${className}} + * @param predicate - Assess the success of the property. Would be considered falsy if it throws or if its output evaluates to false + * @remarks Since ${className === 'Property' ? '0.0.1' : '0.0.7'} + * @public + */ + function ${functionName}<${txCommas(num)}>( + ${commas(num, (v) => `arb${v}:Arbitrary`)}, + predicate: ${predicateFor(num, isAsync)} + ): I${className}WithHooks<[${txCommas(num)}]>;`; +}; + +/** + * @param num {number} + * @param isAsync {boolean} + */ +const generateProperty = (num, isAsync) => { + const functionName = isAsync ? 'asyncProperty' : 'property'; + const className = isAsync ? 'AsyncProperty' : 'Property'; + const converterFunction = isAsync ? 'convertFromNextAsyncPropertyWithHooks' : 'convertFromNextPropertyWithHooks'; + const blocks = [ + // imports + `import { Arbitrary } from '../arbitrary/definition/Arbitrary';`, + `import { genericTuple } from '../../arbitrary/genericTuple';`, + `import { ${converterFunction} } from './ConvertersProperty';`, + `import { ${className}, I${className}WithHooks } from './${className}.generic';`, + `import { AlwaysShrinkableArbitrary } from '../../arbitrary/_internals/AlwaysShrinkableArbitrary';`, + `import { convertFromNext, convertToNext } from '../arbitrary/definition/Converters';`, + // declare all signatures + ...iota(num).map((id) => signatureFor(id + 1, isAsync)), + // declare function + `function ${functionName}(...args: any[]): any { + if (args.length < 2) throw new Error('${functionName} expects at least two parameters'); + const arbs = args.slice(0, args.length -1); + const p = args[args.length -1]; + return ${converterFunction}(new ${className}(genericTuple(arbs.map(arb => convertFromNext(new AlwaysShrinkableArbitrary(convertToNext(arb))))), t => p(...t))); + }`, + // export + `export { ${functionName} };`, + ]; + + return blocks.join('\n'); +}; + +/** + * @param num {number} + * @param isAsync {boolean} + */ +const testBasicCall = (num, isAsync) => { + const functionName = isAsync ? 'asyncProperty' : 'property'; + const kAsync = isAsync ? 'async' : ''; + const kAwait = isAsync ? 'await' : ''; + return ` + it('Should call the underlying arbitraries in ${functionName}${num}', ${kAsync} () => { + let data = null; + const p = ${functionName}( + ${commas(num, (v) => `stubArb.single(${v * v})`)}, + ${kAsync} (${commas(num, (v) => `a${v}:number`)}) => { + data = [${commas(num, (v) => `a${v}`)}]; + return true; + }); + expect(${kAwait} p.run(p.generate(stubRng.mutable.nocall()).value)).toBe(null); + expect(data).toEqual([${commas(num, (v) => `${v * v}`)}]); + }); + `; +}; + +/** + * @param num {number} + * @param isAsync {boolean} + */ +const generatePropertySpec = (num, isAsync) => { + const functionName = isAsync ? 'asyncProperty' : 'property'; + const className = isAsync ? 'AsyncProperty' : 'Property'; + const blocks = [ + // imports + `import * as stubArb from '../../stubs/arbitraries';`, + `import * as stubRng from '../../stubs/generators';`, + `import { ${functionName} } from '../../../../src/check/property/${className}';`, + // start blocks + `describe('${className}', () => {`, + // tests + ...iota(num).map((id) => testBasicCall(id + 1, isAsync)), + // end blocks + `});`, + ]; + + return blocks.join('\n'); +}; + +exports.generateProperty = generateProperty; +exports.generatePropertySpec = generatePropertySpec; diff --git a/renovate.json b/renovate.json index 4021369e..4bcaec1d 100644 --- a/renovate.json +++ b/renovate.json @@ -1,14 +1,10 @@ { - "extends": [ - "config:base" - ], + "extends": ["config:base"], "labels": ["dependencies"], "commitMessagePrefix": "⬆️ ", "packageRules": [ { - "matchPackagePatterns": [ - "*" - ], + "matchPackagePatterns": ["*"], "rangeStrategy": "replace" } ] diff --git a/test/esm/node-extension-cjs/README.md b/test/esm/node-extension-cjs/README.md index 1c779f53..5ad7701a 100644 --- a/test/esm/node-extension-cjs/README.md +++ b/test/esm/node-extension-cjs/README.md @@ -1,3 +1,3 @@ Package defined as module BUT test file is using CJS extension -=> should load the commonjs version \ No newline at end of file +=> should load the commonjs version diff --git a/test/esm/node-extension-mjs/README.md b/test/esm/node-extension-mjs/README.md index b116316a..d77c822d 100644 --- a/test/esm/node-extension-mjs/README.md +++ b/test/esm/node-extension-mjs/README.md @@ -1,3 +1,3 @@ Package defined as commonjs BUT test file is using MJS extension -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/esm/node-with-import/README.md b/test/esm/node-with-import/README.md index 16a25c6f..4fe906ae 100644 --- a/test/esm/node-with-import/README.md +++ b/test/esm/node-with-import/README.md @@ -1,3 +1,3 @@ Package defined as module AND test file uses JS extension -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/esm/node-with-require/README.md b/test/esm/node-with-require/README.md index fd617677..18f53782 100644 --- a/test/esm/node-with-require/README.md +++ b/test/esm/node-with-require/README.md @@ -1,3 +1,3 @@ Package defined as commonjs AND test file uses JS extension -=> should load the commonjs version \ No newline at end of file +=> should load the commonjs version diff --git a/test/esm/rollup-with-import/README.md b/test/esm/rollup-with-import/README.md index 1899b2de..a248d720 100644 --- a/test/esm/rollup-with-import/README.md +++ b/test/esm/rollup-with-import/README.md @@ -1,3 +1,3 @@ Bundlers prefer module If a "module" attribute is defined in package.json -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/esm/rollup-with-require/README.md b/test/esm/rollup-with-require/README.md index 1899b2de..a248d720 100644 --- a/test/esm/rollup-with-require/README.md +++ b/test/esm/rollup-with-require/README.md @@ -1,3 +1,3 @@ Bundlers prefer module If a "module" attribute is defined in package.json -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/esm/webpack-with-import/README.md b/test/esm/webpack-with-import/README.md index 1899b2de..a248d720 100644 --- a/test/esm/webpack-with-import/README.md +++ b/test/esm/webpack-with-import/README.md @@ -1,3 +1,3 @@ Bundlers prefer module If a "module" attribute is defined in package.json -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/esm/webpack-with-require/README.md b/test/esm/webpack-with-require/README.md index 1899b2de..a248d720 100644 --- a/test/esm/webpack-with-require/README.md +++ b/test/esm/webpack-with-require/README.md @@ -1,3 +1,3 @@ Bundlers prefer module If a "module" attribute is defined in package.json -=> should load the module version \ No newline at end of file +=> should load the module version diff --git a/test/legacy/node-8/package.json b/test/legacy/node-8/package.json index 726012f8..1afb27ea 100644 --- a/test/legacy/node-8/package.json +++ b/test/legacy/node-8/package.json @@ -1,10 +1,8 @@ { - "scripts": { - }, - "dependencies": { - "fast-check": "*" - }, - "license": "MIT", - "private": true - } - \ No newline at end of file + "scripts": {}, + "dependencies": { + "fast-check": "*" + }, + "license": "MIT", + "private": true +} diff --git a/test/type/package.json b/test/type/package.json index 59955567..1afb27ea 100644 --- a/test/type/package.json +++ b/test/type/package.json @@ -1,6 +1,5 @@ { - "scripts": { - }, + "scripts": {}, "dependencies": { "fast-check": "*" }, diff --git a/test/type/tsconfig.json b/test/type/tsconfig.json index 195ea288..86e13a7b 100644 --- a/test/type/tsconfig.json +++ b/test/type/tsconfig.json @@ -1,8 +1,8 @@ { - "compilerOptions": { - "lib": ["es2015"], - "noEmit": true, - "skipLibCheck": false, - "strict": true - } -} \ No newline at end of file + "compilerOptions": { + "lib": ["es2015"], + "noEmit": true, + "skipLibCheck": false, + "strict": true + } +} diff --git a/tsconfig.json b/tsconfig.json index 8e55fe76..344e282c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,19 @@ -{ - "compilerOptions": { - "declaration": true, - "importHelpers": false, - "incremental": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "preserveConstEnums": true, - "removeComments": false, - "sourceMap": true, - "strict": true, - "stripInternal": true, - "lib": ["es2017", "es2019.Symbol"], - "module": "commonjs", - "target": "es2017", - "outDir": "lib/", - }, - "exclude": [ - "example", - "test/esm", - "test/legacy", - "test/type" - ] -} +{ + "compilerOptions": { + "declaration": true, + "importHelpers": false, + "incremental": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "preserveConstEnums": true, + "removeComments": false, + "sourceMap": true, + "strict": true, + "stripInternal": true, + "lib": ["es2017", "es2019.Symbol"], + "module": "commonjs", + "target": "es2017", + "outDir": "lib/" + }, + "exclude": ["example", "test/esm", "test/legacy", "test/type"] +} diff --git a/tsconfig.nospec.json b/tsconfig.nospec.json index d6f5cbf3..bdc63200 100644 --- a/tsconfig.nospec.json +++ b/tsconfig.nospec.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.json", - "include": ["src/**/*"] -} \ No newline at end of file + "extends": "./tsconfig.json", + "include": ["src/**/*"] +} diff --git a/tsconfig.publish.json b/tsconfig.publish.json index bca4d0c1..48a82bfa 100644 --- a/tsconfig.publish.json +++ b/tsconfig.publish.json @@ -1,9 +1,9 @@ -{ - "extends": "./tsconfig.nospec.json", - "compilerOptions": { - "declaration": false, - "incremental": false, - "removeComments": true, - "sourceMap": false - } -} \ No newline at end of file +{ + "extends": "./tsconfig.nospec.json", + "compilerOptions": { + "declaration": false, + "incremental": false, + "removeComments": true, + "sourceMap": false + } +} diff --git a/tsconfig.publish.types.json b/tsconfig.publish.types.json index 60354452..fda14156 100644 --- a/tsconfig.publish.types.json +++ b/tsconfig.publish.types.json @@ -1,10 +1,10 @@ -{ - "extends": "./tsconfig.publish.json", - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "incremental": false, - "removeComments": false, - "outDir": "lib/types" - } -} \ No newline at end of file +{ + "extends": "./tsconfig.publish.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "incremental": false, + "removeComments": false, + "outDir": "lib/types" + } +} -- 2.51.2