123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685 |
- package blob
- import (
- "crypto/sha256"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "slices"
- "strings"
- "testing"
- "time"
- "github.com/ollama/ollama/server/internal/internal/testutil"
- )
- func init() {
- debug = true
- }
- var epoch = func() time.Time {
- d := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
- if d.IsZero() {
- panic("time zero")
- }
- return d
- }()
- func TestOpenErrors(t *testing.T) {
- exe, err := os.Executable()
- if err != nil {
- panic(err)
- }
- cases := []struct {
- dir string
- err string
- }{
- {t.TempDir(), ""},
- {"", "empty directory name"},
- {exe, "not a directory"},
- }
- for _, tt := range cases {
- t.Run(tt.dir, func(t *testing.T) {
- _, err := Open(tt.dir)
- if tt.err == "" {
- if err != nil {
- t.Fatal(err)
- }
- return
- }
- if err == nil {
- t.Fatal("expected error")
- }
- if !strings.Contains(err.Error(), tt.err) {
- t.Fatalf("err = %v, want %q", err, tt.err)
- }
- })
- }
- }
- func TestGetFile(t *testing.T) {
- t.Chdir(t.TempDir())
- c, err := Open(".")
- if err != nil {
- t.Fatal(err)
- }
- d := mkdigest("1")
- got := c.GetFile(d)
- cleaned := filepath.Clean(got)
- if cleaned != got {
- t.Fatalf("got is unclean: %q", got)
- }
- if !filepath.IsAbs(got) {
- t.Fatal("got is not absolute")
- }
- abs, _ := filepath.Abs(c.dir)
- if !strings.HasPrefix(got, abs) {
- t.Fatalf("got is not local to %q", c.dir)
- }
- }
- func TestBasic(t *testing.T) {
- c, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- now := epoch
- c.now = func() time.Time { return now }
- checkEntry := entryChecker(t, c)
- checkFailed := func(err error) {
- if err == nil {
- t.Helper()
- t.Fatal("expected error")
- }
- }
- _, err = c.Resolve("invalid")
- checkFailed(err)
- _, err = c.Resolve("h/n/m:t")
- checkFailed(err)
- dx := mkdigest("x")
- d, err := c.Resolve(fmt.Sprintf("h/n/m:t@%s", dx))
- if err != nil {
- t.Fatal(err)
- }
- if d != dx {
- t.Fatalf("d = %v, want %v", d, dx)
- }
- _, err = c.Get(Digest{})
- checkFailed(err)
- // not committed yet
- _, err = c.Get(dx)
- checkFailed(err)
- err = PutBytes(c, dx, "!")
- checkFailed(err)
- err = PutBytes(c, dx, "x")
- if err != nil {
- t.Fatal(err)
- }
- checkEntry(dx, 1, now)
- t0 := now
- now = now.Add(1*time.Hour + 1*time.Minute)
- err = PutBytes(c, dx, "x")
- if err != nil {
- t.Fatal(err)
- }
- // check not updated
- checkEntry(dx, 1, t0)
- }
- type sleepFunc func(d time.Duration) time.Time
- func openTester(t *testing.T) (*DiskCache, sleepFunc) {
- t.Helper()
- c, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- now := epoch
- c.now = func() time.Time { return now }
- return c, func(d time.Duration) time.Time {
- now = now.Add(d)
- return now
- }
- }
- func TestManifestPath(t *testing.T) {
- check := testutil.Checker(t)
- c, sleep := openTester(t)
- d1 := mkdigest("1")
- err := PutBytes(c, d1, "1")
- check(err)
- err = c.Link("h/n/m:t", d1)
- check(err)
- t0 := sleep(0)
- sleep(1 * time.Hour)
- err = c.Link("h/n/m:t", d1) // nop expected
- check(err)
- file := must(c.manifestPath("h/n/m:t"))
- info, err := os.Stat(file)
- check(err)
- testutil.CheckTime(t, info.ModTime(), t0)
- }
- func TestManifestExistsWithoutBlob(t *testing.T) {
- t.Chdir(t.TempDir())
- check := testutil.Checker(t)
- c, err := Open(".")
- check(err)
- checkEntry := entryChecker(t, c)
- man := must(c.manifestPath("h/n/m:t"))
- os.MkdirAll(filepath.Dir(man), 0o777)
- testutil.WriteFile(t, man, "1")
- got, err := c.Resolve("h/n/m:t")
- check(err)
- want := mkdigest("1")
- if got != want {
- t.Fatalf("got = %v, want %v", got, want)
- }
- e, err := c.Get(got)
- check(err)
- checkEntry(got, 1, e.Time)
- }
- func TestPut(t *testing.T) {
- c, sleep := openTester(t)
- check := testutil.Checker(t)
- checkEntry := entryChecker(t, c)
- d := mkdigest("hello, world")
- err := PutBytes(c, d, "hello")
- if err == nil {
- t.Fatal("expected error")
- }
- got, err := c.Get(d)
- if !errors.Is(err, fs.ErrNotExist) {
- t.Fatalf("expected error, got %v", got)
- }
- // Put a valid blob
- err = PutBytes(c, d, "hello, world")
- check(err)
- checkEntry(d, 12, sleep(0))
- // Put a blob with content that does not hash to the digest
- err = PutBytes(c, d, "hello")
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- // Put the valid blob back and check it
- err = PutBytes(c, d, "hello, world")
- check(err)
- checkEntry(d, 12, sleep(0))
- // Put a blob that errors during Read
- err = c.Put(d, &errOnBangReader{s: "!"}, 1)
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- // Put valid blob back and check it
- err = PutBytes(c, d, "hello, world")
- check(err)
- checkEntry(d, 12, sleep(0))
- // Put a blob with mismatched size
- err = c.Put(d, strings.NewReader("hello, world"), 11)
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- // Final byte does not match the digest (testing commit phase)
- err = PutBytes(c, d, "hello, world$")
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- reset := c.setTestHookBeforeFinalWrite(func(f *os.File) {
- // change mode to read-only
- f.Truncate(0)
- f.Chmod(0o400)
- f.Close()
- f1, err := os.OpenFile(f.Name(), os.O_RDONLY, 0)
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { f1.Close() })
- *f = *f1
- })
- defer reset()
- err = PutBytes(c, d, "hello, world")
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- reset()
- }
- func TestImport(t *testing.T) {
- c, _ := openTester(t)
- checkEntry := entryChecker(t, c)
- want := mkdigest("x")
- got, err := c.Import(strings.NewReader("x"), 1)
- if err != nil {
- t.Fatal(err)
- }
- if want != got {
- t.Fatalf("digest = %v, want %v", got, want)
- }
- checkEntry(want, 1, epoch)
- got, err = c.Import(strings.NewReader("x"), 1)
- if err != nil {
- t.Fatal(err)
- }
- if want != got {
- t.Fatalf("digest = %v, want %v", got, want)
- }
- checkEntry(want, 1, epoch)
- }
- func (c *DiskCache) setTestHookBeforeFinalWrite(h func(*os.File)) (reset func()) {
- old := c.testHookBeforeFinalWrite
- c.testHookBeforeFinalWrite = h
- return func() { c.testHookBeforeFinalWrite = old }
- }
- func TestPutGetZero(t *testing.T) {
- c, sleep := openTester(t)
- check := testutil.Checker(t)
- checkEntry := entryChecker(t, c)
- d := mkdigest("x")
- err := PutBytes(c, d, "x")
- check(err)
- checkEntry(d, 1, sleep(0))
- err = os.Truncate(c.GetFile(d), 0)
- check(err)
- _, err = c.Get(d)
- if !errors.Is(err, fs.ErrNotExist) {
- t.Fatalf("err = %v, want fs.ErrNotExist", err)
- }
- }
- func TestPutZero(t *testing.T) {
- c, _ := openTester(t)
- d := mkdigest("x")
- err := c.Put(d, strings.NewReader("x"), 0) // size == 0 (not size of content)
- testutil.Check(t, err)
- checkNotExists(t, c, d)
- }
- func TestCommit(t *testing.T) {
- check := testutil.Checker(t)
- c, err := Open(t.TempDir())
- if err != nil {
- t.Fatal(err)
- }
- checkEntry := entryChecker(t, c)
- now := epoch
- c.now = func() time.Time { return now }
- d1 := mkdigest("1")
- err = c.Link("h/n/m:t", d1)
- if !errors.Is(err, fs.ErrNotExist) {
- t.Fatalf("err = %v, want fs.ErrNotExist", err)
- }
- err = PutBytes(c, d1, "1")
- check(err)
- err = c.Link("h/n/m:t", d1)
- check(err)
- got, err := c.Resolve("h/n/m:t")
- check(err)
- if got != d1 {
- t.Fatalf("d = %v, want %v", got, d1)
- }
- // commit again, more than 1 byte
- d2 := mkdigest("22")
- err = PutBytes(c, d2, "22")
- check(err)
- err = c.Link("h/n/m:t", d2)
- check(err)
- checkEntry(d2, 2, now)
- filename := must(c.manifestPath("h/n/m:t"))
- data, err := os.ReadFile(filename)
- check(err)
- if string(data) != "22" {
- t.Fatalf("data = %q, want %q", data, "22")
- }
- t0 := now
- now = now.Add(1 * time.Hour)
- err = c.Link("h/n/m:t", d2) // same contents; nop
- check(err)
- info, err := os.Stat(filename)
- check(err)
- testutil.CheckTime(t, info.ModTime(), t0)
- }
- func TestManifestInvalidBlob(t *testing.T) {
- c, _ := openTester(t)
- d := mkdigest("1")
- err := c.Link("h/n/m:t", d)
- if err == nil {
- t.Fatal("expected error")
- }
- checkNotExists(t, c, d)
- err = PutBytes(c, d, "1")
- testutil.Check(t, err)
- err = os.WriteFile(c.GetFile(d), []byte("invalid"), 0o666)
- if err != nil {
- t.Fatal(err)
- }
- err = c.Link("h/n/m:t", d)
- if !strings.Contains(err.Error(), "underfoot") {
- t.Fatalf("err = %v, want error to contain %q", err, "underfoot")
- }
- }
- func TestManifestNameReuse(t *testing.T) {
- t.Run("case-insensitive", func(t *testing.T) {
- // This should run on all file system types.
- testManifestNameReuse(t)
- })
- t.Run("case-sensitive", func(t *testing.T) {
- useCaseInsensitiveTempDir(t)
- testManifestNameReuse(t)
- })
- }
- func testManifestNameReuse(t *testing.T) {
- check := testutil.Checker(t)
- c, _ := openTester(t)
- d1 := mkdigest("1")
- err := PutBytes(c, d1, "1")
- check(err)
- err = c.Link("h/n/m:t", d1)
- check(err)
- d2 := mkdigest("22")
- err = PutBytes(c, d2, "22")
- check(err)
- err = c.Link("H/N/M:T", d2)
- check(err)
- var g [2]Digest
- g[0], err = c.Resolve("h/n/m:t")
- check(err)
- g[1], err = c.Resolve("H/N/M:T")
- check(err)
- w := [2]Digest{d2, d2}
- if g != w {
- t.Fatalf("g = %v, want %v", g, w)
- }
- var got []string
- for l, err := range c.links() {
- if err != nil {
- t.Fatal(err)
- }
- got = append(got, l)
- }
- want := []string{"manifests/h/n/m/t"}
- if !slices.Equal(got, want) {
- t.Fatalf("got = %v, want %v", got, want)
- }
- // relink with different case
- err = c.Unlink("h/n/m:t")
- check(err)
- err = c.Link("h/n/m:T", d1)
- check(err)
- got = got[:0]
- for l, err := range c.links() {
- if err != nil {
- t.Fatal(err)
- }
- got = append(got, l)
- }
- // we should have only one link that is same case as the last link
- want = []string{"manifests/h/n/m/T"}
- if !slices.Equal(got, want) {
- t.Fatalf("got = %v, want %v", got, want)
- }
- }
- func TestManifestFile(t *testing.T) {
- cases := []struct {
- in string
- want string
- }{
- {"", ""},
- // valid names
- {"h/n/m:t", "/manifests/h/n/m/t"},
- {"hh/nn/mm:tt", "/manifests/hh/nn/mm/tt"},
- {"%/%/%/%", ""},
- // already a path
- {"h/n/m/t", ""},
- // refs are not names
- {"h/n/m:t@sha256-1", ""},
- {"m@sha256-1", ""},
- {"n/m:t@sha256-1", ""},
- }
- c, _ := openTester(t)
- for _, tt := range cases {
- t.Run(tt.in, func(t *testing.T) {
- got, err := c.manifestPath(tt.in)
- if err != nil && tt.want != "" {
- t.Fatalf("unexpected error: %v", err)
- }
- if err == nil && tt.want == "" {
- t.Fatalf("expected error")
- }
- dir := filepath.ToSlash(c.dir)
- got = filepath.ToSlash(got)
- got = strings.TrimPrefix(got, dir)
- if got != tt.want {
- t.Fatalf("got = %q, want %q", got, tt.want)
- }
- })
- }
- }
- func TestNames(t *testing.T) {
- c, _ := openTester(t)
- check := testutil.Checker(t)
- check(PutBytes(c, mkdigest("1"), "1"))
- check(PutBytes(c, mkdigest("2"), "2"))
- check(c.Link("h/n/m:t", mkdigest("1")))
- check(c.Link("h/n/m:u", mkdigest("2")))
- var got []string
- for l, err := range c.Links() {
- if err != nil {
- t.Fatal(err)
- }
- got = append(got, l)
- }
- want := []string{"h/n/m:t", "h/n/m:u"}
- if !slices.Equal(got, want) {
- t.Fatalf("got = %v, want %v", got, want)
- }
- }
- func mkdigest(s string) Digest {
- return Digest{sha256.Sum256([]byte(s))}
- }
- func checkNotExists(t *testing.T, c *DiskCache, d Digest) {
- t.Helper()
- _, err := c.Get(d)
- if !errors.Is(err, fs.ErrNotExist) {
- t.Fatalf("err = %v, want fs.ErrNotExist", err)
- }
- }
- func entryChecker(t *testing.T, c *DiskCache) func(Digest, int64, time.Time) {
- t.Helper()
- return func(d Digest, size int64, mod time.Time) {
- t.Helper()
- t.Run("checkEntry:"+d.String(), func(t *testing.T) {
- t.Helper()
- defer func() {
- if t.Failed() {
- dumpCacheContents(t, c)
- }
- }()
- e, err := c.Get(d)
- if size == 0 && errors.Is(err, fs.ErrNotExist) {
- err = nil
- }
- if err != nil {
- t.Fatal(err)
- }
- if e.Digest != d {
- t.Errorf("e.Digest = %v, want %v", e.Digest, d)
- }
- if e.Size != size {
- t.Fatalf("e.Size = %v, want %v", e.Size, size)
- }
- testutil.CheckTime(t, e.Time, mod)
- info, err := os.Stat(c.GetFile(d))
- if err != nil {
- t.Fatal(err)
- }
- if info.Size() != size {
- t.Fatalf("info.Size = %v, want %v", info.Size(), size)
- }
- testutil.CheckTime(t, info.ModTime(), mod)
- })
- }
- }
- func must[T any](v T, err error) T {
- if err != nil {
- panic(err)
- }
- return v
- }
- func TestNameToPath(t *testing.T) {
- _, err := nameToPath("h/n/m:t")
- if err != nil {
- t.Fatal(err)
- }
- }
- type errOnBangReader struct {
- s string
- n int
- }
- func (e *errOnBangReader) Read(p []byte) (int, error) {
- if len(p) < 1 {
- return 0, io.ErrShortBuffer
- }
- if e.n >= len(p) {
- return 0, io.EOF
- }
- if e.s[e.n] == '!' {
- return 0, errors.New("bang")
- }
- p[0] = e.s[e.n]
- e.n++
- return 1, nil
- }
- func dumpCacheContents(t *testing.T, c *DiskCache) {
- t.Helper()
- var b strings.Builder
- fsys := os.DirFS(c.dir)
- fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
- t.Helper()
- if err != nil {
- return err
- }
- info, err := d.Info()
- if err != nil {
- return err
- }
- // Format like ls:
- //
- // ; ls -la
- // drwxr-xr-x 224 Jan 13 14:22 blob/sha256-123
- // drwxr-xr-x 224 Jan 13 14:22 manifest/h/n/m
- fmt.Fprintf(&b, " %s % 4d %s %s\n",
- info.Mode(),
- info.Size(),
- info.ModTime().Format("Jan 2 15:04"),
- path,
- )
- return nil
- })
- t.Log()
- t.Logf("cache contents:\n%s", b.String())
- }
|