183 lines
4.8 KiB
Go
183 lines
4.8 KiB
Go
package display
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// diagnosticCapture collects structured reconnect records from Run.
|
|
type diagnosticCapture struct {
|
|
mu sync.Mutex
|
|
records []Diagnostic
|
|
}
|
|
|
|
func (c *diagnosticCapture) record(d Diagnostic) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.records = append(c.records, d)
|
|
}
|
|
|
|
func (c *diagnosticCapture) all() []Diagnostic {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return append([]Diagnostic(nil), c.records...)
|
|
}
|
|
|
|
func TestRunDiagnosesEachFailedOpenWithStructuredFields(t *testing.T) {
|
|
session := newFakeSession()
|
|
var opens int
|
|
opener := fakeOpener{open: func(int) (Session, error) {
|
|
opens++
|
|
if opens <= 2 {
|
|
return nil, errors.New("x11 down")
|
|
}
|
|
return session, nil
|
|
}}
|
|
|
|
capture := &diagnosticCapture{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- Run(ctx, opener, NewStatusTemplate("hello"), Options{
|
|
Target: "x11",
|
|
Layer: "fallback-canvas",
|
|
Diagnostic: capture.record,
|
|
RetryDelay: time.Millisecond,
|
|
MaxDelay: 2 * time.Millisecond,
|
|
})
|
|
}()
|
|
|
|
waitFor(t, func() bool { return session.canvas.lineCount() > 0 }, "connect after retries")
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("run did not stop after cancel")
|
|
}
|
|
|
|
records := capture.all()
|
|
if len(records) != 2 {
|
|
t.Fatalf("records=%d want exactly one per failed open (2)", len(records))
|
|
}
|
|
first, second := records[0], records[1]
|
|
if first.Target != "x11" {
|
|
t.Fatalf("target=%q want x11", first.Target)
|
|
}
|
|
if first.Layer != "fallback-canvas" {
|
|
t.Fatalf("layer=%q want fallback-canvas", first.Layer)
|
|
}
|
|
if first.Event != EventOpenFailed {
|
|
t.Fatalf("event=%q want %q", first.Event, EventOpenFailed)
|
|
}
|
|
if first.Err == nil || first.Err.Error() != "x11 down" {
|
|
t.Fatalf("underlying error %v must be carried in the record", first.Err)
|
|
}
|
|
if first.RetryDelay != time.Millisecond {
|
|
t.Fatalf("first retry delay=%v want %v", first.RetryDelay, time.Millisecond)
|
|
}
|
|
if second.RetryDelay != 2*time.Millisecond {
|
|
t.Fatalf("backed-off retry delay=%v want %v", second.RetryDelay, 2*time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestRunDiagnosesBrokenSessionOnceAndRecovers(t *testing.T) {
|
|
first := newFakeSession()
|
|
second := newFakeSession()
|
|
var opens int
|
|
opener := fakeOpener{open: func(int) (Session, error) {
|
|
opens++
|
|
if opens == 1 {
|
|
return first, nil
|
|
}
|
|
return second, nil
|
|
}}
|
|
|
|
capture := &diagnosticCapture{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- Run(ctx, opener, NewStatusTemplate("hello"), Options{
|
|
Target: "x11",
|
|
Diagnostic: capture.record,
|
|
RetryDelay: time.Millisecond,
|
|
MaxDelay: 2 * time.Millisecond,
|
|
})
|
|
}()
|
|
|
|
waitFor(t, func() bool { return first.canvas.lineCount() == 1 }, "draw on first session")
|
|
first.events <- eventResult{err: errors.New("connection lost")}
|
|
waitFor(t, func() bool { return second.canvas.lineCount() == 1 }, "recover on reconnected session")
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("run did not stop after cancel")
|
|
}
|
|
|
|
records := capture.all()
|
|
if len(records) != 1 {
|
|
t.Fatalf("records=%d want exactly one per broken session (1)", len(records))
|
|
}
|
|
record := records[0]
|
|
if record.Target != "x11" {
|
|
t.Fatalf("target=%q want x11", record.Target)
|
|
}
|
|
if record.Event != EventSessionEnded {
|
|
t.Fatalf("event=%q want %q", record.Event, EventSessionEnded)
|
|
}
|
|
if record.Err == nil || record.Err.Error() != "connection lost" {
|
|
t.Fatalf("underlying error %v must be carried in the record", record.Err)
|
|
}
|
|
if record.RetryDelay != time.Millisecond {
|
|
t.Fatalf("retry delay=%v want the reset %v", record.RetryDelay, time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// blockingOpener fails only after the context is done, so cancellation is
|
|
// the sole reason Run stops: no diagnostic may fire for shutdown.
|
|
type blockingOpener struct{}
|
|
|
|
func (blockingOpener) Open(ctx context.Context) (Session, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
func TestRunStaysSilentWhenCancellationEndsTheLoop(t *testing.T) {
|
|
capture := &diagnosticCapture{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- Run(ctx, blockingOpener{}, NewStatusTemplate("hello"), Options{
|
|
Target: "x11",
|
|
Diagnostic: capture.record,
|
|
RetryDelay: time.Millisecond,
|
|
MaxDelay: 2 * time.Millisecond,
|
|
})
|
|
}()
|
|
|
|
cancel()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("run did not stop after cancel")
|
|
}
|
|
if records := capture.all(); len(records) != 0 {
|
|
t.Fatalf("records=%v must stay empty: shutdown is not a reconnect boundary", records)
|
|
}
|
|
}
|