go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/caching/cache/cache_test.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cache
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"crypto"
    21  	"io"
    22  	"math"
    23  	"os"
    24  	"path/filepath"
    25  	"testing"
    26  
    27  	"go.chromium.org/luci/common/system/filesystem"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  )
    31  
    32  func testCache(t *testing.T, c *Cache) HexDigests {
    33  	var expected HexDigests
    34  	Convey(`Common tests performed on a cache of objects.`, func() {
    35  		// c's policies must have MaxItems == 2 and MaxSize == 1024.
    36  		td := t.TempDir()
    37  		ctx := context.Background()
    38  
    39  		h := crypto.SHA1
    40  		fakeDigest := HexDigest("0123456789012345678901234567890123456789")
    41  		badDigest := HexDigest("012345678901234567890123456789012345678")
    42  		emptyContent := []byte{}
    43  		emptyDigest := HashBytes(h, emptyContent)
    44  		file1Content := []byte("foo")
    45  		file1Digest := HashBytes(h, file1Content)
    46  		file2Content := []byte("foo bar")
    47  		file2Digest := HashBytes(h, file2Content)
    48  		hardlinkContent := []byte("hardlink")
    49  		hardlinkDigest := HashBytes(h, hardlinkContent)
    50  		largeContent := bytes.Repeat([]byte("A"), 1023)
    51  		largeDigest := HashBytes(h, largeContent)
    52  		tooLargeContent := bytes.Repeat([]byte("A"), 1025)
    53  		tooLargeDigest := HashBytes(h, tooLargeContent)
    54  
    55  		So(c.Keys(), ShouldResemble, HexDigests{})
    56  
    57  		So(c.Touch(fakeDigest), ShouldBeFalse)
    58  		So(c.Touch(badDigest), ShouldBeFalse)
    59  
    60  		c.Evict(fakeDigest)
    61  		c.Evict(badDigest)
    62  
    63  		r, err := c.Read(fakeDigest)
    64  		So(r, ShouldBeNil)
    65  		So(err, ShouldNotBeNil)
    66  		r, err = c.Read(badDigest)
    67  		So(r, ShouldBeNil)
    68  		So(err, ShouldNotBeNil)
    69  
    70  		// It's too large to fit in the cache.
    71  		So(c.Add(ctx, tooLargeDigest, bytes.NewBuffer(tooLargeContent)), ShouldNotBeNil)
    72  
    73  		// It gets discarded because it's too large.
    74  		So(c.Add(ctx, largeDigest, bytes.NewBuffer(largeContent)), ShouldBeNil)
    75  		So(c.Add(ctx, emptyDigest, bytes.NewBuffer(emptyContent)), ShouldBeNil)
    76  		So(c.Add(ctx, emptyDigest, bytes.NewBuffer(emptyContent)), ShouldBeNil)
    77  		So(c.Keys(), ShouldResemble, HexDigests{emptyDigest, largeDigest})
    78  		c.Evict(emptyDigest)
    79  		So(c.Keys(), ShouldResemble, HexDigests{largeDigest})
    80  		So(c.Add(ctx, emptyDigest, bytes.NewBuffer(emptyContent)), ShouldBeNil)
    81  
    82  		So(c.Add(ctx, file1Digest, bytes.NewBuffer(file1Content)), ShouldBeNil)
    83  		So(c.Touch(emptyDigest), ShouldBeTrue)
    84  		So(c.Add(ctx, file2Digest, bytes.NewBuffer(file2Content)), ShouldBeNil)
    85  
    86  		r, err = c.Read(file1Digest)
    87  		So(r, ShouldBeNil)
    88  		So(err, ShouldNotBeNil)
    89  		r, err = c.Read(file2Digest)
    90  		So(err, ShouldBeNil)
    91  		actual, err := io.ReadAll(r)
    92  		So(r.Close(), ShouldBeNil)
    93  		So(err, ShouldBeNil)
    94  		So(actual, ShouldResemble, file2Content)
    95  
    96  		expected = HexDigests{file2Digest, emptyDigest}
    97  		So(c.Keys(), ShouldResemble, expected)
    98  
    99  		dest := filepath.Join(td, "foo")
   100  		So(c.Hardlink(fakeDigest, dest, os.FileMode(0600)), ShouldNotBeNil)
   101  		So(c.Hardlink(badDigest, dest, os.FileMode(0600)), ShouldNotBeNil)
   102  		So(c.Hardlink(file2Digest, dest, os.FileMode(0600)), ShouldBeNil)
   103  		// See comment about the fact that it may or may not work.
   104  		_ = c.Hardlink(file2Digest, dest, os.FileMode(0600))
   105  		actual, err = os.ReadFile(dest)
   106  		So(err, ShouldBeNil)
   107  		So(actual, ShouldResemble, file2Content)
   108  
   109  		dest = filepath.Join(td, "hardlink")
   110  		So(c.AddWithHardlink(ctx, hardlinkDigest, bytes.NewBuffer(hardlinkContent), dest, os.ModePerm),
   111  			ShouldBeNil)
   112  		actual, err = os.ReadFile(dest)
   113  		So(err, ShouldBeNil)
   114  		So(actual, ShouldResemble, hardlinkContent)
   115  
   116  		// |emptyDigest| is evicted.
   117  		expected = HexDigests{hardlinkDigest, file2Digest}
   118  
   119  		So(c.Close(), ShouldBeNil)
   120  	})
   121  	return expected
   122  }
   123  
   124  func TestNew(t *testing.T) {
   125  	Convey(`Test the disk-based cache of objects.`, t, func() {
   126  		td := t.TempDir()
   127  
   128  		pol := Policies{MaxSize: 1024, MaxItems: 2}
   129  		h := crypto.SHA1
   130  		c, err := New(pol, td, h)
   131  		So(err, ShouldBeNil)
   132  		expected := testCache(t, c)
   133  
   134  		c, err = New(pol, td, h)
   135  		So(err, ShouldBeNil)
   136  		So(c.Keys(), ShouldResemble, expected)
   137  		So(c.Close(), ShouldBeNil)
   138  
   139  		curdir, err := os.Getwd()
   140  		So(err, ShouldBeNil)
   141  		defer func() {
   142  			So(os.Chdir(curdir), ShouldBeNil)
   143  		}()
   144  
   145  		So(os.Chdir(td), ShouldBeNil)
   146  
   147  		rel, err := filepath.Rel(td, t.TempDir())
   148  		So(err, ShouldBeNil)
   149  		So(filepath.IsAbs(rel), ShouldBeFalse)
   150  		_, err = New(pol, rel, h)
   151  		So(err, ShouldBeNil)
   152  	})
   153  
   154  	Convey(`invalid state.json`, t, func() {
   155  		dir := t.TempDir()
   156  		state := filepath.Join(dir, "state.json")
   157  		invalid := filepath.Join(dir, "invalid file")
   158  		So(os.WriteFile(state, []byte("invalid"), os.ModePerm), ShouldBeNil)
   159  		So(os.WriteFile(invalid, []byte("invalid"), os.ModePerm), ShouldBeNil)
   160  
   161  		c, err := New(Policies{}, dir, crypto.SHA1)
   162  		So(err, ShouldNotBeNil)
   163  		if c == nil {
   164  			t.Errorf("c should not be nil: %v", err)
   165  		}
   166  		So(c, ShouldNotBeNil)
   167  
   168  		So(c.statePath(), ShouldEqual, state)
   169  
   170  		// invalid files should be removed.
   171  		empty, err := filesystem.IsEmptyDir(dir)
   172  		So(err, ShouldBeNil)
   173  		So(empty, ShouldBeTrue)
   174  
   175  		So(c.Close(), ShouldBeNil)
   176  	})
   177  
   178  	Convey(`MinFreeSpace too big`, t, func() {
   179  		ctx := context.Background()
   180  		dir := t.TempDir()
   181  		h := crypto.SHA1
   182  		c, err := New(Policies{MaxSize: 10, MinFreeSpace: math.MaxInt64}, dir, h)
   183  		So(err, ShouldBeNil)
   184  
   185  		file1Content := []byte("foo")
   186  		file1Digest := HashBytes(h, file1Content)
   187  		So(c.Add(ctx, file1Digest, bytes.NewBuffer(file1Content)), ShouldBeNil)
   188  
   189  		So(c.Close(), ShouldBeNil)
   190  	})
   191  
   192  	Convey(`MaxSize 0`, t, func() {
   193  		ctx := context.Background()
   194  		dir := t.TempDir()
   195  		h := crypto.SHA1
   196  		c, err := New(Policies{MaxSize: 0, MaxItems: 1}, dir, h)
   197  		So(err, ShouldBeNil)
   198  
   199  		file1Content := []byte("foo")
   200  		file1Digest := HashBytes(h, file1Content)
   201  		So(c.Add(ctx, file1Digest, bytes.NewBuffer(file1Content)), ShouldBeNil)
   202  		So(c.Keys(), ShouldHaveLength, 1)
   203  		So(c.Close(), ShouldBeNil)
   204  	})
   205  
   206  	Convey(`HardLink will update used`, t, func() {
   207  		dir := t.TempDir()
   208  		h := crypto.SHA1
   209  		onDiskContent := []byte("on disk")
   210  		onDiskDigest := HashBytes(h, onDiskContent)
   211  		notOnDiskContent := []byte("not on disk")
   212  		notOnDiskDigest := HashBytes(h, notOnDiskContent)
   213  
   214  		c, err := New(Policies{}, dir, h)
   215  		defer func() { So(c.Close(), ShouldBeNil) }()
   216  
   217  		So(err, ShouldBeNil)
   218  		So(c, ShouldNotBeNil)
   219  		perm := os.ModePerm
   220  		So(os.WriteFile(c.itemPath(onDiskDigest), onDiskContent, perm), ShouldBeNil)
   221  
   222  		So(c.Used(), ShouldBeEmpty)
   223  		So(c.Hardlink(notOnDiskDigest, filepath.Join(dir, "not_on_disk"), perm), ShouldNotBeNil)
   224  		So(c.Used(), ShouldBeEmpty)
   225  		So(c.Hardlink(onDiskDigest, filepath.Join(dir, "on_disk"), perm), ShouldBeNil)
   226  		So(c.Used(), ShouldHaveLength, 1)
   227  	})
   228  
   229  	Convey(`AddFileWithoutValidation`, t, func() {
   230  		ctx := context.Background()
   231  		dir := t.TempDir()
   232  		cache := filepath.Join(dir, "cache")
   233  		h := crypto.SHA1
   234  		c, err := New(Policies{
   235  			MaxSize:  1,
   236  			MaxItems: 1,
   237  		}, cache, h)
   238  		defer func() { So(c.Close(), ShouldBeNil) }()
   239  		So(err, ShouldBeNil)
   240  
   241  		empty := filepath.Join(dir, "empty")
   242  		So(os.WriteFile(empty, nil, 0600), ShouldBeNil)
   243  
   244  		emptyHash := HashBytes(h, nil)
   245  
   246  		So(c.AddFileWithoutValidation(ctx, emptyHash, empty), ShouldBeNil)
   247  
   248  		So(c.Touch(emptyHash), ShouldBeTrue)
   249  
   250  		// Adding already existing file is fine.
   251  		So(c.AddFileWithoutValidation(ctx, emptyHash, empty), ShouldBeNil)
   252  
   253  		empty2 := filepath.Join(dir, "empty2")
   254  		So(os.WriteFile(empty2, nil, 0600), ShouldBeNil)
   255  		So(c.AddFileWithoutValidation(ctx, emptyHash, empty2), ShouldBeNil)
   256  	})
   257  }