Skip to content

Commit b9ba1a1

Browse files
[procmgr] Add cross-platform E2E tests for process manager
Introduce shared E2E test logic for dd-procmgr on both Linux and Windows using the new-e2e framework: - baseProcmgrSuite with platformConfig to abstract OS differences - Linux suite (procmgrLinuxSuite) using /bin/sleep - Windows suite (procmgrWindowsSuite) using PowerShell Start-Sleep with explicit env vars for the env-cleared child process - Tests: service status, CLI describe, CLI list, missing binary skip - Windows SetupSuite explicitly starts the DEMAND_START service
1 parent 59d9551 commit b9ba1a1

3 files changed

Lines changed: 402 additions & 215 deletions

File tree

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package procmgr
7+
8+
import (
9+
"fmt"
10+
"strings"
11+
"time"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/DataDog/datadog-agent/test/e2e-framework/testing/e2e"
17+
"github.com/DataDog/datadog-agent/test/e2e-framework/testing/environments"
18+
)
19+
20+
// platformConfig holds all platform-specific paths, commands, and config
21+
// snippets so that the shared test methods in baseProcmgrSuite work on both
22+
// Linux and Windows without branching.
23+
type platformConfig struct {
24+
daemonBin string // path to dd-procmgrd binary
25+
cliBin string // path to dd-procmgr CLI binary
26+
configDir string // processes.d directory for agent file provisioning
27+
sleepCommand string // expected COMMAND column value in "list" output
28+
29+
testProcessYAML string // YAML config that starts a long-running sleep process
30+
missingBinaryYAML string // YAML config whose condition_path_exists prevents start
31+
32+
// checkBinCmd returns a shell command that succeeds (exit 0) when the
33+
// given binary path exists on the remote host.
34+
checkBinCmd func(path string) string
35+
36+
// checkSvcRunning is a shell command whose trimmed stdout indicates the
37+
// service is running (compared against svcRunningOutput).
38+
checkSvcRunning string
39+
svcRunningOutput string
40+
41+
// cliCmd returns the full shell command to invoke the procmgr CLI with
42+
// the given arguments (handles quoting differences between bash and
43+
// PowerShell).
44+
cliCmd func(args string) string
45+
}
46+
47+
type baseProcmgrSuite struct {
48+
e2e.BaseSuite[environments.Host]
49+
platform platformConfig
50+
hasCLI bool
51+
}
52+
53+
func (s *baseProcmgrSuite) SetupSuite() {
54+
s.BaseSuite.SetupSuite()
55+
defer s.CleanupOnSetupFailure()
56+
57+
_, err := s.Env().RemoteHost.Execute(s.platform.checkBinCmd(s.platform.daemonBin))
58+
if err != nil {
59+
s.T().Skip("procmgr daemon not included in this agent package; skipping process manager tests")
60+
}
61+
62+
_, err = s.Env().RemoteHost.Execute(s.platform.checkBinCmd(s.platform.cliBin))
63+
s.hasCLI = err == nil
64+
}
65+
66+
func (s *baseProcmgrSuite) requireCLI() {
67+
s.T().Helper()
68+
if !s.hasCLI {
69+
s.T().Skip("dd-procmgr CLI not included in this agent package")
70+
}
71+
}
72+
73+
// ---------------------------------------------------------------------------
74+
// Shared tests — run on both Linux and Windows
75+
// ---------------------------------------------------------------------------
76+
77+
func (s *baseProcmgrSuite) TestBinariesExist() {
78+
s.Env().RemoteHost.MustExecute(s.platform.checkBinCmd(s.platform.daemonBin))
79+
80+
if !s.hasCLI {
81+
s.T().Skip("dd-procmgr CLI not included in this agent package")
82+
}
83+
s.Env().RemoteHost.MustExecute(s.platform.checkBinCmd(s.platform.cliBin))
84+
}
85+
86+
func (s *baseProcmgrSuite) TestServiceRunning() {
87+
require.EventuallyWithT(s.T(), func(t *assert.CollectT) {
88+
out, err := s.Env().RemoteHost.Execute(s.platform.checkSvcRunning)
89+
assert.NoError(t, err)
90+
assert.Equal(t, s.platform.svcRunningOutput, strings.TrimSpace(out))
91+
}, 30*time.Second, 2*time.Second)
92+
}
93+
94+
func (s *baseProcmgrSuite) TestCLIStatus() {
95+
s.requireCLI()
96+
require.EventuallyWithT(s.T(), func(t *assert.CollectT) {
97+
out := s.Env().RemoteHost.MustExecute(s.platform.cliCmd("status"))
98+
assertHasField(t, out, "Version")
99+
assertHasField(t, out, "Uptime")
100+
}, 30*time.Second, 2*time.Second)
101+
}
102+
103+
func (s *baseProcmgrSuite) TestCLIListShowsConfiguredProcess() {
104+
s.requireCLI()
105+
require.EventuallyWithT(s.T(), func(t *assert.CollectT) {
106+
out := s.Env().RemoteHost.MustExecute(s.platform.cliCmd("list"))
107+
assertTableRow(t, out, "test-sleep", map[string]string{
108+
"STATE": "Running",
109+
"COMMAND": s.platform.sleepCommand,
110+
})
111+
}, 30*time.Second, 2*time.Second)
112+
}
113+
114+
func (s *baseProcmgrSuite) TestCLIDescribe() {
115+
s.requireCLI()
116+
require.EventuallyWithT(s.T(), func(t *assert.CollectT) {
117+
out := s.Env().RemoteHost.MustExecute(s.platform.cliCmd("describe test-sleep"))
118+
assertField(t, out, "Name", "test-sleep")
119+
assertField(t, out, "State", "Running")
120+
assertField(t, out, "Command", s.platform.sleepCommand)
121+
}, 30*time.Second, 2*time.Second)
122+
}
123+
124+
func (s *baseProcmgrSuite) TestConditionPathExistsSkipsMissingBinary() {
125+
s.requireCLI()
126+
require.EventuallyWithT(s.T(), func(t *assert.CollectT) {
127+
out := s.Env().RemoteHost.MustExecute(s.platform.cliCmd("list"))
128+
assertTableRow(t, out, "missing-binary", map[string]string{
129+
"STATE": "Created",
130+
"PID": "-",
131+
})
132+
}, 30*time.Second, 2*time.Second)
133+
}
134+
135+
// ---------------------------------------------------------------------------
136+
// CLI output parsing helpers
137+
// ---------------------------------------------------------------------------
138+
139+
func fieldValue(output, label string) string {
140+
needle := label + ":"
141+
for _, line := range strings.Split(output, "\n") {
142+
trimmed := strings.TrimSpace(line)
143+
if strings.HasPrefix(trimmed, needle) {
144+
return strings.TrimSpace(trimmed[len(needle):])
145+
}
146+
}
147+
return ""
148+
}
149+
150+
func assertField(t assert.TestingT, output, label, expected string) {
151+
needle := label + ":"
152+
for _, line := range strings.Split(output, "\n") {
153+
trimmed := strings.TrimSpace(line)
154+
if strings.HasPrefix(trimmed, needle) {
155+
actual := strings.TrimSpace(trimmed[len(needle):])
156+
assert.Equal(t, expected, actual, "field %q", label)
157+
return
158+
}
159+
}
160+
assert.Fail(t, fmt.Sprintf("field %q not found in output:\n%s", label, output))
161+
}
162+
163+
func assertHasField(t assert.TestingT, output, label string) {
164+
needle := label + ":"
165+
for _, line := range strings.Split(output, "\n") {
166+
if strings.HasPrefix(strings.TrimSpace(line), needle) {
167+
return
168+
}
169+
}
170+
assert.Fail(t, fmt.Sprintf("field %q not found in output:\n%s", label, output))
171+
}
172+
173+
func assertTableRow(t assert.TestingT, output, rowName string, expected map[string]string) {
174+
lines := strings.Split(strings.TrimRight(output, "\n"), "\n")
175+
if !assert.GreaterOrEqual(t, len(lines), 2, "list output should have header + at least one row") {
176+
return
177+
}
178+
header := lines[0]
179+
columns := parseTableColumns(header)
180+
181+
for _, line := range lines[1:] {
182+
name := extractColumn(line, 0, columns)
183+
if name != rowName {
184+
continue
185+
}
186+
for col, want := range expected {
187+
idx := -1
188+
for i, c := range columns {
189+
if c.name == col {
190+
idx = i
191+
break
192+
}
193+
}
194+
if !assert.NotEqual(t, -1, idx, "column %q not in header: %s", col, header) {
195+
continue
196+
}
197+
got := extractColumn(line, idx, columns)
198+
assert.Equal(t, want, got, "row %q column %q", rowName, col)
199+
}
200+
return
201+
}
202+
assert.Fail(t, fmt.Sprintf("row %q not found in table output:\n%s", rowName, output))
203+
}
204+
205+
type tableColumn struct {
206+
name string
207+
start int
208+
}
209+
210+
func parseTableColumns(header string) []tableColumn {
211+
var cols []tableColumn
212+
i := 0
213+
for i < len(header) {
214+
for i < len(header) && header[i] == ' ' {
215+
i++
216+
}
217+
if i >= len(header) {
218+
break
219+
}
220+
start := i
221+
for i < len(header) {
222+
if header[i] == ' ' {
223+
j := i
224+
for j < len(header) && header[j] == ' ' {
225+
j++
226+
}
227+
if j >= len(header) || (j-i >= 2) {
228+
break
229+
}
230+
i = j
231+
} else {
232+
i++
233+
}
234+
}
235+
cols = append(cols, tableColumn{name: header[start:i], start: start})
236+
}
237+
return cols
238+
}
239+
240+
func extractColumn(line string, idx int, columns []tableColumn) string {
241+
if idx >= len(columns) {
242+
return ""
243+
}
244+
start := columns[idx].start
245+
end := len(line)
246+
if idx+1 < len(columns) {
247+
end = columns[idx+1].start
248+
}
249+
if start >= len(line) {
250+
return ""
251+
}
252+
if end > len(line) {
253+
end = len(line)
254+
}
255+
return strings.TrimSpace(line[start:end])
256+
}

0 commit comments

Comments
 (0)