mirror of https://github.com/k3d-io/k3d
parent
9323179a3c
commit
03eaba037f
@ -0,0 +1,104 @@ |
||||
/* |
||||
Copyright © 2019 Thorsten Klein <iwilltry42@gmail.com> |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in |
||||
all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
THE SOFTWARE. |
||||
*/ |
||||
|
||||
package docker |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
|
||||
"github.com/docker/docker/api/types/filters" |
||||
|
||||
"github.com/docker/docker/api/types" |
||||
"github.com/docker/docker/client" |
||||
k3d "github.com/rancher/k3d/pkg/types" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// CreateNode creates a new container
|
||||
func (d Docker) CreateNode(node *k3d.Node) error { |
||||
log.Debugln("docker.CreateNode...") |
||||
|
||||
// translate node spec to docker container specs
|
||||
dockerNode, err := TranslateNodeToContainer(node) |
||||
if err != nil { |
||||
log.Errorln("Failed to translate k3d node specification to docker container specifications") |
||||
return err |
||||
} |
||||
|
||||
// create node
|
||||
if err := createContainer(dockerNode, node.Name); err != nil { |
||||
log.Errorln("Failed to create k3d node") |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// DeleteNode deletes a node
|
||||
func (d Docker) DeleteNode(nodeSpec *k3d.Node) error { |
||||
log.Debugln("docker.DeleteNode...") |
||||
return removeContainer(nodeSpec.Name) |
||||
} |
||||
|
||||
// GetNodesByLabel returns a list of existing nodes
|
||||
func (d Docker) GetNodesByLabel(labels map[string]string) ([]*k3d.Node, error) { |
||||
|
||||
// (0) create docker client
|
||||
ctx := context.Background() |
||||
docker, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Failed to create docker client. %+v", err) |
||||
} |
||||
|
||||
// (1) list containers which have the default k3d labels attached
|
||||
filters := filters.NewArgs() |
||||
for k, v := range k3d.DefaultObjectLabels { |
||||
filters.Add("label", fmt.Sprintf("%s=%s", k, v)) |
||||
} |
||||
for k, v := range labels { |
||||
filters.Add("label", fmt.Sprintf("%s=%s", k, v)) |
||||
} |
||||
|
||||
containers, err := docker.ContainerList(ctx, types.ContainerListOptions{ |
||||
Filters: filters, |
||||
All: true, |
||||
}) |
||||
if err != nil { |
||||
log.Errorln("Failed to list containers") |
||||
return nil, err |
||||
} |
||||
|
||||
// (2) convert them to node structs
|
||||
nodes := []*k3d.Node{} |
||||
for _, container := range containers { |
||||
node := &k3d.Node{ |
||||
Name: container.Names[0], |
||||
Role: container.Labels["role"], // TODO: catch keyerror
|
||||
Labels: container.Labels, |
||||
} |
||||
nodes = append(nodes, node) |
||||
} |
||||
|
||||
return nodes, nil |
||||
|
||||
} |
@ -0,0 +1,2 @@ |
||||
*.swp |
||||
*.out |
@ -0,0 +1,13 @@ |
||||
language: go |
||||
|
||||
go: |
||||
- "1.10" |
||||
- "1.11" |
||||
- "1.12" |
||||
|
||||
before_install: |
||||
- go get github.com/mattn/goveralls |
||||
- go get golang.org/x/tools/cover |
||||
|
||||
script: |
||||
- $HOME/gopath/bin/goveralls -service=travis-ci |
@ -0,0 +1,30 @@ |
||||
# go-test/deep Changelog |
||||
|
||||
## v1.0.4 released 2019-09-15 |
||||
|
||||
* Added \`deep:"-"\` structure field tag to ignore field (PR #38) (@flga) |
||||
|
||||
## v1.0.3 released 2019-08-18 |
||||
|
||||
* Fixed issue #31: panic on typed primitives that implement error interface |
||||
|
||||
## v1.0.2 released 2019-07-14 |
||||
|
||||
* Enabled Go module (@radeksimko) |
||||
* Changed supported and tested Go versions: 1.10, 1.11, and 1.12 (dropped 1.9) |
||||
* Changed Error equality: additional struct fields are compared too (PR #29) (@andrewmostello) |
||||
* Fixed typos and ineffassign issues (PR #25) (@tariq1890) |
||||
* Fixed diff order for nil comparison (PR #16) (@gmarik) |
||||
* Fixed slice equality when slices are extracted from the same array (PR #11) (@risteli) |
||||
* Fixed test spelling and messages (PR #19) (@sofuture) |
||||
* Fixed issue #15: panic on comparing struct with anonymous time.Time |
||||
* Fixed issue #18: Panic when comparing structs with time.Time value and CompareUnexportedFields is true |
||||
* Fixed issue #21: Set default MaxDepth = 0 (disabled) (PR #23) |
||||
|
||||
## v1.0.1 released 2018-01-28 |
||||
|
||||
* Fixed issue #12: Arrays are not properly compared (@samlitowitz) |
||||
|
||||
## v1.0.0 releaesd 2017-10-27 |
||||
|
||||
* First release |
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright 2015-2017 Daniel Nichter |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,51 @@ |
||||
# Deep Variable Equality for Humans |
||||
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-test/deep)](https://goreportcard.com/report/github.com/go-test/deep) [![Build Status](https://travis-ci.org/go-test/deep.svg?branch=master)](https://travis-ci.org/go-test/deep) [![Coverage Status](https://coveralls.io/repos/github/go-test/deep/badge.svg?branch=master)](https://coveralls.io/github/go-test/deep?branch=master) [![GoDoc](https://godoc.org/github.com/go-test/deep?status.svg)](https://godoc.org/github.com/go-test/deep) |
||||
|
||||
This package provides a single function: `deep.Equal`. It's like [reflect.DeepEqual](http://golang.org/pkg/reflect/#DeepEqual) but much friendlier to humans (or any sentient being) for two reason: |
||||
|
||||
* `deep.Equal` returns a list of differences |
||||
* `deep.Equal` does not compare unexported fields (by default) |
||||
|
||||
`reflect.DeepEqual` is good (like all things Golang!), but it's a game of [Hunt the Wumpus](https://en.wikipedia.org/wiki/Hunt_the_Wumpus). For large maps, slices, and structs, finding the difference is difficult. |
||||
|
||||
`deep.Equal` doesn't play games with you, it lists the differences: |
||||
|
||||
```go |
||||
package main_test |
||||
|
||||
import ( |
||||
"testing" |
||||
"github.com/go-test/deep" |
||||
) |
||||
|
||||
type T struct { |
||||
Name string |
||||
Numbers []float64 |
||||
} |
||||
|
||||
func TestDeepEqual(t *testing.T) { |
||||
// Can you spot the difference? |
||||
t1 := T{ |
||||
Name: "Isabella", |
||||
Numbers: []float64{1.13459, 2.29343, 3.010100010}, |
||||
} |
||||
t2 := T{ |
||||
Name: "Isabella", |
||||
Numbers: []float64{1.13459, 2.29843, 3.010100010}, |
||||
} |
||||
|
||||
if diff := deep.Equal(t1, t2); diff != nil { |
||||
t.Error(diff) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
|
||||
``` |
||||
$ go test |
||||
--- FAIL: TestDeepEqual (0.00s) |
||||
main_test.go:25: [Numbers.slice[1]: 2.29343 != 2.29843] |
||||
``` |
||||
|
||||
The difference is in `Numbers.slice[1]`: the two values aren't equal using Go `==`. |
@ -0,0 +1,376 @@ |
||||
// Package deep provides function deep.Equal which is like reflect.DeepEqual but
|
||||
// returns a list of differences. This is helpful when comparing complex types
|
||||
// like structures and maps.
|
||||
package deep |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"log" |
||||
"reflect" |
||||
"strings" |
||||
) |
||||
|
||||
var ( |
||||
// FloatPrecision is the number of decimal places to round float values
|
||||
// to when comparing.
|
||||
FloatPrecision = 10 |
||||
|
||||
// MaxDiff specifies the maximum number of differences to return.
|
||||
MaxDiff = 10 |
||||
|
||||
// MaxDepth specifies the maximum levels of a struct to recurse into,
|
||||
// if greater than zero. If zero, there is no limit.
|
||||
MaxDepth = 0 |
||||
|
||||
// LogErrors causes errors to be logged to STDERR when true.
|
||||
LogErrors = false |
||||
|
||||
// CompareUnexportedFields causes unexported struct fields, like s in
|
||||
// T{s int}, to be compared when true.
|
||||
CompareUnexportedFields = false |
||||
) |
||||
|
||||
var ( |
||||
// ErrMaxRecursion is logged when MaxDepth is reached.
|
||||
ErrMaxRecursion = errors.New("recursed to MaxDepth") |
||||
|
||||
// ErrTypeMismatch is logged when Equal passed two different types of values.
|
||||
ErrTypeMismatch = errors.New("variables are different reflect.Type") |
||||
|
||||
// ErrNotHandled is logged when a primitive Go kind is not handled.
|
||||
ErrNotHandled = errors.New("cannot compare the reflect.Kind") |
||||
) |
||||
|
||||
type cmp struct { |
||||
diff []string |
||||
buff []string |
||||
floatFormat string |
||||
} |
||||
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem() |
||||
|
||||
// Equal compares variables a and b, recursing into their structure up to
|
||||
// MaxDepth levels deep (if greater than zero), and returns a list of differences,
|
||||
// or nil if there are none. Some differences may not be found if an error is
|
||||
// also returned.
|
||||
//
|
||||
// If a type has an Equal method, like time.Equal, it is called to check for
|
||||
// equality.
|
||||
//
|
||||
// When comparing a struct, if a field has the tag `deep:"-"` then it will be
|
||||
// ignored.
|
||||
func Equal(a, b interface{}) []string { |
||||
aVal := reflect.ValueOf(a) |
||||
bVal := reflect.ValueOf(b) |
||||
c := &cmp{ |
||||
diff: []string{}, |
||||
buff: []string{}, |
||||
floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), |
||||
} |
||||
if a == nil && b == nil { |
||||
return nil |
||||
} else if a == nil && b != nil { |
||||
c.saveDiff("<nil pointer>", b) |
||||
} else if a != nil && b == nil { |
||||
c.saveDiff(a, "<nil pointer>") |
||||
} |
||||
if len(c.diff) > 0 { |
||||
return c.diff |
||||
} |
||||
|
||||
c.equals(aVal, bVal, 0) |
||||
if len(c.diff) > 0 { |
||||
return c.diff // diffs
|
||||
} |
||||
return nil // no diffs
|
||||
} |
||||
|
||||
func (c *cmp) equals(a, b reflect.Value, level int) { |
||||
if MaxDepth > 0 && level > MaxDepth { |
||||
logError(ErrMaxRecursion) |
||||
return |
||||
} |
||||
|
||||
// Check if one value is nil, e.g. T{x: *X} and T.x is nil
|
||||
if !a.IsValid() || !b.IsValid() { |
||||
if a.IsValid() && !b.IsValid() { |
||||
c.saveDiff(a.Type(), "<nil pointer>") |
||||
} else if !a.IsValid() && b.IsValid() { |
||||
c.saveDiff("<nil pointer>", b.Type()) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// If different types, they can't be equal
|
||||
aType := a.Type() |
||||
bType := b.Type() |
||||
if aType != bType { |
||||
c.saveDiff(aType, bType) |
||||
logError(ErrTypeMismatch) |
||||
return |
||||
} |
||||
|
||||
// Primitive https://golang.org/pkg/reflect/#Kind
|
||||
aKind := a.Kind() |
||||
bKind := b.Kind() |
||||
|
||||
// Do a and b have underlying elements? Yes if they're ptr or interface.
|
||||
aElem := aKind == reflect.Ptr || aKind == reflect.Interface |
||||
bElem := bKind == reflect.Ptr || bKind == reflect.Interface |
||||
|
||||
// If both types implement the error interface, compare the error strings.
|
||||
// This must be done before dereferencing because the interface is on a
|
||||
// pointer receiver. Re https://github.com/go-test/deep/issues/31, a/b might
|
||||
// be primitive kinds; see TestErrorPrimitiveKind.
|
||||
if aType.Implements(errorType) && bType.Implements(errorType) { |
||||
if (!aElem || !a.IsNil()) && (!bElem || !b.IsNil()) { |
||||
aString := a.MethodByName("Error").Call(nil)[0].String() |
||||
bString := b.MethodByName("Error").Call(nil)[0].String() |
||||
if aString != bString { |
||||
c.saveDiff(aString, bString) |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Dereference pointers and interface{}
|
||||
if aElem || bElem { |
||||
if aElem { |
||||
a = a.Elem() |
||||
} |
||||
if bElem { |
||||
b = b.Elem() |
||||
} |
||||
c.equals(a, b, level+1) |
||||
return |
||||
} |
||||
|
||||
switch aKind { |
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Iterable kinds
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
case reflect.Struct: |
||||
/* |
||||
The variables are structs like: |
||||
type T struct { |
||||
FirstName string |
||||
LastName string |
||||
} |
||||
Type = <pkg>.T, Kind = reflect.Struct |
||||
|
||||
Iterate through the fields (FirstName, LastName), recurse into their values. |
||||
*/ |
||||
|
||||
// Types with an Equal() method, like time.Time, only if struct field
|
||||
// is exported (CanInterface)
|
||||
if eqFunc := a.MethodByName("Equal"); eqFunc.IsValid() && eqFunc.CanInterface() { |
||||
// Handle https://github.com/go-test/deep/issues/15:
|
||||
// Don't call T.Equal if the method is from an embedded struct, like:
|
||||
// type Foo struct { time.Time }
|
||||
// First, we'll encounter Equal(Ttime, time.Time) but if we pass b
|
||||
// as the 2nd arg we'll panic: "Call using pkg.Foo as type time.Time"
|
||||
// As far as I can tell, there's no way to see that the method is from
|
||||
// time.Time not Foo. So we check the type of the 1st (0) arg and skip
|
||||
// unless it's b type. Later, we'll encounter the time.Time anonymous/
|
||||
// embedded field and then we'll have Equal(time.Time, time.Time).
|
||||
funcType := eqFunc.Type() |
||||
if funcType.NumIn() == 1 && funcType.In(0) == bType { |
||||
retVals := eqFunc.Call([]reflect.Value{b}) |
||||
if !retVals[0].Bool() { |
||||
c.saveDiff(a, b) |
||||
} |
||||
return |
||||
} |
||||
} |
||||
|
||||
for i := 0; i < a.NumField(); i++ { |
||||
if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { |
||||
continue // skip unexported field, e.g. s in type T struct {s string}
|
||||
} |
||||
|
||||
if aType.Field(i).Tag.Get("deep") == "-" { |
||||
continue // field wants to be ignored
|
||||
} |
||||
|
||||
c.push(aType.Field(i).Name) // push field name to buff
|
||||
|
||||
// Get the Value for each field, e.g. FirstName has Type = string,
|
||||
// Kind = reflect.String.
|
||||
af := a.Field(i) |
||||
bf := b.Field(i) |
||||
|
||||
// Recurse to compare the field values
|
||||
c.equals(af, bf, level+1) |
||||
|
||||
c.pop() // pop field name from buff
|
||||
|
||||
if len(c.diff) >= MaxDiff { |
||||
break |
||||
} |
||||
} |
||||
case reflect.Map: |
||||
/* |
||||
The variables are maps like: |
||||
map[string]int{ |
||||
"foo": 1, |
||||
"bar": 2, |
||||
} |
||||
Type = map[string]int, Kind = reflect.Map |
||||
|
||||
Or: |
||||
type T map[string]int{} |
||||
Type = <pkg>.T, Kind = reflect.Map |
||||
|
||||
Iterate through the map keys (foo, bar), recurse into their values. |
||||
*/ |
||||
|
||||
if a.IsNil() || b.IsNil() { |
||||
if a.IsNil() && !b.IsNil() { |
||||
c.saveDiff("<nil map>", b) |
||||
} else if !a.IsNil() && b.IsNil() { |
||||
c.saveDiff(a, "<nil map>") |
||||
} |
||||
return |
||||
} |
||||
|
||||
if a.Pointer() == b.Pointer() { |
||||
return |
||||
} |
||||
|
||||
for _, key := range a.MapKeys() { |
||||
c.push(fmt.Sprintf("map[%s]", key)) |
||||
|
||||
aVal := a.MapIndex(key) |
||||
bVal := b.MapIndex(key) |
||||
if bVal.IsValid() { |
||||
c.equals(aVal, bVal, level+1) |
||||
} else { |
||||
c.saveDiff(aVal, "<does not have key>") |
||||
} |
||||
|
||||
c.pop() |
||||
|
||||
if len(c.diff) >= MaxDiff { |
||||
return |
||||
} |
||||
} |
||||
|
||||
for _, key := range b.MapKeys() { |
||||
if aVal := a.MapIndex(key); aVal.IsValid() { |
||||
continue |
||||
} |
||||
|
||||
c.push(fmt.Sprintf("map[%s]", key)) |
||||
c.saveDiff("<does not have key>", b.MapIndex(key)) |
||||
c.pop() |
||||
if len(c.diff) >= MaxDiff { |
||||
return |
||||
} |
||||
} |
||||
case reflect.Array: |
||||
n := a.Len() |
||||
for i := 0; i < n; i++ { |
||||
c.push(fmt.Sprintf("array[%d]", i)) |
||||
c.equals(a.Index(i), b.Index(i), level+1) |
||||
c.pop() |
||||
if len(c.diff) >= MaxDiff { |
||||
break |
||||
} |
||||
} |
||||
case reflect.Slice: |
||||
if a.IsNil() || b.IsNil() { |
||||
if a.IsNil() && !b.IsNil() { |
||||
c.saveDiff("<nil slice>", b) |
||||
} else if !a.IsNil() && b.IsNil() { |
||||
c.saveDiff(a, "<nil slice>") |
||||
} |
||||
return |
||||
} |
||||
|
||||
aLen := a.Len() |
||||
bLen := b.Len() |
||||
|
||||
if a.Pointer() == b.Pointer() && aLen == bLen { |
||||
return |
||||
} |
||||
|
||||
n := aLen |
||||
if bLen > aLen { |
||||
n = bLen |
||||
} |
||||
for i := 0; i < n; i++ { |
||||
c.push(fmt.Sprintf("slice[%d]", i)) |
||||
if i < aLen && i < bLen { |
||||
c.equals(a.Index(i), b.Index(i), level+1) |
||||
} else if i < aLen { |
||||
c.saveDiff(a.Index(i), "<no value>") |
||||
} else { |
||||
c.saveDiff("<no value>", b.Index(i)) |
||||
} |
||||
c.pop() |
||||
if len(c.diff) >= MaxDiff { |
||||
break |
||||
} |
||||
} |
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Primitive kinds
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
case reflect.Float32, reflect.Float64: |
||||
// Avoid 0.04147685731961082 != 0.041476857319611
|
||||
// 6 decimal places is close enough
|
||||
aval := fmt.Sprintf(c.floatFormat, a.Float()) |
||||
bval := fmt.Sprintf(c.floatFormat, b.Float()) |
||||
if aval != bval { |
||||
c.saveDiff(a.Float(), b.Float()) |
||||
} |
||||
case reflect.Bool: |
||||
if a.Bool() != b.Bool() { |
||||
c.saveDiff(a.Bool(), b.Bool()) |
||||
} |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
if a.Int() != b.Int() { |
||||
c.saveDiff(a.Int(), b.Int()) |
||||
} |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: |
||||
if a.Uint() != b.Uint() { |
||||
c.saveDiff(a.Uint(), b.Uint()) |
||||
} |
||||
case reflect.String: |
||||
if a.String() != b.String() { |
||||
c.saveDiff(a.String(), b.String()) |
||||
} |
||||
|
||||
default: |
||||
logError(ErrNotHandled) |
||||
} |
||||
} |
||||
|
||||
func (c *cmp) push(name string) { |
||||
c.buff = append(c.buff, name) |
||||
} |
||||
|
||||
func (c *cmp) pop() { |
||||
if len(c.buff) > 0 { |
||||
c.buff = c.buff[0 : len(c.buff)-1] |
||||
} |
||||
} |
||||
|
||||
func (c *cmp) saveDiff(aval, bval interface{}) { |
||||
if len(c.buff) > 0 { |
||||
varName := strings.Join(c.buff, ".") |
||||
c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) |
||||
} else { |
||||
c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval)) |
||||
} |
||||
} |
||||
|
||||
func logError(err error) { |
||||
if LogErrors { |
||||
log.Println(err) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
module github.com/go-test/deep |
Loading…
Reference in new issue