Рыба проекта. Минимальная функциональность
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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 i := 0; i < 20; i++ {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user