blob: 01f69c2afe8e9bdf31e23c60c88df7a7112a0c86 [file] [log] [blame]
Lorenz Brun547b33f2020-04-23 15:27:06 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package fsquota
18
19import (
Tim Windelschmidtaf821c82024-04-23 15:03:52 +020020 "errors"
Lorenz Brun547b33f2020-04-23 15:27:06 +020021 "fmt"
Lorenz Brun547b33f2020-04-23 15:27:06 +020022 "math"
23 "os"
24 "os/exec"
25 "syscall"
26 "testing"
27
28 "github.com/stretchr/testify/require"
29 "golang.org/x/sys/unix"
30)
31
Serge Bazanski216fe7b2021-05-21 18:36:16 +020032// withinTolerance is a helper for asserting that a value is within a certain
33// percentage of the expected value. The tolerance is specified as a float
34// between 0 (exact match) and 1 (between 0 and twice the expected value).
Lorenz Brun547b33f2020-04-23 15:27:06 +020035func withinTolerance(t *testing.T, expected uint64, actual uint64, tolerance float64, name string) {
36 t.Helper()
37 delta := uint64(math.Round(float64(expected) * tolerance))
38 lowerBound := expected - delta
39 upperBound := expected + delta
40 if actual < lowerBound {
41 t.Errorf("Value %v (%v) is too low, expected between %v and %v", name, actual, lowerBound, upperBound)
42 }
43 if actual > upperBound {
44 t.Errorf("Value %v (%v) is too high, expected between %v and %v", name, actual, lowerBound, upperBound)
45 }
46}
47
48func TestBasic(t *testing.T) {
49 if os.Getenv("IN_KTEST") != "true" {
50 t.Skip("Not in ktest")
51 }
Lorenz Brun688ee2b2024-08-21 17:38:46 +020052 // xfsprogs since 5.19.0 / commit 6e0ed3d19c5 refuses to create filesystems
53 // smaller than 300MiB for dubious reasons. Running tests with smaller
54 // filesystems is acceptable according to the commit message, so we do that
55 // here with the unsupported flag.
56 mkfsCmd := exec.Command("/mkfs.xfs", "--unsupported", "-qf", "/dev/ram0")
57 if out, err := mkfsCmd.CombinedOutput(); err != nil {
58 t.Fatal(err, string(out))
Lorenz Brun547b33f2020-04-23 15:27:06 +020059 }
60 if err := os.Mkdir("/test", 0755); err != nil {
61 t.Error(err)
62 }
63
64 if err := unix.Mount("/dev/ram0", "/test", "xfs", unix.MS_NOEXEC|unix.MS_NODEV, "prjquota"); err != nil {
65 t.Fatal(err)
66 }
67 defer unix.Unmount("/test", 0)
68 defer os.RemoveAll("/test")
69 t.Run("SetQuota", func(t *testing.T) {
70 defer func() {
71 os.RemoveAll("/test/set")
72 }()
73 if err := os.Mkdir("/test/set", 0755); err != nil {
74 t.Fatal(err)
75 }
76 if err := SetQuota("/test/set", 1024*1024, 100); err != nil {
77 t.Fatal(err)
78 }
79 })
80 t.Run("SetQuotaAndExhaust", func(t *testing.T) {
81 defer func() {
82 os.RemoveAll("/test/sizequota")
83 }()
84 if err := os.Mkdir("/test/sizequota", 0755); err != nil {
85 t.Fatal(err)
86 }
87 const bytesQuota = 1024 * 1024 // 1MiB
88 if err := SetQuota("/test/sizequota", bytesQuota, 0); err != nil {
89 t.Fatal(err)
90 }
91 testfile, err := os.Create("/test/sizequota/testfile")
92 if err != nil {
93 t.Fatal(err)
94 }
95 testdata := make([]byte, 1024)
96 var bytesWritten int
97 for {
Tim Windelschmidt5e460a92024-04-11 01:33:09 +020098 n, err := testfile.Write(testdata)
Lorenz Brun547b33f2020-04-23 15:27:06 +020099 if err != nil {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +0200100 var pathErr *os.PathError
101 if errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ENOSPC) {
102 // Running out of space is the only acceptable error to continue execution
103 break
Lorenz Brun547b33f2020-04-23 15:27:06 +0200104 }
105 t.Fatal(err)
106 }
107 bytesWritten += n
108 }
109 if bytesWritten > bytesQuota {
110 t.Errorf("Wrote %v bytes, quota is only %v bytes", bytesWritten, bytesQuota)
111 }
112 })
113 t.Run("GetQuotaReadbackAndUtilization", func(t *testing.T) {
114 defer func() {
115 os.RemoveAll("/test/readback")
116 }()
117 if err := os.Mkdir("/test/readback", 0755); err != nil {
118 t.Fatal(err)
119 }
120 const bytesQuota = 1024 * 1024 // 1MiB
121 const inodesQuota = 100
122 if err := SetQuota("/test/readback", bytesQuota, inodesQuota); err != nil {
123 t.Fatal(err)
124 }
125 sizeFileData := make([]byte, 512*1024)
Lorenz Brun764a2de2021-11-22 16:26:36 +0100126 if err := os.WriteFile("/test/readback/512kfile", sizeFileData, 0644); err != nil {
Lorenz Brun547b33f2020-04-23 15:27:06 +0200127 t.Fatal(err)
128 }
129
130 quotaUtil, err := GetQuota("/test/readback")
131 if err != nil {
132 t.Fatal(err)
133 }
134 require.Equal(t, uint64(bytesQuota), quotaUtil.Bytes, "bytes quota readback incorrect")
135 require.Equal(t, uint64(inodesQuota), quotaUtil.Inodes, "inodes quota readback incorrect")
136
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200137 // Give 10% tolerance for quota used values to account for metadata
138 // overhead and internal structures that are also in there. If it's out
139 // by more than that it's an issue anyways.
Lorenz Brun547b33f2020-04-23 15:27:06 +0200140 withinTolerance(t, uint64(len(sizeFileData)), quotaUtil.BytesUsed, 0.1, "BytesUsed")
141
142 // Write 50 inodes for a total of 51 (with the 512K file)
143 for i := 0; i < 50; i++ {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100144 if err := os.WriteFile(fmt.Sprintf("/test/readback/ifile%v", i), []byte("test"), 0644); err != nil {
Lorenz Brun547b33f2020-04-23 15:27:06 +0200145 t.Fatal(err)
146 }
147 }
148
149 quotaUtil, err = GetQuota("/test/readback")
150 if err != nil {
151 t.Fatal(err)
152 }
153
154 withinTolerance(t, 51, quotaUtil.InodesUsed, 0.1, "InodesUsed")
155 })
156}