package app import ( "context" "errors" "io" "net" "net/http" "sync" "sync/atomic" "testing" "time" "git.yuntuo.hk/k2safe/go-omni/config" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/lifecycle" ) func TestFatalShutdownDrainsAndPreservesCause(t *testing.T) { for _, transport := range []string{config.TransportNetHTTP, config.TransportNative} { t.Run(transport, func(t *testing.T) { app := New(config.WithTransport(transport), config.WithShutdownTimeout(2*time.Second)) control := lifecycle.New(context.Background()) defer control.RequestShutdown(nil) entered, release := make(chan struct{}), make(chan struct{}) var releaseOnce sync.Once unblock := func() { releaseOnce.Do(func() { close(release) }) } defer unblock() var closed atomic.Int32 closeErr := errors.New("cleanup diagnostic") app.OnShutdown(func() error { closed.Add(1); return closeErr }) app.GET("/work", func(c *httpx.Context) error { close(entered) <-release if closed.Load() != 0 { return errors.New("closed during request") } return c.OK("done") }) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- app.ServeContext(control.Context(), listener) }() requestDone := make(chan error, 1) client := http.Client{Timeout: 3 * time.Second} go func() { res, err := client.Get("http://" + listener.Addr().String() + "/work") if err == nil { _, err = io.Copy(io.Discard, res.Body) _ = res.Body.Close() if res.StatusCode != 200 { err = errors.New("request failed") } } requestDone <- err }() select { case <-entered: case <-time.After(3 * time.Second): t.Fatal("request never started") } fatal := errors.New("lease lost") control.RequestShutdown(fatal) select { case err := <-done: t.Fatal("did not drain", err) case <-time.After(30 * time.Millisecond): } if closed.Load() != 0 { t.Fatal("closed dependency before drain") } unblock() select { case err := <-requestDone: if err != nil { t.Fatal(err) } case <-time.After(3 * time.Second): t.Fatal("request stuck") } select { case err := <-done: if !errors.Is(err, fatal) || !errors.Is(err, closeErr) { t.Fatal("lost cause", err) } case <-time.After(3 * time.Second): t.Fatal("shutdown stuck") } _ = app.Close() if closed.Load() != 1 { t.Fatal("cleanup repeated") } }) } } func TestCanceledStartupPreservesFault(t *testing.T) { fatal := errors.New("dependency failed during startup") ctx, cancel := context.WithCancelCause(context.Background()) cancel(fatal) app := New() if err := app.RunContext(ctx); !errors.Is(err, fatal) { t.Fatal(err) } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } if err = New().ServeContext(ctx, listener); !errors.Is(err, fatal) { t.Fatal(err) } normal, stop := context.WithCancel(context.Background()) stop() if err = New().RunContext(normal); err != nil { t.Fatal("normal stop failed", err) } }