Ctrl K
ComunidadGuías…

utils/recover_flush_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package utils

import (
	"os"
	"runtime"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/getsentry/sentry-go"
	"go.uber.org/zap"
)

type flushCounter struct{ n atomic.Int64 }

func (f *flushCounter) Configure(sentry.ClientOptions) {}
func (f *flushCounter) SendEvent(*sentry.Event)        {}
func (f *flushCounter) Flush(time.Duration) bool {
	f.n.Add(1)
	return true
}

// Recover runs on every clean return of the goroutines that defer it; a flush
// there waits on Sentry's one transport worker for an event never captured.
func TestRecoverDoesNotFlushSentryWithoutAPanic(t *testing.T) {
	tr := &flushCounter{}
	prev := sentry.CurrentHub().Client()
	if err := sentry.Init(sentry.ClientOptions{Dsn: "https://public@sentry.invalid/1", Transport: tr}); err != nil {
		t.Fatalf("sentry.Init: %v", err)
	}
	t.Cleanup(func() { sentry.CurrentHub().BindClient(prev) })

	func() { defer Recover(zap.NewNop()) }()
	if got := tr.n.Load(); got != 0 {
		t.Fatalf("Recover flushed Sentry %d time(s) on a clean return", got)
	}
}

// eventRecorder is a Sentry transport that keeps the events it is sent.
type eventRecorder struct {
	mu     sync.Mutex
	events []*sentry.Event
}

func (e *eventRecorder) Configure(sentry.ClientOptions) {}
func (e *eventRecorder) SendEvent(ev *sentry.Event) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.events = append(e.events, ev)
}
func (e *eventRecorder) Flush(time.Duration) bool { return true }

// A crash report carries the end of this run's log, never all of it. The log
// holds up to 100 MiB (log.FileCapBytes), the native agent's lines and
// --debug's among them: a panic handler must not read that into memory, and a
// Sentry event cannot carry it (its limit is 1 MB).
func TestHandleRecoveryAttachesABoundedTailOfTheLog(t *testing.T) {
	tr := &eventRecorder{}
	prev := sentry.CurrentHub().Client()
	if err := sentry.Init(sentry.ClientOptions{Dsn: "https://public@sentry.invalid/1", Transport: tr}); err != nil {
		t.Fatalf("sentry.Init: %v", err)
	}
	t.Cleanup(func() {
		sentry.ConfigureScope(func(s *sentry.Scope) { s.RemoveExtra("logfile") })
		sentry.CurrentHub().BindClient(prev)
	})

	// A 100 MiB log, sparse but for the lines it ends with: 2 MiB of them, more
	// than a crash report may carry.
	f, err := os.CreateTemp(t.TempDir(), "keploy-logs-*.txt")
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { _ = f.Close() })
	if err := f.Truncate(100 << 20); err != nil {
		t.Fatal(err)
	}
	if _, err := f.Seek(0, 2); err != nil {
		t.Fatal(err)
	}
	const last = "the line the run logged last\n"
	if _, err := f.WriteString(strings.Repeat("a line of the run\n", (2<<20)/18) + last); err != nil {
		t.Fatal(err)
	}
	prevLog := LogFile
	LogFile = f
	t.Cleanup(func() { LogFile = prevLog })

	var before, after runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&before)
	HandleRecovery(zap.NewNop(), "boom", "Recovered from panic")
	runtime.ReadMemStats(&after)
	if grew := after.TotalAlloc - before.TotalAlloc; grew > 16<<20 {
		t.Errorf("the crash report allocated %d MiB for a 100 MiB log; want a bounded read of its end", grew>>20)
	}

	tr.mu.Lock()
	defer tr.mu.Unlock()
	if len(tr.events) != 1 {
		t.Fatalf("Sentry was sent %d events, want the crash", len(tr.events))
	}
	// Sentry drops an event past 1 MB (1,000,000 bytes), so the tail leaves
	// room for the rest of the event: a literal, not crashReportLogTail, so a
	// bigger crashReportLogTail fails here.
	const tailLimit = 900_000
	got, _ := tr.events[0].Extra["logfile"].(string)
	if len(got) == 0 || len(got) > tailLimit || len(got) > crashReportLogTail || !strings.HasSuffix(got, last) {
		t.Errorf("the crash report carries %d bytes of the log ending %q; want its end, at most %d bytes (under Sentry's 1 MB event limit)",
			len(got), got[max(0, len(got)-40):], min(tailLimit, crashReportLogTail))
	}
}