|
| 1 | +/** |
| 2 | + * GdkPixbuf WASM - GObject Integration Tests |
| 3 | + * Copyright © 2025 Superstruct Ltd |
| 4 | + * SPDX-License-Identifier: LGPL-2.1-or-later |
| 5 | + * |
| 6 | + * Validates GdkPixbuf integration with glib.wasm's unified GObject system. |
| 7 | + */ |
| 8 | + |
| 9 | +import { assertEquals, assertExists } from "@std/assert"; |
| 10 | + |
| 11 | +interface WasmModule { |
| 12 | + ccall: (name: string, returnType: string, argTypes: string[], args: unknown[]) => unknown; |
| 13 | + cwrap: (name: string, returnType: string, argTypes: string[]) => (...args: unknown[]) => unknown; |
| 14 | + _malloc: (size: number) => number; |
| 15 | + _free: (ptr: number) => void; |
| 16 | + HEAPU8: Uint8Array; |
| 17 | + UTF8ToString: (ptr: number) => string; |
| 18 | +} |
| 19 | + |
| 20 | +async function loadGdkPixbufWasm(): Promise<WasmModule> { |
| 21 | + const wasmPath = "./install/wasm/gdk-pixbuf-main.js"; |
| 22 | + |
| 23 | + try { |
| 24 | + const module = await import(wasmPath); |
| 25 | + const createModule = module.default; |
| 26 | + const instance = await createModule(); |
| 27 | + return instance as WasmModule; |
| 28 | + } catch (error) { |
| 29 | + throw new Error(`Failed to load GdkPixbuf WASM: ${error}`); |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +Deno.test("GdkPixbuf - Module initialization", async () => { |
| 34 | + const pixbuf = await loadGdkPixbufWasm(); |
| 35 | + |
| 36 | + const getVersion = pixbuf.cwrap("gdk_pixbuf_wasm_get_version", "string", []); |
| 37 | + const version = getVersion() as string; |
| 38 | + |
| 39 | + assertExists(version, "Version should be defined"); |
| 40 | + assertEquals(version.startsWith("2."), true, "Should be version 2.x"); |
| 41 | +}); |
| 42 | + |
| 43 | +Deno.test("GdkPixbuf - SIDE_MODULE detection", async () => { |
| 44 | + const pixbuf = await loadGdkPixbufWasm(); |
| 45 | + |
| 46 | + const isSideModule = pixbuf.cwrap("gdk_pixbuf_wasm_is_side_module", "number", []); |
| 47 | + const result = isSideModule() as number; |
| 48 | + |
| 49 | + // When built as MAIN for testing, should return 0 |
| 50 | + // When built as SIDE for production, should return 1 |
| 51 | + assertEquals(typeof result, "number", "Should return a number"); |
| 52 | +}); |
| 53 | + |
| 54 | +Deno.test("GdkPixbuf - Type identity with GObject", async () => { |
| 55 | + const pixbuf = await loadGdkPixbufWasm(); |
| 56 | + |
| 57 | + const typeFromName = pixbuf.cwrap("g_type_from_name", "number", ["string"]); |
| 58 | + |
| 59 | + const gobjectType = typeFromName("GObject") as number; |
| 60 | + const pixbufType = typeFromName("GdkPixbuf") as number; |
| 61 | + |
| 62 | + assertExists(gobjectType, "GObject type should exist"); |
| 63 | + assertExists(pixbufType, "GdkPixbuf type should exist"); |
| 64 | + assertEquals(gobjectType > 0, true, "GObject type should be valid"); |
| 65 | + assertEquals(pixbufType > 0, true, "GdkPixbuf type should be valid"); |
| 66 | + |
| 67 | + // Test type hierarchy |
| 68 | + const isA = pixbuf.cwrap("g_type_is_a", "number", ["number", "number"]); |
| 69 | + const result = isA(pixbufType, gobjectType) as number; |
| 70 | + |
| 71 | + assertEquals(result !== 0, true, "GdkPixbuf should be a GObject"); |
| 72 | +}); |
| 73 | + |
| 74 | +Deno.test("GdkPixbuf - Type identity test function", async () => { |
| 75 | + const pixbuf = await loadGdkPixbufWasm(); |
| 76 | + |
| 77 | + const testTypeIdentity = pixbuf.cwrap("gdk_pixbuf_wasm_test_type_identity", "null", []); |
| 78 | + |
| 79 | + // Should not throw |
| 80 | + testTypeIdentity(); |
| 81 | +}); |
| 82 | + |
| 83 | +Deno.test("GdkPixbuf - Cross-module allocation test", async () => { |
| 84 | + const pixbuf = await loadGdkPixbufWasm(); |
| 85 | + |
| 86 | + const testCrossModuleAlloc = pixbuf.cwrap("gdk_pixbuf_wasm_test_cross_module_alloc", "null", []); |
| 87 | + |
| 88 | + // Should not throw, no leaks |
| 89 | + testCrossModuleAlloc(); |
| 90 | +}); |
| 91 | + |
| 92 | +Deno.test("GdkPixbuf - Create and destroy pixbuf", async () => { |
| 93 | + const pixbuf = await loadGdkPixbufWasm(); |
| 94 | + |
| 95 | + const newPixbuf = pixbuf.cwrap("gdk_pixbuf_new", "number", |
| 96 | + ["number", "number", "number", "number", "number"]); |
| 97 | + const objectUnref = pixbuf.cwrap("g_object_unref", "null", ["number"]); |
| 98 | + |
| 99 | + // GDK_COLORSPACE_RGB = 0, has_alpha = 0, bits_per_sample = 8 |
| 100 | + const ptr = newPixbuf(0, 0, 8, 128, 128) as number; |
| 101 | + |
| 102 | + assertExists(ptr, "Pixbuf should be created"); |
| 103 | + assertEquals(ptr > 0, true, "Pointer should be valid"); |
| 104 | + |
| 105 | + // Cleanup |
| 106 | + objectUnref(ptr); |
| 107 | +}); |
| 108 | + |
| 109 | +Deno.test("GdkPixbuf - Pixbuf properties", async () => { |
| 110 | + const pixbuf = await loadGdkPixbufWasm(); |
| 111 | + |
| 112 | + const newPixbuf = pixbuf.cwrap("gdk_pixbuf_new", "number", |
| 113 | + ["number", "number", "number", "number", "number"]); |
| 114 | + const getWidth = pixbuf.cwrap("gdk_pixbuf_get_width", "number", ["number"]); |
| 115 | + const getHeight = pixbuf.cwrap("gdk_pixbuf_get_height", "number", ["number"]); |
| 116 | + const getNChannels = pixbuf.cwrap("gdk_pixbuf_get_n_channels", "number", ["number"]); |
| 117 | + const objectUnref = pixbuf.cwrap("g_object_unref", "null", ["number"]); |
| 118 | + |
| 119 | + const ptr = newPixbuf(0, 0, 8, 256, 128) as number; |
| 120 | + |
| 121 | + assertEquals(getWidth(ptr), 256, "Width should be 256"); |
| 122 | + assertEquals(getHeight(ptr), 128, "Height should be 128"); |
| 123 | + assertEquals(getNChannels(ptr), 3, "Should have 3 channels (RGB)"); |
| 124 | + |
| 125 | + objectUnref(ptr); |
| 126 | +}); |
| 127 | + |
| 128 | +Deno.test("GdkPixbuf - Reference counting", async () => { |
| 129 | + const pixbuf = await loadGdkPixbufWasm(); |
| 130 | + |
| 131 | + const newPixbuf = pixbuf.cwrap("gdk_pixbuf_new", "number", |
| 132 | + ["number", "number", "number", "number", "number"]); |
| 133 | + const objectRef = pixbuf.cwrap("g_object_ref", "number", ["number"]); |
| 134 | + const objectUnref = pixbuf.cwrap("g_object_unref", "null", ["number"]); |
| 135 | + |
| 136 | + const ptr = newPixbuf(0, 0, 8, 64, 64) as number; |
| 137 | + |
| 138 | + // Initial refcount should be 1 (floating reference is sunk for GdkPixbuf) |
| 139 | + |
| 140 | + // Ref (increases to 2) |
| 141 | + objectRef(ptr); |
| 142 | + |
| 143 | + // Unref (back to 1) |
| 144 | + objectUnref(ptr); |
| 145 | + |
| 146 | + // Final unref (should deallocate) |
| 147 | + objectUnref(ptr); |
| 148 | + |
| 149 | + // If we got here without crash, test passed |
| 150 | +}); |
| 151 | + |
| 152 | +Deno.test("GdkPixbuf - Multiple pixbufs", async () => { |
| 153 | + const pixbuf = await loadGdkPixbufWasm(); |
| 154 | + |
| 155 | + const newPixbuf = pixbuf.cwrap("gdk_pixbuf_new", "number", |
| 156 | + ["number", "number", "number", "number", "number"]); |
| 157 | + const objectUnref = pixbuf.cwrap("g_object_unref", "null", ["number"]); |
| 158 | + |
| 159 | + const pixbufs: number[] = []; |
| 160 | + |
| 161 | + // Create 10 pixbufs |
| 162 | + for (let i = 0; i < 10; i++) { |
| 163 | + const ptr = newPixbuf(0, 0, 8, 32 + i * 8, 32 + i * 8) as number; |
| 164 | + assertExists(ptr, `Pixbuf ${i} should be created`); |
| 165 | + pixbufs.push(ptr); |
| 166 | + } |
| 167 | + |
| 168 | + // All should have different pointers |
| 169 | + const uniquePointers = new Set(pixbufs); |
| 170 | + assertEquals(uniquePointers.size, 10, "All pointers should be unique"); |
| 171 | + |
| 172 | + // Cleanup |
| 173 | + for (const ptr of pixbufs) { |
| 174 | + objectUnref(ptr); |
| 175 | + } |
| 176 | +}); |
| 177 | + |
| 178 | +Deno.test("GdkPixbuf - Performance baseline", async () => { |
| 179 | + const pixbuf = await loadGdkPixbufWasm(); |
| 180 | + |
| 181 | + const newPixbuf = pixbuf.cwrap("gdk_pixbuf_new", "number", |
| 182 | + ["number", "number", "number", "number", "number"]); |
| 183 | + const objectUnref = pixbuf.cwrap("g_object_unref", "null", ["number"]); |
| 184 | + |
| 185 | + const iterations = 1000; |
| 186 | + const start = performance.now(); |
| 187 | + |
| 188 | + for (let i = 0; i < iterations; i++) { |
| 189 | + const ptr = newPixbuf(0, 0, 8, 64, 64) as number; |
| 190 | + objectUnref(ptr); |
| 191 | + } |
| 192 | + |
| 193 | + const elapsed = performance.now() - start; |
| 194 | + const opsPerSec = (iterations / elapsed) * 1000; |
| 195 | + |
| 196 | + console.log(`\nPerformance: ${iterations} pixbuf create/destroy cycles`); |
| 197 | + console.log(` Time: ${elapsed.toFixed(2)}ms`); |
| 198 | + console.log(` Rate: ${opsPerSec.toFixed(0)} ops/sec`); |
| 199 | + |
| 200 | + // Sanity check - should be reasonably fast |
| 201 | + assertEquals(elapsed < 5000, true, "Should complete in under 5 seconds"); |
| 202 | +}); |
0 commit comments