feat(sense): establish M1 offline intake skeleton
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FakeScenario struct {
|
||||
Result ProbeResult `json:"result"`
|
||||
ProbeError ErrorCode `json:"probe_error,omitempty"`
|
||||
ClockError ErrorCode `json:"clock_error,omitempty"`
|
||||
DelayMillis int `json:"delay_millis,omitempty"`
|
||||
}
|
||||
|
||||
type fakeFixture struct {
|
||||
Scenarios map[string]FakeScenario `json:"scenarios"`
|
||||
}
|
||||
|
||||
// Fake is deterministic and intended only for tests and offline development.
|
||||
type Fake struct {
|
||||
mu sync.Mutex
|
||||
scenarios map[string]FakeScenario
|
||||
probeCalls map[string]int
|
||||
clockSyncCalls map[string]int
|
||||
}
|
||||
|
||||
func NewFake(scenarios map[string]FakeScenario) *Fake {
|
||||
copyOfScenarios := make(map[string]FakeScenario, len(scenarios))
|
||||
for key, value := range scenarios {
|
||||
copyOfScenarios[key] = value
|
||||
}
|
||||
return &Fake{
|
||||
scenarios: copyOfScenarios, probeCalls: make(map[string]int), clockSyncCalls: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
func LoadFakeFixture(path string) (*Fake, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read ONVIF fixture: %w", err)
|
||||
}
|
||||
var fixture fakeFixture
|
||||
if err := json.Unmarshal(data, &fixture); err != nil {
|
||||
return nil, fmt.Errorf("decode ONVIF fixture: %w", err)
|
||||
}
|
||||
return NewFake(fixture.Scenarios), nil
|
||||
}
|
||||
|
||||
func (f *Fake) Probe(ctx context.Context, target Target) (ProbeResult, error) {
|
||||
scenario, ok := f.scenario(target.EndpointRef)
|
||||
if !ok {
|
||||
return ProbeResult{}, &Error{Code: ErrorUnavailable, Err: fmt.Errorf("fixture endpoint is not configured")}
|
||||
}
|
||||
if err := waitForFakeDelay(ctx, scenario.DelayMillis); err != nil {
|
||||
return ProbeResult{}, &Error{Code: ErrorTimeout, Err: err}
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.probeCalls[target.EndpointRef]++
|
||||
f.mu.Unlock()
|
||||
if scenario.ProbeError != "" {
|
||||
return ProbeResult{}, &Error{Code: scenario.ProbeError, Err: fmt.Errorf("fixture probe failure")}
|
||||
}
|
||||
if scenario.Result.StreamURI == "" || len(scenario.Result.Profiles) == 0 {
|
||||
return ProbeResult{}, &Error{Code: ErrorInvalidReply, Err: fmt.Errorf("fixture lacks profile or stream URI")}
|
||||
}
|
||||
return scenario.Result, nil
|
||||
}
|
||||
|
||||
func (f *Fake) SetSystemDateAndTime(ctx context.Context, target Target, _ time.Time) error {
|
||||
scenario, ok := f.scenario(target.EndpointRef)
|
||||
if !ok {
|
||||
return &Error{Code: ErrorUnavailable, Err: fmt.Errorf("fixture endpoint is not configured")}
|
||||
}
|
||||
if err := waitForFakeDelay(ctx, scenario.DelayMillis); err != nil {
|
||||
return &Error{Code: ErrorTimeout, Err: err}
|
||||
}
|
||||
f.mu.Lock()
|
||||
f.clockSyncCalls[target.EndpointRef]++
|
||||
f.mu.Unlock()
|
||||
if scenario.ClockError != "" {
|
||||
return &Error{Code: scenario.ClockError, Err: fmt.Errorf("fixture clock failure")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fake) ProbeCalls(endpointRef string) int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.probeCalls[endpointRef]
|
||||
}
|
||||
|
||||
func (f *Fake) ClockSyncCalls(endpointRef string) int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.clockSyncCalls[endpointRef]
|
||||
}
|
||||
|
||||
func (f *Fake) scenario(endpointRef string) (FakeScenario, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
scenario, ok := f.scenarios[endpointRef]
|
||||
return scenario, ok
|
||||
}
|
||||
|
||||
func waitForFakeDelay(ctx context.Context, milliseconds int) error {
|
||||
if milliseconds <= 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
timer := time.NewTimer(time.Duration(milliseconds) * time.Millisecond)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user