Skip to content

Commit 6d601ee

Browse files
committed
add agentproc tests
1 parent ae138bf commit 6d601ee

File tree

7 files changed

+359
-58
lines changed

7 files changed

+359
-58
lines changed

agent/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,7 @@ func (a *agent) manageProcessPriorityLoop(ctx context.Context) {
13171317
continue
13181318
}
13191319

1320-
score, err := proc.Nice(a.syscaller)
1320+
score, err := proc.Niceness(a.syscaller)
13211321
if err != nil {
13221322
a.logger.Error(ctx, "unable to get proc niceness",
13231323
slog.F("name", proc.Name()),

agent/agentproc/agentproctest/proc.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package agentproctest
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/spf13/afero"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/v2/agent/agentproc"
11+
"github.com/coder/coder/v2/cryptorand"
12+
)
13+
14+
func GenerateProcess(t *testing.T, fs afero.Fs, dir string) agentproc.Process {
15+
t.Helper()
16+
17+
pid, err := cryptorand.Intn(1<<31 - 1)
18+
require.NoError(t, err)
19+
20+
err = fs.MkdirAll(fmt.Sprintf("/%s/%d", dir, pid), 0555)
21+
require.NoError(t, err)
22+
23+
arg1, err := cryptorand.String(5)
24+
require.NoError(t, err)
25+
26+
arg2, err := cryptorand.String(5)
27+
require.NoError(t, err)
28+
29+
arg3, err := cryptorand.String(5)
30+
require.NoError(t, err)
31+
32+
cmdline := fmt.Sprintf("%s\x00%s\x00%s", arg1, arg2, arg3)
33+
34+
err = afero.WriteFile(fs, fmt.Sprintf("/%s/%d/cmdline", dir, pid), []byte(cmdline), 0444)
35+
require.NoError(t, err)
36+
37+
return agentproc.Process{
38+
PID: int32(pid),
39+
CmdLine: cmdline,
40+
Dir: fmt.Sprintf("%s/%d", dir, pid),
41+
FS: fs,
42+
}
43+
44+
}

agent/agentproc/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
// Package agentproc contains logic for interfacing with local
22
// processes running in the same context as the agent.
33
package agentproc
4+
5+
//go:generate mockgen -destination ./syscallermock_test.go -package agentproc_test github.com/coder/coder/v2/agent/agentproc Syscaller

agent/agentproc/proc.go

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,28 @@
11
package agentproc
22

33
import (
4+
"errors"
45
"path/filepath"
56
"strconv"
67
"strings"
78
"syscall"
89

910
"github.com/spf13/afero"
10-
"golang.org/x/sys/unix"
1111
"golang.org/x/xerrors"
1212
)
1313

1414
const DefaultProcDir = "/proc"
1515

16-
type Syscaller interface {
17-
SetPriority(pid int32, priority int) error
18-
GetPriority(pid int32) (int, error)
19-
Kill(pid int32, sig syscall.Signal) error
20-
}
21-
22-
type UnixSyscaller struct{}
23-
24-
func (UnixSyscaller) SetPriority(pid int32, nice int) error {
25-
err := unix.Setpriority(unix.PRIO_PROCESS, int(pid), nice)
26-
if err != nil {
27-
return xerrors.Errorf("set priority: %w", err)
28-
}
29-
return nil
30-
}
31-
32-
func (UnixSyscaller) GetPriority(pid int32) (int, error) {
33-
nice, err := unix.Getpriority(0, int(pid))
34-
if err != nil {
35-
return 0, xerrors.Errorf("get priority: %w", err)
36-
}
37-
return nice, nil
38-
}
39-
40-
func (UnixSyscaller) Kill(pid int, sig syscall.Signal) error {
41-
err := syscall.Kill(pid, sig)
42-
if err != nil {
43-
return xerrors.Errorf("kill: %w", err)
44-
}
45-
46-
return nil
47-
}
48-
4916
type Process struct {
5017
Dir string
5118
CmdLine string
5219
PID int32
53-
fs afero.Fs
20+
FS afero.Fs
5421
}
5522

5623
func (p *Process) SetOOMAdj(score int) error {
5724
path := filepath.Join(p.Dir, "oom_score_adj")
58-
err := afero.WriteFile(p.fs,
25+
err := afero.WriteFile(p.FS,
5926
path,
6027
[]byte(strconv.Itoa(score)),
6128
0644,
@@ -67,20 +34,20 @@ func (p *Process) SetOOMAdj(score int) error {
6734
return nil
6835
}
6936

70-
func (p *Process) SetNiceness(sc Syscaller, score int) error {
71-
err := sc.SetPriority(p.PID, score)
37+
func (p *Process) Niceness(sc Syscaller) (int, error) {
38+
nice, err := sc.GetPriority(p.PID)
7239
if err != nil {
73-
return xerrors.Errorf("set priority for %q: %w", p.CmdLine, err)
40+
return 0, xerrors.Errorf("get priority for %q: %w", p.CmdLine, err)
7441
}
75-
return nil
42+
return nice, nil
7643
}
7744

78-
func (p *Process) Nice(sc Syscaller) (int, error) {
79-
nice, err := sc.GetPriority(p.PID)
45+
func (p *Process) SetNiceness(sc Syscaller, score int) error {
46+
err := sc.SetPriority(p.PID, score)
8047
if err != nil {
81-
return 0, xerrors.Errorf("get priority for %q: %w", p.CmdLine, err)
48+
return xerrors.Errorf("set priority for %q: %w", p.CmdLine, err)
8249
}
83-
return nice, nil
50+
return nil
8451
}
8552

8653
func (p *Process) Name() string {
@@ -108,7 +75,7 @@ func List(fs afero.Fs, syscaller Syscaller, dir string) ([]*Process, error) {
10875
}
10976

11077
// Check that the process still exists.
111-
exists, err := isProcessExist(syscaller, int32(pid), syscall.Signal(0))
78+
exists, err := isProcessExist(syscaller, int32(pid))
11279
if err != nil {
11380
return nil, xerrors.Errorf("check process exists: %w", err)
11481
}
@@ -128,26 +95,27 @@ func List(fs afero.Fs, syscaller Syscaller, dir string) ([]*Process, error) {
12895
PID: int32(pid),
12996
CmdLine: string(cmdline),
13097
Dir: filepath.Join(dir, entry),
131-
fs: fs,
98+
FS: fs,
13299
})
133100
}
134101

135102
return processes, nil
136103
}
137104

138-
func isProcessExist(syscaller Syscaller, pid int32, sig syscall.Signal) (bool, error) {
139-
err := syscaller.Kill(pid, sig)
105+
func isProcessExist(syscaller Syscaller, pid int32) (bool, error) {
106+
err := syscaller.Kill(pid, syscall.Signal(0))
140107
if err == nil {
141108
return true, nil
142109
}
143110
if err.Error() == "os: process already finished" {
144111
return false, nil
145112
}
146113

147-
errno, ok := err.(syscall.Errno)
148-
if !ok {
114+
var errno syscall.Errno
115+
if !errors.As(err, &errno) {
149116
return false, err
150117
}
118+
151119
switch errno {
152120
case syscall.ESRCH:
153121
return false, nil

agent/agentproc/proc_test.go

Lines changed: 175 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,180 @@
11
package agentproc_test
22

3-
type mockSyscaller struct {
4-
SetPriorityFn func(int32, int) error
3+
import (
4+
"fmt"
5+
"strings"
6+
"syscall"
7+
"testing"
8+
9+
"github.com/golang/mock/gomock"
10+
"github.com/spf13/afero"
11+
"github.com/stretchr/testify/require"
12+
"golang.org/x/xerrors"
13+
14+
"github.com/coder/coder/v2/agent/agentproc"
15+
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
16+
)
17+
18+
func TestList(t *testing.T) {
19+
t.Parallel()
20+
21+
t.Run("OK", func(t *testing.T) {
22+
t.Parallel()
23+
24+
var (
25+
fs = afero.NewMemMapFs()
26+
sc = NewMockSyscaller(gomock.NewController(t))
27+
expectedProcs = make(map[int32]agentproc.Process)
28+
rootDir = agentproc.DefaultProcDir
29+
)
30+
31+
for i := 0; i < 4; i++ {
32+
proc := agentproctest.GenerateProcess(t, fs, rootDir)
33+
expectedProcs[proc.PID] = proc
34+
35+
sc.EXPECT().
36+
Kill(proc.PID, syscall.Signal(0)).
37+
Return(nil)
38+
}
39+
40+
actualProcs, err := agentproc.List(fs, sc, rootDir)
41+
require.NoError(t, err)
42+
require.Len(t, actualProcs, 4)
43+
for _, proc := range actualProcs {
44+
expected, ok := expectedProcs[proc.PID]
45+
require.True(t, ok)
46+
require.Equal(t, expected.PID, proc.PID)
47+
require.Equal(t, expected.CmdLine, proc.CmdLine)
48+
require.Equal(t, expected.Dir, proc.Dir)
49+
}
50+
})
51+
52+
t.Run("FinishedProcess", func(t *testing.T) {
53+
t.Parallel()
54+
55+
var (
56+
fs = afero.NewMemMapFs()
57+
sc = NewMockSyscaller(gomock.NewController(t))
58+
expectedProcs = make(map[int32]agentproc.Process)
59+
rootDir = agentproc.DefaultProcDir
60+
)
61+
62+
for i := 0; i < 3; i++ {
63+
proc := agentproctest.GenerateProcess(t, fs, rootDir)
64+
expectedProcs[proc.PID] = proc
65+
66+
sc.EXPECT().
67+
Kill(proc.PID, syscall.Signal(0)).
68+
Return(nil)
69+
}
70+
71+
// Create a process that's already finished. We're not adding
72+
// it to the map because it should be skipped over.
73+
proc := agentproctest.GenerateProcess(t, fs, rootDir)
74+
sc.EXPECT().
75+
Kill(proc.PID, syscall.Signal(0)).
76+
Return(xerrors.New("os: process already finished"))
77+
78+
actualProcs, err := agentproc.List(fs, sc, rootDir)
79+
require.NoError(t, err)
80+
require.Len(t, actualProcs, 3)
81+
for _, proc := range actualProcs {
82+
expected, ok := expectedProcs[proc.PID]
83+
require.True(t, ok)
84+
require.Equal(t, expected.PID, proc.PID)
85+
require.Equal(t, expected.CmdLine, proc.CmdLine)
86+
require.Equal(t, expected.Dir, proc.Dir)
87+
}
88+
})
89+
90+
t.Run("NoSuchProcess", func(t *testing.T) {
91+
t.Parallel()
92+
93+
var (
94+
fs = afero.NewMemMapFs()
95+
sc = NewMockSyscaller(gomock.NewController(t))
96+
expectedProcs = make(map[int32]agentproc.Process)
97+
rootDir = agentproc.DefaultProcDir
98+
)
99+
100+
for i := 0; i < 3; i++ {
101+
proc := agentproctest.GenerateProcess(t, fs, rootDir)
102+
expectedProcs[proc.PID] = proc
103+
104+
sc.EXPECT().
105+
Kill(proc.PID, syscall.Signal(0)).
106+
Return(nil)
107+
}
108+
109+
// Create a process that doesn't exist. We're not adding
110+
// it to the map because it should be skipped over.
111+
proc := agentproctest.GenerateProcess(t, fs, rootDir)
112+
sc.EXPECT().
113+
Kill(proc.PID, syscall.Signal(0)).
114+
Return(syscall.ESRCH)
115+
116+
actualProcs, err := agentproc.List(fs, sc, rootDir)
117+
require.NoError(t, err)
118+
require.Len(t, actualProcs, 3)
119+
for _, proc := range actualProcs {
120+
expected, ok := expectedProcs[proc.PID]
121+
require.True(t, ok)
122+
require.Equal(t, expected.PID, proc.PID)
123+
require.Equal(t, expected.CmdLine, proc.CmdLine)
124+
require.Equal(t, expected.Dir, proc.Dir)
125+
}
126+
})
5127
}
6128

7-
func (f mockSyscaller) SetPriority(pid int32, nice int) error {
8-
if f.SetPriorityFn == nil {
9-
return nil
10-
}
11-
return f.SetPriorityFn(pid, nice)
129+
// These tests are not very interesting but they provide some modicum of
130+
// confidence.
131+
func TestProcess(t *testing.T) {
132+
t.Parallel()
133+
134+
t.Run("SetOOMAdj", func(t *testing.T) {
135+
t.Parallel()
136+
137+
var (
138+
fs = afero.NewMemMapFs()
139+
dir = agentproc.DefaultProcDir
140+
proc = agentproctest.GenerateProcess(t, fs, agentproc.DefaultProcDir)
141+
expectedScore = -1000
142+
)
143+
144+
err := proc.SetOOMAdj(expectedScore)
145+
require.NoError(t, err)
146+
147+
actualScore, err := afero.ReadFile(fs, fmt.Sprintf("%s/%d/oom_score_adj", dir, proc.PID))
148+
require.NoError(t, err)
149+
require.Equal(t, fmt.Sprintf("%d", expectedScore), strings.TrimSpace(string(actualScore)))
150+
})
151+
152+
t.Run("SetNiceness", func(t *testing.T) {
153+
t.Parallel()
154+
155+
var (
156+
sc = NewMockSyscaller(gomock.NewController(t))
157+
proc = &agentproc.Process{
158+
PID: 32,
159+
}
160+
score = 20
161+
)
162+
163+
sc.EXPECT().SetPriority(proc.PID, score).Return(nil)
164+
err := proc.SetNiceness(sc, score)
165+
require.NoError(t, err)
166+
})
167+
168+
t.Run("Name", func(t *testing.T) {
169+
t.Parallel()
170+
171+
var (
172+
proc = &agentproc.Process{
173+
CmdLine: "helloworld\x00--arg1\x00--arg2",
174+
}
175+
expectedName = "helloworld"
176+
)
177+
178+
require.Equal(t, expectedName, proc.Name())
179+
})
12180
}

0 commit comments

Comments
 (0)