Skip to content

Commit 2beec75

Browse files
[procmgr] Cross-platform E2E tests and Windows install-test expectations (#49871)
### What does this PR do? Extracts shared procmgr E2E test logic into a `baseProcmgrSuite` with a `platformConfig` abstraction, so the same 6 smoke tests (`TestBinariesExist`, `TestServiceRunning`, `TestCLIStatus`, `TestCLIListShowsConfiguredProcess`, `TestCLIDescribe`, `TestConditionPathExistsSkipsMissingBinary`) run on both Linux and Windows without code duplication. Adds `procmgr_win_test.go` with a Windows-specific provisioner (`WindowsServerDefault` EC2) and PowerShell-based service/binary checks. Adds `test/new-e2e/tests/agent-runtimes/**/*` to the `windows_path_3` change-detection list in `.gitlab-ci.yml` so that changes to agent-runtimes E2E tests trigger the Windows CI pipeline (MSI build + Windows E2E jobs). ### Motivation `dd-procmgrd` was integrated into the Windows MSI installer (S23) but had no E2E test coverage for the Windows platform. The existing Linux procmgr E2E tests validated the CLI and service behavior but were not reusable across platforms. This PR closes that gap by making the tests cross-platform and ensuring the Windows E2E pipeline is triggered when agent-runtimes tests change. ### Describe how you validated your changes - `go vet ./test/new-e2e/tests/agent-runtimes/procmgr/...` passes - All pre-commit hooks pass (go-fmt, copyright, update-go, etc.) - Manual verification on a Windows EC2 VM (S23 CI build) confirmed `dd-procmgrd` installs, starts via SCM, responds to CLI commands, and logs to `C:\ProgramData\Datadog\logs\dd-procmgr.log` - Linux procmgr E2E suite passes in CI - Windows procmgr E2E suite passes in CI ### Additional Notes Co-authored-by: josemanuel.almaza <josemanuel.almaza@datadoghq.com>
1 parent 0f019b9 commit 2beec75

4 files changed

Lines changed: 403 additions & 215 deletions

File tree

.gitlab-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
- test/new-e2e/tests/windows/**/*
112112
- test/new-e2e/tests/sysprobe-functional/**/*
113113
- test/new-e2e/tests/process/**/*
114+
- test/new-e2e/tests/agent-runtimes/**/*
114115

115116
include:
116117
- local: .adms/**/*.yaml
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)