Skip to content

Commit 8c65216

Browse files
committed
add agentproc tests
1 parent 7e59db6 commit 8c65216

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy