diff --git a/Makefile b/Makefile index 9b52d01..f9e2116 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ help: ## Show this help @grep -F -h "##" $(MAKEFILE_LIST) | grep -F -v grep -F | sed -e 's/:.*##/:##/' | column -t -s '##' .PHONY: test -test: ## Runs the test - @xvfb-run go test ./... $(ARGS) +test: + @xvfb-run go test ./... .PHONY: pprof pprof: ## Runs pprof server for 'cpu.out' diff --git a/README.md b/README.md index 01a3699..2483223 100644 --- a/README.md +++ b/README.md @@ -91,28 +91,43 @@ func TestGameUI(t *testing.T) { } ``` -The output of this test (that fails) is the following: +An output could be for example ``` ---- FAIL: TestGameUI (7.52s) - ebitest_test.go:28: - Error Trace: /home/xescugc/repos/ebitest/ebitest.go:113 - /home/xescugc/repos/ebitest/ebitest_test.go:28 +--- FAIL: TestGameButton (9.34s) + ebitest_test.go:39: + Error Trace: ebitest.go:150 + ebitest_test.go:39 + Error: selector found + image at: ebitest/_ebitest_dump/019b2e81-8d9a-7bb7-ba16-639756d11e58.png + Test: TestGameButton + ebitest_test.go:43: + Error Trace: ebitest.go:126 + ebitest_test.go:43 Error: selector not found - image at: _ebitest_dump/019b1537-1c60-7041-ad54-0297ea4b0eef.png - Test: TestGameUI + image at: ebitest/_ebitest_dump/019b2e81-966e-7715-8c5f-6a6acc92720c.png + Test: TestGameButton FAIL -FAIL github.com/xescugc/ebitest 7.578s +FAIL github.com/xescugc/ebitest 9.393s FAIL -make: *** [Makefile:7: test] Error 1 +make: *** [Makefile:7: test] Error ``` -And if you open the `_ebitest_dump/019b1537-1c60-7041-ad54-0297ea4b0eef.png` (on the current path) you see +And then you have the `image at: ebitest/_ebitest_dump/019b2e81-8d9a-7bb7-ba16-639756d11e58.png` that expects to not find something, and it finds +it and reports the image with the highlight of what was found. At the top right you can see what was looking for.

- +

+And the `image at: ebitest/_ebitest_dump/019b2e81-966e-7715-8c5f-6a6acc92720c.png` that expect to find something that was not found. At the top right +you can see what was looking for. + +

+ +

+ + ## Run it on a CI If the CI has low resources (like GitHub Actions) it'll most likely fail (check `Known issues#2`) but you @@ -133,6 +148,11 @@ so an expectation may randomly fail. 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. +3/ Size of the screen + +By default the screen is of `640x480` if using `xvfb`. To make it bigger you can directly increase the size with `ebiten.SetWindowSize(750, 750)`, though setting big sizes +I've seen it causes some issues that the click are not where they are expected to be and are a bit off and miss which causes errors. + ## Plans * Add more helpers for assertions (like animations) diff --git a/docs/error_image.png b/docs/error_image.png deleted file mode 100644 index 6033dba..0000000 Binary files a/docs/error_image.png and /dev/null differ diff --git a/docs/should.png b/docs/should.png new file mode 100644 index 0000000..680da7c Binary files /dev/null and b/docs/should.png differ diff --git a/docs/should_not.png b/docs/should_not.png new file mode 100644 index 0000000..5b40420 Binary files /dev/null and b/docs/should_not.png differ diff --git a/ebitest.go b/ebitest.go index ee58304..e0248aa 100644 --- a/ebitest.go +++ b/ebitest.go @@ -24,6 +24,10 @@ const ( findAllSelectors = true ) +var ( + emptyRec image.Rectangle +) + type Ebitest struct { game *Game PingPong *PingPong @@ -116,7 +120,7 @@ func (e *Ebitest) Should(t *testing.T, s interface{}) (*Selector, bool) { if !ok { msg := "selector not found" if e.options.dumpErrorImages { - p := dumpErrorImages(sc, sel.Image()) + p := dumpErrorImages(sc, sel) msg += "\nimage at: " + p } assert.Fail(t, msg) @@ -140,7 +144,7 @@ func (e *Ebitest) ShouldNot(t *testing.T, s interface{}) bool { msg := "selector found" if e.options.dumpErrorImages { - p := dumpErrorImages(sc, sel.Image()) + p := dumpErrorImages(sc, sel) msg += "\nimage at: " + p } assert.Fail(t, msg) @@ -159,7 +163,7 @@ func (e *Ebitest) Must(t *testing.T, s interface{}) *Selector { if !ok { msg := "selector not found" if e.options.dumpErrorImages { - p := dumpErrorImages(sc, sel.Image()) + p := dumpErrorImages(sc, sel) msg += "\nimage at: " + p } require.Fail(t, msg) @@ -184,7 +188,7 @@ func (e *Ebitest) MustNot(t *testing.T, s interface{}) { msg := "selector found" if e.options.dumpErrorImages { - p := dumpErrorImages(sc, sel.Image()) + p := dumpErrorImages(sc, sel) msg += "\nimage at: " + p } require.Fail(t, msg) @@ -193,7 +197,9 @@ func (e *Ebitest) MustNot(t *testing.T, s interface{}) { // GetAll returns all the repeated instances of s or none if nothing is found func (e *Ebitest) GetAll(s interface{}) []*Selector { sc := e.game.GetScreen() - return e.findSelectors(sc, s, findAllSelectors) + sels, _ := e.findSelectors(sc, s, findAllSelectors) + + return sels } // KeyTap taps all the keys at once @@ -222,15 +228,15 @@ func (e *Ebitest) getSelector(s interface{}) *Selector { // findSelector returns a Selector from ss if found. `all` will basically mean it'll return all of them func (e *Ebitest) findSelector(sc image.Image, ss interface{}) (*Selector, bool) { - sels := e.findSelectors(sc, ss, !findAllSelectors) + sels, sel := e.findSelectors(sc, ss, !findAllSelectors) if len(sels) == 0 { - return nil, false + return sel, false } return sels[0], true } // findSelector returns a Selector from ss if found. `all` will basically mean it'll return all of them -func (e *Ebitest) findSelectors(sc image.Image, ss interface{}, all bool) []*Selector { +func (e *Ebitest) findSelectors(sc image.Image, ss interface{}, all bool) ([]*Selector, *Selector) { selectors := make([]*Selector, 0) bsel := e.getSelector(ss) @@ -247,18 +253,50 @@ func (e *Ebitest) findSelectors(sc image.Image, ss interface{}, all bool) []*Sel sel.PingPong = e.PingPong selectors = append(selectors, sel) if !all { - return selectors + return selectors, bsel } } } } - return selectors + return selectors, bsel +} + +// hasImageAt checks if the image sub is image i at the ix, iy +func hasImageAt(i, sub image.Image, ix, iy int) bool { + sx, sy := sub.Bounds().Dx(), sub.Bounds().Dy() + for x := range sx { + for y := range sy { + sc := sub.At(x, y) + ic := i.At(ix+x, iy+y) + + nsc := sc.(color.NRGBA) + // If the source it's transparent we ignore it + // we want to only compare colors so we consider + // it as good + if nsc.A != 255 { + continue + } + + if !equalColors(sc, ic) { + return false + } + } + } + return true +} + +// equalColors checks if c1 and c2 have the same RGB +func equalColors(c1, c2 color.Color) bool { + r1, g1, b1, _ := c1.RGBA() + r2, g2, b2, _ := c2.RGBA() + return r1 == r2 && g1 == g2 && b1 == b2 } // dumpErrorImages dumps a composition of the 2 images into 1 so it displays // what was checked -func dumpErrorImages(s, i image.Image) string { +func dumpErrorImages(s image.Image, sel *Selector) string { + i := sel.Image() sb := s.Bounds() ib := i.Bounds() x := sb.Dx() + ib.Dx() @@ -268,6 +306,10 @@ func dumpErrorImages(s, i image.Image) string { draw.Draw(img, sb, s, image.Point{}, draw.Over) draw.Draw(img, image.Rect(sb.Dx(), 0, x, ib.Dy()), i, image.Point{}, draw.Over) + if sel.Rec() != emptyRec { + drawRectangle(img, sel.Rec(), 2) + } + u, _ := uuid.NewV7() ip := filepath.Join(baseDumpFoler, u.String()+".png") @@ -277,6 +319,24 @@ func dumpErrorImages(s, i image.Image) string { return filepath.Join(wd, ip) } +// drawRectangle will draw in the image(img) the rectangel(rec) with thiknes +func drawRectangle(img *image.RGBA, rec image.Rectangle, thickness int) { + col := color.RGBA{255, 0, 0, 255} + + for t := 0; t < thickness; t++ { + // draw horizontal lines + for x := rec.Min.X; x <= rec.Max.X; x++ { + img.Set(x, rec.Min.Y+t, col) + img.Set(x, rec.Max.Y-t, col) + } + // draw vertical lines + for y := rec.Min.Y; y <= rec.Max.Y; y++ { + img.Set(rec.Min.X+t, y, col) + img.Set(rec.Max.X-t, y, col) + } + } +} + // writeImage writes on the path the image i func writeImage(path string, i image.Image) { f, err := os.Create(path) @@ -289,53 +349,3 @@ func writeImage(path string, i image.Image) { log.Fatal(err) } } - -// hasImageAt checks if the image sub is image i at the ix, iy -func hasImageAt(i, sub image.Image, ix, iy int) bool { - sx, sy := sub.Bounds().Dx(), sub.Bounds().Dy() - for x := range sx { - for y := range sy { - ic := toNRGBA(i.At(ix+x, iy+y)) - sc := toNRGBA(sub.At(x, y)) - sr, sg, sb, sa := sc.RGBA() - - // If the source it's transparent we ignore it - // we want to only compare colors so we consider - // it as good - if sa == 0 || (sr == 0 && sg == 0 && sb == 0) { - continue - } - - if !equalColors(sc, ic) { - return false - } - } - } - return true -} - -// equalColors checks if c1 and c2 have the same RGB -func equalColors(c1, c2 color.Color) bool { - r1, g1, b1, _ := c1.RGBA() - r2, g2, b2, _ := c2.RGBA() - return r1 == r2 && g1 == g2 && b1 == b2 -} - -// toNRGBA convers a pre-multiplied alpha color to a non pre-multiplied alpha one -func toNRGBA(c color.Color) color.Color { - r, g, b, a := c.RGBA() - if a == 0 { - return color.NRGBA{0, 0, 0, 0} - } - - // Since color.Color is alpha pre-multiplied, we need to divide the - // RGB values by alpha again in order to get back the original RGB. - r *= 0xffff - r /= a - g *= 0xffff - g /= a - b *= 0xffff - b /= a - - return color.NRGBA{uint8(r / 65535), uint8(g / 65535), uint8(b / 65535), 255} -} diff --git a/ebitest_test.go b/ebitest_test.go index bff47a4..e55bda3 100644 --- a/ebitest_test.go +++ b/ebitest_test.go @@ -32,10 +32,16 @@ func TestGameButton(t *testing.T) { text1_2 := "Click Me 2" text2 := "Clicked Me" - t1s, _ := et.Should(t, text1) + t1s := et.Must(t, text1) t1_2s, _ := et.Should(t, text1_2) + + // Fails + et.ShouldNot(t, text1_2) et.ShouldNot(t, text2) + // Fails + et.Should(t, text2) + t1s.Click() et.Should(t, text1_2) @@ -47,7 +53,6 @@ func TestGameButton(t *testing.T) { et.ShouldNot(t, text1_2) assert.Len(t, et.GetAll(text2), 2) - //et.KeyTap(ebiten.KeyShift, ebiten.KeyI) et.KeyTap(ebiten.KeyI, ebiten.KeyShift) assert.True(t, g.ClickedShiftI) } diff --git a/utils.go b/utils.go index 2aa24eb..00e099a 100644 --- a/utils.go +++ b/utils.go @@ -9,7 +9,7 @@ import ( // ebitenImageToImage converts an ebiten.Image to an image.Image func ebitenImageToImage(ei *ebiten.Image) image.Image { b := ei.Bounds() - img := image.NewGray(image.Rect(0, 0, b.Dx(), b.Dy())) + img := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) ix, iy := ei.Bounds().Dx(), ei.Bounds().Dy() for x := range ix {