open-nomad/helper/funcs_test.go

532 lines
13 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package helper
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/hashicorp/go-set"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
)
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 TestIsSubset(t *testing.T) {
l := []string{"a", "b", "c"}
s := []string{"d"}
sub, offending := IsSubset(l, l[:1])
must.True(t, sub)
must.SliceEmpty(t, offending)
sub, offending = IsSubset(l, s)
must.False(t, sub)
must.Eq(t, []string{"d"}, offending)
}
func TestIsDisjoint(t *testing.T) {
t.Run("yes", func(t *testing.T) {
a := []string{"a", "b", "c"}
b := []string{"d", "f"}
dis, offending := IsDisjoint(a, b)
must.True(t, dis)
must.SliceEmpty(t, offending)
})
t.Run("no", func(t *testing.T) {
a := []string{"a", "b", "c", "d", "e"}
b := []string{"b", "c", "f", "g"}
dis, offending := IsDisjoint(a, b)
must.False(t, dis)
must.True(t, set.From(offending).EqualSlice(offending))
})
}
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 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 := SliceSetEq(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 := SliceSetEq(tc.B, tc.A); res != tc.Result {
t.Fatalf("expected %t but CompareSliceSetString(%v, %v) -> %t",
tc.Result, tc.B, tc.A, res,
)
}
})
}
}
func TestUniqueMapSliceValues(t *testing.T) {
m := map[string][]string{
"foo": {"1", "2"},
"bar": {"3"},
"baz": nil,
}
act := UniqueMapSliceValues(m)
exp := []string{"1", "2", "3"}
sort.Strings(act)
must.Eq(t, exp, act)
}
func TestCopyMapStringSliceString(t *testing.T) {
m := map[string][]string{
"x": {"a", "b", "c"},
"y": {"1", "2", "3"},
"z": nil,
}
c := CopyMapOfSlice(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 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); !maps.Equal(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 TestTimer_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 TestTimer_NewStoppedTimer(t *testing.T) {
timer, stop := NewStoppedTimer()
defer stop()
select {
case <-timer.C:
must.Unreachable(t)
default:
}
}
func Test_ConvertSlice(t *testing.T) {
t.Run("string wrapper", func(t *testing.T) {
type wrapper struct{ id string }
input := []string{"foo", "bar", "bad", "had"}
cFn := func(id string) *wrapper { return &wrapper{id: id} }
expectedOutput := []*wrapper{{id: "foo"}, {id: "bar"}, {id: "bad"}, {id: "had"}}
actualOutput := ConvertSlice(input, cFn)
require.ElementsMatch(t, expectedOutput, actualOutput)
})
t.Run("int wrapper", func(t *testing.T) {
type wrapper struct{ id int }
input := []int{10, 13, 1987, 2020}
cFn := func(id int) *wrapper { return &wrapper{id: id} }
expectedOutput := []*wrapper{{id: 10}, {id: 13}, {id: 1987}, {id: 2020}}
actualOutput := ConvertSlice(input, cFn)
require.ElementsMatch(t, expectedOutput, actualOutput)
})
}
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) Equal(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, ElementsEqual(a, b))
must.True(t, ElementsEqual(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, ElementsEqual(a, b))
must.False(t, ElementsEqual(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, ElementsEqual(a, b))
must.True(t, ElementsEqual(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, ElementsEqual(a, b))
must.False(t, ElementsEqual(b, a))
})
}
func Test_SliceSetEq(t *testing.T) {
t.Run("empty", func(t *testing.T) {
a := make([]int, 0)
b := make([]int, 0)
must.True(t, SliceSetEq(a, b))
})
t.Run("subset small", func(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
b := []int{1, 2, 3}
must.False(t, SliceSetEq(a, b))
must.False(t, SliceSetEq(b, a))
})
t.Run("subset large", func(t *testing.T) {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
b := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
must.False(t, SliceSetEq(a, b))
must.False(t, SliceSetEq(b, a))
})
t.Run("same small", func(t *testing.T) {
a := []int{1, 2, 3, 4, 5}
b := []int{1, 2, 3, 4, 5}
must.True(t, SliceSetEq(a, b))
})
t.Run("same large", func(t *testing.T) {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
b := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
must.True(t, SliceSetEq(a, b))
})
}