blob: c96cb16eb99a70771ecfee07f63b1efe1aab4eb4 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package filecache_test
// This file defines tests of the API of the filecache package.
//
// Some properties (e.g. garbage collection) cannot be exercised
// through the API, so this test does not attempt to do so.
import (
"bytes"
cryptorand "crypto/rand"
"fmt"
"log"
mathrand "math/rand"
"os"
"os/exec"
"strconv"
"testing"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/gopls/internal/lsp/filecache"
)
func TestBasics(t *testing.T) {
const kind = "TestBasics"
key := uniqueKey() // never used before
value := []byte("hello")
// Get of a never-seen key returns not found.
if _, err := filecache.Get(kind, key); err != filecache.ErrNotFound {
t.Errorf("Get of random key returned err=%q, want not found", err)
}
// Set of a never-seen key and a small value succeeds.
if err := filecache.Set(kind, key, value); err != nil {
t.Errorf("Set failed: %v", err)
}
// Get of the key returns a copy of the value.
if got, err := filecache.Get(kind, key); err != nil {
t.Errorf("Get after Set failed: %v", err)
} else if string(got) != string(value) {
t.Errorf("Get after Set returned different value: got %q, want %q", got, value)
}
// The kind is effectively part of the key.
if _, err := filecache.Get("different-kind", key); err != filecache.ErrNotFound {
t.Errorf("Get with wrong kind returned err=%q, want not found", err)
}
}
// TestConcurrency exercises concurrent access to the same entry.
func TestConcurrency(t *testing.T) {
const kind = "TestConcurrency"
key := uniqueKey()
const N = 100 // concurrency level
// Construct N distinct values, each larger
// than a typical 4KB OS file buffer page.
var values [N][8192]byte
for i := range values {
if _, err := mathrand.Read(values[i][:]); err != nil {
t.Fatalf("rand: %v", err)
}
}
// get calls Get and verifies that the cache entry
// matches one of the values passed to Set.
get := func(mustBeFound bool) error {
got, err := filecache.Get(kind, key)
if err != nil {
if err == filecache.ErrNotFound && !mustBeFound {
return nil // not found
}
return err
}
for _, want := range values {
if bytes.Equal(want[:], got) {
return nil // a match
}
}
return fmt.Errorf("Get returned a value that was never Set")
}
// Perform N concurrent calls to Set and Get.
// All sets must succeed.
// All gets must return nothing, or one of the Set values;
// there is no third possibility.
var group errgroup.Group
for i := range values {
i := i
group.Go(func() error { return filecache.Set(kind, key, values[i][:]) })
group.Go(func() error { return get(false) })
}
if err := group.Wait(); err != nil {
t.Fatal(err)
}
// A final Get must report one of the values that was Set.
if err := get(true); err != nil {
t.Fatalf("final Get failed: %v", err)
}
}
const (
testIPCKind = "TestIPC"
testIPCValueA = "hello"
testIPCValueB = "world"
)
// TestIPC exercises interprocess communication through the cache.
// It calls Set(A) in the parent, { Get(A); Set(B) } in the child
// process, then Get(B) in the parent.
func TestIPC(t *testing.T) {
keyA := uniqueKey()
keyB := uniqueKey()
value := []byte(testIPCValueA)
// Set keyA.
if err := filecache.Set(testIPCKind, keyA, value); err != nil {
t.Fatalf("Set: %v", err)
}
// Call ipcChild in a child process,
// passing it the keys in the environment
// (quoted, to avoid NUL termination of C strings).
// It will Get(A) then Set(B).
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Env = append(os.Environ(),
"ENTRYPOINT=ipcChild",
fmt.Sprintf("KEYA=%q", keyA),
fmt.Sprintf("KEYB=%q", keyB))
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
// Verify keyB.
got, err := filecache.Get(testIPCKind, keyB)
if err != nil {
t.Fatal(err)
}
if string(got) != "world" {
t.Fatalf("Get(keyB) = %q, want %q", got, "world")
}
}
// We define our own main function so that portions of
// some tests can run in a separate (child) process.
func TestMain(m *testing.M) {
switch os.Getenv("ENTRYPOINT") {
case "ipcChild":
ipcChild()
default:
os.Exit(m.Run())
}
}
// ipcChild is the portion of TestIPC that runs in a child process.
func ipcChild() {
getenv := func(name string) (key [32]byte) {
s, _ := strconv.Unquote(os.Getenv(name))
copy(key[:], []byte(s))
return
}
// Verify key A.
got, err := filecache.Get(testIPCKind, getenv("KEYA"))
if err != nil || string(got) != testIPCValueA {
log.Fatalf("child: Get(key) = %q, %v; want %q", got, err, testIPCValueA)
}
// Set key B.
if err := filecache.Set(testIPCKind, getenv("KEYB"), []byte(testIPCValueB)); err != nil {
log.Fatalf("child: Set(keyB) failed: %v", err)
}
}
// uniqueKey returns a key that has never been used before.
func uniqueKey() (key [32]byte) {
if _, err := cryptorand.Read(key[:]); err != nil {
log.Fatalf("rand: %v", err)
}
return
}
func BenchmarkUncontendedGet(b *testing.B) {
const kind = "BenchmarkUncontendedGet"
key := uniqueKey()
var value [8192]byte
if _, err := mathrand.Read(value[:]); err != nil {
b.Fatalf("rand: %v", err)
}
if err := filecache.Set(kind, key, value[:]); err != nil {
b.Fatal(err)
}
b.ResetTimer()
var group errgroup.Group
group.SetLimit(50)
for i := 0; i < b.N; i++ {
group.Go(func() error {
_, err := filecache.Get(kind, key)
return err
})
}
if err := group.Wait(); err != nil {
b.Fatal(err)
}
}