blob: 2850b4fbc671b04b0635d5a02bb2b38bd3acedb7 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun547b33f2020-04-23 15:27:06 +02002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun547b33f2020-04-23 15:27:06 +02003
4package fsquota
5
6import (
Tim Windelschmidtaf821c82024-04-23 15:03:52 +02007 "errors"
Lorenz Brun547b33f2020-04-23 15:27:06 +02008 "fmt"
Lorenz Brun547b33f2020-04-23 15:27:06 +02009 "math"
10 "os"
11 "os/exec"
12 "syscall"
13 "testing"
14
15 "github.com/stretchr/testify/require"
16 "golang.org/x/sys/unix"
17)
18
Serge Bazanski216fe7b2021-05-21 18:36:16 +020019// withinTolerance is a helper for asserting that a value is within a certain
20// percentage of the expected value. The tolerance is specified as a float
21// between 0 (exact match) and 1 (between 0 and twice the expected value).
Lorenz Brun547b33f2020-04-23 15:27:06 +020022func withinTolerance(t *testing.T, expected uint64, actual uint64, tolerance float64, name string) {
23 t.Helper()
24 delta := uint64(math.Round(float64(expected) * tolerance))
25 lowerBound := expected - delta
26 upperBound := expected + delta
27 if actual < lowerBound {
28 t.Errorf("Value %v (%v) is too low, expected between %v and %v", name, actual, lowerBound, upperBound)
29 }
30 if actual > upperBound {
31 t.Errorf("Value %v (%v) is too high, expected between %v and %v", name, actual, lowerBound, upperBound)
32 }
33}
34
35func TestBasic(t *testing.T) {
36 if os.Getenv("IN_KTEST") != "true" {
37 t.Skip("Not in ktest")
38 }
Lorenz Brun688ee2b2024-08-21 17:38:46 +020039 // xfsprogs since 5.19.0 / commit 6e0ed3d19c5 refuses to create filesystems
40 // smaller than 300MiB for dubious reasons. Running tests with smaller
41 // filesystems is acceptable according to the commit message, so we do that
42 // here with the unsupported flag.
43 mkfsCmd := exec.Command("/mkfs.xfs", "--unsupported", "-qf", "/dev/ram0")
44 if out, err := mkfsCmd.CombinedOutput(); err != nil {
45 t.Fatal(err, string(out))
Lorenz Brun547b33f2020-04-23 15:27:06 +020046 }
47 if err := os.Mkdir("/test", 0755); err != nil {
Tim Windelschmidtd0d5d9d2025-03-26 22:07:11 +010048 t.Fatal(err)
Lorenz Brun547b33f2020-04-23 15:27:06 +020049 }
50
51 if err := unix.Mount("/dev/ram0", "/test", "xfs", unix.MS_NOEXEC|unix.MS_NODEV, "prjquota"); err != nil {
52 t.Fatal(err)
53 }
54 defer unix.Unmount("/test", 0)
55 defer os.RemoveAll("/test")
56 t.Run("SetQuota", func(t *testing.T) {
57 defer func() {
58 os.RemoveAll("/test/set")
59 }()
60 if err := os.Mkdir("/test/set", 0755); err != nil {
61 t.Fatal(err)
62 }
63 if err := SetQuota("/test/set", 1024*1024, 100); err != nil {
64 t.Fatal(err)
65 }
66 })
67 t.Run("SetQuotaAndExhaust", func(t *testing.T) {
68 defer func() {
69 os.RemoveAll("/test/sizequota")
70 }()
71 if err := os.Mkdir("/test/sizequota", 0755); err != nil {
72 t.Fatal(err)
73 }
74 const bytesQuota = 1024 * 1024 // 1MiB
75 if err := SetQuota("/test/sizequota", bytesQuota, 0); err != nil {
76 t.Fatal(err)
77 }
78 testfile, err := os.Create("/test/sizequota/testfile")
79 if err != nil {
80 t.Fatal(err)
81 }
82 testdata := make([]byte, 1024)
83 var bytesWritten int
84 for {
Tim Windelschmidt5e460a92024-04-11 01:33:09 +020085 n, err := testfile.Write(testdata)
Lorenz Brun547b33f2020-04-23 15:27:06 +020086 if err != nil {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +020087 var pathErr *os.PathError
88 if errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ENOSPC) {
89 // Running out of space is the only acceptable error to continue execution
90 break
Lorenz Brun547b33f2020-04-23 15:27:06 +020091 }
92 t.Fatal(err)
93 }
94 bytesWritten += n
95 }
96 if bytesWritten > bytesQuota {
97 t.Errorf("Wrote %v bytes, quota is only %v bytes", bytesWritten, bytesQuota)
98 }
99 })
100 t.Run("GetQuotaReadbackAndUtilization", func(t *testing.T) {
101 defer func() {
102 os.RemoveAll("/test/readback")
103 }()
104 if err := os.Mkdir("/test/readback", 0755); err != nil {
105 t.Fatal(err)
106 }
107 const bytesQuota = 1024 * 1024 // 1MiB
108 const inodesQuota = 100
109 if err := SetQuota("/test/readback", bytesQuota, inodesQuota); err != nil {
110 t.Fatal(err)
111 }
112 sizeFileData := make([]byte, 512*1024)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100113 if err := os.WriteFile("/test/readback/512kfile", sizeFileData, 0644); err != nil {
Lorenz Brun547b33f2020-04-23 15:27:06 +0200114 t.Fatal(err)
115 }
116
117 quotaUtil, err := GetQuota("/test/readback")
118 if err != nil {
119 t.Fatal(err)
120 }
121 require.Equal(t, uint64(bytesQuota), quotaUtil.Bytes, "bytes quota readback incorrect")
122 require.Equal(t, uint64(inodesQuota), quotaUtil.Inodes, "inodes quota readback incorrect")
123
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200124 // Give 10% tolerance for quota used values to account for metadata
125 // overhead and internal structures that are also in there. If it's out
126 // by more than that it's an issue anyways.
Lorenz Brun547b33f2020-04-23 15:27:06 +0200127 withinTolerance(t, uint64(len(sizeFileData)), quotaUtil.BytesUsed, 0.1, "BytesUsed")
128
129 // Write 50 inodes for a total of 51 (with the 512K file)
130 for i := 0; i < 50; i++ {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100131 if err := os.WriteFile(fmt.Sprintf("/test/readback/ifile%v", i), []byte("test"), 0644); err != nil {
Lorenz Brun547b33f2020-04-23 15:27:06 +0200132 t.Fatal(err)
133 }
134 }
135
136 quotaUtil, err = GetQuota("/test/readback")
137 if err != nil {
138 t.Fatal(err)
139 }
140
141 withinTolerance(t, 51, quotaUtil.InodesUsed, 0.1, "InodesUsed")
142 })
143}