127 lines
3.4 KiB
Go
127 lines
3.4 KiB
Go
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
|
||
|
|
}
|
||
|
|
}
|