package pkg_test import ( "archive/tar" "bytes" "crypto/sha512" "encoding/binary" "errors" "fmt" "io/fs" "net/http" "os" "path/filepath" "reflect" "syscall" "testing" "unsafe" "hakurei.app/container" "hakurei.app/container/check" "hakurei.app/container/stub" "hakurei.app/internal/pkg" ) // overrideIdent overrides the ID method of [Artifact]. type overrideIdent struct { id pkg.ID pkg.Artifact } func (a overrideIdent) ID() pkg.ID { return a.id } // overrideIdentFile overrides the ID method of [File]. type overrideIdentFile struct { id pkg.ID pkg.File } func (a overrideIdentFile) ID() pkg.ID { return a.id } // A knownIdentArtifact implements [pkg.KnownIdent] and [Artifact] type knownIdentArtifact interface { pkg.KnownIdent pkg.Artifact } // A knownIdentFile implements [pkg.KnownIdent] and [File] type knownIdentFile interface { pkg.KnownIdent pkg.File } // overrideChecksum overrides the Checksum method of [Artifact]. type overrideChecksum struct { checksum pkg.Checksum knownIdentArtifact } func (a overrideChecksum) Checksum() pkg.Checksum { return a.checksum } // overrideChecksumFile overrides the Checksum method of [File]. type overrideChecksumFile struct { checksum pkg.Checksum knownIdentFile } func (a overrideChecksumFile) Checksum() pkg.Checksum { return a.checksum } // A stubArtifact implements [Artifact] with hardcoded behaviour. type stubArtifact struct { kind pkg.Kind params []byte deps []pkg.Artifact cure func(work, temp *check.Absolute, loadData pkg.CacheDataFunc) error } func (a stubArtifact) Kind() pkg.Kind { return a.kind } func (a stubArtifact) Params() []byte { return a.params } func (a stubArtifact) Dependencies() []pkg.Artifact { return a.deps } func (a stubArtifact) Cure( work, temp *check.Absolute, loadData pkg.CacheDataFunc, ) error { return a.cure(work, temp, loadData) } // A stubFile implements [File] with hardcoded behaviour. type stubFile struct { data []byte err error stubArtifact } func (a stubFile) Data() ([]byte, error) { return a.data, a.err } // newStubFile returns an implementation of [pkg.File] with hardcoded behaviour. func newStubFile( kind pkg.Kind, id pkg.ID, sum *pkg.Checksum, data []byte, err error, ) pkg.File { f := overrideIdentFile{id, stubFile{data, err, stubArtifact{ kind, nil, nil, func(*check.Absolute, *check.Absolute, pkg.CacheDataFunc) error { panic("unreachable") }, }}} if sum == nil { return f } else { return overrideChecksumFile{*sum, f} } } func TestIdent(t *testing.T) { t.Parallel() testCases := []struct { name string a pkg.Artifact want pkg.ID }{ {"tar", stubArtifact{ pkg.KindTar, []byte{pkg.TarGzip, 0, 0, 0, 0, 0, 0, 0}, []pkg.Artifact{ overrideIdent{pkg.ID{}, stubArtifact{}}, }, nil, }, pkg.MustDecode( "HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY", )}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := pkg.Ident(tc.a); got != tc.want { t.Errorf("Ident: %s, want %s", pkg.Encode(got), pkg.Encode(tc.want), ) } }) } } // cacheTestCase is a test case passed to checkWithCache where a new instance // of [pkg.Cache] is prepared for the test case, and is validated and removed // on test completion. type cacheTestCase struct { name string early func(t *testing.T, base *check.Absolute) f func(t *testing.T, base *check.Absolute, c *pkg.Cache) want pkg.Checksum } // checkWithCache runs a slice of cacheTestCase. func checkWithCache(t *testing.T, testCases []cacheTestCase) { t.Helper() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Helper() t.Parallel() base := check.MustAbs(t.TempDir()) if err := os.Chmod(base.String(), 0700); err != nil { t.Fatal(err) } t.Cleanup(func() { if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error { if err != nil { t.Error(err) return nil } if !d.IsDir() { return nil } return os.Chmod(path, 0700) }); err != nil { t.Fatal(err) } }) if c, err := pkg.New(base); err != nil { t.Fatalf("New: error = %v", err) } else { if tc.early != nil { tc.early(t, base) } tc.f(t, base, c) } if checksum, err := pkg.HashDir(base); err != nil { t.Fatalf("HashDir: error = %v", err) } else if checksum != tc.want { t.Fatalf("HashDir: %v", &pkg.ChecksumMismatchError{ Got: checksum, Want: tc.want, }) } }) } } // A cureStep contains an [Artifact] to be cured, and the expected outcome. type cureStep struct { name string a pkg.Artifact pathname *check.Absolute checksum pkg.Checksum err error } // cureMany cures many artifacts against a [Cache] and checks their outcomes. func cureMany(t *testing.T, c *pkg.Cache, steps []cureStep) { for _, step := range steps { t.Log("cure step:", step.name) if pathname, checksum, err := c.Cure(step.a); !reflect.DeepEqual(err, step.err) { t.Fatalf("Cure: error = %v, want %v", err, step.err) } else if !pathname.Is(step.pathname) { t.Fatalf("Cure: pathname = %q, want %q", pathname, step.pathname) } else if checksum != step.checksum { t.Fatalf("Cure: checksum = %s, want %s", pkg.Encode(checksum), pkg.Encode(step.checksum)) } else { v := any(err) if err == nil { v = pathname } t.Log(pkg.Encode(checksum)+":", v) } } } func TestCache(t *testing.T) { t.Parallel() const testdata = "" + "\x00\x00\x00\x00" + "\xad\x0b\x00" + "\x04" + "\xfe\xfe\x00\x00" + "\xfe\xca\x00\x00" testdataChecksum := func() pkg.Checksum { h := sha512.New384() h.Write([]byte(testdata)) return (pkg.Checksum)(h.Sum(nil)) }() testCases := []cacheTestCase{ {"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { c.SetStrict(true) identifier := (pkg.ID)(bytes.Repeat([]byte{ 0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f, }, 8)) wantPathname := base.Append( "identifier", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", ) identifier0 := (pkg.ID)(bytes.Repeat([]byte{ 0x71, 0xa7, 0xde, 0x6d, 0xa6, 0xde, }, 8)) wantPathname0 := base.Append( "identifier", "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe", ) cureMany(t, c, []cureStep{ {"initial file", newStubFile( pkg.KindHTTPGet, identifier, &testdataChecksum, []byte(testdata), nil, ), wantPathname, testdataChecksum, nil}, {"identical content", newStubFile( pkg.KindHTTPGet, identifier0, &testdataChecksum, []byte(testdata), nil, ), wantPathname0, testdataChecksum, nil}, {"existing entry", newStubFile( pkg.KindHTTPGet, identifier, &testdataChecksum, []byte(testdata), nil, ), wantPathname, testdataChecksum, nil}, {"checksum mismatch", newStubFile( pkg.KindHTTPGet, pkg.ID{0xff, 0}, new(pkg.Checksum), []byte(testdata), nil, ), nil, pkg.Checksum{}, &pkg.ChecksumMismatchError{ Got: testdataChecksum, }}, {"store without validation", newStubFile( pkg.KindHTTPGet, pkg.MustDecode("vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX"), nil, []byte{0}, nil, ), base.Append( "identifier", "vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX", ), pkg.Checksum{ 0xbe, 0xc0, 0x21, 0xb4, 0xf3, 0x68, 0xe3, 0x06, 0x91, 0x34, 0xe0, 0x12, 0xc2, 0xb4, 0x30, 0x70, 0x83, 0xd3, 0xa9, 0xbd, 0xd2, 0x06, 0xe2, 0x4e, 0x5f, 0x0d, 0x86, 0xe1, 0x3d, 0x66, 0x36, 0x65, 0x59, 0x33, 0xec, 0x2b, 0x41, 0x34, 0x65, 0x96, 0x68, 0x17, 0xa9, 0xc2, 0x08, 0xa1, 0x17, 0x17, }, nil}, {"error passthrough", newStubFile( pkg.KindHTTPGet, pkg.ID{0xff, 1}, nil, nil, stub.UniqueError(0xcafe), ), nil, pkg.Checksum{}, stub.UniqueError(0xcafe)}, {"error caching", newStubFile( pkg.KindHTTPGet, pkg.ID{0xff, 1}, nil, nil, nil, ), nil, pkg.Checksum{}, stub.UniqueError(0xcafe)}, {"cache hit bad type", overrideChecksum{testdataChecksum, overrideIdent{pkg.ID{0xff, 2}, stubArtifact{ kind: pkg.KindTar, }}}, nil, pkg.Checksum{}, pkg.InvalidFileModeError( 0400, )}, }) if c0, err := pkg.New(base); err != nil { t.Fatalf("New: error = %v", err) } else { cureMany(t, c0, []cureStep{ {"cache hit ident", overrideIdent{ id: identifier, }, wantPathname, testdataChecksum, nil}, {"cache miss checksum match", newStubFile( pkg.KindHTTPGet, testdataChecksum, nil, []byte(testdata), nil, ), base.Append( "identifier", pkg.Encode(testdataChecksum), ), testdataChecksum, nil}, }) } }, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2")}, {"directory", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { id := pkg.KindTar.Ident( binary.LittleEndian.AppendUint64(nil, pkg.TarGzip), overrideIdent{testdataChecksum, stubArtifact{}}, ) makeSample := func(work, _ *check.Absolute, _ pkg.CacheDataFunc) error { if err := os.Mkdir(work.String(), 0700); err != nil { return err } if err := os.WriteFile( work.Append("check").String(), []byte{0, 0}, 0400, ); err != nil { return err } if err := os.MkdirAll(work.Append( "lib", "pkgconfig", ).String(), 0700); err != nil { return err } return os.Symlink( "/proc/nonexistent/libedac.so", work.Append( "lib", "libedac.so", ).String(), ) } wantChecksum := pkg.MustDecode( "qRN6in76LndiiOZJheHkwyW8UT1N5-f-bXvHfDvwrMw2fSkOoZdh8pWE1qhLk65b", ) wantPathname := base.Append( "identifier", pkg.Encode(id), ) id0 := pkg.KindTar.Ident( binary.LittleEndian.AppendUint64(nil, pkg.TarGzip), overrideIdent{pkg.ID{}, stubArtifact{}}, ) wantPathname0 := base.Append( "identifier", pkg.Encode(id0), ) makeGarbage := func(work *check.Absolute, wantErr error) error { if err := os.Mkdir(work.String(), 0700); err != nil { return err } mode := fs.FileMode(0) if wantErr == nil { mode = 0500 } if err := os.MkdirAll(work.Append( "lib", "pkgconfig", ).String(), 0700); err != nil { return err } if err := os.WriteFile(work.Append( "lib", "check", ).String(), nil, 0400&mode); err != nil { return err } if err := os.Chmod(work.Append( "lib", "pkgconfig", ).String(), 0500&mode); err != nil { return err } if err := os.Chmod(work.Append( "lib", ).String(), 0500&mode); err != nil { return err } return wantErr } cureMany(t, c, []cureStep{ {"initial directory", overrideChecksum{wantChecksum, overrideIdent{id, stubArtifact{ kind: pkg.KindTar, cure: makeSample, }}}, wantPathname, wantChecksum, nil}, {"identical identifier", overrideChecksum{wantChecksum, overrideIdent{id, stubArtifact{ kind: pkg.KindTar, }}}, wantPathname, wantChecksum, nil}, {"identical checksum", overrideIdent{id0, stubArtifact{ kind: pkg.KindTar, cure: makeSample, }}, wantPathname0, wantChecksum, nil}, {"cure fault", overrideIdent{pkg.ID{0xff, 0}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, _ pkg.CacheDataFunc) error { return makeGarbage(work, stub.UniqueError(0xcafe)) }, }}, nil, pkg.Checksum{}, stub.UniqueError(0xcafe)}, {"checksum mismatch", overrideChecksum{pkg.Checksum{}, overrideIdent{pkg.ID{0xff, 1}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, _ pkg.CacheDataFunc) error { return makeGarbage(work, nil) }, }}}, nil, pkg.Checksum{}, &pkg.ChecksumMismatchError{ Got: pkg.MustDecode( "CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT", ), }}, {"cache hit bad type", newStubFile( pkg.KindHTTPGet, pkg.ID{0xff, 2}, &wantChecksum, []byte(testdata), nil, ), nil, pkg.Checksum{}, pkg.InvalidFileModeError( fs.ModeDir | 0500, )}, {"loadData directory", overrideIdent{pkg.ID{0xff, 3}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { _, err := loadData(overrideChecksumFile{checksum: wantChecksum}) return err }, }}, nil, pkg.Checksum{}, &os.PathError{ Op: "read", Path: base.Append( "checksum", pkg.Encode(wantChecksum), ).String(), Err: syscall.EISDIR, }}, {"no output", overrideIdent{pkg.ID{0xff, 4}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { return nil }, }}, nil, pkg.Checksum{}, pkg.NoOutputError{}}, {"file output", overrideIdent{pkg.ID{0xff, 5}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { return os.WriteFile(work.String(), []byte{0}, 0400) }, }}, nil, pkg.Checksum{}, errors.New("non-file artifact produced regular file")}, {"symlink output", overrideIdent{pkg.ID{0xff, 6}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { return os.Symlink(work.String(), work.String()) }, }}, nil, pkg.Checksum{}, pkg.InvalidFileModeError( fs.ModeSymlink | 0777, )}, }) }, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d")}, {"pending", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) { c.SetStrict(true) wantErr := stub.UniqueError(0xcafe) n, ready := make(chan struct{}), make(chan struct{}) go func() { if _, _, err := c.Cure(overrideIdent{pkg.ID{0xff}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { close(ready) <-n return wantErr }, }}); !reflect.DeepEqual(err, wantErr) { panic(fmt.Sprintf("Cure: error = %v, want %v", err, wantErr)) } }() <-ready go func() { if _, _, err := c.Cure(overrideIdent{pkg.ID{0xff}, stubArtifact{ kind: pkg.KindTar, }}); !reflect.DeepEqual(err, wantErr) { panic(fmt.Sprintf("Cure: error = %v, want %v", err, wantErr)) } }() // check cache activity while a cure is blocking cureMany(t, c, []cureStep{ {"error passthrough", newStubFile( pkg.KindHTTPGet, pkg.ID{0xff, 1}, nil, nil, stub.UniqueError(0xbad), ), nil, pkg.Checksum{}, stub.UniqueError(0xbad)}, {"file output", overrideIdent{pkg.ID{0xff, 2}, stubArtifact{ kind: pkg.KindTar, cure: func(work, _ *check.Absolute, loadData pkg.CacheDataFunc) error { return os.WriteFile(work.String(), []byte{0}, 0400) }, }}, nil, pkg.Checksum{}, errors.New("non-file artifact produced regular file")}, }) identPendingVal := reflect.ValueOf(c).Elem().FieldByName("identPending") identPending := reflect.NewAt( identPendingVal.Type(), unsafe.Pointer(identPendingVal.UnsafeAddr()), ).Elem().Interface().(map[pkg.ID]<-chan struct{}) notify := identPending[pkg.ID{0xff}] go close(n) <-notify }, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")}, } checkWithCache(t, testCases) } func TestErrors(t *testing.T) { t.Parallel() testCases := []struct { name string err error want string }{ {"ChecksumMismatchError", &pkg.ChecksumMismatchError{ Want: (pkg.Checksum)(bytes.Repeat([]byte{ 0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f, }, 8)), }, "got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + " instead of deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, {"ResponseStatusError", pkg.ResponseStatusError( http.StatusNotAcceptable, ), "the requested URL returned non-OK status: Not Acceptable"}, {"DisallowedTypeflagError", pkg.DisallowedTypeflagError( tar.TypeChar, ), "disallowed typeflag '3'"}, {"InvalidFileModeError", pkg.InvalidFileModeError( fs.ModeSymlink | 0777, ), "artifact did not produce a regular file or directory"}, {"NoOutputError", pkg.NoOutputError{ // empty struct }, "artifact cured successfully but did not produce any output"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := tc.err.Error(); got != tc.want { t.Errorf("Error: %q, want %q", got, tc.want) } }) } } func TestNew(t *testing.T) { t.Parallel() t.Run("nonexistent", func(t *testing.T) { t.Parallel() wantErr := &os.PathError{ Op: "mkdir", Path: container.Nonexistent, Err: syscall.ENOENT, } if _, err := pkg.New(check.MustAbs(container.Nonexistent)); !reflect.DeepEqual(err, wantErr) { t.Errorf("New: error = %#v, want %#v", err, wantErr) } }) t.Run("permission", func(t *testing.T) { t.Parallel() tempDir := check.MustAbs(t.TempDir()) if err := os.Chmod(tempDir.String(), 0); err != nil { t.Fatal(err) } else { t.Cleanup(func() { if err = os.Chmod(tempDir.String(), 0700); err != nil { t.Fatal(err) } }) } wantErr := &os.PathError{ Op: "mkdir", Path: tempDir.Append("cache").String(), Err: syscall.EACCES, } if _, err := pkg.New(tempDir.Append("cache")); !reflect.DeepEqual(err, wantErr) { t.Errorf("New: error = %#v, want %#v", err, wantErr) } }) }