vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
3b39a84c
Keenan Nemetz
14d ago
|
1 | package agent |
| 2 | ||
| 3 | import ( | |
| 4 | "path/filepath" | |
| 5 | "strings" | |
| 6 | "testing" | |
|
a29ce1f7
Keenan Nemetz
3d ago
|
7 | |
|
d37d301f
Keenan Nemetz
19h 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
3d ago
|
17 | s := &runnerSession{cacheRoot: root, cacheReuse: true} |
|
3b39a84c
Keenan Nemetz
14d ago
|
18 | |
|
a29ce1f7
Keenan Nemetz
3d 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
3d 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
3d 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
3d 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
3d 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
13d 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
3d 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
13d 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
3d 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
13d 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
3d 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 | } |