| // 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" |
| "strings" |
| "testing" |
| |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/tools/gopls/internal/filecache" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| 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 { |
| if strings.Contains(err.Error(), "operation not supported") || |
| strings.Contains(err.Error(), "not implemented") { |
| t.Skipf("skipping: %v", err) |
| } |
| 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) { |
| if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" { |
| t.Skip(`skipping on plan9-arm builder due to golang/go#58748: failing with 'mount rpc error'`) |
| } |
| 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 { |
| if strings.Contains(err.Error(), "operation not supported") || |
| strings.Contains(err.Error(), "not implemented") { |
| t.Skipf("skipping: %v", err) |
| } |
| 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) { |
| testenv.NeedsExec(t) |
| |
| keyA := uniqueKey() |
| keyB := uniqueKey() |
| value := []byte(testIPCValueA) |
| |
| // Set keyA. |
| if err := filecache.Set(testIPCKind, keyA, value); err != nil { |
| if strings.Contains(err.Error(), "operation not supported") { |
| t.Skipf("skipping: %v", err) |
| } |
| 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() |
| b.SetBytes(int64(len(value))) |
| |
| 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) |
| } |
| } |
| |
| // These two benchmarks are asymmetric: the one for Get imposes a |
| // modest bound on concurrency (50) whereas the one for Set imposes a |
| // much higher concurrency (1000) to test the implementation's |
| // self-imposed bound. |
| |
| func BenchmarkUncontendedSet(b *testing.B) { |
| const kind = "BenchmarkUncontendedSet" |
| key := uniqueKey() |
| var value [8192]byte |
| |
| const P = 1000 // parallelism |
| b.SetBytes(P * int64(len(value))) |
| |
| for i := 0; i < b.N; i++ { |
| // Perform P concurrent calls to Set. All must succeed. |
| var group errgroup.Group |
| for range [P]bool{} { |
| group.Go(func() error { |
| return filecache.Set(kind, key, value[:]) |
| }) |
| } |
| if err := group.Wait(); err != nil { |
| if strings.Contains(err.Error(), "operation not supported") || |
| strings.Contains(err.Error(), "not implemented") { |
| b.Skipf("skipping: %v", err) |
| } |
| b.Fatal(err) |
| } |
| } |
| } |