Browse code

bump gotest.tools v2.3.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2019/04/06 00:02:23
Showing 20 changed files
... ...
@@ -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
 [![GoDoc](https://godoc.org/gotest.tools?status.svg)](https://godoc.org/gotest.tools)
6
-[![CircleCI](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotestyourself/tree/master)
6
+[![CircleCI](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master.svg?style=shield)](https://circleci.com/gh/gotestyourself/gotest.tools/tree/master)
7 7
 [![Go Reportcard](https://goreportcard.com/badge/gotest.tools)](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
... ...
@@ -70,7 +70,6 @@ func filterPrintableExpr(args []ast.Expr) []ast.Expr {
70 70
 			result[i] = starExpr.X
71 71
 			continue
72 72
 		}
73
-		result[i] = nil
74 73
 	}
75 74
 	return result
76 75
 }
... ...
@@ -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
 }