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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 22m ago 4b8c9e1d
history blame raw 4.6 KiB
1package agent
2
3import (
4 "path/filepath"
5 "strings"
6 "testing"
7
8 jobcache "rodent.software/vole/ci/cache"
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.
14func TestCacheNamespacesByRepoAndTrust(t *testing.T) {
15 t.Parallel()
16 root := t.TempDir()
17 s := &runnerSession{cacheRoot: root, cacheReuse: true}
18
19 mounts, release := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/main", Image: "alpine:3.22"})
20 defer release()
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
31 contribMounts, releaseContrib := s.cacheFor(&runnerJob{
32 Repo: "demo", Ref: "refs/heads/contrib/alice/patch", Image: "alpine:3.22",
33 })
34 defer releaseContrib()
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
39 otherMounts, releaseOther := s.cacheFor(&runnerJob{Repo: "other", Ref: "refs/heads/main", Image: "alpine:3.22"})
40 defer releaseOther()
41 if otherMounts[0].Host == mounts[0].Host {
42 t.Fatal("two repositories share a cache namespace")
43 }
44}
45
46func TestCacheCanBeTurnedOff(t *testing.T) {
47 t.Parallel()
48 s := &runnerSession{}
49 mounts, release := s.cacheFor(&runnerJob{Repo: "demo"})
50 defer release()
51 if mounts != nil {
52 t.Fatalf("a disabled cache produced %v", mounts)
53 }
54}
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.
59func TestContributorCachesAreSeparatedByAccount(t *testing.T) {
60 t.Parallel()
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()
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.
71 again, releaseAgain := s.cacheFor(&runnerJob{Repo: "demo", Ref: "refs/heads/contrib/alice/two", Image: "alpine:3.22"})
72 defer releaseAgain()
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}
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.
80func 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.
93func 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.
108func 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}
119