| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cgroup_test |
| |
| import ( |
| "fmt" |
| "internal/runtime/cgroup" |
| "io" |
| "strconv" |
| "strings" |
| "testing" |
| ) |
| |
| const _PATH_MAX = 4096 |
| |
| func TestParseV1Number(t *testing.T) { |
| tests := []struct { |
| name string |
| contents string |
| want int64 |
| wantErr bool |
| }{ |
| { |
| name: "disabled", |
| contents: "-1\n", |
| want: -1, |
| }, |
| { |
| name: "500000", |
| contents: "500000\n", |
| want: 500000, |
| }, |
| { |
| name: "MaxInt64", |
| contents: "9223372036854775807\n", |
| want: 9223372036854775807, |
| }, |
| { |
| name: "missing-newline", |
| contents: "500000", |
| wantErr: true, |
| }, |
| { |
| name: "not-a-number", |
| contents: "123max\n", |
| wantErr: true, |
| }, |
| { |
| name: "v2", |
| contents: "1000 5000\n", |
| wantErr: true, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| got, err := cgroup.ParseV1Number([]byte(tc.contents)) |
| if tc.wantErr { |
| if err == nil { |
| t.Fatalf("parseV1Number got err nil want non-nil") |
| } |
| return |
| } |
| if err != nil { |
| t.Fatalf("parseV1Number got err %v want nil", err) |
| } |
| |
| if got != tc.want { |
| t.Errorf("parseV1Number got %d want %d", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestParseV2Limit(t *testing.T) { |
| tests := []struct { |
| name string |
| contents string |
| want float64 |
| wantOK bool |
| wantErr bool |
| }{ |
| { |
| name: "disabled", |
| contents: "max 100000\n", |
| wantOK: false, |
| }, |
| { |
| name: "5", |
| contents: "500000 100000\n", |
| want: 5, |
| wantOK: true, |
| }, |
| { |
| name: "0.5", |
| contents: "50000 100000\n", |
| want: 0.5, |
| wantOK: true, |
| }, |
| { |
| name: "2.5", |
| contents: "250000 100000\n", |
| want: 2.5, |
| wantOK: true, |
| }, |
| { |
| name: "MaxInt64", |
| contents: "9223372036854775807 9223372036854775807\n", |
| want: 1, |
| wantOK: true, |
| }, |
| { |
| name: "missing-newline", |
| contents: "500000 100000", |
| wantErr: true, |
| }, |
| { |
| name: "v1", |
| contents: "500000\n", |
| wantErr: true, |
| }, |
| { |
| name: "quota-not-a-number", |
| contents: "500000us 100000\n", |
| wantErr: true, |
| }, |
| { |
| name: "period-not-a-number", |
| contents: "500000 100000us\n", |
| wantErr: true, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| got, gotOK, err := cgroup.ParseV2Limit([]byte(tc.contents)) |
| if tc.wantErr { |
| if err == nil { |
| t.Fatalf("parseV1Limit got err nil want non-nil") |
| } |
| return |
| } |
| if err != nil { |
| t.Fatalf("parseV2Limit got err %v want nil", err) |
| } |
| |
| if gotOK != tc.wantOK { |
| t.Errorf("parseV2Limit got ok %v want %v", gotOK, tc.wantOK) |
| } |
| |
| if tc.wantOK && got != tc.want { |
| t.Errorf("parseV2Limit got %f want %f", got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestParseCPURelativePath(t *testing.T) { |
| tests := []struct { |
| name string |
| contents string |
| want string |
| wantVer cgroup.Version |
| wantErr bool |
| }{ |
| { |
| name: "empty", |
| contents: "", |
| wantErr: true, |
| }, |
| { |
| name: "v1", |
| contents: `2:cpu,cpuacct:/a/b/cpu |
| 1:blkio:/a/b/blkio |
| `, |
| want: "/a/b/cpu", |
| wantVer: cgroup.V1, |
| }, |
| { |
| name: "v2", |
| contents: "0::/a/b/c\n", |
| want: "/a/b/c", |
| wantVer: cgroup.V2, |
| }, |
| { |
| name: "mixed", |
| contents: `2:cpu,cpuacct:/a/b/cpu |
| 1:blkio:/a/b/blkio |
| 0::/a/b/v2 |
| `, |
| want: "/a/b/cpu", |
| wantVer: cgroup.V1, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| r := strings.NewReader(tc.contents) |
| read := func(fd int, b []byte) (int, uintptr) { |
| n, err := r.Read(b) |
| if err != nil && err != io.EOF { |
| const dummyErrno = 42 |
| return n, dummyErrno |
| } |
| return n, 0 |
| } |
| |
| var got [cgroup.PathSize]byte |
| var scratch [cgroup.ParseSize]byte |
| n, gotVer, err := cgroup.ParseCPURelativePath(0, read, got[:], scratch[:]) |
| if (err != nil) != tc.wantErr { |
| t.Fatalf("parseCPURelativePath got err %v want %v", err, tc.wantErr) |
| } |
| |
| if gotVer != tc.wantVer { |
| t.Errorf("parseCPURelativePath got cgroup version %d want %d", gotVer, tc.wantVer) |
| } |
| |
| if string(got[:n]) != tc.want { |
| t.Errorf("parseCPURelativePath got %q want %q", string(got[:n]), tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestContainsCPU(t *testing.T) { |
| tests := []struct { |
| in string |
| want bool |
| }{ |
| { |
| in: "", |
| want: false, |
| }, |
| { |
| in: ",", |
| want: false, |
| }, |
| { |
| in: "cpu", |
| want: true, |
| }, |
| { |
| in: "memory,cpu", |
| want: true, |
| }, |
| { |
| in: "cpu,memory", |
| want: true, |
| }, |
| { |
| in: "memory,cpu,block", |
| want: true, |
| }, |
| { |
| in: "memory,cpuacct,block", |
| want: false, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.in, func(t *testing.T) { |
| got := cgroup.ContainsCPU([]byte(tc.in)) |
| if got != tc.want { |
| t.Errorf("containsCPU(%q) got %v want %v", tc.in, got, tc.want) |
| } |
| }) |
| } |
| } |
| |
| func TestParseCPUMount(t *testing.T) { |
| // Used for v2-longline. We want an overlayfs mount to have an option |
| // so long that the entire line can't possibly fit in the scratch |
| // buffer. |
| const lowerPath = "/so/many/overlay/layers" |
| overlayLongLowerDir := lowerPath |
| for i := 0; len(overlayLongLowerDir) < cgroup.ScratchSize; i++ { |
| overlayLongLowerDir += fmt.Sprintf(":%s%d", lowerPath, i) |
| } |
| |
| tests := []struct { |
| name string |
| contents string |
| want string |
| wantErr bool |
| }{ |
| { |
| name: "empty", |
| contents: "", |
| wantErr: true, |
| }, |
| { |
| name: "v1", |
| contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw |
| 20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw |
| 21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw |
| 49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory |
| 54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io |
| 56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct |
| 58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net |
| 59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset |
| `, |
| want: "/sys/fs/cgroup/cpu", |
| }, |
| { |
| name: "v2", |
| contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw |
| 20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw |
| 21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw |
| 25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw |
| `, |
| want: "/sys/fs/cgroup", |
| }, |
| { |
| name: "mixed", |
| contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw |
| 20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw |
| 21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw |
| 25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw |
| 49 22 0:37 / /sys/fs/cgroup/memory rw - cgroup cgroup rw,memory |
| 54 22 0:38 / /sys/fs/cgroup/io rw - cgroup cgroup rw,io |
| 56 22 0:40 / /sys/fs/cgroup/cpu rw - cgroup cgroup rw,cpu,cpuacct |
| 58 22 0:42 / /sys/fs/cgroup/net rw - cgroup cgroup rw,net |
| 59 22 0:43 / /sys/fs/cgroup/cpuset rw - cgroup cgroup rw,cpuset |
| `, |
| want: "/sys/fs/cgroup/cpu", |
| }, |
| { |
| name: "v2-escaped", |
| contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw |
| 20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw |
| 21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw |
| 25 21 0:22 / /sys/fs/cgroup/tab\011tab rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw |
| `, |
| want: `/sys/fs/cgroup/tab tab`, |
| }, |
| { |
| // Overly long line on a different mount doesn't matter. |
| name: "v2-longline", |
| contents: `22 1 8:1 / / rw,relatime - ext4 /dev/root rw |
| 20 22 0:19 / /proc rw,nosuid,nodev,noexec - proc proc rw |
| 21 22 0:20 / /sys rw,nosuid,nodev,noexec - sysfs sysfs rw |
| 262 31 0:72 / /tmp/overlay2/0143e063b02f4801de9c847ad1c5ddc21fd2ead00653064d0c72ea967b248870/merged rw,relatime shared:729 - overlay overlay rw,lowerdir=` + overlayLongLowerDir + `,upperdir=/tmp/diff,workdir=/tmp/work |
| 25 21 0:22 / /sys/fs/cgroup rw,nosuid,nodev,noexec - cgroup2 cgroup2 rw |
| `, |
| want: "/sys/fs/cgroup", |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| r := strings.NewReader(tc.contents) |
| read := func(fd int, b []byte) (int, uintptr) { |
| n, err := r.Read(b) |
| if err != nil && err != io.EOF { |
| const dummyErrno = 42 |
| return n, dummyErrno |
| } |
| return n, 0 |
| } |
| |
| var got [cgroup.PathSize]byte |
| var scratch [cgroup.ParseSize]byte |
| n, err := cgroup.ParseCPUMount(0, read, got[:], scratch[:]) |
| if (err != nil) != tc.wantErr { |
| t.Fatalf("parseCPUMount got err %v want %v", err, tc.wantErr) |
| } |
| |
| if string(got[:n]) != tc.want { |
| t.Errorf("parseCPUMount got %q want %q", string(got[:n]), tc.want) |
| } |
| }) |
| } |
| } |
| |
| // escapePath performs escaping equivalent to Linux's show_path. |
| // |
| // That is, '\', ' ', '\t', and '\n' are converted to octal escape sequences, |
| // like '\040' for space. |
| func escapePath(s string) string { |
| out := make([]rune, 0, len(s)) |
| for _, c := range s { |
| switch c { |
| case '\\', ' ', '\t', '\n': |
| out = append(out, '\\') |
| cs := strconv.FormatInt(int64(c), 8) |
| if len(cs) <= 2 { |
| out = append(out, '0') |
| } |
| if len(cs) <= 1 { |
| out = append(out, '0') |
| } |
| for _, csc := range cs { |
| out = append(out, csc) |
| } |
| default: |
| out = append(out, c) |
| } |
| } |
| return string(out) |
| } |
| |
| func TestEscapePath(t *testing.T) { |
| tests := []struct { |
| name string |
| unescaped string |
| escaped string |
| }{ |
| { |
| name: "boring", |
| unescaped: `/a/b/c`, |
| escaped: `/a/b/c`, |
| }, |
| { |
| name: "space", |
| unescaped: `/a/b b/c`, |
| escaped: `/a/b\040b/c`, |
| }, |
| { |
| name: "tab", |
| unescaped: `/a/b b/c`, |
| escaped: `/a/b\011b/c`, |
| }, |
| { |
| name: "newline", |
| unescaped: `/a/b |
| b/c`, |
| escaped: `/a/b\012b/c`, |
| }, |
| { |
| name: "slash", |
| unescaped: `/a/b\b/c`, |
| escaped: `/a/b\134b/c`, |
| }, |
| { |
| name: "beginning", |
| unescaped: `\b/c`, |
| escaped: `\134b/c`, |
| }, |
| { |
| name: "ending", |
| unescaped: `/a/\`, |
| escaped: `/a/\134`, |
| }, |
| } |
| |
| t.Run("escapePath", func(t *testing.T) { |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| got := escapePath(tc.unescaped) |
| if got != tc.escaped { |
| t.Errorf("escapePath got %q want %q", got, tc.escaped) |
| } |
| }) |
| } |
| }) |
| |
| t.Run("unescapePath", func(t *testing.T) { |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| in := []byte(tc.escaped) |
| out := make([]byte, len(in)) |
| n, err := cgroup.UnescapePath(out, in) |
| if err != nil { |
| t.Errorf("unescapePath got err %v want nil", err) |
| } |
| got := string(out[:n]) |
| if got != tc.unescaped { |
| t.Errorf("unescapePath got %q want %q", got, tc.escaped) |
| } |
| }) |
| } |
| }) |
| } |