├── .github ├── FUNDING.yml ├── linters │ └── .markdown-lint.yml └── workflows │ ├── add-discuss-during-sync.yml │ ├── announce-a-release.yml │ ├── breakage-against-ponyc-latest.yml │ ├── changelog-bot.yml │ ├── generate-documentation.yml │ ├── pr.yml │ ├── prepare-for-a-release.yml │ ├── release-notes-reminder.yml │ ├── release-notes.yml │ ├── release.yml │ └── remove-discuss-during-sync.yml ├── .markdownlintignore ├── .release-notes ├── 0.6.0.md ├── 0.6.1.md ├── 0.7.0.md └── next-release.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_PROCESS.md ├── STYLE_GUIDE.md ├── VERSION ├── corral.json ├── examples ├── async_tcp_property.pony ├── collection_generators.pony ├── custom_class.pony ├── list_reverse.pony └── main.pony ├── lock.json └── ponycheck ├── _test.pony ├── ascii_range.pony ├── for_all.pony ├── generator.pony ├── int_properties.pony ├── ponycheck.pony ├── poperator.pony ├── property.pony ├── property_helper.pony ├── property_runner.pony ├── property_unit_test.pony └── randomness.pony /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: ponyc 2 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/add-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Add discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | issue_comment: 9 | types: 10 | - created 11 | pull_request_target: 12 | types: 13 | - opened 14 | - edited 15 | - ready_for_review 16 | - reopened 17 | pull_request_review: 18 | types: 19 | - submitted 20 | 21 | jobs: 22 | add-label: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Add "discuss during sync" label to active GH entity 26 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 27 | with: 28 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 29 | add-labels: "discuss during sync" 30 | -------------------------------------------------------------------------------- /.github/workflows/announce-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Announce a release 2 | 3 | on: 4 | push: 5 | tags: announce-\d+.\d+.\d+ 6 | 7 | concurrency: announce-a-release 8 | 9 | jobs: 10 | announce: 11 | name: Announcements 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout main 15 | uses: actions/checkout@v2 16 | with: 17 | ref: "main" 18 | token: ${{ secrets.RELEASE_TOKEN }} 19 | - name: Release notes 20 | uses: ponylang/release-bot-action@0.6.1 21 | with: 22 | entrypoint: publish-release-notes-to-github 23 | env: 24 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 25 | - name: Zulip 26 | uses: ponylang/release-bot-action@0.6.1 27 | with: 28 | entrypoint: send-announcement-to-pony-zulip 29 | env: 30 | ZULIP_API_KEY: ${{ secrets.ZULIP_RELEASE_API_KEY }} 31 | ZULIP_EMAIL: ${{ secrets.ZULIP_RELEASE_EMAIL }} 32 | - name: Last Week in Pony 33 | uses: ponylang/release-bot-action@0.6.1 34 | with: 35 | entrypoint: add-announcement-to-last-week-in-pony 36 | env: 37 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 38 | 39 | post-announcement: 40 | name: Tasks to run after the release has been announced 41 | needs: 42 | - announce 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout main 46 | uses: actions/checkout@v2 47 | with: 48 | ref: "main" 49 | token: ${{ secrets.RELEASE_TOKEN }} 50 | - name: Rotate release notes 51 | uses: ponylang/release-bot-action@0.6.1 52 | with: 53 | entrypoint: rotate-release-notes 54 | env: 55 | GIT_USER_NAME: "Ponylang Main Bot" 56 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 57 | - name: Delete announcement trigger tag 58 | uses: ponylang/release-bot-action@0.6.1 59 | with: 60 | entrypoint: delete-announcement-tag 61 | env: 62 | GIT_USER_NAME: "Ponylang Main Bot" 63 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 64 | -------------------------------------------------------------------------------- /.github/workflows/breakage-against-ponyc-latest.yml: -------------------------------------------------------------------------------- 1 | name: ponyc update breakage test 2 | 3 | on: 4 | repository_dispatch: 5 | types: [shared-docker-linux-builders-updated] 6 | 7 | jobs: 8 | vs-ponyc-latest: 9 | name: Test against ponyc main 10 | runs-on: ubuntu-latest 11 | container: 12 | image: ponylang/shared-docker-ci-x86-64-unknown-linux-builder:latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Test 16 | run: make test config=debug 17 | - name: Send alert on failure 18 | if: ${{ failure() }} 19 | uses: zulip/github-actions-zulip@35d7ad8e98444f894dcfe1d4e17332581d28ebeb 20 | with: 21 | api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }} 22 | email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }} 23 | organization-url: 'https://ponylang.zulipchat.com/' 24 | to: notifications 25 | type: stream 26 | topic: ${{ github.repository }} scheduled job failure 27 | content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed. 28 | -------------------------------------------------------------------------------- /.github/workflows/changelog-bot.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Bot 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - CHANGELOG.md 11 | 12 | jobs: 13 | changelog-bot: 14 | runs-on: ubuntu-latest 15 | name: Update CHANGELOG.md 16 | steps: 17 | - name: Update Changelog 18 | uses: docker://ponylang/changelog-bot-action:0.3.3 19 | with: 20 | git_user_name: "Ponylang Main Bot" 21 | git_user_email: "ponylang.main@gmail.com" 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/generate-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Manually generate documentation 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | generate-documentation: 8 | name: Generate documentation for release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Generate documentation and upload 13 | uses: ponylang/library-documentation-action@0.3.0 14 | with: 15 | site_url: "https://ponylang.github.io/ponycheck/" 16 | library_name: "ponycheck" 17 | docs_build_dir: "build/ponycheck-docs" 18 | git_user_name: "Ponylang Main Bot" 19 | git_user_email: "ponylang.main@gmail.com" 20 | env: 21 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | superlinter: 7 | name: Lint bash, docker, markdown, and yaml 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Lint codebase 12 | uses: docker://github/super-linter:v3.8.3 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | VALIDATE_ALL_CODEBASE: true 16 | VALIDATE_BASH: true 17 | VALIDATE_DOCKERFILE: true 18 | VALIDATE_MD: true 19 | VALIDATE_YAML: true 20 | 21 | verify-changelog: 22 | name: Verify CHANGELOG is valid 23 | runs-on: ubuntu-latest 24 | container: 25 | image: ponylang/changelog-tool:release 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Verify CHANGELOG 29 | run: changelog-tool verify 30 | 31 | vs-ponyc-release: 32 | name: Test against recent ponyc release 33 | runs-on: ubuntu-latest 34 | container: 35 | image: ponylang/shared-docker-ci-x86-64-unknown-linux-builder:release 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Test 39 | run: make test config=debug 40 | -------------------------------------------------------------------------------- /.github/workflows/prepare-for-a-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare for a release 2 | 3 | on: 4 | push: 5 | tags: release-\d+.\d+.\d+ 6 | 7 | concurrency: prepare-for-a-release 8 | 9 | jobs: 10 | # all tasks that need to be done before we add an X.Y.Z tag 11 | # should be done as a step in the pre-tagging job. 12 | # think of it like this... if when you later checkout the tag for a release, 13 | # should the change be there? if yes, do it here. 14 | pre-tagging: 15 | name: Tasks run before tagging the release 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout main 19 | uses: actions/checkout@v2 20 | with: 21 | ref: "main" 22 | token: ${{ secrets.RELEASE_TOKEN }} 23 | - name: Update CHANGELOG.md 24 | uses: ponylang/release-bot-action@0.6.1 25 | with: 26 | entrypoint: update-changelog-for-release 27 | env: 28 | GIT_USER_NAME: "Ponylang Main Bot" 29 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 30 | - name: Update VERSION 31 | uses: ponylang/release-bot-action@0.6.1 32 | with: 33 | entrypoint: update-version-in-VERSION 34 | env: 35 | GIT_USER_NAME: "Ponylang Main Bot" 36 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 37 | - name: Update version in README 38 | uses: ponylang/release-bot-action@0.6.1 39 | with: 40 | entrypoint: update-version-in-README 41 | env: 42 | GIT_USER_NAME: "Ponylang Main Bot" 43 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 44 | - name: Update corral.json 45 | uses: ponylang/release-bot-action@0.6.1 46 | with: 47 | entrypoint: update-version-in-corral-json 48 | env: 49 | GIT_USER_NAME: "Ponylang Main Bot" 50 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 51 | 52 | # tag for release 53 | # this will kick off the next stage of the release process 54 | # no additional steps should be added to this job 55 | tag-release: 56 | name: Tag the release 57 | needs: 58 | - pre-tagging 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout main 62 | uses: actions/checkout@v2 63 | with: 64 | ref: "main" 65 | token: ${{ secrets.RELEASE_TOKEN }} 66 | - name: Trigger artefact creation 67 | uses: ponylang/release-bot-action@0.6.1 68 | with: 69 | entrypoint: trigger-artefact-creation 70 | env: 71 | GIT_USER_NAME: "Ponylang Main Bot" 72 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 73 | 74 | # all cleanup tags that should happen after tagging for release should happen 75 | # in the post-tagging job. examples of things you might do: 76 | # add a new unreleased section to a changelog 77 | # set a version back to "snapshot" 78 | # in general, post-tagging is for "going back to normal" from tasks that were 79 | # done during the pre-tagging job 80 | post-tagging: 81 | name: Tasks run after tagging the release 82 | needs: 83 | - tag-release 84 | runs-on: ubuntu-latest 85 | steps: 86 | - name: Checkout main 87 | uses: actions/checkout@v2 88 | with: 89 | ref: "main" 90 | token: ${{ secrets.RELEASE_TOKEN }} 91 | - name: Add "unreleased" section to CHANGELOG.md 92 | uses: ponylang/release-bot-action@0.6.1 93 | with: 94 | entrypoint: add-unreleased-section-to-changelog 95 | env: 96 | GIT_USER_NAME: "Ponylang Main Bot" 97 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 98 | -------------------------------------------------------------------------------- /.github/workflows/release-notes-reminder.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes Reminder 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled] 6 | 7 | jobs: 8 | release-note-reminder: 9 | runs-on: ubuntu-latest 10 | name: Prompt to add release notes 11 | steps: 12 | - name: Prompt to add release notes 13 | uses: docker://ponylang/release-notes-reminder-bot-action:0.1.0 14 | env: 15 | API_CREDENTIALS: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | name: Release Notes 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | paths-ignore: 10 | - .release-notes/next-release.md 11 | - .release-notes/\d+.\d+.\d+.md 12 | 13 | jobs: 14 | release-notes: 15 | runs-on: ubuntu-latest 16 | name: Update release notes 17 | steps: 18 | - name: Update 19 | uses: docker://ponylang/release-notes-bot-action:0.3.3 20 | with: 21 | git_user_name: "Ponylang Main Bot" 22 | git_user_email: "ponylang.main@gmail.com" 23 | env: 24 | API_CREDENTIALS: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - \d+.\d+.\d+ 7 | 8 | concurrency: release 9 | 10 | jobs: 11 | # validation to assure that we should in fact continue with the release should 12 | # be done here. the primary reason for this step is to verify that the release 13 | # was started correctly by pushing a `release-X.Y.Z` tag rather than `X.Y.Z`. 14 | pre-artefact-creation: 15 | name: Tasks to run before artefact creation 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout main 19 | uses: actions/checkout@v2 20 | with: 21 | ref: "main" 22 | token: ${{ secrets.RELEASE_TOKEN }} 23 | - name: Validate CHANGELOG 24 | uses: ponylang/release-bot-action@0.6.1 25 | with: 26 | entrypoint: pre-artefact-changelog-check 27 | 28 | generate-documentation: 29 | name: Generate documentation for release 30 | runs-on: ubuntu-latest 31 | needs: 32 | - pre-artefact-creation 33 | steps: 34 | - name: Checkout main 35 | uses: actions/checkout@v2 36 | with: 37 | ref: "main" 38 | token: ${{ secrets.RELEASE_TOKEN }} 39 | - name: Generate documentation and upload 40 | uses: ponylang/library-documentation-action@0.3.0 41 | with: 42 | site_url: "https://ponylang.github.io/ponycheck/" 43 | library_name: "ponycheck" 44 | docs_build_dir: "build/ponycheck-docs" 45 | git_user_name: "Ponylang Main Bot" 46 | git_user_email: "ponylang.main@gmail.com" 47 | env: 48 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 49 | 50 | trigger-release-announcement: 51 | name: Trigger release announcement 52 | runs-on: ubuntu-latest 53 | needs: 54 | - generate-documentation 55 | steps: 56 | - uses: actions/checkout@v2 57 | with: 58 | ref: "main" 59 | token: ${{ secrets.RELEASE_TOKEN }} 60 | - name: Trigger 61 | uses: ponylang/release-bot-action@0.6.1 62 | with: 63 | entrypoint: trigger-release-announcement 64 | env: 65 | GIT_USER_NAME: "Ponylang Main Bot" 66 | GIT_USER_EMAIL: "ponylang.main@gmail.com" 67 | -------------------------------------------------------------------------------- /.github/workflows/remove-discuss-during-sync.yml: -------------------------------------------------------------------------------- 1 | name: Remove discuss during sync label 2 | 3 | on: 4 | issues: 5 | types: 6 | - closed 7 | pull_request_target: 8 | types: 9 | - closed 10 | 11 | jobs: 12 | remove-label: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Remove label 16 | uses: andymckay/labeler@467347716a3bdbca7f277cb6cd5fa9c5205c5412 17 | with: 18 | repo-token: ${{ secrets.PONYLANG_MAIN_API_TOKEN }} 19 | remove-labels: "discuss during sync" 20 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | CODE_OF_CONDUCT.md 3 | .release-notes/ 4 | -------------------------------------------------------------------------------- /.release-notes/0.6.0.md: -------------------------------------------------------------------------------- 1 | ## Don't export tests as part of public interface 2 | 3 | Prior to this change, some testing only classes were being exported as part of the ponycheck public interface. 4 | 5 | -------------------------------------------------------------------------------- /.release-notes/0.6.1.md: -------------------------------------------------------------------------------- 1 | ## Fix Randomness.u64() returning numbers out of the given range 2 | 3 | When the range was specified, Randomness.u64() could return random numbers outside of it. 4 | -------------------------------------------------------------------------------- /.release-notes/0.7.0.md: -------------------------------------------------------------------------------- 1 | ## Update to work with latest ponyc 2 | 3 | The most recent ponyc implements [RFC #65](https://github.com/ponylang/rfcs/blob/main/text/0065-env-root-not-optional.md) which changes the type of `Env.root`. 4 | 5 | We've updated accordingly. You won't be able to use this and future versions of the library without a corresponding update to your ponyc version. 6 | 7 | -------------------------------------------------------------------------------- /.release-notes/next-release.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ponylang/ponycheck/155d145d7641c1ae8f94cc88bc7567058601186e/.release-notes/next-release.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a CHANGELOG](http://keepachangelog.com/). 4 | 5 | ## [unreleased] - unreleased 6 | 7 | ### Fixed 8 | 9 | 10 | ### Added 11 | 12 | 13 | ### Changed 14 | 15 | 16 | ## [0.7.0] - 2022-01-16 17 | 18 | ### Changed 19 | 20 | - Update for change in type of Env.root ([PR #58](https://github.com/ponylang/ponycheck/pull/58)) 21 | 22 | ## [0.6.1] - 2022-01-11 23 | 24 | ### Fixed 25 | 26 | - Fix Randomness.u64() returning numbers out of the given range ([PR #57](https://github.com/ponylang/ponycheck/pull/57)) 27 | 28 | ## [0.6.0] - 2021-04-03 29 | 30 | ### Changed 31 | 32 | - Don't export test classes as part of public interface ([PR #50](https://github.com/ponylang/ponycheck/pull/50)) 33 | 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainers at coc@ponylang.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | 48 | # Social Rules 49 | 50 | In addition to having a code of conduct as an anti-harassment policy, we have a small set of [social rules](https://www.recurse.com/manual#sub-sec-social-rules) we follow. We (the project maintainers) lifted these rules from the [Recurse Center](https://www.recurse.com). We've seen these rules in effect in other environments. We'd like the Pony community to share a similar positive environment. These rules are intended to be lightweight, and to make more explicit certain social norms that are normally implicit. Most of our social rules really boil down to “don't be a jerk” or “don't be annoying.” Of course, almost nobody sets out to be a jerk or annoying, so telling people not to be jerks isn't a very productive strategy. 51 | 52 | Unlike the anti-harassment policy, violation of the social rules will not result in expulsion from the Pony community or a strong warning from project maintainers. Rather, they are designed to provide some lightweight social structure for community members to use when interacting with each other. 53 | 54 | ## No feigning surprise. 55 | 56 | The first rule means you shouldn't act surprised when people say they don't know something. This applies to both technical things ("What?! I can't believe you don't know what the stack is!") and non-technical things ("You don't know who RMS is?!"). Feigning surprise has absolutely no social or educational benefit: When people feign surprise, it's usually to make them feel better about themselves and others feel worse. And even when that's not the intention, it's almost always the effect. 57 | 58 | ## No well-actually's 59 | 60 | A well-actually happens when someone says something that's almost - but not entirely - correct, and you say, "well, actually…" and then give a minor correction. This is especially annoying when the correction has no bearing on the actual conversation. This doesn't mean we aren't about truth-seeking or that we don't care about being precise. Almost all well-actually's in our experience are about grandstanding, not truth-seeking. 61 | 62 | ## No subtle -isms 63 | 64 | Our last social rule bans subtle racism, sexism, homophobia, transphobia, and other kinds of bias. This one is different from the rest, because it covers a class of behaviors instead of one very specific pattern. 65 | 66 | Subtle -isms are small things that make others feel uncomfortable, things that we all sometimes do by mistake. For example, saying "It's so easy my grandmother could do it" is a subtle -ism. Like the other three social rules, this one is often accidentally broken. Like the other three, it's not a big deal to mess up – you just apologize and move on. 67 | 68 | If you see a subtle -ism in the Pony community, you can point it out to the relevant person, either publicly or privately, or you can ask one of the project maintainers to say something. After this, we ask that all further discussion move off of public channels. If you are a third party, and you don't see what could be biased about the comment that was made, feel free to talk to the project maintainers. Please don't say, "Comment X wasn't homophobic!" Similarly, please don't pile on to someone who made a mistake. The "subtle" in "subtle -isms" means that it's probably not obvious to everyone right away what was wrong with the comment. 69 | 70 | If you have any questions about any part of the code of conduct or social rules, please feel free to reach out to any of the project maintainers. 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You want to contribute to http_server? Awesome. 4 | 5 | There are a number of ways to contribute. As this document is a little long, feel free to jump to the section that applies to you currently: 6 | 7 | * [Bug report](#bug-report) 8 | * [How to contribute](#how-to-contribute) 9 | * [Pull request](#pull-request) 10 | 11 | Additional notes regarding formatting: 12 | 13 | * [Documentation formatting](#documentation-formatting) 14 | * [Code formatting](#code-formatting) 15 | * [File naming](#file-naming) 16 | 17 | ## Bug report 18 | 19 | First of all please [search existing issues](https://github.com/ponylang/http_server/issues) to make sure your issue hasn't already been reported. If you cannot find a suitable issue — [create a new one](https://github.com/ponylang/http_server/issues/new). 20 | 21 | Provide the following details: 22 | 23 | * short summary of what you were trying to achieve, 24 | * a code snippet causing the bug, 25 | * expected result, 26 | * actual results and 27 | * environment details: at least operating system version 28 | 29 | If possible, try to isolate the problem and provide just enough code to demonstrate it. Add any related information which might help to fix the issue. 30 | 31 | ## How to contribute 32 | 33 | This project uses a fairly standard GitHub pull request workflow. If you have already contributed to a project via GitHub pull request, you can skip this section and proceed to the [specific details of what we ask for in a pull request](#pull-request). If this is your first time contributing to a project via GitHub, read on. 34 | 35 | Here is the basic GitHub workflow: 36 | 37 | 1. Fork this repo. you can do this via the GitHub website. This will result in you having your own copy of the repo under your GitHub account. 38 | 2. Clone your forked repo to your local machine 39 | 3. Make a branch for your change 40 | 4. Make your change on that branch 41 | 5. Push your change to your repo 42 | 6. Use the github ui to open a PR 43 | 44 | Some things to note that aren't immediately obvious to folks just starting out: 45 | 46 | 1. Your fork doesn't automatically stay up to date with changes in the main repo. 47 | 2. Any changes you make on your branch that you used for one PR will automatically appear in another PR so if you have more than 1 PR, be sure to always create different branches for them. 48 | 3. Weird things happen with commit history if you don't create your PR branches off of `main` so always make sure you have the `main` branch checked out before creating a branch for a PR 49 | 50 | You can get help using GitHub via [the official documentation](https://help.github.com/). Some highlights include: 51 | 52 | * [Fork A Repo](https://help.github.com/articles/fork-a-repo/) 53 | * [Creating a pull request](https://help.github.com/articles/creating-a-pull-request/) 54 | * [Syncing a fork](https://help.github.com/articles/syncing-a-fork/) 55 | 56 | ## Pull request 57 | 58 | Before issuing a pull request we ask that you squash all your commits into a single logical commit. While your PR is in review, we may ask for additional changes, please do not squash those commits while the review is underway. Once everything is good, I'll then ask you to further squash those commits before merging. We ask that you not squash while a review is underway as it can make it hard to follow what is going on. Additionally, we ask that you: 59 | 60 | * [Write a good commit message](http://chris.beams.io/posts/git-commit/) 61 | * Issue 1 Pull Request per feature. Don't lump unrelated changes together. 62 | 63 | If you aren't sure how to squash multiple commits into one, Steve Klabnik wrote [a handy guide](http://blog.steveklabnik.com/posts/2012-11-08-how-to-squash-commits-in-a-github-pull-request) that you can refer to. 64 | 65 | Once those conditions are met, the PR can be merged. 66 | 67 | Please note, if your changes are purely to things like README, CHANGELOG etc, you can add [skip ci] as the last line of your commit message and your PR won't be run through our continuous integration systems. We ask that you use [skip ci] where appropriate as it helps to get changes through CI faster and doesn't waste resources that CircleCI is kindly donating to the Open Source community. 68 | 69 | ## Documentation formatting 70 | 71 | When contributing to documentation, try to keep the following style guidelines in mind: 72 | 73 | As much as possible all documentation should be textual and in Markdown format. Diagrams are often needed to clarify a point. For any images, an original high-resolution source should be provided as well so updates can be made. 74 | 75 | Documentation is not "source code." As such, it should not be wrapped at 80 columns. Documentation should be allowed to flow naturally until the end of a paragraph. It is expected that the reader will turn on soft wrapping as needed. 76 | 77 | All code examples in documentation should be formatted in a fashion appropriate to the language in question. 78 | 79 | All command line examples in documentation should be presented in a copy and paste friendly fashion. Assume the user is using the `bash` shell. GitHub formatting on long command lines can be unfriendly to copy-and-paste. Long command lines should be broken up using `\` so that each line is no more than 80 columns. Wrapping at 80 columns should result in a good display experience in GitHub. Additionally, continuation lines should be indented two spaces. 80 | 81 | OK: 82 | 83 | ```bash 84 | my_command --some-option foo --path-to-file ../../project/long/line/foo \ 85 | --some-other-option bar 86 | ``` 87 | 88 | Not OK: 89 | 90 | ```bash 91 | my_command --some-option foo --path-to-file ../../project/long/line/foo --some-other-option bar 92 | ``` 93 | 94 | Wherever possible when writing documentation, favor full command options rather than short versions. Full flags are usually much easier to modify because the meaning is clearer. 95 | 96 | OK: 97 | 98 | ```bash 99 | my_command --messages 100 100 | ``` 101 | 102 | Not OK: 103 | 104 | ```bash 105 | my_command -m 100 106 | ``` 107 | 108 | ## Code formatting 109 | 110 | The basics: 111 | 112 | * Indentation 113 | 114 | Indent using spaces, not tabs. Indentation is language specific. 115 | 116 | * Watch your whitespace! 117 | 118 | Use an editor plugin to remove unused trailing whitespace including both at the end of a line and at the end of a file. By the same token, remember to leave a single newline only line at the end of each file. It makes output files to the console much more pleasant. 119 | 120 | * Line Length 121 | 122 | We all have different sized monitors. What might look good on yours might look like awful on another. Be kind and wrap all lines at 80 columns unless you have a good reason not to. 123 | 124 | * Reformatting code to meet standards 125 | 126 | Try to avoid doing it. A commit that changes the formatting for large chunks of a file makes for an ugly commit history when looking for changes. Don't commit code that doesn't conform to coding standards in the first place. If you do reformat code, make sure it is either standalone reformatting with no logic changes or confined solely to code whose logic you touched. For example, updating the indentation in a file? Do not make logic changes along with it. Editing a line that has extra whitespace at the end? Feel free to remove it. 127 | 128 | The details: 129 | 130 | All Pony sources should follow the [Pony standard library style guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 131 | 132 | ## File naming 133 | 134 | Pony code follows the [Pony standard library file naming guidelines](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md#naming). 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021 The Pony Developers 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | config ?= release 2 | 3 | PONYC ?= ponyc 4 | PACKAGE := ponycheck 5 | GET_DEPENDENCIES_WITH := corral fetch 6 | CLEAN_DEPENDENCIES_WITH := corral clean 7 | COMPILE_WITH := corral run -- $(PONYC) 8 | 9 | BUILD_DIR ?= build/$(config) 10 | SRC_DIR ?= $(PACKAGE) 11 | EXAMPLES_DIR := examples 12 | tests_binary := $(BUILD_DIR)/$(PACKAGE) 13 | docs_dir := build/$(PACKAGE)-docs 14 | 15 | ifdef config 16 | ifeq (,$(filter $(config),debug release)) 17 | $(error Unknown configuration "$(config)") 18 | endif 19 | endif 20 | 21 | ifeq ($(config),release) 22 | PONYC = $(COMPILE_WITH) 23 | else 24 | PONYC = $(COMPILE_WITH) --debug 25 | endif 26 | 27 | SOURCE_FILES := $(shell find $(SRC_DIR) -name *.pony) 28 | EXAMPLES_SOURCE_FILES := $(shell find $(EXAMPLES_DIR) -name *.pony) 29 | EXAMPLES_BINARY := $(BUILD_DIR)/examples 30 | 31 | test: unit-tests build-examples run-examples 32 | 33 | unit-tests: $(tests_binary) 34 | $^ --exclude=integration --sequential 35 | 36 | $(tests_binary): $(SOURCE_FILES) | $(BUILD_DIR) 37 | $(GET_DEPENDENCIES_WITH) 38 | $(PONYC) -o $(BUILD_DIR) $(SRC_DIR) 39 | 40 | build-examples: $(EXAMPLES_BINARY) 41 | 42 | run-examples: $(EXAMPLES_BINARY) 43 | $^ 44 | 45 | $(EXAMPLES_BINARY): $(BUILD_DIR)/%: $(SOURCE_FILES) $(EXAMPLES_SOURCE_FILES) | $(BUILD_DIR) 46 | $(GET_DEPENDENCIES_WITH) 47 | $(PONYC) -o $(BUILD_DIR) -b examples $(EXAMPLES_DIR) 48 | 49 | clean: 50 | $(CLEAN_DEPENDENCIES_WITH) 51 | rm -rf $(BUILD_DIR) 52 | 53 | $(docs_dir): $(SOURCE_FILES) 54 | rm -rf $(docs_dir) 55 | $(GET_DEPENDENCIES_WITH) 56 | $(PONYC) --docs-public --pass=docs --output build $(SRC_DIR) 57 | 58 | docs: $(docs_dir) 59 | 60 | .coverage: 61 | mkdir -p .coverage 62 | 63 | coverage: .coverage $(tests_binary) 64 | kcov --include-pattern="$(SRC_DIR)" --exclude-pattern="*/test/*.pony,*/_test.pony" .coverage $(tests_binary) 65 | 66 | TAGS: 67 | ctags --recurse=yes $(SRC_DIR) 68 | 69 | all: test 70 | 71 | $(BUILD_DIR): 72 | mkdir -p $(BUILD_DIR) 73 | 74 | .PHONY: all build-examples clean TAGS test 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PonyCheck 2 | 3 | Property based testing for ponylang (>= 0.19.0). 4 | 5 | ## Status 6 | 7 | PonyCheck is now part of the Pony standard library. This repository is no longer active. 8 | -------------------------------------------------------------------------------- /RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # How to cut a release 2 | 3 | This document is aimed at members of the Pony team who might be cutting a release of this library. It serves as a checklist that can take you through doing a release step-by-step. 4 | 5 | ## Prerequisites 6 | 7 | You must have commit access to this repository 8 | 9 | ## Releasing 10 | 11 | Please note that this document was written with the assumption that you are using a clone of the `ponycheck` repo. You have to be using a clone rather than a fork. It is advised to your do this by making a fresh clone of the `ponycheck` repo from which you will release. 12 | 13 | ```bash 14 | git clone git@github.com:ponylang/ponycheck.git ponycheck-release-clean 15 | cd ponycheck-release-clean 16 | ``` 17 | 18 | Before getting started, you will need a number for the version that you will be releasing as well as an agreed upon "golden commit" that will form the basis of the release. 19 | 20 | The "golden commit" must be `HEAD` on the `main` branch of this repository. At this time, releasing from any other location is not supported. 21 | 22 | For the duration of this document, that we are releasing version is `0.3.1`. Any place you see those values, please substitute your own version. 23 | 24 | ```bash 25 | git tag release-0.3.1 26 | git push origin release-0.3.1 27 | ``` 28 | -------------------------------------------------------------------------------- /STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | ponycheck follows the [Pony standard library Style Guide](https://github.com/ponylang/ponyc/blob/main/STYLE_GUIDE.md). 4 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.0 2 | -------------------------------------------------------------------------------- /corral.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "ponycheck" 4 | ], 5 | "deps": [], 6 | "info": { 7 | "description": "Property based testing library", 8 | "homepage": "https://github.com/ponylang/ponycheck", 9 | "license": "BSD-2-Clause", 10 | "documentation_url": "https://ponylang.github.io/ponycheck/", 11 | "version": "0.7.0", 12 | "name": "ponycheck" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/async_tcp_property.pony: -------------------------------------------------------------------------------- 1 | use "../ponycheck" 2 | use "ponytest" 3 | use "net" 4 | use "itertools" 5 | 6 | class _TCPSenderConnectionNotify is TCPConnectionNotify 7 | let _message: String 8 | 9 | new create(message: String) => 10 | _message = message 11 | 12 | fun ref connected(conn: TCPConnection ref) => 13 | conn.write(_message) 14 | 15 | fun ref connect_failed(conn: TCPConnection ref) => 16 | None 17 | 18 | fun ref received( 19 | conn: TCPConnection ref, 20 | data: Array[U8] iso, 21 | times: USize) 22 | : Bool 23 | => 24 | conn.close() 25 | true 26 | 27 | class val TCPSender 28 | """ 29 | Class under test 30 | 31 | Simple class that sends a string to a tcp server 32 | """ 33 | 34 | let _auth: AmbientAuth 35 | 36 | new val create(auth: AmbientAuth) => 37 | _auth = auth 38 | 39 | fun send( 40 | host: String, 41 | port: String, 42 | message: String): TCPConnection tag^ 43 | => 44 | TCPConnection( 45 | _auth, 46 | recover _TCPSenderConnectionNotify(message) end, 47 | host, 48 | port) 49 | 50 | 51 | // Test Cruft 52 | class MyTCPConnectionNotify is TCPConnectionNotify 53 | let _ph: PropertyHelper 54 | let _expected: String 55 | 56 | new create(ph: PropertyHelper, expected: String) => 57 | _ph = ph 58 | _expected = expected 59 | 60 | fun ref received( 61 | conn: TCPConnection ref, 62 | data: Array[U8] iso, 63 | times: USize) 64 | : Bool 65 | => 66 | _ph.log("received " + data.size().string() + " bytes", true) 67 | // assert we received the expected string 68 | _ph.assert_eq[USize](data.size(), _expected.size()) 69 | for bytes in Iter[U8](_expected.values()).zip[U8]((consume data).values()) do 70 | _ph.assert_eq[U8](bytes._1, bytes._2) 71 | end 72 | // this will signal to the ponycheck engine that this property is done 73 | // it will nonetheless execute until the end 74 | _ph.complete(true) 75 | conn.close() 76 | true 77 | 78 | fun ref connect_failed(conn: TCPConnection ref) => 79 | _ph.fail("connect failed") 80 | conn.close() 81 | 82 | class MyTCPListenNotify is TCPListenNotify 83 | 84 | let _sender: TCPSender 85 | let _ph: PropertyHelper 86 | let _expected: String 87 | 88 | new create( 89 | sender: TCPSender, 90 | ph: PropertyHelper, 91 | expected: String) => 92 | _sender = sender 93 | _ph = ph 94 | _expected = expected 95 | 96 | 97 | fun ref listening(listen: TCPListener ref) => 98 | let address = listen.local_address() 99 | try 100 | (let host, let port) = address.name(where reversedns = None, servicename = false)? 101 | 102 | // now that we know the server's address we can actually send something 103 | _ph.dispose_when_done( 104 | _sender.send(host, port, _expected)) 105 | else 106 | _ph.fail("could not determine server host and port") 107 | end 108 | 109 | fun ref connected(listen: TCPListener ref): TCPConnectionNotify iso^ => 110 | recover iso 111 | MyTCPConnectionNotify(_ph, _expected) 112 | end 113 | 114 | fun ref not_listening(listen: TCPListener ref) => 115 | _ph.fail("not listening") 116 | 117 | class _AsyncTCPSenderProperty is Property1[String] 118 | fun name(): String => "async/tcp_sender" 119 | 120 | fun params(): PropertyParams => 121 | PropertyParams(where async' = true, timeout' = 5_000_000_000) 122 | 123 | fun gen(): Generator[String] => 124 | Generators.unicode() 125 | 126 | fun ref property(sample: String, ph: PropertyHelper) => 127 | let sender = TCPSender(ph.env.root) 128 | ph.dispose_when_done( 129 | TCPListener( 130 | ph.env.root, 131 | recover MyTCPListenNotify(sender, ph, "PONYCHECK") end, 132 | "127.0.0.1", 133 | "0")) 134 | 135 | -------------------------------------------------------------------------------- /examples/collection_generators.pony: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows a rather complex scenario. 3 | 4 | Here we want to generate both a random size that we use as the length of an array that 5 | we create in our property and a random number of operations on random elements 6 | of the array. 7 | 8 | We don't want to use another source of randomness for determining a random element 9 | in our property code, as it is better for reproducability of the property 10 | to make the whole execution only depend on the randomness used when drawing samples 11 | from the Generator. 12 | 13 | This way, given a certain seed in the `PropertyParams` we can actually reliably reproduce a failed example. 14 | """ 15 | use "../ponycheck" 16 | use "collections" 17 | 18 | class val _OperationOnCollection[T, R = String] is Stringable 19 | """represents a certain operation on an element addressed by idx""" 20 | let idx: USize 21 | let op: {(T): R} val 22 | 23 | new val create(idx': USize, op': {(T): R} val) => 24 | idx = idx' 25 | op = op' 26 | 27 | fun string(): String iso^ => 28 | recover 29 | String.>append("_OperationOnCollection(" + idx.string() + ")") 30 | end 31 | 32 | class _OperationOnCollectionProperty is Property1[(USize, Array[_OperationOnCollection[String]])] 33 | fun name(): String => "collections/operation_on_random_collection_elements" 34 | 35 | fun gen(): Generator[(USize, Array[_OperationOnCollection[String]])] => 36 | """ 37 | This generator produces: 38 | * a tuple of the number of elements of a collection to be created in the property code 39 | * an array of a randomly chosen operation on a random element of the collection 40 | 41 | Therefore we first create a generator for the collection size, 42 | then we flat_map over this generator, to generate one for an array of operations, 43 | whose index is randomly chosen from a range of `[0, num_elements)`. 44 | 45 | The first generated value determines the further values, we want to construct a generator 46 | based on the value of another one. For this kind of construct, we use `Generator.flat_map`. 47 | 48 | We then flat_map again over the Generator for the element index (that is bound by `num_elements`) 49 | to get a generator for an `_OperationOnCollection[String]` which needs the index as a constructor argument. 50 | The operation generator depends on the value of the randomly chosen element index, 51 | thus we use `Generator.flat_map` again. 52 | """ 53 | Generators.usize(2, 100).flat_map[(USize, Array[_OperationOnCollection[String]])]( 54 | {(num_elements: USize) => 55 | let elements_generator = 56 | Generators.array_of[_OperationOnCollection[String]]( 57 | Generators.usize(0, num_elements-1) 58 | .flat_map[_OperationOnCollection[String]]({(element) => 59 | Generators.one_of[_OperationOnCollection[String]]( 60 | [ 61 | _OperationOnCollection[String](element, {(s) => recover String.>append(s).>append("foo") end }) 62 | _OperationOnCollection[String](element, {(s) => recover String.>append(s).>append("bar") end }) 63 | ] 64 | ) 65 | }) 66 | ) 67 | Generators.zip2[USize, Array[_OperationOnCollection[String]]]( 68 | Generators.unit[USize](num_elements), 69 | elements_generator) 70 | }) 71 | 72 | fun ref property(sample: (USize, Array[_OperationOnCollection[String]]), h: PropertyHelper) => 73 | (let len, let ops) = sample 74 | 75 | // create and fill the array 76 | let coll = Array[String].create(len) 77 | for i in Range(0, len) do 78 | coll.push(i.string()) 79 | end 80 | 81 | // execute random operations on random elements of the array 82 | for op in ops.values() do 83 | try 84 | let elem = coll(op.idx)? 85 | let res = op.op(elem) 86 | if not h.assert_true(res.contains("foo") or res.contains("bar")) then return end 87 | else 88 | h.fail("illegal access") 89 | end 90 | end 91 | 92 | -------------------------------------------------------------------------------- /examples/custom_class.pony: -------------------------------------------------------------------------------- 1 | use "../ponycheck" 2 | use "itertools" 3 | 4 | primitive Blue is Stringable 5 | fun string(): String iso^ => "blue".clone() 6 | primitive Green is Stringable 7 | fun string(): String iso^ => "green".clone() 8 | primitive Pink is Stringable 9 | fun string(): String iso^ => "pink".clone() 10 | primitive Rose is Stringable 11 | fun string(): String iso^ => "rose".clone() 12 | 13 | type Color is ( Blue | Green | Pink | Rose ) 14 | 15 | class MyLittlePony is Stringable 16 | 17 | let name: String 18 | let cuteness: U64 19 | let color: Color 20 | 21 | new create(name': String, cuteness': U64 = U64.max_value(), color': Color) => 22 | name = name' 23 | cuteness = cuteness' 24 | color = color' 25 | 26 | fun is_cute(): Bool => 27 | (cuteness > 10) or (color is Pink) 28 | 29 | fun string(): String iso^ => 30 | recover 31 | String(17 + name.size()).>append("Pony(\"" + name + "\", " + cuteness.string() + ", " + color.string() + ")") 32 | end 33 | 34 | class _CustomClassMapProperty is Property1[MyLittlePony] 35 | """ 36 | The go-to approach for creating custom classes are the 37 | `Generators.map2`, `Generators.map3` and `Generators.map4` combinators and 38 | of course the `map` method on `Generator` itself (for single argument 39 | constructors). 40 | 41 | Generators created like this have better shrinking support 42 | and their creation is much more readable than the `flat_map` solution below. 43 | """ 44 | fun name(): String => "custom_class/map" 45 | 46 | fun gen(): Generator[MyLittlePony] => 47 | let name_gen = Generators.ascii_letters(5, 10) 48 | let cuteness_gen = Generators.u64(11, 100) 49 | let color_gen = Generators.one_of[Color]([Blue; Green; Pink; Rose] where do_shrink=true) 50 | Generators.map3[String, U64, Color, MyLittlePony]( 51 | name_gen, 52 | cuteness_gen, 53 | color_gen, 54 | {(name, cuteness, color) => 55 | MyLittlePony(name, cuteness, color) 56 | }) 57 | 58 | fun ref property(pony: MyLittlePony, ph: PropertyHelper) => 59 | ph.assert_true(pony.is_cute()) 60 | 61 | class _CustomClassFlatMapProperty is Property1[MyLittlePony] 62 | """ 63 | It is possible to create a generator using `flat_map` on a source 64 | generator, creating a new Generator in the `flat_map` function. This way 65 | it is possible to combine multiple generators into a single one that is based on 66 | multiple generators, one for each constructor argument. 67 | 68 | ### Drawbacks 69 | 70 | * the nested `flat_map` syntax is a little bit cumbersome (e.g. the captured 71 | from the surrounding scope need to be provided explicitly) 72 | * the resuling generator has only limited shrinking support. 73 | Only on the innermost created generator in the last `flat_map` function 74 | will be properly shrunken. 75 | """ 76 | fun name(): String => "custom_class/flat_map" 77 | 78 | fun gen(): Generator[MyLittlePony] => 79 | let name_gen = Generators.ascii_letters(5, 10) 80 | let cuteness_gen = Generators.u64(11, 100) 81 | let color_gen = 82 | Generators.one_of[Color]([Blue; Green; Pink; Rose] where do_shrink=true) 83 | color_gen.flat_map[MyLittlePony]({(color: Color)(cuteness_gen, name_gen) => 84 | name_gen.flat_map[MyLittlePony]({(name: String)(color, cuteness_gen) => 85 | cuteness_gen 86 | .map[MyLittlePony]({(cuteness: U64)(color, name) => 87 | MyLittlePony.create(name, cuteness, color) 88 | }) 89 | }) 90 | }) 91 | 92 | fun ref property(pony: MyLittlePony, ph: PropertyHelper) => 93 | ph.assert_true(pony.is_cute()) 94 | 95 | class _CustomClassCustomGeneratorProperty is Property1[MyLittlePony] 96 | """ 97 | Generating your class given a custom generator is the most flexible 98 | but also the most complicated approach. 99 | 100 | You need to understand the types `GenerateResult[T]` and `ValueAndShrink[T]` 101 | and how a basic `Generator` works. 102 | 103 | You basically have two options on how to implement a Generator: 104 | * Return only the generated value from `generate` (and optionally implement 105 | the `shrink` method to return an `(T^, Iterator[T^])` whose values need to 106 | meet the Generator's requirements 107 | * Return both the generated value and the shrink-Iterator from `generate`. 108 | this way you have the values from any Generators available your Generator 109 | is based upon. 110 | 111 | This Property is presenting the second option, returning a `ValueAndShrink[MyLittlePony]` 112 | from `generate`. 113 | """ 114 | 115 | fun name(): String => "custom_class/custom_generator" 116 | 117 | fun gen(): Generator[MyLittlePony] => 118 | Generator[MyLittlePony]( 119 | object is GenObj[MyLittlePony] 120 | let name_gen: Generator[String] = Generators.ascii_printable(5, 10) 121 | let cuteness_gen: Generator[U64] = Generators.u64(11, 100) 122 | let color_gen: Generator[Color] = 123 | Generators.one_of[Color]([Blue; Green; Pink; Rose] where do_shrink=true) 124 | 125 | fun generate(rnd: Randomness): GenerateResult[MyLittlePony] ? => 126 | (let name, let name_shrinks) = name_gen.generate_and_shrink(rnd)? 127 | (let cuteness, let cuteness_shrinks) = 128 | cuteness_gen.generate_and_shrink(rnd)? 129 | (let color, let color_shrinks) = color_gen.generate_and_shrink(rnd)? 130 | let res = MyLittlePony(consume name, consume cuteness, consume color) 131 | let shrinks = 132 | Iter[String^](name_shrinks) 133 | .zip2[U64^, Color^](cuteness_shrinks, color_shrinks) 134 | .map[MyLittlePony^]({(zipped) => 135 | (let n: String, let cute: U64, let col: Color) = consume zipped 136 | MyLittlePony(consume n, consume cute, consume col) 137 | }) 138 | (consume res, shrinks) 139 | end 140 | ) 141 | 142 | fun ref property(pony: MyLittlePony, ph: PropertyHelper) => 143 | ph.assert_true(pony.is_cute()) 144 | 145 | -------------------------------------------------------------------------------- /examples/list_reverse.pony: -------------------------------------------------------------------------------- 1 | use "ponytest" 2 | use "../ponycheck" 3 | 4 | 5 | class _ListReverseProperty is Property1[Array[USize]] 6 | fun name(): String => "list/reverse" 7 | 8 | fun gen(): Generator[Array[USize]] => 9 | Generators.seq_of[USize, Array[USize]](Generators.usize()) 10 | 11 | fun ref property(arg1: Array[USize], ph: PropertyHelper) => 12 | ph.assert_array_eq[USize](arg1, arg1.reverse().reverse()) 13 | 14 | class _ListReverseOneProperty is Property1[Array[USize]] 15 | fun name(): String => "list/reverse/one" 16 | 17 | fun gen(): Generator[Array[USize]] => 18 | Generators.seq_of[USize, Array[USize]](Generators.usize() where min=1, max=1) 19 | 20 | fun ref property(arg1: Array[USize], ph: PropertyHelper) => 21 | ph.assert_eq[USize](arg1.size(), 1) 22 | ph.assert_array_eq[USize](arg1, arg1.reverse()) 23 | 24 | class _ListReverseMultipleProperties is UnitTest 25 | fun name(): String => "list/properties" 26 | 27 | fun apply(h: TestHelper) ? => 28 | let g = Generators 29 | 30 | let gen1 = recover val g.seq_of[USize, Array[USize]](g.usize()) end 31 | Ponycheck.for_all[Array[USize]](gen1, h)( 32 | {(arg1, ph) => 33 | ph.assert_array_eq[USize](arg1, arg1.reverse().reverse()) 34 | })? 35 | 36 | let gen2 = recover val g.seq_of[USize, Array[USize]](g.usize(), 1, 1) end 37 | Ponycheck.for_all[Array[USize]](gen2, h)( 38 | {(arg1, ph) => 39 | ph.assert_array_eq[USize](arg1, arg1.reverse()) 40 | })? 41 | 42 | -------------------------------------------------------------------------------- /examples/main.pony: -------------------------------------------------------------------------------- 1 | use "../ponycheck" 2 | use "ponytest" 3 | 4 | actor Main is TestList 5 | new create(env: Env) => 6 | PonyTest(env, this) 7 | 8 | new make() => None 9 | 10 | fun tag tests(test: PonyTest) => 11 | test(Property1UnitTest[Array[USize]](_ListReverseProperty)) 12 | test(Property1UnitTest[Array[USize]](_ListReverseOneProperty)) 13 | test(_ListReverseMultipleProperties) 14 | test(Property1UnitTest[MyLittlePony](_CustomClassFlatMapProperty)) 15 | test(Property1UnitTest[MyLittlePony](_CustomClassMapProperty)) 16 | test(Property1UnitTest[MyLittlePony](_CustomClassCustomGeneratorProperty)) 17 | test(Property1UnitTest[String](_AsyncTCPSenderProperty)) 18 | test(Property1UnitTest[(USize, Array[_OperationOnCollection[String]])](_OperationOnCollectionProperty)) 19 | -------------------------------------------------------------------------------- /lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "locks": [] 3 | } 4 | -------------------------------------------------------------------------------- /ponycheck/_test.pony: -------------------------------------------------------------------------------- 1 | use "ponytest" 2 | 3 | use "collections" 4 | use "itertools" 5 | use "random" 6 | use "time" 7 | 8 | actor \nodoc\ Main is TestList 9 | new create(env: Env) => PonyTest(env, this) 10 | 11 | new make() => None 12 | 13 | fun tag tests(test: PonyTest) => 14 | test(_GenRndTest) 15 | test(_GenFilterTest) 16 | test(_GenUnionTest) 17 | test(_GenFrequencyTest) 18 | test(_GenFrequencySafeTest) 19 | test(_GenOneOfTest) 20 | test(_GenOneOfSafeTest) 21 | test(_SeqOfTest) 22 | test(_IsoSeqOfTest) 23 | test(_SetOfTest) 24 | test(_SetOfMaxTest) 25 | test(_SetOfEmptyTest) 26 | test(_SetIsOfIdentityTest) 27 | test(_MapOfEmptyTest) 28 | test(_MapOfMaxTest) 29 | test(_MapOfIdentityTest) 30 | test(_MapIsOfEmptyTest) 31 | test(_MapIsOfMaxTest) 32 | test(_MapIsOfIdentityTest) 33 | 34 | test(_StringifyTest) 35 | 36 | test(_SuccessfulPropertyTest) 37 | test(Property1UnitTest[U8](_SuccessfulProperty)) 38 | test(_FailingPropertyTest) 39 | test(_ErroringPropertyTest) 40 | test(Property2UnitTest[U8, U8](_SuccessfulProperty2)) 41 | test(_SuccessfulProperty2Test) 42 | test(Property3UnitTest[U8, U8, U8](_SuccessfulProperty3)) 43 | test(_SuccessfulProperty3Test) 44 | test(Property4UnitTest[U8, U8, U8, U8](_SuccessfulProperty4)) 45 | test(_SuccessfulProperty4Test) 46 | 47 | test(_ForAllTest) 48 | test(_MultipleForAllTest) 49 | test(_ForAll2Test) 50 | test(_ForAll3Test) 51 | test(_ForAll4Test) 52 | 53 | test(_ASCIIRangeTest) 54 | test(_UTF32CodePointStringTest) 55 | test(_SignedShrinkTest) 56 | test(_UnsignedShrinkTest) 57 | test(_ASCIIStringShrinkTest) 58 | test(_MinASCIIStringShrinkTest) 59 | test(_UnicodeStringShrinkTest) 60 | test(_MinUnicodeStringShrinkTest) 61 | test(_FilterMapShrinkTest) 62 | test(_RunnerInfiniteShrinkTest) 63 | test(_RunnerReportFailedSampleTest) 64 | test(_RunnerErroringGeneratorTest) 65 | test(_RunnerSometimesErroringGeneratorTest) 66 | test(_RunnerAsyncPropertyCompleteTest) 67 | test(_RunnerAsyncPropertyCompleteFalseTest) 68 | test(_RunnerAsyncFailTest) 69 | test(_RunnerAsyncCompleteActionTest) 70 | test(_RunnerAsyncCompleteMultiActionTest) 71 | test(_RunnerAsyncCompleteMultiSucceedActionTest) 72 | test(_RunnerAsyncMultiCompleteSucceedTest) 73 | test(_RunnerAsyncMultiCompleteFailTest) 74 | 75 | test(_SuccessfulIntPropertyTest) 76 | test(IntUnitTest(_SuccessfulIntProperty)) 77 | test(_SuccessfulIntPairPropertyTest) 78 | test(IntPairUnitTest(_SuccessfulIntPairProperty)) 79 | 80 | test(Property1UnitTest[(U8, U8)](_RandomnessProperty[U8, _RandomCaseU8]("U8"))) 81 | test(Property1UnitTest[(I8, I8)](_RandomnessProperty[I8, _RandomCaseI8]("I8"))) 82 | test(Property1UnitTest[(U16, U16)](_RandomnessProperty[U16, _RandomCaseU16]("U16"))) 83 | test(Property1UnitTest[(I16, I16)](_RandomnessProperty[I16, _RandomCaseI16]("I16"))) 84 | test(Property1UnitTest[(U32, U32)](_RandomnessProperty[U32, _RandomCaseU32]("U32"))) 85 | test(Property1UnitTest[(I32, I32)](_RandomnessProperty[I32, _RandomCaseI32]("I32"))) 86 | test(Property1UnitTest[(U64, U64)](_RandomnessProperty[U64, _RandomCaseU64]("U64"))) 87 | test(Property1UnitTest[(I64, I64)](_RandomnessProperty[I64, _RandomCaseI64]("I64"))) 88 | test(Property1UnitTest[(U128, U128)](_RandomnessProperty[U128, _RandomCaseU128]("U128"))) 89 | test(Property1UnitTest[(I128, I128)](_RandomnessProperty[I128, _RandomCaseI128]("I128"))) 90 | test(Property1UnitTest[(ISize, ISize)](_RandomnessProperty[ISize, _RandomCaseISize]("ISize"))) 91 | test(Property1UnitTest[(ILong, ILong)](_RandomnessProperty[ILong, _RandomCaseILong]("ILong"))) 92 | 93 | 94 | class \nodoc\ iso _StringifyTest is UnitTest 95 | fun name(): String => "stringify" 96 | 97 | fun apply(h: TestHelper) => 98 | (let _, var s) = _Stringify.apply[(U8, U8)]((0, 1)) 99 | h.assert_eq[String](s, "(0, 1)") 100 | (let _, s) = _Stringify.apply[(U8, U32, U128)]((0, 1, 2)) 101 | h.assert_eq[String](s, "(0, 1, 2)") 102 | (let _, s) = _Stringify.apply[(U8, (U32, U128))]((0, (1, 2))) 103 | h.assert_eq[String](s, "(0, (1, 2))") 104 | (let _, s) = _Stringify.apply[((U8, U32), U128)](((0, 1), 2)) 105 | h.assert_eq[String](s, "((0, 1), 2)") 106 | let a: Array[U8] = [ U8(0); U8(42) ] 107 | (let _, s) = _Stringify.apply[Array[U8]](a) 108 | h.assert_eq[String](s, "[0 42]") 109 | 110 | class \nodoc\ iso _SuccessfulProperty is Property1[U8] 111 | """ 112 | this just tests that a property is compatible with ponytest 113 | """ 114 | fun name(): String => "as_unit_test/successful/property" 115 | 116 | fun gen(): Generator[U8] => Generators.u8(0, 10) 117 | 118 | fun ref property(arg1: U8, h: PropertyHelper) => 119 | h.assert_true(arg1 <= U8(10)) 120 | 121 | class \nodoc\ iso _SuccessfulPropertyTest is UnitTest 122 | fun name(): String => "as_unit_test/successful" 123 | 124 | fun apply(h: TestHelper) => 125 | let property = recover iso _SuccessfulProperty end 126 | let property_notify = _UnitTestPropertyNotify(h, true) 127 | let property_logger = _UnitTestPropertyLogger(h) 128 | let params = property.params() 129 | h.long_test(params.timeout) 130 | let runner = PropertyRunner[U8]( 131 | consume property, 132 | params, 133 | property_notify, 134 | property_logger, 135 | h.env) 136 | runner.run() 137 | 138 | class \nodoc\ iso _FailingProperty is Property1[U8] 139 | fun name(): String => "as_unit_test/failing/property" 140 | 141 | fun gen(): Generator[U8] => Generators.u8(0, 10) 142 | 143 | fun ref property(arg1: U8, h: PropertyHelper) => 144 | h.assert_true(arg1 <= U8(5)) 145 | 146 | class \nodoc\ iso _FailingPropertyTest is UnitTest 147 | fun name(): String => "as_unit_test/failing" 148 | 149 | fun apply(h: TestHelper) => 150 | let property = recover iso _FailingProperty end 151 | let property_notify = _UnitTestPropertyNotify(h, false) 152 | let property_logger = _UnitTestPropertyLogger(h) 153 | let params = property.params() 154 | h.long_test(params.timeout) 155 | let runner = PropertyRunner[U8]( 156 | consume property, 157 | params, 158 | property_notify, 159 | property_logger, 160 | h.env) 161 | runner.run() 162 | 163 | class \nodoc\ iso _ErroringProperty is Property1[U8] 164 | fun name(): String => "as_unit_test/erroring/property" 165 | 166 | fun gen(): Generator[U8] => Generators.u8(0, 1) 167 | 168 | fun ref property(arg1: U8, h: PropertyHelper) ? => 169 | if arg1 < 2 then 170 | error 171 | end 172 | 173 | class \nodoc\ iso _ErroringPropertyTest is UnitTest 174 | fun name(): String => "as_unit_test/erroring" 175 | 176 | fun apply(h: TestHelper) => 177 | h.long_test(20_000_000_000) 178 | let property = recover iso _ErroringProperty end 179 | let property_notify = _UnitTestPropertyNotify(h, false) 180 | let property_logger = _UnitTestPropertyLogger(h) 181 | let params = property.params() 182 | let runner = PropertyRunner[U8]( 183 | consume property, 184 | params, 185 | property_notify, 186 | property_logger, 187 | h.env) 188 | runner.run() 189 | 190 | 191 | class \nodoc\ iso _SuccessfulProperty2 is Property2[U8, U8] 192 | fun name(): String => "as_unit_test/successful2/property" 193 | fun gen1(): Generator[U8] => Generators.u8(0, 1) 194 | fun gen2(): Generator[U8] => Generators.u8(2, 3) 195 | 196 | fun ref property2(arg1: U8, arg2: U8, h: PropertyHelper) => 197 | h.assert_ne[U8](arg1, arg2) 198 | 199 | class \nodoc\ iso _SuccessfulProperty2Test is UnitTest 200 | fun name(): String => "as_unit_test/successful2" 201 | 202 | fun apply(h: TestHelper) => 203 | let property2 = recover iso _SuccessfulProperty2 end 204 | let property2_notify = _UnitTestPropertyNotify(h, true) 205 | let property2_logger = _UnitTestPropertyLogger(h) 206 | let params = property2.params() 207 | h.long_test(params.timeout) 208 | let runner = PropertyRunner[(U8, U8)]( 209 | consume property2, 210 | params, 211 | property2_notify, 212 | property2_logger, 213 | h.env) 214 | runner.run() 215 | 216 | class \nodoc\ iso _SuccessfulProperty3 is Property3[U8, U8, U8] 217 | fun name(): String => "as_unit_test/successful3/property" 218 | fun gen1(): Generator[U8] => Generators.u8(0, 1) 219 | fun gen2(): Generator[U8] => Generators.u8(2, 3) 220 | fun gen3(): Generator[U8] => Generators.u8(4, 5) 221 | 222 | fun ref property3(arg1: U8, arg2: U8, arg3: U8, h: PropertyHelper) => 223 | h.assert_ne[U8](arg1, arg2) 224 | h.assert_ne[U8](arg2, arg3) 225 | h.assert_ne[U8](arg1, arg3) 226 | 227 | class \nodoc\ iso _SuccessfulProperty3Test is UnitTest 228 | 229 | fun name(): String => "as_unit_test/successful3" 230 | 231 | fun apply(h: TestHelper) => 232 | let property3 = recover iso _SuccessfulProperty3 end 233 | let property3_notify = _UnitTestPropertyNotify(h, true) 234 | let property3_logger = _UnitTestPropertyLogger(h) 235 | let params = property3.params() 236 | h.long_test(params.timeout) 237 | let runner = PropertyRunner[(U8, U8, U8)]( 238 | consume property3, 239 | params, 240 | property3_notify, 241 | property3_logger, 242 | h.env) 243 | runner.run() 244 | 245 | class \nodoc\ iso _SuccessfulProperty4 is Property4[U8, U8, U8, U8] 246 | fun name(): String => "as_unit_test/successful4/property" 247 | fun gen1(): Generator[U8] => Generators.u8(0, 1) 248 | fun gen2(): Generator[U8] => Generators.u8(2, 3) 249 | fun gen3(): Generator[U8] => Generators.u8(4, 5) 250 | fun gen4(): Generator[U8] => Generators.u8(6, 7) 251 | 252 | fun ref property4(arg1: U8, arg2: U8, arg3: U8, arg4: U8, h: PropertyHelper) => 253 | h.assert_ne[U8](arg1, arg2) 254 | h.assert_ne[U8](arg1, arg3) 255 | h.assert_ne[U8](arg1, arg4) 256 | h.assert_ne[U8](arg2, arg3) 257 | h.assert_ne[U8](arg2, arg4) 258 | h.assert_ne[U8](arg3, arg4) 259 | 260 | class \nodoc\ iso _SuccessfulProperty4Test is UnitTest 261 | 262 | fun name(): String => "as_unit_test/successful4" 263 | 264 | fun apply(h: TestHelper) => 265 | let property4 = recover iso _SuccessfulProperty4 end 266 | let property4_notify = _UnitTestPropertyNotify(h, true) 267 | let property4_logger = _UnitTestPropertyLogger(h) 268 | let params = property4.params() 269 | h.long_test(params.timeout) 270 | let runner = PropertyRunner[(U8, U8, U8, U8)]( 271 | consume property4, 272 | params, 273 | property4_notify, 274 | property4_logger, 275 | h.env) 276 | runner.run() 277 | 278 | class \nodoc\ iso _RunnerAsyncPropertyCompleteTest is UnitTest 279 | 280 | fun name(): String => "property_runner/async/complete" 281 | 282 | fun apply(h: TestHelper) => 283 | _Async.run_async_test(h, {(ph) => ph.complete(true) }, true) 284 | 285 | class \nodoc\ iso _RunnerAsyncPropertyCompleteFalseTest is UnitTest 286 | 287 | fun name(): String => "property_runner/async/complete-false" 288 | 289 | fun apply(h: TestHelper) => 290 | _Async.run_async_test(h,{(ph) => ph.complete(false) }, false) 291 | 292 | class \nodoc\ iso _RunnerAsyncFailTest is UnitTest 293 | 294 | fun name(): String => "property_runner/async/fail" 295 | 296 | fun apply(h: TestHelper) => 297 | _Async.run_async_test(h, {(ph) => ph.fail("Oh noes!") }, false) 298 | 299 | class \nodoc\ iso _RunnerAsyncMultiCompleteSucceedTest is UnitTest 300 | 301 | fun name(): String => "property_runner/async/multi_succeed" 302 | 303 | fun apply(h: TestHelper) => 304 | _Async.run_async_test( 305 | h, 306 | {(ph) => 307 | ph.complete(true) 308 | ph.complete(false) 309 | }, true) 310 | 311 | class \nodoc\ iso _RunnerAsyncMultiCompleteFailTest is UnitTest 312 | fun name(): String => "property_runner/async/multi_fail" 313 | 314 | fun apply(h: TestHelper) => 315 | _Async.run_async_test( 316 | h, 317 | {(ph) => 318 | ph.complete(false) 319 | ph.complete(true) 320 | }, false) 321 | 322 | class \nodoc\ iso _RunnerAsyncCompleteActionTest is UnitTest 323 | 324 | fun name(): String => "property_runner/async/complete_action" 325 | 326 | fun apply(h: TestHelper) => 327 | _Async.run_async_test( 328 | h, 329 | {(ph) => 330 | let action = "blaaaa" 331 | ph.expect_action(action) 332 | ph.complete_action(action) 333 | }, 334 | true) 335 | 336 | class \nodoc\ iso _RunnerAsyncCompleteFalseActionTest is UnitTest 337 | 338 | fun name(): String => "property_runner/async/complete_action" 339 | 340 | fun apply(h: TestHelper) => 341 | _Async.run_async_test( 342 | h, 343 | {(ph) => 344 | let action = "blaaaa" 345 | ph.expect_action(action) 346 | ph.fail_action(action) 347 | }, false) 348 | 349 | class \nodoc\ iso _RunnerAsyncCompleteMultiActionTest is UnitTest 350 | 351 | fun name(): String => "property_runner/async/complete_multi_action" 352 | 353 | fun apply(h: TestHelper) => 354 | _Async.run_async_test( 355 | h, 356 | {(ph) => 357 | let action = "only-once" 358 | ph.expect_action(action) 359 | ph.fail_action(action) 360 | ph.complete_action(action) // should be ignored 361 | }, 362 | false) 363 | 364 | class \nodoc\ iso _RunnerAsyncCompleteMultiSucceedActionTest is UnitTest 365 | 366 | fun name(): String => "property_runner/async/complete_multi_fail_action" 367 | 368 | fun apply(h: TestHelper) => 369 | _Async.run_async_test( 370 | h, 371 | {(ph) => 372 | let action = "succeed-once" 373 | ph.expect_action(action) 374 | ph.complete_action(action) 375 | ph.fail_action(action) 376 | }, 377 | true) 378 | 379 | class \nodoc\ iso _ForAllTest is UnitTest 380 | fun name(): String => "ponycheck/for_all" 381 | 382 | fun apply(h: TestHelper) ? => 383 | Ponycheck.for_all[U8](recover Generators.unit[U8](0) end, h)( 384 | {(u, h) => h.assert_eq[U8](u, 0, u.string() + " == 0") })? 385 | 386 | class \nodoc\ iso _MultipleForAllTest is UnitTest 387 | fun name(): String => "ponycheck/multiple_for_all" 388 | 389 | fun apply(h: TestHelper) ? => 390 | Ponycheck.for_all[U8](recover Generators.unit[U8](0) end, h)( 391 | {(u, h) => h.assert_eq[U8](u, 0, u.string() + " == 0") })? 392 | 393 | Ponycheck.for_all[U8](recover Generators.unit[U8](1) end, h)( 394 | {(u, h) => h.assert_eq[U8](u, 1, u.string() + " == 1") })? 395 | 396 | class \nodoc\ iso _ForAll2Test is UnitTest 397 | fun name(): String => "ponycheck/for_all2" 398 | 399 | fun apply(h: TestHelper) ? => 400 | Ponycheck.for_all2[U8, String]( 401 | recover Generators.unit[U8](0) end, 402 | recover Generators.ascii() end, 403 | h)( 404 | {(arg1, arg2, h) => 405 | h.assert_false(arg2.contains(String.from_array([as U8: arg1]))) 406 | })? 407 | 408 | class \nodoc\ iso _ForAll3Test is UnitTest 409 | fun name(): String => "ponycheck/for_all3" 410 | 411 | fun apply(h: TestHelper) ? => 412 | Ponycheck.for_all3[U8, U8, String]( 413 | recover Generators.unit[U8](0) end, 414 | recover Generators.unit[U8](255) end, 415 | recover Generators.ascii() end, 416 | h)( 417 | {(b1, b2, str, h) => 418 | h.assert_false(str.contains(String.from_array([b1]))) 419 | h.assert_false(str.contains(String.from_array([b2]))) 420 | })? 421 | 422 | class \nodoc\ iso _ForAll4Test is UnitTest 423 | fun name(): String => "ponycheck/for_all4" 424 | 425 | fun apply(h: TestHelper) ? => 426 | Ponycheck.for_all4[U8, U8, U8, String]( 427 | recover Generators.unit[U8](0) end, 428 | recover Generators.u8() end, 429 | recover Generators.u8() end, 430 | recover Generators.ascii() end, 431 | h)( 432 | {(b1, b2, b3, str, h) => 433 | let cmp = String.from_array([b1; b2; b3]) 434 | h.assert_false(str.contains(cmp)) 435 | })? 436 | 437 | class \nodoc\ iso _GenRndTest is UnitTest 438 | fun name(): String => "Gen/random_behaviour" 439 | 440 | fun apply(h: TestHelper) ? => 441 | let gen = Generators.u32() 442 | let rnd1 = Randomness(0) 443 | let rnd2 = Randomness(0) 444 | let rnd3 = Randomness(1) 445 | var same: U32 = 0 446 | for x in Range(0, 100) do 447 | let g1 = gen.generate_value(rnd1)? 448 | let g2 = gen.generate_value(rnd2)? 449 | let g3 = gen.generate_value(rnd3)? 450 | h.assert_eq[U32](g1, g2) 451 | if g1 == g3 then 452 | same = same + 1 453 | end 454 | end 455 | h.assert_ne[U32](same, 100) 456 | 457 | 458 | class \nodoc\ iso _GenFilterTest is UnitTest 459 | fun name(): String => "Gen/filter" 460 | 461 | fun apply(h: TestHelper) ? => 462 | """ 463 | ensure that filter condition is met for all generated results 464 | """ 465 | let gen = Generators.u32().filter({ 466 | (u: U32^): (U32^, Bool) => 467 | (u, (u%2) == 0) 468 | }) 469 | let rnd = Randomness(Time.millis()) 470 | for x in Range(0, 100) do 471 | let v = gen.generate_value(rnd)? 472 | h.assert_true((v%2) == 0) 473 | end 474 | 475 | class \nodoc\ iso _GenUnionTest is UnitTest 476 | fun name(): String => "Gen/union" 477 | 478 | fun apply(h: TestHelper) ? => 479 | """ 480 | assert that a unioned Generator 481 | produces shrinks of the same type than the generated value. 482 | """ 483 | let gen = Generators.ascii().union[U8](Generators.u8()) 484 | let rnd = Randomness(Time.millis()) 485 | for x in Range(0, 100) do 486 | let gs = gen.generate(rnd)? 487 | match gs 488 | | (let vs: String, let shrink_iter: Iterator[String^]) => 489 | h.assert_true(true) 490 | | (let vs: U8, let shrink_iter: Iterator[U8^]) => 491 | h.assert_true(true) 492 | | (let vs: U8, let shrink_iter: Iterator[String^]) => 493 | h.fail("u8 value, string shrink iter") 494 | | (let vs: String, let shrink_iter: Iterator[U8^]) => 495 | h.fail("string value, u8 shrink iter") 496 | else 497 | h.fail("invalid type generated") 498 | end 499 | end 500 | 501 | class \nodoc\ iso _GenFrequencyTest is UnitTest 502 | fun name(): String => "Gen/frequency" 503 | 504 | fun apply(h: TestHelper) ? => 505 | """ 506 | ensure that Generators.frequency(...) generators actually return values 507 | from different with given frequency 508 | """ 509 | let gen = Generators.frequency[U8]([ 510 | as WeightedGenerator[U8]: 511 | (1, Generators.unit[U8](0)) 512 | (0, Generators.unit[U8](42)) 513 | (2, Generators.unit[U8](1)) 514 | ]) 515 | let rnd: Randomness ref = Randomness(Time.millis()) 516 | 517 | let generated = Array[U8](100) 518 | for i in Range(0, 100) do 519 | generated.push(gen.generate_value(rnd)?) 520 | end 521 | h.assert_false(generated.contains(U8(42)), "frequency generated value with 0 weight") 522 | h.assert_true(generated.contains(U8(0)), "frequency did not generate value with weight of 1") 523 | h.assert_true(generated.contains(U8(1)), "frequency did not generate value with weight of 2") 524 | 525 | let empty_gen = Generators.frequency[U8](Array[WeightedGenerator[U8]](0)) 526 | 527 | h.assert_error({() ? => 528 | empty_gen.generate_value(Randomness(Time.millis()))? 529 | }) 530 | 531 | class \nodoc\ iso _GenFrequencySafeTest is UnitTest 532 | fun name(): String => "Gen/frequency_safe" 533 | 534 | fun apply(h: TestHelper) => 535 | h.assert_error({() ? => 536 | Generators.frequency_safe[U8](Array[WeightedGenerator[U8]](0))? 537 | }) 538 | 539 | class \nodoc\ iso _GenOneOfTest is UnitTest 540 | fun name(): String => "Gen/one_of" 541 | 542 | fun apply(h: TestHelper) => 543 | let gen = Generators.one_of[U8]([as U8: 0; 1]) 544 | let rnd = Randomness(Time.millis()) 545 | h.assert_true( 546 | Iter[U8^](gen.value_iter(rnd)) 547 | .take(100) 548 | .all({(u: U8): Bool => (u == 0) or (u == 1) }), 549 | "one_of generator generated illegal values") 550 | let empty_gen = Generators.one_of[U8](Array[U8](0)) 551 | 552 | h.assert_error({() ? => 553 | empty_gen.generate_value(Randomness(Time.millis()))? 554 | }) 555 | 556 | class \nodoc\ iso _GenOneOfSafeTest is UnitTest 557 | fun name(): String => "Gen/one_of_safe" 558 | 559 | fun apply(h: TestHelper) => 560 | h.assert_error({() ? => 561 | Generators.one_of_safe[U8](Array[U8](0))? 562 | }) 563 | 564 | class \nodoc\ iso _SeqOfTest is UnitTest 565 | fun name(): String => "Gen/seq_of" 566 | 567 | fun apply(h: TestHelper) ? => 568 | let seq_gen = 569 | Generators.seq_of[U8, Array[U8]]( 570 | Generators.u8(), 571 | 0, 572 | 10) 573 | let rnd = Randomness(Time.millis()) 574 | h.assert_true( 575 | Iter[Array[U8]^](seq_gen.value_iter(rnd)) 576 | .take(100) 577 | .all({ 578 | (a: Array[U8]): Bool => 579 | (a.size() >= 0) and (a.size() <= 10) }), 580 | "Seqs generated with Generators.seq_of are out of bounds") 581 | 582 | match seq_gen.generate(rnd)? 583 | | (let gen_sample: Array[U8], let shrinks: Iter[Array[U8]^]) => 584 | let max_size = gen_sample.size() 585 | h.assert_true( 586 | Iter[Array[U8]^](shrinks) 587 | .all({(a: Array[U8]): Bool => 588 | if not (a.size() < max_size) then 589 | h.log(a.size().string() + " >= " + max_size.string()) 590 | false 591 | else 592 | true 593 | end 594 | }), 595 | "shrinking of Generators.seq_of produces too big Seqs") 596 | else 597 | h.fail("Generators.seq_of did not produce any shrinks") 598 | end 599 | 600 | class \nodoc\ iso _IsoSeqOfTest is UnitTest 601 | let min: USize = 0 602 | let max: USize = 200 603 | fun name(): String => "Gen/iso_seq_of" 604 | 605 | fun apply(h: TestHelper) ? => 606 | let seq_gen = Generators.iso_seq_of[String, Array[String] iso]( 607 | Generators.ascii(), 608 | min, 609 | max 610 | ) 611 | let rnd = Randomness(Time.millis()) 612 | h.assert_true( 613 | Iter[Array[String] iso^](seq_gen.value_iter(rnd)) 614 | .take(100) 615 | .all({ 616 | (a: Array[String] iso): Bool => 617 | (a.size() >= min) and (a.size() <= max) }), 618 | "Seqs generated with Generators.iso_seq_of are out of bounds") 619 | 620 | match seq_gen.generate(rnd)? 621 | | (let gen_sample: Array[String] iso, let shrinks: Iter[Array[String] iso^]) => 622 | let max_size = gen_sample.size() 623 | h.assert_true( 624 | Iter[Array[String] iso^](shrinks) 625 | .all({(a: Array[String] iso): Bool => 626 | if not (a.size() < max_size) then 627 | h.log(a.size().string() + " >= " + max_size.string()) 628 | false 629 | else 630 | true 631 | end 632 | }), 633 | "shrinking of Generators.iso_seq_of produces too big Seqs") 634 | else 635 | h.fail("Generators.iso_seq_of did not produce any shrinks") 636 | end 637 | 638 | class \nodoc\ iso _SetOfTest is UnitTest 639 | fun name(): String => "Gen/set_of" 640 | 641 | fun apply(h: TestHelper) ? => 642 | """ 643 | this mainly tests that a source generator with a smaller range 644 | than max is terminating and generating sane sets 645 | """ 646 | let set_gen = 647 | Generators.set_of[U8]( 648 | Generators.u8(), 649 | 1024) 650 | let rnd = Randomness(Time.millis()) 651 | for i in Range(0, 100) do 652 | let sample: Set[U8] = set_gen.generate_value(rnd)? 653 | h.assert_true(sample.size() <= 256, "something about U8 is not right") 654 | end 655 | 656 | class \nodoc\ iso _SetOfMaxTest is UnitTest 657 | fun name(): String => "Gen/set_of_max" 658 | 659 | fun apply(h: TestHelper) ? => 660 | """ 661 | """ 662 | let rnd = Randomness(Time.millis()) 663 | for size in Range[USize](1, U8.max_value().usize()) do 664 | let set_gen = 665 | Generators.set_of[U8]( 666 | Generators.u8(), 667 | size) 668 | let sample: Set[U8] = set_gen.generate_value(rnd)? 669 | h.assert_true(sample.size() <= size, "generated set is too big.") 670 | end 671 | 672 | 673 | class \nodoc\ iso _SetOfEmptyTest is UnitTest 674 | fun name(): String => "Gen/set_of_empty" 675 | 676 | fun apply(h: TestHelper) ? => 677 | """ 678 | """ 679 | let set_gen = 680 | Generators.set_of[U8]( 681 | Generators.u8(), 682 | 0) 683 | let rnd = Randomness(Time.millis()) 684 | for i in Range(0, 100) do 685 | let sample: Set[U8] = set_gen.generate_value(rnd)? 686 | h.assert_true(sample.size() == 0, "non-empty set created.") 687 | end 688 | 689 | class \nodoc\ iso _SetIsOfIdentityTest is UnitTest 690 | fun name(): String => "Gen/set_is_of_identity" 691 | fun apply(h: TestHelper) ? => 692 | """ 693 | """ 694 | let set_is_gen_same = 695 | Generators.set_is_of[String]( 696 | Generators.unit[String]("the highlander"), 697 | 100) 698 | let rnd = Randomness(Time.millis()) 699 | let sample: SetIs[String] = set_is_gen_same.generate_value(rnd)? 700 | h.assert_true(sample.size() <= 1, 701 | "invalid SetIs instances generated: size " + sample.size().string()) 702 | 703 | class \nodoc\ iso _MapOfEmptyTest is UnitTest 704 | fun name(): String => "Gen/map_of_empty" 705 | 706 | fun apply(h: TestHelper) ? => 707 | """ 708 | """ 709 | let map_gen = 710 | Generators.map_of[String, I64]( 711 | Generators.zip2[String, I64]( 712 | Generators.u8().map[String]({(u: U8): String^ => 713 | let s = u.string() 714 | consume s }), 715 | Generators.i64(-10, 10) 716 | ), 717 | 0) 718 | let rnd = Randomness(Time.millis()) 719 | let sample = map_gen.generate_value(rnd)? 720 | h.assert_eq[USize](sample.size(), 0, "non-empty map created") 721 | 722 | class \nodoc\ iso _MapOfMaxTest is UnitTest 723 | fun name(): String => "Gen/map_of_max" 724 | 725 | fun apply(h: TestHelper) ? => 726 | let rnd = Randomness(Time.millis()) 727 | 728 | for size in Range(1, U8.max_value().usize()) do 729 | let map_gen = 730 | Generators.map_of[String, I64]( 731 | Generators.zip2[String, I64]( 732 | Generators.u16().map[String^]({(u: U16): String^ => 733 | u.string() 734 | }), 735 | Generators.i64(-10, 10) 736 | ), 737 | size) 738 | let sample = map_gen.generate_value(rnd)? 739 | h.assert_true(sample.size() <= size, "generated map is too big.") 740 | end 741 | 742 | class \nodoc\ iso _MapOfIdentityTest is UnitTest 743 | fun name(): String => "Gen/map_of_identity" 744 | 745 | fun apply(h: TestHelper) ? => 746 | let rnd = Randomness(Time.millis()) 747 | let map_gen = 748 | Generators.map_of[String, I64]( 749 | Generators.zip2[String, I64]( 750 | Generators.repeatedly[String]({(): String^ => 751 | let s = recover String.create(14) end 752 | s.add("the highlander") 753 | consume s }), 754 | Generators.i64(-10, 10) 755 | ), 756 | 100) 757 | let sample = map_gen.generate_value(rnd)? 758 | h.assert_true(sample.size() <= 1) 759 | 760 | class \nodoc\ iso _MapIsOfEmptyTest is UnitTest 761 | fun name(): String => "Gen/map_is_of_empty" 762 | 763 | fun apply(h: TestHelper) ? => 764 | """ 765 | """ 766 | let map_is_gen = 767 | Generators.map_is_of[String, I64]( 768 | Generators.zip2[String, I64]( 769 | Generators.u8().map[String]({(u: U8): String^ => 770 | let s = u.string() 771 | consume s }), 772 | Generators.i64(-10, 10) 773 | ), 774 | 0) 775 | let rnd = Randomness(Time.millis()) 776 | let sample = map_is_gen.generate_value(rnd)? 777 | h.assert_eq[USize](sample.size(), 0, "non-empty map created") 778 | 779 | class \nodoc\ iso _MapIsOfMaxTest is UnitTest 780 | fun name(): String => "Gen/map_is_of_max" 781 | 782 | fun apply(h: TestHelper) ? => 783 | let rnd = Randomness(Time.millis()) 784 | 785 | for size in Range(1, U8.max_value().usize()) do 786 | let map_is_gen = 787 | Generators.map_is_of[String, I64]( 788 | Generators.zip2[String, I64]( 789 | Generators.u16().map[String]({(u: U16): String^ => 790 | let s = u.string() 791 | consume s }), 792 | Generators.i64(-10, 10) 793 | ), 794 | size) 795 | let sample = map_is_gen.generate_value(rnd)? 796 | h.assert_true(sample.size() <= size, "generated map is too big.") 797 | end 798 | 799 | class \nodoc\ iso _MapIsOfIdentityTest is UnitTest 800 | fun name(): String => "Gen/map_is_of_identity" 801 | 802 | fun apply(h: TestHelper) ? => 803 | let rnd = Randomness(Time.millis()) 804 | let map_gen = 805 | Generators.map_is_of[String, I64]( 806 | Generators.zip2[String, I64]( 807 | Generators.unit[String]("the highlander"), 808 | Generators.i64(-10, 10) 809 | ), 810 | 100) 811 | let sample = map_gen.generate_value(rnd)? 812 | h.assert_true(sample.size() <= 1) 813 | 814 | class \nodoc\ iso _ASCIIRangeTest is UnitTest 815 | fun name(): String => "Gen/ascii_range" 816 | fun apply(h: TestHelper) ? => 817 | let rnd = Randomness(Time.millis()) 818 | let ascii_gen = Generators.ascii( where min=1, max=1, range=ASCIIAll) 819 | 820 | for i in Range[USize](0, 100) do 821 | let sample = ascii_gen.generate_value(rnd)? 822 | h.assert_true(ASCIIAll().contains(sample), "\"" + sample + "\" not valid ascii") 823 | end 824 | 825 | class \nodoc\ iso _UTF32CodePointStringTest is UnitTest 826 | fun name(): String => "Gen/utf32_codepoint_string" 827 | fun apply(h: TestHelper) ? => 828 | let rnd = Randomness(Time.millis()) 829 | let string_gen = Generators.utf32_codepoint_string( 830 | Generators.u32(), 831 | 50, 832 | 100) 833 | 834 | for i in Range[USize](0, 100) do 835 | let sample = string_gen.generate_value(rnd)? 836 | for cp in sample.runes() do 837 | h.assert_true((cp <= 0xD7FF ) or (cp >= 0xE000), "\"" + sample + "\" invalid utf32") 838 | end 839 | end 840 | 841 | class \nodoc\ iso _SuccessfulIntProperty is IntProperty 842 | fun name(): String => "property/int/property" 843 | 844 | fun ref int_property[T: (Int & Integer[T] val)](x: T, h: PropertyHelper) => 845 | h.assert_eq[T](x.min(T.max_value()), x) 846 | h.assert_eq[T](x.max(T.min_value()), x) 847 | 848 | class \nodoc\ iso _SuccessfulIntPropertyTest is UnitTest 849 | fun name(): String => "property/int" 850 | 851 | fun apply(h: TestHelper) => 852 | let property = recover iso _SuccessfulIntProperty end 853 | let property_notify = _UnitTestPropertyNotify(h, true) 854 | let property_logger = _UnitTestPropertyLogger(h) 855 | let params = property.params() 856 | h.long_test(params.timeout) 857 | let runner = PropertyRunner[IntPropertySample]( 858 | consume property, 859 | params, 860 | property_notify, 861 | property_logger, 862 | h.env) 863 | runner.run() 864 | 865 | class \nodoc\ iso _SuccessfulIntPairProperty is IntPairProperty 866 | fun name(): String => "property/intpair/property" 867 | 868 | fun int_property[T: (Int & Integer[T] val)](x: T, y: T, h: PropertyHelper) => 869 | h.assert_eq[T](x * y, y * x) 870 | 871 | class \nodoc\ iso _SuccessfulIntPairPropertyTest is UnitTest 872 | fun name(): String => "property/intpair" 873 | 874 | fun apply(h: TestHelper) => 875 | let property = recover iso _SuccessfulIntPairProperty end 876 | let property_notify = _UnitTestPropertyNotify(h, true) 877 | let property_logger = _UnitTestPropertyLogger(h) 878 | let params = property.params() 879 | h.long_test(params.timeout) 880 | let runner = PropertyRunner[IntPairPropertySample]( 881 | consume property, 882 | params, 883 | property_notify, 884 | property_logger, 885 | h.env) 886 | runner.run() 887 | 888 | class \nodoc\ iso _InfiniteShrinkProperty is Property1[String] 889 | fun name(): String => "property_runner/inifinite_shrink/property" 890 | 891 | fun gen(): Generator[String] => 892 | Generator[String]( 893 | object is GenObj[String] 894 | fun generate(r: Randomness): String^ => 895 | "decided by fair dice roll, totally random" 896 | 897 | fun shrink(t: String): ValueAndShrink[String] => 898 | (t, Iter[String^].repeat_value(t)) 899 | end) 900 | 901 | fun ref property(arg1: String, ph: PropertyHelper) => 902 | ph.assert_true(arg1.size() > 100) // assume this failing 903 | 904 | 905 | class \nodoc\ iso _RunnerInfiniteShrinkTest is UnitTest 906 | """ 907 | ensure that having a failing property with an infinite generator 908 | is not shrinking infinitely 909 | """ 910 | fun name(): String => "property_runner/infinite_shrink" 911 | 912 | fun apply(h: TestHelper) => 913 | 914 | let property = recover iso _InfiniteShrinkProperty end 915 | let params = property.params() 916 | 917 | h.long_test(params.timeout) 918 | 919 | let runner = PropertyRunner[String]( 920 | consume property, 921 | params, 922 | _UnitTestPropertyNotify(h, false), 923 | _UnitTestPropertyLogger(h), 924 | h.env) 925 | runner.run() 926 | 927 | class \nodoc\ iso _ErroringGeneratorProperty is Property1[String] 928 | fun name(): String => "property_runner/erroring_generator/property" 929 | 930 | fun gen(): Generator[String] => 931 | Generator[String]( 932 | object is GenObj[String] 933 | fun generate(r: Randomness): String^ ? => 934 | error 935 | end) 936 | 937 | fun ref property(sample: String, h: PropertyHelper) => 938 | None 939 | 940 | class \nodoc\ iso _RunnerErroringGeneratorTest is UnitTest 941 | fun name(): String => "property_runner/erroring_generator" 942 | 943 | fun apply(h: TestHelper) => 944 | let property = recover iso _ErroringGeneratorProperty end 945 | let params = property.params() 946 | 947 | h.long_test(params.timeout) 948 | 949 | let runner = PropertyRunner[String]( 950 | consume property, 951 | params, 952 | _UnitTestPropertyNotify(h, false), 953 | _UnitTestPropertyLogger(h), 954 | h.env) 955 | runner.run() 956 | 957 | class \nodoc\ iso _SometimesErroringGeneratorProperty is Property1[String] 958 | fun name(): String => "property_runner/sometimes_erroring_generator" 959 | fun params(): PropertyParams => 960 | PropertyParams(where 961 | num_samples' = 3, 962 | seed' = 6, // known seed to produce a value, an error and a value 963 | max_generator_retries' = 1 964 | ) 965 | fun gen(): Generator[String] => 966 | Generator[String]( 967 | object is GenObj[String] 968 | fun generate(r: Randomness): String^ ? => 969 | match (r.u64() % 2) 970 | | 0 => "foo" 971 | else 972 | error 973 | end 974 | end 975 | ) 976 | 977 | fun ref property(sample: String, h: PropertyHelper) => 978 | None 979 | 980 | 981 | class \nodoc\ iso _RunnerSometimesErroringGeneratorTest is UnitTest 982 | fun name(): String => "property_runner/sometimes_erroring_generator" 983 | 984 | fun apply(h: TestHelper) => 985 | let property = recover iso _SometimesErroringGeneratorProperty end 986 | let params = property.params() 987 | 988 | h.long_test(params.timeout) 989 | 990 | let runner = PropertyRunner[String]( 991 | consume property, 992 | params, 993 | _UnitTestPropertyNotify(h, true), 994 | _UnitTestPropertyLogger(h), 995 | h.env) 996 | runner.run() 997 | 998 | class \nodoc\ iso _ReportFailedSampleProperty is Property1[U8] 999 | fun name(): String => "property_runner/sample_reporting/property" 1000 | fun gen(): Generator[U8] => Generators.u8(0, 1) 1001 | fun ref property(sample: U8, h: PropertyHelper) => 1002 | h.assert_eq[U8](sample, U8(0)) 1003 | 1004 | class \nodoc\ iso _RunnerReportFailedSampleTest is UnitTest 1005 | fun name(): String => "property_runner/sample_reporting" 1006 | fun apply(h: TestHelper) => 1007 | let property = recover iso _ReportFailedSampleProperty end 1008 | let params = property.params() 1009 | 1010 | h.long_test(params.timeout) 1011 | 1012 | let logger = 1013 | object val is PropertyLogger 1014 | fun log(msg: String, verbose: Bool) => 1015 | if msg.contains("Property failed for sample 1 ") then 1016 | h.complete(true) 1017 | elseif msg.contains("Propety failed for sample 0 ") then 1018 | h.fail("wrong sample reported.") 1019 | h.complete(false) 1020 | end 1021 | end 1022 | let notify = 1023 | object val is PropertyResultNotify 1024 | fun fail(msg: String) => 1025 | h.log("FAIL: " + msg) 1026 | fun complete(success: Bool) => 1027 | h.assert_false(success, "property did not fail") 1028 | end 1029 | 1030 | let runner = PropertyRunner[U8]( 1031 | consume property, 1032 | params, 1033 | _UnitTestPropertyNotify(h, false), 1034 | logger, 1035 | h.env) 1036 | runner.run() 1037 | 1038 | trait \nodoc\ _ShrinkTest is UnitTest 1039 | fun shrink[T](gen: Generator[T], shrink_elem: T): Iterator[T^] => 1040 | (_, let shrinks': Iterator[T^]) = gen.shrink(consume shrink_elem) 1041 | shrinks' 1042 | 1043 | fun _collect_shrinks[T](gen: Generator[T], shrink_elem: T): Array[T] => 1044 | Iter[T^](shrink[T](gen, consume shrink_elem)).collect[Array[T]](Array[T]) 1045 | 1046 | fun _size(shrinks: Iterator[Any^]): USize => 1047 | Iter[Any^](shrinks).count() 1048 | 1049 | fun _test_int_constraints[T: (Int & Integer[T] val)]( 1050 | h: TestHelper, 1051 | gen: Generator[T], 1052 | x: T, 1053 | min: T = T.min_value() 1054 | ) ? 1055 | => 1056 | let shrinks = shrink[T](gen, min) 1057 | h.assert_false(shrinks.has_next(), "non-empty shrinks for minimal value " + min.string()) 1058 | 1059 | let shrinks1 = _collect_shrinks[T](gen, min + 1) 1060 | h.assert_array_eq[T]([min], shrinks1, "didn't include min in shrunken list of samples") 1061 | 1062 | let shrinks2 = shrink[T](gen, x) 1063 | h.assert_true( 1064 | Iter[T^](shrinks2) 1065 | .all( 1066 | {(u: T): Bool => 1067 | match x.compare(min) 1068 | | Less => 1069 | (u <= min) and (u > x) 1070 | | Equal => true 1071 | | Greater => 1072 | (u >= min) and (u < x) 1073 | end 1074 | }), 1075 | "generated shrinks from " + x.string() + " that violate minimum or maximum") 1076 | 1077 | let count_shrinks = shrink[T](gen, x) 1078 | let max_count = 1079 | if (x - min) < 0 then 1080 | -(x - min) 1081 | else 1082 | x - min 1083 | end 1084 | let actual_count = T.from[USize](Iter[T^](count_shrinks).count()) 1085 | h.assert_true( 1086 | actual_count <= max_count, 1087 | "generated too much values from " + x.string() + " : " + actual_count.string() + " > " + max_count.string()) 1088 | 1089 | class \nodoc\ iso _UnsignedShrinkTest is _ShrinkTest 1090 | fun name(): String => "shrink/unsigned_generators" 1091 | 1092 | fun apply(h: TestHelper)? => 1093 | let gen = Generators.u8() 1094 | _test_int_constraints[U8](h, gen, U8(42))? 1095 | _test_int_constraints[U8](h, gen, U8.max_value())? 1096 | 1097 | let min = U64(10) 1098 | let gen_min = Generators.u64(where min=min) 1099 | _test_int_constraints[U64](h, gen_min, 42, min)? 1100 | 1101 | class \nodoc\ iso _SignedShrinkTest is _ShrinkTest 1102 | fun name(): String => "shrink/signed_generators" 1103 | 1104 | fun apply(h: TestHelper) ? => 1105 | let gen = Generators.i64() 1106 | _test_int_constraints[I64](h, gen, (I64.min_value() + 100))? 1107 | 1108 | let gen2 = Generators.i64(-10, 20) 1109 | _test_int_constraints[I64](h, gen2, 20, -10)? 1110 | _test_int_constraints[I64](h, gen2, 30, -10)? 1111 | _test_int_constraints[I64](h, gen2, -12, -10)? // weird case but should still work 1112 | 1113 | 1114 | class \nodoc\ iso _ASCIIStringShrinkTest is _ShrinkTest 1115 | fun name(): String => "shrink/ascii_string_generators" 1116 | 1117 | fun apply(h: TestHelper) => 1118 | let gen = Generators.ascii(where min=0) 1119 | 1120 | let shrinks_min = shrink[String](gen, "") 1121 | h.assert_false(shrinks_min.has_next(), "non-empty shrinks for minimal value") 1122 | 1123 | let sample = "ABCDEF" 1124 | let shrinks = _collect_shrinks[String](gen, sample) 1125 | h.assert_array_eq[String]( 1126 | ["ABCDE"; "ABCD"; "ABC"; "AB"; "A"; ""], 1127 | shrinks) 1128 | 1129 | let short_sample = "A" 1130 | let short_shrinks = _collect_shrinks[String](gen, short_sample) 1131 | h.assert_array_eq[String]([""], short_shrinks, "shrinking 'A' returns wrong results") 1132 | 1133 | class \nodoc\ iso _MinASCIIStringShrinkTest is _ShrinkTest 1134 | fun name(): String => "shrink/min_ascii_string_generators" 1135 | 1136 | fun apply(h: TestHelper) => 1137 | let min: USize = 10 1138 | let gen = Generators.ascii(where min=min) 1139 | 1140 | let shrinks_min = shrink[String](gen, "abcdefghi") 1141 | h.assert_false(shrinks_min.has_next(), "generated non-empty shrinks for string smaller than minimum") 1142 | 1143 | let shrinks = shrink[String](gen, "abcdefghijlkmnop") 1144 | h.assert_true( 1145 | Iter[String](shrinks) 1146 | .all({(s: String): Bool => s.size() >= min}), "generated shrinks that violate minimum string length") 1147 | 1148 | class \nodoc\ iso _UnicodeStringShrinkTest is _ShrinkTest 1149 | fun name(): String => "shrink/unicode_string_generators" 1150 | 1151 | fun apply(h: TestHelper) => 1152 | let gen = Generators.unicode() 1153 | 1154 | let shrinks_min = shrink[String](gen, "") 1155 | h.assert_false(shrinks_min.has_next(), "non-empty shrinks for minimal value") 1156 | 1157 | let sample2 = "ΣΦΩ" 1158 | let shrinks2 = _collect_shrinks[String](gen, sample2) 1159 | h.assert_false(shrinks2.contains(sample2)) 1160 | h.assert_true(shrinks2.size() > 0, "empty shrinks for non-minimal unicode string") 1161 | 1162 | let sample3 = "Σ" 1163 | let shrinks3 = _collect_shrinks[String](gen, sample3) 1164 | h.assert_array_eq[String]([""], shrinks3, "minimal non-empty string not properly shrunk") 1165 | 1166 | class \nodoc\ iso _MinUnicodeStringShrinkTest is _ShrinkTest 1167 | fun name(): String => "shrink/min_unicode_string_generators" 1168 | 1169 | fun apply(h: TestHelper) => 1170 | let min = USize(5) 1171 | let gen = Generators.unicode(where min=min) 1172 | 1173 | let min_sample = "ΣΦΩ" 1174 | let shrinks_min = shrink[String](gen, min_sample) 1175 | h.assert_false(shrinks_min.has_next(), "non-empty shrinks for minimal value") 1176 | 1177 | let sample = "ΣΦΩΣΦΩ" 1178 | let shrinks = _collect_shrinks[String](gen, sample) 1179 | h.assert_true( 1180 | Iter[String](shrinks.values()) 1181 | .all({(s: String): Bool => s.codepoints() >= min}), 1182 | "generated shrinks that violate minimum string length") 1183 | h.assert_false( 1184 | shrinks.contains(sample), 1185 | "shrinks contain sample value") 1186 | 1187 | class \nodoc\ iso _FilterMapShrinkTest is _ShrinkTest 1188 | fun name(): String => "shrink/filter_map" 1189 | 1190 | fun apply(h: TestHelper) => 1191 | let gen: Generator[U64] = 1192 | Generators.u8() 1193 | .filter({(byte) => (byte, byte > 10) }) 1194 | .map[U64]({(byte) => (byte * 2).u64() }) 1195 | // shrink from 100 and only expect even values > 20 1196 | let shrink_iter = shrink[U64](gen, U64(100)) 1197 | h.assert_true( 1198 | Iter[U64](shrink_iter) 1199 | .all({(u) => 1200 | (u > 20) and ((u % 2) == 0) }), 1201 | "shrinking does not maintain filter invariants") 1202 | 1203 | primitive \nodoc\ _Async 1204 | """ 1205 | utility to run tests for async properties 1206 | """ 1207 | fun run_async_test( 1208 | h: TestHelper, 1209 | action: {(PropertyHelper): None} val, 1210 | should_succeed: Bool = true) 1211 | => 1212 | """ 1213 | Run the given action in an asynchronous property 1214 | providing if you expect success or failure with `should_succeed`. 1215 | """ 1216 | let property = _AsyncProperty(action) 1217 | let params = property.params() 1218 | h.long_test(params.timeout) 1219 | 1220 | let runner = PropertyRunner[String]( 1221 | consume property, 1222 | params, 1223 | _UnitTestPropertyNotify(h, should_succeed), 1224 | _UnitTestPropertyLogger(h), 1225 | h.env) 1226 | runner.run() 1227 | 1228 | class \nodoc\ val _UnitTestPropertyLogger is PropertyLogger 1229 | """ 1230 | just forwarding logs to the TestHelper log 1231 | with a custom prefix 1232 | """ 1233 | let _th: TestHelper 1234 | 1235 | new val create(th: TestHelper) => 1236 | _th = th 1237 | 1238 | fun log(msg: String, verbose: Bool) => 1239 | _th.log("[PROPERTY] " + msg, verbose) 1240 | 1241 | class \nodoc\ val _UnitTestPropertyNotify is PropertyResultNotify 1242 | let _th: TestHelper 1243 | let _should_succeed: Bool 1244 | 1245 | new val create(th: TestHelper, should_succeed: Bool = true) => 1246 | _should_succeed = should_succeed 1247 | _th = th 1248 | 1249 | fun fail(msg: String) => 1250 | _th.log("FAIL: " + msg) 1251 | 1252 | fun complete(success: Bool) => 1253 | _th.log("COMPLETE: " + success.string()) 1254 | let result = (success and _should_succeed) or ((not success) and (not _should_succeed)) 1255 | _th.complete(result) 1256 | 1257 | 1258 | actor \nodoc\ _AsyncDelayingActor 1259 | """ 1260 | running the given action in a behavior 1261 | """ 1262 | 1263 | let _ph: PropertyHelper 1264 | let _action: {(PropertyHelper): None} val 1265 | 1266 | new create(ph: PropertyHelper, action: {(PropertyHelper): None} val) => 1267 | _ph = ph 1268 | _action = action 1269 | 1270 | be do_it() => 1271 | _action.apply(_ph) 1272 | 1273 | class \nodoc\ iso _AsyncProperty is Property1[String] 1274 | """ 1275 | A simple property running the given action 1276 | asynchronously in an `AsyncDelayingActor`. 1277 | """ 1278 | 1279 | let _action: {(PropertyHelper): None} val 1280 | new iso create(action: {(PropertyHelper): None } val) => 1281 | _action = action 1282 | 1283 | fun name(): String => "property_runner/async/property" 1284 | 1285 | fun params(): PropertyParams => 1286 | PropertyParams(where async' = true) 1287 | 1288 | fun gen(): Generator[String] => 1289 | Generators.ascii_printable() 1290 | 1291 | fun ref property(arg1: String, ph: PropertyHelper) => 1292 | _AsyncDelayingActor(ph, _action).do_it() 1293 | 1294 | interface \nodoc\ val _RandomCase[A: Comparable[A] #read] 1295 | new val create() 1296 | 1297 | fun test(min: A, max: A): A 1298 | 1299 | fun generator(): Generator[A] 1300 | 1301 | primitive \nodoc\ _RandomCaseU8 is _RandomCase[U8] 1302 | fun test(min: U8, max: U8): U8 => 1303 | let rnd = Randomness(Time.millis()) 1304 | rnd.u8(min, max) 1305 | 1306 | fun generator(): Generator[U8] => 1307 | Generators.u8() 1308 | 1309 | primitive \nodoc\ _RandomCaseU16 is _RandomCase[U16] 1310 | fun test(min: U16, max: U16): U16 => 1311 | let rnd = Randomness(Time.millis()) 1312 | rnd.u16(min, max) 1313 | 1314 | fun generator(): Generator[U16] => 1315 | Generators.u16() 1316 | 1317 | primitive \nodoc\ _RandomCaseU32 is _RandomCase[U32] 1318 | fun test(min: U32, max: U32): U32 => 1319 | let rnd = Randomness(Time.millis()) 1320 | rnd.u32(min, max) 1321 | 1322 | fun generator(): Generator[U32] => 1323 | Generators.u32() 1324 | 1325 | primitive \nodoc\ _RandomCaseU64 is _RandomCase[U64] 1326 | fun test(min: U64, max: U64): U64 => 1327 | let rnd = Randomness(Time.millis()) 1328 | rnd.u64(min, max) 1329 | 1330 | fun generator(): Generator[U64] => 1331 | Generators.u64() 1332 | 1333 | primitive \nodoc\ _RandomCaseU128 is _RandomCase[U128] 1334 | fun test(min: U128, max: U128): U128 => 1335 | let rnd = Randomness(Time.millis()) 1336 | rnd.u128(min, max) 1337 | 1338 | fun generator(): Generator[U128] => 1339 | Generators.u128() 1340 | 1341 | primitive \nodoc\ _RandomCaseI8 is _RandomCase[I8] 1342 | fun test(min: I8, max: I8): I8 => 1343 | let rnd = Randomness(Time.millis()) 1344 | rnd.i8(min, max) 1345 | 1346 | fun generator(): Generator[I8] => 1347 | Generators.i8() 1348 | 1349 | primitive \nodoc\ _RandomCaseI16 is _RandomCase[I16] 1350 | fun test(min: I16, max: I16): I16 => 1351 | let rnd = Randomness(Time.millis()) 1352 | rnd.i16(min, max) 1353 | 1354 | fun generator(): Generator[I16] => 1355 | Generators.i16() 1356 | 1357 | primitive \nodoc\ _RandomCaseI32 is _RandomCase[I32] 1358 | fun test(min: I32, max: I32): I32 => 1359 | let rnd = Randomness(Time.millis()) 1360 | rnd.i32(min, max) 1361 | 1362 | fun generator(): Generator[I32] => 1363 | Generators.i32() 1364 | 1365 | primitive \nodoc\ _RandomCaseI64 is _RandomCase[I64] 1366 | fun test(min: I64, max: I64): I64 => 1367 | let rnd = Randomness(Time.millis()) 1368 | rnd.i64(min, max) 1369 | 1370 | fun generator(): Generator[I64] => 1371 | Generators.i64() 1372 | 1373 | primitive \nodoc\ _RandomCaseI128 is _RandomCase[I128] 1374 | fun test(min: I128, max: I128): I128 => 1375 | let rnd = Randomness(Time.millis()) 1376 | rnd.i128(min, max) 1377 | 1378 | fun generator(): Generator[I128] => 1379 | Generators.i128() 1380 | 1381 | primitive \nodoc\ _RandomCaseISize is _RandomCase[ISize] 1382 | fun test(min: ISize, max: ISize): ISize => 1383 | let rnd = Randomness(Time.millis()) 1384 | rnd.isize(min, max) 1385 | 1386 | fun generator(): Generator[ISize] => 1387 | Generators.isize() 1388 | 1389 | primitive \nodoc\ _RandomCaseILong is _RandomCase[ILong] 1390 | fun test(min: ILong, max: ILong): ILong => 1391 | let rnd = Randomness(Time.millis()) 1392 | rnd.ilong(min, max) 1393 | 1394 | fun generator(): Generator[ILong] => 1395 | Generators.ilong() 1396 | 1397 | class \nodoc\ iso _RandomnessProperty[A: Comparable[A] #read, R: _RandomCase[A] val] is Property1[(A, A)] 1398 | """ 1399 | Ensure Randomness generates a random number within the given range. 1400 | """ 1401 | let _type_name: String 1402 | 1403 | new iso create(type_name: String) => 1404 | _type_name = type_name 1405 | 1406 | fun name(): String => "randomness/" + _type_name 1407 | 1408 | fun gen(): Generator[(A, A)] => 1409 | let min = R.generator() 1410 | let max = R.generator() 1411 | Generators.zip2[A, A](min, max) 1412 | .filter( 1413 | { (pair) => (pair, (pair._1 <= pair._2)) } 1414 | ) 1415 | 1416 | fun property(arg1: (A, A), ph: PropertyHelper) => 1417 | (let min, let max) = arg1 1418 | 1419 | let value = R.test(min, max) 1420 | ph.assert_true(value >= min) 1421 | ph.assert_true(value <= max) 1422 | -------------------------------------------------------------------------------- /ponycheck/ascii_range.pony: -------------------------------------------------------------------------------- 1 | 2 | primitive ASCIINUL 3 | fun apply(): String => "\x00" 4 | 5 | primitive ASCIIDigits 6 | fun apply(): String => "0123456789" 7 | 8 | primitive ASCIIWhiteSpace 9 | fun apply(): String => " \t\n\r\x0b\x0c" 10 | 11 | primitive ASCIIPunctuation 12 | fun apply(): String => "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" 13 | 14 | primitive ASCIILettersLower 15 | fun apply(): String => "abcdefghijklmnopqrstuvwxyz" 16 | 17 | primitive ASCIILettersUpper 18 | fun apply(): String => "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 19 | 20 | primitive ASCIILetters 21 | fun apply(): String => ASCIILettersLower() + ASCIILettersUpper() 22 | 23 | primitive ASCIIPrintable 24 | fun apply(): String => 25 | ASCIIDigits() 26 | + ASCIILetters() 27 | + ASCIIPunctuation() 28 | + ASCIIWhiteSpace() 29 | 30 | primitive ASCIINonPrintable 31 | fun apply(): String => 32 | "\x01\x02\x03\x04\x05\x06\x07\x08\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" 33 | 34 | primitive ASCIIAll 35 | """ 36 | represents all ASCII characters 37 | excluding the NUL (\x00) character for its special treatment in C strings 38 | """ 39 | fun apply(): String => 40 | ASCIIPrintable() + ASCIINonPrintable() 41 | 42 | primitive ASCIIAllWithNUL 43 | """ 44 | represents all ASCII characters 45 | including the NUL (\x00) character for its special treatment in C strings 46 | """ 47 | fun apply(): String => 48 | ASCIIAll() + ASCIINUL() 49 | 50 | type ASCIIRange is 51 | ( ASCIINUL 52 | | ASCIIDigits 53 | | ASCIIWhiteSpace 54 | | ASCIIPunctuation 55 | | ASCIILettersLower 56 | | ASCIILettersUpper 57 | | ASCIILetters 58 | | ASCIIPrintable 59 | | ASCIINonPrintable 60 | | ASCIIAll 61 | | ASCIIAllWithNUL) 62 | -------------------------------------------------------------------------------- /ponycheck/for_all.pony: -------------------------------------------------------------------------------- 1 | use "ponytest" 2 | 3 | class ForAll[T] 4 | let _gen: Generator[T] val 5 | let _helper: TestHelper 6 | 7 | new create(gen': Generator[T] val, testHelper: TestHelper) => 8 | _gen = gen' 9 | _helper = testHelper 10 | 11 | fun ref apply(prop: {(T, PropertyHelper) ?} val) ? => 12 | """execute""" 13 | Property1UnitTest[T]( 14 | object iso is Property1[T] 15 | fun name(): String => "" 16 | 17 | fun gen(): Generator[T] => _gen 18 | 19 | fun ref property(arg1: T, h: PropertyHelper) ? => 20 | prop(consume arg1, h)? 21 | end 22 | ).apply(_helper)? 23 | 24 | class ForAll2[T1, T2] 25 | let _gen1: Generator[T1] val 26 | let _gen2: Generator[T2] val 27 | let _helper: TestHelper 28 | 29 | new create( 30 | gen1': Generator[T1] val, 31 | gen2': Generator[T2] val, 32 | h: TestHelper) 33 | => 34 | _gen1 = gen1' 35 | _gen2 = gen2' 36 | _helper = h 37 | 38 | fun ref apply(prop: {(T1, T2, PropertyHelper) ?} val) ? => 39 | Property2UnitTest[T1, T2]( 40 | object iso is Property2[T1, T2] 41 | fun name(): String => "" 42 | fun gen1(): Generator[T1] => _gen1 43 | fun gen2(): Generator[T2] => _gen2 44 | fun ref property2(arg1: T1, arg2: T2, h: PropertyHelper) ? => 45 | prop(consume arg1, consume arg2, h)? 46 | end 47 | ).apply(_helper)? 48 | 49 | class ForAll3[T1, T2, T3] 50 | let _gen1: Generator[T1] val 51 | let _gen2: Generator[T2] val 52 | let _gen3: Generator[T3] val 53 | let _helper: TestHelper 54 | 55 | new create( 56 | gen1': Generator[T1] val, 57 | gen2': Generator[T2] val, 58 | gen3': Generator[T3] val, 59 | h: TestHelper) 60 | => 61 | _gen1 = gen1' 62 | _gen2 = gen2' 63 | _gen3 = gen3' 64 | _helper = h 65 | 66 | fun ref apply(prop: {(T1, T2, T3, PropertyHelper) ?} val) ? => 67 | Property3UnitTest[T1, T2, T3]( 68 | object iso is Property3[T1, T2, T3] 69 | fun name(): String => "" 70 | fun gen1(): Generator[T1] => _gen1 71 | fun gen2(): Generator[T2] => _gen2 72 | fun gen3(): Generator[T3] => _gen3 73 | fun ref property3(arg1: T1, arg2: T2, arg3: T3, h: PropertyHelper) ? => 74 | prop(consume arg1, consume arg2, consume arg3, h)? 75 | end 76 | ).apply(_helper)? 77 | 78 | class ForAll4[T1, T2, T3, T4] 79 | let _gen1: Generator[T1] val 80 | let _gen2: Generator[T2] val 81 | let _gen3: Generator[T3] val 82 | let _gen4: Generator[T4] val 83 | let _helper: TestHelper 84 | 85 | new create( 86 | gen1': Generator[T1] val, 87 | gen2': Generator[T2] val, 88 | gen3': Generator[T3] val, 89 | gen4': Generator[T4] val, 90 | h: TestHelper) 91 | => 92 | _gen1 = gen1' 93 | _gen2 = gen2' 94 | _gen3 = gen3' 95 | _gen4 = gen4' 96 | _helper = h 97 | 98 | fun ref apply(prop: {(T1, T2, T3, T4, PropertyHelper) ?} val) ? => 99 | Property4UnitTest[T1, T2, T3, T4]( 100 | object iso is Property4[T1, T2, T3, T4] 101 | fun name(): String => "" 102 | fun gen1(): Generator[T1] => _gen1 103 | fun gen2(): Generator[T2] => _gen2 104 | fun gen3(): Generator[T3] => _gen3 105 | fun gen4(): Generator[T4] => _gen4 106 | fun ref property4(arg1: T1, arg2: T2, arg3: T3, arg4: T4, h: PropertyHelper) ? => 107 | prop(consume arg1, consume arg2, consume arg3, consume arg4, h)? 108 | end 109 | ).apply(_helper)? 110 | 111 | -------------------------------------------------------------------------------- /ponycheck/generator.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "assert" 3 | use "itertools" 4 | use "debug" 5 | 6 | type ValueAndShrink[T1] is (T1^, Iterator[T1^]) 7 | """ 8 | Possible return type for [`Generator.generate`](ponycheck-Generator.md#generate). 9 | Represents a generated value and an Iterator of shrunken values. 10 | """ 11 | 12 | type GenerateResult[T2] is (T2^ | ValueAndShrink[T2]) 13 | """ 14 | Return type for [`Generator.generate`](ponycheck-Generator.md#generate). 15 | 16 | Either a single value only or a Tuple of a value and an Iterator 17 | of shrunken values based upon this value. 18 | """ 19 | 20 | class CountdownIter[T: (Int & Integer[T] val) = USize] is Iterator[T] 21 | """ 22 | `from` is exclusive, `to` is inclusive 23 | """ 24 | var _cur: T 25 | let _to: T 26 | 27 | new create(from: T, to: T = T.min_value()) => 28 | _cur = from 29 | _to = to 30 | 31 | fun ref has_next(): Bool => 32 | _cur > _to 33 | 34 | fun ref next(): T => 35 | let res = _cur - 1 36 | _cur = res 37 | res 38 | 39 | trait box GenObj[T] 40 | fun generate(rnd: Randomness): GenerateResult[T] ? 41 | 42 | fun shrink(t: T): ValueAndShrink[T] => 43 | (consume t, Poperator[T].empty()) 44 | 45 | fun generate_value(rnd: Randomness): T^ ? => 46 | """ 47 | simply generate a value and ignore any possible 48 | shrink values 49 | """ 50 | let g = this 51 | match g.generate(rnd)? 52 | | let t: T => consume t 53 | | (let t: T, _) => consume t 54 | end 55 | 56 | fun generate_and_shrink(rnd: Randomness): ValueAndShrink[T] ? => 57 | """ 58 | generate a value and also return a shrink result 59 | even if the generator does not return any when calling `generate`. 60 | """ 61 | let g = this 62 | match g.generate(rnd)? 63 | | let t: T => g.shrink(consume t) 64 | | (let t: T, let shrinks: Iterator[T^])=> (consume t, shrinks) 65 | end 66 | 67 | fun iter(rnd: Randomness): Iterator[GenerateResult[T]]^ => 68 | let that: GenObj[T] = this 69 | 70 | object is Iterator[GenerateResult[T]] 71 | fun ref has_next(): Bool => true 72 | fun ref next(): GenerateResult[T] ? => that.generate(rnd)? 73 | end 74 | 75 | fun value_iter(rnd: Randomness): Iterator[T^] => 76 | let that: GenObj[T] = this 77 | 78 | object is Iterator[T^] 79 | fun ref has_next(): Bool => true 80 | fun ref next(): T^ ? => 81 | match that.generate(rnd)? 82 | | let value_only: T => consume value_only 83 | | (let v: T, _) => consume v 84 | end 85 | end 86 | 87 | fun value_and_shrink_iter(rnd: Randomness): Iterator[ValueAndShrink[T]] => 88 | let that: GenObj[T] = this 89 | 90 | object is Iterator[ValueAndShrink[T]] 91 | fun ref has_next(): Bool => true 92 | fun ref next(): ValueAndShrink[T] ? => 93 | match that.generate(rnd)? 94 | | let value_only: T => that.shrink(consume value_only) 95 | | (let v: T, let shrinks: Iterator[T^]) => (consume v, consume shrinks) 96 | end 97 | end 98 | 99 | 100 | class box Generator[T] is GenObj[T] 101 | """ 102 | A Generator is capable of generating random values of a certain type `T` 103 | given a source of `Randomness` 104 | and knows how to shrink or simplify values of that type. 105 | 106 | When testing a property against one or more given Generators 107 | those generators' `generate` methods are being called many times 108 | to generate sample values that are then used to validate the property. 109 | 110 | When a failing sample is found, the ponycheck engine is trying to find a 111 | smaller or more simple sample by `shrinking` it. 112 | If the generator did not provide any shrinked samples 113 | as a result of `generate`, its `shrink` method is called 114 | to obtain simpler results. Ponycheck obtains more shrunken samples until 115 | the property is not failing anymore. 116 | The last failing sample, which is considered the most simple one, 117 | is then reported to the user. 118 | """ 119 | let _gen: GenObj[T] 120 | 121 | new create(gen: GenObj[T]) => 122 | _gen = gen 123 | 124 | fun generate(rnd: Randomness): GenerateResult[T] ? => 125 | """ 126 | Let this generator generate a value 127 | given a source of `Randomness`. 128 | 129 | Also allow for returning a value and pre-generated shrink results 130 | as a `ValueAndShrink[T]` instance, a tuple of `(T^, Seq[T])`. 131 | This helps propagating shrink results through all kinds of Generator 132 | combinators like `filter`, `map` and `flatMap`. 133 | 134 | If implementing a custom `Generator` based on another one, 135 | a Generator Combinator, you should use shrunken values 136 | returned by `generate` to also return shrunken values based on them 137 | 138 | If generating an example value is costly, it might be more efficient 139 | to simply return the generated value and only shrink in big steps or do no 140 | shrinking at all. 141 | If generating values is lightweight, shrunken values should also be returned. 142 | """ 143 | _gen.generate(rnd)? 144 | 145 | fun shrink(t: T): ValueAndShrink[T] => 146 | """ 147 | Simplify the given value. 148 | 149 | As the returned value can also be `iso`, it needs to be consumed and returned 150 | 151 | It is preffered to already return a `ValueAndShrink` from `generate`. 152 | """ 153 | _gen.shrink(consume t) 154 | 155 | fun generate_value(rnd: Randomness): T^ ? => 156 | _gen.generate_value(rnd)? 157 | 158 | fun generate_and_shrink(rnd: Randomness): ValueAndShrink[T] ? => 159 | _gen.generate_and_shrink(rnd)? 160 | 161 | fun filter(predicate: {(T): (T^, Bool)} box): Generator[T] => 162 | """ 163 | apply `predicate` to the values generated by this Generator 164 | and only values for which `predicate` returns `true`. 165 | 166 | Example: 167 | 168 | ```pony 169 | let even_i32s = 170 | Generators.i32() 171 | .filter( 172 | {(t) => (t, ((t % 2) == 0)) }) 173 | ``` 174 | """ 175 | Generator[T]( 176 | object is GenObj[T] 177 | fun generate(rnd: Randomness): GenerateResult[T] ? => 178 | (let t: T, let shrunken: Iterator[T^]) = _gen.generate_and_shrink(rnd)? 179 | (let t1, let matches) = predicate(consume t) 180 | if not matches then 181 | generate(rnd)? // recurse, this might recurse infinitely 182 | else 183 | // filter the shrunken examples 184 | (consume t1, _filter_shrunken(shrunken)) 185 | end 186 | 187 | fun shrink(t: T): ValueAndShrink[T] => 188 | """ 189 | shrink `t` using the generator this one filters upon 190 | and call the filter predicate on the shrunken values 191 | """ 192 | (let s, let shrunken: Iterator[T^]) = _gen.shrink(consume t) 193 | (consume s, _filter_shrunken(shrunken)) 194 | 195 | fun _filter_shrunken(shrunken: Iterator[T^]): Iterator[T^] => 196 | Iter[T^](shrunken) 197 | .filter_map[T^]({ 198 | (t: T): (T^| None) => 199 | match predicate(consume t) 200 | | (let matching: T, true) => consume matching 201 | end 202 | }) 203 | end) 204 | 205 | fun map[U](fn: {(T): U^} box) 206 | : Generator[U] 207 | => 208 | """ 209 | apply function `fn` to each value of this iterator 210 | and yield the results. 211 | 212 | Example: 213 | 214 | ```pony 215 | let single_code_point_string_gen = 216 | Generators.u32() 217 | .map[String]({(u) => String.from_utf32(u) }) 218 | ``` 219 | """ 220 | Generator[U]( 221 | object is GenObj[U] 222 | fun generate(rnd: Randomness): GenerateResult[U] ? => 223 | (let generated: T, let shrunken: Iterator[T^]) = 224 | _gen.generate_and_shrink(rnd)? 225 | 226 | (fn(consume generated), _map_shrunken(shrunken)) 227 | 228 | fun shrink(u: U): ValueAndShrink[U] => 229 | """ 230 | We can only shrink if T is a subtype of U. 231 | 232 | This method should in general not be called on this generator 233 | as it is always returning shrinks with the call to `generate` 234 | and they should be used for executing the shrink, but in case 235 | a strange hierarchy of generators is used, which does not make use of 236 | the pre-generated shrink results, we keep this method here. 237 | """ 238 | match u 239 | | let ut: T => 240 | (let uts: T, let shrunken: Iterator[T^]) = _gen.shrink(consume ut) 241 | (fn(consume uts), _map_shrunken(shrunken)) 242 | else 243 | (consume u, Poperator[U].empty()) 244 | end 245 | 246 | fun _map_shrunken(shrunken: Iterator[T^]): Iterator[U^] => 247 | Iter[T^](shrunken) 248 | .map[U^]({(t) => fn(consume t) }) 249 | 250 | end) 251 | 252 | fun flat_map[U](fn: {(T): Generator[U]} box): Generator[U] => 253 | """ 254 | For each value of this generator create a generator that is then combined. 255 | """ 256 | // TODO: enable proper shrinking: 257 | Generator[U]( 258 | object is GenObj[U] 259 | fun generate(rnd: Randomness): GenerateResult[U] ? => 260 | let value: T = _gen.generate_value(rnd)? 261 | fn(consume value).generate_and_shrink(rnd)? 262 | 263 | end) 264 | 265 | fun union[U](other: Generator[U]): Generator[(T | U)] => 266 | """ 267 | Create a generator that produces the value of this generator or the other 268 | with the same probability, returning a union type of this generator and the other one. 269 | """ 270 | Generator[(T | U)]( 271 | object is GenObj[(T | U)] 272 | fun generate(rnd: Randomness): GenerateResult[(T | U)] ? => 273 | if rnd.bool() then 274 | _gen.generate_and_shrink(rnd)? 275 | else 276 | other.generate_and_shrink(rnd)? 277 | end 278 | 279 | fun shrink(t: (T | U)): ValueAndShrink[(T | U)] => 280 | match consume t 281 | | let tt: T => _gen.shrink(consume tt) 282 | | let tu: U => other.shrink(consume tu) 283 | end 284 | end 285 | ) 286 | 287 | type WeightedGenerator[T] is (USize, Generator[T] box) 288 | """ 289 | A generator with an associated weight, used in Generators.frequency. 290 | """ 291 | 292 | primitive Generators 293 | """ 294 | Convenience combinators and factories for common types and kind of Generators. 295 | """ 296 | 297 | fun unit[T](t: T, do_shrink: Bool = false): Generator[box->T] => 298 | """ 299 | Generate a reference to the same value over and over again. 300 | 301 | This reference will be of type ``box->T`` and not just ``T`` 302 | as this generator will need to keep a reference to the given value. 303 | """ 304 | Generator[box->T]( 305 | object is GenObj[box->T] 306 | let _t: T = consume t 307 | fun generate(rnd: Randomness): GenerateResult[box->T] => 308 | if do_shrink then 309 | (_t, Iter[box->T].repeat_value(_t)) 310 | else 311 | _t 312 | end 313 | end) 314 | 315 | fun none[T: None](): Generator[(T | None)] => Generators.unit[(T | None)](None) 316 | 317 | fun repeatedly[T](f: {(): T^ ?} box): Generator[T] => 318 | """ 319 | Generate values by calling the lambda ``f`` repeatedly, 320 | once for every invocation of ``generate``. 321 | 322 | ``f`` needs to return an ephemeral type ``T^``, that means 323 | in most cases it needs to consume its returned value. 324 | Otherwise we would end up with 325 | an alias for ``T`` which is ``T!``. 326 | (e.g. ``String iso`` would be returned as ``String iso!`` 327 | which is a ``String tag``). 328 | 329 | Example: 330 | 331 | ```pony 332 | Generators.repeatedly[Writer]({(): Writer^ => 333 | let writer = Writer.>write("consume me, please") 334 | consume writer 335 | }) 336 | ``` 337 | """ 338 | Generator[T]( 339 | object is GenObj[T] 340 | fun generate(rnd: Randomness): GenerateResult[T] ? => 341 | f()? 342 | end) 343 | 344 | 345 | fun seq_of[T, S: Seq[T] ref]( 346 | gen: Generator[T], 347 | min: USize = 0, 348 | max: USize = 100) 349 | : Generator[S] 350 | => 351 | """ 352 | Create a `Seq` from the values of the given Generator with an optional minimum and 353 | maximum size, defaults are 0 and 100 respectively. 354 | """ 355 | 356 | Generator[S]( 357 | object is GenObj[S] 358 | let _gen: GenObj[T] = gen 359 | fun generate(rnd: Randomness): GenerateResult[S] => 360 | let size = rnd.usize(min, max) 361 | 362 | let result: S = 363 | Iter[T^](_gen.value_iter(rnd)) 364 | .take(size) 365 | .collect[S](S.create(size)) 366 | 367 | // create shrink_iter with smaller seqs and elements generated from _gen.value_iter 368 | let shrink_iter = 369 | Iter[USize](CountdownIter(size, min)) //Range(size, min, -1)) 370 | // .skip(1) 371 | .map_stateful[S^]({ 372 | (s: USize): S^ => 373 | Iter[T^](_gen.value_iter(rnd)) 374 | .take(s) 375 | .collect[S](S.create(s)) 376 | }) 377 | (consume result, shrink_iter) 378 | end) 379 | 380 | fun iso_seq_of[T: Any #send, S: Seq[T] iso]( 381 | gen: Generator[T], 382 | min: USize = 0, 383 | max: USize = 100) 384 | : Generator[S] 385 | => 386 | """ 387 | Generate a `Seq[T]` where `T` must be sendable (have a reference capability of `tag`, `val` or `iso`). 388 | 389 | The constraint of the elements being sendable stems from the fact that there is no other way to populate 390 | the iso seq if the elements might be non-sendable (i.e. ref), as then the seq would leak references via its elements. 391 | """ 392 | Generator[S]( 393 | object is GenObj[S] 394 | let _gen: GenObj[T] = gen 395 | fun generate(rnd: Randomness): GenerateResult[S] => 396 | let size = rnd.usize(min, max) 397 | 398 | let result: S = recover iso S.create(size) end 399 | let iter = _gen.value_iter(rnd) 400 | var i = USize(0) 401 | 402 | for elem in iter do 403 | if i >= size then break end 404 | 405 | result.push(consume elem) 406 | i = i + 1 407 | end 408 | // create shrink_iter with smaller seqs and elements generated from _gen.value_iter 409 | let shrink_iter = 410 | Iter[USize](CountdownIter(size, min)) //Range(size, min, -1)) 411 | // .skip(1) 412 | .map_stateful[S^]({ 413 | (s: USize): S^ => 414 | let res = recover iso S.create(s) end 415 | let s_iter = _gen.value_iter(rnd) 416 | var j = USize(0) 417 | 418 | for s_elem in s_iter do 419 | if j >= s then break end 420 | res.push(consume s_elem) 421 | j = j + 1 422 | end 423 | consume res 424 | }) 425 | (consume result, shrink_iter) 426 | end 427 | ) 428 | 429 | fun array_of[T]( 430 | gen: Generator[T], 431 | min: USize = 0, 432 | max: USize = 100) 433 | : Generator[Array[T]] 434 | => 435 | Generators.seq_of[T, Array[T]](gen, min, max) 436 | 437 | fun shuffled_array_gen[T]( 438 | gen: Generator[Array[T]]) 439 | : Generator[Array[T]] 440 | => 441 | Generator[Array[T]]( 442 | object is GenObj[Array[T]] 443 | let _gen: GenObj[Array[T]] = gen 444 | fun generate(rnd: Randomness): GenerateResult[Array[T]] ? => 445 | (let arr, let source_shrink_iter) = _gen.generate_and_shrink(rnd)? 446 | rnd.shuffle[T](arr) 447 | let shrink_iter = 448 | Iter[Array[T]](source_shrink_iter) 449 | .map_stateful[Array[T]^]({ 450 | (shrink_arr: Array[T]): Array[T]^ => 451 | rnd.shuffle[T](shrink_arr) 452 | consume shrink_arr 453 | }) 454 | (consume arr, shrink_iter) 455 | end 456 | ) 457 | 458 | fun shuffled_iter[T](array: Array[T]): Generator[Iterator[this->T!]] => 459 | Generator[Iterator[this->T!]]( 460 | object is GenObj[Iterator[this->T!]] 461 | fun generate(rnd: Randomness): GenerateResult[Iterator[this->T!]] => 462 | let cloned = array.clone() 463 | rnd.shuffle[this->T!](cloned) 464 | cloned.values() 465 | end 466 | ) 467 | 468 | fun list_of[T]( 469 | gen: Generator[T], 470 | min: USize = 0, 471 | max: USize = 100) 472 | : Generator[List[T]] 473 | => 474 | Generators.seq_of[T, List[T]](gen, min, max) 475 | 476 | fun set_of[T: (Hashable #read & Equatable[T] #read)]( 477 | gen: Generator[T], 478 | max: USize = 100) 479 | : Generator[Set[T]] 480 | => 481 | """ 482 | Create a generator for ``Set`` filled with values 483 | of the given generator ``gen``. 484 | The returned sets will have a size up to ``max`` 485 | but tend to have fewer than ``max`` 486 | depending on the source generator ``gen``. 487 | 488 | E.g. if the given generator is for ``U8`` values and ``max`` is set to 1024 489 | the set will only ever be of size 256 max. 490 | 491 | Also for efficiency purposes and to not loop forever 492 | this generator will only try to add at most ``max`` values to the set. 493 | If there are duplicates, the set won't grow. 494 | """ 495 | Generator[Set[T]]( 496 | object is GenObj[Set[T]] 497 | let _gen: GenObj[T] = gen 498 | fun generate(rnd: Randomness): GenerateResult[Set[T]] => 499 | let size = rnd.usize(0, max) 500 | let result: Set[T] = 501 | Set[T].create(size).>union( 502 | Iter[T^](_gen.value_iter(rnd)) 503 | .take(size) 504 | ) 505 | let shrink_iter: Iterator[Set[T]^] = 506 | Iter[USize](CountdownIter(size, 0)) // Range(size, 0, -1)) 507 | //.skip(1) 508 | .map_stateful[Set[T]^]({ 509 | (s: USize): Set[T]^ => 510 | Set[T].create(s).>union( 511 | Iter[T^](_gen.value_iter(rnd)).take(s) 512 | ) 513 | }) 514 | (consume result, shrink_iter) 515 | end) 516 | 517 | fun set_is_of[T]( 518 | gen: Generator[T], 519 | max: USize = 100) 520 | : Generator[SetIs[T]] 521 | => 522 | """ 523 | Create a generator for ``SetIs`` filled with values 524 | of the given generator ``gen``. 525 | The returned ``SetIs`` will have a size up to ``max`` 526 | but tend to have fewer entries 527 | depending on the source generator ``gen``. 528 | 529 | E.g. if the given generator is for ``U8`` values and ``max`` is set to 1024 530 | the set will only ever be of size 256 max. 531 | 532 | Also for efficiency purposes and to not loop forever 533 | this generator will only try to add at most ``max`` values to the set. 534 | If there are duplicates, the set won't grow. 535 | """ 536 | // TODO: how to remove code duplications 537 | Generator[SetIs[T]]( 538 | object is GenObj[SetIs[T]] 539 | fun generate(rnd: Randomness): GenerateResult[SetIs[T]] => 540 | let size = rnd.usize(0, max) 541 | 542 | let result: SetIs[T] = 543 | SetIs[T].create(size).>union( 544 | Iter[T^](gen.value_iter(rnd)) 545 | .take(size) 546 | ) 547 | let shrink_iter: Iterator[SetIs[T]^] = 548 | Iter[USize](CountdownIter(size, 0)) //Range(size, 0, -1)) 549 | //.skip(1) 550 | .map_stateful[SetIs[T]^]({ 551 | (s: USize): SetIs[T]^ => 552 | SetIs[T].create(s).>union( 553 | Iter[T^](gen.value_iter(rnd)).take(s) 554 | ) 555 | }) 556 | (consume result, shrink_iter) 557 | end) 558 | 559 | fun map_of[K: (Hashable #read & Equatable[K] #read), V]( 560 | gen: Generator[(K, V)], 561 | max: USize = 100) 562 | : Generator[Map[K, V]] 563 | => 564 | """ 565 | Create a generator for ``Map`` from a generator of key-value tuples. 566 | The generated maps will have a size up to ``max`` 567 | but tend to have fewer entries depending on the source generator ``gen``. 568 | 569 | If the generator generates key-value pairs with 570 | duplicate keys (based on structural equality) 571 | the pair that is generated later will overwrite earlier entries in the map. 572 | """ 573 | Generator[Map[K, V]]( 574 | object is GenObj[Map[K, V]] 575 | fun generate(rnd: Randomness): GenerateResult[Map[K, V]] => 576 | let size = rnd.usize(0, max) 577 | 578 | let result: Map[K, V] = 579 | Map[K, V].create(size).>concat( 580 | Iter[(K^, V^)](gen.value_iter(rnd)) 581 | .take(size) 582 | ) 583 | let shrink_iter: Iterator[Map[K, V]^] = 584 | Iter[USize](CountdownIter(size, 0)) // Range(size, 0, -1)) 585 | // .skip(1) 586 | .map_stateful[Map[K, V]^]({ 587 | (s: USize): Map[K, V]^ => 588 | Map[K, V].create(s).>concat( 589 | Iter[(K^, V^)](gen.value_iter(rnd)).take(s) 590 | ) 591 | }) 592 | (consume result, shrink_iter) 593 | 594 | end) 595 | 596 | fun map_is_of[K, V]( 597 | gen: Generator[(K, V)], 598 | max: USize = 100) 599 | : Generator[MapIs[K, V]] 600 | => 601 | """ 602 | Create a generator for ``MapIs`` from a generator of key-value tuples. 603 | The generated maps will have a size up to ``max`` 604 | but tend to have fewer entries depending on the source generator ``gen``. 605 | 606 | If the generator generates key-value pairs with 607 | duplicate keys (based on identity) 608 | the pair that is generated later will overwrite earlier entries in the map. 609 | """ 610 | Generator[MapIs[K, V]]( 611 | object is GenObj[MapIs[K, V]] 612 | fun generate(rnd: Randomness): GenerateResult[MapIs[K, V]] => 613 | let size = rnd.usize(0, max) 614 | 615 | let result: MapIs[K, V] = 616 | MapIs[K, V].create(size).>concat( 617 | Iter[(K^, V^)](gen.value_iter(rnd)) 618 | .take(size) 619 | ) 620 | let shrink_iter: Iterator[MapIs[K, V]^] = 621 | Iter[USize](CountdownIter(size, 0)) //Range(size, 0, -1)) 622 | // .skip(1) 623 | .map_stateful[MapIs[K, V]^]({ 624 | (s: USize): MapIs[K, V]^ => 625 | MapIs[K, V].create(s).>concat( 626 | Iter[(K^, V^)](gen.value_iter(rnd)).take(s) 627 | ) 628 | }) 629 | (consume result, shrink_iter) 630 | end) 631 | 632 | 633 | fun one_of[T](xs: ReadSeq[T], do_shrink: Bool = false): Generator[box->T] => 634 | """ 635 | Generate a random value from the given ReadSeq. 636 | This generator will generate nothing if the given xs is empty. 637 | 638 | Generators created with this method do not support shrinking. 639 | If `do_shrink` is set to `true`, it will return the same value 640 | for each shrink round. Otherwise it will return nothing. 641 | """ 642 | 643 | Generator[box->T]( 644 | object is GenObj[box->T] 645 | fun generate(rnd: Randomness): GenerateResult[box->T] ? => 646 | let idx = rnd.usize(0, xs.size() - 1) 647 | let res = xs(idx)? 648 | if do_shrink then 649 | (res, Iter[box->T].repeat_value(res)) 650 | else 651 | res 652 | end 653 | end) 654 | 655 | fun one_of_safe[T](xs: ReadSeq[T], do_shrink: Bool = false): Generator[box->T] ? => 656 | """ 657 | Version of `one_of` that will error if `xs` is empty. 658 | """ 659 | Fact(xs.size() > 0, "cannot use one_of_safe on empty ReadSeq")? 660 | Generators.one_of[T](xs, do_shrink) 661 | 662 | fun frequency[T]( 663 | weighted_generators: ReadSeq[WeightedGenerator[T]]) 664 | : Generator[T] 665 | => 666 | """ 667 | chose a value of one of the given Generators, 668 | while controlling the distribution with the associated weights. 669 | 670 | The weights are of type ``USize`` and control how likely a value is chosen. 671 | The likelihood of a value ``v`` to be chosen 672 | is ``weight_v`` / ``weights_sum``. 673 | If all ``weighted_generators`` have equal size the distribution 674 | will be uniform. 675 | 676 | Example of a generator to output odd ``U8`` values 677 | twice as likely as even ones: 678 | 679 | ```pony 680 | Generators.frequency[U8]([ 681 | (1, Generators.u8().filter({(u) => (u, (u % 2) == 0 })) 682 | (2, Generators.u8().filter({(u) => (u, (u % 2) != 0 })) 683 | ]) 684 | ``` 685 | """ 686 | 687 | // nasty hack to avoid handling the theoretical error case where we have 688 | // no generator and thus would have to change the type signature 689 | Generator[T]( 690 | object is GenObj[T] 691 | fun generate(rnd: Randomness): GenerateResult[T] ? => 692 | let weight_sum: USize = 693 | Iter[WeightedGenerator[T]](weighted_generators.values()) 694 | .fold[USize]( 695 | 0, 696 | // segfaults when types are removed - TODO: investigate 697 | {(acc: USize, weighted_gen: WeightedGenerator[T]): USize^ => 698 | weighted_gen._1 + acc 699 | }) 700 | let desired_sum = rnd.usize(0, weight_sum) 701 | var running_sum: USize = 0 702 | var chosen: (Generator[T] | None) = None 703 | for weighted_gen in weighted_generators.values() do 704 | let new_sum = running_sum + weighted_gen._1 705 | if ((desired_sum == 0) or ((running_sum < desired_sum) and (desired_sum <= new_sum))) then 706 | // we just crossed or reached the desired sum 707 | chosen = weighted_gen._2 708 | break 709 | else 710 | // update running sum 711 | running_sum = new_sum 712 | end 713 | end 714 | match chosen 715 | | let x: Generator[T] box => x.generate(rnd)? 716 | | None => 717 | Debug("chosen is None, desired_sum: " + desired_sum.string() + 718 | "running_sum: " + running_sum.string()) 719 | error 720 | end 721 | end) 722 | 723 | fun frequency_safe[T]( 724 | weighted_generators: ReadSeq[WeightedGenerator[T]]) 725 | : Generator[T] ? 726 | => 727 | """ 728 | Version of `frequency` that errors if the given `weighted_generators` is 729 | empty. 730 | """ 731 | Fact(weighted_generators.size() > 0, 732 | "cannot use frequency_safe on empty ReadSeq[WeightedGenerator]")? 733 | Generators.frequency[T](weighted_generators) 734 | 735 | fun zip2[T1, T2]( 736 | gen1: Generator[T1], 737 | gen2: Generator[T2]) 738 | : Generator[(T1, T2)] 739 | => 740 | """ 741 | zip two generators into a generator of a 2-tuple 742 | containing the values generated by both generators. 743 | """ 744 | Generator[(T1, T2)]( 745 | object is GenObj[(T1, T2)] 746 | fun generate(rnd: Randomness): GenerateResult[(T1, T2)] ? => 747 | (let v1: T1, let shrinks1: Iterator[T1^]) = 748 | gen1.generate_and_shrink(rnd)? 749 | (let v2: T2, let shrinks2: Iterator[T2^]) = 750 | gen2.generate_and_shrink(rnd)? 751 | ((consume v1, consume v2), Iter[T1^](shrinks1).zip[T2^](shrinks2)) 752 | 753 | fun shrink(t: (T1, T2)): ValueAndShrink[(T1, T2)] => 754 | (let t1, let t2) = consume t 755 | (let t11, let t1_shrunken: Iterator[T1^]) = gen1.shrink(consume t1) 756 | (let t21, let t2_shrunken: Iterator[T2^]) = gen2.shrink(consume t2) 757 | 758 | let shrunken = Iter[T1^](t1_shrunken).zip[T2^](t2_shrunken) 759 | ((consume t11, consume t21), shrunken) 760 | end) 761 | 762 | fun zip3[T1, T2, T3]( 763 | gen1: Generator[T1], 764 | gen2: Generator[T2], 765 | gen3: Generator[T3]) 766 | : Generator[(T1, T2, T3)] 767 | => 768 | """ 769 | zip three generators into a generator of a 3-tuple 770 | containing the values generated by those three generators. 771 | """ 772 | Generator[(T1, T2, T3)]( 773 | object is GenObj[(T1, T2, T3)] 774 | fun generate(rnd: Randomness): GenerateResult[(T1, T2, T3)] ? => 775 | (let v1: T1, let shrinks1: Iterator[T1^]) = 776 | gen1.generate_and_shrink(rnd)? 777 | (let v2: T2, let shrinks2: Iterator[T2^]) = 778 | gen2.generate_and_shrink(rnd)? 779 | (let v3: T3, let shrinks3: Iterator[T3^]) = 780 | gen3.generate_and_shrink(rnd)? 781 | ((consume v1, consume v2, consume v3), 782 | Iter[T1^](shrinks1).zip2[T2^, T3^](shrinks2, shrinks3)) 783 | 784 | fun shrink(t: (T1, T2, T3)): ValueAndShrink[(T1, T2, T3)] => 785 | (let t1, let t2, let t3) = consume t 786 | (let t11, let t1_shrunken: Iterator[T1^]) = gen1.shrink(consume t1) 787 | (let t21, let t2_shrunken: Iterator[T2^]) = gen2.shrink(consume t2) 788 | (let t31, let t3_shrunken: Iterator[T3^]) = gen3.shrink(consume t3) 789 | 790 | let shrunken = Iter[T1^](t1_shrunken).zip2[T2^, T3^](t2_shrunken, t3_shrunken) 791 | ((consume t11, consume t21, consume t31), shrunken) 792 | end) 793 | 794 | fun zip4[T1, T2, T3, T4]( 795 | gen1: Generator[T1], 796 | gen2: Generator[T2], 797 | gen3: Generator[T3], 798 | gen4: Generator[T4]) 799 | : Generator[(T1, T2, T3, T4)] 800 | => 801 | """ 802 | zip four generators into a generator of a 4-tuple 803 | containing the values generated by those four generators. 804 | """ 805 | Generator[(T1, T2, T3, T4)]( 806 | object is GenObj[(T1, T2, T3, T4)] 807 | fun generate(rnd: Randomness): GenerateResult[(T1, T2, T3, T4)] ? => 808 | (let v1: T1, let shrinks1: Iterator[T1^]) = 809 | gen1.generate_and_shrink(rnd)? 810 | (let v2: T2, let shrinks2: Iterator[T2^]) = 811 | gen2.generate_and_shrink(rnd)? 812 | (let v3: T3, let shrinks3: Iterator[T3^]) = 813 | gen3.generate_and_shrink(rnd)? 814 | (let v4: T4, let shrinks4: Iterator[T4^]) = 815 | gen4.generate_and_shrink(rnd)? 816 | ((consume v1, consume v2, consume v3, consume v4), 817 | Iter[T1^](shrinks1).zip3[T2^, T3^, T4^](shrinks2, shrinks3, shrinks4)) 818 | 819 | fun shrink(t: (T1, T2, T3, T4)): ValueAndShrink[(T1, T2, T3, T4)] => 820 | (let t1, let t2, let t3, let t4) = consume t 821 | (let t11, let t1_shrunken) = gen1.shrink(consume t1) 822 | (let t21, let t2_shrunken) = gen2.shrink(consume t2) 823 | (let t31, let t3_shrunken) = gen3.shrink(consume t3) 824 | (let t41, let t4_shrunken) = gen4.shrink(consume t4) 825 | 826 | let shrunken = Iter[T1^](t1_shrunken) 827 | .zip3[T2^, T3^, T4^](t2_shrunken, t3_shrunken, t4_shrunken) 828 | ((consume t11, consume t21, consume t31, consume t41), shrunken) 829 | end) 830 | 831 | fun map2[T1, T2, T3]( 832 | gen1: Generator[T1], 833 | gen2: Generator[T2], 834 | fn: {(T1, T2): T3^}) 835 | : Generator[T3] 836 | => 837 | """ 838 | convenience combinator for mapping 2 generators into 1 839 | """ 840 | Generators.zip2[T1, T2](gen1, gen2) 841 | .map[T3]({(arg) => 842 | (let arg1, let arg2) = consume arg 843 | fn(consume arg1, consume arg2) 844 | }) 845 | 846 | fun map3[T1, T2, T3, T4]( 847 | gen1: Generator[T1], 848 | gen2: Generator[T2], 849 | gen3: Generator[T3], 850 | fn: {(T1, T2, T3): T4^}) 851 | : Generator[T4] 852 | => 853 | """ 854 | convenience combinator for mapping 3 generators into 1 855 | """ 856 | Generators.zip3[T1, T2, T3](gen1, gen2, gen3) 857 | .map[T4]({(arg) => 858 | (let arg1, let arg2, let arg3) = consume arg 859 | fn(consume arg1, consume arg2, consume arg3) 860 | }) 861 | 862 | fun map4[T1, T2, T3, T4, T5]( 863 | gen1: Generator[T1], 864 | gen2: Generator[T2], 865 | gen3: Generator[T3], 866 | gen4: Generator[T4], 867 | fn: {(T1, T2, T3, T4): T5^}) 868 | : Generator[T5] 869 | => 870 | """ 871 | convenience combinator for mapping 4 generators into 1 872 | """ 873 | Generators.zip4[T1, T2, T3, T4](gen1, gen2, gen3, gen4) 874 | .map[T5]({(arg) => 875 | (let arg1, let arg2, let arg3, let arg4) = consume arg 876 | fn(consume arg1, consume arg2, consume arg3, consume arg4) 877 | }) 878 | 879 | fun bool(): Generator[Bool] => 880 | """ 881 | create a generator of bool values. 882 | """ 883 | Generator[Bool]( 884 | object is GenObj[Bool] 885 | fun generate(rnd: Randomness): Bool => 886 | rnd.bool() 887 | end) 888 | 889 | fun _int_shrink[T: (Int & Integer[T] val)](t: T^, min: T): ValueAndShrink[T] => 890 | """ 891 | """ 892 | let relation = t.compare(min) 893 | let t_copy: T = T.create(t) 894 | //Debug(t.string() + " is " + relation.string() + " than min " + min.string()) 895 | let sub_iter = 896 | object is Iterator[T^] 897 | var _cur: T = t_copy 898 | var _subtract: F64 = 1.0 899 | var _overflow: Bool = false 900 | 901 | fun ref _next_minuend(): T => 902 | // f(x) = x + (2^-5 * x^2) 903 | T.from[F64](_subtract = _subtract + (0.03125 * _subtract * _subtract)) 904 | 905 | fun ref has_next(): Bool => 906 | match relation 907 | | Less => (_cur < min) and not _overflow 908 | | Equal => false 909 | | Greater => (_cur > min) and not _overflow 910 | end 911 | 912 | fun ref next(): T^ ? => 913 | match relation 914 | | Less => 915 | let minuend: T = _next_minuend() 916 | let old = _cur 917 | _cur = _cur + minuend 918 | if old > _cur then 919 | _overflow = true 920 | end 921 | old 922 | | Equal => error 923 | | Greater => 924 | let minuend: T = _next_minuend() 925 | let old = _cur 926 | _cur = _cur - minuend 927 | if old < _cur then 928 | _overflow = true 929 | end 930 | old 931 | end 932 | end 933 | 934 | let min_iter = 935 | match relation 936 | | let _: (Less | Greater) => Poperator[T]([min]) 937 | | Equal => Poperator[T].empty() 938 | end 939 | 940 | let shrunken_iter = Iter[T].chain( 941 | [ 942 | Iter[T^](sub_iter).skip(1) 943 | min_iter 944 | ].values()) 945 | (consume t, shrunken_iter) 946 | 947 | fun u8( 948 | min: U8 = U8.min_value(), 949 | max: U8 = U8.max_value()) 950 | : Generator[U8] 951 | => 952 | """ 953 | create a generator for U8 values 954 | """ 955 | let that = this 956 | Generator[U8]( 957 | object is GenObj[U8] 958 | fun generate(rnd: Randomness): U8^ => 959 | rnd.u8(min, max) 960 | 961 | fun shrink(u: U8): ValueAndShrink[U8] => 962 | that._int_shrink[U8](consume u, min) 963 | end) 964 | 965 | fun u16( 966 | min: U16 = U16.min_value(), 967 | max: U16 = U16.max_value()) 968 | : Generator[U16] 969 | => 970 | """ 971 | create a generator for U16 values 972 | """ 973 | let that = this 974 | Generator[U16]( 975 | object is GenObj[U16] 976 | fun generate(rnd: Randomness): U16^ => 977 | rnd.u16(min, max) 978 | 979 | fun shrink(u: U16): ValueAndShrink[U16] => 980 | that._int_shrink[U16](consume u, min) 981 | end) 982 | 983 | fun u32( 984 | min: U32 = U32.min_value(), 985 | max: U32 = U32.max_value()) 986 | : Generator[U32] 987 | => 988 | """ 989 | create a generator for U32 values 990 | """ 991 | let that = this 992 | Generator[U32]( 993 | object is GenObj[U32] 994 | fun generate(rnd: Randomness): U32^ => 995 | rnd.u32(min, max) 996 | 997 | fun shrink(u: U32): ValueAndShrink[U32] => 998 | that._int_shrink[U32](consume u, min) 999 | end) 1000 | 1001 | fun u64( 1002 | min: U64 = U64.min_value(), 1003 | max: U64 = U64.max_value()) 1004 | : Generator[U64] 1005 | => 1006 | """ 1007 | create a generator for U64 values 1008 | """ 1009 | let that = this 1010 | Generator[U64]( 1011 | object is GenObj[U64] 1012 | fun generate(rnd: Randomness): U64^ => 1013 | rnd.u64(min, max) 1014 | 1015 | fun shrink(u: U64): ValueAndShrink[U64] => 1016 | that._int_shrink[U64](consume u, min) 1017 | end) 1018 | 1019 | fun u128( 1020 | min: U128 = U128.min_value(), 1021 | max: U128 = U128.max_value()) 1022 | : Generator[U128] 1023 | => 1024 | """ 1025 | create a generator for U128 values 1026 | """ 1027 | let that = this 1028 | Generator[U128]( 1029 | object is GenObj[U128] 1030 | fun generate(rnd: Randomness): U128^ => 1031 | rnd.u128(min, max) 1032 | 1033 | fun shrink(u: U128): ValueAndShrink[U128] => 1034 | that._int_shrink[U128](consume u, min) 1035 | end) 1036 | 1037 | fun usize( 1038 | min: USize = USize.min_value(), 1039 | max: USize = USize.max_value()) 1040 | : Generator[USize] 1041 | => 1042 | """ 1043 | create a generator for USize values 1044 | """ 1045 | let that = this 1046 | Generator[USize]( 1047 | object is GenObj[USize] 1048 | fun generate(rnd: Randomness): GenerateResult[USize] => 1049 | rnd.usize(min, max) 1050 | 1051 | fun shrink(u: USize): ValueAndShrink[USize] => 1052 | that._int_shrink[USize](consume u, min) 1053 | end) 1054 | 1055 | fun ulong( 1056 | min: ULong = ULong.min_value(), 1057 | max: ULong = ULong.max_value()) 1058 | : Generator[ULong] 1059 | => 1060 | """ 1061 | create a generator for ULong values 1062 | """ 1063 | let that = this 1064 | Generator[ULong]( 1065 | object is GenObj[ULong] 1066 | fun generate(rnd: Randomness): ULong^ => 1067 | rnd.ulong(min, max) 1068 | 1069 | fun shrink(u: ULong): ValueAndShrink[ULong] => 1070 | that._int_shrink[ULong](consume u, min) 1071 | end) 1072 | 1073 | fun i8( 1074 | min: I8 = I8.min_value(), 1075 | max: I8 = I8.max_value()) 1076 | : Generator[I8] 1077 | => 1078 | """ 1079 | create a generator for I8 values 1080 | """ 1081 | let that = this 1082 | Generator[I8]( 1083 | object is GenObj[I8] 1084 | fun generate(rnd: Randomness): I8^ => 1085 | rnd.i8(min, max) 1086 | 1087 | fun shrink(i: I8): ValueAndShrink[I8] => 1088 | that._int_shrink[I8](consume i, min) 1089 | end) 1090 | 1091 | fun i16( 1092 | min: I16 = I16.min_value(), 1093 | max: I16 = I16.max_value()) 1094 | : Generator[I16] 1095 | => 1096 | """ 1097 | create a generator for I16 values 1098 | """ 1099 | let that = this 1100 | Generator[I16]( 1101 | object is GenObj[I16] 1102 | fun generate(rnd: Randomness): I16^ => 1103 | rnd.i16(min, max) 1104 | 1105 | fun shrink(i: I16): ValueAndShrink[I16] => 1106 | that._int_shrink[I16](consume i, min) 1107 | end) 1108 | 1109 | fun i32( 1110 | min: I32 = I32.min_value(), 1111 | max: I32 = I32.max_value()) 1112 | : Generator[I32] 1113 | => 1114 | """ 1115 | create a generator for I32 values 1116 | """ 1117 | let that = this 1118 | Generator[I32]( 1119 | object is GenObj[I32] 1120 | fun generate(rnd: Randomness): I32^ => 1121 | rnd.i32(min, max) 1122 | 1123 | fun shrink(i: I32): ValueAndShrink[I32] => 1124 | that._int_shrink[I32](consume i, min) 1125 | end) 1126 | 1127 | fun i64( 1128 | min: I64 = I64.min_value(), 1129 | max: I64 = I64.max_value()) 1130 | : Generator[I64] 1131 | => 1132 | """ 1133 | create a generator for I64 values 1134 | """ 1135 | let that = this 1136 | Generator[I64]( 1137 | object is GenObj[I64] 1138 | fun generate(rnd: Randomness): I64^ => 1139 | rnd.i64(min, max) 1140 | 1141 | fun shrink(i: I64): ValueAndShrink[I64] => 1142 | that._int_shrink[I64](consume i, min) 1143 | end) 1144 | 1145 | fun i128( 1146 | min: I128 = I128.min_value(), 1147 | max: I128 = I128.max_value()) 1148 | : Generator[I128] 1149 | => 1150 | """ 1151 | create a generator for I128 values 1152 | """ 1153 | let that = this 1154 | Generator[I128]( 1155 | object is GenObj[I128] 1156 | fun generate(rnd: Randomness): I128^ => 1157 | rnd.i128(min, max) 1158 | 1159 | fun shrink(i: I128): ValueAndShrink[I128] => 1160 | that._int_shrink[I128](consume i, min) 1161 | end) 1162 | 1163 | fun ilong( 1164 | min: ILong = ILong.min_value(), 1165 | max: ILong = ILong.max_value()) 1166 | : Generator[ILong] 1167 | => 1168 | """ 1169 | create a generator for ILong values 1170 | """ 1171 | let that = this 1172 | Generator[ILong]( 1173 | object is GenObj[ILong] 1174 | fun generate(rnd: Randomness): ILong^ => 1175 | rnd.ilong(min, max) 1176 | 1177 | fun shrink(i: ILong): ValueAndShrink[ILong] => 1178 | that._int_shrink[ILong](consume i, min) 1179 | end) 1180 | 1181 | fun isize( 1182 | min: ISize = ISize.min_value(), 1183 | max: ISize = ISize.max_value()) 1184 | : Generator[ISize] 1185 | => 1186 | """ 1187 | create a generator for ISize values 1188 | """ 1189 | let that = this 1190 | Generator[ISize]( 1191 | object is GenObj[ISize] 1192 | fun generate(rnd: Randomness): ISize^ => 1193 | rnd.isize(min, max) 1194 | 1195 | fun shrink(i: ISize): ValueAndShrink[ISize] => 1196 | that._int_shrink[ISize](consume i, min) 1197 | end) 1198 | 1199 | 1200 | fun byte_string( 1201 | gen: Generator[U8], 1202 | min: USize = 0, 1203 | max: USize = 100) 1204 | : Generator[String] 1205 | => 1206 | """ 1207 | create a generator for strings 1208 | generated from the bytes returned by the generator ``gen`` 1209 | with a minimum length of ``min`` (default: 0) 1210 | and a maximum length of ``max`` (default: 100). 1211 | """ 1212 | Generator[String]( 1213 | object is GenObj[String] 1214 | fun generate(rnd: Randomness): GenerateResult[String] => 1215 | let size = rnd.usize(min, max) 1216 | let gen_iter = Iter[U8^](gen.value_iter(rnd)) 1217 | .take(size) 1218 | let arr: Array[U8] iso = recover Array[U8](size) end 1219 | for b in gen_iter do 1220 | arr.push(b) 1221 | end 1222 | String.from_iso_array(consume arr) 1223 | 1224 | fun shrink(s: String): ValueAndShrink[String] => 1225 | """ 1226 | shrink string until ``min`` length. 1227 | """ 1228 | var str: String = s.trim(0, s.size()-1) 1229 | let shorten_iter: Iterator[String^] = 1230 | object is Iterator[String^] 1231 | fun ref has_next(): Bool => str.size() > min 1232 | fun ref next(): String^ => 1233 | str = str.trim(0, str.size()-1) 1234 | end 1235 | let min_iter = 1236 | if s.size() > min then 1237 | Poperator[String]([s.trim(0, min)]) 1238 | else 1239 | Poperator[String].empty() 1240 | end 1241 | let shrink_iter = 1242 | Iter[String^].chain([ 1243 | shorten_iter 1244 | min_iter 1245 | ].values()) 1246 | (consume s, shrink_iter) 1247 | end) 1248 | 1249 | fun ascii( 1250 | min: USize = 0, 1251 | max: USize = 100, 1252 | range: ASCIIRange = ASCIIAll) 1253 | : Generator[String] 1254 | => 1255 | """ 1256 | create a generator for strings withing the given ``range`` 1257 | with a minimum length of ``min`` (default: 0) 1258 | and a maximum length of ``max`` (default: 100). 1259 | """ 1260 | let range_bytes = range.apply() 1261 | let fallback = U8(0) 1262 | let range_bytes_gen = usize(0, range_bytes.size()-1) 1263 | .map[U8]({(size) => 1264 | try 1265 | range_bytes(size)? 1266 | else 1267 | // should never happen 1268 | fallback 1269 | end }) 1270 | byte_string(range_bytes_gen, min, max) 1271 | 1272 | fun ascii_printable( 1273 | min: USize = 0, 1274 | max: USize = 100) 1275 | : Generator[String] 1276 | => 1277 | """ 1278 | create a generator for strings of printable ascii characters 1279 | with a minimum length of ``min`` (default: 0) 1280 | and a maximum length of ``max`` (default: 100). 1281 | """ 1282 | ascii(min, max, ASCIIPrintable) 1283 | 1284 | fun ascii_numeric( 1285 | min: USize = 0, 1286 | max: USize = 100) 1287 | : Generator[String] 1288 | => 1289 | """ 1290 | create a generator for strings of numeric ascii characters 1291 | with a minimum length of ``min`` (default: 0) 1292 | and a maximum length of ``max`` (default: 100). 1293 | """ 1294 | ascii(min, max, ASCIIDigits) 1295 | 1296 | fun ascii_letters( 1297 | min: USize = 0, 1298 | max: USize = 100) 1299 | : Generator[String] 1300 | => 1301 | """ 1302 | create a generator for strings of ascii letters 1303 | with a minimum length of ``min`` (default: 0) 1304 | and a maximum length of ``max`` (default: 100). 1305 | """ 1306 | ascii(min, max, ASCIILetters) 1307 | 1308 | fun utf32_codepoint_string( 1309 | gen: Generator[U32], 1310 | min: USize = 0, 1311 | max: USize = 100) 1312 | : Generator[String] 1313 | => 1314 | """ 1315 | create a generator for strings 1316 | from a generator of unicode codepoints 1317 | with a minimum length of ``min`` codepoints (default: 0) 1318 | and a maximum length of ``max`` codepoints (default: 100). 1319 | 1320 | Note that the byte length of the generated string can be up to 4 times 1321 | the size in code points. 1322 | """ 1323 | Generator[String]( 1324 | object is GenObj[String] 1325 | fun generate(rnd: Randomness): GenerateResult[String] => 1326 | let size = rnd.usize(min, max) 1327 | let gen_iter = Iter[U32^](gen.value_iter(rnd)) 1328 | .filter({(cp) => 1329 | // excluding surrogate pairs 1330 | (cp <= 0xD7FF) or (cp >= 0xE000) }) 1331 | .take(size) 1332 | let s: String iso = recover String(size) end 1333 | for code_point in gen_iter do 1334 | s.push_utf32(code_point) 1335 | end 1336 | s 1337 | 1338 | fun shrink(s: String): ValueAndShrink[String] => 1339 | """ 1340 | strip off codepoints from the end, not just bytes, so we 1341 | maintain a valid utf8 string 1342 | 1343 | only shrink until given ``min`` is hit 1344 | """ 1345 | var shrink_base = s 1346 | let s_len = s.codepoints() 1347 | let shrink_iter: Iterator[String^] = 1348 | if s_len > min then 1349 | Iter[String^].repeat_value(consume shrink_base) 1350 | .map_stateful[String^]( 1351 | object 1352 | var len: USize = s_len - 1 1353 | fun ref apply(str: String): String => 1354 | Generators._trim_codepoints(str, len = len - 1) 1355 | end 1356 | ).take(s_len - min) 1357 | // take_while is buggy in pony < 0.21.0 1358 | //.take_while({(t) => t.codepoints() > min}) 1359 | else 1360 | Poperator[String].empty() 1361 | end 1362 | (consume s, shrink_iter) 1363 | end) 1364 | 1365 | fun _trim_codepoints(s: String, trim_to: USize): String => 1366 | recover val 1367 | Iter[U32](s.runes()) 1368 | .take(trim_to) 1369 | .fold[String ref]( 1370 | String.create(trim_to), 1371 | {(acc, cp) => acc.>push_utf32(cp) }) 1372 | end 1373 | 1374 | fun unicode( 1375 | min: USize = 0, 1376 | max: USize = 100) 1377 | : Generator[String] 1378 | => 1379 | """ 1380 | create a generator for unicode strings 1381 | with a minimum length of ``min`` codepoints (default: 0) 1382 | and a maximum length of ``max`` codepoints (default: 100). 1383 | 1384 | Note that the byte length of the generated string can be up to 4 times 1385 | the size in code points. 1386 | """ 1387 | let range_1 = u32(0x0, 0xD7FF) 1388 | let range_1_size: USize = 0xD7FF 1389 | // excluding surrogate pairs 1390 | // this might be duplicate work but increases efficiency 1391 | let range_2 = u32(0xE000, 0x10FFFF) 1392 | let range_2_size = U32(0x10FFFF - 0xE000).usize() 1393 | 1394 | let code_point_gen = 1395 | frequency[U32]([ 1396 | (range_1_size, range_1) 1397 | (range_2_size, range_2) 1398 | ]) 1399 | utf32_codepoint_string(code_point_gen, min, max) 1400 | 1401 | fun unicode_bmp( 1402 | min: USize = 0, 1403 | max: USize = 100) 1404 | : Generator[String] 1405 | => 1406 | """ 1407 | create a generator for unicode strings 1408 | from the basic multilingual plane only 1409 | with a minimum length of ``min`` codepoints (default: 0) 1410 | and a maximum length of ``max`` codepoints (default: 100). 1411 | 1412 | Note that the byte length of the generated string can be up to 4 times 1413 | the size in code points. 1414 | """ 1415 | let range_1 = u32(0x0, 0xD7FF) 1416 | let range_1_size: USize = 0xD7FF 1417 | // excluding surrogate pairs 1418 | // this might be duplicate work but increases efficiency 1419 | let range_2 = u32(0xE000, 0xFFFF) 1420 | let range_2_size = U32(0xFFFF - 0xE000).usize() 1421 | 1422 | let code_point_gen = 1423 | frequency[U32]([ 1424 | (range_1_size, range_1) 1425 | (range_2_size, range_2) 1426 | ]) 1427 | utf32_codepoint_string(code_point_gen, min, max) 1428 | 1429 | -------------------------------------------------------------------------------- /ponycheck/int_properties.pony: -------------------------------------------------------------------------------- 1 | primitive _StringifyIntArg 2 | fun apply(choice: U8, int: U128): String iso ^ => 3 | let num = 4 | match choice % 14 5 | | 0 => "U8(" + int.u8().string() + ")" 6 | | 1 => "U16(" + int.u16().string() + ")" 7 | | 2 => "U32(" + int.u32().string() + ")" 8 | | 3 => "U64(" + int.u64().string() + ")" 9 | | 4 => "ULong(" + int.ulong().string() + ")" 10 | | 5 => "USize(" + int.usize().string() + ")" 11 | | 6 => "U128(" + int.string() + ")" 12 | | 7 => "I8(" + int.i8().string() + ")" 13 | | 8 => "I16(" + int.i16().string() + ")" 14 | | 9 => "I32(" + int.i32().string() + ")" 15 | | 10 => "I64(" + int.i64().string() + ")" 16 | | 11 => "ILong(" + int.ilong().string() + ")" 17 | | 12 => "ISize(" + int.isize().string() + ")" 18 | | 13 => "I128(" + int.i128().string() + ")" 19 | else 20 | "" 21 | end 22 | num.clone() 23 | 24 | class IntPropertySample is Stringable 25 | let choice: U8 26 | let int: U128 27 | 28 | new create(choice': U8, int': U128) => 29 | choice = choice' 30 | int = int' 31 | 32 | fun string(): String iso^ => 33 | _StringifyIntArg(choice, int) 34 | 35 | type IntUnitTest is Property1UnitTest[IntPropertySample] 36 | 37 | trait IntProperty is Property1[IntPropertySample] 38 | """ 39 | A property implementation for conveniently evaluating properties 40 | for all Pony Integer types at once. 41 | 42 | The property needs to be formulated inside the method `int_property`: 43 | 44 | ```pony 45 | class DivisionByZeroProperty is IntProperty 46 | fun name(): String => "div/0" 47 | 48 | fun int_property[T: (Int & Integer[T] val)](x: T, h: PropertyHelper)? => 49 | h.assert_eq[T](T.from[U8](0), x / T.from[U8](0)) 50 | ``` 51 | """ 52 | fun gen(): Generator[IntPropertySample] => 53 | Generators.map2[U8, U128, IntPropertySample]( 54 | Generators.u8(), 55 | Generators.u128(), 56 | {(choice, int) => IntPropertySample(choice, int) }) 57 | 58 | fun ref property(sample: IntPropertySample, h: PropertyHelper) ? => 59 | let x = sample.int 60 | match sample.choice % 14 61 | | 0 => int_property[U8](x.u8(), h)? 62 | | 1 => int_property[U16](x.u16(), h)? 63 | | 2 => int_property[U32](x.u32(), h)? 64 | | 3 => int_property[U64](x.u64(), h)? 65 | | 4 => int_property[ULong](x.ulong(), h)? 66 | | 5 => int_property[USize](x.usize(), h)? 67 | | 6 => int_property[U128](x, h)? 68 | | 7 => int_property[I8](x.i8(), h)? 69 | | 8 => int_property[I16](x.i16(), h)? 70 | | 9 => int_property[I32](x.i32(), h)? 71 | | 10 => int_property[I64](x.i64(), h)? 72 | | 11 => int_property[ILong](x.ilong(), h)? 73 | | 12 => int_property[ISize](x.isize(), h)? 74 | | 13 => int_property[I128](x.i128(), h)? 75 | else 76 | h.log("rem is broken") 77 | error 78 | end 79 | 80 | fun ref int_property[T: (Int & Integer[T] val)](x: T, h: PropertyHelper)? 81 | 82 | class IntPairPropertySample is Stringable 83 | let choice: U8 84 | let int1: U128 85 | let int2: U128 86 | 87 | new create(choice': U8, int1': U128, int2': U128) => 88 | choice = choice' 89 | int1 = int1' 90 | int2 = int2' 91 | 92 | fun string(): String iso^ => 93 | let num1: String val = _StringifyIntArg(choice, int1) 94 | let num2: String val = _StringifyIntArg(choice, int2) 95 | "".join(["("; num1; ", "; num2; ")"].values()) 96 | 97 | 98 | type IntPairUnitTest is Property1UnitTest[IntPairPropertySample] 99 | 100 | trait IntPairProperty is Property1[IntPairPropertySample] 101 | """ 102 | A property implementation for conveniently evaluating properties 103 | for pairs of integers of all Pony integer types at once. 104 | 105 | The property needs to be formulated inside the method `int_property`: 106 | 107 | ```pony 108 | class CommutativeMultiplicationProperty is IntPairProperty 109 | fun name(): String => "commutativity/mul" 110 | 111 | fun int_property[T: (Int & Integer[T] val)](x: T, y: T, h: PropertyHelper)? => 112 | h.assert_eq[T](x * y, y * x) 113 | ``` 114 | """ 115 | fun gen(): Generator[IntPairPropertySample] => 116 | Generators.map3[U8, U128, U128, IntPairPropertySample]( 117 | Generators.u8(), 118 | Generators.u128(), 119 | Generators.u128(), 120 | {(choice, int1, int2) => IntPairPropertySample(choice, int1, int2) }) 121 | 122 | fun ref property(sample: IntPairPropertySample, h: PropertyHelper) ? => 123 | let x = sample.int1 124 | let y = sample.int2 125 | match sample.choice % 14 126 | | 0 => int_property[U8](x.u8(), y.u8(), h)? 127 | | 1 => int_property[U16](x.u16(), y.u16(), h)? 128 | | 2 => int_property[U32](x.u32(), y.u32(), h)? 129 | | 3 => int_property[U64](x.u64(), y.u64(), h)? 130 | | 4 => int_property[ULong](x.ulong(), y.ulong(), h)? 131 | | 5 => int_property[USize](x.usize(), y.usize(), h)? 132 | | 6 => int_property[U128](x, y, h)? 133 | | 7 => int_property[I8](x.i8(), y.i8(), h)? 134 | | 8 => int_property[I16](x.i16(), y.i16(), h)? 135 | | 9 => int_property[I32](x.i32(), y.i32(), h)? 136 | | 10 => int_property[I64](x.i64(), y.i64(), h)? 137 | | 11 => int_property[ILong](x.ilong(), y.ilong(), h)? 138 | | 12 => int_property[ISize](x.isize(), y.isize(), h)? 139 | | 13 => int_property[I128](x.i128(), y.i128(), h)? 140 | else 141 | h.log("rem is broken") 142 | error 143 | end 144 | 145 | fun ref int_property[T: (Int & Integer[T] val)](x: T, y: T, h: PropertyHelper)? 146 | -------------------------------------------------------------------------------- /ponycheck/ponycheck.pony: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | Ponycheck is a library for property based testing 4 | with tight integration into ponytest. 5 | 6 | ## Property Based Testing 7 | 8 | In _traditional_ unit testing the developer specifies one or more input 9 | examples manually for the class or system under test and assert on certain 10 | output conditions. The difficulty here is to find enough examples to cover 11 | all branches and cases of the class or system under test. 12 | 13 | In property bases testing the developer defines a property, a kind of predicate 14 | for the class or system under test that should hold for all kinds or just a 15 | subset of possible input values. The property based testing engine then 16 | generates a big number of random input values and checks if the property holds 17 | for all of them. The developer only needs to specify the possible set of input values 18 | using a Generator. 19 | 20 | This testing technique is great for finding edge cases that would easily go unnoticed 21 | with manually constructed test samples. In general it can lead to much higher 22 | coverage than traditional unit-testing, with much less code to write. 23 | 24 | ## How Ponycheck implements Property Based Testing 25 | 26 | A property based test in ponycheck consists of the following: 27 | 28 | * A name (for integration into ponytest mostly) 29 | * One or more generators, depending on how your property is layed out. 30 | There are tons of them defined on primitive 31 | [Generators](ponycheck-Generators.md). 32 | * A `property` method that asserts a certain property for each sample 33 | generated by the [Generator(s)](ponycheck-Generator.md) with the help of 34 | [PropertyHelper](ponycheck-PropertyHelper.md) which tries to expose a similar API 35 | as [TestHelper](ponytest-TestHelper.md). 36 | * Optionally, the method ``params()`` can be used to configure how ponycheck executes 37 | the property by specifying a custom [PropertyParams](ponycheck-PropertyParams.md) object. 38 | 39 | The classical list-reverse example: 40 | 41 | ```pony 42 | use "collections" 43 | use "ponycheck" 44 | 45 | class ListReverseProperty is Property1[List[USize]] 46 | fun name(): String => "list/reverse" 47 | 48 | fun gen(): Generator[List[USize]] => 49 | Generators.list_of[USize](Generators.usize()) 50 | 51 | fun property(arg1: List[USize], ph: PropertyHelper) => 52 | ph.array_eq[USize](arg1, arg1.reverse().reverse()) 53 | ``` 54 | 55 | ## Integration into Ponytest 56 | 57 | There are two ways of integrating a [Property](ponycheck-Property1.md) into 58 | [ponytest](ponytest--index.md): 59 | 60 | 1. In order to pass your Property to the ponytest engine, you need to wrap it inside a [Property1UnitTest](ponycheck-Property1UnitTest.md). 61 | 62 | ```pony 63 | actor Main is TestList 64 | new create(env: Env) => PonyTest(env, this) 65 | 66 | fun tag tests(test: PonyTest) => 67 | test(Property1UnitTest[String](MyStringProperty)) 68 | ``` 69 | 70 | 2. Run as much [Properties](ponycheck-Property1.md) as you wish inside one ponytest 71 | [UnitTest](ponytest-UnitTest.md) using the convenience function 72 | [Ponycheck.for_all](ponycheck-Ponycheck.md#for_all) providing a 73 | [Generator](ponycheck-Generator), the [TestHelper](ponytest-TestHelper.md) and the 74 | actual property function. (Note that the property function is supplied in a 75 | second application of the result to `for_all`.) 76 | 77 | ```pony 78 | class ListReversePropertyWithinAUnitTest is UnitTest 79 | fun name(): String => "list/reverse/forall" 80 | 81 | fun apply(h: TestHelper) => 82 | let gen = recover val Generators.list_of[USize](Generators.usize()) end 83 | Ponycheck.for_all[List[USize]](gen, h)( 84 | {(sample, ph) => 85 | ph.array_eq[Usize](arg1, arg1.reverse().reverse()) 86 | }) 87 | // ... possibly more properties, using ``Ponycheck.for_all`` 88 | ``` 89 | 90 | Independent of how you integrate with [ponytest](ponytest--index.md), 91 | the ponycheck machinery will instantiate the provided Generator, and will 92 | execute it for a configurable number of samples. 93 | 94 | If the property fails using an assertion method of 95 | [PropertyHelper](ponycheck-PropertyHelper.md), 96 | the failed example will be shrunken by the generator 97 | to obtain a smaller and more informative, still failing, sample 98 | for reporting. 99 | 100 | """ 101 | use "ponytest" 102 | 103 | primitive Ponycheck 104 | fun for_all[T](gen: Generator[T] val, h: TestHelper): ForAll[T] => 105 | """ 106 | convenience method for running 1 to many properties as part of 107 | one ponytest UnitTest. 108 | 109 | Example: 110 | 111 | ```pony 112 | class MyTestWithSomeProperties is UnitTest 113 | fun name(): String => "mytest/withMultipleProperties" 114 | 115 | fun apply(h: TestHelper) => 116 | Ponycheck.for_all[U8](recover Generators.unit[U8](0) end, h)( 117 | {(u, h) => 118 | h.assert_eq(u, 0) 119 | consume u 120 | }) 121 | ``` 122 | """ 123 | ForAll[T](gen, h) 124 | 125 | fun for_all2[T1, T2]( 126 | gen1: Generator[T1] val, 127 | gen2: Generator[T2] val, 128 | h: TestHelper) 129 | : ForAll2[T1, T2] 130 | => 131 | ForAll2[T1, T2](gen1, gen2, h) 132 | 133 | fun for_all3[T1, T2, T3]( 134 | gen1: Generator[T1] val, 135 | gen2: Generator[T2] val, 136 | gen3: Generator[T3] val, 137 | h: TestHelper) 138 | : ForAll3[T1, T2, T3] 139 | => 140 | ForAll3[T1, T2, T3](gen1, gen2, gen3, h) 141 | 142 | fun for_all4[T1, T2, T3, T4]( 143 | gen1: Generator[T1] val, 144 | gen2: Generator[T2] val, 145 | gen3: Generator[T3] val, 146 | gen4: Generator[T4] val, 147 | h: TestHelper) 148 | : ForAll4[T1, T2, T3, T4] 149 | => 150 | ForAll4[T1, T2, T3, T4](gen1, gen2, gen3, gen4, h) 151 | -------------------------------------------------------------------------------- /ponycheck/poperator.pony: -------------------------------------------------------------------------------- 1 | class ref Poperator[T] is Iterator[T^] 2 | """ 3 | iterate over a [Seq](builtin-Seq.md) descructively by `pop`ing its elements 4 | 5 | once `has_next()` returns `false`, the [Seq](builtin-Seq.md) is empty. 6 | 7 | Nominee for the annual pony class-naming awards. 8 | """ 9 | 10 | let _seq: Seq[T] 11 | 12 | new create(seq: Seq[T]) => 13 | _seq = seq 14 | 15 | new empty() => 16 | _seq = Array[T](0) 17 | 18 | fun ref has_next(): Bool => 19 | _seq.size() > 0 20 | 21 | fun ref next(): T^ ? => 22 | _seq.pop()? 23 | 24 | -------------------------------------------------------------------------------- /ponycheck/property.pony: -------------------------------------------------------------------------------- 1 | use "time" 2 | 3 | class val PropertyParams is Stringable 4 | """ 5 | Parameters to control Property Execution 6 | 7 | * seed: the seed for the source of Randomness 8 | * num_samples: the number of samples to produce from the property generator 9 | * max_shrink_rounds: the maximum rounds of shrinking to perform 10 | * max_generator_retries: the maximum number of retries to do if a generator fails to generate a sample 11 | * timeout: the timeout for the ponytest runner, in nanoseconds 12 | * async: if true the property is expected to finish asynchronously by calling 13 | `PropertyHelper.complete(...)` 14 | """ 15 | let seed: U64 16 | let num_samples: USize 17 | let max_shrink_rounds: USize 18 | let max_generator_retries: USize 19 | let timeout: U64 20 | let async: Bool 21 | 22 | new val create( 23 | num_samples': USize = 100, 24 | seed': U64 = Time.millis(), 25 | max_shrink_rounds': USize = 10, 26 | max_generator_retries': USize = 5, 27 | timeout': U64 = 60_000_000_000, 28 | async': Bool = false) 29 | => 30 | num_samples = num_samples' 31 | seed = seed' 32 | max_shrink_rounds = max_shrink_rounds' 33 | max_generator_retries = max_generator_retries' 34 | timeout = timeout' 35 | async = async' 36 | 37 | fun string(): String iso^ => 38 | recover 39 | String() 40 | .>append("Params(seed=") 41 | .>append(seed.string()) 42 | .>append(")") 43 | end 44 | 45 | trait Property1[T] 46 | """ 47 | A property that consumes 1 argument of type `T`. 48 | 49 | A property is defined by a [Generator](ponycheck-Generator.md), returned by the [`gen()`](ponycheck-Property1.md#gen) method 50 | and a [`property`](ponycheck-Property1#property) method that consumes the generators output and 51 | verifies a custom property with the help of a [PropertyHelper](ponycheck-PropertyHelper.md). 52 | 53 | A property is verified if no failed assertion on [PropertyHelper](ponycheck-PropertyHelper.md) has been 54 | reported for all the samples it consumed. 55 | 56 | The property execution can be customized by returning a custom 57 | [PropertyParams](ponycheck-PropertyParams.md) from the [`params()`]*ponycheck-Property1.md#params) method. 58 | 59 | The [`gen()`](ponycheck-Property1.md#gen) method is called exactly once to instantiate the generator. 60 | The generator produces [PropertyParams.num_samples](ponycheck-PropertyParams.md#num_samples) samples and each is 61 | passed to the [property](ponycheck-Property1.md#property) method for verification. 62 | 63 | If the property did not verify, the given sample is shrunken, if the 64 | generator supports shrinking. 65 | The smallest shrunken sample will then be reported to the user. 66 | 67 | A [Property1](ponycheck-Property1.md) can be run with [Ponytest](ponytest--index.md). 68 | To that end it needs to be wrapped into a [Property1UnitTest](ponycheck-Property1UnitTest.md). 69 | """ 70 | fun name(): String 71 | """ 72 | The name of the property used for reporting during execution. 73 | """ 74 | 75 | fun params(): PropertyParams => 76 | """ 77 | Returns parameters to customize execution of this Property. 78 | """ 79 | PropertyParams 80 | 81 | fun gen(): Generator[T] 82 | """ 83 | The [Generator](ponycheck-Generator.md) used to produce samples to verify. 84 | """ 85 | 86 | fun ref property(arg1: T, h: PropertyHelper) ? 87 | """ 88 | A method verifying that a certain property holds for all given `arg1` 89 | with the help of [PropertyHelper](ponycheck-PropertyHelper.md) `h`. 90 | """ 91 | 92 | trait Property2[T1, T2] is Property1[(T1, T2)] 93 | 94 | fun gen1(): Generator[T1] 95 | """ 96 | The Generator for the first argument to your `property2`. 97 | """ 98 | 99 | fun gen2(): Generator[T2] 100 | """ 101 | The Generator for the second argument to your `property2`. 102 | """ 103 | 104 | fun gen(): Generator[(T1, T2)] => 105 | Generators.zip2[T1, T2]( 106 | gen1(), 107 | gen2()) 108 | 109 | fun ref property(arg1: (T1, T2), h: PropertyHelper) ? => 110 | (let x, let y) = consume arg1 111 | property2(consume x, consume y, h)? 112 | 113 | fun ref property2(arg1: T1, arg2: T2, h: PropertyHelper) ? 114 | """ 115 | A method verifying that a certain property holds for all given `arg1` and `arg2` 116 | with the help of [PropertyHelper](ponycheck-PropertyHelper.md) `h`. 117 | """ 118 | 119 | trait Property3[T1, T2, T3] is Property1[(T1, T2, T3)] 120 | 121 | fun gen1(): Generator[T1] 122 | """ 123 | The Generator for the first argument to your `property3` method. 124 | """ 125 | 126 | fun gen2(): Generator[T2] 127 | """ 128 | The Generator for the second argument to your `property3` method. 129 | """ 130 | 131 | fun gen3(): Generator[T3] 132 | """ 133 | The Generator for the third argument to your `property3` method. 134 | """ 135 | 136 | fun gen(): Generator[(T1, T2, T3)] => 137 | Generators.zip3[T1, T2, T3]( 138 | gen1(), 139 | gen2(), 140 | gen3()) 141 | 142 | fun ref property(arg1: (T1, T2, T3), h: PropertyHelper) ? => 143 | (let x, let y, let z) = consume arg1 144 | property3(consume x, consume y, consume z, h)? 145 | 146 | fun ref property3(arg1: T1, arg2: T2, arg3: T3, h: PropertyHelper) ? 147 | """ 148 | A method verifying that a certain property holds for all given `arg1`, `arg2` and `arg3` 149 | with the help of [PropertyHelper](ponycheck-PropertyHelper.md) `h`. 150 | """ 151 | 152 | trait Property4[T1, T2, T3, T4] is Property1[(T1, T2, T3, T4)] 153 | 154 | fun gen1(): Generator[T1] 155 | """ 156 | The Generator for the first argument to your `property4` method. 157 | """ 158 | 159 | fun gen2(): Generator[T2] 160 | """ 161 | The Generator for the second argument to your `property4` method. 162 | """ 163 | 164 | fun gen3(): Generator[T3] 165 | """ 166 | The Generator for the third argument to your `property4` method. 167 | """ 168 | 169 | fun gen4(): Generator[T4] 170 | """ 171 | The Generator for the fourth argument to your `property4` method. 172 | """ 173 | 174 | fun gen(): Generator[(T1, T2, T3, T4)] => 175 | Generators.zip4[T1, T2, T3, T4]( 176 | gen1(), 177 | gen2(), 178 | gen3(), 179 | gen4()) 180 | 181 | fun ref property(arg1: (T1, T2, T3, T4), h: PropertyHelper) ? => 182 | (let x1, let x2, let x3, let x4) = consume arg1 183 | property4(consume x1, consume x2, consume x3, consume x4, h)? 184 | 185 | fun ref property4(arg1: T1, arg2: T2, arg3: T3, arg4: T4, h: PropertyHelper) ? 186 | """ 187 | A method verifying that a certain property holds for all given `arg1`, `arg2`, `arg3`, `arg4` 188 | with the help of [PropertyHelper](ponycheck-PropertyHelper.md) `h`. 189 | """ 190 | 191 | -------------------------------------------------------------------------------- /ponycheck/property_helper.pony: -------------------------------------------------------------------------------- 1 | 2 | interface val _PropertyRunNotify 3 | """ 4 | simple callback for notifying the runner 5 | that a run completed 6 | """ 7 | fun apply(success: Bool) 8 | 9 | interface tag _IPropertyRunner 10 | """ 11 | interface for a PropertyRunner without the generic type parameter 12 | 13 | and only with the behaviours we are interested in. 14 | """ 15 | 16 | be expect_action(name: String) 17 | 18 | be complete_action(name: String, ph: PropertyHelper) 19 | 20 | be fail_action(name: String, ph: PropertyHelper) 21 | 22 | be dispose_when_done(disposable: DisposableActor) 23 | 24 | be log(msg: String, verbose: Bool = false) 25 | 26 | 27 | class val PropertyHelper 28 | """ 29 | Helper for ponycheck properties. 30 | 31 | Mirrors the [TestHelper](ponytest-TestHelper.md) API as close as possible. 32 | 33 | Contains assertion functions and functions for completing asynchronous 34 | properties, for expecting and completing or failing actions. 35 | 36 | Internally a new PropertyHelper will be created for each call to 37 | a property with a new sample and also for every shrink run. 38 | So don't assume anything about the identity of the PropertyHelper inside of 39 | your Properties. 40 | 41 | This class is `val` by default so it can be safely passed around to other 42 | actors. 43 | 44 | It exposes the process [Env](builtin-Env.md) as public `env` field in order to 45 | give access to the root authority and other stuff. 46 | """ 47 | let _runner: _IPropertyRunner 48 | let _run_notify: _PropertyRunNotify 49 | let _run_context: String 50 | 51 | let env: Env 52 | 53 | new val create(env': Env, 54 | runner: _IPropertyRunner, 55 | run_notify: _PropertyRunNotify, 56 | run_context: String) => 57 | env = env' 58 | _runner = runner 59 | _run_notify = run_notify 60 | _run_context = run_context 61 | 62 | /****** START DUPLICATION FROM TESTHELPER ********/ 63 | 64 | fun log(msg: String, verbose: Bool = false) => 65 | """ 66 | Log the given message. 67 | 68 | The verbose parameter allows messages to be printed only when the --verbose 69 | command line option is used. For example, by default assert failures are 70 | logged, but passes are not. With --verbose both passes and fails are 71 | reported. 72 | 73 | Logs are printed one test at a time to avoid interleaving log lines from 74 | concurrent tests. 75 | """ 76 | _runner.log(msg, verbose) 77 | 78 | fun fail(msg: String = "Test failed") => 79 | """ 80 | Flag the test as having failed. 81 | """ 82 | _fail(msg) 83 | 84 | fun assert_false( 85 | predicate: Bool, 86 | msg: String val = "", 87 | loc: SourceLoc val = __loc) 88 | : Bool val 89 | => 90 | """ 91 | Assert that the given expression is false. 92 | """ 93 | if predicate then 94 | _fail(_fmt_msg(loc, "Assert false failed. " + msg)) 95 | return false 96 | end 97 | _runner.log(_fmt_msg(loc, "Assert false passed. " + msg)) 98 | true 99 | 100 | fun assert_true( 101 | predicate: Bool, 102 | msg: String val = "", 103 | loc: SourceLoc val = __loc) 104 | : Bool val 105 | => 106 | """ 107 | Assert that the given expression is true. 108 | """ 109 | if not predicate then 110 | _fail(_fmt_msg(loc, "Assert true failed. " + msg)) 111 | return false 112 | end 113 | _runner.log(_fmt_msg(loc, "Assert true passed. " + msg)) 114 | true 115 | 116 | fun assert_error( 117 | test: {(): None ?} box, 118 | msg: String = "", 119 | loc: SourceLoc = __loc) 120 | : Bool 121 | => 122 | """ 123 | Assert that the given test function throws an error when run. 124 | """ 125 | try 126 | test()? 127 | _fail(_fmt_msg(loc, "Assert error failed. " + msg)) 128 | false 129 | else 130 | _runner.log(_fmt_msg(loc, "Assert error passed. " + msg), true) 131 | true 132 | end 133 | 134 | fun assert_no_error( 135 | test: {(): None ?} box, 136 | msg: String = "", 137 | loc: SourceLoc = __loc) 138 | : Bool 139 | => 140 | """ 141 | Assert that the given test function does not throw an error when run. 142 | """ 143 | try 144 | test()? 145 | _runner.log(_fmt_msg(loc, "Assert no error passed. " + msg), true) 146 | true 147 | else 148 | _fail(_fmt_msg(loc, "Assert no error failed. " + msg)) 149 | false 150 | end 151 | 152 | fun assert_is[A]( 153 | expect: A, 154 | actual: A, 155 | msg: String = "", 156 | loc: SourceLoc = __loc) 157 | : Bool 158 | => 159 | """ 160 | Assert that the 2 given expressions resolve to the same instance 161 | """ 162 | if expect isnt actual then 163 | _fail(_fmt_msg(loc, "Assert is failed. " + msg 164 | + " Expected (" + (digestof expect).string() + ") is (" 165 | + (digestof actual).string() + ")")) 166 | return false 167 | end 168 | 169 | _runner.log( 170 | _fmt_msg(loc, "Assert is passed. " + msg 171 | + " Got (" + (digestof expect).string() + ") is (" 172 | + (digestof actual).string() + ")"), 173 | true) 174 | true 175 | 176 | fun assert_isnt[A]( 177 | not_expect: A, 178 | actual: A, 179 | msg: String = "", 180 | loc: SourceLoc = __loc) 181 | : Bool 182 | => 183 | """ 184 | Assert that the 2 given expressions resolve to different instances. 185 | """ 186 | if not_expect is actual then 187 | _fail(_fmt_msg(loc, "Assert isn't failed. " + msg 188 | + " Expected (" + (digestof not_expect).string() + ") isnt (" 189 | + (digestof actual).string() + ")")) 190 | return false 191 | end 192 | 193 | _runner.log( 194 | _fmt_msg(loc, "Assert isn't passed. " + msg 195 | + " Got (" + (digestof not_expect).string() + ") isnt (" 196 | + (digestof actual).string() + ")"), 197 | true) 198 | true 199 | 200 | fun assert_eq[A: (Equatable[A] #read & Stringable #read)]( 201 | expect: A, 202 | actual: A, 203 | msg: String = "", 204 | loc: SourceLoc = __loc) 205 | : Bool 206 | => 207 | """ 208 | Assert that the 2 given expressions are equal. 209 | """ 210 | if expect != actual then 211 | _fail(_fmt_msg(loc, "Assert eq failed. " + msg 212 | + " Expected (" + expect.string() + ") == (" + actual.string() + ")")) 213 | return false 214 | end 215 | 216 | _runner.log(_fmt_msg(loc, "Assert eq passed. " + msg 217 | + " Got (" + expect.string() + ") == (" + actual.string() + ")"), 218 | true) 219 | true 220 | 221 | fun assert_ne[A: (Equatable[A] #read & Stringable #read)]( 222 | not_expect: A, 223 | actual: A, 224 | msg: String = "", 225 | loc: SourceLoc = __loc) 226 | : Bool 227 | => 228 | """ 229 | Assert that the 2 given expressions are not equal. 230 | """ 231 | if not_expect == actual then 232 | _fail(_fmt_msg(loc, "Assert ne failed. " + msg 233 | + " Expected (" + not_expect.string() + ") != (" + actual.string() 234 | + ")")) 235 | return false 236 | end 237 | 238 | _runner.log( 239 | _fmt_msg(loc, "Assert ne passed. " + msg 240 | + " Got (" + not_expect.string() + ") != (" + actual.string() + ")"), 241 | true) 242 | true 243 | 244 | fun assert_array_eq[A: (Equatable[A] #read & Stringable #read)]( 245 | expect: ReadSeq[A], 246 | actual: ReadSeq[A], 247 | msg: String = "", 248 | loc: SourceLoc = __loc) 249 | : Bool 250 | => 251 | """ 252 | Assert that the contents of the 2 given ReadSeqs are equal. 253 | """ 254 | var ok = true 255 | 256 | if expect.size() != actual.size() then 257 | ok = false 258 | else 259 | try 260 | var i: USize = 0 261 | while i < expect.size() do 262 | if expect(i)? != actual(i)? then 263 | ok = false 264 | break 265 | end 266 | 267 | i = i + 1 268 | end 269 | else 270 | ok = false 271 | end 272 | end 273 | 274 | if not ok then 275 | _fail(_fmt_msg(loc, "Assert EQ failed. " + msg + " Expected (" 276 | + _print_array[A](expect) + ") == (" + _print_array[A](actual) + ")")) 277 | return false 278 | end 279 | 280 | _runner.log( 281 | _fmt_msg(loc, "Assert EQ passed. " + msg + " Got (" 282 | + _print_array[A](expect) + ") == (" + _print_array[A](actual) + ")"), 283 | true) 284 | true 285 | 286 | fun assert_array_eq_unordered[A: (Equatable[A] #read & Stringable #read)]( 287 | expect: ReadSeq[A], 288 | actual: ReadSeq[A], 289 | msg: String = "", 290 | loc: SourceLoc = __loc) 291 | : Bool 292 | => 293 | """ 294 | Assert that the contents of the 2 given ReadSeqs are equal ignoring order. 295 | """ 296 | try 297 | let missing = Array[box->A] 298 | let consumed = Array[Bool].init(false, actual.size()) 299 | for e in expect.values() do 300 | var found = false 301 | var i: USize = -1 302 | for a in actual.values() do 303 | i = i + 1 304 | if consumed(i)? then continue end 305 | if e == a then 306 | consumed.update(i, true)? 307 | found = true 308 | break 309 | end 310 | end 311 | if not found then 312 | missing.push(e) 313 | end 314 | end 315 | 316 | let extra = Array[box->A] 317 | for (i, c) in consumed.pairs() do 318 | if not c then extra.push(actual(i)?) end 319 | end 320 | 321 | if (extra.size() != 0) or (missing.size() != 0) then 322 | _fail( 323 | _fmt_msg(loc, "Assert EQ_UNORDERED failed. " + msg 324 | + " Expected (" + _print_array[A](expect) + ") == (" 325 | + _print_array[A](actual) + "):" 326 | + "\nMissing: " + _print_array[box->A](missing) 327 | + "\nExtra: " + _print_array[box->A](extra) 328 | ) 329 | ) 330 | return false 331 | end 332 | _runner.log( 333 | _fmt_msg( 334 | loc, 335 | "Assert EQ_UNORDERED passed. " 336 | + msg 337 | + " Got (" 338 | + _print_array[A](expect) 339 | + ") == (" 340 | + _print_array[A](actual) 341 | + ")" 342 | ), 343 | true 344 | ) 345 | true 346 | else 347 | _fail("Assert EQ_UNORDERED failed from an internal error.") 348 | false 349 | end 350 | 351 | fun _print_array[A: Stringable #read](array: ReadSeq[A]): String => 352 | """ 353 | Generate a printable string of the contents of the given readseq to use in 354 | error messages. 355 | """ 356 | "[len=" + array.size().string() + ": " + ", ".join(array.values()) + "]" 357 | 358 | 359 | /****** END DUPLICATION FROM TESTHELPER *********/ 360 | 361 | fun expect_action(name: String) => 362 | """ 363 | expect some action of the given name to complete 364 | for the property to hold. 365 | 366 | If all expected actions are completed successfully, 367 | the property is considered successful. 368 | 369 | If 1 action fails, the property is considered failing. 370 | 371 | Call `complete_action(name)` or `fail_action(name)` 372 | to mark some action as completed. 373 | 374 | Example: 375 | 376 | ```pony 377 | actor AsyncActor 378 | 379 | let _ph: PropertyHelper 380 | 381 | new create(ph: PropertyHelper) => 382 | _ph = ph 383 | 384 | be complete(s: String) => 385 | if (s.size() % 2) == 0 then 386 | _ph.complete_action("is_even") 387 | else 388 | _ph.fail_action("is_even") 389 | 390 | class EvenStringProperty is Property1[String] 391 | fun name(): String => "even_string" 392 | 393 | fun gen(): Generator[String] => 394 | Generators.ascii() 395 | 396 | fun property(arg1: String, ph: PropertyHelper) => 397 | ph.expect_action("is_even") 398 | AsyncActor(ph).check(arg1) 399 | ``` 400 | 401 | """ 402 | _runner.expect_action(name) 403 | 404 | fun val complete_action(name: String) => 405 | """ 406 | Complete an expected action successfully. 407 | 408 | If all expected actions are completed successfully, 409 | the property is considered successful. 410 | 411 | If 1 action fails, the property is considered failing. 412 | 413 | If the action `name` was not expected, i.e. was not registered using 414 | `expect_action`, nothing happens. 415 | """ 416 | _runner.complete_action(name, this) 417 | 418 | fun val fail_action(name: String) => 419 | """ 420 | Mark an expected action as failed. 421 | 422 | If all expected actions are completed successfully, 423 | the property is considered successful. 424 | 425 | If 1 action fails, the property is considered failing. 426 | """ 427 | _runner.fail_action(name, this) 428 | 429 | fun complete(success: Bool) => 430 | """ 431 | Complete an asynchronous property successfully. 432 | 433 | Once this method is called the property 434 | is considered successful or failing 435 | depending on the value of the parameter `success`. 436 | 437 | For more fine grained control over completing or failing 438 | a property that consists of many steps, consider using 439 | `expect_action`, `complete_action` and `fail_action`. 440 | """ 441 | _run_notify.apply(success) 442 | 443 | fun dispose_when_done(disposable: DisposableActor) => 444 | """ 445 | Dispose the actor after a property run / a shrink is done. 446 | """ 447 | _runner.dispose_when_done(disposable) 448 | 449 | fun _fail(msg: String) => 450 | _runner.log(msg) 451 | _run_notify.apply(false) 452 | 453 | fun _fmt_msg(loc: SourceLoc, msg: String): String => 454 | let msg_prefix = _run_context + " " + _format_loc(loc) 455 | if msg.size() > 0 then 456 | msg_prefix + ": " + msg 457 | else 458 | msg_prefix 459 | end 460 | 461 | fun _format_loc(loc: SourceLoc): String => 462 | loc.file() + ":" + loc.line().string() 463 | 464 | 465 | -------------------------------------------------------------------------------- /ponycheck/property_runner.pony: -------------------------------------------------------------------------------- 1 | use "debug" 2 | use "collections" 3 | 4 | interface val PropertyLogger 5 | fun log(msg: String, verbose: Bool = false) 6 | 7 | interface val PropertyResultNotify 8 | fun fail(msg: String) 9 | """ 10 | called when a Property has failed (did not hold for a sample) 11 | or when execution errored. 12 | 13 | Does not necessarily denote completeness of the property execution, 14 | see `complete(success: Bool)` for that purpose. 15 | """ 16 | 17 | fun complete(success: Bool) 18 | """ 19 | called when the Property execution is complete 20 | signalling whether it was successful or not. 21 | """ 22 | 23 | actor PropertyRunner[T] 24 | """ 25 | Actor executing a Property1 implementation 26 | in a way that allows garbage collection between single 27 | property executions, because it uses recursive behaviours 28 | for looping. 29 | """ 30 | let _prop1: Property1[T] 31 | let _params: PropertyParams 32 | let _rnd: Randomness 33 | let _notify: PropertyResultNotify 34 | let _gen: Generator[T] 35 | let _logger: PropertyLogger 36 | let _env: Env 37 | 38 | let _expected_actions: Set[String] = Set[String] 39 | let _disposables: Array[DisposableActor] = Array[DisposableActor] 40 | var _shrinker: Iterator[T^] = _EmptyIterator[T^] 41 | var _sample_repr: String = "" 42 | var _pass: Bool = true 43 | 44 | // keep track of which runs/shrinks we expect 45 | var _expected_round: USize = 0 46 | 47 | new create( 48 | p1: Property1[T] iso, 49 | params: PropertyParams, 50 | notify: PropertyResultNotify, 51 | logger: PropertyLogger, 52 | env: Env 53 | ) => 54 | _env = env 55 | _prop1 = consume p1 56 | _params = params 57 | _logger = logger 58 | _notify = notify 59 | _rnd = Randomness(_params.seed) 60 | _gen = _prop1.gen() 61 | 62 | 63 | // RUNNING PROPERTIES // 64 | 65 | be complete_run(round: USize, success: Bool) => 66 | """ 67 | complete a property run 68 | 69 | this behaviour is called from the PropertyHelper 70 | or from `_finished`. 71 | """ 72 | 73 | // verify that this is an expected call 74 | if _expected_round != round then 75 | _logger.log("unexpected complete msg for run " + round.string() + 76 | ". expecting run " + _expected_round.string(), true) 77 | return 78 | else 79 | _expected_round = round + 1 80 | end 81 | 82 | _pass = success // in case of sync property - signal failure 83 | 84 | if not success then 85 | // found a bad example, try to shrink it 86 | if not _shrinker.has_next() then 87 | _logger.log("no shrinks available") 88 | fail(_sample_repr, 0) 89 | else 90 | _expected_round = 0 // reset rounds for shrinking 91 | do_shrink(_sample_repr) 92 | end 93 | else 94 | // property holds, recurse 95 | run(round + 1) 96 | end 97 | 98 | fun ref _generate_with_retry(max_retries: USize): ValueAndShrink[T] ? => 99 | var tries: USize = 0 100 | repeat 101 | try 102 | return _gen.generate_and_shrink(_rnd)? 103 | else 104 | tries = tries + 1 105 | end 106 | until (tries > max_retries) end 107 | 108 | error 109 | 110 | be run(round: USize = 0) => 111 | if round >= _params.num_samples then 112 | complete() // all samples have been successful 113 | return 114 | end 115 | 116 | // prepare property run 117 | (var sample, _shrinker) = 118 | try 119 | _generate_with_retry(_params.max_generator_retries)? 120 | else 121 | // break out if we were not able to generate a sample 122 | _notify.fail( 123 | "Unable to generate samples from the given iterator, tried " + 124 | _params.max_generator_retries.string() + " times." + 125 | " (round: " + round.string() + ")") 126 | _notify.complete(false) 127 | return 128 | end 129 | 130 | 131 | // create a string representation before consuming ``sample`` with property 132 | (sample, _sample_repr) = _Stringify.apply[T](consume sample) 133 | let run_notify = recover val this~complete_run(round) end 134 | let helper = PropertyHelper(_env, this, run_notify, _params.string() + " Run(" + 135 | round.string() + ")") 136 | _pass = true // will be set to false by fail calls 137 | 138 | try 139 | _prop1.property(consume sample, helper)? 140 | else 141 | fail(_sample_repr, 0 where err=true) 142 | return 143 | end 144 | // dispatch to another behavior 145 | // as complete_run might have set _pass already through a call to 146 | // complete_run 147 | _run_finished(round) 148 | 149 | be _run_finished(round: USize) => 150 | if not _params.async and _pass then 151 | // otherwise complete_run has already been called 152 | complete_run(round, true) 153 | end 154 | 155 | // SHRINKING // 156 | 157 | be complete_shrink(failed_repr: String, last_repr: String, shrink_round: USize, success: Bool) => 158 | 159 | // verify that this is an expected call 160 | if _expected_round != shrink_round then 161 | _logger.log("unexpected complete msg for shrink run " + shrink_round.string() + 162 | ". expecting run " + _expected_round.string(), true) 163 | return 164 | else 165 | _expected_round = shrink_round + 1 166 | end 167 | 168 | _pass = success // in case of sync property - signal failure 169 | 170 | if success then 171 | // we have a sample that did not fail and thus can stop shrinking 172 | fail(failed_repr, shrink_round) 173 | 174 | else 175 | // we have a failing shrink sample, recurse 176 | do_shrink(last_repr, shrink_round + 1) 177 | end 178 | 179 | be do_shrink(failed_repr: String, shrink_round: USize = 0) => 180 | 181 | // shrink iters can be infinite, so we need to limit 182 | // the examples we consider during shrinking 183 | if shrink_round == _params.max_shrink_rounds then 184 | fail(failed_repr, shrink_round) 185 | return 186 | end 187 | 188 | (let shrink, let current_repr) = 189 | try 190 | _Stringify.apply[T](_shrinker.next()?) 191 | else 192 | // no more shrink samples, report previous failed example 193 | fail(failed_repr, shrink_round) 194 | return 195 | end 196 | // callback for asynchronous shrinking or aborting on error case 197 | let run_notify = 198 | recover val 199 | this~complete_shrink(failed_repr, current_repr, shrink_round) 200 | end 201 | let helper = PropertyHelper( 202 | _env, 203 | this, 204 | run_notify, 205 | _params.string() + " Shrink(" + shrink_round.string() + ")") 206 | _pass = true // will be set to false by fail calls 207 | 208 | try 209 | _prop1.property(consume shrink, helper)? 210 | else 211 | fail(current_repr, shrink_round where err=true) 212 | return 213 | end 214 | // dispatch to another behaviour 215 | // to ensure _complete_shrink has been called already 216 | _shrink_finished(failed_repr, current_repr, shrink_round) 217 | 218 | be _shrink_finished( 219 | failed_repr: String, 220 | current_repr: String, 221 | shrink_round: USize) 222 | => 223 | if not _params.async and _pass then 224 | // directly complete the shrink run 225 | complete_shrink(failed_repr, current_repr, shrink_round, true) 226 | end 227 | 228 | // interface towards PropertyHelper 229 | 230 | be expect_action(name: String) => 231 | _logger.log("Action expected: " + name) 232 | _expected_actions.set(name) 233 | 234 | be complete_action(name: String, ph: PropertyHelper) => 235 | _logger.log("Action completed: " + name) 236 | _finish_action(name, true, ph) 237 | 238 | be fail_action(name: String, ph: PropertyHelper) => 239 | _logger.log("Action failed: " + name) 240 | _finish_action(name, false, ph) 241 | 242 | fun ref _finish_action(name: String, success: Bool, ph: PropertyHelper) => 243 | try 244 | _expected_actions.extract(name)? 245 | 246 | // call back into the helper to invoke the current run_notify 247 | // that we don't have access to otherwise 248 | if not success then 249 | ph.complete(false) 250 | elseif _expected_actions.size() == 0 then 251 | ph.complete(true) 252 | end 253 | else 254 | _logger.log("action '" + name + "' finished unexpectedly. ignoring.") 255 | end 256 | 257 | be dispose_when_done(disposable: DisposableActor) => 258 | _disposables.push(disposable) 259 | 260 | be dispose() => 261 | _dispose() 262 | 263 | fun ref _dispose() => 264 | for disposable in Poperator[DisposableActor](_disposables) do 265 | disposable.dispose() 266 | end 267 | 268 | be log(msg: String, verbose: Bool = false) => 269 | _logger.log(msg, verbose) 270 | 271 | // end interface towards PropertyHelper 272 | 273 | fun ref complete() => 274 | """ 275 | complete the Property execution successfully 276 | """ 277 | _notify.complete(true) 278 | 279 | fun ref fail(repr: String, rounds: USize = 0, err: Bool = false) => 280 | """ 281 | complete the Property execution 282 | while signalling failure to the notify 283 | """ 284 | if err then 285 | _report_error(repr, rounds) 286 | else 287 | _report_failed(repr, rounds) 288 | end 289 | _notify.complete(false) 290 | 291 | fun _report_error(sample_repr: String, 292 | shrink_rounds: USize = 0, 293 | loc: SourceLoc = __loc) => 294 | """ 295 | report an error that happened during property evaluation 296 | and signal failure to the notify 297 | """ 298 | _notify.fail( 299 | "Property errored for sample " 300 | + sample_repr 301 | + " (after " 302 | + shrink_rounds.string() 303 | + " shrinks)" 304 | ) 305 | 306 | fun _report_failed(sample_repr: String, 307 | shrink_rounds: USize = 0, 308 | loc: SourceLoc = __loc) => 309 | """ 310 | report a failed property and signal failure to the notify 311 | """ 312 | _notify.fail( 313 | "Property failed for sample " 314 | + sample_repr 315 | + " (after " 316 | + shrink_rounds.string() 317 | + " shrinks)" 318 | ) 319 | 320 | 321 | class _EmptyIterator[T] 322 | fun ref has_next(): Bool => false 323 | fun ref next(): T^ ? => error 324 | 325 | primitive _Stringify 326 | fun apply[T](t: T): (T^, String) => 327 | """turn anything into a string""" 328 | let digest = (digestof t) 329 | let s = 330 | match t 331 | | let str: Stringable => 332 | str.string() 333 | | let rs: ReadSeq[Stringable] => 334 | "[" + " ".join(rs.values()) + "]" 335 | | (let s1: Stringable, let s2: Stringable) => 336 | "(" + s1.string() + ", " + s2.string() + ")" 337 | | (let s1: Stringable, let s2: ReadSeq[Stringable]) => 338 | "(" + s1.string() + ", [" + " ".join(s2.values()) + "])" 339 | | (let s1: ReadSeq[Stringable], let s2: Stringable) => 340 | "([" + " ".join(s1.values()) + "], " + s2.string() + ")" 341 | | (let s1: ReadSeq[Stringable], let s2: ReadSeq[Stringable]) => 342 | "([" + " ".join(s1.values()) + "], [" + " ".join(s2.values()) + "])" 343 | | (let s1: Stringable, let s2: Stringable, let s3: Stringable) => 344 | "(" + s1.string() + ", " + s2.string() + ", " + s3.string() + ")" 345 | | ((let s1: Stringable, let s2: Stringable), let s3: Stringable) => 346 | "((" + s1.string() + ", " + s2.string() + "), " + s3.string() + ")" 347 | | (let s1: Stringable, (let s2: Stringable, let s3: Stringable)) => 348 | "(" + s1.string() + ", (" + s2.string() + ", " + s3.string() + "))" 349 | else 350 | "" 351 | end 352 | (consume t, consume s) 353 | 354 | -------------------------------------------------------------------------------- /ponycheck/property_unit_test.pony: -------------------------------------------------------------------------------- 1 | use "ponytest" 2 | 3 | class iso Property1UnitTest[T] is UnitTest 4 | """ 5 | provides plumbing for integration of ponycheck 6 | [Properties](ponycheck-Property1.md) into [ponytest](ponytest--index.md). 7 | 8 | Wrap your properties into this class and use it in a 9 | [TestList](ponytest-TestList.md): 10 | 11 | ```pony 12 | use "ponytest" 13 | use "ponycheck" 14 | 15 | class MyProperty is Property1[String] 16 | fun name(): String => "my_property" 17 | 18 | fun gen(): Generator[String] => 19 | Generatos.ascii_printable() 20 | 21 | fun property(arg1: String, h: PropertyHelper) => 22 | h.assert_true(arg1.size() > 0) 23 | 24 | actor Main is TestList 25 | new create(env: Env) => PonyTest(env, this) 26 | 27 | fun tag tests(test: PonyTest) => 28 | test(Property1UnitTest[String](MyProperty)) 29 | 30 | ``` 31 | """ 32 | 33 | var _prop1: ( Property1[T] iso | None ) 34 | let _name: String 35 | 36 | new iso create(p1: Property1[T] iso, name': (String | None) = None) => 37 | """ 38 | Wrap a [Property1](ponycheck-Property1.md) to make it mimick the a ponytest 39 | [UnitTest](ponytest-UnitTest.md). 40 | 41 | If `name'` is given, use this as the test name, if not use the properties `name()`. 42 | """ 43 | _name = 44 | match name' 45 | | None => p1.name() 46 | | let s: String => s 47 | end 48 | _prop1 = consume p1 49 | 50 | 51 | fun name(): String => _name 52 | 53 | fun ref apply(h: TestHelper) ? => 54 | let prop = ((_prop1 = None) as Property1[T] iso^) 55 | let params = prop.params() 56 | h.long_test(params.timeout) 57 | let property_runner = 58 | PropertyRunner[T]( 59 | consume prop, 60 | params, 61 | h, // treat it as PropertyResultNotify 62 | h, // is also a PropertyLogger for us 63 | h.env 64 | ) 65 | h.dispose_when_done(property_runner) 66 | property_runner.run() 67 | 68 | class iso Property2UnitTest[T1, T2] is UnitTest 69 | 70 | var _prop2: ( Property2[T1, T2] iso | None ) 71 | let _name: String 72 | 73 | new iso create(p2: Property2[T1, T2] iso, name': (String | None) = None) => 74 | _name = 75 | match name' 76 | | None => p2.name() 77 | | let s: String => s 78 | end 79 | _prop2 = consume p2 80 | 81 | fun name(): String => _name 82 | 83 | fun ref apply(h: TestHelper) ? => 84 | let prop = ((_prop2 = None) as Property2[T1, T2] iso^) 85 | let params = prop.params() 86 | h.long_test(params.timeout) 87 | let property_runner = 88 | PropertyRunner[(T1, T2)]( 89 | consume prop, 90 | params, 91 | h, // PropertyResultNotify 92 | h, // PropertyLogger 93 | h.env 94 | ) 95 | h.dispose_when_done(property_runner) 96 | property_runner.run() 97 | 98 | class iso Property3UnitTest[T1, T2, T3] is UnitTest 99 | 100 | var _prop3: ( Property3[T1, T2, T3] iso | None ) 101 | let _name: String 102 | 103 | new iso create(p3: Property3[T1, T2, T3] iso, name': (String | None) = None) => 104 | _name = 105 | match name' 106 | | None => p3.name() 107 | | let s: String => s 108 | end 109 | _prop3 = consume p3 110 | 111 | fun name(): String => _name 112 | 113 | fun ref apply(h: TestHelper) ? => 114 | let prop = ((_prop3 = None) as Property3[T1, T2, T3] iso^) 115 | let params = prop.params() 116 | h.long_test(params.timeout) 117 | let property_runner = 118 | PropertyRunner[(T1, T2, T3)]( 119 | consume prop, 120 | params, 121 | h, // PropertyResultNotify 122 | h, // PropertyLogger 123 | h.env 124 | ) 125 | h.dispose_when_done(property_runner) 126 | property_runner.run() 127 | 128 | class iso Property4UnitTest[T1, T2, T3, T4] is UnitTest 129 | 130 | var _prop4: ( Property4[T1, T2, T3, T4] iso | None ) 131 | let _name: String 132 | 133 | new iso create(p4: Property4[T1, T2, T3, T4] iso, name': (String | None) = None) => 134 | _name = 135 | match name' 136 | | None => p4.name() 137 | | let s: String => s 138 | end 139 | _prop4 = consume p4 140 | 141 | fun name(): String => _name 142 | 143 | fun ref apply(h: TestHelper) ? => 144 | let prop = ((_prop4 = None) as Property4[T1, T2, T3, T4] iso^) 145 | let params = prop.params() 146 | h.long_test(params.timeout) 147 | let property_runner = 148 | PropertyRunner[(T1, T2, T3, T4)]( 149 | consume prop, 150 | params, 151 | h, // PropertyResultNotify 152 | h, // PropertyLogger 153 | h.env 154 | ) 155 | h.dispose_when_done(property_runner) 156 | property_runner.run() 157 | 158 | -------------------------------------------------------------------------------- /ponycheck/randomness.pony: -------------------------------------------------------------------------------- 1 | use "random" 2 | 3 | class ref Randomness 4 | """ 5 | Source of randomness, providing methods for generatic uniformly distributed 6 | values from a given closed interval: [min, max] 7 | in order for the user to be able to generate every possible value for a given 8 | primitive numeric type. 9 | 10 | All primitive number method create numbers in range [min, max) 11 | """ 12 | let _random: Random 13 | 14 | new ref create(seed1: U64 = 42, seed2: U64 = 0) => 15 | _random = Rand(seed1, seed2) 16 | 17 | fun ref u8(min: U8 = U8.min_value(), max: U8 = U8.max_value()): U8 => 18 | """ 19 | generates a U8 in closed interval [min, max] 20 | (default: [min_value, max_value]) 21 | 22 | behavior is undefined if min > max. 23 | """ 24 | if (min == U8.min_value()) and (max == U8.max_value()) then 25 | _random.u8() 26 | else 27 | min + _random.int((max - min).u64() + 1).u8() 28 | end 29 | 30 | fun ref u16(min: U16 = U16.min_value(), max: U16 = U16.max_value()): U16 => 31 | """ 32 | generates a U16 in closed interval [min, max] 33 | (default: [min_value, max_value]) 34 | 35 | behavior is undefined if min > max. 36 | """ 37 | if (min == U16.min_value()) and (max == U16.max_value()) then 38 | _random.u16() 39 | else 40 | min + _random.int((max - min).u64() + 1).u16() 41 | end 42 | 43 | fun ref u32(min: U32 = U32.min_value(), max: U32 = U32.max_value()): U32 => 44 | """ 45 | generates a U32 in closed interval [min, max] 46 | (default: [min_value, max_value]) 47 | 48 | behavior is undefined if min > max. 49 | """ 50 | if (min == U32.min_value()) and (max == U32.max_value()) then 51 | _random.u32() 52 | else 53 | min + _random.int((max - min).u64() + 1).u32() 54 | end 55 | 56 | fun ref u64(min: U64 = U64.min_value(), max: U64 = U64.max_value()): U64 => 57 | """ 58 | generates a U64 in closed interval [min, max] 59 | (default: [min_value, max_value]) 60 | 61 | behavior is undefined if min > max. 62 | """ 63 | if (min == U64.min_value()) and (max == U64.max_value()) then 64 | _random.u64() 65 | elseif min > U32.max_value().u64() then 66 | (u32((min >> 32).u32(), (max >> 32).u32()).u64() << 32) or _random.u32().u64() 67 | elseif max > U32.max_value().u64() then 68 | let high = (u32((min >> 32).u32(), (max >> 32).u32()).u64() << 32).u64() 69 | let low = 70 | if high > 0 then 71 | _random.u32().u64() 72 | else 73 | u32(min.u32(), U32.max_value()).u64() 74 | end 75 | high or low 76 | else 77 | // range within U32 range 78 | u32(min.u32(), max.u32()).u64() 79 | end 80 | 81 | fun ref u128( 82 | min: U128 = U128.min_value(), 83 | max: U128 = U128.max_value()) 84 | : U128 85 | => 86 | """ 87 | generates a U128 in closed interval [min, max] 88 | (default: [min_value, max_value]) 89 | 90 | behavior is undefined if min > max. 91 | """ 92 | if (min == U128.min_value()) and (max == U128.max_value()) then 93 | _random.u128() 94 | elseif min > U64.max_value().u128() then 95 | // both above U64 range - chose random low 64 bits 96 | (u64((min >> 64).u64(), (max >> 64).u64()).u128() << 64) or u64().u128() 97 | elseif max > U64.max_value().u128() then 98 | // min below U64 max value 99 | let high = (u64((min >> 64).u64(), (max >> 64).u64()).u128() << 64) 100 | let low = 101 | if high > 0 then 102 | // number will be bigger than U64 max anyway, so chose a random lower u64 103 | u64().u128() 104 | else 105 | // number <= U64 max, so chose lower u64 while considering requested range min 106 | u64(min.u64(), U64.max_value()).u128() 107 | end 108 | high or low 109 | else 110 | // range within u64 range 111 | u64(min.u64(), max.u64()).u128() 112 | end 113 | 114 | fun ref ulong( 115 | min: ULong = ULong.min_value(), 116 | max: ULong = ULong.max_value()) 117 | : ULong 118 | => 119 | """ 120 | generates a ULong in closed interval [min, max] 121 | (default: [min_value, max_value]) 122 | 123 | behavior is undefined if min > max. 124 | """ 125 | u64(min.u64(), max.u64()).ulong() 126 | 127 | fun ref usize( 128 | min: USize = USize.min_value(), 129 | max: USize = USize.max_value()) 130 | : USize 131 | => 132 | """ 133 | generates a USize in closed interval [min, max] 134 | (default: [min_value, max_value]) 135 | 136 | behavior is undefined if min > max. 137 | """ 138 | u64(min.u64(), max.u64()).usize() 139 | 140 | fun ref i8(min: I8 = I8.min_value(), max: I8 = I8.max_value()): I8 => 141 | """ 142 | generates a I8 in closed interval [min, max] 143 | (default: [min_value, max_value]) 144 | 145 | behavior is undefined if min > max. 146 | """ 147 | min + u8(0, (max - min).u8()).i8() 148 | 149 | fun ref i16(min: I16 = I16.min_value(), max: I16 = I16.max_value()): I16 => 150 | """ 151 | generates a I16 in closed interval [min, max] 152 | (default: [min_value, max_value]) 153 | 154 | behavior is undefined if min > max. 155 | """ 156 | min + u16(0, (max - min).u16()).i16() 157 | 158 | fun ref i32(min: I32 = I32.min_value(), max: I32 = I32.max_value()): I32 => 159 | """ 160 | generates a I32 in closed interval [min, max] 161 | (default: [min_value, max_value]) 162 | 163 | behavior is undefined if min > max. 164 | """ 165 | min + u32(0, (max - min).u32()).i32() 166 | 167 | fun ref i64(min: I64 = I64.min_value(), max: I64 = I64.max_value()): I64 => 168 | """ 169 | generates a I64 in closed interval [min, max] 170 | (default: [min_value, max_value]) 171 | 172 | behavior is undefined if min > max. 173 | """ 174 | min + u64(0, (max - min).u64()).i64() 175 | 176 | 177 | fun ref i128( 178 | min: I128 = I128.min_value(), 179 | max: I128 = I128.max_value()) 180 | : I128 181 | => 182 | """ 183 | generates a I128 in closed interval [min, max] 184 | (default: [min_value, max_value]) 185 | 186 | behavior is undefined if min > max. 187 | """ 188 | min + u128(0, (max - min).u128()).i128() 189 | 190 | 191 | fun ref ilong( 192 | min: ILong = ILong.min_value(), 193 | max: ILong = ILong.max_value()) 194 | : ILong 195 | => 196 | """ 197 | generates a ILong in closed interval [min, max] 198 | (default: [min_value, max_value]) 199 | 200 | behavior is undefined if min > max. 201 | """ 202 | min + ulong(0, (max - min).ulong()).ilong() 203 | 204 | fun ref isize( 205 | min: ISize = ISize.min_value(), 206 | max: ISize = ISize.max_value()) 207 | : ISize 208 | => 209 | """ 210 | generates a ISize in closed interval [min, max] 211 | (default: [min_value, max_value]) 212 | 213 | behavior is undefined if min > max. 214 | """ 215 | min + usize(0, (max - min).usize()).isize() 216 | 217 | 218 | fun ref f32(min: F32 = 0.0, max: F32 = 1.0): F32 => 219 | """ 220 | generates a F32 in closed interval [min, max] 221 | (default: [0.0, 1.0]) 222 | """ 223 | (_random.real().f32() * (max-min)) + min 224 | 225 | 226 | fun ref f64(min: F64 = 0.0, max: F64 = 1.0): F64 => 227 | """ 228 | generates a F64 in closed interval [min, max] 229 | (default: [0.0, 1.0]) 230 | """ 231 | (_random.real() * (max-min)) + min 232 | 233 | fun ref bool(): Bool => 234 | """ 235 | generates a random Bool value 236 | """ 237 | (_random.next() % 2) == 0 238 | 239 | fun ref shuffle[T](array: Array[T] ref) => 240 | _random.shuffle[T](array) 241 | 242 | 243 | --------------------------------------------------------------------------------