| // 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, filecache.Bytes); 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, filecache.Bytes); 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, filecache.Bytes); 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, filecache.Bytes) | |
| 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 { | |
| 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, filecache.Bytes) | |
| 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"), filecache.Bytes) | |
| 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.SetBytes(int64(len(value))) | |
| var group errgroup.Group | |
| group.SetLimit(5) | |
| for b.Loop() { | |
| group.Go(func() error { | |
| _, err := filecache.Get(kind, key, filecache.Bytes) | |
| 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 (5) 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 b.Loop() { | |
| // 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) | |
| } | |
| } | |
| } |