├── .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 | [](https://pkg.go.dev/github.com/shoenig/test)
6 | [](https://github.com/shoenig/test/blob/main/LICENSE)
7 | [](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 |
--------------------------------------------------------------------------------