blob: 624380f3db9b835b07d80a58e4964e2e58cb3439 [file] [log] [blame]
package util
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/deepequal/assertdeep"
)
func TestSSliceEqual(t *testing.T) {
testcases := []struct {
a []string
b []string
want bool
}{
{
a: []string{},
b: []string{},
want: true,
},
{
a: nil,
b: []string{},
want: false,
},
{
a: nil,
b: nil,
want: true,
},
{
a: []string{"foo"},
b: []string{},
want: false,
},
{
a: []string{"foo", "bar"},
b: []string{"bar", "foo"},
want: false,
},
{
a: []string{"foo", "bar"},
b: []string{"foo", "bar"},
want: true,
},
}
for _, tc := range testcases {
if got, want := SSliceEqual(tc.a, tc.b), tc.want; got != want {
t.Errorf("SSliceEqual(%#v, %#v): Got %v Want %v", tc.a, tc.b, got, want)
}
}
}
func TestInsertString(t *testing.T) {
assertdeep.Equal(t, []string{"a"}, insertString([]string{}, 0, "a"))
assertdeep.Equal(t, []string{"b", "a"}, insertString([]string{"a"}, 0, "b"))
assertdeep.Equal(t, []string{"b", "c", "a"}, insertString([]string{"b", "a"}, 1, "c"))
assertdeep.Equal(t, []string{"b", "c", "a", "d"}, insertString([]string{"b", "c", "a"}, 3, "d"))
}
func TestInsertStringSorted(t *testing.T) {
assertdeep.Equal(t, []string{"a"}, InsertStringSorted([]string{}, "a"))
assertdeep.Equal(t, []string{"a"}, InsertStringSorted([]string{"a"}, "a"))
assertdeep.Equal(t, []string{"a", "b"}, InsertStringSorted([]string{"a"}, "b"))
assertdeep.Equal(t, []string{"0", "a", "b"}, InsertStringSorted([]string{"a", "b"}, "0"))
assertdeep.Equal(t, []string{"0", "a", "b"}, InsertStringSorted([]string{"0", "a", "b"}, "b"))
}
func TestIsNil(t *testing.T) {
require.True(t, IsNil(nil))
require.False(t, IsNil(false))
require.False(t, IsNil(0))
require.False(t, IsNil(""))
require.False(t, IsNil([0]int{}))
type Empty struct{}
require.False(t, IsNil(Empty{}))
require.True(t, IsNil(chan interface{}(nil)))
require.False(t, IsNil(make(chan interface{})))
var f func()
require.True(t, IsNil(f))
require.False(t, IsNil(func() {}))
require.True(t, IsNil(map[bool]bool(nil)))
require.False(t, IsNil(make(map[bool]bool)))
require.True(t, IsNil([]int(nil)))
require.False(t, IsNil([][]int{nil}))
require.True(t, IsNil((*int)(nil)))
var i int
require.False(t, IsNil(&i))
var pi *int
require.True(t, IsNil(pi))
require.True(t, IsNil(&pi))
var ppi **int
require.True(t, IsNil(&ppi))
var c chan interface{}
require.True(t, IsNil(&c))
var w io.Writer
require.True(t, IsNil(w))
w = (*bytes.Buffer)(nil)
require.True(t, IsNil(w))
w = &bytes.Buffer{}
require.False(t, IsNil(w))
require.False(t, IsNil(&w))
var ii interface{}
ii = &pi
require.True(t, IsNil(ii))
}
func TestMD5Hash(t *testing.T) {
m_1 := map[string]string{"key1": "val1"}
m_2 := map[string]string{}
var m_3 map[string]string = nil
m_4 := map[string]string{
"k3": "v1",
"k2": "v2",
"k1": "v3",
"k4": "v4",
}
h_1, err := MD5Sum(m_1)
require.NoError(t, err)
h_2, err := MD5Sum(m_2)
require.NoError(t, err)
h_3, err := MD5Sum(m_3)
require.NoError(t, err)
require.Equal(t, 32, len(h_1))
require.Equal(t, 32, len(h_2))
require.Equal(t, 32, len(h_3))
require.NotEqual(t, h_1, h_2)
require.NotEqual(t, h_1, h_3)
require.Equal(t, h_2, h_3)
// Ensure that we get the same hash every time.
h_4, err := MD5Sum(m_4)
require.NoError(t, err)
for i := 0; i < 100; i++ {
h, err := MD5Sum(m_4)
require.NoError(t, err)
require.Equal(t, h_4, h)
}
h, err := MD5Sum(map[string]string{
"k4": "v4",
"k2": "v2",
"k3": "v1",
"k1": "v3",
})
require.NoError(t, err)
require.Equal(t, h_4, h)
}
func TestIsDirEmpty(t *testing.T) {
d, err := os.MkdirTemp(os.TempDir(), "test_empty")
require.NoError(t, err)
defer RemoveAll(d)
// Directory is initially empty.
empty, err := IsDirEmpty(d)
require.NoError(t, err)
require.True(t, empty)
// Add a file in the directory.
f, err := os.CreateTemp(d, "test_file")
require.NoError(t, err)
_, err = f.WriteString("testing")
Close(f)
require.NoError(t, err)
empty, err = IsDirEmpty(d)
require.NoError(t, err)
require.False(t, empty)
// Test non existent directory.
empty, err = IsDirEmpty(path.Join(d, "nonexistent_dir"))
require.NotNil(t, err)
}
type DomainTestCase struct {
DomainA string
DomainB string
Match bool
}
func TestValidateCommit(t *testing.T) {
tc := map[string]bool{
"": false,
"abc123": false,
"abcde12345abcde12345abcde12345abcde12345": true,
"abcde12345abcde12345abcde12345abcde1234": false,
"abcde12345abcde12345abcde12345abcde123456": false,
"abcde12345abcde12345abcde12345abcde1234g": false,
"abcde12345abcde12345abcde12345abcde1234 ": false,
}
for input, expect := range tc {
require.Equal(t, ValidateCommit(input), expect)
}
}
func TestParseIntSet(t *testing.T) {
test := func(input string, expect []int, expectErr string) {
res, err := ParseIntSet(input)
if expectErr != "" {
require.Contains(t, err.Error(), expectErr)
} else {
require.NoError(t, err)
require.Equal(t, expect, res)
}
}
test("", []int{}, "")
test("19", []int{19}, "")
test("1,2,3", []int{1, 2, 3}, "")
test("1-3", []int{1, 2, 3}, "")
test("1,2,4-6", []int{1, 2, 4, 5, 6}, "")
test("a", nil, "parsing \"a\": invalid syntax")
test(" 4, 6, 9 - 11", nil, "parsing \" 4\": invalid syntax")
test("4-9-10", nil, "Invalid expression \"4-9-10\"")
test("9-3", nil, "Cannot have a range whose beginning is greater than its end (9 vs 3)")
test("1-3,11-13,21-23", []int{1, 2, 3, 11, 12, 13, 21, 22, 23}, "")
test("-2", nil, "Invalid expression \"-2\"")
test("2-", nil, "Invalid expression \"2-\"")
}
func TestTruncate(t *testing.T) {
s := "abcdefghijkl"
require.Equal(t, "", Truncate(s, 0))
require.Equal(t, "a", Truncate(s, 1))
require.Equal(t, "ab", Truncate(s, 2))
require.Equal(t, "abc", Truncate(s, 3))
require.Equal(t, "a...", Truncate(s, 4))
require.Equal(t, "ab...", Truncate(s, 5))
require.Equal(t, s, Truncate(s, len(s)))
require.Equal(t, s, Truncate(s, len(s)+1))
}
func TestWithWriteFile(t *testing.T) {
tmp, err := os.MkdirTemp("", "whatever")
require.NoError(t, err)
targetFile := filepath.Join(tmp, "this", "is", "in", "a", "subdir.txt")
err = WithWriteFile(targetFile, func(w io.Writer) error {
_, err := w.Write([]byte("some words"))
return err
})
require.NoError(t, err)
require.FileExists(t, targetFile)
b, err := os.ReadFile(targetFile)
require.NoError(t, err)
assert.Equal(t, "some words", string(b))
}
type fakeWriter struct {
writeFn func(p []byte) (int, error)
}
func (w *fakeWriter) Write(p []byte) (int, error) {
return w.writeFn(p)
}
func TestWithGzipWriter(t *testing.T) {
write := func(w io.Writer, msg string) error {
_, err := w.Write([]byte(msg))
return err
}
// No error.
require.NoError(t, WithGzipWriter(ioutil.Discard, func(w io.Writer) error {
return write(w, "hi")
}))
// Contained function returns an error.
expectErr := errors.New("nope")
require.EqualError(t, WithGzipWriter(ioutil.Discard, func(w io.Writer) error {
return expectErr
}), expectErr.Error())
// Underlying io.Writer returns an error.
fw := &fakeWriter{
writeFn: func(p []byte) (int, error) {
return -1, expectErr
},
}
require.EqualError(t, WithGzipWriter(fw, func(w io.Writer) error {
return write(w, "hi")
}), expectErr.Error())
// Close() returns an error.
fw.writeFn = func(p []byte) (int, error) {
// Look for the gzip footer and return an error when we see it.
// WARNING: this contains a checksum.
if string(p) == "\xac*\x93\xd8\x02\x00\x00\x00" {
return -1, expectErr
}
return len(p), nil
}
err := WithGzipWriter(fw, func(w io.Writer) error {
return write(w, "hi")
})
require.Error(t, err)
assert.Contains(t, err.Error(), "closing gzip.Writer: nope")
}
func TestChunkIter_IteratesInBatches(t *testing.T) {
check := func(length, chunkSize int, expect [][]int) {
var actual [][]int
require.NoError(t, ChunkIter(length, chunkSize, func(start, end int) error {
actual = append(actual, []int{start, end})
return nil
}))
assert.Equal(t, expect, actual)
}
check(10, 5, [][]int{{0, 5}, {5, 10}})
check(4, 1, [][]int{{0, 1}, {1, 2}, {2, 3}, {3, 4}})
check(7, 5, [][]int{{0, 5}, {5, 7}})
// For an empty slice, we still want exactly one callback, in case there's extra work
// being done after iterating over the slice.
check(0, 5, [][]int{{0, 0}})
}
func TestChunkIter_InvalidBatches_Error(t *testing.T) {
assert.Error(t, ChunkIter(10, -1, func(int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
assert.Error(t, ChunkIter(10, 0, func(int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
}
func TestChunkIter_InvalidLength_Error(t *testing.T) {
assert.Error(t, ChunkIter(-1, 10, func(int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
}
func TestChunkIter_ErrorReturnedOnChunk_StopsAndReturnsError(t *testing.T) {
called := 0
err := ChunkIter(10, 3, func(int, int) error {
called++
return fmt.Errorf("oops, robots took over")
})
require.Error(t, err)
assert.Contains(t, err.Error(), "oops")
assert.Equal(t, 1, called, "stop working after error")
}
func TestChunkIterParallel_IteratesInBatches(t *testing.T) {
check := func(length, chunkSize int, expect []int, expectedCallbackCount int32) {
actual := make([]int, length)
ctx := context.Background()
calledTimes := int32(0)
require.NoError(t, ChunkIterParallel(ctx, length, chunkSize, func(eCtx context.Context, start, end int) error {
assert.NoError(t, eCtx.Err())
for i := start; i < end; i++ {
actual[i] = start
}
atomic.AddInt32(&calledTimes, 1)
return nil
}))
assert.Equal(t, expect, actual)
assert.Equal(t, expectedCallbackCount, calledTimes)
}
check(10, 5, []int{0, 0, 0, 0, 0, 5, 5, 5, 5, 5}, 2)
check(4, 1, []int{0, 1, 2, 3}, 4)
check(7, 4, []int{0, 0, 0, 0, 4, 4, 4}, 2)
// For an empty slice, we still want exactly one callback, in case there's extra work
// being done after iterating over the slice.
check(0, 5, []int{}, 1)
}
func TestChunkIterParallel_InvalidBatches_Error(t *testing.T) {
ctx := context.Background()
require.Error(t, ChunkIterParallel(ctx, 10, -1, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
require.Error(t, ChunkIterParallel(ctx, 10, 0, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
}
func TestChunkIterParallel_InvalidLength_Error(t *testing.T) {
ctx := context.Background()
require.Error(t, ChunkIterParallel(ctx, -1, 10, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
}
func TestChunkIterParallel_ErrorReturnedOnChunk_StopsAndReturnsError(t *testing.T) {
err := ChunkIterParallel(context.Background(), 10, 3, func(context.Context, int, int) error {
return fmt.Errorf("oops, robots took over")
})
require.Error(t, err)
// Either we'll see the error that we return or, due to the parallelism, a canceled context
// error due to the fact that the errgroup cancels the group context on an error.
if !(strings.Contains(err.Error(), "oops") || strings.Contains(err.Error(), "canceled")) {
assert.Fail(t, "unexpected error %s", err.Error())
}
}
func TestChunkIterParallel_CancelledContext_ReturnsImmediatelyWithError(t *testing.T) {
// If the context is already in an error state, don't call the passed in function, just error.
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := ChunkIterParallel(ctx, 10, 3, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called because the original context was no good.")
return nil
})
require.Error(t, err)
assert.Contains(t, err.Error(), "canceled")
}
func TestChunkIterParallelPool_IteratesInChunks_Success(t *testing.T) {
check := func(length, chunkSize int, expect []int, expectedCallbackCount int32) {
actual := make([]int, length)
ctx := context.Background()
calledTimes := int32(0)
require.NoError(t, ChunkIterParallelPool(ctx, length, chunkSize, 2, func(eCtx context.Context, start, end int) error {
assert.NoError(t, eCtx.Err())
for i := start; i < end; i++ {
actual[i] = start
}
atomic.AddInt32(&calledTimes, 1)
return nil
}))
assert.Equal(t, expect, actual)
assert.Equal(t, expectedCallbackCount, calledTimes)
}
check(10, 5, []int{0, 0, 0, 0, 0, 5, 5, 5, 5, 5}, 2)
check(4, 1, []int{0, 1, 2, 3}, 4)
check(7, 4, []int{0, 0, 0, 0, 4, 4, 4}, 2)
// For an empty slice, we still want exactly one callback, in case there's extra work
// being done after iterating over the slice.
check(0, 5, []int{}, 1)
}
func TestChunkIterParallelPool_InvalidArgs_Error(t *testing.T) {
ctx := context.Background()
require.Error(t, ChunkIterParallelPool(ctx, -1, 10, 2, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
require.Error(t, ChunkIterParallelPool(ctx, 10, 0, 2, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
require.Error(t, ChunkIterParallelPool(ctx, 10, 5, 0, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called")
return nil
}))
}
func TestChunkIterParallelPool_ErrorReturnedOnChunk_StopsAndReturnsError(t *testing.T) {
err := ChunkIterParallelPool(context.Background(), 10, 3, 2, func(context.Context, int, int) error {
return fmt.Errorf("oops, robots took over")
})
require.Error(t, err)
// Either we'll see the error that we return or, due to the parallelism, a canceled context
// error due to the fact that the errgroup cancels the group context on an error.
if !(strings.Contains(err.Error(), "oops") || strings.Contains(err.Error(), "canceled")) {
assert.Fail(t, "unexpected error %s", err.Error())
}
}
func TestChunkIterParallelPool_CancelledContext_ReturnsImmediatelyWithError(t *testing.T) {
// If the context is already in an error state, don't call the passed in function, just error.
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := ChunkIterParallelPool(ctx, 10, 3, 2, func(context.Context, int, int) error {
require.Fail(t, "shouldn't be called because the original context was no good.")
return nil
})
require.Error(t, err)
assert.Contains(t, err.Error(), "canceled")
}
func TestRoundUpToPowerOf2(t *testing.T) {
test := func(input, output int32) {
require.Equal(t, output, RoundUpToPowerOf2(input))
}
test(0, 1)
test(1, 1)
test(2, 2)
test(3, 4)
test(4, 4)
test(5, 8)
test(7, 8)
test(8, 8)
test(9, 16)
test(16, 16)
test(17, 32)
test(25, 32)
test(32, 32)
test(33, 64)
test(50, 64)
test(64, 64)
for i := 64; i < (1 << 31); i = i << 1 {
test(int32(i-1), int32(i))
test(int32(i), int32(i))
}
}
func TestPowerSet(t *testing.T) {
test := func(inp int, expect [][]int) {
assertdeep.Equal(t, expect, PowerSet(inp))
}
test(0, [][]int{{}})
test(1, [][]int{{}, {0}})
test(2, [][]int{{}, {0}, {1}, {0, 1}})
test(3, [][]int{{}, {0}, {1}, {0, 1}, {2}, {0, 2}, {1, 2}, {0, 1, 2}})
}
func TestSSliceDedup(t *testing.T) {
require.Equal(t, []string{}, SSliceDedup([]string{}))
require.Equal(t, []string{"foo"}, SSliceDedup([]string{"foo"}))
require.Equal(t, []string{"foo"}, SSliceDedup([]string{"foo", "foo"}))
require.Equal(t, []string{"foo", "bar"}, SSliceDedup([]string{"foo", "bar"}))
require.Equal(t, []string{"foo", "bar"}, SSliceDedup([]string{"foo", "foo", "bar"}))
require.Equal(t, []string{"foo", "bar"}, SSliceDedup([]string{"foo", "bar", "bar"}))
require.Equal(t, []string{"foo", "bar"}, SSliceDedup([]string{"foo", "foo", "bar", "bar"}))
require.Equal(t, []string{"foo", "baz", "bar"}, SSliceDedup([]string{"foo", "foo", "baz", "bar", "bar"}))
require.Equal(t, []string{"foo", "baz", "bar"}, SSliceDedup([]string{"foo", "foo", "baz", "bar", "bar", "baz"}))
require.Equal(t, []string{"foo", "bar", "baz"}, SSliceDedup([]string{"foo", "foo", "bar", "baz", "bar", "baz"}))
}
func TestCopyFile(t *testing.T) {
tmp, err := os.MkdirTemp("", "")
require.NoError(t, err)
defer func() {
require.NoError(t, os.RemoveAll(tmp))
}()
// Helper for writing a file, copying it, and checking the result.
fileNum := 0
testCopy := func(mode os.FileMode, contents []byte) {
// Write the source file.
src := filepath.Join(tmp, fmt.Sprintf("src-%d", fileNum))
dst := filepath.Join(tmp, fmt.Sprintf("dst-%d", fileNum))
fileNum++
require.NoError(t, os.WriteFile(src, contents, mode))
// Set the mode again to work around umask.
require.NoError(t, os.Chmod(src, mode))
srcStat, err := os.Stat(src)
require.NoError(t, err)
// Self-check; ensure that we actually got the mode we wanted for the
// source file.
require.Equal(t, mode, srcStat.Mode())
// Copy the file.
require.NoError(t, CopyFile(src, dst))
// Check the mode and contents of the resulting file.
dstStat, err := os.Stat(dst)
require.NoError(t, err)
require.Equal(t, srcStat.Mode(), dstStat.Mode())
resultContents, err := os.ReadFile(dst)
require.NoError(t, err)
require.Equal(t, contents, resultContents)
}
testCopy(0644, []byte("hello world"))
testCopy(0755, []byte("run this"))
testCopy(0600, []byte("private stuff here"))
testCopy(0777, []byte("this is for everyone!"))
}
func TestFirstNonEmpty(t *testing.T) {
assert.Equal(t, "", FirstNonEmpty())
assert.Equal(t, "", FirstNonEmpty(""))
assert.Equal(t, "a", FirstNonEmpty("a", "b"))
assert.Equal(t, "c", FirstNonEmpty("", "", "c"))
}
func TestSplitLines_StripsTrailingNewline(t *testing.T) {
assert.Equal(t, []string{"this", "that"}, SplitLines("this\nthat\n"))
}
func TestWordWrap(t *testing.T) {
check := func(inp, expect string) {
require.Equal(t, expect, WordWrap(inp, 20))
}
check(`blah blah blah`, `blah blah blah`)
check(`blah blah blah blah blah`, `blah blah blah blah
blah`)
check(`blahblahblahblahblahblah`, `blahblahblahblahblahblah`)
check(`blah blah
threeshortwords thatshouldsplit toseparatelines
thisisareallylongwordthatshouldnotbebrokenupontothreelines
blahblahblah blah blah`,
`blah blah
threeshortwords
thatshouldsplit
toseparatelines
thisisareallylongwordthatshouldnotbebrokenupontothreelines
blahblahblah blah
blah`)
check(`Ünicðdéchäracterssho uldbehandledcorrectly`,
`Ünicðdéchäracterssho
uldbehandledcorrectly`)
check(`Consume spaces appropriately to avoid weird breaks`,
`Consume spaces
appropriately to
avoid weird
breaks`)
check(` longwordafterspaces`, ` longwordafterspaces`)
check(`here are some words that split onto multiple lines.`,
`here are some words
that split onto
multiple lines.`)
}