Files
MyGoNavi/internal/jvm/monitoring_manager_test.go
tianqijiuyun-latiao 5fc29a6fd3 feat(i18n): 推进多语言剩余切片闭环
- 补齐 DataGrid、DataViewer、DefinitionViewer、JVM 等模块多语言文案与回归测试
- 收口 JVM 前后端展示、诊断、监控和资源呈现相关多语言路径
- 更新六语言共享词典并保留 raw 边界
2026-06-16 12:40:33 +08:00

440 lines
14 KiB
Go

package jvm
import (
"context"
"errors"
"strings"
"sync"
"testing"
"time"
"GoNavi-Wails/internal/connection"
)
type fakeMonitoringProvider struct {
snapshot JVMMonitoringSnapshot
snapshotErr error
}
type blockingMonitoringProvider struct {
fakeMonitoringProvider
started chan struct{}
release chan struct{}
once sync.Once
}
type fakeProviderWithoutMonitoring struct{}
func (f fakeMonitoringProvider) Mode() string { return ModeJMX }
func (f fakeMonitoringProvider) TestConnection(context.Context, connection.ConnectionConfig) error {
return nil
}
func (f fakeMonitoringProvider) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]Capability, error) {
return nil, nil
}
func (f fakeMonitoringProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]ResourceSummary, error) {
return nil, nil
}
func (f fakeMonitoringProvider) GetValue(context.Context, connection.ConnectionConfig, string) (ValueSnapshot, error) {
return ValueSnapshot{}, nil
}
func (f fakeMonitoringProvider) PreviewChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ChangePreview, error) {
return ChangePreview{}, nil
}
func (f fakeMonitoringProvider) ApplyChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ApplyResult, error) {
return ApplyResult{}, nil
}
func (f fakeMonitoringProvider) GetMonitoringSnapshot(context.Context, connection.ConnectionConfig, *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
return f.snapshot, f.snapshotErr
}
func (p *blockingMonitoringProvider) GetMonitoringSnapshot(context.Context, connection.ConnectionConfig, *JVMMonitoringPoint) (JVMMonitoringSnapshot, error) {
p.once.Do(func() {
close(p.started)
})
<-p.release
return p.snapshot, p.snapshotErr
}
func (f fakeProviderWithoutMonitoring) Mode() string { return ModeJMX }
func (f fakeProviderWithoutMonitoring) TestConnection(context.Context, connection.ConnectionConfig) error {
return nil
}
func (f fakeProviderWithoutMonitoring) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]Capability, error) {
return nil, nil
}
func (f fakeProviderWithoutMonitoring) ListResources(context.Context, connection.ConnectionConfig, string) ([]ResourceSummary, error) {
return nil, nil
}
func (f fakeProviderWithoutMonitoring) GetValue(context.Context, connection.ConnectionConfig, string) (ValueSnapshot, error) {
return ValueSnapshot{}, nil
}
func (f fakeProviderWithoutMonitoring) PreviewChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ChangePreview, error) {
return ChangePreview{}, nil
}
func (f fakeProviderWithoutMonitoring) ApplyChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ApplyResult, error) {
return ApplyResult{}, nil
}
func swapMonitoringProviderFactory(factory func(mode string) (Provider, error)) func() {
prev := monitoringProviderFactory
monitoringProviderFactory = factory
return func() { monitoringProviderFactory = prev }
}
func TestMonitoringRingBufferKeepsLatestPoints(t *testing.T) {
manager := newMonitoringManagerForTest(3)
session := manager.ensureSession("conn-1", ModeJMX)
for i := 1; i <= 5; i++ {
session.appendPoint(JVMMonitoringPoint{Timestamp: int64(i)})
}
snapshot := session.snapshot()
if len(snapshot.Points) != 3 {
t.Fatalf("expected 3 points, got %d", len(snapshot.Points))
}
if snapshot.Points[0].Timestamp != 3 || snapshot.Points[2].Timestamp != 5 {
t.Fatalf("unexpected points order: %#v", snapshot.Points)
}
}
func TestMonitoringSessionSnapshotCarriesProviderWarningsAndGCEvents(t *testing.T) {
manager := newMonitoringManagerForTest(5)
session := manager.ensureSession("conn-2", ModeEndpoint)
session.running = true
session.availableMetrics = []string{"heap.used", "thread.count", "memory.rss"}
session.missingMetrics = []string{"cpu.process", "gc.events"}
session.providerWarnings = []string{"endpoint metrics degraded"}
session.recentGCEvents = []RecentGCEvent{
{
Timestamp: 1713945600000,
Name: "G1 Young Generation",
Cause: "G1 Evacuation Pause",
Action: "end of minor GC",
DurationMs: 21,
BeforeUsedBytes: 734003200,
AfterUsedBytes: 503316480,
},
}
session.appendPoint(JVMMonitoringPoint{
Timestamp: 1713945600000,
ThreadCount: 18,
HeapUsedBytes: 503316480,
ProcessRssBytes: 1073741824,
})
snapshot := session.snapshot()
if !snapshot.Running {
t.Fatalf("expected session to be running")
}
if snapshot.ProviderMode != ModeEndpoint {
t.Fatalf("expected provider mode %q, got %q", ModeEndpoint, snapshot.ProviderMode)
}
if len(snapshot.AvailableMetrics) != 3 {
t.Fatalf("expected available metrics, got %#v", snapshot.AvailableMetrics)
}
if len(snapshot.MissingMetrics) != 2 || snapshot.MissingMetrics[0] != "cpu.process" {
t.Fatalf("unexpected missing metrics: %#v", snapshot.MissingMetrics)
}
if len(snapshot.ProviderWarnings) != 1 {
t.Fatalf("expected provider warning, got %#v", snapshot.ProviderWarnings)
}
if len(snapshot.RecentGCEvents) != 1 {
t.Fatalf("expected recent gc event, got %#v", snapshot.RecentGCEvents)
}
if len(snapshot.Points) != 1 || snapshot.Points[0].ThreadCount != 18 || snapshot.Points[0].HeapUsedBytes != 503316480 {
t.Fatalf("unexpected points snapshot: %#v", snapshot.Points)
}
}
func TestMonitoringManagerStartSamplesImmediatelyAndReturnsHistory(t *testing.T) {
manager := newMonitoringManagerForTest(5)
restore := swapMonitoringProviderFactory(func(mode string) (Provider, error) {
return fakeMonitoringProvider{
snapshot: JVMMonitoringSnapshot{
Point: JVMMonitoringPoint{
Timestamp: 1713945600000,
ThreadCount: 12,
HeapUsedBytes: 268435456,
ProcessCpuLoad: 0.42,
},
AvailableMetrics: []string{"thread.count", "heap.used"},
MissingMetrics: []string{"cpu.process"},
ProviderWarnings: []string{"jmx cpu metric unavailable"},
},
}, nil
})
defer restore()
readOnly := true
cfg := connection.ConnectionConfig{
ID: "conn-monitor",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
ReadOnly: &readOnly,
PreferredMode: ModeJMX,
AllowedModes: []string{ModeJMX},
},
}
snapshot, err := manager.Start(context.Background(), cfg, "")
if err != nil {
t.Fatalf("Start returned error: %v", err)
}
if !snapshot.Running {
t.Fatalf("expected started session to be running")
}
if len(snapshot.Points) != 1 || snapshot.Points[0].ThreadCount != 12 || snapshot.Points[0].HeapUsedBytes != 268435456 {
t.Fatalf("unexpected initial points: %#v", snapshot.Points)
}
history, err := manager.GetHistory("conn-monitor", ModeJMX)
if err != nil {
t.Fatalf("GetHistory returned error: %v", err)
}
if len(history.MissingMetrics) != 1 || history.MissingMetrics[0] != "cpu.process" {
t.Fatalf("unexpected history missing metrics: %#v", history.MissingMetrics)
}
if len(history.ProviderWarnings) != 1 {
t.Fatalf("unexpected provider warnings: %#v", history.ProviderWarnings)
}
}
func TestMonitoringManagerStopMarksSessionStopped(t *testing.T) {
manager := newMonitoringManagerForTest(5)
restore := swapMonitoringProviderFactory(func(mode string) (Provider, error) {
return fakeMonitoringProvider{
snapshot: JVMMonitoringSnapshot{
Point: JVMMonitoringPoint{Timestamp: 1713945600000, ThreadCount: 7},
},
}, nil
})
defer restore()
cfg := connection.ConnectionConfig{
ID: "conn-stop",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: ModeEndpoint,
AllowedModes: []string{ModeEndpoint},
},
}
if _, err := manager.Start(context.Background(), cfg, ModeEndpoint); err != nil {
t.Fatalf("Start returned error: %v", err)
}
if err := manager.Stop("conn-stop", ModeEndpoint); err != nil {
t.Fatalf("Stop returned error: %v", err)
}
history, err := manager.GetHistory("conn-stop", ModeEndpoint)
if err != nil {
t.Fatalf("GetHistory returned error: %v", err)
}
if history.Running {
t.Fatalf("expected session to stop running, got %#v", history)
}
}
func TestMonitoringManagerReturnsLocalizedErrorsForFixedMonitoringFailures(t *testing.T) {
manager := newMonitoringManagerForTest(5)
_, err := manager.GetHistory("conn-missing", ModeJMX)
assertMonitoringLocalizedError(t, err, "jvm.backend.monitoring.error.session_not_found", map[string]any{
"connectionId": "conn-missing",
"providerMode": ModeJMX,
})
err = manager.Stop("conn-missing", ModeAgent)
assertMonitoringLocalizedError(t, err, "jvm.backend.monitoring.error.session_not_found", map[string]any{
"connectionId": "conn-missing",
"providerMode": ModeAgent,
})
restore := swapMonitoringProviderFactory(func(mode string) (Provider, error) {
return fakeProviderWithoutMonitoring{}, nil
})
defer restore()
_, err = manager.Start(context.Background(), connection.ConnectionConfig{
ID: "conn-monitor",
Type: "jvm",
Host: "orders.internal",
JVM: connection.JVMConfig{
PreferredMode: ModeJMX,
AllowedModes: []string{ModeJMX},
},
}, "")
assertMonitoringLocalizedError(t, err, "jvm.backend.monitoring.error.snapshot_unsupported", map[string]any{
"provider": "JMX",
})
}
func assertMonitoringLocalizedError(t *testing.T, err error, key string, params map[string]any) {
t.Helper()
if err == nil {
t.Fatalf("expected localized error %q, got nil", key)
}
var localized *LocalizedError
if !errors.As(err, &localized) {
t.Fatalf("expected LocalizedError %q, got %T: %v", key, err, err)
}
if localized.Key != key {
t.Fatalf("expected localized key %q, got %q", key, localized.Key)
}
for name, expected := range params {
if localized.Params[name] != expected {
t.Fatalf("expected param %s=%#v, got %#v in %#v", name, expected, localized.Params[name], localized.Params)
}
}
}
func TestMonitoringSessionIgnoresStaleStopFromPreviousSampler(t *testing.T) {
session := &monitoringSession{}
firstGeneration := session.reset("conn-race", ModeJMX)
session.markRunning(firstGeneration)
secondGeneration := session.reset("conn-race", ModeJMX)
session.markRunning(secondGeneration)
session.markStopped(firstGeneration)
if snapshot := session.snapshot(); !snapshot.Running {
t.Fatalf("expected stale sampler stop to be ignored, got %#v", snapshot)
}
session.markStopped(secondGeneration)
if snapshot := session.snapshot(); snapshot.Running {
t.Fatalf("expected active generation stop to mark stopped, got %#v", snapshot)
}
}
func TestMonitoringSessionIgnoresStalePointFromPreviousSampler(t *testing.T) {
manager := newMonitoringManager(5, time.Millisecond)
session := &monitoringSession{limit: 5}
provider := &blockingMonitoringProvider{
fakeMonitoringProvider: fakeMonitoringProvider{
snapshot: JVMMonitoringSnapshot{
Point: JVMMonitoringPoint{
Timestamp: 1713945600000,
ThreadCount: 8,
},
AvailableMetrics: []string{"thread.count"},
},
},
started: make(chan struct{}),
release: make(chan struct{}),
}
firstGeneration := session.reset("conn-race", ModeJMX)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() {
manager.runSampler(ctx, provider, connection.ConnectionConfig{}, session, firstGeneration)
close(done)
}()
select {
case <-provider.started:
case <-time.After(time.Second):
t.Fatal("sampler did not start within 1s")
}
secondGeneration := session.reset("conn-race", ModeJMX)
session.markRunning(secondGeneration)
close(provider.release)
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("sampler did not stop within 1s")
}
snapshot := session.snapshot()
if !snapshot.Running {
t.Fatalf("expected new generation to remain running, got %#v", snapshot)
}
if len(snapshot.Points) != 0 {
t.Fatalf("expected stale sampler point to be ignored, got %#v", snapshot.Points)
}
}
func TestFinalizeMonitoringSnapshotPreservesProviderDeltaWhenClassTotalMissing(t *testing.T) {
snapshot := JVMMonitoringSnapshot{
Point: JVMMonitoringPoint{
Timestamp: 1713945602000,
ClassLoadDelta: 3,
},
AvailableMetrics: []string{"class.delta"},
}
finalizeMonitoringSnapshot(&snapshot, &JVMMonitoringPoint{
Timestamp: 1713945600000,
LoadedClassCount: 200,
})
if snapshot.Point.ClassLoadDelta != 3 {
t.Fatalf("expected provider class delta to be preserved, got %#v", snapshot.Point)
}
}
func TestMonitoringSamplerStopsAfterConsecutiveFailures(t *testing.T) {
manager := newMonitoringManager(5, time.Millisecond)
session := &monitoringSession{limit: 5}
generation := session.reset("conn-fail", ModeJMX)
session.markRunning(generation)
provider := fakeMonitoringProvider{snapshotErr: errors.New("collector unavailable")}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
go func() {
manager.runSampler(ctx, provider, connection.ConnectionConfig{}, session, generation)
close(done)
}()
deadline := time.After(time.Second)
for {
select {
case <-done:
snapshot := session.snapshot()
if snapshot.Running {
t.Fatalf("expected session to stop after consecutive failures, got %#v", snapshot)
}
if len(snapshot.ProviderWarnings) == 0 {
t.Fatalf("expected provider warnings to explain sampling failure")
}
expectedFinalWarning := "__gonavi_i18n__:jvm.backend.monitoring.warning.sample_auto_stopped:count=3"
if snapshot.ProviderWarnings[len(snapshot.ProviderWarnings)-1] != expectedFinalWarning {
t.Fatalf("expected final warning to be structured, got %#v", snapshot.ProviderWarnings)
}
for _, warning := range snapshot.ProviderWarnings {
if strings.Contains(warning, "监控采样连续失败") {
t.Fatalf("expected no localized Chinese warning in manager snapshot, got %#v", snapshot.ProviderWarnings)
}
}
return
case <-deadline:
t.Fatal("sampler did not stop after consecutive failures")
case <-time.After(10 * time.Millisecond):
}
}
}
func TestMonitoringSessionDeduplicatesProviderWarnings(t *testing.T) {
session := &monitoringSession{}
session.appendWarning("collector unavailable")
session.appendWarning("collector unavailable")
session.appendWarning(" collector unavailable ")
snapshot := session.snapshot()
if len(snapshot.ProviderWarnings) != 1 {
t.Fatalf("expected duplicate provider warnings to be collapsed, got %#v", snapshot.ProviderWarnings)
}
}