internal/pkg: record cache variant on-disk
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m16s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Hakurei (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m25s
All checks were successful
Test / Create distribution (push) Successful in 1m4s
Test / Sandbox (push) Successful in 2m47s
Test / ShareFS (push) Successful in 3m51s
Test / Sandbox (race detector) (push) Successful in 5m16s
Test / Hakurei (race detector) (push) Successful in 6m23s
Test / Hakurei (push) Successful in 2m39s
Test / Flake checks (push) Successful in 1m25s
This makes custom artifacts much less error-prone to use. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
@@ -432,6 +432,12 @@ func (e InvalidKindError) Error() string {
|
|||||||
// register is not safe for concurrent use. register must not be called after
|
// register is not safe for concurrent use. register must not be called after
|
||||||
// the first instance of [Cache] has been opened.
|
// the first instance of [Cache] has been opened.
|
||||||
func register(k Kind, f IRReadFunc) {
|
func register(k Kind, f IRReadFunc) {
|
||||||
|
openMu.Lock()
|
||||||
|
defer openMu.Unlock()
|
||||||
|
|
||||||
|
if opened {
|
||||||
|
panic("attempting to register after open")
|
||||||
|
}
|
||||||
if _, ok := irArtifact[k]; ok {
|
if _, ok := irArtifact[k]; ok {
|
||||||
panic("attempting to register " + strconv.Itoa(int(k)) + " twice")
|
panic("attempting to register " + strconv.Itoa(int(k)) + " twice")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -70,6 +71,64 @@ func MustDecode(s string) (checksum Checksum) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// extension is a string uniquely identifying a set of custom [Artifact]
|
||||||
|
// implementations registered by calling [Register].
|
||||||
|
extension string
|
||||||
|
|
||||||
|
// openMu synchronises access to global state for initialisation.
|
||||||
|
openMu sync.Mutex
|
||||||
|
// opened is false if [Open] was never called.
|
||||||
|
opened bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extension returns a string uniquely identifying the currently registered set
|
||||||
|
// of custom [Artifact], or the zero value if none was registered.
|
||||||
|
func Extension() string { return extension }
|
||||||
|
|
||||||
|
// ValidExtension returns whether s is valid for use in a call to SetExtension.
|
||||||
|
func ValidExtension(s string) bool {
|
||||||
|
if l := len(s); l == 0 || l > 128 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range s {
|
||||||
|
if v < 'a' || v > 'z' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidExtension is returned for a variant identification string for which
|
||||||
|
// [ValidExtension] returns false.
|
||||||
|
var ErrInvalidExtension = errors.New("invalid extension variant identification string")
|
||||||
|
|
||||||
|
// SetExtension sets the extension variant identification string. SetExtension
|
||||||
|
// must be called before [Open] if custom [Artifact] implementations had been
|
||||||
|
// recorded by calling [Register].
|
||||||
|
//
|
||||||
|
// The variant identification string must be between 1 and 128 bytes long and
|
||||||
|
// consists of only bytes between 'a' and 'z'.
|
||||||
|
//
|
||||||
|
// SetExtension is not safe for concurrent use. SetExtension is called at most
|
||||||
|
// once and must not be called after the first instance of Cache has been opened.
|
||||||
|
func SetExtension(s string) {
|
||||||
|
openMu.Lock()
|
||||||
|
defer openMu.Unlock()
|
||||||
|
|
||||||
|
if opened {
|
||||||
|
panic("attempting to set extension after open")
|
||||||
|
}
|
||||||
|
if extension != "" {
|
||||||
|
panic("attempting to set extension twice")
|
||||||
|
}
|
||||||
|
if !ValidExtension(s) {
|
||||||
|
panic(ErrInvalidExtension)
|
||||||
|
}
|
||||||
|
extension = s
|
||||||
|
statusHeader = makeStatusHeader(s)
|
||||||
|
}
|
||||||
|
|
||||||
// common holds elements and receives methods shared between different contexts.
|
// common holds elements and receives methods shared between different contexts.
|
||||||
type common struct {
|
type common struct {
|
||||||
// Context specific to this [Artifact]. The toplevel context in [Cache] must
|
// Context specific to this [Artifact]. The toplevel context in [Cache] must
|
||||||
@@ -102,19 +161,27 @@ type TContext struct {
|
|||||||
common
|
common
|
||||||
}
|
}
|
||||||
|
|
||||||
// statusHeader is the header written to all status files in dirStatus.
|
// makeStatusHeader creates the header written to every status file. This should
|
||||||
var statusHeader = func() string {
|
// not be called directly, its result is stored in statusHeader and will not
|
||||||
|
// change after the first [Cache] is opened.
|
||||||
|
func makeStatusHeader(extension string) string {
|
||||||
s := programName
|
s := programName
|
||||||
if v := info.Version(); v != info.FallbackVersion {
|
if v := info.Version(); v != info.FallbackVersion {
|
||||||
s += " " + v
|
s += " " + v
|
||||||
}
|
}
|
||||||
|
if extension != "" {
|
||||||
|
s += " with " + extension + " extensions"
|
||||||
|
}
|
||||||
s += " (" + runtime.GOARCH + ")"
|
s += " (" + runtime.GOARCH + ")"
|
||||||
if name, err := os.Hostname(); err == nil {
|
if name, err := os.Hostname(); err == nil {
|
||||||
s += " on " + name
|
s += " on " + name
|
||||||
}
|
}
|
||||||
s += "\n\n"
|
s += "\n\n"
|
||||||
return s
|
return s
|
||||||
}()
|
}
|
||||||
|
|
||||||
|
// statusHeader is the header written to all status files in dirStatus.
|
||||||
|
var statusHeader = makeStatusHeader("")
|
||||||
|
|
||||||
// prepareStatus initialises the status file once.
|
// prepareStatus initialises the status file once.
|
||||||
func (t *TContext) prepareStatus() error {
|
func (t *TContext) prepareStatus() error {
|
||||||
@@ -427,6 +494,9 @@ const (
|
|||||||
// KindFile is the kind of [Artifact] returned by [NewFile].
|
// KindFile is the kind of [Artifact] returned by [NewFile].
|
||||||
KindFile
|
KindFile
|
||||||
|
|
||||||
|
// _kindEnd is the total number of kinds and does not denote a kind.
|
||||||
|
_kindEnd
|
||||||
|
|
||||||
// KindCustomOffset is the first [Kind] value reserved for implementations
|
// KindCustomOffset is the first [Kind] value reserved for implementations
|
||||||
// not from this package.
|
// not from this package.
|
||||||
KindCustomOffset = 1 << 31
|
KindCustomOffset = 1 << 31
|
||||||
@@ -441,6 +511,9 @@ const (
|
|||||||
// fileLock is the file name appended to Cache.base for guaranteeing
|
// fileLock is the file name appended to Cache.base for guaranteeing
|
||||||
// exclusive access to the cache directory.
|
// exclusive access to the cache directory.
|
||||||
fileLock = "lock"
|
fileLock = "lock"
|
||||||
|
// fileVariant is the file name appended to Cache.base holding the variant
|
||||||
|
// identification string set by a prior call to [SetExtension].
|
||||||
|
fileVariant = "variant"
|
||||||
|
|
||||||
// dirIdentifier is the directory name appended to Cache.base for storing
|
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||||
// artifacts named after their [ID].
|
// artifacts named after their [ID].
|
||||||
@@ -540,6 +613,10 @@ const (
|
|||||||
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
|
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
|
||||||
// to support kernels without Landlock LSM enabled.
|
// to support kernels without Landlock LSM enabled.
|
||||||
CHostAbstract
|
CHostAbstract
|
||||||
|
|
||||||
|
// CPromoteVariant allows [pkg.Open] to promote an unextended on-disk cache
|
||||||
|
// to the current extension variant. This is a one-way operation.
|
||||||
|
CPromoteVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
// toplevel holds [context.WithCancel] over caller-supplied context, where all
|
// toplevel holds [context.WithCancel] over caller-supplied context, where all
|
||||||
@@ -1930,6 +2007,20 @@ func (c *Cache) Close() {
|
|||||||
c.unlock()
|
c.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnsupportedVariantError describes an on-disk cache with an extension variant
|
||||||
|
// identification string that differs from the value returned by [Extension].
|
||||||
|
type UnsupportedVariantError string
|
||||||
|
|
||||||
|
func (e UnsupportedVariantError) Error() string {
|
||||||
|
return "unsupported variant " + strconv.Quote(string(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrWouldPromote is returned by [Open] if the [CPromoteVariant] bit is not
|
||||||
|
// set and the on-disk cache requires variant promotion.
|
||||||
|
ErrWouldPromote = errors.New("operation would promote unextended cache")
|
||||||
|
)
|
||||||
|
|
||||||
// Open returns the address of a newly opened instance of [Cache].
|
// Open returns the address of a newly opened instance of [Cache].
|
||||||
//
|
//
|
||||||
// Concurrent cures of a [FloodArtifact] dependency graph is limited to the
|
// Concurrent cures of a [FloodArtifact] dependency graph is limited to the
|
||||||
@@ -1961,6 +2052,14 @@ func open(
|
|||||||
base *check.Absolute,
|
base *check.Absolute,
|
||||||
lock bool,
|
lock bool,
|
||||||
) (*Cache, error) {
|
) (*Cache, error) {
|
||||||
|
openMu.Lock()
|
||||||
|
defer openMu.Unlock()
|
||||||
|
opened = true
|
||||||
|
|
||||||
|
if extension == "" && len(irArtifact) != int(_kindEnd) {
|
||||||
|
panic("attempting to open cache with incomplete variant setup")
|
||||||
|
}
|
||||||
|
|
||||||
if cures < 1 {
|
if cures < 1 {
|
||||||
cures = runtime.NumCPU()
|
cures = runtime.NumCPU()
|
||||||
}
|
}
|
||||||
@@ -1974,8 +2073,10 @@ func open(
|
|||||||
dirStatus,
|
dirStatus,
|
||||||
dirWork,
|
dirWork,
|
||||||
} {
|
} {
|
||||||
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
|
if err := os.MkdirAll(
|
||||||
!errors.Is(err, os.ErrExist) {
|
base.Append(name).String(),
|
||||||
|
0700,
|
||||||
|
); err != nil && !errors.Is(err, os.ErrExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2013,6 +2114,45 @@ func open(
|
|||||||
c.unlock = func() {}
|
c.unlock = func() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variantPath := base.Append(fileVariant).String()
|
||||||
|
if p, err := os.ReadFile(variantPath); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
c.unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// nonexistence implies newly created cache, or a cache predating
|
||||||
|
// variant identification strings, in which case it is silently promoted
|
||||||
|
if err = os.WriteFile(
|
||||||
|
variantPath,
|
||||||
|
[]byte(extension),
|
||||||
|
0400,
|
||||||
|
); err != nil {
|
||||||
|
c.unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if s := string(p); s == "" {
|
||||||
|
if extension != "" {
|
||||||
|
if flags&CPromoteVariant == 0 {
|
||||||
|
c.unlock()
|
||||||
|
return nil, ErrWouldPromote
|
||||||
|
}
|
||||||
|
if err = os.WriteFile(
|
||||||
|
variantPath,
|
||||||
|
[]byte(extension),
|
||||||
|
0400,
|
||||||
|
); err != nil {
|
||||||
|
c.unlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !ValidExtension(s) {
|
||||||
|
c.unlock()
|
||||||
|
return nil, ErrInvalidExtension
|
||||||
|
} else if s != extension {
|
||||||
|
c.unlock()
|
||||||
|
return nil, UnsupportedVariantError(s)
|
||||||
|
}
|
||||||
|
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,25 @@ func unsafeOpen(
|
|||||||
lock bool,
|
lock bool,
|
||||||
) (*pkg.Cache, error)
|
) (*pkg.Cache, error)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// extension is a string uniquely identifying a set of custom [Artifact]
|
||||||
|
// implementations registered by calling [Register].
|
||||||
|
//
|
||||||
|
//go:linkname extension hakurei.app/internal/pkg.extension
|
||||||
|
extension string
|
||||||
|
|
||||||
|
// opened is false if [Open] was never called.
|
||||||
|
//
|
||||||
|
//go:linkname opened hakurei.app/internal/pkg.opened
|
||||||
|
opened bool
|
||||||
|
|
||||||
|
// irArtifact refers to artifact IR interpretation functions and must not be
|
||||||
|
// written to directly.
|
||||||
|
//
|
||||||
|
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
|
||||||
|
irArtifact map[pkg.Kind]pkg.IRReadFunc
|
||||||
|
)
|
||||||
|
|
||||||
// newRContext returns the address of a new [pkg.RContext] unsafely created for
|
// newRContext returns the address of a new [pkg.RContext] unsafely created for
|
||||||
// the specified [testing.TB].
|
// the specified [testing.TB].
|
||||||
func newRContext(tb testing.TB, c *pkg.Cache) *pkg.RContext {
|
func newRContext(tb testing.TB, c *pkg.Cache) *pkg.RContext {
|
||||||
@@ -342,9 +361,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
|
|||||||
restoreTemp = true
|
restoreTemp = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// destroy lock file to avoid changing cache checksums
|
// destroy lock and variant file to avoid changing cache checksums
|
||||||
if err := os.Remove(base.Append("lock").String()); err != nil {
|
for _, s := range []string{
|
||||||
t.Fatal(err)
|
"lock",
|
||||||
|
"variant",
|
||||||
|
} {
|
||||||
|
pathname := base.Append(s)
|
||||||
|
if p, err := os.ReadFile(pathname.String()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(p) != 0 {
|
||||||
|
t.Fatalf("file %q: %q", s, string(p))
|
||||||
|
}
|
||||||
|
if err := os.Remove(pathname.String()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// destroy non-deterministic status files
|
// destroy non-deterministic status files
|
||||||
@@ -1101,6 +1131,10 @@ func TestErrors(t *testing.T) {
|
|||||||
Want: pkg.IRKindIdent,
|
Want: pkg.IRKindIdent,
|
||||||
Ancillary: 0xcafe,
|
Ancillary: 0xcafe,
|
||||||
}, "got invalid kind 48879 IR value (0xcafe) instead of ident"},
|
}, "got invalid kind 48879 IR value (0xcafe) instead of ident"},
|
||||||
|
|
||||||
|
{"UnsupportedVariantError", pkg.UnsupportedVariantError(
|
||||||
|
"rosa",
|
||||||
|
), `unsupported variant "rosa"`},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -1309,6 +1343,8 @@ func (a earlyFailureF) Cure(*pkg.FContext) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDependencyCureErrorEarly(t *testing.T) {
|
func TestDependencyCureErrorEarly(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
checkWithCache(t, []cacheTestCase{
|
checkWithCache(t, []cacheTestCase{
|
||||||
{"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
|
{"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
|
||||||
_, _, err := c.Cure(earlyFailureF(8))
|
_, _, err := c.Cure(earlyFailureF(8))
|
||||||
@@ -1319,7 +1355,7 @@ func TestDependencyCureErrorEarly(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestOpen(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("nonexistent", func(t *testing.T) {
|
t.Run("nonexistent", func(t *testing.T) {
|
||||||
@@ -1367,3 +1403,219 @@ func TestNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtensionRegister(t *testing.T) {
|
||||||
|
extensionOld := extension
|
||||||
|
openedOld := opened
|
||||||
|
t.Cleanup(func() { extension = extensionOld; opened = openedOld })
|
||||||
|
extension = ""
|
||||||
|
opened = false
|
||||||
|
|
||||||
|
t.Run("set", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { extension = "" })
|
||||||
|
|
||||||
|
const want = "rosa"
|
||||||
|
pkg.SetExtension(want)
|
||||||
|
if got := pkg.Extension(); got != want {
|
||||||
|
t.Fatalf("Extension: %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("twice", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { extension = "" })
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
const wantPanic = "attempting to set extension twice"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pkg.SetExtension("rosa")
|
||||||
|
pkg.SetExtension("rosa")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
var wantPanic = pkg.ErrInvalidExtension
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %#v, want %#v", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pkg.SetExtension(" ")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("opened", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
|
||||||
|
if _, err := pkg.Open(
|
||||||
|
t.Context(),
|
||||||
|
message.New(log.Default()),
|
||||||
|
0, 0, 0,
|
||||||
|
check.MustAbs(container.Nonexistent),
|
||||||
|
); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("variant", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
const wantPanic = "attempting to set extension after open"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pkg.SetExtension("rosa")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("register", func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
const wantPanic = "attempting to register after open"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pkg.Register(pkg.KindCustomOffset, nil)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("incomplete", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { delete(irArtifact, pkg.KindCustomOffset) })
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
const wantPanic = "attempting to open cache with incomplete variant setup"
|
||||||
|
if r := recover(); r != wantPanic {
|
||||||
|
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
pkg.Register(pkg.KindCustomOffset, nil)
|
||||||
|
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
_, _ = pkg.Open(nil, nil, 0, 0, 0, nil)
|
||||||
|
panic("unreachable")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("create", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { extension = "" })
|
||||||
|
const want = "rosa"
|
||||||
|
pkg.SetExtension(want)
|
||||||
|
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
if c, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
0, 0, 0,
|
||||||
|
base,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, err := os.ReadFile(base.Append("variant").String()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if string(got) != want {
|
||||||
|
t.Fatalf("variant: %q", string(got))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("access", func(t *testing.T) {
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
|
||||||
|
if err := os.WriteFile(base.Append("variant").String(), nil, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantErr := &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: base.Append("variant").String(),
|
||||||
|
Err: syscall.EACCES,
|
||||||
|
}
|
||||||
|
if _, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
0, 0, 0,
|
||||||
|
base,
|
||||||
|
); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Fatalf("Open: error = %v, want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("promote", func(t *testing.T) {
|
||||||
|
t.Cleanup(func() { extension = "" })
|
||||||
|
const want = "rosa"
|
||||||
|
pkg.SetExtension(want)
|
||||||
|
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
|
||||||
|
variantPath := base.Append("variant")
|
||||||
|
if err := os.WriteFile(variantPath.String(), nil, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
0, 0, 0,
|
||||||
|
base,
|
||||||
|
); !reflect.DeepEqual(err, pkg.ErrWouldPromote) {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, err := os.ReadFile(variantPath.String()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if len(p) != 0 {
|
||||||
|
t.Fatalf("variant: %q", string(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
pkg.CPromoteVariant, 0, 0,
|
||||||
|
base,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
} else {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, err := os.ReadFile(variantPath.String()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if string(p) != want {
|
||||||
|
t.Fatalf("variant: %q, want %q", string(p), want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("open invalid", func(t *testing.T) {
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
|
||||||
|
variantPath := base.Append("variant")
|
||||||
|
if err := os.WriteFile(variantPath.String(), make([]byte, 129), 0400); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
0, 0, 0,
|
||||||
|
base,
|
||||||
|
); !reflect.DeepEqual(err, pkg.ErrInvalidExtension) {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unsupported", func(t *testing.T) {
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
t.Cleanup(func() { opened = false })
|
||||||
|
|
||||||
|
variantPath := base.Append("variant")
|
||||||
|
if err := os.WriteFile(variantPath.String(), []byte("rosa"), 0400); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := pkg.Open(
|
||||||
|
t.Context(), nil,
|
||||||
|
0, 0, 0,
|
||||||
|
base,
|
||||||
|
); !reflect.DeepEqual(err, pkg.UnsupportedVariantError("rosa")) {
|
||||||
|
t.Fatalf("Open: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import (
|
|||||||
"hakurei.app/internal/pkg"
|
"hakurei.app/internal/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Extension is the variant identification string of custom artifact
|
||||||
|
// implementations registered by package rosa.
|
||||||
|
const Extension = "rosa"
|
||||||
|
|
||||||
|
func init() { pkg.SetExtension(Extension) }
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// kindEtc is the kind of [pkg.Artifact] of cureEtc.
|
// kindEtc is the kind of [pkg.Artifact] of cureEtc.
|
||||||
kindEtc = iota + pkg.KindCustomOffset
|
kindEtc = iota + pkg.KindCustomOffset
|
||||||
|
|||||||
Reference in New Issue
Block a user