返回 DeepSeek-Reasonix
hang_watchdog_test.go
根目录 / desktop / hang_watchdog_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9
10 "reasonix/internal/config"
11 )
12
13 func TestHangAgeBucket(t *testing.T) {
14 cases := []struct {
15 age time.Duration
16 want string
17 }{
18 {12 * time.Second, "s_10_15"},
19 {16 * time.Second, "s_15_30"},
20 {45 * time.Second, "s_30_60"},
21 {2 * time.Minute, "m_1_5"},
22 {10 * time.Minute, "m_5_plus"},
23 }
24 for _, c := range cases {
25 if got := hangAgeBucket(c.age); got != c.want {
26 t.Errorf("hangAgeBucket(%s) = %q, want %q", c.age, got, c.want)
27 }
28 }
29 }
30
31 func TestMainThreadHangReportIsStructuredPerformanceReport(t *testing.T) {
32 last := time.Date(2026, 7, 4, 9, 19, 11, 0, time.UTC)
33 observed := last.Add(16 * time.Second)
34
35 r := mainThreadHangReport(16*time.Second, last, observed)
36
37 wantLabel, wantErrorType, _, _ := mainThreadDiagnosticIdentity()
38 if r.Kind != "performance" || r.Source != "native.watchdog" || r.Label != wantLabel {
39 t.Fatalf("unexpected report identity: %+v", r)
40 }
41 if r.SchemaVersion != 2 || r.ErrorType != wantErrorType || r.TopFrame == "" || r.OccurredAt == "" {
42 t.Fatalf("structured fields missing: %+v", r)
43 }
44 for _, want := range []string{"Reasonix detected", "last heartbeat:", "bucket: s_15_30", "goroutines:"} {
45 if !strings.Contains(r.Message, want) {
46 t.Fatalf("hang report message missing %q:\n%s", want, r.Message)
47 }
48 }
49 }
50
51 func TestMainThreadHeartbeatAgeIgnoresWallClockJump(t *testing.T) {
52 oldBase := mainThreadClockBase
53 oldElapsed := mainThreadLastHeartbeatElapsed.Load()
54 oldWall := mainThreadLastHeartbeatWall.Load()
55 t.Cleanup(func() {
56 mainThreadClockBase = oldBase
57 mainThreadLastHeartbeatElapsed.Store(oldElapsed)
58 mainThreadLastHeartbeatWall.Store(oldWall)
59 })
60
61 base := time.Now()
62 mainThreadClockBase = base
63 mainThreadLastHeartbeatElapsed.Store(int64(time.Second))
64 mainThreadLastHeartbeatWall.Store(base.Add(-time.Hour).UnixNano())
65
66 age, _, ok := mainThreadHeartbeatAge(base.Add(2 * time.Second))
67 if !ok {
68 t.Fatal("expected heartbeat age")
69 }
70 if age != time.Second {
71 t.Fatalf("age = %s, want monotonic elapsed 1s despite wall-clock jump", age)
72 }
73 }
74
75 func TestWatchdogResetsHeartbeatAfterSleepGap(t *testing.T) {
76 oldBase := mainThreadClockBase
77 oldElapsed := mainThreadLastHeartbeatElapsed.Load()
78 oldWall := mainThreadLastHeartbeatWall.Load()
79 t.Cleanup(func() {
80 mainThreadClockBase = oldBase
81 mainThreadLastHeartbeatElapsed.Store(oldElapsed)
82 mainThreadLastHeartbeatWall.Store(oldWall)
83 })
84
85 base := time.Now()
86 mainThreadClockBase = base
87 recordMainThreadHeartbeat(base)
88 wake := base.Add(8 * time.Hour)
89 if !resetMainThreadHeartbeatAfterSleep(base, wake) {
90 t.Fatal("expected sleep gap to reset the heartbeat")
91 }
92 age, _, ok := mainThreadHeartbeatAge(wake.Add(mainThreadHangCheckInterval))
93 if !ok || age != mainThreadHangCheckInterval {
94 t.Fatalf("age after wake = %s, ok=%v; want %s", age, ok, mainThreadHangCheckInterval)
95 }
96 if resetMainThreadHeartbeatAfterSleep(wake, wake.Add(mainThreadHangCheckInterval)) {
97 t.Fatal("ordinary watchdog interval was treated as sleep")
98 }
99 }
100
101 func TestRecordMainThreadHangWritesPendingReportAndMetrics(t *testing.T) {
102 t.Cleanup(func() {
103 removeAllPendingCrashes()
104 os.Remove(filepath.Join(config.MemoryUserDir(), metricsPendingFile))
105 })
106 app := NewApp()
107 app.metrics.Store(newMetricsAggregator(config.MemoryUserDir()))
108
109 last := time.Now().Add(-20 * time.Second)
110 app.recordMainThreadHang(20*time.Second, last, time.Now())
111
112 r, ok := readPending(t)
113 if !ok {
114 t.Fatal("expected pending hang report")
115 }
116 wantLabel, _, _, _ := mainThreadDiagnosticIdentity()
117 if r.Kind != "performance" || r.Source != "native.watchdog" || r.Label != wantLabel {
118 t.Fatalf("pending report = %+v", r)
119 }
120 c := readCounters(filepath.Join(config.MemoryUserDir(), metricsPendingFile))
121 metricBucket := mainThreadMetricBucket()
122 if got := c["desktop_hang"][metricBucket]; got != 1 {
123 t.Fatalf("desktop_hang/%s = %d, want 1", metricBucket, got)
124 }
125 if got := c["desktop_hang_age"]["s_15_30"]; got != 1 {
126 t.Fatalf("desktop_hang_age/s_15_30 = %d, want 1", got)
127 }
128 }
129
129 lines GO