v0.23.0 333c710

ciartifacts_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ciartifacts_test
4
5import (
6	"crypto/sha256"
7	"encoding/hex"
8	"errors"
9	"io"
10	"os"
11	"path/filepath"
12	"strings"
13	"testing"
14
15	"github.com/stretchr/testify/assert"
16	"github.com/stretchr/testify/require"
17
18	"bnema.dev/ermite/internal/adapters/ciartifacts"
19	"bnema.dev/ermite/internal/domain"
20	"bnema.dev/ermite/internal/ports"
21)
22
23var ref = ports.CIArtifactRef{RunID: 7, ID: 42}
24
25func digestOf(s string) string {
26	sum := sha256.Sum256([]byte(s))
27	return "sha256:" + hex.EncodeToString(sum[:])
28}
29
30func entries(t *testing.T, dir string) []string {
31	t.Helper()
32	es, err := os.ReadDir(dir)
33	if errors.Is(err, os.ErrNotExist) {
34		return nil
35	}
36	require.NoError(t, err)
37	var out []string
38	for _, e := range es {
39		out = append(out, e.Name())
40	}
41	return out
42}
43
44func TestBlocksAssembleInTheOrderOfTheList(t *testing.T) {
45	ctx := t.Context()
46	root := t.TempDir()
47	s := ciartifacts.New(root)
48
49	// Uploaded out of order; the list decides.
50	for file, data := range map[string]string{"02.aa": "world", "01.bb": "hello ", "03.cc": "!"} {
51		n, err := s.PutBlock(ctx, ref, file, strings.NewReader(data))
52		require.NoError(t, err)
53		assert.EqualValues(t, len(data), n)
54	}
55	size, digest, err := s.Assemble(ctx, ref, []string{"01.bb", "02.aa", "03.cc", "03.cc"})
56	require.NoError(t, err)
57	assert.EqualValues(t, len("hello world!!"), size)
58	assert.Equal(t, digestOf("hello world!!"), digest, "a repeated id is repeated content")
59
60	// The documented layout, named by ids only.
61	b, err := os.ReadFile(filepath.Join(root, "00000000000000000007", "00000000000000000042"))
62	require.NoError(t, err)
63	assert.Equal(t, "hello world!!", string(b))
64	for _, e := range entries(t, filepath.Join(root, "00000000000000000007")) {
65		assert.False(t, strings.HasPrefix(e, ".tmp-"), "no temporary file left: %s", e)
66	}
67
68	// Offset reads.
69	r, err := s.Open(ctx, ref, 6)
70	require.NoError(t, err)
71	rest, err := io.ReadAll(r)
72	require.NoError(t, err)
73	require.NoError(t, r.Close())
74	assert.Equal(t, "world!!", string(rest))
75
76	// A new assembly replaces the content whole.
77	size, digest, err = s.Assemble(ctx, ref, nil)
78	require.NoError(t, err)
79	assert.Zero(t, size, "an empty list is an empty content")
80	assert.Equal(t, digestOf(""), digest)
81
82	ids, err := s.ArtifactIDs(ctx, 7)
83	require.NoError(t, err)
84	assert.Equal(t, []int64{42}, ids)
85	runs, err := s.RunIDs(ctx)
86	require.NoError(t, err)
87	assert.Equal(t, []int64{7}, runs)
88
89	require.NoError(t, s.RemoveBlocks(ctx, ref))
90	assert.Equal(t, []string{"00000000000000000042"}, entries(t, filepath.Join(root, "00000000000000000007")))
91	require.NoError(t, s.Remove(ctx, ref))
92	assert.Empty(t, entries(t, filepath.Join(root, "00000000000000000007")))
93	_, err = s.Open(ctx, ref, 0)
94	assert.ErrorIs(t, err, domain.ErrNotFound)
95	require.NoError(t, s.RemoveRuns(ctx, []int64{7}))
96	assert.Empty(t, entries(t, root))
97}
98
99func TestAMissingBlockIsNotFoundAndLeavesNoContent(t *testing.T) {
100	ctx := t.Context()
101	root := t.TempDir()
102	s := ciartifacts.New(root)
103	_, err := s.PutBlock(ctx, ref, "01.aa", strings.NewReader("x"))
104	require.NoError(t, err)
105	_, _, err = s.Assemble(ctx, ref, []string{"01.aa", "02.aa"})
106	assert.ErrorIs(t, err, domain.ErrNotFound)
107	_, err = s.Open(ctx, ref, 0)
108	assert.ErrorIs(t, err, domain.ErrNotFound, "no content appears")
109	for _, e := range entries(t, filepath.Join(root, "00000000000000000007")) {
110		assert.False(t, strings.HasPrefix(e, ".tmp-"), e)
111	}
112}
113
114type failingReader struct{ n int }
115
116func (f *failingReader) Read(p []byte) (int, error) {
117	if f.n == 0 {
118		return 0, errors.New("stream broken")
119	}
120	f.n--
121	p[0] = 'x'
122	return 1, nil
123}
124
125func TestABrokenSourceLeavesNoBlock(t *testing.T) {
126	ctx := t.Context()
127	root := t.TempDir()
128	s := ciartifacts.New(root)
129	_, err := s.PutBlock(ctx, ref, "01.aa", &failingReader{n: 3})
130	require.EqualError(t, err, "stream broken", "the source's error as it is")
131	assert.Empty(t, entries(t, filepath.Join(root, "00000000000000000007", "00000000000000000042.blocks")))
132
133	// A file name is never reused: the second put of a name fails.
134	_, err = s.PutBlock(ctx, ref, "01.aa", strings.NewReader("a"))
135	require.NoError(t, err)
136	_, err = s.PutBlock(ctx, ref, "01.aa", strings.NewReader("b"))
137	assert.Error(t, err)
138}
139
140func TestNamesOutsideTheRulesNeverReachTheFileSystem(t *testing.T) {
141	ctx := t.Context()
142	root := t.TempDir()
143	s := ciartifacts.New(filepath.Join(root, "store"))
144	for _, name := range []string{"", "../x", "a/b", ".hidden", "..", "ZZ", "01.aa/../../x", strings.Repeat("a", 201)} {
145		_, err := s.PutBlock(ctx, ref, name, strings.NewReader("x"))
146		assert.ErrorIs(t, err, domain.ErrInvalid, "%q", name)
147		_, _, err = s.Assemble(ctx, ref, []string{name})
148		assert.Error(t, err, "%q", name)
149	}
150	for _, bad := range []ports.CIArtifactRef{{RunID: 0, ID: 1}, {RunID: 1, ID: -1}} {
151		_, err := s.PutBlock(ctx, bad, "01.aa", strings.NewReader("x"))
152		assert.ErrorIs(t, err, domain.ErrInvalid)
153		_, err = s.Open(ctx, bad, 0)
154		assert.ErrorIs(t, err, domain.ErrNotFound)
155	}
156	for _, e := range entries(t, root) {
157		assert.Equal(t, "store", e, "nothing written beside the store")
158	}
159}
160
161func TestOrphanListingSeesEveryKindOfFile(t *testing.T) {
162	ctx := t.Context()
163	root := t.TempDir()
164	s := ciartifacts.New(root)
165	run := filepath.Join(root, "00000000000000000009")
166	require.NoError(t, os.MkdirAll(filepath.Join(run, "00000000000000000003.blocks"), 0o700))
167	require.NoError(t, os.WriteFile(filepath.Join(run, "00000000000000000001"), nil, 0o600))
168	require.NoError(t, os.WriteFile(filepath.Join(run, ".tmp-00000000000000000002-123"), nil, 0o600))
169	require.NoError(t, os.WriteFile(filepath.Join(run, "junk"), nil, 0o600))
170	require.NoError(t, os.MkdirAll(filepath.Join(root, "not-a-run"), 0o700))
171
172	ids, err := s.ArtifactIDs(ctx, 9)
173	require.NoError(t, err)
174	assert.Equal(t, []int64{1, 2, 3}, ids)
175	runs, err := s.RunIDs(ctx)
176	require.NoError(t, err)
177	assert.Equal(t, []int64{9}, runs)
178
179	// Remove takes the assembly in progress too.
180	require.NoError(t, s.Remove(ctx, ports.CIArtifactRef{RunID: 9, ID: 2}))
181	ids, _ = s.ArtifactIDs(ctx, 9)
182	assert.Equal(t, []int64{1, 3}, ids)
183}

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 4

Tags 45

↑↓ moveenter openesc close