Skip to content

Commit 41fbc76

Browse files
committed
Merged frosttest into frostutil.
Added comments to test.go. Improved test.go to fail tests if OnTestMain wasn't called, and to say why, instead of just hanging because nothing was on the other side of the channel(s). Updated ebitengine dependency to 2.4.10. Updated readme.
1 parent 2f9af7e commit 41fbc76

File tree

7 files changed

+309
-13
lines changed

7 files changed

+309
-13
lines changed

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ module github.com/amanitaverna/frostutil
33
go 1.19
44

55
require (
6-
github.com/amanitaverna/frosttest v1.0.0
7-
github.com/hajimehoshi/ebiten/v2 v2.4.9
6+
github.com/hajimehoshi/ebiten/v2 v2.4.10
87
github.com/stretchr/testify v1.8.1
98
)
109

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
2-
github.com/amanitaverna/frosttest v1.0.0 h1:mROiXw8C7hSFpFnkzhHEbwhoG+aVOAesvljzFoBTmCM=
3-
github.com/amanitaverna/frosttest v1.0.0/go.mod h1:ZMrmEtlAlnHC75UBZQJplHXoRQgfBHSFvm3nRNweJTw=
42
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
64
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -11,8 +9,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2
119
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
1210
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
1311
github.com/hajimehoshi/bitmapfont/v2 v2.2.2/go.mod h1:Ua/x9Dkz7M9CU4zr1VHWOqGwjKdXbOTRsH7lWfb1Co0=
14-
github.com/hajimehoshi/ebiten/v2 v2.4.9 h1:EEOeGXJpcK9LMaGs/EIYxrgIMO/rbPHDtv8Fteyi4s8=
15-
github.com/hajimehoshi/ebiten/v2 v2.4.9/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
12+
github.com/hajimehoshi/ebiten/v2 v2.4.10 h1:3w/yotz42zDfb1f7jAzdESZInQZsqj4/4beosW9te9M=
13+
github.com/hajimehoshi/ebiten/v2 v2.4.10/go.mod h1:BZcqCU4XHmScUi+lsKexocWcf4offMFwfp8dVGIB/G4=
1614
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
1715
github.com/hajimehoshi/file2byteslice v1.0.0 h1:ljd5KTennqyJ4vG9i/5jS8MD1prof97vlH5JOdtw3WU=
1816
github.com/hajimehoshi/file2byteslice v1.0.0/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=

image_test.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"image"
77
"testing"
88

9-
"github.com/amanitaverna/frosttest"
109
"github.com/amanitaverna/frostutil"
1110
"github.com/hajimehoshi/ebiten/v2"
1211
"github.com/stretchr/testify/assert"
@@ -155,7 +154,7 @@ func checkImagePatternImpl(pixelData []byte, rowBytes, stride int, alphaTestMode
155154

156155
// Tests NewEImageFromImage and NewImageFromEImage
157156
func Test_ImageConversion(t *testing.T) {
158-
frosttest.QueueUpdateTest(t, test_ImageConversion)
157+
frostutil.QueueUpdateTest(t, test_ImageConversion)
159158
}
160159

161160
func test_ImageConversion(t *testing.T) {
@@ -204,7 +203,7 @@ func test_ImageConversionImpl(ass *assert.Assertions, img image.Image, alphaTest
204203

205204
// Tests CopyImage. We want to verify that it correctly copies *ebiten.Image, *image.NRGBA, and *image.RGBA images.
206205
func Test_CopyImage(t *testing.T) {
207-
frosttest.QueueUpdateTest(t, test_CopyImage)
206+
frostutil.QueueUpdateTest(t, test_CopyImage)
208207
}
209208

210209
// Tests CopyImage. We want to verify that it correctly copies *ebiten.Image, *image.NRGBA, and *image.RGBA images.
@@ -276,7 +275,7 @@ func Test_CopyImageLines(t *testing.T) {
276275

277276
// Test SlowImageCopy
278277
func Test_SlowImageCopy(t *testing.T) {
279-
frosttest.QueueUpdateTest(t, test_SlowImageCopy)
278+
frostutil.QueueUpdateTest(t, test_SlowImageCopy)
280279
}
281280

282281
// Tests SlowImageCopy. We want to verify that it correctly copies *ebiten.Image, *image.NRGBA, and *image.RGBA images.

main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ package frostutil_test
33
import (
44
"testing"
55

6-
"github.com/amanitaverna/frosttest"
6+
"github.com/amanitaverna/frostutil"
77
)
88

99
func TestMain(m *testing.M) {
10-
frosttest.OnTestMain(m)
10+
frostutil.OnTestMain(m)
1111
}

matchesImage.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package frostutil
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"image"
7+
"image/png"
8+
"os"
9+
"strings"
10+
"testing"
11+
12+
"github.com/hajimehoshi/ebiten/v2"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
const (
18+
expectedFolder string = "testdata/expected"
19+
failedFolder string = "testdata/failed"
20+
pngStr string = ".png"
21+
)
22+
23+
// MatchesImage compares an image.Image to "testdata/expected/<imageName>.png". If img is not nil, it attempts to open "testdata/expected/<imageName>.png".
24+
// If it succeeds, it converts it to an image.Image, and then compares the two images.
25+
// If it fails, it writes the image to "testdata/failed/<imageName>.png" and raises a test failure.
26+
// It can handle *ebiten.Images and save them as PNGs.
27+
// Also returns true if the images match, and false if they don't.
28+
func MatchesImage(t *testing.T, imageName string, img image.Image) bool {
29+
if assert.NotNil(t, img) {
30+
filename := expectedFolder + "/" + imageName + pngStr
31+
fr, err := os.Open(filename)
32+
failedBuilder := &strings.Builder{}
33+
if err != nil {
34+
if os.IsNotExist(err) {
35+
failedBuilder.WriteString(filename)
36+
failedBuilder.WriteString(" doesn't exist.")
37+
} else {
38+
failedBuilder.WriteString(fmt.Sprintf("os.Open(%v) failed: %v", filename, err))
39+
}
40+
} else {
41+
require.NotNil(t, fr)
42+
defer fr.Close()
43+
r := bufio.NewReader(fr)
44+
pngImg, err := png.Decode(r)
45+
require.NoError(t, err)
46+
require.NotNil(t, pngImg)
47+
bounds := img.Bounds()
48+
if pngImg.Bounds().Dx() != bounds.Dx() || pngImg.Bounds().Dy() != bounds.Dy() {
49+
failedBuilder.WriteString(fmt.Sprintf("Dimensions of %v (%v, %v) don't match. Expected (%v, %v).\n", imageName, bounds.Dx(), bounds.Dy(), pngImg.Bounds().Dx(), pngImg.Bounds().Dy()))
50+
} else {
51+
for y := 0; y < bounds.Dy() && failedBuilder.Len() < 1000; y++ {
52+
for x := 0; x < bounds.Dx() && failedBuilder.Len() < 1000; x++ {
53+
c1 := img.At(x+bounds.Min.X, y+bounds.Min.Y)
54+
c2 := pngImg.At(x+pngImg.Bounds().Min.X, y+pngImg.Bounds().Min.Y)
55+
r1, g1, b1, a1 := ToNRGBA(c1)
56+
r2, g2, b2, a2 := ToNRGBA(c2)
57+
if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 {
58+
r1a, g1a, b1a, a1a := c1.RGBA()
59+
failedBuilder.WriteString("Pixel (")
60+
failedBuilder.WriteString(fmt.Sprintf("%v, %v", x, y))
61+
failedBuilder.WriteString(") of ")
62+
failedBuilder.WriteString(imageName)
63+
failedBuilder.WriteString(" doesn't match. Got NRGBA #")
64+
failedBuilder.WriteString(fmt.Sprintf("%02x%02x%02x%02x, expected NRGBA #%02x%02x%02x%02x. *ebiten.Image's RGBA color here is %04x%04x%04x%04x\n", r1, g1, b1, a1, r2, g2, b2, a2, r1a, g1a, b1a, a1a))
65+
}
66+
}
67+
}
68+
}
69+
if failedBuilder.Len() > 1000 {
70+
failedBuilder.WriteString("...")
71+
}
72+
}
73+
failed := failedBuilder.String()
74+
if len(failed) > 0 {
75+
failedFilename := failedFolder + "/" + imageName + pngStr
76+
os.MkdirAll(failedFolder, 0644) //read and write permissions for the owner, read-only for group and others
77+
fw, err := os.Create(failedFilename)
78+
require.NoError(t, err)
79+
defer fw.Close()
80+
w := bufio.NewWriter(fw)
81+
eImg, isEImg := img.(*ebiten.Image)
82+
if isEImg {
83+
png.Encode(w, NewImageFromEImage(eImg))
84+
} else {
85+
png.Encode(w, img)
86+
}
87+
w.Flush()
88+
assert.Fail(t, failed)
89+
}
90+
return len(failed) == 0
91+
} else {
92+
return false
93+
}
94+
}

readme.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
This package contains a number of utility functions related to math, strings, functional things (map, compose, partial application), colors, and images (*ebiten.Image, *image.NRGBA, and *image.RGBA):
1+
This package contains a number of utility functions related to math, strings, functional things (map, compose, partial application), colors, and images (*ebiten.Image, *image.NRGBA, and *image.RGBA), as well as a framework to enable running tests under Ebitengine:
22

33
In util.go:
44
- Min and Max functions for all signed or unsigned integers and floats, using generics.
@@ -28,3 +28,40 @@ In image.go:
2828
- CopyImage, which quickly and efficiently copies an image's pixel data to a new image of the same type (*ebiten.Image, *image.NRGBA, or *image.RGBA) and returns the copy. If given any other type of image, it creates a new *image.RGBA and copies the pixel data into it very slowly using At and Set.
2929
- CopyImageLines copies image data line by line. It is slower than copying the entire pixel data buffer at once, but useful if the source and destination images have different strides (because of padding, for instance). As far as I know, this shouldn't come up with images loaded from PNGs, but it might with other image formats.
3030
- SlowImageCopy copies pixel data from iImg to oImg pixel by pixel using (Image).At and (Image).Set. It's called by CopyImage or NewEImageFromImage if iImg isn't an *ebiten.Image, *image.NRGBA, or *image.RGBA. Since image.Image doesn't have a Set method, oImg must still be one of those three for this to work. If it isn't one of those, it returns an error.
31+
32+
In matchesImage.go:
33+
- MatchesImage, which takes a *testing.T, an image name, and an image, and compares the image to the expected output (which should be a .png file in testdata/expected). If it fails to match, or the expected image is missing, it reports a failure to the *testing.T, attempts to write the failed image to testdata/failed (creating the folder if it doesn't exist), and returns false. If it matches, it returns true. It accepts both regular images and *ebiten.Images. If you just didn't have an expected image yet and it is correct, you can move the output image from testdata/failed to testdata/expected and the next run should pass, assuming the output is the same every time.
34+
35+
Finally, test.go contains the code that enables testing things under Ebitengine in the Layout, Update, and Draw methods. To use this, every package that needs to test things under Ebitengine first needs a single file whose name should start with "test" which contains this function:
36+
```go
37+
func TestMain(m *testing.M) {
38+
frostutil.OnTestMain(m)
39+
}
40+
```
41+
I personally keep this in main_test.go. You can see one in this package, since image_test.go includes tests that run under Ebitengine.
42+
43+
OnTestMain ensures that every test run in the package under Ebitengine runs in the main/OS thread. It sets up and runs Ebitengine, runs your test functions (via m.Run) (which should call QueueLayoutTest, QueueUpdateTest, and/or QueueDrawTest if they need to run test code under Layout, Update, or Draw), waits for m.Run and all the tests to finish, and then prompts Update to tell Ebitengine to shut down.
44+
45+
And then for the test files you have where you want to test things under Ebitengine, you can write tests like so (note that you don't have to queue them all from one test function, this is just to show all three Queue functions):
46+
```go
47+
func Test_SomeTests(t *testing.T) {
48+
frostutil.QueueLayoutTest(t, test_SomeLayoutTest)
49+
frostutil.QueueUpdateTest(t, test_SomeUpdateTest)
50+
frostutil.QueueDrawTest(t, test_SomeDrawTest)
51+
}
52+
53+
func test_SomeLayoutTest(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
54+
// Your actual test here. Also return screen dimensions for the Layout function to return:
55+
return outsideWidth, outsideHeight // you can return something else if you like
56+
}
57+
58+
func test_SomeUpdateTest(t *testing.T) {
59+
// Your actual test here
60+
}
61+
62+
func test_SomeDrawTest(t *testing.T, screen *ebiten.Image) {
63+
// Your actual test here
64+
}
65+
```
66+
67+
You can have multiple tests per file, of course, and they can each queue as many things as they want.

test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package frostutil
2+
3+
import (
4+
"errors"
5+
"runtime"
6+
"testing"
7+
"time"
8+
9+
"github.com/hajimehoshi/ebiten/v2"
10+
)
11+
12+
func init() {
13+
runtime.LockOSThread()
14+
}
15+
16+
// Any test function meant to run in Update must have this signature
17+
type UpdateTestFunc func(t *testing.T)
18+
19+
// Any test function meant to run in Draw must have this signature
20+
type DrawTestFunc func(t *testing.T, screen *ebiten.Image)
21+
22+
// Any test function meant to run in Layout must have this signature
23+
type LayoutTestFunc func(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int)
24+
25+
var updateTests chan *UpdateTest = make(chan *UpdateTest, 1)
26+
var drawTests chan *DrawTest = make(chan *DrawTest, 1)
27+
var layoutTests chan *LayoutTest = make(chan *LayoutTest, 1)
28+
var awaitUpdateTestCompletion chan bool = make(chan bool)
29+
var awaitDrawTestCompletion chan bool = make(chan bool)
30+
var awaitLayoutTestCompletion chan bool = make(chan bool)
31+
var hasTestMain bool // set to true by OnTestMain prior to calling m.Run(), if it is false in Queue*Test, then OnTestMain was never called.
32+
var testsQueued int
33+
34+
// TestGame contains the Update, Layout, and Draw methods that Ebitengine calls.
35+
type TestGame struct {
36+
screenWidth, screenHeight int
37+
}
38+
39+
// This has to be called from a TestMain(m *testing.M) function in any package that uses QueueUpdateTest, QueueDrawTest, or QueueLayoutTest.
40+
// It sets up and runs Ebitengine, runs your test functions (via m.Run) which should call Queue*Test, waits for it to finish,
41+
// and then closes the channels and sets their variables to nil, which prompts Update to tell Ebitengine to shut down.
42+
func OnTestMain(m *testing.M) {
43+
runtime.LockOSThread()
44+
f := func() {
45+
hasTestMain = true
46+
m.Run()
47+
close(updateTests)
48+
close(drawTests)
49+
close(layoutTests)
50+
drawTests = nil
51+
layoutTests = nil
52+
updateTests = nil
53+
for testsQueued > 0 {
54+
time.Sleep(100 * time.Millisecond)
55+
}
56+
}
57+
go f()
58+
ebiten.SetWindowSize(1280, 720)
59+
ebiten.SetWindowTitle("Test")
60+
//time.Sleep(time.Second)
61+
testGame := &TestGame{screenWidth: 1920, screenHeight: 1080}
62+
ebiten.RunGame(testGame)
63+
runtime.UnlockOSThread()
64+
}
65+
66+
// UpdateTest pointers are sent through a channel from QueueUpdateTest to *TestGame.Update.
67+
type UpdateTest struct {
68+
t *testing.T
69+
f UpdateTestFunc
70+
}
71+
72+
// DrawTest pointers are sent through a channel from QueueDrawTest to *TestGame.Draw.
73+
type DrawTest struct {
74+
t *testing.T
75+
f DrawTestFunc
76+
}
77+
78+
// LayoutTest pointers are sent through a channel from QueueLayoutTest to *TestGame.Layout.
79+
type LayoutTest struct {
80+
t *testing.T
81+
f LayoutTestFunc
82+
}
83+
84+
// Each time Update is called by Ebitengine, it retrieves an update test, if any are queued, from the updateTests channel, runs it,
85+
// and then lets QueueUpdateTest know that it has finished running it (so that it will return).
86+
// If updateTests is nil, then it returns an error to tell Ebitengine to shut down.
87+
func (game *TestGame) Update() (err error) {
88+
if updateTests != nil {
89+
if len(updateTests) > 0 {
90+
test := <-updateTests
91+
test.f(test.t)
92+
awaitUpdateTestCompletion <- true
93+
}
94+
} else {
95+
err = errors.New("Done")
96+
}
97+
return
98+
}
99+
100+
// Each time Draw is called by Ebitengine, it retrieves a draw test, if any are queued, from the drawTests channel, runs it,
101+
// and then lets QueueDrawTest know that it has finished running it (so that it will return).
102+
// If drawTests is nil, it does nothing.
103+
func (game *TestGame) Draw(screen *ebiten.Image) {
104+
if drawTests != nil {
105+
if len(drawTests) > 0 {
106+
test := <-drawTests
107+
test.f(test.t, screen)
108+
awaitDrawTestCompletion <- true
109+
}
110+
}
111+
}
112+
113+
// Each time Layout is called by Ebitengine, it retrieves a layout test, if any are queued, from the layoutTests channel, runs it,
114+
// records the screenWidth and screenHeight that it returns, and then lets QueueLayoutTest know that it has finished running it (so that it will return).
115+
// If layoutTests is nil, it does nothing.
116+
// It returns the screenWidth and screenHeight returned by the last layout test, or 1920 and 1080 if no layout tests were ever queued.
117+
func (game *TestGame) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
118+
if layoutTests != nil {
119+
if len(layoutTests) > 0 {
120+
test := <-layoutTests
121+
game.screenWidth, game.screenHeight = test.f(test.t, outsideWidth, outsideHeight)
122+
awaitLayoutTestCompletion <- true
123+
}
124+
}
125+
return game.screenWidth, game.screenHeight
126+
}
127+
128+
// QueueUpdateTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f,
129+
// and sends them through the updateTests channel for Update. It waits for Update to let it know that it has finished running f(t), and then returns.
130+
// If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueUpdateTest.
131+
func QueueUpdateTest(t *testing.T, f func(t *testing.T)) {
132+
if hasTestMain {
133+
testsQueued++
134+
updateTests <- &UpdateTest{t, f}
135+
<-awaitUpdateTestCompletion
136+
testsQueued--
137+
} else {
138+
t.Fatal("Missing call to frostutil.OnTestMain. OnTestMain must be called from a TestMain(m *testing.M) function in every package in which you want to use QueueLayoutTest, QueueUpdateTest, and/or QueueDrawTest.")
139+
}
140+
}
141+
142+
// QueueDrawTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f,
143+
// and sends them through the drawTests channel for Draw. It waits for Draw to let it know that it has finished running f(t, screen), and then returns.
144+
// If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueDrawTest.
145+
func QueueDrawTest(t *testing.T, f func(t *testing.T, screen *ebiten.Image)) {
146+
if hasTestMain {
147+
testsQueued++
148+
drawTests <- &DrawTest{t, f}
149+
<-awaitDrawTestCompletion
150+
testsQueued--
151+
} else {
152+
t.Fatal("Missing call to frostutil.OnTestMain. OnTestMain must be called from a TestMain(m *testing.M) function in every package in which you want to use QueueLayoutTest, QueueUpdateTest, and/or QueueDrawTest.")
153+
}
154+
}
155+
156+
// QueueLayoutTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f,
157+
// and sends them through the layoutTests channel for Layout. It waits for Layout to let it know that it has finished running f(t, outsideWidth, outsideHeight),
158+
// and then returns.
159+
// If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueLayoutTest.
160+
func QueueLayoutTest(t *testing.T, f func(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int)) {
161+
if hasTestMain {
162+
testsQueued++
163+
layoutTests <- &LayoutTest{t, f}
164+
<-awaitLayoutTestCompletion
165+
testsQueued--
166+
} else {
167+
t.Fatal("Missing call to frostutil.OnTestMain. OnTestMain must be called from a TestMain(m *testing.M) function in every package in which you want to use QueueLayoutTest, QueueUpdateTest, and/or QueueDrawTest.")
168+
}
169+
}

0 commit comments

Comments
 (0)