192 lines
5.1 KiB
Go
192 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
func readPending(t *testing.T) (crashReport, bool) {
|
|
t.Helper()
|
|
body, err := os.ReadFile(pendingCrashPath())
|
|
if err != nil {
|
|
return crashReport{}, false
|
|
}
|
|
var r crashReport
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
t.Fatalf("pending file not valid JSON: %v", err)
|
|
}
|
|
return r, true
|
|
}
|
|
|
|
func TestRecoverToPendingCapturesAndReraises(t *testing.T) {
|
|
t.Cleanup(func() { os.Remove(pendingCrashPath()) })
|
|
|
|
func() {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("recoverToPending must re-raise the panic")
|
|
}
|
|
}()
|
|
app := NewApp()
|
|
defer app.recoverToPending("unit")
|
|
panic(`boom at C:\Users\alice\proj\x.go`)
|
|
}()
|
|
|
|
r, ok := readPending(t)
|
|
if !ok {
|
|
t.Fatal("expected a pending crash file")
|
|
}
|
|
if r.Kind != "crash" {
|
|
t.Errorf("kind = %q, want crash", r.Kind)
|
|
}
|
|
if !strings.Contains(r.Message, "[go panic] unit:") {
|
|
t.Errorf("message missing site prefix: %q", r.Message)
|
|
}
|
|
if r.Source != "go" || r.Label != "unit" || r.ErrorMessage == "" || r.Stack == "" || r.TopFrame == "" {
|
|
t.Errorf("structured panic metadata missing: %+v", r)
|
|
}
|
|
if strings.Contains(r.Message, `Users\alice`) {
|
|
t.Errorf("message not scrubbed: %q", r.Message)
|
|
}
|
|
}
|
|
|
|
func TestWritePendingCrashCaps(t *testing.T) {
|
|
t.Cleanup(func() { os.Remove(pendingCrashPath()) })
|
|
writePendingCrash("big", "x", []byte(strings.Repeat("a", 64<<10)))
|
|
r, ok := readPending(t)
|
|
if !ok {
|
|
t.Fatal("expected a pending crash file")
|
|
}
|
|
if len(r.Message) > maxCrashDetailBytes {
|
|
t.Errorf("message len = %d, want <= %d", len(r.Message), maxCrashDetailBytes)
|
|
}
|
|
}
|
|
|
|
func TestWritePendingReportCanAvoidOverwritingExistingCrash(t *testing.T) {
|
|
t.Cleanup(func() { os.Remove(pendingCrashPath()) })
|
|
writePendingCrash("panic", "boom", []byte("stack"))
|
|
before, ok := readPending(t)
|
|
if !ok {
|
|
t.Fatal("expected initial pending crash")
|
|
}
|
|
|
|
hang := baseCrashReport("performance")
|
|
hang.Source = "native.watchdog"
|
|
hang.Label = "mac.main_thread.hang"
|
|
hang.Message = "hang"
|
|
if writePendingReport(hang, false) {
|
|
t.Fatal("writePendingReport overwrite=false should not replace existing report")
|
|
}
|
|
after, ok := readPending(t)
|
|
if !ok {
|
|
t.Fatal("expected pending crash after skipped write")
|
|
}
|
|
if after.Label != before.Label || after.Message != before.Message {
|
|
t.Fatalf("pending crash was overwritten: before=%+v after=%+v", before, after)
|
|
}
|
|
}
|
|
|
|
func TestWritePendingReportNonOverwriteAllowsOneConcurrentWriter(t *testing.T) {
|
|
t.Cleanup(func() { os.Remove(pendingCrashPath()) })
|
|
const writers = 32
|
|
start := make(chan struct{})
|
|
var ready sync.WaitGroup
|
|
var done sync.WaitGroup
|
|
var successes atomic.Int32
|
|
|
|
for i := 0; i < writers; i++ {
|
|
ready.Add(1)
|
|
done.Add(1)
|
|
go func() {
|
|
defer done.Done()
|
|
report := baseCrashReport("performance")
|
|
report.Source = "native.watchdog"
|
|
report.Label = "mac.main_thread.hang"
|
|
report.Message = strings.Repeat("hang", 1024)
|
|
ready.Done()
|
|
<-start
|
|
if writePendingReport(report, false) {
|
|
successes.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
ready.Wait()
|
|
close(start)
|
|
done.Wait()
|
|
|
|
if got := successes.Load(); got != 1 {
|
|
t.Fatalf("successful non-overwrite writers = %d, want 1", got)
|
|
}
|
|
if _, ok := readPending(t); !ok {
|
|
t.Fatal("expected one pending report")
|
|
}
|
|
}
|
|
|
|
func TestWritePendingCrashScrubsSensitiveText(t *testing.T) {
|
|
t.Cleanup(func() { os.Remove(pendingCrashPath()) })
|
|
apiKey := "sk-proj-" + "abcdefghijklmnopqrstuvwxyz1234567890"
|
|
bearer := "abcdefghijklmnopqrstuvwxyz1234567890ABCDE"
|
|
longHex := "0123456789abcdef0123456789abcdef"
|
|
|
|
writePendingCrash("unit", "boom api_key="+apiKey+" user alice@example.com", []byte("goroutine\nAuthorization: Bearer "+bearer+"\n/home/alice/project/x.go:12\nhash "+longHex))
|
|
r, ok := readPending(t)
|
|
if !ok {
|
|
t.Fatal("expected a pending crash file")
|
|
}
|
|
freeText := strings.Join([]string{r.Message, r.ErrorMessage, r.Stack, r.TopFrame}, "\n")
|
|
for _, leaked := range []string{apiKey, bearer, longHex, "alice@example.com", "/home/alice"} {
|
|
if strings.Contains(freeText, leaked) {
|
|
t.Fatalf("sensitive value leaked %q in %+v", leaked, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlushPendingCrashSendsAndClears(t *testing.T) {
|
|
oldVersion, oldEndpoint := version, crashEndpoint
|
|
t.Cleanup(func() {
|
|
version, crashEndpoint = oldVersion, oldEndpoint
|
|
os.Remove(pendingCrashPath())
|
|
})
|
|
version = "v9.9.9"
|
|
|
|
var hits atomic.Int32
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
hits.Add(1)
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}))
|
|
defer srv.Close()
|
|
crashEndpoint = srv.URL
|
|
|
|
writePendingCrash("flush", "boom", []byte("stack"))
|
|
NewApp().flushPendingCrash()
|
|
|
|
if hits.Load() != 1 {
|
|
t.Errorf("server hits = %d, want 1", hits.Load())
|
|
}
|
|
if _, ok := readPending(t); ok {
|
|
t.Error("pending file should be cleared after a successful send")
|
|
}
|
|
}
|
|
|
|
func TestFlushPendingCrashDevGuard(t *testing.T) {
|
|
oldVersion := version
|
|
t.Cleanup(func() {
|
|
version = oldVersion
|
|
os.Remove(pendingCrashPath())
|
|
})
|
|
version = "dev"
|
|
|
|
writePendingCrash("dev", "boom", []byte("stack"))
|
|
NewApp().flushPendingCrash()
|
|
|
|
if _, ok := readPending(t); !ok {
|
|
t.Error("dev build must leave the pending file untouched")
|
|
}
|
|
}
|