This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch main
in repository ego.
View the commit online.
commit c8227ca02605743db9be36fb9438e03c43bd9d71
Author: [email protected] <[email protected]>
AuthorDate: Mon Mar 9 10:24:52 2026 -0600
feat: add buffer-backed headless EFL rendering test harness
Introduce a public test infrastructure package for headless rendering using
EFL's buffer backend. This enables deterministic, display-independent pixel
rendering in CI environments.
The harness provides:
- Run() for TestMain integration with automatic EFL initialization and
shutdown
- BufferWindow for creating off-screen ARGB32 canvases
- Render() with automatic alpha un-premultiplication from EFL's native
format to standard image.NRGBA
- Thread-safe operation via EFL's synchronization primitives
This harness is designed for use both by ego's internal tests and by
external developers who need deterministic rendering without display
dependencies.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
tests/harness/harness.go | 133 ++++++++++++++++++++++++++++++++++++++++++
tests/harness/harness_test.go | 67 +++++++++++++++++++++
2 files changed, 200 insertions(+)
diff --git a/tests/harness/harness.go b/tests/harness/harness.go
new file mode 100644
index 0000000..712435e
--- /dev/null
+++ b/tests/harness/harness.go
@@ -0,0 +1,133 @@
+// Package harness provides public test infrastructure that wraps EFL's buffer
+// backend for headless rendering. It is designed to be used both by ego's own
+// tests and by external application developers who need deterministic,
+// display-independent rendering in CI environments.
+package harness
+
+/*
+#cgo pkg-config: ecore-evas evas elementary
+
+#include <Ecore_Evas.h>
+#include <Evas.h>
+*/
+import "C"
+
+import (
+ "image"
+ "testing"
+ "unsafe"
+
+ "git.enlightenment.org/cedric/ego/efl"
+)
+
+// Run initializes headless EFL, calls the optional setup function, runs the
+// test suite, then shuts EFL down and waits for full teardown before returning
+// the exit code. It is intended for use in TestMain:
+//
+// func TestMain(m *testing.M) { os.Exit(harness.Run(m, nil)) }
+//
+// If setup is non-nil it is invoked on the EFL thread via efl.Sync after Init
+// returns but before m.Run is called.
+func Run(m *testing.M, setup func()) int {
+ efl.Init()
+
+ if setup != nil {
+ efl.Sync(setup)
+ }
+
+ code := m.Run()
+
+ efl.Shutdown()
+ efl.Wait()
+
+ return code
+}
+
+// BufferWindow is a headless buffer-backed Ecore_Evas window. All methods that
+// interact with EFL (NewBufferWindow, Render, Free) must be called on the EFL
+// thread, typically via efl.Sync.
+type BufferWindow struct {
+ ee *C.Ecore_Evas
+ w int
+ h int
+}
+
+// NewBufferWindow creates a buffer-backed Ecore_Evas of the given pixel
+// dimensions. It must be called on the EFL thread (inside efl.Sync or
+// efl.Post). Returns nil if EFL fails to allocate the buffer.
+func NewBufferWindow(w, h int) *BufferWindow {
+ ee := C.ecore_evas_buffer_new(C.int(w), C.int(h))
+ if ee == nil {
+ return nil
+ }
+ C.ecore_evas_show(ee)
+ return &BufferWindow{ee: ee, w: w, h: h}
+}
+
+// Evas returns the Evas canvas pointer associated with the buffer window. The
+// returned value is only valid while the BufferWindow has not been freed.
+func (bw *BufferWindow) Evas() unsafe.Pointer {
+ return unsafe.Pointer(C.ecore_evas_get(bw.ee))
+}
+
+// EcoreEvas returns the raw Ecore_Evas pointer. The returned value is only
+// valid while the BufferWindow has not been freed.
+func (bw *BufferWindow) EcoreEvas() unsafe.Pointer {
+ return unsafe.Pointer(bw.ee)
+}
+
+// Render forces a synchronous render pass and returns the resulting pixel data
+// as an image.NRGBA. The source pixels are ARGB32 with premultiplied alpha;
+// Render un-premultiplies them before returning.
+//
+// Must be called on the EFL thread.
+func (bw *BufferWindow) Render() image.Image {
+ C.ecore_evas_manual_render(bw.ee)
+
+ pixels := C.ecore_evas_buffer_pixels_get(bw.ee)
+
+ img := image.NewNRGBA(image.Rect(0, 0, bw.w, bw.h))
+
+ if pixels == nil {
+ return img
+ }
+
+ // Reinterpret the pixel buffer as a slice of uint32 values. Each uint32
+ // holds one ARGB32 pixel with premultiplied alpha in host byte order.
+ n := bw.w * bw.h
+ src := unsafe.Slice((*uint32)(pixels), n)
+
+ for i, px := range src {
+ // EFL stores ARGB as: bits 31-24 = A, 23-16 = R, 15-8 = G, 7-0 = B.
+ a := uint8(px >> 24)
+ r := uint8(px >> 16)
+ g := uint8(px >> 8)
+ b := uint8(px)
+
+ // Un-premultiply alpha. When a == 0 the pixel is fully transparent and
+ // the colour components are irrelevant; when a == 255 no scaling is
+ // needed. For intermediate values we scale each channel by 255/a using
+ // integer arithmetic with rounding.
+ if a > 0 && a < 255 {
+ fa := uint32(a)
+ r = uint8((uint32(r)*255 + fa/2) / fa)
+ g = uint8((uint32(g)*255 + fa/2) / fa)
+ b = uint8((uint32(b)*255 + fa/2) / fa)
+ }
+
+ off := i * 4
+ img.Pix[off+0] = r
+ img.Pix[off+1] = g
+ img.Pix[off+2] = b
+ img.Pix[off+3] = a
+ }
+
+ return img
+}
+
+// Free releases the Ecore_Evas and all associated resources. It must be called
+// on the EFL thread. After Free, the BufferWindow must not be used.
+func (bw *BufferWindow) Free() {
+ C.ecore_evas_free(bw.ee)
+ bw.ee = nil
+}
diff --git a/tests/harness/harness_test.go b/tests/harness/harness_test.go
new file mode 100644
index 0000000..069c960
--- /dev/null
+++ b/tests/harness/harness_test.go
@@ -0,0 +1,67 @@
+package harness_test
+
+import (
+ "image"
+ "os"
+ "testing"
+
+ "git.enlightenment.org/cedric/ego/efl"
+ "git.enlightenment.org/cedric/ego/tests/harness"
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(harness.Run(m, nil))
+}
+
+// TestBufferWindowCreate verifies that a buffer window can be created on the
+// EFL thread and that it returns valid non-nil Evas and EcoreEvas pointers.
+func TestBufferWindowCreate(t *testing.T) {
+ var bw *harness.BufferWindow
+
+ efl.Sync(func() {
+ bw = harness.NewBufferWindow(64, 64)
+ })
+
+ if bw == nil {
+ t.Fatal("NewBufferWindow returned nil")
+ }
+
+ if bw.Evas() == nil {
+ t.Error("Evas() returned nil pointer")
+ }
+
+ if bw.EcoreEvas() == nil {
+ t.Error("EcoreEvas() returned nil pointer")
+ }
+
+ efl.Sync(func() {
+ bw.Free()
+ })
+}
+
+// TestBufferWindowRender verifies that Render returns an image whose bounds
+// match the requested window dimensions.
+func TestBufferWindowRender(t *testing.T) {
+ const w, h = 100, 100
+
+ var img image.Image
+
+ efl.Sync(func() {
+ bw := harness.NewBufferWindow(w, h)
+ if bw == nil {
+ // Surface creation failure is reported after Sync returns.
+ return
+ }
+ img = bw.Render()
+ bw.Free()
+ })
+
+ if img == nil {
+ t.Fatal("NewBufferWindow returned nil inside Sync")
+ }
+
+ bounds := img.Bounds()
+ if bounds.Dx() != w || bounds.Dy() != h {
+ t.Errorf("expected image size %dx%d, got %dx%d", w, h, bounds.Dx(), bounds.Dy())
+ }
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.