├── .copywrite.hcl ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── SECURITY.md ├── dependabot.yaml ├── release.yaml └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── assert.go ├── assert_test.go ├── examples_test.go ├── examples_unix_test.go ├── generate.go ├── go.mod ├── go.sum ├── interfaces └── interfaces.go ├── internal ├── assertions │ └── assertions.go ├── constraints │ └── constraints.go └── util │ ├── slices.go │ └── slices_test.go ├── invocations.go ├── invocations_test.go ├── must ├── assert.go ├── assert_test.go ├── examples_test.go ├── examples_unix_test.go ├── fs_default.go ├── fs_windows.go ├── invocations.go ├── invocations_test.go ├── must.go ├── must_test.go ├── scripts.go ├── scripts_test.go ├── settings.go ├── settings_test.go └── testdata │ └── dir1 │ └── file1 ├── portal ├── portal.go ├── portal_default.go ├── portal_test.go └── portal_windows.go ├── scripts.go ├── scripts ├── changes.sh └── generate.sh ├── scripts_test.go ├── settings.go ├── settings_test.go ├── skip ├── skip.go └── skip_test.go ├── test.go ├── test_test.go ├── testdata └── dir1 │ └── file1 ├── util ├── examples_test.go ├── tempfile.go └── tempfile_test.go └── wait ├── wait.go └── wait_test.go /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_holder = "The Test Authors" 6 | copyright_year = 2022 7 | header_ignore = [ 8 | ".golangci.yaml", 9 | ".copywrite.hcl", 10 | ".github/**", 11 | "scripts/**", 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/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, religion, or sexual identity 10 | 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 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of 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 35 | address, 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 e-mail 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 | nobody@example.com. 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 86 | of 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 93 | permanent 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 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at nobody@example.com. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | We take security bugs seriously. We appreciate your efforts to reasonably disclose 2 | your finding and will make every effort to acknowledge your contributions. 3 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - "dependencies" 9 | -------------------------------------------------------------------------------- /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: Changes 4 | labels: 5 | - "*" 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Run CI Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - 'README.md' 6 | - 'LICENSE' 7 | push: 8 | branches: 9 | - 'main' 10 | jobs: 11 | run-copywrite: 12 | timeout-minutes: 5 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: hashicorp/setup-copywrite@v1.1.3 17 | - name: verify copyright 18 | run: | 19 | copywrite headers --plan 20 | run-lint: 21 | timeout-minutes: 5 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: hashicorp/setup-golang@v3 26 | with: 27 | version-file: go.mod 28 | - uses: golangci/golangci-lint-action@v3 29 | with: 30 | version: v1.60.3 31 | run-changes: 32 | timeout-minutes: 5 33 | needs: 34 | - 'run-copywrite' 35 | runs-on: ubuntu-24.04 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: hashicorp/setup-golang@v3 39 | with: 40 | version-file: go.mod 41 | - name: Check for changes 42 | run: | 43 | make changes 44 | run-tests: 45 | timeout-minutes: 5 46 | needs: 47 | - 'run-copywrite' 48 | - 'run-lint' 49 | - 'run-changes' 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | os: 54 | - ubuntu-24.04 55 | - macos-14 56 | - windows-2022 57 | runs-on: ${{matrix.os}} 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: hashicorp/setup-golang@v3 61 | with: 62 | version-file: go.mod 63 | - name: Run Go Test 64 | run: | 65 | make test 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Temporary files 15 | *.bak 16 | 17 | # Go work files 18 | go.work 19 | go.work.sum 20 | 21 | # macOS files 22 | .DS_Store 23 | 24 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | linters: 4 | enable: 5 | - gofmt 6 | - errcheck 7 | - errname 8 | - errorlint 9 | - bodyclose 10 | - durationcheck 11 | - whitespace 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License, version 2.0 2 | 3 | 1. Definitions 4 | 5 | 1.1. "Contributor" 6 | 7 | means each individual or legal entity that creates, contributes to the 8 | creation of, or owns Covered Software. 9 | 10 | 1.2. "Contributor Version" 11 | 12 | means the combination of the Contributions of others (if any) used by a 13 | Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | 21 | means Source Code Form to which the initial Contributor has attached the 22 | notice in Exhibit A, the Executable Form of such Source Code Form, and 23 | Modifications of such Source Code Form, in each case including portions 24 | thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | a. that the initial Contributor has attached the notice described in 30 | Exhibit B to the Covered Software; or 31 | 32 | b. that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the terms of 34 | a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | 38 | means any form of the work other than Source Code Form. 39 | 40 | 1.7. "Larger Work" 41 | 42 | means a work that combines Covered Software with other material, in a 43 | separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | 47 | means this document. 48 | 49 | 1.9. "Licensable" 50 | 51 | means having the right to grant, to the maximum extent possible, whether 52 | at the time of the initial grant or subsequently, any and all of the 53 | rights conveyed by this License. 54 | 55 | 1.10. "Modifications" 56 | 57 | means any of the following: 58 | 59 | a. any file in Source Code Form that results from an addition to, 60 | deletion from, or modification of the contents of Covered Software; or 61 | 62 | b. any new file in Source Code Form that contains any Covered Software. 63 | 64 | 1.11. "Patent Claims" of a Contributor 65 | 66 | means any patent claim(s), including without limitation, method, 67 | process, and apparatus claims, in any patent Licensable by such 68 | Contributor that would be infringed, but for the grant of the License, 69 | by the making, using, selling, offering for sale, having made, import, 70 | or transfer of either its Contributions or its Contributor Version. 71 | 72 | 1.12. "Secondary License" 73 | 74 | means either the GNU General Public License, Version 2.0, the GNU Lesser 75 | General Public License, Version 2.1, the GNU Affero General Public 76 | License, Version 3.0, or any later versions of those licenses. 77 | 78 | 1.13. "Source Code Form" 79 | 80 | means the form of the work preferred for making modifications. 81 | 82 | 1.14. "You" (or "Your") 83 | 84 | means an individual or a legal entity exercising rights under this 85 | License. For legal entities, "You" includes any entity that controls, is 86 | controlled by, or is under common control with You. For purposes of this 87 | definition, "control" means (a) the power, direct or indirect, to cause 88 | the direction or management of such entity, whether by contract or 89 | otherwise, or (b) ownership of more than fifty percent (50%) of the 90 | outstanding shares or beneficial ownership of such entity. 91 | 92 | 93 | 2. License Grants and Conditions 94 | 95 | 2.1. Grants 96 | 97 | Each Contributor hereby grants You a world-wide, royalty-free, 98 | non-exclusive license: 99 | 100 | a. under intellectual property rights (other than patent or trademark) 101 | Licensable by such Contributor to use, reproduce, make available, 102 | modify, display, perform, distribute, and otherwise exploit its 103 | Contributions, either on an unmodified basis, with Modifications, or 104 | as part of a Larger Work; and 105 | 106 | b. under Patent Claims of such Contributor to make, use, sell, offer for 107 | sale, have made, import, and otherwise transfer either its 108 | Contributions or its Contributor Version. 109 | 110 | 2.2. Effective Date 111 | 112 | The licenses granted in Section 2.1 with respect to any Contribution 113 | become effective for each Contribution on the date the Contributor first 114 | distributes such Contribution. 115 | 116 | 2.3. Limitations on Grant Scope 117 | 118 | The licenses granted in this Section 2 are the only rights granted under 119 | this License. No additional rights or licenses will be implied from the 120 | distribution or licensing of Covered Software under this License. 121 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 122 | Contributor: 123 | 124 | a. for any code that a Contributor has removed from Covered Software; or 125 | 126 | b. for infringements caused by: (i) Your and any other third party's 127 | modifications of Covered Software, or (ii) the combination of its 128 | Contributions with other software (except as part of its Contributor 129 | Version); or 130 | 131 | c. under Patent Claims infringed by Covered Software in the absence of 132 | its Contributions. 133 | 134 | This License does not grant any rights in the trademarks, service marks, 135 | or logos of any Contributor (except as may be necessary to comply with 136 | the notice requirements in Section 3.4). 137 | 138 | 2.4. Subsequent Licenses 139 | 140 | No Contributor makes additional grants as a result of Your choice to 141 | distribute the Covered Software under a subsequent version of this 142 | License (see Section 10.2) or under the terms of a Secondary License (if 143 | permitted under the terms of Section 3.3). 144 | 145 | 2.5. Representation 146 | 147 | Each Contributor represents that the Contributor believes its 148 | Contributions are its original creation(s) or it has sufficient rights to 149 | grant the rights to its Contributions conveyed by this License. 150 | 151 | 2.6. Fair Use 152 | 153 | This License is not intended to limit any rights You have under 154 | applicable copyright doctrines of fair use, fair dealing, or other 155 | equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under 169 | the terms of this License. You must inform recipients that the Source 170 | Code Form of the Covered Software is governed by the terms of this 171 | License, and how they can obtain a copy of this License. You may not 172 | attempt to alter or restrict the recipients' rights in the Source Code 173 | Form. 174 | 175 | 3.2. Distribution of Executable Form 176 | 177 | If You distribute Covered Software in Executable Form then: 178 | 179 | a. such Covered Software must also be made available in Source Code Form, 180 | as described in Section 3.1, and You must inform recipients of the 181 | Executable Form how they can obtain a copy of such Source Code Form by 182 | reasonable means in a timely manner, at a charge no more than the cost 183 | of distribution to the recipient; and 184 | 185 | b. You may distribute such Executable Form under the terms of this 186 | License, or sublicense it under different terms, provided that the 187 | license for the Executable Form does not attempt to limit or alter the 188 | recipients' rights in the Source Code Form under this License. 189 | 190 | 3.3. Distribution of a Larger Work 191 | 192 | You may create and distribute a Larger Work under terms of Your choice, 193 | provided that You also comply with the requirements of this License for 194 | the Covered Software. If the Larger Work is a combination of Covered 195 | Software with a work governed by one or more Secondary Licenses, and the 196 | Covered Software is not Incompatible With Secondary Licenses, this 197 | License permits You to additionally distribute such Covered Software 198 | under the terms of such Secondary License(s), so that the recipient of 199 | the Larger Work may, at their option, further distribute the Covered 200 | Software under the terms of either this License or such Secondary 201 | License(s). 202 | 203 | 3.4. Notices 204 | 205 | You may not remove or alter the substance of any license notices 206 | (including copyright notices, patent notices, disclaimers of warranty, or 207 | limitations of liability) contained within the Source Code Form of the 208 | Covered Software, except that You may alter any license notices to the 209 | extent required to remedy known factual inaccuracies. 210 | 211 | 3.5. Application of Additional Terms 212 | 213 | You may choose to offer, and to charge a fee for, warranty, support, 214 | indemnity or liability obligations to one or more recipients of Covered 215 | Software. However, You may do so only on Your own behalf, and not on 216 | behalf of any Contributor. You must make it absolutely clear that any 217 | such warranty, support, indemnity, or liability obligation is offered by 218 | You alone, and You hereby agree to indemnify every Contributor for any 219 | liability incurred by such Contributor as a result of warranty, support, 220 | indemnity or liability terms You offer. You may include additional 221 | disclaimers of warranty and limitations of liability specific to any 222 | jurisdiction. 223 | 224 | 4. Inability to Comply Due to Statute or Regulation 225 | 226 | If it is impossible for You to comply with any of the terms of this License 227 | with respect to some or all of the Covered Software due to statute, 228 | judicial order, or regulation then You must: (a) comply with the terms of 229 | this License to the maximum extent possible; and (b) describe the 230 | limitations and the code they affect. Such description must be placed in a 231 | text file included with all distributions of the Covered Software under 232 | this License. Except to the extent prohibited by statute or regulation, 233 | such description must be sufficiently detailed for a recipient of ordinary 234 | skill to be able to understand it. 235 | 236 | 5. Termination 237 | 238 | 5.1. The rights granted under this License will terminate automatically if You 239 | fail to comply with any of its terms. However, if You become compliant, 240 | then the rights granted under this License from a particular Contributor 241 | are reinstated (a) provisionally, unless and until such Contributor 242 | explicitly and finally terminates Your grants, and (b) on an ongoing 243 | basis, if such Contributor fails to notify You of the non-compliance by 244 | some reasonable means prior to 60 days after You have come back into 245 | compliance. Moreover, Your grants from a particular Contributor are 246 | reinstated on an ongoing basis if such Contributor notifies You of the 247 | non-compliance by some reasonable means, this is the first time You have 248 | received notice of non-compliance with this License from such 249 | Contributor, and You become compliant prior to 30 days after Your receipt 250 | of the notice. 251 | 252 | 5.2. If You initiate litigation against any entity by asserting a patent 253 | infringement claim (excluding declaratory judgment actions, 254 | counter-claims, and cross-claims) alleging that a Contributor Version 255 | directly or indirectly infringes any patent, then the rights granted to 256 | You by any and all Contributors for the Covered Software under Section 257 | 2.1 of this License shall terminate. 258 | 259 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 260 | license agreements (excluding distributors and resellers) which have been 261 | validly granted by You or Your distributors under this License prior to 262 | termination shall survive termination. 263 | 264 | 6. Disclaimer of Warranty 265 | 266 | Covered Software is provided under this License on an "as is" basis, 267 | without warranty of any kind, either expressed, implied, or statutory, 268 | including, without limitation, warranties that the Covered Software is free 269 | of defects, merchantable, fit for a particular purpose or non-infringing. 270 | The entire risk as to the quality and performance of the Covered Software 271 | is with You. Should any Covered Software prove defective in any respect, 272 | You (not any Contributor) assume the cost of any necessary servicing, 273 | repair, or correction. This disclaimer of warranty constitutes an essential 274 | part of this License. No use of any Covered Software is authorized under 275 | this License except under this disclaimer. 276 | 277 | 7. Limitation of Liability 278 | 279 | Under no circumstances and under no legal theory, whether tort (including 280 | negligence), contract, or otherwise, shall any Contributor, or anyone who 281 | distributes Covered Software as permitted above, be liable to You for any 282 | direct, indirect, special, incidental, or consequential damages of any 283 | character including, without limitation, damages for lost profits, loss of 284 | goodwill, work stoppage, computer failure or malfunction, or any and all 285 | other commercial damages or losses, even if such party shall have been 286 | informed of the possibility of such damages. This limitation of liability 287 | shall not apply to liability for death or personal injury resulting from 288 | such party's negligence to the extent applicable law prohibits such 289 | limitation. Some jurisdictions do not allow the exclusion or limitation of 290 | incidental or consequential damages, so this exclusion and limitation may 291 | not apply to You. 292 | 293 | 8. Litigation 294 | 295 | Any litigation relating to this License may be brought only in the courts 296 | of a jurisdiction where the defendant maintains its principal place of 297 | business and such litigation shall be governed by laws of that 298 | jurisdiction, without reference to its conflict-of-law provisions. Nothing 299 | in this Section shall prevent a party's ability to bring cross-claims or 300 | counter-claims. 301 | 302 | 9. Miscellaneous 303 | 304 | This License represents the complete agreement concerning the subject 305 | matter hereof. If any provision of this License is held to be 306 | unenforceable, such provision shall be reformed only to the extent 307 | necessary to make it enforceable. Any law or regulation which provides that 308 | the language of a contract shall be construed against the drafter shall not 309 | be used to construe this License against a Contributor. 310 | 311 | 312 | 10. Versions of the License 313 | 314 | 10.1. New Versions 315 | 316 | Mozilla Foundation is the license steward. Except as provided in Section 317 | 10.3, no one other than the license steward has the right to modify or 318 | publish new versions of this License. Each version will be given a 319 | distinguishing version number. 320 | 321 | 10.2. Effect of New Versions 322 | 323 | You may distribute the Covered Software under the terms of the version 324 | of the License under which You originally received the Covered Software, 325 | or under the terms of any subsequent version published by the license 326 | steward. 327 | 328 | 10.3. Modified Versions 329 | 330 | If you create software not governed by this License, and you want to 331 | create a new license for such software, you may create and use a 332 | modified version of this License if you rename the license and remove 333 | any references to the name of the license steward (except to note that 334 | such modified license differs from this License). 335 | 336 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 337 | Licenses If You choose to distribute Source Code Form that is 338 | Incompatible With Secondary Licenses under the terms of this version of 339 | the License, the notice described in Exhibit B of this License must be 340 | attached. 341 | 342 | Exhibit A - Source Code Form License Notice 343 | 344 | This Source Code Form is subject to the 345 | terms of the Mozilla Public License, v. 346 | 2.0. If a copy of the MPL was not 347 | distributed with this file, You can 348 | obtain one at 349 | http://mozilla.org/MPL/2.0/. 350 | 351 | If it is not possible or desirable to put the notice in a particular file, 352 | then You may include the notice in a location (such as a LICENSE file in a 353 | relevant directory) where a recipient would be likely to look for such a 354 | notice. 355 | 356 | You may add additional accurate notices of copyright ownership. 357 | 358 | Exhibit B - "Incompatible With Secondary Licenses" Notice 359 | 360 | This Source Code Form is "Incompatible 361 | With Secondary Licenses", as defined by 362 | the Mozilla Public License, v. 2.0. 363 | 364 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = bash 2 | 3 | default: test 4 | 5 | .PHONY: test 6 | test: 7 | @echo "--> Running Tests ..." 8 | @go test -v -race ./... 9 | 10 | .PHONY: vet 11 | vet: 12 | @echo "--> Vet Go sources ..." 13 | @go vet ./... 14 | 15 | .PHONY: generate 16 | generate: 17 | @echo "--> Go generate ..." 18 | @go generate ./... 19 | 20 | .PHONY: changes 21 | changes: generate 22 | @echo "--> Checking for source diffs ..." 23 | @go mod tidy 24 | @go fmt ./... 25 | @./scripts/changes.sh 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # test 2 | 3 | 4 | 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/shoenig/test.svg)](https://pkg.go.dev/github.com/shoenig/test) 6 | [![MPL License](https://img.shields.io/github/license/shoenig/test?color=g&style=flat-square)](https://github.com/shoenig/test/blob/main/LICENSE) 7 | [![Run CI Tests](https://github.com/shoenig/test/actions/workflows/ci.yaml/badge.svg)](https://github.com/shoenig/test/actions/workflows/ci.yaml) 8 | 9 | `test` is a modern and generics oriented testing assertions library for Go. 10 | 11 | There are five key packages, 12 | 13 | - `must` - assertions causing test failure and halt the test case immediately 14 | - `test` - assertions causing test failure and allow the test case to continue 15 | - `wait` - utilities for waiting on conditionals in tests 16 | - `skip` - utilities for skipping test cases in some situations 17 | - `util` - utilities for writing concise tests, e.g. managing temp files 18 | - `portal` - utilities for allocating free ports for network listeners in tests 19 | 20 | ### Changes 21 | :ballot_box_with_check: v1.11.0 adds an ErrorAs helper 22 | 23 | - FS examples are more reliable 24 | - Examples run on non-Unix OS when possible 25 | 26 | :ballot_box_with_check: v1.10.0 adds a `util` package for helpers that return values 27 | 28 | - Adds ability to create and automatically clean up temporary files 29 | - Adds `SliceEqOp` and `MapEqOp` helpers 30 | 31 | :ballot_box_with_check: v1.9.0 substantially improves filesystem tests 32 | 33 | - Greater compatibility with Windows 34 | - Fixed assertions on possible errors 35 | 36 | :ballot_box_with_check: v1.8.0 introduces the `skip` package for skipping tests! 37 | 38 | - New helper functions for skipping out tests based on some given criteria 39 | 40 | :ballot_box_with_check: v1.7.0 marks the first stable release! 41 | 42 | - Going forward no breaking changes will be made without a v2 major version 43 | 44 | :ballot_box_with_check: v0.6.0 adds support for custom `cmp.Option` values 45 | 46 | - Adds ability to customize `cmp.Equal` behavior via `cmp.Option` arguments 47 | - Adds assertions for existence of single map key 48 | - Fixes some error outputs 49 | 50 | ### Requirements 51 | 52 | Only depends on `github.com/google/go-cmp`. 53 | 54 | The minimum Go version is `go1.18`. 55 | 56 | ### Install 57 | 58 | Use `go get` to grab the latest version of `test`. 59 | 60 | ```shell 61 | go get -u github.com/shoenig/test@latest 62 | ``` 63 | 64 | ### Influence 65 | 66 | This library was made after a ~decade of using [testify](https://github.com/stretchr/testify), 67 | quite possibly the most used library in the whole Go ecosystem. All credit of 68 | inspiration belongs them. 69 | 70 | ### Philosophy 71 | 72 | Go has always lacked a strong definition of equivalency, and until recently lacked the 73 | language features necessary to make type-safe yet generic assertive statements based on 74 | the contents of values. 75 | 76 | This `test` (and companion `must`) package aims to provide a test-case assertion library 77 | where the caller is in control of how types are compared, and to do so in a strongly typed 78 | way - avoiding erroneous comparisons in the first place. 79 | 80 | Generally there are 4 ways of asserting equivalence between types. 81 | 82 | #### the `==` operator 83 | 84 | Functions like `EqOp` and `ContainsOp` work on types that are `comparable`, i.e., are 85 | compatible with Go's built-in `==` and `!=` operators. 86 | 87 | #### a comparator function 88 | 89 | Functions like `EqFunc` and `ContainsFunc` work on any type, as the caller passes in a 90 | function that takes two arguments of that type, returning a boolean indicating equivalence. 91 | 92 | #### an `.Equal` method 93 | 94 | Functions like `Equal` and `ContainsEqual` work on types implementing the `EqualFunc` 95 | generic interface (i.e. implement an `.Equal` method). The `.Equal` method is called 96 | to determine equivalence. 97 | 98 | #### the `cmp.Equal` or `reflect.DeepEqual` functions 99 | 100 | Functions like `Eq` and `Contains` work on any type, using the `cmp.Equal` or `reflect.DeepEqual` 101 | functions to determine equivalence. Although this is the easiest / most compatible way 102 | to "just compare stuff", it's the least deterministic way of comparing instances of a type. 103 | Changes to the underlying types may cause unexpected changes in their equivalence (e.g., 104 | the addition of unexported fields, function field types, etc.). Assertions that make 105 | use of `cmp.Equal` configured with custom `cmp.Option` values. 106 | 107 | #### output 108 | 109 | When possible, a nice `diff` output is created to show why an equivalence has failed. This 110 | is done via the `cmp.Diff` function. For incompatible types, their `GoString` values are 111 | printed instead. 112 | 113 | All output is directed through `t.Log` functions, and is visible only if test verbosity is 114 | turned on (e.g., `go test -v`). 115 | 116 | #### fail fast vs. fail later 117 | 118 | The `test` and `must` packages are identical, except for how test cases behave when encountering 119 | a failure. Sometimes it is helpful for a test case to continue running even though a failure has 120 | occurred (e.g., it contains cleanup logic not captured via a `t.Cleanup` function). Other times, it 121 | makes sense to fail immediately and stop the test case execution. 122 | 123 | ### `go-cmp` Options 124 | 125 | The test assertions that rely on `cmp.Equal` can be customized in how objects 126 | are compared by [specifying custom](https://github.com/google/go-cmp/blob/master/cmp/options.go#L16) 127 | `cmp.Option` values. These can be configured through `test.Cmp` and `must.Cmp` helpers. 128 | Google provides some common custom behaviors in the [cmpopts](https://github.com/google/go-cmp/tree/master/cmp/cmpopts) 129 | package. The [protocmp](https://github.com/protocolbuffers/protobuf-go/tree/master/testing/protocmp) 130 | package is also particularly helpful when working with Protobuf types. 131 | 132 | Here is an example of comparing two slices, but using a custom Option to sort 133 | the slices so that the order of elements does not matter. 134 | 135 | ```go 136 | a := []int{3, 5, 1, 6, 7} 137 | b := []int{1, 7, 6, 3, 5} 138 | must.Eq(t, a, b, must.Cmp(cmpopts.SortSlices(func(i, j int) bool { 139 | return i < j 140 | }))) 141 | ``` 142 | 143 | ### PostScripts 144 | 145 | Some tests are large and complex (like e2e testing). It can be helpful to provide more context 146 | on test case failures beyond the actual assertion. Logging could do this, but often we want to 147 | only produce output on failure. 148 | 149 | The `test` and `must` packages provide a `PostScript` interface which can be implemented to 150 | add more context in the output of failed tests. There are handy implementations of the `PostScript` 151 | interface provided - `Sprint`, `Sprintf`, `Values`, and `Func`. 152 | 153 | By adding one or more `PostScript` to an assertion, on failure the error message will be appended 154 | with the additional context. 155 | 156 | ```golang 157 | // Add a single Sprintf-string to the output of a failed test assertion. 158 | must.Eq(t, exp, result, must.Sprintf("some more context: %v", value)) 159 | ``` 160 | 161 | ```golang 162 | // Add a formatted key-value map to the output of a failed test assertion. 163 | must.Eq(t, exp, result, must.Values( 164 | "one", 1, 165 | "two", 2, 166 | "fruit", "banana", 167 | )) 168 | ``` 169 | 170 | ```golang 171 | // Add the output from a closure to the output of a failed test assertion. 172 | must.Eq(t, exp, result, must.Func(func() string { 173 | // ... something interesting 174 | return s 175 | }) 176 | ``` 177 | 178 | ### Skip 179 | 180 | Sometimes it makes sense to just skip running a certain test case. Maybe the 181 | operating system is incompatible or a certain required command is not installed. 182 | The `skip` package provides utilities for skipping tests under some given 183 | conditions. 184 | 185 | 186 | ```go 187 | skip.OperatingSystem(t, "windows", "plan9", "dragonfly") 188 | ``` 189 | 190 | ```go 191 | skip.NotArchitecture(t, "amd64", "arm64") 192 | ``` 193 | 194 | ```go 195 | skip.CommandUnavailable(t, "java") 196 | ``` 197 | 198 | ```go 199 | skip.EnvironmentVariableSet(t, "CI") 200 | ``` 201 | 202 | ### Util 203 | 204 | How often have you written a helper method for writing a temporary file in unit 205 | tests? With the `util` package, that boilerplate is resolved once and for all. 206 | 207 | ```go 208 | path := util.TempFile(t, 209 | util.Mode(0o644), 210 | util.String("some content!"), 211 | ) 212 | ``` 213 | 214 | The file referenced by `path` will be cleaned up automatically at the end of 215 | the test run, similar to `t.TempDir()`. 216 | 217 | ### Wait 218 | 219 | Sometimes a test needs to wait on a condition for a non-deterministic amount of time. 220 | For these cases, the `wait` package provides utilities for configuring conditionals 221 | that can assert some condition becomes true, or that some condition remains true - 222 | whether for a specified amount time, or a specific number of iterations. 223 | 224 | A `Constraint` is created in one of two forms 225 | 226 | - `InitialSuccess` - assert a function eventually returns a positive result 227 | - `ContinualSuccess` - assert a function continually returns a positive result 228 | 229 | A `Constraint` may be configured with a few Option functions. 230 | 231 | - `Timeout` - set a time bound on the constraint 232 | - `Attempts` - set an iteration bound on the constraint 233 | - `Gap` - set the iteration interval pace 234 | - `BoolFunc` - set a predicate function of type `func() bool` 235 | - `ErrorFunc` - set a predicate function of type `func() error` 236 | - `TestFunc` - set a predicate function of type `func() (bool, error)` 237 | 238 | #### Assertions form 239 | 240 | The `test` and `must` package implement an assertion helper for using the `wait` package. 241 | 242 | ```go 243 | must.Wait(t, wait.InitialSuccess(wait.ErrorFunc(f))) 244 | ``` 245 | 246 | ```go 247 | must.Wait(t, wait.ContinualSuccess( 248 | wait.ErrorFunc(f), 249 | wait.Attempts(100), 250 | wait.Gap(10 * time.Millisecond), 251 | )) 252 | ``` 253 | 254 | #### Fundamental form 255 | 256 | Although the 99% use case is via the `test` or `must` packages as described above, 257 | the `wait` package can also be used in isolation by calling `Run()` directly. An 258 | error is returned if the conditional failed, and nil otherwise. 259 | 260 | ```go 261 | c := wait.InitialSuccess( 262 | BoolFunc(f), 263 | Timeout(10 * time.Seconds), 264 | Gap(1 * time.Second), 265 | ) 266 | err := c.Run() 267 | ``` 268 | 269 | ### Examples (equality) 270 | 271 | ```go 272 | import "github.com/shoenig/test/must" 273 | 274 | // ... 275 | 276 | e1 := Employee{ID: 100, Name: "Alice"} 277 | e2 := Employee{ID: 101, Name: "Bob"} 278 | 279 | // using cmp.Equal (like magic!) 280 | must.Eq(t, e1, e2) 281 | 282 | // using == operator 283 | must.EqOp(t, e1, e2) 284 | 285 | // using a custom comparator 286 | must.EqFunc(t, e1, e2, func(a, b *Employee) bool { 287 | return a.ID == b.ID 288 | }) 289 | 290 | // using .Equal method 291 | must.Equal(t, e1, e2) 292 | ``` 293 | 294 | ### Output 295 | 296 | The `test` and `must` package attempt to create useful, readable output when an assertion goes awry. Some random examples below. 297 | 298 | ```text 299 | test_test.go:779: expected different file permissions 300 | ↪ name: find 301 | ↪ exp: -rw-rwx-wx 302 | ↪ got: -rwxr-xr-x 303 | ``` 304 | 305 | ```text 306 | tests_test.go:569: expected maps of same values via 'eq' function 307 | ↪ difference: 308 | map[int]test.Person{ 309 | 0: {ID: 100, Name: "Alice"}, 310 | 1: { 311 | ID: 101, 312 | - Name: "Bob", 313 | + Name: "Bob B.", 314 | }, 315 | } 316 | ``` 317 | 318 | ```text 319 | test_test.go:520: expected slice[1].Less(slice[2]) 320 | ↪ slice[1]: &{200 Bob} 321 | ↪ slice[2]: &{150 Carl} 322 | ``` 323 | 324 | ```text 325 | test_test.go:688: expected maps of same values via .Equal method 326 | ↪ differential ↷ 327 | map[int]*test.Person{ 328 | 0: &{ID: 100, Name: "Alice"}, 329 | 1: &{ 330 | - ID: 101, 331 | + ID: 200, 332 | Name: "Bob", 333 | }, 334 | } 335 | ``` 336 | 337 | ```text 338 | test_test.go:801: expected regexp match 339 | ↪ s: abcX 340 | ↪ re: abc\d 341 | ``` 342 | 343 | ### License 344 | 345 | Open source under the [MPL](LICENSE) 346 | -------------------------------------------------------------------------------- /assert.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | // T is the minimal set of functions to be implemented by any testing framework 7 | // compatible with the test package. 8 | type T interface { 9 | Helper() 10 | Errorf(string, ...any) 11 | } 12 | 13 | func errorf(t T, msg string, args ...any) { 14 | t.Helper() 15 | t.Errorf(msg, args...) 16 | } 17 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func (it *internalTest) Errorf(s string, args ...any) { 12 | if !it.trigger { 13 | it.trigger = true 14 | } 15 | msg := strings.TrimSpace(fmt.Sprintf(s, args...)) 16 | it.capture = msg 17 | it.t.Log(msg) 18 | } 19 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io/fs" 10 | "math" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "testing/fstest" 15 | "time" 16 | 17 | "github.com/shoenig/test/wait" 18 | ) 19 | 20 | var t = new(myT) 21 | 22 | // myT is a substitute for testing.T for use in examples 23 | type myT struct{} 24 | 25 | func (t *myT) Errorf(s string, args ...any) { 26 | s = fmt.Sprintf(s, args...) 27 | fmt.Println(s) 28 | } 29 | 30 | func (t *myT) Fatalf(s string, args ...any) { 31 | s = fmt.Sprintf(s, args...) 32 | fmt.Println(s) 33 | } 34 | 35 | func (t *myT) Helper() { 36 | // nothing 37 | } 38 | 39 | type myContainer[T comparable] struct { 40 | items map[T]struct{} 41 | } 42 | 43 | func newContainer[T comparable](items ...T) *myContainer[T] { 44 | c := &myContainer[T]{items: make(map[T]struct{})} 45 | for _, item := range items { 46 | c.items[item] = struct{}{} 47 | } 48 | return c 49 | } 50 | 51 | func (c *myContainer[T]) Contains(item T) bool { 52 | _, exists := c.items[item] 53 | return exists 54 | } 55 | 56 | func (c *myContainer[T]) Empty() bool { 57 | return c.Size() == 0 58 | } 59 | 60 | func (c *myContainer[T]) Size() int { 61 | return len(c.items) 62 | } 63 | 64 | type score int 65 | 66 | func (s score) Less(other score) bool { 67 | return s < other 68 | } 69 | 70 | func (s score) Equal(other score) bool { 71 | return s == other 72 | } 73 | 74 | type scores []score 75 | 76 | func (s scores) Min() score { 77 | min := s[0] 78 | for i := 1; i < len(s); i++ { 79 | if s[i] < min { 80 | min = s[i] 81 | } 82 | } 83 | return min 84 | } 85 | 86 | func (s scores) Max() score { 87 | max := s[0] 88 | for i := 1; i < len(s); i++ { 89 | if s[i] > max { 90 | max = s[i] 91 | } 92 | } 93 | return max 94 | } 95 | 96 | func (s scores) Len() int { 97 | return len(s) 98 | } 99 | 100 | type employee struct { 101 | first string 102 | last string 103 | id int 104 | } 105 | 106 | func (e *employee) Equal(o *employee) bool { 107 | if e == nil || o == nil { 108 | return e == o 109 | } 110 | switch { 111 | case e.first != o.first: 112 | return false 113 | case e.last != o.last: 114 | return false 115 | case e.id != o.id: 116 | return false 117 | } 118 | return true 119 | } 120 | 121 | func (e *employee) Copy() *employee { 122 | return &employee{ 123 | first: e.first, 124 | last: e.last, 125 | id: e.id, 126 | } 127 | } 128 | 129 | func ExampleAscending() { 130 | nums := []int{1, 3, 4, 4, 9} 131 | Ascending(t, nums) 132 | // Output: 133 | } 134 | 135 | func ExampleAscendingCmp() { 136 | labels := []string{"Fun", "great", "Happy", "joyous"} 137 | AscendingCmp(t, labels, func(a, b string) int { 138 | A := strings.ToLower(a) 139 | B := strings.ToLower(b) 140 | switch { 141 | case A == B: 142 | return 0 143 | case A < B: 144 | return -1 145 | default: 146 | return 1 147 | } 148 | }) 149 | // Output: 150 | } 151 | 152 | func ExampleAscendingFunc() { 153 | labels := []string{"Fun", "great", "Happy", "joyous"} 154 | AscendingFunc(t, labels, func(a, b string) bool { 155 | A := strings.ToLower(a) 156 | B := strings.ToLower(b) 157 | return A < B 158 | }) 159 | // Output: 160 | } 161 | 162 | func ExampleAscendingLess() { 163 | nums := []score{4, 6, 7, 9} 164 | AscendingLess(t, nums) 165 | // Output: 166 | } 167 | 168 | func ExampleBetween() { 169 | lower, upper := 3, 9 170 | value := 5 171 | Between(t, lower, value, upper) 172 | // Output: 173 | } 174 | 175 | func ExampleBetweenExclusive() { 176 | lower, upper := 2, 8 177 | value := 4 178 | BetweenExclusive(t, lower, value, upper) 179 | // Output: 180 | } 181 | 182 | func ExampleContains() { 183 | // container implements .Contains method 184 | container := newContainer(2, 4, 6, 8) 185 | Contains[int](t, 4, container) 186 | // Output: 187 | } 188 | 189 | func ExampleContainsSubset() { 190 | // container implements .Contains method 191 | container := newContainer(1, 2, 3, 4, 5, 6) 192 | ContainsSubset[int](t, []int{2, 4, 6}, container) 193 | // Output: 194 | } 195 | 196 | func ExampleDescending() { 197 | nums := []int{9, 6, 5, 4, 4, 2, 1} 198 | Descending(t, nums) 199 | // Output: 200 | } 201 | 202 | func ExampleDescendingCmp() { 203 | nums := []int{9, 5, 3, 3, 1, -2} 204 | DescendingCmp(t, nums, func(a, b int) int { 205 | return a - b 206 | }) 207 | // Output: 208 | } 209 | 210 | func ExampleDescendingFunc() { 211 | words := []string{"Foo", "baz", "Bar", "AND"} 212 | DescendingFunc(t, words, func(a, b string) bool { 213 | lowerA := strings.ToLower(a) 214 | lowerB := strings.ToLower(b) 215 | return lowerA < lowerB 216 | }) 217 | // Output: 218 | } 219 | 220 | func ExampleDescendingLess() { 221 | nums := []score{9, 6, 3, 1, 0} 222 | DescendingLess(t, nums) 223 | // Output: 224 | } 225 | 226 | func ExampleDirExistsFS() { 227 | fsys := fstest.MapFS{ 228 | "foo": &fstest.MapFile{Mode: fs.ModeDir}, 229 | } 230 | DirExistsFS(t, fsys, "foo") 231 | // Output: 232 | } 233 | 234 | func ExampleDirNotExistsFS() { 235 | fsys := fstest.MapFS{} 236 | DirNotExistsFS(t, fsys, "does/not/exist") 237 | // Output: 238 | } 239 | 240 | func ExampleEmpty() { 241 | // container implements .Empty method 242 | container := newContainer[string]() 243 | Empty(t, container) 244 | // Output: 245 | } 246 | 247 | func ExampleEq() { 248 | actual := "hello" 249 | Eq(t, "hello", actual) 250 | // Output: 251 | } 252 | 253 | func ExampleEqError() { 254 | err := errors.New("undefined error") 255 | EqError(t, err, "undefined error") 256 | // Output: 257 | } 258 | 259 | func ExampleEqFunc() { 260 | EqFunc(t, "abcd", "dcba", func(a, b string) bool { 261 | if len(a) != len(b) { 262 | return false 263 | } 264 | l := len(a) 265 | for i := 0; i < l; i++ { 266 | if a[i] != b[l-1-i] { 267 | return false 268 | } 269 | } 270 | return true 271 | }) 272 | // Output: 273 | } 274 | 275 | func ExampleEqJSON() { 276 | a := `{"foo":"bar","numbers":[1,2,3]}` 277 | b := `{"numbers":[1,2,3],"foo":"bar"}` 278 | EqJSON(t, a, b) 279 | // Output: 280 | } 281 | 282 | func ExampleEqOp() { 283 | EqOp(t, 123, 123) 284 | // Output: 285 | } 286 | 287 | func ExampleEqual() { 288 | // score implements .Equal method 289 | Equal(t, score(1000), score(1000)) 290 | // Output: 291 | } 292 | 293 | func ExampleError() { 294 | Error(t, errors.New("error")) 295 | // Output: 296 | } 297 | 298 | func ExampleErrorContains() { 299 | err := errors.New("error beer not found") 300 | ErrorContains(t, err, "beer") 301 | // Output: 302 | } 303 | 304 | func ExampleErrorIs() { 305 | e1 := errors.New("e1") 306 | e2 := fmt.Errorf("e2: %w", e1) 307 | e3 := fmt.Errorf("e3: %w", e2) 308 | ErrorIs(t, e3, e1) 309 | // Output: 310 | } 311 | 312 | func ExampleErrorAs() { 313 | e1 := FakeError("e1") 314 | e2 := fmt.Errorf("e2: %w", e1) 315 | e3 := fmt.Errorf("e3: %w", e2) 316 | var target FakeError 317 | ErrorAs(t, e3, &target) 318 | fmt.Println(target.Error()) 319 | // Output: e1 320 | } 321 | 322 | func ExampleFalse() { 323 | False(t, 1 == int('a')) 324 | // Output: 325 | } 326 | 327 | func ExampleFileContainsFS() { 328 | fsys := fstest.MapFS{ 329 | "example": &fstest.MapFile{ 330 | Data: []byte("foo bar baz"), 331 | }, 332 | } 333 | FileContainsFS(t, fsys, "example", "bar") 334 | // Output: 335 | } 336 | 337 | func ExampleFileExistsFS() { 338 | fsys := fstest.MapFS{ 339 | "example": &fstest.MapFile{}, 340 | } 341 | FileExistsFS(t, fsys, "example") 342 | // Output: 343 | } 344 | 345 | func ExampleFileModeFS() { 346 | fsys := fstest.MapFS{ 347 | "example": &fstest.MapFile{Mode: 0600}, 348 | } 349 | FileModeFS(t, fsys, "example", fs.FileMode(0600)) 350 | // Output: 351 | } 352 | 353 | func ExampleFileNotExistsFS() { 354 | fsys := fstest.MapFS{} 355 | FileNotExistsFS(t, fsys, "not_existing_file") 356 | // Output: 357 | } 358 | 359 | func ExampleFilePathValid() { 360 | FilePathValid(t, "foo/bar/baz") 361 | // Output: 362 | } 363 | 364 | func ExampleGreater() { 365 | Greater(t, 30, 42) 366 | // Output: 367 | } 368 | 369 | func ExampleGreaterEq() { 370 | GreaterEq(t, 30.1, 30.3) 371 | // Output: 372 | } 373 | 374 | func ExampleInDelta() { 375 | InDelta(t, 30.5, 30.54, .1) 376 | // Output: 377 | } 378 | 379 | func ExampleInDeltaSlice() { 380 | nums := []int{51, 48, 55, 49, 52} 381 | base := []int{52, 44, 51, 51, 47} 382 | InDeltaSlice(t, nums, base, 5) 383 | // Output: 384 | } 385 | 386 | func ExampleLen() { 387 | nums := []int{1, 3, 5, 9} 388 | Len(t, 4, nums) 389 | // Output: 390 | } 391 | 392 | func ExampleLength() { 393 | s := scores{89, 93, 91, 99, 88} 394 | Length(t, 5, s) 395 | // Output: 396 | } 397 | 398 | func ExampleLess() { 399 | // compare using < operator 400 | s := score(50) 401 | Less(t, 66, s) 402 | // Output: 403 | } 404 | 405 | func ExampleLessEq() { 406 | s := score(50) 407 | LessEq(t, 50, s) 408 | // Output: 409 | } 410 | 411 | func ExampleLesser() { 412 | // compare using .Less method 413 | s := score(50) 414 | Lesser(t, 66, s) 415 | // Output: 416 | } 417 | 418 | func ExampleMapContainsKey() { 419 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 420 | MapContainsKey(t, numbers, "one") 421 | // Output: 422 | } 423 | 424 | func ExampleMapContainsKeys() { 425 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 426 | keys := []string{"one", "two"} 427 | MapContainsKeys(t, numbers, keys) 428 | // Output: 429 | } 430 | 431 | func ExampleMapContainsValues() { 432 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 433 | values := []int{1, 2} 434 | MapContainsValues(t, numbers, values) 435 | // Output: 436 | } 437 | 438 | func ExampleMapContainsValuesEqual() { 439 | // employee implements .Equal 440 | m := map[int]*employee{ 441 | 0: {first: "armon", id: 101}, 442 | 1: {first: "mitchell", id: 100}, 443 | 2: {first: "dave", id: 102}, 444 | } 445 | expect := []*employee{ 446 | {first: "armon", id: 101}, 447 | {first: "dave", id: 102}, 448 | } 449 | MapContainsValuesEqual(t, m, expect) 450 | // Output: 451 | } 452 | 453 | func ExampleMapContainsValuesFunc() { 454 | m := map[int]string{ 455 | 0: "Zero", 456 | 1: "ONE", 457 | 2: "two", 458 | } 459 | f := func(a, b string) bool { 460 | return strings.EqualFold(a, b) 461 | } 462 | MapContainsValuesFunc(t, m, []string{"one", "two"}, f) 463 | // Output: 464 | } 465 | 466 | func ExampleMapEmpty() { 467 | m := make(map[int]int) 468 | MapEmpty(t, m) 469 | // Output: 470 | } 471 | 472 | func ExampleMapEq() { 473 | m1 := map[string]int{"one": 1, "two": 2, "three": 3} 474 | m2 := map[string]int{"one": 1, "two": 2, "three": 3} 475 | MapEq(t, m1, m2) 476 | // Output: 477 | } 478 | 479 | func ExampleMapEqFunc() { 480 | m1 := map[int]string{ 481 | 0: "Zero", 482 | 1: "one", 483 | 2: "TWO", 484 | } 485 | m2 := map[int]string{ 486 | 0: "ZERO", 487 | 1: "ONE", 488 | 2: "TWO", 489 | } 490 | MapEqFunc(t, m1, m2, func(a, b string) bool { 491 | return strings.EqualFold(a, b) 492 | }) 493 | // Output: 494 | } 495 | 496 | func ExampleMapEqual() { 497 | armon := &employee{first: "armon", id: 101} 498 | mitchell := &employee{first: "mitchell", id: 100} 499 | m1 := map[int]*employee{ 500 | 0: mitchell, 501 | 1: armon, 502 | } 503 | m2 := map[int]*employee{ 504 | 0: mitchell, 505 | 1: armon, 506 | } 507 | MapEqual(t, m1, m2) 508 | // Output: 509 | } 510 | 511 | func ExampleMapEqOp() { 512 | m1 := map[int]string{ 513 | 1: "one", 514 | 2: "two", 515 | } 516 | m2 := map[int]string{ 517 | 1: "one", 518 | 2: "two", 519 | } 520 | MapEqOp(t, m1, m2) 521 | // Output: 522 | } 523 | 524 | func ExampleMapLen() { 525 | m := map[int]string{ 526 | 1: "one", 527 | 2: "two", 528 | } 529 | MapLen(t, 2, m) 530 | // Output: 531 | } 532 | 533 | func ExampleMapNotContainsKey() { 534 | m := map[string]int{ 535 | "one": 1, 536 | "two": 2, 537 | "three": 3, 538 | } 539 | MapNotContainsKey(t, m, "four") 540 | // Output: 541 | } 542 | 543 | func ExampleMapNotContainsKeys() { 544 | m := map[string]int{ 545 | "one": 1, 546 | "two": 2, 547 | } 548 | MapNotContainsKeys(t, m, []string{"three", "four"}) 549 | // Output: 550 | } 551 | 552 | func ExampleMapNotContainsValues() { 553 | m := map[int]string{ 554 | 1: "one", 555 | 2: "two", 556 | } 557 | MapNotContainsValues(t, m, []string{"three", "four"}) 558 | // Output: 559 | } 560 | 561 | func ExampleMapNotContainsValuesEqual() { 562 | m := map[int]*employee{ 563 | 0: {first: "mitchell", id: 100}, 564 | 1: {first: "armon", id: 101}, 565 | } 566 | MapNotContainsValuesEqual(t, m, []*employee{ 567 | {first: "dave", id: 103}, 568 | }) 569 | // Output: 570 | } 571 | 572 | func ExampleMapNotContainsValuesFunc() { 573 | m := map[int]string{ 574 | 1: "One", 575 | 2: "TWO", 576 | 3: "three", 577 | } 578 | f := func(a, b string) bool { 579 | return strings.EqualFold(a, b) 580 | } 581 | MapNotContainsValuesFunc(t, m, []string{"four", "five"}, f) 582 | // Output: 583 | } 584 | 585 | func ExampleMapNotEmpty() { 586 | m := map[string]int{ 587 | "one": 1, 588 | } 589 | MapNotEmpty(t, m) 590 | // Output: 591 | } 592 | 593 | func ExampleMax() { 594 | s := scores{89, 88, 91, 90, 87} 595 | Max[score](t, 91, s) 596 | // Output: 597 | } 598 | 599 | func ExampleMin() { 600 | s := scores{89, 88, 90, 91} 601 | Min[score](t, 88, s) 602 | // Output: 603 | } 604 | 605 | func ExampleNegative() { 606 | Negative(t, -9) 607 | // Output: 608 | } 609 | 610 | func ExampleNil() { 611 | var e *employee 612 | Nil(t, e) 613 | // Output: 614 | } 615 | 616 | func ExampleNoError() { 617 | var err error 618 | NoError(t, err) 619 | // Output: 620 | } 621 | 622 | func ExampleNonNegative() { 623 | NonNegative(t, 4) 624 | // Output: 625 | } 626 | 627 | func ExampleNonPositive() { 628 | NonPositive(t, -3) 629 | // Output: 630 | } 631 | 632 | func ExampleNonZero() { 633 | NonZero(t, .001) 634 | // Output: 635 | } 636 | 637 | func ExampleNotContains() { 638 | c := newContainer("mage", "warrior", "priest", "paladin", "hunter") 639 | NotContains[string](t, "rogue", c) 640 | // Output: 641 | } 642 | 643 | func ExampleNotEmpty() { 644 | c := newContainer("one", "two", "three") 645 | NotEmpty(t, c) 646 | // Output: 647 | } 648 | 649 | func ExampleNotEq() { 650 | NotEq(t, "one", "two") 651 | // Output: 652 | } 653 | 654 | func ExampleNotEqFunc() { 655 | NotEqFunc(t, 4.1, 5.2, func(a, b float64) bool { 656 | return math.Round(a) == math.Round(b) 657 | }) 658 | // Output: 659 | } 660 | 661 | func ExampleNotEqOp() { 662 | NotEqOp(t, 1, 2) 663 | // Output: 664 | } 665 | 666 | func ExampleNotEqual() { 667 | e1 := &employee{first: "alice"} 668 | e2 := &employee{first: "bob"} 669 | NotEqual(t, e1, e2) 670 | // Output: 671 | } 672 | 673 | func ExampleNotNil() { 674 | e := &employee{first: "bob"} 675 | NotNil(t, e) 676 | // Output: 677 | } 678 | 679 | func ExampleOne() { 680 | One(t, 1) 681 | // Output: 682 | } 683 | 684 | func ExamplePositive() { 685 | Positive(t, 42) 686 | // Output: 687 | } 688 | 689 | func ExampleRegexCompiles() { 690 | RegexCompiles(t, `[a-z]{7}`) 691 | // Output: 692 | } 693 | 694 | func ExampleRegexCompilesPOSIX() { 695 | RegexCompilesPOSIX(t, `[a-z]{3}`) 696 | // Output: 697 | } 698 | 699 | func ExampleRegexMatch() { 700 | re := regexp.MustCompile(`[a-z]{6}`) 701 | RegexMatch(t, re, "cookie") 702 | // Output: 703 | } 704 | 705 | func ExampleSize() { 706 | c := newContainer("pie", "brownie", "cake", "cookie") 707 | Size(t, 4, c) 708 | // Output: 709 | } 710 | 711 | func ExampleSliceContains() { 712 | drinks := []string{"ale", "lager", "cider", "wine"} 713 | SliceContains(t, drinks, "cider") 714 | // Output: 715 | } 716 | 717 | func ExampleSliceContainsAll() { 718 | nums := []int{2, 4, 6, 7, 8} 719 | SliceContainsAll(t, nums, []int{7, 8, 2, 6, 4}) 720 | // Output: 721 | } 722 | 723 | func ExampleSliceContainsAllEqual() { 724 | dave := &employee{first: "dave", id: 8} 725 | armon := &employee{first: "armon", id: 2} 726 | mitchell := &employee{first: "mitchell", id: 1} 727 | SliceContainsAllEqual(t, 728 | []*employee{dave, armon, mitchell}, 729 | []*employee{mitchell, dave, armon}) 730 | // Output: 731 | } 732 | 733 | func ExampleSliceContainsAllFunc() { 734 | // comparing slice to element of same type 735 | SliceContainsAllFunc(t, 736 | []string{"UP", "DoWn", "LefT", "RiGHT"}, 737 | []string{"left", "down", "up", "right"}, 738 | func(a, b string) bool { 739 | return strings.EqualFold(a, b) 740 | }) 741 | 742 | // comparing slice to element of different type 743 | SliceContainsAllFunc(t, 744 | []string{"2", "4", "6", "8"}, 745 | []int{2, 6, 4, 8}, 746 | func(a string, b int) bool { 747 | return a == strconv.Itoa(b) 748 | }) 749 | // Output: 750 | } 751 | 752 | func ExampleSliceContainsAllOp() { 753 | SliceContainsAllOp(t, 754 | []int{1, 2, 3, 4, 5}, 755 | []int{5, 4, 3, 2, 1}) 756 | // Output: 757 | } 758 | 759 | func ExampleSliceContainsEqual() { 760 | dave := &employee{first: "dave", id: 8} 761 | armon := &employee{first: "armon", id: 2} 762 | mitchell := &employee{first: "mitchell", id: 1} 763 | employees := []*employee{dave, armon, mitchell} 764 | SliceContainsEqual(t, employees, &employee{first: "dave", id: 8}) 765 | // Output: 766 | } 767 | 768 | func ExampleSliceContainsFunc() { 769 | // comparing slice to element of same type 770 | words := []string{"UP", "DoWn", "LefT", "RiGHT"} 771 | SliceContainsFunc(t, words, "left", func(a, b string) bool { 772 | return strings.EqualFold(a, b) 773 | }) 774 | 775 | // comparing slice to element of different type 776 | nums := []string{"2", "4", "6", "8"} 777 | SliceContainsFunc(t, nums, 4, func(a string, b int) bool { 778 | return a == strconv.Itoa(b) 779 | }) 780 | // Output: 781 | } 782 | 783 | func ExampleSliceContainsOp() { 784 | nums := []int{1, 2, 3, 4, 5} 785 | SliceContainsOp(t, nums, 3) 786 | // Output: 787 | } 788 | 789 | func ExampleSliceContainsSubset() { 790 | nums := []int{10, 20, 30, 40, 50} 791 | SliceContainsSubset(t, nums, []int{40, 10, 30}) 792 | // Output: 793 | } 794 | 795 | func ExampleSliceContainsSubsetEqual() { 796 | dave := &employee{first: "dave", id: 8} 797 | armon := &employee{first: "armon", id: 2} 798 | mitchell := &employee{first: "mitchell", id: 1} 799 | employees := []*employee{dave, armon, mitchell} 800 | subset := []*employee{mitchell, dave} 801 | SliceContainsSubsetEqual(t, employees, subset) 802 | // Output: 803 | } 804 | 805 | func ExampleSliceContainsSubsetFunc() { 806 | // comparing slice to element of same type 807 | words := []string{"UP", "DoWn", "LefT", "RiGHT"} 808 | wordsSubset := []string{"left", "down"} 809 | SliceContainsSubsetFunc(t, words, wordsSubset, func(a, b string) bool { 810 | return strings.EqualFold(a, b) 811 | }) 812 | 813 | // comparing slice to element of different type 814 | nums := []string{"2", "4", "6", "8"} 815 | numsSubset := []int{4, 6} 816 | SliceContainsSubsetFunc(t, nums, numsSubset, func(a string, b int) bool { 817 | return a == strconv.Itoa(b) 818 | }) 819 | // Output: 820 | } 821 | 822 | func ExampleSliceContainsSubsetOp() { 823 | nums := []int{1, 2, 3, 4, 5} 824 | subset := []int{5, 4, 3} 825 | SliceContainsSubsetOp(t, nums, subset) 826 | // Output: 827 | } 828 | 829 | func ExampleSliceEmpty() { 830 | var ints []int 831 | SliceEmpty(t, ints) 832 | // Output: 833 | } 834 | 835 | func ExampleSliceEqFunc() { 836 | ints := []int{2, 4, 6} 837 | strings := []string{"2", "4", "6"} 838 | SliceEqFunc(t, ints, strings, func(exp string, value int) bool { 839 | return strconv.Itoa(value) == exp 840 | }) 841 | // Output: 842 | } 843 | 844 | func ExampleSliceEqual() { 845 | // type employee implements .Equal 846 | dave := &employee{first: "dave"} 847 | armon := &employee{first: "armon"} 848 | mitchell := &employee{first: "mitchell"} 849 | s1 := []*employee{dave, armon, mitchell} 850 | s2 := []*employee{dave, armon, mitchell} 851 | SliceEqual(t, s1, s2) 852 | // Output: 853 | } 854 | 855 | func ExampleSliceEqOp() { 856 | s1 := []int{1, 3, 3, 7} 857 | s2 := []int{1, 3, 3, 7} 858 | SliceEqOp(t, s1, s2) 859 | // Output: 860 | } 861 | 862 | func ExampleSliceLen() { 863 | SliceLen(t, 4, []float64{32, 1.2, 0.01, 9e4}) 864 | // Output: 865 | } 866 | 867 | func ExampleSliceNotContains() { 868 | SliceNotContains(t, []int{1, 2, 4, 5}, 3) 869 | // Output: 870 | } 871 | 872 | func ExampleSliceNotContainsFunc() { 873 | // comparing slice to element of same type 874 | f := func(a, b int) bool { 875 | return a == b 876 | } 877 | SliceNotContainsFunc(t, []int{10, 20, 30}, 50, f) 878 | 879 | // comparing slice to element of different type 880 | g := func(s string, b int) bool { 881 | return strconv.Itoa(b) == s 882 | } 883 | SliceNotContainsFunc(t, []string{"1", "2", "3"}, 5, g) 884 | //Output: 885 | } 886 | 887 | func ExampleSliceNotEmpty() { 888 | SliceNotEmpty(t, []int{2, 4, 6, 8}) 889 | // Output: 890 | } 891 | 892 | func ExampleStrContains() { 893 | StrContains(t, "Visit https://github.com today!", "https://") 894 | // Output: 895 | } 896 | 897 | func ExampleStrContainsAny() { 898 | StrContainsAny(t, "glyph", "aeiouy") 899 | // Output: 900 | } 901 | 902 | func ExampleStrContainsFields() { 903 | StrContainsFields(t, "apple banana cherry grape strawberry", []string{"banana", "grape"}) 904 | // Output: 905 | } 906 | 907 | func ExampleStrContainsFold() { 908 | StrContainsFold(t, "one two three", "TWO") 909 | // Output: 910 | } 911 | 912 | func ExampleStrCount() { 913 | StrCount(t, "see sally sell sea shells by the sea shore", "se", 4) 914 | // Output: 915 | } 916 | 917 | func ExampleStrEqFold() { 918 | StrEqFold(t, "So MANY test Cases!", "so many test cases!") 919 | // Output: 920 | } 921 | 922 | func ExampleStrHasPrefix() { 923 | StrHasPrefix(t, "hello", "hello world!") 924 | // Output: 925 | } 926 | 927 | func ExampleStrHasSuffix() { 928 | StrHasSuffix(t, "world!", "hello world!") 929 | // Output: 930 | } 931 | 932 | func ExampleStrNotContains() { 933 | StrNotContains(t, "public static void main", "def") 934 | // Output: 935 | } 936 | 937 | func ExampleStrNotContainsAny() { 938 | StrNotContainsAny(t, "The quick brown fox", "alyz") 939 | // Output: 940 | } 941 | 942 | func ExampleStrNotContainsFold() { 943 | StrNotContainsFold(t, "This is some text.", "Absent") 944 | // Output: 945 | } 946 | 947 | func ExampleStrNotEqFold() { 948 | StrNotEqFold(t, "This Is SOME text.", "THIS is some TEXT!") 949 | // Output: 950 | } 951 | 952 | func ExampleStrNotHasPrefix() { 953 | StrNotHasPrefix(t, "public static void main", "private") 954 | // Output: 955 | } 956 | 957 | func ExampleStructEqual() { 958 | original := &employee{ 959 | first: "mitchell", 960 | last: "hashimoto", 961 | id: 1, 962 | } 963 | StructEqual(t, original, Tweaks[*employee]{{ 964 | Field: "first", 965 | Apply: func(e *employee) { e.first = "modified" }, 966 | }, { 967 | Field: "last", 968 | Apply: func(e *employee) { e.last = "modified" }, 969 | }, { 970 | Field: "id", 971 | Apply: func(e *employee) { e.id = 999 }, 972 | }}) 973 | // Output: 974 | } 975 | 976 | func ExampleTrue() { 977 | True(t, true) 978 | // Output: 979 | } 980 | 981 | func ExampleUUIDv4() { 982 | UUIDv4(t, "60bf6bb2-dceb-c986-2d47-07ac5d14f247") 983 | // Output: 984 | } 985 | 986 | func ExampleUnreachable() { 987 | if "foo" < "bar" { 988 | Unreachable(t) 989 | } 990 | // Output: 991 | } 992 | 993 | func ExampleValidJSON() { 994 | js := `{"key": ["v1", "v2"]}` 995 | ValidJSON(t, js) 996 | // Output: 997 | } 998 | 999 | func ExampleValidJSONBytes() { 1000 | js := []byte(`{"key": ["v1", "v2"]}`) 1001 | ValidJSONBytes(t, js) 1002 | // Output: 1003 | } 1004 | 1005 | func ExampleWait_initial_success() { 1006 | Wait(t, wait.InitialSuccess( 1007 | wait.BoolFunc(func() bool { 1008 | // will be retried until returns true 1009 | // or timeout is exceeded 1010 | return true 1011 | }), 1012 | wait.Timeout(1*time.Second), 1013 | wait.Gap(100*time.Millisecond), 1014 | )) 1015 | // Output: 1016 | } 1017 | 1018 | func ExampleWait_continual_success() { 1019 | Wait(t, wait.ContinualSuccess( 1020 | wait.BoolFunc(func() bool { 1021 | // will be retried until timeout expires 1022 | // and will fail test if false is ever returned 1023 | return true 1024 | }), 1025 | wait.Timeout(1*time.Second), 1026 | wait.Gap(100*time.Millisecond), 1027 | )) 1028 | // Output: 1029 | } 1030 | 1031 | func ExampleZero() { 1032 | Zero(t, 0) 1033 | Zero(t, 0.0) 1034 | // Output: 1035 | } 1036 | -------------------------------------------------------------------------------- /examples_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build unix 5 | 6 | package test 7 | 8 | import ( 9 | "io/fs" 10 | "os" 11 | ) 12 | 13 | func ExampleDirExists() { 14 | DirExists(t, "/tmp") 15 | // Output: 16 | } 17 | 18 | func ExampleDirNotExists() { 19 | DirNotExists(t, "/does/not/exist") 20 | // Output: 21 | } 22 | 23 | func ExampleFileContains() { 24 | _ = os.WriteFile("/tmp/example", []byte("foo bar baz"), fs.FileMode(0600)) 25 | FileContains(t, "/tmp/example", "bar") 26 | // Output: 27 | } 28 | 29 | func ExampleFileExists() { 30 | _ = os.WriteFile("/tmp/example", []byte{}, fs.FileMode(0600)) 31 | FileExists(t, "/tmp/example") 32 | // Output: 33 | } 34 | 35 | func ExampleFileMode() { 36 | _ = os.WriteFile("/tmp/example_fm", []byte{}, fs.FileMode(0600)) 37 | FileMode(t, "/tmp/example_fm", fs.FileMode(0600)) 38 | // Output: 39 | } 40 | 41 | func ExampleFileNotExists() { 42 | FileNotExists(t, "/tmp/not_existing_file") 43 | // Output: 44 | } 45 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | // Generate must package. 7 | 8 | //go:generate ./scripts/generate.sh 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shoenig/test 2 | 3 | go 1.18 4 | 5 | require github.com/google/go-cmp v0.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | -------------------------------------------------------------------------------- /interfaces/interfaces.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package interfaces 5 | 6 | import ( 7 | "math" 8 | 9 | "github.com/shoenig/test/internal/constraints" 10 | ) 11 | 12 | // MinFunc represents a type implementing the Min method. 13 | type MinFunc[T any] interface { 14 | Min() T 15 | } 16 | 17 | // MaxFunc represents a type implementing the Max method. 18 | type MaxFunc[T any] interface { 19 | Max() T 20 | } 21 | 22 | // EqualFunc represents a type implementing the Equal method. 23 | type EqualFunc[A any] interface { 24 | Equal(A) bool 25 | } 26 | 27 | // CopyFunc represents a type implementing the Copy method. 28 | type CopyFunc[A any] interface { 29 | Copy() A 30 | } 31 | 32 | // CopyEqual represents a type satisfying both EqualFunc and CopyFunc. 33 | type CopyEqual[T any] interface { 34 | EqualFunc[T] 35 | CopyFunc[T] 36 | } 37 | 38 | // TweakFunc is used for modifying a value in tests. 39 | type TweakFunc[E CopyEqual[E]] func(E) 40 | 41 | // LessFunc represents any type implementing the Less method. 42 | type LessFunc[A any] interface { 43 | Less(A) bool 44 | } 45 | 46 | // Map represents any map type where keys are comparable. 47 | type Map[K comparable, V any] interface { 48 | ~map[K]V 49 | } 50 | 51 | // MapEqualFunc represents any map type where keys are comparable and values implement .Equal method. 52 | type MapEqualFunc[K comparable, V EqualFunc[V]] interface { 53 | ~map[K]V 54 | } 55 | 56 | // Number is float, integer, or complex. 57 | type Number interface { 58 | constraints.Ordered 59 | constraints.Float | constraints.Integer | constraints.Complex 60 | } 61 | 62 | // Numeric returns false if n is Inf/NaN. 63 | // 64 | // Always returns true for integral values. 65 | func Numeric[N Number](n N) bool { 66 | check := func(f float64) bool { 67 | if math.IsNaN(f) { 68 | return false 69 | } else if math.IsInf(f, 0) { 70 | return false 71 | } 72 | return true 73 | } 74 | return check(float64(n)) 75 | } 76 | 77 | // The LengthFunc interface is satisfied by a type that implements Len(). 78 | type LengthFunc interface { 79 | Len() int 80 | } 81 | 82 | // The SizeFunc interface is satisfied by a type that implements Size(). 83 | type SizeFunc interface { 84 | Size() int 85 | } 86 | 87 | // The EmptyFunc interface is satisfied by a type that implements Empty(). 88 | type EmptyFunc interface { 89 | Empty() bool 90 | } 91 | 92 | // The ContainsFunc interface is satisfied by a type that implements Contains(T). 93 | type ContainsFunc[T any] interface { 94 | Contains(T) bool 95 | } 96 | -------------------------------------------------------------------------------- /internal/constraints/constraints.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package constraints defines a set of useful constraints to be used 6 | // with type parameters. 7 | package constraints 8 | 9 | // Signed is a constraint that permits any signed integer type. 10 | // If future releases of Go add new predeclared signed integer types, 11 | // this constraint will be modified to include them. 12 | type Signed interface { 13 | ~int | ~int8 | ~int16 | ~int32 | ~int64 14 | } 15 | 16 | // Unsigned is a constraint that permits any unsigned integer type. 17 | // If future releases of Go add new predeclared unsigned integer types, 18 | // this constraint will be modified to include them. 19 | type Unsigned interface { 20 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 21 | } 22 | 23 | // Integer is a constraint that permits any integer type. 24 | // If future releases of Go add new predeclared integer types, 25 | // this constraint will be modified to include them. 26 | type Integer interface { 27 | Signed | Unsigned 28 | } 29 | 30 | // Float is a constraint that permits any floating-point type. 31 | // If future releases of Go add new predeclared floating-point types, 32 | // this constraint will be modified to include them. 33 | type Float interface { 34 | ~float32 | ~float64 35 | } 36 | 37 | // Complex is a constraint that permits any complex numeric type. 38 | // If future releases of Go add new predeclared complex numeric types, 39 | // this constraint will be modified to include them. 40 | type Complex interface { 41 | ~complex64 | ~complex128 42 | } 43 | 44 | // Ordered is a constraint that permits any ordered type: any type 45 | // that supports the operators < <= >= >. 46 | // If future releases of Go add new ordered types, 47 | // this constraint will be modified to include them. 48 | type Ordered interface { 49 | Integer | Float | ~string 50 | } 51 | -------------------------------------------------------------------------------- /internal/util/slices.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | // CloneSliceFunc creates a copy of A by first applying convert to each element. 7 | func CloneSliceFunc[A, B any](original []A, convert func(item A) B) []B { 8 | clone := make([]B, len(original)) 9 | for i := 0; i < len(original); i++ { 10 | clone[i] = convert(original[i]) 11 | } 12 | return clone 13 | } 14 | -------------------------------------------------------------------------------- /internal/util/slices_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util 5 | 6 | import ( 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func TestCloneSliceFunc(t *testing.T) { 12 | t.Run("empty", func(t *testing.T) { 13 | result := CloneSliceFunc([]int{}, func(i int) string { 14 | return strconv.Itoa(i) 15 | }) 16 | if len(result) > 0 { 17 | t.Fatal("expected empty slice") 18 | } 19 | }) 20 | 21 | t.Run("non empty", func(t *testing.T) { 22 | original := []int{1, 4, 5} 23 | result := CloneSliceFunc(original, func(i int) string { 24 | return strconv.Itoa(i) 25 | }) 26 | if len(result) != 3 { 27 | t.Fatal("expected length of 3") 28 | } 29 | if result[0] != "1" { 30 | t.Fatal("expected result[0] == 1") 31 | } 32 | if result[1] != "4" { 33 | t.Fatal("expected result[1] == 4") 34 | } 35 | if result[2] != "5" { 36 | t.Fatal("expected result[2] == 5") 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /invocations.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package test provides a modern generic testing assertions library. 5 | package test 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/shoenig/test/internal/assertions" 11 | ) 12 | 13 | func passing(result string) bool { 14 | return result == "" 15 | } 16 | 17 | func fail(t T, msg string, scripts ...PostScript) { 18 | t.Helper() 19 | c := assertions.Caller() 20 | s := c + msg + "\n" + run(scripts...) 21 | errorf(t, "\n"+strings.TrimSpace(s)+"\n") 22 | } 23 | 24 | func invoke(t T, result string, settings ...Setting) { 25 | t.Helper() 26 | result = strings.TrimSpace(result) 27 | if !passing(result) { 28 | fail(t, result, scripts(settings...)...) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /invocations_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | type testScript struct { 12 | label string 13 | content string 14 | } 15 | 16 | func (ts *testScript) Label() string { 17 | return ts.label 18 | } 19 | 20 | func (ts *testScript) Content() string { 21 | return ts.content 22 | } 23 | 24 | type internalTest struct { 25 | t *testing.T 26 | trigger bool 27 | helper bool 28 | exp string 29 | capture string 30 | } 31 | 32 | func (it *internalTest) TestPostScript(value string) Setting { 33 | return func(s *Settings) { 34 | s.postScripts = append(s.postScripts, &testScript{ 35 | label: "label: " + value, 36 | content: "content: " + value, 37 | }) 38 | } 39 | } 40 | 41 | func (it *internalTest) Helper() { 42 | it.helper = true 43 | } 44 | 45 | func (it *internalTest) assert() { 46 | if !it.helper { 47 | it.t.Fatal("should be marked as helper") 48 | } 49 | if !it.trigger { 50 | it.t.Fatalf("condition expected to trigger; did not") 51 | } 52 | if !strings.Contains(it.capture, it.exp) { 53 | it.t.Fatalf("expected message %q in output, got %q", it.exp, it.capture) 54 | } 55 | } 56 | 57 | func (it *internalTest) assertNot() { 58 | if !it.helper { 59 | it.t.Fatal("should be marked as helper") 60 | } 61 | if it.trigger { 62 | it.t.Fatalf("condition expected not to trigger; it did\ngot message %q in output", it.capture) 63 | } 64 | } 65 | 66 | func (it *internalTest) post() { 67 | if !strings.Contains(it.capture, "PostScript |") { 68 | it.t.Fatal("expected post-script output") 69 | } 70 | } 71 | 72 | func newCase(t *testing.T, msg string) *internalTest { 73 | return &internalTest{ 74 | t: t, 75 | trigger: false, 76 | exp: msg, 77 | } 78 | } 79 | 80 | func newCapture(t *testing.T) *internalTest { 81 | return &internalTest{ 82 | t: t, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /must/assert.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package must 5 | 6 | // T is the minimal set of functions to be implemented by any testing framework 7 | // compatible with the must package. 8 | type T interface { 9 | Helper() 10 | Fatalf(string, ...any) 11 | } 12 | 13 | func errorf(t T, msg string, args ...any) { 14 | t.Helper() 15 | t.Fatalf(msg, args...) 16 | } 17 | -------------------------------------------------------------------------------- /must/assert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package must 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func (it *internalTest) Fatalf(s string, args ...any) { 12 | if !it.trigger { 13 | it.trigger = true 14 | } 15 | msg := strings.TrimSpace(fmt.Sprintf(s, args...)) 16 | it.capture = msg 17 | it.t.Log(msg) 18 | } 19 | -------------------------------------------------------------------------------- /must/examples_test.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "io/fs" 12 | "math" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "testing/fstest" 17 | "time" 18 | 19 | "github.com/shoenig/test/wait" 20 | ) 21 | 22 | var t = new(myT) 23 | 24 | // myT is a substitute for testing.T for use in examples 25 | type myT struct{} 26 | 27 | func (t *myT) Errorf(s string, args ...any) { 28 | s = fmt.Sprintf(s, args...) 29 | fmt.Println(s) 30 | } 31 | 32 | func (t *myT) Fatalf(s string, args ...any) { 33 | s = fmt.Sprintf(s, args...) 34 | fmt.Println(s) 35 | } 36 | 37 | func (t *myT) Helper() { 38 | // nothing 39 | } 40 | 41 | type myContainer[T comparable] struct { 42 | items map[T]struct{} 43 | } 44 | 45 | func newContainer[T comparable](items ...T) *myContainer[T] { 46 | c := &myContainer[T]{items: make(map[T]struct{})} 47 | for _, item := range items { 48 | c.items[item] = struct{}{} 49 | } 50 | return c 51 | } 52 | 53 | func (c *myContainer[T]) Contains(item T) bool { 54 | _, exists := c.items[item] 55 | return exists 56 | } 57 | 58 | func (c *myContainer[T]) Empty() bool { 59 | return c.Size() == 0 60 | } 61 | 62 | func (c *myContainer[T]) Size() int { 63 | return len(c.items) 64 | } 65 | 66 | type score int 67 | 68 | func (s score) Less(other score) bool { 69 | return s < other 70 | } 71 | 72 | func (s score) Equal(other score) bool { 73 | return s == other 74 | } 75 | 76 | type scores []score 77 | 78 | func (s scores) Min() score { 79 | min := s[0] 80 | for i := 1; i < len(s); i++ { 81 | if s[i] < min { 82 | min = s[i] 83 | } 84 | } 85 | return min 86 | } 87 | 88 | func (s scores) Max() score { 89 | max := s[0] 90 | for i := 1; i < len(s); i++ { 91 | if s[i] > max { 92 | max = s[i] 93 | } 94 | } 95 | return max 96 | } 97 | 98 | func (s scores) Len() int { 99 | return len(s) 100 | } 101 | 102 | type employee struct { 103 | first string 104 | last string 105 | id int 106 | } 107 | 108 | func (e *employee) Equal(o *employee) bool { 109 | if e == nil || o == nil { 110 | return e == o 111 | } 112 | switch { 113 | case e.first != o.first: 114 | return false 115 | case e.last != o.last: 116 | return false 117 | case e.id != o.id: 118 | return false 119 | } 120 | return true 121 | } 122 | 123 | func (e *employee) Copy() *employee { 124 | return &employee{ 125 | first: e.first, 126 | last: e.last, 127 | id: e.id, 128 | } 129 | } 130 | 131 | func ExampleAscending() { 132 | nums := []int{1, 3, 4, 4, 9} 133 | Ascending(t, nums) 134 | // Output: 135 | } 136 | 137 | func ExampleAscendingCmp() { 138 | labels := []string{"Fun", "great", "Happy", "joyous"} 139 | AscendingCmp(t, labels, func(a, b string) int { 140 | A := strings.ToLower(a) 141 | B := strings.ToLower(b) 142 | switch { 143 | case A == B: 144 | return 0 145 | case A < B: 146 | return -1 147 | default: 148 | return 1 149 | } 150 | }) 151 | // Output: 152 | } 153 | 154 | func ExampleAscendingFunc() { 155 | labels := []string{"Fun", "great", "Happy", "joyous"} 156 | AscendingFunc(t, labels, func(a, b string) bool { 157 | A := strings.ToLower(a) 158 | B := strings.ToLower(b) 159 | return A < B 160 | }) 161 | // Output: 162 | } 163 | 164 | func ExampleAscendingLess() { 165 | nums := []score{4, 6, 7, 9} 166 | AscendingLess(t, nums) 167 | // Output: 168 | } 169 | 170 | func ExampleBetween() { 171 | lower, upper := 3, 9 172 | value := 5 173 | Between(t, lower, value, upper) 174 | // Output: 175 | } 176 | 177 | func ExampleBetweenExclusive() { 178 | lower, upper := 2, 8 179 | value := 4 180 | BetweenExclusive(t, lower, value, upper) 181 | // Output: 182 | } 183 | 184 | func ExampleContains() { 185 | // container implements .Contains method 186 | container := newContainer(2, 4, 6, 8) 187 | Contains[int](t, 4, container) 188 | // Output: 189 | } 190 | 191 | func ExampleContainsSubset() { 192 | // container implements .Contains method 193 | container := newContainer(1, 2, 3, 4, 5, 6) 194 | ContainsSubset[int](t, []int{2, 4, 6}, container) 195 | // Output: 196 | } 197 | 198 | func ExampleDescending() { 199 | nums := []int{9, 6, 5, 4, 4, 2, 1} 200 | Descending(t, nums) 201 | // Output: 202 | } 203 | 204 | func ExampleDescendingCmp() { 205 | nums := []int{9, 5, 3, 3, 1, -2} 206 | DescendingCmp(t, nums, func(a, b int) int { 207 | return a - b 208 | }) 209 | // Output: 210 | } 211 | 212 | func ExampleDescendingFunc() { 213 | words := []string{"Foo", "baz", "Bar", "AND"} 214 | DescendingFunc(t, words, func(a, b string) bool { 215 | lowerA := strings.ToLower(a) 216 | lowerB := strings.ToLower(b) 217 | return lowerA < lowerB 218 | }) 219 | // Output: 220 | } 221 | 222 | func ExampleDescendingLess() { 223 | nums := []score{9, 6, 3, 1, 0} 224 | DescendingLess(t, nums) 225 | // Output: 226 | } 227 | 228 | func ExampleDirExistsFS() { 229 | fsys := fstest.MapFS{ 230 | "foo": &fstest.MapFile{Mode: fs.ModeDir}, 231 | } 232 | DirExistsFS(t, fsys, "foo") 233 | // Output: 234 | } 235 | 236 | func ExampleDirNotExistsFS() { 237 | fsys := fstest.MapFS{} 238 | DirNotExistsFS(t, fsys, "does/not/exist") 239 | // Output: 240 | } 241 | 242 | func ExampleEmpty() { 243 | // container implements .Empty method 244 | container := newContainer[string]() 245 | Empty(t, container) 246 | // Output: 247 | } 248 | 249 | func ExampleEq() { 250 | actual := "hello" 251 | Eq(t, "hello", actual) 252 | // Output: 253 | } 254 | 255 | func ExampleEqError() { 256 | err := errors.New("undefined error") 257 | EqError(t, err, "undefined error") 258 | // Output: 259 | } 260 | 261 | func ExampleEqFunc() { 262 | EqFunc(t, "abcd", "dcba", func(a, b string) bool { 263 | if len(a) != len(b) { 264 | return false 265 | } 266 | l := len(a) 267 | for i := 0; i < l; i++ { 268 | if a[i] != b[l-1-i] { 269 | return false 270 | } 271 | } 272 | return true 273 | }) 274 | // Output: 275 | } 276 | 277 | func ExampleEqJSON() { 278 | a := `{"foo":"bar","numbers":[1,2,3]}` 279 | b := `{"numbers":[1,2,3],"foo":"bar"}` 280 | EqJSON(t, a, b) 281 | // Output: 282 | } 283 | 284 | func ExampleEqOp() { 285 | EqOp(t, 123, 123) 286 | // Output: 287 | } 288 | 289 | func ExampleEqual() { 290 | // score implements .Equal method 291 | Equal(t, score(1000), score(1000)) 292 | // Output: 293 | } 294 | 295 | func ExampleError() { 296 | Error(t, errors.New("error")) 297 | // Output: 298 | } 299 | 300 | func ExampleErrorContains() { 301 | err := errors.New("error beer not found") 302 | ErrorContains(t, err, "beer") 303 | // Output: 304 | } 305 | 306 | func ExampleErrorIs() { 307 | e1 := errors.New("e1") 308 | e2 := fmt.Errorf("e2: %w", e1) 309 | e3 := fmt.Errorf("e3: %w", e2) 310 | ErrorIs(t, e3, e1) 311 | // Output: 312 | } 313 | 314 | func ExampleErrorAs() { 315 | e1 := FakeError("e1") 316 | e2 := fmt.Errorf("e2: %w", e1) 317 | e3 := fmt.Errorf("e3: %w", e2) 318 | var target FakeError 319 | ErrorAs(t, e3, &target) 320 | fmt.Println(target.Error()) 321 | // Output: e1 322 | } 323 | 324 | func ExampleFalse() { 325 | False(t, 1 == int('a')) 326 | // Output: 327 | } 328 | 329 | func ExampleFileContainsFS() { 330 | fsys := fstest.MapFS{ 331 | "example": &fstest.MapFile{ 332 | Data: []byte("foo bar baz"), 333 | }, 334 | } 335 | FileContainsFS(t, fsys, "example", "bar") 336 | // Output: 337 | } 338 | 339 | func ExampleFileExistsFS() { 340 | fsys := fstest.MapFS{ 341 | "example": &fstest.MapFile{}, 342 | } 343 | FileExistsFS(t, fsys, "example") 344 | // Output: 345 | } 346 | 347 | func ExampleFileModeFS() { 348 | fsys := fstest.MapFS{ 349 | "example": &fstest.MapFile{Mode: 0600}, 350 | } 351 | FileModeFS(t, fsys, "example", fs.FileMode(0600)) 352 | // Output: 353 | } 354 | 355 | func ExampleFileNotExistsFS() { 356 | fsys := fstest.MapFS{} 357 | FileNotExistsFS(t, fsys, "not_existing_file") 358 | // Output: 359 | } 360 | 361 | func ExampleFilePathValid() { 362 | FilePathValid(t, "foo/bar/baz") 363 | // Output: 364 | } 365 | 366 | func ExampleGreater() { 367 | Greater(t, 30, 42) 368 | // Output: 369 | } 370 | 371 | func ExampleGreaterEq() { 372 | GreaterEq(t, 30.1, 30.3) 373 | // Output: 374 | } 375 | 376 | func ExampleInDelta() { 377 | InDelta(t, 30.5, 30.54, .1) 378 | // Output: 379 | } 380 | 381 | func ExampleInDeltaSlice() { 382 | nums := []int{51, 48, 55, 49, 52} 383 | base := []int{52, 44, 51, 51, 47} 384 | InDeltaSlice(t, nums, base, 5) 385 | // Output: 386 | } 387 | 388 | func ExampleLen() { 389 | nums := []int{1, 3, 5, 9} 390 | Len(t, 4, nums) 391 | // Output: 392 | } 393 | 394 | func ExampleLength() { 395 | s := scores{89, 93, 91, 99, 88} 396 | Length(t, 5, s) 397 | // Output: 398 | } 399 | 400 | func ExampleLess() { 401 | // compare using < operator 402 | s := score(50) 403 | Less(t, 66, s) 404 | // Output: 405 | } 406 | 407 | func ExampleLessEq() { 408 | s := score(50) 409 | LessEq(t, 50, s) 410 | // Output: 411 | } 412 | 413 | func ExampleLesser() { 414 | // compare using .Less method 415 | s := score(50) 416 | Lesser(t, 66, s) 417 | // Output: 418 | } 419 | 420 | func ExampleMapContainsKey() { 421 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 422 | MapContainsKey(t, numbers, "one") 423 | // Output: 424 | } 425 | 426 | func ExampleMapContainsKeys() { 427 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 428 | keys := []string{"one", "two"} 429 | MapContainsKeys(t, numbers, keys) 430 | // Output: 431 | } 432 | 433 | func ExampleMapContainsValues() { 434 | numbers := map[string]int{"one": 1, "two": 2, "three": 3} 435 | values := []int{1, 2} 436 | MapContainsValues(t, numbers, values) 437 | // Output: 438 | } 439 | 440 | func ExampleMapContainsValuesEqual() { 441 | // employee implements .Equal 442 | m := map[int]*employee{ 443 | 0: {first: "armon", id: 101}, 444 | 1: {first: "mitchell", id: 100}, 445 | 2: {first: "dave", id: 102}, 446 | } 447 | expect := []*employee{ 448 | {first: "armon", id: 101}, 449 | {first: "dave", id: 102}, 450 | } 451 | MapContainsValuesEqual(t, m, expect) 452 | // Output: 453 | } 454 | 455 | func ExampleMapContainsValuesFunc() { 456 | m := map[int]string{ 457 | 0: "Zero", 458 | 1: "ONE", 459 | 2: "two", 460 | } 461 | f := func(a, b string) bool { 462 | return strings.EqualFold(a, b) 463 | } 464 | MapContainsValuesFunc(t, m, []string{"one", "two"}, f) 465 | // Output: 466 | } 467 | 468 | func ExampleMapEmpty() { 469 | m := make(map[int]int) 470 | MapEmpty(t, m) 471 | // Output: 472 | } 473 | 474 | func ExampleMapEq() { 475 | m1 := map[string]int{"one": 1, "two": 2, "three": 3} 476 | m2 := map[string]int{"one": 1, "two": 2, "three": 3} 477 | MapEq(t, m1, m2) 478 | // Output: 479 | } 480 | 481 | func ExampleMapEqFunc() { 482 | m1 := map[int]string{ 483 | 0: "Zero", 484 | 1: "one", 485 | 2: "TWO", 486 | } 487 | m2 := map[int]string{ 488 | 0: "ZERO", 489 | 1: "ONE", 490 | 2: "TWO", 491 | } 492 | MapEqFunc(t, m1, m2, func(a, b string) bool { 493 | return strings.EqualFold(a, b) 494 | }) 495 | // Output: 496 | } 497 | 498 | func ExampleMapEqual() { 499 | armon := &employee{first: "armon", id: 101} 500 | mitchell := &employee{first: "mitchell", id: 100} 501 | m1 := map[int]*employee{ 502 | 0: mitchell, 503 | 1: armon, 504 | } 505 | m2 := map[int]*employee{ 506 | 0: mitchell, 507 | 1: armon, 508 | } 509 | MapEqual(t, m1, m2) 510 | // Output: 511 | } 512 | 513 | func ExampleMapEqOp() { 514 | m1 := map[int]string{ 515 | 1: "one", 516 | 2: "two", 517 | } 518 | m2 := map[int]string{ 519 | 1: "one", 520 | 2: "two", 521 | } 522 | MapEqOp(t, m1, m2) 523 | // Output: 524 | } 525 | 526 | func ExampleMapLen() { 527 | m := map[int]string{ 528 | 1: "one", 529 | 2: "two", 530 | } 531 | MapLen(t, 2, m) 532 | // Output: 533 | } 534 | 535 | func ExampleMapNotContainsKey() { 536 | m := map[string]int{ 537 | "one": 1, 538 | "two": 2, 539 | "three": 3, 540 | } 541 | MapNotContainsKey(t, m, "four") 542 | // Output: 543 | } 544 | 545 | func ExampleMapNotContainsKeys() { 546 | m := map[string]int{ 547 | "one": 1, 548 | "two": 2, 549 | } 550 | MapNotContainsKeys(t, m, []string{"three", "four"}) 551 | // Output: 552 | } 553 | 554 | func ExampleMapNotContainsValues() { 555 | m := map[int]string{ 556 | 1: "one", 557 | 2: "two", 558 | } 559 | MapNotContainsValues(t, m, []string{"three", "four"}) 560 | // Output: 561 | } 562 | 563 | func ExampleMapNotContainsValuesEqual() { 564 | m := map[int]*employee{ 565 | 0: {first: "mitchell", id: 100}, 566 | 1: {first: "armon", id: 101}, 567 | } 568 | MapNotContainsValuesEqual(t, m, []*employee{ 569 | {first: "dave", id: 103}, 570 | }) 571 | // Output: 572 | } 573 | 574 | func ExampleMapNotContainsValuesFunc() { 575 | m := map[int]string{ 576 | 1: "One", 577 | 2: "TWO", 578 | 3: "three", 579 | } 580 | f := func(a, b string) bool { 581 | return strings.EqualFold(a, b) 582 | } 583 | MapNotContainsValuesFunc(t, m, []string{"four", "five"}, f) 584 | // Output: 585 | } 586 | 587 | func ExampleMapNotEmpty() { 588 | m := map[string]int{ 589 | "one": 1, 590 | } 591 | MapNotEmpty(t, m) 592 | // Output: 593 | } 594 | 595 | func ExampleMax() { 596 | s := scores{89, 88, 91, 90, 87} 597 | Max[score](t, 91, s) 598 | // Output: 599 | } 600 | 601 | func ExampleMin() { 602 | s := scores{89, 88, 90, 91} 603 | Min[score](t, 88, s) 604 | // Output: 605 | } 606 | 607 | func ExampleNegative() { 608 | Negative(t, -9) 609 | // Output: 610 | } 611 | 612 | func ExampleNil() { 613 | var e *employee 614 | Nil(t, e) 615 | // Output: 616 | } 617 | 618 | func ExampleNoError() { 619 | var err error 620 | NoError(t, err) 621 | // Output: 622 | } 623 | 624 | func ExampleNonNegative() { 625 | NonNegative(t, 4) 626 | // Output: 627 | } 628 | 629 | func ExampleNonPositive() { 630 | NonPositive(t, -3) 631 | // Output: 632 | } 633 | 634 | func ExampleNonZero() { 635 | NonZero(t, .001) 636 | // Output: 637 | } 638 | 639 | func ExampleNotContains() { 640 | c := newContainer("mage", "warrior", "priest", "paladin", "hunter") 641 | NotContains[string](t, "rogue", c) 642 | // Output: 643 | } 644 | 645 | func ExampleNotEmpty() { 646 | c := newContainer("one", "two", "three") 647 | NotEmpty(t, c) 648 | // Output: 649 | } 650 | 651 | func ExampleNotEq() { 652 | NotEq(t, "one", "two") 653 | // Output: 654 | } 655 | 656 | func ExampleNotEqFunc() { 657 | NotEqFunc(t, 4.1, 5.2, func(a, b float64) bool { 658 | return math.Round(a) == math.Round(b) 659 | }) 660 | // Output: 661 | } 662 | 663 | func ExampleNotEqOp() { 664 | NotEqOp(t, 1, 2) 665 | // Output: 666 | } 667 | 668 | func ExampleNotEqual() { 669 | e1 := &employee{first: "alice"} 670 | e2 := &employee{first: "bob"} 671 | NotEqual(t, e1, e2) 672 | // Output: 673 | } 674 | 675 | func ExampleNotNil() { 676 | e := &employee{first: "bob"} 677 | NotNil(t, e) 678 | // Output: 679 | } 680 | 681 | func ExampleOne() { 682 | One(t, 1) 683 | // Output: 684 | } 685 | 686 | func ExamplePositive() { 687 | Positive(t, 42) 688 | // Output: 689 | } 690 | 691 | func ExampleRegexCompiles() { 692 | RegexCompiles(t, `[a-z]{7}`) 693 | // Output: 694 | } 695 | 696 | func ExampleRegexCompilesPOSIX() { 697 | RegexCompilesPOSIX(t, `[a-z]{3}`) 698 | // Output: 699 | } 700 | 701 | func ExampleRegexMatch() { 702 | re := regexp.MustCompile(`[a-z]{6}`) 703 | RegexMatch(t, re, "cookie") 704 | // Output: 705 | } 706 | 707 | func ExampleSize() { 708 | c := newContainer("pie", "brownie", "cake", "cookie") 709 | Size(t, 4, c) 710 | // Output: 711 | } 712 | 713 | func ExampleSliceContains() { 714 | drinks := []string{"ale", "lager", "cider", "wine"} 715 | SliceContains(t, drinks, "cider") 716 | // Output: 717 | } 718 | 719 | func ExampleSliceContainsAll() { 720 | nums := []int{2, 4, 6, 7, 8} 721 | SliceContainsAll(t, nums, []int{7, 8, 2, 6, 4}) 722 | // Output: 723 | } 724 | 725 | func ExampleSliceContainsAllEqual() { 726 | dave := &employee{first: "dave", id: 8} 727 | armon := &employee{first: "armon", id: 2} 728 | mitchell := &employee{first: "mitchell", id: 1} 729 | SliceContainsAllEqual(t, 730 | []*employee{dave, armon, mitchell}, 731 | []*employee{mitchell, dave, armon}) 732 | // Output: 733 | } 734 | 735 | func ExampleSliceContainsAllFunc() { 736 | // comparing slice to element of same type 737 | SliceContainsAllFunc(t, 738 | []string{"UP", "DoWn", "LefT", "RiGHT"}, 739 | []string{"left", "down", "up", "right"}, 740 | func(a, b string) bool { 741 | return strings.EqualFold(a, b) 742 | }) 743 | 744 | // comparing slice to element of different type 745 | SliceContainsAllFunc(t, 746 | []string{"2", "4", "6", "8"}, 747 | []int{2, 6, 4, 8}, 748 | func(a string, b int) bool { 749 | return a == strconv.Itoa(b) 750 | }) 751 | // Output: 752 | } 753 | 754 | func ExampleSliceContainsAllOp() { 755 | SliceContainsAllOp(t, 756 | []int{1, 2, 3, 4, 5}, 757 | []int{5, 4, 3, 2, 1}) 758 | // Output: 759 | } 760 | 761 | func ExampleSliceContainsEqual() { 762 | dave := &employee{first: "dave", id: 8} 763 | armon := &employee{first: "armon", id: 2} 764 | mitchell := &employee{first: "mitchell", id: 1} 765 | employees := []*employee{dave, armon, mitchell} 766 | SliceContainsEqual(t, employees, &employee{first: "dave", id: 8}) 767 | // Output: 768 | } 769 | 770 | func ExampleSliceContainsFunc() { 771 | // comparing slice to element of same type 772 | words := []string{"UP", "DoWn", "LefT", "RiGHT"} 773 | SliceContainsFunc(t, words, "left", func(a, b string) bool { 774 | return strings.EqualFold(a, b) 775 | }) 776 | 777 | // comparing slice to element of different type 778 | nums := []string{"2", "4", "6", "8"} 779 | SliceContainsFunc(t, nums, 4, func(a string, b int) bool { 780 | return a == strconv.Itoa(b) 781 | }) 782 | // Output: 783 | } 784 | 785 | func ExampleSliceContainsOp() { 786 | nums := []int{1, 2, 3, 4, 5} 787 | SliceContainsOp(t, nums, 3) 788 | // Output: 789 | } 790 | 791 | func ExampleSliceContainsSubset() { 792 | nums := []int{10, 20, 30, 40, 50} 793 | SliceContainsSubset(t, nums, []int{40, 10, 30}) 794 | // Output: 795 | } 796 | 797 | func ExampleSliceContainsSubsetEqual() { 798 | dave := &employee{first: "dave", id: 8} 799 | armon := &employee{first: "armon", id: 2} 800 | mitchell := &employee{first: "mitchell", id: 1} 801 | employees := []*employee{dave, armon, mitchell} 802 | subset := []*employee{mitchell, dave} 803 | SliceContainsSubsetEqual(t, employees, subset) 804 | // Output: 805 | } 806 | 807 | func ExampleSliceContainsSubsetFunc() { 808 | // comparing slice to element of same type 809 | words := []string{"UP", "DoWn", "LefT", "RiGHT"} 810 | wordsSubset := []string{"left", "down"} 811 | SliceContainsSubsetFunc(t, words, wordsSubset, func(a, b string) bool { 812 | return strings.EqualFold(a, b) 813 | }) 814 | 815 | // comparing slice to element of different type 816 | nums := []string{"2", "4", "6", "8"} 817 | numsSubset := []int{4, 6} 818 | SliceContainsSubsetFunc(t, nums, numsSubset, func(a string, b int) bool { 819 | return a == strconv.Itoa(b) 820 | }) 821 | // Output: 822 | } 823 | 824 | func ExampleSliceContainsSubsetOp() { 825 | nums := []int{1, 2, 3, 4, 5} 826 | subset := []int{5, 4, 3} 827 | SliceContainsSubsetOp(t, nums, subset) 828 | // Output: 829 | } 830 | 831 | func ExampleSliceEmpty() { 832 | var ints []int 833 | SliceEmpty(t, ints) 834 | // Output: 835 | } 836 | 837 | func ExampleSliceEqFunc() { 838 | ints := []int{2, 4, 6} 839 | strings := []string{"2", "4", "6"} 840 | SliceEqFunc(t, ints, strings, func(exp string, value int) bool { 841 | return strconv.Itoa(value) == exp 842 | }) 843 | // Output: 844 | } 845 | 846 | func ExampleSliceEqual() { 847 | // type employee implements .Equal 848 | dave := &employee{first: "dave"} 849 | armon := &employee{first: "armon"} 850 | mitchell := &employee{first: "mitchell"} 851 | s1 := []*employee{dave, armon, mitchell} 852 | s2 := []*employee{dave, armon, mitchell} 853 | SliceEqual(t, s1, s2) 854 | // Output: 855 | } 856 | 857 | func ExampleSliceEqOp() { 858 | s1 := []int{1, 3, 3, 7} 859 | s2 := []int{1, 3, 3, 7} 860 | SliceEqOp(t, s1, s2) 861 | // Output: 862 | } 863 | 864 | func ExampleSliceLen() { 865 | SliceLen(t, 4, []float64{32, 1.2, 0.01, 9e4}) 866 | // Output: 867 | } 868 | 869 | func ExampleSliceNotContains() { 870 | SliceNotContains(t, []int{1, 2, 4, 5}, 3) 871 | // Output: 872 | } 873 | 874 | func ExampleSliceNotContainsFunc() { 875 | // comparing slice to element of same type 876 | f := func(a, b int) bool { 877 | return a == b 878 | } 879 | SliceNotContainsFunc(t, []int{10, 20, 30}, 50, f) 880 | 881 | // comparing slice to element of different type 882 | g := func(s string, b int) bool { 883 | return strconv.Itoa(b) == s 884 | } 885 | SliceNotContainsFunc(t, []string{"1", "2", "3"}, 5, g) 886 | //Output: 887 | } 888 | 889 | func ExampleSliceNotEmpty() { 890 | SliceNotEmpty(t, []int{2, 4, 6, 8}) 891 | // Output: 892 | } 893 | 894 | func ExampleStrContains() { 895 | StrContains(t, "Visit https://github.com today!", "https://") 896 | // Output: 897 | } 898 | 899 | func ExampleStrContainsAny() { 900 | StrContainsAny(t, "glyph", "aeiouy") 901 | // Output: 902 | } 903 | 904 | func ExampleStrContainsFields() { 905 | StrContainsFields(t, "apple banana cherry grape strawberry", []string{"banana", "grape"}) 906 | // Output: 907 | } 908 | 909 | func ExampleStrContainsFold() { 910 | StrContainsFold(t, "one two three", "TWO") 911 | // Output: 912 | } 913 | 914 | func ExampleStrCount() { 915 | StrCount(t, "see sally sell sea shells by the sea shore", "se", 4) 916 | // Output: 917 | } 918 | 919 | func ExampleStrEqFold() { 920 | StrEqFold(t, "So MANY test Cases!", "so many test cases!") 921 | // Output: 922 | } 923 | 924 | func ExampleStrHasPrefix() { 925 | StrHasPrefix(t, "hello", "hello world!") 926 | // Output: 927 | } 928 | 929 | func ExampleStrHasSuffix() { 930 | StrHasSuffix(t, "world!", "hello world!") 931 | // Output: 932 | } 933 | 934 | func ExampleStrNotContains() { 935 | StrNotContains(t, "public static void main", "def") 936 | // Output: 937 | } 938 | 939 | func ExampleStrNotContainsAny() { 940 | StrNotContainsAny(t, "The quick brown fox", "alyz") 941 | // Output: 942 | } 943 | 944 | func ExampleStrNotContainsFold() { 945 | StrNotContainsFold(t, "This is some text.", "Absent") 946 | // Output: 947 | } 948 | 949 | func ExampleStrNotEqFold() { 950 | StrNotEqFold(t, "This Is SOME text.", "THIS is some TEXT!") 951 | // Output: 952 | } 953 | 954 | func ExampleStrNotHasPrefix() { 955 | StrNotHasPrefix(t, "public static void main", "private") 956 | // Output: 957 | } 958 | 959 | func ExampleStructEqual() { 960 | original := &employee{ 961 | first: "mitchell", 962 | last: "hashimoto", 963 | id: 1, 964 | } 965 | StructEqual(t, original, Tweaks[*employee]{{ 966 | Field: "first", 967 | Apply: func(e *employee) { e.first = "modified" }, 968 | }, { 969 | Field: "last", 970 | Apply: func(e *employee) { e.last = "modified" }, 971 | }, { 972 | Field: "id", 973 | Apply: func(e *employee) { e.id = 999 }, 974 | }}) 975 | // Output: 976 | } 977 | 978 | func ExampleTrue() { 979 | True(t, true) 980 | // Output: 981 | } 982 | 983 | func ExampleUUIDv4() { 984 | UUIDv4(t, "60bf6bb2-dceb-c986-2d47-07ac5d14f247") 985 | // Output: 986 | } 987 | 988 | func ExampleUnreachable() { 989 | if "foo" < "bar" { 990 | Unreachable(t) 991 | } 992 | // Output: 993 | } 994 | 995 | func ExampleValidJSON() { 996 | js := `{"key": ["v1", "v2"]}` 997 | ValidJSON(t, js) 998 | // Output: 999 | } 1000 | 1001 | func ExampleValidJSONBytes() { 1002 | js := []byte(`{"key": ["v1", "v2"]}`) 1003 | ValidJSONBytes(t, js) 1004 | // Output: 1005 | } 1006 | 1007 | func ExampleWait_initial_success() { 1008 | Wait(t, wait.InitialSuccess( 1009 | wait.BoolFunc(func() bool { 1010 | // will be retried until returns true 1011 | // or timeout is exceeded 1012 | return true 1013 | }), 1014 | wait.Timeout(1*time.Second), 1015 | wait.Gap(100*time.Millisecond), 1016 | )) 1017 | // Output: 1018 | } 1019 | 1020 | func ExampleWait_continual_success() { 1021 | Wait(t, wait.ContinualSuccess( 1022 | wait.BoolFunc(func() bool { 1023 | // will be retried until timeout expires 1024 | // and will fail test if false is ever returned 1025 | return true 1026 | }), 1027 | wait.Timeout(1*time.Second), 1028 | wait.Gap(100*time.Millisecond), 1029 | )) 1030 | // Output: 1031 | } 1032 | 1033 | func ExampleZero() { 1034 | Zero(t, 0) 1035 | Zero(t, 0.0) 1036 | // Output: 1037 | } 1038 | -------------------------------------------------------------------------------- /must/examples_unix_test.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | //go:build unix 7 | 8 | package must 9 | 10 | import ( 11 | "io/fs" 12 | "os" 13 | ) 14 | 15 | func ExampleDirExists() { 16 | DirExists(t, "/tmp") 17 | // Output: 18 | } 19 | 20 | func ExampleDirNotExists() { 21 | DirNotExists(t, "/does/not/exist") 22 | // Output: 23 | } 24 | 25 | func ExampleFileContains() { 26 | _ = os.WriteFile("/tmp/example", []byte("foo bar baz"), fs.FileMode(0600)) 27 | FileContains(t, "/tmp/example", "bar") 28 | // Output: 29 | } 30 | 31 | func ExampleFileExists() { 32 | _ = os.WriteFile("/tmp/example", []byte{}, fs.FileMode(0600)) 33 | FileExists(t, "/tmp/example") 34 | // Output: 35 | } 36 | 37 | func ExampleFileMode() { 38 | _ = os.WriteFile("/tmp/example_fm", []byte{}, fs.FileMode(0600)) 39 | FileMode(t, "/tmp/example_fm", fs.FileMode(0600)) 40 | // Output: 41 | } 42 | 43 | func ExampleFileNotExists() { 44 | FileNotExists(t, "/tmp/not_existing_file") 45 | // Output: 46 | } 47 | -------------------------------------------------------------------------------- /must/fs_default.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | //go:build !windows 4 | 5 | package must 6 | 7 | var ( 8 | fsRoot = "/" 9 | ) 10 | -------------------------------------------------------------------------------- /must/fs_windows.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | //go:build windows 4 | 5 | package must 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | var ( 12 | fsRoot = os.Getenv("HOMEDRIVE") 13 | ) 14 | 15 | func init() { 16 | if fsRoot == "" { 17 | fsRoot = "C:" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /must/invocations.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | // Package test provides a modern generic testing assertions library. 7 | package must 8 | 9 | import ( 10 | "strings" 11 | 12 | "github.com/shoenig/test/internal/assertions" 13 | ) 14 | 15 | func passing(result string) bool { 16 | return result == "" 17 | } 18 | 19 | func fail(t T, msg string, scripts ...PostScript) { 20 | t.Helper() 21 | c := assertions.Caller() 22 | s := c + msg + "\n" + run(scripts...) 23 | errorf(t, "\n"+strings.TrimSpace(s)+"\n") 24 | } 25 | 26 | func invoke(t T, result string, settings ...Setting) { 27 | t.Helper() 28 | result = strings.TrimSpace(result) 29 | if !passing(result) { 30 | fail(t, result, scripts(settings...)...) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /must/invocations_test.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import ( 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type testScript struct { 14 | label string 15 | content string 16 | } 17 | 18 | func (ts *testScript) Label() string { 19 | return ts.label 20 | } 21 | 22 | func (ts *testScript) Content() string { 23 | return ts.content 24 | } 25 | 26 | type internalTest struct { 27 | t *testing.T 28 | trigger bool 29 | helper bool 30 | exp string 31 | capture string 32 | } 33 | 34 | func (it *internalTest) TestPostScript(value string) Setting { 35 | return func(s *Settings) { 36 | s.postScripts = append(s.postScripts, &testScript{ 37 | label: "label: " + value, 38 | content: "content: " + value, 39 | }) 40 | } 41 | } 42 | 43 | func (it *internalTest) Helper() { 44 | it.helper = true 45 | } 46 | 47 | func (it *internalTest) assert() { 48 | if !it.helper { 49 | it.t.Fatal("should be marked as helper") 50 | } 51 | if !it.trigger { 52 | it.t.Fatalf("condition expected to trigger; did not") 53 | } 54 | if !strings.Contains(it.capture, it.exp) { 55 | it.t.Fatalf("expected message %q in output, got %q", it.exp, it.capture) 56 | } 57 | } 58 | 59 | func (it *internalTest) assertNot() { 60 | if !it.helper { 61 | it.t.Fatal("should be marked as helper") 62 | } 63 | if it.trigger { 64 | it.t.Fatalf("condition expected not to trigger; it did\ngot message %q in output", it.capture) 65 | } 66 | } 67 | 68 | func (it *internalTest) post() { 69 | if !strings.Contains(it.capture, "PostScript |") { 70 | it.t.Fatal("expected post-script output") 71 | } 72 | } 73 | 74 | func newCase(t *testing.T, msg string) *internalTest { 75 | return &internalTest{ 76 | t: t, 77 | trigger: false, 78 | exp: msg, 79 | } 80 | } 81 | 82 | func newCapture(t *testing.T) *internalTest { 83 | return &internalTest{ 84 | t: t, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /must/scripts.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import ( 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | func run(posts ...PostScript) string { 14 | s := new(strings.Builder) 15 | for _, post := range posts { 16 | s.WriteString("↪ PostScript | ") 17 | s.WriteString(post.Label()) 18 | s.WriteString(" ↷\n") 19 | s.WriteString(post.Content()) 20 | s.WriteString("\n") 21 | } 22 | return s.String() 23 | } 24 | 25 | // A PostScript is used to annotate a test failure with additional information. 26 | // 27 | // Can be useful in large e2e style test cases, where adding additional context 28 | // beyond an assertion helps in debugging. 29 | type PostScript interface { 30 | // Label should categorize what is in Content. 31 | Label() string 32 | 33 | // Content contains extra contextual information for debugging a test failure. 34 | Content() string 35 | } 36 | 37 | type script struct { 38 | label string 39 | content string 40 | } 41 | 42 | func (s *script) Label() string { 43 | return strings.TrimSpace(s.label) 44 | } 45 | func (s *script) Content() string { 46 | return "\t" + strings.TrimSpace(s.content) 47 | } 48 | 49 | // Sprintf appends a Sprintf-string as an annotation to the output of a test case failure. 50 | func Sprintf(msg string, args ...any) Setting { 51 | return func(s *Settings) { 52 | s.postScripts = append(s.postScripts, &script{ 53 | label: "annotation", 54 | content: fmt.Sprintf(msg, args...), 55 | }) 56 | } 57 | } 58 | 59 | // Sprint appends a Sprint-string as an annotation to the output of a test case failure. 60 | func Sprint(args ...any) Setting { 61 | return func(s *Settings) { 62 | s.postScripts = append(s.postScripts, &script{ 63 | label: "annotation", 64 | content: strings.TrimSpace(fmt.Sprintln(args...)), 65 | }) 66 | } 67 | } 68 | 69 | // Values adds formatted key-val mappings as an annotation to the output of a test case failure. 70 | func Values(vals ...any) Setting { 71 | b := new(strings.Builder) 72 | n := len(vals) 73 | for i := 0; i < n-1; i += 2 { 74 | s := fmt.Sprintf("\t%#v => %#v\n", vals[i], vals[i+1]) 75 | b.WriteString(s) 76 | } 77 | if n%2 != 0 { 78 | s := fmt.Sprintf("\t%v => ", vals[n-1]) 79 | b.WriteString(s) 80 | } 81 | content := b.String() 82 | return func(s *Settings) { 83 | s.postScripts = append(s.postScripts, &script{ 84 | label: "mapping", 85 | content: content, 86 | }) 87 | } 88 | } 89 | 90 | // Func adds the string produced by f as an annotation to the output of a test case failure. 91 | func Func(f func() string) Setting { 92 | return func(s *Settings) { 93 | s.postScripts = append(s.postScripts, &script{ 94 | label: "function", 95 | content: f(), 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /must/scripts_test.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import "testing" 9 | 10 | func TestPostScript_Label(t *testing.T) { 11 | var s PostScript = &script{label: "\nhello "} 12 | if s.Label() != "hello" { 13 | t.FailNow() 14 | } 15 | } 16 | 17 | func TestPostScript_Content(t *testing.T) { 18 | var s PostScript = &script{content: "\nhello "} 19 | if s.Content() != "\thello" { 20 | t.FailNow() 21 | } 22 | } 23 | 24 | func TestPostScript_Sprintf(t *testing.T) { 25 | ps := Sprintf("foo %s %d", "baz", 1) 26 | result := run(scripts(ps)...) 27 | exp := "↪ PostScript | annotation ↷\n\tfoo baz 1\n" 28 | if result != exp { 29 | t.Fatalf("exp %s, got %s", exp, result) 30 | } 31 | } 32 | 33 | func TestPostScript_KV(t *testing.T) { 34 | ps := Values("one", 1, "foo", "bar") 35 | result := run(scripts(ps)...) 36 | exp := "↪ PostScript | mapping ↷\n\t\"one\" => 1\n\t\"foo\" => \"bar\"\n" 37 | if result != exp { 38 | t.Fatalf("exp %s, got %s", exp, result) 39 | } 40 | } 41 | 42 | func TestPostScript_Func(t *testing.T) { 43 | ps := Func(func() string { 44 | return "hello" 45 | }) 46 | result := run(scripts(ps)...) 47 | exp := "↪ PostScript | function ↷\n\thello\n" 48 | if result != exp { 49 | t.Fatalf("exp %s, got %s", exp, result) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /must/settings.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import ( 9 | "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | // Settings are used to manage a collection of Setting values used to modify 13 | // the behavior of a test case assertion. Currently supports specifying custom 14 | // error output content, and custom cmp.Option comparators / transforms. 15 | // 16 | // Use Cmp for specifying custom cmp.Option values. 17 | // 18 | // Use Sprint, Sprintf, Values, Func for specifying custom failure output messages. 19 | type Settings struct { 20 | postScripts []PostScript 21 | cmpOptions []cmp.Option 22 | } 23 | 24 | // A Setting changes the behavior of a test case assertion. 25 | type Setting func(s *Settings) 26 | 27 | // Cmp enables configuring cmp.Option values for modifying the behavior of the 28 | // cmp.Equal function. Custom cmp.Option values control how the cmp.Equal function 29 | // determines equality between the two objects. 30 | // 31 | // https://github.com/google/go-cmp/blob/master/cmp/options.go#L16 32 | func Cmp(options ...cmp.Option) Setting { 33 | return func(s *Settings) { 34 | s.cmpOptions = append(s.cmpOptions, options...) 35 | } 36 | } 37 | 38 | func options(settings ...Setting) []cmp.Option { 39 | s := new(Settings) 40 | for _, setting := range settings { 41 | setting(s) 42 | } 43 | return s.cmpOptions 44 | } 45 | 46 | func scripts(settings ...Setting) []PostScript { 47 | s := new(Settings) 48 | for _, setting := range settings { 49 | setting(s) 50 | } 51 | return s.postScripts 52 | } 53 | -------------------------------------------------------------------------------- /must/settings_test.go: -------------------------------------------------------------------------------- 1 | // Code generated via scripts/generate.sh. DO NOT EDIT. 2 | 3 | // Copyright (c) The Test Authors 4 | // SPDX-License-Identifier: MPL-2.0 5 | 6 | package must 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp/cmpopts" 12 | ) 13 | 14 | var cmpSortSlices = Cmp(cmpopts.SortSlices(func(i, j int) bool { 15 | return i < j 16 | })) 17 | 18 | func TestCmp_Eq(t *testing.T) { 19 | a := []int{3, 5, 1, 6, 7} 20 | b := []int{1, 7, 6, 3, 5} 21 | Eq(t, a, b, cmpSortSlices) 22 | } 23 | 24 | func TestCmp_NotEq(t *testing.T) { 25 | a := []int{3, 5, 1, 6, 0} 26 | b := []int{1, 7, 6, 3, 5} 27 | NotEq(t, a, b, cmpSortSlices) 28 | } 29 | 30 | func TestCmp_SliceContains(t *testing.T) { 31 | a := [][]int{{1}, {1, 2}} 32 | SliceContains(t, a, []int{2, 1}, cmpSortSlices) 33 | } 34 | 35 | func TestCmp_SliceNotContains(t *testing.T) { 36 | a := [][]int{{1}, {1, 2}} 37 | SliceNotContains(t, a, []int{3, 1}, cmpSortSlices) 38 | } 39 | 40 | func TestCmp_MapContainsValues(t *testing.T) { 41 | m1 := map[string][]int{ 42 | "one": {1, 3, 5, 7}, 43 | } 44 | MapContainsValues(t, m1, [][]int{{7, 5, 1, 3}}, cmpSortSlices) 45 | } 46 | 47 | func TestCmp_MapNotContainsValues(t *testing.T) { 48 | m1 := map[string][]int{ 49 | "one": {1, 3, 5, 7}, 50 | } 51 | MapNotContainsValues(t, m1, [][]int{{0, 5, 1, 3}}, cmpSortSlices) 52 | } 53 | -------------------------------------------------------------------------------- /must/testdata/dir1/file1: -------------------------------------------------------------------------------- 1 | real data -------------------------------------------------------------------------------- /portal/portal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package portal (Port Allocator) provides a helper for reserving free TCP ports 5 | // across multiple processes on the same machine. This works by asking the kernel 6 | // for available ports in the ephemeral port range. It does so by binding to an 7 | // address with port 0 (e.g. 127.0.0.1:0), modifying the socket to disable SO_LINGER, 8 | // close the connection, and finally return the port that was used. This *probably* 9 | // works well, because the kernel re-uses ports in an LRU fashion, implying the 10 | // test code asking for the ports *should* be the only thing immediately asking 11 | // to bind that port again. 12 | package portal 13 | 14 | import ( 15 | "io" 16 | "net" 17 | "strconv" 18 | "sync" 19 | ) 20 | 21 | const ( 22 | defaultAddress = "127.0.0.1" 23 | ) 24 | 25 | type FatalTester interface { 26 | Fatalf(msg string, args ...any) 27 | } 28 | 29 | // A Grabber is used to grab open ports. 30 | type Grabber interface { 31 | // Grab n port allocations. 32 | Grab(n int) []int 33 | 34 | // One port allocation. 35 | One() int 36 | } 37 | 38 | // New creates a new Grabber with the given options. 39 | func New(t FatalTester, opts ...Option) Grabber { 40 | g := &grabber{ 41 | t: t, 42 | ip: net.ParseIP(defaultAddress), 43 | } 44 | 45 | for _, opt := range opts { 46 | opt(g) 47 | } 48 | 49 | return g 50 | } 51 | 52 | type grabber struct { 53 | t FatalTester 54 | ip net.IP 55 | lock sync.Mutex 56 | } 57 | 58 | type Option func(Grabber) 59 | 60 | // WithAddress specifies which address on which to allocate ports. 61 | func WithAddress(address string) Option { 62 | return func(g Grabber) { 63 | g.(*grabber).ip = net.ParseIP(address) 64 | } 65 | } 66 | 67 | func (g *grabber) Grab(n int) []int { 68 | g.lock.Lock() 69 | defer g.lock.Unlock() 70 | 71 | ports := make([]int, n) 72 | closers := make([]io.Closer, n) 73 | 74 | for i := 0; i < n; i++ { 75 | p, c := g.one() 76 | ports[i] = p 77 | closers[i] = c 78 | } 79 | 80 | for _, c := range closers { 81 | _ = c.Close() 82 | } 83 | 84 | return ports 85 | } 86 | 87 | func (g *grabber) One() int { 88 | g.lock.Lock() 89 | defer g.lock.Unlock() 90 | 91 | p, c := g.one() 92 | _ = c.Close() 93 | return p 94 | } 95 | 96 | // one will acquire one port; the caller must hold the lock and also close 97 | // the returned listener - this minimized the chances of reallocating the same 98 | // port 99 | func (g *grabber) one() (int, io.Closer) { 100 | tcpAddr := &net.TCPAddr{IP: g.ip, Port: 0} 101 | l, listenErr := net.ListenTCP("tcp", tcpAddr) 102 | if listenErr != nil { 103 | g.t.Fatalf("failed to acquire port: %v", listenErr) 104 | } 105 | 106 | if setErr := setSocketOpt(l); setErr != nil { 107 | g.t.Fatalf("failed to modify socket: %v", setErr) 108 | } 109 | 110 | _, port, splitErr := net.SplitHostPort(l.Addr().String()) 111 | if splitErr != nil { 112 | g.t.Fatalf("failed to parse address: %v", splitErr) 113 | } 114 | 115 | p, parseErr := strconv.Atoi(port) 116 | if parseErr != nil { 117 | g.t.Fatalf("failed to parse port: %v", parseErr) 118 | } 119 | 120 | return p, l 121 | } 122 | -------------------------------------------------------------------------------- /portal/portal_default.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | 6 | package portal 7 | 8 | import ( 9 | "fmt" 10 | "net" 11 | "syscall" 12 | ) 13 | 14 | func setSocketOpt(l *net.TCPListener) error { 15 | f, fileErr := l.File() 16 | if fileErr != nil { 17 | return fmt.Errorf("failed to open socket file: %w", fileErr) 18 | } 19 | 20 | h := int(f.Fd()) 21 | setErr := syscall.SetsockoptLinger(h, syscall.SOL_SOCKET, syscall.SO_LINGER, &syscall.Linger{Onoff: 0, Linger: 0}) 22 | if setErr != nil { 23 | return fmt.Errorf("failed to set linger option: %w", setErr) 24 | } 25 | 26 | closeErr := f.Close() 27 | if closeErr != nil { 28 | return fmt.Errorf("failed to close socket file: %w", closeErr) 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /portal/portal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package portal 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestGrabber_New(t *testing.T) { 11 | g := New(t) 12 | 13 | ip := g.(*grabber).ip.String() 14 | if ip != defaultAddress { 15 | t.Fatalf("expected default address to be %s, got: %s", defaultAddress, ip) 16 | } 17 | } 18 | 19 | func checkPort(t *testing.T, port int) { 20 | if !(port >= 1024) { 21 | t.Fatalf("expected port above 1024, got: %v", port) 22 | } 23 | } 24 | 25 | func TestGrabber_GetOne(t *testing.T) { 26 | g := New(t) 27 | port := g.One() 28 | checkPort(t, port) 29 | } 30 | 31 | func TestGrabber_Get(t *testing.T) { 32 | g := New(t) 33 | ports := g.Grab(5) 34 | for _, port := range ports { 35 | checkPort(t, port) 36 | } 37 | } 38 | 39 | func TestGrabber_WithAddress(t *testing.T) { 40 | g := New(t, WithAddress("0.0.0.0")) 41 | ports := g.Grab(5) 42 | for _, port := range ports { 43 | checkPort(t, port) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /portal/portal_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | 6 | package portal 7 | 8 | import ( 9 | "net" 10 | ) 11 | 12 | func setSocketOpt(l *net.TCPListener) error { 13 | // windows does not support modifying the socket; good luck! 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /scripts.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func run(posts ...PostScript) string { 12 | s := new(strings.Builder) 13 | for _, post := range posts { 14 | s.WriteString("↪ PostScript | ") 15 | s.WriteString(post.Label()) 16 | s.WriteString(" ↷\n") 17 | s.WriteString(post.Content()) 18 | s.WriteString("\n") 19 | } 20 | return s.String() 21 | } 22 | 23 | // A PostScript is used to annotate a test failure with additional information. 24 | // 25 | // Can be useful in large e2e style test cases, where adding additional context 26 | // beyond an assertion helps in debugging. 27 | type PostScript interface { 28 | // Label should categorize what is in Content. 29 | Label() string 30 | 31 | // Content contains extra contextual information for debugging a test failure. 32 | Content() string 33 | } 34 | 35 | type script struct { 36 | label string 37 | content string 38 | } 39 | 40 | func (s *script) Label() string { 41 | return strings.TrimSpace(s.label) 42 | } 43 | func (s *script) Content() string { 44 | return "\t" + strings.TrimSpace(s.content) 45 | } 46 | 47 | // Sprintf appends a Sprintf-string as an annotation to the output of a test case failure. 48 | func Sprintf(msg string, args ...any) Setting { 49 | return func(s *Settings) { 50 | s.postScripts = append(s.postScripts, &script{ 51 | label: "annotation", 52 | content: fmt.Sprintf(msg, args...), 53 | }) 54 | } 55 | } 56 | 57 | // Sprint appends a Sprint-string as an annotation to the output of a test case failure. 58 | func Sprint(args ...any) Setting { 59 | return func(s *Settings) { 60 | s.postScripts = append(s.postScripts, &script{ 61 | label: "annotation", 62 | content: strings.TrimSpace(fmt.Sprintln(args...)), 63 | }) 64 | } 65 | } 66 | 67 | // Values adds formatted key-val mappings as an annotation to the output of a test case failure. 68 | func Values(vals ...any) Setting { 69 | b := new(strings.Builder) 70 | n := len(vals) 71 | for i := 0; i < n-1; i += 2 { 72 | s := fmt.Sprintf("\t%#v => %#v\n", vals[i], vals[i+1]) 73 | b.WriteString(s) 74 | } 75 | if n%2 != 0 { 76 | s := fmt.Sprintf("\t%v => ", vals[n-1]) 77 | b.WriteString(s) 78 | } 79 | content := b.String() 80 | return func(s *Settings) { 81 | s.postScripts = append(s.postScripts, &script{ 82 | label: "mapping", 83 | content: content, 84 | }) 85 | } 86 | } 87 | 88 | // Func adds the string produced by f as an annotation to the output of a test case failure. 89 | func Func(f func() string) Setting { 90 | return func(s *Settings) { 91 | s.postScripts = append(s.postScripts, &script{ 92 | label: "function", 93 | content: f(), 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/changes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | files=$(git ls-files -m) 6 | if [ -n "$files" ]; then 7 | echo "Files have been modified ..." 8 | echo "$files" 9 | exit 1 10 | fi 11 | -------------------------------------------------------------------------------- /scripts/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | apply() { 6 | original="${1}" 7 | clone="must/${original}" 8 | cp "${original}" "${clone}" 9 | sed -i.bak "s|package test|package must|g" "${clone}" 10 | sed -i.bak -e "1s|^|// Code generated via scripts/generate.sh. DO NOT EDIT.\n\n|g" "${clone}" 11 | } 12 | 13 | apply invocations.go 14 | apply invocations_test.go 15 | apply settings.go 16 | apply settings_test.go 17 | apply scripts.go 18 | apply scripts_test.go 19 | apply test.go 20 | apply test_test.go 21 | apply examples_test.go 22 | apply examples_unix_test.go 23 | 24 | cp -R testdata must/ 25 | 26 | # rename core test files 27 | mv must/test.go must/must.go 28 | mv must/test_test.go must/must_test.go 29 | 30 | # cleanup *.bak files 31 | find . -name *.bak | xargs rm 32 | -------------------------------------------------------------------------------- /scripts_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import "testing" 7 | 8 | func TestPostScript_Label(t *testing.T) { 9 | var s PostScript = &script{label: "\nhello "} 10 | if s.Label() != "hello" { 11 | t.FailNow() 12 | } 13 | } 14 | 15 | func TestPostScript_Content(t *testing.T) { 16 | var s PostScript = &script{content: "\nhello "} 17 | if s.Content() != "\thello" { 18 | t.FailNow() 19 | } 20 | } 21 | 22 | func TestPostScript_Sprintf(t *testing.T) { 23 | ps := Sprintf("foo %s %d", "baz", 1) 24 | result := run(scripts(ps)...) 25 | exp := "↪ PostScript | annotation ↷\n\tfoo baz 1\n" 26 | if result != exp { 27 | t.Fatalf("exp %s, got %s", exp, result) 28 | } 29 | } 30 | 31 | func TestPostScript_KV(t *testing.T) { 32 | ps := Values("one", 1, "foo", "bar") 33 | result := run(scripts(ps)...) 34 | exp := "↪ PostScript | mapping ↷\n\t\"one\" => 1\n\t\"foo\" => \"bar\"\n" 35 | if result != exp { 36 | t.Fatalf("exp %s, got %s", exp, result) 37 | } 38 | } 39 | 40 | func TestPostScript_Func(t *testing.T) { 41 | ps := Func(func() string { 42 | return "hello" 43 | }) 44 | result := run(scripts(ps)...) 45 | exp := "↪ PostScript | function ↷\n\thello\n" 46 | if result != exp { 47 | t.Fatalf("exp %s, got %s", exp, result) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | // Settings are used to manage a collection of Setting values used to modify 11 | // the behavior of a test case assertion. Currently supports specifying custom 12 | // error output content, and custom cmp.Option comparators / transforms. 13 | // 14 | // Use Cmp for specifying custom cmp.Option values. 15 | // 16 | // Use Sprint, Sprintf, Values, Func for specifying custom failure output messages. 17 | type Settings struct { 18 | postScripts []PostScript 19 | cmpOptions []cmp.Option 20 | } 21 | 22 | // A Setting changes the behavior of a test case assertion. 23 | type Setting func(s *Settings) 24 | 25 | // Cmp enables configuring cmp.Option values for modifying the behavior of the 26 | // cmp.Equal function. Custom cmp.Option values control how the cmp.Equal function 27 | // determines equality between the two objects. 28 | // 29 | // https://github.com/google/go-cmp/blob/master/cmp/options.go#L16 30 | func Cmp(options ...cmp.Option) Setting { 31 | return func(s *Settings) { 32 | s.cmpOptions = append(s.cmpOptions, options...) 33 | } 34 | } 35 | 36 | func options(settings ...Setting) []cmp.Option { 37 | s := new(Settings) 38 | for _, setting := range settings { 39 | setting(s) 40 | } 41 | return s.cmpOptions 42 | } 43 | 44 | func scripts(settings ...Setting) []PostScript { 45 | s := new(Settings) 46 | for _, setting := range settings { 47 | setting(s) 48 | } 49 | return s.postScripts 50 | } 51 | -------------------------------------------------------------------------------- /settings_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | ) 11 | 12 | var cmpSortSlices = Cmp(cmpopts.SortSlices(func(i, j int) bool { 13 | return i < j 14 | })) 15 | 16 | func TestCmp_Eq(t *testing.T) { 17 | a := []int{3, 5, 1, 6, 7} 18 | b := []int{1, 7, 6, 3, 5} 19 | Eq(t, a, b, cmpSortSlices) 20 | } 21 | 22 | func TestCmp_NotEq(t *testing.T) { 23 | a := []int{3, 5, 1, 6, 0} 24 | b := []int{1, 7, 6, 3, 5} 25 | NotEq(t, a, b, cmpSortSlices) 26 | } 27 | 28 | func TestCmp_SliceContains(t *testing.T) { 29 | a := [][]int{{1}, {1, 2}} 30 | SliceContains(t, a, []int{2, 1}, cmpSortSlices) 31 | } 32 | 33 | func TestCmp_SliceNotContains(t *testing.T) { 34 | a := [][]int{{1}, {1, 2}} 35 | SliceNotContains(t, a, []int{3, 1}, cmpSortSlices) 36 | } 37 | 38 | func TestCmp_MapContainsValues(t *testing.T) { 39 | m1 := map[string][]int{ 40 | "one": {1, 3, 5, 7}, 41 | } 42 | MapContainsValues(t, m1, [][]int{{7, 5, 1, 3}}, cmpSortSlices) 43 | } 44 | 45 | func TestCmp_MapNotContainsValues(t *testing.T) { 46 | m1 := map[string][]int{ 47 | "one": {1, 3, 5, 7}, 48 | } 49 | MapNotContainsValues(t, m1, [][]int{{0, 5, 1, 3}}, cmpSortSlices) 50 | } 51 | -------------------------------------------------------------------------------- /skip/skip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package skip provides helper functions for skipping test cases. 5 | package skip 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "runtime" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // T is the minimal set of functions to be implemented by any testing 19 | // framework compatible with the skip package. 20 | type T interface { 21 | Skipf(string, ...any) 22 | Fatalf(string, ...any) 23 | } 24 | 25 | // OperatingSystem will skip the test if the Go runtime detects the operating 26 | // system matches one of the given names. 27 | func OperatingSystem(t T, names ...string) { 28 | os := runtime.GOOS 29 | for _, name := range names { 30 | if os == strings.ToLower(name) { 31 | t.Skipf("operating system excluded from tests %q", os) 32 | } 33 | } 34 | } 35 | 36 | // NotOperatingSystem will skip the test if the Go runtime detects the operating 37 | // system does not match one of the given names. 38 | func NotOperatingSystem(t T, names ...string) { 39 | os := runtime.GOOS 40 | for _, name := range names { 41 | if os == strings.ToLower(name) { 42 | return 43 | } 44 | } 45 | t.Skipf("operating excluded from tests %q", os) 46 | } 47 | 48 | // UserRoot will skip the test if the test is being run as the root user. 49 | // 50 | // Uses the effective UID value to determine user. 51 | func UserRoot(t T) { 52 | euid := os.Geteuid() 53 | if euid == 0 { 54 | t.Skipf("test must not run as root") 55 | } 56 | } 57 | 58 | // NotUserRoot will skip the test if the test is not being run as the root user. 59 | // 60 | // Uses the effective UID value to determine user. 61 | func NotUserRoot(t T) { 62 | euid := os.Geteuid() 63 | if euid != 0 { 64 | t.Skipf("test must run as root") 65 | } 66 | } 67 | 68 | // Architecture will skip the test if the Go runtime detects the system 69 | // architecture matches one of the given names. 70 | func Architecture(t T, names ...string) { 71 | arch := runtime.GOARCH 72 | for _, name := range names { 73 | if arch == strings.ToLower(name) { 74 | t.Skipf("arch excluded from tests %q", arch) 75 | } 76 | } 77 | } 78 | 79 | // NotArchitecture will skip the test if the Go runtime the system architecture 80 | // does not match one of the given names. 81 | func NotArchitecture(t T, names ...string) { 82 | arch := runtime.GOARCH 83 | for _, name := range names { 84 | if arch == strings.ToLower(name) { 85 | return 86 | } 87 | } 88 | t.Skipf("arch excluded from tests %q", arch) 89 | } 90 | 91 | func cmdAvailable(name string) bool { 92 | _, err := exec.LookPath(name) 93 | return !errors.Is(err, exec.ErrNotFound) 94 | } 95 | 96 | // CommandUnavailable will skip the test if the given command cannot be found on 97 | // the system PATH. 98 | func CommandUnavailable(t T, command string) { 99 | if !cmdAvailable(command) { 100 | t.Skipf("command %q not detected on system", command) 101 | } 102 | } 103 | 104 | // DockerUnavailable will skip the test if the docker command cannot be found on 105 | // the system PATH. 106 | func DockerUnavailable(t T) { 107 | if !cmdAvailable("docker") { 108 | t.Skipf("docker not detected on system") 109 | } 110 | } 111 | 112 | // PodmanUnavailable will skip the test if the podman command cannot be found on 113 | // the system PATH. 114 | func PodmanUnavailable(t T) { 115 | if !cmdAvailable("podman") { 116 | t.Skipf("podman not detected on system") 117 | } 118 | } 119 | 120 | // MinimumCores will skip the test if the system does not meet the minimum 121 | // number of CPU cores. 122 | func MinimumCores(t T, num int) { 123 | cpus := runtime.NumCPU() 124 | if cpus < num { 125 | t.Skipf("system does not meet minimum cpu cores") 126 | } 127 | } 128 | 129 | // MaximumCores will skip the test if the number of cores on the system 130 | // exceeds the given maximum. 131 | func MaximumCores(t T, num int) { 132 | cpus := runtime.NumCPU() 133 | if cpus > num { 134 | t.Skipf("system exceeds maximum cpu cores") 135 | } 136 | } 137 | 138 | // CgroupsVersion will skip the test if the system does not match the given 139 | // cgroups version. 140 | func CgroupsVersion(t T, version int) { 141 | if runtime.GOOS != "linux" { 142 | t.Skipf("cgroups requires linux") 143 | } 144 | 145 | mType := mountType(t, "/sys/fs/cgroup") 146 | 147 | switch mType { 148 | case "tmpfs": 149 | // this is a cgroups v1 system 150 | if version == 2 { 151 | t.Skipf("system does not match cgroups version 2") 152 | } 153 | case "cgroup2": 154 | // this is a cgroups v2 system 155 | if version == 1 { 156 | t.Skipf("system does not match cgroups version 1") 157 | } 158 | default: 159 | t.Fatalf("unknown cgroups mount type %q", mType) 160 | } 161 | } 162 | 163 | func mountType(t T, path string) string { 164 | ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) 165 | defer cancel() 166 | 167 | cmd := exec.CommandContext(ctx, "df", "-T", path) 168 | b, err := cmd.CombinedOutput() 169 | if err != nil { 170 | t.Fatalf("unable to run df command: %v", err) 171 | } 172 | 173 | // need the first token of the second line 174 | output := string(b) 175 | tokenRe := regexp.MustCompile(`on\s+([\w]+)\s+`) 176 | results := tokenRe.FindStringSubmatch(output) 177 | if len(results) != 2 { 178 | t.Fatalf("no mount type for path") 179 | } 180 | return results[1] 181 | } 182 | 183 | // EnvironmentVariableSet will skip the test if the given environment variable 184 | // is set to any value. 185 | func EnvironmentVariableSet(t T, name string) { 186 | if name == "" { 187 | t.Fatalf("environment variable name must be set") 188 | } 189 | 190 | _, exists := os.LookupEnv(name) 191 | if exists { 192 | t.Skipf("environment variable %q is set", name) 193 | } 194 | } 195 | 196 | // EnvironmentVariableNotSet will skip the test if the given environment 197 | // variable is not set in the system environment. 198 | func EnvironmentVariableNotSet(t T, name string) { 199 | if name == "" { 200 | t.Fatalf("environment variable name must be set") 201 | } 202 | _, exists := os.LookupEnv(name) 203 | if !exists { 204 | t.Skipf("environment variable %q is not set", name) 205 | } 206 | } 207 | 208 | // EnvironmentVariableMatches will skip the test if the system environment 209 | // matches one of the given environment variable values. 210 | func EnvironmentVariableMatches(t T, name string, values ...string) { 211 | if len(values) == 0 { 212 | t.Fatalf("no possible environment variable values given") 213 | } 214 | 215 | // if not set in the system, then it must not match 216 | actual, exists := os.LookupEnv(name) 217 | if !exists { 218 | return 219 | } 220 | 221 | for _, value := range values { 222 | if value == actual { 223 | t.Skipf("environment variable %q matches %q", name, value) 224 | } 225 | } 226 | } 227 | 228 | // EnvironmentVariableNotMatches will skip the test if the system environment 229 | // does not match one of the given environment variable values. 230 | func EnvironmentVariableNotMatches(t T, name string, values ...string) { 231 | if len(values) == 0 { 232 | t.Fatalf("no possible environment variable values given") 233 | } 234 | 235 | actual, exists := os.LookupEnv(name) 236 | if !exists { 237 | t.Skipf("environment variable %q not set", name) 238 | } 239 | 240 | for _, value := range values { 241 | if actual == value { 242 | return 243 | } 244 | } 245 | 246 | t.Skipf("environment variable %q does not match values (is %q)", name, actual) 247 | } 248 | 249 | // Error will skip the test if err is not nil. 250 | func Error(t T, err error) { 251 | if err != nil { 252 | t.Skipf("skipping test due to non-nil error: %v", err) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /skip/skip_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build linux || darwin 5 | 6 | package skip 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | ) 12 | 13 | func TestSkip_OperatingSystem(t *testing.T) { 14 | OperatingSystem(t, "darwin", "linux", "windows") 15 | t.Fatal("expected to skip test") 16 | } 17 | 18 | func TestSkip_NotOperatingSystem(t *testing.T) { 19 | NotOperatingSystem(t, "windows") 20 | t.Fatal("expected to skip test") 21 | } 22 | 23 | func TestSkip_UserRoot(t *testing.T) { 24 | t.Skip("requires root") 25 | UserRoot(t) 26 | t.Fatal("expected to skip test") 27 | } 28 | 29 | func TestSkip_NotUserRoot(t *testing.T) { 30 | NotUserRoot(t) 31 | t.Fatal("expected to skip test") 32 | } 33 | 34 | func TestSkip_Architecture(t *testing.T) { 35 | Architecture(t, "arm64", "amd64") 36 | t.Fatal("expected to skip test") 37 | } 38 | 39 | func TestSkip_NotArchitecture(t *testing.T) { 40 | NotArchitecture(t, "itanium", "mips") 41 | t.Fatal("expected to skip test") 42 | } 43 | 44 | func TestSkip_DockerUnavailable(t *testing.T) { 45 | t.Skip("skip docker test") // gha runner 46 | 47 | DockerUnavailable(t) 48 | t.Fatal("expected to skip test") 49 | } 50 | 51 | func TestSkip_PodmanUnavailable(t *testing.T) { 52 | t.Skip("skip podman test") // gha runner 53 | 54 | PodmanUnavailable(t) 55 | t.Fatal("expected to skip test") 56 | } 57 | 58 | func TestSkip_CommandUnavailable(t *testing.T) { 59 | CommandUnavailable(t, "doesnotexist") 60 | t.Fatal("expected to skip test") 61 | } 62 | 63 | func TestSkip_MinimumCores(t *testing.T) { 64 | MinimumCores(t, 2048) 65 | t.Fatal("expected to skip test") 66 | } 67 | 68 | func TestSkip_MaximumCores(t *testing.T) { 69 | MaximumCores(t, 2) 70 | t.Fatal("expected to skip test") 71 | } 72 | 73 | func TestSkip_CgroupsVersion(t *testing.T) { 74 | CgroupsVersion(t, 1) 75 | t.Fatal("expected to skip test") 76 | } 77 | 78 | func TestSkip_EnvironmentVariableSet(t *testing.T) { 79 | t.Setenv("EXAMPLE", "value") 80 | 81 | EnvironmentVariableSet(t, "EXAMPLE") 82 | t.Fatal("expected to skip test") 83 | } 84 | 85 | func TestSkip_EnvironmentVariableNotSet(t *testing.T) { 86 | EnvironmentVariableNotSet(t, "DOESNOTEXIST") 87 | t.Fatal("expected to skip test") 88 | } 89 | 90 | func TestSkip_EnvironmentVariableMatches(t *testing.T) { 91 | t.Setenv("EXAMPLE", "foo") 92 | 93 | EnvironmentVariableMatches(t, "EXAMPLE", "bar", "foo", "baz") 94 | t.Fatal("expected to skip test") 95 | } 96 | 97 | func TestSkip_EnvironmentVariableNotMatches(t *testing.T) { 98 | t.Setenv("EXAMPLE", "other") 99 | 100 | EnvironmentVariableNotMatches(t, "EXAMPLE", "bar", "foo", "baz") 101 | t.Fatal("expected to skip test") 102 | } 103 | 104 | func TestSkip_Error(t *testing.T) { 105 | err := errors.New("oops") 106 | Error(t, err) 107 | t.Fatal("expected to skip test") 108 | } 109 | -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "io" 8 | "io/fs" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/shoenig/test/interfaces" 13 | "github.com/shoenig/test/internal/assertions" 14 | "github.com/shoenig/test/internal/constraints" 15 | "github.com/shoenig/test/internal/util" 16 | "github.com/shoenig/test/wait" 17 | ) 18 | 19 | // ErrorAssertionFunc allows passing Error and NoError in table driven tests 20 | type ErrorAssertionFunc func(t T, err error, settings ...Setting) 21 | 22 | // Nil asserts a is nil. 23 | func Nil(t T, a any, settings ...Setting) { 24 | t.Helper() 25 | invoke(t, assertions.Nil(a), settings...) 26 | } 27 | 28 | // NotNil asserts a is not nil. 29 | func NotNil(t T, a any, settings ...Setting) { 30 | t.Helper() 31 | invoke(t, assertions.NotNil(a), settings...) 32 | } 33 | 34 | // True asserts that condition is true. 35 | func True(t T, condition bool, settings ...Setting) { 36 | t.Helper() 37 | invoke(t, assertions.True(condition), settings...) 38 | } 39 | 40 | // False asserts condition is false. 41 | func False(t T, condition bool, settings ...Setting) { 42 | t.Helper() 43 | invoke(t, assertions.False(condition), settings...) 44 | } 45 | 46 | // Unreachable asserts a code path is not executed. 47 | func Unreachable(t T, settings ...Setting) { 48 | t.Helper() 49 | invoke(t, assertions.Unreachable(), settings...) 50 | } 51 | 52 | // Error asserts err is a non-nil error. 53 | func Error(t T, err error, settings ...Setting) { 54 | t.Helper() 55 | invoke(t, assertions.Error(err), settings...) 56 | } 57 | 58 | // EqError asserts err contains message msg. 59 | func EqError(t T, err error, msg string, settings ...Setting) { 60 | t.Helper() 61 | invoke(t, assertions.EqError(err, msg), settings...) 62 | } 63 | 64 | // ErrorIs asserts err 65 | func ErrorIs(t T, err error, target error, settings ...Setting) { 66 | t.Helper() 67 | invoke(t, assertions.ErrorIs(err, target), settings...) 68 | } 69 | 70 | // ErrorAs asserts err's tree contains an error that matches target. 71 | // If so, it sets target to the error value. 72 | func ErrorAs[E error, Target *E](t T, err error, target Target, settings ...Setting) { 73 | t.Helper() 74 | invoke(t, assertions.ErrorAs(err, target), settings...) 75 | } 76 | 77 | // NoError asserts err is a nil error. 78 | func NoError(t T, err error, settings ...Setting) { 79 | t.Helper() 80 | invoke(t, assertions.NoError(err), settings...) 81 | } 82 | 83 | // ErrorContains asserts err contains sub. 84 | func ErrorContains(t T, err error, sub string, settings ...Setting) { 85 | t.Helper() 86 | invoke(t, assertions.ErrorContains(err, sub), settings...) 87 | } 88 | 89 | // Eq asserts exp and val are equal using cmp.Equal. 90 | func Eq[A any](t T, exp, val A, settings ...Setting) { 91 | t.Helper() 92 | invoke(t, assertions.Eq(exp, val, options(settings...)...), settings...) 93 | } 94 | 95 | // EqOp asserts exp == val. 96 | func EqOp[C comparable](t T, exp, val C, settings ...Setting) { 97 | t.Helper() 98 | invoke(t, assertions.EqOp(exp, val), settings...) 99 | } 100 | 101 | // EqFunc asserts exp and val are equal using eq. 102 | func EqFunc[A any](t T, exp, val A, eq func(a, b A) bool, settings ...Setting) { 103 | t.Helper() 104 | invoke(t, assertions.EqFunc(exp, val, eq), settings...) 105 | } 106 | 107 | // NotEq asserts exp and val are not equal using cmp.Equal. 108 | func NotEq[A any](t T, exp, val A, settings ...Setting) { 109 | t.Helper() 110 | invoke(t, assertions.NotEq(exp, val, options(settings...)...), settings...) 111 | } 112 | 113 | // NotEqOp asserts exp != val. 114 | func NotEqOp[C comparable](t T, exp, val C, settings ...Setting) { 115 | t.Helper() 116 | invoke(t, assertions.NotEqOp(exp, val), settings...) 117 | } 118 | 119 | // NotEqFunc asserts exp and val are not equal using eq. 120 | func NotEqFunc[A any](t T, exp, val A, eq func(a, b A) bool, settings ...Setting) { 121 | t.Helper() 122 | invoke(t, assertions.NotEqFunc(exp, val, eq), settings...) 123 | } 124 | 125 | // EqJSON asserts exp and val are equivalent JSON. 126 | func EqJSON(t T, exp, val string, settings ...Setting) { 127 | t.Helper() 128 | invoke(t, assertions.EqJSON(exp, val), settings...) 129 | } 130 | 131 | // ValidJSON asserts js is valid JSON. 132 | func ValidJSON(t T, js string, settings ...Setting) { 133 | t.Helper() 134 | invoke(t, assertions.ValidJSON(js), settings...) 135 | } 136 | 137 | // ValidJSONBytes asserts js is valid JSON. 138 | func ValidJSONBytes(t T, js []byte, settings ...Setting) { 139 | t.Helper() 140 | invoke(t, assertions.ValidJSONBytes(js)) 141 | } 142 | 143 | // Equal asserts val.Equal(exp). 144 | func Equal[E interfaces.EqualFunc[E]](t T, exp, val E, settings ...Setting) { 145 | t.Helper() 146 | invoke(t, assertions.Equal(exp, val), settings...) 147 | } 148 | 149 | // NotEqual asserts !val.Equal(exp). 150 | func NotEqual[E interfaces.EqualFunc[E]](t T, exp, val E, settings ...Setting) { 151 | t.Helper() 152 | invoke(t, assertions.NotEqual(exp, val), settings...) 153 | } 154 | 155 | // Lesser asserts val.Less(exp). 156 | func Lesser[L interfaces.LessFunc[L]](t T, exp, val L, settings ...Setting) { 157 | t.Helper() 158 | invoke(t, assertions.Lesser(exp, val), settings...) 159 | } 160 | 161 | // SliceEqFunc asserts elements of val satisfy eq for the corresponding element in exp. 162 | func SliceEqFunc[A, B any](t T, exp []B, val []A, eq func(expectation A, value B) bool, settings ...Setting) { 163 | t.Helper() 164 | invoke(t, assertions.EqSliceFunc(exp, val, eq), settings...) 165 | } 166 | 167 | // SliceEqual asserts val[n].Equal(exp[n]) for each element n. 168 | func SliceEqual[E interfaces.EqualFunc[E]](t T, exp, val []E, settings ...Setting) { 169 | t.Helper() 170 | invoke(t, assertions.SliceEqual(exp, val), settings...) 171 | } 172 | 173 | // SliceEqOp asserts exp[n] == val[n] for each element n. 174 | func SliceEqOp[A comparable, S ~[]A](t T, exp, val S, settings ...Setting) { 175 | t.Helper() 176 | invoke(t, assertions.SliceEqOp(exp, val), settings...) 177 | } 178 | 179 | // SliceEmpty asserts slice is empty. 180 | func SliceEmpty[A any](t T, slice []A, settings ...Setting) { 181 | t.Helper() 182 | invoke(t, assertions.SliceEmpty(slice), settings...) 183 | } 184 | 185 | // SliceNotEmpty asserts slice is not empty. 186 | func SliceNotEmpty[A any](t T, slice []A, settings ...Setting) { 187 | t.Helper() 188 | invoke(t, assertions.SliceNotEmpty(slice), settings...) 189 | } 190 | 191 | // SliceLen asserts slice is of length n. 192 | func SliceLen[A any](t T, n int, slice []A, settings ...Setting) { 193 | t.Helper() 194 | invoke(t, assertions.SliceLen(n, slice), settings...) 195 | } 196 | 197 | // Len asserts slice is of length n. 198 | // 199 | // Shorthand function for SliceLen. For checking Len() of a struct, 200 | // use the Length() assertion. 201 | func Len[A any](t T, n int, slice []A, settings ...Setting) { 202 | t.Helper() 203 | invoke(t, assertions.SliceLen(n, slice), settings...) 204 | } 205 | 206 | // SliceContainsOp asserts item exists in slice using == operator. 207 | func SliceContainsOp[C comparable](t T, slice []C, item C, settings ...Setting) { 208 | t.Helper() 209 | invoke(t, assertions.SliceContainsOp(slice, item), settings...) 210 | } 211 | 212 | // SliceContainsFunc asserts item exists in slice, using eq to compare elements. 213 | func SliceContainsFunc[A, B any](t T, slice []A, item B, eq func(a A, b B) bool, settings ...Setting) { 214 | t.Helper() 215 | invoke(t, assertions.SliceContainsFunc(slice, item, eq), settings...) 216 | } 217 | 218 | // SliceContainsEqual asserts item exists in slice, using Equal to compare elements. 219 | func SliceContainsEqual[E interfaces.EqualFunc[E]](t T, slice []E, item E, settings ...Setting) { 220 | t.Helper() 221 | invoke(t, assertions.SliceContainsEqual(slice, item), settings...) 222 | } 223 | 224 | // SliceContains asserts item exists in slice, using cmp.Equal to compare elements. 225 | func SliceContains[A any](t T, slice []A, item A, settings ...Setting) { 226 | t.Helper() 227 | invoke(t, assertions.SliceContains(slice, item, options(settings...)...), settings...) 228 | } 229 | 230 | // SliceNotContains asserts item does not exist in slice, using cmp.Equal to 231 | // compare elements. 232 | func SliceNotContains[A any](t T, slice []A, item A, settings ...Setting) { 233 | t.Helper() 234 | invoke(t, assertions.SliceNotContains(slice, item), settings...) 235 | } 236 | 237 | // SliceNotContainsFunc asserts item does not exist in slice, using eq to compare 238 | // elements. 239 | func SliceNotContainsFunc[A, B any](t T, slice []A, item B, eq func(a A, b B) bool, settings ...Setting) { 240 | t.Helper() 241 | invoke(t, assertions.SliceNotContainsFunc(slice, item, eq), settings...) 242 | } 243 | 244 | // SliceContainsAllOp asserts slice and items contain the same elements, but in 245 | // no particular order, using the == operator. The number of elements 246 | // in slice and items must be the same. 247 | func SliceContainsAllOp[C comparable](t T, slice, items []C, settings ...Setting) { 248 | t.Helper() 249 | invoke(t, assertions.SliceContainsAllOp(slice, items), settings...) 250 | } 251 | 252 | // SliceContainsAllFunc asserts slice and items contain the same elements, but in 253 | // no particular order, using eq to compare elements. The number of elements 254 | // in slice and items must be the same. 255 | func SliceContainsAllFunc[A, B any](t T, slice []A, items []B, eq func(a A, b B) bool, settings ...Setting) { 256 | t.Helper() 257 | invoke(t, assertions.SliceContainsAllFunc(slice, items, eq), settings...) 258 | } 259 | 260 | // SliceContainsAllEqual asserts slice and items contain the same elements, but in 261 | // no particular order, using Equal to compare elements. The number of elements 262 | // in slice and items must be the same. 263 | func SliceContainsAllEqual[E interfaces.EqualFunc[E]](t T, slice, items []E, settings ...Setting) { 264 | t.Helper() 265 | invoke(t, assertions.SliceContainsAllEqual(slice, items), settings...) 266 | } 267 | 268 | // SliceContainsAll asserts slice and items contain the same elements, but in 269 | // no particular order, using cmp.Equal to compare elements. The number of elements 270 | // in slice and items must be the same. 271 | func SliceContainsAll[A any](t T, slice, items []A, settings ...Setting) { 272 | t.Helper() 273 | invoke(t, assertions.SliceContainsAll(slice, items, options(settings...)...), settings...) 274 | } 275 | 276 | // SliceContainsSubsetOp asserts slice contains each item in items, in no particular 277 | // order, using the == operator. There could be additional elements 278 | // in slice not in items. 279 | func SliceContainsSubsetOp[C comparable](t T, slice, items []C, settings ...Setting) { 280 | t.Helper() 281 | invoke(t, assertions.SliceContainsSubsetOp(slice, items), settings...) 282 | } 283 | 284 | // SliceContainsSubsetFunc asserts slice contains each item in items, in no particular 285 | // order, using eq to compare elements. There could be additional elements 286 | // in slice not in items. 287 | func SliceContainsSubsetFunc[A, B any](t T, slice []A, items []B, eq func(a A, b B) bool, settings ...Setting) { 288 | t.Helper() 289 | invoke(t, assertions.SliceContainsSubsetFunc(slice, items, eq), settings...) 290 | } 291 | 292 | // SliceContainsSubsetEqual asserts slice contains each item in items, in no particular 293 | // order, using Equal to compare elements. There could be additional elements 294 | // in slice not in items. 295 | func SliceContainsSubsetEqual[E interfaces.EqualFunc[E]](t T, slice, items []E, settings ...Setting) { 296 | t.Helper() 297 | invoke(t, assertions.SliceContainsSubsetEqual(slice, items), settings...) 298 | } 299 | 300 | // SliceContainsSubset asserts slice contains each item in items, in no particular 301 | // order, using cmp.Equal to compare elements. There could be additional elements 302 | // in slice not in items. 303 | func SliceContainsSubset[A any](t T, slice, items []A, settings ...Setting) { 304 | t.Helper() 305 | invoke(t, assertions.SliceContainsSubset(slice, items, options(settings...)...), settings...) 306 | } 307 | 308 | // Positive asserts n > 0. 309 | func Positive[N interfaces.Number](t T, n N, settings ...Setting) { 310 | t.Helper() 311 | invoke(t, assertions.Positive(n), settings...) 312 | } 313 | 314 | // NonPositive asserts n ≤ 0. 315 | func NonPositive[N interfaces.Number](t T, n N, settings ...Setting) { 316 | t.Helper() 317 | invoke(t, assertions.NonPositive(n), settings...) 318 | } 319 | 320 | // Negative asserts n < 0. 321 | func Negative[N interfaces.Number](t T, n N, settings ...Setting) { 322 | t.Helper() 323 | invoke(t, assertions.Negative(n), settings...) 324 | } 325 | 326 | // NonNegative asserts n >= 0. 327 | func NonNegative[N interfaces.Number](t T, n N, settings ...Setting) { 328 | t.Helper() 329 | invoke(t, assertions.NonNegative(n), settings...) 330 | } 331 | 332 | // Zero asserts n == 0. 333 | func Zero[N interfaces.Number](t T, n N, settings ...Setting) { 334 | t.Helper() 335 | invoke(t, assertions.Zero(n), settings...) 336 | } 337 | 338 | // NonZero asserts n != 0. 339 | func NonZero[N interfaces.Number](t T, n N, settings ...Setting) { 340 | t.Helper() 341 | invoke(t, assertions.NonZero(n), settings...) 342 | } 343 | 344 | // One asserts n == 1. 345 | func One[N interfaces.Number](t T, n N, settings ...Setting) { 346 | t.Helper() 347 | invoke(t, assertions.One(n), settings...) 348 | } 349 | 350 | // Less asserts val < exp. 351 | func Less[O constraints.Ordered](t T, exp, val O, settings ...Setting) { 352 | t.Helper() 353 | invoke(t, assertions.Less(exp, val), settings...) 354 | } 355 | 356 | // LessEq asserts val ≤ exp. 357 | func LessEq[O constraints.Ordered](t T, exp, val O, settings ...Setting) { 358 | t.Helper() 359 | invoke(t, assertions.LessEq(exp, val), settings...) 360 | } 361 | 362 | // Greater asserts val > exp. 363 | func Greater[O constraints.Ordered](t T, exp, val O, settings ...Setting) { 364 | t.Helper() 365 | invoke(t, assertions.Greater(exp, val), settings...) 366 | } 367 | 368 | // GreaterEq asserts val ≥ exp. 369 | func GreaterEq[O constraints.Ordered](t T, exp, val O, settings ...Setting) { 370 | t.Helper() 371 | invoke(t, assertions.GreaterEq(exp, val), settings...) 372 | } 373 | 374 | // Between asserts lower ≤ val ≤ upper. 375 | func Between[O constraints.Ordered](t T, lower, val, upper O, settings ...Setting) { 376 | t.Helper() 377 | invoke(t, assertions.Between(lower, val, upper), settings...) 378 | } 379 | 380 | // BetweenExclusive asserts lower < val < upper. 381 | func BetweenExclusive[O constraints.Ordered](t T, lower, val, upper O, settings ...Setting) { 382 | t.Helper() 383 | invoke(t, assertions.BetweenExclusive(lower, val, upper), settings...) 384 | } 385 | 386 | // Min asserts collection.Min() is equal to expect. 387 | // 388 | // The equality method may be configured with Cmp options. 389 | func Min[A any, C interfaces.MinFunc[A]](t T, expect A, collection C, settings ...Setting) { 390 | t.Helper() 391 | invoke(t, assertions.Min(expect, collection, options(settings...)...), settings...) 392 | } 393 | 394 | // Max asserts collection.Max() is equal to expect. 395 | // 396 | // The equality method may be configured with Cmp options. 397 | func Max[A any, C interfaces.MaxFunc[A]](t T, expect A, collection C, settings ...Setting) { 398 | t.Helper() 399 | invoke(t, assertions.Max(expect, collection, options(settings...)...), settings...) 400 | } 401 | 402 | // Ascending asserts slice[n] ≤ slice[n+1] for each element. 403 | func Ascending[O constraints.Ordered](t T, slice []O, settings ...Setting) { 404 | t.Helper() 405 | invoke(t, assertions.Ascending(slice), settings...) 406 | } 407 | 408 | // AscendingFunc asserts slice[n] is less than slice[n+1] for each element using the less comparator. 409 | func AscendingFunc[A any](t T, slice []A, less func(A, A) bool, settings ...Setting) { 410 | t.Helper() 411 | invoke(t, assertions.AscendingFunc(slice, less), settings...) 412 | } 413 | 414 | // AscendingCmp asserts slice[n] is less than slice[n+1] for each element using the cmp comparator. 415 | func AscendingCmp[A any](t T, slice []A, compare func(A, A) int, settings ...Setting) { 416 | t.Helper() 417 | invoke(t, assertions.AscendingCmp(slice, compare), settings...) 418 | } 419 | 420 | // AscendingLess asserts slice[n].Less(slice[n+1]) for each element. 421 | func AscendingLess[L interfaces.LessFunc[L]](t T, slice []L, settings ...Setting) { 422 | t.Helper() 423 | invoke(t, assertions.AscendingLess(slice), settings...) 424 | } 425 | 426 | // Descending asserts slice[n] ≥ slice[n+1] for each element. 427 | func Descending[O constraints.Ordered](t T, slice []O, settings ...Setting) { 428 | t.Helper() 429 | invoke(t, assertions.Descending(slice), settings...) 430 | } 431 | 432 | // DescendingFunc asserts slice[n+1] is less than slice[n] for each element using the less comparator. 433 | func DescendingFunc[A any](t T, slice []A, less func(A, A) bool, settings ...Setting) { 434 | t.Helper() 435 | invoke(t, assertions.DescendingFunc(slice, less), settings...) 436 | } 437 | 438 | // DescendingCmp asserts slice[n+1] is ≤ slice[n] for each element. 439 | func DescendingCmp[A any](t T, slice []A, compare func(A, A) int, settings ...Setting) { 440 | t.Helper() 441 | invoke(t, assertions.DescendingCmp(slice, compare), settings...) 442 | } 443 | 444 | // DescendingLess asserts slice[n+1].Less(slice[n]) for each element. 445 | func DescendingLess[L interfaces.LessFunc[L]](t T, slice []L, settings ...Setting) { 446 | t.Helper() 447 | invoke(t, assertions.DescendingLess(slice), settings...) 448 | } 449 | 450 | // InDelta asserts a and b are within delta of each other. 451 | func InDelta[N interfaces.Number](t T, a, b, delta N, settings ...Setting) { 452 | t.Helper() 453 | invoke(t, assertions.InDelta(a, b, delta), settings...) 454 | } 455 | 456 | // InDeltaSlice asserts each element a[n] is within delta of b[n]. 457 | func InDeltaSlice[N interfaces.Number](t T, a, b []N, delta N, settings ...Setting) { 458 | t.Helper() 459 | invoke(t, assertions.InDeltaSlice(a, b, delta), settings...) 460 | } 461 | 462 | // MapEq asserts maps exp and val contain the same key/val pairs, using 463 | // cmp.Equal function to compare vals. 464 | func MapEq[M1, M2 interfaces.Map[K, V], K comparable, V any](t T, exp M1, val M2, settings ...Setting) { 465 | t.Helper() 466 | invoke(t, assertions.MapEq(exp, val, options(settings...)), settings...) 467 | } 468 | 469 | // MapEqFunc asserts maps exp and val contain the same key/val pairs, using eq to 470 | // compare vals. 471 | func MapEqFunc[M1, M2 interfaces.Map[K, V], K comparable, V any](t T, exp M1, val M2, eq func(V, V) bool, settings ...Setting) { 472 | t.Helper() 473 | invoke(t, assertions.MapEqFunc(exp, val, eq), settings...) 474 | } 475 | 476 | // MapEqual asserts maps exp and val contain the same key/val pairs, using Equal 477 | // method to compare val 478 | func MapEqual[M interfaces.MapEqualFunc[K, V], K comparable, V interfaces.EqualFunc[V]](t T, exp, val M, settings ...Setting) { 479 | t.Helper() 480 | invoke(t, assertions.MapEqual(exp, val), settings...) 481 | } 482 | 483 | // MapEqOp asserts maps exp and val contain the same key/val pairs, using == to 484 | // compare vals. 485 | func MapEqOp[M interfaces.Map[K, V], K, V comparable](t T, exp M, val M, settings ...Setting) { 486 | t.Helper() 487 | invoke(t, assertions.MapEqOp(exp, val), settings...) 488 | } 489 | 490 | // MapLen asserts map is of size n. 491 | func MapLen[M ~map[K]V, K comparable, V any](t T, n int, m M, settings ...Setting) { 492 | t.Helper() 493 | invoke(t, assertions.MapLen(n, m), settings...) 494 | } 495 | 496 | // MapEmpty asserts map is empty. 497 | func MapEmpty[M ~map[K]V, K comparable, V any](t T, m M, settings ...Setting) { 498 | t.Helper() 499 | invoke(t, assertions.MapEmpty(m), settings...) 500 | } 501 | 502 | // MapNotEmpty asserts map is not empty. 503 | func MapNotEmpty[M ~map[K]V, K comparable, V any](t T, m M, settings ...Setting) { 504 | t.Helper() 505 | invoke(t, assertions.MapNotEmpty(m), settings...) 506 | } 507 | 508 | // MapContainsKey asserts m contains key. 509 | func MapContainsKey[M ~map[K]V, K comparable, V any](t T, m M, key K, settings ...Setting) { 510 | t.Helper() 511 | invoke(t, assertions.MapContainsKey(m, key), settings...) 512 | } 513 | 514 | // MapNotContainsKey asserts m does not contain key. 515 | func MapNotContainsKey[M ~map[K]V, K comparable, V any](t T, m M, key K, settings ...Setting) { 516 | t.Helper() 517 | invoke(t, assertions.MapNotContainsKey(m, key), settings...) 518 | } 519 | 520 | // MapContainsKeys asserts m contains each key in keys. 521 | func MapContainsKeys[M ~map[K]V, K comparable, V any](t T, m M, keys []K, settings ...Setting) { 522 | t.Helper() 523 | invoke(t, assertions.MapContainsKeys(m, keys), settings...) 524 | } 525 | 526 | // MapNotContainsKeys asserts m does not contain any key in keys. 527 | func MapNotContainsKeys[M ~map[K]V, K comparable, V any](t T, m M, keys []K, settings ...Setting) { 528 | t.Helper() 529 | invoke(t, assertions.MapNotContainsKeys(m, keys), settings...) 530 | } 531 | 532 | // MapContainsValues asserts m contains each val in vals. 533 | func MapContainsValues[M ~map[K]V, K comparable, V any](t T, m M, vals []V, settings ...Setting) { 534 | t.Helper() 535 | invoke(t, assertions.MapContainsValues(m, vals, options(settings...)), settings...) 536 | } 537 | 538 | // MapNotContainsValues asserts m does not contain any value in vals. 539 | func MapNotContainsValues[M ~map[K]V, K comparable, V any](t T, m M, vals []V, settings ...Setting) { 540 | t.Helper() 541 | invoke(t, assertions.MapNotContainsValues(m, vals, options(settings...)), settings...) 542 | } 543 | 544 | // MapContainsValuesFunc asserts m contains each val in vals using the eq function. 545 | func MapContainsValuesFunc[M ~map[K]V, K comparable, V any](t T, m M, vals []V, eq func(V, V) bool, settings ...Setting) { 546 | t.Helper() 547 | invoke(t, assertions.MapContainsValuesFunc(m, vals, eq), settings...) 548 | } 549 | 550 | // MapNotContainsValuesFunc asserts m does not contain any value in vals using the eq function. 551 | func MapNotContainsValuesFunc[M ~map[K]V, K comparable, V any](t T, m M, vals []V, eq func(V, V) bool, settings ...Setting) { 552 | t.Helper() 553 | invoke(t, assertions.MapNotContainsValuesFunc(m, vals, eq), settings...) 554 | } 555 | 556 | // MapContainsValuesEqual asserts m contains each val in vals using the V.Equal method. 557 | func MapContainsValuesEqual[M ~map[K]V, K comparable, V interfaces.EqualFunc[V]](t T, m M, vals []V, settings ...Setting) { 558 | t.Helper() 559 | invoke(t, assertions.MapContainsValuesEqual(m, vals), settings...) 560 | } 561 | 562 | // MapNotContainsValuesEqual asserts m does not contain any value in vals using the V.Equal method. 563 | func MapNotContainsValuesEqual[M ~map[K]V, K comparable, V interfaces.EqualFunc[V]](t T, m M, vals []V, settings ...Setting) { 564 | t.Helper() 565 | invoke(t, assertions.MapNotContainsValuesEqual(m, vals), settings...) 566 | } 567 | 568 | // MapContainsValue asserts m contains val. 569 | func MapContainsValue[M ~map[K]V, K comparable, V any](t T, m M, val V, settings ...Setting) { 570 | t.Helper() 571 | invoke(t, assertions.MapContainsValue(m, val, options(settings...)), settings...) 572 | } 573 | 574 | // MapNotContainsValue asserts m does not contain val. 575 | func MapNotContainsValue[M ~map[K]V, K comparable, V any](t T, m M, val V, settings ...Setting) { 576 | t.Helper() 577 | invoke(t, assertions.MapNotContainsValue(m, val, options(settings...)), settings...) 578 | } 579 | 580 | // MapContainsValueFunc asserts m contains val using the eq function. 581 | func MapContainsValueFunc[M ~map[K]V, K comparable, V any](t T, m M, val V, eq func(V, V) bool, settings ...Setting) { 582 | t.Helper() 583 | invoke(t, assertions.MapContainsValueFunc(m, val, eq), settings...) 584 | } 585 | 586 | // MapNotContainsValueFunc asserts m does not contain val using the eq function. 587 | func MapNotContainsValueFunc[M ~map[K]V, K comparable, V any](t T, m M, val V, eq func(V, V) bool, settings ...Setting) { 588 | t.Helper() 589 | invoke(t, assertions.MapNotContainsValueFunc(m, val, eq), settings...) 590 | } 591 | 592 | // MapContainsValueEqual asserts m contains val using the V.Equal method. 593 | func MapContainsValueEqual[M ~map[K]V, K comparable, V interfaces.EqualFunc[V]](t T, m M, val V, settings ...Setting) { 594 | t.Helper() 595 | invoke(t, assertions.MapContainsValueEqual(m, val), settings...) 596 | } 597 | 598 | // MapNotContainsValueEqual asserts m does not contain val using the V.Equal method. 599 | func MapNotContainsValueEqual[M ~map[K]V, K comparable, V interfaces.EqualFunc[V]](t T, m M, val V, settings ...Setting) { 600 | t.Helper() 601 | invoke(t, assertions.MapNotContainsValueEqual(m, val), settings...) 602 | } 603 | 604 | // FileExistsFS asserts file exists on the fs.FS filesystem. 605 | // 606 | // Example, 607 | // FileExistsFS(t, os.DirFS("/etc"), "hosts") 608 | func FileExistsFS(t T, system fs.FS, file string, settings ...Setting) { 609 | t.Helper() 610 | invoke(t, assertions.FileExistsFS(system, file), settings...) 611 | } 612 | 613 | // FileExists asserts file exists on the OS filesystem. 614 | func FileExists(t T, file string, settings ...Setting) { 615 | t.Helper() 616 | invoke(t, assertions.FileExists(file), settings...) 617 | } 618 | 619 | // FileNotExistsFS asserts file does not exist on the fs.FS filesystem. 620 | // 621 | // Example, 622 | // FileNotExist(t, os.DirFS("/bin"), "exploit.exe") 623 | func FileNotExistsFS(t T, system fs.FS, file string, settings ...Setting) { 624 | t.Helper() 625 | invoke(t, assertions.FileNotExistsFS(system, file), settings...) 626 | } 627 | 628 | // FileNotExists asserts file does not exist on the OS filesystem. 629 | func FileNotExists(t T, file string, settings ...Setting) { 630 | t.Helper() 631 | invoke(t, assertions.FileNotExists(file), settings...) 632 | } 633 | 634 | // DirExistsFS asserts directory exists on the fs.FS filesystem. 635 | // 636 | // Example, 637 | // DirExistsFS(t, os.DirFS("/usr/local"), "bin") 638 | func DirExistsFS(t T, system fs.FS, directory string, settings ...Setting) { 639 | t.Helper() 640 | directory = strings.TrimPrefix(directory, "/") 641 | invoke(t, assertions.DirExistsFS(system, directory), settings...) 642 | } 643 | 644 | // DirExists asserts directory exists on the OS filesystem. 645 | func DirExists(t T, directory string, settings ...Setting) { 646 | t.Helper() 647 | invoke(t, assertions.DirExists(directory), settings...) 648 | } 649 | 650 | // DirNotExistsFS asserts directory does not exist on the fs.FS filesystem. 651 | // 652 | // Example, 653 | // DirNotExistsFS(t, os.DirFS("/tmp"), "scratch") 654 | func DirNotExistsFS(t T, system fs.FS, directory string, settings ...Setting) { 655 | t.Helper() 656 | invoke(t, assertions.DirNotExistsFS(system, directory), settings...) 657 | } 658 | 659 | // DirNotExists asserts directory does not exist on the OS filesystem. 660 | func DirNotExists(t T, directory string, settings ...Setting) { 661 | t.Helper() 662 | invoke(t, assertions.DirNotExists(directory), settings...) 663 | } 664 | 665 | // FileModeFS asserts the file or directory at path on fs.FS has exactly the given permission bits. 666 | // 667 | // Example, 668 | // FileModeFS(t, os.DirFS("/bin"), "find", 0655) 669 | func FileModeFS(t T, system fs.FS, path string, permissions fs.FileMode, settings ...Setting) { 670 | t.Helper() 671 | invoke(t, assertions.FileModeFS(system, path, permissions), settings...) 672 | } 673 | 674 | // FileMode asserts the file or directory at path on the OS filesystem has exactly the given permission bits. 675 | func FileMode(t T, path string, permissions fs.FileMode, settings ...Setting) { 676 | t.Helper() 677 | invoke(t, assertions.FileMode(path, permissions), settings...) 678 | } 679 | 680 | // DirModeFS asserts the directory at path on fs.FS has exactly the given permission bits. 681 | // 682 | // Example, 683 | // DirModeFS(t, os.DirFS("/"), "bin", 0655) 684 | func DirModeFS(t T, system fs.FS, path string, permissions fs.FileMode, settings ...Setting) { 685 | t.Helper() 686 | invoke(t, assertions.DirModeFS(system, path, permissions), settings...) 687 | } 688 | 689 | // DirMode asserts the directory at path on the OS filesystem has exactly the given permission bits. 690 | func DirMode(t T, path string, permissions fs.FileMode, settings ...Setting) { 691 | t.Helper() 692 | invoke(t, assertions.DirMode(path, permissions), settings...) 693 | } 694 | 695 | // FileContainsFS asserts the file on fs.FS contains content as a substring. 696 | // 697 | // Often os.DirFS is used to interact with the host filesystem. 698 | // Example, 699 | // FileContainsFS(t, os.DirFS("/etc"), "hosts", "localhost") 700 | func FileContainsFS(t T, system fs.FS, file, content string, settings ...Setting) { 701 | t.Helper() 702 | invoke(t, assertions.FileContainsFS(system, file, content), settings...) 703 | } 704 | 705 | // FileContains asserts the file on the OS filesystem contains content as a substring. 706 | func FileContains(t T, file, content string, settings ...Setting) { 707 | t.Helper() 708 | invoke(t, assertions.FileContains(file, content), settings...) 709 | } 710 | 711 | // FilePathValid asserts path is a valid file path. 712 | func FilePathValid(t T, path string, settings ...Setting) { 713 | t.Helper() 714 | invoke(t, assertions.FilePathValid(path), settings...) 715 | } 716 | 717 | // Close asserts c.Close does not cause an error. 718 | func Close(t T, c io.Closer) { 719 | t.Helper() 720 | invoke(t, assertions.Close(c)) 721 | } 722 | 723 | // StrEqFold asserts exp and val are equivalent, ignoring case. 724 | func StrEqFold(t T, exp, val string, settings ...Setting) { 725 | t.Helper() 726 | invoke(t, assertions.StrEqFold(exp, val), settings...) 727 | } 728 | 729 | // StrNotEqFold asserts exp and val are not equivalent, ignoring case. 730 | func StrNotEqFold(t T, exp, val string, settings ...Setting) { 731 | t.Helper() 732 | invoke(t, assertions.StrNotEqFold(exp, val), settings...) 733 | } 734 | 735 | // StrContains asserts s contains substring sub. 736 | func StrContains(t T, s, sub string, settings ...Setting) { 737 | t.Helper() 738 | invoke(t, assertions.StrContains(s, sub), settings...) 739 | } 740 | 741 | // StrContainsFold asserts s contains substring sub, ignoring case. 742 | func StrContainsFold(t T, s, sub string, settings ...Setting) { 743 | t.Helper() 744 | invoke(t, assertions.StrContainsFold(s, sub), settings...) 745 | } 746 | 747 | // StrNotContains asserts s does not contain substring sub. 748 | func StrNotContains(t T, s, sub string, settings ...Setting) { 749 | t.Helper() 750 | invoke(t, assertions.StrNotContains(s, sub), settings...) 751 | } 752 | 753 | // StrNotContainsFold asserts s does not contain substring sub, ignoring case. 754 | func StrNotContainsFold(t T, s, sub string, settings ...Setting) { 755 | t.Helper() 756 | invoke(t, assertions.StrNotContainsFold(s, sub), settings...) 757 | } 758 | 759 | // StrContainsAny asserts s contains at least one character in chars. 760 | func StrContainsAny(t T, s, chars string, settings ...Setting) { 761 | t.Helper() 762 | invoke(t, assertions.StrContainsAny(s, chars), settings...) 763 | } 764 | 765 | // StrNotContainsAny asserts s does not contain any character in chars. 766 | func StrNotContainsAny(t T, s, chars string, settings ...Setting) { 767 | t.Helper() 768 | invoke(t, assertions.StrNotContainsAny(s, chars), settings...) 769 | } 770 | 771 | // StrCount asserts s contains exactly count instances of substring sub. 772 | func StrCount(t T, s, sub string, count int, settings ...Setting) { 773 | t.Helper() 774 | invoke(t, assertions.StrCount(s, sub, count), settings...) 775 | } 776 | 777 | // StrContainsFields asserts that fields is a subset of the result of strings.Fields(s). 778 | func StrContainsFields(t T, s string, fields []string, settings ...Setting) { 779 | t.Helper() 780 | invoke(t, assertions.StrContainsFields(s, fields), settings...) 781 | } 782 | 783 | // StrHasPrefix asserts that s starts with prefix. 784 | func StrHasPrefix(t T, prefix, s string, settings ...Setting) { 785 | t.Helper() 786 | invoke(t, assertions.StrHasPrefix(prefix, s), settings...) 787 | } 788 | 789 | // StrNotHasPrefix asserts that s does not start with prefix. 790 | func StrNotHasPrefix(t T, prefix, s string, settings ...Setting) { 791 | t.Helper() 792 | invoke(t, assertions.StrNotHasPrefix(prefix, s), settings...) 793 | } 794 | 795 | // StrHasSuffix asserts that s ends with suffix. 796 | func StrHasSuffix(t T, suffix, s string, settings ...Setting) { 797 | t.Helper() 798 | invoke(t, assertions.StrHasSuffix(suffix, s), settings...) 799 | } 800 | 801 | // StrNotHasSuffix asserts that s does not end with suffix. 802 | func StrNotHasSuffix(t T, suffix, s string, settings ...Setting) { 803 | t.Helper() 804 | invoke(t, assertions.StrNotHasSuffix(suffix, s), settings...) 805 | } 806 | 807 | // RegexMatch asserts regular expression re matches string s. 808 | func RegexMatch(t T, re *regexp.Regexp, s string, settings ...Setting) { 809 | t.Helper() 810 | invoke(t, assertions.RegexMatch(re, s), settings...) 811 | } 812 | 813 | // RegexCompiles asserts expr compiles as a valid regular expression. 814 | func RegexCompiles(t T, expr string, settings ...Setting) { 815 | t.Helper() 816 | invoke(t, assertions.RegexpCompiles(expr), settings...) 817 | } 818 | 819 | // RegexCompilesPOSIX asserts expr compiles as a valid POSIX regular expression. 820 | func RegexCompilesPOSIX(t T, expr string, settings ...Setting) { 821 | t.Helper() 822 | invoke(t, assertions.RegexpCompilesPOSIX(expr), settings...) 823 | } 824 | 825 | // UUIDv4 asserts id meets the criteria of a v4 UUID. 826 | func UUIDv4(t T, id string, settings ...Setting) { 827 | t.Helper() 828 | invoke(t, assertions.UUIDv4(id), settings...) 829 | } 830 | 831 | // Size asserts s.Size() is equal to exp. 832 | func Size(t T, exp int, s interfaces.SizeFunc, settings ...Setting) { 833 | t.Helper() 834 | invoke(t, assertions.Size(exp, s), settings...) 835 | } 836 | 837 | // Length asserts l.Len() is equal to exp. 838 | func Length(t T, exp int, l interfaces.LengthFunc, settings ...Setting) { 839 | t.Helper() 840 | invoke(t, assertions.Length(exp, l), settings...) 841 | } 842 | 843 | // Empty asserts e.Empty() is true. 844 | func Empty(t T, e interfaces.EmptyFunc, settings ...Setting) { 845 | t.Helper() 846 | invoke(t, assertions.Empty(e), settings...) 847 | } 848 | 849 | // NotEmpty asserts e.Empty() is false. 850 | func NotEmpty(t T, e interfaces.EmptyFunc, settings ...Setting) { 851 | t.Helper() 852 | invoke(t, assertions.NotEmpty(e), settings...) 853 | } 854 | 855 | // Contains asserts container.ContainsFunc(element) is true. 856 | func Contains[C any](t T, element C, container interfaces.ContainsFunc[C], settings ...Setting) { 857 | t.Helper() 858 | invoke(t, assertions.Contains(element, container), settings...) 859 | } 860 | 861 | // ContainsSubset asserts each element in elements exists in container, in no particular order. 862 | // There may be elements in container beyond what is present in elements. 863 | func ContainsSubset[C any](t T, elements []C, container interfaces.ContainsFunc[C], settings ...Setting) { 864 | t.Helper() 865 | invoke(t, assertions.ContainsSubset(elements, container), settings...) 866 | } 867 | 868 | // NotContains asserts container.ContainsFunc(element) is false. 869 | func NotContains[C any](t T, element C, container interfaces.ContainsFunc[C], settings ...Setting) { 870 | t.Helper() 871 | invoke(t, assertions.NotContains(element, container), settings...) 872 | } 873 | 874 | // Wait asserts wc. 875 | func Wait(t T, wc *wait.Constraint, settings ...Setting) { 876 | t.Helper() 877 | invoke(t, assertions.Wait(wc), settings...) 878 | } 879 | 880 | // Tweak is used to modify a struct and assert its Equal method captures the 881 | // modification. 882 | // 883 | // Field is the name of the struct field and is used only for error printing. 884 | // Apply is a function that modifies E. 885 | type Tweak[E interfaces.CopyEqual[E]] struct { 886 | Field string 887 | Apply interfaces.TweakFunc[E] 888 | } 889 | 890 | // Tweaks is a slice of Tweak. 891 | type Tweaks[E interfaces.CopyEqual[E]] []Tweak[E] 892 | 893 | // StructEqual will apply each Tweak and assert E.Equal captures the modification. 894 | func StructEqual[E interfaces.CopyEqual[E]](t T, original E, tweaks Tweaks[E], settings ...Setting) { 895 | t.Helper() 896 | invoke(t, assertions.StructEqual( 897 | original, 898 | util.CloneSliceFunc( 899 | tweaks, 900 | func(tweak Tweak[E]) assertions.Tweak[E] { 901 | return assertions.Tweak[E]{Field: tweak.Field, Apply: tweak.Apply} 902 | }, 903 | ), 904 | ), settings...) 905 | } 906 | -------------------------------------------------------------------------------- /testdata/dir1/file1: -------------------------------------------------------------------------------- 1 | real data -------------------------------------------------------------------------------- /util/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util_test 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | "github.com/shoenig/test/util" 12 | ) 13 | 14 | var t = new(testing.T) 15 | 16 | func ExampleTempFile() { 17 | path := util.TempFile(t, 18 | util.String("hello!"), 19 | util.Mode(0o640), 20 | ) 21 | 22 | b, _ := os.ReadFile(path) 23 | fmt.Println(string(b)) 24 | // Output: hello! 25 | } 26 | -------------------------------------------------------------------------------- /util/tempfile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package util provides utility functions for writing concise test cases. 5 | package util 6 | 7 | import ( 8 | "errors" 9 | "io/fs" 10 | "os" 11 | ) 12 | 13 | type T interface { 14 | TempDir() string 15 | Helper() 16 | Errorf(format string, args ...any) 17 | Fatalf(format string, args ...any) 18 | Cleanup(func()) 19 | } 20 | 21 | type TempFileSettings struct { 22 | data []byte 23 | mode *fs.FileMode 24 | namePattern string 25 | dir *string 26 | } 27 | 28 | type TempFileSetting func(s *TempFileSettings) 29 | 30 | // Pattern sets the filename to pattern with a random string appended. 31 | // If pattern contains a '*', the last '*' will be replaced by the 32 | // random string. 33 | func Pattern(pattern string) TempFileSetting { 34 | return func(s *TempFileSettings) { 35 | s.namePattern = pattern 36 | } 37 | } 38 | 39 | // Mode sets the temporary file's mode. 40 | func Mode(mode fs.FileMode) TempFileSetting { 41 | return func(s *TempFileSettings) { 42 | s.mode = &mode 43 | } 44 | } 45 | 46 | // String writes data to the temporary file. 47 | func String(data string) TempFileSetting { 48 | return func(s *TempFileSettings) { 49 | s.data = []byte(data) 50 | } 51 | } 52 | 53 | // Bytes writes data to the temporary file. 54 | func Bytes(data []byte) TempFileSetting { 55 | return func(s *TempFileSettings) { 56 | s.data = data 57 | } 58 | } 59 | 60 | // Dir specifies a directory path to contain the temporary file. 61 | // If dir is the empty string, the file will be created in the 62 | // default directory for temporary files, as returned by os.TempDir. 63 | // A temporary file created in a custom directory will still be deleted 64 | // after the test runs, though the directory may not. 65 | func Dir(dir string) TempFileSetting { 66 | return func(s *TempFileSettings) { 67 | s.dir = &dir 68 | } 69 | } 70 | 71 | // TempFile creates a temporary file that is deleted after the test is 72 | // completed. If the file cannot be deleted, the test fails with a message 73 | // containing its path. TempFile creates a new file every time it is called. 74 | // By default, each file thus created is in a unique directory as 75 | // created by (*testing.T).TempDir(); this directory is also deleted 76 | // after the test is completed. 77 | func TempFile(t T, settings ...TempFileSetting) (path string) { 78 | t.Helper() 79 | path, err := tempFile(t, settings...) 80 | t.Cleanup(func() { 81 | err := os.Remove(path) 82 | if err != nil { 83 | t.Fatalf("failed to clean up temp file: %s", path) 84 | } 85 | }) 86 | if err != nil { 87 | t.Fatalf("TempFile: %v", err) 88 | } 89 | return path 90 | } 91 | 92 | type tempFileT interface { 93 | Helper() 94 | TempDir() string 95 | } 96 | 97 | // tempFile returns errors instead of relying upon T to stop execution, for ease 98 | // of testing TempFile. 99 | func tempFile(t tempFileT, settings ...TempFileSetting) (path string, err error) { 100 | t.Helper() 101 | var allSettings TempFileSettings 102 | for _, setting := range settings { 103 | setting(&allSettings) 104 | } 105 | if allSettings.mode == nil { 106 | allSettings.mode = new(fs.FileMode) 107 | *allSettings.mode = 0600 108 | } 109 | if allSettings.dir == nil { 110 | allSettings.dir = new(string) 111 | *allSettings.dir = t.TempDir() 112 | } 113 | 114 | file, err := os.CreateTemp(*allSettings.dir, allSettings.namePattern) 115 | if errors.Is(err, fs.ErrNotExist) { 116 | return "", errors.New("directory does not exist") 117 | } 118 | if err != nil { 119 | return "", err 120 | } 121 | path = file.Name() 122 | _, err = file.Write(allSettings.data) 123 | if err != nil { 124 | file.Close() 125 | return path, err 126 | } 127 | err = file.Close() 128 | if err != nil { 129 | return path, err 130 | } 131 | err = os.Chmod(path, *allSettings.mode) 132 | if err != nil { 133 | return path, err 134 | } 135 | return file.Name(), nil 136 | } 137 | -------------------------------------------------------------------------------- /util/tempfile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package util_test 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io/fs" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/shoenig/test/util" 17 | ) 18 | 19 | func trackHelper(t util.T) *helperTracker { 20 | return &helperTracker{t: t} 21 | } 22 | 23 | type helperTracker struct { 24 | helperCalled bool 25 | t util.T 26 | } 27 | 28 | func (t *helperTracker) TempDir() string { 29 | t.Helper() 30 | return t.t.TempDir() 31 | } 32 | 33 | func (t *helperTracker) Helper() { 34 | t.t.Helper() 35 | t.helperCalled = true 36 | } 37 | 38 | func (t *helperTracker) Errorf(s string, args ...any) { 39 | t.Helper() 40 | t.t.Errorf(s, args) 41 | } 42 | 43 | func (t *helperTracker) Fatalf(s string, args ...any) { 44 | t.Helper() 45 | t.t.Fatalf(s, args...) 46 | } 47 | 48 | func (t *helperTracker) Cleanup(f func()) { 49 | t.Helper() 50 | t.t.Cleanup(f) 51 | } 52 | 53 | func trackFailure(t util.T) *failureTracker { 54 | return &failureTracker{t: t} 55 | } 56 | 57 | type failureTracker struct { 58 | failed bool 59 | log bytes.Buffer 60 | t util.T 61 | } 62 | 63 | func (t *failureTracker) TempDir() string { 64 | t.Helper() 65 | return t.t.TempDir() 66 | } 67 | 68 | func (t *failureTracker) Helper() { 69 | t.t.Helper() 70 | } 71 | 72 | func (t *failureTracker) Errorf(s string, args ...any) { 73 | t.Helper() 74 | t.failed = true 75 | fmt.Fprintf(&t.log, s+"\n", args...) 76 | } 77 | 78 | func (t *failureTracker) Fatalf(s string, args ...any) { 79 | t.Helper() 80 | t.failed = true 81 | fmt.Fprintf(&t.log, s+"\n", args...) 82 | } 83 | 84 | func (t *failureTracker) Cleanup(f func()) { 85 | t.Helper() 86 | t.t.Cleanup(f) 87 | } 88 | 89 | func (t *failureTracker) AssertFailedWith(msg string) { 90 | t.Helper() 91 | if !t.failed { 92 | t.t.Fatalf("expected test to fail with message %q", msg) 93 | } 94 | strlog := t.log.String() 95 | if !strings.Contains(strlog, msg) { 96 | t.t.Fatalf("expected test to fail with message %q\ngot message %q", msg, strlog) 97 | } 98 | } 99 | 100 | func TestTempFile(t *testing.T) { 101 | t.Run("creates a read/write temp file by default", func(t *testing.T) { 102 | th := trackHelper(t) 103 | path := util.TempFile(th) 104 | if !th.helperCalled { 105 | t.Errorf("expected TempFile to call Helper") 106 | } 107 | info, err := os.Stat(path) 108 | if err != nil { 109 | t.Fatalf("failed to stat temp file: %v", err) 110 | } 111 | mode := info.Mode() 112 | if mode&0400 == 0 || mode&0200 == 0 { 113 | t.Fatalf("expected at least u+rw permission, got %03o", mode) 114 | } 115 | }) 116 | 117 | t.Run("using multiple options", func(t *testing.T) { 118 | var expectedMode fs.FileMode = 0444 119 | expectedData := "important data" 120 | prefix := "harvey-" 121 | pattern := prefix + "*" 122 | path := util.TempFile(t, 123 | util.Mode(expectedMode), 124 | util.Pattern(pattern), 125 | util.String(expectedData)) 126 | 127 | t.Run("Mode sets a custom file mode", func(t *testing.T) { 128 | info, err := os.Stat(path) 129 | if err != nil { 130 | t.Fatalf("failed to stat temp file: %v", err) 131 | } 132 | actualMode := info.Mode() 133 | if expectedMode != actualMode { 134 | t.Fatalf("file has wrong mode\nexpected %03o\ngot %03o", expectedMode, actualMode) 135 | } 136 | }) 137 | t.Run("sets a name pattern", func(t *testing.T) { 138 | if !strings.Contains(path, prefix) { 139 | t.Fatalf("filename does not match pattern\nexpected to contain %s\ngot %s", prefix, path) 140 | } 141 | }) 142 | t.Run("sets string data", func(t *testing.T) { 143 | actualData, err := os.ReadFile(path) 144 | if err != nil { 145 | t.Fatalf("failed to read temp file: %v", err) 146 | } 147 | if expectedData != string(actualData) { 148 | t.Fatalf("temp file contains wrong data\nexpected %q\ngot %q", expectedData, string(actualData)) 149 | } 150 | }) 151 | }) 152 | 153 | t.Run("Bytes sets binary data", func(t *testing.T) { 154 | expectedData := []byte("important data") 155 | path := util.TempFile(t, util.Bytes(expectedData)) 156 | actualData, err := os.ReadFile(path) 157 | if err != nil { 158 | t.Fatalf("failed to read temp file: %v", err) 159 | } 160 | if !bytes.Equal(expectedData, actualData) { 161 | t.Fatalf("temp file contains wrong data\nexpected %q\ngot %q", string(expectedData), actualData) 162 | } 163 | }) 164 | 165 | t.Run("cleans up file (and nothing else) in custom dir", func(t *testing.T) { 166 | dir := t.TempDir() 167 | existing, err := os.CreateTemp(dir, "") 168 | if err != nil { 169 | t.Fatalf("failed to create temporary file: %v", err) 170 | } 171 | existingPath := existing.Name() 172 | existing.Close() 173 | var newPath string 174 | 175 | t.Run("uses custom directory", func(t *testing.T) { 176 | newPath = util.TempFile(t, util.Dir(dir)) 177 | entries, err := os.ReadDir(dir) 178 | if err != nil { 179 | t.Fatalf("failed to read directory: %v", err) 180 | } 181 | var found bool 182 | for _, entry := range entries { 183 | if entry.Name() == filepath.Base(newPath) { 184 | found = true 185 | break 186 | } 187 | } 188 | if !found { 189 | t.Fatalf("did not find temporary file in %s", dir) 190 | } 191 | }) 192 | 193 | if newPath == "" { 194 | t.Fatal("expected non-empty path") 195 | } 196 | _, err = os.Stat(newPath) 197 | if !errors.Is(err, fs.ErrNotExist) { 198 | if err == nil { 199 | t.Errorf("expected temp file not to exist: %s", newPath) 200 | } else { 201 | t.Errorf("unexpected error: %v", err) 202 | } 203 | } 204 | _, err = os.Stat(existingPath) 205 | if err != nil { 206 | if errors.Is(err, fs.ErrNotExist) { 207 | t.Error("expected pre-existing file not to be deleted") 208 | } else { 209 | t.Errorf("unexpected error statting pre-existing file: %v", err) 210 | } 211 | } 212 | }) 213 | 214 | t.Run("fails if specified dir doesn't exist", func(t *testing.T) { 215 | fakeDir := filepath.Join(t.TempDir(), "fake") 216 | tracker := trackFailure(t) 217 | path := util.TempFile(tracker, util.Dir(fakeDir)) 218 | if path != "" { 219 | t.Errorf("expected empty path\ngot %q", path) 220 | } 221 | tracker.AssertFailedWith("TempFile: directory does not exist") 222 | }) 223 | } 224 | -------------------------------------------------------------------------------- /wait/wait.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package wait provides constructs for waiting on conditionals within specified constraints. 5 | package wait 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "math" 12 | "time" 13 | ) 14 | 15 | var ( 16 | ErrTimeoutExceeded = errors.New("wait: timeout exceeded") 17 | ErrAttemptsExceeded = errors.New("wait: attempts exceeded") 18 | ErrConditionUnsatisfied = errors.New("wait: condition unsatisfied") 19 | ErrNoFunction = errors.New("wait: no function specified") 20 | ) 21 | 22 | const ( 23 | defaultTimeout = 3 * time.Second 24 | defaultGap = 250 * time.Millisecond 25 | ) 26 | 27 | // A Constraint is something a test assertion can wait on before marking the 28 | // result to be a failure. A Constraint is used in conjunction with either the 29 | // InitialSuccess or ContinualSuccess option. A call to Run will execute the given 30 | // function, returning nil or error depending on the Constraint configuration and 31 | // the results of the function. 32 | // 33 | // InitialSuccess - retry a function until it returns a positive result. If the 34 | // function never returns a positive result before the Constraint threshold is 35 | // exceeded, an error is returned from Run(). 36 | // 37 | // ContinualSuccess - retry a function asserting it returns a positive result until 38 | // the Constraint threshold is exceeded. If at any point the function returns a 39 | // negative result, an error is returned from Run(). 40 | // 41 | // A Constraint threshold is configured via either Timeout or Attempts (not both). 42 | // 43 | // Timeout - Constraint is time bound. 44 | // 45 | // Attempts - Constraint is iteration bound. 46 | // 47 | // The use of Gap controls the pace of attempts by setting the amount of time to 48 | // wait in between each attempt. 49 | type Constraint struct { 50 | continual bool // (initial || continual) success 51 | now time.Time 52 | deadline time.Time 53 | gap time.Duration 54 | iterations int 55 | r runnable 56 | } 57 | 58 | // InitialSuccess creates a new Constraint configured by opts that will wait for a 59 | // positive result upon calling Constraint.Run. If the threshold of the Constraint 60 | // is exceeded before reaching a positive result, an error is returned from the 61 | // call to Constraint.Run. 62 | // 63 | // Timeout is used to set a maximum amount of time to wait for success. 64 | // Attempts is used to set a maximum number of attempts to wait for success. 65 | // Gap is used to control the amount of time to wait between retries. 66 | // 67 | // One of ErrorFunc, BoolFunc, or TestFunc represents the function that will 68 | // be run under the constraint. 69 | func InitialSuccess(opts ...Option) *Constraint { 70 | c := &Constraint{now: time.Now()} 71 | c.setup(opts...) 72 | return c 73 | } 74 | 75 | // ContinualSuccess creates a new Constraint configured by opts that will assert 76 | // a positive result upon calling Constraint.Run, repeating the call until the 77 | // Constraint reaches its threshold. If the result is negative, an error is 78 | // returned from the call to Constraint.Run. 79 | // 80 | // Timeout is used to set the amount of time to assert success. 81 | // Attempts is used to set the number of iterations to assert success. 82 | // Gap is used to control the amount of time to wait between iterations. 83 | // 84 | // One of ErrorFunc, BoolFunc, or TestFunc represents the function that will 85 | // be run under the constraint. 86 | func ContinualSuccess(opts ...Option) *Constraint { 87 | c := &Constraint{now: time.Now(), continual: true} 88 | c.setup(opts...) 89 | return c 90 | } 91 | 92 | // Timeout sets a time bound on a Constraint. 93 | // 94 | // If set, the Attempts constraint configuration is disabled. 95 | // 96 | // Default 3 seconds. 97 | func Timeout(duration time.Duration) Option { 98 | return func(c *Constraint) { 99 | c.deadline = time.Now().Add(duration) 100 | c.iterations = math.MaxInt 101 | } 102 | } 103 | 104 | // Attempts sets an iteration bound on a Constraint. 105 | // 106 | // If set, the Timeout constraint configuration is disabled. 107 | // 108 | // By default a Timeout constraint is set and the Attempts bound is disabled. 109 | func Attempts(max int) Option { 110 | return func(c *Constraint) { 111 | c.iterations = max 112 | c.deadline = time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC) 113 | } 114 | } 115 | 116 | // Gap sets the amount of time to wait between attempts. 117 | // 118 | // Default 250 milliseconds. 119 | func Gap(duration time.Duration) Option { 120 | return func(c *Constraint) { 121 | c.gap = duration 122 | } 123 | } 124 | 125 | // BoolFunc executes f under the thresholds of a Constraint. 126 | func BoolFunc(f func() bool) Option { 127 | return func(c *Constraint) { 128 | if c.continual { 129 | c.r = boolFuncContinual(f) 130 | } else { 131 | c.r = boolFuncInitial(f) 132 | } 133 | } 134 | } 135 | 136 | // Option is used to configure a Constraint. 137 | // 138 | // Understood Option functions include Timeout, Attempts, Gap, InitialSuccess, 139 | // and ContinualSuccess. 140 | type Option func(*Constraint) 141 | 142 | type runnable func(*runner) *result 143 | 144 | type runner struct { 145 | c *Constraint 146 | attempts int 147 | } 148 | 149 | type result struct { 150 | Err error 151 | } 152 | 153 | func boolFuncContinual(f func() bool) runnable { 154 | bg := context.Background() 155 | return func(r *runner) *result { 156 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 157 | defer cancel() 158 | 159 | timer := time.NewTimer(0) 160 | defer timer.Stop() 161 | 162 | for { 163 | // make an attempt 164 | if !f() { 165 | return &result{Err: ErrConditionUnsatisfied} 166 | } 167 | 168 | // used another attempt 169 | r.attempts++ 170 | 171 | // reached the desired attempts 172 | if r.attempts >= r.c.iterations { 173 | return &result{Err: nil} 174 | } 175 | 176 | // reset timer to gap interval 177 | timer.Reset(r.c.gap) 178 | 179 | // wait for gap or time 180 | select { 181 | case <-ctx.Done(): 182 | return &result{Err: nil} 183 | case <-timer.C: 184 | // continue 185 | } 186 | } 187 | } 188 | } 189 | 190 | func boolFuncInitial(f func() bool) runnable { 191 | bg := context.Background() 192 | return func(r *runner) *result { 193 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 194 | defer cancel() 195 | 196 | timer := time.NewTimer(0) 197 | defer timer.Stop() 198 | 199 | for { 200 | // make an attempt 201 | if f() { 202 | return &result{Err: nil} 203 | } 204 | 205 | // used another attempt 206 | r.attempts++ 207 | 208 | // check iterations 209 | if r.attempts > r.c.iterations { 210 | return &result{Err: ErrAttemptsExceeded} 211 | } 212 | 213 | // reset timer to gap interval 214 | timer.Reset(r.c.gap) 215 | 216 | // wait for gap or timeout 217 | select { 218 | case <-ctx.Done(): 219 | return &result{Err: ErrTimeoutExceeded} 220 | case <-timer.C: 221 | // continue 222 | } 223 | } 224 | } 225 | } 226 | 227 | // ErrorFunc will retry f while it returns a non-nil error, or until a wait 228 | // constraint threshold is exceeded. 229 | func ErrorFunc(f func() error) Option { 230 | return func(c *Constraint) { 231 | if c.continual { 232 | c.r = errFuncContinual(f) 233 | } else { 234 | c.r = errFuncInitial(f) 235 | } 236 | } 237 | } 238 | 239 | func errFuncContinual(f func() error) runnable { 240 | bg := context.Background() 241 | return func(r *runner) *result { 242 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 243 | defer cancel() 244 | 245 | timer := time.NewTimer(0) 246 | defer timer.Stop() 247 | 248 | for { 249 | // make an attempt 250 | if err := f(); err != nil { 251 | return &result{Err: err} 252 | } 253 | 254 | // used another attempt 255 | r.attempts++ 256 | 257 | // reached the desired attempts 258 | if r.attempts >= r.c.iterations { 259 | return &result{Err: nil} 260 | } 261 | 262 | // reset timer to gap interval 263 | timer.Reset(r.c.gap) 264 | 265 | // wait for gap or time 266 | select { 267 | case <-ctx.Done(): 268 | return &result{Err: nil} 269 | case <-timer.C: 270 | // continue 271 | } 272 | } 273 | } 274 | } 275 | 276 | func errFuncInitial(f func() error) runnable { 277 | bg := context.Background() 278 | return func(r *runner) *result { 279 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 280 | defer cancel() 281 | 282 | timer := time.NewTimer(0) 283 | defer timer.Stop() 284 | 285 | for { 286 | // make an attempt 287 | err := f() 288 | if err == nil { 289 | return &result{Err: nil} 290 | } 291 | 292 | // used another attempt 293 | r.attempts++ 294 | 295 | // check iterations 296 | if r.attempts > r.c.iterations { 297 | return &result{ 298 | Err: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), err), 299 | } 300 | } 301 | 302 | // reset timer to gap interval 303 | timer.Reset(r.c.gap) 304 | 305 | // wait for gap or timeout 306 | select { 307 | case <-ctx.Done(): 308 | return &result{ 309 | Err: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), err), 310 | } 311 | case <-timer.C: 312 | // continue 313 | } 314 | } 315 | } 316 | } 317 | 318 | // TestFunc will retry f while it returns false, or until a wait constraint 319 | // threshold is exceeded. If f never succeeds, the latest returned error is 320 | // wrapped into the result. 321 | func TestFunc(f func() (bool, error)) Option { 322 | return func(c *Constraint) { 323 | if c.continual { 324 | c.r = testFuncContinual(f) 325 | } else { 326 | c.r = testFuncInitial(f) 327 | } 328 | } 329 | } 330 | 331 | func testFuncContinual(f func() (bool, error)) runnable { 332 | bg := context.Background() 333 | return func(r *runner) *result { 334 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 335 | defer cancel() 336 | 337 | timer := time.NewTimer(0) 338 | defer timer.Stop() 339 | 340 | for { 341 | // make an attempt 342 | ok, err := f() 343 | if !ok { 344 | return &result{Err: fmt.Errorf("%s: %w", ErrConditionUnsatisfied.Error(), err)} 345 | } 346 | 347 | // used another attempt 348 | r.attempts++ 349 | 350 | // reached the desired attempts 351 | if r.attempts >= r.c.iterations { 352 | return &result{Err: nil} 353 | } 354 | 355 | // reset timer to gap interval 356 | timer.Reset(r.c.gap) 357 | 358 | // wait for gap or time 359 | select { 360 | case <-ctx.Done(): 361 | return &result{Err: nil} 362 | case <-timer.C: 363 | // continue 364 | } 365 | } 366 | } 367 | } 368 | 369 | func testFuncInitial(f func() (bool, error)) runnable { 370 | bg := context.Background() 371 | return func(r *runner) *result { 372 | ctx, cancel := context.WithDeadline(bg, r.c.deadline) 373 | defer cancel() 374 | 375 | timer := time.NewTimer(0) 376 | defer timer.Stop() 377 | 378 | for { 379 | // make an attempt 380 | ok, err := f() 381 | if ok { 382 | return &result{Err: nil} 383 | } 384 | 385 | // set default error 386 | if err == nil { 387 | err = ErrConditionUnsatisfied 388 | } 389 | 390 | // used another attempt 391 | r.attempts++ 392 | 393 | // check iterations 394 | if r.attempts > r.c.iterations { 395 | return &result{ 396 | Err: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), err), 397 | } 398 | } 399 | 400 | // reset timer to gap interval 401 | timer.Reset(r.c.gap) 402 | 403 | // wait for gap or timeout 404 | select { 405 | case <-ctx.Done(): 406 | return &result{ 407 | Err: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), err), 408 | } 409 | case <-timer.C: 410 | // continue 411 | } 412 | } 413 | } 414 | } 415 | 416 | func (c *Constraint) setup(opts ...Option) { 417 | for _, opt := range append([]Option{ 418 | Timeout(defaultTimeout), 419 | Gap(defaultGap), 420 | }, opts...) { 421 | opt(c) 422 | } 423 | } 424 | 425 | // Run the Constraint and produce an error result. 426 | func (c *Constraint) Run() error { 427 | if c.r == nil { 428 | return ErrNoFunction 429 | } 430 | return c.r(&runner{ 431 | c: c, 432 | attempts: 0, 433 | }).Err 434 | } 435 | -------------------------------------------------------------------------------- /wait/wait_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) The Test Authors 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package wait 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "math/rand" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | oops = errors.New("oops") 16 | boolFnTrue = func() bool { return true } 17 | boolFnFalse = func() bool { return false } 18 | errFnNil = func() error { return nil } 19 | errFnNotNil = func() error { return oops } 20 | tFnNil = func() (bool, error) { return true, nil } 21 | tFnNotNil = func() (bool, error) { return false, oops } 22 | ) 23 | 24 | func eqErr(t *testing.T, exp, err error) { 25 | t.Helper() 26 | 27 | if exp == nil || err == nil { 28 | if !errors.Is(exp, err) { 29 | t.Fatalf("exp: %v, err: %v", exp, err) 30 | } 31 | return 32 | } 33 | expect := exp.Error() 34 | actual := err.Error() 35 | if expect != actual { 36 | t.Fatalf("exp: %s, err: %s", expect, actual) 37 | } 38 | } 39 | 40 | func TestNoFunction(t *testing.T) { 41 | t.Parallel() 42 | 43 | ctx := InitialSuccess() 44 | err := ctx.Run() 45 | if !errors.Is(err, ErrNoFunction) { 46 | t.Fatalf("exp: %v, err: %v", ErrNoFunction, err) 47 | } 48 | } 49 | 50 | func TestContinual_BoolFunc(t *testing.T) { 51 | t.Parallel() 52 | 53 | cases := []struct { 54 | name string 55 | opts []Option 56 | exp error 57 | }{ 58 | { 59 | name: "defaults ok", 60 | opts: []Option{BoolFunc(boolFnTrue)}, 61 | }, 62 | { 63 | name: "defaults fail", 64 | opts: []Option{BoolFunc(boolFnFalse)}, 65 | exp: ErrConditionUnsatisfied, 66 | }, 67 | { 68 | name: "randomly fail", 69 | opts: []Option{ 70 | BoolFunc(func() bool { 71 | return rand.Int()%3 != 0 72 | }), 73 | Gap(1 * time.Millisecond), 74 | }, 75 | exp: ErrConditionUnsatisfied, 76 | }, 77 | } 78 | 79 | for _, tc := range cases { 80 | t.Run(tc.name, func(t *testing.T) { 81 | c := ContinualSuccess(tc.opts...) 82 | err := c.Run() 83 | eqErr(t, tc.exp, err) 84 | }) 85 | } 86 | } 87 | 88 | func TestInitial_BoolFunc(t *testing.T) { 89 | t.Parallel() 90 | 91 | cases := []struct { 92 | name string 93 | opts []Option 94 | exp error 95 | }{ 96 | { 97 | name: "defaults ok", 98 | opts: []Option{BoolFunc(boolFnTrue)}, 99 | }, 100 | { 101 | name: "defaults fail", 102 | opts: []Option{BoolFunc(boolFnFalse)}, 103 | exp: ErrTimeoutExceeded, 104 | }, 105 | { 106 | name: "iterations exceeded", 107 | opts: []Option{ 108 | BoolFunc(boolFnFalse), 109 | Attempts(3), 110 | }, 111 | exp: ErrAttemptsExceeded, 112 | }, 113 | { 114 | name: "short timeout", 115 | opts: []Option{ 116 | BoolFunc(boolFnFalse), 117 | Timeout(100 * time.Millisecond), 118 | }, 119 | exp: ErrTimeoutExceeded, 120 | }, 121 | { 122 | name: "short gap", 123 | opts: []Option{ 124 | BoolFunc(boolFnFalse), 125 | Attempts(10), 126 | Gap(1 * time.Millisecond), 127 | }, 128 | exp: ErrAttemptsExceeded, 129 | }, 130 | { 131 | name: "randomly pass", 132 | opts: []Option{ 133 | BoolFunc(func() bool { 134 | return rand.Int()%3 == 0 135 | }), 136 | Gap(1 * time.Millisecond), 137 | }, 138 | }, 139 | } 140 | 141 | for _, tc := range cases { 142 | t.Run(tc.name, func(t *testing.T) { 143 | c := InitialSuccess(tc.opts...) 144 | err := c.Run() 145 | eqErr(t, tc.exp, err) 146 | }) 147 | } 148 | } 149 | 150 | func TestContinual_ErrorFunc(t *testing.T) { 151 | t.Parallel() 152 | 153 | cases := []struct { 154 | name string 155 | opts []Option 156 | exp error 157 | }{ 158 | { 159 | name: "defaults ok", 160 | opts: []Option{ErrorFunc(errFnNil)}, 161 | }, 162 | { 163 | name: "defaults fail", 164 | opts: []Option{ErrorFunc(errFnNotNil)}, 165 | exp: oops, 166 | }, 167 | { 168 | name: "randomly fail", 169 | opts: []Option{ 170 | ErrorFunc(func() error { 171 | if rand.Int()%3 != 0 { 172 | return nil 173 | } 174 | return oops 175 | }), 176 | Gap(1 * time.Millisecond), 177 | }, 178 | exp: oops, 179 | }, 180 | } 181 | 182 | for _, tc := range cases { 183 | t.Run(tc.name, func(t *testing.T) { 184 | c := ContinualSuccess(tc.opts...) 185 | err := c.Run() 186 | eqErr(t, tc.exp, err) 187 | }) 188 | } 189 | } 190 | 191 | func TestInitial_ErrorFunc(t *testing.T) { 192 | t.Parallel() 193 | 194 | cases := []struct { 195 | name string 196 | opts []Option 197 | exp error 198 | }{ 199 | { 200 | name: "defaults ok", 201 | opts: []Option{ErrorFunc(errFnNil)}, 202 | }, 203 | { 204 | name: "defaults fail", 205 | opts: []Option{ErrorFunc(errFnNotNil)}, 206 | exp: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), oops), 207 | }, 208 | { 209 | name: "attempts exceeded", 210 | opts: []Option{ 211 | ErrorFunc(errFnNotNil), 212 | Attempts(3), 213 | }, 214 | exp: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), oops), 215 | }, 216 | { 217 | name: "short timeout", 218 | opts: []Option{ 219 | ErrorFunc(errFnNotNil), 220 | Attempts(1000), 221 | Timeout(100 * time.Millisecond), 222 | }, 223 | exp: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), oops), 224 | }, 225 | { 226 | name: "short gap", 227 | opts: []Option{ 228 | ErrorFunc(errFnNotNil), 229 | Attempts(10), 230 | Gap(1 * time.Millisecond), 231 | }, 232 | exp: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), oops), 233 | }, 234 | { 235 | name: "randomly pass", 236 | opts: []Option{ 237 | ErrorFunc(func() error { 238 | if rand.Int()%3 != 0 { 239 | return errors.New("not divisible by 3") 240 | } 241 | return nil 242 | }), 243 | Gap(1 * time.Millisecond), 244 | }, 245 | }, 246 | } 247 | 248 | for _, tc := range cases { 249 | t.Run(tc.name, func(t *testing.T) { 250 | c := InitialSuccess(tc.opts...) 251 | err := c.Run() 252 | eqErr(t, tc.exp, err) 253 | }) 254 | } 255 | } 256 | 257 | func TestContinual_TestFunc(t *testing.T) { 258 | t.Parallel() 259 | 260 | cases := []struct { 261 | name string 262 | opts []Option 263 | exp error 264 | }{ 265 | { 266 | name: "defaults ok", 267 | opts: []Option{TestFunc(tFnNil)}, 268 | }, 269 | { 270 | name: "defaults fail", 271 | opts: []Option{TestFunc(tFnNotNil)}, 272 | exp: fmt.Errorf("%s: %w", ErrConditionUnsatisfied.Error(), oops), 273 | }, 274 | { 275 | name: "randomly fail", 276 | opts: []Option{ 277 | TestFunc(func() (bool, error) { 278 | if rand.Int()%3 != 0 { 279 | return true, nil 280 | } 281 | return false, oops 282 | }), 283 | Gap(1 * time.Millisecond), 284 | }, 285 | exp: fmt.Errorf("%s: %w", ErrConditionUnsatisfied.Error(), oops), 286 | }, 287 | } 288 | 289 | for _, tc := range cases { 290 | t.Run(tc.name, func(t *testing.T) { 291 | c := ContinualSuccess(tc.opts...) 292 | err := c.Run() 293 | eqErr(t, tc.exp, err) 294 | }) 295 | } 296 | } 297 | 298 | func TestInitial_TestFunc(t *testing.T) { 299 | t.Parallel() 300 | 301 | cases := []struct { 302 | name string 303 | opts []Option 304 | exp error 305 | }{ 306 | { 307 | name: "defaults ok", 308 | opts: []Option{TestFunc(tFnNil)}, 309 | }, 310 | { 311 | name: "defaults fail", 312 | opts: []Option{TestFunc(tFnNotNil)}, 313 | exp: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), oops), 314 | }, 315 | { 316 | name: "default fail without error", 317 | opts: []Option{ 318 | TestFunc(func() (bool, error) { 319 | return false, nil 320 | }), 321 | }, 322 | exp: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), ErrConditionUnsatisfied), 323 | }, 324 | { 325 | name: "attempts exceeded", 326 | opts: []Option{ 327 | TestFunc(tFnNotNil), 328 | Attempts(3), 329 | }, 330 | exp: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), oops), 331 | }, 332 | { 333 | name: "short timeout", 334 | opts: []Option{ 335 | TestFunc(tFnNotNil), 336 | Attempts(1000), 337 | Timeout(100 * time.Millisecond), 338 | }, 339 | exp: fmt.Errorf("%s: %w", ErrTimeoutExceeded.Error(), oops), 340 | }, 341 | { 342 | name: "short gap", 343 | opts: []Option{ 344 | TestFunc(tFnNotNil), 345 | Attempts(10), 346 | Gap(1 * time.Millisecond), 347 | }, 348 | exp: fmt.Errorf("%s: %w", ErrAttemptsExceeded.Error(), oops), 349 | }, 350 | { 351 | name: "randomly pass", 352 | opts: []Option{ 353 | TestFunc(func() (bool, error) { 354 | if rand.Int()%3 != 0 { 355 | return false, errors.New("not divisible by 3") 356 | } 357 | return true, nil 358 | }), 359 | }, 360 | }, 361 | } 362 | 363 | for _, tc := range cases { 364 | t.Run(tc.name, func(t *testing.T) { 365 | c := InitialSuccess(tc.opts...) 366 | err := c.Run() 367 | eqErr(t, tc.exp, err) 368 | }) 369 | } 370 | } 371 | --------------------------------------------------------------------------------