Files
2026-07-13 13:00:08 +08:00

100 lines
2.9 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"reasonix/internal/config"
)
func TestHangAgeBucket(t *testing.T) {
cases := []struct {
age time.Duration
want string
}{
{12 * time.Second, "s_10_15"},
{16 * time.Second, "s_15_30"},
{45 * time.Second, "s_30_60"},
{2 * time.Minute, "m_1_5"},
{10 * time.Minute, "m_5_plus"},
}
for _, c := range cases {
if got := hangAgeBucket(c.age); got != c.want {
t.Errorf("hangAgeBucket(%s) = %q, want %q", c.age, got, c.want)
}
}
}
func TestMainThreadHangReportIsStructuredPerformanceReport(t *testing.T) {
last := time.Date(2026, 7, 4, 9, 19, 11, 0, time.UTC)
observed := last.Add(16 * time.Second)
r := mainThreadHangReport(16*time.Second, last, observed)
if r.Kind != "performance" || r.Source != "native.watchdog" || r.Label != "mac.main_thread.hang" {
t.Fatalf("unexpected report identity: %+v", r)
}
if r.SchemaVersion != 2 || r.ErrorType != "MacMainThreadHang" || r.TopFrame == "" || r.OccurredAt == "" {
t.Fatalf("structured fields missing: %+v", r)
}
for _, want := range []string{"Reasonix detected", "last heartbeat:", "bucket: s_15_30", "goroutines:"} {
if !strings.Contains(r.Message, want) {
t.Fatalf("hang report message missing %q:\n%s", want, r.Message)
}
}
}
func TestMainThreadHeartbeatAgeIgnoresWallClockJump(t *testing.T) {
oldBase := mainThreadClockBase
oldElapsed := mainThreadLastHeartbeatElapsed.Load()
oldWall := mainThreadLastHeartbeatWall.Load()
t.Cleanup(func() {
mainThreadClockBase = oldBase
mainThreadLastHeartbeatElapsed.Store(oldElapsed)
mainThreadLastHeartbeatWall.Store(oldWall)
})
base := time.Now()
mainThreadClockBase = base
mainThreadLastHeartbeatElapsed.Store(int64(time.Second))
mainThreadLastHeartbeatWall.Store(base.Add(-time.Hour).UnixNano())
age, _, ok := mainThreadHeartbeatAge(base.Add(2 * time.Second))
if !ok {
t.Fatal("expected heartbeat age")
}
if age != time.Second {
t.Fatalf("age = %s, want monotonic elapsed 1s despite wall-clock jump", age)
}
}
func TestRecordMainThreadHangWritesPendingReportAndMetrics(t *testing.T) {
t.Cleanup(func() {
os.Remove(pendingCrashPath())
os.Remove(filepath.Join(config.MemoryUserDir(), metricsPendingFile))
})
app := NewApp()
app.metrics.Store(newMetricsAggregator(config.MemoryUserDir()))
last := time.Now().Add(-20 * time.Second)
app.recordMainThreadHang(20*time.Second, last, time.Now())
r, ok := readPending(t)
if !ok {
t.Fatal("expected pending hang report")
}
if r.Kind != "performance" || r.Source != "native.watchdog" || r.Label != "mac.main_thread.hang" {
t.Fatalf("pending report = %+v", r)
}
c := readCounters(filepath.Join(config.MemoryUserDir(), metricsPendingFile))
if got := c["desktop_hang"]["main_thread"]; got != 1 {
t.Fatalf("desktop_hang/main_thread = %d, want 1", got)
}
if got := c["desktop_hang_age"]["s_15_30"]; got != 1 {
t.Fatalf("desktop_hang_age/s_15_30 = %d, want 1", got)
}
}