Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<p align="center">
<img src="docs/error_image.png" width=50% height=50%>
<img src="docs/should_not.png" width=50% height=50%>
</p>

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.

<p align="center">
<img src="docs/should.png" width=50% height=50%>
</p>


## 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
Expand All @@ -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)
Expand Down
Binary file removed docs/error_image.png
Binary file not shown.
Binary file added docs/should.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/should_not.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
132 changes: 71 additions & 61 deletions ebitest.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const (
findAllSelectors = true
)

var (
emptyRec image.Rectangle
)

type Ebitest struct {
game *Game
PingPong *PingPong
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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}
}
9 changes: 7 additions & 2 deletions ebitest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
2 changes: 1 addition & 1 deletion utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down