v3.3.1 31f1eec

log_stream_test.go RawHistoryFolder

1package docker
2
3import (
4	"bufio"
5	"context"
6	"encoding/binary"
7	"net/http"
8	"net/http/httptest"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/moby/moby/api/pkg/stdcopy"
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/require"
16
17	"github.com/bnema/gordon/internal/domain"
18)
19
20func TestRuntime_StreamContainerLogs_DemuxesTimestampedLines(t *testing.T) {
21	var gotFollow string
22	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23		gotFollow = r.URL.Query().Get("follow")
24		w.Header().Set("Content-Type", "application/vnd.docker.multiplexed-stream")
25		_, _ = w.Write(muxFrame(stdcopy.Stdout, "2026-09-10T12:00:01.5Z hello\n"))
26		_, _ = w.Write(muxFrame(stdcopy.Stderr, "2026-09-10T12:00:02Z boom\n"))
27	}))
28	defer server.Close()
29
30	runtime := newRuntimeForHTTPServer(t, server)
31	var lines []domain.ContainerLogLine
32	err := runtime.StreamContainerLogs(context.Background(), "c1", time.Time{}, func(l domain.ContainerLogLine) {
33		lines = append(lines, l)
34	})
35
36	require.NoError(t, err)
37	assert.Equal(t, "1", gotFollow)
38	require.Len(t, lines, 2)
39	byStream := map[string]domain.ContainerLogLine{}
40	for _, l := range lines {
41		byStream[l.Stream] = l
42	}
43	assert.Equal(t, "hello", byStream[domain.LogStreamStdout].Body)
44	assert.Equal(t, time.Date(2026, 9, 10, 12, 0, 1, 500000000, time.UTC), byStream[domain.LogStreamStdout].Time)
45	assert.Equal(t, "boom", byStream[domain.LogStreamStderr].Body)
46}
47
48// muxFrame encodes one Docker multiplexed-stream frame.
49func muxFrame(stream stdcopy.StdType, payload string) []byte {
50	frame := make([]byte, 8, 8+len(payload))
51	frame[0] = byte(stream)
52	binary.BigEndian.PutUint32(frame[4:], uint32(len(payload)))
53	return append(frame, payload...)
54}
55
56func TestRuntime_StreamContainerLogs_NotFound(t *testing.T) {
57	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
58		w.Header().Set("Content-Type", "application/json")
59		w.WriteHeader(http.StatusNotFound)
60		_, _ = w.Write([]byte(`{"message":"No such container: gone"}`))
61	}))
62	defer server.Close()
63
64	runtime := newRuntimeForHTTPServer(t, server)
65	err := runtime.StreamContainerLogs(context.Background(), "gone", time.Time{}, func(domain.ContainerLogLine) {})
66
67	assert.ErrorIs(t, err, domain.ErrContainerNotFound)
68}
69
70func TestReadBoundedLine_TruncatesOversizedLines(t *testing.T) {
71	long := strings.Repeat("x", maxLogLineBytes+100)
72	reader := bufio.NewReaderSize(strings.NewReader(long+"\nnext\n"), 16)
73
74	first, err := readBoundedLine(reader)
75	require.NoError(t, err)
76	second, err := readBoundedLine(reader)
77	require.NoError(t, err)
78
79	assert.Len(t, first, maxLogLineBytes)
80	assert.Equal(t, "next", second)
81}
82
83func TestParseTimestampedLine_KeepsBodyWithoutTimestamp(t *testing.T) {
84	line := parseTimestampedLine("plain text line", domain.LogStreamStdout)
85	assert.Equal(t, "plain text line", line.Body)
86}
87
88func TestParseTimestampedLine_SanitizesInvalidUTF8(t *testing.T) {
89	line := parseTimestampedLine("2026-09-10T12:00:01Z bad\xffbytes", domain.LogStreamStdout)
90	assert.Equal(t, "bad\uFFFDbytes", line.Body)
91
92	plain := parseTimestampedLine("plain\xff", domain.LogStreamStdout)
93	assert.Equal(t, "plain\uFFFD", plain.Body)
94}

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 7

Tags 106

↑↓ moveenter openesc close