Files
nezha_domains/integration/agentcompat/internal/scenario/stress_fd_diagnostic_test.go
T
2026-07-20 22:08:01 +00:00

172 lines
8.7 KiB
Go

//go:build linux && agentcompat
package scenario
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/nezhahq/nezha/integration/agentcompat/internal/agent"
processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process"
)
func TestFDDiagnosticEnabled_OnlyAcceptsExactOne(t *testing.T) {
for _, value := range []string{"", "0", "true", "01", " 1", "1 ", "\t1"} {
require.False(t, fdDiagnosticEnabled(value), "value=%q", value)
}
require.True(t, fdDiagnosticEnabled("1"))
}
func TestFDDiagnosticCandidates_ExcludeDashboardAndMatchOrdinals(t *testing.T) {
collector := newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
agentOne := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "one"})
agentTwo := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 2, PID: 102, BaselineCount: 8, EndCount: 8, Target: "two"})
dashboard := stressDiagnosticDashboardWindow(t, stressDiagnosticDashboardWindowSpec{PID: 100, BaselineCount: 8, EndCount: 9})
collector.RecordBaseline(dashboard.baseline)
collector.RecordBaseline(agentTwo.baseline)
collector.RecordBaseline(agentOne.baseline)
collector.RecordEnd(t.Context(), agentTwo.end)
collector.RecordEnd(t.Context(), dashboard.end)
collector.RecordEnd(t.Context(), agentOne.end)
records := collector.WaitRecords()
require.Len(t, records, 1)
require.Equal(t, 1, records[0].AgentOrdinal)
require.Equal(t, 101, records[0].AgentPID)
}
func TestFDDiagnosticCandidates_UseOnlyFinalSampleCounts(t *testing.T) {
collector := newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 8, Target: "stable"})
pair.end.Window.Samples[0].NonStdioFDCount = 9
collector.RecordBaseline(pair.baseline)
collector.RecordEnd(t.Context(), pair.end)
require.Empty(t, collector.WaitRecords())
pair.end.Window.Samples[len(pair.end.Window.Samples)-1].NonStdioFDCount = 9
collector = newFDDiagnosticCollector(fdDiagnosticCollectorSpec{Enabled: true, TailResultCapacity: 8, Sample: fdDiagnosticTestSampler})
collector.RecordBaseline(pair.baseline)
collector.RecordEnd(t.Context(), pair.end)
require.Len(t, collector.WaitRecords(), 1)
}
func TestFDDiagnosticRecord_ReportsIdentityChangeWithoutTail(t *testing.T) {
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "changed"})
pair.end.Identity = agent.ProcessIdentity{Generation: 2, PID: 202}
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
record, tail := newFDDiagnosticRecord(candidate, 999)
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
require.False(t, tail)
require.Empty(t, record.Lifecycle)
}
func TestFDDiagnosticRecord_RejectsStressProcessPIDMismatch(t *testing.T) {
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "process-pid-mismatch"})
pair.end.Process.PID = 202
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
record, tail := newFDDiagnosticRecord(candidate, 999)
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
require.False(t, tail)
}
func TestFDDiagnosticRecord_RejectsZeroRuntimeGeneration(t *testing.T) {
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "zero-generation"})
pair.end.Identity.Generation = 0
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
record, tail := newFDDiagnosticRecord(candidate, 999)
require.Equal(t, "process_identity_changed", record.LifecycleStatus)
require.False(t, tail)
}
func TestFDDiagnosticRecord_EncodesStableObservedAtForEveryWindowSample(t *testing.T) {
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "timestamps"})
baselineTime := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
endTime := baselineTime.Add(time.Minute)
for index := range pair.baseline.Window.Samples {
pair.baseline.Window.Samples[index].SampledAt = baselineTime.Add(time.Duration(index) * time.Second)
pair.end.Window.Samples[index].SampledAt = endTime.Add(time.Duration(index) * time.Second)
}
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
record, tail := newFDDiagnosticRecord(candidate, 999)
encoded, err := json.Marshal(record)
require.True(t, tail)
require.NoError(t, err)
require.Equal(t, baselineTime, record.Baseline.Samples[0].ObservedAt)
require.Equal(t, endTime, record.End.Samples[0].ObservedAt)
require.Contains(t, string(encoded), `"observed_at":"2025-01-02T03:04:05Z"`)
}
func TestFDDiagnosticRecord_DiffsFinalObservationsByExactIdentity(t *testing.T) {
pair := stressDiagnosticAgentWindow(t, stressDiagnosticAgentWindowSpec{Ordinal: 1, PID: 101, BaselineCount: 8, EndCount: 9, Target: "diff"})
pair.baseline.Window.Samples[4].FDObservations = []processharness.FDObservation{{Number: 5, Target: "beta"}, {Number: 4, Target: "alpha"}}
pair.end.Window.Samples[4].FDObservations = []processharness.FDObservation{{Number: 6, Target: "gamma"}, {Number: 5, Target: "beta"}}
candidate := fdDiagnosticCandidate{Baseline: pair.baseline, End: pair.end}
record, tail := newFDDiagnosticRecord(candidate, 999)
require.True(t, tail)
require.Equal(t, []processharness.FDObservation{{Number: 6, Target: "gamma"}}, record.AddedFinal)
require.Equal(t, []processharness.FDObservation{{Number: 4, Target: "alpha"}}, record.RemovedFinal)
}
func TestFDDiagnosticLifecycle_ClassifiesEachFinalAddition(t *testing.T) {
added := []processharness.FDObservation{{Number: 3, Target: "before"}, {Number: 4, Target: "during"}, {Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}
tail := []fdDiagnosticSample{
stressDiagnosticSample(1, []processharness.FDObservation{{Number: 4, Target: "during"}, {Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}),
stressDiagnosticSample(2, []processharness.FDObservation{{Number: 5, Target: "through"}, {Number: 7, Target: "other"}}),
stressDiagnosticSample(3, []processharness.FDObservation{{Number: 5, Target: "through"}, {Number: 6, Target: "intermittent"}, {Number: 7, Target: "reused"}}),
}
lifecycle := classifyFDDiagnosticLifecycle(added, tail)
require.Equal(t, []fdDiagnosticLifecycle{
{Observation: processharness.FDObservation{Number: 3, Target: "before"}, Status: "cleared_before_tail"},
{Observation: processharness.FDObservation{Number: 4, Target: "during"}, Status: "cleared_during_tail"},
{Observation: processharness.FDObservation{Number: 5, Target: "through"}, Status: "observed_through_tail"},
{Observation: processharness.FDObservation{Number: 6, Target: "intermittent"}, Status: "intermittent_or_reused"},
{Observation: processharness.FDObservation{Number: 7, Target: "reused"}, Status: "intermittent_or_reused", NumberReused: true},
}, lifecycle)
}
func TestFDDiagnosticFields_DoNotChangeStressEvaluationOrEvidenceJSON(t *testing.T) {
resource := stressDashboardResourceFixture(100)
beforeEvaluation, err := EvaluateStressResource(resource)
require.NoError(t, err)
for sampleIndex := range resource.Baseline.Samples {
resource.Baseline.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
resource.End.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
}
afterEvaluation, err := EvaluateStressResource(resource)
require.NoError(t, err)
profile := mustStressPRFullProfile(t)
evidence := validStressEvidence(t, profile)
beforeEvidence, err := json.Marshal(evidence)
require.NoError(t, err)
for iteration := range evidence.Iterations {
for resourceIndex := range evidence.Iterations[iteration].Resources {
for sampleIndex := range evidence.Iterations[iteration].Resources[resourceIndex].Baseline.Samples {
evidence.Iterations[iteration].Resources[resourceIndex].Baseline.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
evidence.Iterations[iteration].Resources[resourceIndex].End.Samples[sampleIndex].FDObservations = []processharness.FDObservation{{Number: 9, Target: "diagnostic"}}
}
}
}
afterEvidence, err := json.Marshal(evidence)
require.NoError(t, err)
require.Equal(t, beforeEvaluation, afterEvaluation)
require.Equal(t, string(beforeEvidence), string(afterEvidence))
}