transport_observability_test.go RawHistoryFolder

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) }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close