├── .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 | [](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 | [](https://hex.pm/packages/domino)
4 | [](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": "[](https://hex.pm/packages/domino) [](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": "[](https://hex.pm/packages/qcheck_viewer) [](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 |
--------------------------------------------------------------------------------