diff --git a/container/container_test.go b/container/container_test.go index 8016aba..036d06e 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -20,7 +20,6 @@ import ( "hakurei.app/container/seccomp" "hakurei.app/container/vfs" "hakurei.app/hst" - "hakurei.app/internal/hlog" ) const ( @@ -207,20 +206,13 @@ var containerTestCases = []struct { } func TestContainer(t *testing.T) { - { - oldVerbose := hlog.Load() - oldOutput := container.GetOutput() - hlog.Store(testing.Verbose()) - container.SetOutput(hlog.Output{}) - t.Cleanup(func() { hlog.Store(oldVerbose) }) - t.Cleanup(func() { container.SetOutput(oldOutput) }) - } + replaceOutput(t) t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) { wantErr := context.Canceled wantExitCode := 0 if err := c.Wait(); !errors.Is(err, wantErr) { - hlog.PrintBaseError(err, "wait:") + container.GetOutput().PrintBaseErr(err, "wait:") t.Errorf("Wait: error = %v, want %v", err, wantErr) } if ps := c.ProcessState(); ps == nil { @@ -235,7 +227,7 @@ func TestContainer(t *testing.T) { }, func(t *testing.T, c *container.Container) { var exitError *exec.ExitError if err := c.Wait(); !errors.As(err, &exitError) { - hlog.PrintBaseError(err, "wait:") + container.GetOutput().PrintBaseErr(err, "wait:") t.Errorf("Wait: error = %v", err) } if code := exitError.ExitCode(); code != blockExitCodeInterrupt { @@ -315,16 +307,16 @@ func TestContainer(t *testing.T) { if err := c.Start(); err != nil { _, _ = output.WriteTo(os.Stdout) - hlog.PrintBaseError(err, "start:") + container.GetOutput().PrintBaseErr(err, "start:") t.Fatalf("cannot start container: %v", err) } else if err = c.Serve(); err != nil { _, _ = output.WriteTo(os.Stdout) - hlog.PrintBaseError(err, "serve:") + container.GetOutput().PrintBaseErr(err, "serve:") t.Errorf("cannot serve setup params: %v", err) } if err := c.Wait(); err != nil { _, _ = output.WriteTo(os.Stdout) - hlog.PrintBaseError(err, "wait:") + container.GetOutput().PrintBaseErr(err, "wait:") t.Fatalf("wait: %v", err) } }) @@ -378,10 +370,10 @@ func testContainerCancel( } if err := c.Start(); err != nil { - hlog.PrintBaseError(err, "start:") + container.GetOutput().PrintBaseErr(err, "start:") t.Fatalf("cannot start container: %v", err) } else if err = c.Serve(); err != nil { - hlog.PrintBaseError(err, "serve:") + container.GetOutput().PrintBaseErr(err, "serve:") t.Errorf("cannot serve setup params: %v", err) } <-ready diff --git a/container/msg_test.go b/container/msg_test.go new file mode 100644 index 0000000..6945c89 --- /dev/null +++ b/container/msg_test.go @@ -0,0 +1,139 @@ +package container_test + +import ( + "log" + "strings" + "sync/atomic" + "syscall" + "testing" + + "hakurei.app/container" + "hakurei.app/internal/hlog" +) + +func TestDefaultMsg(t *testing.T) { + { + w := log.Writer() + f := log.Flags() + t.Cleanup(func() { log.SetOutput(w); log.SetFlags(f) }) + } + msg := new(container.DefaultMsg) + + t.Run("is verbose", func(t *testing.T) { + if !msg.IsVerbose() { + t.Error("IsVerbose unexpected outcome") + } + }) + + t.Run("verbose", func(t *testing.T) { + log.SetOutput(panicWriter{}) + msg.Suspend() + msg.Verbose() + msg.Verbosef("\x00") + msg.Resume() + + buf := new(strings.Builder) + log.SetOutput(buf) + log.SetFlags(0) + msg.Verbose() + msg.Verbosef("\x00") + + want := "\n\x00\n" + if buf.String() != want { + t.Errorf("Verbose: %q, want %q", buf.String(), want) + } + }) + + t.Run("wrapErr", func(t *testing.T) { + buf := new(strings.Builder) + log.SetOutput(buf) + log.SetFlags(0) + if err := msg.WrapErr(syscall.EBADE, "\x00", "\x00"); err != syscall.EBADE { + t.Errorf("WrapErr: %v", err) + } + msg.PrintBaseErr(syscall.ENOTRECOVERABLE, "cannot cuddle cat:") + + want := "\x00 \x00\ncannot cuddle cat: state not recoverable\n" + if buf.String() != want { + t.Errorf("WrapErr: %q, want %q", buf.String(), want) + } + }) + + t.Run("inactive", func(t *testing.T) { + { + inactive := msg.Resume() + if inactive { + t.Cleanup(func() { msg.Suspend() }) + } + } + + if msg.Resume() { + t.Error("Resume unexpected outcome") + } + + msg.Suspend() + if !msg.Resume() { + t.Error("Resume unexpected outcome") + } + }) + + // the function is a noop + t.Run("beforeExit", func(t *testing.T) { msg.BeforeExit() }) +} + +type panicWriter struct{} + +func (panicWriter) Write([]byte) (int, error) { panic("unreachable") } + +func saveRestoreOutput(t *testing.T) { + out := container.GetOutput() + t.Cleanup(func() { container.SetOutput(out) }) +} + +func replaceOutput(t *testing.T) { + saveRestoreOutput(t) + container.SetOutput(&testOutput{t: t}) +} + +type testOutput struct { + t *testing.T + suspended atomic.Bool +} + +func (out *testOutput) IsVerbose() bool { return testing.Verbose() } + +func (out *testOutput) Verbose(v ...any) { + if !out.IsVerbose() { + return + } + out.t.Log(v...) +} + +func (out *testOutput) Verbosef(format string, v ...any) { + if !out.IsVerbose() { + return + } + out.t.Logf(format, v...) +} + +func (out *testOutput) WrapErr(err error, a ...any) error { return hlog.WrapErr(err, a...) } +func (out *testOutput) PrintBaseErr(err error, fallback string) { hlog.PrintBaseError(err, fallback) } + +func (out *testOutput) Suspend() { + if out.suspended.CompareAndSwap(false, true) { + out.Verbose("suspend called") + return + } + out.Verbose("suspend called on suspended output") +} + +func (out *testOutput) Resume() bool { + if out.suspended.CompareAndSwap(true, false) { + out.Verbose("resume called") + return true + } + out.Verbose("resume called on unsuspended output") + return false +} + +func (out *testOutput) BeforeExit() { out.Verbose("beforeExit called") } diff --git a/container/output_test.go b/container/output_test.go new file mode 100644 index 0000000..3fb2ac7 --- /dev/null +++ b/container/output_test.go @@ -0,0 +1,110 @@ +package container + +import ( + "reflect" + "syscall" + "testing" +) + +func TestGetSetOutput(t *testing.T) { + { + out := GetOutput() + t.Cleanup(func() { SetOutput(out) }) + } + + t.Run("default", func(t *testing.T) { + SetOutput(new(stubOutput)) + if v, ok := GetOutput().(*DefaultMsg); ok { + t.Fatalf("SetOutput: got unexpected output %#v", v) + } + SetOutput(nil) + if _, ok := GetOutput().(*DefaultMsg); !ok { + t.Fatalf("SetOutput: got unexpected output %#v", GetOutput()) + } + }) + + t.Run("stub", func(t *testing.T) { + SetOutput(new(stubOutput)) + if _, ok := GetOutput().(*stubOutput); !ok { + t.Fatalf("SetOutput: got unexpected output %#v", GetOutput()) + } + }) +} + +func TestWrapErr(t *testing.T) { + { + out := GetOutput() + t.Cleanup(func() { SetOutput(out) }) + } + + var wrapFp *func(error, ...any) error + s := new(stubOutput) + SetOutput(s) + wrapFp = &s.wrapF + + testCases := []struct { + name string + f func(t *testing.T) + wantErr error + wantA []any + }{ + {"suffix nil", func(t *testing.T) { + if err := wrapErrSuffix(nil, "\x00"); err != nil { + t.Errorf("wrapErrSuffix: %v", err) + } + }, nil, nil}, + {"suffix val", func(t *testing.T) { + if err := wrapErrSuffix(syscall.ENOTRECOVERABLE, "\x00\x00"); err != syscall.ENOTRECOVERABLE { + t.Errorf("wrapErrSuffix: %v", err) + } + }, syscall.ENOTRECOVERABLE, []any{"\x00\x00", syscall.ENOTRECOVERABLE}}, + {"self nil", func(t *testing.T) { + if err := wrapErrSelf(nil); err != nil { + t.Errorf("wrapErrSelf: %v", err) + } + }, nil, nil}, + {"self val", func(t *testing.T) { + if err := wrapErrSelf(syscall.ENOTRECOVERABLE); err != syscall.ENOTRECOVERABLE { + t.Errorf("wrapErrSelf: %v", err) + } + }, syscall.ENOTRECOVERABLE, []any{"state not recoverable"}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var ( + gotErr error + gotA []any + ) + *wrapFp = func(err error, a ...any) error { gotErr = err; gotA = a; return err } + + tc.f(t) + if gotErr != tc.wantErr { + t.Errorf("WrapErr: err = %v, want %v", gotErr, tc.wantErr) + } + + if !reflect.DeepEqual(gotA, tc.wantA) { + t.Errorf("WrapErr: a = %v, want %v", gotA, tc.wantA) + } + }) + } +} + +type stubOutput struct { + wrapF func(error, ...any) error +} + +func (*stubOutput) IsVerbose() bool { panic("unreachable") } +func (*stubOutput) Verbose(...any) { panic("unreachable") } +func (*stubOutput) Verbosef(string, ...any) { panic("unreachable") } +func (*stubOutput) PrintBaseErr(error, string) { panic("unreachable") } +func (*stubOutput) Suspend() { panic("unreachable") } +func (*stubOutput) Resume() bool { panic("unreachable") } +func (*stubOutput) BeforeExit() { panic("unreachable") } + +func (s *stubOutput) WrapErr(err error, v ...any) error { + if s.wrapF == nil { + panic("unreachable") + } + return s.wrapF(err, v...) +}