1package ui23import (4 "context"5 "fmt"6 "io"7 "strings"8 "testing"9 "time"1011 tea "charm.land/bubbletea/v2"1213 "src.rest/cat/012/internal/live"14 "src.rest/cat/012/internal/notebook"15 "src.rest/cat/012/internal/sheet"16)1718// streamed is a notebook of one cell run as a stream, its output19// selected, and a function printing lines as the stream's pipeline and20// showing them as the output, as a poll and the next update of the21// output do.22func streamed(t *testing.T) (*Model, func(lines ...string)) {23 t.Helper()24 m := sized(sheet.New(), 100, 30)25 m.OpenNotebook()26 id := m.book().NewCellID()27 m.sheet.SetNotebookCells("cells", []notebook.Cell{{ID: id, Source: "log = tail -f app.log | lines | parse '{level} {msg}'"}})28 printed := make(chan string)29 src := live.NewStream(func(ctx context.Context, w io.Writer) error {30 for {31 select {32 case <-ctx.Done():33 return ctx.Err()34 case l := <-printed:35 io.WriteString(w, l+"\n")36 }37 }38 })39 src.Wait = 5 * time.Second40 t.Cleanup(src.Close)41 st := &nbStream{nbRun: &nbRun{nbQueued: nbQueued{m.sheet, id}, start: time.Now()}, src: src, count: 1}42 m.nb.runs.streams = map[int]*nbStream{id: st}43 m.showStream(st)44 return m, func(lines ...string) {45 t.Helper()46 printed <- strings.Join(lines, "\n")47 if _, ok := src.Poll(context.Background()); !ok {48 t.Fatal("the poll found nothing")49 }50 m.showStream(st)51 if m.nbView().SelectedGrid() == nil {52 m.nbView().Select(0, true)53 }54 screen(m)55 }56}5758// selectedGrid is the selected output's grid.59func selectedGrid(t *testing.T, m *Model) *outGrid {60 t.Helper()61 g, ok := m.nbView().SelectedGrid().(*outGrid)62 if !ok || g.child == nil {63 t.Fatalf("no grid:\n%s", screen(m))64 }65 return g66}6768// A stream's grid takes the rows as they arrive, its columns widening69// to fit a longer row, but not while the reader is in it; the pointer70// and the filter stay.71func TestStreamGridWidens(t *testing.T) {72 m, feed := streamed(t)73 feed(`{level: info, msg: started}`, `{level: info, msg: "GET /"}`)74 g := selectedGrid(t, m)75 msg := g.child.sheet.ColWidth(1)76 if msg != sheet.DefaultWidth || g.Rows() != 2 {77 t.Fatalf("msg %d wide, %d rows", msg, g.Rows())78 }79 long := "slow query on the orders table, 812ms"80 feed(`{level: warn, msg: "` + long[:20] + `"}`)81 if selectedGrid(t, m) != g || g.Rows() != 3 {82 t.Fatalf("the grid was made again, or has %d rows", g.Rows())83 }84 if got := g.child.sheet.ColWidth(1); got != sheet.FitWidth(long[:20]) {85 t.Errorf("msg is %d wide, want %d", got, sheet.FitWidth(long[:20]))86 }87 if s := screen(m); !strings.Contains(s, long[:20]) || !strings.Contains(s, "● live, 3 rows") {88 t.Errorf("the new row:\n%s", s)89 }9091 // In the grid, a filter hiding warnings, the pointer on row 2.92 press(t, m, "<enter>", "<down>")93 g.child.sheet.CreateFilter(g.table())94 g.child.sheet.FilterColumn(0, sheet.Criteria{Hidden: []string{"warn"}})95 feed(`{level: info, msg: "`+long+`"}`, `{level: warn, msg: hidden}`)96 if got := g.child.sheet.ColWidth(1); got != sheet.FitWidth(long[:20]) {97 t.Errorf("msg widened to %d under the pointer", got)98 }99 if g.child.cur != addr("A3") || g.Rows() != 3 || !g.child.sheet.RowHidden(5) {100 t.Errorf("pointer at %s, %d rows showing, row 5 hidden %v", g.child.cur, g.Rows(), g.child.sheet.RowHidden(5))101 }102 press(t, m, "<esc>")103 screen(m) // drawn once left104 if got := g.child.sheet.ColWidth(1); got != sheet.FitWidth(long) {105 t.Errorf("left, msg is %d wide, want %d", got, sheet.FitWidth(long))106 }107108 // A column the reader resized keeps its width.109 g.child.sheet.SetColWidth(0, 6)110 feed(`{level: "a level wider than six", msg: x}`)111 if got := g.child.sheet.ColWidth(0); got != 6 {112 t.Errorf("level is %d wide, resized to 6", got)113 }114}115116// A stream printing 10,000 rows in batches of 250, a frame drawn after117// each as the output updates, costs each batch the rows it brings, well118// within a frame, rather than the rows so far.119func TestStreamGridAtFrameSpeed(t *testing.T) {120 if testing.Short() {121 t.Skip("streams 10,000 rows")122 }123 m, feed := streamed(t)124 term := newFakeTerm(200, 60)125 m.Update(tea.WindowSizeMsg{Width: 200, Height: 60})126 batch := make([]string, 250)127 var worst, all time.Duration128 for b := range 40 {129 for i := range batch {130 n := b*len(batch) + i131 batch[i] = fmt.Sprintf(`{time: "09:%02d:%02d", level: info, msg: "GET /orders/%d 200"}`, n/60%60, n%60, n)132 }133 start := time.Now()134 feed(batch...)135 term.frame(m)136 d := time.Since(start)137 worst, all = max(worst, d), all+d138 }139 if g := selectedGrid(t, m); g.Rows() != 10000 {140 t.Fatalf("%d rows", g.Rows())141 }142 t.Logf("a batch of 250 rows through to its frame: %s on average, %s at worst", all/40, worst)143 if per := all / 40; per > 8*time.Millisecond {144 t.Errorf("a batch takes %s", per)145 }146}