mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 18:35:44 +03:00
Before this change, Client had 2 copies of the config object: config and configCopy. There was no guidance around which to use where (other than configCopy's comment to pass it to alloc runners), both are shared among goroutines and mutated in data racy ways. At least at one point I think the idea was to have `config` be mutable and then grab a lock to overwrite `configCopy`'s pointer atomically. This would have allowed alloc runners to read their config copies in data race safe ways, but this isn't how the current implementation worked. This change takes the following approach to safely handling configs in the client: 1. `Client.config` is the only copy of the config and all access must go through the `Client.configLock` mutex 2. Since the mutex *only protects the config pointer itself and not fields inside the Config struct:* all config mutation must be done on a *copy* of the config, and then Client's config pointer is overwritten while the mutex is acquired. Alloc runners and other goroutines with the old config pointer will not see config updates. 3. Deep copying is implemented on the Config struct to satisfy the previous approach. The TLS Keyloader is an exception because it has its own internal locking to support mutating in place. An unfortunate complication but one I couldn't find a way to untangle in a timely fashion. 4. To facilitate deep copying I made an *internally backward incompatible API change:* our `helper/funcs` used to turn containers (slices and maps) with 0 elements into nils. This probably saves a few memory allocations but makes it very easy to cause panics. Since my new config handling approach uses more copying, it became very difficult to ensure all code that used containers on configs could handle nils properly. Since this code has caused panics in the past, I fixed it: nil containers are copied as nil, but 0-element containers properly return a new 0-element container. No more "downgrading to nil!"
617 lines
15 KiB
Go
617 lines
15 KiB
Go
package helper
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/helper/pointer"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_Min(t *testing.T) {
|
|
t.Run("int", func(t *testing.T) {
|
|
a := 1
|
|
b := 2
|
|
must.Eq(t, 1, Min(a, b))
|
|
must.Eq(t, 1, Min(b, a))
|
|
})
|
|
|
|
t.Run("float64", func(t *testing.T) {
|
|
a := 1.1
|
|
b := 2.2
|
|
must.Eq(t, 1.1, Min(a, b))
|
|
must.Eq(t, 1.1, Min(b, a))
|
|
})
|
|
|
|
t.Run("string", func(t *testing.T) {
|
|
a := "cat"
|
|
b := "dog"
|
|
must.Eq(t, "cat", Min(a, b))
|
|
must.Eq(t, "cat", Min(b, a))
|
|
})
|
|
}
|
|
|
|
func Test_Max(t *testing.T) {
|
|
t.Run("int", func(t *testing.T) {
|
|
a := 1
|
|
b := 2
|
|
must.Eq(t, 2, Max(a, b))
|
|
must.Eq(t, 2, Max(b, a))
|
|
})
|
|
|
|
t.Run("float64", func(t *testing.T) {
|
|
a := 1.1
|
|
b := 2.2
|
|
must.Eq(t, 2.2, Max(a, b))
|
|
must.Eq(t, 2.2, Max(b, a))
|
|
})
|
|
|
|
t.Run("string", func(t *testing.T) {
|
|
a := "cat"
|
|
b := "dog"
|
|
must.Eq(t, "dog", Max(a, b))
|
|
must.Eq(t, "dog", Max(b, a))
|
|
})
|
|
}
|
|
|
|
func Test_CopyMap(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
var m map[string]int
|
|
result := CopyMap(m)
|
|
must.Nil(t, result)
|
|
})
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
m := make(map[string]int, 10)
|
|
result := CopyMap(m)
|
|
must.MapEq(t, map[string]int{}, result)
|
|
})
|
|
|
|
t.Run("elements", func(t *testing.T) {
|
|
m := map[string]int{"a": 1, "b": 2}
|
|
result := CopyMap(m)
|
|
result["a"] = -1
|
|
must.MapEq(t, map[string]int{"a": -1, "b": 2}, result)
|
|
must.MapEq(t, map[string]int{"a": 1, "b": 2}, m) // not modified
|
|
})
|
|
}
|
|
|
|
func TestSliceStringIsSubset(t *testing.T) {
|
|
l := []string{"a", "b", "c"}
|
|
s := []string{"d"}
|
|
|
|
sub, offending := SliceStringIsSubset(l, l[:1])
|
|
if !sub || len(offending) != 0 {
|
|
t.Fatalf("bad %v %v", sub, offending)
|
|
}
|
|
|
|
sub, offending = SliceStringIsSubset(l, s)
|
|
if sub || len(offending) == 0 || offending[0] != "d" {
|
|
t.Fatalf("bad %v %v", sub, offending)
|
|
}
|
|
}
|
|
|
|
func TestSliceStringContains(t *testing.T) {
|
|
list := []string{"a", "b", "c"}
|
|
require.True(t, SliceStringContains(list, "a"))
|
|
require.True(t, SliceStringContains(list, "b"))
|
|
require.True(t, SliceStringContains(list, "c"))
|
|
require.False(t, SliceStringContains(list, "d"))
|
|
}
|
|
|
|
func TestSliceStringHasPrefix(t *testing.T) {
|
|
list := []string{"alpha", "bravo", "charlie", "definitely", "most definitely"}
|
|
// At least one string in the slice above starts with the following test prefix strings
|
|
require.True(t, SliceStringHasPrefix(list, "a"))
|
|
require.True(t, SliceStringHasPrefix(list, "b"))
|
|
require.True(t, SliceStringHasPrefix(list, "c"))
|
|
require.True(t, SliceStringHasPrefix(list, "d"))
|
|
require.True(t, SliceStringHasPrefix(list, "mos"))
|
|
require.True(t, SliceStringHasPrefix(list, "def"))
|
|
require.False(t, SliceStringHasPrefix(list, "delta"))
|
|
|
|
}
|
|
|
|
func TestStringHasPrefixInSlice(t *testing.T) {
|
|
prefixes := []string{"a", "b", "c", "definitely", "most definitely"}
|
|
// The following strings all start with at least one prefix in the slice above
|
|
require.True(t, StringHasPrefixInSlice("alpha", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("bravo", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("charlie", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("definitely", prefixes))
|
|
require.True(t, StringHasPrefixInSlice("most definitely", prefixes))
|
|
|
|
require.False(t, StringHasPrefixInSlice("mos", prefixes))
|
|
require.False(t, StringHasPrefixInSlice("def", prefixes))
|
|
require.False(t, StringHasPrefixInSlice("delta", prefixes))
|
|
|
|
}
|
|
|
|
func TestCompareTimePtrs(t *testing.T) {
|
|
t.Run("nil", func(t *testing.T) {
|
|
a := (*time.Duration)(nil)
|
|
b := (*time.Duration)(nil)
|
|
require.True(t, CompareTimePtrs(a, b))
|
|
c := pointer.Of(3 * time.Second)
|
|
require.False(t, CompareTimePtrs(a, c))
|
|
require.False(t, CompareTimePtrs(c, a))
|
|
})
|
|
|
|
t.Run("not nil", func(t *testing.T) {
|
|
a := pointer.Of(1 * time.Second)
|
|
b := pointer.Of(1 * time.Second)
|
|
c := pointer.Of(2 * time.Second)
|
|
require.True(t, CompareTimePtrs(a, b))
|
|
require.False(t, CompareTimePtrs(a, c))
|
|
})
|
|
}
|
|
|
|
func TestCompareSliceSetString(t *testing.T) {
|
|
cases := []struct {
|
|
A []string
|
|
B []string
|
|
Result bool
|
|
}{
|
|
{
|
|
A: []string{},
|
|
B: []string{},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{},
|
|
B: []string{"a"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a"},
|
|
B: []string{"a"},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{"a"},
|
|
B: []string{"b"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"b"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"a"},
|
|
Result: false,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"a", "b"},
|
|
Result: true,
|
|
},
|
|
{
|
|
A: []string{"a", "b"},
|
|
B: []string{"b", "a"},
|
|
Result: true,
|
|
},
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
tc := tc
|
|
t.Run(fmt.Sprintf("case-%da", i), func(t *testing.T) {
|
|
if res := CompareSliceSetString(tc.A, tc.B); res != tc.Result {
|
|
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
|
tc.Result, tc.A, tc.B, res,
|
|
)
|
|
}
|
|
})
|
|
|
|
// Function is commutative so compare B and A
|
|
t.Run(fmt.Sprintf("case-%db", i), func(t *testing.T) {
|
|
if res := CompareSliceSetString(tc.B, tc.A); res != tc.Result {
|
|
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
|
|
tc.Result, tc.B, tc.A, res,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMapStringStringSliceValueSet(t *testing.T) {
|
|
m := map[string][]string{
|
|
"foo": {"1", "2"},
|
|
"bar": {"3"},
|
|
"baz": nil,
|
|
}
|
|
|
|
act := MapStringStringSliceValueSet(m)
|
|
exp := []string{"1", "2", "3"}
|
|
sort.Strings(act)
|
|
if !reflect.DeepEqual(act, exp) {
|
|
t.Fatalf("Bad; got %v; want %v", act, exp)
|
|
}
|
|
}
|
|
|
|
func TestSetToSliceString(t *testing.T) {
|
|
set := map[string]struct{}{
|
|
"foo": {},
|
|
"bar": {},
|
|
"baz": {},
|
|
}
|
|
expect := []string{"foo", "bar", "baz"}
|
|
got := SetToSliceString(set)
|
|
require.ElementsMatch(t, expect, got)
|
|
}
|
|
|
|
func TestCopyMapStringSliceString(t *testing.T) {
|
|
m := map[string][]string{
|
|
"x": {"a", "b", "c"},
|
|
"y": {"1", "2", "3"},
|
|
"z": nil,
|
|
}
|
|
|
|
c := CopyMapStringSliceString(m)
|
|
if !reflect.DeepEqual(c, m) {
|
|
t.Fatalf("%#v != %#v", m, c)
|
|
}
|
|
|
|
c["x"][1] = "---"
|
|
if reflect.DeepEqual(c, m) {
|
|
t.Fatalf("Shared slices: %#v == %#v", m["x"], c["x"])
|
|
}
|
|
}
|
|
|
|
func TestCopyMapSliceInterface(t *testing.T) {
|
|
m := map[string]interface{}{
|
|
"foo": "bar",
|
|
"baz": 2,
|
|
}
|
|
|
|
c := CopyMapStringInterface(m)
|
|
require.True(t, reflect.DeepEqual(m, c))
|
|
|
|
m["foo"] = "zzz"
|
|
require.False(t, reflect.DeepEqual(m, c))
|
|
}
|
|
|
|
func TestMergeMapStringString(t *testing.T) {
|
|
type testCase struct {
|
|
map1 map[string]string
|
|
map2 map[string]string
|
|
expected map[string]string
|
|
}
|
|
|
|
cases := []testCase{
|
|
{map[string]string{"foo": "bar"}, map[string]string{"baz": "qux"}, map[string]string{"foo": "bar", "baz": "qux"}},
|
|
{map[string]string{"foo": "bar"}, nil, map[string]string{"foo": "bar"}},
|
|
{nil, map[string]string{"baz": "qux"}, map[string]string{"baz": "qux"}},
|
|
{nil, nil, map[string]string{}},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
if output := MergeMapStringString(c.map1, c.map2); !CompareMapStringString(output, c.expected) {
|
|
t.Errorf("MergeMapStringString(%q, %q) -> %q != %q", c.map1, c.map2, output, c.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCleanEnvVar(t *testing.T) {
|
|
type testCase struct {
|
|
input string
|
|
expected string
|
|
}
|
|
cases := []testCase{
|
|
{"asdf", "asdf"},
|
|
{"ASDF", "ASDF"},
|
|
{"0sdf", "_sdf"},
|
|
{"asd0", "asd0"},
|
|
{"_asd", "_asd"},
|
|
{"-asd", "_asd"},
|
|
{"asd.fgh", "asd.fgh"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A______________________________Z"},
|
|
{"A\U0001f4a9Z", "A____Z"},
|
|
}
|
|
for _, c := range cases {
|
|
if output := CleanEnvVar(c.input, '_'); output != c.expected {
|
|
t.Errorf("CleanEnvVar(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkCleanEnvVar(b *testing.B) {
|
|
in := "NOMAD_ADDR_redis-cache"
|
|
replacement := byte('_')
|
|
b.SetBytes(int64(len(in)))
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
CleanEnvVar(in, replacement)
|
|
}
|
|
}
|
|
|
|
type testCase struct {
|
|
input string
|
|
expected string
|
|
}
|
|
|
|
func commonCleanFilenameCases() (cases []testCase) {
|
|
// Common set of test cases for all 3 TestCleanFilenameX functions
|
|
cases = []testCase{
|
|
{"asdf", "asdf"},
|
|
{"ASDF", "ASDF"},
|
|
{"0sdf", "0sdf"},
|
|
{"asd0", "asd0"},
|
|
{"_asd", "_asd"},
|
|
{"-asd", "-asd"},
|
|
{"asd.fgh", "asd.fgh"},
|
|
{"Linux/Forbidden", "Linux_Forbidden"},
|
|
{"Windows<>:\"/\\|?*Forbidden", "Windows_________Forbidden"},
|
|
{`Windows<>:"/\|?*Forbidden_StringLiteral`, "Windows_________Forbidden_StringLiteral"},
|
|
}
|
|
return cases
|
|
}
|
|
|
|
func TestCleanFilename(t *testing.T) {
|
|
cases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A💩Z"}, // CleanFilename allows unicode
|
|
{"A💩Z", "A💩Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range cases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilename(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilename(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanFilenameASCIIOnly(t *testing.T) {
|
|
ASCIIOnlyCases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A_Z"}, // CleanFilenameASCIIOnly does not allow unicode
|
|
{"A💩Z", "A_Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A~!@#$%^&_()_+-={}[]__;_'__,___Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range ASCIIOnlyCases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilenameASCIIOnly(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilenameASCIIOnly(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanFilenameStrict(t *testing.T) {
|
|
strictCases := append(
|
|
[]testCase{
|
|
{"A\U0001f4a9Z", "A💩Z"}, // CleanFilenameStrict allows unicode
|
|
{"A💩Z", "A💩Z"},
|
|
{"A~!@#$%^&*()_+-={}[]|\\;:'\"<,>?/Z", "A_!___%^______-_{}_____________Z"},
|
|
}, commonCleanFilenameCases()...)
|
|
|
|
for i, c := range strictCases {
|
|
t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
|
|
output := CleanFilenameStrict(c.input, "_")
|
|
failMsg := fmt.Sprintf("CleanFilenameStrict(%q, '_') -> %q != %q", c.input, output, c.expected)
|
|
require.Equal(t, c.expected, output, failMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckNamespaceScope(t *testing.T) {
|
|
cases := []struct {
|
|
desc string
|
|
provided string
|
|
requested []string
|
|
offending []string
|
|
}{
|
|
{
|
|
desc: "root ns requesting namespace",
|
|
provided: "",
|
|
requested: []string{"engineering"},
|
|
},
|
|
{
|
|
desc: "matching parent ns with child",
|
|
provided: "engineering",
|
|
requested: []string{"engineering", "engineering/sub-team"},
|
|
},
|
|
{
|
|
desc: "mismatch ns",
|
|
provided: "engineering",
|
|
requested: []string{"finance", "engineering/sub-team", "eng"},
|
|
offending: []string{"finance", "eng"},
|
|
},
|
|
{
|
|
desc: "mismatch child",
|
|
provided: "engineering/sub-team",
|
|
requested: []string{"engineering/new-team", "engineering/sub-team", "engineering/sub-team/child"},
|
|
offending: []string{"engineering/new-team"},
|
|
},
|
|
{
|
|
desc: "matching prefix",
|
|
provided: "engineering",
|
|
requested: []string{"engineering/new-team", "engineering/new-team/sub-team"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
offending := CheckNamespaceScope(tc.provided, tc.requested)
|
|
require.Equal(t, offending, tc.offending)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPathEscapesSandbox(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
dir string
|
|
expected bool
|
|
}{
|
|
{
|
|
// this is the ${NOMAD_SECRETS_DIR} case
|
|
name: "ok joined absolute path inside sandbox",
|
|
path: filepath.Join("/alloc", "/secrets"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail unjoined absolute path outside sandbox",
|
|
path: "/secrets",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ok joined relative path inside sandbox",
|
|
path: filepath.Join("/alloc", "./safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail unjoined relative path outside sandbox",
|
|
path: "./safe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "ok relative path traversal constrained to sandbox",
|
|
path: filepath.Join("/alloc", "../../alloc/safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ok unjoined absolute path traversal constrained to sandbox",
|
|
path: filepath.Join("/alloc", "/../alloc/safe"),
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "ok unjoined absolute path traversal constrained to sandbox",
|
|
path: "/../alloc/safe",
|
|
dir: "/alloc",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fail joined relative path traverses outside sandbox",
|
|
path: filepath.Join("/alloc", "../../../unsafe"),
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail unjoined relative path traverses outside sandbox",
|
|
path: "../../../unsafe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail joined absolute path tries to transverse outside sandbox",
|
|
path: filepath.Join("/alloc", "/alloc/../../unsafe"),
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "fail unjoined absolute path tries to transverse outside sandbox",
|
|
path: "/alloc/../../unsafe",
|
|
dir: "/alloc",
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
caseMsg := fmt.Sprintf("path: %v\ndir: %v", tc.path, tc.dir)
|
|
escapes := PathEscapesSandbox(tc.dir, tc.path)
|
|
require.Equal(t, tc.expected, escapes, caseMsg)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_NewSafeTimer(t *testing.T) {
|
|
t.Run("zero", func(t *testing.T) {
|
|
timer, stop := NewSafeTimer(0)
|
|
defer stop()
|
|
<-timer.C
|
|
})
|
|
|
|
t.Run("positive", func(t *testing.T) {
|
|
timer, stop := NewSafeTimer(1)
|
|
defer stop()
|
|
<-timer.C
|
|
})
|
|
}
|
|
|
|
func Test_IsMethodHTTP(t *testing.T) {
|
|
t.Run("is method", func(t *testing.T) {
|
|
cases := []string{
|
|
"GET", "Get", "get",
|
|
"HEAD", "Head", "head",
|
|
"POST", "Post", "post",
|
|
"PUT", "Put", "put",
|
|
"PATCH", "Patch", "patch",
|
|
"DELETE", "Delete", "delete",
|
|
"CONNECT", "Connect", "connect",
|
|
"OPTIONS", "Options", "options",
|
|
"TRACE", "Trace", "trace",
|
|
}
|
|
for _, tc := range cases {
|
|
result := IsMethodHTTP(tc)
|
|
must.True(t, result)
|
|
}
|
|
})
|
|
|
|
t.Run("is not method", func(t *testing.T) {
|
|
not := []string{"GETTER", "!GET", ""}
|
|
for _, tc := range not {
|
|
result := IsMethodHTTP(tc)
|
|
must.False(t, result)
|
|
}
|
|
})
|
|
}
|
|
|
|
type employee struct {
|
|
id int
|
|
name string
|
|
}
|
|
|
|
func (e *employee) Equals(o *employee) bool {
|
|
return e.id == o.id // name can be different
|
|
}
|
|
|
|
func Test_ElementsEquals(t *testing.T) {
|
|
t.Run("empty", func(t *testing.T) {
|
|
a := []*employee(nil)
|
|
var b []*employee
|
|
must.True(t, ElementsEquals(a, b))
|
|
must.True(t, ElementsEquals(b, a))
|
|
})
|
|
|
|
t.Run("different sizes", func(t *testing.T) {
|
|
a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
|
|
b := []*employee{{1, "mitchell"}, {2, "armon"}}
|
|
must.False(t, ElementsEquals(a, b))
|
|
must.False(t, ElementsEquals(b, a))
|
|
})
|
|
|
|
t.Run("equal", func(t *testing.T) {
|
|
a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
|
|
b := []*employee{{1, "M.H."}, {2, "A.D."}, {3, "J.P."}}
|
|
must.True(t, ElementsEquals(a, b))
|
|
must.True(t, ElementsEquals(b, a))
|
|
})
|
|
|
|
t.Run("different", func(t *testing.T) {
|
|
a := []*employee{{1, "mitchell"}, {2, "armon"}, {3, "jack"}}
|
|
b := []*employee{{0, "mitchell."}, {2, "armon"}, {3, "jack"}}
|
|
must.False(t, ElementsEquals(a, b))
|
|
must.False(t, ElementsEquals(b, a))
|
|
})
|
|
}
|