FILE / ScuroNeko/Laniakea

runners_test.go

Исходный файл и его история в репозитории.
FILE 24040fe164649c3e4a7ef2f9d37b9747957b0cc1
Files
Laniakea/runners_test.go
T
ScuroNeko 29b208eeec
Golang lint / lint (push) Successful in 11m32s
(new): v1.2 release
2026-08-19 14:58:25 +03:00

156 lines
3.6 KiB
Go

package laniakea
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
"git.scuroneko.dev/scuroneko/sneklog/v2"
)
type runnerObserver struct {
recordingObserver
}
func TestExecRunnersRunsOnceSyncRunner(t *testing.T) {
var calls atomic.Int32
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
runners: []Runner[NoData]{
NewRunner("sync-once", func(*Bot[NoData]) error {
calls.Add(1)
return nil
}).Async(false),
},
}
bot.ExecRunners(context.Background())
if got := calls.Load(); got != 1 {
t.Fatalf("unexpected sync runner call count: %d", got)
}
}
func TestExecRunnersStopsBackgroundRunnerOnCancel(t *testing.T) {
var calls atomic.Int32
triggered := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
runners: []Runner[NoData]{
NewRunner("background", func(*Bot[NoData]) error {
if calls.Add(1) == 1 {
triggered <- struct{}{}
}
return nil
}).Every(5 * time.Millisecond),
},
}
bot.ExecRunners(ctx)
select {
case <-triggered:
case <-time.After(time.Second):
t.Fatal("background runner did not execute")
}
cancel()
bot.runnerBgWG.Wait()
if calls.Load() == 0 {
t.Fatal("expected background runner to be called at least once")
}
}
func TestExecRunnersEmitObserverEvents(t *testing.T) {
observer := &runnerObserver{}
wantErr := errors.New("runner failed")
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
observer: observer,
runners: []Runner[NoData]{
NewRunner("sync-once", func(*Bot[NoData]) error {
return wantErr
}).Async(false),
},
}
bot.ExecRunners(context.Background())
if len(observer.runners) != 1 {
t.Fatalf("expected one runner-finished event, got %d", len(observer.runners))
}
if got := observer.runners[0]; got.Name != "sync-once" || !errors.Is(got.Err, wantErr) {
t.Fatalf("unexpected runner-finished event: %#v", got)
}
if len(observer.errors) != 1 {
t.Fatalf("expected one error event, got %d", len(observer.errors))
}
if got := observer.errors[0]; got.HandlerKind != HandlerRunnerKind || got.HandlerName != "sync-once" || !errors.Is(got.Err, wantErr) {
t.Fatalf("unexpected runner error event: %#v", got)
}
}
func TestExecRunnersRecoversRunnerPanic(t *testing.T) {
observer := &runnerObserver{}
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
observer: observer,
runners: []Runner[NoData]{
NewRunner("panic", func(*Bot[NoData]) error {
panic("boom")
}).Async(false),
},
}
defer func() {
if err := bot.logger.Close(); err != nil {
t.Fatalf("Close returned error: %v", err)
}
}()
bot.ExecRunners(context.Background())
if len(observer.runners) != 1 || observer.runners[0].Err == nil {
t.Fatalf("expected recovered panic in runner event, got %#v", observer.runners)
}
if len(observer.errors) != 1 || observer.errors[0].Err == nil {
t.Fatalf("expected recovered panic in error event, got %#v", observer.errors)
}
}
func TestContextRunnerStopsActiveCallbackOnCancel(t *testing.T) {
started := make(chan struct{})
stopped := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
bot := &Bot[NoData]{
logger: sneklog.NewLogger(),
runners: []Runner[NoData]{
NewContextRunner("cancelable", func(ctx context.Context, _ *Bot[NoData]) error {
close(started)
<-ctx.Done()
close(stopped)
return ctx.Err()
}),
},
}
bot.ExecRunners(ctx)
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("context runner did not start")
}
cancel()
bot.runnerOnceWG.Wait()
select {
case <-stopped:
default:
t.Fatal("context runner did not observe cancellation")
}
}