Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · ci/agent/cache_test.go

Line attribution on main · 118 line(s)

3b39a84c
Keenan Nemetz
14d ago
1 package agent
2
3 import (
4 "path/filepath"
5 "strings"
6 "testing"
a29ce1f7
Keenan Nemetz
2d ago
7
d37d301f
Keenan Nemetz
15h ago
8 jobcache "rodent.software/vole/ci/cache"
3b39a84c
Keenan Nemetz
14d ago
9 )
10
11 // A cache is shared mutable state, so which namespace a job gets is a policy
12 // decision: one repository never sees another's, and an untrusted contributor
13 // branch never sees what a protected branch builds into.
14 func TestCacheNamespacesByRepoAndTrust(t *testing.T) {
15 t.Parallel()
16 root := t.TempDir()
a29ce1f7
Keenan Nemetz
2d ago
17 s := &runnerSession{cacheRoot: root, cacheReuse: true}
3b39a84c
Keenan Nemetz
14d ago
18
a29ce1f7
Keenan Nemetz
2d ago
19 mounts, release := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/main", Image: "alpine:3.22"})
20 defer release()
3b39a84c
Keenan Nemetz
14d ago
21 if len(mounts) != 1 || mounts[0].Path != "/cache" {
22 t.Fatalf("mounts = %+v, want one at /cache", mounts)
23 }
24 if !strings.HasPrefix(mounts[0].Host, root+string(filepath.Separator)) {
25 t.Fatalf("cache %q is not under the runner's cache root", mounts[0].Host)
26 }
27 if !strings.Contains(mounts[0].Host, "demo") {
28 t.Fatalf("cache %q is not namespaced by repository", mounts[0].Host)
29 }
30
a29ce1f7
Keenan Nemetz
2d ago
31 contribMounts, releaseContrib := s.cacheFor(&runnerJob{
3b39a84c
Keenan Nemetz
14d ago
32 Repo: "demo", Ref: "refs/heads/contrib/alice/patch", Image: "alpine:3.22",
33 })
a29ce1f7
Keenan Nemetz
2d ago
34 defer releaseContrib()
3b39a84c
Keenan Nemetz
14d ago
35 if contribMounts[0].Host == mounts[0].Host {
36 t.Fatalf("a contributor branch shares the protected branch's cache: %s", mounts[0].Host)
37 }
38
a29ce1f7
Keenan Nemetz
2d ago
39 otherMounts, releaseOther := s.cacheFor(&runnerJob{Repo: "other", Ref: "refs/heads/main", Image: "alpine:3.22"})
40 defer releaseOther()
3b39a84c
Keenan Nemetz
14d ago
41 if otherMounts[0].Host == mounts[0].Host {
42 t.Fatal("two repositories share a cache namespace")
43 }
44 }
45
46 func TestCacheCanBeTurnedOff(t *testing.T) {
47 t.Parallel()
48 s := &runnerSession{}
a29ce1f7
Keenan Nemetz
2d ago
49 mounts, release := s.cacheFor(&runnerJob{Repo: "demo"})
50 defer release()
51 if mounts != nil {
3b39a84c
Keenan Nemetz
14d ago
52 t.Fatalf("a disabled cache produced %v", mounts)
53 }
54 }
6bfc4294
Keenan Nemetz
12d ago
55
56 // Two contributors get two caches. One contributor's untrusted branch must not be
57 // able to seed the cache another contributor's branch builds from, or the second
58 // branch inherits whatever the first one wrote.
59 func TestContributorCachesAreSeparatedByAccount(t *testing.T) {
60 t.Parallel()
a29ce1f7
Keenan Nemetz
2d ago
61 s := &runnerSession{cacheRoot: t.TempDir(), cacheReuse: true}
62 alice, releaseAlice := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/contrib/alice/one", Image: "alpine:3.22"})
63 defer releaseAlice()
64 bob, releaseBob := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/contrib/bob/one", Image: "alpine:3.22"})
65 defer releaseBob()
6bfc4294
Keenan Nemetz
12d ago
66 if alice[0].Host == bob[0].Host {
67 t.Fatalf("two contributors share a cache: %s", alice[0].Host)
68 }
69 // The same contributor's second branch reuses their cache, which is the point of
70 // namespacing by account rather than by branch.
a29ce1f7
Keenan Nemetz
2d ago
71 again, releaseAgain := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/contrib/alice/two", Image: "alpine:3.22"})
72 defer releaseAgain()
6bfc4294
Keenan Nemetz
12d ago
73 if again[0].Host != alice[0].Host {
74 t.Fatalf("one contributor's branches got different caches: %s vs %s", alice[0].Host, again[0].Host)
75 }
76 }
a29ce1f7
Keenan Nemetz
2d ago
77
78 // A job holds its namespace, so eviction has to skip it: the trimmer runs on the
79 // runner's own beat and may pass a mounted namespace in the middle of a build.
80 func TestAReusedCacheIsHeldAgainstEviction(t *testing.T) {
81 t.Parallel()
82 s := &runnerSession{cacheRoot: t.TempDir(), cacheReuse: true}
83 mounts, release := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/main", Image: "alpine:3.22"})
84 defer release()
85 if _, held := jobcache.TryLockExclusive(s.cacheRoot, mounts[0].Host); held {
86 t.Fatal("eviction could take a namespace a job has mounted")
87 }
88 }
89
90 // Two jobs of the same repository share their namespace: they are meant to run at
91 // once, and the tools inside them provide their own locking. Taking the second
92 // shared lock returning at all is the assertion.
93 func TestTwoJobsMayShareOneNamespace(t *testing.T) {
94 t.Parallel()
95 s := &runnerSession{cacheRoot: t.TempDir(), cacheReuse: true}
96 job := &runnerJob{Repo: "demo", Ref: "refs/heads/main", Image: "alpine:3.22"}
97 first, releaseFirst := s.cacheFor(job)
98 defer releaseFirst()
99 second, releaseSecond := s.cacheFor(job)
100 defer releaseSecond()
101 if first[0].Host != second[0].Host {
102 t.Fatalf("one repository got two caches: %s vs %s", first[0].Host, second[0].Host)
103 }
104 }
105
106 // --no-cache empties the namespace when the job ends, so it must not run while
107 // anybody else is inside it.
108 func TestNoCacheHoldsTheNamespaceExclusively(t *testing.T) {
109 t.Parallel()
110 s := &runnerSession{cacheRoot: t.TempDir(), cacheReuse: false}
111 mounts, release := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/main", Image: "alpine:3.22"})
112 defer release()
113 other, held := jobcache.TryLockExclusive(s.cacheRoot, mounts[0].Host)
114 if held {
115 other()
116 t.Fatal("a --no-cache job did not hold its namespace exclusively")
117 }
118 }