aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-17 16:09:14 +0900
committerOphestra <cat@gensokyo.uk>2026-03-17 16:09:14 +0900
commitbac583f89edd1360150143ff51f7264c01aa69ec (patch)
tree47914efe1ae9fae256ef93725097bdb9255c03d5 /internal
parent722989c68256469963aee963bed9c6ab4bf55b67 (diff)
internal/stub: move from container
This package solves a very specific stubbing use case, in a less than elegant manner. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
-rw-r--r--internal/env/env_test.go2
-rw-r--r--internal/outcome/dispatcher_test.go2
-rw-r--r--internal/outcome/hsu_test.go2
-rw-r--r--internal/outcome/shim_test.go2
-rw-r--r--internal/outcome/spaccount_test.go2
-rw-r--r--internal/outcome/spcontainer_test.go2
-rw-r--r--internal/outcome/spdbus_test.go2
-rw-r--r--internal/outcome/sppipewire_test.go2
-rw-r--r--internal/outcome/sppulse_test.go2
-rw-r--r--internal/outcome/spruntime_test.go2
-rw-r--r--internal/outcome/sptmpdir_test.go2
-rw-r--r--internal/outcome/spwayland_test.go2
-rw-r--r--internal/outcome/spx11_test.go2
-rw-r--r--internal/pipewire/pipewire_test.go2
-rw-r--r--internal/pkg/exec_test.go2
-rw-r--r--internal/pkg/pkg_test.go2
-rw-r--r--internal/pkg/tar_test.go2
-rw-r--r--internal/store/data_test.go2
-rw-r--r--internal/store/segment_test.go2
-rw-r--r--internal/stub/call.go37
-rw-r--r--internal/stub/call_test.go27
-rw-r--r--internal/stub/errors.go28
-rw-r--r--internal/stub/errors_test.go42
-rw-r--r--internal/stub/exit.go44
-rw-r--r--internal/stub/exit_test.go111
-rw-r--r--internal/stub/stub.go153
-rw-r--r--internal/stub/stub_test.go335
-rw-r--r--internal/system/acl_test.go2
-rw-r--r--internal/system/dbus_test.go2
-rw-r--r--internal/system/dispatcher_test.go2
-rw-r--r--internal/system/link_test.go2
-rw-r--r--internal/system/mkdir_test.go2
-rw-r--r--internal/system/pipewire_test.go2
-rw-r--r--internal/system/system_test.go2
-rw-r--r--internal/system/wayland_test.go2
-rw-r--r--internal/system/xhost_test.go2
-rw-r--r--internal/wayland/wayland_test.go2
37 files changed, 806 insertions, 29 deletions
diff --git a/internal/env/env_test.go b/internal/env/env_test.go
index 0bfcf83b..0e5db4a9 100644
--- a/internal/env/env_test.go
+++ b/internal/env/env_test.go
@@ -7,10 +7,10 @@ import (
"hakurei.app/check"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/env"
+ "hakurei.app/internal/stub"
)
func TestPaths(t *testing.T) {
diff --git a/internal/outcome/dispatcher_test.go b/internal/outcome/dispatcher_test.go
index a11fb869..a099df58 100644
--- a/internal/outcome/dispatcher_test.go
+++ b/internal/outcome/dispatcher_test.go
@@ -22,8 +22,8 @@ import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
"hakurei.app/message"
)
diff --git a/internal/outcome/hsu_test.go b/internal/outcome/hsu_test.go
index 4ee7fa9c..f5654445 100644
--- a/internal/outcome/hsu_test.go
+++ b/internal/outcome/hsu_test.go
@@ -9,8 +9,8 @@ import (
"testing"
"unsafe"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
)
func TestHsu(t *testing.T) {
diff --git a/internal/outcome/shim_test.go b/internal/outcome/shim_test.go
index aa8220b8..08f037da 100644
--- a/internal/outcome/shim_test.go
+++ b/internal/outcome/shim_test.go
@@ -12,11 +12,11 @@ import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
- "hakurei.app/container/stub"
"hakurei.app/ext"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/env"
+ "hakurei.app/internal/stub"
)
func TestShimEntrypoint(t *testing.T) {
diff --git a/internal/outcome/spaccount_test.go b/internal/outcome/spaccount_test.go
index c1693dd8..f94e4591 100644
--- a/internal/outcome/spaccount_test.go
+++ b/internal/outcome/spaccount_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
)
func TestSpAccountOp(t *testing.T) {
diff --git a/internal/outcome/spcontainer_test.go b/internal/outcome/spcontainer_test.go
index d1f26468..7dd47735 100644
--- a/internal/outcome/spcontainer_test.go
+++ b/internal/outcome/spcontainer_test.go
@@ -11,11 +11,11 @@ import (
"hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
- "hakurei.app/container/stub"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
)
diff --git a/internal/outcome/spdbus_test.go b/internal/outcome/spdbus_test.go
index 350fc326..6f357193 100644
--- a/internal/outcome/spdbus_test.go
+++ b/internal/outcome/spdbus_test.go
@@ -5,11 +5,11 @@ import (
"testing"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/helper"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
"hakurei.app/message"
)
diff --git a/internal/outcome/sppipewire_test.go b/internal/outcome/sppipewire_test.go
index 3259915c..1e3dab6a 100644
--- a/internal/outcome/sppipewire_test.go
+++ b/internal/outcome/sppipewire_test.go
@@ -4,9 +4,9 @@ import (
"testing"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/pipewire"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
)
diff --git a/internal/outcome/sppulse_test.go b/internal/outcome/sppulse_test.go
index 9b014470..f1abccaa 100644
--- a/internal/outcome/sppulse_test.go
+++ b/internal/outcome/sppulse_test.go
@@ -9,9 +9,9 @@ import (
"hakurei.app/check"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
)
diff --git a/internal/outcome/spruntime_test.go b/internal/outcome/spruntime_test.go
index bd7ad37c..e083f7c3 100644
--- a/internal/outcome/spruntime_test.go
+++ b/internal/outcome/spruntime_test.go
@@ -5,10 +5,10 @@ import (
"hakurei.app/container"
"hakurei.app/container/std"
- "hakurei.app/container/stub"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
)
diff --git a/internal/outcome/sptmpdir_test.go b/internal/outcome/sptmpdir_test.go
index fd9dc1df..620f51b0 100644
--- a/internal/outcome/sptmpdir_test.go
+++ b/internal/outcome/sptmpdir_test.go
@@ -5,10 +5,10 @@ import (
"hakurei.app/container"
"hakurei.app/container/std"
- "hakurei.app/container/stub"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
)
diff --git a/internal/outcome/spwayland_test.go b/internal/outcome/spwayland_test.go
index 828cc86c..7cc4ba84 100644
--- a/internal/outcome/spwayland_test.go
+++ b/internal/outcome/spwayland_test.go
@@ -4,9 +4,9 @@ import (
"testing"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/system"
"hakurei.app/internal/wayland"
)
diff --git a/internal/outcome/spx11_test.go b/internal/outcome/spx11_test.go
index d4c10902..0ede2d65 100644
--- a/internal/outcome/spx11_test.go
+++ b/internal/outcome/spx11_test.go
@@ -5,9 +5,9 @@ import (
"testing"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
)
func TestSpX11Op(t *testing.T) {
diff --git a/internal/pipewire/pipewire_test.go b/internal/pipewire/pipewire_test.go
index 9305e7ec..0187391c 100644
--- a/internal/pipewire/pipewire_test.go
+++ b/internal/pipewire/pipewire_test.go
@@ -8,8 +8,8 @@ import (
"testing"
"time"
- "hakurei.app/container/stub"
"hakurei.app/internal/pipewire"
+ "hakurei.app/internal/stub"
)
func TestContext(t *testing.T) {
diff --git a/internal/pkg/exec_test.go b/internal/pkg/exec_test.go
index 5b0fa765..baf9b8de 100644
--- a/internal/pkg/exec_test.go
+++ b/internal/pkg/exec_test.go
@@ -14,9 +14,9 @@ import (
"unique"
"hakurei.app/check"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/pkg"
+ "hakurei.app/internal/stub"
)
// testtoolBin is the container test tool binary made available to the
diff --git a/internal/pkg/pkg_test.go b/internal/pkg/pkg_test.go
index 3c68dbcc..6b5d5fc4 100644
--- a/internal/pkg/pkg_test.go
+++ b/internal/pkg/pkg_test.go
@@ -23,9 +23,9 @@ import (
"hakurei.app/check"
"hakurei.app/container"
- "hakurei.app/container/stub"
"hakurei.app/fhs"
"hakurei.app/internal/pkg"
+ "hakurei.app/internal/stub"
"hakurei.app/message"
)
diff --git a/internal/pkg/tar_test.go b/internal/pkg/tar_test.go
index 09e1e4db..c604dc78 100644
--- a/internal/pkg/tar_test.go
+++ b/internal/pkg/tar_test.go
@@ -13,8 +13,8 @@ import (
"testing/fstest"
"hakurei.app/check"
- "hakurei.app/container/stub"
"hakurei.app/internal/pkg"
+ "hakurei.app/internal/stub"
)
func TestTar(t *testing.T) {
diff --git a/internal/store/data_test.go b/internal/store/data_test.go
index fe0e2124..08eb4f9c 100644
--- a/internal/store/data_test.go
+++ b/internal/store/data_test.go
@@ -11,8 +11,8 @@ import (
"testing"
"time"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
)
func TestEntryData(t *testing.T) {
diff --git a/internal/store/segment_test.go b/internal/store/segment_test.go
index 671f27a0..61b97c26 100644
--- a/internal/store/segment_test.go
+++ b/internal/store/segment_test.go
@@ -13,9 +13,9 @@ import (
_ "unsafe" // for go:linkname
"hakurei.app/check"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/store"
+ "hakurei.app/internal/stub"
)
// Made available here for direct validation of state entry files.
diff --git a/internal/stub/call.go b/internal/stub/call.go
new file mode 100644
index 00000000..585a69ef
--- /dev/null
+++ b/internal/stub/call.go
@@ -0,0 +1,37 @@
+package stub
+
+import (
+ "slices"
+)
+
+// ExpectArgs is an array primarily for storing expected function arguments.
+// Its actual use is defined by the implementation.
+type ExpectArgs = [5]any
+
+// An Expect stores expected calls of a goroutine.
+type Expect struct {
+ Calls []Call
+
+ // Tracks are handed out to descendant goroutines in order.
+ Tracks []Expect
+}
+
+// A Call holds expected arguments of a function call and its outcome.
+type Call struct {
+ // Name is the function Name of this call. Must be unique.
+ Name string
+ // Args are the expected arguments of this Call.
+ Args ExpectArgs
+ // Ret is the return value of this Call.
+ Ret any
+ // Err is the returned error of this Call.
+ Err error
+}
+
+// Error returns [Call.Err] if all arguments are true, or [ErrCheck] otherwise.
+func (k *Call) Error(ok ...bool) error {
+ if !slices.Contains(ok, false) {
+ return k.Err
+ }
+ return ErrCheck
+}
diff --git a/internal/stub/call_test.go b/internal/stub/call_test.go
new file mode 100644
index 00000000..76705c8d
--- /dev/null
+++ b/internal/stub/call_test.go
@@ -0,0 +1,27 @@
+package stub_test
+
+import (
+ "reflect"
+ "testing"
+
+ "hakurei.app/internal/stub"
+)
+
+func TestCallError(t *testing.T) {
+ t.Parallel()
+
+ t.Run("contains false", func(t *testing.T) {
+ t.Parallel()
+ if err := new(stub.Call).Error(true, false, true); !reflect.DeepEqual(err, stub.ErrCheck) {
+ t.Errorf("Error: %#v, want %#v", err, stub.ErrCheck)
+ }
+ })
+
+ t.Run("passthrough", func(t *testing.T) {
+ t.Parallel()
+ wantErr := stub.UniqueError(0xbabe)
+ if err := (&stub.Call{Err: wantErr}).Error(true); !reflect.DeepEqual(err, wantErr) {
+ t.Errorf("Error: %#v, want %#v", err, wantErr)
+ }
+ })
+}
diff --git a/internal/stub/errors.go b/internal/stub/errors.go
new file mode 100644
index 00000000..c89149be
--- /dev/null
+++ b/internal/stub/errors.go
@@ -0,0 +1,28 @@
+package stub
+
+import (
+ "errors"
+ "strconv"
+)
+
+var (
+ ErrCheck = errors.New("one or more arguments did not match")
+)
+
+// UniqueError is an error that only equivalates to another [UniqueError] with
+// the same magic value.
+type UniqueError uintptr
+
+func (e UniqueError) Error() string {
+ return "unique error " +
+ strconv.FormatUint(uint64(e), 10) +
+ " injected by the test suite"
+}
+
+func (e UniqueError) Is(target error) bool {
+ var u UniqueError
+ if !errors.As(target, &u) {
+ return false
+ }
+ return e == u
+}
diff --git a/internal/stub/errors_test.go b/internal/stub/errors_test.go
new file mode 100644
index 00000000..2e5d9930
--- /dev/null
+++ b/internal/stub/errors_test.go
@@ -0,0 +1,42 @@
+package stub_test
+
+import (
+ "errors"
+ "syscall"
+ "testing"
+
+ "hakurei.app/internal/stub"
+)
+
+func TestUniqueError(t *testing.T) {
+ t.Parallel()
+
+ t.Run("format", func(t *testing.T) {
+ t.Parallel()
+ want := "unique error 2989 injected by the test suite"
+ if got := stub.UniqueError(0xbad).Error(); got != want {
+ t.Errorf("Error: %q, want %q", got, want)
+ }
+ })
+
+ t.Run("is", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("type", func(t *testing.T) {
+ t.Parallel()
+ if errors.Is(stub.UniqueError(0), syscall.ENOTRECOVERABLE) {
+ t.Error("Is: unexpected true")
+ }
+ })
+
+ t.Run("val", func(t *testing.T) {
+ t.Parallel()
+ if errors.Is(stub.UniqueError(0), stub.UniqueError(1)) {
+ t.Error("Is: unexpected true")
+ }
+ if !errors.Is(stub.UniqueError(0xbad), stub.UniqueError(0xbad)) {
+ t.Error("Is: unexpected false")
+ }
+ })
+ })
+}
diff --git a/internal/stub/exit.go b/internal/stub/exit.go
new file mode 100644
index 00000000..062ee2e0
--- /dev/null
+++ b/internal/stub/exit.go
@@ -0,0 +1,44 @@
+package stub
+
+import "testing"
+
+// PanicExit is a magic panic value treated as a simulated exit.
+const PanicExit = 0xdead
+
+const (
+ panicFailNow = 0xcafe0 + iota
+ panicFatal
+ panicFatalf
+)
+
+// HandleExit must be deferred before calling with the stub.
+func HandleExit(t testing.TB) {
+ switch r := recover(); r {
+ case PanicExit:
+ break
+
+ case panicFailNow:
+ t.FailNow()
+
+ case panicFatal, panicFatalf, nil:
+ break
+
+ default:
+ panic(r)
+ }
+}
+
+// handleExitNew handles exits from goroutines created by [Stub.New].
+func handleExitNew(t testing.TB) {
+ switch r := recover(); r {
+ case PanicExit, panicFatal, panicFatalf, nil:
+ break
+
+ case panicFailNow:
+ t.Fail()
+ break
+
+ default:
+ panic(r)
+ }
+}
diff --git a/internal/stub/exit_test.go b/internal/stub/exit_test.go
new file mode 100644
index 00000000..c977d2ec
--- /dev/null
+++ b/internal/stub/exit_test.go
@@ -0,0 +1,111 @@
+package stub_test
+
+import (
+ "testing"
+ _ "unsafe" // for go:linkname
+
+ "hakurei.app/internal/stub"
+)
+
+// Made available here to check panic recovery behaviour.
+//
+//go:linkname handleExitNew hakurei.app/internal/stub.handleExitNew
+func handleExitNew(t testing.TB)
+
+// overrideTFailNow overrides the Fail and FailNow method.
+type overrideTFailNow struct {
+ *testing.T
+ failNow bool
+ fail bool
+}
+
+func (o *overrideTFailNow) FailNow() {
+ if o.failNow {
+ o.Errorf("attempted to FailNow twice")
+ }
+ o.failNow = true
+}
+
+func (o *overrideTFailNow) Fail() {
+ if o.fail {
+ o.Errorf("attempted to Fail twice")
+ }
+ o.fail = true
+}
+
+func TestHandleExit(t *testing.T) {
+ t.Parallel()
+
+ t.Run("exit", func(t *testing.T) {
+ t.Parallel()
+ defer stub.HandleExit(t)
+ panic(stub.PanicExit)
+ })
+
+ t.Run("goexit", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("FailNow", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideTFailNow{T: t}
+ defer func() {
+ if !ot.failNow {
+ t.Errorf("FailNow was never called")
+ }
+ }()
+ defer stub.HandleExit(ot)
+ panic(0xcafe0)
+ })
+
+ t.Run("Fail", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideTFailNow{T: t}
+ defer func() {
+ if !ot.fail {
+ t.Errorf("Fail was never called")
+ }
+ }()
+ defer handleExitNew(ot)
+ panic(0xcafe0)
+ })
+ })
+
+ t.Run("nil", func(t *testing.T) {
+ t.Parallel()
+ defer stub.HandleExit(t)
+ })
+
+ t.Run("passthrough", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("toplevel", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := 0xcafe
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+
+ }()
+ defer stub.HandleExit(t)
+ panic(0xcafe)
+ })
+
+ t.Run("new", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := 0xcafe
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+
+ }()
+ defer handleExitNew(t)
+ panic(0xcafe)
+ })
+ })
+}
diff --git a/internal/stub/stub.go b/internal/stub/stub.go
new file mode 100644
index 00000000..d60e92dd
--- /dev/null
+++ b/internal/stub/stub.go
@@ -0,0 +1,153 @@
+// Package stub provides function call level stubbing and validation for library
+// functions that are impossible to check otherwise.
+package stub
+
+import (
+ "reflect"
+ "sync"
+ "testing"
+)
+
+// this should prevent stub from being inadvertently imported outside tests
+var _ = func() {
+ if !testing.Testing() {
+ panic("stub imported while not in a test")
+ }
+}
+
+const (
+ // A CallSeparator denotes an injected separation between two groups of calls.
+ CallSeparator = "\x00"
+)
+
+// A Stub is a collection of tracks of expected calls.
+type Stub[K any] struct {
+ testing.TB
+
+ // makeK creates a new K for a descendant [Stub].
+ // This function may be called concurrently.
+ makeK func(s *Stub[K]) K
+
+ // want is a hierarchy of expected calls.
+ want Expect
+ // pos is the current position in [Expect.Calls].
+ pos int
+ // goroutine counts the number of goroutines created by this [Stub].
+ goroutine int
+ // sub stores the addresses of descendant [Stub] created by New.
+ sub []*Stub[K]
+ // wg waits for all descendants to complete.
+ wg *sync.WaitGroup
+}
+
+// New creates a root [Stub].
+func New[K any](tb testing.TB, makeK func(s *Stub[K]) K, want Expect) *Stub[K] {
+ return &Stub[K]{TB: tb, makeK: makeK, want: want, wg: new(sync.WaitGroup)}
+}
+
+func (s *Stub[K]) FailNow() { s.Helper(); panic(panicFailNow) }
+func (s *Stub[K]) Fatal(args ...any) { s.Helper(); s.Error(args...); panic(panicFatal) }
+func (s *Stub[K]) Fatalf(format string, args ...any) {
+ s.Helper()
+ s.Errorf(format, args...)
+ panic(panicFatalf)
+}
+
+func (s *Stub[K]) SkipNow() { s.Helper(); panic("invalid call to SkipNow") }
+func (s *Stub[K]) Skip(...any) { s.Helper(); panic("invalid call to Skip") }
+func (s *Stub[K]) Skipf(string, ...any) { s.Helper(); panic("invalid call to Skipf") }
+
+// New calls f in a new goroutine
+func (s *Stub[K]) New(f func(k K)) {
+ s.Helper()
+
+ s.Expects("New")
+ if len(s.want.Tracks) <= s.goroutine {
+ s.Fatal("New: track overrun")
+ }
+ ds := &Stub[K]{TB: s.TB, makeK: s.makeK, want: s.want.Tracks[s.goroutine], wg: s.wg}
+ s.goroutine++
+ s.sub = append(s.sub, ds)
+ s.wg.Add(1)
+ go func() {
+ s.Helper()
+
+ defer s.wg.Done()
+ defer handleExitNew(s.TB)
+ f(s.makeK(ds))
+ }()
+}
+
+// Pos returns the current position of [Stub] in its [Expect.Calls]
+func (s *Stub[K]) Pos() int { return s.pos }
+
+// Len returns the length of [Expect.Calls].
+func (s *Stub[K]) Len() int { return len(s.want.Calls) }
+
+// VisitIncomplete calls f on an incomplete s and all its descendants.
+func (s *Stub[K]) VisitIncomplete(f func(s *Stub[K])) {
+ s.Helper()
+ s.wg.Wait()
+
+ if s.want.Calls != nil && len(s.want.Calls) != s.pos {
+ f(s)
+ }
+ for _, ds := range s.sub {
+ ds.VisitIncomplete(f)
+ }
+}
+
+// Expects checks the name of and returns the current [Call] and advances pos.
+func (s *Stub[K]) Expects(name string) (expect *Call) {
+ s.Helper()
+
+ if len(s.want.Calls) == s.pos {
+ s.Fatal("Expects: advancing beyond expected calls")
+ }
+ expect = &s.want.Calls[s.pos]
+ if name != expect.Name {
+ if expect.Name == CallSeparator {
+ s.Fatalf("Expects: func = %s, separator overrun", name)
+ }
+ if name == CallSeparator {
+ s.Fatalf("Expects: separator, want %s", expect.Name)
+ }
+ s.Fatalf("Expects: func = %s, want %s", name, expect.Name)
+ }
+ s.pos++
+ return
+}
+
+// CheckArg checks an argument comparable with the == operator. Avoid using this with pointers.
+func CheckArg[T comparable, K any](s *Stub[K], arg string, got T, n int) bool {
+ s.Helper()
+
+ pos := s.pos - 1
+ if pos < 0 || pos >= len(s.want.Calls) {
+ panic("invalid call to CheckArg")
+ }
+ expect := s.want.Calls[pos]
+ want, ok := expect.Args[n].(T)
+ if !ok || got != want {
+ s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
+ return false
+ }
+ return true
+}
+
+// CheckArgReflect checks an argument of any type.
+func CheckArgReflect[K any](s *Stub[K], arg string, got any, n int) bool {
+ s.Helper()
+
+ pos := s.pos - 1
+ if pos < 0 || pos >= len(s.want.Calls) {
+ panic("invalid call to CheckArgReflect")
+ }
+ expect := s.want.Calls[pos]
+ want := expect.Args[n]
+ if !reflect.DeepEqual(got, want) {
+ s.Errorf("%s: %s = %#v, want %#v (%d)", expect.Name, arg, got, want, pos)
+ return false
+ }
+ return true
+}
diff --git a/internal/stub/stub_test.go b/internal/stub/stub_test.go
new file mode 100644
index 00000000..9c9b0eb8
--- /dev/null
+++ b/internal/stub/stub_test.go
@@ -0,0 +1,335 @@
+package stub
+
+import (
+ "reflect"
+ "sync/atomic"
+ "testing"
+)
+
+// stubHolder embeds [Stub].
+type stubHolder struct{ *Stub[stubHolder] }
+
+// overrideT allows some methods of [testing.T] to be overridden.
+type overrideT struct {
+ *testing.T
+
+ error atomic.Pointer[func(args ...any)]
+ errorf atomic.Pointer[func(format string, args ...any)]
+}
+
+func (t *overrideT) Error(args ...any) {
+ fp := t.error.Load()
+ if fp == nil || *fp == nil {
+ t.T.Error(args...)
+ return
+ }
+ (*fp)(args...)
+}
+
+func (t *overrideT) Errorf(format string, args ...any) {
+ fp := t.errorf.Load()
+ if fp == nil || *fp == nil {
+ t.T.Errorf(format, args...)
+ return
+ }
+ (*fp)(format, args...)
+}
+
+func TestStub(t *testing.T) {
+ t.Parallel()
+
+ t.Run("goexit", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("FailNow", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ if r := recover(); r != panicFailNow {
+ t.Errorf("recover: %v", r)
+ }
+ }()
+ stubHolder{&Stub[stubHolder]{TB: t}}.FailNow()
+ })
+
+ t.Run("SkipNow", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := "invalid call to SkipNow"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ stubHolder{&Stub[stubHolder]{TB: t}}.SkipNow()
+ })
+
+ t.Run("Skip", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := "invalid call to Skip"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ stubHolder{&Stub[stubHolder]{TB: t}}.Skip()
+ })
+
+ t.Run("Skipf", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := "invalid call to Skipf"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ stubHolder{&Stub[stubHolder]{TB: t}}.Skipf("")
+ })
+ })
+
+ t.Run("new", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("success", func(t *testing.T) {
+ t.Parallel()
+
+ s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"New", ExpectArgs{}, nil, nil},
+ }, Tracks: []Expect{{Calls: []Call{
+ {"done", ExpectArgs{0xbabe}, nil, nil},
+ }}}})
+
+ s.New(func(k stubHolder) {
+ expect := k.Expects("done")
+ if expect.Name != "done" {
+ t.Errorf("New: Name = %s, want done", expect.Name)
+ }
+ if expect.Args != (ExpectArgs{0xbabe}) {
+ t.Errorf("New: Args = %#v", expect.Args)
+ }
+ if expect.Ret != nil {
+ t.Errorf("New: Ret = %#v", expect.Ret)
+ }
+ if expect.Err != nil {
+ t.Errorf("New: Err = %#v", expect.Err)
+ }
+ })
+
+ if pos := s.Pos(); pos != 1 {
+ t.Errorf("Pos: %d, want 1", pos)
+ }
+ if l := s.Len(); l != 1 {
+ t.Errorf("Len: %d, want 1", l)
+ }
+
+ s.VisitIncomplete(func(s *Stub[stubHolder]) { panic("unreachable") })
+ })
+
+ t.Run("overrun", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideT{T: t}
+ ot.error.Store(checkError(t, "New: track overrun"))
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"New", ExpectArgs{}, nil, nil},
+ {"panic", ExpectArgs{"unreachable"}, nil, nil},
+ }})
+ func() { defer HandleExit(t); s.New(func(k stubHolder) { panic("unreachable") }) }()
+
+ var visit int
+ s.VisitIncomplete(func(s *Stub[stubHolder]) {
+ visit++
+ if visit > 1 {
+ panic("unexpected visit count")
+ }
+
+ want := Call{"panic", ExpectArgs{"unreachable"}, nil, nil}
+ if got := s.want.Calls[s.pos]; !reflect.DeepEqual(got, want) {
+ t.Errorf("VisitIncomplete: %#v, want %#v", got, want)
+ }
+ })
+ })
+
+ t.Run("expects", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("overrun", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideT{T: t}
+ ot.error.Store(checkError(t, "Expects: advancing beyond expected calls"))
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
+ func() { defer HandleExit(t); s.Expects("unreachable") }()
+ })
+
+ t.Run("separator", func(t *testing.T) {
+ t.Parallel()
+
+ t.Run("overrun", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideT{T: t}
+ ot.errorf.Store(checkErrorf(t, "Expects: func = %s, separator overrun", "meow"))
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {CallSeparator, ExpectArgs{}, nil, nil},
+ }})
+ func() { defer HandleExit(t); s.Expects("meow") }()
+ })
+
+ t.Run("mismatch", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideT{T: t}
+ ot.errorf.Store(checkErrorf(t, "Expects: separator, want %s", "panic"))
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"panic", ExpectArgs{}, nil, nil},
+ }})
+ func() { defer HandleExit(t); s.Expects(CallSeparator) }()
+ })
+ })
+
+ t.Run("mismatch", func(t *testing.T) {
+ t.Parallel()
+
+ ot := &overrideT{T: t}
+ ot.errorf.Store(checkErrorf(t, "Expects: func = %s, want %s", "meow", "nya"))
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"nya", ExpectArgs{}, nil, nil},
+ }})
+ func() { defer HandleExit(t); s.Expects("meow") }()
+ })
+ })
+ })
+}
+
+func TestCheckArg(t *testing.T) {
+ t.Run("oob negative", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := "invalid call to CheckArg"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
+ CheckArg(s, "unreachable", struct{}{}, 0)
+ })
+
+ ot := &overrideT{T: t}
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"panic", ExpectArgs{PanicExit}, nil, nil},
+ {"meow", ExpectArgs{-1}, nil, nil},
+ }})
+
+ t.Run("match", func(t *testing.T) {
+ s.Expects("panic")
+ if !CheckArg(s, "v", PanicExit, 0) {
+ t.Errorf("CheckArg: unexpected false")
+ }
+ })
+
+ t.Run("mismatch", func(t *testing.T) {
+ defer HandleExit(t)
+ s.Expects("meow")
+ ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
+ if CheckArg(s, "time", 0, 0) {
+ t.Errorf("CheckArg: unexpected true")
+ }
+ })
+
+ t.Run("oob", func(t *testing.T) {
+ s.pos++
+ defer func() {
+ want := "invalid call to CheckArg"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ CheckArg(s, "unreachable", struct{}{}, 0)
+ })
+}
+
+func TestCheckArgReflect(t *testing.T) {
+ t.Parallel()
+
+ t.Run("oob lower", func(t *testing.T) {
+ t.Parallel()
+
+ defer func() {
+ want := "invalid call to CheckArgReflect"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ s := New(t, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{})
+ CheckArgReflect(s, "unreachable", struct{}{}, 0)
+ })
+
+ ot := &overrideT{T: t}
+ s := New(ot, func(s *Stub[stubHolder]) stubHolder { return stubHolder{s} }, Expect{Calls: []Call{
+ {"panic", ExpectArgs{PanicExit}, nil, nil},
+ {"meow", ExpectArgs{-1}, nil, nil},
+ }})
+ t.Run("match", func(t *testing.T) {
+ s.Expects("panic")
+ if !CheckArgReflect(s, "v", PanicExit, 0) {
+ t.Errorf("CheckArgReflect: unexpected false")
+ }
+ })
+ t.Run("mismatch", func(t *testing.T) {
+ defer HandleExit(t)
+ s.Expects("meow")
+ ot.errorf.Store(checkErrorf(t, "%s: %s = %#v, want %#v (%d)", "meow", "time", 0, -1, 1))
+ if CheckArgReflect(s, "time", 0, 0) {
+ t.Errorf("CheckArgReflect: unexpected true")
+ }
+ })
+ t.Run("oob", func(t *testing.T) {
+ s.pos++
+ defer func() {
+ want := "invalid call to CheckArgReflect"
+ if r := recover(); r != want {
+ t.Errorf("recover: %v, want %v", r, want)
+ }
+ }()
+ CheckArgReflect(s, "unreachable", struct{}{}, 0)
+ })
+}
+
+func checkError(t *testing.T, wantArgs ...any) *func(args ...any) {
+ var called bool
+ f := func(args ...any) {
+ if called {
+ panic("invalid call to error")
+ }
+ called = true
+
+ if !reflect.DeepEqual(args, wantArgs) {
+ t.Errorf("Error: %#v, want %#v", args, wantArgs)
+ }
+ panic(PanicExit)
+ }
+ return &f
+}
+
+func checkErrorf(t *testing.T, wantFormat string, wantArgs ...any) *func(format string, args ...any) {
+ var called bool
+ f := func(format string, args ...any) {
+ if called {
+ panic("invalid call to errorf")
+ }
+ called = true
+
+ if format != wantFormat {
+ t.Errorf("Errorf: format = %q, want %q", format, wantFormat)
+ }
+ if !reflect.DeepEqual(args, wantArgs) {
+ t.Errorf("Errorf: args = %#v, want %#v", args, wantArgs)
+ }
+ panic(PanicExit)
+ }
+ return &f
+}
diff --git a/internal/system/acl_test.go b/internal/system/acl_test.go
index 70feaa8b..01f9d9b1 100644
--- a/internal/system/acl_test.go
+++ b/internal/system/acl_test.go
@@ -5,9 +5,9 @@ import (
"syscall"
"testing"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
)
func TestACLUpdateOp(t *testing.T) {
diff --git a/internal/system/dbus_test.go b/internal/system/dbus_test.go
index 26e5b87a..f71a6cb5 100644
--- a/internal/system/dbus_test.go
+++ b/internal/system/dbus_test.go
@@ -9,10 +9,10 @@ import (
"syscall"
"testing"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/dbus"
"hakurei.app/internal/helper"
+ "hakurei.app/internal/stub"
)
func TestDBusProxyOp(t *testing.T) {
diff --git a/internal/system/dispatcher_test.go b/internal/system/dispatcher_test.go
index 01236e4b..977140ec 100644
--- a/internal/system/dispatcher_test.go
+++ b/internal/system/dispatcher_test.go
@@ -10,11 +10,11 @@ import (
"unsafe"
"hakurei.app/check"
- "hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/acl"
"hakurei.app/internal/dbus"
"hakurei.app/internal/pipewire"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/xcb"
)
diff --git a/internal/system/link_test.go b/internal/system/link_test.go
index b1d845b6..9297cf00 100644
--- a/internal/system/link_test.go
+++ b/internal/system/link_test.go
@@ -3,8 +3,8 @@ package system
import (
"testing"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
)
func TestHardlinkOp(t *testing.T) {
diff --git a/internal/system/mkdir_test.go b/internal/system/mkdir_test.go
index e5d97b3a..f226a48d 100644
--- a/internal/system/mkdir_test.go
+++ b/internal/system/mkdir_test.go
@@ -4,7 +4,7 @@ import (
"os"
"testing"
- "hakurei.app/container/stub"
+ "hakurei.app/internal/stub"
)
func TestMkdirOp(t *testing.T) {
diff --git a/internal/system/pipewire_test.go b/internal/system/pipewire_test.go
index a39237f8..052495cc 100644
--- a/internal/system/pipewire_test.go
+++ b/internal/system/pipewire_test.go
@@ -8,9 +8,9 @@ import (
"testing"
"time"
- "hakurei.app/container/stub"
"hakurei.app/internal/acl"
"hakurei.app/internal/pipewire"
+ "hakurei.app/internal/stub"
)
func TestPipeWireOp(t *testing.T) {
diff --git a/internal/system/system_test.go b/internal/system/system_test.go
index f82aa932..d0b1154e 100644
--- a/internal/system/system_test.go
+++ b/internal/system/system_test.go
@@ -9,8 +9,8 @@ import (
"testing"
"hakurei.app/check"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/xcb"
"hakurei.app/message"
)
diff --git a/internal/system/wayland_test.go b/internal/system/wayland_test.go
index 0fdaf818..76e6cc93 100644
--- a/internal/system/wayland_test.go
+++ b/internal/system/wayland_test.go
@@ -4,8 +4,8 @@ import (
"os"
"testing"
- "hakurei.app/container/stub"
"hakurei.app/internal/acl"
+ "hakurei.app/internal/stub"
)
func TestWaylandOp(t *testing.T) {
diff --git a/internal/system/xhost_test.go b/internal/system/xhost_test.go
index 72ecf2d6..b7a591d3 100644
--- a/internal/system/xhost_test.go
+++ b/internal/system/xhost_test.go
@@ -3,8 +3,8 @@ package system
import (
"testing"
- "hakurei.app/container/stub"
"hakurei.app/hst"
+ "hakurei.app/internal/stub"
"hakurei.app/internal/xcb"
)
diff --git a/internal/wayland/wayland_test.go b/internal/wayland/wayland_test.go
index 1db5f35d..8cc0a10e 100644
--- a/internal/wayland/wayland_test.go
+++ b/internal/wayland/wayland_test.go
@@ -7,7 +7,7 @@ import (
"syscall"
"testing"
- "hakurei.app/container/stub"
+ "hakurei.app/internal/stub"
)
func TestError(t *testing.T) {