Files

394 lines
8.2 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"testing"
"time"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/healthz"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain"
)
type fakeScheduler struct {
mu sync.Mutex
startCalled bool
stopCalled bool
stoppedCtx context.Context
stopCancel context.CancelFunc
}
func (f *fakeScheduler) Start() {
f.mu.Lock()
f.startCalled = true
f.mu.Unlock()
}
func (f *fakeScheduler) Stop() context.Context {
f.mu.Lock()
f.stopCalled = true
f.mu.Unlock()
return f.stoppedCtx
}
func (f *fakeScheduler) wasStarted() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.startCalled
}
func (f *fakeScheduler) wasStopped() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.stopCalled
}
type exitPanic int
func (e exitPanic) Error() string { return fmt.Sprintf("exit %d", int(e)) }
func TestRunCmd_Use(t *testing.T) {
if runCmd.Use != "run" {
t.Errorf("runCmd.Use = %q, want %q", runCmd.Use, "run")
}
}
func TestRunCmd_SIGTERM_stopsScheduler(t *testing.T) {
origNewScheduler := newScheduler
origOsExit := osExitFunc
origRunOnce := runOnceFunc
defer func() {
newScheduler = origNewScheduler
osExitFunc = origOsExit
runOnceFunc = origRunOnce
}()
fake := &fakeScheduler{}
fake.stoppedCtx, fake.stopCancel = context.WithCancel(context.Background())
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
return fake, nil
}
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
return nil
}
var exitCode int
exitPanicked := false
osExitFunc = func(code int) {
exitCode = code
exitPanicked = true
panic(exitPanic(code))
}
cfg := &config.Config{}
cfg.Healthz.Port = 0
cfg.Backup.Cron = "* * * * *"
cfg.ShutdownTimeout = 100 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(exitPanic); ok {
errCh <- nil
return
}
errCh <- fmt.Errorf("unexpected panic: %v", r)
return
}
}()
errCh <- runWithConfig(ctx, cfg)
}()
time.Sleep(50 * time.Millisecond)
if !fake.wasStarted() {
t.Fatal("scheduler.Start was not called")
}
cancel()
go func() {
time.Sleep(50 * time.Millisecond)
fake.stopCancel()
}()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("runWithConfig returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for runWithConfig to exit")
}
if !fake.wasStopped() {
t.Error("scheduler.Stop was not called")
}
if exitPanicked {
t.Errorf("unexpected os.Exit call with code %d", exitCode)
}
}
func TestRunCmd_SIGTERM_forcesExitAfterTimeout(t *testing.T) {
origNewScheduler := newScheduler
origOsExit := osExitFunc
origNewHealthz := newHealthz
origRunOnce := runOnceFunc
defer func() {
newScheduler = origNewScheduler
osExitFunc = origOsExit
newHealthz = origNewHealthz
runOnceFunc = origRunOnce
}()
fake := &fakeScheduler{}
var cancelFunc context.CancelFunc
fake.stoppedCtx, cancelFunc = context.WithCancel(context.Background())
defer cancelFunc()
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
return fake, nil
}
newHealthz = func(port int) (healthz.Server, error) {
return healthz.New(port)
}
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
return nil
}
var exitCode int
exitPanicked := false
osExitFunc = func(code int) {
exitCode = code
exitPanicked = true
panic(exitPanic(code))
}
cfg := &config.Config{}
cfg.Healthz.Port = 0
cfg.Backup.Cron = "* * * * * *"
cfg.ShutdownTimeout = 100 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(exitPanic); ok {
errCh <- nil
return
}
errCh <- fmt.Errorf("unexpected panic: %v", r)
return
}
}()
errCh <- runWithConfig(ctx, cfg)
}()
time.Sleep(50 * time.Millisecond)
cancel()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("runWithConfig returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for runWithConfig to exit")
}
if !fake.wasStopped() {
t.Error("scheduler.Stop was not called")
}
if !exitPanicked {
t.Fatal("expected os.Exit to be called")
}
if exitCode != 0 {
t.Errorf("os.Exit code = %d, want 0", exitCode)
}
}
func TestRunCmd_Healthz503DuringShutdown(t *testing.T) {
origNewScheduler := newScheduler
origOsExit := osExitFunc
origNewHealthz := newHealthz
origRunOnce := runOnceFunc
defer func() {
newScheduler = origNewScheduler
osExitFunc = origOsExit
newHealthz = origNewHealthz
runOnceFunc = origRunOnce
}()
fake := &fakeScheduler{}
fake.stoppedCtx, fake.stopCancel = context.WithCancel(context.Background())
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
return fake, nil
}
srv, err := healthz.New(0)
if err != nil {
t.Fatalf("failed to create healthz server: %v", err)
}
newHealthz = func(port int) (healthz.Server, error) {
return srv, nil
}
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
return nil
}
osExitFunc = func(code int) { panic(exitPanic(code)) }
cfg := &config.Config{}
cfg.Healthz.Port = 0
cfg.Backup.Cron = "* * * * * *"
cfg.ShutdownTimeout = 5 * time.Second
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(exitPanic); ok {
errCh <- nil
return
}
errCh <- fmt.Errorf("unexpected panic: %v", r)
return
}
}()
errCh <- runWithConfig(ctx, cfg)
}()
time.Sleep(100 * time.Millisecond)
addr := srv.Addr()
resp, err := http.Get("http://" + addr + "/healthz")
if err != nil {
t.Fatalf("healthz request failed: %v", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("healthz status before shutdown = %d, want %d", resp.StatusCode, http.StatusOK)
}
cancel()
var status503 bool
for iteration := 0; iteration < 20; iteration++ {
resp, err = http.Get("http://" + addr + "/healthz")
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusServiceUnavailable {
status503 = true
break
}
}
time.Sleep(10 * time.Millisecond)
}
if !status503 {
t.Error("did not observe /healthz returning 503 during shutdown")
}
fake.stopCancel()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("runWithConfig returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for runWithConfig to exit")
}
}
func TestRunCmd_HealthzBindFailure(t *testing.T) {
origNewScheduler := newScheduler
origNewHealthz := newHealthz
origOsExit := osExitFunc
origRunOnce := runOnceFunc
defer func() {
newScheduler = origNewScheduler
newHealthz = origNewHealthz
osExitFunc = origOsExit
runOnceFunc = origRunOnce
}()
fake := &fakeScheduler{}
var cancelFunc context.CancelFunc
fake.stoppedCtx, cancelFunc = context.WithCancel(context.Background())
defer cancelFunc()
newScheduler = func(expr string, job func()) (domain.Scheduler, error) {
return fake, nil
}
newHealthz = func(port int) (healthz.Server, error) {
return nil, errors.New("bind failed")
}
runOnceFunc = func(ctx context.Context, cfg *config.Config) error {
return nil
}
var exitCode int
exitPanicked := false
osExitFunc = func(code int) {
exitCode = code
exitPanicked = true
panic(exitPanic(code))
}
cfg := &config.Config{}
cfg.Backup.Cron = "* * * * * *"
errCh := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(exitPanic); ok {
errCh <- nil
return
}
errCh <- fmt.Errorf("unexpected panic: %v", r)
return
}
}()
errCh <- runWithConfig(context.Background(), cfg)
}()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("runWithConfig returned error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for runWithConfig to exit")
}
if !exitPanicked {
t.Fatal("expected os.Exit to be called on healthz bind failure")
}
if exitCode != 1 {
t.Errorf("os.Exit code = %d, want 1", exitCode)
}
}