1// SPDX-License-Identifier: MIT
2
3package disk
4
5import (
6 "context"
7 "errors"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "bnema.dev/ermite-runner/internal/cmdrun"
14)
15
16func TestCreateScratchIsSparseExclusiveAndNoFollow(t *testing.T) {
17 dir := t.TempDir()
18 run := &cmdrun.Fake{}
19 p := filepath.Join(dir, "scratch.ext4")
20 if err := CreateScratch(context.Background(), run, p, 64, os.Getuid()); err != nil {
21 t.Fatal(err)
22 }
23 st, _ := os.Stat(p)
24 if st.Size() != 64<<20 || st.Mode().Perm() != 0o600 {
25 t.Fatalf("%v", st)
26 }
27 if got := run.Lines(); len(got) != 1 || !strings.Contains(got[0], "mkfs.ext4") || !strings.Contains(got[0], "^has_journal") {
28 t.Fatalf("mkfs call: %v", got)
29 }
30 if err := CreateScratch(context.Background(), run, p, 64, -1); err == nil {
31 t.Fatal("an existing file must be refused")
32 }
33 victim := filepath.Join(dir, "victim")
34 _ = os.WriteFile(victim, []byte("x"), 0o600)
35 link := filepath.Join(dir, "link")
36 _ = os.Symlink(victim, link)
37 if err := CreateScratch(context.Background(), run, link, 1, -1); err == nil {
38 t.Fatal("a symlink must be refused")
39 }
40 // mkfs failure is reported.
41 bad := &cmdrun.Fake{Handle: func(cmdrun.Call) ([]byte, error) { return nil, errors.New("boom") }}
42 if err := CreateScratch(context.Background(), bad, filepath.Join(dir, "x.ext4"), 1, -1); err == nil || !strings.Contains(err.Error(), "format scratch disk") {
43 t.Fatalf("%v", err)
44 }
45}
46
47// C14: the mkfs.ext4 version is checked.
48func TestCheckMkfs(t *testing.T) {
49 ver := func(out string, err error) cmdrun.Runner {
50 return &cmdrun.Fake{Handle: func(cmdrun.Call) ([]byte, error) { return []byte(out), err }}
51 }
52 for name, tc := range map[string]struct {
53 r cmdrun.Runner
54 ok bool
55 }{
56 "current": {ver("mke2fs 1.47.4 (6-Mar-2025)\n", nil), true},
57 "exit 1 on -V": {ver("mke2fs 1.46.5 (30-Dec-2021)\nUsing EXT2FS Library version 1.46.5\n", errors.New("exit status 1")), true},
58 "minimum": {ver("mke2fs 1.43 (17-May-2016)\n", nil), true},
59 "too old": {ver("mke2fs 1.42.13 (17-May-2015)\n", nil), false},
60 "major 0": {ver("mke2fs 0.9 (1-Jan-2000)\n", nil), false},
61 "garbage": {ver("hello\n", nil), false},
62 "missing": {ver("", errors.New("executable file not found")), false},
63 } {
64 err := CheckMkfs(context.Background(), tc.r)
65 if (err == nil) != tc.ok {
66 t.Errorf("%s: err=%v ok=%v", name, err, tc.ok)
67 }
68 }
69 if err := CheckMkfs(context.Background(), ver("mke2fs 1.42.9 (4-Feb-2014)", nil)); err == nil || !strings.Contains(err.Error(), "1.43") {
70 t.Errorf("the error must name the minimum: %v", err)
71 }
72}
73
74const mountinfo = `22 28 0:21 / /sys rw,nosuid shared:7 - sysfs sysfs rw
7536 28 7:0 / /var/lib/ermite-runner/containers/er00112233/scratch rw,nosuid,nodev shared:9 - ext4 /dev/loop0 rw
7637 28 7:1 / /srv/with\040space rw - ext4 /dev/loop1 rw
77`
78
79func TestMounterIsMountedAndUnmount(t *testing.T) {
80 mi := filepath.Join(t.TempDir(), "mountinfo")
81 _ = os.WriteFile(mi, []byte(mountinfo), 0o644)
82 f := &cmdrun.Fake{}
83 m := Mounter{Run: f, Mountinfo: mi}
84 for dir, want := range map[string]bool{
85 "/var/lib/ermite-runner/containers/er00112233/scratch": true,
86 "/srv/with space": true,
87 "/var/lib/ermite-runner/containers/er00112233": false,
88 "/nowhere": false,
89 } {
90 if got, err := m.IsMounted(dir); err != nil || got != want {
91 t.Errorf("%s: %v %v", dir, got, err)
92 }
93 }
94 if err := m.Unmount(context.Background(), "/nowhere"); err != nil || len(f.Calls) != 0 {
95 t.Fatalf("a directory that is not mounted needs no umount: %v %v", err, f.Lines())
96 }
97 if err := m.Unmount(context.Background(), "/var/lib/ermite-runner/containers/er00112233/scratch"); err != nil {
98 t.Fatal(err)
99 }
100 if got := f.Lines(); len(got) != 1 || got[0] != "umount /var/lib/ermite-runner/containers/er00112233/scratch" {
101 t.Fatalf("%v", got)
102 }
103 if err := m.Mount(context.Background(), "/s/scratch.ext4", "/s/mnt"); err != nil {
104 t.Fatal(err)
105 }
106 if last := f.Lines()[1]; !strings.Contains(last, "loop,noatime") || !strings.HasSuffix(last, "/s/scratch.ext4 /s/mnt") {
107 t.Fatalf("mount call: %s", last)
108 }
109 bad := Mounter{Run: &cmdrun.Fake{Handle: func(cmdrun.Call) ([]byte, error) { return nil, errors.New("busy") }}, Mountinfo: mi}
110 if err := bad.Unmount(context.Background(), "/srv/with space"); err == nil {
111 t.Fatal("umount failure must be reported")
112 }
113 if _, err := (Mounter{Mountinfo: "/nonexistent/mountinfo"}).IsMounted("/x"); err == nil {
114 t.Fatal("an unreadable mountinfo must be an error, not 'not mounted'")
115 }
116}
117
118func TestUnescapeMountinfo(t *testing.T) {
119 for in, want := range map[string]string{
120 `/plain`: "/plain", `/a\040b`: "/a b", `/tab\011x`: "/tab\tx", `/bs\134x`: `/bs\x`, `/bad\9zz`: `/bad\9zz`, `/end\04`: `/end\04`,
121 } {
122 if got := unescapeMountinfo(in); got != want {
123 t.Errorf("%q: got %q want %q", in, got, want)
124 }
125 }
126}