104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package monitor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"silverpose/v2/internal/alert"
|
|
"silverpose/v2/internal/fall"
|
|
"silverpose/v2/internal/pose"
|
|
"silverpose/v2/internal/source"
|
|
)
|
|
|
|
func TestMonitorPublishesLatestCompletedFrameAndOneAlert(t *testing.T) {
|
|
frames := make(chan source.Frame, 2)
|
|
frames <- source.Frame{BGR: []byte{1, 1, 1, 1, 1, 1}, Width: 2, Height: 1, Sequence: 1}
|
|
frames <- source.Frame{BGR: []byte{2, 2, 2, 2, 2, 2}, Width: 2, Height: 1, Sequence: 2}
|
|
close(frames)
|
|
statuses := make(chan source.Update, 1)
|
|
statuses <- source.Update{Status: source.Connected, Message: "视频流已连接"}
|
|
close(statuses)
|
|
processor := &fakeProcessor{result: fall.FrameResult{
|
|
People: []fall.PersonAnalysis{{State: fall.Confirmed}},
|
|
Events: []fall.Event{{EventID: "FALL-run-000001", TrackID: "P-0001", ConfigVersion: "cfg-safe", State: fall.Confirmed}},
|
|
}}
|
|
runtime := &fakeRuntime{output: make([]float32, pose.YOLOPoseOutputValues)}
|
|
monitor, err := New(fakeSource{frames: frames, statuses: statuses}, runtime, processor, 0.25, alert.NewDispatcher(t.TempDir(), "lobby"), func() time.Time {
|
|
return time.Date(2026, 7, 22, 8, 0, 0, 0, time.UTC)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go monitor.Run(context.Background())
|
|
<-monitor.Done()
|
|
|
|
update, ok := <-monitor.Updates()
|
|
if !ok {
|
|
t.Fatal("monitor emitted no update")
|
|
}
|
|
if update.Sequence != 2 || update.Image == nil || update.SourceStatus != source.Connected {
|
|
t.Fatalf("update = %#v", update)
|
|
}
|
|
record, ok := <-monitor.Alerts()
|
|
if !ok || !record.Written || record.Event.EventID != "FALL-run-000001" {
|
|
t.Fatalf("alert record = %#v, open = %t", record, ok)
|
|
}
|
|
if !runtime.closed {
|
|
t.Fatal("runtime was not closed")
|
|
}
|
|
}
|
|
|
|
func TestMonitorDoesNotCreateFallEvidenceOnSourceFailure(t *testing.T) {
|
|
frames := make(chan source.Frame)
|
|
close(frames)
|
|
statuses := make(chan source.Update, 1)
|
|
statuses <- source.Update{Status: source.Retrying, Message: "视频流已断开,正在重连"}
|
|
close(statuses)
|
|
processor := &fakeProcessor{}
|
|
monitor, err := New(fakeSource{frames: frames, statuses: statuses}, &fakeRuntime{}, processor, 0.25, alert.NewDispatcher(t.TempDir(), "lobby"), time.Now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go monitor.Run(context.Background())
|
|
<-monitor.Done()
|
|
|
|
update := <-monitor.Updates()
|
|
if update.SourceStatus != source.Retrying || len(update.Events) != 0 || processor.calls != 0 {
|
|
t.Fatalf("update = %#v, process calls = %d", update, processor.calls)
|
|
}
|
|
}
|
|
|
|
type fakeSource struct {
|
|
frames <-chan source.Frame
|
|
statuses <-chan source.Update
|
|
}
|
|
|
|
func (source fakeSource) Frames() <-chan source.Frame { return source.frames }
|
|
func (source fakeSource) Statuses() <-chan source.Update { return source.statuses }
|
|
|
|
type fakeRuntime struct {
|
|
output []float32
|
|
err error
|
|
closed bool
|
|
}
|
|
|
|
func (runtime *fakeRuntime) Run(input []float32) ([]float32, error) {
|
|
if len(input) == 0 {
|
|
return nil, errors.New("missing input")
|
|
}
|
|
return runtime.output, runtime.err
|
|
}
|
|
func (runtime *fakeRuntime) Close() { runtime.closed = true }
|
|
|
|
type fakeProcessor struct {
|
|
result fall.FrameResult
|
|
calls int
|
|
}
|
|
|
|
func (processor *fakeProcessor) Process(frame fall.Frame) fall.FrameResult {
|
|
processor.calls++
|
|
return processor.result
|
|
}
|