File: manager_linux_test.go

package info (click to toggle)
singularity-container 4.0.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 21,672 kB
  • sloc: asm: 3,857; sh: 2,125; ansic: 1,677; awk: 414; makefile: 110; python: 99
file content (221 lines) | stat: -rw-r--r-- 5,538 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Copyright (c) 2018-2021, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.

package cgroups

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/sylabs/singularity/v4/internal/pkg/test"
	"github.com/sylabs/singularity/v4/internal/pkg/test/tool/require"
)

// This file contains tests that will run under cgroups v1 & v2, and test utility functions.

type (
	CgroupTestFunc func(t *testing.T, systemd bool)
	CgroupTest     struct {
		name     string
		testFunc CgroupTestFunc
	}
)
type CgroupTests []CgroupTest

func TestCgroups(t *testing.T) {
	tests := CgroupTests{
		{
			name:     "GetFromPid",
			testFunc: testGetFromPid,
		},
	}
	runCgroupfsTests(t, tests)
	runSystemdTests(t, tests)
}

func runCgroupfsTests(t *testing.T, tests CgroupTests) {
	t.Run("cgroupfs", func(t *testing.T) {
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				tt.testFunc(t, false)
			})
		}
	})
}

func runSystemdTests(t *testing.T, tests CgroupTests) {
	t.Run("systemd", func(t *testing.T) {
		for _, tt := range tests {
			t.Run(tt.name, func(t *testing.T) {
				tt.testFunc(t, true)
			})
		}
	})
}

func testGetFromPid(t *testing.T, systemd bool) {
	test.EnsurePrivilege(t)
	require.Cgroups(t)

	// We create either a cgroupfs or systemd cgroup initially
	pid, manager, cleanup := testManager(t, systemd)
	defer cleanup()

	// We can only retrieve a cgroupfs managed cgroup from pid
	pidMgr, err := GetManagerForPid(pid)
	if err != nil {
		t.Fatalf("While getting cgroup manager for pid: %v", err)
	}

	relPath, err := manager.GetCgroupRelPath()
	if err != nil {
		t.Fatalf("While getting manager cgroup relative path")
	}

	if pidMgr.group != relPath {
		t.Errorf("Expected %s for cgroup from pid, got %s", manager.group, pidMgr.cgroup)
	}
}

// ensureInt asserts that the content of path is the integer wantInt
func ensureInt(t *testing.T, path string, wantInt int64) {
	file, err := os.Open(path)
	if err != nil {
		t.Errorf("while opening %q: %v", path, err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	hasData := scanner.Scan()
	if !hasData {
		t.Errorf("no data found in %q", path)
	}

	val, err := strconv.ParseInt(scanner.Text(), 10, 64)
	if err != nil {
		t.Errorf("could not parse %q: %v", path, err)
	}

	if val != wantInt {
		t.Errorf("found %d in %q, expected %d", val, path, wantInt)
	}
}

// ensureContainsInt asserts that the content of path contains the integer wantInt
func ensureContainsInt(t *testing.T, path string, wantInt int64) {
	file, err := os.Open(path)
	if err != nil {
		t.Errorf("while opening %q: %v", path, err)
		return
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		val, err := strconv.ParseInt(scanner.Text(), 10, 64)
		if err != nil {
			t.Errorf("could not parse %q: %v", path, err)
		}
		if val == wantInt {
			return
		}
	}

	t.Fatalf("%s did not contain expected value %d", path, wantInt)
}

// ensureStateBecomes asserts that a process pid has any of the wanted
// states, or reaches one of these states in a 5 second window.
func ensureStateBecomes(t *testing.T, pid int, wantStates string) {
	const retries = 5
	const delay = time.Second

	file, err := os.Open(fmt.Sprintf("/proc/%d/status", pid))
	if err != nil {
		t.Error(err)
	}
	defer file.Close()

	procState := ""

	for r := 0; r <= retries; r++ {
		if r > 0 {
			t.Logf("Process %d has state %q, need %q - retrying %d/%d", pid, procState, wantStates, r, retries)
			time.Sleep(delay)
		}

		if _, err := file.Seek(0, io.SeekStart); err != nil {
			t.Fatalf("Could not seek to start of /proc/%d/status", pid)
		}

		scanner := bufio.NewScanner(file)

		for scanner.Scan() {
			// State:	R (running)
			if strings.HasPrefix(scanner.Text(), "State:\t") {
				f := strings.Fields(scanner.Text())
				if len(f) < 2 {
					t.Errorf("Could not check process state - not enough fields: %s", scanner.Text())
				}
				procState = f[1]
			}
		}

		if strings.ContainsAny(procState, wantStates) {
			return
		}
	}

	t.Errorf("Process %d did not reach expected state %q", pid, wantStates)
}

// testManager returns a cgroup manager, that has created a cgroup with a `cat /dev/zero` process,
// and example resource config.
func testManager(t *testing.T, systemd bool) (pid int, manager *Manager, cleanup func()) {
	// Create process to put into a cgroup
	t.Log("Creating test process")
	cmd := exec.Command("/bin/cat", "/dev/zero")
	if err := cmd.Start(); err != nil {
		t.Fatalf("While starting test process: %v", err)
	}
	pid = cmd.Process.Pid
	strPid := strconv.Itoa(pid)
	group := filepath.Join("/singularity", strPid)
	if systemd {
		group = "system.slice:singularity:" + strPid
	}

	cgroupsToml := "example/cgroups.toml"
	// Some systems, e.g. ppc64le may not have a 2MB page size, so don't
	// apply a 2MB hugetlb limit if that's the case.
	_, err := os.Stat("/sys/fs/cgroup/dev-hugepages.mount/hugetlb.2MB.max")
	if os.IsNotExist(err) {
		t.Log("No hugetlb.2MB.max - using alternate cgroups test file")
		cgroupsToml = "example/cgroups-no-hugetlb.toml"
	}

	manager, err = NewManagerWithFile(cgroupsToml, pid, group, systemd)
	if err != nil {
		t.Fatalf("While creating new cgroup: %v", err)
	}

	cleanup = func() {
		cmd.Process.Kill()
		cmd.Process.Wait()
		manager.Destroy()
	}

	return pid, manager, cleanup
}