replay.go RawHistoryFolder

1// Package replaytest owns the protocol-neutral transport replay fixture and
2// assertions shared by transport adapter tests. The transcript is a typed
3// Output conversation carried as serialized Protobuf envelopes; adapters
4// prove they preserve it byte-for-byte. The transcript stays value-only:
5// envelopes are composed from generated wire types here, never through a
6// concrete adapter.
7package replaytest
8
9import (
10	"bytes"
11	"errors"
12	"fmt"
13	"reflect"
14	"testing"
15
16	"google.golang.org/protobuf/proto"
17
18	"github.com/bnema/vev/internal/domain"
19	"github.com/bnema/vev/internal/protocol"
20	"github.com/bnema/vev/internal/protocol/wire"
21)
22
23func mustEncodeOutput(m protocol.Output) []byte {
24	m.Context = &protocol.ViewContext{
25		Publication: m.New,
26		Route:       protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "replay"}},
27		TabID:       "tab-1", FocusedPaneID: "pane-1",
28	}
29	if err := protocol.ValidateOutput(m); err != nil {
30		panic(err)
31	}
32	envelope := &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_Output{Output: &wire.Output{
33		Epoch:        m.Epoch,
34		Base:         m.Base,
35		NewState:     m.New,
36		Echo:         m.Echo,
37		ViewRevision: m.ViewRevision,
38		Cols:         uint32(m.Size.Cols),
39		Rows:         uint32(m.Size.Rows),
40		Full:         m.Full,
41		Context: &wire.ViewContext{Publication: m.Context.Publication, Route: &wire.CommittedRouteIdentity{
42			Target: &wire.ExactTarget{
43				LifecycleId: &wire.LifecycleID{Value: append([]byte(nil), m.Context.Route.Target.LifecycleID[:]...)},
44				SessionName: m.Context.Route.Target.SessionName,
45			},
46			Ephemeral: m.Context.Route.Ephemeral,
47		}, TabId: string(m.Context.TabID), FocusedPaneId: string(m.Context.FocusedPaneID)},
48		Encoding:           0,
49		UncompressedLength: uint64(len(m.Data)),
50		Data:               append([]byte(nil), m.Data...),
51	}}}
52	raw, err := proto.MarshalOptions{Deterministic: true}.Marshal(envelope)
53	if err != nil {
54		panic(err)
55	}
56	return raw
57}
58
59var transcript = []wire.Envelope{
60	{Payload: mustEncodeOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},
61	{Payload: mustEncodeOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},
62}
63
64// Transcript returns a deep copy of the canonical replay transcript so an
65// adapter cannot mutate the fixture observed by another adapter.
66func Transcript() []wire.Envelope {
67	return cloneEnvelopes(transcript)
68}
69
70// Exchange carries already-composed envelopes through one transport adapter.
71type Exchange func(t *testing.T, envelopes []wire.Envelope) []wire.Envelope
72
73// Run verifies that an adapter preserves the canonical transcript exactly.
74func Run(t *testing.T, exchange Exchange) {
75	t.Helper()
76	want := Transcript()
77	got := exchange(t, cloneEnvelopes(want))
78	if len(got) != len(want) {
79		t.Fatalf("replayed envelope count = %d, want %d", len(got), len(want))
80	}
81	for i := range want {
82		if !bytes.Equal(got[i].Payload, want[i].Payload) {
83			t.Errorf("envelope %d payload differs in %d bytes, want byte-for-byte equality", i, len(want[i].Payload))
84			continue
85		}
86		gotOutput, err := decodeOutput(got[i].Payload)
87		if err != nil {
88			t.Errorf("envelope %d output decode: %v", i, err)
89			continue
90		}
91		wantOutput, err := decodeOutput(want[i].Payload)
92		if err != nil {
93			t.Fatalf("canonical envelope %d output decode: %v", i, err)
94		}
95		if !reflect.DeepEqual(gotOutput, wantOutput) {
96			t.Errorf("envelope %d output = %#v, want %#v", i, gotOutput, wantOutput)
97		}
98	}
99}
100
101func decodeOutput(payload []byte) (protocol.Output, error) {
102	envelope := &wire.ServerEnvelope{}
103	if err := wire.ScanEnvelope(envelope, payload); err != nil {
104		return protocol.Output{}, err
105	}
106	if err := (proto.UnmarshalOptions{DiscardUnknown: false}.Unmarshal(payload, envelope)); err != nil {
107		return protocol.Output{}, err
108	}
109	output, ok := envelope.Payload.(*wire.ServerEnvelope_Output)
110	if !ok || output.Output == nil {
111		return protocol.Output{}, &protocol.DecodeFailure{Category: protocol.DecodeWrongDirection, Err: errors.New("replaytest: expected Output envelope")}
112	}
113	decoded := protocol.Output{
114		Epoch: output.Output.GetEpoch(), Base: output.Output.GetBase(), New: output.Output.GetNewState(),
115		Echo: output.Output.GetEcho(), ViewRevision: output.Output.GetViewRevision(),
116		Size: domain.Size{Cols: int(output.Output.GetCols()), Rows: int(output.Output.GetRows())},
117		Full: output.Output.GetFull(), Data: append([]byte(nil), output.Output.GetData()...),
118	}
119	if context := output.Output.GetContext(); context != nil {
120		route, err := committedRouteFromWire(context.GetRoute())
121		if err != nil {
122			return protocol.Output{}, fmt.Errorf("replaytest: invalid route: %w", err)
123		}
124		decoded.Context = &protocol.ViewContext{
125			Publication:   context.GetPublication(),
126			Route:         route,
127			TabID:         domain.TabStableID(context.GetTabId()),
128			FocusedPaneID: domain.PaneStableID(context.GetFocusedPaneId()),
129		}
130	}
131	if err := protocol.ValidateOutput(decoded); err != nil {
132		return protocol.Output{}, fmt.Errorf("replaytest: invalid output: %w", err)
133	}
134	return decoded, nil
135}
136
137func cloneEnvelopes(envelopes []wire.Envelope) []wire.Envelope {
138	cloned := make([]wire.Envelope, len(envelopes))
139	for i, envelope := range envelopes {
140		cloned[i] = wire.Envelope{Payload: append([]byte(nil), envelope.Payload...)}
141	}
142	return cloned
143}

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