1package app
2
3import (
4 "bufio"
5 "encoding/json"
6 "errors"
7 "io"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/bnema/vev/internal/ports"
16 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17 "github.com/stretchr/testify/mock"
18)
19
20func TestPerformanceTraceUsesHarnessProcessMapping(t *testing.T) {
21 path := filepath.Join(t.TempDir(), "process.jsonl")
22 t.Setenv("VEV_PERF_TRACE", path)
23 t.Setenv("VEV_PERF_PROCESS_ID", "idle-local-r001-daemon-01")
24 t.Setenv("VEV_PERF_SCENARIO", "1x4-idle-local")
25 t.Setenv("VEV_PERF_RUN", "1")
26
27 clock := portsmocks.NewMockClock(t)
28 clock.EXPECT().Now().Return(time.Unix(0, 1)).Once()
29 observer, closer, err := performanceTrace(clock)
30 if err != nil {
31 t.Fatal(err)
32 }
33 if observer == nil || closer == nil {
34 t.Fatal("performance trace was not configured")
35 }
36 // performanceTrace's serialized return type makes a raw JSONL sink
37 // unrepresentable to application transport wiring.
38 observer.ObserveRuntime(ports.RuntimeMark{Schema: ports.RuntimeMarkSchema, Component: "daemon", Scenario: "runtime", Run: 1, Kind: ports.RuntimeEmitEnd, Valid: true})
39 if err := closer.Close(); err != nil {
40 t.Fatal(err)
41 }
42 if err := closer.Close(); err != nil {
43 t.Fatal(err)
44 }
45 f, err := os.Open(path)
46 if err != nil {
47 t.Fatal(err)
48 }
49 defer func() {
50 if err := f.Close(); err != nil {
51 t.Errorf("close trace file: %v", err)
52 }
53 }()
54 var mark struct {
55 ProcessID string `json:"process_id"`
56 Scenario string `json:"scenario"`
57 Run uint64 `json:"run"`
58 Sequence uint64 `json:"sequence"`
59 RequestID uint64 `json:"request_id"`
60 Epoch uint64 `json:"epoch"`
61 }
62 if err := json.NewDecoder(bufio.NewReader(f)).Decode(&mark); err != nil {
63 t.Fatal(err)
64 }
65 if mark.ProcessID != "idle-local-r001-daemon-01" || mark.Scenario != "1x4-idle-local" || mark.Run != 1 || mark.Sequence == 0 || mark.RequestID == 0 || mark.Epoch == 0 {
66 t.Fatalf("mark does not match harness mapping: %+v", mark)
67 }
68}
69
70func TestPerformanceTraceWithFactoriesCreatesOneConfiguredObserver(t *testing.T) {
71 t.Setenv("VEV_PERF_TRACE", "trace.jsonl")
72 t.Setenv("VEV_PERF_PROCESS_ID", "client-01")
73 t.Setenv("VEV_PERF_SCENARIO", "lossy-remote")
74 t.Setenv("VEV_PERF_RUN", "7")
75
76 clock := portsmocks.NewMockClock(t)
77 sink := portsmocks.NewMockRuntimeObserver(t)
78 sink.EXPECT().ObserveRuntime(mock.MatchedBy(func(mark ports.RuntimeMark) bool {
79 return mark.Component == "client" && mark.Scenario == "lossy-remote" && mark.Run == 7 &&
80 mark.Sequence != 0 && mark.RequestID != 0 && mark.Epoch != 0 && mark.Kind == ports.RuntimeEmitEnd
81 })).Once()
82
83 var sinkCalls, correlationCalls int
84 observer, closer, err := performanceTraceWithFactories(clock,
85 func(path string, gotClock ports.Clock, processID string) (ports.RuntimeObserver, io.Closer, error) {
86 sinkCalls++
87 if path != "trace.jsonl" || gotClock != clock || processID != "client-01" {
88 t.Fatalf("trace sink config = (%q, %v, %q)", path, gotClock, processID)
89 }
90 return sink, errorCloser{}, nil
91 },
92 func(got ports.RuntimeObserver, inputs ports.RuntimeCorrelationInputs) (ports.RuntimeObserver, error) {
93 correlationCalls++
94 if got != sink || inputs != (ports.RuntimeCorrelationInputs{Scenario: "lossy-remote", Run: 7}) {
95 t.Fatalf("trace correlation config = (%v, %+v)", got, inputs)
96 }
97 return ports.NewRuntimeCorrelationObserver(got, inputs)
98 },
99 )
100 if err != nil {
101 t.Fatal(err)
102 }
103 if sinkCalls != 1 || correlationCalls != 1 || observer == nil || closer == nil {
104 t.Fatalf("trace setup = sink calls %d, correlation calls %d, observer %v, closer %v; want one configured observer", sinkCalls, correlationCalls, observer, closer)
105 }
106 observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeEmitEnd, 0, true))
107 if err := closer.Close(); err != nil {
108 t.Fatal(err)
109 }
110}
111
112func TestPerformanceTraceSetupRollbackJoinsCloseError(t *testing.T) {
113 setupErr := errors.New("correlation setup failed")
114 closeErr := errors.New("rollback close failed")
115
116 tests := []struct {
117 name string
118 rawRun string
119 correlationErr error
120 wantPrimaryText string
121 wantPrimaryErr error
122 wantNumError bool
123 }{
124 {
125 name: "validation",
126 rawRun: "not-a-run",
127 wantPrimaryText: `invalid VEV_PERF_RUN "not-a-run"`,
128 wantNumError: true,
129 },
130 {
131 name: "correlation",
132 rawRun: "1",
133 correlationErr: setupErr,
134 wantPrimaryText: "runtime trace correlation",
135 wantPrimaryErr: setupErr,
136 },
137 }
138
139 for _, tc := range tests {
140 t.Run(tc.name, func(t *testing.T) {
141 t.Setenv("VEV_PERF_TRACE", "unused-by-test-sink")
142 t.Setenv("VEV_PERF_PROCESS_ID", "test-process")
143 t.Setenv("VEV_PERF_RUN", tc.rawRun)
144
145 newSink := func(string, ports.Clock, string) (ports.RuntimeObserver, io.Closer, error) {
146 return runtimeObserverFunc(func(ports.RuntimeMark) {}), errorCloser{err: closeErr}, nil
147 }
148 newCorrelation := func(observer ports.RuntimeObserver, inputs ports.RuntimeCorrelationInputs) (ports.RuntimeObserver, error) {
149 if tc.correlationErr != nil {
150 return nil, tc.correlationErr
151 }
152 return ports.NewRuntimeCorrelationObserver(observer, inputs)
153 }
154
155 observer, closer, err := performanceTraceWithFactories(nil, newSink, newCorrelation)
156 if observer != nil || closer != nil {
157 t.Fatalf("performanceTrace() returned observer=%v closer=%v after setup failure", observer, closer)
158 }
159 if !errors.Is(err, closeErr) {
160 t.Errorf("performanceTrace() error = %v, want joined rollback error %v", err, closeErr)
161 }
162 if tc.wantPrimaryErr != nil && !errors.Is(err, tc.wantPrimaryErr) {
163 t.Errorf("performanceTrace() error = %v, want primary setup error %v", err, tc.wantPrimaryErr)
164 }
165 if tc.wantNumError {
166 var numErr *strconv.NumError
167 if !errors.As(err, &numErr) {
168 t.Errorf("performanceTrace() error = %v, want discoverable parse error", err)
169 }
170 }
171 if !strings.Contains(err.Error(), tc.wantPrimaryText) {
172 t.Errorf("performanceTrace() error = %q, want context %q", err, tc.wantPrimaryText)
173 }
174 })
175 }
176}
177
178type errorCloser struct{ err error }
179
180func (c errorCloser) Close() error { return c.err }
181
182type runtimeObserverFunc func(ports.RuntimeMark)
183
184func (f runtimeObserverFunc) ObserveRuntime(mark ports.RuntimeMark) { f(mark) }