1// SPDX-License-Identifier: MIT
2
3package sandbox
4
5import (
6 "bytes"
7 "context"
8 "errors"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13)
14
15// S3: one runner per host, whatever its state directory.
16func TestHostLockIsExclusiveAndReleasable(t *testing.T) {
17 p := filepath.Join(t.TempDir(), "run", "ermite-runner.lock")
18 a, err := AcquireHostLock(p)
19 if err != nil {
20 t.Fatal(err)
21 }
22 if _, err := AcquireHostLock(p); err == nil || !strings.Contains(err.Error(), "another ermite-runner is running on this host") {
23 t.Fatalf("a second runner must be refused with a clear message, got %v", err)
24 }
25 if err := a.Release(); err != nil {
26 t.Fatal(err)
27 }
28 if err := a.Release(); err != nil {
29 t.Fatalf("double release: %v", err)
30 }
31 b, err := AcquireHostLock(p)
32 if err != nil {
33 t.Fatalf("the lock must be free after Release: %v", err)
34 }
35 _ = b.Release()
36 var nilLock *HostLock
37 if err := nilLock.Release(); err != nil {
38 t.Fatal(err)
39 }
40}
41
42func TestHostLockErrorNamesTheOverride(t *testing.T) {
43 _, err := AcquireHostLock("/proc/nope/ermite.lock")
44 if err == nil || !strings.Contains(err.Error(), EnvLockFile) {
45 t.Fatalf("got %v", err)
46 }
47}
48
49func TestConfigNewSettings(t *testing.T) {
50 c, err := LoadConfig(env(nil))
51 if err != nil {
52 t.Fatal(err)
53 }
54 if c.JobPids != 32768 || c.DenyPrivileged || c.LockFile != "/run/ermite-runner.lock" ||
55 c.DiskBandwidthMiB != 400 || c.DiskIOPS != 20000 || c.NetMbit != 1000 {
56 t.Fatalf("%+v", c)
57 }
58 c, err = LoadConfig(env(map[string]string{
59 EnvJobPids: "1000", EnvDenyPrivileged: "1", EnvLockFile: "/tmp/x.lock",
60 EnvJobDiskBW: "0", EnvJobDiskIOPS: "5000", EnvJobNetBandwidth: "100",
61 }))
62 if err != nil {
63 t.Fatal(err)
64 }
65 if c.JobPids != 1000 || !c.DenyPrivileged || c.LockFile != "/tmp/x.lock" || c.DiskBandwidthMiB != 0 || c.DiskIOPS != 5000 || c.NetMbit != 100 {
66 t.Fatalf("%+v", c)
67 }
68 for name, m := range map[string]map[string]string{
69 "pids low": {EnvJobPids: "3"},
70 "deny": {EnvDenyPrivileged: "maybe"},
71 "lock rel": {EnvLockFile: "run/x.lock"},
72 "bw negative": {EnvJobDiskBW: "-1"},
73 "net not int": {EnvJobNetBandwidth: "fast"},
74 } {
75 if _, err := LoadConfig(env(m)); err == nil {
76 t.Errorf("%s accepted", name)
77 }
78 }
79}
80
81// C8: leaked slots stay taken until ReleaseLeaked; Busy ignores them.
82func TestSlotsLeakedLifecycle(t *testing.T) {
83 s := NewSlots(3)
84 a, _ := s.Acquire()
85 b, _ := s.Acquire()
86 s.MarkLeaked(a)
87 if s.InUse() != 2 || s.Busy() != 1 {
88 t.Fatalf("InUse=%d Busy=%d", s.InUse(), s.Busy())
89 }
90 c, _ := s.Acquire()
91 if c == a || c == b {
92 t.Fatalf("a leaked slot must not be handed out again: %d", c)
93 }
94 s.Release(a) // a normal release also clears the leak flag
95 if s.InUse() != 2 {
96 t.Fatal(s.InUse())
97 }
98 s.MarkLeaked(b)
99 s.ReleaseLeaked()
100 if s.InUse() != 1 || s.Busy() != 1 {
101 t.Fatalf("after ReleaseLeaked InUse=%d Busy=%d", s.InUse(), s.Busy())
102 }
103}
104
105func TestErrLeakedIsMatchable(t *testing.T) {
106 err := errors.Join(errors.New("remove container: busy"), ErrLeaked)
107 if !errors.Is(err, ErrLeaked) {
108 t.Fatal("errors.Is must see ErrLeaked through a join")
109 }
110}
111
112// C7: Abort cancels a boot in progress and forbids a later one.
113func TestBootGate(t *testing.T) {
114 var g BootGate
115 ctx, err := g.Begin(context.Background())
116 if err != nil {
117 t.Fatal(err)
118 }
119 go func() { time.Sleep(20 * time.Millisecond); g.Abort() }()
120 select {
121 case <-ctx.Done():
122 if !errors.Is(context.Cause(ctx), ErrAborted) {
123 t.Fatalf("cause %v", context.Cause(ctx))
124 }
125 case <-time.After(2 * time.Second):
126 t.Fatal("Abort did not cancel the boot context")
127 }
128 g.End()
129 if _, err := g.Begin(context.Background()); !errors.Is(err, ErrAborted) {
130 t.Fatalf("Begin after Abort: %v", err)
131 }
132 // Parent cancellation still works, and End releases the context.
133 var g2 BootGate
134 parent, cancel := context.WithCancel(context.Background())
135 c2, _ := g2.Begin(parent)
136 cancel()
137 <-c2.Done()
138 g2.End()
139 g2.End()
140}
141
142func TestWarnIfLowSpace(t *testing.T) {
143 var buf bytes.Buffer
144 ctx := logCtx(&buf)
145 WarnIfLowSpace(ctx, "/x", 2, 64*1024, func(string) (int64, error) { return 10 * 1024, nil })
146 if !strings.Contains(buf.String(), "less free space") || !strings.Contains(buf.String(), `"worst_case_mib":131072`) {
147 t.Fatalf("expected a warning, got %q", buf.String())
148 }
149 buf.Reset()
150 WarnIfLowSpace(ctx, "/x", 2, 1024, func(string) (int64, error) { return 10 * 1024, nil })
151 if buf.Len() != 0 {
152 t.Fatalf("no warning expected: %q", buf.String())
153 }
154 WarnIfLowSpace(ctx, "/x", 1, 1, func(string) (int64, error) { return 0, errors.New("statfs failed") })
155 if !strings.Contains(buf.String(), "cannot check") {
156 t.Fatalf("a statfs error must be logged: %q", buf.String())
157 }
158 if n, err := FreeMiB(t.TempDir()); err != nil || n <= 0 {
159 t.Fatalf("FreeMiB: %d %v", n, err)
160 }
161}