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.

Reply via email to