100 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|