Skip to content

Commit

Permalink
feat: use vt on teatest/v2
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos Alexandro Becker <[email protected]>
  • Loading branch information
caarlos0 committed Nov 13, 2024
1 parent a30b032 commit bc6939f
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 127 deletions.
16 changes: 8 additions & 8 deletions exp/teatest/v2/app_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package teatest_test

import (
"bytes"
"fmt"
"io"
"regexp"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -35,9 +35,9 @@ func TestApp(t *testing.T) {
t.Fatal(err)
}

out := readBts(t, tm.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))
if !regexp.MustCompile(`This program will exit in \d+ seconds`).Match(out) {
t.Fatalf("output does not match the given regular expression: %s", string(out))
out := tm.FinalOutput(t, teatest.WithFinalTimeout(time.Second))
if !regexp.MustCompile(`This program will exit in \d+ seconds`).MatchString(out) {
t.Fatalf("output does not match the given regular expression: %q", out)
}
teatest.RequireEqualOutput(t, out)

Expand All @@ -56,12 +56,12 @@ func TestAppInteractive(t *testing.T) {
time.Sleep(time.Second + time.Millisecond*200)
tm.Send("ignored msg")

if bts := readBts(t, tm.Output()); !bytes.Contains(bts, []byte("This program will exit in 9 seconds")) {
t.Fatalf("output does not match: expected %q", string(bts))
if s := tm.Output(); !strings.Contains(s, "This program will exit in 9 seconds") {
t.Fatalf("output does not match: expected %q", string(s))
}

teatest.WaitFor(t, tm.Output(), func(out []byte) bool {
return bytes.Contains(out, []byte("This program will exit in 7 seconds"))
teatest.WaitForOutput(t, tm, func(s string) bool {
return strings.Contains(s, "This program will exit in 7 seconds")
}, teatest.WithDuration(5*time.Second), teatest.WithCheckInterval(time.Millisecond*10))

tm.Send(tea.KeyPressMsg{
Expand Down
25 changes: 14 additions & 11 deletions exp/teatest/v2/go.mod
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
module github.com/charmbracelet/x/exp/teatest/v2

go 1.19
go 1.22.8

require (
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20241113134142-c71ad13e23d6
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a
github.com/charmbracelet/x/vt v0.0.0-20241017211702-84fa5b7bb18e
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.3.2 // indirect
github.com/charmbracelet/colorprofile v0.1.7 // indirect
github.com/charmbracelet/x/ansi v0.4.6-0.20241110171603-a30b032a5ae2 // indirect
github.com/charmbracelet/x/cellbuf v0.0.6-0.20241110171603-a30b032a5ae2 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 // indirect
github.com/charmbracelet/x/windows v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
)

replace github.com/charmbracelet/x/vt => ../../../vt

replace github.com/charmbracelet/x/cellbuf => ../../../cellbuf
37 changes: 15 additions & 22 deletions exp/teatest/v2/go.sum
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1 h1:OZtpLCsuuPplC+1oyUo+/eAN7e9MC2UyZWKlKrVlUnw=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.1/go.mod h1:j0gn4ft5CE7NDYNZjAA3hBM8t2OPjI8urxuAD0oR4w8=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY=
github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20241113134142-c71ad13e23d6 h1:kRj022q2jfr69oRZNnhev/Em44M7/TjV7jvWpyQ9PMo=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20241113134142-c71ad13e23d6/go.mod h1:Az92EQe8w9w+TgIPiTjbZtVohnlxwHiVDNJMPUTSg2o=
github.com/charmbracelet/colorprofile v0.1.7 h1:q7PtMQrRBBnLNE2EbtbNUtouu979EivKcDGGaimhyO8=
github.com/charmbracelet/colorprofile v0.1.7/go.mod h1:d3UYToTrNmsD2p9/lbiya16H1WahndM0miDlJWXWf4U=
github.com/charmbracelet/x/ansi v0.4.6-0.20241110171603-a30b032a5ae2 h1:iW1rX9FDCWBSIusGCmCLQdd2f9gN9c88KJyvCt1EsRA=
github.com/charmbracelet/x/ansi v0.4.6-0.20241110171603-a30b032a5ae2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91 h1:D5OO0lVavz7A+Swdhp62F9gbkibxmz9B2hZ/jVdMPf0=
github.com/charmbracelet/x/wcwidth v0.0.0-20241011142426-46044092ad91/go.mod h1:Ey8PFmYwH+/td9bpiEx07Fdx9ZVkxfIjWXxBluxF4Nw=
github.com/charmbracelet/x/windows v0.2.0 h1:ilXA1GJjTNkgOm94CLPeSz7rar54jtFatdmoiONPuEw=
github.com/charmbracelet/x/windows v0.2.0/go.mod h1:ZibNFR49ZFqCXgP76sYanisxRyC+EYrBE7TTknD8s1s=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
4 changes: 2 additions & 2 deletions exp/teatest/v2/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func TestAppSendToOtherProgram(t *testing.T) {
tm1.Type("q")
tm2.Type("q")

out1 := readBts(t, tm1.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))
out2 := readBts(t, tm2.FinalOutput(t, teatest.WithFinalTimeout(time.Second)))
out1 := tm1.FinalOutput(t, teatest.WithFinalTimeout(time.Second))
out2 := tm2.FinalOutput(t, teatest.WithFinalTimeout(time.Second))

if string(out1) != string(out2) {
t.Errorf("output of both models should be the same, got:\n%v\nand:\n%v\n", string(out1), string(out2))
Expand Down
84 changes: 27 additions & 57 deletions exp/teatest/v2/teatest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
package teatest

import (
"bytes"
"fmt"
"io"
"os"
"os/signal"
"sync"
Expand All @@ -14,6 +12,7 @@ import (

tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/x/exp/golden"
"github.com/charmbracelet/x/vt"
)

// Program defines the subset of the tea.Program API we need for testing.
Expand Down Expand Up @@ -63,22 +62,22 @@ func WithDuration(d time.Duration) WaitForOption {
}
}

// WaitFor keeps reading from r until the condition matches.
// WaitForOutput keeps reading from r until the condition matches.
// Default duration is 1s, default check interval is 50ms.
// These defaults can be changed with WithDuration and WithCheckInterval.
func WaitFor(
func WaitForOutput(
tb testing.TB,
r io.Reader,
condition func(bts []byte) bool,
tm *TestModel,
condition func(string) bool,
options ...WaitForOption,
) {
tb.Helper()
if err := doWaitFor(r, condition, options...); err != nil {
if err := doWaitFor(tm, condition, options...); err != nil {
tb.Fatal(err)
}
}

func doWaitFor(r io.Reader, condition func(bts []byte) bool, options ...WaitForOption) error {
func doWaitFor(tm *TestModel, condition func(string) bool, options ...WaitForOption) error {
wf := WaitingForContext{
Duration: time.Second,
CheckInterval: 50 * time.Millisecond, //nolint: gomnd
Expand All @@ -88,26 +87,21 @@ func doWaitFor(r io.Reader, condition func(bts []byte) bool, options ...WaitForO
opt(&wf)
}

var b bytes.Buffer
start := time.Now()
for time.Since(start) <= wf.Duration {
if _, err := io.ReadAll(io.TeeReader(r, &b)); err != nil {
return fmt.Errorf("WaitFor: %w", err)
}
if condition(b.Bytes()) {
if condition(tm.Output()) {
return nil
}
time.Sleep(wf.CheckInterval)
}
return fmt.Errorf("WaitFor: condition not met after %s. Last output:\n%s", wf.Duration, b.String())
return fmt.Errorf("WaitFor: condition not met after %s. Last output:\n%q", wf.Duration, tm.Output())
}

// TestModel is a model that is being tested.
type TestModel struct {
program *tea.Program

in *bytes.Buffer
out io.ReadWriter
term *vt.Terminal

modelCh chan tea.Model
model tea.Model
Expand All @@ -118,17 +112,24 @@ type TestModel struct {

// NewTestModel makes a new TestModel which can be used for tests.
func NewTestModel(tb testing.TB, m tea.Model, options ...TestOption) *TestModel {
var opts TestModelOptions
for _, opt := range options {
opt(&opts)
}
if opts.size.Width == 0 {
opts.size.Width, opts.size.Height = 70, 40
}

tm := &TestModel{
in: bytes.NewBuffer(nil),
out: safe(bytes.NewBuffer(nil)),
term: vt.NewTerminal(opts.size.Width, opts.size.Height),
modelCh: make(chan tea.Model, 1),
doneCh: make(chan bool, 1),
}

tm.program = tea.NewProgram(
m,
tea.WithInput(tm.in),
tea.WithOutput(tm.out),
tea.WithInput(tm.term),
tea.WithOutput(tm.term),
tea.WithoutSignals(),
)

Expand All @@ -149,14 +150,7 @@ func NewTestModel(tb testing.TB, m tea.Model, options ...TestOption) *TestModel
tm.program.Kill()
}()

var opts TestModelOptions
for _, opt := range options {
opt(&opts)
}

if opts.size.Width != 0 {
tm.program.Send(opts.size)
}
tm.program.Send(opts.size)
return tm
}

Expand Down Expand Up @@ -229,14 +223,14 @@ func (tm *TestModel) FinalModel(tb testing.TB, opts ...FinalOpt) tea.Model {
// FinalOutput returns the program's final output io.Reader.
// This method only returns once the program has finished running or when it
// times out.
func (tm *TestModel) FinalOutput(tb testing.TB, opts ...FinalOpt) io.Reader {
func (tm *TestModel) FinalOutput(tb testing.TB, opts ...FinalOpt) string {
tm.waitDone(tb, opts)
return tm.Output()
}

// Output returns the program's current output io.Reader.
func (tm *TestModel) Output() io.Reader {
return tm.out
func (tm *TestModel) Output() string {
return tm.term.String()
}

// Send sends messages to the underlying program.
Expand Down Expand Up @@ -271,31 +265,7 @@ func (tm *TestModel) GetProgram() *tea.Program {
// Important: this uses the system `diff` tool.
//
// You can update the golden files by running your tests with the -update flag.
func RequireEqualOutput(tb testing.TB, out []byte) {
func RequireEqualOutput(tb testing.TB, out string) {
tb.Helper()
golden.RequireEqualEscape(tb, out, true)
}

func safe(rw io.ReadWriter) io.ReadWriter {
return &safeReadWriter{rw: rw}
}

// safeReadWriter implements io.ReadWriter, but locks reads and writes.
type safeReadWriter struct {
rw io.ReadWriter
m sync.RWMutex
}

// Read implements io.ReadWriter.
func (s *safeReadWriter) Read(p []byte) (n int, err error) {
s.m.RLock()
defer s.m.RUnlock()
return s.rw.Read(p) //nolint: wrapcheck
}

// Write implements io.ReadWriter.
func (s *safeReadWriter) Write(p []byte) (int, error) {
s.m.Lock()
defer s.m.Unlock()
return s.rw.Write(p) //nolint: wrapcheck
golden.RequireEqualEscape(tb, []byte(out), true)
}
47 changes: 22 additions & 25 deletions exp/teatest/v2/teatest_test.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
package teatest

import (
"fmt"
"strings"
"testing"
"testing/iotest"
"time"

tea "github.com/charmbracelet/bubbletea/v2"
)

func TestWaitForErrorReader(t *testing.T) {
err := doWaitFor(iotest.ErrReader(fmt.Errorf("fake")), func(bts []byte) bool {
return true
}, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond))
if err == nil {
t.Fatal("expected an error, got nil")
}
if err.Error() != "WaitFor: fake" {
t.Fatalf("unexpected error: %s", err.Error())
}
}
// func TestWaitForErrorReader(t *testing.T) {
// err := doWaitFor(iotest.ErrReader(fmt.Errorf("fake")), func(bts []byte) bool {
// return true
// }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond))
// if err == nil {
// t.Fatal("expected an error, got nil")
// }
// if err.Error() != "WaitFor: fake" {
// t.Fatalf("unexpected error: %s", err.Error())
// }
// }

func TestWaitForTimeout(t *testing.T) {
err := doWaitFor(strings.NewReader("nope"), func(bts []byte) bool {
return false
}, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond))
if err == nil {
t.Fatal("expected an error, got nil")
}
if err.Error() != "WaitFor: condition not met after 1ms. Last output:\nnope" {
t.Fatalf("unexpected error: %s", err.Error())
}
}
// func TestWaitForTimeout(t *testing.T) {
// err := doWaitFor(strings.NewReader("nope"), func(bts []byte) bool {
// return false
// }, WithDuration(time.Millisecond), WithCheckInterval(10*time.Microsecond))
// if err == nil {
// t.Fatal("expected an error, got nil")
// }
// if err.Error() != "WaitFor: condition not met after 1ms. Last output:\nnope" {
// t.Fatalf("unexpected error: %s", err.Error())
// }
// }

type m string

Expand Down
2 changes: 1 addition & 1 deletion go.work
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
go 1.21
go 1.22.8

use (
./ansi
Expand Down
Loading

0 comments on commit bc6939f

Please sign in to comment.