From 1a33bf5232a07a34f16b47c38f2fe9428829ecaf Mon Sep 17 00:00:00 2001 From: xescugc Date: Sun, 14 Dec 2025 01:11:39 +0100 Subject: [PATCH] ebitest: Now It's more reliable (100%?) Added more synchronization on the Click with the game which made it not faile in 100 test. Added full path on the images output Moved the 't' from the initialization to every assertion --- README.md | 18 +++++++------- ebitest.go | 62 +++++++++++++++++++++++++------------------------ ebitest_test.go | 10 ++++---- game.go | 11 ++++++++- ping_pong.go | 35 ++++++++++++++++++++++++++++ selector.go | 7 +++--- tic_tac_toe.go | 38 ++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 49 deletions(-) create mode 100644 tic_tac_toe.go diff --git a/README.md b/README.md index 8a7ee2a..a4aa72d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ Initialize Ebitest with `ebitest.Run(t, g)` with `t *testing.Test` and `g ebiten * `WithFace|Color`: To set the default values when the using the assertions with a text value. * `WithDumpErrorImages`: Which will generate an image when a test fail with the failed assertion on the folder `_ebitest_dump/` -If you need some extra interactions that are not implemented (yet) you can directly use [robotgo](https://github.com/go-vgo/robotgo). +If you need some extra interactions that are not implemented (yet) you can directly use [robotgo](https://github.com/go-vgo/robotgo), +but those may fail as they are not synchronized internally so I would recommend opening an issue and I'll add it. ## Example @@ -66,7 +67,7 @@ import ( func TestGameUI(t *testing.T) { face, _ := loadFont(20) g := newGameUI() - et := ebitest.Run(t, g, + et := ebitest.Run(g, ebitest.WithFace(face), ebitest.WithColor(color.White), ebitest.WithDumpErrorImages(), @@ -76,13 +77,13 @@ func TestGameUI(t *testing.T) { text1 := "Click Me" text2 := "Clicked Me" - t1s, _ := et.Should(text1) - et.ShouldNot(text2) + t1s, _ := et.Should(t, text1) + et.ShouldNot(t, text2) t1s.Click() - et.Should(text1) - et.Should(text2) + et.Should(t, text1) + et.Should(t, text2) } ``` @@ -126,11 +127,10 @@ Basically you cannot run more than one test as even calling `Ebitest.Close()` th Due to the nature of this test (the game is running on a goroutine) there may be the case in which an input is not registered by the game so an expectation may randomly fail. -I kind of fixed it using a custom [PingPong](./ping_pong.go) that basically forces a context switch but it still fails in low resource like -GitHub [Actions](https://github.com/xescugc/ebitest/actions) for example. +I kind of fixed it (100 consecutive test pass) using a custom [PingPong](./ping_pong.go) and [TicTacToe](./tic_tac_toe.go) that basically forces a context switch and synchronizes Input+Game.Update+Game.Draw but it still fails in low resource like GitHub [Actions](https://github.com/xescugc/ebitest/actions) for example. ## Plans * Add more helpers for assertions (like animations) -* Add more inputs (potentially just port all the [robotgo](https://github.com/go-vgo/robotgo) lib) +* Add more inputs (potentially just port all the [robotgo](https://github.com/go-vgo/robotgo) lib) synchronized * Others diff --git a/ebitest.go b/ebitest.go index bb392b1..7f37e81 100644 --- a/ebitest.go +++ b/ebitest.go @@ -16,6 +16,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -24,7 +25,6 @@ const ( type Ebitest struct { game *Game - t *testing.T PingPong *PingPong ctxCancelFn context.CancelFunc @@ -63,7 +63,7 @@ func WithDumpErrorImages() optionsFn { } } -func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest { +func Run(game ebiten.Game, opts ...optionsFn) *Ebitest { ctx, cfn := context.WithCancel(context.TODO()) pingPong := NewPingPong() g := newGame(ctx, game, pingPong) @@ -76,7 +76,6 @@ func Run(t *testing.T, game ebiten.Game, opts ...optionsFn) *Ebitest { et := &Ebitest{ game: g, ctxCancelFn: cfn, - t: t, PingPong: pingPong, endGameChan: endGameChan, } @@ -107,18 +106,19 @@ func (e *Ebitest) Close() { // Should checks if selector(s) is present in the game and returns it // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' -func (e *Ebitest) Should(s interface{}) (*Selector, bool) { - e.t.Helper() +func (e *Ebitest) Should(t *testing.T, s interface{}) (*Selector, bool) { + t.Helper() e.PingPong.Ping() + sc := e.game.GetScreen() - sel, ok := e.findSelector(s) + sel, ok := e.findSelector(sc, s) if !ok { msg := "selector not found" if e.options.dumpErrorImages { - p := dumpErrorImages(e.game.GetScreen(), sel.Image()) + p := dumpErrorImages(sc, sel.Image()) msg += "\nimage at: " + p } - assert.Fail(e.t, msg) + assert.Fail(t, msg) return nil, false } @@ -127,39 +127,41 @@ func (e *Ebitest) Should(s interface{}) (*Selector, bool) { // ShouldNot checks if selector(s) is not present in the game // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' -func (e *Ebitest) ShouldNot(s interface{}) bool { - e.t.Helper() +func (e *Ebitest) ShouldNot(t *testing.T, s interface{}) bool { + t.Helper() e.PingPong.Ping() + sc := e.game.GetScreen() - sel, ok := e.findSelector(s) + sel, ok := e.findSelector(sc, s) if !ok { return true } msg := "selector found" if e.options.dumpErrorImages { - p := dumpErrorImages(e.game.GetScreen(), sel.Image()) + p := dumpErrorImages(sc, sel.Image()) msg += "\nimage at: " + p } - assert.Fail(e.t, msg) + assert.Fail(t, msg) return false } // Must checks if selector(s) is present in the game and returns it. // If it's not present it'll fail the test // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' -func (e *Ebitest) Must(s interface{}) *Selector { - e.t.Helper() +func (e *Ebitest) Must(t *testing.T, s interface{}) *Selector { + t.Helper() e.PingPong.Ping() + sc := e.game.GetScreen() - sel, ok := e.findSelector(s) + sel, ok := e.findSelector(sc, s) if !ok { msg := "selector not found" if e.options.dumpErrorImages { - p := dumpErrorImages(e.game.GetScreen(), sel.Image()) + p := dumpErrorImages(sc, sel.Image()) msg += "\nimage at: " + p } - assert.Fail(e.t, msg) + require.Fail(t, msg) return nil } @@ -169,21 +171,22 @@ func (e *Ebitest) Must(s interface{}) *Selector { // MustNot checks if selector(s) is not present in the game. // If it's present it'll fail the test // s can be a: 'string', 'image.Image', '*ebiten.Image' and '*ebitest.Selector' -func (e *Ebitest) MustNot(s interface{}) { - e.t.Helper() +func (e *Ebitest) MustNot(t *testing.T, s interface{}) { + t.Helper() e.PingPong.Ping() + sc := e.game.GetScreen() - sel, ok := e.findSelector(s) + sel, ok := e.findSelector(sc, s) if !ok { return } msg := "selector found" if e.options.dumpErrorImages { - p := dumpErrorImages(e.game.GetScreen(), sel.Image()) + p := dumpErrorImages(sc, sel.Image()) msg += "\nimage at: " + p } - assert.Fail(e.t, msg) + require.Fail(t, msg) return } @@ -204,16 +207,14 @@ func (e *Ebitest) getSelector(s interface{}) *Selector { } // findSelector returns a Selector from ss if found -func (e *Ebitest) findSelector(ss interface{}) (*Selector, bool) { +func (e *Ebitest) findSelector(sc image.Image, ss interface{}) (*Selector, bool) { sel := e.getSelector(ss) - s := e.game.GetScreen() - - sx := s.Bounds().Dx() - sy := s.Bounds().Dy() + sx := sc.Bounds().Dx() + sy := sc.Bounds().Dy() for x := range sx { for y := range sy { - if hasImageAt(s, sel.Image(), x, y) { + if hasImageAt(sc, sel.Image(), x, y) { selx := sel.Image().Bounds().Dx() sely := sel.Image().Bounds().Dy() @@ -244,7 +245,8 @@ func dumpErrorImages(s, i image.Image) string { ip := filepath.Join(baseDumpFoler, u.String()+".png") writeImage(ip, img) - return ip + wd, _ := os.Getwd() + return filepath.Join(wd, ip) } // writeImage writes on the path the image i diff --git a/ebitest_test.go b/ebitest_test.go index fb5c141..ba910ba 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -13,7 +13,7 @@ import ( func TestGameButton(t *testing.T) { face, _ := testdata.LoadFont(20) g := testdata.NewGame() - et := ebitest.Run(t, g, + et := ebitest.Run(g, ebitest.WithFace(face), ebitest.WithColor(color.White), ebitest.WithDumpErrorImages(), @@ -30,11 +30,11 @@ func TestGameButton(t *testing.T) { text1 := "Click Me" text2 := "Clicked Me" - t1s, _ := et.Should(text1) - et.ShouldNot(text2) + t1s, _ := et.Should(t, text1) + et.ShouldNot(t, text2) t1s.Click() - et.ShouldNot(text1) - et.Should(text2) + et.ShouldNot(t, text1) + et.Should(t, text2) } diff --git a/game.go b/game.go index 89b9d3c..83cdf0a 100644 --- a/game.go +++ b/game.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" ) type Game struct { @@ -16,6 +17,8 @@ type Game struct { ctx context.Context pingPong *PingPong + + clickTTT *TicTacToe } func newGame(ctx context.Context, g ebiten.Game, pp *PingPong) *Game { @@ -23,6 +26,7 @@ func newGame(ctx context.Context, g ebiten.Game, pp *PingPong) *Game { game: g, ctx: ctx, pingPong: pp, + clickTTT: NewTicTacToe(), } } @@ -50,8 +54,11 @@ func (g *Game) Update() error { case <-g.ctx.Done(): return ebiten.Termination default: - return g.game.Update() } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + g.clickTTT.Tac() + } + return g.game.Update() } // Draw implements Ebiten's Draw method. @@ -61,6 +68,8 @@ func (g *Game) Draw(screen *ebiten.Image) { g.SetScreen(ebitenImageToImage(screen)) g.pingPong.Pong() + g.pingPong.ClickPong(g.clickTTT) + g.clickTTT.Toe() return } diff --git a/ping_pong.go b/ping_pong.go index 7ab978d..1c024aa 100644 --- a/ping_pong.go +++ b/ping_pong.go @@ -1,9 +1,20 @@ package ebitest +import ( + "github.com/go-vgo/robotgo" +) + // PingPong used to switch contexts between goroutines type PingPong struct { ping chan struct{} pong chan struct{} + + clickPing chan Ball + clickPong chan struct{} +} + +type Ball struct { + X, Y int } // NewPingPong initialines a new PingPong @@ -11,6 +22,9 @@ func NewPingPong() *PingPong { return &PingPong{ ping: make(chan struct{}, 1), pong: make(chan struct{}, 1), + + clickPing: make(chan Ball, 1), + clickPong: make(chan struct{}, 1), } } @@ -28,3 +42,24 @@ func (pp *PingPong) Pong() { pp.pong <- struct{}{} } } + +// Ping sends a ping and waits for a Pong +func (pp *PingPong) ClickPing(b Ball) { + pp.clickPing <- b + <-pp.clickPong +} + +// Pong will read on ping if any present and then +// will send to pong +func (pp *PingPong) ClickPong(ttt *TicTacToe) { + if len(pp.clickPing) != 0 { + b := <-pp.clickPing + robotgo.Move(b.X, b.Y) + robotgo.Click("left") + ttt.Tic() + go func() { + <-ttt.toe + pp.clickPong <- struct{}{} + }() + } +} diff --git a/selector.go b/selector.go index 750a555..900d39e 100644 --- a/selector.go +++ b/selector.go @@ -4,7 +4,6 @@ import ( "image" "image/color" - "github.com/go-vgo/robotgo" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/text/v2" ) @@ -39,9 +38,9 @@ func NewFromImage(i image.Image) *Selector { func (s *Selector) Click() { cx, cy := s.center() - robotgo.Move(cx, cy) - robotgo.Click("left", true) - s.PingPong.Ping() + //robotgo.Move(cx, cy) + //robotgo.Click("left") + s.PingPong.ClickPing(Ball{X: cx, Y: cy}) } func (s *Selector) center() (int, int) { diff --git a/tic_tac_toe.go b/tic_tac_toe.go new file mode 100644 index 0000000..e5870b7 --- /dev/null +++ b/tic_tac_toe.go @@ -0,0 +1,38 @@ +package ebitest + +// TicTacToe is a synchronizer of 3 channesl +type TicTacToe struct { + tic chan struct{} + tac chan struct{} + toe chan struct{} +} + +// NewTicTacToe returns a new TicTacToe +func NewTicTacToe() *TicTacToe { + return &TicTacToe{ + tic: make(chan struct{}, 1), + tac: make(chan struct{}, 1), + toe: make(chan struct{}, 1), + } +} + +// Tic first +func (ttt *TicTacToe) Tic() { + ttt.tic <- struct{}{} +} + +// Tac second +func (ttt *TicTacToe) Tac() { + if len(ttt.tic) != 0 { + <-ttt.tic + ttt.tac <- struct{}{} + } +} + +// Toe third +func (ttt *TicTacToe) Toe() { + if len(ttt.tac) != 0 { + <-ttt.tac + ttt.toe <- struct{}{} + } +}