├── .git-blame-ignore-revs ├── .github ├── pull_request_template.md └── workflows │ ├── build_viewer.yml │ └── test.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE-APACHE.txt ├── LICENSE-MIT.txt ├── README.md ├── birdie_snapshots ├── apply_with_shrinking_test.accepted ├── bit_array_shrinking_test.accepted ├── bool_true_shrink_tree_test.accepted ├── custom_type_tree_test.accepted ├── custom_type_tree_with_bind_test.accepted ├── dict_generators_shrink_on_size_then_on_elements_test.accepted ├── int_option_tree_test.accepted ├── int_tree_root_2_shrink_towards_6_test.accepted ├── int_tree_root_8_shrink_towards_zero_test.accepted ├── list_generators_shrink_on_size_then_on_elements_test.accepted ├── set_generators_shrink_on_size_then_on_elements_test.accepted └── string_generators_shrink_on_size_then_on_characters_test.accepted ├── gleam.toml ├── justfile ├── manifest.toml ├── qcheck_viewer ├── .gitignore ├── README.md ├── domino │ ├── .gitignore │ ├── README.md │ ├── gleam.toml │ ├── manifest.toml │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── domino.gleam │ │ └── domino_ffi.mjs │ └── test │ │ └── domino_test.gleam ├── gleam.toml ├── index.html ├── manifest.toml ├── package-lock.json ├── package.json ├── src │ ├── qcheck_viewer.gleam │ └── qcheck_viewer_ffi.mjs └── test │ └── qcheck_viewer_test.gleam ├── src ├── qcheck.gleam ├── qcheck │ ├── random.gleam │ ├── shrink.gleam │ ├── test_error_message.gleam │ └── tree.gleam ├── qcheck_ffi.erl └── qcheck_ffi.mjs └── test ├── examples ├── basic_example_test.gleam ├── parameter_example_test.gleam ├── parsing_example_test.gleam └── using_use_test.gleam ├── qcheck ├── assert_test.gleam ├── config_test.gleam ├── gen_algebra_test.gleam ├── gen_bit_array_test.gleam ├── gen_bool_test.gleam ├── gen_codepoint_test.gleam ├── gen_custom_types_test.gleam ├── gen_dict_test.gleam ├── gen_float_test.gleam ├── gen_int_test.gleam ├── gen_list_test.gleam ├── gen_nil_test.gleam ├── gen_option_test.gleam ├── gen_set_test.gleam ├── gen_string_test.gleam ├── gen_unicode_test.gleam ├── generate_test.gleam ├── large_number_test.gleam ├── random_test.gleam └── tree_test.gleam └── qcheck_test.gleam /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Run gleam format 1.2.1 -- Mon Jul 1 09:28:24 2024 -0400 2 | af86c0d29b4db9166feb7f5f236f79d34698d4a9 3 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for your interest in the project! 2 | 3 | - Bug reports, feature requests, suggestions and ideas are welcomed. Please open an [issue](https://github.com/mooreryan/gleam_qcheck/issues/new/choose) to start a discussion. 4 | - External contributions will generally not be accepted without prior discussion. 5 | - If you have an idea for a new feature, please open an issue for discussion prior to working on a pull request. 6 | - Small pull requests for bug fixes, typos, or other changes with limited scope may be accepted. If in doubt, please open an issue for discussion first. 7 | -------------------------------------------------------------------------------- /.github/workflows/build_viewer.yml: -------------------------------------------------------------------------------- 1 | name: build_viewer 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | build_viewer: 12 | permissions: 13 | contents: write 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: erlef/setup-beam@v1 20 | with: 21 | otp-version: "27.0.1" 22 | gleam-version: "1.6.3" 23 | rebar3-version: "3" 24 | - run: echo "LOCAL_PATH=$HOME/.local/bin" >> $GITHUB_ENV 25 | - name: Setup path 26 | run: | 27 | mkdir -p "$LOCAL_PATH" 28 | echo "$LOCAL_PATH" >> $GITHUB_PATH 29 | - name: Setup just 30 | run: | 31 | \curl \ 32 | --proto '=https' \ 33 | --tlsv1.2 \ 34 | -sSf \ 35 | https://just.systems/install.sh \ 36 | | bash -s -- \ 37 | --to "$LOCAL_PATH" 38 | - name: Setup Gleam project 39 | run: just qv_setup_for_gh_pages 40 | - name: Build site 41 | run: just qv_build_site 42 | - name: Make docs dir 43 | run: | 44 | git checkout gh-pages 45 | if [ -d docs ]; then rm -r docs; fi 46 | mv qcheck_viewer/dist docs 47 | 48 | # TODO: if there are no site changes, this step will fail with 49 | # "nothing added to the commit". 50 | - name: Commit site changes 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | run: | 54 | git config --global user.name github-actions 55 | git config --global user.email github-actions@github.com 56 | git add docs 57 | git commit -m "Update docs site" 58 | git push 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - staging 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: erlef/setup-beam@v1 16 | with: 17 | otp-version: "27.0.1" 18 | gleam-version: "1.6.3" 19 | rebar3-version: "3" 20 | - run: gleam deps download 21 | - run: gleam test 22 | - run: gleam format --check src test 23 | - run: gleam test --target=javascript 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | 6 | _* 7 | 8 | /*.trace 9 | /*.fprof 10 | 11 | /bench2 12 | /codebook.toml 13 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 27.2 2 | gleam 1.8.1 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.0.0] -- 2025-03-01 10 | 11 | - Major rewrite of the public API 12 | - Not backward compatible with pre-1.0 versions 13 | 14 |
15 | Pre-1.0 Versions 16 | 17 | - [0.0.8](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.8) -- 2024-12-31 18 | - [0.0.7](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.7) -- 2024-12-11 19 | - [0.0.6](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.6) -- 2024-09-30 20 | - [0.0.5](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.5) -- 2024-09-23 21 | - [0.0.4](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.4) -- 2024-09-16 22 | - [0.0.3](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.3) -- 2024-05-15 23 | - [0.0.2](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.2) -- 2024-05-06 24 | - [0.0.1](https://github.com/mooreryan/gleam_qcheck/releases/tag/v0.0.1) -- 2024-04-28 25 | 26 |
27 | 28 | [Unreleased]: https://github.com/mooreryan/gleam_qcheck/compare/v1.0.0...HEAD 29 | [1.0.0]: https://github.com/mooreryan/gleam_qcheck/compare/v0.0.8...v1.0.0 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | admin at iroki.net. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in the project! 4 | 5 | - Bug reports, feature requests, suggestions and ideas are welcomed. Please open an [issue](https://github.com/mooreryan/gleam_qcheck/issues/new/choose) to start a discussion. 6 | - External contributions will generally not be accepted without prior discussion. 7 | - If you have an idea for a new feature, please open an issue for discussion prior to working on a pull request. 8 | - Small pull requests for bug fixes, typos, or other changes with limited scope may be accepted. If in doubt, please open an issue for discussion first. 9 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 - 2025 Ryan M. Moore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qcheck 2 | 3 | QuickCheck-inspired property-based testing with integrated shrinking for [Gleam](https://gleam.run/). 4 | 5 | Rather than specifying test cases manually, you describe the invariants that values of a given type must satisfy ("properties"). Then, generators generate lots of values (test cases) on which the properties are checked. Finally, if a value is found for which a given property does not hold, that value is "shrunk" in order to find an nice, informative counter-example that is presented to you. 6 | 7 | While there are a ton of great articles introducing quickcheck or property-based testing, here are a couple general resources that you may enjoy: 8 | 9 | - [An introduction to property based testing](https://fsharpforfunandprofit.com/pbt/) 10 | - [What is Property Based Testing?](https://hypothesis.works/articles/what-is-property-based-testing/) 11 | 12 | You might also be interested in checking out [this project](https://github.com/mooreryan/gleam_stdlib_testing) that uses qcheck to test Gleam's stdlib. 13 | 14 | ## Usage & Examples 15 | 16 | - See the API docs for detailed usage, 17 | - See [qcheck_viewer](https://mooreryan.github.io/gleam_qcheck/) to visualize the distributions of some of the qcheck generators. 18 | 19 | ### Basic example 20 | 21 | Here is a short example to get you started. It assumes you are using [gleeunit](https://github.com/lpil/gleeunit) to run the tests, but any test runner that reasonably handles panics will do. 22 | 23 | ```gleam 24 | import qcheck 25 | 26 | pub fn int_addition_commutativity__test() { 27 | use n <- qcheck.given(qcheck.small_non_negative_int()) 28 | should.equal(n + 1, 1 + n) 29 | } 30 | 31 | pub fn int_addition_commutativity__failures_shrink_to_zero__test() { 32 | use n <- qcheck.given(qcheck.small_non_negative_int()) 33 | should.not_equal(n + 1, 1 + n) 34 | } 35 | ``` 36 | 37 | That second example will fail with an error that may look something like this if you are targeting Erlang. 38 | 39 | ``` 40 | Failures: 41 | 42 | 1) examples/basic_example_test.small_non_negative_int__failures_shrink_to_zero__test 43 | Failure: <<"TestError[original_value: 5; shrunk_value: 0; shrink_steps: 1; error: Errored( 44 | atom.create_from_string(\"assertNotEqual\")( 45 | [Module(GleeunitFfi), Line(17), Expression([65, 99, 116, 117, 97, 108]), Value(6)] 46 | ) 47 | );]">> 48 | stacktrace: 49 | qcheck_ffi.fail 50 | ``` 51 | 52 | - `qcheck.given` sets up the test 53 | - If a property holds for all generated values, then `qcheck.given` returns `Nil`. 54 | - If a property does not hold for all generated values, then `qcheck.given` will panic. 55 | - `qcheck.small_non_negative_int()` generates small integers greater than or equal to zero. 56 | - `should.equal(n + 1, 1 + n)` is the property being tested in the first test. 57 | - It should be true for all generated values. 58 | - The return value of `qcheck.given` will be `Nil`, because the property does hold for all generated values. 59 | - `should.not_equal(n + 1, 1 + n)` is the property being tested in the second test. 60 | - It should be false for all generated values. 61 | - `qcheck.given` will be panic, because the property does not hold for all generated values. 62 | 63 | ### In-depth example 64 | 65 | Here is a more in-depth example. We will create a simple `Point` type, write some serialization functions, and then check that the serializing round-trips. 66 | 67 | First here is some code to define a `Point`. 68 | 69 | ```gleam 70 | type Point { 71 | Point(Int, Int) 72 | } 73 | 74 | fn make_point(x: Int, y: Int) -> Point { 75 | Point(x, y) 76 | } 77 | 78 | fn point_equal(p1: Point, p2: Point) -> Bool { 79 | let Point(x1, y1) = p1 80 | let Point(x2, y2) = p2 81 | 82 | x1 == x2 && y1 == y2 83 | } 84 | 85 | fn point_to_string(point: Point) -> String { 86 | let Point(x, y) = point 87 | "(" <> int.to_string(x) <> " " <> int.to_string(y) <> ")" 88 | } 89 | ``` 90 | 91 | Next, let's write a function that parses the string representation into a `Point`. The string representation is pretty simple, `Point(1, 2)` would be represented by the following string: `(1 2)`. 92 | 93 | Here is one possible way to parse that string representation into a `Point`. (Note that this implementation is intentionally broken for illustration.) 94 | 95 | ```gleam 96 | fn point_of_string(string: String) -> Result(Point, String) { 97 | // Create the regex. 98 | use re <- result.try( 99 | regex.from_string("\\((\\d+) (\\d+)\\)") 100 | |> result.map_error(string.inspect), 101 | ) 102 | 103 | // Ensure there is a single match. 104 | use submatches <- result.try(case regex.scan(re, string) { 105 | [Match(_content, submatches)] -> Ok(submatches) 106 | _ -> Error("expected a single match") 107 | }) 108 | 109 | // Ensure both submatches are present. 110 | use xy <- result.try(case submatches { 111 | [Some(x), Some(y)] -> Ok(#(x, y)) 112 | _ -> Error("expected two submatches") 113 | }) 114 | 115 | // Try to parse both x and y values as integers. 116 | use xy <- result.try(case int.parse(xy.0), int.parse(xy.1) { 117 | Ok(x), Ok(y) -> Ok(#(x, y)) 118 | Error(Nil), Ok(_) -> Error("failed to parse x value") 119 | Ok(_), Error(Nil) -> Error("failed to parse y value") 120 | Error(Nil), Error(Nil) -> Error("failed to parse x and y values") 121 | }) 122 | 123 | Ok(Point(xy.0, xy.1)) 124 | } 125 | ``` 126 | 127 | Now we would like to test our implementation. Of course, we could make some examples and test it like so: 128 | 129 | ```gleam 130 | import gleeunit/should 131 | 132 | pub fn roundtrip_test() { 133 | let point = Point(1, 2) 134 | let parsed_point = point |> point_to_string |> point_of_string 135 | 136 | point_equal(point, parsed_point) |> should.be_true 137 | } 138 | ``` 139 | 140 | That's fine, and you can imagine taking some corner cases like putting in `0` or `-1` or the max and min values for integers on your selected target. Rather, let's think of a property to test. 141 | 142 | I mention round-tripping, but how can you write a property to test it. Something like, "given a valid point, when serializing it to a string, and then deserializing that string into another point, both points should always be equal". 143 | 144 | Okay, first we need to write a generator of valid points. In this case, it isn't too interesting as any integer can be used for both `x` and `y` values of the point. So we can use `generator.map2` like so: 145 | 146 | ```gleam 147 | fn point_generator() { 148 | qcheck.map2(qcheck.int_uniform(), qcheck.int_uniform(), make_point) 149 | } 150 | ``` 151 | 152 | Alternatively, if you prefer the `use` syntax, you could write: 153 | 154 | ```gleam 155 | fn point_generator() { 156 | use x, y <- qcheck.map2(qcheck.int_uniform(), qcheck.int_uniform()) 157 | 158 | make_point(x, y) 159 | } 160 | ``` 161 | 162 | Now that we have the point generator, we can write a property test. (It uses the `gleeunit/should.be_true` function again.) 163 | 164 | ```gleam 165 | pub fn point_serialization_roundtripping__test() { 166 | use generated_point <- qcheck.given(point_generator()) 167 | 168 | let assert Ok(parsed_point) = 169 | generated_point 170 | |> point_to_string 171 | |> point_of_string 172 | 173 | should.be_true(point_equal(generated_point, parsed_point)) 174 | } 175 | ``` 176 | 177 | Let's try and run the test. (Note that your output won't look exactly like this.) 178 | 179 | ``` 180 | $ gleam test 181 | 182 | 1) examples/parsing_example_test.point_serialization_roundtripping__test: module 'examples@parsing_example_test' 183 | Failure: <<"TestError[original_value: Point(-875333649, -1929681101); shrunk_value: Point(0, -1); shrink_steps: 31; error: Errored(dict.from_list([#(Function, \"point_serialization_roundtripping__test\"), #(Line, 74), #(Message, \"Assertion pattern match failed\"), #(Module, \"examples/parsing_example_test\"), #(Value, Error(\"expected a single match\")), #(GleamError, LetAssert)]));]">> 184 | stacktrace: 185 | qcheck_ffi.fail 186 | output: 187 | ``` 188 | 189 | There is a failure. Now, currently, this output is pretty noisy. Here are the important parts to highlight. 190 | 191 | - `original_value: Point(-875333649, -1929681101)` 192 | - This is the original counter-example that causes the test to fail. 193 | - `shrunk_value: Point(0, -1)` 194 | - Because `qcheck` generators have integrated shrinking, that counter-example "shrinks" to this simpler example. 195 | - The "shrunk" examples can help you better identify what the problem may be. 196 | - `Error(\"expected a single match\"))` 197 | - Here is the error message that actually caused the failure. 198 | 199 | So we see a failure with `Point(0, -1)`, which means it probably has something to do with the negative number. Also, we see that the `Error("expected a single match")` is what triggered the failure. That error comes about when `regex.scan` fails in the `point_of_string` function. 200 | 201 | Given those two pieces of information, we can infer that the issue is in our regular expression definition: `regex.from_string("\\((\\d+) (\\d+)\\)")`. And now we may notice that we are not allowing for negative numbers in the regular expression. To fix it, change that line to the following: 202 | 203 | ```gleam 204 | regex.from_string("\\((-?\\d+) (-?\\d+)\\)") 205 | ``` 206 | 207 | That is allowing an optional `-` sign in front of the integers. Now when you rerun the `gleam test`, everything passes. 208 | 209 | You could imagine combining a property test like the one above, with a few well chosen examples to anchor everything, into a nice little test suite that exercises the serialization of points in a small amount of test code. 210 | 211 | (The full code for this example can be found in `test/examples/parsing_example_test.gleam`.) 212 | 213 | ### Applicative style 214 | 215 | The applicative style provides a nice interface for creating generators for custom types. 216 | 217 | ```gleam 218 | import qcheck 219 | 220 | /// A simple Box type with position (x, y) and dimensions (width, height). 221 | type Box { 222 | Box(x: Int, y: Int, w: Int, h: Int) 223 | } 224 | 225 | fn box_generator() { 226 | // Lift the Box creating function into the Generator structure. 227 | qcheck.return({ 228 | use x <- qcheck.parameter 229 | use y <- qcheck.parameter 230 | use w <- qcheck.parameter 231 | use h <- qcheck.parameter 232 | Box(x:, y:, w:, h:) 233 | }) 234 | // Set the `x` generator. 235 | |> qcheck.apply(qcheck.int_uniform_inclusive(-100, 100)) 236 | // Set the `y` generator. 237 | |> qcheck.apply(qcheck.int_uniform_inclusive(-100, 100)) 238 | // Set the `width` generator. 239 | |> qcheck.apply(qcheck.int_uniform_inclusive(1, 100)) 240 | // Set the `height` generator. 241 | |> qcheck.apply(qcheck.int_uniform_inclusive(1, 100)) 242 | } 243 | ``` 244 | 245 | ### Integrating with testing frameworks 246 | 247 | You don't have to do anything special to integrate `qcheck` with a testing framework like [gleeunit](https://github.com/lpil/gleeunit). The only thing required is that your testing framework of choice be able to handle panics/exceptions. 248 | 249 | _Note: [startest](https://github.com/maxdeviant/startest) should be fine._ 250 | 251 | You may also be interested in [qcheck_gleeunit_utils](https://github.com/mooreryan/qcheck_gleeunit_utils) for running your tests in parallel and controlling test timeouts when using gleeunit and targeting Erlang. 252 | 253 | ## Acknowledgements 254 | 255 | Very heavily inspired by the [qcheck](https://github.com/c-cube/qcheck) and [base_quickcheck](https://github.com/janestreet/base_quickcheck) OCaml packages. 256 | 257 | ## Contributing 258 | 259 | Thank you for your interest in the project! 260 | 261 | - Bug reports, feature requests, suggestions and ideas are welcomed. Please open an [issue](https://github.com/mooreryan/gleam_qcheck/issues/new/choose) to start a discussion. 262 | - External contributions will generally not be accepted without prior discussion. 263 | - If you have an idea for a new feature, please open an issue for discussion prior to working on a pull request. 264 | - Small pull requests for bug fixes, typos, or other changes with limited scope may be accepted. If in doubt, please open an issue for discussion first. 265 | 266 | ## License 267 | 268 | [![license MIT or Apache 269 | 2.0](https://img.shields.io/badge/license-MIT%20or%20Apache%202.0-blue)](https://github.com/mooreryan/gleam_qcheck) 270 | 271 | Copyright (c) 2024 - 2025 Ryan M. Moore 272 | 273 | Licensed under the Apache License, Version 2.0 or the MIT license, at your option. This program may not be copied, modified, or distributed except according to those terms. 274 | -------------------------------------------------------------------------------- /birdie_snapshots/apply_with_shrinking_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: apply_with_shrinking__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: apply_with_shrinking__test 6 | --- 7 | (1, 2) 8 | -(0, 2) 9 | --(0, 0) 10 | --(0, 1) 11 | ---(0, 0) 12 | -(1, 0) 13 | --(0, 0) 14 | -(1, 1) 15 | --(0, 1) 16 | ---(0, 0) 17 | --(1, 0) 18 | ---(0, 0) 19 | -------------------------------------------------------------------------------- /birdie_snapshots/bit_array_shrinking_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.2.5 3 | title: bit_array_shrinking__test 4 | file: ./test/qcheck/gen_bit_array_test.gleam 5 | test_name: bit_array_shrinking__test 6 | --- 7 | <<4:size(3)>> 8 | -<<0:size(1)>> 9 | --<<1:size(1)>> 10 | -<<0:size(2)>> 11 | --<<0:size(1)>> 12 | ---<<1:size(1)>> 13 | --<<3:size(2)>> 14 | -<<3:size(3)>> 15 | -------------------------------------------------------------------------------- /birdie_snapshots/bool_true_shrink_tree_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: bool_true_shrink_tree__test 4 | file: ./test/gen_string_test.gleam 5 | test_name: bool_true_shrink_tree__test 6 | --- 7 | True 8 | -False 9 | -------------------------------------------------------------------------------- /birdie_snapshots/custom_type_tree_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: custom_type_tree__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: custom_type_tree__test 6 | --- 7 | First(4) 8 | -First(0) 9 | -First(2) 10 | --First(0) 11 | --Second(1) 12 | ---First(0) 13 | -Second(3) 14 | --First(0) 15 | --Second(1) 16 | ---First(0) 17 | --First(2) 18 | ---First(0) 19 | ---Second(1) 20 | ----First(0) 21 | -------------------------------------------------------------------------------- /birdie_snapshots/custom_type_tree_with_bind_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: custom_type_tree_with_bind__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: custom_type_tree_with_bind__test 6 | --- 7 | 3* 8 | -0* 9 | -1* 10 | --0* 11 | --0* 12 | -2* 13 | --0* 14 | --1* 15 | ---0* 16 | ---0* 17 | --0* 18 | --1* 19 | ---0* 20 | -0* 21 | -1* 22 | --0* 23 | -2* 24 | --0* 25 | --1* 26 | ---0* 27 | -------------------------------------------------------------------------------- /birdie_snapshots/dict_generators_shrink_on_size_then_on_elements_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: dict_generators_shrink_on_size_then_on_elements__test 4 | file: ./test/qcheck/gen_dict_test.gleam 5 | test_name: dict_generators_shrink_on_size_then_on_elements__test 6 | --- 7 | { 0 => 11, 1 => 10, 2 => 10, } 8 | -{ } 9 | -{ 1 => 10, } 10 | --{ } 11 | --{ 0 => 10, } 12 | -{ 1 => 10, 2 => 10, } 13 | --{ } 14 | --{ 1 => 10, } 15 | ---{ } 16 | ---{ 0 => 10, } 17 | --{ 0 => 10, 1 => 10, } 18 | ---{ 0 => 10, } 19 | --{ 1 => 10, } 20 | ---{ 0 => 10, 1 => 10, } 21 | ----{ 0 => 10, } 22 | ---{ 0 => 10, 1 => 10, } 23 | ----{ 0 => 10, } 24 | --{ 0 => 10, 2 => 10, } 25 | ---{ 0 => 10, } 26 | ---{ 0 => 10, 1 => 10, } 27 | ----{ 0 => 10, } 28 | -{ 0 => 10, 1 => 10, 2 => 10, } 29 | --{ 0 => 10, 1 => 10, } 30 | ---{ 0 => 10, } 31 | --{ 0 => 10, 1 => 10, } 32 | ---{ 0 => 10, 1 => 10, } 33 | ----{ 0 => 10, } 34 | ---{ 0 => 10, 1 => 10, } 35 | ----{ 0 => 10, } 36 | --{ 0 => 10, 2 => 10, } 37 | ---{ 0 => 10, } 38 | ---{ 0 => 10, 1 => 10, } 39 | ----{ 0 => 10, } 40 | -{ 0 => 10, 1 => 10, } 41 | --{ 0 => 10, 1 => 10, } 42 | ---{ 0 => 10, } 43 | --{ 0 => 10, } 44 | ---{ 0 => 10, } 45 | -{ 0 => 11, 1 => 10, } 46 | --{ 0 => 10, 1 => 10, } 47 | ---{ 0 => 10, 1 => 10, } 48 | ----{ 0 => 10, } 49 | ---{ 0 => 10, 1 => 10, } 50 | ----{ 0 => 10, } 51 | --{ 0 => 10, 1 => 10, } 52 | ---{ 0 => 10, 1 => 10, } 53 | ----{ 0 => 10, } 54 | ---{ 0 => 10, } 55 | ----{ 0 => 10, } 56 | --{ 0 => 10, 1 => 10, } 57 | ---{ 0 => 10, 1 => 10, } 58 | ----{ 0 => 10, } 59 | ---{ 0 => 10, } 60 | ----{ 0 => 10, } 61 | -{ 0 => 10, 2 => 10, } 62 | --{ 0 => 10, 2 => 10, } 63 | ---{ 0 => 10, } 64 | ---{ 0 => 10, 1 => 10, } 65 | ----{ 0 => 10, } 66 | --{ 0 => 10, } 67 | ---{ 0 => 10, } 68 | --{ 0 => 10, 1 => 10, } 69 | ---{ 0 => 10, 1 => 10, } 70 | ----{ 0 => 10, } 71 | ---{ 0 => 10, } 72 | ----{ 0 => 10, } 73 | -------------------------------------------------------------------------------- /birdie_snapshots/int_option_tree_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: int_option_tree__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: int_option_tree__test 6 | --- 7 | 4 8 | -N 9 | -0 10 | --N 11 | -2 12 | --N 13 | --0 14 | ---N 15 | --1 16 | ---N 17 | ---0 18 | ----N 19 | -3 20 | --N 21 | --0 22 | ---N 23 | --1 24 | ---N 25 | ---0 26 | ----N 27 | --2 28 | ---N 29 | ---0 30 | ----N 31 | ---1 32 | ----N 33 | ----0 34 | -----N 35 | -------------------------------------------------------------------------------- /birdie_snapshots/int_tree_root_2_shrink_towards_6_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: int_tree_root_2_shrink_towards_6__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: int_tree_root_2_shrink_towards_6__test 6 | --- 7 | 2 8 | -6 9 | -4 10 | --6 11 | --5 12 | ---6 13 | -3 14 | --6 15 | --4 16 | ---6 17 | ---5 18 | ----6 19 | -------------------------------------------------------------------------------- /birdie_snapshots/int_tree_root_8_shrink_towards_zero_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.2 3 | title: int_tree_root_8_shrink_towards_zero__test 4 | file: ./test/qcheck/tree_test.gleam 5 | test_name: int_tree_root_8_shrink_towards_zero__test 6 | --- 7 | 8 8 | -0 9 | -4 10 | --0 11 | --2 12 | ---0 13 | ---1 14 | ----0 15 | --3 16 | ---0 17 | ---1 18 | ----0 19 | ---2 20 | ----0 21 | ----1 22 | -----0 23 | -6 24 | --0 25 | --3 26 | ---0 27 | ---1 28 | ----0 29 | ---2 30 | ----0 31 | ----1 32 | -----0 33 | --5 34 | ---0 35 | ---2 36 | ----0 37 | ----1 38 | -----0 39 | ---3 40 | ----0 41 | ----1 42 | -----0 43 | ----2 44 | -----0 45 | -----1 46 | ------0 47 | ---4 48 | ----0 49 | ----2 50 | -----0 51 | -----1 52 | ------0 53 | ----3 54 | -----0 55 | -----1 56 | ------0 57 | -----2 58 | ------0 59 | ------1 60 | -------0 61 | -7 62 | --0 63 | --3 64 | ---0 65 | ---1 66 | ----0 67 | ---2 68 | ----0 69 | ----1 70 | -----0 71 | --5 72 | ---0 73 | ---2 74 | ----0 75 | ----1 76 | -----0 77 | ---3 78 | ----0 79 | ----1 80 | -----0 81 | ----2 82 | -----0 83 | -----1 84 | ------0 85 | ---4 86 | ----0 87 | ----2 88 | -----0 89 | -----1 90 | ------0 91 | ----3 92 | -----0 93 | -----1 94 | ------0 95 | -----2 96 | ------0 97 | ------1 98 | -------0 99 | --6 100 | ---0 101 | ---3 102 | ----0 103 | ----1 104 | -----0 105 | ----2 106 | -----0 107 | -----1 108 | ------0 109 | ---5 110 | ----0 111 | ----2 112 | -----0 113 | -----1 114 | ------0 115 | ----3 116 | -----0 117 | -----1 118 | ------0 119 | -----2 120 | ------0 121 | ------1 122 | -------0 123 | ----4 124 | -----0 125 | -----2 126 | ------0 127 | ------1 128 | -------0 129 | -----3 130 | ------0 131 | ------1 132 | -------0 133 | ------2 134 | -------0 135 | -------1 136 | --------0 137 | -------------------------------------------------------------------------------- /birdie_snapshots/list_generators_shrink_on_size_then_on_elements_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: list_generators_shrink_on_size_then_on_elements__test 4 | file: ./test/qcheck/gen_list_test.gleam 5 | test_name: list_generators_shrink_on_size_then_on_elements__test 6 | --- 7 | [0,1,2] 8 | -[] 9 | -[2] 10 | --[] 11 | --[0] 12 | --[1] 13 | ---[0] 14 | -[1,2] 15 | --[] 16 | --[2] 17 | ---[] 18 | ---[0] 19 | ---[1] 20 | ----[0] 21 | --[0,2] 22 | ---[0,0] 23 | ---[0,1] 24 | ----[0,0] 25 | --[1,0] 26 | ---[0,0] 27 | --[1,1] 28 | ---[0,1] 29 | ----[0,0] 30 | ---[1,0] 31 | ----[0,0] 32 | -[0,0,2] 33 | --[0,0,0] 34 | --[0,0,1] 35 | ---[0,0,0] 36 | -[0,1,0] 37 | --[0,0,0] 38 | -[0,1,1] 39 | --[0,0,1] 40 | ---[0,0,0] 41 | --[0,1,0] 42 | ---[0,0,0] 43 | -------------------------------------------------------------------------------- /birdie_snapshots/set_generators_shrink_on_size_then_on_elements_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: set_generators_shrink_on_size_then_on_elements__test 4 | file: ./test/qcheck/gen_set_test.gleam 5 | test_name: set_generators_shrink_on_size_then_on_elements__test 6 | --- 7 | [0,1,2] 8 | -[] 9 | -[2] 10 | --[] 11 | --[0] 12 | --[1] 13 | ---[0] 14 | -[1,2] 15 | --[] 16 | --[2] 17 | ---[] 18 | ---[0] 19 | ---[1] 20 | ----[0] 21 | --[0,2] 22 | ---[0] 23 | ---[0,1] 24 | ----[0] 25 | --[0,1] 26 | ---[0] 27 | --[1] 28 | ---[0,1] 29 | ----[0] 30 | ---[0,1] 31 | ----[0] 32 | -[0,2] 33 | --[0] 34 | --[0,1] 35 | ---[0] 36 | -[0,1] 37 | --[0] 38 | -[0,1] 39 | --[0,1] 40 | ---[0] 41 | --[0,1] 42 | ---[0] 43 | -------------------------------------------------------------------------------- /birdie_snapshots/string_generators_shrink_on_size_then_on_characters_test.accepted: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1.1.4 3 | title: string_generators_shrink_on_size_then_on_characters__test 4 | file: ./test/gen_string_test.gleam 5 | test_name: string_generators_shrink_on_size_then_on_characters__test 6 | --- 7 | baac 8 | -ba 9 | --aa 10 | -baa 11 | --ba 12 | ---aa 13 | --aaa 14 | -aaac 15 | --aaaa 16 | --aaab 17 | ---aaaa 18 | -baaa 19 | --aaaa 20 | -baab 21 | --aaab 22 | ---aaaa 23 | --baaa 24 | ---aaaa 25 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "qcheck" 2 | version = "1.0.0" 3 | 4 | internal_modules = ["qcheck/test_error_message"] 5 | 6 | description = "QuickCheck-inspired property testing with integrated shrinking" 7 | licences = ["Apache-2.0", "MIT"] 8 | repository = { type = "github", user = "mooreryan", repo = "gleam_qcheck" } 9 | 10 | [dependencies] 11 | gleam_stdlib = ">= 0.52.0" 12 | prng = ">= 4.0.1 and < 5.0.0" 13 | exception = ">= 2.0.0 and < 3.0.0" 14 | gleam_regexp = ">= 1.0.0 and < 2.0.0" 15 | gleam_yielder = ">= 1.1.0 and < 2.0.0" 16 | 17 | [dev-dependencies] 18 | birdie = ">= 1.1.2 and < 2.0.0" 19 | gleeunit = ">= 1.0.0 and < 2.0.0" 20 | qcheck_gleeunit_utils = ">= 0.1.0 and < 0.2.0" 21 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | check: 2 | gleam check 3 | 4 | check_no_cache: 5 | rm -r build/dev/*/qcheck; gleam check 6 | 7 | checkw: 8 | watchexec --no-process-group gleam check 9 | 10 | testw: 11 | watchexec --no-process-group gleam test 12 | 13 | test: 14 | #!/usr/bin/env bash 15 | set -euxo pipefail 16 | 17 | gleam test 18 | gleam test --target=javascript 19 | 20 | test_review: 21 | gleam run -m birdie 22 | 23 | find_todos: 24 | rg -g '!build' -g '!justfile' -i todo 25 | 26 | bench_full: 27 | #!/usr/bin/env bash 28 | set -euxo pipefail 29 | 30 | cd bench 31 | 32 | gleam run -m full_benchmark -- bench_out 33 | 34 | bench_x: 35 | #!/usr/bin/env bash 36 | set -euxo pipefail 37 | 38 | cd bench2 39 | 40 | gleam run -- bench_x_out 41 | 42 | qv_dev: 43 | #!/usr/bin/env bash 44 | set -euxo pipefail 45 | 46 | cd qcheck_viewer 47 | 48 | gleam run -m lustre/dev start 49 | 50 | qv_test: 51 | #!/usr/bin/env bash 52 | set -euxo pipefail 53 | 54 | cd qcheck_viewer 55 | 56 | gleam test 57 | 58 | qv_build_site: 59 | #!/usr/bin/env bash 60 | set -euxo pipefail 61 | 62 | cd qcheck_viewer 63 | 64 | if [ -d dist ]; then rm -r dist; fi 65 | mkdir -p dist/priv/static 66 | gleam run -m lustre/dev build --outdir=dist/priv/static --minify 67 | mv dist/priv/static/qcheck_viewer.min.mjs \ 68 | dist/priv/static/qcheck_viewer.mjs 69 | cp index.html dist 70 | 71 | qv_setup_for_gh_pages: 72 | #!/usr/bin/env bash 73 | set -euxo pipefail 74 | 75 | cd qcheck_viewer 76 | 77 | npm install 78 | gleam deps download 79 | 80 | # Find the `name` in the code. 81 | find name: 82 | rg {{ name }} -g '*.gleam' src/ test/ 83 | 84 | # You SHOULD have a clean working directory (git) and run `find name` first to verify. 85 | rename current_name new_name: 86 | rg {{ current_name }} -l -0 -g '*.gleam' src/ test/ | xargs -0 sed -i '' 's/{{ current_name }}/{{ new_name }}/g' 87 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 | { name = "birdie", version = "1.2.6", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "1363F4C7E7433A4A8350CC682BCDDBA5BBC6F66C94EFC63BC43025F796C4F6D0" }, 7 | { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, 8 | { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 9 | { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, 10 | { name = "glance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "106111453AE9BA959184302B7DADF2E8CF322B27A7CB68EE78F3EE43FEACCE2C" }, 11 | { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, 12 | { name = "gleam_community_ansi", version = "1.4.2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "479DEDC748D08B310C9FEB9C4CBEC46B95C874F7F4F2844304D6D20CA78A8BB5" }, 13 | { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, 14 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, 15 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, 16 | { name = "gleam_regexp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "7F5E0C0BBEB3C58E57C9CB05FA9002F970C85AD4A63BA1E55CBCB35C15809179" }, 17 | { name = "gleam_stdlib", version = "0.55.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "32D8F4AE03771516950047813A9E359249BD9FBA5C33463FDB7B953D6F8E896B" }, 18 | { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 19 | { name = "gleeunit", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "0E6C83834BA65EDCAAF4FE4FB94AC697D9262D83E6F58A750D63C9F6C8A9D9FF" }, 20 | { name = "glexer", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "F74FB4F78C3C1E158DF15A7226F33A662672F58EEF1DFE6593B7FCDA38B0A0EB" }, 21 | { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, 22 | { name = "prng", version = "4.0.1", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib", "gleam_yielder"], otp_app = "prng", source = "hex", outer_checksum = "695AB70E4BE713042062E901975FC08D1EC725B85B808D4786A14C406ADFBCF1" }, 23 | { name = "qcheck_gleeunit_utils", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleeunit"], otp_app = "qcheck_gleeunit_utils", source = "hex", outer_checksum = "0C262D4636DEFC0BE8FA525DC193C2CC1E9D12A3BC26F9CA1F6B7FBB0E20C7B5" }, 24 | { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, 25 | { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 26 | { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 27 | { name = "trie_again", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "5B19176F52B1BD98831B57FDC97BD1F88C8A403D6D8C63471407E78598E27184" }, 28 | ] 29 | 30 | [requirements] 31 | birdie = { version = ">= 1.1.2 and < 2.0.0" } 32 | exception = { version = ">= 2.0.0 and < 3.0.0" } 33 | gleam_regexp = { version = ">= 1.0.0 and < 2.0.0" } 34 | gleam_stdlib = { version = ">= 0.52.0" } 35 | gleam_yielder = { version = ">= 1.1.0 and < 2.0.0" } 36 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 37 | prng = { version = ">= 4.0.1 and < 5.0.0" } 38 | qcheck_gleeunit_utils = { version = ">= 0.1.0 and < 0.2.0" } 39 | -------------------------------------------------------------------------------- /qcheck_viewer/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | 6 | /dist 7 | /docs 8 | /node_modules 9 | /priv 10 | /vite_site 11 | -------------------------------------------------------------------------------- /qcheck_viewer/README.md: -------------------------------------------------------------------------------- 1 | # qcheck_viewer 2 | 3 | `cheerio` is needed for the tests. 4 | 5 | ``` 6 | npm install --save-dev cheerio 7 | ``` 8 | 9 | You will also need to add 10 | 11 | ``` 12 | "type": "module" 13 | ``` 14 | 15 | to the `./node_modules/vega-embed/package.json` if you want to run the tests. 16 | 17 | ## Vega 18 | 19 | Using vegaEmbed is very heavy. It adds ~800kb to the bundle size. Would be nice to find an alternative. 20 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | 6 | /node_modules 7 | /priv 8 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/README.md: -------------------------------------------------------------------------------- 1 | # domino 2 | 3 | [![Package Version](https://img.shields.io/hexpm/v/domino)](https://hex.pm/packages/domino) 4 | [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/domino/) 5 | 6 | ```sh 7 | gleam add domino@1 8 | ``` 9 | ```gleam 10 | import domino 11 | 12 | pub fn main() { 13 | // TODO: An example of the project in use 14 | } 15 | ``` 16 | 17 | Further documentation can be found at . 18 | 19 | ## Development 20 | 21 | ```sh 22 | gleam run # Run the project 23 | gleam test # Run the tests 24 | ``` 25 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "domino" 2 | version = "1.0.0" 3 | target = "javascript" 4 | 5 | # Fill out these fields if you intend to generate HTML documentation or publish 6 | # your project to the Hex package manager. 7 | # 8 | # description = "" 9 | # licences = ["Apache-2.0"] 10 | # repository = { type = "github", user = "", repo = "" } 11 | # links = [{ title = "Website", href = "" }] 12 | # 13 | # For a full reference of all the available options, you can have a look at 14 | # https://gleam.run/writing-gleam/gleam-toml/. 15 | 16 | [dependencies] 17 | gleam_stdlib = ">= 0.44.0 and < 2.0.0" 18 | 19 | [dev-dependencies] 20 | gleeunit = ">= 1.0.0 and < 2.0.0" 21 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_stdlib", version = "0.52.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "50703862DF26453B277688FFCDBE9DD4AC45B3BD9742C0B370DB62BC1629A07D" }, 6 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 7 | ] 8 | 9 | [requirements] 10 | gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 11 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 12 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domino", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "domino", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cheerio": "^1.0.0" 13 | } 14 | }, 15 | "node_modules/boolbase": { 16 | "version": "1.0.0", 17 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 18 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 19 | "license": "ISC" 20 | }, 21 | "node_modules/cheerio": { 22 | "version": "1.0.0", 23 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", 24 | "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", 25 | "license": "MIT", 26 | "dependencies": { 27 | "cheerio-select": "^2.1.0", 28 | "dom-serializer": "^2.0.0", 29 | "domhandler": "^5.0.3", 30 | "domutils": "^3.1.0", 31 | "encoding-sniffer": "^0.2.0", 32 | "htmlparser2": "^9.1.0", 33 | "parse5": "^7.1.2", 34 | "parse5-htmlparser2-tree-adapter": "^7.0.0", 35 | "parse5-parser-stream": "^7.1.2", 36 | "undici": "^6.19.5", 37 | "whatwg-mimetype": "^4.0.0" 38 | }, 39 | "engines": { 40 | "node": ">=18.17" 41 | }, 42 | "funding": { 43 | "url": "https://github.com/cheeriojs/cheerio?sponsor=1" 44 | } 45 | }, 46 | "node_modules/cheerio-select": { 47 | "version": "2.1.0", 48 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", 49 | "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", 50 | "license": "BSD-2-Clause", 51 | "dependencies": { 52 | "boolbase": "^1.0.0", 53 | "css-select": "^5.1.0", 54 | "css-what": "^6.1.0", 55 | "domelementtype": "^2.3.0", 56 | "domhandler": "^5.0.3", 57 | "domutils": "^3.0.1" 58 | }, 59 | "funding": { 60 | "url": "https://github.com/sponsors/fb55" 61 | } 62 | }, 63 | "node_modules/css-select": { 64 | "version": "5.1.0", 65 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 66 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 67 | "license": "BSD-2-Clause", 68 | "dependencies": { 69 | "boolbase": "^1.0.0", 70 | "css-what": "^6.1.0", 71 | "domhandler": "^5.0.2", 72 | "domutils": "^3.0.1", 73 | "nth-check": "^2.0.1" 74 | }, 75 | "funding": { 76 | "url": "https://github.com/sponsors/fb55" 77 | } 78 | }, 79 | "node_modules/css-what": { 80 | "version": "6.1.0", 81 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 82 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 83 | "license": "BSD-2-Clause", 84 | "engines": { 85 | "node": ">= 6" 86 | }, 87 | "funding": { 88 | "url": "https://github.com/sponsors/fb55" 89 | } 90 | }, 91 | "node_modules/dom-serializer": { 92 | "version": "2.0.0", 93 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 94 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 95 | "license": "MIT", 96 | "dependencies": { 97 | "domelementtype": "^2.3.0", 98 | "domhandler": "^5.0.2", 99 | "entities": "^4.2.0" 100 | }, 101 | "funding": { 102 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 103 | } 104 | }, 105 | "node_modules/domelementtype": { 106 | "version": "2.3.0", 107 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 108 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 109 | "funding": [ 110 | { 111 | "type": "github", 112 | "url": "https://github.com/sponsors/fb55" 113 | } 114 | ], 115 | "license": "BSD-2-Clause" 116 | }, 117 | "node_modules/domhandler": { 118 | "version": "5.0.3", 119 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 120 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 121 | "license": "BSD-2-Clause", 122 | "dependencies": { 123 | "domelementtype": "^2.3.0" 124 | }, 125 | "engines": { 126 | "node": ">= 4" 127 | }, 128 | "funding": { 129 | "url": "https://github.com/fb55/domhandler?sponsor=1" 130 | } 131 | }, 132 | "node_modules/domutils": { 133 | "version": "3.2.2", 134 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", 135 | "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 136 | "license": "BSD-2-Clause", 137 | "dependencies": { 138 | "dom-serializer": "^2.0.0", 139 | "domelementtype": "^2.3.0", 140 | "domhandler": "^5.0.3" 141 | }, 142 | "funding": { 143 | "url": "https://github.com/fb55/domutils?sponsor=1" 144 | } 145 | }, 146 | "node_modules/encoding-sniffer": { 147 | "version": "0.2.0", 148 | "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", 149 | "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", 150 | "license": "MIT", 151 | "dependencies": { 152 | "iconv-lite": "^0.6.3", 153 | "whatwg-encoding": "^3.1.1" 154 | }, 155 | "funding": { 156 | "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" 157 | } 158 | }, 159 | "node_modules/entities": { 160 | "version": "4.5.0", 161 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 162 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 163 | "license": "BSD-2-Clause", 164 | "engines": { 165 | "node": ">=0.12" 166 | }, 167 | "funding": { 168 | "url": "https://github.com/fb55/entities?sponsor=1" 169 | } 170 | }, 171 | "node_modules/htmlparser2": { 172 | "version": "9.1.0", 173 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", 174 | "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", 175 | "funding": [ 176 | "https://github.com/fb55/htmlparser2?sponsor=1", 177 | { 178 | "type": "github", 179 | "url": "https://github.com/sponsors/fb55" 180 | } 181 | ], 182 | "license": "MIT", 183 | "dependencies": { 184 | "domelementtype": "^2.3.0", 185 | "domhandler": "^5.0.3", 186 | "domutils": "^3.1.0", 187 | "entities": "^4.5.0" 188 | } 189 | }, 190 | "node_modules/iconv-lite": { 191 | "version": "0.6.3", 192 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 193 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 194 | "license": "MIT", 195 | "dependencies": { 196 | "safer-buffer": ">= 2.1.2 < 3.0.0" 197 | }, 198 | "engines": { 199 | "node": ">=0.10.0" 200 | } 201 | }, 202 | "node_modules/nth-check": { 203 | "version": "2.1.1", 204 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 205 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 206 | "license": "BSD-2-Clause", 207 | "dependencies": { 208 | "boolbase": "^1.0.0" 209 | }, 210 | "funding": { 211 | "url": "https://github.com/fb55/nth-check?sponsor=1" 212 | } 213 | }, 214 | "node_modules/parse5": { 215 | "version": "7.2.1", 216 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", 217 | "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", 218 | "license": "MIT", 219 | "dependencies": { 220 | "entities": "^4.5.0" 221 | }, 222 | "funding": { 223 | "url": "https://github.com/inikulin/parse5?sponsor=1" 224 | } 225 | }, 226 | "node_modules/parse5-htmlparser2-tree-adapter": { 227 | "version": "7.1.0", 228 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", 229 | "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", 230 | "license": "MIT", 231 | "dependencies": { 232 | "domhandler": "^5.0.3", 233 | "parse5": "^7.0.0" 234 | }, 235 | "funding": { 236 | "url": "https://github.com/inikulin/parse5?sponsor=1" 237 | } 238 | }, 239 | "node_modules/parse5-parser-stream": { 240 | "version": "7.1.2", 241 | "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", 242 | "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", 243 | "license": "MIT", 244 | "dependencies": { 245 | "parse5": "^7.0.0" 246 | }, 247 | "funding": { 248 | "url": "https://github.com/inikulin/parse5?sponsor=1" 249 | } 250 | }, 251 | "node_modules/safer-buffer": { 252 | "version": "2.1.2", 253 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 254 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 255 | "license": "MIT" 256 | }, 257 | "node_modules/undici": { 258 | "version": "6.21.0", 259 | "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", 260 | "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", 261 | "license": "MIT", 262 | "engines": { 263 | "node": ">=18.17" 264 | } 265 | }, 266 | "node_modules/whatwg-encoding": { 267 | "version": "3.1.1", 268 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", 269 | "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", 270 | "license": "MIT", 271 | "dependencies": { 272 | "iconv-lite": "0.6.3" 273 | }, 274 | "engines": { 275 | "node": ">=18" 276 | } 277 | }, 278 | "node_modules/whatwg-mimetype": { 279 | "version": "4.0.0", 280 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", 281 | "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", 282 | "license": "MIT", 283 | "engines": { 284 | "node": ">=18" 285 | } 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domino", 3 | "version": "1.0.0", 4 | "description": "[![Package Version](https://img.shields.io/hexpm/v/domino)](https://hex.pm/packages/domino) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/domino/)", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cheerio": "^1.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/src/domino.gleam: -------------------------------------------------------------------------------- 1 | import gleam/dynamic/decode 2 | 3 | pub type Domino 4 | 5 | pub type DominoApi 6 | 7 | @external(javascript, "./domino_ffi.mjs", "from_string") 8 | pub fn from_string(_input: String) -> DominoApi 9 | 10 | /// WARNING: Do not call io.debug on the output of this function. It will cause 11 | /// a stack overflow. 12 | @external(javascript, "./domino_ffi.mjs", "select") 13 | pub fn select(domino_api: DominoApi, selector: String) -> Domino 14 | 15 | @external(javascript, "./domino_ffi.mjs", "text") 16 | pub fn text(domino: Domino) -> String 17 | 18 | /// Gets the attribute value for only the first element in the matched set. 19 | @external(javascript, "./domino_ffi.mjs", "attr") 20 | pub fn attr(domino: Domino, name: String) -> String 21 | 22 | /// Gets the attribute value for only the first element in the matched set. 23 | @external(javascript, "./domino_ffi.mjs", "attrs") 24 | pub fn attrs(domino: Domino) -> decode.Dynamic 25 | 26 | @external(javascript, "./domino_ffi.mjs", "length") 27 | pub fn length(domino: Domino) -> Int 28 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/src/domino_ffi.mjs: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | 3 | export function from_string(string) { 4 | return cheerio.load(string); 5 | } 6 | 7 | export function select(cheerio_api, selector) { 8 | return cheerio_api(selector); 9 | } 10 | 11 | export function text(cheerio_object) { 12 | return cheerio_object.text(); 13 | } 14 | 15 | export function attr(cheerio_object, attr_name) { 16 | return cheerio_object.attr(attr_name); 17 | } 18 | 19 | export function attrs(cheerio_object) { 20 | return cheerio_object.attr(); 21 | } 22 | 23 | export function length(cheerio_object) { 24 | return cheerio_object.length; 25 | } 26 | -------------------------------------------------------------------------------- /qcheck_viewer/domino/test/domino_test.gleam: -------------------------------------------------------------------------------- 1 | import domino 2 | import gleam/dynamic/decode 3 | import gleeunit 4 | import gleeunit/should 5 | 6 | pub fn main() { 7 | gleeunit.main() 8 | } 9 | 10 | pub fn text_test() { 11 | domino.from_string("

Hello,

World!

") 12 | |> domino.select("p") 13 | |> domino.text() 14 | |> should.equal("Hello, World!") 15 | } 16 | 17 | pub fn attr_test() { 18 | domino.from_string("

Hello,

World!

") 19 | |> domino.select("p") 20 | |> domino.attr("class") 21 | |> should.equal("first") 22 | } 23 | 24 | pub fn attrs_test() { 25 | let attrs = 26 | domino.from_string( 27 | "

28 | Hello, 29 |

30 |

31 | World! 32 |

", 33 | ) 34 | |> domino.select("p") 35 | |> domino.attrs() 36 | 37 | let decoder = { 38 | use class <- decode.field("class", decode.string) 39 | use style <- decode.field("style", decode.string) 40 | decode.success(#(class, style)) 41 | } 42 | 43 | decode.run(attrs, decoder) 44 | |> should.equal(Ok(#("first greeting", "color: purple; font-weight: bold"))) 45 | } 46 | 47 | pub fn length_test() { 48 | domino.from_string("

Hello,

World!

") 49 | |> domino.select("p") 50 | |> domino.length 51 | |> should.equal(2) 52 | } 53 | 54 | pub fn length_1_test() { 55 | domino.from_string("

Hello,

World!

") 56 | |> domino.select("div") 57 | |> domino.length 58 | |> should.equal(0) 59 | } 60 | -------------------------------------------------------------------------------- /qcheck_viewer/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "qcheck_viewer" 2 | target = "javascript" 3 | version = "1.0.0" 4 | 5 | [dependencies] 6 | gleam_stdlib = ">= 0.44.0 and < 2.0.0" 7 | lustre = ">= 4.6.3 and < 5.0.0" 8 | qcheck = { path = ".." } 9 | gleam_json = ">= 2.3.0 and < 3.0.0" 10 | 11 | [dev-dependencies] 12 | gleeunit = ">= 1.0.0 and < 2.0.0" 13 | lustre_dev_tools = ">= 1.6.5 and < 2.0.0" 14 | domino = { path = "./domino" } 15 | -------------------------------------------------------------------------------- /qcheck_viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | qcheck viewer 8 | 9 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /qcheck_viewer/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 | { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, 7 | { name = "domino", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "domino" }, 8 | { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 9 | { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 10 | { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, 11 | { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, 12 | { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, 13 | { name = "gleam_community_ansi", version = "1.4.2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "479DEDC748D08B310C9FEB9C4CBEC46B95C874F7F4F2844304D6D20CA78A8BB5" }, 14 | { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, 15 | { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, 16 | { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" }, 17 | { name = "gleam_erlang", version = "0.33.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "A1D26B80F01901B59AABEE3475DD4C18D27D58FA5C897D922FCB9B099749C064" }, 18 | { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, 19 | { name = "gleam_httpc", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "76FEEC99473E568EBA34336A37CF3D54629ACE77712950DC9BB097B5FD664664" }, 20 | { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, 21 | { name = "gleam_otp", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "FA0EB761339749B4E82D63016C6A18C4E6662DA05BAB6F1346F9AF2E679E301A" }, 22 | { name = "gleam_package_interface", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "80D8B1842ACC6CF50E53FF1B220FF57E2B3A60FAF19DD885EC683CDED64C2C52" }, 23 | { name = "gleam_regexp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "A3655FDD288571E90EE9C4009B719FEF59FA16AFCDF3952A76A125AF23CF1592" }, 24 | { name = "gleam_stdlib", version = "0.52.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "50703862DF26453B277688FFCDBE9DD4AC45B3BD9742C0B370DB62BC1629A07D" }, 25 | { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 26 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 27 | { name = "glint", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "EA4B47B5A6147CA524AE81862EE1BE1C5A194757B26897233BD26BD3F7A54930" }, 28 | { name = "glisten", version = "7.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "028C0882EAC7ABEDEFBE92CE4D1FEDADE95FA81B1B1AB099C4F91C133BEF2C42" }, 29 | { name = "gramps", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "630BDE35E465511945253A06EBCDE8D5E4B8B1988F4AC6B8FAC297DEF55B4CA2" }, 30 | { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 31 | { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 32 | { name = "lustre", version = "4.6.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "BDF833368F6C8F152F948D5B6A79866E9881CB80CB66C0685B3327E7DCBFB12F" }, 33 | { name = "lustre_dev_tools", version = "1.6.5", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_deque", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_regexp", "gleam_stdlib", "glint", "glisten", "lustre", "mist", "repeatedly", "simplifile", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "3B362BDCBB56B22BF9DFF6765E4C2CD8171374ACE8EA1B75EA3D6F1FB4937094" }, 34 | { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 35 | { name = "mist", version = "4.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "9F87DA535A75AA1B8B37D27E59B1E60A1BE34EB2CA63BE8B211D75E5D10BEEAF" }, 36 | { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 37 | { name = "prng", version = "4.0.1", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib", "gleam_yielder"], otp_app = "prng", source = "hex", outer_checksum = "695AB70E4BE713042062E901975FC08D1EC725B85B808D4786A14C406ADFBCF1" }, 38 | { name = "qcheck", version = "0.0.8", build_tools = ["gleam"], requirements = ["exception", "gleam_regexp", "gleam_stdlib", "gleam_yielder", "prng"], source = "local", path = ".." }, 39 | { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, 40 | { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 41 | { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, 42 | { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 43 | { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, 44 | { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" }, 45 | { name = "wisp", version = "1.5.1", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "DB7968F777CA77B41AAC8067A5151B022E857E1EECF16BFC9D5F914D0F628844" }, 46 | ] 47 | 48 | [requirements] 49 | domino = { path = "./domino" } 50 | gleam_json = { version = ">= 2.3.0 and < 3.0.0" } 51 | gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 52 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 53 | lustre = { version = ">= 4.6.3 and < 5.0.0" } 54 | lustre_dev_tools = { version = ">= 1.6.5 and < 2.0.0" } 55 | qcheck = { path = ".." } 56 | -------------------------------------------------------------------------------- /qcheck_viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qcheck_viewer", 3 | "version": "1.0.0", 4 | "description": "[![Package Version](https://img.shields.io/hexpm/v/qcheck_viewer)](https://hex.pm/packages/qcheck_viewer) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/qcheck_viewer/)", 5 | "main": "./src/qcheck_viewer_ffi.mjs", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "vega": "^5.30.0", 16 | "vega-embed": "^6.29.0", 17 | "vega-lite": "^5.23.0" 18 | }, 19 | "type": "module", 20 | "devDependencies": { 21 | "cheerio": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /qcheck_viewer/src/qcheck_viewer.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/json 3 | import gleam/option 4 | import gleam/string 5 | import lustre 6 | import lustre/attribute 7 | import lustre/effect 8 | import lustre/element 9 | import lustre/element/html 10 | import lustre/event 11 | import qcheck 12 | 13 | const default_int_range_low: Int = -100 14 | 15 | const default_int_range_high: Int = 100 16 | 17 | pub const max_int: Int = 2_147_483_647 18 | 19 | pub const min_int: Int = -2_147_483_648 20 | 21 | pub const id_error_message = "error-message" 22 | 23 | pub fn main() { 24 | let app = lustre.application(init, update, view) 25 | let assert Ok(_) = lustre.start(app, "#app", Nil) 26 | 27 | Nil 28 | } 29 | 30 | // MARK: Model 31 | 32 | pub type Model { 33 | Model( 34 | function: QcheckFunction, 35 | int_range_low: Int, 36 | int_range_high: Int, 37 | error_message: option.Option(String), 38 | ) 39 | } 40 | 41 | pub fn default_model() -> Model { 42 | Model( 43 | function: IntUniform, 44 | int_range_low: default_int_range_low, 45 | int_range_high: default_int_range_high, 46 | error_message: option.None, 47 | ) 48 | } 49 | 50 | pub type QcheckFunction { 51 | CharAlpha 52 | CharAlphaNumeric 53 | CharDigit 54 | CharLowercase 55 | CharPrintable 56 | CharPrintableUniform 57 | CharUniformInclusive 58 | CharUppercase 59 | CharWhitespace 60 | Float 61 | FloatUniformInclusive 62 | IntSmallPositiveOrZero 63 | IntSmallStrictlyPositive 64 | IntUniform 65 | IntUniformInclusive 66 | } 67 | 68 | fn qcheck_function_html_options( 69 | model_function: QcheckFunction, 70 | ) -> List(element.Element(_)) { 71 | [ 72 | qcheck_function_to_html_option( 73 | IntSmallPositiveOrZero, 74 | current_function: model_function, 75 | ), 76 | qcheck_function_to_html_option( 77 | IntSmallStrictlyPositive, 78 | current_function: model_function, 79 | ), 80 | qcheck_function_to_html_option(IntUniform, current_function: model_function), 81 | qcheck_function_to_html_option( 82 | IntUniformInclusive, 83 | current_function: model_function, 84 | ), 85 | qcheck_function_to_html_option(Float, current_function: model_function), 86 | qcheck_function_to_html_option( 87 | FloatUniformInclusive, 88 | current_function: model_function, 89 | ), 90 | qcheck_function_to_html_option( 91 | CharUniformInclusive, 92 | current_function: model_function, 93 | ), 94 | qcheck_function_to_html_option( 95 | CharUppercase, 96 | current_function: model_function, 97 | ), 98 | qcheck_function_to_html_option( 99 | CharLowercase, 100 | current_function: model_function, 101 | ), 102 | qcheck_function_to_html_option(CharDigit, current_function: model_function), 103 | qcheck_function_to_html_option( 104 | CharPrintableUniform, 105 | current_function: model_function, 106 | ), 107 | qcheck_function_to_html_option(CharAlpha, current_function: model_function), 108 | qcheck_function_to_html_option( 109 | CharAlphaNumeric, 110 | current_function: model_function, 111 | ), 112 | qcheck_function_to_html_option( 113 | CharWhitespace, 114 | current_function: model_function, 115 | ), 116 | qcheck_function_to_html_option( 117 | CharPrintable, 118 | current_function: model_function, 119 | ), 120 | ] 121 | } 122 | 123 | fn qcheck_function_to_html_option( 124 | qcheck_function: QcheckFunction, 125 | current_function current_function: QcheckFunction, 126 | ) -> element.Element(_) { 127 | html.option( 128 | [ 129 | qcheck_function_to_attribute(qcheck_function), 130 | attribute.selected(qcheck_function == current_function), 131 | ], 132 | qcheck_function_to_string(qcheck_function), 133 | ) 134 | } 135 | 136 | pub fn qcheck_function_to_string(qcheck_function: QcheckFunction) -> String { 137 | case qcheck_function { 138 | IntUniform -> "uniform_int" 139 | IntUniformInclusive -> "bounded_int" 140 | IntSmallStrictlyPositive -> "small_strictly_positive_int" 141 | IntSmallPositiveOrZero -> "small_non_negative_int" 142 | Float -> "float" 143 | FloatUniformInclusive -> "bounded_float" 144 | CharUniformInclusive -> "bounded_codepoint" 145 | CharUppercase -> "uppercase_ascii_codepoint" 146 | CharLowercase -> "lowercase_ascii_codepoint" 147 | CharDigit -> "ascii_digit_codepoint" 148 | CharPrintableUniform -> "uniform_printable_ascii_codepoint" 149 | CharAlpha -> "alphabetic_ascii_codepoint" 150 | CharAlphaNumeric -> "alphanumeric_ascii_codepoint" 151 | CharWhitespace -> "ascii_whitespace_codepoint" 152 | CharPrintable -> "printable_ascii_codepoint" 153 | } 154 | } 155 | 156 | fn qcheck_function_to_attribute( 157 | qcheck_function: QcheckFunction, 158 | ) -> attribute.Attribute(_) { 159 | qcheck_function |> qcheck_function_to_string |> attribute.value 160 | } 161 | 162 | fn qcheck_function_from_string( 163 | function_name: String, 164 | ) -> Result(QcheckFunction, String) { 165 | case function_name { 166 | "uniform_int" -> Ok(IntUniform) 167 | "bounded_int" -> Ok(IntUniformInclusive) 168 | "small_strictly_positive_int" -> Ok(IntSmallPositiveOrZero) 169 | "small_non_negative_int" -> Ok(IntSmallStrictlyPositive) 170 | "float" -> Ok(Float) 171 | "bounded_float" -> Ok(FloatUniformInclusive) 172 | "bounded_codepoint" -> Ok(CharUniformInclusive) 173 | "uppercase_ascii_codepoint" -> Ok(CharUppercase) 174 | "lowercase_ascii_codepoint" -> Ok(CharLowercase) 175 | "ascii_digit_codepoint" -> Ok(CharDigit) 176 | "uniform_printable_ascii_codepoint" -> Ok(CharPrintableUniform) 177 | "alphabetic_ascii_codepoint" -> Ok(CharAlpha) 178 | "alphanumeric_ascii_codepoint" -> Ok(CharAlphaNumeric) 179 | "ascii_whitespace_codepoint" -> Ok(CharWhitespace) 180 | "printable_ascii_codepoint" -> Ok(CharPrintable) 181 | _ -> Error("bad function name") 182 | } 183 | } 184 | 185 | fn init(_flags: _) -> #(Model, effect.Effect(Msg)) { 186 | #(default_model(), effect.from(fn(dispatch) { dispatch(EmbedPlot) })) 187 | } 188 | 189 | // MARK: Update 190 | 191 | pub type Msg { 192 | UserChangedFunction(String) 193 | UserUpdatedIntRangeLow(Result(Int, String)) 194 | UserUpdatedIntRangeHigh(Result(Int, String)) 195 | EmbedPlot 196 | SetErrorMessage(String) 197 | DismissError 198 | } 199 | 200 | pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { 201 | case msg { 202 | UserChangedFunction(function) -> { 203 | case qcheck_function_from_string(function) { 204 | Ok(function) -> #(Model(..model, function:), effect.none()) 205 | Error(message) -> #( 206 | Model(..model, error_message: option.Some(message)), 207 | effect.none(), 208 | ) 209 | } 210 | } 211 | EmbedPlot -> { 212 | #(model, effect.from(fn(_) { model |> generate_histogram |> embed_plot })) 213 | } 214 | SetErrorMessage(error_message) -> #( 215 | Model(..model, error_message: option.Some(error_message)), 216 | effect.none(), 217 | ) 218 | DismissError -> #(Model(..model, error_message: option.None), effect.none()) 219 | UserUpdatedIntRangeLow(Ok(int_range_low)) -> #( 220 | Model(..model, int_range_low:), 221 | effect.none(), 222 | ) 223 | UserUpdatedIntRangeHigh(Ok(int_range_high)) -> #( 224 | Model(..model, int_range_high:), 225 | effect.none(), 226 | ) 227 | UserUpdatedIntRangeLow(Error(error_message)) -> #( 228 | Model( 229 | ..model, 230 | int_range_low: default_int_range_low, 231 | error_message: option.Some(error_message), 232 | ), 233 | effect.none(), 234 | ) 235 | UserUpdatedIntRangeHigh(Error(error_message)) -> #( 236 | Model( 237 | ..model, 238 | int_range_high: default_int_range_high, 239 | error_message: option.Some(error_message), 240 | ), 241 | effect.none(), 242 | ) 243 | } 244 | } 245 | 246 | // MARK: View 247 | 248 | pub fn view(model: Model) -> element.Element(Msg) { 249 | html.div([], [ 250 | html.h1([], [element.text("qcheck generator viewer")]), 251 | html.p([], [ 252 | element.text( 253 | "Select a function name, and set any required options to create a histogram of generated values.", 254 | ), 255 | ]), 256 | html.p([], [ 257 | element.text("Note that generated values for "), 258 | html.code([], [element.text("char_utf_codepoint")]), 259 | element.text(" are represented as integers."), 260 | ]), 261 | maybe_show_error(model.error_message), 262 | // Options 263 | html.div([], [ 264 | html.h2([], [element.text("Options")]), 265 | // Select function 266 | select_function(model), 267 | html.br([]), 268 | maybe_function_options(model), 269 | // html.br([]), 270 | maybe_generate_button(model.error_message), 271 | ]), 272 | // Plot 273 | html.div([], [ 274 | html.h2([], [element.text("Data")]), 275 | html.div([attribute.id("plot")], [element.text("Click 'Generate'")]), 276 | ]), 277 | ]) 278 | } 279 | 280 | fn select_function(model: Model) { 281 | html.label([], [ 282 | html.text("qcheck function"), 283 | html.br([]), 284 | html.select( 285 | [event.on_input(UserChangedFunction)], 286 | qcheck_function_html_options(model.function), 287 | ), 288 | ]) 289 | } 290 | 291 | fn maybe_show_error(error_message) { 292 | case error_message { 293 | option.None -> element.none() 294 | option.Some(error_message) -> { 295 | html.div( 296 | [ 297 | attribute.id(id_error_message), 298 | attribute.style([ 299 | #("background", "#fee2e2"), 300 | #("padding", "1rem"), 301 | #("margin", "1rem 0"), 302 | #("border-radius", "0.375rem"), 303 | ]), 304 | ], 305 | [ 306 | html.span([], [element.text(error_message)]), 307 | html.button( 308 | [ 309 | event.on_click(DismissError), 310 | attribute.style([#("margin-left", "1rem")]), 311 | ], 312 | [element.text("x")], 313 | ), 314 | ], 315 | ) 316 | } 317 | } 318 | } 319 | 320 | fn maybe_function_options(model: Model) { 321 | case model.function { 322 | // TODO: make a float input box for the float functions. 323 | IntUniformInclusive | FloatUniformInclusive | CharUniformInclusive -> { 324 | html.div([], [ 325 | html.label([], [ 326 | html.text("High"), 327 | html.br([]), 328 | html.input([ 329 | attribute.name("range-high"), 330 | attribute.type_("number"), 331 | attribute.property("value", model.int_range_high), 332 | event.on_input(parse_int_range_high(_, low: model.int_range_low)), 333 | ]), 334 | ]), 335 | html.br([]), 336 | html.label([], [ 337 | html.text("Low"), 338 | html.br([]), 339 | html.input([ 340 | attribute.name("range-low"), 341 | attribute.type_("number"), 342 | attribute.property("value", model.int_range_low), 343 | event.on_input(parse_int_range_low(_, high: model.int_range_high)), 344 | ]), 345 | ]), 346 | ]) 347 | } 348 | _ -> element.none() 349 | } 350 | } 351 | 352 | fn maybe_generate_button(error_message) { 353 | case error_message { 354 | option.None -> 355 | html.button([event.on_click(EmbedPlot)], [element.text("Generate")]) 356 | option.Some(_) -> element.none() 357 | } 358 | } 359 | 360 | // TODO: if user types `-` as if to start a negative number, it will give an 361 | // error because that doesn't parse. It's a bit confusing. 362 | 363 | pub fn parse_int_range_low(new_low, high high) { 364 | case int.parse(new_low) { 365 | Ok(low) if low >= high -> 366 | UserUpdatedIntRangeLow(Error("bad int range: low >= high")) 367 | Ok(low) if low < min_int -> 368 | UserUpdatedIntRangeLow(Error( 369 | "bad int range: low < " <> int.to_string(min_int), 370 | )) 371 | Ok(low) if low > max_int -> 372 | UserUpdatedIntRangeLow(Error( 373 | "bad int range: low > " <> int.to_string(max_int), 374 | )) 375 | Ok(low) -> UserUpdatedIntRangeLow(Ok(low)) 376 | Error(Nil) -> UserUpdatedIntRangeLow(Error("bad int range low")) 377 | } 378 | } 379 | 380 | pub fn parse_int_range_high(new_high, low low) { 381 | case int.parse(new_high) { 382 | Ok(high) if high <= low -> 383 | UserUpdatedIntRangeHigh(Error("bad int range: high <= low")) 384 | Ok(high) if high < min_int -> 385 | UserUpdatedIntRangeHigh(Error( 386 | "bad int range: high < " <> int.to_string(min_int), 387 | )) 388 | Ok(high) if high > max_int -> 389 | UserUpdatedIntRangeHigh(Error( 390 | "bad int range: high > " <> int.to_string(max_int), 391 | )) 392 | Ok(high) -> UserUpdatedIntRangeHigh(Ok(high)) 393 | Error(Nil) -> UserUpdatedIntRangeHigh(Error("bad int range high")) 394 | } 395 | } 396 | 397 | // MARK: Plots 398 | 399 | // This is the vega-lite spec for histogram. 400 | fn histogram(from entries, of inner_type, bin bin) { 401 | json.object([ 402 | #("$schema", json.string("https://vega.github.io/schema/vega-lite/v5.json")), 403 | #( 404 | "data", 405 | json.object([#("values", json.array(from: entries, of: inner_type))]), 406 | ), 407 | #("mark", json.string("bar")), 408 | #( 409 | "encoding", 410 | json.object([ 411 | #( 412 | "y", 413 | json.object(case bin { 414 | True -> [#("bin", json.bool(True)), #("field", json.string("data"))] 415 | False -> [#("field", json.string("data"))] 416 | }), 417 | ), 418 | #("x", json.object([#("aggregate", json.string("count"))])), 419 | ]), 420 | ), 421 | ]) 422 | } 423 | 424 | @external(javascript, "./qcheck_viewer_ffi.mjs", "vega_embed") 425 | fn vega_embed(id: String, vega_lite_spec: json.Json) -> Nil 426 | 427 | fn embed_plot(vega_lite_spec: json.Json) -> Nil { 428 | vega_embed("#plot", vega_lite_spec) 429 | } 430 | 431 | fn generate_histogram(model: Model) -> json.Json { 432 | case model.function { 433 | IntUniform -> gen_histogram(qcheck.uniform_int(), of: json.int, bin: True) 434 | IntUniformInclusive -> 435 | gen_histogram( 436 | qcheck.bounded_int(model.int_range_low, model.int_range_high), 437 | of: json.int, 438 | bin: True, 439 | ) 440 | IntSmallPositiveOrZero -> 441 | gen_histogram(qcheck.small_non_negative_int(), of: json.int, bin: False) 442 | IntSmallStrictlyPositive -> 443 | gen_histogram( 444 | qcheck.small_strictly_positive_int(), 445 | of: json.int, 446 | bin: False, 447 | ) 448 | Float -> gen_histogram(qcheck.float(), of: json.float, bin: True) 449 | FloatUniformInclusive -> 450 | gen_histogram( 451 | qcheck.bounded_float( 452 | int.to_float(model.int_range_low), 453 | int.to_float(model.int_range_high), 454 | ), 455 | of: json.float, 456 | bin: True, 457 | ) 458 | 459 | CharUniformInclusive -> 460 | gen_histogram( 461 | qcheck.bounded_codepoint(model.int_range_low, model.int_range_high), 462 | of: codepoint_to_json, 463 | bin: False, 464 | ) 465 | CharUppercase -> 466 | gen_histogram( 467 | qcheck.uppercase_ascii_codepoint(), 468 | of: codepoint_to_json, 469 | bin: False, 470 | ) 471 | CharLowercase -> 472 | gen_histogram( 473 | qcheck.lowercase_ascii_codepoint(), 474 | of: codepoint_to_json, 475 | bin: False, 476 | ) 477 | CharDigit -> 478 | gen_histogram( 479 | qcheck.ascii_digit_codepoint(), 480 | of: codepoint_to_json, 481 | bin: False, 482 | ) 483 | CharPrintableUniform -> 484 | gen_histogram( 485 | qcheck.uniform_printable_ascii_codepoint(), 486 | of: codepoint_to_json, 487 | bin: False, 488 | ) 489 | CharAlpha -> 490 | gen_histogram( 491 | qcheck.alphabetic_ascii_codepoint(), 492 | of: codepoint_to_json, 493 | bin: False, 494 | ) 495 | CharAlphaNumeric -> 496 | gen_histogram( 497 | qcheck.alphanumeric_ascii_codepoint(), 498 | of: codepoint_to_json, 499 | bin: False, 500 | ) 501 | CharWhitespace -> 502 | gen_histogram( 503 | qcheck.ascii_whitespace_codepoint(), 504 | of: codepoint_to_json, 505 | bin: False, 506 | ) 507 | CharPrintable -> 508 | gen_histogram( 509 | qcheck.printable_ascii_codepoint(), 510 | of: codepoint_to_json, 511 | bin: False, 512 | ) 513 | } 514 | } 515 | 516 | fn codepoint_to_json(codepoint: UtfCodepoint) -> json.Json { 517 | string.from_utf_codepoints([codepoint]) |> json.string 518 | } 519 | 520 | fn gen_histogram(generator, of to_json, bin bin) { 521 | let #(data, _seed) = 522 | generator |> qcheck.generate(10_000, qcheck.random_seed()) 523 | 524 | data |> histogram(of: to_json, bin:) 525 | } 526 | -------------------------------------------------------------------------------- /qcheck_viewer/src/qcheck_viewer_ffi.mjs: -------------------------------------------------------------------------------- 1 | import vegaEmbed from "../../../../node_modules/vega-embed/build/vega-embed.module.js"; 2 | 3 | export function vega_embed(id, vega_lite_spec) { 4 | requestAnimationFrame(() => { 5 | vegaEmbed(id, vega_lite_spec); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /qcheck_viewer/test/qcheck_viewer_test.gleam: -------------------------------------------------------------------------------- 1 | import domino 2 | import gleam/int 3 | import gleam/option 4 | import gleam/string 5 | import gleeunit 6 | import gleeunit/should 7 | import lustre/element 8 | import qcheck 9 | import qcheck_viewer as qv 10 | 11 | pub fn main() { 12 | gleeunit.main() 13 | } 14 | 15 | // MARK: view error messages 16 | 17 | pub fn view_shows_error_if_it_is_in_model_test() { 18 | let error_message = "yo!!!" 19 | 20 | let model = 21 | qv.Model(..qv.default_model(), error_message: option.Some(error_message)) 22 | 23 | qv.view(model) 24 | |> domino_from_element 25 | |> domino.select("#" <> qv.id_error_message) 26 | |> domino.text 27 | |> string.contains(error_message) 28 | |> should.be_true 29 | } 30 | 31 | pub fn view_doesnt_show_error_if_it_is_not_in_model_test() { 32 | let model = qv.Model(..qv.default_model(), error_message: option.None) 33 | 34 | qv.view(model) 35 | |> domino_from_element 36 | |> domino.select("#" <> qv.id_error_message) 37 | |> domino.length 38 | |> should.equal(0) 39 | } 40 | 41 | // MARK: view function options 42 | 43 | pub fn int_range_high_is_shown_for_correct_functions_test() { 44 | use function <- qcheck.given(qcheck_function_generator()) 45 | let model = qv.Model(..qv.default_model(), function:) 46 | let input = 47 | qv.view(model) 48 | |> domino_from_element 49 | |> domino.select("input[name='range-high']") 50 | 51 | case model.function { 52 | qv.IntUniformInclusive | qv.FloatUniformInclusive -> 53 | should.equal(domino.length(input), 1) 54 | _ -> should.equal(domino.length(input), 0) 55 | } 56 | } 57 | 58 | pub fn int_range_low_is_shown_for_correct_functions_test() { 59 | use function <- qcheck.given(qcheck_function_generator()) 60 | let model = qv.Model(..qv.default_model(), function:) 61 | let input = 62 | qv.view(model) 63 | |> domino_from_element 64 | |> domino.select("input[name='range-low']") 65 | 66 | case model.function { 67 | qv.IntUniformInclusive | qv.FloatUniformInclusive -> 68 | should.equal(domino.length(input), 1) 69 | _ -> should.equal(domino.length(input), 0) 70 | } 71 | } 72 | 73 | // MARK: update 74 | 75 | pub fn user_changed_function_test() { 76 | use function <- qcheck.given(qcheck_function_generator()) 77 | let model = qv.Model(..qv.default_model(), function:) 78 | let msg = qv.UserChangedFunction(qv.qcheck_function_to_string(function)) 79 | let #(model, _) = qv.update(model, msg) 80 | should.be_true( 81 | model.function == function && model.error_message == option.None, 82 | ) 83 | } 84 | 85 | // MARK: parsing ints 86 | 87 | // Parsing ints: min and max values 88 | 89 | pub fn parse_int_range_high__val_is_gte_int_min__test() { 90 | let assert qv.UserUpdatedIntRangeHigh(result) = 91 | { qv.min_int - 1 } 92 | |> int.to_string 93 | |> qv.parse_int_range_high(low: qv.min_int - 2) 94 | 95 | result |> should.be_error 96 | } 97 | 98 | pub fn parse_int_range_low__val_is_gte_int_min__test() { 99 | let assert qv.UserUpdatedIntRangeLow(result) = 100 | { qv.min_int - 1 } 101 | |> int.to_string 102 | |> qv.parse_int_range_low(high: qv.min_int) 103 | 104 | result |> should.be_error 105 | } 106 | 107 | pub fn parse_int_range_high__val_is_lte_int_max__test() { 108 | let assert qv.UserUpdatedIntRangeHigh(result) = 109 | { qv.max_int + 1 } 110 | |> int.to_string 111 | |> qv.parse_int_range_high(low: qv.max_int) 112 | 113 | result |> should.be_error 114 | } 115 | 116 | pub fn parse_int_range_low__val_is_lte_int_max__test() { 117 | let assert qv.UserUpdatedIntRangeLow(result) = 118 | { qv.max_int + 1 } 119 | |> int.to_string 120 | |> qv.parse_int_range_low(high: qv.max_int + 2) 121 | 122 | result |> should.be_error 123 | } 124 | 125 | // Parsing ints: good values 126 | 127 | pub fn parse_int_range_high__good_values__test() { 128 | use #(low, high) <- qcheck.given(good_high_low_values_generator()) 129 | 130 | let assert qv.UserUpdatedIntRangeHigh(Ok(result)) = 131 | qv.parse_int_range_high(int.to_string(high), low:) 132 | 133 | should.equal(result, high) 134 | } 135 | 136 | pub fn parse_int_range_low__good_values__test() { 137 | use #(low, high) <- qcheck.given(good_high_low_values_generator()) 138 | 139 | let assert qv.UserUpdatedIntRangeLow(Ok(result)) = 140 | qv.parse_int_range_low(int.to_string(low), high: high) 141 | 142 | should.equal(result, low) 143 | } 144 | 145 | fn good_high_low_values_generator() { 146 | use low <- qcheck.bind(qcheck.bounded_int(qv.min_int, qv.max_int - 1)) 147 | use high <- qcheck.map(qcheck.bounded_int(low, qv.max_int)) 148 | let assert True = qv.min_int <= low && low < high && high <= qv.max_int 149 | #(low, high) 150 | } 151 | 152 | // Parsing ints: Low-high ordering 153 | 154 | pub fn parse_int_range_high__high_must_be_gt_low__test() { 155 | use #(low, high) <- qcheck.given(high_lt_low_generator()) 156 | 157 | let assert qv.UserUpdatedIntRangeHigh(result) = 158 | qv.parse_int_range_high(int.to_string(high), low:) 159 | 160 | should.be_error(result) |> ignore 161 | } 162 | 163 | fn ignore(_) { 164 | Nil 165 | } 166 | 167 | pub fn parse_int_range_low__high_must_be_gt_low__test() { 168 | use #(low, high) <- qcheck.given(high_lt_low_generator()) 169 | 170 | let assert qv.UserUpdatedIntRangeLow(result) = 171 | qv.parse_int_range_low(int.to_string(low), high:) 172 | 173 | should.be_error(result) |> ignore 174 | } 175 | 176 | fn high_lt_low_generator() { 177 | use low <- qcheck.bind(qcheck.bounded_int(qv.min_int + 1, qv.max_int)) 178 | use high <- qcheck.map(qcheck.bounded_int(qv.min_int, low)) 179 | let assert True = high < low 180 | #(low, high) 181 | } 182 | 183 | // Parsing non-integers 184 | 185 | pub fn parse_int_range_high__high_must_be_an_int__test() { 186 | use high <- qcheck.given(non_digit_char_generator()) 187 | 188 | let assert qv.UserUpdatedIntRangeHigh(result) = 189 | qv.parse_int_range_high(high, low: qv.min_int) 190 | 191 | should.be_error(result) |> ignore 192 | } 193 | 194 | pub fn parse_int_range_low__low_must_be_an_int__test() { 195 | use low <- qcheck.given(non_digit_char_generator()) 196 | 197 | let assert qv.UserUpdatedIntRangeLow(result) = 198 | qv.parse_int_range_low(low, high: qv.max_int) 199 | 200 | should.be_error(result) |> ignore 201 | } 202 | 203 | fn non_digit_char_generator() { 204 | use codepoint <- qcheck.map(qcheck.codepoint()) 205 | let char = string.from_utf_codepoints([codepoint]) 206 | case char { 207 | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> "a" 208 | char -> char 209 | } 210 | } 211 | 212 | // MARK: Type generators 213 | 214 | fn qcheck_function_generator() { 215 | qcheck.from_generators(qcheck.return(qv.IntUniform), [ 216 | qcheck.return(qv.IntUniformInclusive), 217 | qcheck.return(qv.IntSmallPositiveOrZero), 218 | qcheck.return(qv.IntSmallStrictlyPositive), 219 | qcheck.return(qv.Float), 220 | qcheck.return(qv.FloatUniformInclusive), 221 | qcheck.return(qv.CharUniformInclusive), 222 | qcheck.return(qv.CharUppercase), 223 | qcheck.return(qv.CharLowercase), 224 | qcheck.return(qv.CharDigit), 225 | qcheck.return(qv.CharPrintableUniform), 226 | qcheck.return(qv.CharAlpha), 227 | qcheck.return(qv.CharAlphaNumeric), 228 | qcheck.return(qv.CharWhitespace), 229 | qcheck.return(qv.CharPrintable), 230 | ]) 231 | } 232 | 233 | // MARK: domino utils 234 | 235 | fn domino_from_element(element) { 236 | element |> element.to_string |> domino.from_string 237 | } 238 | -------------------------------------------------------------------------------- /src/qcheck/random.gleam: -------------------------------------------------------------------------------- 1 | //// Random 2 | //// 3 | //// The random module provides basic random value generators that can be used 4 | //// to define Generators. 5 | //// 6 | //// They are mostly inteded for internal use or "advanced" manual construction 7 | //// of generators. In typical usage, you will probably not need to interact 8 | //// with these functions much, if at all. As such, they are currently mostly 9 | //// undocumented. 10 | //// 11 | 12 | import gleam/int 13 | import gleam/list 14 | import gleam/order.{Eq, Gt, Lt} 15 | import gleam/pair 16 | import gleam/yielder.{type Yielder} 17 | import prng/random as prng_random 18 | import prng/seed as prng_seed 19 | 20 | // MARK: Seeds 21 | 22 | /// An opaque type representing a seed value used to initialize random generators. 23 | /// 24 | pub opaque type Seed { 25 | Seed(seed: prng_seed.Seed) 26 | } 27 | 28 | /// `seed(n) creates a new seed from the given integer, `n`. 29 | /// 30 | /// ### Example 31 | /// 32 | /// Use a specific seed for the `Config`. 33 | /// 34 | /// ``` 35 | /// let config = 36 | /// qcheck.default_config() 37 | /// |> qcheck.with_seed(qcheck.seed(124)) 38 | /// ``` 39 | /// 40 | pub fn seed(n: Int) -> Seed { 41 | prng_seed.new(n) |> Seed 42 | } 43 | 44 | /// `random_seed()` creates a new randomly-generated seed. You can use it when 45 | /// you don't care about having specifically reproducible results. 46 | /// 47 | /// ### Example 48 | /// 49 | /// Use a random seed for the `Config`. 50 | /// 51 | /// ``` 52 | /// let config = 53 | /// qcheck.default_config() 54 | /// |> qcheck.with_seed(qcheck.random_seed()) 55 | /// ``` 56 | /// 57 | pub fn random_seed() -> Seed { 58 | prng_seed.random() |> Seed 59 | } 60 | 61 | /// Attempting to generate values below this limit will not lead to good random results. 62 | /// 63 | pub const min_int = prng_random.min_int 64 | 65 | /// Attempting to generate values below this limit will not lead to good random results. 66 | /// 67 | pub const max_int = prng_random.max_int 68 | 69 | pub opaque type Generator(a) { 70 | Generator(generator: prng_random.Generator(a)) 71 | } 72 | 73 | pub fn step(generator: Generator(a), seed: Seed) -> #(a, Seed) { 74 | let #(a, seed) = prng_random.step(generator.generator, seed.seed) 75 | #(a, Seed(seed)) 76 | } 77 | 78 | pub fn int(from from: Int, to to: Int) -> Generator(Int) { 79 | prng_random.int(from, to) |> Generator 80 | } 81 | 82 | pub fn float(from from: Float, to to: Float) -> Generator(Float) { 83 | prng_random.float(from, to) |> Generator 84 | } 85 | 86 | /// Like `weighted` but uses `Floats` to specify the weights. 87 | /// 88 | /// Generally you should prefer `weighted` as it is faster. 89 | /// 90 | pub fn float_weighted( 91 | first: #(Float, a), 92 | others: List(#(Float, a)), 93 | ) -> Generator(a) { 94 | prng_random.weighted(first, others) |> Generator 95 | } 96 | 97 | pub fn weighted(first: #(Int, a), others: List(#(Int, a))) -> Generator(a) { 98 | let normalise = fn(pair: #(Int, a)) { int.absolute_value(pair.first(pair)) } 99 | let total = normalise(first) + int.sum(list.map(others, normalise)) 100 | 101 | prng_random.map(prng_random.int(0, total - 1), get_by_weight(first, others, _)) 102 | |> Generator 103 | } 104 | 105 | pub fn uniform(first: a, others: List(a)) -> Generator(a) { 106 | weighted(#(1, first), list.map(others, pair.new(1, _))) 107 | } 108 | 109 | pub fn choose(one: a, other: a) -> Generator(a) { 110 | uniform(one, [other]) 111 | } 112 | 113 | fn get_by_weight(first: #(Int, a), others: List(#(Int, a)), countdown: Int) -> a { 114 | let #(weight, value) = first 115 | case others { 116 | [] -> value 117 | [second, ..rest] -> { 118 | let positive_weight = int.absolute_value(weight) 119 | case int.compare(countdown, positive_weight) { 120 | Lt -> value 121 | Gt | Eq -> get_by_weight(second, rest, countdown - positive_weight) 122 | } 123 | } 124 | } 125 | } 126 | 127 | pub fn bind(generator: Generator(a), f: fn(a) -> Generator(b)) -> Generator(b) { 128 | prng_random.then(generator.generator, fn(a) { 129 | // We need to unwrap and wrap the values of this function since we're 130 | // "hiding" the prng.random implementation. 131 | let generator = f(a) 132 | generator.generator 133 | }) 134 | |> Generator 135 | } 136 | 137 | /// `then` is an alias for `bind`. 138 | /// 139 | pub fn then(generator: Generator(a), f: fn(a) -> Generator(b)) -> Generator(b) { 140 | bind(generator, f) 141 | } 142 | 143 | pub fn map(generator: Generator(a), fun: fn(a) -> b) -> Generator(b) { 144 | prng_random.map(generator.generator, fun) |> Generator 145 | } 146 | 147 | pub fn to_random_yielder(generator: Generator(a)) -> Yielder(a) { 148 | prng_random.to_random_yielder(generator.generator) 149 | } 150 | 151 | pub fn to_yielder(generator: Generator(a), seed: Seed) -> Yielder(a) { 152 | prng_random.to_yielder(generator.generator, seed.seed) 153 | } 154 | 155 | pub fn random_sample(generator: Generator(a)) -> a { 156 | prng_random.random_sample(generator.generator) 157 | } 158 | 159 | pub fn sample(generator: Generator(a), seed: Seed) -> a { 160 | prng_random.sample(generator.generator, seed.seed) 161 | } 162 | 163 | pub fn constant(value: a) -> Generator(a) { 164 | prng_random.constant(value) |> Generator 165 | } 166 | -------------------------------------------------------------------------------- /src/qcheck/shrink.gleam: -------------------------------------------------------------------------------- 1 | //// Shrinking helper functions 2 | //// 3 | //// This module contains helper functions that can be used to build custom generators (not by composing other generators). 4 | //// 5 | //// They are mostly intended for internal use or "advanced" manual construction 6 | //// of generators. In typical usage, you will probably not need to interact 7 | //// with these functions much, if at all. As such, they are currently mostly 8 | //// undocumented. 9 | //// 10 | //// In fact, if you are using these functions a lot, file a issue on GitHub 11 | //// and let me know if there are any generator combinators that you're missing. 12 | //// 13 | 14 | import gleam/yielder 15 | 16 | fn float_half_difference(x: Float, y: Float) -> Float { 17 | { x /. 2.0 } -. { y /. 2.0 } 18 | } 19 | 20 | fn int_half_difference(x: Int, y: Int) -> Int { 21 | { x / 2 } - { y / 2 } 22 | } 23 | 24 | fn int_shrink_step( 25 | x x: Int, 26 | current_shrink current_shrink: Int, 27 | ) -> yielder.Step(Int, Int) { 28 | case x == current_shrink { 29 | True -> yielder.Done 30 | False -> { 31 | let half_difference = int_half_difference(x, current_shrink) 32 | 33 | case half_difference == 0 { 34 | True -> { 35 | yielder.Next(current_shrink, x) 36 | } 37 | False -> { 38 | yielder.Next(current_shrink, current_shrink + half_difference) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | 45 | fn float_shrink_step( 46 | x x: Float, 47 | current_shrink current_shrink: Float, 48 | ) -> yielder.Step(Float, Float) { 49 | case x == current_shrink { 50 | True -> yielder.Done 51 | False -> { 52 | let half_difference = float_half_difference(x, current_shrink) 53 | 54 | case half_difference == 0.0 { 55 | True -> { 56 | yielder.Next(current_shrink, x) 57 | } 58 | False -> { 59 | yielder.Next(current_shrink, current_shrink +. half_difference) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | pub fn int_towards(destination: Int) -> fn(Int) -> yielder.Yielder(Int) { 67 | fn(x) { 68 | yielder.unfold(destination, fn(current_shrink) { 69 | int_shrink_step(x: x, current_shrink: current_shrink) 70 | }) 71 | } 72 | } 73 | 74 | pub fn float_towards(destination: Float) -> fn(Float) -> yielder.Yielder(Float) { 75 | fn(x) { 76 | yielder.unfold(destination, fn(current_shrink) { 77 | float_shrink_step(x: x, current_shrink: current_shrink) 78 | }) 79 | // (Arbitrarily) Limit to the first 15 elements as dividing a `Float` by 2 80 | // doesn't converge quickly towards the destination. 81 | |> yielder.take(15) 82 | } 83 | } 84 | 85 | /// The `atomic` shrinker treats types as atomic, and never attempts to produce 86 | /// smaller values. 87 | pub fn atomic() -> fn(a) -> yielder.Yielder(a) { 88 | fn(_) { yielder.empty() } 89 | } 90 | -------------------------------------------------------------------------------- /src/qcheck/test_error_message.gleam: -------------------------------------------------------------------------------- 1 | //// This is an internal module used for printing qcheck test errors. 2 | //// 3 | 4 | import gleam/option.{Some} 5 | import gleam/regexp 6 | import gleam/result 7 | import gleam/string 8 | 9 | pub opaque type TestErrorMessage { 10 | TestErrorMessage( 11 | original_value: String, 12 | shrunk_value: String, 13 | shrink_steps: String, 14 | ) 15 | } 16 | 17 | pub fn shrunk_value(msg: TestErrorMessage) -> String { 18 | msg.shrunk_value 19 | } 20 | 21 | fn new_test_error_message( 22 | original_value original_value: String, 23 | shrunk_value shrunk_value: String, 24 | shrink_steps shrink_steps: String, 25 | ) -> TestErrorMessage { 26 | TestErrorMessage( 27 | original_value: original_value, 28 | shrunk_value: shrunk_value, 29 | shrink_steps: shrink_steps, 30 | ) 31 | } 32 | 33 | fn regexp_first_submatch( 34 | pattern pattern: String, 35 | in value: String, 36 | ) -> Result(String, String) { 37 | regexp.from_string(pattern) 38 | // Convert regexp.CompileError to a String 39 | |> result.map_error(string.inspect) 40 | // Apply the regular expression 41 | |> result.map(regexp.scan(_, value)) 42 | // We should see only a single match 43 | |> result.then(fn(matches) { 44 | case matches { 45 | [match] -> Ok(match) 46 | _ -> Error("expected exactly one match") 47 | } 48 | }) 49 | // We should see only a single successful submatch 50 | |> result.then(fn(match) { 51 | let regexp.Match(_content, submatches) = match 52 | 53 | case submatches { 54 | [Some(submatch)] -> Ok(submatch) 55 | _ -> Error("expected exactly one submatch") 56 | } 57 | }) 58 | } 59 | 60 | /// Mainly for asserting values in qcheck internal tests. 61 | /// 62 | fn get_original_value(test_error_str: String) -> Result(String, String) { 63 | regexp_first_submatch(pattern: "original_value: (.+?);", in: test_error_str) 64 | } 65 | 66 | /// Mainly for asserting values in qcheck internal tests. 67 | /// 68 | fn get_shrunk_value(test_error_str: String) -> Result(String, String) { 69 | regexp_first_submatch(pattern: "shrunk_value: (.+?);", in: test_error_str) 70 | } 71 | 72 | /// Mainly for asserting values in qcheck internal tests. 73 | /// 74 | fn get_shrink_steps(test_error_str: String) -> Result(String, String) { 75 | regexp_first_submatch(pattern: "shrink_steps: (.+?);", in: test_error_str) 76 | } 77 | 78 | /// This function should only be called to rescue a function that may call 79 | /// `failwith` at some point to raise an exception. It will likely 80 | /// raise otherwise. 81 | /// 82 | /// This function is internal. Breaking changes may occur without a major 83 | /// version update. 84 | /// 85 | pub fn rescue(thunk: fn() -> a) -> Result(a, TestErrorMessage) { 86 | case rescue_error(thunk) { 87 | Ok(a) -> Ok(a) 88 | Error(err) -> { 89 | // If this assert causes a panic, then you have an implementation error. 90 | let assert Ok(test_error_message) = { 91 | use original_value <- result.then(get_original_value(err)) 92 | use shrunk_value <- result.then(get_shrunk_value(err)) 93 | use shrink_steps <- result.then(get_shrink_steps(err)) 94 | 95 | Ok(new_test_error_message( 96 | original_value: original_value, 97 | shrunk_value: shrunk_value, 98 | shrink_steps: shrink_steps, 99 | )) 100 | } 101 | 102 | Error(test_error_message) 103 | } 104 | } 105 | } 106 | 107 | @external(erlang, "qcheck_ffi", "rescue_error") 108 | @external(javascript, "../qcheck_ffi.mjs", "rescue_error") 109 | fn rescue_error(f: fn() -> a) -> Result(a, String) 110 | -------------------------------------------------------------------------------- /src/qcheck/tree.gleam: -------------------------------------------------------------------------------- 1 | //// Trees 2 | //// 3 | //// This module contains functions for creating and manipulating shrink trees. 4 | //// 5 | //// They are mostly inteded for internal use or "advanced" manual construction 6 | //// of generators. In typical usage, you will probably not need to interact 7 | //// with these functions much, if at all. As such, they are currently mostly 8 | //// undocumented. 9 | //// 10 | //// In fact, if you are using these functions a lot, file a issue on GitHub 11 | //// and let me know if there are any generator combinators that you're missing. 12 | //// 13 | //// There are functions for dealing with the [Tree](#Tree) type directly, but 14 | //// they are low-level and you should not need to use them much. 15 | //// 16 | //// - The [Tree](#Tree) type 17 | //// - [new](#new) 18 | //// - [return](#return) 19 | //// - [map](#map) 20 | //// - [map2](#map2) 21 | //// - [bind](#bind) 22 | //// - [apply](#apply) 23 | //// - [collect](#collect) 24 | //// - [sequence_trees](#sequence_trees) 25 | //// - [option](#option) 26 | //// - [to_string](#to_string) 27 | //// - [to_string_with_max_depth](#to_string_with_max_depth) 28 | //// 29 | 30 | import gleam/option.{type Option, None, Some} 31 | import gleam/string 32 | import gleam/yielder.{type Yielder} 33 | 34 | pub type Tree(a) { 35 | Tree(a, Yielder(Tree(a))) 36 | } 37 | 38 | // `shrink` should probably be `shrink_steps` or `make_shrink_steps` 39 | pub fn new(x: a, shrink: fn(a) -> Yielder(a)) -> Tree(a) { 40 | let shrink_trees = 41 | shrink(x) 42 | |> yielder.map(new(_, shrink)) 43 | 44 | Tree(x, shrink_trees) 45 | } 46 | 47 | pub fn map(tree: Tree(a), f: fn(a) -> b) -> Tree(b) { 48 | let Tree(x, xs) = tree 49 | let y = f(x) 50 | let ys = yielder.map(xs, fn(smaller_x) { map(smaller_x, f) }) 51 | 52 | Tree(y, ys) 53 | } 54 | 55 | pub fn bind(tree: Tree(a), f: fn(a) -> Tree(b)) -> Tree(b) { 56 | let Tree(x, xs) = tree 57 | 58 | let Tree(y, ys_of_x) = f(x) 59 | 60 | let ys_of_xs = yielder.map(xs, fn(smaller_x) { bind(smaller_x, f) }) 61 | 62 | let ys = yielder.append(ys_of_xs, ys_of_x) 63 | 64 | Tree(y, ys) 65 | } 66 | 67 | pub fn apply(f: Tree(fn(a) -> b), x: Tree(a)) -> Tree(b) { 68 | let Tree(x0, xs) = x 69 | let Tree(f0, fs) = f 70 | 71 | let y = f0(x0) 72 | 73 | let ys = 74 | yielder.append( 75 | yielder.map(fs, fn(f_) { apply(f_, x) }), 76 | yielder.map(xs, fn(x_) { apply(f, x_) }), 77 | ) 78 | 79 | Tree(y, ys) 80 | } 81 | 82 | pub fn return(x: a) -> Tree(a) { 83 | Tree(x, yielder.empty()) 84 | } 85 | 86 | pub fn map2(a: Tree(a), b: Tree(b), f: fn(a, b) -> c) -> Tree(c) { 87 | { 88 | use x1 <- parameter 89 | use x2 <- parameter 90 | f(x1, x2) 91 | } 92 | |> return 93 | |> apply(a) 94 | |> apply(b) 95 | } 96 | 97 | /// `sequence_trees(list_of_trees)` sequences a list of trees into a tree of lists. 98 | /// 99 | pub fn sequence_trees(l: List(Tree(a))) -> Tree(List(a)) { 100 | case l { 101 | [] -> return([]) 102 | [hd, ..tl] -> { 103 | map2(hd, sequence_trees(tl), list_cons) 104 | } 105 | } 106 | } 107 | 108 | fn yielder_cons(element: a, yielder: fn() -> Yielder(a)) -> Yielder(a) { 109 | yielder.yield(element, yielder) 110 | } 111 | 112 | pub fn option(tree: Tree(a)) -> Tree(Option(a)) { 113 | let Tree(x, xs) = tree 114 | 115 | // Shrink trees will all have None as a value. 116 | let shrinks = yielder_cons(return(None), fn() { yielder.map(xs, option) }) 117 | 118 | Tree(Some(x), shrinks) 119 | } 120 | 121 | // Debugging trees 122 | 123 | /// Collect values of the tree into a list, while processing them with the 124 | /// mapping given function `f`. 125 | /// 126 | pub fn collect(tree: Tree(a), f: fn(a) -> b) -> List(b) { 127 | do_collect(tree, f, []) 128 | } 129 | 130 | fn do_collect(tree: Tree(a), f: fn(a) -> b, acc: List(b)) -> List(b) { 131 | let Tree(root, children) = tree 132 | 133 | let acc = 134 | yielder.fold(children, acc, fn(a_list, a_tree) { 135 | do_collect(a_tree, f, a_list) 136 | }) 137 | 138 | [f(root), ..acc] 139 | } 140 | 141 | /// `to_string(tree, element_to_string)` converts a tree into an unspecified string representation. 142 | /// 143 | /// - `element_to_string`: a function that converts individual elements of the tree to strings. 144 | /// 145 | pub fn to_string(tree: Tree(a), a_to_string: fn(a) -> String) -> String { 146 | do_to_string(tree, a_to_string, level: 0, max_level: 99_999_999, acc: []) 147 | } 148 | 149 | /// Like `to_string` but with a configurable `max_depth`. 150 | /// 151 | pub fn to_string_max_depth( 152 | tree: Tree(a), 153 | a_to_string: fn(a) -> String, 154 | max_depth: Int, 155 | ) -> String { 156 | do_to_string(tree, a_to_string, level: 0, max_level: max_depth, acc: []) 157 | } 158 | 159 | fn do_to_string( 160 | tree: Tree(a), 161 | a_to_string a_to_string: fn(a) -> String, 162 | level level: Int, 163 | max_level max_level: Int, 164 | acc acc: List(String), 165 | ) -> String { 166 | case tree { 167 | Tree(root, children) -> { 168 | let padding = string.repeat("-", times: level) 169 | 170 | let children = case level > max_level { 171 | False -> 172 | children 173 | |> yielder.map(fn(tree) { 174 | do_to_string(tree, a_to_string, level + 1, max_level, acc) 175 | }) 176 | |> yielder.to_list 177 | |> string.join("") 178 | 179 | True -> 180 | children 181 | |> yielder.map(fn(_) { "" }) 182 | |> yielder.to_list 183 | |> string.join("") 184 | } 185 | 186 | let root = padding <> a_to_string(root) 187 | 188 | root <> "\n" <> children 189 | } 190 | } 191 | } 192 | 193 | fn parameter(f: fn(x) -> y) -> fn(x) -> y { 194 | f 195 | } 196 | 197 | fn list_cons(x, xs) { 198 | [x, ..xs] 199 | } 200 | -------------------------------------------------------------------------------- /src/qcheck_ffi.erl: -------------------------------------------------------------------------------- 1 | -module(qcheck_ffi). 2 | 3 | -export([fail/1, rescue_error/1]). 4 | 5 | -spec fail(string()) -> no_return(). 6 | fail(String) -> 7 | erlang:error(String). 8 | 9 | -spec rescue_error(fun()) -> {ok, any()} | {error, string()}. 10 | rescue_error(F) -> 11 | try 12 | {ok, F()} 13 | catch 14 | error:String -> {error, String} 15 | end. 16 | -------------------------------------------------------------------------------- /src/qcheck_ffi.mjs: -------------------------------------------------------------------------------- 1 | import { Ok, Error as GError } from "./gleam.mjs"; 2 | 3 | export function fail(msg) { 4 | throw new Error(msg); 5 | } 6 | 7 | export function rescue_error(f) { 8 | try { 9 | return new Ok(f()); 10 | } catch (e) { 11 | if (e instanceof Error) { 12 | // `e` should be a string 13 | return new GError(e.message); 14 | } else { 15 | // rethrow the error 16 | throw e; 17 | } 18 | } 19 | } 20 | 21 | export function do_nothing() { 22 | return 1; 23 | } 24 | -------------------------------------------------------------------------------- /test/examples/basic_example_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | 4 | pub fn int_addition_commutativity__test() { 5 | use n <- qcheck.given(qcheck.small_non_negative_int()) 6 | should.equal(n + 1, 1 + n) 7 | } 8 | // Uncomment this function when you need to generate the error message for the basic example in the README. 9 | // 10 | // pub fn int_addition_commutativity__failures_shrink_to_zero__test() { 11 | // use n <- qcheck.given(qcheck.small_non_negative_int()) 12 | // should.not_equal(n + 1, 1 + n) 13 | // } 14 | -------------------------------------------------------------------------------- /test/examples/parameter_example_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | 4 | type Box { 5 | Box(x: Int, y: Int, w: Int, h: Int) 6 | } 7 | 8 | fn box_generator() { 9 | qcheck.return({ 10 | use x <- qcheck.parameter 11 | use y <- qcheck.parameter 12 | use w <- qcheck.parameter 13 | use h <- qcheck.parameter 14 | Box(x:, y:, w:, h:) 15 | }) 16 | |> qcheck.apply(qcheck.bounded_int(-100, 100)) 17 | |> qcheck.apply(qcheck.bounded_int(-100, 100)) 18 | |> qcheck.apply(qcheck.bounded_int(1, 100)) 19 | |> qcheck.apply(qcheck.bounded_int(1, 100)) 20 | } 21 | 22 | pub fn parameter_example__test() { 23 | use _box <- qcheck.given(box_generator()) 24 | 25 | // Test some interesting property of boxes here. 26 | 27 | // (This `True` is a standin for your property.) 28 | should.be_true(True) 29 | } 30 | -------------------------------------------------------------------------------- /test/examples/parsing_example_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/option.{Some} 3 | import gleam/regexp.{Match} 4 | import gleam/result 5 | import gleam/string 6 | import gleeunit/should 7 | import qcheck.{type Generator} 8 | 9 | type Point { 10 | Point(Int, Int) 11 | } 12 | 13 | fn make_point(x: Int, y: Int) -> Point { 14 | Point(x, y) 15 | } 16 | 17 | fn point_generator() -> Generator(Point) { 18 | qcheck.map2(qcheck.uniform_int(), qcheck.uniform_int(), make_point) 19 | } 20 | 21 | fn point_equal(p1: Point, p2: Point) -> Bool { 22 | let Point(x1, y1) = p1 23 | let Point(x2, y2) = p2 24 | 25 | x1 == x2 && y1 == y2 26 | } 27 | 28 | fn point_to_string(point: Point) -> String { 29 | let Point(x, y) = point 30 | "(" <> int.to_string(x) <> " " <> int.to_string(y) <> ")" 31 | } 32 | 33 | fn point_of_string(string: String) -> Result(Point, String) { 34 | use re <- result.try( 35 | // This is the one that is intentionally broken: 36 | // regexp.from_string("\\((\\d+) (\\d+)\\)") 37 | // And this is the one that is fixed to be okay with negative integers. 38 | regexp.from_string("\\((-?\\d+) (-?\\d+)\\)") 39 | |> result.map_error(string.inspect), 40 | ) 41 | 42 | use submatches <- result.try(case regexp.scan(re, string) { 43 | [Match(_content, submatches)] -> Ok(submatches) 44 | _ -> Error("expected a single match") 45 | }) 46 | 47 | use xy <- result.try(case submatches { 48 | [Some(x), Some(y)] -> Ok(#(x, y)) 49 | _ -> Error("expected two submatches") 50 | }) 51 | 52 | use xy <- result.try(case int.parse(xy.0), int.parse(xy.1) { 53 | Ok(x), Ok(y) -> Ok(#(x, y)) 54 | Error(Nil), Ok(_) -> Error("failed to parse x value") 55 | Ok(_), Error(Nil) -> Error("failed to parse y value") 56 | Error(Nil), Error(Nil) -> Error("failed to parse x and y values") 57 | }) 58 | 59 | Ok(Point(xy.0, xy.1)) 60 | } 61 | 62 | pub fn point_serialization_roundtripping__test() { 63 | use generated_point <- qcheck.given(point_generator()) 64 | 65 | let assert Ok(parsed_point) = 66 | generated_point 67 | |> point_to_string 68 | |> point_of_string 69 | 70 | point_equal(generated_point, parsed_point) |> should.be_true 71 | } 72 | -------------------------------------------------------------------------------- /test/examples/using_use_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/string 2 | import gleeunit/should 3 | import qcheck 4 | 5 | const test_count: Int = 2500 6 | 7 | pub fn using_use__test() { 8 | let generator = { 9 | use n <- qcheck.map(qcheck.small_non_negative_int()) 10 | n + 10 11 | } 12 | 13 | use n <- qcheck.given(generator) 14 | should.be_true(n >= 10) 15 | } 16 | 17 | type Person { 18 | Person(name: String, age: Int) 19 | } 20 | 21 | fn make_person(name, age) { 22 | let name = case name { 23 | "" -> Error("name must be a non-empty string") 24 | name -> Ok(name) 25 | } 26 | 27 | let age = case age >= 0 { 28 | False -> Error("age must be >= 0") 29 | True -> Ok(age) 30 | } 31 | 32 | case name, age { 33 | Ok(name), Ok(age) -> Ok(Person(name, age)) 34 | Error(e), Ok(_) | Ok(_), Error(e) -> Error([e]) 35 | Error(e1), Error(e2) -> Error([e1, e2]) 36 | } 37 | } 38 | 39 | fn valid_name_and_age_generator() { 40 | let name_generator = qcheck.non_empty_string() 41 | let age_generator = qcheck.bounded_int(from: 0, to: 129) 42 | 43 | use name, age <- qcheck.map2(name_generator, age_generator) 44 | #(name, age) 45 | } 46 | 47 | pub fn person__test() { 48 | use #(name, age) <- qcheck.run( 49 | qcheck.default_config() |> qcheck.with_test_count(test_count), 50 | valid_name_and_age_generator(), 51 | ) 52 | 53 | make_person(name, age) |> should.be_ok |> ignore 54 | } 55 | 56 | fn ignore(_) { 57 | Nil 58 | } 59 | 60 | pub fn bind_with_use__test() { 61 | let generator = { 62 | use bool <- qcheck.bind(qcheck.bool()) 63 | 64 | case bool { 65 | True -> { 66 | use n <- qcheck.map(qcheck.small_non_negative_int()) 67 | Ok(n) 68 | } 69 | False -> { 70 | use s <- qcheck.map(qcheck.non_empty_string()) 71 | Error(s) 72 | } 73 | } 74 | } 75 | 76 | use generated_value <- qcheck.run( 77 | qcheck.default_config() |> qcheck.with_test_count(test_count), 78 | generator, 79 | ) 80 | 81 | case generated_value { 82 | Ok(n) -> should.be_true(n >= 0) 83 | Error(s) -> { string.length(s) >= 0 } |> should.be_true 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/qcheck/assert_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/string 2 | import gleeunit/should 3 | import qcheck 4 | import qcheck/test_error_message 5 | 6 | pub fn given_assertion_with_be_ok__test() { 7 | use x <- qcheck.given(qcheck.float()) 8 | Ok(x) |> should.be_ok |> ignore 9 | } 10 | 11 | fn ignore(_: a) -> Nil { 12 | Nil 13 | } 14 | 15 | pub fn given_failing__test() { 16 | let assert Error(msg) = { 17 | use <- test_error_message.rescue 18 | use n <- qcheck.given(qcheck.small_non_negative_int()) 19 | n |> should.equal(n + 1) 20 | } 21 | 22 | test_error_message.shrunk_value(msg) 23 | |> should.equal(string.inspect(0)) 24 | } 25 | // TODO: delete this test file 26 | -------------------------------------------------------------------------------- /test/qcheck/config_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | import qcheck/test_error_message 4 | 5 | pub fn negative_seeds_are_ok__test() { 6 | use n <- qcheck.run( 7 | qcheck.default_config() |> qcheck.with_seed(qcheck.seed(-1)), 8 | qcheck.small_strictly_positive_int(), 9 | ) 10 | 11 | should.be_true(n > 0) 12 | } 13 | 14 | pub fn negative_test_counts_are_replaced_with_a_good_value__test() { 15 | let assert Error(_) = { 16 | use <- test_error_message.rescue 17 | use n <- qcheck.run( 18 | qcheck.default_config() |> qcheck.with_test_count(-1), 19 | qcheck.small_strictly_positive_int(), 20 | ) 21 | 22 | // This will only fail if the negative test count is replaced with a 23 | // reasonable value. 24 | // 25 | // If the test count was left as negative or 0, then this property would 26 | // never be executed. 27 | should.be_true(n <= 0) 28 | } 29 | } 30 | 31 | pub fn zero_test_counts_are_replaced_with_a_good_value__test() { 32 | let assert Error(_) = { 33 | use <- test_error_message.rescue 34 | use n <- qcheck.run( 35 | qcheck.default_config() |> qcheck.with_test_count(0), 36 | qcheck.small_strictly_positive_int(), 37 | ) 38 | 39 | should.be_true(n <= 0) 40 | } 41 | } 42 | 43 | pub fn config_replaces_bad_args_with_good_ones__test() { 44 | let assert Error(_) = { 45 | use <- test_error_message.rescue 46 | use n <- qcheck.run( 47 | qcheck.config(test_count: -1, max_retries: -1, seed: qcheck.seed(-1)), 48 | qcheck.small_strictly_positive_int(), 49 | ) 50 | 51 | should.be_true(n <= 0) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/qcheck/gen_algebra_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/option.{Some} 3 | import gleam/regexp 4 | import gleam/result 5 | import gleam/string 6 | import gleeunit/should 7 | import qcheck 8 | import qcheck/test_error_message 9 | 10 | // map 11 | // 12 | // 13 | 14 | pub fn map__test() { 15 | let assert Error(msg) = { 16 | use <- test_error_message.rescue 17 | use n <- qcheck.run( 18 | qcheck.default_config(), 19 | qcheck.small_non_negative_int() |> qcheck.map(int.to_float), 20 | ) 21 | 22 | should.be_true(n == 0.0 || n >. 1.0) 23 | } 24 | 25 | let shrunk_value = test_error_message.shrunk_value(msg) 26 | 27 | // Value differs on Erlang and JS targets. 28 | should.be_true(shrunk_value == "1.0" || shrunk_value == "1") 29 | } 30 | 31 | fn in_range(min, max) { 32 | fn(x) { min <= x && x <= max } 33 | } 34 | 35 | pub fn map2__test() { 36 | let min = -100 37 | let max = 100 38 | 39 | let in_range = in_range(min, max) 40 | 41 | let gen_int = qcheck.bounded_int(min, max) 42 | 43 | use tup2 <- qcheck.run( 44 | qcheck.default_config(), 45 | qcheck.map2(gen_int, gen_int, fn(a, b) { #(a, b) }), 46 | ) 47 | let #(a, b) = tup2 48 | should.be_true(in_range(a) && in_range(b)) 49 | } 50 | 51 | pub fn map3__test() { 52 | let min = -100 53 | let max = 100 54 | 55 | let in_range = in_range(min, max) 56 | 57 | let gen_int = qcheck.bounded_int(min, max) 58 | 59 | use tup3 <- qcheck.run( 60 | qcheck.default_config(), 61 | qcheck.map3(gen_int, gen_int, gen_int, fn(a, b, c) { #(a, b, c) }), 62 | ) 63 | let #(a, b, c) = tup3 64 | should.be_true(in_range(a) && in_range(b) && in_range(c)) 65 | } 66 | 67 | pub fn map4__test() { 68 | let min = -100 69 | let max = 100 70 | 71 | let in_range = in_range(min, max) 72 | 73 | let gen_int = qcheck.bounded_int(min, max) 74 | 75 | use tup4 <- qcheck.run( 76 | qcheck.default_config(), 77 | qcheck.map4(gen_int, gen_int, gen_int, gen_int, fn(a, b, c, d) { 78 | #(a, b, c, d) 79 | }), 80 | ) 81 | let #(a, b, c, d) = tup4 82 | should.be_true(in_range(a) && in_range(b) && in_range(c) && in_range(d)) 83 | } 84 | 85 | pub fn map5__test() { 86 | let min = -100 87 | let max = 100 88 | 89 | let in_range = in_range(min, max) 90 | 91 | let gen_int = qcheck.bounded_int(min, max) 92 | 93 | use tup5 <- qcheck.run( 94 | qcheck.default_config(), 95 | qcheck.map5(gen_int, gen_int, gen_int, gen_int, gen_int, fn(a, b, c, d, e) { 96 | #(a, b, c, d, e) 97 | }), 98 | ) 99 | let #(a, b, c, d, e) = tup5 100 | 101 | should.be_true( 102 | in_range(a) && in_range(b) && in_range(c) && in_range(d) && in_range(e), 103 | ) 104 | } 105 | 106 | pub fn map6__test() { 107 | let min = -100 108 | let max = 100 109 | 110 | let in_range = in_range(min, max) 111 | 112 | let gen_int = qcheck.bounded_int(min, max) 113 | 114 | use tup6 <- qcheck.run( 115 | qcheck.default_config(), 116 | qcheck.map6( 117 | gen_int, 118 | gen_int, 119 | gen_int, 120 | gen_int, 121 | gen_int, 122 | gen_int, 123 | fn(a, b, c, d, e, f) { #(a, b, c, d, e, f) }, 124 | ), 125 | ) 126 | 127 | let #(a, b, c, d, e, f) = tup6 128 | 129 | should.be_true( 130 | in_range(a) 131 | && in_range(b) 132 | && in_range(c) 133 | && in_range(d) 134 | && in_range(e) 135 | && in_range(f), 136 | ) 137 | } 138 | 139 | pub fn map4_with_apply__test() { 140 | let min = -100 141 | let max = 100 142 | 143 | let in_range = in_range(min, max) 144 | 145 | let gen_int = qcheck.bounded_int(min, max) 146 | 147 | let generator = 148 | qcheck.return({ 149 | use a <- qcheck.parameter 150 | use b <- qcheck.parameter 151 | use c <- qcheck.parameter 152 | use d <- qcheck.parameter 153 | #(a, b, c, d) 154 | }) 155 | |> qcheck.apply(gen_int) 156 | |> qcheck.apply(gen_int) 157 | |> qcheck.apply(gen_int) 158 | |> qcheck.apply(gen_int) 159 | 160 | use tup4 <- qcheck.run(qcheck.default_config(), generator) 161 | 162 | let #(a, b, c, d) = tup4 163 | 164 | should.be_true(in_range(a) && in_range(b) && in_range(c) && in_range(d)) 165 | } 166 | 167 | pub fn tuple2__test() { 168 | let min = -100 169 | let max = 100 170 | 171 | let in_range = in_range(min, max) 172 | 173 | let gen_int = qcheck.bounded_int(min, max) 174 | 175 | use tup2 <- qcheck.run( 176 | qcheck.default_config(), 177 | qcheck.tuple2(gen_int, gen_int), 178 | ) 179 | 180 | let #(a, b) = tup2 181 | 182 | should.be_true(in_range(a) && in_range(b)) 183 | } 184 | 185 | pub fn tuple3__test() { 186 | let min = -100 187 | let max = 100 188 | 189 | let in_range = in_range(min, max) 190 | 191 | let gen_int = qcheck.bounded_int(min, max) 192 | 193 | use tup3 <- qcheck.run( 194 | qcheck.default_config(), 195 | qcheck.tuple3(gen_int, gen_int, gen_int), 196 | ) 197 | 198 | let #(a, b, c) = tup3 199 | should.be_true(in_range(a) && in_range(b) && in_range(c)) 200 | } 201 | 202 | pub fn tuple4__test() { 203 | let min = -100 204 | let max = 100 205 | 206 | let in_range = in_range(min, max) 207 | 208 | let gen_int = qcheck.bounded_int(min, max) 209 | 210 | use tup4 <- qcheck.run( 211 | qcheck.default_config(), 212 | qcheck.tuple4(gen_int, gen_int, gen_int, gen_int), 213 | ) 214 | let #(a, b, c, d) = tup4 215 | should.be_true(in_range(a) && in_range(b) && in_range(c) && in_range(d)) 216 | } 217 | 218 | pub fn tuple5__test() { 219 | let min = -100 220 | let max = 100 221 | 222 | let in_range = in_range(min, max) 223 | 224 | let gen_int = qcheck.bounded_int(min, max) 225 | 226 | use tup5 <- qcheck.run( 227 | qcheck.default_config(), 228 | qcheck.tuple5(gen_int, gen_int, gen_int, gen_int, gen_int), 229 | ) 230 | let #(a, b, c, d, e) = tup5 231 | should.be_true( 232 | in_range(a) && in_range(b) && in_range(c) && in_range(d) && in_range(e), 233 | ) 234 | } 235 | 236 | pub fn tuple6__test() { 237 | let min = -100 238 | let max = 100 239 | 240 | let in_range = in_range(min, max) 241 | 242 | let gen_int = qcheck.bounded_int(min, max) 243 | 244 | use tup6 <- qcheck.run( 245 | qcheck.default_config(), 246 | qcheck.tuple6(gen_int, gen_int, gen_int, gen_int, gen_int, gen_int), 247 | ) 248 | 249 | let #(a, b, c, d, e, f) = tup6 250 | 251 | should.be_true( 252 | in_range(a) 253 | && in_range(b) 254 | && in_range(c) 255 | && in_range(d) 256 | && in_range(e) 257 | && in_range(f), 258 | ) 259 | } 260 | 261 | // bind 262 | // 263 | // The following tests exercise the shrinking behavior when using bind to 264 | // generate custom types. 265 | // 266 | // 267 | 268 | type Either(a, b) { 269 | First(a) 270 | Second(b) 271 | } 272 | 273 | pub fn shrinking_works_with_bind_and_custom_types_test() { 274 | let assert Error(msg) = { 275 | use <- test_error_message.rescue 276 | qcheck.run( 277 | qcheck.default_config(), 278 | qcheck.uniform_int() 279 | |> qcheck.bind(fn(n) { 280 | // n >= 0 here will set the shrinker starting on the `First` case, as that 281 | // is what 0 will become. 282 | case n >= 0 { 283 | True -> 284 | qcheck.bounded_int(10, 19) 285 | |> qcheck.map(First) 286 | False -> 287 | qcheck.bounded_int(90, 99) 288 | |> qcheck.map(int.to_float) 289 | |> qcheck.map(Second) 290 | } 291 | }), 292 | fn(either) { 293 | // Adding the two extra failing cases for First and Second to test the 294 | // shrinking. 295 | case either { 296 | First(15) -> should.be_true(False) 297 | First(14) -> should.be_true(False) 298 | First(_) -> should.be_true(True) 299 | Second(95.0) -> should.be_true(False) 300 | Second(94.0) -> should.be_true(False) 301 | Second(_) -> should.be_true(True) 302 | } 303 | }, 304 | ) 305 | } 306 | test_error_message.shrunk_value(msg) 307 | |> should.equal("First(14)") 308 | } 309 | 310 | pub fn shrinking_works_with_bind_and_custom_types_2_test() { 311 | let assert Error(msg) = { 312 | use <- test_error_message.rescue 313 | qcheck.run( 314 | qcheck.default_config(), 315 | qcheck.uniform_int() 316 | |> qcheck.bind(fn(n) { 317 | // n > 0 here will set the shrinker starting on the `Second` case, as that 318 | // is what 0 will become. 319 | case n > 0 { 320 | True -> 321 | qcheck.bounded_int(10, 19) 322 | |> qcheck.map(First) 323 | False -> 324 | qcheck.bounded_int(90, 99) 325 | |> qcheck.map(int.to_float) 326 | |> qcheck.map(Second) 327 | } 328 | }), 329 | fn(either) { 330 | case either { 331 | First(15) -> should.be_true(False) 332 | First(14) -> should.be_true(False) 333 | First(_) -> should.be_true(True) 334 | Second(95.0) -> should.be_true(False) 335 | Second(94.0) -> should.be_true(False) 336 | Second(_) -> should.be_true(True) 337 | } 338 | }, 339 | ) 340 | } 341 | 342 | let shrunk_value = test_error_message.shrunk_value(msg) 343 | 344 | // Value differs on Erlang and JS targets. 345 | should.be_true(shrunk_value == "Second(94.0)" || shrunk_value == "Second(94)") 346 | } 347 | 348 | pub fn shrinking_works_with_bind_and_custom_types_3_test() { 349 | let assert Error(msg) = { 350 | use <- test_error_message.rescue 351 | qcheck.run( 352 | qcheck.default_config(), 353 | qcheck.uniform_int() 354 | |> qcheck.bind(fn(n) { 355 | case n > 0 { 356 | True -> 357 | qcheck.bounded_int(10, 19) 358 | |> qcheck.map(First) 359 | False -> 360 | qcheck.bounded_int(90, 99) 361 | |> qcheck.map(int.to_float) 362 | |> qcheck.map(Second) 363 | } 364 | }), 365 | // None of the `Second` shrinks will trigger a failure. 366 | fn(either) { 367 | case either { 368 | First(15) -> should.be_true(False) 369 | First(14) -> should.be_true(False) 370 | _ -> should.be_true(True) 371 | } 372 | }, 373 | ) 374 | } 375 | 376 | test_error_message.shrunk_value(msg) 377 | |> should.equal("First(14)") 378 | } 379 | 380 | // apply 381 | // 382 | // 383 | 384 | fn curry3(f) { 385 | fn(a) { fn(b) { fn(c) { f(a, b, c) } } } 386 | } 387 | 388 | pub fn apply__test() { 389 | let tuple3 = 390 | fn(a, b, c) { #(a, b, c) } 391 | |> curry3 392 | 393 | let generator = 394 | tuple3 395 | |> qcheck.return 396 | |> qcheck.apply(qcheck.bounded_int(-5, 5)) 397 | |> qcheck.apply(qcheck.bounded_int(-10, 10)) 398 | |> qcheck.apply(qcheck.bounded_int(-100, 100)) 399 | 400 | use ns <- qcheck.run(qcheck.default_config(), generator) 401 | 402 | let #(a, b, c) = ns 403 | let a_prop = -5 <= a && a <= 5 404 | let b_prop = -10 <= b && b <= 10 405 | let c_prop = -100 <= c && c <= 100 406 | 407 | should.be_true(a_prop && b_prop && c_prop) 408 | } 409 | 410 | pub fn shrinking_works_with_apply__test() { 411 | let tuple3 = 412 | fn(a, b, c) { #(a, b, c) } 413 | |> curry3 414 | 415 | let generator = 416 | tuple3 417 | |> qcheck.return 418 | |> qcheck.apply(qcheck.bounded_int(-5, 5)) 419 | |> qcheck.apply(qcheck.bounded_int(-10, 10)) 420 | |> qcheck.apply(qcheck.bounded_int(-100, 100)) 421 | 422 | let assert Error(msg) = { 423 | use <- test_error_message.rescue 424 | 425 | use ns <- qcheck.run(qcheck.default_config(), generator) 426 | 427 | let #(a, b, c) = ns 428 | let a_prop = -5 <= a && a <= 3 429 | let b_prop = -10 <= b && b <= 10 430 | let c_prop = -100 <= c && c <= 100 431 | 432 | should.be_true(a_prop && b_prop && c_prop) 433 | } 434 | test_error_message.shrunk_value(msg) 435 | |> should.equal("#(4, 0, 0)") 436 | 437 | let assert Error(msg) = { 438 | use <- test_error_message.rescue 439 | use ns <- qcheck.run(qcheck.default_config(), generator) 440 | 441 | let #(a, b, c) = ns 442 | let a_prop = -3 <= a && a <= 5 443 | let b_prop = -10 <= b && b <= 10 444 | let c_prop = -100 <= c && c <= 100 445 | 446 | should.be_true(a_prop && b_prop && c_prop) 447 | } 448 | 449 | test_error_message.shrunk_value(msg) 450 | |> should.equal("#(-4, 0, 0)") 451 | 452 | let assert Error(msg) = { 453 | use <- test_error_message.rescue 454 | use ns <- qcheck.run(qcheck.default_config(), generator) 455 | 456 | let #(a, b, c) = ns 457 | let a_prop = -5 <= a && a <= 5 458 | let b_prop = -10 <= b && b <= 5 459 | let c_prop = -100 <= c && c <= 100 460 | 461 | should.be_true(a_prop && b_prop && c_prop) 462 | } 463 | test_error_message.shrunk_value(msg) 464 | |> should.equal("#(0, 6, 0)") 465 | 466 | let assert Error(msg) = { 467 | use <- test_error_message.rescue 468 | use ns <- qcheck.run(qcheck.default_config(), generator) 469 | 470 | let #(a, b, c) = ns 471 | let a_prop = -5 <= a && a <= 5 472 | let b_prop = -5 <= b && b <= 10 473 | let c_prop = -100 <= c && c <= 100 474 | 475 | should.be_true(a_prop && b_prop && c_prop) 476 | } 477 | test_error_message.shrunk_value(msg) 478 | |> should.equal("#(0, -6, 0)") 479 | 480 | let assert Error(msg) = { 481 | use <- test_error_message.rescue 482 | use ns <- qcheck.run(qcheck.default_config(), generator) 483 | 484 | let #(a, b, c) = ns 485 | let a_prop = -5 <= a && a <= 5 486 | let b_prop = -10 <= b && b <= 10 487 | let c_prop = -100 <= c && c <= 50 488 | 489 | should.be_true(a_prop && b_prop && c_prop) 490 | } 491 | 492 | test_error_message.shrunk_value(msg) 493 | |> should.equal("#(0, 0, 51)") 494 | 495 | let assert Error(msg) = { 496 | use <- test_error_message.rescue 497 | use ns <- qcheck.run(qcheck.default_config(), generator) 498 | 499 | let #(a, b, c) = ns 500 | let a_prop = -5 <= a && a <= 5 501 | let b_prop = -10 <= b && b <= 10 502 | let c_prop = -50 <= c && c <= 100 503 | 504 | should.be_true(a_prop && b_prop && c_prop) 505 | } 506 | 507 | test_error_message.shrunk_value(msg) 508 | |> should.equal("#(0, 0, -51)") 509 | 510 | let assert Error(msg) = { 511 | use <- test_error_message.rescue 512 | use ns <- qcheck.run(qcheck.default_config(), generator) 513 | 514 | let #(a, b, c) = ns 515 | let a_prop = -5 <= a && a <= 3 516 | let b_prop = -10 <= b && b <= 5 517 | let c_prop = -100 <= c && c <= 50 518 | 519 | should.be_true(a_prop || b_prop || c_prop) 520 | } 521 | 522 | test_error_message.shrunk_value(msg) 523 | |> should.equal("#(4, 6, 51)") 524 | 525 | let assert Error(msg) = { 526 | use <- test_error_message.rescue 527 | use ns <- qcheck.run(qcheck.default_config(), generator) 528 | 529 | let #(a, b, c) = ns 530 | let a_prop = -3 <= a && a <= 3 531 | let b_prop = -5 <= b && b <= 5 532 | let c_prop = -50 <= c && c <= 50 533 | 534 | should.be_true(a_prop || b_prop || c_prop) 535 | } 536 | 537 | let parse_numbers = fn(str) { 538 | regexp.from_string("#\\((-?\\d+), (-?\\d+), (-?\\d+)\\)") 539 | // Convert regexp.CompileError to a String 540 | |> result.map_error(string.inspect) 541 | // Apply the regular expression 542 | |> result.map(regexp.scan(_, str)) 543 | // We should see only a single match 544 | |> result.then(fn(matches) { 545 | case matches { 546 | [match] -> Ok(match) 547 | _ -> Error("expected exactly one match") 548 | } 549 | }) 550 | // Get submatches 551 | |> result.then(fn(match) { 552 | let regexp.Match(_content, submatches) = match 553 | 554 | case submatches { 555 | [Some(a), Some(b), Some(c)] -> Ok(#(a, b, c)) 556 | _ -> Error("expected exactly one submatch") 557 | } 558 | }) 559 | // Parse to ints 560 | |> result.then(fn(tup) { 561 | let #(a, b, c) = tup 562 | 563 | // The way this is set up, the failing values will either be positve or 564 | // negative for each "slot", so we must map the absolute value. 565 | case int.parse(a), int.parse(b), int.parse(c) { 566 | Ok(a), Ok(b), Ok(c) -> 567 | Ok(#( 568 | int.absolute_value(a), 569 | int.absolute_value(b), 570 | int.absolute_value(c), 571 | )) 572 | _, _, _ -> panic 573 | } 574 | }) 575 | } 576 | 577 | let assert Ok(numbers) = 578 | test_error_message.shrunk_value(msg) 579 | |> parse_numbers 580 | 581 | numbers 582 | |> should.equal(#(4, 6, 51)) 583 | } 584 | 585 | pub fn bind_and_then_are_aliases__test() { 586 | let generator_with_bind = { 587 | use length <- qcheck.bind(qcheck.small_strictly_positive_int()) 588 | qcheck.bounded_int(0, length) 589 | } 590 | 591 | let generator_with_then = { 592 | use length <- qcheck.then(qcheck.small_strictly_positive_int()) 593 | qcheck.bounded_int(0, length) 594 | } 595 | 596 | use seed <- qcheck.given(qcheck.uniform_int()) 597 | let seed = qcheck.seed(seed) 598 | let count = 10 599 | 600 | should.equal( 601 | qcheck.generate(generator_with_bind, count, seed), 602 | qcheck.generate(generator_with_then, count, seed), 603 | ) 604 | } 605 | -------------------------------------------------------------------------------- /test/qcheck/gen_bit_array_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/bit_array 3 | import gleam/list 4 | import gleam/string 5 | import gleeunit/should 6 | import qcheck 7 | import qcheck/tree 8 | 9 | // MARK: Bit arrays 10 | 11 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 12 | pub fn bit_array__smoke_test() -> Nil { 13 | use bits <- qcheck.given(qcheck.bit_array()) 14 | should.be_true(bit_array.bit_size(bits) >= 0) 15 | } 16 | 17 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 18 | pub fn non_empty_bit_array__doesnt_generate_empty_arrays__test() -> Nil { 19 | use bits <- qcheck.given(qcheck.non_empty_bit_array()) 20 | should.be_true(bit_array.bit_size(bits) >= 1) 21 | } 22 | 23 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 24 | pub fn fixed_size_bit_array_from__makes_arrays_with_valid_size_and_values__test() -> Nil { 25 | let bit_size = 5 26 | use bits <- qcheck.given(qcheck.fixed_size_bit_array_from( 27 | // Bit size 5 can encode 0-31 28 | qcheck.bounded_int(1, 30), 29 | 5, 30 | )) 31 | let assert <> = bits 32 | should.equal(bit_array.bit_size(bits), bit_size) 33 | 34 | let value_okay = 1 <= value && value <= 30 35 | should.be_true(value_okay) 36 | } 37 | 38 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 39 | pub fn fixed_size_bit_array__makes_arrays_with_valid_size__test() -> Nil { 40 | let generator = { 41 | use bit_size <- qcheck.bind(qcheck.small_non_negative_int()) 42 | use bit_array <- qcheck.map(qcheck.fixed_size_bit_array(bit_size)) 43 | #(bit_array, bit_size) 44 | } 45 | 46 | use #(bit_array, expected_bit_size) <- qcheck.given(generator) 47 | bit_array.bit_size(bit_array) |> should.equal(expected_bit_size) 48 | } 49 | 50 | // NOTE: this shrinking looks weird, but it is "correct" in terms of how the 51 | // values and sizes are generated. The bit array generators first shrink on 52 | // size, then on values. But, depending on the value generator, it may generate 53 | // values outside of the range of a "shrunk" bit array. 54 | // 55 | // Here, generated values range from [3, 5], sizes range from [1, 3]. In the 56 | // example, the first generated bit array is <<4:size(3)>>. The value 4 will 57 | // never shrink up to 5, and, given the way int generators work, shrinking will 58 | // never generate values outside of the range of the generator. So shrunk values 59 | // in all shrinks will only ever be 4 or 3. 60 | // 61 | // Here is an overview of the first couple shrinks. `size(3)` is shrunk to is 62 | // size(1). A value of 4 cannot be represented by one bit so it becomes 0 (4 = 63 | // 0b100, and in 1 bit that is 0). So rather than seeing <<4:size(1)>> you see 64 | // <<0:size(1)>>. The next shrink based on that one would be <<3:size(1)>>, 65 | // because because it will shrink on values next. Again, 3 cannot be 66 | // represented in 1 bit (3 = 0b011 -> 1:size(1)). So you will see 67 | // <<1:size(1)>>. 68 | // 69 | // The other shrinks follow a similar pattern. 70 | // 71 | // TODO: need to add this to the docs 72 | // 73 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 74 | pub fn bit_array_shrinking__test() -> Nil { 75 | let generator = 76 | qcheck.generic_bit_array(qcheck.bounded_int(3, 5), qcheck.bounded_int(1, 3)) 77 | let #(tree, _seed) = qcheck.generate_tree(generator, qcheck.seed(11)) 78 | 79 | tree 80 | |> tree.to_string(string.inspect) 81 | |> birdie.snap("bit_array_shrinking__test") 82 | } 83 | 84 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 85 | pub fn fixed_size_bit_array_from__shrinks_are_the_correct_size__test() -> Nil { 86 | use #(bit_size, seed) <- qcheck.given(qcheck.tuple2( 87 | // Do NOT raise the size up. With large sizes, you can get test timeouts 88 | // because collecting a giant shrink tree is expensive. 89 | qcheck.bounded_int(0, 10), 90 | qcheck.uniform_int(), 91 | )) 92 | 93 | let #(tree, _seed) = 94 | qcheck.generate_tree( 95 | qcheck.fixed_size_bit_array_from(qcheck.bounded_int(200, 202), bit_size), 96 | qcheck.seed(seed), 97 | ) 98 | 99 | let sizes = tree.collect(tree, bit_array.bit_size) 100 | 101 | should.be_true({ 102 | use size <- list.all(sizes) 103 | size == bit_size 104 | }) 105 | } 106 | 107 | // MARK: Bit arrays (UTF-8) 108 | 109 | pub fn utf8_bit_array__generates_valid_utf8_bit_arrays__test() { 110 | use utf8_bytes <- qcheck.given(qcheck.utf8_bit_array()) 111 | bit_array.is_utf8(utf8_bytes) |> should.be_true 112 | } 113 | 114 | pub fn utf8_bit_array__is_byte_aligned__test() { 115 | use utf8_bytes <- qcheck.given(qcheck.utf8_bit_array()) 116 | 117 | let bit_size = bit_array.bit_size(utf8_bytes) 118 | let byte_size = bit_array.byte_size(utf8_bytes) 119 | 120 | // Could do the % 8, but this also tests a property of bit_array module itself 121 | // for free. 122 | should.be_true(bit_size == 0 || bit_size == byte_size * 8) 123 | } 124 | 125 | pub fn non_empty_utf8_bit_array__generates_valid_non_empty_utf8_bit_arrays__test() { 126 | use utf8_bytes <- qcheck.given(qcheck.non_empty_utf8_bit_array()) 127 | should.be_true( 128 | bit_array.is_utf8(utf8_bytes) && bit_array.bit_size(utf8_bytes) >= 8, 129 | ) 130 | } 131 | 132 | pub fn fixed_size_utf8_bit_array__generates_valid_utf8_bit_arrays_with_given_num_codepoints__test() { 133 | let generator = { 134 | use num_codepoints <- qcheck.bind(qcheck.small_non_negative_int()) 135 | use bit_array <- qcheck.map(qcheck.fixed_size_utf8_bit_array(num_codepoints)) 136 | #(bit_array, num_codepoints) 137 | } 138 | use #(utf8_bytes, expected_num_codepoints) <- qcheck.given(generator) 139 | 140 | let is_valid_utf8 = bit_array.is_utf8(utf8_bytes) 141 | 142 | let codepoints = utf8_bytes_to_codepoints(utf8_bytes) 143 | 144 | let correct_num_codepoints = 145 | list.length(codepoints) == expected_num_codepoints 146 | 147 | should.be_true(is_valid_utf8 && correct_num_codepoints) 148 | } 149 | 150 | pub fn fixed_size_utf8_bit_array_from__generates_valid_utf8_bit_arrays_with_correct_num_codepoints__test() { 151 | let utf_codepoint = fn(n) { 152 | let assert Ok(cp) = string.utf_codepoint(n) 153 | cp 154 | } 155 | let generator = { 156 | use num_codepoints <- qcheck.bind(qcheck.small_non_negative_int()) 157 | use bit_array <- qcheck.map(qcheck.fixed_size_utf8_bit_array_from( 158 | qcheck.map(qcheck.bounded_int(0, 255), utf_codepoint), 159 | num_codepoints, 160 | )) 161 | #(bit_array, num_codepoints) 162 | } 163 | use #(utf8_bytes, expected_num_codepoints) <- qcheck.given(generator) 164 | 165 | let is_valid_utf8 = bit_array.is_utf8(utf8_bytes) 166 | 167 | let codepoints = utf8_bytes_to_codepoints(utf8_bytes) 168 | 169 | let correct_num_codepoints = 170 | list.length(codepoints) == expected_num_codepoints 171 | 172 | should.be_true(is_valid_utf8 && correct_num_codepoints) 173 | } 174 | 175 | // MARK: Bit arrays (byte-aligned) 176 | 177 | pub fn byte_aligned_bit_array__bit_size_is_always_divisible_by_8__test() { 178 | use bytes <- qcheck.given(qcheck.byte_aligned_bit_array()) 179 | should.be_true(bit_array.bit_size(bytes) % 8 == 0) 180 | } 181 | 182 | pub fn non_empty_byte_aligned_bit_array__bit_size_is_always_divisible_by_8__test() { 183 | use bytes <- qcheck.given(qcheck.non_empty_byte_aligned_bit_array()) 184 | should.be_true( 185 | bit_array.bit_size(bytes) % 8 == 0 && bit_array.bit_size(bytes) > 0, 186 | ) 187 | } 188 | 189 | pub fn fixed_size_byte_aligned_bit_array_from__makes_arrays_with_valid_size__test() { 190 | let generator = { 191 | use byte_size <- qcheck.bind(qcheck.small_non_negative_int()) 192 | use bit_array <- qcheck.map(qcheck.fixed_size_byte_aligned_bit_array_from( 193 | qcheck.bounded_int(0, 10), 194 | byte_size, 195 | )) 196 | #(bit_array, byte_size) 197 | } 198 | use #(bit_array, expected_byte_size) <- qcheck.given(generator) 199 | should.equal(bit_array.byte_size(bit_array), expected_byte_size) 200 | } 201 | 202 | pub fn fixed_size_byte_aligned_bit_array__makes_arrays_of_correct_size__test() { 203 | let generator = { 204 | use byte_size <- qcheck.bind(qcheck.small_non_negative_int()) 205 | use bytes <- qcheck.map(qcheck.fixed_size_byte_aligned_bit_array(byte_size)) 206 | #(bytes, byte_size) 207 | } 208 | use #(bytes, expected_byte_size) <- qcheck.given(generator) 209 | should.equal(bit_array.byte_size(bytes), expected_byte_size) 210 | } 211 | 212 | pub fn generic_byte_aligned_bit_array__test() { 213 | use bytes <- qcheck.given(qcheck.generic_byte_aligned_bit_array( 214 | values_from: qcheck.bounded_int(0, 255), 215 | byte_size_from: qcheck.bounded_int(0, 8), 216 | )) 217 | should.be_true(bit_array.byte_size(bytes) <= 8) 218 | } 219 | 220 | // MARK: Negative sizes 221 | 222 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 223 | pub fn fixed_size_bit_array_from__negative_sizes_yield_empty_bit_arrays__test() -> Nil { 224 | use bits <- qcheck.given({ 225 | use bit_size <- qcheck.bind(negative_numbers()) 226 | qcheck.fixed_size_bit_array_from(qcheck.bounded_int(0, 255), bit_size) 227 | }) 228 | 229 | should.equal(bit_array.bit_size(bits), 0) 230 | } 231 | 232 | pub fn fixed_size_byte_aligned_bit_array_from__negative_sizes_yield_empty_bit_arrays__test() { 233 | use bytes <- qcheck.given({ 234 | use byte_size <- qcheck.bind(negative_numbers()) 235 | qcheck.fixed_size_byte_aligned_bit_array_from( 236 | qcheck.bounded_int(0, 255), 237 | byte_size, 238 | ) 239 | }) 240 | 241 | should.equal(bit_array.bit_size(bytes), 0) 242 | } 243 | 244 | pub fn fixed_size_utf8_bit_array_from__negative_sizes_yield_empty_bit_arrays__test() { 245 | use bytes <- qcheck.given({ 246 | use num_codepoints <- qcheck.bind(negative_numbers()) 247 | qcheck.fixed_size_utf8_bit_array_from(qcheck.codepoint(), num_codepoints) 248 | }) 249 | 250 | should.equal(bit_array.bit_size(bytes), 0) 251 | } 252 | 253 | // Previous versions would crash if the size was too big. 254 | pub fn fixed_size_bit_array__doesnt_crash_for_huge_numbers__test() { 255 | use _ <- qcheck.run( 256 | qcheck.default_config() |> qcheck.with_test_count(10), 257 | qcheck.fixed_size_bit_array(1024), 258 | ) 259 | should.be_true(True) 260 | } 261 | 262 | // MARK: Sized 263 | 264 | pub fn sizing_bit_arrays__test() -> Nil { 265 | use bytes <- qcheck.given({ 266 | qcheck.fixed_size_byte_aligned_bit_array 267 | |> qcheck.sized_from(qcheck.bounded_int(0, 10)) 268 | }) 269 | 270 | let byte_size = bit_array.byte_size(bytes) 271 | 272 | should.be_true(0 <= byte_size && byte_size <= 10) 273 | } 274 | 275 | // MARK: Utils 276 | 277 | fn utf8_bytes_to_codepoints(utf8_bytes) { 278 | utf8_bytes 279 | |> bit_array.to_string 280 | |> ok_exn 281 | |> string.to_utf_codepoints 282 | } 283 | 284 | fn ok_exn(x) { 285 | let assert Ok(x) = x 286 | x 287 | } 288 | 289 | fn negative_numbers() -> qcheck.Generator(Int) { 290 | qcheck.bounded_int(-1_000_000, -1) 291 | } 292 | -------------------------------------------------------------------------------- /test/qcheck/gen_bool_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/bool 3 | import qcheck 4 | import qcheck/tree 5 | 6 | pub fn bool_true_shrink_tree__test() { 7 | let #(tree, _seed) = 8 | qcheck.generate_tree( 9 | qcheck.bool(), 10 | // Don't change this seed--it generates `True` to start. 11 | qcheck.seed(2), 12 | ) 13 | 14 | tree 15 | |> tree.to_string(bool.to_string) 16 | |> birdie.snap("bool_true_shrink_tree__test") 17 | } 18 | -------------------------------------------------------------------------------- /test/qcheck/gen_codepoint_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/list 3 | import gleam/string 4 | import gleeunit/should 5 | import qcheck 6 | import qcheck/test_error_message 7 | 8 | pub fn bounded_codepoint__test() { 9 | use codepoint <- qcheck.run( 10 | qcheck.default_config(), 11 | qcheck.bounded_codepoint(500, 1000), 12 | ) 13 | let n = string.utf_codepoint_to_int(codepoint) 14 | should.be_true(500 <= n && n <= 1000) 15 | } 16 | 17 | pub fn bounded_codepoint__ranges_that_include_invalid_codepoints_are_okay__test() { 18 | use codepoint <- qcheck.run( 19 | qcheck.default_config() |> qcheck.with_test_count(10_000), 20 | qcheck.bounded_codepoint(55_200, 58_000), 21 | ) 22 | let n = string.utf_codepoint_to_int(codepoint) 23 | should.be_true(55_200 <= n && n <= 58_000) 24 | } 25 | 26 | pub fn bounded_codepoint__ranges_that_include_only_invalid_codepoints_are_corrected__test() { 27 | use codepoint <- qcheck.run( 28 | qcheck.default_config() |> qcheck.with_test_count(10_000), 29 | qcheck.bounded_codepoint(55_296, 57_343), 30 | ) 31 | let n = string.utf_codepoint_to_int(codepoint) 32 | should.equal(n, 97) 33 | } 34 | 35 | pub fn bounded_codepoint__low_greater_than_hi__test() { 36 | use codepoint <- qcheck.run( 37 | qcheck.default_config() |> qcheck.with_test_count(10_000), 38 | qcheck.bounded_codepoint(70, 65), 39 | ) 40 | let n = string.utf_codepoint_to_int(codepoint) 41 | should.be_true(65 <= n && n <= 70) 42 | } 43 | 44 | pub fn bounded_codepoint__codepoints_out_of_range__test() { 45 | qcheck.run( 46 | qcheck.default_config() |> qcheck.with_test_count(10_000), 47 | qcheck.bounded_codepoint(-2_000_000, 2_000_000), 48 | it_doesnt_crash, 49 | ) 50 | } 51 | 52 | fn it_doesnt_crash(_) { 53 | should.be_true(True) 54 | } 55 | 56 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 57 | pub fn bounded_codepoint__failures_shink_ok__test() -> Nil { 58 | let expected = string.inspect(500) 59 | 60 | let assert Error(msg) = { 61 | use <- test_error_message.rescue 62 | use codepoint <- qcheck.run( 63 | qcheck.default_config(), 64 | qcheck.bounded_codepoint(500, 1000), 65 | ) 66 | let n = string.utf_codepoint_to_int(codepoint) 67 | should.be_true(600 <= n && n <= 900) 68 | } 69 | 70 | test_error_message.shrunk_value(msg) 71 | |> should.equal(expected) 72 | } 73 | 74 | fn has_one_codepoint_in_range( 75 | codepoint: UtfCodepoint, 76 | low: Int, 77 | high: Int, 78 | ) -> Bool { 79 | let n = string.utf_codepoint_to_int(codepoint) 80 | 81 | low <= n && n <= high 82 | } 83 | 84 | fn assert_has_one_codepoint_in_range( 85 | codepoint: UtfCodepoint, 86 | low: Int, 87 | high: Int, 88 | ) -> Nil { 89 | let n = string.utf_codepoint_to_int(codepoint) 90 | 91 | should.be_true(low <= n && n <= high) 92 | } 93 | 94 | pub fn uppercase_character__test() { 95 | qcheck.run( 96 | qcheck.default_config(), 97 | qcheck.uppercase_ascii_codepoint(), 98 | assert_has_one_codepoint_in_range(_, int("A"), int("Z")), 99 | ) 100 | } 101 | 102 | pub fn uppercase_character__failures_shink_ok__test() { 103 | // "Z" is less than "a" => "Z" is "closer" to "a" so that is the shrink 104 | // target. 105 | let expected = inspect_first_codepoint("Z") 106 | 107 | let assert Error(msg) = { 108 | use <- test_error_message.rescue 109 | 110 | qcheck.run( 111 | qcheck.default_config(), 112 | qcheck.uppercase_ascii_codepoint(), 113 | assert_has_one_codepoint_in_range(_, int("A") + 2, int("Z") - 2), 114 | ) 115 | } 116 | test_error_message.shrunk_value(msg) 117 | |> should.equal(expected) 118 | } 119 | 120 | pub fn lowercase_character__test() { 121 | qcheck.run( 122 | qcheck.default_config(), 123 | qcheck.lowercase_ascii_codepoint(), 124 | assert_has_one_codepoint_in_range(_, int("a"), int("z")), 125 | ) 126 | } 127 | 128 | pub fn lowercase_character__failures_shink_ok__test() { 129 | // "Z" is less than "a" => "Z" is "closer" to "a" so that is the shrink 130 | // target. 131 | let expected = inspect_first_codepoint("a") 132 | 133 | let assert Error(msg) = { 134 | use <- test_error_message.rescue 135 | 136 | qcheck.run( 137 | qcheck.default_config(), 138 | qcheck.lowercase_ascii_codepoint(), 139 | assert_has_one_codepoint_in_range(_, int("a") + 2, int("z") - 2), 140 | ) 141 | } 142 | test_error_message.shrunk_value(msg) 143 | |> should.equal(expected) 144 | } 145 | 146 | pub fn digit_character__test() { 147 | qcheck.run( 148 | qcheck.default_config(), 149 | qcheck.ascii_digit_codepoint(), 150 | assert_has_one_codepoint_in_range(_, int("0"), int("9")), 151 | ) 152 | } 153 | 154 | pub fn digit_character__failures_shink_ok__test() { 155 | // "9" is less than "a" => "9" is "closer" to "a" so that is the shrink 156 | // target. 157 | let expected = inspect_first_codepoint("9") 158 | 159 | let assert Error(msg) = { 160 | use <- test_error_message.rescue 161 | 162 | qcheck.run( 163 | qcheck.default_config(), 164 | qcheck.ascii_digit_codepoint(), 165 | assert_has_one_codepoint_in_range(_, int("0") + 2, int("9") - 2), 166 | ) 167 | } 168 | test_error_message.shrunk_value(msg) 169 | |> should.equal(expected) 170 | } 171 | 172 | pub fn uniform_printable_character__test() { 173 | qcheck.run( 174 | qcheck.default_config(), 175 | qcheck.uniform_printable_ascii_codepoint(), 176 | assert_has_one_codepoint_in_range(_, int(" "), int("~")), 177 | ) 178 | } 179 | 180 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 181 | pub fn uniform_printable_character__failures_shink_ok__test() -> Nil { 182 | let assert Error(msg) = { 183 | use <- test_error_message.rescue 184 | qcheck.run( 185 | qcheck.default_config(), 186 | qcheck.uniform_printable_ascii_codepoint(), 187 | // TODO: These tests with `int` need to be fixed 188 | assert_has_one_codepoint_in_range(_, int(" ") + 2, int("~") - 2), 189 | ) 190 | } 191 | 192 | // Printable chars shrink to `"a"`, so either of these could be valid. 193 | test_error_message.shrunk_value(msg) 194 | |> should_be_one_of(["!", "}"]) 195 | } 196 | 197 | // TODO: the edge conditions don't matter -- test if the distribution is approximately uniform 198 | pub fn uniform_character__test() { 199 | qcheck.run( 200 | qcheck.default_config(), 201 | qcheck.uniform_codepoint(), 202 | // TODO this test is basically pointless. 203 | assert_has_one_codepoint_in_range(_, 0x0000, 0x10FFFF), 204 | ) 205 | } 206 | 207 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 208 | pub fn uniform_character__failures_shink_ok__test() -> Nil { 209 | let assert Error(msg) = { 210 | use <- test_error_message.rescue 211 | qcheck.run( 212 | qcheck.default_config(), 213 | qcheck.uniform_codepoint(), 214 | assert_has_one_codepoint_in_range(_, 2, 253), 215 | ) 216 | } 217 | 218 | let s = 219 | test_error_message.shrunk_value(msg) 220 | |> string.replace(each: "\"", with: "") 221 | 222 | // `uniform_codepoint` shrinks towards `"a"`, so either of these could be valid. 223 | let check = 224 | int_parse_exn(s) == 1 225 | || int_parse_exn(s) == 254 226 | // Technically, this comes from a bug in the `string.replace` function 227 | // above, OR potentially in the shrinking functions. For now, we stick this 228 | // in. See notes for more info. 229 | || s == "\\u{0001}" 230 | 231 | should.be_true(check) 232 | } 233 | 234 | pub fn alphabetic_character__test() { 235 | use s <- qcheck.run( 236 | qcheck.default_config(), 237 | qcheck.alphabetic_ascii_codepoint(), 238 | ) 239 | should.be_true( 240 | has_one_codepoint_in_range(s, int("A"), int("Z")) 241 | || has_one_codepoint_in_range(s, int("a"), int("z")), 242 | ) 243 | } 244 | 245 | pub fn alphabetic_character__failures_shrink_ok__test() { 246 | // If the property is false, then we know the lowercase generator was selected 247 | // and that shrinks to "a". 248 | let expected = inspect_first_codepoint("a") 249 | 250 | let assert Error(msg) = { 251 | use <- test_error_message.rescue 252 | use s <- qcheck.run( 253 | qcheck.default_config(), 254 | qcheck.alphabetic_ascii_codepoint(), 255 | ) 256 | assert_has_one_codepoint_in_range(s, int("A"), int("Z")) 257 | } 258 | 259 | test_error_message.shrunk_value(msg) 260 | |> should.equal(expected) 261 | } 262 | 263 | pub fn alphabetic_character__failures_shrink_ok_2__test() { 264 | // If the property is false, then we know the uppercase generator was selected 265 | // and that shrinks to "Z". 266 | let expected = inspect_first_codepoint("Z") 267 | 268 | let assert Error(msg) = { 269 | use <- test_error_message.rescue 270 | qcheck.run( 271 | qcheck.default_config(), 272 | qcheck.alphabetic_ascii_codepoint(), 273 | assert_has_one_codepoint_in_range(_, int("a"), int("z")), 274 | ) 275 | } 276 | test_error_message.shrunk_value(msg) 277 | |> should.equal(expected) 278 | } 279 | 280 | pub fn alphanumeric_character__test() { 281 | use s <- qcheck.run( 282 | qcheck.default_config(), 283 | qcheck.alphanumeric_ascii_codepoint(), 284 | ) 285 | should.be_true( 286 | has_one_codepoint_in_range(s, int("A"), int("Z")) 287 | || has_one_codepoint_in_range(s, int("a"), int("z")) 288 | || has_one_codepoint_in_range(s, int("0"), int("9")), 289 | ) 290 | } 291 | 292 | // TODO: The shrink tests are all broken on javascript because the test error 293 | // messages are platform dependent. 294 | 295 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 296 | pub fn alphanumeric_character__failures_shrink_ok__test() -> Nil { 297 | let assert Error(msg) = { 298 | use <- test_error_message.rescue 299 | use _ <- qcheck.run( 300 | qcheck.default_config(), 301 | qcheck.alphanumeric_ascii_codepoint(), 302 | ) 303 | should.be_true(False) 304 | } 305 | 306 | // Depending on the selected generator, any of these could be the shrink 307 | // target. 308 | test_error_message.shrunk_value(msg) 309 | |> should_be_one_of(["a", "Z", "9"]) 310 | } 311 | 312 | pub fn character_from__test() { 313 | use s <- qcheck.run( 314 | qcheck.default_config(), 315 | qcheck.codepoint_from_strings("b", ["c", "x", "y", "z"]), 316 | ) 317 | case string.from_utf_codepoints([s]) { 318 | "b" -> should.be_true(True) 319 | "c" -> should.be_true(True) 320 | "x" -> should.be_true(True) 321 | "y" -> should.be_true(True) 322 | "z" -> should.be_true(True) 323 | _ -> should.be_true(False) 324 | } 325 | } 326 | 327 | pub fn character_from__failures_shrink_ok__test() { 328 | let expected = string.to_utf_codepoints("b") |> hd_exn |> string.inspect 329 | 330 | let assert Error(msg) = { 331 | use <- test_error_message.rescue 332 | use s <- qcheck.run( 333 | qcheck.default_config(), 334 | qcheck.codepoint_from_strings("b", ["c", "x", "y", "z"]), 335 | ) 336 | should.equal(string.to_utf_codepoints("q"), [s]) 337 | } 338 | test_error_message.shrunk_value(msg) 339 | |> should.equal(expected) 340 | } 341 | 342 | pub fn codepoint_from_strings__doesnt_crash_on_multicodepoint_chars__test() { 343 | let e_accent = "é" 344 | let assert True = e_accent == "\u{0065}\u{0301}" 345 | use _ <- qcheck.run( 346 | qcheck.default_config(), 347 | qcheck.codepoint_from_strings(e_accent, [e_accent]), 348 | ) 349 | should.be_true(True) 350 | } 351 | 352 | pub fn whitespace_character__test() { 353 | use codepoint <- qcheck.run( 354 | qcheck.default_config(), 355 | qcheck.ascii_whitespace_codepoint(), 356 | ) 357 | case string.utf_codepoint_to_int(codepoint) { 358 | // Horizontal tab 359 | 9 -> should.be_true(True) 360 | // Line feed 361 | 10 -> should.be_true(True) 362 | // Vertical tab 363 | 11 -> should.be_true(True) 364 | // Form feed 365 | 12 -> should.be_true(True) 366 | // Carriage return 367 | 13 -> should.be_true(True) 368 | // Space 369 | 32 -> should.be_true(True) 370 | _ -> should.be_true(False) 371 | } 372 | } 373 | 374 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 375 | pub fn whitespace_character__failures_shrink_ok__test() -> Nil { 376 | let assert Error(msg) = { 377 | use <- test_error_message.rescue 378 | use codepoint <- qcheck.run( 379 | qcheck.default_config(), 380 | qcheck.ascii_whitespace_codepoint(), 381 | ) 382 | case string.utf_codepoint_to_int(codepoint) { 383 | // Horizontal tab 384 | 9 -> should.be_true(True) 385 | // Line feed 386 | 10 -> should.be_true(False) 387 | // Vertical tab 388 | 11 -> should.be_true(True) 389 | // Form feed 390 | 12 -> should.be_true(True) 391 | // Carriage return 392 | 13 -> should.be_true(False) 393 | // Space 394 | 32 -> should.be_true(False) 395 | _ -> should.be_true(False) 396 | } 397 | } 398 | 399 | test_error_message.shrunk_value(msg) 400 | |> should_be_one_of(["\n", "\r"]) 401 | } 402 | 403 | pub fn printable_character__test() { 404 | use s <- qcheck.run( 405 | qcheck.default_config(), 406 | qcheck.printable_ascii_codepoint(), 407 | ) 408 | should.be_true( 409 | has_one_codepoint_in_range(s, int("A"), int("Z")) 410 | || has_one_codepoint_in_range(s, int("a"), int("z")) 411 | || has_one_codepoint_in_range(s, int("0"), int("9")) 412 | || has_one_codepoint_in_range(s, int(" "), int("~")), 413 | ) 414 | } 415 | 416 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 417 | pub fn printable_character__failures_shrink_ok__test() -> Nil { 418 | let assert Error(msg) = { 419 | use <- test_error_message.rescue 420 | use _ <- qcheck.run( 421 | qcheck.default_config(), 422 | qcheck.printable_ascii_codepoint(), 423 | ) 424 | should.be_true(False) 425 | } 426 | 427 | // Depending on the selected generator, any of these could be the shrink 428 | // target. 429 | test_error_message.shrunk_value(msg) 430 | |> should_be_one_of(["a", "Z", "9", " "]) 431 | } 432 | 433 | pub fn char__test() { 434 | qcheck.run( 435 | qcheck.default_config(), 436 | qcheck.codepoint(), 437 | assert_has_one_codepoint_in_range(_, 0x0000, 0x10FFFF), 438 | ) 439 | } 440 | 441 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 442 | pub fn char__failures_shrink_ok__test() -> Nil { 443 | let assert Error(msg) = { 444 | use <- test_error_message.rescue 445 | use _ <- qcheck.run(qcheck.default_config(), qcheck.codepoint()) 446 | should.be_true(False) 447 | } 448 | 449 | // Depending on the selected generator, any of these could be the shrink 450 | // target. 451 | test_error_message.shrunk_value(msg) 452 | |> should_be_one_of(["a", "Z", "9", " ", "\u{0000}", "\u{00FF}"]) 453 | } 454 | 455 | // MARK: utils 456 | // 457 | // 458 | 459 | fn int(c) { 460 | string.to_utf_codepoints(c) 461 | |> list.first 462 | |> ok_exn 463 | |> string.utf_codepoint_to_int 464 | } 465 | 466 | fn should_be_one_of(x, strings) { 467 | let x = 468 | int_parse_exn(x) 469 | |> utf_codepoint_exn 470 | |> list_return 471 | |> string.from_utf_codepoints 472 | 473 | let assert Ok(_) = strings |> list.find(one_that: fn(el) { el == x }) 474 | 475 | Nil 476 | } 477 | 478 | fn utf_codepoint_exn(n) { 479 | let assert Ok(cp) = string.utf_codepoint(n) 480 | 481 | cp 482 | } 483 | 484 | fn list_return(a) { 485 | [a] 486 | } 487 | 488 | fn ok_exn(result) { 489 | let assert Ok(x) = result 490 | 491 | x 492 | } 493 | 494 | fn hd_exn(l: List(a)) -> a { 495 | case l { 496 | [h, ..] -> h 497 | [] -> panic as "no head" 498 | } 499 | } 500 | 501 | /// This seemingly nonsensical function is check against error messages. 502 | /// 503 | fn inspect_first_codepoint(string: String) -> String { 504 | string.to_utf_codepoints(string) |> hd_exn |> string.inspect 505 | } 506 | 507 | @external(javascript, "../qcheck_ffi.mjs", "do_nothing") 508 | fn int_parse_exn(string: String) -> Int { 509 | let assert Ok(int) = int.parse(string) 510 | int 511 | } 512 | -------------------------------------------------------------------------------- /test/qcheck/gen_custom_types_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/string 2 | import gleeunit/should 3 | import qcheck 4 | import qcheck/test_error_message 5 | 6 | pub fn custom_type_passing_test() { 7 | use my_int <- qcheck.run( 8 | qcheck.default_config(), 9 | qcheck.small_non_negative_int() |> qcheck.map(MyInt), 10 | ) 11 | let MyInt(n) = my_int 12 | should.equal(my_int_to_int(my_int), n) 13 | } 14 | 15 | pub fn custom_type_failing_test() { 16 | let assert Error(msg) = { 17 | use <- test_error_message.rescue 18 | use my_int <- qcheck.run( 19 | qcheck.default_config(), 20 | qcheck.small_non_negative_int() |> qcheck.map(MyInt), 21 | ) 22 | let MyInt(n) = my_int 23 | should.be_true(n < 10) 24 | } 25 | test_error_message.shrunk_value(msg) 26 | |> should.equal(string.inspect(MyInt(10))) 27 | } 28 | 29 | type Either(a, b) { 30 | First(a) 31 | Second(b) 32 | } 33 | 34 | fn even_odd(n: Int) -> Either(Int, Int) { 35 | case n % 2 == 0 { 36 | True -> First(n) 37 | False -> Second(n) 38 | } 39 | } 40 | 41 | pub fn either_passing_test() { 42 | use v <- qcheck.run( 43 | qcheck.default_config(), 44 | qcheck.small_non_negative_int() |> qcheck.map(even_odd), 45 | ) 46 | case v { 47 | First(n) -> should.equal(n % 2, 0) 48 | Second(n) -> should.equal(n % 2, 1) 49 | } 50 | } 51 | 52 | pub fn either_failing_test() { 53 | let run = fn(property) { 54 | qcheck.run( 55 | qcheck.default_config(), 56 | qcheck.small_non_negative_int() |> qcheck.map(even_odd), 57 | property, 58 | ) 59 | } 60 | 61 | let assert Error(msg) = { 62 | use <- test_error_message.rescue 63 | use v <- run 64 | case v { 65 | First(n) -> should.equal(n % 2, 1) 66 | Second(n) -> should.equal(n % 2, 0) 67 | } 68 | } 69 | test_error_message.shrunk_value(msg) 70 | |> should.equal(string.inspect(First(0))) 71 | 72 | // The n == 0 will prevent the First(0) from being a shrink that fails 73 | // the property. 74 | let assert Error(msg) = { 75 | use <- test_error_message.rescue 76 | use v <- run 77 | case v { 78 | First(n) -> should.be_true(n == 0 || n % 2 == 1) 79 | Second(n) -> should.be_true(n % 2 == 0) 80 | } 81 | } 82 | test_error_message.shrunk_value(msg) 83 | |> should.equal(string.inspect(Second(1))) 84 | 85 | // The n == 1 will prevent the Second(1) from being a shrink that 86 | // fails the property. 87 | let assert Error(msg) = { 88 | use <- test_error_message.rescue 89 | use v <- run 90 | case v { 91 | First(n) -> should.be_true(n == 0 || n % 2 == 1) 92 | Second(n) -> should.be_true(n == 1 || n % 2 == 0) 93 | } 94 | } 95 | test_error_message.shrunk_value(msg) 96 | |> should.equal(string.inspect(First(2))) 97 | } 98 | 99 | // utils 100 | // 101 | // 102 | 103 | type MyInt { 104 | MyInt(Int) 105 | } 106 | 107 | fn my_int_to_int(my_int) { 108 | let MyInt(n) = my_int 109 | n 110 | } 111 | -------------------------------------------------------------------------------- /test/qcheck/gen_dict_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/dict 3 | import gleam/int 4 | import gleam/list 5 | import gleeunit/should 6 | import qcheck 7 | import qcheck/tree 8 | 9 | pub fn generic_dict__generates_valid_values__test() { 10 | use d <- qcheck.run( 11 | qcheck.default_config(), 12 | qcheck.generic_dict( 13 | qcheck.bounded_int(0, 2), 14 | qcheck.bounded_int(10, 12), 15 | qcheck.bounded_int(0, 5), 16 | ), 17 | ) 18 | 19 | let size_is_good = dict.size(d) <= 5 20 | 21 | let keys_are_good = 22 | dict.keys(d) 23 | |> list.all(fn(n) { n == 0 || n == 1 || n == 2 }) 24 | 25 | let values_are_good = 26 | dict.values(d) 27 | |> list.all(fn(n) { n == 10 || n == 11 || n == 12 }) 28 | 29 | should.be_true(size_is_good && keys_are_good && values_are_good) 30 | } 31 | 32 | import gleam/string_tree 33 | 34 | fn int_int_dict_to_string(dict: dict.Dict(Int, Int)) -> String { 35 | dict 36 | |> dict.to_list 37 | // Manually sort because the internal "sorting" is not stable across Erlang 38 | // and JavaScript. 39 | |> list.sort(fn(kv1, kv2) { 40 | let #(key1, _) = kv1 41 | let #(key2, _) = kv2 42 | 43 | int.compare(key1, key2) 44 | }) 45 | |> list.fold(string_tree.from_string("{ "), fn(acc, kv) { 46 | let #(k, v) = kv 47 | string_tree.append( 48 | acc, 49 | int.to_string(k) <> " => " <> int.to_string(v) <> ", ", 50 | ) 51 | }) 52 | |> string_tree.append(" }") 53 | |> string_tree.to_string() 54 | } 55 | 56 | pub fn dict_generators_shrink_on_size_then_on_elements__test() { 57 | let #(tree, _seed) = 58 | qcheck.generate_tree( 59 | qcheck.generic_dict( 60 | keys_from: qcheck.bounded_int(0, 2), 61 | values_from: qcheck.bounded_int(10, 12), 62 | size_from: qcheck.bounded_int(0, 3), 63 | ), 64 | qcheck.seed(12), 65 | ) 66 | 67 | tree 68 | |> tree.to_string(int_int_dict_to_string) 69 | |> birdie.snap("dict_generators_shrink_on_size_then_on_elements__test") 70 | } 71 | 72 | pub fn generic_dict__allows_empty_dict__test() { 73 | use _ <- qcheck.given(qcheck.generic_dict( 74 | qcheck.bounded_int(0, 2), 75 | qcheck.bounded_int(10, 12), 76 | qcheck.constant(0), 77 | )) 78 | should.be_true(True) 79 | } 80 | -------------------------------------------------------------------------------- /test/qcheck/gen_float_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/float 2 | import gleam/string 3 | import gleeunit/should 4 | import qcheck 5 | import qcheck/test_error_message 6 | 7 | pub fn float__failures_shrink_towards_zero__test() { 8 | let assert Error(msg) = { 9 | use <- test_error_message.rescue 10 | use _ <- qcheck.run(qcheck.default_config(), qcheck.float()) 11 | should.be_true(False) 12 | } 13 | test_error_message.shrunk_value(msg) 14 | |> should.equal(string.inspect(0.0)) 15 | } 16 | 17 | // float_uniform_inclusive 18 | // 19 | // 20 | 21 | pub fn float_uniform_range__test() { 22 | let assert Error(msg) = { 23 | use <- test_error_message.rescue 24 | use x <- qcheck.run( 25 | qcheck.default_config(), 26 | qcheck.bounded_float(-10.0, 10.0), 27 | ) 28 | should.be_true(-5.0 <=. x && x <=. 5.0) 29 | } 30 | 31 | let assert Ok(x) = 32 | test_error_message.shrunk_value(msg) 33 | |> float.parse 34 | 35 | should.be_true(-6.0 <=. x && x <=. -5.0 || 5.0 <=. x && x <=. 6.0) 36 | } 37 | 38 | // This test ensures that you aren't shrinking to zero if the float range doesn't 39 | // include zero. 40 | pub fn positive_float_uniform_range_not_including_zero__shrinks_ok__test() { 41 | let assert Error(msg) = { 42 | use <- test_error_message.rescue 43 | use x <- qcheck.run( 44 | qcheck.default_config(), 45 | qcheck.bounded_float(5.0, 10.0), 46 | ) 47 | should.be_true(7.0 <=. x && x <=. 8.0) 48 | } 49 | 50 | test_error_message.shrunk_value(msg) 51 | |> should.equal(string.inspect(5.0)) 52 | } 53 | 54 | // This test ensures that you aren't shrinking to zero if the float range doesn't 55 | // include zero. 56 | pub fn negative_float_uniform_range_not_including_zero__shrinks_ok__test() { 57 | let assert Error(msg) = { 58 | use <- test_error_message.rescue 59 | use x <- qcheck.run( 60 | qcheck.default_config(), 61 | qcheck.bounded_float(-10.0, -5.0), 62 | ) 63 | should.be_true(-8.0 >=. x && x >=. -7.0) 64 | } 65 | 66 | test_error_message.shrunk_value(msg) 67 | |> should.equal(string.inspect(-5.0)) 68 | } 69 | 70 | pub fn float_uniform_inclusive__high_less_than_low_ok__test() { 71 | use n <- qcheck.given(qcheck.bounded_float(10.0, -10.0)) 72 | should.be_true(-10.0 <=. n && n <=. 10.0) 73 | } 74 | -------------------------------------------------------------------------------- /test/qcheck/gen_int_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/int 2 | import gleam/string 3 | import gleeunit/should 4 | import qcheck 5 | import qcheck/test_error_message 6 | 7 | // small_non_negative_int 8 | // 9 | // 10 | 11 | pub fn small_non_negative_int__test() { 12 | use n <- qcheck.given(qcheck.small_non_negative_int()) 13 | should.equal(n + 1, 1 + n) 14 | } 15 | 16 | pub fn small_non_negative_int__failures_shrink_to_zero__test() { 17 | let assert Error(msg) = { 18 | use <- test_error_message.rescue 19 | use n <- qcheck.given(qcheck.small_non_negative_int()) 20 | should.not_equal(n + 1, 1 + n) 21 | } 22 | 23 | test_error_message.shrunk_value(msg) 24 | |> should.equal(string.inspect(0)) 25 | } 26 | 27 | pub fn small_non_negative_int__failures_shrink_to_smaller_values__test() { 28 | let assert Error(msg) = { 29 | use <- test_error_message.rescue 30 | use n <- qcheck.run( 31 | qcheck.default_config(), 32 | qcheck.small_non_negative_int(), 33 | ) 34 | should.be_true(n == 0 || n > 1) 35 | } 36 | test_error_message.shrunk_value(msg) 37 | |> should.equal(string.inspect(1)) 38 | } 39 | 40 | // small_strictly_positive_int 41 | // 42 | // 43 | 44 | pub fn small_strictly_positive_int__test() { 45 | use n <- qcheck.run( 46 | qcheck.default_config(), 47 | qcheck.small_strictly_positive_int(), 48 | ) 49 | should.be_true(n > 0) 50 | } 51 | 52 | pub fn small_strictly_positive_int__failures_shrink_ok__test() { 53 | let assert Error(msg) = { 54 | use <- test_error_message.rescue 55 | use n <- qcheck.run( 56 | qcheck.default_config(), 57 | qcheck.small_strictly_positive_int(), 58 | ) 59 | should.be_true(n > 1) 60 | } 61 | test_error_message.shrunk_value(msg) 62 | |> should.equal(string.inspect(1)) 63 | 64 | let assert Error(msg) = { 65 | use <- test_error_message.rescue 66 | use n <- qcheck.run( 67 | qcheck.default_config(), 68 | qcheck.small_strictly_positive_int(), 69 | ) 70 | should.be_true(n == 1 || n > 2) 71 | } 72 | test_error_message.shrunk_value(msg) 73 | |> should.equal(string.inspect(2)) 74 | } 75 | 76 | // uniform_int 77 | // 78 | // 79 | 80 | pub fn uniform_int__test() { 81 | use n <- qcheck.run(qcheck.default_config(), qcheck.uniform_int()) 82 | should.equal(n + 1, 1 + n) 83 | } 84 | 85 | pub fn uniform_int__failures_shrink_ok__test() { 86 | let assert Error(msg) = { 87 | use <- test_error_message.rescue 88 | use n <- qcheck.run(qcheck.default_config(), qcheck.uniform_int()) 89 | should.be_true(n < 55_555) 90 | } 91 | 92 | test_error_message.shrunk_value(msg) 93 | |> should.equal(string.inspect(55_555)) 94 | } 95 | 96 | pub fn uniform_int__negative_numbers_shrink_towards_zero__test() { 97 | let assert Error(msg) = { 98 | use <- test_error_message.rescue 99 | use n <- qcheck.run(qcheck.default_config(), qcheck.uniform_int()) 100 | should.be_true(n > -5) 101 | } 102 | test_error_message.shrunk_value(msg) 103 | |> should.equal(string.inspect(-5)) 104 | } 105 | 106 | // bounded_int 107 | // 108 | // 109 | 110 | pub fn uniform_int_range__test() { 111 | let assert Error(msg) = { 112 | use <- test_error_message.rescue 113 | use n <- qcheck.run(qcheck.default_config(), qcheck.bounded_int(-10, 10)) 114 | should.be_true(-5 <= n && n <= 5) 115 | } 116 | 117 | let assert Ok(n) = 118 | test_error_message.shrunk_value(msg) 119 | |> int.parse 120 | 121 | should.be_true(n == -6 || n == 6) 122 | // case run_result { 123 | // // One of either glexer or glance is broken with Error(-6) here, so use the 124 | // // guard for now. 125 | // Error(n) if n == -6 || n == 6 -> True 126 | // _ -> False 127 | // } 128 | // |> should.be_true 129 | } 130 | 131 | // This test ensures that you aren't shrinking to zero if the int range doesn't 132 | // include zero. 133 | pub fn positive_uniform_int_range_not_including_zero__shrinks_ok__test() { 134 | let assert Error(msg) = { 135 | use <- test_error_message.rescue 136 | use n <- qcheck.run(qcheck.default_config(), qcheck.bounded_int(5, 10)) 137 | should.be_true(7 <= n && n <= 8) 138 | } 139 | 140 | test_error_message.shrunk_value(msg) 141 | |> should.equal(string.inspect(5)) 142 | } 143 | 144 | // This test ensures that you aren't shrinking to zero if the int range doesn't 145 | // include zero. 146 | pub fn negative_uniform_int_range_not_including_zero__shrinks_ok__test() { 147 | let assert Error(msg) = { 148 | use <- test_error_message.rescue 149 | use n <- qcheck.run(qcheck.default_config(), qcheck.bounded_int(-10, -5)) 150 | should.be_true(-8 >= n && n >= -7) 151 | } 152 | 153 | test_error_message.shrunk_value(msg) 154 | |> should.equal(string.inspect(-5)) 155 | } 156 | 157 | pub fn bounded_int__high_less_than_low_ok__test() { 158 | use n <- qcheck.given(qcheck.bounded_int(10, -10)) 159 | should.be_true(-10 <= n && n <= 10) 160 | } 161 | -------------------------------------------------------------------------------- /test/qcheck/gen_list_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/int 3 | import gleam/list 4 | import gleam/string 5 | import gleam/yielder 6 | import gleeunit/should 7 | import qcheck 8 | import qcheck/tree 9 | import qcheck_gleeunit_utils/test_spec 10 | 11 | pub fn generic_list__generates_valid_values__test() { 12 | use l <- qcheck.run( 13 | qcheck.default_config(), 14 | qcheck.generic_list( 15 | elements_from: qcheck.bounded_int(-5, 5), 16 | length_from: qcheck.bounded_int(2, 5), 17 | ), 18 | ) 19 | let len = list.length(l) 20 | should.be_true( 21 | 2 <= len && len <= 5 && list.all(l, fn(n) { -5 <= n && n <= 5 }), 22 | ) 23 | } 24 | 25 | pub fn list_from__generates_valid_values__test() { 26 | use l <- qcheck.run( 27 | qcheck.default_config(), 28 | qcheck.list_from(qcheck.bounded_int(-1000, 1000)), 29 | ) 30 | should.be_true(list.all(l, fn(n) { -1000 <= n && n <= 1000 })) 31 | } 32 | 33 | fn int_list_to_string(l) { 34 | "[" 35 | <> { 36 | list.map(l, int.to_string) 37 | |> string.join(",") 38 | } 39 | <> "]" 40 | } 41 | 42 | pub fn list_generators_shrink_on_size_then_on_elements__test() { 43 | let #(tree, _seed) = 44 | qcheck.generate_tree( 45 | qcheck.generic_list( 46 | elements_from: qcheck.bounded_int(-1, 2), 47 | length_from: qcheck.bounded_int(0, 3), 48 | ), 49 | qcheck.seed(10_003), 50 | ) 51 | 52 | tree 53 | |> tree.to_string(int_list_to_string) 54 | |> birdie.snap("list_generators_shrink_on_size_then_on_elements__test") 55 | } 56 | 57 | pub fn generic_list_doesnt_shrink_out_of_length_range__test_() { 58 | use <- test_spec.make_with_timeout(60) 59 | let min_length = 2 60 | let max_length = 4 61 | 62 | let #(tree, _seed) = 63 | qcheck.generate_tree( 64 | qcheck.generic_list( 65 | elements_from: qcheck.bounded_int(1, 2), 66 | length_from: qcheck.bounded_int(min_length, max_length), 67 | ), 68 | qcheck.random_seed(), 69 | ) 70 | 71 | all_lengths_good(tree, min_length:, max_length:) 72 | } 73 | 74 | pub fn fixed_length_list_from__generates_correct_length__test() { 75 | use #(list, expected_length) <- qcheck.given({ 76 | use length <- qcheck.bind(qcheck.small_non_negative_int()) 77 | use list <- qcheck.map(qcheck.fixed_length_list_from( 78 | qcheck.small_non_negative_int(), 79 | length, 80 | )) 81 | #(list, length) 82 | }) 83 | 84 | should.equal(list.length(list), expected_length) 85 | } 86 | 87 | fn all_lengths_good(tree, min_length min_length, max_length max_length) { 88 | let tree.Tree(root, children) = tree 89 | 90 | let root_length = list.length(root) 91 | let assert True = min_length <= root_length && root_length <= max_length 92 | 93 | children 94 | |> yielder.each(fn(tree) { all_lengths_good(tree, min_length, max_length) }) 95 | } 96 | -------------------------------------------------------------------------------- /test/qcheck/gen_nil_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | 4 | pub fn nil_only_generates_nil__test() { 5 | use nil <- qcheck.run(qcheck.default_config(), qcheck.nil()) 6 | case nil { 7 | Nil -> should.be_true(True) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/qcheck/gen_option_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/option.{None, Some} 2 | import gleam/string 3 | import gleeunit/should 4 | import qcheck 5 | import qcheck/test_error_message 6 | 7 | pub fn option__test() { 8 | use int_option <- qcheck.run( 9 | qcheck.default_config(), 10 | qcheck.small_non_negative_int() 11 | |> qcheck.option_from, 12 | ) 13 | case int_option { 14 | Some(n) -> should.equal(n + 1, 1 + n) 15 | None -> should.be_true(True) 16 | } 17 | } 18 | 19 | pub fn option__failures_shrink_ok__test() { 20 | let run = fn(property) { 21 | qcheck.run( 22 | qcheck.default_config(), 23 | qcheck.small_non_negative_int() |> qcheck.option_from, 24 | property, 25 | ) 26 | } 27 | 28 | let assert Error(msg) = { 29 | use <- test_error_message.rescue 30 | use n <- run 31 | case n { 32 | Some(n) -> should.equal(n, n + 1) 33 | None -> should.be_true(True) 34 | } 35 | } 36 | test_error_message.shrunk_value(msg) 37 | |> should.equal(string.inspect(Some(0))) 38 | 39 | let assert Error(msg) = { 40 | use <- test_error_message.rescue 41 | use n <- run 42 | case n { 43 | Some(n) -> should.be_true(n <= 5 || n == n + 1) 44 | None -> should.be_true(True) 45 | } 46 | } 47 | test_error_message.shrunk_value(msg) 48 | |> should.equal(string.inspect(Some(6))) 49 | 50 | let assert Error(msg) = { 51 | use <- test_error_message.rescue 52 | use n <- run 53 | case n { 54 | Some(n) -> should.equal(n, n) 55 | None -> should.be_true(False) 56 | } 57 | } 58 | test_error_message.shrunk_value(msg) 59 | |> should.equal(string.inspect(None)) 60 | } 61 | 62 | pub fn option_sometimes_generates_none__test() { 63 | let assert Error(msg) = { 64 | use <- test_error_message.rescue 65 | use value <- qcheck.run( 66 | qcheck.default_config(), 67 | qcheck.small_non_negative_int() |> qcheck.option_from, 68 | ) 69 | // All values are `Some` (False) 70 | should.be_true(option.is_some(value)) 71 | } 72 | test_error_message.shrunk_value(msg) 73 | |> should.equal(string.inspect(None)) 74 | } 75 | -------------------------------------------------------------------------------- /test/qcheck/gen_set_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/int 3 | import gleam/list 4 | import gleam/set 5 | import gleam/string 6 | import gleeunit/should 7 | import qcheck 8 | import qcheck/tree 9 | 10 | pub fn generic_set__generates_valid_values__test() { 11 | use s <- qcheck.run( 12 | qcheck.default_config(), 13 | qcheck.generic_set( 14 | elements_from: qcheck.bounded_int(-5, 5), 15 | size_from: qcheck.bounded_int(0, 5), 16 | ), 17 | ) 18 | let len = set.size(s) 19 | let correct_elements = 20 | set.to_list(s) 21 | |> list.all(fn(n) { -5 <= n && n <= 5 }) 22 | should.be_true(len <= 5 && correct_elements) 23 | } 24 | 25 | fn int_set_to_string(s) { 26 | "[" 27 | <> { 28 | set.to_list(s) 29 | // Manually sort because the internal "sorting" is not stable across Erlang 30 | // and JavaScript. 31 | |> list.sort(int.compare) 32 | |> list.map(int.to_string) 33 | |> string.join(",") 34 | } 35 | <> "]" 36 | } 37 | 38 | // Note: the shrinks don't look quite as you would expect compared to the list 39 | // test because sets cannot have duplicates as the lists can. 40 | pub fn set_generators_shrink_on_size_then_on_elements__test() { 41 | let #(tree, _seed) = 42 | qcheck.generate_tree( 43 | qcheck.generic_set( 44 | elements_from: qcheck.bounded_int(-1, 2), 45 | size_from: qcheck.bounded_int(0, 3), 46 | ), 47 | qcheck.seed(10_003), 48 | ) 49 | 50 | tree 51 | |> tree.to_string(int_set_to_string) 52 | |> birdie.snap("set_generators_shrink_on_size_then_on_elements__test") 53 | } 54 | -------------------------------------------------------------------------------- /test/qcheck/gen_string_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/function 3 | import gleam/list 4 | import gleam/regexp 5 | import gleam/string 6 | import gleam/yielder 7 | import gleeunit/should 8 | import qcheck 9 | import qcheck/test_error_message 10 | import qcheck/tree.{type Tree, Tree} 11 | 12 | const test_count: Int = 5000 13 | 14 | pub fn generic_string__test() { 15 | let assert Ok(all_letters) = 16 | regexp.compile( 17 | "^[a-z]+$", 18 | regexp.Options(case_insensitive: False, multi_line: False), 19 | ) 20 | 21 | let has_only_a_through_z = fn(s) { regexp.check(all_letters, s) } 22 | 23 | use s <- qcheck.run( 24 | qcheck.default_config(), 25 | // a - z 26 | qcheck.generic_string( 27 | qcheck.bounded_codepoint(97, 122), 28 | qcheck.bounded_int(1, 10), 29 | ), 30 | ) 31 | let s_len = string.length(s) 32 | should.be_true(1 <= s_len && s_len <= 10 && has_only_a_through_z(s)) 33 | } 34 | 35 | pub fn generic_string__failure_does_not_mess_up_shrinks__test() { 36 | let assert Error(msg) = { 37 | use <- test_error_message.rescue 38 | use s <- qcheck.run( 39 | qcheck.default_config(), 40 | // a - z 41 | qcheck.generic_string( 42 | qcheck.bounded_codepoint(97, 122), 43 | // The empty string should not be generated because it is outside of the 44 | // possible generated lengths. 45 | qcheck.bounded_int(3, 6), 46 | ), 47 | ) 48 | should.be_true( 49 | string.contains(s, "a") 50 | || string.contains(s, "b") 51 | || string.length(s) >= 4, 52 | ) 53 | } 54 | test_error_message.shrunk_value(msg) 55 | |> should.equal(string.inspect("ccc")) 56 | } 57 | 58 | pub fn generic_string__shrinks_okay_2__test() { 59 | let assert Error(msg) = { 60 | use <- test_error_message.rescue 61 | use s <- qcheck.run( 62 | qcheck.default_config(), 63 | // a - z 64 | qcheck.generic_string( 65 | qcheck.bounded_codepoint(97, 122), 66 | qcheck.bounded_int(1, 10), 67 | ), 68 | ) 69 | let len = string.length(s) 70 | should.be_true(len <= 5 || len >= 10 || string.contains(s, "a")) 71 | } 72 | test_error_message.shrunk_value(msg) 73 | |> should.equal(string.inspect("bbbbbb")) 74 | } 75 | 76 | pub fn fixed_length_string_from__shrinks_okay__test() { 77 | let assert Error(msg) = { 78 | use <- test_error_message.rescue 79 | use s <- qcheck.run( 80 | qcheck.default_config(), 81 | // a - z 82 | qcheck.bounded_codepoint(97, 122) 83 | |> qcheck.fixed_length_string_from(2), 84 | ) 85 | should.be_false(string.contains(s, "x")) 86 | } 87 | test_error_message.shrunk_value(msg) 88 | |> should_be_one_of(["ax", "xa"]) 89 | } 90 | 91 | pub fn generic_string__shrinks_okay__test() { 92 | let assert Error(msg) = { 93 | use <- test_error_message.rescue 94 | use s <- qcheck.run( 95 | qcheck.default_config(), 96 | // a - z 97 | qcheck.generic_string( 98 | qcheck.bounded_codepoint(97, 122), 99 | qcheck.bounded_int(1, 10), 100 | ), 101 | ) 102 | let len = string.length(s) 103 | should.be_true(len <= 5 || len >= 10) 104 | } 105 | test_error_message.shrunk_value(msg) 106 | |> should.equal(string.inspect("aaaaaa")) 107 | } 108 | 109 | pub fn string_generators_shrink_on_size_then_on_characters__test() { 110 | let #(tree, _seed) = 111 | qcheck.generate_tree( 112 | qcheck.generic_string( 113 | // Shrinks to `a` 114 | qcheck.bounded_codepoint(97, 99), 115 | qcheck.bounded_int(2, 5), 116 | ), 117 | qcheck.seed(3), 118 | ) 119 | 120 | tree 121 | |> tree.to_string(function.identity) 122 | |> birdie.snap("string_generators_shrink_on_size_then_on_characters__test") 123 | } 124 | 125 | fn check_tree_nodes(tree: Tree(a), predicate: fn(a) -> Bool) -> Bool { 126 | let Tree(root, children) = tree 127 | 128 | let all_true = fn(it) { 129 | it 130 | |> yielder.all(function.identity) 131 | } 132 | 133 | case predicate(root) { 134 | True -> 135 | yielder.map(children, fn(tree: Tree(a)) { 136 | check_tree_nodes(tree, predicate) 137 | }) 138 | |> all_true 139 | False -> False 140 | } 141 | } 142 | 143 | pub fn string_generators_with_specific_length_dont_shrink_on_length__test() { 144 | // Keep this low to keep the speed of the test high. 145 | let length = 3 146 | 147 | let #(tree, _seed) = 148 | qcheck.generate_tree( 149 | qcheck.fixed_length_string_from( 150 | // Shrinks to `a` 151 | qcheck.bounded_codepoint(97, 99), 152 | length, 153 | ), 154 | // Use a random seed here so it tests a new tree each run. 155 | qcheck.random_seed(), 156 | ) 157 | 158 | let string_length_is = fn(length) { fn(s) { string.length(s) == length } } 159 | 160 | tree 161 | |> check_tree_nodes(string_length_is(length)) 162 | |> should.be_true 163 | } 164 | 165 | // The string shrinking is basically tested above and not tested here in the 166 | // context of the `qcheck.run`. 167 | 168 | pub fn string_smoke_test() { 169 | use s <- qcheck.run( 170 | qcheck.default_config() |> qcheck.with_test_count(test_count), 171 | qcheck.string(), 172 | ) 173 | should.be_true(string.length(s) >= 0) 174 | } 175 | 176 | pub fn non_empty_string_generates_non_empty_strings__test() { 177 | use s <- qcheck.run( 178 | qcheck.default_config() |> qcheck.with_test_count(test_count), 179 | qcheck.non_empty_string(), 180 | ) 181 | should.be_true(string.length(s) > 0) 182 | } 183 | 184 | pub fn fixed_length_string__generates_length_n_strings__test() { 185 | use s <- qcheck.run( 186 | qcheck.default_config() |> qcheck.with_test_count(test_count), 187 | // This generator will frequently generate codepoints that combine with 188 | // others to make multicodepoint graphemes. I.e., there grapheme length is 189 | // less than their number of codepoints. 190 | qcheck.fixed_length_string_from(qcheck.codepoint(), 3), 191 | ) 192 | should.equal(string.length(s), 3) 193 | } 194 | 195 | pub fn fixed_length_string__generates_length_n_strings_2__test() { 196 | use s <- qcheck.run( 197 | qcheck.default_config() |> qcheck.with_test_count(test_count), 198 | qcheck.fixed_length_string_from( 199 | // This generator will never generate codepoints that can combine to form 200 | // a multicodepoint string. 201 | qcheck.lowercase_ascii_codepoint(), 202 | 3, 203 | ), 204 | ) 205 | should.equal(string.length(s), 3) 206 | } 207 | 208 | pub fn string_from__generates_correct_values__test() { 209 | let assert Ok(all_ascii_lowercase) = 210 | regexp.compile( 211 | "^[a-z]+$", 212 | regexp.Options(case_insensitive: False, multi_line: False), 213 | ) 214 | 215 | use s <- qcheck.run( 216 | qcheck.default_config() 217 | |> qcheck.with_test_count(test_count), 218 | qcheck.string_from(qcheck.lowercase_ascii_codepoint()), 219 | ) 220 | should.be_true(string.is_empty(s) || regexp.check(all_ascii_lowercase, s)) 221 | } 222 | 223 | pub fn non_empty_string_from__generates_correct_values__test() { 224 | let assert Ok(all_ascii_lowercase) = 225 | regexp.compile( 226 | "^[a-z]+$", 227 | regexp.Options(case_insensitive: False, multi_line: False), 228 | ) 229 | 230 | use s <- qcheck.run( 231 | qcheck.default_config() |> qcheck.with_test_count(test_count), 232 | qcheck.non_empty_string_from(qcheck.lowercase_ascii_codepoint()), 233 | ) 234 | should.be_true(regexp.check(all_ascii_lowercase, s)) 235 | } 236 | 237 | // utils 238 | // 239 | // 240 | 241 | fn should_be_one_of(x, strings) { 242 | let assert Ok(_) = 243 | strings 244 | |> list.map(string.inspect) 245 | |> list.find(one_that: fn(el) { el == x }) 246 | 247 | Nil 248 | } 249 | -------------------------------------------------------------------------------- /test/qcheck/gen_unicode_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | 4 | // This test just ensures that we create `UtfCodepoint`s without raising 5 | // exceptions: once Gleam returns us a value if type `UtfCodepoint`, we know 6 | // that it is valid. 7 | pub fn utf_codepoint__smoke_test() { 8 | use _ <- qcheck.given(qcheck.uniform_codepoint()) 9 | should.be_true(True) 10 | } 11 | -------------------------------------------------------------------------------- /test/qcheck/generate_test.gleam: -------------------------------------------------------------------------------- 1 | import gleam/list 2 | import gleeunit/should 3 | import qcheck 4 | 5 | pub fn generate__test() { 6 | let #(numbers, _seed) = 7 | qcheck.generate(qcheck.bounded_int(-10, 10), 100, qcheck.random_seed()) 8 | 9 | let result = { 10 | use number <- list.all(numbers) 11 | -10 <= number && number <= 10 12 | } 13 | 14 | result |> should.be_true 15 | list.length(numbers) |> should.equal(100) 16 | } 17 | -------------------------------------------------------------------------------- /test/qcheck/large_number_test.gleam: -------------------------------------------------------------------------------- 1 | import gleeunit/should 2 | import qcheck 3 | 4 | // See https://github.com/mooreryan/gleam_qcheck/issues/7 5 | pub fn large_numbers__test() { 6 | use _ <- qcheck.given(qcheck.bounded_int(0, 100_000_000)) 7 | should.be_true(True) 8 | } 9 | -------------------------------------------------------------------------------- /test/qcheck/random_test.gleam: -------------------------------------------------------------------------------- 1 | // These tests are adapted from tests for the `random` module in the `prng` 2 | // package. 3 | 4 | import gleam/dict.{type Dict} 5 | import gleam/float 6 | import gleam/int 7 | import gleam/list 8 | import gleam/option.{type Option, None, Some} 9 | import gleam/order.{type Order} 10 | import gleam/string 11 | import gleam/yielder 12 | import gleeunit/should 13 | import qcheck/random 14 | 15 | pub fn qcheck_random_weighted_never_returns_value_with_zero_weight_test() { 16 | let languages = random.weighted(#(1, "Gleam"), [#(0, "TypeScript")]) 17 | do_test(for_all: languages, that: fn(language) { language == "Gleam" }) 18 | } 19 | 20 | pub fn uniform_generates_values_from_the_given_list_test() { 21 | let examples = random.uniform(1, [2, 3]) 22 | do_test(for_all: examples, that: fn(n) { n == 1 || n == 2 || n == 3 }) 23 | } 24 | 25 | pub fn choose_behaves_the_same_as_uniform_test() { 26 | let gen1 = random.choose(1, 2) 27 | let gen2 = random.uniform(1, [2]) 28 | behaves_the_same(gen1, gen2) 29 | } 30 | 31 | pub fn uniform_behaves_like_weighted_when_all_weights_are_equal_test() { 32 | let gen1 = random.uniform("a", ["b", "c"]) 33 | let gen2 = random.weighted(#(2, "a"), [#(2, "b"), #(2, "c")]) 34 | 35 | assert_similar_distributions(gen1, gen2, string.compare) 36 | } 37 | 38 | pub fn weighted_with_different_but_proportional_weights_test() { 39 | let gen1 = random.weighted(#(2, "a"), [#(2, "b"), #(2, "c")]) 40 | let gen2 = random.weighted(#(1, "a"), [#(1, "b"), #(1, "c")]) 41 | 42 | assert_similar_distributions(gen1, gen2, string.compare) 43 | } 44 | 45 | // MARK: utils 46 | 47 | fn do_test( 48 | for_all generator: random.Generator(a), 49 | that property: fn(a) -> Bool, 50 | ) -> Nil { 51 | let number_of_samples = 1000 52 | let samples = 53 | random.to_random_yielder(generator) 54 | |> yielder.take(number_of_samples) 55 | |> yielder.to_list 56 | 57 | // The yielder should be infinite, so we _must_ always have 1000 samples 58 | list.length(samples) 59 | |> should.equal(number_of_samples) 60 | 61 | // Check that all generated values respect the given property 62 | list.all(samples, property) 63 | |> should.equal(True) 64 | } 65 | 66 | fn behaves_the_same(gen1: random.Generator(a), gen2: random.Generator(a)) -> Nil { 67 | let seed = 68 | random.int(random.min_int, random.max_int) 69 | |> random.map(random.seed) 70 | |> random.random_sample 71 | 72 | let samples1 = 73 | random.to_yielder(gen1, seed) 74 | |> yielder.take(1000) 75 | |> yielder.to_list 76 | let samples2 = 77 | random.to_yielder(gen2, seed) 78 | |> yielder.take(1000) 79 | |> yielder.to_list 80 | 81 | should.equal(samples1, samples2) 82 | } 83 | 84 | /// Check that two distributions are similar. 85 | /// 86 | fn assert_similar_distributions( 87 | gen1: random.Generator(a), 88 | gen2: random.Generator(a), 89 | compare: fn(a, a) -> Order, 90 | ) -> Nil { 91 | let seed = 92 | random.int(random.min_int, random.max_int) 93 | |> random.map(random.seed) 94 | |> random.random_sample 95 | 96 | let samples1 = 97 | random.to_yielder(gen1, seed) 98 | |> yielder.take(100_000) 99 | |> yielder.to_list 100 | let samples2 = 101 | random.to_yielder(gen2, seed) 102 | |> yielder.take(100_000) 103 | |> yielder.to_list 104 | 105 | let proportions1 = samples1 |> frequencies |> proportions 106 | let proportions2 = samples2 |> frequencies |> proportions 107 | 108 | proportions_are_loosely_equal(proportions1, proportions2, compare) 109 | |> should.be_true 110 | } 111 | 112 | /// Count the number of times each element appears in a list. 113 | /// 114 | fn frequencies(lst: List(a)) -> Dict(a, Int) { 115 | list.fold(lst, dict.new(), fn(counts, item) { 116 | dict.upsert(counts, item, increment) 117 | }) 118 | } 119 | 120 | /// Given a dict of frequencies, return a dict of proportions. 121 | /// 122 | fn proportions(frequencies: Dict(a, Int)) -> Dict(a, Float) { 123 | let total = dict.values(frequencies) |> list.fold(0, int.add) |> int.to_float 124 | 125 | dict.map_values(frequencies, fn(_item, count) { int.to_float(count) /. total }) 126 | } 127 | 128 | /// Given a dict of proportions, sort it by the keys. 129 | /// 130 | fn sort_proportions( 131 | proportions: Dict(a, b), 132 | compare_elem: fn(a, a) -> Order, 133 | ) -> List(#(a, b)) { 134 | proportions 135 | |> dict.to_list 136 | |> list.sort(fn(a, b) { 137 | let #(elem_a, _) = a 138 | let #(elem_b, _) = b 139 | compare_elem(elem_a, elem_b) 140 | }) 141 | } 142 | 143 | /// Given two dicts of proportions, check that they are similar. 144 | /// 145 | /// The proportions are considered similar if the keys are the same and the 146 | /// proportions are within 1% of each other. 147 | /// 148 | fn proportions_are_loosely_equal( 149 | proportions1: Dict(a, Float), 150 | proportions2: Dict(a, Float), 151 | compare_elem: fn(a, a) -> Order, 152 | ) -> Bool { 153 | case 154 | list.strict_zip( 155 | sort_proportions(proportions1, compare_elem), 156 | sort_proportions(proportions2, compare_elem), 157 | ) 158 | { 159 | Ok(zipped) -> 160 | zipped 161 | |> list.all(fn(tup) { 162 | let #(#(elem_a, proportion_a), #(elem_b, proportion_b)) = tup 163 | 164 | elem_a == elem_b 165 | && float.loosely_equals(proportion_a, proportion_b, tolerating: 0.01) 166 | }) 167 | 168 | // The lengths are different, so fail. 169 | Error(Nil) -> False 170 | } 171 | } 172 | 173 | /// Increment a value in a dict. 174 | fn increment(x: Option(Int)) -> Int { 175 | case x { 176 | Some(n) -> n + 1 177 | None -> 1 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/qcheck/tree_test.gleam: -------------------------------------------------------------------------------- 1 | import birdie 2 | import gleam/int 3 | import gleam/option.{None, Some} 4 | import gleam/yielder 5 | import gleeunit/should 6 | import qcheck/shrink 7 | import qcheck/tree.{type Tree, Tree} 8 | 9 | fn identity(x) { 10 | x 11 | } 12 | 13 | pub fn int_tree_root_8_shrink_towards_zero__test() { 14 | tree.new(8, shrink.int_towards(0)) 15 | |> tree.to_string(int.to_string) 16 | |> birdie.snap("int_tree_root_8_shrink_towards_zero__test") 17 | } 18 | 19 | pub fn int_tree_root_2_shrink_towards_6__test() { 20 | tree.new(2, shrink.int_towards(6)) 21 | |> tree.to_string(int.to_string) 22 | |> birdie.snap("int_tree_root_2_shrink_towards_6__test") 23 | } 24 | 25 | pub fn int_tree_atomic_shrinker__test() { 26 | tree.new(10, shrink.atomic()) 27 | |> tree.to_string(int.to_string) 28 | |> should.equal("10\n") 29 | } 30 | 31 | pub fn int_option_tree__test() { 32 | tree.new(4, shrink.int_towards(0)) 33 | |> tree.option() 34 | |> tree.to_string(fn(n) { 35 | case n { 36 | None -> "N" 37 | Some(n) -> int.to_string(n) 38 | } 39 | }) 40 | |> birdie.snap("int_option_tree__test") 41 | } 42 | 43 | type Either(a, b) { 44 | First(a) 45 | Second(b) 46 | } 47 | 48 | fn either_to_string(either: Either(a, b), a_to_string, b_to_string) -> String { 49 | case either { 50 | First(a) -> "First(" <> a_to_string(a) <> ")" 51 | Second(b) -> "Second(" <> b_to_string(b) <> ")" 52 | } 53 | } 54 | 55 | pub fn custom_type_tree__test() { 56 | tree.new(4, shrink.int_towards(0)) 57 | |> tree.map(fn(n) { 58 | case n % 2 == 0 { 59 | True -> First(n) 60 | False -> Second(n) 61 | } 62 | }) 63 | |> tree.to_string(fn(either) { 64 | either 65 | |> either_to_string(int.to_string, int.to_string) 66 | }) 67 | |> birdie.snap("custom_type_tree__test") 68 | } 69 | 70 | pub fn trivial_map_test() { 71 | do_trivial_map_test(5) 72 | } 73 | 74 | fn do_trivial_map_test(i) { 75 | case i <= 0 { 76 | True -> Nil 77 | False -> { 78 | let a = 79 | tree.new(i, shrink.int_towards(0)) 80 | |> tree.to_string(int.to_string) 81 | 82 | let b = 83 | tree.new(i, shrink.int_towards(0)) 84 | |> tree.map(identity) 85 | |> tree.to_string(int.to_string) 86 | 87 | should.equal(a, b) 88 | } 89 | } 90 | } 91 | 92 | // bind 93 | // 94 | // 95 | 96 | type MyInt { 97 | MyInt(Int) 98 | } 99 | 100 | // You need a custom shrinker here for the bind. 101 | fn my_int_towards_zero() { 102 | fn(my_int) { 103 | let MyInt(n) = my_int 104 | shrink.int_towards(0)(n) 105 | |> yielder.map(MyInt) 106 | } 107 | } 108 | 109 | fn my_int_to_string(my_int) { 110 | let MyInt(n) = my_int 111 | 112 | int.to_string(n) <> "*" 113 | } 114 | 115 | // Note, these trees will not be the same as the ones generated with the map. 116 | pub fn custom_type_tree_with_bind__test() { 117 | tree.new(3, shrink.int_towards(0)) 118 | |> tree.bind(fn(n) { tree.new(MyInt(n), my_int_towards_zero()) }) 119 | |> tree.to_string(my_int_to_string) 120 | |> birdie.snap("custom_type_tree_with_bind__test") 121 | } 122 | 123 | fn curry2(f) { 124 | fn(a) { fn(b) { f(a, b) } } 125 | } 126 | 127 | fn curry3(f) { 128 | fn(a) { fn(b) { fn(c) { f(a, b, c) } } } 129 | } 130 | 131 | // apply 132 | // 133 | // 134 | 135 | pub fn apply__test() { 136 | let int3_tuple_to_string = fn(abc) { 137 | let #(a, b, c) = abc 138 | int.to_string(a) <> ", " <> int.to_string(b) <> ", " <> int.to_string(c) 139 | } 140 | 141 | let tuple3 = 142 | fn(a, b, c) { #(a, b, c) } 143 | |> curry3 144 | 145 | let make_tree = fn(root: a) -> Tree(a) { tree.new(root, shrink.atomic()) } 146 | 147 | let result = 148 | tuple3 149 | |> tree.return 150 | |> tree.apply(make_tree(3)) 151 | |> tree.apply(make_tree(33)) 152 | |> tree.apply(make_tree(333)) 153 | 154 | let expected = make_tree(#(3, 33, 333)) 155 | 156 | should.equal( 157 | tree.to_string(result, int3_tuple_to_string), 158 | tree.to_string(expected, int3_tuple_to_string), 159 | ) 160 | } 161 | 162 | pub fn apply_with_shrinking__test() { 163 | let int2_tuple_to_string = fn(abc) { 164 | let #(a, b) = abc 165 | "(" <> int.to_string(a) <> ", " <> int.to_string(b) <> ")" 166 | } 167 | 168 | let tuple2 = 169 | fn(a, b) { #(a, b) } 170 | |> curry2 171 | 172 | let make_int_tree = fn(root: Int) -> Tree(Int) { 173 | tree.new(root, shrink.int_towards(0)) 174 | } 175 | 176 | let result = 177 | tuple2 178 | |> tree.return 179 | |> tree.apply(make_int_tree(1)) 180 | |> tree.apply(make_int_tree(2)) 181 | 182 | result 183 | |> tree.to_string(int2_tuple_to_string) 184 | |> birdie.snap("apply_with_shrinking__test") 185 | } 186 | -------------------------------------------------------------------------------- /test/qcheck_test.gleam: -------------------------------------------------------------------------------- 1 | import qcheck_gleeunit_utils/run 2 | 3 | pub fn main() { 4 | run.run_gleeunit() 5 | } 6 | --------------------------------------------------------------------------------