Skip to content

Commit 790c02f

Browse files
committed
test: drive the real nativeImage FFI with a generated PNG fixture asserting exact scalar getSize on macOS and Linux
1 parent 14fef2a commit 790c02f

3 files changed

Lines changed: 247 additions & 0 deletions

File tree

tests/fixtures/tiny-png.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { rmSync, writeFileSync } from 'node:fs';
2+
import { tmpdir } from 'node:os';
3+
import { join } from 'node:path';
4+
import { deflateSync } from 'node:zlib';
5+
6+
/**
7+
* A deterministic tiny-PNG fixture for the `nativeImage` integration tests.
8+
*
9+
* The bytes are GENERATED in code (a minimal 8-bit RGBA PNG encoder) rather than
10+
* embedded, so the asserted width/height are guaranteed to match the file the
11+
* native decoder reads — proving the scalar `getSize` path returns the EXACT
12+
* known dimensions, not a coincidence of some opaque blob.
13+
*/
14+
15+
/** The fixture's known dimensions, asserted exactly by the integration tests. */
16+
export const TINY_PNG_WIDTH = 3;
17+
export const TINY_PNG_HEIGHT = 2;
18+
19+
const CRC_TABLE = (() => {
20+
const table = new Uint32Array(256);
21+
for (let n = 0; n < 256; n += 1) {
22+
let c = n;
23+
for (let k = 0; k < 8; k += 1) {
24+
c = (c & 1) === 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
25+
}
26+
table[n] = c >>> 0;
27+
}
28+
return table;
29+
})();
30+
31+
const crc32 = (buffer: Uint8Array): number => {
32+
let crc = 0xffffffff;
33+
for (let i = 0; i < buffer.length; i += 1) {
34+
const byte = buffer[i] ?? 0;
35+
crc = (CRC_TABLE[(crc ^ byte) & 0xff] ?? 0) ^ (crc >>> 8);
36+
}
37+
return (crc ^ 0xffffffff) >>> 0;
38+
};
39+
40+
const chunk = (type: string, data: Uint8Array): Buffer => {
41+
const length = Buffer.alloc(4);
42+
length.writeUInt32BE(data.length, 0);
43+
const body = Buffer.concat([Buffer.from(type, 'ascii'), Buffer.from(data)]);
44+
const crc = Buffer.alloc(4);
45+
crc.writeUInt32BE(crc32(body), 0);
46+
return Buffer.concat([length, body, crc]);
47+
};
48+
49+
/** Build the fixture's PNG bytes (a {@link TINY_PNG_WIDTH}×{@link TINY_PNG_HEIGHT} opaque-red image). */
50+
export const makeTinyPng = (): Uint8Array => {
51+
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
52+
const ihdr = Buffer.alloc(13);
53+
ihdr.writeUInt32BE(TINY_PNG_WIDTH, 0);
54+
ihdr.writeUInt32BE(TINY_PNG_HEIGHT, 4);
55+
ihdr[8] = 8; // bit depth
56+
ihdr[9] = 6; // color type: RGBA
57+
const stride = 1 + TINY_PNG_WIDTH * 4;
58+
const raw = Buffer.alloc(TINY_PNG_HEIGHT * stride);
59+
for (let y = 0; y < TINY_PNG_HEIGHT; y += 1) {
60+
const row = y * stride;
61+
raw[row] = 0; // filter: none
62+
for (let x = 0; x < TINY_PNG_WIDTH; x += 1) {
63+
const p = row + 1 + x * 4;
64+
raw[p] = 255; // R
65+
raw[p + 3] = 255; // A
66+
}
67+
}
68+
return new Uint8Array(
69+
Buffer.concat([
70+
signature,
71+
chunk('IHDR', ihdr),
72+
chunk('IDAT', deflateSync(raw)),
73+
chunk('IEND', Buffer.alloc(0)),
74+
]),
75+
);
76+
};
77+
78+
/** Write the fixture PNG to a unique temp file and return its absolute path. */
79+
export const writeTinyPngFile = (): string => {
80+
const path = join(tmpdir(), `sambar-native-image-${process.pid}-${Date.now()}.png`);
81+
writeFileSync(path, makeTinyPng());
82+
return path;
83+
};
84+
85+
/** Delete a fixture file written by {@link writeTinyPngFile} (best-effort). */
86+
export const removeTinyPngFile = (path: string): void => {
87+
rmSync(path, { force: true });
88+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, test } from 'bun:test';
2+
import { currentPlatform } from '../../../src/common/platform';
3+
import { nativeImage, setNativeImageBackendForTesting } from '../../../src/main/api/native-image';
4+
import { loadGdkPixbufFFI } from '../../../src/main/platform/linux/gdk-pixbuf-ffi';
5+
import {
6+
removeTinyPngFile,
7+
TINY_PNG_HEIGHT,
8+
TINY_PNG_WIDTH,
9+
makeTinyPng,
10+
writeTinyPngFile,
11+
} from '../../fixtures/tiny-png';
12+
13+
/**
14+
* Real `nativeImage` on a Linux host, driving the live GdkPixbuf FFI. Mirrors
15+
* the macOS integration suite: `getSize()` must return the EXACT known fixture
16+
* dimensions via the scalar `gdk_pixbuf_get_width`/`get_height` getters, and
17+
* `toPNG()` must carry the PNG signature.
18+
*
19+
* GdkPixbuf does NOT require `gtk_init_check`, but the loader must resolve the
20+
* library first; if `libgdk_pixbuf-2.0.so.0` is absent the load throws and the
21+
* test fails loudly (rather than silently passing), which is the honest signal
22+
* that the CI runner needs the package installed.
23+
*/
24+
if (currentPlatform() === 'linux') {
25+
describe('gdk-native-image on Linux', () => {
26+
test('gdk-pixbuf-ffi resolves the load/query/encode symbols', () => {
27+
const pixbuf = loadGdkPixbufFFI();
28+
for (const name of [
29+
'gdk_pixbuf_new_from_file',
30+
'gdk_pixbuf_new_from_stream',
31+
'gdk_pixbuf_get_width',
32+
'gdk_pixbuf_get_height',
33+
'gdk_pixbuf_save_to_bufferv',
34+
] as const) {
35+
expect(typeof pixbuf.symbols[name]).toBe('function');
36+
}
37+
});
38+
39+
test('createFromPath getSize returns the EXACT known fixture dimensions via the scalar path', () => {
40+
setNativeImageBackendForTesting(undefined);
41+
const fixture = writeTinyPngFile();
42+
try {
43+
const image = nativeImage.createFromPath(fixture);
44+
expect(image.isEmpty()).toBe(false);
45+
const size = image.getSize();
46+
expect(size.width).toBe(TINY_PNG_WIDTH);
47+
expect(size.height).toBe(TINY_PNG_HEIGHT);
48+
} finally {
49+
removeTinyPngFile(fixture);
50+
}
51+
});
52+
53+
test('createFromBuffer decodes PNG bytes to the same exact dimensions', () => {
54+
setNativeImageBackendForTesting(undefined);
55+
const image = nativeImage.createFromBuffer(makeTinyPng());
56+
expect(image.isEmpty()).toBe(false);
57+
expect(image.getSize().width).toBe(TINY_PNG_WIDTH);
58+
expect(image.getSize().height).toBe(TINY_PNG_HEIGHT);
59+
});
60+
61+
test('toPNG returns non-empty bytes starting with the PNG signature', () => {
62+
setNativeImageBackendForTesting(undefined);
63+
const fixture = writeTinyPngFile();
64+
try {
65+
const png = nativeImage.createFromPath(fixture).toPNG();
66+
expect(png.length > 0).toBe(true);
67+
expect(png[0]).toBe(0x89);
68+
expect(png[1]).toBe(0x50);
69+
expect(png[2]).toBe(0x4e);
70+
expect(png[3]).toBe(0x47);
71+
} finally {
72+
removeTinyPngFile(fixture);
73+
}
74+
});
75+
76+
test('a bad path decodes to an empty image (no crash)', () => {
77+
setNativeImageBackendForTesting(undefined);
78+
const image = nativeImage.createFromPath('/no/such/sambar/image.png');
79+
expect(image.isEmpty()).toBe(true);
80+
expect(image.getSize()).toEqual({ width: 0, height: 0 });
81+
expect(image.toPNG().length).toBe(0);
82+
});
83+
});
84+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2+
import { currentPlatform } from '../../../src/common/platform';
3+
import { nativeImage, setNativeImageBackendForTesting } from '../../../src/main/api/native-image';
4+
import {
5+
removeTinyPngFile,
6+
TINY_PNG_HEIGHT,
7+
TINY_PNG_WIDTH,
8+
makeTinyPng,
9+
writeTinyPngFile,
10+
} from '../../fixtures/tiny-png';
11+
12+
/**
13+
* Real `nativeImage` on a macOS host, driving the live `NSBitmapImageRep`/`NSData`
14+
* FFI. The headline assertion is that `getSize()` returns the EXACT known
15+
* dimensions of the generated fixture via the SCALAR `pixelsWide`/`pixelsHigh`
16+
* path — proving the struct-free workaround for the un-returnable `NSImage.size`
17+
* `NSSize` struct.
18+
*
19+
* No NSApplication / window server is needed: `NSBitmapImageRep` decoding is
20+
* pure Foundation/AppKit image work, not UI.
21+
*/
22+
if (currentPlatform() === 'macos') {
23+
describe('cocoa-native-image', () => {
24+
let fixture: string;
25+
26+
beforeAll(() => {
27+
// Use the real macOS backend (clear any fake a unit test may have left set).
28+
setNativeImageBackendForTesting(undefined);
29+
fixture = writeTinyPngFile();
30+
});
31+
32+
afterAll(() => {
33+
removeTinyPngFile(fixture);
34+
});
35+
36+
test('createFromPath getSize returns the EXACT known fixture dimensions via the scalar path', () => {
37+
const image = nativeImage.createFromPath(fixture);
38+
expect(image.isEmpty()).toBe(false);
39+
const size = image.getSize();
40+
expect(size.width).toBe(TINY_PNG_WIDTH);
41+
expect(size.height).toBe(TINY_PNG_HEIGHT);
42+
});
43+
44+
test('createFromBuffer decodes PNG bytes to the same exact dimensions', () => {
45+
const image = nativeImage.createFromBuffer(makeTinyPng());
46+
expect(image.isEmpty()).toBe(false);
47+
expect(image.getSize().width).toBe(TINY_PNG_WIDTH);
48+
expect(image.getSize().height).toBe(TINY_PNG_HEIGHT);
49+
});
50+
51+
test('toPNG returns non-empty bytes starting with the PNG signature', () => {
52+
const png = nativeImage.createFromPath(fixture).toPNG();
53+
expect(png.length > 0).toBe(true);
54+
// 0x89 'P' 'N' 'G'
55+
expect(png[0]).toBe(0x89);
56+
expect(png[1]).toBe(0x50);
57+
expect(png[2]).toBe(0x4e);
58+
expect(png[3]).toBe(0x47);
59+
});
60+
61+
test('toDataURL starts with the PNG data-URL prefix and carries a payload', () => {
62+
const url = nativeImage.createFromPath(fixture).toDataURL();
63+
expect(url.startsWith('data:image/png;base64,')).toBe(true);
64+
expect(url.length > 'data:image/png;base64,'.length).toBe(true);
65+
});
66+
67+
test('a bad path decodes to an empty image (no crash)', () => {
68+
const image = nativeImage.createFromPath('/no/such/sambar/image.png');
69+
expect(image.isEmpty()).toBe(true);
70+
expect(image.getSize()).toEqual({ width: 0, height: 0 });
71+
expect(image.toPNG().length).toBe(0);
72+
expect(image.toDataURL()).toBe('data:image/png;base64,');
73+
});
74+
});
75+
}

0 commit comments

Comments
 (0)