Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -19,7 +19,7 @@ golang.org/x/sys d455e41777fca6e8a5a79e34a14b8368bc11d9ba |
| 19 | 19 |
github.com/docker/go-units 47565b4f722fb6ceae66b95f853feed578a4a51c # v0.3.3 |
| 20 | 20 |
github.com/docker/go-connections 7395e3f8aa162843a74ed6d48e79627d9792ac55 # v0.4.0 |
| 21 | 21 |
golang.org/x/text f21a4dfb5e38f5895301dc265a8def02365cc3d0 # v0.3.0 |
| 22 |
-gotest.tools v2.1.0 |
|
| 22 |
+gotest.tools 1083505acf35a0bd8a696b26837e1fb3187a7a83 # v2.3.0 |
|
| 23 | 23 |
github.com/google/go-cmp v0.2.0 |
| 24 | 24 |
|
| 25 | 25 |
github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5 |
| ... | ... |
@@ -1,202 +1,13 @@ |
| 1 |
+Copyright 2018 gotest.tools authors |
|
| 1 | 2 |
|
| 2 |
- Apache License |
|
| 3 |
- Version 2.0, January 2004 |
|
| 4 |
- http://www.apache.org/licenses/ |
|
| 3 |
+Licensed under the Apache License, Version 2.0 (the "License"); |
|
| 4 |
+you may not use this file except in compliance with the License. |
|
| 5 |
+You may obtain a copy of the License at |
|
| 5 | 6 |
|
| 6 |
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|
| 7 |
+ http://www.apache.org/licenses/LICENSE-2.0 |
|
| 7 | 8 |
|
| 8 |
- 1. Definitions. |
|
| 9 |
- |
|
| 10 |
- "License" shall mean the terms and conditions for use, reproduction, |
|
| 11 |
- and distribution as defined by Sections 1 through 9 of this document. |
|
| 12 |
- |
|
| 13 |
- "Licensor" shall mean the copyright owner or entity authorized by |
|
| 14 |
- the copyright owner that is granting the License. |
|
| 15 |
- |
|
| 16 |
- "Legal Entity" shall mean the union of the acting entity and all |
|
| 17 |
- other entities that control, are controlled by, or are under common |
|
| 18 |
- control with that entity. For the purposes of this definition, |
|
| 19 |
- "control" means (i) the power, direct or indirect, to cause the |
|
| 20 |
- direction or management of such entity, whether by contract or |
|
| 21 |
- otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|
| 22 |
- outstanding shares, or (iii) beneficial ownership of such entity. |
|
| 23 |
- |
|
| 24 |
- "You" (or "Your") shall mean an individual or Legal Entity |
|
| 25 |
- exercising permissions granted by this License. |
|
| 26 |
- |
|
| 27 |
- "Source" form shall mean the preferred form for making modifications, |
|
| 28 |
- including but not limited to software source code, documentation |
|
| 29 |
- source, and configuration files. |
|
| 30 |
- |
|
| 31 |
- "Object" form shall mean any form resulting from mechanical |
|
| 32 |
- transformation or translation of a Source form, including but |
|
| 33 |
- not limited to compiled object code, generated documentation, |
|
| 34 |
- and conversions to other media types. |
|
| 35 |
- |
|
| 36 |
- "Work" shall mean the work of authorship, whether in Source or |
|
| 37 |
- Object form, made available under the License, as indicated by a |
|
| 38 |
- copyright notice that is included in or attached to the work |
|
| 39 |
- (an example is provided in the Appendix below). |
|
| 40 |
- |
|
| 41 |
- "Derivative Works" shall mean any work, whether in Source or Object |
|
| 42 |
- form, that is based on (or derived from) the Work and for which the |
|
| 43 |
- editorial revisions, annotations, elaborations, or other modifications |
|
| 44 |
- represent, as a whole, an original work of authorship. For the purposes |
|
| 45 |
- of this License, Derivative Works shall not include works that remain |
|
| 46 |
- separable from, or merely link (or bind by name) to the interfaces of, |
|
| 47 |
- the Work and Derivative Works thereof. |
|
| 48 |
- |
|
| 49 |
- "Contribution" shall mean any work of authorship, including |
|
| 50 |
- the original version of the Work and any modifications or additions |
|
| 51 |
- to that Work or Derivative Works thereof, that is intentionally |
|
| 52 |
- submitted to Licensor for inclusion in the Work by the copyright owner |
|
| 53 |
- or by an individual or Legal Entity authorized to submit on behalf of |
|
| 54 |
- the copyright owner. For the purposes of this definition, "submitted" |
|
| 55 |
- means any form of electronic, verbal, or written communication sent |
|
| 56 |
- to the Licensor or its representatives, including but not limited to |
|
| 57 |
- communication on electronic mailing lists, source code control systems, |
|
| 58 |
- and issue tracking systems that are managed by, or on behalf of, the |
|
| 59 |
- Licensor for the purpose of discussing and improving the Work, but |
|
| 60 |
- excluding communication that is conspicuously marked or otherwise |
|
| 61 |
- designated in writing by the copyright owner as "Not a Contribution." |
|
| 62 |
- |
|
| 63 |
- "Contributor" shall mean Licensor and any individual or Legal Entity |
|
| 64 |
- on behalf of whom a Contribution has been received by Licensor and |
|
| 65 |
- subsequently incorporated within the Work. |
|
| 66 |
- |
|
| 67 |
- 2. Grant of Copyright License. Subject to the terms and conditions of |
|
| 68 |
- this License, each Contributor hereby grants to You a perpetual, |
|
| 69 |
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
| 70 |
- copyright license to reproduce, prepare Derivative Works of, |
|
| 71 |
- publicly display, publicly perform, sublicense, and distribute the |
|
| 72 |
- Work and such Derivative Works in Source or Object form. |
|
| 73 |
- |
|
| 74 |
- 3. Grant of Patent License. Subject to the terms and conditions of |
|
| 75 |
- this License, each Contributor hereby grants to You a perpetual, |
|
| 76 |
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|
| 77 |
- (except as stated in this section) patent license to make, have made, |
|
| 78 |
- use, offer to sell, sell, import, and otherwise transfer the Work, |
|
| 79 |
- where such license applies only to those patent claims licensable |
|
| 80 |
- by such Contributor that are necessarily infringed by their |
|
| 81 |
- Contribution(s) alone or by combination of their Contribution(s) |
|
| 82 |
- with the Work to which such Contribution(s) was submitted. If You |
|
| 83 |
- institute patent litigation against any entity (including a |
|
| 84 |
- cross-claim or counterclaim in a lawsuit) alleging that the Work |
|
| 85 |
- or a Contribution incorporated within the Work constitutes direct |
|
| 86 |
- or contributory patent infringement, then any patent licenses |
|
| 87 |
- granted to You under this License for that Work shall terminate |
|
| 88 |
- as of the date such litigation is filed. |
|
| 89 |
- |
|
| 90 |
- 4. Redistribution. You may reproduce and distribute copies of the |
|
| 91 |
- Work or Derivative Works thereof in any medium, with or without |
|
| 92 |
- modifications, and in Source or Object form, provided that You |
|
| 93 |
- meet the following conditions: |
|
| 94 |
- |
|
| 95 |
- (a) You must give any other recipients of the Work or |
|
| 96 |
- Derivative Works a copy of this License; and |
|
| 97 |
- |
|
| 98 |
- (b) You must cause any modified files to carry prominent notices |
|
| 99 |
- stating that You changed the files; and |
|
| 100 |
- |
|
| 101 |
- (c) You must retain, in the Source form of any Derivative Works |
|
| 102 |
- that You distribute, all copyright, patent, trademark, and |
|
| 103 |
- attribution notices from the Source form of the Work, |
|
| 104 |
- excluding those notices that do not pertain to any part of |
|
| 105 |
- the Derivative Works; and |
|
| 106 |
- |
|
| 107 |
- (d) If the Work includes a "NOTICE" text file as part of its |
|
| 108 |
- distribution, then any Derivative Works that You distribute must |
|
| 109 |
- include a readable copy of the attribution notices contained |
|
| 110 |
- within such NOTICE file, excluding those notices that do not |
|
| 111 |
- pertain to any part of the Derivative Works, in at least one |
|
| 112 |
- of the following places: within a NOTICE text file distributed |
|
| 113 |
- as part of the Derivative Works; within the Source form or |
|
| 114 |
- documentation, if provided along with the Derivative Works; or, |
|
| 115 |
- within a display generated by the Derivative Works, if and |
|
| 116 |
- wherever such third-party notices normally appear. The contents |
|
| 117 |
- of the NOTICE file are for informational purposes only and |
|
| 118 |
- do not modify the License. You may add Your own attribution |
|
| 119 |
- notices within Derivative Works that You distribute, alongside |
|
| 120 |
- or as an addendum to the NOTICE text from the Work, provided |
|
| 121 |
- that such additional attribution notices cannot be construed |
|
| 122 |
- as modifying the License. |
|
| 123 |
- |
|
| 124 |
- You may add Your own copyright statement to Your modifications and |
|
| 125 |
- may provide additional or different license terms and conditions |
|
| 126 |
- for use, reproduction, or distribution of Your modifications, or |
|
| 127 |
- for any such Derivative Works as a whole, provided Your use, |
|
| 128 |
- reproduction, and distribution of the Work otherwise complies with |
|
| 129 |
- the conditions stated in this License. |
|
| 130 |
- |
|
| 131 |
- 5. Submission of Contributions. Unless You explicitly state otherwise, |
|
| 132 |
- any Contribution intentionally submitted for inclusion in the Work |
|
| 133 |
- by You to the Licensor shall be under the terms and conditions of |
|
| 134 |
- this License, without any additional terms or conditions. |
|
| 135 |
- Notwithstanding the above, nothing herein shall supersede or modify |
|
| 136 |
- the terms of any separate license agreement you may have executed |
|
| 137 |
- with Licensor regarding such Contributions. |
|
| 138 |
- |
|
| 139 |
- 6. Trademarks. This License does not grant permission to use the trade |
|
| 140 |
- names, trademarks, service marks, or product names of the Licensor, |
|
| 141 |
- except as required for reasonable and customary use in describing the |
|
| 142 |
- origin of the Work and reproducing the content of the NOTICE file. |
|
| 143 |
- |
|
| 144 |
- 7. Disclaimer of Warranty. Unless required by applicable law or |
|
| 145 |
- agreed to in writing, Licensor provides the Work (and each |
|
| 146 |
- Contributor provides its Contributions) on an "AS IS" BASIS, |
|
| 147 |
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|
| 148 |
- implied, including, without limitation, any warranties or conditions |
|
| 149 |
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|
| 150 |
- PARTICULAR PURPOSE. You are solely responsible for determining the |
|
| 151 |
- appropriateness of using or redistributing the Work and assume any |
|
| 152 |
- risks associated with Your exercise of permissions under this License. |
|
| 153 |
- |
|
| 154 |
- 8. Limitation of Liability. In no event and under no legal theory, |
|
| 155 |
- whether in tort (including negligence), contract, or otherwise, |
|
| 156 |
- unless required by applicable law (such as deliberate and grossly |
|
| 157 |
- negligent acts) or agreed to in writing, shall any Contributor be |
|
| 158 |
- liable to You for damages, including any direct, indirect, special, |
|
| 159 |
- incidental, or consequential damages of any character arising as a |
|
| 160 |
- result of this License or out of the use or inability to use the |
|
| 161 |
- Work (including but not limited to damages for loss of goodwill, |
|
| 162 |
- work stoppage, computer failure or malfunction, or any and all |
|
| 163 |
- other commercial damages or losses), even if such Contributor |
|
| 164 |
- has been advised of the possibility of such damages. |
|
| 165 |
- |
|
| 166 |
- 9. Accepting Warranty or Additional Liability. While redistributing |
|
| 167 |
- the Work or Derivative Works thereof, You may choose to offer, |
|
| 168 |
- and charge a fee for, acceptance of support, warranty, indemnity, |
|
| 169 |
- or other liability obligations and/or rights consistent with this |
|
| 170 |
- License. However, in accepting such obligations, You may act only |
|
| 171 |
- on Your own behalf and on Your sole responsibility, not on behalf |
|
| 172 |
- of any other Contributor, and only if You agree to indemnify, |
|
| 173 |
- defend, and hold each Contributor harmless for any liability |
|
| 174 |
- incurred by, or claims asserted against, such Contributor by reason |
|
| 175 |
- of your accepting any such warranty or additional liability. |
|
| 176 |
- |
|
| 177 |
- END OF TERMS AND CONDITIONS |
|
| 178 |
- |
|
| 179 |
- APPENDIX: How to apply the Apache License to your work. |
|
| 180 |
- |
|
| 181 |
- To apply the Apache License to your work, attach the following |
|
| 182 |
- boilerplate notice, with the fields enclosed by brackets "[]" |
|
| 183 |
- replaced with your own identifying information. (Don't include |
|
| 184 |
- the brackets!) The text should be enclosed in the appropriate |
|
| 185 |
- comment syntax for the file format. We also recommend that a |
|
| 186 |
- file or class name and description of purpose be included on the |
|
| 187 |
- same "printed page" as the copyright notice for easier |
|
| 188 |
- identification within third-party archives. |
|
| 189 |
- |
|
| 190 |
- Copyright [yyyy] [name of copyright owner] |
|
| 191 |
- |
|
| 192 |
- Licensed under the Apache License, Version 2.0 (the "License"); |
|
| 193 |
- you may not use this file except in compliance with the License. |
|
| 194 |
- You may obtain a copy of the License at |
|
| 195 |
- |
|
| 196 |
- http://www.apache.org/licenses/LICENSE-2.0 |
|
| 197 |
- |
|
| 198 |
- Unless required by applicable law or agreed to in writing, software |
|
| 199 |
- distributed under the License is distributed on an "AS IS" BASIS, |
|
| 200 |
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
| 201 |
- See the License for the specific language governing permissions and |
|
| 202 |
- limitations under the License. |
|
| 9 |
+Unless required by applicable law or agreed to in writing, software |
|
| 10 |
+distributed under the License is distributed on an "AS IS" BASIS, |
|
| 11 |
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
| 12 |
+See the License for the specific language governing permissions and |
|
| 13 |
+limitations under the License. |
| ... | ... |
@@ -3,7 +3,7 @@ |
| 3 | 3 |
A collection of packages to augment `testing` and support common patterns. |
| 4 | 4 |
|
| 5 | 5 |
[](https://godoc.org/gotest.tools) |
| 6 |
-[](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master) |
|
| 6 |
+[](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master) |
|
| 7 | 7 |
[](https://goreportcard.com/report/gotest.tools) |
| 8 | 8 |
|
| 9 | 9 |
|
| ... | ... |
@@ -29,3 +29,7 @@ A collection of packages to augment `testing` and support common patterns. |
| 29 | 29 |
* [gotest.tools/gotestsum](https://github.com/gotestyourself/gotestsum) - go test runner with custom output |
| 30 | 30 |
* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces |
| 31 | 31 |
* [jonboulle/clockwork](https://github.com/jonboulle/clockwork) - a fake clock for testing code that uses `time` |
| 32 |
+ |
|
| 33 |
+## Contributing |
|
| 34 |
+ |
|
| 35 |
+See [CONTRIBUTING.md](CONTRIBUTING.md). |
| ... | ... |
@@ -4,6 +4,7 @@ package cmp // import "gotest.tools/assert/cmp" |
| 4 | 4 |
import ( |
| 5 | 5 |
"fmt" |
| 6 | 6 |
"reflect" |
| 7 |
+ "regexp" |
|
| 7 | 8 |
"strings" |
| 8 | 9 |
|
| 9 | 10 |
"github.com/google/go-cmp/cmp" |
| ... | ... |
@@ -58,6 +59,39 @@ func toResult(success bool, msg string) Result {
|
| 58 | 58 |
return ResultFailure(msg) |
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 |
+// RegexOrPattern may be either a *regexp.Regexp or a string that is a valid |
|
| 62 |
+// regexp pattern. |
|
| 63 |
+type RegexOrPattern interface{}
|
|
| 64 |
+ |
|
| 65 |
+// Regexp succeeds if value v matches regular expression re. |
|
| 66 |
+// |
|
| 67 |
+// Example: |
|
| 68 |
+// assert.Assert(t, cmp.Regexp("^[0-9a-f]{32}$", str))
|
|
| 69 |
+// r := regexp.MustCompile("^[0-9a-f]{32}$")
|
|
| 70 |
+// assert.Assert(t, cmp.Regexp(r, str)) |
|
| 71 |
+func Regexp(re RegexOrPattern, v string) Comparison {
|
|
| 72 |
+ match := func(re *regexp.Regexp) Result {
|
|
| 73 |
+ return toResult( |
|
| 74 |
+ re.MatchString(v), |
|
| 75 |
+ fmt.Sprintf("value %q does not match regexp %q", v, re.String()))
|
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ return func() Result {
|
|
| 79 |
+ switch regex := re.(type) {
|
|
| 80 |
+ case *regexp.Regexp: |
|
| 81 |
+ return match(regex) |
|
| 82 |
+ case string: |
|
| 83 |
+ re, err := regexp.Compile(regex) |
|
| 84 |
+ if err != nil {
|
|
| 85 |
+ return ResultFailure(err.Error()) |
|
| 86 |
+ } |
|
| 87 |
+ return match(re) |
|
| 88 |
+ default: |
|
| 89 |
+ return ResultFailure(fmt.Sprintf("invalid type %T for regex pattern", regex))
|
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 61 | 94 |
// Equal succeeds if x == y. See assert.Equal for full documentation. |
| 62 | 95 |
func Equal(x, y interface{}) Comparison {
|
| 63 | 96 |
return func() Result {
|
| ... | ... |
@@ -186,7 +220,7 @@ func Error(err error, message string) Comparison {
|
| 186 | 186 |
return ResultFailure("expected an error, got nil")
|
| 187 | 187 |
case err.Error() != message: |
| 188 | 188 |
return ResultFailure(fmt.Sprintf( |
| 189 |
- "expected error %q, got %+v", message, err)) |
|
| 189 |
+ "expected error %q, got %s", message, formatErrorMessage(err))) |
|
| 190 | 190 |
} |
| 191 | 191 |
return ResultSuccess |
| 192 | 192 |
} |
| ... | ... |
@@ -201,12 +235,22 @@ func ErrorContains(err error, substring string) Comparison {
|
| 201 | 201 |
return ResultFailure("expected an error, got nil")
|
| 202 | 202 |
case !strings.Contains(err.Error(), substring): |
| 203 | 203 |
return ResultFailure(fmt.Sprintf( |
| 204 |
- "expected error to contain %q, got %+v", substring, err)) |
|
| 204 |
+ "expected error to contain %q, got %s", substring, formatErrorMessage(err))) |
|
| 205 | 205 |
} |
| 206 | 206 |
return ResultSuccess |
| 207 | 207 |
} |
| 208 | 208 |
} |
| 209 | 209 |
|
| 210 |
+func formatErrorMessage(err error) string {
|
|
| 211 |
+ if _, ok := err.(interface {
|
|
| 212 |
+ Cause() error |
|
| 213 |
+ }); ok {
|
|
| 214 |
+ return fmt.Sprintf("%q\n%+v", err, err)
|
|
| 215 |
+ } |
|
| 216 |
+ // This error was not wrapped with github.com/pkg/errors |
|
| 217 |
+ return fmt.Sprintf("%q", err)
|
|
| 218 |
+} |
|
| 219 |
+ |
|
| 210 | 220 |
// Nil succeeds if obj is a nil interface, pointer, or function. |
| 211 | 221 |
// |
| 212 | 222 |
// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, |
| ... | ... |
@@ -9,31 +9,37 @@ import ( |
| 9 | 9 |
"gotest.tools/internal/source" |
| 10 | 10 |
) |
| 11 | 11 |
|
| 12 |
-// Result of a Comparison. |
|
| 12 |
+// A Result of a Comparison. |
|
| 13 | 13 |
type Result interface {
|
| 14 | 14 |
Success() bool |
| 15 | 15 |
} |
| 16 | 16 |
|
| 17 |
-type result struct {
|
|
| 17 |
+// StringResult is an implementation of Result that reports the error message |
|
| 18 |
+// string verbatim and does not provide any templating or formatting of the |
|
| 19 |
+// message. |
|
| 20 |
+type StringResult struct {
|
|
| 18 | 21 |
success bool |
| 19 | 22 |
message string |
| 20 | 23 |
} |
| 21 | 24 |
|
| 22 |
-func (r result) Success() bool {
|
|
| 25 |
+// Success returns true if the comparison was successful. |
|
| 26 |
+func (r StringResult) Success() bool {
|
|
| 23 | 27 |
return r.success |
| 24 | 28 |
} |
| 25 | 29 |
|
| 26 |
-func (r result) FailureMessage() string {
|
|
| 30 |
+// FailureMessage returns the message used to provide additional information |
|
| 31 |
+// about the failure. |
|
| 32 |
+func (r StringResult) FailureMessage() string {
|
|
| 27 | 33 |
return r.message |
| 28 | 34 |
} |
| 29 | 35 |
|
| 30 | 36 |
// ResultSuccess is a constant which is returned by a ComparisonWithResult to |
| 31 | 37 |
// indicate success. |
| 32 |
-var ResultSuccess = result{success: true}
|
|
| 38 |
+var ResultSuccess = StringResult{success: true}
|
|
| 33 | 39 |
|
| 34 | 40 |
// ResultFailure returns a failed Result with a failure message. |
| 35 |
-func ResultFailure(message string) Result {
|
|
| 36 |
- return result{message: message}
|
|
| 41 |
+func ResultFailure(message string) StringResult {
|
|
| 42 |
+ return StringResult{message: message}
|
|
| 37 | 43 |
} |
| 38 | 44 |
|
| 39 | 45 |
// ResultFromError returns ResultSuccess if err is nil. Otherwise ResultFailure |
| ... | ... |
@@ -79,6 +79,9 @@ func ToMap(env []string) map[string]string {
|
| 79 | 79 |
} |
| 80 | 80 |
|
| 81 | 81 |
func getParts(raw string) (string, string) {
|
| 82 |
+ if raw == "" {
|
|
| 83 |
+ return "", "" |
|
| 84 |
+ } |
|
| 82 | 85 |
// Environment variables on windows can begin with = |
| 83 | 86 |
// http://blogs.msdn.com/b/oldnewthing/archive/2010/05/06/10008132.aspx |
| 84 | 87 |
parts := strings.SplitN(raw[1:], "=", 2) |
| ... | ... |
@@ -7,6 +7,8 @@ import ( |
| 7 | 7 |
"io/ioutil" |
| 8 | 8 |
"os" |
| 9 | 9 |
"path/filepath" |
| 10 |
+ "runtime" |
|
| 11 |
+ "strings" |
|
| 10 | 12 |
|
| 11 | 13 |
"gotest.tools/assert" |
| 12 | 14 |
"gotest.tools/x/subtest" |
| ... | ... |
@@ -40,20 +42,25 @@ func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
|
| 40 | 40 |
if ht, ok := t.(helperT); ok {
|
| 41 | 41 |
ht.Helper() |
| 42 | 42 |
} |
| 43 |
- tempfile, err := ioutil.TempFile("", prefix+"-")
|
|
| 43 |
+ tempfile, err := ioutil.TempFile("", cleanPrefix(prefix)+"-")
|
|
| 44 | 44 |
assert.NilError(t, err) |
| 45 | 45 |
file := &File{path: tempfile.Name()}
|
| 46 | 46 |
assert.NilError(t, tempfile.Close()) |
| 47 |
- |
|
| 48 |
- for _, op := range ops {
|
|
| 49 |
- assert.NilError(t, op(file)) |
|
| 50 |
- } |
|
| 47 |
+ assert.NilError(t, applyPathOps(file, ops)) |
|
| 51 | 48 |
if tc, ok := t.(subtest.TestContext); ok {
|
| 52 | 49 |
tc.AddCleanup(file.Remove) |
| 53 | 50 |
} |
| 54 | 51 |
return file |
| 55 | 52 |
} |
| 56 | 53 |
|
| 54 |
+func cleanPrefix(prefix string) string {
|
|
| 55 |
+ // windows requires both / and \ are replaced |
|
| 56 |
+ if runtime.GOOS == "windows" {
|
|
| 57 |
+ prefix = strings.Replace(prefix, string(os.PathSeparator), "-", -1) |
|
| 58 |
+ } |
|
| 59 |
+ return strings.Replace(prefix, "/", "-", -1) |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 57 | 62 |
// Path returns the full path to the file |
| 58 | 63 |
func (f *File) Path() string {
|
| 59 | 64 |
return f.path |
| ... | ... |
@@ -76,13 +83,10 @@ func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
|
| 76 | 76 |
if ht, ok := t.(helperT); ok {
|
| 77 | 77 |
ht.Helper() |
| 78 | 78 |
} |
| 79 |
- path, err := ioutil.TempDir("", prefix+"-")
|
|
| 79 |
+ path, err := ioutil.TempDir("", cleanPrefix(prefix)+"-")
|
|
| 80 | 80 |
assert.NilError(t, err) |
| 81 | 81 |
dir := &Dir{path: path}
|
| 82 |
- |
|
| 83 |
- for _, op := range ops {
|
|
| 84 |
- assert.NilError(t, op(dir)) |
|
| 85 |
- } |
|
| 82 |
+ assert.NilError(t, applyPathOps(dir, ops)) |
|
| 86 | 83 |
if tc, ok := t.(subtest.TestContext); ok {
|
| 87 | 84 |
tc.AddCleanup(dir.Remove) |
| 88 | 85 |
} |
| ... | ... |
@@ -24,7 +24,9 @@ type resource struct {
|
| 24 | 24 |
|
| 25 | 25 |
type file struct {
|
| 26 | 26 |
resource |
| 27 |
- content io.ReadCloser |
|
| 27 |
+ content io.ReadCloser |
|
| 28 |
+ ignoreCariageReturn bool |
|
| 29 |
+ compareContentFunc func(b []byte) CompareResult |
|
| 28 | 30 |
} |
| 29 | 31 |
|
| 30 | 32 |
func (f *file) Type() string {
|
| ... | ... |
@@ -42,7 +44,8 @@ func (f *symlink) Type() string {
|
| 42 | 42 |
|
| 43 | 43 |
type directory struct {
|
| 44 | 44 |
resource |
| 45 |
- items map[string]dirEntry |
|
| 45 |
+ items map[string]dirEntry |
|
| 46 |
+ filepathGlobs map[string]*filePath |
|
| 46 | 47 |
} |
| 47 | 48 |
|
| 48 | 49 |
func (f *directory) Type() string {
|
| ... | ... |
@@ -94,8 +97,9 @@ func newDirectory(path string, info os.FileInfo) (*directory, error) {
|
| 94 | 94 |
} |
| 95 | 95 |
|
| 96 | 96 |
return &directory{
|
| 97 |
- resource: newResourceFromInfo(info), |
|
| 98 |
- items: items, |
|
| 97 |
+ resource: newResourceFromInfo(info), |
|
| 98 |
+ items: items, |
|
| 99 |
+ filepathGlobs: make(map[string]*filePath), |
|
| 99 | 100 |
}, nil |
| 100 | 101 |
} |
| 101 | 102 |
|
| ... | ... |
@@ -113,6 +117,9 @@ func getTypedResource(path string, info os.FileInfo) (dirEntry, error) {
|
| 113 | 113 |
|
| 114 | 114 |
func newSymlink(path string, info os.FileInfo) (*symlink, error) {
|
| 115 | 115 |
target, err := os.Readlink(path) |
| 116 |
+ if err != nil {
|
|
| 117 |
+ return nil, err |
|
| 118 |
+ } |
|
| 116 | 119 |
return &symlink{
|
| 117 | 120 |
resource: newResourceFromInfo(info), |
| 118 | 121 |
target: target, |
| ... | ... |
@@ -122,6 +129,9 @@ func newSymlink(path string, info os.FileInfo) (*symlink, error) {
|
| 122 | 122 |
func newFile(path string, info os.FileInfo) (*file, error) {
|
| 123 | 123 |
// TODO: defer file opening to reduce number of open FDs? |
| 124 | 124 |
readCloser, err := os.Open(path) |
| 125 |
+ if err != nil {
|
|
| 126 |
+ return nil, err |
|
| 127 |
+ } |
|
| 125 | 128 |
return &file{
|
| 126 | 129 |
resource: newResourceFromInfo(info), |
| 127 | 130 |
content: readCloser, |
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"time" |
| 11 | 11 |
|
| 12 | 12 |
"github.com/pkg/errors" |
| 13 |
+ "gotest.tools/assert" |
|
| 13 | 14 |
) |
| 14 | 15 |
|
| 15 | 16 |
const defaultFileMode = 0644 |
| ... | ... |
@@ -144,6 +145,14 @@ func WithDir(name string, ops ...PathOp) PathOp {
|
| 144 | 144 |
} |
| 145 | 145 |
} |
| 146 | 146 |
|
| 147 |
+// Apply the PathOps to the File |
|
| 148 |
+func Apply(t assert.TestingT, path Path, ops ...PathOp) {
|
|
| 149 |
+ if ht, ok := t.(helperT); ok {
|
|
| 150 |
+ ht.Helper() |
|
| 151 |
+ } |
|
| 152 |
+ assert.NilError(t, applyPathOps(path, ops)) |
|
| 153 |
+} |
|
| 154 |
+ |
|
| 147 | 155 |
func applyPathOps(path Path, ops []PathOp) error {
|
| 148 | 156 |
for _, op := range ops {
|
| 149 | 157 |
if err := op(path); err != nil {
|
| ... | ... |
@@ -172,23 +181,35 @@ func copyDirectory(source, dest string) error {
|
| 172 | 172 |
for _, entry := range entries {
|
| 173 | 173 |
sourcePath := filepath.Join(source, entry.Name()) |
| 174 | 174 |
destPath := filepath.Join(dest, entry.Name()) |
| 175 |
- if entry.IsDir() {
|
|
| 175 |
+ switch {
|
|
| 176 |
+ case entry.IsDir(): |
|
| 176 | 177 |
if err := os.Mkdir(destPath, 0755); err != nil {
|
| 177 | 178 |
return err |
| 178 | 179 |
} |
| 179 | 180 |
if err := copyDirectory(sourcePath, destPath); err != nil {
|
| 180 | 181 |
return err |
| 181 | 182 |
} |
| 182 |
- continue |
|
| 183 |
- } |
|
| 184 |
- // TODO: handle symlinks |
|
| 185 |
- if err := copyFile(sourcePath, destPath); err != nil {
|
|
| 186 |
- return err |
|
| 183 |
+ case entry.Mode()&os.ModeSymlink != 0: |
|
| 184 |
+ if err := copySymLink(sourcePath, destPath); err != nil {
|
|
| 185 |
+ return err |
|
| 186 |
+ } |
|
| 187 |
+ default: |
|
| 188 |
+ if err := copyFile(sourcePath, destPath); err != nil {
|
|
| 189 |
+ return err |
|
| 190 |
+ } |
|
| 187 | 191 |
} |
| 188 | 192 |
} |
| 189 | 193 |
return nil |
| 190 | 194 |
} |
| 191 | 195 |
|
| 196 |
+func copySymLink(source, dest string) error {
|
|
| 197 |
+ link, err := os.Readlink(source) |
|
| 198 |
+ if err != nil {
|
|
| 199 |
+ return err |
|
| 200 |
+ } |
|
| 201 |
+ return os.Symlink(link, dest) |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 192 | 204 |
func copyFile(source, dest string) error {
|
| 193 | 205 |
content, err := ioutil.ReadFile(source) |
| 194 | 206 |
if err != nil {
|
| ... | ... |
@@ -219,7 +240,7 @@ func WithSymlink(path, target string) PathOp {
|
| 219 | 219 |
func WithHardlink(path, target string) PathOp {
|
| 220 | 220 |
return func(root Path) error {
|
| 221 | 221 |
if _, ok := root.(manifestDirectory); ok {
|
| 222 |
- return errors.New("WithHardlink yet implemented for manifests")
|
|
| 222 |
+ return errors.New("WithHardlink not implemented for manifests")
|
|
| 223 | 223 |
} |
| 224 | 224 |
return os.Link(filepath.Join(root.Path(), target), filepath.Join(root.Path(), path)) |
| 225 | 225 |
} |
| ... | ... |
@@ -230,7 +251,7 @@ func WithHardlink(path, target string) PathOp {
|
| 230 | 230 |
func WithTimestamps(atime, mtime time.Time) PathOp {
|
| 231 | 231 |
return func(root Path) error {
|
| 232 | 232 |
if _, ok := root.(manifestDirectory); ok {
|
| 233 |
- return errors.New("WithTimestamp yet implemented for manifests")
|
|
| 233 |
+ return errors.New("WithTimestamp not implemented for manifests")
|
|
| 234 | 234 |
} |
| 235 | 235 |
return os.Chtimes(root.Path(), atime, mtime) |
| 236 | 236 |
} |
| ... | ... |
@@ -64,6 +64,13 @@ func (p *directoryPath) AddFile(path string, ops ...PathOp) error {
|
| 64 | 64 |
return applyPathOps(exp, ops) |
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
+func (p *directoryPath) AddGlobFiles(glob string, ops ...PathOp) error {
|
|
| 68 |
+ newFile := &file{resource: newResource(0)}
|
|
| 69 |
+ newFilePath := &filePath{file: newFile}
|
|
| 70 |
+ p.directory.filepathGlobs[glob] = newFilePath |
|
| 71 |
+ return applyPathOps(newFilePath, ops) |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 67 | 74 |
func (p *directoryPath) AddDirectory(path string, ops ...PathOp) error {
|
| 68 | 75 |
newDir := newDirectoryWithDefaults() |
| 69 | 76 |
p.directory.items[path] = newDir |
| ... | ... |
@@ -87,8 +94,9 @@ func Expected(t assert.TestingT, ops ...PathOp) Manifest {
|
| 87 | 87 |
|
| 88 | 88 |
func newDirectoryWithDefaults() *directory {
|
| 89 | 89 |
return &directory{
|
| 90 |
- resource: newResource(defaultRootDirMode), |
|
| 91 |
- items: make(map[string]dirEntry), |
|
| 90 |
+ resource: newResource(defaultRootDirMode), |
|
| 91 |
+ items: make(map[string]dirEntry), |
|
| 92 |
+ filepathGlobs: make(map[string]*filePath), |
|
| 92 | 93 |
} |
| 93 | 94 |
} |
| 94 | 95 |
|
| ... | ... |
@@ -127,6 +135,15 @@ func MatchAnyFileContent(path Path) error {
|
| 127 | 127 |
return nil |
| 128 | 128 |
} |
| 129 | 129 |
|
| 130 |
+// MatchContentIgnoreCarriageReturn is a PathOp that ignores cariage return |
|
| 131 |
+// discrepancies. |
|
| 132 |
+func MatchContentIgnoreCarriageReturn(path Path) error {
|
|
| 133 |
+ if m, ok := path.(*filePath); ok {
|
|
| 134 |
+ m.file.ignoreCariageReturn = true |
|
| 135 |
+ } |
|
| 136 |
+ return nil |
|
| 137 |
+} |
|
| 138 |
+ |
|
| 130 | 139 |
const anyFile = "*" |
| 131 | 140 |
|
| 132 | 141 |
// MatchExtraFiles is a PathOp that updates a Manifest to allow a directory |
| ... | ... |
@@ -138,6 +155,37 @@ func MatchExtraFiles(path Path) error {
|
| 138 | 138 |
return nil |
| 139 | 139 |
} |
| 140 | 140 |
|
| 141 |
+// CompareResult is the result of comparison. |
|
| 142 |
+// |
|
| 143 |
+// See gotest.tools/assert/cmp.StringResult for a convenient implementation of |
|
| 144 |
+// this interface. |
|
| 145 |
+type CompareResult interface {
|
|
| 146 |
+ Success() bool |
|
| 147 |
+ FailureMessage() string |
|
| 148 |
+} |
|
| 149 |
+ |
|
| 150 |
+// MatchFileContent is a PathOp that updates a Manifest to use the provided |
|
| 151 |
+// function to determine if a file's content matches the expectation. |
|
| 152 |
+func MatchFileContent(f func([]byte) CompareResult) PathOp {
|
|
| 153 |
+ return func(path Path) error {
|
|
| 154 |
+ if m, ok := path.(*filePath); ok {
|
|
| 155 |
+ m.file.compareContentFunc = f |
|
| 156 |
+ } |
|
| 157 |
+ return nil |
|
| 158 |
+ } |
|
| 159 |
+} |
|
| 160 |
+ |
|
| 161 |
+// MatchFilesWithGlob is a PathOp that updates a Manifest to match files using |
|
| 162 |
+// glob pattern, and check them using the ops. |
|
| 163 |
+func MatchFilesWithGlob(glob string, ops ...PathOp) PathOp {
|
|
| 164 |
+ return func(path Path) error {
|
|
| 165 |
+ if m, ok := path.(*directoryPath); ok {
|
|
| 166 |
+ m.AddGlobFiles(glob, ops...) |
|
| 167 |
+ } |
|
| 168 |
+ return nil |
|
| 169 |
+ } |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 141 | 172 |
// anyFileMode is represented by uint32_max |
| 142 | 173 |
const anyFileMode os.FileMode = 4294967295 |
| 143 | 174 |
|
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"io/ioutil" |
| 7 | 7 |
"os" |
| 8 | 8 |
"path/filepath" |
| 9 |
+ "runtime" |
|
| 9 | 10 |
"sort" |
| 10 | 11 |
"strings" |
| 11 | 12 |
|
| ... | ... |
@@ -67,6 +68,11 @@ func eqResource(x, y resource) []problem {
|
| 67 | 67 |
return p |
| 68 | 68 |
} |
| 69 | 69 |
|
| 70 |
+func removeCarriageReturn(in []byte) []byte {
|
|
| 71 |
+ return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
|
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+// nolint: gocyclo |
|
| 70 | 75 |
func eqFile(x, y *file) []problem {
|
| 71 | 76 |
p := eqResource(x.resource, y.resource) |
| 72 | 77 |
|
| ... | ... |
@@ -96,6 +102,19 @@ func eqFile(x, y *file) []problem {
|
| 96 | 96 |
return p |
| 97 | 97 |
} |
| 98 | 98 |
|
| 99 |
+ if x.compareContentFunc != nil {
|
|
| 100 |
+ r := x.compareContentFunc(yContent) |
|
| 101 |
+ if !r.Success() {
|
|
| 102 |
+ p = append(p, existenceProblem("content", r.FailureMessage()))
|
|
| 103 |
+ } |
|
| 104 |
+ return p |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ if x.ignoreCariageReturn || y.ignoreCariageReturn {
|
|
| 108 |
+ xContent = removeCarriageReturn(xContent) |
|
| 109 |
+ yContent = removeCarriageReturn(yContent) |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 99 | 112 |
if !bytes.Equal(xContent, yContent) {
|
| 100 | 113 |
p = append(p, diffContent(xContent, yContent)) |
| 101 | 114 |
} |
| ... | ... |
@@ -126,7 +145,13 @@ func indent(s, prefix string) string {
|
| 126 | 126 |
|
| 127 | 127 |
func eqSymlink(x, y *symlink) []problem {
|
| 128 | 128 |
p := eqResource(x.resource, y.resource) |
| 129 |
- if x.target != y.target {
|
|
| 129 |
+ xTarget := x.target |
|
| 130 |
+ yTarget := y.target |
|
| 131 |
+ if runtime.GOOS == "windows" {
|
|
| 132 |
+ xTarget = strings.ToLower(xTarget) |
|
| 133 |
+ yTarget = strings.ToLower(yTarget) |
|
| 134 |
+ } |
|
| 135 |
+ if xTarget != yTarget {
|
|
| 130 | 136 |
p = append(p, notEqual("target", x.target, y.target))
|
| 131 | 137 |
} |
| 132 | 138 |
return p |
| ... | ... |
@@ -135,11 +160,13 @@ func eqSymlink(x, y *symlink) []problem {
|
| 135 | 135 |
func eqDirectory(path string, x, y *directory) []failure {
|
| 136 | 136 |
p := eqResource(x.resource, y.resource) |
| 137 | 137 |
var f []failure |
| 138 |
+ matchedFiles := make(map[string]bool) |
|
| 138 | 139 |
|
| 139 | 140 |
for _, name := range sortedKeys(x.items) {
|
| 140 | 141 |
if name == anyFile {
|
| 141 | 142 |
continue |
| 142 | 143 |
} |
| 144 |
+ matchedFiles[name] = true |
|
| 143 | 145 |
xEntry := x.items[name] |
| 144 | 146 |
yEntry, ok := y.items[name] |
| 145 | 147 |
if !ok {
|
| ... | ... |
@@ -155,19 +182,30 @@ func eqDirectory(path string, x, y *directory) []failure {
|
| 155 | 155 |
f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...) |
| 156 | 156 |
} |
| 157 | 157 |
|
| 158 |
- if _, ok := x.items[anyFile]; !ok {
|
|
| 158 |
+ if len(x.filepathGlobs) != 0 {
|
|
| 159 | 159 |
for _, name := range sortedKeys(y.items) {
|
| 160 |
- if _, ok := x.items[name]; !ok {
|
|
| 161 |
- yEntry := y.items[name] |
|
| 162 |
- p = append(p, existenceProblem(name, "unexpected %s", yEntry.Type())) |
|
| 163 |
- } |
|
| 160 |
+ m := matchGlob(name, y.items[name], x.filepathGlobs) |
|
| 161 |
+ matchedFiles[name] = m.match |
|
| 162 |
+ f = append(f, m.failures...) |
|
| 164 | 163 |
} |
| 165 | 164 |
} |
| 166 | 165 |
|
| 167 |
- if len(p) > 0 {
|
|
| 168 |
- f = append(f, failure{path: path, problems: p})
|
|
| 166 |
+ if _, ok := x.items[anyFile]; ok {
|
|
| 167 |
+ return maybeAppendFailure(f, path, p) |
|
| 169 | 168 |
} |
| 170 |
- return f |
|
| 169 |
+ for _, name := range sortedKeys(y.items) {
|
|
| 170 |
+ if !matchedFiles[name] {
|
|
| 171 |
+ p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type())) |
|
| 172 |
+ } |
|
| 173 |
+ } |
|
| 174 |
+ return maybeAppendFailure(f, path, p) |
|
| 175 |
+} |
|
| 176 |
+ |
|
| 177 |
+func maybeAppendFailure(failures []failure, path string, problems []problem) []failure {
|
|
| 178 |
+ if len(problems) > 0 {
|
|
| 179 |
+ return append(failures, failure{path: path, problems: problems})
|
|
| 180 |
+ } |
|
| 181 |
+ return failures |
|
| 171 | 182 |
} |
| 172 | 183 |
|
| 173 | 184 |
func sortedKeys(items map[string]dirEntry) []string {
|
| ... | ... |
@@ -199,6 +237,30 @@ func eqEntry(path string, x, y dirEntry) []failure {
|
| 199 | 199 |
return nil |
| 200 | 200 |
} |
| 201 | 201 |
|
| 202 |
+type globMatch struct {
|
|
| 203 |
+ match bool |
|
| 204 |
+ failures []failure |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch {
|
|
| 208 |
+ m := globMatch{}
|
|
| 209 |
+ |
|
| 210 |
+ for glob, expectedFile := range globs {
|
|
| 211 |
+ ok, err := filepath.Match(glob, name) |
|
| 212 |
+ if err != nil {
|
|
| 213 |
+ p := errProblem("failed to match glob pattern", err)
|
|
| 214 |
+ f := failure{path: name, problems: []problem{p}}
|
|
| 215 |
+ m.failures = append(m.failures, f) |
|
| 216 |
+ } |
|
| 217 |
+ if ok {
|
|
| 218 |
+ m.match = true |
|
| 219 |
+ m.failures = eqEntry(name, expectedFile.file, yEntry) |
|
| 220 |
+ return m |
|
| 221 |
+ } |
|
| 222 |
+ } |
|
| 223 |
+ return m |
|
| 224 |
+} |
|
| 225 |
+ |
|
| 202 | 226 |
func formatFailures(failures []failure) string {
|
| 203 | 227 |
sort.Slice(failures, func(i, j int) bool {
|
| 204 | 228 |
return failures[i].path < failures[j].path |
| ... | ... |
@@ -132,18 +132,21 @@ func (r *Result) String() string {
|
| 132 | 132 |
if r.Timeout {
|
| 133 | 133 |
timeout = " (timeout)" |
| 134 | 134 |
} |
| 135 |
+ var errString string |
|
| 136 |
+ if r.Error != nil {
|
|
| 137 |
+ errString = "\nError: " + r.Error.Error() |
|
| 138 |
+ } |
|
| 135 | 139 |
|
| 136 | 140 |
return fmt.Sprintf(` |
| 137 | 141 |
Command: %s |
| 138 |
-ExitCode: %d%s |
|
| 139 |
-Error: %v |
|
| 142 |
+ExitCode: %d%s%s |
|
| 140 | 143 |
Stdout: %v |
| 141 | 144 |
Stderr: %v |
| 142 | 145 |
`, |
| 143 | 146 |
strings.Join(r.Cmd.Args, " "), |
| 144 | 147 |
r.ExitCode, |
| 145 | 148 |
timeout, |
| 146 |
- r.Error, |
|
| 149 |
+ errString, |
|
| 147 | 150 |
r.Stdout(), |
| 148 | 151 |
r.Stderr()) |
| 149 | 152 |
} |
| ... | ... |
@@ -1,4 +1,38 @@ |
| 1 | 1 |
package icmd |
| 2 | 2 |
|
| 3 |
+import ( |
|
| 4 |
+ "io" |
|
| 5 |
+ "time" |
|
| 6 |
+) |
|
| 7 |
+ |
|
| 3 | 8 |
// CmdOp is an operation which modified a Cmd structure used to execute commands |
| 4 | 9 |
type CmdOp func(*Cmd) |
| 10 |
+ |
|
| 11 |
+// WithTimeout sets the timeout duration of the command |
|
| 12 |
+func WithTimeout(timeout time.Duration) CmdOp {
|
|
| 13 |
+ return func(c *Cmd) {
|
|
| 14 |
+ c.Timeout = timeout |
|
| 15 |
+ } |
|
| 16 |
+} |
|
| 17 |
+ |
|
| 18 |
+// WithEnv sets the environment variable of the command. |
|
| 19 |
+// Each arguments are in the form of KEY=VALUE |
|
| 20 |
+func WithEnv(env ...string) CmdOp {
|
|
| 21 |
+ return func(c *Cmd) {
|
|
| 22 |
+ c.Env = env |
|
| 23 |
+ } |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// Dir sets the working directory of the command |
|
| 27 |
+func Dir(path string) CmdOp {
|
|
| 28 |
+ return func(c *Cmd) {
|
|
| 29 |
+ c.Dir = path |
|
| 30 |
+ } |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+// WithStdin sets the standard input of the command to the specified reader |
|
| 34 |
+func WithStdin(r io.Reader) CmdOp {
|
|
| 35 |
+ return func(c *Cmd) {
|
|
| 36 |
+ c.Stdin = r |
|
| 37 |
+ } |
|
| 38 |
+} |
| ... | ... |
@@ -1,4 +1,4 @@ |
| 1 |
-/* Package difflib is a partial port of Python difflib module. |
|
| 1 |
+/*Package difflib is a partial port of Python difflib module. |
|
| 2 | 2 |
|
| 3 | 3 |
Original source: https://github.com/pmezard/go-difflib |
| 4 | 4 |
|
| ... | ... |
@@ -20,12 +20,14 @@ func max(a, b int) int {
|
| 20 | 20 |
return b |
| 21 | 21 |
} |
| 22 | 22 |
|
| 23 |
+// Match stores line numbers of size of match |
|
| 23 | 24 |
type Match struct {
|
| 24 | 25 |
A int |
| 25 | 26 |
B int |
| 26 | 27 |
Size int |
| 27 | 28 |
} |
| 28 | 29 |
|
| 30 |
+// OpCode identifies the type of diff |
|
| 29 | 31 |
type OpCode struct {
|
| 30 | 32 |
Tag byte |
| 31 | 33 |
I1 int |
| ... | ... |
@@ -73,19 +75,20 @@ type SequenceMatcher struct {
|
| 73 | 73 |
opCodes []OpCode |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
+// NewMatcher returns a new SequenceMatcher |
|
| 76 | 77 |
func NewMatcher(a, b []string) *SequenceMatcher {
|
| 77 | 78 |
m := SequenceMatcher{autoJunk: true}
|
| 78 | 79 |
m.SetSeqs(a, b) |
| 79 | 80 |
return &m |
| 80 | 81 |
} |
| 81 | 82 |
|
| 82 |
-// Set two sequences to be compared. |
|
| 83 |
+// SetSeqs sets two sequences to be compared. |
|
| 83 | 84 |
func (m *SequenceMatcher) SetSeqs(a, b []string) {
|
| 84 | 85 |
m.SetSeq1(a) |
| 85 | 86 |
m.SetSeq2(b) |
| 86 | 87 |
} |
| 87 | 88 |
|
| 88 |
-// Set the first sequence to be compared. The second sequence to be compared is |
|
| 89 |
+// SetSeq1 sets the first sequence to be compared. The second sequence to be compared is |
|
| 89 | 90 |
// not changed. |
| 90 | 91 |
// |
| 91 | 92 |
// SequenceMatcher computes and caches detailed information about the second |
| ... | ... |
@@ -103,7 +106,7 @@ func (m *SequenceMatcher) SetSeq1(a []string) {
|
| 103 | 103 |
m.opCodes = nil |
| 104 | 104 |
} |
| 105 | 105 |
|
| 106 |
-// Set the second sequence to be compared. The first sequence to be compared is |
|
| 106 |
+// SetSeq2 sets the second sequence to be compared. The first sequence to be compared is |
|
| 107 | 107 |
// not changed. |
| 108 | 108 |
func (m *SequenceMatcher) SetSeq2(b []string) {
|
| 109 | 109 |
if &b == &m.b {
|
| ... | ... |
@@ -129,12 +132,12 @@ func (m *SequenceMatcher) chainB() {
|
| 129 | 129 |
m.bJunk = map[string]struct{}{}
|
| 130 | 130 |
if m.IsJunk != nil {
|
| 131 | 131 |
junk := m.bJunk |
| 132 |
- for s, _ := range b2j {
|
|
| 132 |
+ for s := range b2j {
|
|
| 133 | 133 |
if m.IsJunk(s) {
|
| 134 | 134 |
junk[s] = struct{}{}
|
| 135 | 135 |
} |
| 136 | 136 |
} |
| 137 |
- for s, _ := range junk {
|
|
| 137 |
+ for s := range junk {
|
|
| 138 | 138 |
delete(b2j, s) |
| 139 | 139 |
} |
| 140 | 140 |
} |
| ... | ... |
@@ -149,7 +152,7 @@ func (m *SequenceMatcher) chainB() {
|
| 149 | 149 |
popular[s] = struct{}{}
|
| 150 | 150 |
} |
| 151 | 151 |
} |
| 152 |
- for s, _ := range popular {
|
|
| 152 |
+ for s := range popular {
|
|
| 153 | 153 |
delete(b2j, s) |
| 154 | 154 |
} |
| 155 | 155 |
} |
| ... | ... |
@@ -259,7 +262,7 @@ func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
|
| 259 | 259 |
return Match{A: besti, B: bestj, Size: bestsize}
|
| 260 | 260 |
} |
| 261 | 261 |
|
| 262 |
-// Return list of triples describing matching subsequences. |
|
| 262 |
+// GetMatchingBlocks returns a list of triples describing matching subsequences. |
|
| 263 | 263 |
// |
| 264 | 264 |
// Each triple is of the form (i, j, n), and means that |
| 265 | 265 |
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in |
| ... | ... |
@@ -323,7 +326,7 @@ func (m *SequenceMatcher) GetMatchingBlocks() []Match {
|
| 323 | 323 |
return m.matchingBlocks |
| 324 | 324 |
} |
| 325 | 325 |
|
| 326 |
-// Return list of 5-tuples describing how to turn a into b. |
|
| 326 |
+// GetOpCodes returns a list of 5-tuples describing how to turn a into b. |
|
| 327 | 327 |
// |
| 328 | 328 |
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple |
| 329 | 329 |
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the |
| ... | ... |
@@ -374,7 +377,7 @@ func (m *SequenceMatcher) GetOpCodes() []OpCode {
|
| 374 | 374 |
return m.opCodes |
| 375 | 375 |
} |
| 376 | 376 |
|
| 377 |
-// Isolate change clusters by eliminating ranges with no changes. |
|
| 377 |
+// GetGroupedOpCodes isolates change clusters by eliminating ranges with no changes. |
|
| 378 | 378 |
// |
| 379 | 379 |
// Return a generator of groups with up to n lines of context. |
| 380 | 380 |
// Each group is in the same format as returned by GetOpCodes(). |
| ... | ... |
@@ -384,7 +387,7 @@ func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
|
| 384 | 384 |
} |
| 385 | 385 |
codes := m.GetOpCodes() |
| 386 | 386 |
if len(codes) == 0 {
|
| 387 |
- codes = []OpCode{OpCode{'e', 0, 1, 0, 1}}
|
|
| 387 |
+ codes = []OpCode{{'e', 0, 1, 0, 1}}
|
|
| 388 | 388 |
} |
| 389 | 389 |
// Fixup leading and trailing groups if they show no changes. |
| 390 | 390 |
if codes[0].Tag == 'e' {
|
| 391 | 391 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,53 @@ |
| 0 |
+package source |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "go/ast" |
|
| 4 |
+ "go/token" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/pkg/errors" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func scanToDeferLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
|
|
| 10 |
+ var matchedNode ast.Node |
|
| 11 |
+ ast.Inspect(node, func(node ast.Node) bool {
|
|
| 12 |
+ switch {
|
|
| 13 |
+ case node == nil || matchedNode != nil: |
|
| 14 |
+ return false |
|
| 15 |
+ case fileset.Position(node.End()).Line == lineNum: |
|
| 16 |
+ if funcLit, ok := node.(*ast.FuncLit); ok {
|
|
| 17 |
+ matchedNode = funcLit |
|
| 18 |
+ return false |
|
| 19 |
+ } |
|
| 20 |
+ } |
|
| 21 |
+ return true |
|
| 22 |
+ }) |
|
| 23 |
+ debug("defer line node: %s", debugFormatNode{matchedNode})
|
|
| 24 |
+ return matchedNode |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func guessDefer(node ast.Node) (ast.Node, error) {
|
|
| 28 |
+ defers := collectDefers(node) |
|
| 29 |
+ switch len(defers) {
|
|
| 30 |
+ case 0: |
|
| 31 |
+ return nil, errors.New("failed to expression in defer")
|
|
| 32 |
+ case 1: |
|
| 33 |
+ return defers[0].Call, nil |
|
| 34 |
+ default: |
|
| 35 |
+ return nil, errors.Errorf( |
|
| 36 |
+ "ambiguous call expression: multiple (%d) defers in call block", |
|
| 37 |
+ len(defers)) |
|
| 38 |
+ } |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+func collectDefers(node ast.Node) []*ast.DeferStmt {
|
|
| 42 |
+ var defers []*ast.DeferStmt |
|
| 43 |
+ ast.Inspect(node, func(node ast.Node) bool {
|
|
| 44 |
+ if d, ok := node.(*ast.DeferStmt); ok {
|
|
| 45 |
+ defers = append(defers, d) |
|
| 46 |
+ debug("defer: %s", debugFormatNode{d})
|
|
| 47 |
+ return false |
|
| 48 |
+ } |
|
| 49 |
+ return true |
|
| 50 |
+ }) |
|
| 51 |
+ return defers |
|
| 52 |
+} |
| ... | ... |
@@ -24,9 +24,30 @@ func FormattedCallExprArg(stackIndex int, argPos int) (string, error) {
|
| 24 | 24 |
if err != nil {
|
| 25 | 25 |
return "", err |
| 26 | 26 |
} |
| 27 |
+ if argPos >= len(args) {
|
|
| 28 |
+ return "", errors.New("failed to find expression")
|
|
| 29 |
+ } |
|
| 27 | 30 |
return FormatNode(args[argPos]) |
| 28 | 31 |
} |
| 29 | 32 |
|
| 33 |
+// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at |
|
| 34 |
+// the index in the call stack. |
|
| 35 |
+func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
|
|
| 36 |
+ _, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex) |
|
| 37 |
+ if !ok {
|
|
| 38 |
+ return nil, errors.New("failed to get call stack")
|
|
| 39 |
+ } |
|
| 40 |
+ debug("call stack position: %s:%d", filename, lineNum)
|
|
| 41 |
+ |
|
| 42 |
+ node, err := getNodeAtLine(filename, lineNum) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return nil, err |
|
| 45 |
+ } |
|
| 46 |
+ debug("found node: %s", debugFormatNode{node})
|
|
| 47 |
+ |
|
| 48 |
+ return getCallExprArgs(node) |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 30 | 51 |
func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
|
| 31 | 52 |
fileset := token.NewFileSet() |
| 32 | 53 |
astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) |
| ... | ... |
@@ -34,49 +55,44 @@ func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
|
| 34 | 34 |
return nil, errors.Wrapf(err, "failed to parse source file: %s", filename) |
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 |
- node := scanToLine(fileset, astFile, lineNum) |
|
| 38 |
- if node == nil {
|
|
| 39 |
- return nil, errors.Errorf( |
|
| 40 |
- "failed to find an expression on line %d in %s", lineNum, filename) |
|
| 37 |
+ if node := scanToLine(fileset, astFile, lineNum); node != nil {
|
|
| 38 |
+ return node, nil |
|
| 41 | 39 |
} |
| 42 |
- return node, nil |
|
| 40 |
+ if node := scanToDeferLine(fileset, astFile, lineNum); node != nil {
|
|
| 41 |
+ node, err := guessDefer(node) |
|
| 42 |
+ if err != nil || node != nil {
|
|
| 43 |
+ return node, err |
|
| 44 |
+ } |
|
| 45 |
+ } |
|
| 46 |
+ return nil, errors.Errorf( |
|
| 47 |
+ "failed to find an expression on line %d in %s", lineNum, filename) |
|
| 43 | 48 |
} |
| 44 | 49 |
|
| 45 | 50 |
func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
|
| 46 |
- v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset}
|
|
| 47 |
- ast.Walk(v, node) |
|
| 48 |
- return v.matchedNode |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 |
-type scanToLineVisitor struct {
|
|
| 52 |
- lineNum int |
|
| 53 |
- matchedNode ast.Node |
|
| 54 |
- fileset *token.FileSet |
|
| 55 |
-} |
|
| 56 |
- |
|
| 57 |
-func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor {
|
|
| 58 |
- if node == nil || v.matchedNode != nil {
|
|
| 59 |
- return nil |
|
| 60 |
- } |
|
| 61 |
- if v.nodePosition(node).Line == v.lineNum {
|
|
| 62 |
- v.matchedNode = node |
|
| 63 |
- return nil |
|
| 64 |
- } |
|
| 65 |
- return v |
|
| 51 |
+ var matchedNode ast.Node |
|
| 52 |
+ ast.Inspect(node, func(node ast.Node) bool {
|
|
| 53 |
+ switch {
|
|
| 54 |
+ case node == nil || matchedNode != nil: |
|
| 55 |
+ return false |
|
| 56 |
+ case nodePosition(fileset, node).Line == lineNum: |
|
| 57 |
+ matchedNode = node |
|
| 58 |
+ return false |
|
| 59 |
+ } |
|
| 60 |
+ return true |
|
| 61 |
+ }) |
|
| 62 |
+ return matchedNode |
|
| 66 | 63 |
} |
| 67 | 64 |
|
| 68 | 65 |
// In golang 1.9 the line number changed from being the line where the statement |
| 69 | 66 |
// ended to the line where the statement began. |
| 70 |
-func (v *scanToLineVisitor) nodePosition(node ast.Node) token.Position {
|
|
| 67 |
+func nodePosition(fileset *token.FileSet, node ast.Node) token.Position {
|
|
| 71 | 68 |
if goVersionBefore19 {
|
| 72 |
- return v.fileset.Position(node.End()) |
|
| 69 |
+ return fileset.Position(node.End()) |
|
| 73 | 70 |
} |
| 74 |
- return v.fileset.Position(node.Pos()) |
|
| 71 |
+ return fileset.Position(node.Pos()) |
|
| 75 | 72 |
} |
| 76 | 73 |
|
| 77 |
-var goVersionBefore19 = isGOVersionBefore19() |
|
| 78 |
- |
|
| 79 |
-func isGOVersionBefore19() bool {
|
|
| 74 |
+var goVersionBefore19 = func() bool {
|
|
| 80 | 75 |
version := runtime.Version() |
| 81 | 76 |
// not a release version |
| 82 | 77 |
if !strings.HasPrefix(version, "go") {
|
| ... | ... |
@@ -89,7 +105,7 @@ func isGOVersionBefore19() bool {
|
| 89 | 89 |
} |
| 90 | 90 |
minor, err := strconv.ParseInt(parts[1], 10, 32) |
| 91 | 91 |
return err == nil && parts[0] == "1" && minor < 9 |
| 92 |
-} |
|
| 92 |
+}() |
|
| 93 | 93 |
|
| 94 | 94 |
func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
|
| 95 | 95 |
visitor := &callExprVisitor{}
|
| ... | ... |
@@ -97,6 +113,7 @@ func getCallExprArgs(node ast.Node) ([]ast.Expr, error) {
|
| 97 | 97 |
if visitor.expr == nil {
|
| 98 | 98 |
return nil, errors.New("failed to find call expression")
|
| 99 | 99 |
} |
| 100 |
+ debug("callExpr: %s", debugFormatNode{visitor.expr})
|
|
| 100 | 101 |
return visitor.expr.Args, nil |
| 101 | 102 |
} |
| 102 | 103 |
|
| ... | ... |
@@ -108,10 +125,14 @@ func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
|
| 108 | 108 |
if v.expr != nil || node == nil {
|
| 109 | 109 |
return nil |
| 110 | 110 |
} |
| 111 |
- debug("visit (%T): %s", node, debugFormatNode{node})
|
|
| 111 |
+ debug("visit: %s", debugFormatNode{node})
|
|
| 112 | 112 |
|
| 113 |
- if callExpr, ok := node.(*ast.CallExpr); ok {
|
|
| 114 |
- v.expr = callExpr |
|
| 113 |
+ switch typed := node.(type) {
|
|
| 114 |
+ case *ast.CallExpr: |
|
| 115 |
+ v.expr = typed |
|
| 116 |
+ return nil |
|
| 117 |
+ case *ast.DeferStmt: |
|
| 118 |
+ ast.Walk(v, typed.Call.Fun) |
|
| 115 | 119 |
return nil |
| 116 | 120 |
} |
| 117 | 121 |
return v |
| ... | ... |
@@ -124,25 +145,7 @@ func FormatNode(node ast.Node) (string, error) {
|
| 124 | 124 |
return buf.String(), err |
| 125 | 125 |
} |
| 126 | 126 |
|
| 127 |
-// CallExprArgs returns the ast.Expr slice for the args of an ast.CallExpr at |
|
| 128 |
-// the index in the call stack. |
|
| 129 |
-func CallExprArgs(stackIndex int) ([]ast.Expr, error) {
|
|
| 130 |
- _, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex) |
|
| 131 |
- if !ok {
|
|
| 132 |
- return nil, errors.New("failed to get call stack")
|
|
| 133 |
- } |
|
| 134 |
- debug("call stack position: %s:%d", filename, lineNum)
|
|
| 135 |
- |
|
| 136 |
- node, err := getNodeAtLine(filename, lineNum) |
|
| 137 |
- if err != nil {
|
|
| 138 |
- return nil, err |
|
| 139 |
- } |
|
| 140 |
- debug("found node (%T): %s", node, debugFormatNode{node})
|
|
| 141 |
- |
|
| 142 |
- return getCallExprArgs(node) |
|
| 143 |
-} |
|
| 144 |
- |
|
| 145 |
-var debugEnabled = os.Getenv("GOTESTYOURSELF_DEBUG") != ""
|
|
| 127 |
+var debugEnabled = os.Getenv("GOTESTTOOLS_DEBUG") != ""
|
|
| 146 | 128 |
|
| 147 | 129 |
func debug(format string, args ...interface{}) {
|
| 148 | 130 |
if debugEnabled {
|
| ... | ... |
@@ -159,5 +162,5 @@ func (n debugFormatNode) String() string {
|
| 159 | 159 |
if err != nil {
|
| 160 | 160 |
return fmt.Sprintf("failed to format %s: %s", n.Node, err)
|
| 161 | 161 |
} |
| 162 |
- return out |
|
| 162 |
+ return fmt.Sprintf("(%T) %s", n.Node, out)
|
|
| 163 | 163 |
} |
| 164 | 164 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,39 @@ |
| 0 |
+package poll |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "net" |
|
| 4 |
+ "os" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// Check is a function which will be used as check for the WaitOn method. |
|
| 8 |
+type Check func(t LogT) Result |
|
| 9 |
+ |
|
| 10 |
+// FileExists looks on filesystem and check that path exists. |
|
| 11 |
+func FileExists(path string) Check {
|
|
| 12 |
+ return func(t LogT) Result {
|
|
| 13 |
+ _, err := os.Stat(path) |
|
| 14 |
+ if os.IsNotExist(err) {
|
|
| 15 |
+ t.Logf("waiting on file %s to exist", path)
|
|
| 16 |
+ return Continue("file %s does not exist", path)
|
|
| 17 |
+ } |
|
| 18 |
+ if err != nil {
|
|
| 19 |
+ return Error(err) |
|
| 20 |
+ } |
|
| 21 |
+ |
|
| 22 |
+ return Success() |
|
| 23 |
+ } |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// Connection try to open a connection to the address on the |
|
| 27 |
+// named network. See net.Dial for a description of the network and |
|
| 28 |
+// address parameters. |
|
| 29 |
+func Connection(network, address string) Check {
|
|
| 30 |
+ return func(t LogT) Result {
|
|
| 31 |
+ _, err := net.Dial(network, address) |
|
| 32 |
+ if err != nil {
|
|
| 33 |
+ t.Logf("waiting on socket %s://%s to be available...", network, address)
|
|
| 34 |
+ return Continue("socket %s://%s not available", network, address)
|
|
| 35 |
+ } |
|
| 36 |
+ return Success() |
|
| 37 |
+ } |
|
| 38 |
+} |
| ... | ... |
@@ -104,7 +104,7 @@ func Error(err error) Result {
|
| 104 | 104 |
// WaitOn a condition or until a timeout. Poll by calling check and exit when |
| 105 | 105 |
// check returns a done Result. To fail a test and exit polling with an error |
| 106 | 106 |
// return a error result. |
| 107 |
-func WaitOn(t TestingT, check func(t LogT) Result, pollOps ...SettingOp) {
|
|
| 107 |
+func WaitOn(t TestingT, check Check, pollOps ...SettingOp) {
|
|
| 108 | 108 |
if ht, ok := t.(helperT); ok {
|
| 109 | 109 |
ht.Helper() |
| 110 | 110 |
} |
| ... | ... |
@@ -19,17 +19,29 @@ type skipT interface {
|
| 19 | 19 |
Log(args ...interface{})
|
| 20 | 20 |
} |
| 21 | 21 |
|
| 22 |
+// Result of skip function |
|
| 23 |
+type Result interface {
|
|
| 24 |
+ Skip() bool |
|
| 25 |
+ Message() string |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 22 | 28 |
type helperT interface {
|
| 23 | 29 |
Helper() |
| 24 | 30 |
} |
| 25 | 31 |
|
| 26 |
-// BoolOrCheckFunc can be a bool or func() bool, other types will panic |
|
| 32 |
+// BoolOrCheckFunc can be a bool, func() bool, or func() Result. Other types will panic |
|
| 27 | 33 |
type BoolOrCheckFunc interface{}
|
| 28 | 34 |
|
| 29 |
-// If the condition expression evaluates to true, or the condition function returns |
|
| 30 |
-// true, skip the test. |
|
| 35 |
+// If the condition expression evaluates to true, skip the test. |
|
| 36 |
+// |
|
| 37 |
+// The condition argument may be one of three types: bool, func() bool, or |
|
| 38 |
+// func() SkipResult. |
|
| 39 |
+// When called with a bool, the test will be skip if the condition evaluates to true. |
|
| 40 |
+// When called with a func() bool, the test will be skip if the function returns true. |
|
| 41 |
+// When called with a func() Result, the test will be skip if the Skip method |
|
| 42 |
+// of the result returns true. |
|
| 31 | 43 |
// The skip message will contain the source code of the expression. |
| 32 |
-// Extra message text can be passed as a format string with args |
|
| 44 |
+// Extra message text can be passed as a format string with args. |
|
| 33 | 45 |
func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) {
|
| 34 | 46 |
if ht, ok := t.(helperT); ok {
|
| 35 | 47 |
ht.Helper() |
| ... | ... |
@@ -41,12 +53,18 @@ func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) {
|
| 41 | 41 |
if check() {
|
| 42 | 42 |
t.Skip(format.WithCustomMessage(getFunctionName(check), msgAndArgs...)) |
| 43 | 43 |
} |
| 44 |
+ case func() Result: |
|
| 45 |
+ result := check() |
|
| 46 |
+ if result.Skip() {
|
|
| 47 |
+ msg := getFunctionName(check) + ": " + result.Message() |
|
| 48 |
+ t.Skip(format.WithCustomMessage(msg, msgAndArgs...)) |
|
| 49 |
+ } |
|
| 44 | 50 |
default: |
| 45 | 51 |
panic(fmt.Sprintf("invalid type for condition arg: %T", check))
|
| 46 | 52 |
} |
| 47 | 53 |
} |
| 48 | 54 |
|
| 49 |
-func getFunctionName(function func() bool) string {
|
|
| 55 |
+func getFunctionName(function interface{}) string {
|
|
| 50 | 56 |
funcPath := runtime.FuncForPC(reflect.ValueOf(function).Pointer()).Name() |
| 51 | 57 |
return strings.SplitN(path.Base(funcPath), ".", 2)[1] |
| 52 | 58 |
} |