Merge pull request #1330 from DigitalVeer/cpm-generate-status-tests

Add unit tests for the custom plugin monitor
This commit is contained in:
kubernetes-prow[bot]
2026-08-22 08:23:16 +00:00
committed by GitHub
@@ -17,15 +17,706 @@ limitations under the License.
package custompluginmonitor
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/node-problem-detector/pkg/custompluginmonitor/plugin"
cpmtypes "k8s.io/node-problem-detector/pkg/custompluginmonitor/types"
"k8s.io/node-problem-detector/pkg/problemdaemon"
"k8s.io/node-problem-detector/pkg/problemmetrics"
"k8s.io/node-problem-detector/pkg/types"
"k8s.io/node-problem-detector/pkg/util"
"k8s.io/node-problem-detector/pkg/util/metrics"
"k8s.io/node-problem-detector/pkg/util/tomb"
)
const (
testSource = "cpm-test"
testConfigPath = "test-config-path"
testCondition = "TestCondition"
testConditionOK = "TestConditionOK"
testConditionOKMsg = "test condition is ok"
otherCondition = "OtherCondition"
otherConditionOK = "OtherConditionOK"
otherOKMsg = "other condition is ok"
testProblemReason = "TestConditionProblem"
testTempReason = "TestTempProblem"
testWait = 3 * time.Second
settleWait = 200 * time.Millisecond
)
// testEpoch is a fixed past instant stamped into Transition before a test runs.
var testEpoch = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
func TestRegistration(t *testing.T) {
assert.NotPanics(t,
func() { problemdaemon.GetProblemDaemonHandlerOrDie("custom-plugin-monitor") },
"Custom plugin monitor failed to register itself as a problem daemon.")
}
// testOptions holds fields the tests and structs below may need.
type testOptions struct {
defaultConditions []types.Condition
rules []*cpmtypes.CustomRule
messageChange bool
enableMetricsReporting bool
skipInitialStatus bool
}
// defaultTestConditions returns two conditions. The condition under test is second.
func defaultTestConditions() []types.Condition {
return []types.Condition{
{Type: otherCondition, Reason: otherConditionOK, Message: otherOKMsg},
{Type: testCondition, Reason: testConditionOK, Message: testConditionOKMsg},
}
}
// newTestConfig builds a config with the defaults applied.
func newTestConfig(t *testing.T, o testOptions) cpmtypes.CustomPluginConfig {
t.Helper()
cfg := cpmtypes.CustomPluginConfig{
Plugin: "custom",
Source: testSource,
DefaultConditions: o.defaultConditions,
Rules: o.rules,
}
require.NoError(t, (&cfg).ApplyConfiguration())
messageChange := o.messageChange
metricsReporting := o.enableMetricsReporting
skipInitialStatus := o.skipInitialStatus
cfg.PluginGlobalConfig.EnableMessageChangeBasedConditionUpdate = &messageChange
cfg.PluginGlobalConfig.SkipInitialStatus = &skipInitialStatus
cfg.EnableMetricsReporting = &metricsReporting
return cfg
}
// newTestMonitor builds a monitor with its conditions initialized.
func newTestMonitor(t *testing.T, o testOptions) *customPluginMonitor {
t.Helper()
cfg := newTestConfig(t, o)
c := &customPluginMonitor{
configPath: testConfigPath,
config: cfg,
plugin: plugin.NewPlugin(cfg),
statusChan: make(chan *types.Status, 1000),
tomb: tomb.NewTomb(),
}
c.initializeConditions()
for i := range c.conditions {
c.conditions[i].Transition = testEpoch
}
return c
}
// testPluginScript returns the path of a script in the plugin package's test data.
func testPluginScript(name string) string {
ext := "sh"
if runtime.GOOS == "windows" {
ext = "cmd"
}
return filepath.Join("plugin", "test-data", name+"."+ext)
}
// permResult builds a permanent-rule result for the condition under test.
func permResult(status cpmtypes.Status, reason, message string) cpmtypes.Result {
return cpmtypes.Result{
Rule: &cpmtypes.CustomRule{Type: types.Perm, Condition: testCondition, Reason: reason},
ExitStatus: status,
Message: message,
}
}
// stubProblemMetrics replaces the global metrics manager with a stub. Do not call t.Parallel.
func stubProblemMetrics(t *testing.T) (*metrics.FakeInt64Metric, *metrics.FakeInt64Metric) {
t.Helper()
original := problemmetrics.GlobalProblemMetricsManager
t.Cleanup(func() { problemmetrics.GlobalProblemMetricsManager = original })
pmm, counter, gauge := problemmetrics.NewProblemMetricsManagerStub()
problemmetrics.GlobalProblemMetricsManager = pmm
return counter, gauge
}
// receiveStatus returns the next status from the channel or fails the test after testWait.
func receiveStatus(t *testing.T, ch <-chan *types.Status) *types.Status {
t.Helper()
select {
case status := <-ch:
return status
case <-time.After(testWait):
t.Fatal("timed out waiting for a status")
return nil
}
}
// requireNoStatus fails the test if a status arrives within settleWait.
func requireNoStatus(t *testing.T, ch <-chan *types.Status) {
t.Helper()
select {
case status := <-ch:
t.Fatalf("expected no status, got %+v", status)
case <-time.After(settleWait):
}
}
// requireStop calls Stop and fails the test if Stop does not return within testWait.
func requireStop(t *testing.T, c *customPluginMonitor) {
t.Helper()
stopped := make(chan struct{})
go func() {
c.Stop()
close(stopped)
}()
select {
case <-stopped:
case <-time.After(testWait):
t.Fatal("Stop did not return")
}
}
// step is one generateStatus call plus everything expected right after it.
type step struct {
exitStatus cpmtypes.Status
ruleReason string
message string
wantStatus types.ConditionStatus
wantReason string
wantMessage string
wantEvent bool
}
func TestGenerateStatusForConditions(t *testing.T) {
testCases := []struct {
name string
messageChange bool
ruleCondition string
steps []step
}{
{
name: "stays false, no event",
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.OK, ruleReason: testProblemReason, message: "still fine",
wantStatus: types.False, wantReason: testConditionOK, wantMessage: testConditionOKMsg, wantEvent: false,
},
{
exitStatus: cpmtypes.OK, ruleReason: testProblemReason, message: "still fine",
wantStatus: types.False, wantReason: testConditionOK, wantMessage: testConditionOKMsg, wantEvent: false,
},
},
},
{
name: "false to true then back to false restores the default",
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "boom",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "boom", wantEvent: true,
},
{
exitStatus: cpmtypes.OK, ruleReason: testProblemReason, message: "all good",
wantStatus: types.False, wantReason: testConditionOK, wantMessage: testConditionOKMsg, wantEvent: true,
},
},
},
{
name: "true to unknown keeps the result message",
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "boom",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "boom", wantEvent: true,
},
{
exitStatus: cpmtypes.Unknown, ruleReason: testProblemReason, message: "plugin timed out",
wantStatus: types.Unknown, wantReason: testConditionOK, wantMessage: "plugin timed out", wantEvent: true,
},
},
},
{
name: "unknown transitions",
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.Unknown, ruleReason: testProblemReason, message: "cannot exec",
wantStatus: types.Unknown, wantReason: testConditionOK, wantMessage: "cannot exec", wantEvent: true,
},
{
exitStatus: cpmtypes.Unknown, ruleReason: testProblemReason, message: "cannot exec either",
wantStatus: types.Unknown, wantReason: testConditionOK, wantMessage: "cannot exec", wantEvent: false,
},
{
exitStatus: cpmtypes.OK, ruleReason: testProblemReason, message: "all good",
wantStatus: types.False, wantReason: testConditionOK, wantMessage: testConditionOKMsg, wantEvent: true,
},
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "boom",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "boom", wantEvent: true,
},
},
},
{
name: "reason change while true updates",
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: "ReasonA", message: "m",
wantStatus: types.True, wantReason: "ReasonA", wantMessage: "m", wantEvent: true,
},
{
exitStatus: cpmtypes.NonOK, ruleReason: "ReasonB", message: "m",
wantStatus: types.True, wantReason: "ReasonB", wantMessage: "m", wantEvent: true,
},
},
},
{
name: "message change ignored when the flag is off",
messageChange: false,
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "first",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "first", wantEvent: true,
},
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "second",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "first", wantEvent: false,
},
},
},
{
name: "message change applied when the flag is on",
messageChange: true,
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "first",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "first", wantEvent: true,
},
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "second",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "second", wantEvent: true,
},
},
},
{
name: "identical result while true does not update",
messageChange: true,
ruleCondition: testCondition,
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "same",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "same", wantEvent: true,
},
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "same",
wantStatus: types.True, wantReason: testProblemReason, wantMessage: "same", wantEvent: false,
},
},
},
{
name: "rule for an unknown condition changes nothing",
ruleCondition: "NoSuchCondition",
steps: []step{
{
exitStatus: cpmtypes.NonOK, ruleReason: testProblemReason, message: "boom",
wantStatus: types.False, wantReason: testConditionOK, wantMessage: testConditionOKMsg, wantEvent: false,
},
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
c := newTestMonitor(t, testOptions{
defaultConditions: defaultTestConditions(),
messageChange: test.messageChange,
})
for i, s := range test.steps {
result := cpmtypes.Result{
Rule: &cpmtypes.CustomRule{
Type: types.Perm,
Condition: test.ruleCondition,
Reason: s.ruleReason,
},
ExitStatus: s.exitStatus,
Message: s.message,
}
got := c.generateStatus(result)
// status.Conditions is an alias for c.conditions.
require.Len(t, got.Conditions, 2, "step %d", i)
assert.Equal(t, testSource, got.Source, "step %d", i)
cond := got.Conditions[1]
assert.Equal(t, testCondition, cond.Type, "step %d", i)
assert.Equal(t, s.wantStatus, cond.Status, "step %d", i)
assert.Equal(t, s.wantReason, cond.Reason, "step %d", i)
assert.Equal(t, s.wantMessage, cond.Message, "step %d", i)
// The rule does not target the first condition, so it must not change.
other := got.Conditions[0]
assert.Equal(t, types.False, other.Status, "step %d", i)
assert.Equal(t, otherConditionOK, other.Reason, "step %d", i)
assert.Equal(t, otherOKMsg, other.Message, "step %d", i)
assert.True(t, other.Transition.Equal(testEpoch), "step %d", i)
if !s.wantEvent {
assert.Empty(t, got.Events, "step %d", i)
continue
}
require.Len(t, got.Events, 1, "step %d", i)
event := got.Events[0]
// Assert severity literally; the want below reuses the production helper.
wantSeverity := types.Info
if s.wantStatus == types.True {
wantSeverity = types.Warn
}
assert.Equal(t, wantSeverity, event.Severity, "step %d", i)
// Feed the observed timestamp back so no clock value is asserted.
want := util.GenerateConditionChangeEvent(
testCondition, s.wantStatus, s.wantReason, s.wantMessage, event.Timestamp)
assert.Equal(t, want, event, "step %d", i)
assert.True(t, cond.Transition.Equal(event.Timestamp), "step %d", i)
assert.False(t, cond.Transition.Equal(testEpoch), "step %d", i)
}
})
}
}
func TestGenerateStatusForTemporaryProblem(t *testing.T) {
testCases := []struct {
name string
exitStatus cpmtypes.Status
wantEvent bool
}{
{name: "ok produces no event", exitStatus: cpmtypes.OK, wantEvent: false},
{name: "non-ok produces an event", exitStatus: cpmtypes.NonOK, wantEvent: true},
{name: "unknown produces an event", exitStatus: cpmtypes.Unknown, wantEvent: true},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
c := newTestMonitor(t, testOptions{defaultConditions: defaultTestConditions()})
result := cpmtypes.Result{
// Condition is set on purpose. A temporary rule must ignore it.
Rule: &cpmtypes.CustomRule{Type: types.Temp, Condition: testCondition, Reason: testTempReason},
ExitStatus: test.exitStatus,
Message: "temporary problem detail",
}
got := c.generateStatus(result)
assert.Equal(t, testSource, got.Source)
if !test.wantEvent {
assert.Empty(t, got.Events)
} else {
require.Len(t, got.Events, 1)
event := got.Events[0]
assert.Equal(t, types.Warn, event.Severity)
assert.Equal(t, testTempReason, event.Reason)
// A temporary event carries the raw plugin output.
assert.Equal(t, "temporary problem detail", event.Message)
assert.False(t, event.Timestamp.IsZero())
}
// No condition may move.
require.Len(t, got.Conditions, 2)
for _, cond := range got.Conditions {
assert.Equal(t, types.False, cond.Status)
assert.True(t, cond.Transition.Equal(testEpoch))
}
})
}
}
func TestGenerateStatusMetrics(t *testing.T) {
testCases := []struct {
name string
enableMetricsReporting bool
results []cpmtypes.Result
expectedMetrics []metrics.Int64MetricRepresentation
}{
{
name: "reporting disabled records nothing",
enableMetricsReporting: false,
results: []cpmtypes.Result{permResult(cpmtypes.NonOK, testProblemReason, "boom")},
expectedMetrics: []metrics.Int64MetricRepresentation{},
},
{
name: "problem detected increments the counter and raises the gauge",
enableMetricsReporting: true,
results: []cpmtypes.Result{permResult(cpmtypes.NonOK, testProblemReason, "boom")},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testProblemReason}, Value: 1},
{Name: "problem_gauge", Labels: map[string]string{"type": otherCondition, "reason": otherConditionOK}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testProblemReason}, Value: 1},
},
},
{
name: "problem resolved does not increment the counter",
enableMetricsReporting: true,
results: []cpmtypes.Result{
permResult(cpmtypes.NonOK, testProblemReason, "boom"),
permResult(cpmtypes.OK, testProblemReason, "all good"),
},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testProblemReason}, Value: 1},
{Name: "problem_gauge", Labels: map[string]string{"type": otherCondition, "reason": otherConditionOK}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testProblemReason}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testConditionOK}, Value: 0},
},
},
{
name: "unknown condition is not an active problem",
enableMetricsReporting: true,
results: []cpmtypes.Result{permResult(cpmtypes.Unknown, testProblemReason, "cannot exec")},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_gauge", Labels: map[string]string{"type": otherCondition, "reason": otherConditionOK}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testConditionOK}, Value: 0},
},
},
{
name: "temporary problem increments the counter and refreshes every gauge",
enableMetricsReporting: true,
results: []cpmtypes.Result{{
Rule: &cpmtypes.CustomRule{Type: types.Temp, Reason: testTempReason},
ExitStatus: cpmtypes.NonOK,
Message: "temporary problem detail",
}},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testTempReason}, Value: 1},
{Name: "problem_gauge", Labels: map[string]string{"type": otherCondition, "reason": otherConditionOK}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testConditionOK}, Value: 0},
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
fakeCounter, fakeGauge := stubProblemMetrics(t)
c := newTestMonitor(t, testOptions{
defaultConditions: defaultTestConditions(),
enableMetricsReporting: test.enableMetricsReporting,
})
for _, result := range test.results {
c.generateStatus(result)
}
gotMetrics := append(fakeCounter.ListMetrics(), fakeGauge.ListMetrics()...)
assert.ElementsMatch(t, test.expectedMetrics, gotMetrics,
"expected metrics: %+v, got: %+v", test.expectedMetrics, gotMetrics)
})
}
}
func TestGenerateStatusSurvivesMetricsFailure(t *testing.T) {
original := problemmetrics.GlobalProblemMetricsManager
t.Cleanup(func() { problemmetrics.GlobalProblemMetricsManager = original })
problemmetrics.GlobalProblemMetricsManager = &problemmetrics.ProblemMetricsManager{}
c := newTestMonitor(t, testOptions{
defaultConditions: defaultTestConditions(),
enableMetricsReporting: true,
})
var got *types.Status
require.NotPanics(t, func() {
got = c.generateStatus(permResult(cpmtypes.NonOK, testProblemReason, "boom"))
})
// A metrics backend failure must not change what is reported.
require.Len(t, got.Conditions, 2)
assert.Equal(t, types.True, got.Conditions[1].Status)
assert.Equal(t, testProblemReason, got.Conditions[1].Reason)
assert.Equal(t, "boom", got.Conditions[1].Message)
require.Len(t, got.Events, 1)
assert.Equal(t, types.Warn, got.Events[0].Severity)
}
func TestGenerateStatusPreservesConfiguredDefaults(t *testing.T) {
c := newTestMonitor(t, testOptions{defaultConditions: defaultTestConditions()})
wantReason := c.config.DefaultConditions[1].Reason
wantMessage := c.config.DefaultConditions[1].Message
c.generateStatus(permResult(cpmtypes.NonOK, testProblemReason, "boom"))
got := c.generateStatus(permResult(cpmtypes.OK, testProblemReason, "all good"))
require.Len(t, got.Conditions, 2)
assert.Equal(t, types.False, got.Conditions[1].Status)
assert.Equal(t, wantReason, got.Conditions[1].Reason)
assert.Equal(t, wantMessage, got.Conditions[1].Message)
assert.Equal(t, wantReason, c.config.DefaultConditions[1].Reason)
assert.Equal(t, wantMessage, c.config.DefaultConditions[1].Message)
assert.Empty(t, c.config.DefaultConditions[1].Status)
}
func TestMonitorLoopReportsPluginResults(t *testing.T) {
// A one-day interval makes sure the plugin runs only once.
interval := 24 * time.Hour
intervalString := interval.String()
rule := &cpmtypes.CustomRule{
Type: types.Perm,
Condition: testCondition,
Reason: testProblemReason,
Path: testPluginScript("non-ok"),
InvokeIntervalString: &intervalString,
}
c := newTestMonitor(t, testOptions{
defaultConditions: defaultTestConditions(),
rules: []*cpmtypes.CustomRule{rule},
skipInitialStatus: true,
})
statusChan, err := c.Start()
require.NoError(t, err)
status := receiveStatus(t, statusChan)
require.Len(t, status.Conditions, 2)
assert.Equal(t, types.True, status.Conditions[1].Status)
assert.Equal(t, testProblemReason, status.Conditions[1].Reason)
require.Len(t, status.Events, 1)
assert.Equal(t, types.Warn, status.Events[0].Severity)
requireStop(t, c)
// Stop must also shut down the plugin it owns.
select {
case _, open := <-c.plugin.GetResultChan():
assert.False(t, open, "Stop did not close the plugin result channel")
case <-time.After(testWait):
t.Fatal("Stop did not close the plugin result channel")
}
}
func TestMonitorLoopExitsWhenResultChannelCloses(t *testing.T) {
c := newTestMonitor(t, testOptions{defaultConditions: defaultTestConditions()})
// monitorLoop must initialize the conditions itself.
c.conditions = nil
startedAt := time.Now()
// Run monitorLoop directly; Stop would deadlock on the closed-channel path.
go c.plugin.Run()
returned := make(chan struct{})
go func() {
c.monitorLoop()
close(returned)
}()
status := receiveStatus(t, c.statusChan)
assert.Equal(t, testSource, status.Source)
assert.Empty(t, status.Events)
require.Len(t, status.Conditions, 2)
for _, cond := range status.Conditions {
assert.Equal(t, types.False, cond.Status)
assert.WithinRange(t, cond.Transition, startedAt, time.Now())
}
// Stopping the plugin closes its result channel.
c.plugin.Stop()
requireNoStatus(t, c.statusChan)
select {
case <-returned:
case <-time.After(testWait):
t.Fatal("monitorLoop did not return after the result channel closed")
}
}
func TestInitializeProblemMetricsOrDie(t *testing.T) {
testCases := []struct {
name string
rules []*cpmtypes.CustomRule
expectedMetrics []metrics.Int64MetricRepresentation
}{
{
name: "no rules",
rules: nil,
expectedMetrics: []metrics.Int64MetricRepresentation{},
},
{
name: "temporary rule gets a counter but no gauge",
rules: []*cpmtypes.CustomRule{{Type: types.Temp, Reason: testTempReason}},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testTempReason}, Value: 0},
},
},
{
name: "permanent rule gets a counter and a gauge",
rules: []*cpmtypes.CustomRule{
{Type: types.Perm, Condition: testCondition, Reason: testProblemReason},
},
expectedMetrics: []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testProblemReason}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testProblemReason}, Value: 0},
},
},
}
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
fakeCounter, fakeGauge := stubProblemMetrics(t)
initializeProblemMetricsOrDie(test.rules)
gotMetrics := append(fakeCounter.ListMetrics(), fakeGauge.ListMetrics()...)
assert.ElementsMatch(t, test.expectedMetrics, gotMetrics,
"expected metrics: %+v, got: %+v", test.expectedMetrics, gotMetrics)
})
}
}
func TestNewCustomPluginMonitorOrDie(t *testing.T) {
fakeCounter, fakeGauge := stubProblemMetrics(t)
raw, err := json.Marshal(map[string]any{
"plugin": "custom",
"pluginConfig": map[string]any{
"invoke_interval": "10s",
"timeout": "5s",
},
"source": testSource,
"conditions": []map[string]any{
{"type": testCondition, "reason": testConditionOK, "message": testConditionOKMsg},
},
"rules": []map[string]any{
{
"type": "permanent",
"condition": testCondition,
"reason": testProblemReason,
"path": testPluginScript("ok"),
},
},
})
require.NoError(t, err)
configPath := filepath.Join(t.TempDir(), "config.json")
require.NoError(t, os.WriteFile(configPath, raw, 0o600))
monitor := NewCustomPluginMonitorOrDie(configPath)
require.NotNil(t, monitor)
c, ok := monitor.(*customPluginMonitor)
require.True(t, ok)
assert.Equal(t, configPath, c.configPath)
assert.Equal(t, testSource, c.config.Source)
require.Len(t, c.config.Rules, 1)
assert.Equal(t, testProblemReason, c.config.Rules[0].Reason)
// ApplyConfiguration must set these pointers before use.
require.NotNil(t, c.config.PluginGlobalConfig.InvokeInterval)
assert.Equal(t, 10*time.Second, *c.config.PluginGlobalConfig.InvokeInterval)
require.NotNil(t, c.config.PluginGlobalConfig.Timeout)
require.NotNil(t, c.config.PluginGlobalConfig.Concurrency)
require.NotNil(t, c.config.PluginGlobalConfig.MaxOutputLength)
require.NotNil(t, c.config.PluginGlobalConfig.SkipInitialStatus)
require.NotNil(t, c.config.EnableMetricsReporting)
assert.True(t, *c.config.EnableMetricsReporting)
assert.NotNil(t, c.plugin)
assert.NotNil(t, c.tomb)
// The send in monitorLoop is the only writer and must not block.
assert.NotZero(t, cap(c.statusChan))
expectedMetrics := []metrics.Int64MetricRepresentation{
{Name: "problem_counter", Labels: map[string]string{"reason": testProblemReason}, Value: 0},
{Name: "problem_gauge", Labels: map[string]string{"type": testCondition, "reason": testProblemReason}, Value: 0},
}
gotMetrics := append(fakeCounter.ListMetrics(), fakeGauge.ListMetrics()...)
assert.ElementsMatch(t, expectedMetrics, gotMetrics)
}