feat: spool gateway publishes to disk
This commit is contained in:
153
go/vehicle-gateway/internal/eventbus/durable_sink_test.go
Normal file
153
go/vehicle-gateway/internal/eventbus/durable_sink_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
||||
)
|
||||
|
||||
func TestDurableSinkSpoolsUnifiedWhenRawWasSpooled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
delegate := &scriptedSink{rawErrors: []error{errSpoolTest}}
|
||||
sink := NewDurableSink(delegate, DurableConfig{Directory: dir})
|
||||
env := durableTestEnvelope()
|
||||
|
||||
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
||||
t.Fatalf("PublishRaw() error = %v", err)
|
||||
}
|
||||
if err := sink.PublishUnified(context.Background(), env); err != nil {
|
||||
t.Fatalf("PublishUnified() error = %v", err)
|
||||
}
|
||||
|
||||
if delegate.rawCalls != 1 {
|
||||
t.Fatalf("raw calls = %d, want 1", delegate.rawCalls)
|
||||
}
|
||||
if delegate.unifiedCalls != 0 {
|
||||
t.Fatalf("unified should not be delegated while raw is spooled, calls = %d", delegate.unifiedCalls)
|
||||
}
|
||||
files := spoolFiles(t, dir)
|
||||
if len(files) != 2 {
|
||||
t.Fatalf("spool files = %d, want 2: %#v", len(files), files)
|
||||
}
|
||||
assertSpoolKind(t, files[0], "raw")
|
||||
assertSpoolKind(t, files[1], "unified")
|
||||
}
|
||||
|
||||
func TestDurableSinkReplayPublishesInFileOrderAndDeletesFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
delegate := &scriptedSink{rawErrors: []error{errSpoolTest}}
|
||||
sink := NewDurableSink(delegate, DurableConfig{Directory: dir})
|
||||
env := durableTestEnvelope()
|
||||
|
||||
if err := sink.PublishRaw(context.Background(), env); err != nil {
|
||||
t.Fatalf("PublishRaw() error = %v", err)
|
||||
}
|
||||
if err := sink.PublishUnified(context.Background(), env); err != nil {
|
||||
t.Fatalf("PublishUnified() error = %v", err)
|
||||
}
|
||||
delegate.rawErrors = nil
|
||||
|
||||
if err := sink.ReplayOnce(context.Background()); err != nil {
|
||||
t.Fatalf("ReplayOnce() error = %v", err)
|
||||
}
|
||||
|
||||
got := delegate.calls
|
||||
want := []string{"raw", "raw", "unified"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("calls = %#v, want %#v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("calls[%d] = %q, want %q; all=%#v", i, got[i], want[i], got)
|
||||
}
|
||||
}
|
||||
if files := spoolFiles(t, dir); len(files) != 0 {
|
||||
t.Fatalf("spool files after replay = %#v, want none", files)
|
||||
}
|
||||
}
|
||||
|
||||
func durableTestEnvelope() envelope.FrameEnvelope {
|
||||
return envelope.FrameEnvelope{
|
||||
Protocol: envelope.ProtocolJT808,
|
||||
MessageID: "0x0200",
|
||||
Phone: "013079963379",
|
||||
VIN: "LKLG7C4E3NA774736",
|
||||
Sequence: 183,
|
||||
EventTimeMS: 1782903940000,
|
||||
ReceivedAtMS: 1782917751903,
|
||||
RawHex: "0200002201307996337900B7",
|
||||
}
|
||||
}
|
||||
|
||||
func spoolFiles(t *testing.T, dir string) []string {
|
||||
t.Helper()
|
||||
matches, err := filepath.Glob(filepath.Join(dir, "*.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob spool files: %v", err)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func assertSpoolKind(t *testing.T, path string, kind string) {
|
||||
t.Helper()
|
||||
payload, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read spool file: %v", err)
|
||||
}
|
||||
if !containsString(string(payload), `"kind":"`+kind+`"`) {
|
||||
t.Fatalf("spool file %s payload = %s, want kind %q", path, payload, kind)
|
||||
}
|
||||
}
|
||||
|
||||
func containsString(value string, needle string) bool {
|
||||
return len(needle) == 0 || (len(value) >= len(needle) && indexString(value, needle) >= 0)
|
||||
}
|
||||
|
||||
func indexString(value string, needle string) int {
|
||||
for i := 0; i+len(needle) <= len(value); i++ {
|
||||
if value[i:i+len(needle)] == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
var errSpoolTest = errors.New("delegate unavailable")
|
||||
|
||||
type scriptedSink struct {
|
||||
rawErrors []error
|
||||
unifiedErrors []error
|
||||
rawCalls int
|
||||
unifiedCalls int
|
||||
calls []string
|
||||
}
|
||||
|
||||
func (s *scriptedSink) PublishRaw(context.Context, envelope.FrameEnvelope) error {
|
||||
s.rawCalls++
|
||||
s.calls = append(s.calls, "raw")
|
||||
if len(s.rawErrors) > 0 {
|
||||
err := s.rawErrors[0]
|
||||
s.rawErrors = s.rawErrors[1:]
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scriptedSink) PublishUnified(context.Context, envelope.FrameEnvelope) error {
|
||||
s.unifiedCalls++
|
||||
s.calls = append(s.calls, "unified")
|
||||
if len(s.unifiedErrors) > 0 {
|
||||
err := s.unifiedErrors[0]
|
||||
s.unifiedErrors = s.unifiedErrors[1:]
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scriptedSink) Close() error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user