| ... | ... |
@@ -20,7 +20,8 @@ github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d |
| 20 | 20 |
golang.org/x/text f72d8390a633d5dfb0cc84043294db9f6c935756 |
| 21 | 21 |
github.com/stretchr/testify 4d4bfba8f1d1027c4fdbe371823030df51419987 |
| 22 | 22 |
github.com/pmezard/go-difflib v1.0.0 |
| 23 |
-github.com/gotestyourself/gotestyourself v1.1.0 |
|
| 23 |
+github.com/gotestyourself/gotestyourself 511344eed30e4384f010579a593dfb442033a692 |
|
| 24 |
+github.com/google/go-cmp v0.1.0 |
|
| 24 | 25 |
|
| 25 | 26 |
github.com/RackSec/srslog 456df3a81436d29ba874f3590eeeee25d666f8a5 |
| 26 | 27 |
github.com/imdario/mergo 0.2.1 |
| 27 | 28 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+Copyright (c) 2017 The Go Authors. All rights reserved. |
|
| 1 |
+ |
|
| 2 |
+Redistribution and use in source and binary forms, with or without |
|
| 3 |
+modification, are permitted provided that the following conditions are |
|
| 4 |
+met: |
|
| 5 |
+ |
|
| 6 |
+ * Redistributions of source code must retain the above copyright |
|
| 7 |
+notice, this list of conditions and the following disclaimer. |
|
| 8 |
+ * Redistributions in binary form must reproduce the above |
|
| 9 |
+copyright notice, this list of conditions and the following disclaimer |
|
| 10 |
+in the documentation and/or other materials provided with the |
|
| 11 |
+distribution. |
|
| 12 |
+ * Neither the name of Google Inc. nor the names of its |
|
| 13 |
+contributors may be used to endorse or promote products derived from |
|
| 14 |
+this software without specific prior written permission. |
|
| 15 |
+ |
|
| 16 |
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
| 17 |
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
| 18 |
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|
| 19 |
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
| 20 |
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
| 21 |
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
| 22 |
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
| 23 |
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
| 24 |
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
| 25 |
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|
| 26 |
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 0 | 27 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,44 @@ |
| 0 |
+# Package for equality of Go values |
|
| 1 |
+ |
|
| 2 |
+[][godoc] |
|
| 3 |
+[][travis] |
|
| 4 |
+ |
|
| 5 |
+This package is intended to be a more powerful and safer alternative to |
|
| 6 |
+`reflect.DeepEqual` for comparing whether two values are semantically equal. |
|
| 7 |
+ |
|
| 8 |
+The primary features of `cmp` are: |
|
| 9 |
+ |
|
| 10 |
+* When the default behavior of equality does not suit the needs of the test, |
|
| 11 |
+ custom equality functions can override the equality operation. |
|
| 12 |
+ For example, an equality function may report floats as equal so long as they |
|
| 13 |
+ are within some tolerance of each other. |
|
| 14 |
+ |
|
| 15 |
+* Types that have an `Equal` method may use that method to determine equality. |
|
| 16 |
+ This allows package authors to determine the equality operation for the types |
|
| 17 |
+ that they define. |
|
| 18 |
+ |
|
| 19 |
+* If no custom equality functions are used and no `Equal` method is defined, |
|
| 20 |
+ equality is determined by recursively comparing the primitive kinds on both |
|
| 21 |
+ values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported |
|
| 22 |
+ fields are not compared by default; they result in panics unless suppressed |
|
| 23 |
+ by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explictly |
|
| 24 |
+ compared using the `AllowUnexported` option. |
|
| 25 |
+ |
|
| 26 |
+See the [GoDoc documentation][godoc] for more information. |
|
| 27 |
+ |
|
| 28 |
+This is not an official Google product. |
|
| 29 |
+ |
|
| 30 |
+[godoc]: https://godoc.org/github.com/google/go-cmp/cmp |
|
| 31 |
+[travis]: https://travis-ci.org/google/go-cmp |
|
| 32 |
+ |
|
| 33 |
+## Install |
|
| 34 |
+ |
|
| 35 |
+``` |
|
| 36 |
+go get -u github.com/google/go-cmp/cmp |
|
| 37 |
+``` |
|
| 38 |
+ |
|
| 39 |
+## License |
|
| 40 |
+ |
|
| 41 |
+BSD - See [LICENSE][license] file |
|
| 42 |
+ |
|
| 43 |
+[license]: https://github.com/google/go-cmp/blob/master/LICENSE |
| 0 | 44 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,529 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// Package cmp determines equality of values. |
|
| 5 |
+// |
|
| 6 |
+// This package is intended to be a more powerful and safer alternative to |
|
| 7 |
+// reflect.DeepEqual for comparing whether two values are semantically equal. |
|
| 8 |
+// |
|
| 9 |
+// The primary features of cmp are: |
|
| 10 |
+// |
|
| 11 |
+// • When the default behavior of equality does not suit the needs of the test, |
|
| 12 |
+// custom equality functions can override the equality operation. |
|
| 13 |
+// For example, an equality function may report floats as equal so long as they |
|
| 14 |
+// are within some tolerance of each other. |
|
| 15 |
+// |
|
| 16 |
+// • Types that have an Equal method may use that method to determine equality. |
|
| 17 |
+// This allows package authors to determine the equality operation for the types |
|
| 18 |
+// that they define. |
|
| 19 |
+// |
|
| 20 |
+// • If no custom equality functions are used and no Equal method is defined, |
|
| 21 |
+// equality is determined by recursively comparing the primitive kinds on both |
|
| 22 |
+// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported |
|
| 23 |
+// fields are not compared by default; they result in panics unless suppressed |
|
| 24 |
+// by using an Ignore option (see cmpopts.IgnoreUnexported) or explictly compared |
|
| 25 |
+// using the AllowUnexported option. |
|
| 26 |
+package cmp |
|
| 27 |
+ |
|
| 28 |
+import ( |
|
| 29 |
+ "fmt" |
|
| 30 |
+ "reflect" |
|
| 31 |
+ |
|
| 32 |
+ "github.com/google/go-cmp/cmp/internal/diff" |
|
| 33 |
+ "github.com/google/go-cmp/cmp/internal/function" |
|
| 34 |
+ "github.com/google/go-cmp/cmp/internal/value" |
|
| 35 |
+) |
|
| 36 |
+ |
|
| 37 |
+// BUG: Maps with keys containing NaN values cannot be properly compared due to |
|
| 38 |
+// the reflection package's inability to retrieve such entries. Equal will panic |
|
| 39 |
+// anytime it comes across a NaN key, but this behavior may change. |
|
| 40 |
+// |
|
| 41 |
+// See https://golang.org/issue/11104 for more details. |
|
| 42 |
+ |
|
| 43 |
+var nothing = reflect.Value{}
|
|
| 44 |
+ |
|
| 45 |
+// Equal reports whether x and y are equal by recursively applying the |
|
| 46 |
+// following rules in the given order to x and y and all of their sub-values: |
|
| 47 |
+// |
|
| 48 |
+// • If two values are not of the same type, then they are never equal |
|
| 49 |
+// and the overall result is false. |
|
| 50 |
+// |
|
| 51 |
+// • Let S be the set of all Ignore, Transformer, and Comparer options that |
|
| 52 |
+// remain after applying all path filters, value filters, and type filters. |
|
| 53 |
+// If at least one Ignore exists in S, then the comparison is ignored. |
|
| 54 |
+// If the number of Transformer and Comparer options in S is greater than one, |
|
| 55 |
+// then Equal panics because it is ambiguous which option to use. |
|
| 56 |
+// If S contains a single Transformer, then use that to transform the current |
|
| 57 |
+// values and recursively call Equal on the output values. |
|
| 58 |
+// If S contains a single Comparer, then use that to compare the current values. |
|
| 59 |
+// Otherwise, evaluation proceeds to the next rule. |
|
| 60 |
+// |
|
| 61 |
+// • If the values have an Equal method of the form "(T) Equal(T) bool" or |
|
| 62 |
+// "(T) Equal(I) bool" where T is assignable to I, then use the result of |
|
| 63 |
+// x.Equal(y). Otherwise, no such method exists and evaluation proceeds to |
|
| 64 |
+// the next rule. |
|
| 65 |
+// |
|
| 66 |
+// • Lastly, try to compare x and y based on their basic kinds. |
|
| 67 |
+// Simple kinds like booleans, integers, floats, complex numbers, strings, and |
|
| 68 |
+// channels are compared using the equivalent of the == operator in Go. |
|
| 69 |
+// Functions are only equal if they are both nil, otherwise they are unequal. |
|
| 70 |
+// Pointers are equal if the underlying values they point to are also equal. |
|
| 71 |
+// Interfaces are equal if their underlying concrete values are also equal. |
|
| 72 |
+// |
|
| 73 |
+// Structs are equal if all of their fields are equal. If a struct contains |
|
| 74 |
+// unexported fields, Equal panics unless the AllowUnexported option is used or |
|
| 75 |
+// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. |
|
| 76 |
+// |
|
| 77 |
+// Arrays, slices, and maps are equal if they are both nil or both non-nil |
|
| 78 |
+// with the same length and the elements at each index or key are equal. |
|
| 79 |
+// Note that a non-nil empty slice and a nil slice are not equal. |
|
| 80 |
+// To equate empty slices and maps, consider using cmpopts.EquateEmpty. |
|
| 81 |
+// Map keys are equal according to the == operator. |
|
| 82 |
+// To use custom comparisons for map keys, consider using cmpopts.SortMaps. |
|
| 83 |
+func Equal(x, y interface{}, opts ...Option) bool {
|
|
| 84 |
+ s := newState(opts) |
|
| 85 |
+ s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) |
|
| 86 |
+ return s.result.Equal() |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 89 |
+// Diff returns a human-readable report of the differences between two values. |
|
| 90 |
+// It returns an empty string if and only if Equal returns true for the same |
|
| 91 |
+// input values and options. The output string will use the "-" symbol to |
|
| 92 |
+// indicate elements removed from x, and the "+" symbol to indicate elements |
|
| 93 |
+// added to y. |
|
| 94 |
+// |
|
| 95 |
+// Do not depend on this output being stable. |
|
| 96 |
+func Diff(x, y interface{}, opts ...Option) string {
|
|
| 97 |
+ r := new(defaultReporter) |
|
| 98 |
+ opts = Options{Options(opts), r}
|
|
| 99 |
+ eq := Equal(x, y, opts...) |
|
| 100 |
+ d := r.String() |
|
| 101 |
+ if (d == "") != eq {
|
|
| 102 |
+ panic("inconsistent difference and equality results")
|
|
| 103 |
+ } |
|
| 104 |
+ return d |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+type state struct {
|
|
| 108 |
+ // These fields represent the "comparison state". |
|
| 109 |
+ // Calling statelessCompare must not result in observable changes to these. |
|
| 110 |
+ result diff.Result // The current result of comparison |
|
| 111 |
+ curPath Path // The current path in the value tree |
|
| 112 |
+ reporter reporter // Optional reporter used for difference formatting |
|
| 113 |
+ |
|
| 114 |
+ // dynChecker triggers pseudo-random checks for option correctness. |
|
| 115 |
+ // It is safe for statelessCompare to mutate this value. |
|
| 116 |
+ dynChecker dynChecker |
|
| 117 |
+ |
|
| 118 |
+ // These fields, once set by processOption, will not change. |
|
| 119 |
+ exporters map[reflect.Type]bool // Set of structs with unexported field visibility |
|
| 120 |
+ opts Options // List of all fundamental and filter options |
|
| 121 |
+} |
|
| 122 |
+ |
|
| 123 |
+func newState(opts []Option) *state {
|
|
| 124 |
+ s := new(state) |
|
| 125 |
+ for _, opt := range opts {
|
|
| 126 |
+ s.processOption(opt) |
|
| 127 |
+ } |
|
| 128 |
+ return s |
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+func (s *state) processOption(opt Option) {
|
|
| 132 |
+ switch opt := opt.(type) {
|
|
| 133 |
+ case nil: |
|
| 134 |
+ case Options: |
|
| 135 |
+ for _, o := range opt {
|
|
| 136 |
+ s.processOption(o) |
|
| 137 |
+ } |
|
| 138 |
+ case coreOption: |
|
| 139 |
+ type filtered interface {
|
|
| 140 |
+ isFiltered() bool |
|
| 141 |
+ } |
|
| 142 |
+ if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() {
|
|
| 143 |
+ panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt))
|
|
| 144 |
+ } |
|
| 145 |
+ s.opts = append(s.opts, opt) |
|
| 146 |
+ case visibleStructs: |
|
| 147 |
+ if s.exporters == nil {
|
|
| 148 |
+ s.exporters = make(map[reflect.Type]bool) |
|
| 149 |
+ } |
|
| 150 |
+ for t := range opt {
|
|
| 151 |
+ s.exporters[t] = true |
|
| 152 |
+ } |
|
| 153 |
+ case reporter: |
|
| 154 |
+ if s.reporter != nil {
|
|
| 155 |
+ panic("difference reporter already registered")
|
|
| 156 |
+ } |
|
| 157 |
+ s.reporter = opt |
|
| 158 |
+ default: |
|
| 159 |
+ panic(fmt.Sprintf("unknown option %T", opt))
|
|
| 160 |
+ } |
|
| 161 |
+} |
|
| 162 |
+ |
|
| 163 |
+// statelessCompare compares two values and returns the result. |
|
| 164 |
+// This function is stateless in that it does not alter the current result, |
|
| 165 |
+// or output to any registered reporters. |
|
| 166 |
+func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result {
|
|
| 167 |
+ // We do not save and restore the curPath because all of the compareX |
|
| 168 |
+ // methods should properly push and pop from the path. |
|
| 169 |
+ // It is an implementation bug if the contents of curPath differs from |
|
| 170 |
+ // when calling this function to when returning from it. |
|
| 171 |
+ |
|
| 172 |
+ oldResult, oldReporter := s.result, s.reporter |
|
| 173 |
+ s.result = diff.Result{} // Reset result
|
|
| 174 |
+ s.reporter = nil // Remove reporter to avoid spurious printouts |
|
| 175 |
+ s.compareAny(vx, vy) |
|
| 176 |
+ res := s.result |
|
| 177 |
+ s.result, s.reporter = oldResult, oldReporter |
|
| 178 |
+ return res |
|
| 179 |
+} |
|
| 180 |
+ |
|
| 181 |
+func (s *state) compareAny(vx, vy reflect.Value) {
|
|
| 182 |
+ // TODO: Support cyclic data structures. |
|
| 183 |
+ |
|
| 184 |
+ // Rule 0: Differing types are never equal. |
|
| 185 |
+ if !vx.IsValid() || !vy.IsValid() {
|
|
| 186 |
+ s.report(vx.IsValid() == vy.IsValid(), vx, vy) |
|
| 187 |
+ return |
|
| 188 |
+ } |
|
| 189 |
+ if vx.Type() != vy.Type() {
|
|
| 190 |
+ s.report(false, vx, vy) // Possible for path to be empty |
|
| 191 |
+ return |
|
| 192 |
+ } |
|
| 193 |
+ t := vx.Type() |
|
| 194 |
+ if len(s.curPath) == 0 {
|
|
| 195 |
+ s.curPath.push(&pathStep{typ: t})
|
|
| 196 |
+ defer s.curPath.pop() |
|
| 197 |
+ } |
|
| 198 |
+ vx, vy = s.tryExporting(vx, vy) |
|
| 199 |
+ |
|
| 200 |
+ // Rule 1: Check whether an option applies on this node in the value tree. |
|
| 201 |
+ if s.tryOptions(vx, vy, t) {
|
|
| 202 |
+ return |
|
| 203 |
+ } |
|
| 204 |
+ |
|
| 205 |
+ // Rule 2: Check whether the type has a valid Equal method. |
|
| 206 |
+ if s.tryMethod(vx, vy, t) {
|
|
| 207 |
+ return |
|
| 208 |
+ } |
|
| 209 |
+ |
|
| 210 |
+ // Rule 3: Recursively descend into each value's underlying kind. |
|
| 211 |
+ switch t.Kind() {
|
|
| 212 |
+ case reflect.Bool: |
|
| 213 |
+ s.report(vx.Bool() == vy.Bool(), vx, vy) |
|
| 214 |
+ return |
|
| 215 |
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
| 216 |
+ s.report(vx.Int() == vy.Int(), vx, vy) |
|
| 217 |
+ return |
|
| 218 |
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
|
| 219 |
+ s.report(vx.Uint() == vy.Uint(), vx, vy) |
|
| 220 |
+ return |
|
| 221 |
+ case reflect.Float32, reflect.Float64: |
|
| 222 |
+ s.report(vx.Float() == vy.Float(), vx, vy) |
|
| 223 |
+ return |
|
| 224 |
+ case reflect.Complex64, reflect.Complex128: |
|
| 225 |
+ s.report(vx.Complex() == vy.Complex(), vx, vy) |
|
| 226 |
+ return |
|
| 227 |
+ case reflect.String: |
|
| 228 |
+ s.report(vx.String() == vy.String(), vx, vy) |
|
| 229 |
+ return |
|
| 230 |
+ case reflect.Chan, reflect.UnsafePointer: |
|
| 231 |
+ s.report(vx.Pointer() == vy.Pointer(), vx, vy) |
|
| 232 |
+ return |
|
| 233 |
+ case reflect.Func: |
|
| 234 |
+ s.report(vx.IsNil() && vy.IsNil(), vx, vy) |
|
| 235 |
+ return |
|
| 236 |
+ case reflect.Ptr: |
|
| 237 |
+ if vx.IsNil() || vy.IsNil() {
|
|
| 238 |
+ s.report(vx.IsNil() && vy.IsNil(), vx, vy) |
|
| 239 |
+ return |
|
| 240 |
+ } |
|
| 241 |
+ s.curPath.push(&indirect{pathStep{t.Elem()}})
|
|
| 242 |
+ defer s.curPath.pop() |
|
| 243 |
+ s.compareAny(vx.Elem(), vy.Elem()) |
|
| 244 |
+ return |
|
| 245 |
+ case reflect.Interface: |
|
| 246 |
+ if vx.IsNil() || vy.IsNil() {
|
|
| 247 |
+ s.report(vx.IsNil() && vy.IsNil(), vx, vy) |
|
| 248 |
+ return |
|
| 249 |
+ } |
|
| 250 |
+ if vx.Elem().Type() != vy.Elem().Type() {
|
|
| 251 |
+ s.report(false, vx.Elem(), vy.Elem()) |
|
| 252 |
+ return |
|
| 253 |
+ } |
|
| 254 |
+ s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}})
|
|
| 255 |
+ defer s.curPath.pop() |
|
| 256 |
+ s.compareAny(vx.Elem(), vy.Elem()) |
|
| 257 |
+ return |
|
| 258 |
+ case reflect.Slice: |
|
| 259 |
+ if vx.IsNil() || vy.IsNil() {
|
|
| 260 |
+ s.report(vx.IsNil() && vy.IsNil(), vx, vy) |
|
| 261 |
+ return |
|
| 262 |
+ } |
|
| 263 |
+ fallthrough |
|
| 264 |
+ case reflect.Array: |
|
| 265 |
+ s.compareArray(vx, vy, t) |
|
| 266 |
+ return |
|
| 267 |
+ case reflect.Map: |
|
| 268 |
+ s.compareMap(vx, vy, t) |
|
| 269 |
+ return |
|
| 270 |
+ case reflect.Struct: |
|
| 271 |
+ s.compareStruct(vx, vy, t) |
|
| 272 |
+ return |
|
| 273 |
+ default: |
|
| 274 |
+ panic(fmt.Sprintf("%v kind not handled", t.Kind()))
|
|
| 275 |
+ } |
|
| 276 |
+} |
|
| 277 |
+ |
|
| 278 |
+func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) {
|
|
| 279 |
+ if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported {
|
|
| 280 |
+ if sf.force {
|
|
| 281 |
+ // Use unsafe pointer arithmetic to get read-write access to an |
|
| 282 |
+ // unexported field in the struct. |
|
| 283 |
+ vx = unsafeRetrieveField(sf.pvx, sf.field) |
|
| 284 |
+ vy = unsafeRetrieveField(sf.pvy, sf.field) |
|
| 285 |
+ } else {
|
|
| 286 |
+ // We are not allowed to export the value, so invalidate them |
|
| 287 |
+ // so that tryOptions can panic later if not explicitly ignored. |
|
| 288 |
+ vx = nothing |
|
| 289 |
+ vy = nothing |
|
| 290 |
+ } |
|
| 291 |
+ } |
|
| 292 |
+ return vx, vy |
|
| 293 |
+} |
|
| 294 |
+ |
|
| 295 |
+func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool {
|
|
| 296 |
+ // If there were no FilterValues, we will not detect invalid inputs, |
|
| 297 |
+ // so manually check for them and append invalid if necessary. |
|
| 298 |
+ // We still evaluate the options since an ignore can override invalid. |
|
| 299 |
+ opts := s.opts |
|
| 300 |
+ if !vx.IsValid() || !vy.IsValid() {
|
|
| 301 |
+ opts = Options{opts, invalid{}}
|
|
| 302 |
+ } |
|
| 303 |
+ |
|
| 304 |
+ // Evaluate all filters and apply the remaining options. |
|
| 305 |
+ if opt := opts.filter(s, vx, vy, t); opt != nil {
|
|
| 306 |
+ return opt.apply(s, vx, vy) |
|
| 307 |
+ } |
|
| 308 |
+ return false |
|
| 309 |
+} |
|
| 310 |
+ |
|
| 311 |
+func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool {
|
|
| 312 |
+ // Check if this type even has an Equal method. |
|
| 313 |
+ m, ok := t.MethodByName("Equal")
|
|
| 314 |
+ if !ok || !function.IsType(m.Type, function.EqualAssignable) {
|
|
| 315 |
+ return false |
|
| 316 |
+ } |
|
| 317 |
+ |
|
| 318 |
+ eq := s.callTTBFunc(m.Func, vx, vy) |
|
| 319 |
+ s.report(eq, vx, vy) |
|
| 320 |
+ return true |
|
| 321 |
+} |
|
| 322 |
+ |
|
| 323 |
+func (s *state) callTRFunc(f, v reflect.Value) reflect.Value {
|
|
| 324 |
+ if !s.dynChecker.Next() {
|
|
| 325 |
+ return f.Call([]reflect.Value{v})[0]
|
|
| 326 |
+ } |
|
| 327 |
+ |
|
| 328 |
+ // Run the function twice and ensure that we get the same results back. |
|
| 329 |
+ // We run in goroutines so that the race detector (if enabled) can detect |
|
| 330 |
+ // unsafe mutations to the input. |
|
| 331 |
+ c := make(chan reflect.Value) |
|
| 332 |
+ go detectRaces(c, f, v) |
|
| 333 |
+ want := f.Call([]reflect.Value{v})[0]
|
|
| 334 |
+ if got := <-c; !s.statelessCompare(got, want).Equal() {
|
|
| 335 |
+ // To avoid false-positives with non-reflexive equality operations, |
|
| 336 |
+ // we sanity check whether a value is equal to itself. |
|
| 337 |
+ if !s.statelessCompare(want, want).Equal() {
|
|
| 338 |
+ return want |
|
| 339 |
+ } |
|
| 340 |
+ fn := getFuncName(f.Pointer()) |
|
| 341 |
+ panic(fmt.Sprintf("non-deterministic function detected: %s", fn))
|
|
| 342 |
+ } |
|
| 343 |
+ return want |
|
| 344 |
+} |
|
| 345 |
+ |
|
| 346 |
+func (s *state) callTTBFunc(f, x, y reflect.Value) bool {
|
|
| 347 |
+ if !s.dynChecker.Next() {
|
|
| 348 |
+ return f.Call([]reflect.Value{x, y})[0].Bool()
|
|
| 349 |
+ } |
|
| 350 |
+ |
|
| 351 |
+ // Swapping the input arguments is sufficient to check that |
|
| 352 |
+ // f is symmetric and deterministic. |
|
| 353 |
+ // We run in goroutines so that the race detector (if enabled) can detect |
|
| 354 |
+ // unsafe mutations to the input. |
|
| 355 |
+ c := make(chan reflect.Value) |
|
| 356 |
+ go detectRaces(c, f, y, x) |
|
| 357 |
+ want := f.Call([]reflect.Value{x, y})[0].Bool()
|
|
| 358 |
+ if got := <-c; !got.IsValid() || got.Bool() != want {
|
|
| 359 |
+ fn := getFuncName(f.Pointer()) |
|
| 360 |
+ panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn))
|
|
| 361 |
+ } |
|
| 362 |
+ return want |
|
| 363 |
+} |
|
| 364 |
+ |
|
| 365 |
+func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
|
|
| 366 |
+ var ret reflect.Value |
|
| 367 |
+ defer func() {
|
|
| 368 |
+ recover() // Ignore panics, let the other call to f panic instead |
|
| 369 |
+ c <- ret |
|
| 370 |
+ }() |
|
| 371 |
+ ret = f.Call(vs)[0] |
|
| 372 |
+} |
|
| 373 |
+ |
|
| 374 |
+func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) {
|
|
| 375 |
+ step := &sliceIndex{pathStep{t.Elem()}, 0, 0}
|
|
| 376 |
+ s.curPath.push(step) |
|
| 377 |
+ |
|
| 378 |
+ // Compute an edit-script for slices vx and vy. |
|
| 379 |
+ eq, es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result {
|
|
| 380 |
+ step.xkey, step.ykey = ix, iy |
|
| 381 |
+ return s.statelessCompare(vx.Index(ix), vy.Index(iy)) |
|
| 382 |
+ }) |
|
| 383 |
+ |
|
| 384 |
+ // Equal or no edit-script, so report entire slices as is. |
|
| 385 |
+ if eq || es == nil {
|
|
| 386 |
+ s.curPath.pop() // Pop first since we are reporting the whole slice |
|
| 387 |
+ s.report(eq, vx, vy) |
|
| 388 |
+ return |
|
| 389 |
+ } |
|
| 390 |
+ |
|
| 391 |
+ // Replay the edit-script. |
|
| 392 |
+ var ix, iy int |
|
| 393 |
+ for _, e := range es {
|
|
| 394 |
+ switch e {
|
|
| 395 |
+ case diff.UniqueX: |
|
| 396 |
+ step.xkey, step.ykey = ix, -1 |
|
| 397 |
+ s.report(false, vx.Index(ix), nothing) |
|
| 398 |
+ ix++ |
|
| 399 |
+ case diff.UniqueY: |
|
| 400 |
+ step.xkey, step.ykey = -1, iy |
|
| 401 |
+ s.report(false, nothing, vy.Index(iy)) |
|
| 402 |
+ iy++ |
|
| 403 |
+ default: |
|
| 404 |
+ step.xkey, step.ykey = ix, iy |
|
| 405 |
+ if e == diff.Identity {
|
|
| 406 |
+ s.report(true, vx.Index(ix), vy.Index(iy)) |
|
| 407 |
+ } else {
|
|
| 408 |
+ s.compareAny(vx.Index(ix), vy.Index(iy)) |
|
| 409 |
+ } |
|
| 410 |
+ ix++ |
|
| 411 |
+ iy++ |
|
| 412 |
+ } |
|
| 413 |
+ } |
|
| 414 |
+ s.curPath.pop() |
|
| 415 |
+ return |
|
| 416 |
+} |
|
| 417 |
+ |
|
| 418 |
+func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) {
|
|
| 419 |
+ if vx.IsNil() || vy.IsNil() {
|
|
| 420 |
+ s.report(vx.IsNil() && vy.IsNil(), vx, vy) |
|
| 421 |
+ return |
|
| 422 |
+ } |
|
| 423 |
+ |
|
| 424 |
+ // We combine and sort the two map keys so that we can perform the |
|
| 425 |
+ // comparisons in a deterministic order. |
|
| 426 |
+ step := &mapIndex{pathStep: pathStep{t.Elem()}}
|
|
| 427 |
+ s.curPath.push(step) |
|
| 428 |
+ defer s.curPath.pop() |
|
| 429 |
+ for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) {
|
|
| 430 |
+ step.key = k |
|
| 431 |
+ vvx := vx.MapIndex(k) |
|
| 432 |
+ vvy := vy.MapIndex(k) |
|
| 433 |
+ switch {
|
|
| 434 |
+ case vvx.IsValid() && vvy.IsValid(): |
|
| 435 |
+ s.compareAny(vvx, vvy) |
|
| 436 |
+ case vvx.IsValid() && !vvy.IsValid(): |
|
| 437 |
+ s.report(false, vvx, nothing) |
|
| 438 |
+ case !vvx.IsValid() && vvy.IsValid(): |
|
| 439 |
+ s.report(false, nothing, vvy) |
|
| 440 |
+ default: |
|
| 441 |
+ // It is possible for both vvx and vvy to be invalid if the |
|
| 442 |
+ // key contained a NaN value in it. There is no way in |
|
| 443 |
+ // reflection to be able to retrieve these values. |
|
| 444 |
+ // See https://golang.org/issue/11104 |
|
| 445 |
+ panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath))
|
|
| 446 |
+ } |
|
| 447 |
+ } |
|
| 448 |
+} |
|
| 449 |
+ |
|
| 450 |
+func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) {
|
|
| 451 |
+ var vax, vay reflect.Value // Addressable versions of vx and vy |
|
| 452 |
+ |
|
| 453 |
+ step := &structField{}
|
|
| 454 |
+ s.curPath.push(step) |
|
| 455 |
+ defer s.curPath.pop() |
|
| 456 |
+ for i := 0; i < t.NumField(); i++ {
|
|
| 457 |
+ vvx := vx.Field(i) |
|
| 458 |
+ vvy := vy.Field(i) |
|
| 459 |
+ step.typ = t.Field(i).Type |
|
| 460 |
+ step.name = t.Field(i).Name |
|
| 461 |
+ step.idx = i |
|
| 462 |
+ step.unexported = !isExported(step.name) |
|
| 463 |
+ if step.unexported {
|
|
| 464 |
+ // Defer checking of unexported fields until later to give an |
|
| 465 |
+ // Ignore a chance to ignore the field. |
|
| 466 |
+ if !vax.IsValid() || !vay.IsValid() {
|
|
| 467 |
+ // For unsafeRetrieveField to work, the parent struct must |
|
| 468 |
+ // be addressable. Create a new copy of the values if |
|
| 469 |
+ // necessary to make them addressable. |
|
| 470 |
+ vax = makeAddressable(vx) |
|
| 471 |
+ vay = makeAddressable(vy) |
|
| 472 |
+ } |
|
| 473 |
+ step.force = s.exporters[t] |
|
| 474 |
+ step.pvx = vax |
|
| 475 |
+ step.pvy = vay |
|
| 476 |
+ step.field = t.Field(i) |
|
| 477 |
+ } |
|
| 478 |
+ s.compareAny(vvx, vvy) |
|
| 479 |
+ } |
|
| 480 |
+} |
|
| 481 |
+ |
|
| 482 |
+// report records the result of a single comparison. |
|
| 483 |
+// It also calls Report if any reporter is registered. |
|
| 484 |
+func (s *state) report(eq bool, vx, vy reflect.Value) {
|
|
| 485 |
+ if eq {
|
|
| 486 |
+ s.result.NSame++ |
|
| 487 |
+ } else {
|
|
| 488 |
+ s.result.NDiff++ |
|
| 489 |
+ } |
|
| 490 |
+ if s.reporter != nil {
|
|
| 491 |
+ s.reporter.Report(vx, vy, eq, s.curPath) |
|
| 492 |
+ } |
|
| 493 |
+} |
|
| 494 |
+ |
|
| 495 |
+// dynChecker tracks the state needed to periodically perform checks that |
|
| 496 |
+// user provided functions are symmetric and deterministic. |
|
| 497 |
+// The zero value is safe for immediate use. |
|
| 498 |
+type dynChecker struct{ curr, next int }
|
|
| 499 |
+ |
|
| 500 |
+// Next increments the state and reports whether a check should be performed. |
|
| 501 |
+// |
|
| 502 |
+// Checks occur every Nth function call, where N is a triangular number: |
|
| 503 |
+// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... |
|
| 504 |
+// See https://en.wikipedia.org/wiki/Triangular_number |
|
| 505 |
+// |
|
| 506 |
+// This sequence ensures that the cost of checks drops significantly as |
|
| 507 |
+// the number of functions calls grows larger. |
|
| 508 |
+func (dc *dynChecker) Next() bool {
|
|
| 509 |
+ ok := dc.curr == dc.next |
|
| 510 |
+ if ok {
|
|
| 511 |
+ dc.curr = 0 |
|
| 512 |
+ dc.next++ |
|
| 513 |
+ } |
|
| 514 |
+ dc.curr++ |
|
| 515 |
+ return ok |
|
| 516 |
+} |
|
| 517 |
+ |
|
| 518 |
+// makeAddressable returns a value that is always addressable. |
|
| 519 |
+// It returns the input verbatim if it is already addressable, |
|
| 520 |
+// otherwise it creates a new value and returns an addressable copy. |
|
| 521 |
+func makeAddressable(v reflect.Value) reflect.Value {
|
|
| 522 |
+ if v.CanAddr() {
|
|
| 523 |
+ return v |
|
| 524 |
+ } |
|
| 525 |
+ vc := reflect.New(v.Type()).Elem() |
|
| 526 |
+ vc.Set(v) |
|
| 527 |
+ return vc |
|
| 528 |
+} |
| 0 | 529 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,17 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// +build !debug |
|
| 5 |
+ |
|
| 6 |
+package diff |
|
| 7 |
+ |
|
| 8 |
+var debug debugger |
|
| 9 |
+ |
|
| 10 |
+type debugger struct{}
|
|
| 11 |
+ |
|
| 12 |
+func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc {
|
|
| 13 |
+ return f |
|
| 14 |
+} |
|
| 15 |
+func (debugger) Update() {}
|
|
| 16 |
+func (debugger) Finish() {}
|
| 0 | 17 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,122 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// +build debug |
|
| 5 |
+ |
|
| 6 |
+package diff |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "fmt" |
|
| 10 |
+ "strings" |
|
| 11 |
+ "sync" |
|
| 12 |
+ "time" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+// The algorithm can be seen running in real-time by enabling debugging: |
|
| 16 |
+// go test -tags=debug -v |
|
| 17 |
+// |
|
| 18 |
+// Example output: |
|
| 19 |
+// === RUN TestDifference/#34 |
|
| 20 |
+// ┌───────────────────────────────┐ |
|
| 21 |
+// │ \ · · · · · · · · · · · · · · │ |
|
| 22 |
+// │ · # · · · · · · · · · · · · · │ |
|
| 23 |
+// │ · \ · · · · · · · · · · · · · │ |
|
| 24 |
+// │ · · \ · · · · · · · · · · · · │ |
|
| 25 |
+// │ · · · X # · · · · · · · · · · │ |
|
| 26 |
+// │ · · · # \ · · · · · · · · · · │ |
|
| 27 |
+// │ · · · · · # # · · · · · · · · │ |
|
| 28 |
+// │ · · · · · # \ · · · · · · · · │ |
|
| 29 |
+// │ · · · · · · · \ · · · · · · · │ |
|
| 30 |
+// │ · · · · · · · · \ · · · · · · │ |
|
| 31 |
+// │ · · · · · · · · · \ · · · · · │ |
|
| 32 |
+// │ · · · · · · · · · · \ · · # · │ |
|
| 33 |
+// │ · · · · · · · · · · · \ # # · │ |
|
| 34 |
+// │ · · · · · · · · · · · # # # · │ |
|
| 35 |
+// │ · · · · · · · · · · # # # # · │ |
|
| 36 |
+// │ · · · · · · · · · # # # # # · │ |
|
| 37 |
+// │ · · · · · · · · · · · · · · \ │ |
|
| 38 |
+// └───────────────────────────────┘ |
|
| 39 |
+// [.Y..M.XY......YXYXY.|] |
|
| 40 |
+// |
|
| 41 |
+// The grid represents the edit-graph where the horizontal axis represents |
|
| 42 |
+// list X and the vertical axis represents list Y. The start of the two lists |
|
| 43 |
+// is the top-left, while the ends are the bottom-right. The '·' represents |
|
| 44 |
+// an unexplored node in the graph. The '\' indicates that the two symbols |
|
| 45 |
+// from list X and Y are equal. The 'X' indicates that two symbols are similar |
|
| 46 |
+// (but not exactly equal) to each other. The '#' indicates that the two symbols |
|
| 47 |
+// are different (and not similar). The algorithm traverses this graph trying to |
|
| 48 |
+// make the paths starting in the top-left and the bottom-right connect. |
|
| 49 |
+// |
|
| 50 |
+// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents |
|
| 51 |
+// the currently established path from the forward and reverse searches, |
|
| 52 |
+// seperated by a '|' character. |
|
| 53 |
+ |
|
| 54 |
+const ( |
|
| 55 |
+ updateDelay = 100 * time.Millisecond |
|
| 56 |
+ finishDelay = 500 * time.Millisecond |
|
| 57 |
+ ansiTerminal = true // ANSI escape codes used to move terminal cursor |
|
| 58 |
+) |
|
| 59 |
+ |
|
| 60 |
+var debug debugger |
|
| 61 |
+ |
|
| 62 |
+type debugger struct {
|
|
| 63 |
+ sync.Mutex |
|
| 64 |
+ p1, p2 EditScript |
|
| 65 |
+ fwdPath, revPath *EditScript |
|
| 66 |
+ grid []byte |
|
| 67 |
+ lines int |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc {
|
|
| 71 |
+ dbg.Lock() |
|
| 72 |
+ dbg.fwdPath, dbg.revPath = p1, p2 |
|
| 73 |
+ top := "┌─" + strings.Repeat("──", nx) + "┐\n"
|
|
| 74 |
+ row := "│ " + strings.Repeat("· ", nx) + "│\n"
|
|
| 75 |
+ btm := "└─" + strings.Repeat("──", nx) + "┘\n"
|
|
| 76 |
+ dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) |
|
| 77 |
+ dbg.lines = strings.Count(dbg.String(), "\n") |
|
| 78 |
+ fmt.Print(dbg) |
|
| 79 |
+ |
|
| 80 |
+ // Wrap the EqualFunc so that we can intercept each result. |
|
| 81 |
+ return func(ix, iy int) (r Result) {
|
|
| 82 |
+ cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")]
|
|
| 83 |
+ for i := range cell {
|
|
| 84 |
+ cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot |
|
| 85 |
+ } |
|
| 86 |
+ switch r = f(ix, iy); {
|
|
| 87 |
+ case r.Equal(): |
|
| 88 |
+ cell[0] = '\\' |
|
| 89 |
+ case r.Similar(): |
|
| 90 |
+ cell[0] = 'X' |
|
| 91 |
+ default: |
|
| 92 |
+ cell[0] = '#' |
|
| 93 |
+ } |
|
| 94 |
+ return |
|
| 95 |
+ } |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+func (dbg *debugger) Update() {
|
|
| 99 |
+ dbg.print(updateDelay) |
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+func (dbg *debugger) Finish() {
|
|
| 103 |
+ dbg.print(finishDelay) |
|
| 104 |
+ dbg.Unlock() |
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+func (dbg *debugger) String() string {
|
|
| 108 |
+ dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] |
|
| 109 |
+ for i := len(*dbg.revPath) - 1; i >= 0; i-- {
|
|
| 110 |
+ dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) |
|
| 111 |
+ } |
|
| 112 |
+ return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2)
|
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+func (dbg *debugger) print(d time.Duration) {
|
|
| 116 |
+ if ansiTerminal {
|
|
| 117 |
+ fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor
|
|
| 118 |
+ } |
|
| 119 |
+ fmt.Print(dbg) |
|
| 120 |
+ time.Sleep(d) |
|
| 121 |
+} |
| 0 | 122 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,373 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// Package diff implements an algorithm for producing edit-scripts. |
|
| 5 |
+// The edit-script is a sequence of operations needed to transform one list |
|
| 6 |
+// of symbols into another (or vice-versa). The edits allowed are insertions, |
|
| 7 |
+// deletions, and modifications. The summation of all edits is called the |
|
| 8 |
+// Levenshtein distance as this problem is well-known in computer science. |
|
| 9 |
+// |
|
| 10 |
+// This package prioritizes performance over accuracy. That is, the run time |
|
| 11 |
+// is more important than obtaining a minimal Levenshtein distance. |
|
| 12 |
+package diff |
|
| 13 |
+ |
|
| 14 |
+// EditType represents a single operation within an edit-script. |
|
| 15 |
+type EditType uint8 |
|
| 16 |
+ |
|
| 17 |
+const ( |
|
| 18 |
+ // Identity indicates that a symbol pair is identical in both list X and Y. |
|
| 19 |
+ Identity EditType = iota |
|
| 20 |
+ // UniqueX indicates that a symbol only exists in X and not Y. |
|
| 21 |
+ UniqueX |
|
| 22 |
+ // UniqueY indicates that a symbol only exists in Y and not X. |
|
| 23 |
+ UniqueY |
|
| 24 |
+ // Modified indicates that a symbol pair is a modification of each other. |
|
| 25 |
+ Modified |
|
| 26 |
+) |
|
| 27 |
+ |
|
| 28 |
+// EditScript represents the series of differences between two lists. |
|
| 29 |
+type EditScript []EditType |
|
| 30 |
+ |
|
| 31 |
+// String returns a human-readable string representing the edit-script where |
|
| 32 |
+// Identity, UniqueX, UniqueY, and Modified are represented by the |
|
| 33 |
+// '.', 'X', 'Y', and 'M' characters, respectively. |
|
| 34 |
+func (es EditScript) String() string {
|
|
| 35 |
+ b := make([]byte, len(es)) |
|
| 36 |
+ for i, e := range es {
|
|
| 37 |
+ switch e {
|
|
| 38 |
+ case Identity: |
|
| 39 |
+ b[i] = '.' |
|
| 40 |
+ case UniqueX: |
|
| 41 |
+ b[i] = 'X' |
|
| 42 |
+ case UniqueY: |
|
| 43 |
+ b[i] = 'Y' |
|
| 44 |
+ case Modified: |
|
| 45 |
+ b[i] = 'M' |
|
| 46 |
+ default: |
|
| 47 |
+ panic("invalid edit-type")
|
|
| 48 |
+ } |
|
| 49 |
+ } |
|
| 50 |
+ return string(b) |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+// stats returns a histogram of the number of each type of edit operation. |
|
| 54 |
+func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) {
|
|
| 55 |
+ for _, e := range es {
|
|
| 56 |
+ switch e {
|
|
| 57 |
+ case Identity: |
|
| 58 |
+ s.NI++ |
|
| 59 |
+ case UniqueX: |
|
| 60 |
+ s.NX++ |
|
| 61 |
+ case UniqueY: |
|
| 62 |
+ s.NY++ |
|
| 63 |
+ case Modified: |
|
| 64 |
+ s.NM++ |
|
| 65 |
+ default: |
|
| 66 |
+ panic("invalid edit-type")
|
|
| 67 |
+ } |
|
| 68 |
+ } |
|
| 69 |
+ return |
|
| 70 |
+} |
|
| 71 |
+ |
|
| 72 |
+// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if |
|
| 73 |
+// lists X and Y are equal. |
|
| 74 |
+func (es EditScript) Dist() int { return len(es) - es.stats().NI }
|
|
| 75 |
+ |
|
| 76 |
+// LenX is the length of the X list. |
|
| 77 |
+func (es EditScript) LenX() int { return len(es) - es.stats().NY }
|
|
| 78 |
+ |
|
| 79 |
+// LenY is the length of the Y list. |
|
| 80 |
+func (es EditScript) LenY() int { return len(es) - es.stats().NX }
|
|
| 81 |
+ |
|
| 82 |
+// EqualFunc reports whether the symbols at indexes ix and iy are equal. |
|
| 83 |
+// When called by Difference, the index is guaranteed to be within nx and ny. |
|
| 84 |
+type EqualFunc func(ix int, iy int) Result |
|
| 85 |
+ |
|
| 86 |
+// Result is the result of comparison. |
|
| 87 |
+// NSame is the number of sub-elements that are equal. |
|
| 88 |
+// NDiff is the number of sub-elements that are not equal. |
|
| 89 |
+type Result struct{ NSame, NDiff int }
|
|
| 90 |
+ |
|
| 91 |
+// Equal indicates whether the symbols are equal. Two symbols are equal |
|
| 92 |
+// if and only if NDiff == 0. If Equal, then they are also Similar. |
|
| 93 |
+func (r Result) Equal() bool { return r.NDiff == 0 }
|
|
| 94 |
+ |
|
| 95 |
+// Similar indicates whether two symbols are similar and may be represented |
|
| 96 |
+// by using the Modified type. As a special case, we consider binary comparisons |
|
| 97 |
+// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar.
|
|
| 98 |
+// |
|
| 99 |
+// The exact ratio of NSame to NDiff to determine similarity may change. |
|
| 100 |
+func (r Result) Similar() bool {
|
|
| 101 |
+ // Use NSame+1 to offset NSame so that binary comparisons are similar. |
|
| 102 |
+ return r.NSame+1 >= r.NDiff |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// Difference reports whether two lists of lengths nx and ny are equal |
|
| 106 |
+// given the definition of equality provided as f. |
|
| 107 |
+// |
|
| 108 |
+// This function may return a edit-script, which is a sequence of operations |
|
| 109 |
+// needed to convert one list into the other. If non-nil, the following |
|
| 110 |
+// invariants for the edit-script are maintained: |
|
| 111 |
+// • eq == (es.Dist()==0) |
|
| 112 |
+// • nx == es.LenX() |
|
| 113 |
+// • ny == es.LenY() |
|
| 114 |
+// |
|
| 115 |
+// This algorithm is not guaranteed to be an optimal solution (i.e., one that |
|
| 116 |
+// produces an edit-script with a minimal Levenshtein distance). This algorithm |
|
| 117 |
+// favors performance over optimality. The exact output is not guaranteed to |
|
| 118 |
+// be stable and may change over time. |
|
| 119 |
+func Difference(nx, ny int, f EqualFunc) (eq bool, es EditScript) {
|
|
| 120 |
+ es = searchGraph(nx, ny, f) |
|
| 121 |
+ st := es.stats() |
|
| 122 |
+ eq = len(es) == st.NI |
|
| 123 |
+ if !eq && st.NI < (nx+ny)/4 {
|
|
| 124 |
+ return eq, nil // Edit-script more distracting than helpful |
|
| 125 |
+ } |
|
| 126 |
+ return eq, es |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+func searchGraph(nx, ny int, f EqualFunc) EditScript {
|
|
| 130 |
+ // This algorithm is based on traversing what is known as an "edit-graph". |
|
| 131 |
+ // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" |
|
| 132 |
+ // by Eugene W. Myers. Since D can be as large as N itself, this is |
|
| 133 |
+ // effectively O(N^2). Unlike the algorithm from that paper, we are not |
|
| 134 |
+ // interested in the optimal path, but at least some "decent" path. |
|
| 135 |
+ // |
|
| 136 |
+ // For example, let X and Y be lists of symbols: |
|
| 137 |
+ // X = [A B C A B B A] |
|
| 138 |
+ // Y = [C B A B A C] |
|
| 139 |
+ // |
|
| 140 |
+ // The edit-graph can be drawn as the following: |
|
| 141 |
+ // A B C A B B A |
|
| 142 |
+ // ┌─────────────┐ |
|
| 143 |
+ // C │_|_|\|_|_|_|_│ 0 |
|
| 144 |
+ // B │_|\|_|_|\|\|_│ 1 |
|
| 145 |
+ // A │\|_|_|\|_|_|\│ 2 |
|
| 146 |
+ // B │_|\|_|_|\|\|_│ 3 |
|
| 147 |
+ // A │\|_|_|\|_|_|\│ 4 |
|
| 148 |
+ // C │ | |\| | | | │ 5 |
|
| 149 |
+ // └─────────────┘ 6 |
|
| 150 |
+ // 0 1 2 3 4 5 6 7 |
|
| 151 |
+ // |
|
| 152 |
+ // List X is written along the horizontal axis, while list Y is written |
|
| 153 |
+ // along the vertical axis. At any point on this grid, if the symbol in |
|
| 154 |
+ // list X matches the corresponding symbol in list Y, then a '\' is drawn. |
|
| 155 |
+ // The goal of any minimal edit-script algorithm is to find a path from the |
|
| 156 |
+ // top-left corner to the bottom-right corner, while traveling through the |
|
| 157 |
+ // fewest horizontal or vertical edges. |
|
| 158 |
+ // A horizontal edge is equivalent to inserting a symbol from list X. |
|
| 159 |
+ // A vertical edge is equivalent to inserting a symbol from list Y. |
|
| 160 |
+ // A diagonal edge is equivalent to a matching symbol between both X and Y. |
|
| 161 |
+ |
|
| 162 |
+ // Invariants: |
|
| 163 |
+ // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx |
|
| 164 |
+ // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny |
|
| 165 |
+ // |
|
| 166 |
+ // In general: |
|
| 167 |
+ // • fwdFrontier.X < revFrontier.X |
|
| 168 |
+ // • fwdFrontier.Y < revFrontier.Y |
|
| 169 |
+ // Unless, it is time for the algorithm to terminate. |
|
| 170 |
+ fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)}
|
|
| 171 |
+ revPath := path{-1, point{nx, ny}, make(EditScript, 0)}
|
|
| 172 |
+ fwdFrontier := fwdPath.point // Forward search frontier |
|
| 173 |
+ revFrontier := revPath.point // Reverse search frontier |
|
| 174 |
+ |
|
| 175 |
+ // Search budget bounds the cost of searching for better paths. |
|
| 176 |
+ // The longest sequence of non-matching symbols that can be tolerated is |
|
| 177 |
+ // approximately the square-root of the search budget. |
|
| 178 |
+ searchBudget := 4 * (nx + ny) // O(n) |
|
| 179 |
+ |
|
| 180 |
+ // The algorithm below is a greedy, meet-in-the-middle algorithm for |
|
| 181 |
+ // computing sub-optimal edit-scripts between two lists. |
|
| 182 |
+ // |
|
| 183 |
+ // The algorithm is approximately as follows: |
|
| 184 |
+ // • Searching for differences switches back-and-forth between |
|
| 185 |
+ // a search that starts at the beginning (the top-left corner), and |
|
| 186 |
+ // a search that starts at the end (the bottom-right corner). The goal of |
|
| 187 |
+ // the search is connect with the search from the opposite corner. |
|
| 188 |
+ // • As we search, we build a path in a greedy manner, where the first |
|
| 189 |
+ // match seen is added to the path (this is sub-optimal, but provides a |
|
| 190 |
+ // decent result in practice). When matches are found, we try the next pair |
|
| 191 |
+ // of symbols in the lists and follow all matches as far as possible. |
|
| 192 |
+ // • When searching for matches, we search along a diagonal going through |
|
| 193 |
+ // through the "frontier" point. If no matches are found, we advance the |
|
| 194 |
+ // frontier towards the opposite corner. |
|
| 195 |
+ // • This algorithm terminates when either the X coordinates or the |
|
| 196 |
+ // Y coordinates of the forward and reverse frontier points ever intersect. |
|
| 197 |
+ // |
|
| 198 |
+ // This algorithm is correct even if searching only in the forward direction |
|
| 199 |
+ // or in the reverse direction. We do both because it is commonly observed |
|
| 200 |
+ // that two lists commonly differ because elements were added to the front |
|
| 201 |
+ // or end of the other list. |
|
| 202 |
+ // |
|
| 203 |
+ // Running the tests with the "debug" build tag prints a visualization of |
|
| 204 |
+ // the algorithm running in real-time. This is educational for understanding |
|
| 205 |
+ // how the algorithm works. See debug_enable.go. |
|
| 206 |
+ f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) |
|
| 207 |
+ for {
|
|
| 208 |
+ // Forward search from the beginning. |
|
| 209 |
+ if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
|
| 210 |
+ break |
|
| 211 |
+ } |
|
| 212 |
+ for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
|
| 213 |
+ // Search in a diagonal pattern for a match. |
|
| 214 |
+ z := zigzag(i) |
|
| 215 |
+ p := point{fwdFrontier.X + z, fwdFrontier.Y - z}
|
|
| 216 |
+ switch {
|
|
| 217 |
+ case p.X >= revPath.X || p.Y < fwdPath.Y: |
|
| 218 |
+ stop1 = true // Hit top-right corner |
|
| 219 |
+ case p.Y >= revPath.Y || p.X < fwdPath.X: |
|
| 220 |
+ stop2 = true // Hit bottom-left corner |
|
| 221 |
+ case f(p.X, p.Y).Equal(): |
|
| 222 |
+ // Match found, so connect the path to this point. |
|
| 223 |
+ fwdPath.connect(p, f) |
|
| 224 |
+ fwdPath.append(Identity) |
|
| 225 |
+ // Follow sequence of matches as far as possible. |
|
| 226 |
+ for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
|
| 227 |
+ if !f(fwdPath.X, fwdPath.Y).Equal() {
|
|
| 228 |
+ break |
|
| 229 |
+ } |
|
| 230 |
+ fwdPath.append(Identity) |
|
| 231 |
+ } |
|
| 232 |
+ fwdFrontier = fwdPath.point |
|
| 233 |
+ stop1, stop2 = true, true |
|
| 234 |
+ default: |
|
| 235 |
+ searchBudget-- // Match not found |
|
| 236 |
+ } |
|
| 237 |
+ debug.Update() |
|
| 238 |
+ } |
|
| 239 |
+ // Advance the frontier towards reverse point. |
|
| 240 |
+ if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y {
|
|
| 241 |
+ fwdFrontier.X++ |
|
| 242 |
+ } else {
|
|
| 243 |
+ fwdFrontier.Y++ |
|
| 244 |
+ } |
|
| 245 |
+ |
|
| 246 |
+ // Reverse search from the end. |
|
| 247 |
+ if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
|
| 248 |
+ break |
|
| 249 |
+ } |
|
| 250 |
+ for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
|
| 251 |
+ // Search in a diagonal pattern for a match. |
|
| 252 |
+ z := zigzag(i) |
|
| 253 |
+ p := point{revFrontier.X - z, revFrontier.Y + z}
|
|
| 254 |
+ switch {
|
|
| 255 |
+ case fwdPath.X >= p.X || revPath.Y < p.Y: |
|
| 256 |
+ stop1 = true // Hit bottom-left corner |
|
| 257 |
+ case fwdPath.Y >= p.Y || revPath.X < p.X: |
|
| 258 |
+ stop2 = true // Hit top-right corner |
|
| 259 |
+ case f(p.X-1, p.Y-1).Equal(): |
|
| 260 |
+ // Match found, so connect the path to this point. |
|
| 261 |
+ revPath.connect(p, f) |
|
| 262 |
+ revPath.append(Identity) |
|
| 263 |
+ // Follow sequence of matches as far as possible. |
|
| 264 |
+ for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
|
| 265 |
+ if !f(revPath.X-1, revPath.Y-1).Equal() {
|
|
| 266 |
+ break |
|
| 267 |
+ } |
|
| 268 |
+ revPath.append(Identity) |
|
| 269 |
+ } |
|
| 270 |
+ revFrontier = revPath.point |
|
| 271 |
+ stop1, stop2 = true, true |
|
| 272 |
+ default: |
|
| 273 |
+ searchBudget-- // Match not found |
|
| 274 |
+ } |
|
| 275 |
+ debug.Update() |
|
| 276 |
+ } |
|
| 277 |
+ // Advance the frontier towards forward point. |
|
| 278 |
+ if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y {
|
|
| 279 |
+ revFrontier.X-- |
|
| 280 |
+ } else {
|
|
| 281 |
+ revFrontier.Y-- |
|
| 282 |
+ } |
|
| 283 |
+ } |
|
| 284 |
+ |
|
| 285 |
+ // Join the forward and reverse paths and then append the reverse path. |
|
| 286 |
+ fwdPath.connect(revPath.point, f) |
|
| 287 |
+ for i := len(revPath.es) - 1; i >= 0; i-- {
|
|
| 288 |
+ t := revPath.es[i] |
|
| 289 |
+ revPath.es = revPath.es[:i] |
|
| 290 |
+ fwdPath.append(t) |
|
| 291 |
+ } |
|
| 292 |
+ debug.Finish() |
|
| 293 |
+ return fwdPath.es |
|
| 294 |
+} |
|
| 295 |
+ |
|
| 296 |
+type path struct {
|
|
| 297 |
+ dir int // +1 if forward, -1 if reverse |
|
| 298 |
+ point // Leading point of the EditScript path |
|
| 299 |
+ es EditScript |
|
| 300 |
+} |
|
| 301 |
+ |
|
| 302 |
+// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types |
|
| 303 |
+// to the edit-script to connect p.point to dst. |
|
| 304 |
+func (p *path) connect(dst point, f EqualFunc) {
|
|
| 305 |
+ if p.dir > 0 {
|
|
| 306 |
+ // Connect in forward direction. |
|
| 307 |
+ for dst.X > p.X && dst.Y > p.Y {
|
|
| 308 |
+ switch r := f(p.X, p.Y); {
|
|
| 309 |
+ case r.Equal(): |
|
| 310 |
+ p.append(Identity) |
|
| 311 |
+ case r.Similar(): |
|
| 312 |
+ p.append(Modified) |
|
| 313 |
+ case dst.X-p.X >= dst.Y-p.Y: |
|
| 314 |
+ p.append(UniqueX) |
|
| 315 |
+ default: |
|
| 316 |
+ p.append(UniqueY) |
|
| 317 |
+ } |
|
| 318 |
+ } |
|
| 319 |
+ for dst.X > p.X {
|
|
| 320 |
+ p.append(UniqueX) |
|
| 321 |
+ } |
|
| 322 |
+ for dst.Y > p.Y {
|
|
| 323 |
+ p.append(UniqueY) |
|
| 324 |
+ } |
|
| 325 |
+ } else {
|
|
| 326 |
+ // Connect in reverse direction. |
|
| 327 |
+ for p.X > dst.X && p.Y > dst.Y {
|
|
| 328 |
+ switch r := f(p.X-1, p.Y-1); {
|
|
| 329 |
+ case r.Equal(): |
|
| 330 |
+ p.append(Identity) |
|
| 331 |
+ case r.Similar(): |
|
| 332 |
+ p.append(Modified) |
|
| 333 |
+ case p.Y-dst.Y >= p.X-dst.X: |
|
| 334 |
+ p.append(UniqueY) |
|
| 335 |
+ default: |
|
| 336 |
+ p.append(UniqueX) |
|
| 337 |
+ } |
|
| 338 |
+ } |
|
| 339 |
+ for p.X > dst.X {
|
|
| 340 |
+ p.append(UniqueX) |
|
| 341 |
+ } |
|
| 342 |
+ for p.Y > dst.Y {
|
|
| 343 |
+ p.append(UniqueY) |
|
| 344 |
+ } |
|
| 345 |
+ } |
|
| 346 |
+} |
|
| 347 |
+ |
|
| 348 |
+func (p *path) append(t EditType) {
|
|
| 349 |
+ p.es = append(p.es, t) |
|
| 350 |
+ switch t {
|
|
| 351 |
+ case Identity, Modified: |
|
| 352 |
+ p.add(p.dir, p.dir) |
|
| 353 |
+ case UniqueX: |
|
| 354 |
+ p.add(p.dir, 0) |
|
| 355 |
+ case UniqueY: |
|
| 356 |
+ p.add(0, p.dir) |
|
| 357 |
+ } |
|
| 358 |
+ debug.Update() |
|
| 359 |
+} |
|
| 360 |
+ |
|
| 361 |
+type point struct{ X, Y int }
|
|
| 362 |
+ |
|
| 363 |
+func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy }
|
|
| 364 |
+ |
|
| 365 |
+// zigzag maps a consecutive sequence of integers to a zig-zag sequence. |
|
| 366 |
+// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] |
|
| 367 |
+func zigzag(x int) int {
|
|
| 368 |
+ if x&1 != 0 {
|
|
| 369 |
+ x = ^x |
|
| 370 |
+ } |
|
| 371 |
+ return x >> 1 |
|
| 372 |
+} |
| 0 | 373 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,49 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// Package function identifies function types. |
|
| 5 |
+package function |
|
| 6 |
+ |
|
| 7 |
+import "reflect" |
|
| 8 |
+ |
|
| 9 |
+type funcType int |
|
| 10 |
+ |
|
| 11 |
+const ( |
|
| 12 |
+ _ funcType = iota |
|
| 13 |
+ |
|
| 14 |
+ ttbFunc // func(T, T) bool |
|
| 15 |
+ tibFunc // func(T, I) bool |
|
| 16 |
+ trFunc // func(T) R |
|
| 17 |
+ |
|
| 18 |
+ Equal = ttbFunc // func(T, T) bool |
|
| 19 |
+ EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool |
|
| 20 |
+ Transformer = trFunc // func(T) R |
|
| 21 |
+ ValueFilter = ttbFunc // func(T, T) bool |
|
| 22 |
+ Less = ttbFunc // func(T, T) bool |
|
| 23 |
+) |
|
| 24 |
+ |
|
| 25 |
+var boolType = reflect.TypeOf(true) |
|
| 26 |
+ |
|
| 27 |
+// IsType reports whether the reflect.Type is of the specified function type. |
|
| 28 |
+func IsType(t reflect.Type, ft funcType) bool {
|
|
| 29 |
+ if t == nil || t.Kind() != reflect.Func || t.IsVariadic() {
|
|
| 30 |
+ return false |
|
| 31 |
+ } |
|
| 32 |
+ ni, no := t.NumIn(), t.NumOut() |
|
| 33 |
+ switch ft {
|
|
| 34 |
+ case ttbFunc: // func(T, T) bool |
|
| 35 |
+ if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
|
|
| 36 |
+ return true |
|
| 37 |
+ } |
|
| 38 |
+ case tibFunc: // func(T, I) bool |
|
| 39 |
+ if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
|
|
| 40 |
+ return true |
|
| 41 |
+ } |
|
| 42 |
+ case trFunc: // func(T) R |
|
| 43 |
+ if ni == 1 && no == 1 {
|
|
| 44 |
+ return true |
|
| 45 |
+ } |
|
| 46 |
+ } |
|
| 47 |
+ return false |
|
| 48 |
+} |
| 0 | 49 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,259 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// Package value provides functionality for reflect.Value types. |
|
| 5 |
+package value |
|
| 6 |
+ |
|
| 7 |
+import ( |
|
| 8 |
+ "fmt" |
|
| 9 |
+ "reflect" |
|
| 10 |
+ "strings" |
|
| 11 |
+ "unicode" |
|
| 12 |
+ "unicode/utf8" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+// formatFakePointers controls whether to substitute pointer addresses with nil. |
|
| 16 |
+// This is used for deterministic testing. |
|
| 17 |
+var formatFakePointers = false |
|
| 18 |
+ |
|
| 19 |
+var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() |
|
| 20 |
+ |
|
| 21 |
+// Format formats the value v as a string. |
|
| 22 |
+// |
|
| 23 |
+// This is similar to fmt.Sprintf("%+v", v) except this:
|
|
| 24 |
+// * Prints the type unless it can be elided |
|
| 25 |
+// * Avoids printing struct fields that are zero |
|
| 26 |
+// * Prints a nil-slice as being nil, not empty |
|
| 27 |
+// * Prints map entries in deterministic order |
|
| 28 |
+func Format(v reflect.Value, useStringer bool) string {
|
|
| 29 |
+ return formatAny(v, formatConfig{useStringer, true, true, !formatFakePointers}, nil)
|
|
| 30 |
+} |
|
| 31 |
+ |
|
| 32 |
+type formatConfig struct {
|
|
| 33 |
+ useStringer bool // Should the String method be used if available? |
|
| 34 |
+ printType bool // Should we print the type before the value? |
|
| 35 |
+ followPointers bool // Should we recursively follow pointers? |
|
| 36 |
+ realPointers bool // Should we print the real address of pointers? |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) string {
|
|
| 40 |
+ // TODO: Should this be a multi-line printout in certain situations? |
|
| 41 |
+ |
|
| 42 |
+ if !v.IsValid() {
|
|
| 43 |
+ return "<non-existent>" |
|
| 44 |
+ } |
|
| 45 |
+ if conf.useStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
|
|
| 46 |
+ if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
|
|
| 47 |
+ return "<nil>" |
|
| 48 |
+ } |
|
| 49 |
+ return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String())
|
|
| 50 |
+ } |
|
| 51 |
+ |
|
| 52 |
+ switch v.Kind() {
|
|
| 53 |
+ case reflect.Bool: |
|
| 54 |
+ return formatPrimitive(v.Type(), v.Bool(), conf) |
|
| 55 |
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
| 56 |
+ return formatPrimitive(v.Type(), v.Int(), conf) |
|
| 57 |
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
|
| 58 |
+ if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr {
|
|
| 59 |
+ // Unnamed uints are usually bytes or words, so use hexadecimal. |
|
| 60 |
+ return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) |
|
| 61 |
+ } |
|
| 62 |
+ return formatPrimitive(v.Type(), v.Uint(), conf) |
|
| 63 |
+ case reflect.Float32, reflect.Float64: |
|
| 64 |
+ return formatPrimitive(v.Type(), v.Float(), conf) |
|
| 65 |
+ case reflect.Complex64, reflect.Complex128: |
|
| 66 |
+ return formatPrimitive(v.Type(), v.Complex(), conf) |
|
| 67 |
+ case reflect.String: |
|
| 68 |
+ return formatPrimitive(v.Type(), fmt.Sprintf("%q", v), conf)
|
|
| 69 |
+ case reflect.UnsafePointer, reflect.Chan, reflect.Func: |
|
| 70 |
+ return formatPointer(v, conf) |
|
| 71 |
+ case reflect.Ptr: |
|
| 72 |
+ if v.IsNil() {
|
|
| 73 |
+ if conf.printType {
|
|
| 74 |
+ return fmt.Sprintf("(%v)(nil)", v.Type())
|
|
| 75 |
+ } |
|
| 76 |
+ return "<nil>" |
|
| 77 |
+ } |
|
| 78 |
+ if visited[v.Pointer()] || !conf.followPointers {
|
|
| 79 |
+ return formatPointer(v, conf) |
|
| 80 |
+ } |
|
| 81 |
+ visited = insertPointer(visited, v.Pointer()) |
|
| 82 |
+ return "&" + formatAny(v.Elem(), conf, visited) |
|
| 83 |
+ case reflect.Interface: |
|
| 84 |
+ if v.IsNil() {
|
|
| 85 |
+ if conf.printType {
|
|
| 86 |
+ return fmt.Sprintf("%v(nil)", v.Type())
|
|
| 87 |
+ } |
|
| 88 |
+ return "<nil>" |
|
| 89 |
+ } |
|
| 90 |
+ return formatAny(v.Elem(), conf, visited) |
|
| 91 |
+ case reflect.Slice: |
|
| 92 |
+ if v.IsNil() {
|
|
| 93 |
+ if conf.printType {
|
|
| 94 |
+ return fmt.Sprintf("%v(nil)", v.Type())
|
|
| 95 |
+ } |
|
| 96 |
+ return "<nil>" |
|
| 97 |
+ } |
|
| 98 |
+ if visited[v.Pointer()] {
|
|
| 99 |
+ return formatPointer(v, conf) |
|
| 100 |
+ } |
|
| 101 |
+ visited = insertPointer(visited, v.Pointer()) |
|
| 102 |
+ fallthrough |
|
| 103 |
+ case reflect.Array: |
|
| 104 |
+ var ss []string |
|
| 105 |
+ subConf := conf |
|
| 106 |
+ subConf.printType = v.Type().Elem().Kind() == reflect.Interface |
|
| 107 |
+ for i := 0; i < v.Len(); i++ {
|
|
| 108 |
+ s := formatAny(v.Index(i), subConf, visited) |
|
| 109 |
+ ss = append(ss, s) |
|
| 110 |
+ } |
|
| 111 |
+ s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
|
| 112 |
+ if conf.printType {
|
|
| 113 |
+ return v.Type().String() + s |
|
| 114 |
+ } |
|
| 115 |
+ return s |
|
| 116 |
+ case reflect.Map: |
|
| 117 |
+ if v.IsNil() {
|
|
| 118 |
+ if conf.printType {
|
|
| 119 |
+ return fmt.Sprintf("%v(nil)", v.Type())
|
|
| 120 |
+ } |
|
| 121 |
+ return "<nil>" |
|
| 122 |
+ } |
|
| 123 |
+ if visited[v.Pointer()] {
|
|
| 124 |
+ return formatPointer(v, conf) |
|
| 125 |
+ } |
|
| 126 |
+ visited = insertPointer(visited, v.Pointer()) |
|
| 127 |
+ |
|
| 128 |
+ var ss []string |
|
| 129 |
+ subConf := conf |
|
| 130 |
+ subConf.printType = v.Type().Elem().Kind() == reflect.Interface |
|
| 131 |
+ for _, k := range SortKeys(v.MapKeys()) {
|
|
| 132 |
+ sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited)
|
|
| 133 |
+ sv := formatAny(v.MapIndex(k), subConf, visited) |
|
| 134 |
+ ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
|
|
| 135 |
+ } |
|
| 136 |
+ s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
|
| 137 |
+ if conf.printType {
|
|
| 138 |
+ return v.Type().String() + s |
|
| 139 |
+ } |
|
| 140 |
+ return s |
|
| 141 |
+ case reflect.Struct: |
|
| 142 |
+ var ss []string |
|
| 143 |
+ subConf := conf |
|
| 144 |
+ subConf.printType = true |
|
| 145 |
+ for i := 0; i < v.NumField(); i++ {
|
|
| 146 |
+ vv := v.Field(i) |
|
| 147 |
+ if isZero(vv) {
|
|
| 148 |
+ continue // Elide zero value fields |
|
| 149 |
+ } |
|
| 150 |
+ name := v.Type().Field(i).Name |
|
| 151 |
+ subConf.useStringer = conf.useStringer && isExported(name) |
|
| 152 |
+ s := formatAny(vv, subConf, visited) |
|
| 153 |
+ ss = append(ss, fmt.Sprintf("%s: %s", name, s))
|
|
| 154 |
+ } |
|
| 155 |
+ s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
|
| 156 |
+ if conf.printType {
|
|
| 157 |
+ return v.Type().String() + s |
|
| 158 |
+ } |
|
| 159 |
+ return s |
|
| 160 |
+ default: |
|
| 161 |
+ panic(fmt.Sprintf("%v kind not handled", v.Kind()))
|
|
| 162 |
+ } |
|
| 163 |
+} |
|
| 164 |
+ |
|
| 165 |
+func formatPrimitive(t reflect.Type, v interface{}, conf formatConfig) string {
|
|
| 166 |
+ if conf.printType && t.PkgPath() != "" {
|
|
| 167 |
+ return fmt.Sprintf("%v(%v)", t, v)
|
|
| 168 |
+ } |
|
| 169 |
+ return fmt.Sprintf("%v", v)
|
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+func formatPointer(v reflect.Value, conf formatConfig) string {
|
|
| 173 |
+ p := v.Pointer() |
|
| 174 |
+ if !conf.realPointers {
|
|
| 175 |
+ p = 0 // For deterministic printing purposes |
|
| 176 |
+ } |
|
| 177 |
+ s := formatHex(uint64(p)) |
|
| 178 |
+ if conf.printType {
|
|
| 179 |
+ return fmt.Sprintf("(%v)(%s)", v.Type(), s)
|
|
| 180 |
+ } |
|
| 181 |
+ return s |
|
| 182 |
+} |
|
| 183 |
+ |
|
| 184 |
+func formatHex(u uint64) string {
|
|
| 185 |
+ var f string |
|
| 186 |
+ switch {
|
|
| 187 |
+ case u <= 0xff: |
|
| 188 |
+ f = "0x%02x" |
|
| 189 |
+ case u <= 0xffff: |
|
| 190 |
+ f = "0x%04x" |
|
| 191 |
+ case u <= 0xffffff: |
|
| 192 |
+ f = "0x%06x" |
|
| 193 |
+ case u <= 0xffffffff: |
|
| 194 |
+ f = "0x%08x" |
|
| 195 |
+ case u <= 0xffffffffff: |
|
| 196 |
+ f = "0x%010x" |
|
| 197 |
+ case u <= 0xffffffffffff: |
|
| 198 |
+ f = "0x%012x" |
|
| 199 |
+ case u <= 0xffffffffffffff: |
|
| 200 |
+ f = "0x%014x" |
|
| 201 |
+ case u <= 0xffffffffffffffff: |
|
| 202 |
+ f = "0x%016x" |
|
| 203 |
+ } |
|
| 204 |
+ return fmt.Sprintf(f, u) |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+// insertPointer insert p into m, allocating m if necessary. |
|
| 208 |
+func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool {
|
|
| 209 |
+ if m == nil {
|
|
| 210 |
+ m = make(map[uintptr]bool) |
|
| 211 |
+ } |
|
| 212 |
+ m[p] = true |
|
| 213 |
+ return m |
|
| 214 |
+} |
|
| 215 |
+ |
|
| 216 |
+// isZero reports whether v is the zero value. |
|
| 217 |
+// This does not rely on Interface and so can be used on unexported fields. |
|
| 218 |
+func isZero(v reflect.Value) bool {
|
|
| 219 |
+ switch v.Kind() {
|
|
| 220 |
+ case reflect.Bool: |
|
| 221 |
+ return v.Bool() == false |
|
| 222 |
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
| 223 |
+ return v.Int() == 0 |
|
| 224 |
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
|
| 225 |
+ return v.Uint() == 0 |
|
| 226 |
+ case reflect.Float32, reflect.Float64: |
|
| 227 |
+ return v.Float() == 0 |
|
| 228 |
+ case reflect.Complex64, reflect.Complex128: |
|
| 229 |
+ return v.Complex() == 0 |
|
| 230 |
+ case reflect.String: |
|
| 231 |
+ return v.String() == "" |
|
| 232 |
+ case reflect.UnsafePointer: |
|
| 233 |
+ return v.Pointer() == 0 |
|
| 234 |
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: |
|
| 235 |
+ return v.IsNil() |
|
| 236 |
+ case reflect.Array: |
|
| 237 |
+ for i := 0; i < v.Len(); i++ {
|
|
| 238 |
+ if !isZero(v.Index(i)) {
|
|
| 239 |
+ return false |
|
| 240 |
+ } |
|
| 241 |
+ } |
|
| 242 |
+ return true |
|
| 243 |
+ case reflect.Struct: |
|
| 244 |
+ for i := 0; i < v.NumField(); i++ {
|
|
| 245 |
+ if !isZero(v.Field(i)) {
|
|
| 246 |
+ return false |
|
| 247 |
+ } |
|
| 248 |
+ } |
|
| 249 |
+ return true |
|
| 250 |
+ } |
|
| 251 |
+ return false |
|
| 252 |
+} |
|
| 253 |
+ |
|
| 254 |
+// isExported reports whether the identifier is exported. |
|
| 255 |
+func isExported(id string) bool {
|
|
| 256 |
+ r, _ := utf8.DecodeRuneInString(id) |
|
| 257 |
+ return unicode.IsUpper(r) |
|
| 258 |
+} |
| 0 | 259 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,111 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+package value |
|
| 5 |
+ |
|
| 6 |
+import ( |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "math" |
|
| 9 |
+ "reflect" |
|
| 10 |
+ "sort" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+// SortKeys sorts a list of map keys, deduplicating keys if necessary. |
|
| 14 |
+// The type of each value must be comparable. |
|
| 15 |
+func SortKeys(vs []reflect.Value) []reflect.Value {
|
|
| 16 |
+ if len(vs) == 0 {
|
|
| 17 |
+ return vs |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ // Sort the map keys. |
|
| 21 |
+ sort.Sort(valueSorter(vs)) |
|
| 22 |
+ |
|
| 23 |
+ // Deduplicate keys (fails for NaNs). |
|
| 24 |
+ vs2 := vs[:1] |
|
| 25 |
+ for _, v := range vs[1:] {
|
|
| 26 |
+ if v.Interface() != vs2[len(vs2)-1].Interface() {
|
|
| 27 |
+ vs2 = append(vs2, v) |
|
| 28 |
+ } |
|
| 29 |
+ } |
|
| 30 |
+ return vs2 |
|
| 31 |
+} |
|
| 32 |
+ |
|
| 33 |
+// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. |
|
| 34 |
+type valueSorter []reflect.Value |
|
| 35 |
+ |
|
| 36 |
+func (vs valueSorter) Len() int { return len(vs) }
|
|
| 37 |
+func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) }
|
|
| 38 |
+func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
|
| 39 |
+ |
|
| 40 |
+// isLess is a generic function for sorting arbitrary map keys. |
|
| 41 |
+// The inputs must be of the same type and must be comparable. |
|
| 42 |
+func isLess(x, y reflect.Value) bool {
|
|
| 43 |
+ switch x.Type().Kind() {
|
|
| 44 |
+ case reflect.Bool: |
|
| 45 |
+ return !x.Bool() && y.Bool() |
|
| 46 |
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
|
| 47 |
+ return x.Int() < y.Int() |
|
| 48 |
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
|
| 49 |
+ return x.Uint() < y.Uint() |
|
| 50 |
+ case reflect.Float32, reflect.Float64: |
|
| 51 |
+ fx, fy := x.Float(), y.Float() |
|
| 52 |
+ return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) |
|
| 53 |
+ case reflect.Complex64, reflect.Complex128: |
|
| 54 |
+ cx, cy := x.Complex(), y.Complex() |
|
| 55 |
+ rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) |
|
| 56 |
+ if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) {
|
|
| 57 |
+ return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) |
|
| 58 |
+ } |
|
| 59 |
+ return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) |
|
| 60 |
+ case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: |
|
| 61 |
+ return x.Pointer() < y.Pointer() |
|
| 62 |
+ case reflect.String: |
|
| 63 |
+ return x.String() < y.String() |
|
| 64 |
+ case reflect.Array: |
|
| 65 |
+ for i := 0; i < x.Len(); i++ {
|
|
| 66 |
+ if isLess(x.Index(i), y.Index(i)) {
|
|
| 67 |
+ return true |
|
| 68 |
+ } |
|
| 69 |
+ if isLess(y.Index(i), x.Index(i)) {
|
|
| 70 |
+ return false |
|
| 71 |
+ } |
|
| 72 |
+ } |
|
| 73 |
+ return false |
|
| 74 |
+ case reflect.Struct: |
|
| 75 |
+ for i := 0; i < x.NumField(); i++ {
|
|
| 76 |
+ if isLess(x.Field(i), y.Field(i)) {
|
|
| 77 |
+ return true |
|
| 78 |
+ } |
|
| 79 |
+ if isLess(y.Field(i), x.Field(i)) {
|
|
| 80 |
+ return false |
|
| 81 |
+ } |
|
| 82 |
+ } |
|
| 83 |
+ return false |
|
| 84 |
+ case reflect.Interface: |
|
| 85 |
+ vx, vy := x.Elem(), y.Elem() |
|
| 86 |
+ if !vx.IsValid() || !vy.IsValid() {
|
|
| 87 |
+ return !vx.IsValid() && vy.IsValid() |
|
| 88 |
+ } |
|
| 89 |
+ tx, ty := vx.Type(), vy.Type() |
|
| 90 |
+ if tx == ty {
|
|
| 91 |
+ return isLess(x.Elem(), y.Elem()) |
|
| 92 |
+ } |
|
| 93 |
+ if tx.Kind() != ty.Kind() {
|
|
| 94 |
+ return vx.Kind() < vy.Kind() |
|
| 95 |
+ } |
|
| 96 |
+ if tx.String() != ty.String() {
|
|
| 97 |
+ return tx.String() < ty.String() |
|
| 98 |
+ } |
|
| 99 |
+ if tx.PkgPath() != ty.PkgPath() {
|
|
| 100 |
+ return tx.PkgPath() < ty.PkgPath() |
|
| 101 |
+ } |
|
| 102 |
+ // This can happen in rare situations, so we fallback to just comparing |
|
| 103 |
+ // the unique pointer for a reflect.Type. This guarantees deterministic |
|
| 104 |
+ // ordering within a program, but it is obviously not stable. |
|
| 105 |
+ return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() |
|
| 106 |
+ default: |
|
| 107 |
+ // Must be Func, Map, or Slice; which are not comparable. |
|
| 108 |
+ panic(fmt.Sprintf("%T is not comparable", x.Type()))
|
|
| 109 |
+ } |
|
| 110 |
+} |
| 0 | 111 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,446 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+package cmp |
|
| 5 |
+ |
|
| 6 |
+import ( |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "reflect" |
|
| 9 |
+ "runtime" |
|
| 10 |
+ "strings" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/google/go-cmp/cmp/internal/function" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+// Option configures for specific behavior of Equal and Diff. In particular, |
|
| 16 |
+// the fundamental Option functions (Ignore, Transformer, and Comparer), |
|
| 17 |
+// configure how equality is determined. |
|
| 18 |
+// |
|
| 19 |
+// The fundamental options may be composed with filters (FilterPath and |
|
| 20 |
+// FilterValues) to control the scope over which they are applied. |
|
| 21 |
+// |
|
| 22 |
+// The cmp/cmpopts package provides helper functions for creating options that |
|
| 23 |
+// may be used with Equal and Diff. |
|
| 24 |
+type Option interface {
|
|
| 25 |
+ // filter applies all filters and returns the option that remains. |
|
| 26 |
+ // Each option may only read s.curPath and call s.callTTBFunc. |
|
| 27 |
+ // |
|
| 28 |
+ // An Options is returned only if multiple comparers or transformers |
|
| 29 |
+ // can apply simultaneously and will only contain values of those types |
|
| 30 |
+ // or sub-Options containing values of those types. |
|
| 31 |
+ filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// applicableOption represents the following types: |
|
| 35 |
+// Fundamental: ignore | invalid | *comparer | *transformer |
|
| 36 |
+// Grouping: Options |
|
| 37 |
+type applicableOption interface {
|
|
| 38 |
+ Option |
|
| 39 |
+ |
|
| 40 |
+ // apply executes the option and reports whether the option was applied. |
|
| 41 |
+ // Each option may mutate s. |
|
| 42 |
+ apply(s *state, vx, vy reflect.Value) bool |
|
| 43 |
+} |
|
| 44 |
+ |
|
| 45 |
+// coreOption represents the following types: |
|
| 46 |
+// Fundamental: ignore | invalid | *comparer | *transformer |
|
| 47 |
+// Filters: *pathFilter | *valuesFilter |
|
| 48 |
+type coreOption interface {
|
|
| 49 |
+ Option |
|
| 50 |
+ isCore() |
|
| 51 |
+} |
|
| 52 |
+ |
|
| 53 |
+type core struct{}
|
|
| 54 |
+ |
|
| 55 |
+func (core) isCore() {}
|
|
| 56 |
+ |
|
| 57 |
+// Options is a list of Option values that also satisfies the Option interface. |
|
| 58 |
+// Helper comparison packages may return an Options value when packing multiple |
|
| 59 |
+// Option values into a single Option. When this package processes an Options, |
|
| 60 |
+// it will be implicitly expanded into a flat list. |
|
| 61 |
+// |
|
| 62 |
+// Applying a filter on an Options is equivalent to applying that same filter |
|
| 63 |
+// on all individual options held within. |
|
| 64 |
+type Options []Option |
|
| 65 |
+ |
|
| 66 |
+func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) {
|
|
| 67 |
+ for _, opt := range opts {
|
|
| 68 |
+ switch opt := opt.filter(s, vx, vy, t); opt.(type) {
|
|
| 69 |
+ case ignore: |
|
| 70 |
+ return ignore{} // Only ignore can short-circuit evaluation
|
|
| 71 |
+ case invalid: |
|
| 72 |
+ out = invalid{} // Takes precedence over comparer or transformer
|
|
| 73 |
+ case *comparer, *transformer, Options: |
|
| 74 |
+ switch out.(type) {
|
|
| 75 |
+ case nil: |
|
| 76 |
+ out = opt |
|
| 77 |
+ case invalid: |
|
| 78 |
+ // Keep invalid |
|
| 79 |
+ case *comparer, *transformer, Options: |
|
| 80 |
+ out = Options{out, opt} // Conflicting comparers or transformers
|
|
| 81 |
+ } |
|
| 82 |
+ } |
|
| 83 |
+ } |
|
| 84 |
+ return out |
|
| 85 |
+} |
|
| 86 |
+ |
|
| 87 |
+func (opts Options) apply(s *state, _, _ reflect.Value) bool {
|
|
| 88 |
+ const warning = "ambiguous set of applicable options" |
|
| 89 |
+ const help = "consider using filters to ensure at most one Comparer or Transformer may apply" |
|
| 90 |
+ var ss []string |
|
| 91 |
+ for _, opt := range flattenOptions(nil, opts) {
|
|
| 92 |
+ ss = append(ss, fmt.Sprint(opt)) |
|
| 93 |
+ } |
|
| 94 |
+ set := strings.Join(ss, "\n\t") |
|
| 95 |
+ panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help))
|
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+func (opts Options) String() string {
|
|
| 99 |
+ var ss []string |
|
| 100 |
+ for _, opt := range opts {
|
|
| 101 |
+ ss = append(ss, fmt.Sprint(opt)) |
|
| 102 |
+ } |
|
| 103 |
+ return fmt.Sprintf("Options{%s}", strings.Join(ss, ", "))
|
|
| 104 |
+} |
|
| 105 |
+ |
|
| 106 |
+// FilterPath returns a new Option where opt is only evaluated if filter f |
|
| 107 |
+// returns true for the current Path in the value tree. |
|
| 108 |
+// |
|
| 109 |
+// The option passed in may be an Ignore, Transformer, Comparer, Options, or |
|
| 110 |
+// a previously filtered Option. |
|
| 111 |
+func FilterPath(f func(Path) bool, opt Option) Option {
|
|
| 112 |
+ if f == nil {
|
|
| 113 |
+ panic("invalid path filter function")
|
|
| 114 |
+ } |
|
| 115 |
+ if opt := normalizeOption(opt); opt != nil {
|
|
| 116 |
+ return &pathFilter{fnc: f, opt: opt}
|
|
| 117 |
+ } |
|
| 118 |
+ return nil |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+type pathFilter struct {
|
|
| 122 |
+ core |
|
| 123 |
+ fnc func(Path) bool |
|
| 124 |
+ opt Option |
|
| 125 |
+} |
|
| 126 |
+ |
|
| 127 |
+func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
|
| 128 |
+ if f.fnc(s.curPath) {
|
|
| 129 |
+ return f.opt.filter(s, vx, vy, t) |
|
| 130 |
+ } |
|
| 131 |
+ return nil |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 134 |
+func (f pathFilter) String() string {
|
|
| 135 |
+ fn := getFuncName(reflect.ValueOf(f.fnc).Pointer()) |
|
| 136 |
+ return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt)
|
|
| 137 |
+} |
|
| 138 |
+ |
|
| 139 |
+// FilterValues returns a new Option where opt is only evaluated if filter f, |
|
| 140 |
+// which is a function of the form "func(T, T) bool", returns true for the |
|
| 141 |
+// current pair of values being compared. If the type of the values is not |
|
| 142 |
+// assignable to T, then this filter implicitly returns false. |
|
| 143 |
+// |
|
| 144 |
+// The filter function must be |
|
| 145 |
+// symmetric (i.e., agnostic to the order of the inputs) and |
|
| 146 |
+// deterministic (i.e., produces the same result when given the same inputs). |
|
| 147 |
+// If T is an interface, it is possible that f is called with two values with |
|
| 148 |
+// different concrete types that both implement T. |
|
| 149 |
+// |
|
| 150 |
+// The option passed in may be an Ignore, Transformer, Comparer, Options, or |
|
| 151 |
+// a previously filtered Option. |
|
| 152 |
+func FilterValues(f interface{}, opt Option) Option {
|
|
| 153 |
+ v := reflect.ValueOf(f) |
|
| 154 |
+ if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
|
|
| 155 |
+ panic(fmt.Sprintf("invalid values filter function: %T", f))
|
|
| 156 |
+ } |
|
| 157 |
+ if opt := normalizeOption(opt); opt != nil {
|
|
| 158 |
+ vf := &valuesFilter{fnc: v, opt: opt}
|
|
| 159 |
+ if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
|
| 160 |
+ vf.typ = ti |
|
| 161 |
+ } |
|
| 162 |
+ return vf |
|
| 163 |
+ } |
|
| 164 |
+ return nil |
|
| 165 |
+} |
|
| 166 |
+ |
|
| 167 |
+type valuesFilter struct {
|
|
| 168 |
+ core |
|
| 169 |
+ typ reflect.Type // T |
|
| 170 |
+ fnc reflect.Value // func(T, T) bool |
|
| 171 |
+ opt Option |
|
| 172 |
+} |
|
| 173 |
+ |
|
| 174 |
+func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
|
| 175 |
+ if !vx.IsValid() || !vy.IsValid() {
|
|
| 176 |
+ return invalid{}
|
|
| 177 |
+ } |
|
| 178 |
+ if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) {
|
|
| 179 |
+ return f.opt.filter(s, vx, vy, t) |
|
| 180 |
+ } |
|
| 181 |
+ return nil |
|
| 182 |
+} |
|
| 183 |
+ |
|
| 184 |
+func (f valuesFilter) String() string {
|
|
| 185 |
+ fn := getFuncName(f.fnc.Pointer()) |
|
| 186 |
+ return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
|
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+// Ignore is an Option that causes all comparisons to be ignored. |
|
| 190 |
+// This value is intended to be combined with FilterPath or FilterValues. |
|
| 191 |
+// It is an error to pass an unfiltered Ignore option to Equal. |
|
| 192 |
+func Ignore() Option { return ignore{} }
|
|
| 193 |
+ |
|
| 194 |
+type ignore struct{ core }
|
|
| 195 |
+ |
|
| 196 |
+func (ignore) isFiltered() bool { return false }
|
|
| 197 |
+func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} }
|
|
| 198 |
+func (ignore) apply(_ *state, _, _ reflect.Value) bool { return true }
|
|
| 199 |
+func (ignore) String() string { return "Ignore()" }
|
|
| 200 |
+ |
|
| 201 |
+// invalid is a sentinel Option type to indicate that some options could not |
|
| 202 |
+// be evaluated due to unexported fields. |
|
| 203 |
+type invalid struct{ core }
|
|
| 204 |
+ |
|
| 205 |
+func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} }
|
|
| 206 |
+func (invalid) apply(s *state, _, _ reflect.Value) bool {
|
|
| 207 |
+ const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" |
|
| 208 |
+ panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
|
|
| 209 |
+} |
|
| 210 |
+ |
|
| 211 |
+// Transformer returns an Option that applies a transformation function that |
|
| 212 |
+// converts values of a certain type into that of another. |
|
| 213 |
+// |
|
| 214 |
+// The transformer f must be a function "func(T) R" that converts values of |
|
| 215 |
+// type T to those of type R and is implicitly filtered to input values |
|
| 216 |
+// assignable to T. The transformer must not mutate T in any way. |
|
| 217 |
+// If T and R are the same type, an additional filter must be applied to |
|
| 218 |
+// act as the base case to prevent an infinite recursion applying the same |
|
| 219 |
+// transform to itself (see the SortedSlice example). |
|
| 220 |
+// |
|
| 221 |
+// The name is a user provided label that is used as the Transform.Name in the |
|
| 222 |
+// transformation PathStep. If empty, an arbitrary name is used. |
|
| 223 |
+func Transformer(name string, f interface{}) Option {
|
|
| 224 |
+ v := reflect.ValueOf(f) |
|
| 225 |
+ if !function.IsType(v.Type(), function.Transformer) || v.IsNil() {
|
|
| 226 |
+ panic(fmt.Sprintf("invalid transformer function: %T", f))
|
|
| 227 |
+ } |
|
| 228 |
+ if name == "" {
|
|
| 229 |
+ name = "λ" // Lambda-symbol as place-holder for anonymous transformer |
|
| 230 |
+ } |
|
| 231 |
+ if !isValid(name) {
|
|
| 232 |
+ panic(fmt.Sprintf("invalid name: %q", name))
|
|
| 233 |
+ } |
|
| 234 |
+ tr := &transformer{name: name, fnc: reflect.ValueOf(f)}
|
|
| 235 |
+ if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
|
| 236 |
+ tr.typ = ti |
|
| 237 |
+ } |
|
| 238 |
+ return tr |
|
| 239 |
+} |
|
| 240 |
+ |
|
| 241 |
+type transformer struct {
|
|
| 242 |
+ core |
|
| 243 |
+ name string |
|
| 244 |
+ typ reflect.Type // T |
|
| 245 |
+ fnc reflect.Value // func(T) R |
|
| 246 |
+} |
|
| 247 |
+ |
|
| 248 |
+func (tr *transformer) isFiltered() bool { return tr.typ != nil }
|
|
| 249 |
+ |
|
| 250 |
+func (tr *transformer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
|
| 251 |
+ if tr.typ == nil || t.AssignableTo(tr.typ) {
|
|
| 252 |
+ return tr |
|
| 253 |
+ } |
|
| 254 |
+ return nil |
|
| 255 |
+} |
|
| 256 |
+ |
|
| 257 |
+func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool {
|
|
| 258 |
+ // Update path before calling the Transformer so that dynamic checks |
|
| 259 |
+ // will use the updated path. |
|
| 260 |
+ s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr})
|
|
| 261 |
+ defer s.curPath.pop() |
|
| 262 |
+ |
|
| 263 |
+ vx = s.callTRFunc(tr.fnc, vx) |
|
| 264 |
+ vy = s.callTRFunc(tr.fnc, vy) |
|
| 265 |
+ s.compareAny(vx, vy) |
|
| 266 |
+ return true |
|
| 267 |
+} |
|
| 268 |
+ |
|
| 269 |
+func (tr transformer) String() string {
|
|
| 270 |
+ return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer()))
|
|
| 271 |
+} |
|
| 272 |
+ |
|
| 273 |
+// Comparer returns an Option that determines whether two values are equal |
|
| 274 |
+// to each other. |
|
| 275 |
+// |
|
| 276 |
+// The comparer f must be a function "func(T, T) bool" and is implicitly |
|
| 277 |
+// filtered to input values assignable to T. If T is an interface, it is |
|
| 278 |
+// possible that f is called with two values of different concrete types that |
|
| 279 |
+// both implement T. |
|
| 280 |
+// |
|
| 281 |
+// The equality function must be: |
|
| 282 |
+// • Symmetric: equal(x, y) == equal(y, x) |
|
| 283 |
+// • Deterministic: equal(x, y) == equal(x, y) |
|
| 284 |
+// • Pure: equal(x, y) does not modify x or y |
|
| 285 |
+func Comparer(f interface{}) Option {
|
|
| 286 |
+ v := reflect.ValueOf(f) |
|
| 287 |
+ if !function.IsType(v.Type(), function.Equal) || v.IsNil() {
|
|
| 288 |
+ panic(fmt.Sprintf("invalid comparer function: %T", f))
|
|
| 289 |
+ } |
|
| 290 |
+ cm := &comparer{fnc: v}
|
|
| 291 |
+ if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
|
| 292 |
+ cm.typ = ti |
|
| 293 |
+ } |
|
| 294 |
+ return cm |
|
| 295 |
+} |
|
| 296 |
+ |
|
| 297 |
+type comparer struct {
|
|
| 298 |
+ core |
|
| 299 |
+ typ reflect.Type // T |
|
| 300 |
+ fnc reflect.Value // func(T, T) bool |
|
| 301 |
+} |
|
| 302 |
+ |
|
| 303 |
+func (cm *comparer) isFiltered() bool { return cm.typ != nil }
|
|
| 304 |
+ |
|
| 305 |
+func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
|
| 306 |
+ if cm.typ == nil || t.AssignableTo(cm.typ) {
|
|
| 307 |
+ return cm |
|
| 308 |
+ } |
|
| 309 |
+ return nil |
|
| 310 |
+} |
|
| 311 |
+ |
|
| 312 |
+func (cm *comparer) apply(s *state, vx, vy reflect.Value) bool {
|
|
| 313 |
+ eq := s.callTTBFunc(cm.fnc, vx, vy) |
|
| 314 |
+ s.report(eq, vx, vy) |
|
| 315 |
+ return true |
|
| 316 |
+} |
|
| 317 |
+ |
|
| 318 |
+func (cm comparer) String() string {
|
|
| 319 |
+ return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer()))
|
|
| 320 |
+} |
|
| 321 |
+ |
|
| 322 |
+// AllowUnexported returns an Option that forcibly allows operations on |
|
| 323 |
+// unexported fields in certain structs, which are specified by passing in a |
|
| 324 |
+// value of each struct type. |
|
| 325 |
+// |
|
| 326 |
+// Users of this option must understand that comparing on unexported fields |
|
| 327 |
+// from external packages is not safe since changes in the internal |
|
| 328 |
+// implementation of some external package may cause the result of Equal |
|
| 329 |
+// to unexpectedly change. However, it may be valid to use this option on types |
|
| 330 |
+// defined in an internal package where the semantic meaning of an unexported |
|
| 331 |
+// field is in the control of the user. |
|
| 332 |
+// |
|
| 333 |
+// For some cases, a custom Comparer should be used instead that defines |
|
| 334 |
+// equality as a function of the public API of a type rather than the underlying |
|
| 335 |
+// unexported implementation. |
|
| 336 |
+// |
|
| 337 |
+// For example, the reflect.Type documentation defines equality to be determined |
|
| 338 |
+// by the == operator on the interface (essentially performing a shallow pointer |
|
| 339 |
+// comparison) and most attempts to compare *regexp.Regexp types are interested |
|
| 340 |
+// in only checking that the regular expression strings are equal. |
|
| 341 |
+// Both of these are accomplished using Comparers: |
|
| 342 |
+// |
|
| 343 |
+// Comparer(func(x, y reflect.Type) bool { return x == y })
|
|
| 344 |
+// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() })
|
|
| 345 |
+// |
|
| 346 |
+// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore |
|
| 347 |
+// all unexported fields on specified struct types. |
|
| 348 |
+func AllowUnexported(types ...interface{}) Option {
|
|
| 349 |
+ if !supportAllowUnexported {
|
|
| 350 |
+ panic("AllowUnexported is not supported on App Engine Classic or GopherJS")
|
|
| 351 |
+ } |
|
| 352 |
+ m := make(map[reflect.Type]bool) |
|
| 353 |
+ for _, typ := range types {
|
|
| 354 |
+ t := reflect.TypeOf(typ) |
|
| 355 |
+ if t.Kind() != reflect.Struct {
|
|
| 356 |
+ panic(fmt.Sprintf("invalid struct type: %T", typ))
|
|
| 357 |
+ } |
|
| 358 |
+ m[t] = true |
|
| 359 |
+ } |
|
| 360 |
+ return visibleStructs(m) |
|
| 361 |
+} |
|
| 362 |
+ |
|
| 363 |
+type visibleStructs map[reflect.Type]bool |
|
| 364 |
+ |
|
| 365 |
+func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption {
|
|
| 366 |
+ panic("not implemented")
|
|
| 367 |
+} |
|
| 368 |
+ |
|
| 369 |
+// reporter is an Option that configures how differences are reported. |
|
| 370 |
+type reporter interface {
|
|
| 371 |
+ // TODO: Not exported yet. |
|
| 372 |
+ // |
|
| 373 |
+ // Perhaps add PushStep and PopStep and change Report to only accept |
|
| 374 |
+ // a PathStep instead of the full-path? Adding a PushStep and PopStep makes |
|
| 375 |
+ // it clear that we are traversing the value tree in a depth-first-search |
|
| 376 |
+ // manner, which has an effect on how values are printed. |
|
| 377 |
+ |
|
| 378 |
+ Option |
|
| 379 |
+ |
|
| 380 |
+ // Report is called for every comparison made and will be provided with |
|
| 381 |
+ // the two values being compared, the equality result, and the |
|
| 382 |
+ // current path in the value tree. It is possible for x or y to be an |
|
| 383 |
+ // invalid reflect.Value if one of the values is non-existent; |
|
| 384 |
+ // which is possible with maps and slices. |
|
| 385 |
+ Report(x, y reflect.Value, eq bool, p Path) |
|
| 386 |
+} |
|
| 387 |
+ |
|
| 388 |
+// normalizeOption normalizes the input options such that all Options groups |
|
| 389 |
+// are flattened and groups with a single element are reduced to that element. |
|
| 390 |
+// Only coreOptions and Options containing coreOptions are allowed. |
|
| 391 |
+func normalizeOption(src Option) Option {
|
|
| 392 |
+ switch opts := flattenOptions(nil, Options{src}); len(opts) {
|
|
| 393 |
+ case 0: |
|
| 394 |
+ return nil |
|
| 395 |
+ case 1: |
|
| 396 |
+ return opts[0] |
|
| 397 |
+ default: |
|
| 398 |
+ return opts |
|
| 399 |
+ } |
|
| 400 |
+} |
|
| 401 |
+ |
|
| 402 |
+// flattenOptions copies all options in src to dst as a flat list. |
|
| 403 |
+// Only coreOptions and Options containing coreOptions are allowed. |
|
| 404 |
+func flattenOptions(dst, src Options) Options {
|
|
| 405 |
+ for _, opt := range src {
|
|
| 406 |
+ switch opt := opt.(type) {
|
|
| 407 |
+ case nil: |
|
| 408 |
+ continue |
|
| 409 |
+ case Options: |
|
| 410 |
+ dst = flattenOptions(dst, opt) |
|
| 411 |
+ case coreOption: |
|
| 412 |
+ dst = append(dst, opt) |
|
| 413 |
+ default: |
|
| 414 |
+ panic(fmt.Sprintf("invalid option type: %T", opt))
|
|
| 415 |
+ } |
|
| 416 |
+ } |
|
| 417 |
+ return dst |
|
| 418 |
+} |
|
| 419 |
+ |
|
| 420 |
+// getFuncName returns a short function name from the pointer. |
|
| 421 |
+// The string parsing logic works up until Go1.9. |
|
| 422 |
+func getFuncName(p uintptr) string {
|
|
| 423 |
+ fnc := runtime.FuncForPC(p) |
|
| 424 |
+ if fnc == nil {
|
|
| 425 |
+ return "<unknown>" |
|
| 426 |
+ } |
|
| 427 |
+ name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm" |
|
| 428 |
+ if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") {
|
|
| 429 |
+ // Strip the package name from method name. |
|
| 430 |
+ name = strings.TrimSuffix(name, ")-fm") |
|
| 431 |
+ name = strings.TrimSuffix(name, ")·fm") |
|
| 432 |
+ if i := strings.LastIndexByte(name, '('); i >= 0 {
|
|
| 433 |
+ methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc" |
|
| 434 |
+ if j := strings.LastIndexByte(methodName, '.'); j >= 0 {
|
|
| 435 |
+ methodName = methodName[j+1:] // E.g., "myfunc" |
|
| 436 |
+ } |
|
| 437 |
+ name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc" |
|
| 438 |
+ } |
|
| 439 |
+ } |
|
| 440 |
+ if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
|
| 441 |
+ // Strip the package name. |
|
| 442 |
+ name = name[i+1:] // E.g., "mypkg.(mytype).myfunc" |
|
| 443 |
+ } |
|
| 444 |
+ return name |
|
| 445 |
+} |
| 0 | 446 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,293 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+package cmp |
|
| 5 |
+ |
|
| 6 |
+import ( |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "reflect" |
|
| 9 |
+ "strings" |
|
| 10 |
+ "unicode" |
|
| 11 |
+ "unicode/utf8" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type ( |
|
| 15 |
+ // Path is a list of PathSteps describing the sequence of operations to get |
|
| 16 |
+ // from some root type to the current position in the value tree. |
|
| 17 |
+ // The first Path element is always an operation-less PathStep that exists |
|
| 18 |
+ // simply to identify the initial type. |
|
| 19 |
+ // |
|
| 20 |
+ // When traversing structs with embedded structs, the embedded struct will |
|
| 21 |
+ // always be accessed as a field before traversing the fields of the |
|
| 22 |
+ // embedded struct themselves. That is, an exported field from the |
|
| 23 |
+ // embedded struct will never be accessed directly from the parent struct. |
|
| 24 |
+ Path []PathStep |
|
| 25 |
+ |
|
| 26 |
+ // PathStep is a union-type for specific operations to traverse |
|
| 27 |
+ // a value's tree structure. Users of this package never need to implement |
|
| 28 |
+ // these types as values of this type will be returned by this package. |
|
| 29 |
+ PathStep interface {
|
|
| 30 |
+ String() string |
|
| 31 |
+ Type() reflect.Type // Resulting type after performing the path step |
|
| 32 |
+ isPathStep() |
|
| 33 |
+ } |
|
| 34 |
+ |
|
| 35 |
+ // SliceIndex is an index operation on a slice or array at some index Key. |
|
| 36 |
+ SliceIndex interface {
|
|
| 37 |
+ PathStep |
|
| 38 |
+ Key() int // May return -1 if in a split state |
|
| 39 |
+ |
|
| 40 |
+ // SplitKeys returns the indexes for indexing into slices in the |
|
| 41 |
+ // x and y values, respectively. These indexes may differ due to the |
|
| 42 |
+ // insertion or removal of an element in one of the slices, causing |
|
| 43 |
+ // all of the indexes to be shifted. If an index is -1, then that |
|
| 44 |
+ // indicates that the element does not exist in the associated slice. |
|
| 45 |
+ // |
|
| 46 |
+ // Key is guaranteed to return -1 if and only if the indexes returned |
|
| 47 |
+ // by SplitKeys are not the same. SplitKeys will never return -1 for |
|
| 48 |
+ // both indexes. |
|
| 49 |
+ SplitKeys() (x int, y int) |
|
| 50 |
+ |
|
| 51 |
+ isSliceIndex() |
|
| 52 |
+ } |
|
| 53 |
+ // MapIndex is an index operation on a map at some index Key. |
|
| 54 |
+ MapIndex interface {
|
|
| 55 |
+ PathStep |
|
| 56 |
+ Key() reflect.Value |
|
| 57 |
+ isMapIndex() |
|
| 58 |
+ } |
|
| 59 |
+ // TypeAssertion represents a type assertion on an interface. |
|
| 60 |
+ TypeAssertion interface {
|
|
| 61 |
+ PathStep |
|
| 62 |
+ isTypeAssertion() |
|
| 63 |
+ } |
|
| 64 |
+ // StructField represents a struct field access on a field called Name. |
|
| 65 |
+ StructField interface {
|
|
| 66 |
+ PathStep |
|
| 67 |
+ Name() string |
|
| 68 |
+ Index() int |
|
| 69 |
+ isStructField() |
|
| 70 |
+ } |
|
| 71 |
+ // Indirect represents pointer indirection on the parent type. |
|
| 72 |
+ Indirect interface {
|
|
| 73 |
+ PathStep |
|
| 74 |
+ isIndirect() |
|
| 75 |
+ } |
|
| 76 |
+ // Transform is a transformation from the parent type to the current type. |
|
| 77 |
+ Transform interface {
|
|
| 78 |
+ PathStep |
|
| 79 |
+ Name() string |
|
| 80 |
+ Func() reflect.Value |
|
| 81 |
+ isTransform() |
|
| 82 |
+ } |
|
| 83 |
+) |
|
| 84 |
+ |
|
| 85 |
+func (pa *Path) push(s PathStep) {
|
|
| 86 |
+ *pa = append(*pa, s) |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 89 |
+func (pa *Path) pop() {
|
|
| 90 |
+ *pa = (*pa)[:len(*pa)-1] |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// Last returns the last PathStep in the Path. |
|
| 94 |
+// If the path is empty, this returns a non-nil PathStep that reports a nil Type. |
|
| 95 |
+func (pa Path) Last() PathStep {
|
|
| 96 |
+ if len(pa) > 0 {
|
|
| 97 |
+ return pa[len(pa)-1] |
|
| 98 |
+ } |
|
| 99 |
+ return pathStep{}
|
|
| 100 |
+} |
|
| 101 |
+ |
|
| 102 |
+// String returns the simplified path to a node. |
|
| 103 |
+// The simplified path only contains struct field accesses. |
|
| 104 |
+// |
|
| 105 |
+// For example: |
|
| 106 |
+// MyMap.MySlices.MyField |
|
| 107 |
+func (pa Path) String() string {
|
|
| 108 |
+ var ss []string |
|
| 109 |
+ for _, s := range pa {
|
|
| 110 |
+ if _, ok := s.(*structField); ok {
|
|
| 111 |
+ ss = append(ss, s.String()) |
|
| 112 |
+ } |
|
| 113 |
+ } |
|
| 114 |
+ return strings.TrimPrefix(strings.Join(ss, ""), ".") |
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+// GoString returns the path to a specific node using Go syntax. |
|
| 118 |
+// |
|
| 119 |
+// For example: |
|
| 120 |
+// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField |
|
| 121 |
+func (pa Path) GoString() string {
|
|
| 122 |
+ var ssPre, ssPost []string |
|
| 123 |
+ var numIndirect int |
|
| 124 |
+ for i, s := range pa {
|
|
| 125 |
+ var nextStep PathStep |
|
| 126 |
+ if i+1 < len(pa) {
|
|
| 127 |
+ nextStep = pa[i+1] |
|
| 128 |
+ } |
|
| 129 |
+ switch s := s.(type) {
|
|
| 130 |
+ case *indirect: |
|
| 131 |
+ numIndirect++ |
|
| 132 |
+ pPre, pPost := "(", ")"
|
|
| 133 |
+ switch nextStep.(type) {
|
|
| 134 |
+ case *indirect: |
|
| 135 |
+ continue // Next step is indirection, so let them batch up |
|
| 136 |
+ case *structField: |
|
| 137 |
+ numIndirect-- // Automatic indirection on struct fields |
|
| 138 |
+ case nil: |
|
| 139 |
+ pPre, pPost = "", "" // Last step; no need for parenthesis |
|
| 140 |
+ } |
|
| 141 |
+ if numIndirect > 0 {
|
|
| 142 |
+ ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect))
|
|
| 143 |
+ ssPost = append(ssPost, pPost) |
|
| 144 |
+ } |
|
| 145 |
+ numIndirect = 0 |
|
| 146 |
+ continue |
|
| 147 |
+ case *transform: |
|
| 148 |
+ ssPre = append(ssPre, s.trans.name+"(")
|
|
| 149 |
+ ssPost = append(ssPost, ")") |
|
| 150 |
+ continue |
|
| 151 |
+ case *typeAssertion: |
|
| 152 |
+ // Elide type assertions immediately following a transform to |
|
| 153 |
+ // prevent overly verbose path printouts. |
|
| 154 |
+ // Some transforms return interface{} because of Go's lack of
|
|
| 155 |
+ // generics, but typically take in and return the exact same |
|
| 156 |
+ // concrete type. Other times, the transform creates an anonymous |
|
| 157 |
+ // struct, which will be very verbose to print. |
|
| 158 |
+ if _, ok := nextStep.(*transform); ok {
|
|
| 159 |
+ continue |
|
| 160 |
+ } |
|
| 161 |
+ } |
|
| 162 |
+ ssPost = append(ssPost, s.String()) |
|
| 163 |
+ } |
|
| 164 |
+ for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 {
|
|
| 165 |
+ ssPre[i], ssPre[j] = ssPre[j], ssPre[i] |
|
| 166 |
+ } |
|
| 167 |
+ return strings.Join(ssPre, "") + strings.Join(ssPost, "") |
|
| 168 |
+} |
|
| 169 |
+ |
|
| 170 |
+type ( |
|
| 171 |
+ pathStep struct {
|
|
| 172 |
+ typ reflect.Type |
|
| 173 |
+ } |
|
| 174 |
+ |
|
| 175 |
+ sliceIndex struct {
|
|
| 176 |
+ pathStep |
|
| 177 |
+ xkey, ykey int |
|
| 178 |
+ } |
|
| 179 |
+ mapIndex struct {
|
|
| 180 |
+ pathStep |
|
| 181 |
+ key reflect.Value |
|
| 182 |
+ } |
|
| 183 |
+ typeAssertion struct {
|
|
| 184 |
+ pathStep |
|
| 185 |
+ } |
|
| 186 |
+ structField struct {
|
|
| 187 |
+ pathStep |
|
| 188 |
+ name string |
|
| 189 |
+ idx int |
|
| 190 |
+ |
|
| 191 |
+ // These fields are used for forcibly accessing an unexported field. |
|
| 192 |
+ // pvx, pvy, and field are only valid if unexported is true. |
|
| 193 |
+ unexported bool |
|
| 194 |
+ force bool // Forcibly allow visibility |
|
| 195 |
+ pvx, pvy reflect.Value // Parent values |
|
| 196 |
+ field reflect.StructField // Field information |
|
| 197 |
+ } |
|
| 198 |
+ indirect struct {
|
|
| 199 |
+ pathStep |
|
| 200 |
+ } |
|
| 201 |
+ transform struct {
|
|
| 202 |
+ pathStep |
|
| 203 |
+ trans *transformer |
|
| 204 |
+ } |
|
| 205 |
+) |
|
| 206 |
+ |
|
| 207 |
+func (ps pathStep) Type() reflect.Type { return ps.typ }
|
|
| 208 |
+func (ps pathStep) String() string {
|
|
| 209 |
+ if ps.typ == nil {
|
|
| 210 |
+ return "<nil>" |
|
| 211 |
+ } |
|
| 212 |
+ s := ps.typ.String() |
|
| 213 |
+ if s == "" || strings.ContainsAny(s, "{}\n") {
|
|
| 214 |
+ return "root" // Type too simple or complex to print |
|
| 215 |
+ } |
|
| 216 |
+ return fmt.Sprintf("{%s}", s)
|
|
| 217 |
+} |
|
| 218 |
+ |
|
| 219 |
+func (si sliceIndex) String() string {
|
|
| 220 |
+ switch {
|
|
| 221 |
+ case si.xkey == si.ykey: |
|
| 222 |
+ return fmt.Sprintf("[%d]", si.xkey)
|
|
| 223 |
+ case si.ykey == -1: |
|
| 224 |
+ // [5->?] means "I don't know where X[5] went" |
|
| 225 |
+ return fmt.Sprintf("[%d->?]", si.xkey)
|
|
| 226 |
+ case si.xkey == -1: |
|
| 227 |
+ // [?->3] means "I don't know where Y[3] came from" |
|
| 228 |
+ return fmt.Sprintf("[?->%d]", si.ykey)
|
|
| 229 |
+ default: |
|
| 230 |
+ // [5->3] means "X[5] moved to Y[3]" |
|
| 231 |
+ return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey)
|
|
| 232 |
+ } |
|
| 233 |
+} |
|
| 234 |
+func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) }
|
|
| 235 |
+func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) }
|
|
| 236 |
+func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) }
|
|
| 237 |
+func (in indirect) String() string { return "*" }
|
|
| 238 |
+func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) }
|
|
| 239 |
+ |
|
| 240 |
+func (si sliceIndex) Key() int {
|
|
| 241 |
+ if si.xkey != si.ykey {
|
|
| 242 |
+ return -1 |
|
| 243 |
+ } |
|
| 244 |
+ return si.xkey |
|
| 245 |
+} |
|
| 246 |
+func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey }
|
|
| 247 |
+func (mi mapIndex) Key() reflect.Value { return mi.key }
|
|
| 248 |
+func (sf structField) Name() string { return sf.name }
|
|
| 249 |
+func (sf structField) Index() int { return sf.idx }
|
|
| 250 |
+func (tf transform) Name() string { return tf.trans.name }
|
|
| 251 |
+func (tf transform) Func() reflect.Value { return tf.trans.fnc }
|
|
| 252 |
+ |
|
| 253 |
+func (pathStep) isPathStep() {}
|
|
| 254 |
+func (sliceIndex) isSliceIndex() {}
|
|
| 255 |
+func (mapIndex) isMapIndex() {}
|
|
| 256 |
+func (typeAssertion) isTypeAssertion() {}
|
|
| 257 |
+func (structField) isStructField() {}
|
|
| 258 |
+func (indirect) isIndirect() {}
|
|
| 259 |
+func (transform) isTransform() {}
|
|
| 260 |
+ |
|
| 261 |
+var ( |
|
| 262 |
+ _ SliceIndex = sliceIndex{}
|
|
| 263 |
+ _ MapIndex = mapIndex{}
|
|
| 264 |
+ _ TypeAssertion = typeAssertion{}
|
|
| 265 |
+ _ StructField = structField{}
|
|
| 266 |
+ _ Indirect = indirect{}
|
|
| 267 |
+ _ Transform = transform{}
|
|
| 268 |
+ |
|
| 269 |
+ _ PathStep = sliceIndex{}
|
|
| 270 |
+ _ PathStep = mapIndex{}
|
|
| 271 |
+ _ PathStep = typeAssertion{}
|
|
| 272 |
+ _ PathStep = structField{}
|
|
| 273 |
+ _ PathStep = indirect{}
|
|
| 274 |
+ _ PathStep = transform{}
|
|
| 275 |
+) |
|
| 276 |
+ |
|
| 277 |
+// isExported reports whether the identifier is exported. |
|
| 278 |
+func isExported(id string) bool {
|
|
| 279 |
+ r, _ := utf8.DecodeRuneInString(id) |
|
| 280 |
+ return unicode.IsUpper(r) |
|
| 281 |
+} |
|
| 282 |
+ |
|
| 283 |
+// isValid reports whether the identifier is valid. |
|
| 284 |
+// Empty and underscore-only strings are not valid. |
|
| 285 |
+func isValid(id string) bool {
|
|
| 286 |
+ ok := id != "" && id != "_" |
|
| 287 |
+ for j, c := range id {
|
|
| 288 |
+ ok = ok && (j > 0 || !unicode.IsDigit(c)) |
|
| 289 |
+ ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)) |
|
| 290 |
+ } |
|
| 291 |
+ return ok |
|
| 292 |
+} |
| 0 | 293 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,53 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+package cmp |
|
| 5 |
+ |
|
| 6 |
+import ( |
|
| 7 |
+ "fmt" |
|
| 8 |
+ "reflect" |
|
| 9 |
+ "strings" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/google/go-cmp/cmp/internal/value" |
|
| 12 |
+) |
|
| 13 |
+ |
|
| 14 |
+type defaultReporter struct {
|
|
| 15 |
+ Option |
|
| 16 |
+ diffs []string // List of differences, possibly truncated |
|
| 17 |
+ ndiffs int // Total number of differences |
|
| 18 |
+ nbytes int // Number of bytes in diffs |
|
| 19 |
+ nlines int // Number of lines in diffs |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+var _ reporter = (*defaultReporter)(nil) |
|
| 23 |
+ |
|
| 24 |
+func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) {
|
|
| 25 |
+ if eq {
|
|
| 26 |
+ return // Ignore equal results |
|
| 27 |
+ } |
|
| 28 |
+ const maxBytes = 4096 |
|
| 29 |
+ const maxLines = 256 |
|
| 30 |
+ r.ndiffs++ |
|
| 31 |
+ if r.nbytes < maxBytes && r.nlines < maxLines {
|
|
| 32 |
+ sx := value.Format(x, true) |
|
| 33 |
+ sy := value.Format(y, true) |
|
| 34 |
+ if sx == sy {
|
|
| 35 |
+ // Stringer is not helpful, so rely on more exact formatting. |
|
| 36 |
+ sx = value.Format(x, false) |
|
| 37 |
+ sy = value.Format(y, false) |
|
| 38 |
+ } |
|
| 39 |
+ s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
|
|
| 40 |
+ r.diffs = append(r.diffs, s) |
|
| 41 |
+ r.nbytes += len(s) |
|
| 42 |
+ r.nlines += strings.Count(s, "\n") |
|
| 43 |
+ } |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func (r *defaultReporter) String() string {
|
|
| 47 |
+ s := strings.Join(r.diffs, "") |
|
| 48 |
+ if r.ndiffs == len(r.diffs) {
|
|
| 49 |
+ return s |
|
| 50 |
+ } |
|
| 51 |
+ return fmt.Sprintf("%s... %d more differences ...", s, len(r.diffs)-r.ndiffs)
|
|
| 52 |
+} |
| 0 | 53 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,15 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// +build appengine js |
|
| 5 |
+ |
|
| 6 |
+package cmp |
|
| 7 |
+ |
|
| 8 |
+import "reflect" |
|
| 9 |
+ |
|
| 10 |
+const supportAllowUnexported = false |
|
| 11 |
+ |
|
| 12 |
+func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value {
|
|
| 13 |
+ panic("unsafeRetrieveField is not implemented")
|
|
| 14 |
+} |
| 0 | 15 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,23 @@ |
| 0 |
+// Copyright 2017, The Go Authors. All rights reserved. |
|
| 1 |
+// Use of this source code is governed by a BSD-style |
|
| 2 |
+// license that can be found in the LICENSE.md file. |
|
| 3 |
+ |
|
| 4 |
+// +build !appengine,!js |
|
| 5 |
+ |
|
| 6 |
+package cmp |
|
| 7 |
+ |
|
| 8 |
+import ( |
|
| 9 |
+ "reflect" |
|
| 10 |
+ "unsafe" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+const supportAllowUnexported = true |
|
| 14 |
+ |
|
| 15 |
+// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct |
|
| 16 |
+// such that the value has read-write permissions. |
|
| 17 |
+// |
|
| 18 |
+// The parent struct, v, must be addressable, while f must be a StructField |
|
| 19 |
+// describing the field to retrieve. |
|
| 20 |
+func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value {
|
|
| 21 |
+ return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() |
|
| 22 |
+} |
| ... | ... |
@@ -10,24 +10,23 @@ patterns. |
| 10 | 10 |
|
| 11 | 11 |
## Packages |
| 12 | 12 |
|
| 13 |
+* [assert](http://godoc.org/github.com/gotestyourself/gotestyourself/assert) - |
|
| 14 |
+ compare values and fail the test when the comparison fails |
|
| 15 |
+* [env](http://godoc.org/github.com/gotestyourself/gotestyourself/env) - |
|
| 16 |
+ test code that uses environment variables |
|
| 13 | 17 |
* [fs](http://godoc.org/github.com/gotestyourself/gotestyourself/fs) - |
| 14 | 18 |
create test files and directories |
| 15 | 19 |
* [golden](http://godoc.org/github.com/gotestyourself/gotestyourself/golden) - |
| 16 | 20 |
compare large multi-line strings |
| 17 |
-* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) - |
|
| 18 |
- a program to summarize `go test` output and test failures |
|
| 19 | 21 |
* [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) - |
| 20 | 22 |
execute binaries and test the output |
| 21 | 23 |
* [poll](http://godoc.org/github.com/gotestyourself/gotestyourself/poll) - |
| 22 | 24 |
test asynchronous code by polling until a desired state is reached |
| 23 | 25 |
* [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) - |
| 24 | 26 |
skip tests based on conditions |
| 27 |
+* [testsum](http://godoc.org/github.com/gotestyourself/gotestyourself/testsum) - |
|
| 28 |
+ a program to summarize `go test` output and test failures |
|
| 25 | 29 |
|
| 26 | 30 |
## Related |
| 27 | 31 |
|
| 28 |
-* [testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and |
|
| 29 |
- [testify/require](https://godoc.org/github.com/stretchr/testify/require) - |
|
| 30 |
- assertion libraries with common assertions |
|
| 31 |
-* [golang/mock](https://github.com/golang/mock) - generate mocks for interfaces |
|
| 32 |
-* [testify/suite](https://godoc.org/github.com/stretchr/testify/suite) - |
|
| 33 |
- group test into suites to share common setup/teardown logic |
|
| 32 |
+* [maxbrunsfeld/counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) - generate fakes for interfaces |
| 34 | 33 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,186 @@ |
| 0 |
+/*Package assert provides assertions and checks for comparing expected values to |
|
| 1 |
+actual values. When an assertion or check fails a helpful error message is |
|
| 2 |
+printed. |
|
| 3 |
+ |
|
| 4 |
+Assert and Check |
|
| 5 |
+ |
|
| 6 |
+Assert() and Check() both accept a Comparison, and fail the test when the |
|
| 7 |
+comparison fails. The one difference is that Assert() will end the test execution |
|
| 8 |
+immediately (using t.FailNow()) whereas Check() will fail the test (using t.Fail()), |
|
| 9 |
+return the value of the comparison, then proceed with the rest of the test case. |
|
| 10 |
+ |
|
| 11 |
+Example Usage |
|
| 12 |
+ |
|
| 13 |
+The example below shows assert used with some common types. |
|
| 14 |
+ |
|
| 15 |
+ |
|
| 16 |
+ import ( |
|
| 17 |
+ "testing" |
|
| 18 |
+ |
|
| 19 |
+ "github.com/gotestyourself/gotestyourself/assert" |
|
| 20 |
+ is "github.com/gotestyourself/gotestyourself/assert/cmp" |
|
| 21 |
+ ) |
|
| 22 |
+ |
|
| 23 |
+ func TestEverything(t *testing.T) {
|
|
| 24 |
+ // booleans |
|
| 25 |
+ assert.Assert(t, isOk) |
|
| 26 |
+ assert.Assert(t, !missing) |
|
| 27 |
+ |
|
| 28 |
+ // primitives |
|
| 29 |
+ assert.Equal(t, count, 1) |
|
| 30 |
+ assert.Equal(t, msg, "the message") |
|
| 31 |
+ assert.Assert(t, total != 10) // NotEqual |
|
| 32 |
+ |
|
| 33 |
+ // errors |
|
| 34 |
+ assert.NilError(t, closer.Close()) |
|
| 35 |
+ assert.Assert(t, is.Error(err, "the exact error message")) |
|
| 36 |
+ assert.Assert(t, is.ErrorContains(err, "includes this")) |
|
| 37 |
+ |
|
| 38 |
+ // complex types |
|
| 39 |
+ assert.Assert(t, is.Len(items, 3)) |
|
| 40 |
+ assert.Assert(t, len(sequence) != 0) // NotEmpty |
|
| 41 |
+ assert.Assert(t, is.Contains(mapping, "key")) |
|
| 42 |
+ assert.Assert(t, is.Compare(result, myStruct{Name: "title"}))
|
|
| 43 |
+ |
|
| 44 |
+ // pointers and interface |
|
| 45 |
+ assert.Assert(t, is.Nil(ref)) |
|
| 46 |
+ assert.Assert(t, ref != nil) // NotNil |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+Comparisons |
|
| 50 |
+ |
|
| 51 |
+https://godoc.org/github.com/gotestyourself/gotestyourself/assert/cmp provides |
|
| 52 |
+many common comparisons. Additional comparisons can be written to compare |
|
| 53 |
+values in other ways. |
|
| 54 |
+ |
|
| 55 |
+Below is an example of a custom comparison using a regex pattern: |
|
| 56 |
+ |
|
| 57 |
+ func RegexP(value string, pattern string) func() (bool, string) {
|
|
| 58 |
+ return func() (bool, string) {
|
|
| 59 |
+ re := regexp.MustCompile(pattern) |
|
| 60 |
+ msg := fmt.Sprintf("%q did not match pattern %q", value, pattern)
|
|
| 61 |
+ return re.MatchString(value), msg |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+ |
|
| 65 |
+*/ |
|
| 66 |
+package assert |
|
| 67 |
+ |
|
| 68 |
+import ( |
|
| 69 |
+ "fmt" |
|
| 70 |
+ |
|
| 71 |
+ "github.com/gotestyourself/gotestyourself/assert/cmp" |
|
| 72 |
+ "github.com/gotestyourself/gotestyourself/internal/format" |
|
| 73 |
+ "github.com/gotestyourself/gotestyourself/internal/source" |
|
| 74 |
+) |
|
| 75 |
+ |
|
| 76 |
+// BoolOrComparison can be a bool, or Comparison. Other types will panic. |
|
| 77 |
+type BoolOrComparison interface{}
|
|
| 78 |
+ |
|
| 79 |
+// Comparison is a function which compares values and returns true if the actual |
|
| 80 |
+// value matches the expected value. If the values do not match it returns a message |
|
| 81 |
+// with details about why it failed. |
|
| 82 |
+// |
|
| 83 |
+// https://godoc.org/github.com/gotestyourself/gotestyourself/assert/cmp |
|
| 84 |
+// provides many general purpose Comparisons. |
|
| 85 |
+type Comparison func() (success bool, message string) |
|
| 86 |
+ |
|
| 87 |
+// TestingT is the subset of testing.T used by the assert package. |
|
| 88 |
+type TestingT interface {
|
|
| 89 |
+ FailNow() |
|
| 90 |
+ Fail() |
|
| 91 |
+ Log(args ...interface{})
|
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+type helperT interface {
|
|
| 95 |
+ Helper() |
|
| 96 |
+} |
|
| 97 |
+ |
|
| 98 |
+// stackIndex = Assert()/Check(), assert() |
|
| 99 |
+const stackIndex = 2 |
|
| 100 |
+const comparisonArgPos = 1 |
|
| 101 |
+ |
|
| 102 |
+const failureMessage = "assertion failed: " |
|
| 103 |
+ |
|
| 104 |
+func assert( |
|
| 105 |
+ t TestingT, |
|
| 106 |
+ failer func(), |
|
| 107 |
+ comparison BoolOrComparison, |
|
| 108 |
+ msgAndArgs ...interface{},
|
|
| 109 |
+) bool {
|
|
| 110 |
+ if ht, ok := t.(helperT); ok {
|
|
| 111 |
+ ht.Helper() |
|
| 112 |
+ } |
|
| 113 |
+ switch check := comparison.(type) {
|
|
| 114 |
+ case bool: |
|
| 115 |
+ if check {
|
|
| 116 |
+ return true |
|
| 117 |
+ } |
|
| 118 |
+ source, err := source.GetCondition(stackIndex, comparisonArgPos) |
|
| 119 |
+ if err != nil {
|
|
| 120 |
+ t.Log(err.Error()) |
|
| 121 |
+ } |
|
| 122 |
+ |
|
| 123 |
+ msg := " is false" |
|
| 124 |
+ t.Log(format.WithCustomMessage(failureMessage+source+msg, msgAndArgs...)) |
|
| 125 |
+ failer() |
|
| 126 |
+ return false |
|
| 127 |
+ |
|
| 128 |
+ case Comparison: |
|
| 129 |
+ return runCompareFunc(failer, t, check, msgAndArgs...) |
|
| 130 |
+ |
|
| 131 |
+ case func() (success bool, message string): |
|
| 132 |
+ return runCompareFunc(failer, t, check, msgAndArgs...) |
|
| 133 |
+ |
|
| 134 |
+ default: |
|
| 135 |
+ panic(fmt.Sprintf("comparison arg must be bool or Comparison, not %T", comparison))
|
|
| 136 |
+ } |
|
| 137 |
+} |
|
| 138 |
+ |
|
| 139 |
+func runCompareFunc(failer func(), t TestingT, f Comparison, msgAndArgs ...interface{}) bool {
|
|
| 140 |
+ if ht, ok := t.(helperT); ok {
|
|
| 141 |
+ ht.Helper() |
|
| 142 |
+ } |
|
| 143 |
+ if success, message := f(); !success {
|
|
| 144 |
+ t.Log(format.WithCustomMessage(failureMessage+message, msgAndArgs...)) |
|
| 145 |
+ failer() |
|
| 146 |
+ return false |
|
| 147 |
+ } |
|
| 148 |
+ return true |
|
| 149 |
+} |
|
| 150 |
+ |
|
| 151 |
+// Assert performs a comparison, marks the test as having failed if the comparison |
|
| 152 |
+// returns false, and stops execution immediately. |
|
| 153 |
+func Assert(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) {
|
|
| 154 |
+ if ht, ok := t.(helperT); ok {
|
|
| 155 |
+ ht.Helper() |
|
| 156 |
+ } |
|
| 157 |
+ assert(t, t.FailNow, comparison, msgAndArgs...) |
|
| 158 |
+} |
|
| 159 |
+ |
|
| 160 |
+// Check performs a comparison and marks the test as having failed if the comparison |
|
| 161 |
+// returns false. Returns the result of the comparison. |
|
| 162 |
+func Check(t TestingT, comparison BoolOrComparison, msgAndArgs ...interface{}) bool {
|
|
| 163 |
+ if ht, ok := t.(helperT); ok {
|
|
| 164 |
+ ht.Helper() |
|
| 165 |
+ } |
|
| 166 |
+ return assert(t, t.Fail, comparison, msgAndArgs...) |
|
| 167 |
+} |
|
| 168 |
+ |
|
| 169 |
+// NilError fails the test immediately if the last arg is a non-nil error. |
|
| 170 |
+// This is equivalent to Assert(t, cmp.NilError(err)). |
|
| 171 |
+func NilError(t TestingT, err error, msgAndArgs ...interface{}) {
|
|
| 172 |
+ if ht, ok := t.(helperT); ok {
|
|
| 173 |
+ ht.Helper() |
|
| 174 |
+ } |
|
| 175 |
+ assert(t, t.FailNow, cmp.NilError(err), msgAndArgs...) |
|
| 176 |
+} |
|
| 177 |
+ |
|
| 178 |
+// Equal uses the == operator to assert two values are equal and fails the test |
|
| 179 |
+// if they are not equal. This is equivalent to Assert(t, cmp.Equal(x, y)). |
|
| 180 |
+func Equal(t TestingT, x, y interface{}, msgAndArgs ...interface{}) {
|
|
| 181 |
+ if ht, ok := t.(helperT); ok {
|
|
| 182 |
+ ht.Helper() |
|
| 183 |
+ } |
|
| 184 |
+ assert(t, t.FailNow, cmp.Equal(x, y), msgAndArgs...) |
|
| 185 |
+} |
| 0 | 186 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,201 @@ |
| 0 |
+/*Package cmp provides Comparisons for Assert and Check*/ |
|
| 1 |
+package cmp |
|
| 2 |
+ |
|
| 3 |
+import ( |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "reflect" |
|
| 6 |
+ "strings" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/google/go-cmp/cmp" |
|
| 9 |
+ "github.com/pmezard/go-difflib/difflib" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+// Compare two complex values using https://godoc.org/github.com/google/go-cmp/cmp |
|
| 13 |
+// and succeeds if the values are equal. |
|
| 14 |
+// |
|
| 15 |
+// The comparison can be customized using comparison Options. |
|
| 16 |
+func Compare(x, y interface{}, opts ...cmp.Option) func() (bool, string) {
|
|
| 17 |
+ return func() (bool, string) {
|
|
| 18 |
+ diff := cmp.Diff(x, y, opts...) |
|
| 19 |
+ return diff == "", "\n" + diff |
|
| 20 |
+ } |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// Equal succeeds if x == y. |
|
| 24 |
+func Equal(x, y interface{}) func() (success bool, message string) {
|
|
| 25 |
+ return func() (bool, string) {
|
|
| 26 |
+ return x == y, fmt.Sprintf("%v (%T) != %v (%T)", x, x, y, y)
|
|
| 27 |
+ } |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// Len succeeds if the sequence has the expected length. |
|
| 31 |
+func Len(seq interface{}, expected int) func() (bool, string) {
|
|
| 32 |
+ return func() (success bool, message string) {
|
|
| 33 |
+ defer func() {
|
|
| 34 |
+ if e := recover(); e != nil {
|
|
| 35 |
+ success = false |
|
| 36 |
+ message = fmt.Sprintf("type %T does not have a length", seq)
|
|
| 37 |
+ } |
|
| 38 |
+ }() |
|
| 39 |
+ value := reflect.ValueOf(seq) |
|
| 40 |
+ length := value.Len() |
|
| 41 |
+ if length == expected {
|
|
| 42 |
+ return true, "" |
|
| 43 |
+ } |
|
| 44 |
+ msg := fmt.Sprintf("expected %s (length %d) to have length %d", seq, length, expected)
|
|
| 45 |
+ return false, msg |
|
| 46 |
+ } |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 49 |
+// NilError succeeds if the last argument is a nil error. |
|
| 50 |
+func NilError(arg interface{}, args ...interface{}) func() (bool, string) {
|
|
| 51 |
+ return func() (bool, string) {
|
|
| 52 |
+ msgFunc := func(value reflect.Value) string {
|
|
| 53 |
+ return fmt.Sprintf("error is not nil: %s", value.Interface().(error).Error())
|
|
| 54 |
+ } |
|
| 55 |
+ if len(args) == 0 {
|
|
| 56 |
+ return isNil(arg, msgFunc)() |
|
| 57 |
+ } |
|
| 58 |
+ return isNil(args[len(args)-1], msgFunc)() |
|
| 59 |
+ } |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// Contains succeeds if item is in collection. Collection may be a string, map, |
|
| 63 |
+// slice, or array. |
|
| 64 |
+// |
|
| 65 |
+// If collection is a string, item must also be a string, and is compared using |
|
| 66 |
+// strings.Contains(). |
|
| 67 |
+// If collection is a Map, contains will succeed if item is a key in the map. |
|
| 68 |
+// If collection is a slice or array, item is compared to each item in the |
|
| 69 |
+// sequence using reflect.DeepEqual(). |
|
| 70 |
+func Contains(collection interface{}, item interface{}) func() (bool, string) {
|
|
| 71 |
+ return func() (bool, string) {
|
|
| 72 |
+ colValue := reflect.ValueOf(collection) |
|
| 73 |
+ if !colValue.IsValid() {
|
|
| 74 |
+ return false, fmt.Sprintf("nil does not contain items")
|
|
| 75 |
+ } |
|
| 76 |
+ msg := fmt.Sprintf("%v does not contain %v", collection, item)
|
|
| 77 |
+ |
|
| 78 |
+ itemValue := reflect.ValueOf(item) |
|
| 79 |
+ switch colValue.Type().Kind() {
|
|
| 80 |
+ case reflect.String: |
|
| 81 |
+ if itemValue.Type().Kind() != reflect.String {
|
|
| 82 |
+ return false, "string may only contain strings" |
|
| 83 |
+ } |
|
| 84 |
+ success := strings.Contains(colValue.String(), itemValue.String()) |
|
| 85 |
+ return success, fmt.Sprintf("string %q does not contain %q", collection, item)
|
|
| 86 |
+ |
|
| 87 |
+ case reflect.Map: |
|
| 88 |
+ if itemValue.Type() != colValue.Type().Key() {
|
|
| 89 |
+ return false, fmt.Sprintf( |
|
| 90 |
+ "%v can not contain a %v key", colValue.Type(), itemValue.Type()) |
|
| 91 |
+ } |
|
| 92 |
+ index := colValue.MapIndex(itemValue) |
|
| 93 |
+ return index.IsValid(), msg |
|
| 94 |
+ |
|
| 95 |
+ case reflect.Slice, reflect.Array: |
|
| 96 |
+ for i := 0; i < colValue.Len(); i++ {
|
|
| 97 |
+ if reflect.DeepEqual(colValue.Index(i).Interface(), item) {
|
|
| 98 |
+ return true, "" |
|
| 99 |
+ } |
|
| 100 |
+ } |
|
| 101 |
+ return false, msg |
|
| 102 |
+ default: |
|
| 103 |
+ return false, fmt.Sprintf("type %T does not contain items", collection)
|
|
| 104 |
+ } |
|
| 105 |
+ } |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+// Panics succeeds if f() panics. |
|
| 109 |
+func Panics(f func()) func() (bool, string) {
|
|
| 110 |
+ return func() (success bool, message string) {
|
|
| 111 |
+ defer func() {
|
|
| 112 |
+ if err := recover(); err != nil {
|
|
| 113 |
+ success = true |
|
| 114 |
+ } |
|
| 115 |
+ }() |
|
| 116 |
+ f() |
|
| 117 |
+ return false, "did not panic" |
|
| 118 |
+ } |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+// EqualMultiLine succeeds if the two strings are equal. If they are not equal |
|
| 122 |
+// the failure message will be the difference between the two strings. |
|
| 123 |
+func EqualMultiLine(x, y string) func() (bool, string) {
|
|
| 124 |
+ return func() (bool, string) {
|
|
| 125 |
+ if x == y {
|
|
| 126 |
+ return true, "" |
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
|
|
| 130 |
+ A: difflib.SplitLines(x), |
|
| 131 |
+ B: difflib.SplitLines(y), |
|
| 132 |
+ FromFile: "left", |
|
| 133 |
+ ToFile: "right", |
|
| 134 |
+ Context: 3, |
|
| 135 |
+ }) |
|
| 136 |
+ if err != nil {
|
|
| 137 |
+ return false, fmt.Sprintf("failed to produce diff: %s", err)
|
|
| 138 |
+ } |
|
| 139 |
+ return false, "\n" + diff |
|
| 140 |
+ } |
|
| 141 |
+} |
|
| 142 |
+ |
|
| 143 |
+// Error succeeds if err is a non-nil error, and the error message equals the |
|
| 144 |
+// expected message. |
|
| 145 |
+func Error(err error, message string) func() (bool, string) {
|
|
| 146 |
+ return func() (bool, string) {
|
|
| 147 |
+ switch {
|
|
| 148 |
+ case err == nil: |
|
| 149 |
+ return false, "expected an error, got nil" |
|
| 150 |
+ case err.Error() != message: |
|
| 151 |
+ return false, fmt.Sprintf( |
|
| 152 |
+ "expected error message %q, got %q", message, err.Error()) |
|
| 153 |
+ } |
|
| 154 |
+ return true, "" |
|
| 155 |
+ } |
|
| 156 |
+} |
|
| 157 |
+ |
|
| 158 |
+// ErrorContains succeeds if err is a non-nil error, and the error message contains |
|
| 159 |
+// the expected substring. |
|
| 160 |
+func ErrorContains(err error, substring string) func() (bool, string) {
|
|
| 161 |
+ return func() (bool, string) {
|
|
| 162 |
+ switch {
|
|
| 163 |
+ case err == nil: |
|
| 164 |
+ return false, "expected an error, got nil" |
|
| 165 |
+ case !strings.Contains(err.Error(), substring): |
|
| 166 |
+ return false, fmt.Sprintf( |
|
| 167 |
+ "expected error message to contain %q, got %q", substring, err.Error()) |
|
| 168 |
+ } |
|
| 169 |
+ return true, "" |
|
| 170 |
+ } |
|
| 171 |
+} |
|
| 172 |
+ |
|
| 173 |
+// Nil succeeds if obj is a nil interface, pointer, or function. |
|
| 174 |
+// |
|
| 175 |
+// Use NilError() for comparing errors. Use Len(obj, 0) for comparing slices, |
|
| 176 |
+// maps, and channels. |
|
| 177 |
+func Nil(obj interface{}) func() (bool, string) {
|
|
| 178 |
+ msgFunc := func(value reflect.Value) string {
|
|
| 179 |
+ return fmt.Sprintf("%v (type %s) is not nil", reflect.Indirect(value), value.Type())
|
|
| 180 |
+ } |
|
| 181 |
+ return isNil(obj, msgFunc) |
|
| 182 |
+} |
|
| 183 |
+ |
|
| 184 |
+func isNil(obj interface{}, msgFunc func(reflect.Value) string) func() (bool, string) {
|
|
| 185 |
+ return func() (bool, string) {
|
|
| 186 |
+ if obj == nil {
|
|
| 187 |
+ return true, "" |
|
| 188 |
+ } |
|
| 189 |
+ value := reflect.ValueOf(obj) |
|
| 190 |
+ kind := value.Type().Kind() |
|
| 191 |
+ if kind >= reflect.Chan && kind <= reflect.Slice {
|
|
| 192 |
+ if value.IsNil() {
|
|
| 193 |
+ return true, "" |
|
| 194 |
+ } |
|
| 195 |
+ return false, msgFunc(value) |
|
| 196 |
+ } |
|
| 197 |
+ |
|
| 198 |
+ return false, fmt.Sprintf("%v (type %s) can not be nil", value, value.Type())
|
|
| 199 |
+ } |
|
| 200 |
+} |
| ... | ... |
@@ -8,29 +8,42 @@ import ( |
| 8 | 8 |
"os" |
| 9 | 9 |
"path/filepath" |
| 10 | 10 |
|
| 11 |
- "github.com/stretchr/testify/require" |
|
| 11 |
+ "github.com/gotestyourself/gotestyourself/assert" |
|
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
// Path objects return their filesystem path. Both File and Dir implement Path. |
| 15 | 15 |
type Path interface {
|
| 16 | 16 |
Path() string |
| 17 |
+ Remove() |
|
| 17 | 18 |
} |
| 18 | 19 |
|
| 20 |
+var ( |
|
| 21 |
+ _ Path = &Dir{}
|
|
| 22 |
+ _ Path = &File{}
|
|
| 23 |
+) |
|
| 24 |
+ |
|
| 19 | 25 |
// File is a temporary file on the filesystem |
| 20 | 26 |
type File struct {
|
| 21 | 27 |
path string |
| 22 | 28 |
} |
| 23 | 29 |
|
| 30 |
+type helperT interface {
|
|
| 31 |
+ Helper() |
|
| 32 |
+} |
|
| 33 |
+ |
|
| 24 | 34 |
// NewFile creates a new file in a temporary directory using prefix as part of |
| 25 | 35 |
// the filename. The PathOps are applied to the before returning the File. |
| 26 |
-func NewFile(t require.TestingT, prefix string, ops ...PathOp) *File {
|
|
| 36 |
+func NewFile(t assert.TestingT, prefix string, ops ...PathOp) *File {
|
|
| 37 |
+ if ht, ok := t.(helperT); ok {
|
|
| 38 |
+ ht.Helper() |
|
| 39 |
+ } |
|
| 27 | 40 |
tempfile, err := ioutil.TempFile("", prefix+"-")
|
| 28 |
- require.NoError(t, err) |
|
| 41 |
+ assert.NilError(t, err) |
|
| 29 | 42 |
file := &File{path: tempfile.Name()}
|
| 30 |
- require.NoError(t, tempfile.Close()) |
|
| 43 |
+ assert.NilError(t, tempfile.Close()) |
|
| 31 | 44 |
|
| 32 | 45 |
for _, op := range ops {
|
| 33 |
- require.NoError(t, op(file)) |
|
| 46 |
+ assert.NilError(t, op(file)) |
|
| 34 | 47 |
} |
| 35 | 48 |
return file |
| 36 | 49 |
} |
| ... | ... |
@@ -53,13 +66,16 @@ type Dir struct {
|
| 53 | 53 |
|
| 54 | 54 |
// NewDir returns a new temporary directory using prefix as part of the directory |
| 55 | 55 |
// name. The PathOps are applied before returning the Dir. |
| 56 |
-func NewDir(t require.TestingT, prefix string, ops ...PathOp) *Dir {
|
|
| 56 |
+func NewDir(t assert.TestingT, prefix string, ops ...PathOp) *Dir {
|
|
| 57 |
+ if ht, ok := t.(helperT); ok {
|
|
| 58 |
+ ht.Helper() |
|
| 59 |
+ } |
|
| 57 | 60 |
path, err := ioutil.TempDir("", prefix+"-")
|
| 58 |
- require.NoError(t, err) |
|
| 61 |
+ assert.NilError(t, err) |
|
| 59 | 62 |
dir := &Dir{path: path}
|
| 60 | 63 |
|
| 61 | 64 |
for _, op := range ops {
|
| 62 |
- require.NoError(t, op(dir)) |
|
| 65 |
+ assert.NilError(t, op(dir)) |
|
| 63 | 66 |
} |
| 64 | 67 |
return dir |
| 65 | 68 |
} |
| ... | ... |
@@ -31,14 +31,17 @@ func AsUser(uid, gid int) PathOp {
|
| 31 | 31 |
} |
| 32 | 32 |
|
| 33 | 33 |
// WithFile creates a file in the directory at path with content |
| 34 |
-func WithFile(filename, content string) PathOp {
|
|
| 34 |
+func WithFile(filename, content string, ops ...PathOp) PathOp {
|
|
| 35 | 35 |
return func(path Path) error {
|
| 36 |
- return createFile(path.Path(), filename, content) |
|
| 36 |
+ fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) |
|
| 37 |
+ if err := createFile(fullpath, content); err != nil {
|
|
| 38 |
+ return err |
|
| 39 |
+ } |
|
| 40 |
+ return applyPathOps(&File{path: fullpath}, ops)
|
|
| 37 | 41 |
} |
| 38 | 42 |
} |
| 39 | 43 |
|
| 40 |
-func createFile(dir, filename, content string) error {
|
|
| 41 |
- fullpath := filepath.Join(dir, filepath.FromSlash(filename)) |
|
| 44 |
+func createFile(fullpath string, content string) error {
|
|
| 42 | 45 |
return ioutil.WriteFile(fullpath, []byte(content), 0644) |
| 43 | 46 |
} |
| 44 | 47 |
|
| ... | ... |
@@ -46,7 +49,8 @@ func createFile(dir, filename, content string) error {
|
| 46 | 46 |
func WithFiles(files map[string]string) PathOp {
|
| 47 | 47 |
return func(path Path) error {
|
| 48 | 48 |
for filename, content := range files {
|
| 49 |
- if err := createFile(path.Path(), filename, content); err != nil {
|
|
| 49 |
+ fullpath := filepath.Join(path.Path(), filepath.FromSlash(filename)) |
|
| 50 |
+ if err := createFile(fullpath, content); err != nil {
|
|
| 50 | 51 |
return err |
| 51 | 52 |
} |
| 52 | 53 |
} |
| ... | ... |
@@ -61,6 +65,35 @@ func FromDir(source string) PathOp {
|
| 61 | 61 |
} |
| 62 | 62 |
} |
| 63 | 63 |
|
| 64 |
+// WithDir creates a subdirectory in the directory at path. Additional PathOp |
|
| 65 |
+// can be used to modify the subdirectory |
|
| 66 |
+func WithDir(name string, ops ...PathOp) PathOp {
|
|
| 67 |
+ return func(path Path) error {
|
|
| 68 |
+ fullpath := filepath.Join(path.Path(), filepath.FromSlash(name)) |
|
| 69 |
+ err := os.MkdirAll(fullpath, 0755) |
|
| 70 |
+ if err != nil {
|
|
| 71 |
+ return err |
|
| 72 |
+ } |
|
| 73 |
+ return applyPathOps(&Dir{path: fullpath}, ops)
|
|
| 74 |
+ } |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+func applyPathOps(path Path, ops []PathOp) error {
|
|
| 78 |
+ for _, op := range ops {
|
|
| 79 |
+ if err := op(path); err != nil {
|
|
| 80 |
+ return err |
|
| 81 |
+ } |
|
| 82 |
+ } |
|
| 83 |
+ return nil |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+// WithMode sets the file mode on the directory or file at path |
|
| 87 |
+func WithMode(mode os.FileMode) PathOp {
|
|
| 88 |
+ return func(path Path) error {
|
|
| 89 |
+ return os.Chmod(path.Path(), mode) |
|
| 90 |
+ } |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 64 | 93 |
func copyDirectory(source, dest string) error {
|
| 65 | 94 |
entries, err := ioutil.ReadDir(source) |
| 66 | 95 |
if err != nil {
|
| ... | ... |
@@ -7,8 +7,6 @@ import ( |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"io" |
| 9 | 9 |
"os/exec" |
| 10 |
- "path/filepath" |
|
| 11 |
- "runtime" |
|
| 12 | 10 |
"strings" |
| 13 | 11 |
"sync" |
| 14 | 12 |
"time" |
| ... | ... |
@@ -18,8 +16,12 @@ type testingT interface {
|
| 18 | 18 |
Fatalf(string, ...interface{})
|
| 19 | 19 |
} |
| 20 | 20 |
|
| 21 |
+type helperT interface {
|
|
| 22 |
+ Helper() |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 21 | 25 |
// None is a token to inform Result.Assert that the output should be empty |
| 22 |
-const None string = "[NOTHING]" |
|
| 26 |
+const None = "[NOTHING]" |
|
| 23 | 27 |
|
| 24 | 28 |
type lockedBuffer struct {
|
| 25 | 29 |
m sync.RWMutex |
| ... | ... |
@@ -51,17 +53,16 @@ type Result struct {
|
| 51 | 51 |
|
| 52 | 52 |
// Assert compares the Result against the Expected struct, and fails the test if |
| 53 | 53 |
// any of the expectations are not met. |
| 54 |
+// TODO: deprecate and replace with assert.CompareFunc |
|
| 54 | 55 |
func (r *Result) Assert(t testingT, exp Expected) *Result {
|
| 56 |
+ if ht, ok := t.(helperT); ok {
|
|
| 57 |
+ ht.Helper() |
|
| 58 |
+ } |
|
| 55 | 59 |
err := r.Compare(exp) |
| 56 | 60 |
if err == nil {
|
| 57 | 61 |
return r |
| 58 | 62 |
} |
| 59 |
- _, file, line, ok := runtime.Caller(1) |
|
| 60 |
- if ok {
|
|
| 61 |
- t.Fatalf("at %s:%d - %s\n", filepath.Base(file), line, err.Error())
|
|
| 62 |
- } else {
|
|
| 63 |
- t.Fatalf("(no file/line info) - %s", err.Error())
|
|
| 64 |
- } |
|
| 63 |
+ t.Fatalf(err.Error() + "\n") |
|
| 65 | 64 |
return nil |
| 66 | 65 |
} |
| 67 | 66 |
|
| 68 | 67 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+package format |
|
| 1 |
+ |
|
| 2 |
+import "fmt" |
|
| 3 |
+ |
|
| 4 |
+// Message accepts a msgAndArgs varargs and formats it using fmt.Sprintf |
|
| 5 |
+func Message(msgAndArgs ...interface{}) string {
|
|
| 6 |
+ switch len(msgAndArgs) {
|
|
| 7 |
+ case 0: |
|
| 8 |
+ return "" |
|
| 9 |
+ case 1: |
|
| 10 |
+ return fmt.Sprintf("%v", msgAndArgs[0])
|
|
| 11 |
+ default: |
|
| 12 |
+ return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) |
|
| 13 |
+ } |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+// WithCustomMessage accepts one or two messages and formats them appropriately |
|
| 17 |
+func WithCustomMessage(source string, msgAndArgs ...interface{}) string {
|
|
| 18 |
+ custom := Message(msgAndArgs...) |
|
| 19 |
+ switch {
|
|
| 20 |
+ case custom == "": |
|
| 21 |
+ return source |
|
| 22 |
+ case source == "": |
|
| 23 |
+ return custom |
|
| 24 |
+ } |
|
| 25 |
+ return fmt.Sprintf("%s: %s", source, custom)
|
|
| 26 |
+} |
| 0 | 27 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,109 @@ |
| 0 |
+package source |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "go/ast" |
|
| 5 |
+ "go/format" |
|
| 6 |
+ "go/parser" |
|
| 7 |
+ "go/token" |
|
| 8 |
+ "runtime" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/pkg/errors" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+const baseStackIndex = 1 |
|
| 14 |
+ |
|
| 15 |
+// GetCondition returns the condition string by reading it from the file |
|
| 16 |
+// identified in the callstack. In golang 1.9 the line number changed from |
|
| 17 |
+// being the line where the statement ended to the line where the statement began. |
|
| 18 |
+func GetCondition(stackIndex int, argPos int) (string, error) {
|
|
| 19 |
+ _, filename, lineNum, ok := runtime.Caller(baseStackIndex + stackIndex) |
|
| 20 |
+ if !ok {
|
|
| 21 |
+ return "", errors.New("failed to get caller info")
|
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ node, err := getNodeAtLine(filename, lineNum) |
|
| 25 |
+ if err != nil {
|
|
| 26 |
+ return "", err |
|
| 27 |
+ } |
|
| 28 |
+ return getArgSourceFromAST(node, argPos) |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+func getNodeAtLine(filename string, lineNum int) (ast.Node, error) {
|
|
| 32 |
+ fileset := token.NewFileSet() |
|
| 33 |
+ astFile, err := parser.ParseFile(fileset, filename, nil, parser.AllErrors) |
|
| 34 |
+ if err != nil {
|
|
| 35 |
+ return nil, errors.Wrapf(err, "failed to parse source file: %s", filename) |
|
| 36 |
+ } |
|
| 37 |
+ |
|
| 38 |
+ node := scanToLine(fileset, astFile, lineNum) |
|
| 39 |
+ if node == nil {
|
|
| 40 |
+ return nil, errors.Wrapf(err, |
|
| 41 |
+ "failed to find an expression on line %d in %s", lineNum, filename) |
|
| 42 |
+ } |
|
| 43 |
+ return node, nil |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+func scanToLine(fileset *token.FileSet, node ast.Node, lineNum int) ast.Node {
|
|
| 47 |
+ v := &scanToLineVisitor{lineNum: lineNum, fileset: fileset}
|
|
| 48 |
+ ast.Walk(v, node) |
|
| 49 |
+ return v.matchedNode |
|
| 50 |
+} |
|
| 51 |
+ |
|
| 52 |
+type scanToLineVisitor struct {
|
|
| 53 |
+ lineNum int |
|
| 54 |
+ matchedNode ast.Node |
|
| 55 |
+ fileset *token.FileSet |
|
| 56 |
+} |
|
| 57 |
+ |
|
| 58 |
+func (v *scanToLineVisitor) Visit(node ast.Node) ast.Visitor {
|
|
| 59 |
+ if node == nil || v.matchedNode != nil {
|
|
| 60 |
+ return nil |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ var position token.Position |
|
| 64 |
+ switch {
|
|
| 65 |
+ case runtime.Version() < "go1.9": |
|
| 66 |
+ position = v.fileset.Position(node.End()) |
|
| 67 |
+ default: |
|
| 68 |
+ position = v.fileset.Position(node.Pos()) |
|
| 69 |
+ } |
|
| 70 |
+ |
|
| 71 |
+ if position.Line == v.lineNum {
|
|
| 72 |
+ v.matchedNode = node |
|
| 73 |
+ return nil |
|
| 74 |
+ } |
|
| 75 |
+ return v |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+func getArgSourceFromAST(node ast.Node, argPos int) (string, error) {
|
|
| 79 |
+ visitor := &callExprVisitor{}
|
|
| 80 |
+ ast.Walk(visitor, node) |
|
| 81 |
+ if visitor.expr == nil {
|
|
| 82 |
+ return "", errors.Errorf("unexpected ast")
|
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ buf := new(bytes.Buffer) |
|
| 86 |
+ err := format.Node(buf, token.NewFileSet(), visitor.expr.Args[argPos]) |
|
| 87 |
+ return buf.String(), err |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+type callExprVisitor struct {
|
|
| 91 |
+ expr *ast.CallExpr |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+func (v *callExprVisitor) Visit(node ast.Node) ast.Visitor {
|
|
| 95 |
+ switch typed := node.(type) {
|
|
| 96 |
+ case nil: |
|
| 97 |
+ return nil |
|
| 98 |
+ case *ast.IfStmt: |
|
| 99 |
+ ast.Walk(v, typed.Cond) |
|
| 100 |
+ case *ast.CallExpr: |
|
| 101 |
+ v.expr = typed |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ if v.expr != nil {
|
|
| 105 |
+ return nil |
|
| 106 |
+ } |
|
| 107 |
+ return v |
|
| 108 |
+} |
| ... | ... |
@@ -19,6 +19,10 @@ type LogT interface {
|
| 19 | 19 |
Logf(format string, args ...interface{})
|
| 20 | 20 |
} |
| 21 | 21 |
|
| 22 |
+type helperT interface {
|
|
| 23 |
+ Helper() |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 22 | 26 |
// Settings are used to configure the behaviour of WaitOn |
| 23 | 27 |
type Settings struct {
|
| 24 | 28 |
// Timeout is the maximum time to wait for the condition. Defaults to 10s |
| ... | ... |
@@ -101,6 +105,9 @@ func Error(err error) Result {
|
| 101 | 101 |
// check returns a done Result. To fail a test and exit polling with an error |
| 102 | 102 |
// return a error result. |
| 103 | 103 |
func WaitOn(t TestingT, check func(t LogT) Result, pollOps ...SettingOp) {
|
| 104 |
+ if ht, ok := t.(helperT); ok {
|
|
| 105 |
+ ht.Helper() |
|
| 106 |
+ } |
|
| 104 | 107 |
config := defaultConfig() |
| 105 | 108 |
for _, pollOp := range pollOps {
|
| 106 | 109 |
pollOp(config) |
| ... | ... |
@@ -3,19 +3,14 @@ |
| 3 | 3 |
package skip |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
- "bytes" |
|
| 7 | 6 |
"fmt" |
| 8 |
- "go/ast" |
|
| 9 |
- "go/format" |
|
| 10 |
- "go/parser" |
|
| 11 |
- "go/token" |
|
| 12 |
- "io/ioutil" |
|
| 13 | 7 |
"path" |
| 14 | 8 |
"reflect" |
| 15 | 9 |
"runtime" |
| 16 | 10 |
"strings" |
| 17 | 11 |
|
| 18 |
- "github.com/pkg/errors" |
|
| 12 |
+ "github.com/gotestyourself/gotestyourself/internal/format" |
|
| 13 |
+ "github.com/gotestyourself/gotestyourself/internal/source" |
|
| 19 | 14 |
) |
| 20 | 15 |
|
| 21 | 16 |
type skipT interface {
|
| ... | ... |
@@ -23,14 +18,29 @@ type skipT interface {
|
| 23 | 23 |
Log(args ...interface{})
|
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 |
+type helperT interface {
|
|
| 27 |
+ Helper() |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// BoolOrCheckFunc can be a bool or func() bool, other types will panic |
|
| 31 |
+type BoolOrCheckFunc interface{}
|
|
| 32 |
+ |
|
| 26 | 33 |
// If skips the test if the check function returns true. The skip message will |
| 27 | 34 |
// contain the name of the check function. Extra message text can be passed as a |
| 28 | 35 |
// format string with args |
| 29 |
-func If(t skipT, check func() bool, msgAndArgs ...interface{}) {
|
|
| 30 |
- if check() {
|
|
| 31 |
- t.Skip(formatWithCustomMessage( |
|
| 32 |
- getFunctionName(check), |
|
| 33 |
- formatMessage(msgAndArgs...))) |
|
| 36 |
+func If(t skipT, condition BoolOrCheckFunc, msgAndArgs ...interface{}) {
|
|
| 37 |
+ if ht, ok := t.(helperT); ok {
|
|
| 38 |
+ ht.Helper() |
|
| 39 |
+ } |
|
| 40 |
+ switch check := condition.(type) {
|
|
| 41 |
+ case bool: |
|
| 42 |
+ ifCondition(t, check, msgAndArgs...) |
|
| 43 |
+ case func() bool: |
|
| 44 |
+ if check() {
|
|
| 45 |
+ t.Skip(format.WithCustomMessage(getFunctionName(check), msgAndArgs...)) |
|
| 46 |
+ } |
|
| 47 |
+ default: |
|
| 48 |
+ panic(fmt.Sprintf("invalid type for condition arg: %T", check))
|
|
| 34 | 49 |
} |
| 35 | 50 |
} |
| 36 | 51 |
|
| ... | ... |
@@ -42,92 +52,30 @@ func getFunctionName(function func() bool) string {
|
| 42 | 42 |
// IfCondition skips the test if the condition is true. The skip message will |
| 43 | 43 |
// contain the source of the expression passed as the condition. Extra message |
| 44 | 44 |
// text can be passed as a format string with args. |
| 45 |
+// |
|
| 46 |
+// Deprecated: Use If() which now accepts bool arguments |
|
| 45 | 47 |
func IfCondition(t skipT, condition bool, msgAndArgs ...interface{}) {
|
| 46 |
- if !condition {
|
|
| 47 |
- return |
|
| 48 |
- } |
|
| 49 |
- source, err := getConditionSource() |
|
| 50 |
- if err != nil {
|
|
| 51 |
- t.Log(err.Error()) |
|
| 52 |
- t.Skip(formatMessage(msgAndArgs...)) |
|
| 48 |
+ if ht, ok := t.(helperT); ok {
|
|
| 49 |
+ ht.Helper() |
|
| 53 | 50 |
} |
| 54 |
- t.Skip(formatWithCustomMessage(source, formatMessage(msgAndArgs...))) |
|
| 51 |
+ ifCondition(t, condition, msgAndArgs...) |
|
| 55 | 52 |
} |
| 56 | 53 |
|
| 57 |
-func getConditionSource() (string, error) {
|
|
| 58 |
- const callstackIndex = 3 |
|
| 59 |
- lines, err := getSourceLine(callstackIndex) |
|
| 60 |
- if err != nil {
|
|
| 61 |
- return "", err |
|
| 54 |
+func ifCondition(t skipT, condition bool, msgAndArgs ...interface{}) {
|
|
| 55 |
+ if ht, ok := t.(helperT); ok {
|
|
| 56 |
+ ht.Helper() |
|
| 62 | 57 |
} |
| 63 |
- |
|
| 64 |
- for i := range lines {
|
|
| 65 |
- source := strings.Join(lines[len(lines)-i-1:], "\n") |
|
| 66 |
- node, err := parser.ParseExpr(source) |
|
| 67 |
- if err == nil {
|
|
| 68 |
- return getConditionArgFromAST(node) |
|
| 69 |
- } |
|
| 70 |
- } |
|
| 71 |
- return "", errors.Wrapf(err, "failed to parse source") |
|
| 72 |
-} |
|
| 73 |
- |
|
| 74 |
-// maxContextLines is the maximum number of lines to scan for a complete |
|
| 75 |
-// skip.If() statement |
|
| 76 |
-const maxContextLines = 10 |
|
| 77 |
- |
|
| 78 |
-// getSourceLines returns the source line which called skip.If() along with a |
|
| 79 |
-// few preceding lines. To properly parse the AST a complete statement is |
|
| 80 |
-// required, and that statement may be split across multiple lines, so include |
|
| 81 |
-// up to maxContextLines. |
|
| 82 |
-func getSourceLine(stackIndex int) ([]string, error) {
|
|
| 83 |
- _, filename, line, ok := runtime.Caller(stackIndex) |
|
| 84 |
- if !ok {
|
|
| 85 |
- return nil, errors.New("failed to get caller info")
|
|
| 58 |
+ if !condition {
|
|
| 59 |
+ return |
|
| 86 | 60 |
} |
| 87 |
- |
|
| 88 |
- raw, err := ioutil.ReadFile(filename) |
|
| 61 |
+ const ( |
|
| 62 |
+ stackIndex = 2 |
|
| 63 |
+ argPos = 1 |
|
| 64 |
+ ) |
|
| 65 |
+ source, err := source.GetCondition(stackIndex, argPos) |
|
| 89 | 66 |
if err != nil {
|
| 90 |
- return nil, errors.Wrapf(err, "failed to read source file: %s", filename) |
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- lines := strings.Split(string(raw), "\n") |
|
| 94 |
- if len(lines) < line {
|
|
| 95 |
- return nil, errors.Errorf("file %s does not have line %d", filename, line)
|
|
| 96 |
- } |
|
| 97 |
- firstLine := line - maxContextLines |
|
| 98 |
- if firstLine < 0 {
|
|
| 99 |
- firstLine = 0 |
|
| 100 |
- } |
|
| 101 |
- return lines[firstLine:line], nil |
|
| 102 |
-} |
|
| 103 |
- |
|
| 104 |
-func getConditionArgFromAST(node ast.Expr) (string, error) {
|
|
| 105 |
- switch expr := node.(type) {
|
|
| 106 |
- case *ast.CallExpr: |
|
| 107 |
- buf := new(bytes.Buffer) |
|
| 108 |
- err := format.Node(buf, token.NewFileSet(), expr.Args[1]) |
|
| 109 |
- return buf.String(), err |
|
| 110 |
- } |
|
| 111 |
- return "", errors.New("unexpected ast")
|
|
| 112 |
-} |
|
| 113 |
- |
|
| 114 |
-func formatMessage(msgAndArgs ...interface{}) string {
|
|
| 115 |
- switch len(msgAndArgs) {
|
|
| 116 |
- case 0: |
|
| 117 |
- return "" |
|
| 118 |
- case 1: |
|
| 119 |
- return msgAndArgs[0].(string) |
|
| 120 |
- default: |
|
| 121 |
- return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) |
|
| 122 |
- } |
|
| 123 |
-} |
|
| 124 |
- |
|
| 125 |
-func formatWithCustomMessage(source, custom string) string {
|
|
| 126 |
- switch {
|
|
| 127 |
- case custom == "": |
|
| 128 |
- return source |
|
| 129 |
- case source == "": |
|
| 130 |
- return custom |
|
| 67 |
+ t.Log(err.Error()) |
|
| 68 |
+ t.Skip(format.Message(msgAndArgs...)) |
|
| 131 | 69 |
} |
| 132 |
- return fmt.Sprintf("%s: %s", source, custom)
|
|
| 70 |
+ t.Skip(format.WithCustomMessage(source, msgAndArgs...)) |
|
| 133 | 71 |
} |