v0.4.4 edcdd4c

archives_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package host
4
5import (
6	"context"
7	"net/http"
8	"net/http/httptest"
9	"path/filepath"
10	"strings"
11	"sync/atomic"
12	"testing"
13
14	"github.com/bnema/zerowrap"
15
16	ermite "bnema.dev/ermite-api/gen/ermite"
17
18	"bnema.dev/ermite-runner/internal/actionscache"
19	"bnema.dev/ermite-runner/internal/cachesvc"
20	"bnema.dev/ermite-runner/internal/execstream/execpb"
21)
22
23type archiveRig struct {
24	r       *Runner
25	srv     *httptest.Server
26	hits    atomic.Int32
27	status  atomic.Int32
28	archive *actionscache.Cache
29}
30
31func newArchiveRig(t *testing.T) *archiveRig {
32	t.Helper()
33	a := &archiveRig{}
34	a.status.Store(http.StatusOK)
35	a.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36		a.hits.Add(1)
37		if s := int(a.status.Load()); s != http.StatusOK {
38			http.Error(w, "slow down", s)
39			return
40		}
41		_, _ = w.Write([]byte("tarball of " + r.URL.Path))
42	}))
43	t.Cleanup(a.srv.Close)
44	ac, err := actionscache.New(actionscache.Config{Dir: filepath.Join(t.TempDir(), "a"), Max: 1 << 20})
45	if err != nil {
46		t.Fatal(err)
47	}
48	svc, err := cachesvc.New(cachesvc.Config{Dir: filepath.Join(t.TempDir(), "c"), Archives: ac})
49	if err != nil {
50		t.Fatal(err)
51	}
52	t.Cleanup(func() { _ = svc.CloseAll() })
53	a.archive = ac
54	a.r = &Runner{Cache: svc, Archives: ac}
55	return a
56}
57
58func (a *archiveRig) job(id string) *jobRun {
59	j := &jobRun{r: a.r, log: zerowrap.Logger{}, ctx: context.Background(),
60		job:      &ermite.Job{JobId: id, Ref: "refs/heads/main"},
61		contexts: `{"github":{"repository":"o/r"}}`}
62	return j
63}
64
65func (a *archiveRig) action(sha string) (*ermite.ResolvedAction, *execpb.ResolvedAction) {
66	o := &ermite.ResolvedAction{Ref: "x/y@v1", NameWithOwner: "x/y", ResolvedSha: sha, TarballUrl: a.srv.URL + "/x/y/tarball/" + sha, Token: "ignored-for-github"}
67	c := &execpb.ResolvedAction{Ref: o.Ref, NameWithOwner: "x/y", ResolvedSha: sha, TarballUrl: o.TarballUrl, Token: "t", AuthOrigin: "o"}
68	return o, c
69}
70
71// Two jobs resolving the same commit cost one download, and the sandbox is
72// pointed at the cache with no credentials.
73func TestCacheArchivesSharedAcrossJobs(t *testing.T) {
74	a := newArchiveRig(t)
75	sha := strings.Repeat("a", 40)
76	for _, id := range []string{"j1", "j2"} {
77		o, c := a.action(sha)
78		a.job(id).cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
79		if !strings.HasPrefix(c.TarballUrl, "http://127.0.0.1:9089"+cachesvc.ArchivePath) || c.Token != "" || c.AuthOrigin != "" {
80			t.Fatalf("%s: not redirected to the cache: %+v", id, c)
81		}
82		if !strings.Contains(c.TarballUrl, "job="+id) {
83			t.Errorf("%s: the URL is not bound to the job: %s", id, c.TarballUrl)
84		}
85	}
86	if a.hits.Load() != 1 {
87		t.Fatalf("%d downloads for one commit", a.hits.Load())
88	}
89}
90
91// A moved tag resolves to a new sha, hence a new entry: the ref is never a key.
92func TestCacheArchivesKeyedBySha(t *testing.T) {
93	a := newArchiveRig(t)
94	for _, sha := range []string{strings.Repeat("a", 40), strings.Repeat("b", 40)} {
95		o, c := a.action(sha)
96		a.job("j").cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
97	}
98	if a.hits.Load() != 2 {
99		t.Fatalf("%d downloads for two commits", a.hits.Load())
100	}
101}
102
103// Whatever cannot be cached keeps its original URL and token, so the sandbox
104// downloads it with its own retries and messages.
105func TestCacheArchivesFallsBack(t *testing.T) {
106	a := newArchiveRig(t)
107	a.status.Store(http.StatusTooManyRequests)
108	o, c := a.action(strings.Repeat("a", 40))
109	origURL := c.TarballUrl
110	a.job("j").cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
111	if c.TarballUrl != origURL || c.Token != "t" || c.AuthOrigin != "o" {
112		t.Errorf("429: %+v", c)
113	}
114
115	a.status.Store(http.StatusOK)
116	for name, mutate := range map[string]func(*ermite.ResolvedAction){
117		"a ref instead of a sha": func(o *ermite.ResolvedAction) { o.ResolvedSha = "v1" },
118		"no sha":                 func(o *ermite.ResolvedAction) { o.ResolvedSha = "" },
119		"an error":               func(o *ermite.ResolvedAction) { o.Error = "boom" },
120		"no url":                 func(o *ermite.ResolvedAction) { o.TarballUrl = "" },
121	} {
122		o, c := a.action(strings.Repeat("c", 40))
123		mutate(o)
124		before := a.hits.Load()
125		a.job("j").cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
126		if c.Token != "t" || strings.Contains(c.TarballUrl, cachesvc.ArchivePath) || a.hits.Load() != before {
127			t.Errorf("%s: cached anyway: %+v", name, c)
128		}
129	}
130
131	// No cache channel for the job (no repository): the guest has no relay.
132	o, c = a.action(strings.Repeat("d", 40))
133	j := a.job("j")
134	j.contexts = `{}`
135	j.cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
136	if c.Token != "t" || strings.Contains(c.TarballUrl, cachesvc.ArchivePath) {
137		t.Errorf("job without a cache scope: %+v", c)
138	}
139
140	// Cache off.
141	a.r.Archives = nil
142	o, c = a.action(strings.Repeat("e", 40))
143	a.job("j").cacheArchives(context.Background(), []*ermite.ResolvedAction{o}, []*execpb.ResolvedAction{c})
144	if c.Token != "t" {
145		t.Errorf("cache off: %+v", c)
146	}
147}

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 11

↑↓ moveenter openesc close