diff options
Diffstat (limited to 'pixpat-python/tests')
| -rw-r--r-- | pixpat-python/tests/test_basic.py | 539 | ||||
| -rw-r--r-- | pixpat-python/tests/test_numpy.py | 110 | ||||
| -rw-r--r-- | pixpat-python/tests/test_threading.py | 187 |
3 files changed, 0 insertions, 836 deletions
diff --git a/pixpat-python/tests/test_basic.py b/pixpat-python/tests/test_basic.py deleted file mode 100644 index 2ed8bb7..0000000 --- a/pixpat-python/tests/test_basic.py +++ /dev/null @@ -1,539 +0,0 @@ -"""Smoke tests for the pixpat Python bindings.""" - -import pytest - -import pixpat - - -def test_supported_formats_nonempty(): - formats = pixpat.supported_formats() - assert isinstance(formats, list) - assert len(formats) > 0 - assert all(isinstance(f, str) for f in formats) - assert 'XRGB8888' in formats - assert 'NV12' in formats - - -def test_is_supported(): - assert pixpat.is_supported('XRGB8888') - assert pixpat.is_supported('NV12') - assert not pixpat.is_supported('NOT_A_REAL_FORMAT') - - -def test_draw_pattern_xrgb8888(): - w, h = 64, 32 - stride = w * 4 - data = bytearray(stride * h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'smpte') - assert any(b != 0 for b in data) - - -def test_draw_pattern_unknown_pattern_raises(): - data = bytearray(64 * 32 * 4) - buf = pixpat.Buffer([data], 'XRGB8888', 64, 32, [64 * 4]) - with pytest.raises(ValueError): - pixpat.draw_pattern(buf, 'bogus') - - -def test_draw_pattern_unknown_format_raises(): - data = bytearray(64 * 32 * 4) - buf = pixpat.Buffer([data], 'NOT_A_REAL_FORMAT', 64, 32, [64 * 4]) - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf) - - -def test_draw_pattern_readonly_buffer_raises(): - data = bytes(64 * 32 * 4) - buf = pixpat.Buffer([data], 'XRGB8888', 64, 32, [64 * 4]) - with pytest.raises((TypeError, BufferError)): - pixpat.draw_pattern(buf) - - -def _alloc_xrgb(w, h): - stride = w * 4 - return bytearray(stride * h), stride - - -def _first_pixel_bgr(data): - # BGR888-style byte order: pixpat XRGB8888 stores B at byte 0, - # G at byte 1, R at byte 2, X at byte 3. See - # README "Format names and byte order". - return data[0], data[1], data[2] - - -def test_draw_pattern_plain_red_str_params(): - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'plain', params='color=ff0000') - b, g, r = _first_pixel_bgr(data) - assert (b, g, r) == (0, 0, 0xFF) - - -def test_draw_pattern_plain_red_dict_params(): - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'plain', params={'color': 'ff0000'}) - b, g, r = _first_pixel_bgr(data) - assert (b, g, r) == (0, 0, 0xFF) - - -def test_draw_pattern_plain_alpha_passes_through_to_argb(): - w, h = 16, 8 - stride = w * 4 - data = bytearray(stride * h) - buf = pixpat.Buffer([data], 'ARGB8888', w, h, [stride]) - # 8-char form is alpha-first: AARRGGBB. - pixpat.draw_pattern(buf, 'plain', params={'color': '80112233'}) - # ARGB8888 byte order (LSB-first): B, G, R, A - assert (data[0], data[1], data[2], data[3]) == (0x33, 0x22, 0x11, 0x80) - - -def test_draw_pattern_plain_accepts_0x_prefix(): - w, h = 16, 8 - stride = w * 4 - data = bytearray(stride * h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'plain', params={'color': '0xff0000'}) - assert (data[0], data[1], data[2]) == (0, 0, 0xFF) - - -def test_draw_pattern_plain_16bpc_rgb(): - """16-bpc form: 12 hex chars, RRRRGGGGBBBB. ABGR16161616 names - A-B-G-R MSB-first in a 64-bit word; stored little-endian, the - bytes are R lo, R hi, G lo, G hi, B lo, B hi, A lo, A hi.""" - w, h = 4, 2 - stride = w * 8 - data = bytearray(stride * h) - buf = pixpat.Buffer([data], 'ABGR16161616', w, h, [stride]) - pixpat.draw_pattern(buf, 'plain', params={'color': '0xfedc00000000'}) - # R=0xFEDC, G=0x0000, B=0x0000, A=0xFFFF (default opaque). - pix = bytes(data[:8]) - assert pix == b'\xdc\xfe\x00\x00\x00\x00\xff\xff' - - -def test_draw_pattern_plain_16bpc_argb(): - w, h = 4, 2 - stride = w * 8 - data = bytearray(stride * h) - buf = pixpat.Buffer([data], 'ABGR16161616', w, h, [stride]) - # 16-char form is alpha-first: AAAARRRRGGGGBBBB. - pixpat.draw_pattern(buf, 'plain', params={'color': '0x80007f000000ffff'}) - pix = bytes(data[:8]) - # R=0x7F00, G=0x0000, B=0xFFFF, A=0x8000. - assert pix == b'\x00\x7f\x00\x00\xff\xff\x00\x80' - - -def test_draw_pattern_plain_missing_color_raises(): - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf, 'plain') - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf, 'plain', params='') - - -def test_draw_pattern_plain_malformed_color_raises(): - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - for bad in ('color=zzzzzz', 'color=abc', 'color=', 'color=1234567'): - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf, 'plain', params=bad) - - -def test_draw_pattern_malformed_params_string_raises(): - """Top-level params parse failure (not pattern-specific).""" - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - for bad in ('foo,bar', '=ff0000', ',color=ff0000'): - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf, 'plain', params=bad) - - -def test_draw_pattern_dict_params_with_separators_rejected_by_python(): - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - with pytest.raises(ValueError): - pixpat.draw_pattern(buf, 'plain', params={'color': 'ff,00,00'}) - with pytest.raises(ValueError): - pixpat.draw_pattern(buf, 'plain', params={'col=or': 'ff0000'}) - - -def test_draw_pattern_unknown_params_keys_ignored(): - """Patterns silently ignore keys they don't read; unknown keys - alongside the recognised one still let the call succeed.""" - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'plain', params={'color': 'ff0000', 'unknown_key': '42'}) - b, g, r = _first_pixel_bgr(data) - assert (b, g, r) == (0, 0, 0xFF) - - -def test_draw_pattern_params_ignored_by_kmstest(): - """Patterns that don't read params accept arbitrary params strings.""" - w, h = 32, 16 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'kmstest', params={'whatever': 'foo'}) - assert any(b != 0 for b in data) - - -def _xrgb_pixel_at(data, stride, x, y): - off = y * stride + x * 4 - return data[off], data[off + 1], data[off + 2] # B, G, R - - -@pytest.mark.parametrize( - 'pattern,extra', - [ - ('hramp', {}), - ('vramp', {}), - ('dramp', {}), - ('zoneplate', {}), - ('checker', {}), - ('checker', {'params': 'cell=1'}), - ('checker', {'params': {'cell': '32'}}), - ], -) -def test_draw_pattern_phase3_smoke(pattern, extra): - w, h = 64, 32 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, pattern, **extra) - assert any(b != 0 for b in data) - - -def test_draw_pattern_checker_default_cell_first_pixel_white(): - w, h = 64, 32 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'checker') - # (0,0) is in the first cell -> white. - assert _xrgb_pixel_at(data, stride, 0, 0) == (0xFF, 0xFF, 0xFF) - # Default cell=8: (8,0) is the next cell horizontally -> black. - assert _xrgb_pixel_at(data, stride, 8, 0) == (0, 0, 0) - - -def test_draw_pattern_checker_cell1_alternates(): - w, h = 16, 8 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'checker', params={'cell': '1'}) - p00 = _xrgb_pixel_at(data, stride, 0, 0) - p10 = _xrgb_pixel_at(data, stride, 1, 0) - p01 = _xrgb_pixel_at(data, stride, 0, 1) - p11 = _xrgb_pixel_at(data, stride, 1, 1) - assert p00 != p10 - assert p00 != p01 - assert p00 == p11 # diagonal repeats - - -@pytest.mark.parametrize('cell', ['0', '-1', 'oops', '1.5']) -def test_draw_pattern_checker_invalid_cell_raises(cell): - w, h = 16, 8 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - with pytest.raises(pixpat.PixpatError): - pixpat.draw_pattern(buf, 'checker', params={'cell': cell}) - - -def test_draw_pattern_hramp_stripes(): - """hramp: 4 horizontal stripes (R, G, B, gray), each ramping along x. - The right edge (x=W-1) hits 0xFF in the active channel(s).""" - w, h = 32, 16 # h=16 → stripes at rows [0,3], [4,7], [8,11], [12,15] - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'hramp') - assert _xrgb_pixel_at(data, stride, w - 1, 0) == (0, 0, 0xFF) # R - assert _xrgb_pixel_at(data, stride, w - 1, 4) == (0, 0xFF, 0) # G - assert _xrgb_pixel_at(data, stride, w - 1, 8) == (0xFF, 0, 0) # B - assert _xrgb_pixel_at(data, stride, w - 1, h - 1) == (0xFF, 0xFF, 0xFF) # gray - # Left edge is the 0 end of every ramp. - for y in (0, 4, 8, 12): - assert _xrgb_pixel_at(data, stride, 0, y) == (0, 0, 0) - - -def test_draw_pattern_vramp_columns(): - """vramp: 4 vertical columns (R, G, B, gray), each ramping along y.""" - w, h = 16, 32 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'vramp') - # Bottom row hits max in the active channel(s). - assert _xrgb_pixel_at(data, stride, 0, h - 1) == (0, 0, 0xFF) # R - assert _xrgb_pixel_at(data, stride, 4, h - 1) == (0, 0xFF, 0) # G - assert _xrgb_pixel_at(data, stride, 8, h - 1) == (0xFF, 0, 0) # B - assert _xrgb_pixel_at(data, stride, 12, h - 1) == (0xFF, 0xFF, 0xFF) # gray - # Top row is the 0 end. - for x in (0, 4, 8, 12): - assert _xrgb_pixel_at(data, stride, x, 0) == (0, 0, 0) - # Within a column, luma is monotonic. - prev = -1 - for y in range(h): - b, g, r = _xrgb_pixel_at(data, stride, 12, y) # gray column - assert b == g == r - assert b >= prev - prev = b - - -def test_draw_pattern_zoneplate_center_white(): - """Even sizes don't sample exactly at the center; pick odd dimensions - so (W//2, H//2) is the center pixel where cos(0)=1 → white.""" - w, h = 65, 33 - data, stride = _alloc_xrgb(w, h) - buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride]) - pixpat.draw_pattern(buf, 'zoneplate') - b, g, r = _xrgb_pixel_at(data, stride, w // 2, h // 2) - # Center pixel: phase=0, gray ≈ 1.0. - assert b == g == r and b >= 0xFE - - -def test_convert_roundtrip_xrgb8888(): - w, h = 64, 32 - src = bytearray(w * h * 4) - mid = bytearray(w * h * 8) - dst = bytearray(w * h * 4) - pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), 'smpte') - pixpat.convert( - pixpat.Buffer([mid], 'ABGR16161616', w, h, [w * 8]), - pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), - ) - pixpat.convert( - pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]), - pixpat.Buffer([mid], 'ABGR16161616', w, h, [w * 8]), - ) - assert src == dst - - -def test_convert_readonly_bytes_src(): - """Source planes may be read-only (bytes); the C library only reads src.""" - w, h = 64, 32 - src_writable = bytearray(w * h * 4) - pixpat.draw_pattern(pixpat.Buffer([src_writable], 'XRGB8888', w, h, [w * 4]), 'smpte') - src_readonly = bytes(src_writable) - dst = bytearray(w * h * 8) - pixpat.convert( - pixpat.Buffer([dst], 'ABGR16161616', w, h, [w * 8]), - pixpat.Buffer([src_readonly], 'XRGB8888', w, h, [w * 4]), - ) - assert any(b != 0 for b in dst) - - -def test_convert_dimension_mismatch_raises(): - w, h = 64, 32 - src = bytearray(w * h * 4) - dst = bytearray((w * 2) * h * 8) - with pytest.raises(ValueError, match='dimensions'): - pixpat.convert( - pixpat.Buffer([dst], 'ABGR16161616', w * 2, h, [w * 2 * 8]), - pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), - ) - - -def test_draw_pattern_nv12_semi_planar(): - """Semi-planar NV12: separate Y and interleaved-UV planes.""" - w, h = 64, 32 - y = bytearray(w * h) - uv = bytearray(w * (h // 2)) - pixpat.draw_pattern(pixpat.Buffer([y, uv], 'NV12', w, h, [w, w]), 'smpte') - assert any(b != 0 for b in y) - assert any(b != 0 for b in uv) - - -def test_draw_pattern_yuv420_three_planes(): - """Fully planar YUV420: Y, U and V all at separate plane pointers.""" - w, h = 64, 32 - yp = bytearray(w * h) - up = bytearray((w // 2) * (h // 2)) - vp = bytearray((w // 2) * (h // 2)) - pixpat.draw_pattern( - pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]), - 'smpte', - ) - assert any(b != 0 for b in yp) - assert any(b != 0 for b in up) - assert any(b != 0 for b in vp) - - -def test_convert_xrgb_to_nv12_writes_both_planes(): - """Convert into a multi-plane destination must populate every plane.""" - w, h = 64, 32 - src = bytearray(w * h * 4) - pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), 'smpte') - y = bytearray(w * h) - uv = bytearray(w * (h // 2)) - pixpat.convert( - pixpat.Buffer([y, uv], 'NV12', w, h, [w, w]), - pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), - ) - assert any(b != 0 for b in y) - assert any(b != 0 for b in uv) - - -def test_convert_yuv420_src_three_planes(): - """Planar YUV is a supported convert source — the C library reads - all three plane pointers, so make sure the binding wires them up.""" - w, h = 64, 32 - yp = bytearray(w * h) - up = bytearray((w // 2) * (h // 2)) - vp = bytearray((w // 2) * (h // 2)) - pixpat.draw_pattern( - pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]), - 'smpte', - ) - dst = bytearray(w * h * 4) - pixpat.convert( - pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]), - pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]), - ) - assert any(b != 0 for b in dst) - - -def test_convert_bayer_src(): - """Bayer formats are supported as a convert source (decoded by - nearest-neighbor demosaic per the public header docstring).""" - w, h = 64, 32 - src = bytearray(w * h) - pixpat.draw_pattern(pixpat.Buffer([src], 'SRGGB8', w, h, [w]), 'smpte') - dst = bytearray(w * h * 4) - pixpat.convert( - pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]), - pixpat.Buffer([src], 'SRGGB8', w, h, [w]), - ) - assert any(b != 0 for b in dst) - - -def test_convert_roundtrip_yuyv(): - """YUYV (packed 4:2:2) — chroma is averaged on write and replicated on - read, so the second leg must not change the buffer.""" - w, h = 64, 32 - src = bytearray(w * h * 2) - mid = bytearray(w * h * 8) - dst = bytearray(w * h * 2) - pixpat.draw_pattern(pixpat.Buffer([src], 'YUYV', w, h, [w * 2]), 'smpte') - pixpat.convert( - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - pixpat.Buffer([src], 'YUYV', w, h, [w * 2]), - ) - pixpat.convert( - pixpat.Buffer([dst], 'YUYV', w, h, [w * 2]), - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - ) - assert src == dst - - -def test_convert_roundtrip_y8(): - """Y8 grayscale — chroma is discarded on write and synthesized on read, - Y values themselves should roundtrip exactly.""" - w, h = 64, 32 - src = bytearray(w * h) - mid = bytearray(w * h * 8) - dst = bytearray(w * h) - pixpat.draw_pattern(pixpat.Buffer([src], 'Y8', w, h, [w]), 'smpte') - pixpat.convert( - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - pixpat.Buffer([src], 'Y8', w, h, [w]), - ) - pixpat.convert( - pixpat.Buffer([dst], 'Y8', w, h, [w]), - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - ) - assert src == dst - - -def test_convert_roundtrip_nv12(): - """NV12 semi-planar 4:2:0 — chroma averaged into a 2x2 block on write, - replicated on read; second leg is a no-op.""" - w, h = 64, 32 - y_src = bytearray(w * h) - uv_src = bytearray(w * (h // 2)) - mid = bytearray(w * h * 8) - y_dst = bytearray(w * h) - uv_dst = bytearray(w * (h // 2)) - pixpat.draw_pattern(pixpat.Buffer([y_src, uv_src], 'NV12', w, h, [w, w]), 'smpte') - pixpat.convert( - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - pixpat.Buffer([y_src, uv_src], 'NV12', w, h, [w, w]), - ) - pixpat.convert( - pixpat.Buffer([y_dst, uv_dst], 'NV12', w, h, [w, w]), - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - ) - assert y_src == y_dst - assert uv_src == uv_dst - - -def test_convert_roundtrip_xv15(): - """P030 semi-planar 4:2:0, 10 bits packed 3-per-32-bit. Width and height - chosen so the Y plane fits 3 samples per word and chroma is on a 2x2 grid.""" - w, h = 96, 32 # multiples of 3 (Y group) and 2 (v_sub) - y_words = (w // 3) * h - uv_words = (w // 3 // 2) * (h // 2) - y_src = bytearray(y_words * 4) - uv_src = bytearray(uv_words * 8) - mid = bytearray(w * h * 8) - y_dst = bytearray(y_words * 4) - uv_dst = bytearray(uv_words * 8) - pixpat.draw_pattern( - pixpat.Buffer([y_src, uv_src], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]), - 'smpte', - ) - pixpat.convert( - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - pixpat.Buffer([y_src, uv_src], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]), - ) - pixpat.convert( - pixpat.Buffer([y_dst, uv_dst], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]), - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - ) - assert y_src == y_dst - assert uv_src == uv_dst - - -def test_convert_roundtrip_x403(): - """T430 fully-planar 4:4:4 with 3-samples-per-32-bit packing on each - plane; no chroma subsampling so first leg is already lossless.""" - w, h = 96, 32 # width must be a multiple of 3 - plane_words = (w // 3) * h - y_src = bytearray(plane_words * 4) - cb_src = bytearray(plane_words * 4) - cr_src = bytearray(plane_words * 4) - mid = bytearray(w * h * 8) - y_dst = bytearray(plane_words * 4) - cb_dst = bytearray(plane_words * 4) - cr_dst = bytearray(plane_words * 4) - strides = [(w // 3) * 4] * 3 - pixpat.draw_pattern( - pixpat.Buffer([y_src, cb_src, cr_src], 'T430', w, h, strides), - 'smpte', - ) - pixpat.convert( - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - pixpat.Buffer([y_src, cb_src, cr_src], 'T430', w, h, strides), - ) - pixpat.convert( - pixpat.Buffer([y_dst, cb_dst, cr_dst], 'T430', w, h, strides), - pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]), - ) - assert y_src == y_dst - assert cb_src == cb_dst - assert cr_src == cr_dst - - -def test_strides_length_mismatch_multi_plane_raises(): - """Strides count must match planes count — caught in Python before C.""" - w, h = 64, 32 - y = bytearray(w * h) - uv = bytearray(w * (h // 2)) - with pytest.raises(ValueError, match='strides'): - pixpat.draw_pattern( - pixpat.Buffer([y, uv], 'NV12', w, h, [w]), # 1 stride, 2 planes - 'smpte', - ) diff --git a/pixpat-python/tests/test_numpy.py b/pixpat-python/tests/test_numpy.py deleted file mode 100644 index 6dbd7bb..0000000 --- a/pixpat-python/tests/test_numpy.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Empirical check that pixpat works correctly with numpy. - -pixpat does not depend on numpy. This file proves that ndarrays work -as ``Buffer`` planes through the buffer protocol, in the shapes a -caller would typically use. Skipped when numpy is missing. -""" - -import pixpat -import pytest - -np = pytest.importorskip('numpy') - - -def test_xrgb8888_into_uint8_3d_ndarray(): - w, h = 64, 32 - arr = np.zeros((h, w, 4), dtype=np.uint8) - pixpat.draw_pattern(pixpat.Buffer([arr], 'XRGB8888', w, h, [arr.strides[0]]), 'smpte') - assert arr.any() - - -def test_abgr16161616_into_uint16_3d_ndarray(): - w, h = 64, 32 - arr = np.zeros((h, w, 4), dtype=np.uint16) - pixpat.draw_pattern(pixpat.Buffer([arr], 'ABGR16161616', w, h, [arr.strides[0]]), 'smpte') - assert arr.any() - assert arr.dtype == np.uint16 - - -def test_nv12_into_two_ndarrays(): - w, h = 64, 32 - y = np.zeros((h, w), dtype=np.uint8) - uv = np.zeros((h // 2, w), dtype=np.uint8) # interleaved U,V at half-height - pixpat.draw_pattern( - pixpat.Buffer( - planes=[y, uv], - fmt='NV12', - width=w, - height=h, - strides=[y.strides[0], uv.strides[0]], - ), - 'smpte', - ) - assert y.any() - assert uv.any() - - -def test_convert_with_readonly_ndarray_src(): - """A numpy view with writeable=False must work as the convert source.""" - w, h = 64, 32 - src = np.zeros((h, w, 4), dtype=np.uint8) - pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), 'smpte') - src.flags.writeable = False - - dst = np.zeros((h, w, 4), dtype=np.uint16) - pixpat.convert( - pixpat.Buffer([dst], 'ABGR16161616', w, h, [dst.strides[0]]), - pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), - ) - assert dst.any() - - -def test_roundtrip_ndarrays_byte_for_byte(): - """End-to-end XRGB8888 -> ABGR16161616 -> XRGB8888 with ndarrays only.""" - w, h = 64, 32 - src = np.zeros((h, w, 4), dtype=np.uint8) - mid = np.zeros((h, w, 4), dtype=np.uint16) - dst = np.zeros((h, w, 4), dtype=np.uint8) - - pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), 'smpte') - pixpat.convert( - pixpat.Buffer([mid], 'ABGR16161616', w, h, [mid.strides[0]]), - pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), - ) - pixpat.convert( - pixpat.Buffer([dst], 'XRGB8888', w, h, [dst.strides[0]]), - pixpat.Buffer([mid], 'ABGR16161616', w, h, [mid.strides[0]]), - ) - assert np.array_equal(src, dst) - - -def test_writable_view_via_view_method(): - """`arr.view()` shares memory and is writable by default — drawing into - it must update the original.""" - w, h = 64, 32 - arr = np.zeros((h, w, 4), dtype=np.uint8) - view = arr.view() - pixpat.draw_pattern(pixpat.Buffer([view], 'XRGB8888', w, h, [view.strides[0]]), 'smpte') - assert arr.any() # the original sees the writes - - -def test_noncontiguous_source_smoke(): - """Discovery test: what happens if the caller hands us a non-C-contiguous - array? pixpat reads ``stride`` bytes per row, so any row-contiguous - layout where ``arr.strides[0]`` is the row stride should work; a - transposed array (where rows aren't even contiguous) is misuse. - - This test passes a *valid* row-contiguous slice — the top half of a - bigger image, taken via slicing — and expects success. It exists to - document the contract: 'rows must be contiguous in memory; pass - arr.strides[0] as the stride'. - """ - big = np.zeros((64, 64, 4), dtype=np.uint8) - top_half = big[:32] # shape (32, 64, 4); rows still C-contiguous - assert top_half.strides[0] == 64 * 4 - pixpat.draw_pattern( - pixpat.Buffer([top_half], 'XRGB8888', 64, 32, [top_half.strides[0]]), - 'smpte', - ) - assert big[:32].any() - assert not big[32:].any() # the other half stayed untouched diff --git a/pixpat-python/tests/test_threading.py b/pixpat-python/tests/test_threading.py deleted file mode 100644 index 99d4620..0000000 --- a/pixpat-python/tests/test_threading.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Multi-threaded == single-threaded parity tests. - -The chosen ``num_threads`` is supposed to be transparent: the output -bytes must match the single-threaded output exactly. Anything else is -a threading bug. - -These tests parametrize across one format per writer template so that -every code path in the dispatcher is covered. -""" - -import pytest - -import pixpat - - -# Width must be divisible by 3 (T430/XYYY2101010 pack 3-per-32-bit), 4 -# (Bayer 10P packs 4 pixels per group), and 2 (h_sub). -# Height must be divisible by 2 (v_sub for 4:2:0 formats). -# -# Two sizes: a small one for fast format-coverage parametrization, and -# a larger one (~HD-width) where worker-stripe alignment can interact -# with the writers' chroma logic in non-obvious ways. -SMALL = (192, 64) -LARGE = (1920, 64) - - -def _alloc(fmt, w, h): - """Return (planes, strides) sized for `fmt` at (w, h).""" - if fmt in ('NV12', 'NV21'): # semi-planar 4:2:0 - return [bytearray(w * h), bytearray(w * (h // 2))], [w, w] - if fmt in ('NV16', 'NV61'): # semi-planar 4:2:2 - return [bytearray(w * h), bytearray(w * h)], [w, w] - if fmt == 'P030': # semi-planar 4:2:0, 10-bit packed 3-per-32 - ys, uvs = (w // 3) * 4, (w // 3 // 2) * 8 - return [bytearray(ys * h), bytearray(uvs * (h // 2))], [ys, uvs] - if fmt == 'P230': # semi-planar 4:2:2, 10-bit packed 3-per-32 - ys, uvs = (w // 3) * 4, (w // 3 // 2) * 8 - return [bytearray(ys * h), bytearray(uvs * h)], [ys, uvs] - if fmt in ('YUV420', 'YVU420'): # planar 4:2:0 - return ( - [bytearray(w * h), bytearray((w // 2) * (h // 2)), bytearray((w // 2) * (h // 2))], - [w, w // 2, w // 2], - ) - if fmt in ('YUV422', 'YVU422'): # planar 4:2:2 - return ( - [bytearray(w * h), bytearray((w // 2) * h), bytearray((w // 2) * h)], - [w, w // 2, w // 2], - ) - if fmt in ('YUV444', 'YVU444'): # planar 4:4:4 - return [bytearray(w * h)] * 3, [w] * 3 - if fmt == 'T430': # planar packed 4:4:4, 10-bit - s = (w // 3) * 4 - return [bytearray(s * h)] * 3, [s] * 3 - if fmt in ('Y8', 'R8', 'RGB332'): - return [bytearray(w * h)], [w] - if fmt == 'XYYY2101010': - s = (w // 3) * 4 - return [bytearray(s * h)], [s] - if fmt in ('YUYV', 'YVYU', 'UYVY', 'VYUY'): - return [bytearray(w * h * 2)], [w * 2] - if fmt in ('Y210', 'Y212', 'Y216'): - # 4:2:2, two pixels per 64-bit word. - return [bytearray(w * h * 4)], [w * 4] - if fmt == 'VUY888': - # 24-bit packed YUV, 3 bytes per pixel (storage uint32_t). - return [bytearray(w * h * 3)], [w * 3] - if fmt in ('XVUY2101010', 'XVUY8888'): - return [bytearray(w * h * 4)], [w * 4] - if fmt in ('AVUY16161616', 'ABGR16161616'): - return [bytearray(w * h * 8)], [w * 8] - # MIPI CSI-2 byte packing (Bayer SXXXX10P/12P and grayscale Y10P/Y12P). - if fmt.endswith('10P'): - s = (w // 4) * 5 - return [bytearray(s * h)], [s] - if fmt.endswith('12P'): - s = (w // 2) * 3 - return [bytearray(s * h)], [s] - # Plain Bayer - if fmt.startswith('S') and fmt[-1] == '8': - return [bytearray(w * h)], [w] - if fmt.startswith('S'): - return [bytearray(w * h * 2)], [w * 2] - # 16-bit RGB - if fmt.endswith('565') or fmt.endswith('1555') or fmt.endswith('4444'): - return [bytearray(w * h * 2)], [w * 2] - # 32-bit RGB (8888 / 2101010 / 1010102 / 888 in 32-bit storage) - return [bytearray(w * h * 4)], [w * 4] - - -# One format per writer template in pixpat.cpp. -DRAW_FORMATS = [ - 'XRGB8888', # ARGB_Writer - 'RGB565', # ARGB_Writer (16-bit) - 'ABGR16161616', # ARGB_Writer (normalized wide) - 'XVUY2101010', # YUV_Writer - 'AVUY16161616', # YUV_Writer (normalized wide) - 'YUYV', # YUVPackedWriter - 'NV12', # YUVSemiPlanarWriter v_sub=2 - 'NV16', # YUVSemiPlanarWriter v_sub=1 - 'P030', # YUVSemiPlanarWriter v_sub=2, 10-bit packed - 'P230', # YUVSemiPlanarWriter v_sub=1, 10-bit packed - 'YUV420', # YUVPlanarWriter v_sub=2 - 'YUV422', # YUVPlanarWriter v_sub=1 - 'YUV444', # YUVPlanarWriter no subsampling - 'T430', # YUVPlanarPackedWriter - 'Y8', # Y_Writer (1 sample per byte) - 'XYYY2101010', # Y_Writer (3 samples per word) - 'Y10P', # GrayPacked_Writer (CSI-2 byte packing, Y-only) - 'Y210', # YUVPackedWriter with X-padding entries (4:2:2 in 64-bit word) - 'R8', # MonoRGB_Writer (single R channel) - 'SRGGB8', # Bayer_Writer - 'SRGGB10', # Bayer_Writer - 'SRGGB10P', # BayerPacked_Writer - 'SRGGB12P', # BayerPacked_Writer -] - -# Convert sources: same minus Bayer (Bayer formats have no read path). -CONVERT_FORMATS = [f for f in DRAW_FORMATS if not f.startswith('S')] - - -@pytest.mark.parametrize('fmt', DRAW_FORMATS) -@pytest.mark.parametrize( - 'pattern', - ['smpte', 'kmstest', 'checker', 'hramp', 'vramp', 'dramp', 'zoneplate'], -) -@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large']) -def test_draw_pattern_threaded_matches_single(fmt, pattern, size): - """draw_pattern with num_threads=4 must produce identical bytes - to num_threads=1 for every supported format.""" - w, h = size - s_planes, strides = _alloc(fmt, w, h) - pixpat.draw_pattern(pixpat.Buffer(s_planes, fmt, w, h, strides), pattern, num_threads=1) - - t_planes, _ = _alloc(fmt, w, h) - pixpat.draw_pattern(pixpat.Buffer(t_planes, fmt, w, h, strides), pattern, num_threads=4) - - for i, (s, t) in enumerate(zip(s_planes, t_planes)): - assert s == t, f'{fmt} ({pattern}, {w}x{h}): plane {i} differs threaded vs single' - - -@pytest.mark.parametrize('fmt', ['XRGB8888', 'NV12', 'YUV420', 'BGR888']) -@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large']) -def test_draw_pattern_plain_threaded_matches_single(fmt, size): - """draw_pattern with params= must also be bit-identical across - thread counts. Spot-check a few representative formats.""" - w, h = size - params = {'color': '12ab34'} - s_planes, strides = _alloc(fmt, w, h) - pixpat.draw_pattern( - pixpat.Buffer(s_planes, fmt, w, h, strides), 'plain', num_threads=1, params=params - ) - - t_planes, _ = _alloc(fmt, w, h) - pixpat.draw_pattern( - pixpat.Buffer(t_planes, fmt, w, h, strides), 'plain', num_threads=4, params=params - ) - - for i, (s, t) in enumerate(zip(s_planes, t_planes)): - assert s == t, f'{fmt} (plain, {w}x{h}): plane {i} differs threaded vs single' - - -@pytest.mark.parametrize('src_fmt', CONVERT_FORMATS) -@pytest.mark.parametrize('dst_fmt', ['ABGR16161616', 'AVUY16161616', 'XRGB8888', 'NV12', 'YUV420']) -@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large']) -def test_convert_threaded_matches_single(src_fmt, dst_fmt, size): - """convert with num_threads=4 must produce identical bytes - to num_threads=1.""" - w, h = size - src_planes, src_strides = _alloc(src_fmt, w, h) - pixpat.draw_pattern(pixpat.Buffer(src_planes, src_fmt, w, h, src_strides), 'smpte') - - s_dst_planes, dst_strides = _alloc(dst_fmt, w, h) - pixpat.convert( - pixpat.Buffer(s_dst_planes, dst_fmt, w, h, dst_strides), - pixpat.Buffer(src_planes, src_fmt, w, h, src_strides), - num_threads=1, - ) - - t_dst_planes, _ = _alloc(dst_fmt, w, h) - pixpat.convert( - pixpat.Buffer(t_dst_planes, dst_fmt, w, h, dst_strides), - pixpat.Buffer(src_planes, src_fmt, w, h, src_strides), - num_threads=4, - ) - - for i, (s, t) in enumerate(zip(s_dst_planes, t_dst_planes)): - assert s == t, f'{src_fmt}->{dst_fmt} ({w}x{h}): plane {i} differs threaded vs single' |
