"""Tests for the generated top-level array-aware facade.""" from __future__ import annotations import ctypes import importlib import sys import types from pathlib import Path import numpy as np import pytest ROOT_DIR = Path(__file__).resolve().parents[1] pytestmark = pytest.mark.skipif( not (ROOT_DIR / 'datoviz' / '_array_facade.py').exists(), reason='array facade has not been generated', ) def _raw_function(argtypes, restype=ctypes.c_int): calls = [] def func(*args): calls.append(args) return 17 func.argtypes = argtypes func.restype = restype func.__doc__ = 'raw function' func.calls = calls return func @pytest.fixture() def fake_facade(monkeypatch): raw = types.ModuleType('datoviz.raw') raw.DvzVisual = type('DvzVisual', (), {}) raw.DvzSizeSpace = type('DvzSizeSpace', (), {'DVZ_SIZE_LOGICAL': 0}) raw.DVZ_SIZE_LOGICAL = raw.DvzSizeSpace.DVZ_SIZE_LOGICAL class DvzAxisTicks(ctypes.Structure): _fields_ = [ ('struct_size', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('count', ctypes.c_uint32), ('values', ctypes.POINTER(ctypes.c_double)), ('labels', ctypes.POINTER(ctypes.c_char_p)), ] class DvzColorbarTicks(ctypes.Structure): _fields_ = [ ('struct_size', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('count', ctypes.c_uint32), ('values', ctypes.POINTER(ctypes.c_double)), ('labels', ctypes.POINTER(ctypes.c_char_p)), ] class DvzVisualDataUpdate(ctypes.Structure): _fields_ = [ ('attr_name', ctypes.c_char_p), ('data', ctypes.c_void_p), ('item_count', ctypes.c_uint32), ] class DvzSampledFieldDesc(ctypes.Structure): _fields_ = [ ('struct_size', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('dim', ctypes.c_int), ('format', ctypes.c_int), ('semantic', ctypes.c_int), ('color_role', ctypes.c_int), ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('depth', ctypes.c_uint32), ] class DvzFieldDataView(ctypes.Structure): _fields_ = [ ('struct_size', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('data', ctypes.c_void_p), ('bytes_per_row', ctypes.c_uint64), ('rows_per_image', ctypes.c_uint64), ] class DvzFieldRegion(ctypes.Structure): _fields_ = [ ('x', ctypes.c_uint32), ('y', ctypes.c_uint32), ('z', ctypes.c_uint32), ('width', ctypes.c_uint32), ('height', ctypes.c_uint32), ('depth', ctypes.c_uint32), ] raw.DvzAxisTicks = DvzAxisTicks raw.DvzColorbarTicks = DvzColorbarTicks raw.DvzVisualDataUpdate = DvzVisualDataUpdate raw.DvzSampledFieldDesc = DvzSampledFieldDesc raw.DvzFieldDataView = DvzFieldDataView raw.DvzFieldRegion = DvzFieldRegion raw.DVZ_FIELD_DIM_2D = 0 raw.DVZ_FIELD_DIM_3D = 1 raw.DVZ_FIELD_FORMAT_R8_UNORM = 0 raw.DVZ_FIELD_FORMAT_R32_UINT = 9 raw.DVZ_FIELD_FORMAT_R32_FLOAT = 11 raw.DVZ_FIELD_FORMAT_RGBA8_UNORM = 22 raw.DVZ_FIELD_SEMANTIC_SCALAR = 1 raw.DVZ_FIELD_SEMANTIC_COLOR = 4 raw.DVZ_FIELD_SEMANTIC_LABEL = 5 raw.DVZ_COLOR_ROLE_NONE = 0 raw.DVZ_COLOR_ROLE_SRGB_COLOR = 1 raw.DVZ_COLOR_ROLE_DATA = 3 raw.dvz_axis_set_ticks = _raw_function( [ctypes.c_void_p, ctypes.POINTER(DvzAxisTicks)], ctypes.c_bool ) raw.dvz_colorbar_set_ticks = _raw_function( [ctypes.c_void_p, ctypes.POINTER(DvzColorbarTicks)], ctypes.c_bool ) raw.dvz_passthrough = _raw_function([ctypes.c_int, ctypes.c_int]) raw.dvz_scene_buffer_set_data = _raw_function( [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint64], ctypes.c_bool ) raw.dvz_visual_set_data = _raw_function( [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_uint32] ) raw.dvz_visual_set_data_many = _raw_function( [ctypes.c_void_p, ctypes.POINTER(DvzVisualDataUpdate), ctypes.c_uint32] ) raw.dvz_visual_set_data_range = _raw_function( [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_uint32] ) raw.dvz_visual_set_index_data = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32), ctypes.c_uint32] ) raw.dvz_bars_set_intervals = _raw_function( [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.c_uint32, ] ) raw.dvz_band_set_bounds = _raw_function( [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.c_uint32, ] ) raw.dvz_band_set_center = _raw_function( [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ctypes.c_uint32, ] ) raw.dvz_text_set_items = _raw_function( [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32] ) raw.dvz_text_set_positions = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_double), ctypes.c_uint32] ) raw.dvz_text_set_offsets = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_uint32] ) raw.dvz_text_set_anchors = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_uint32] ) raw.dvz_text_set_sizes = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_uint32] ) raw.dvz_text_set_colors = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_uint32] ) raw.dvz_text_set_angles = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_float), ctypes.c_uint32] ) raw.dvz_view_window = _raw_function( [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_char_p], ctypes.c_void_p, ) raw.dvz_view_canvas = _raw_function([ctypes.c_void_p], ctypes.c_void_p) raw.dvz_view_framebuffer_size = _raw_function( [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32)], None, ) raw.dvz_canvas_capture_rgba_into = _raw_function( [ ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint8), ctypes.c_size_t, ] ) def sampled_field_desc(): desc = DvzSampledFieldDesc() desc.struct_size = ctypes.sizeof(DvzSampledFieldDesc) return desc def field_data_view(): view = DvzFieldDataView() view.struct_size = ctypes.sizeof(DvzFieldDataView) return view raw.dvz_sampled_field_desc = sampled_field_desc raw.dvz_field_data_view = field_data_view raw.dvz_sampled_field = _raw_function( [ctypes.c_void_p, ctypes.POINTER(DvzSampledFieldDesc)], ctypes.c_void_p, ) sampled_field_set_data_calls = [] def sampled_field_set_data(field, view): sampled_field_set_data_calls.append((field, view)) return 0 sampled_field_set_data.argtypes = [ctypes.c_void_p, ctypes.POINTER(DvzFieldDataView)] sampled_field_set_data.restype = ctypes.c_int sampled_field_set_data.__doc__ = 'sampled field set data' sampled_field_set_data.calls = sampled_field_set_data_calls raw.dvz_sampled_field_set_data = sampled_field_set_data sampled_field_update_region_calls = [] def sampled_field_update_region(field, region, view): sampled_field_update_region_calls.append((field, region, view)) return 0 sampled_field_update_region.argtypes = [ ctypes.c_void_p, DvzFieldRegion, ctypes.POINTER(DvzFieldDataView), ] sampled_field_update_region.restype = ctypes.c_int sampled_field_update_region.__doc__ = 'sampled field update region' sampled_field_update_region.calls = sampled_field_update_region_calls raw.dvz_sampled_field_update_region = sampled_field_update_region raw.__all__ = [ name for name in vars(raw) if name.startswith(('dvz_', 'Dvz', 'DVZ_')) ] monkeypatch.setitem(sys.modules, 'datoviz.raw', raw) sys.modules.pop('datoviz._array_facade', None) import datoviz datoviz.raw = raw if '_array_facade' in datoviz.__dict__: delattr(datoviz, '_array_facade') facade = importlib.import_module('datoviz._array_facade') yield raw, facade sys.modules.pop('datoviz._array_facade', None) def test_array_facade_exports_raw_names_and_passthrough(fake_facade): raw, facade = fake_facade assert 'dvz_visual_set_data' in facade.__all__ assert 'dvz_colorbar_set_ticks' in facade.__all__ assert 'dvz_passthrough' in facade.__all__ assert 'DVZ_SIZE_LOGICAL' in facade.__all__ assert facade.DvzVisual is raw.DvzVisual assert facade.DvzSizeSpace is raw.DvzSizeSpace assert facade.DVZ_SIZE_LOGICAL == raw.DVZ_SIZE_LOGICAL assert facade.dvz_passthrough(1, 2) == 17 assert raw.dvz_passthrough.calls[-1] == (1, 2) def test_top_level_package_reexports_raw_enum_constants(fake_facade): raw, facade = fake_facade import datoviz assert datoviz.DVZ_SIZE_LOGICAL == raw.DVZ_SIZE_LOGICAL assert datoviz.DvzSizeSpace is raw.DvzSizeSpace assert 'DVZ_SIZE_LOGICAL' in dir(datoviz) def test_visual_set_data_encodes_string_and_infers_shape0(fake_facade): raw, facade = fake_facade data = np.arange(6, dtype=np.float32).reshape(3, 2) assert facade.dvz_visual_set_data(ctypes.c_void_p(1), 'position', data) == 17 visual, attr_name, pointer, item_count = raw.dvz_visual_set_data.calls[-1] assert visual.value == 1 assert attr_name == b'position' assert pointer.value == data.ctypes.data assert item_count == 3 def test_view_window_encodes_title(fake_facade): raw, facade = fake_facade assert facade.dvz_view_window(ctypes.c_void_p(1), ctypes.c_void_p(2), 320, 240, 'Datoviz') == 17 app, figure, width, height, title = raw.dvz_view_window.calls[-1] assert app.value == 1 assert figure.value == 2 assert width == 320 assert height == 240 assert title == b'Datoviz' def test_visual_set_data_range_infers_count_after_first_item(fake_facade): raw, facade = fake_facade data = np.arange(4, dtype=np.float32).reshape(2, 2) facade.dvz_visual_set_data_range(ctypes.c_void_p(2), 'position', 5, data) _, attr_name, first_item, pointer, item_count = raw.dvz_visual_set_data_range.calls[-1] assert attr_name == b'position' assert pointer.value == data.ctypes.data assert first_item == 5 assert item_count == 2 def test_visual_set_data_many_accepts_mapping(fake_facade): raw, facade = fake_facade positions = np.arange(9, dtype=np.float32).reshape(3, 3) colors = np.full((3, 4), 255, dtype=np.uint8) assert facade.dvz_visual_set_data_many( ctypes.c_void_p(7), {'position': positions, 'color': colors} ) == 17 visual, updates, update_count = raw.dvz_visual_set_data_many.calls[-1] assert visual.value == 7 assert update_count == 2 assert updates[0].attr_name == b'position' assert updates[0].data == positions.ctypes.data assert updates[0].item_count == 3 assert updates[1].attr_name == b'color' assert updates[1].data == colors.ctypes.data assert updates[1].item_count == 3 def test_visual_set_data_many_copies_non_contiguous_inputs(fake_facade): raw, facade = fake_facade data = np.arange(12, dtype=np.float32).reshape(3, 4)[:, ::2] observed = {} def inspect_many(_visual, updates, update_count): observed['update_count'] = update_count observed['pointer'] = updates[0].data copied = np.ctypeslib.as_array((ctypes.c_float * data.size).from_address(updates[0].data)) observed['data'] = copied.reshape(data.shape).copy() return 17 inspect_many.argtypes = raw.dvz_visual_set_data_many.argtypes inspect_many.restype = raw.dvz_visual_set_data_many.restype inspect_many.__doc__ = raw.dvz_visual_set_data_many.__doc__ inspect_many.calls = [] raw.dvz_visual_set_data_many = inspect_many facade.dvz_visual_set_data_many(ctypes.c_void_p(8), {'position': data}) assert observed['update_count'] == 1 assert observed['pointer'] != data.ctypes.data np.testing.assert_array_equal(observed['data'], np.ascontiguousarray(data)) def test_visual_set_data_many_rejects_inconsistent_counts_before_raw_call(fake_facade): raw, facade = fake_facade with pytest.raises(ValueError, match='item count 2 does not match expected 3'): facade.dvz_visual_set_data_many( ctypes.c_void_p(9), { 'position': np.zeros((3, 3), dtype=np.float32), 'color': np.zeros((2, 4), dtype=np.uint8), }, ) assert raw.dvz_visual_set_data_many.calls == [] def test_visual_set_data_many_raw_passthrough_requires_count(fake_facade): raw, facade = fake_facade updates = (raw.DvzVisualDataUpdate * 1)() with pytest.raises(TypeError, match='update_count must be provided'): facade.dvz_visual_set_data_many(ctypes.c_void_p(10), updates) facade.dvz_visual_set_data_many(ctypes.c_void_p(10), updates, 1) _, raw_updates, update_count = raw.dvz_visual_set_data_many.calls[-1] assert raw_updates is updates assert update_count == 1 def test_axis_set_ticks_accepts_values_and_labels(fake_facade): raw, facade = fake_facade values = np.array([0.0, 5.0, 10.0], dtype=np.float64) assert facade.dvz_axis_set_ticks(ctypes.c_void_p(11), values, ['zero', 'five', 'ten']) == 17 axis, ticks_ptr = raw.dvz_axis_set_ticks.calls[-1] ticks = ticks_ptr._obj assert axis.value == 11 assert ticks.struct_size == ctypes.sizeof(raw.DvzAxisTicks) assert ticks.flags == 0 assert ticks.count == 3 copied_values = np.ctypeslib.as_array(ticks.values, shape=(3,)) np.testing.assert_array_equal(copied_values, values) assert [ticks.labels[i] for i in range(3)] == [b'zero', b'five', b'ten'] def test_axis_set_ticks_accepts_empty_explicit_ticks(fake_facade): raw, facade = fake_facade facade.dvz_axis_set_ticks(ctypes.c_void_p(12), []) _, ticks_ptr = raw.dvz_axis_set_ticks.calls[-1] ticks = ticks_ptr._obj assert ticks.count == 0 assert not bool(ticks.values) assert not bool(ticks.labels) def test_axis_set_ticks_rejects_label_count_mismatch_before_raw_call(fake_facade): raw, facade = fake_facade with pytest.raises(ValueError, match='labels length 1 does not match tick count 2'): facade.dvz_axis_set_ticks(ctypes.c_void_p(13), [0.0, 1.0], ['zero']) assert raw.dvz_axis_set_ticks.calls == [] def test_axis_set_ticks_raw_descriptor_passthrough(fake_facade): raw, facade = fake_facade ticks = raw.DvzAxisTicks() ticks.struct_size = ctypes.sizeof(raw.DvzAxisTicks) facade.dvz_axis_set_ticks(ctypes.c_void_p(14), ctypes.byref(ticks)) _, raw_ticks = raw.dvz_axis_set_ticks.calls[-1] assert raw_ticks._obj is ticks with pytest.raises(TypeError, match='labels must be omitted'): facade.dvz_axis_set_ticks(ctypes.c_void_p(14), ctypes.byref(ticks), ['unused']) def test_colorbar_set_ticks_accepts_values_and_labels(fake_facade): raw, facade = fake_facade values = np.array([0.0, 0.5, 1.0], dtype=np.float64) assert facade.dvz_colorbar_set_ticks( ctypes.c_void_p(15), values, ['low', 'mid', 'high'] ) == 17 colorbar, ticks_ptr = raw.dvz_colorbar_set_ticks.calls[-1] ticks = ticks_ptr._obj assert colorbar.value == 15 assert ticks.struct_size == ctypes.sizeof(raw.DvzColorbarTicks) assert ticks.flags == 0 assert ticks.count == 3 copied_values = np.ctypeslib.as_array(ticks.values, shape=(3,)) np.testing.assert_array_equal(copied_values, values) assert [ticks.labels[i] for i in range(3)] == [b'low', b'mid', b'high'] def test_colorbar_set_ticks_accepts_empty_explicit_ticks(fake_facade): raw, facade = fake_facade facade.dvz_colorbar_set_ticks(ctypes.c_void_p(16), []) _, ticks_ptr = raw.dvz_colorbar_set_ticks.calls[-1] ticks = ticks_ptr._obj assert ticks.count == 0 assert not bool(ticks.values) assert not bool(ticks.labels) def test_colorbar_set_ticks_rejects_label_count_mismatch_before_raw_call(fake_facade): raw, facade = fake_facade with pytest.raises(ValueError, match='labels length 1 does not match tick count 2'): facade.dvz_colorbar_set_ticks(ctypes.c_void_p(17), [0.0, 1.0], ['low']) assert raw.dvz_colorbar_set_ticks.calls == [] def test_colorbar_set_ticks_raw_descriptor_passthrough(fake_facade): raw, facade = fake_facade ticks = raw.DvzColorbarTicks() ticks.struct_size = ctypes.sizeof(raw.DvzColorbarTicks) facade.dvz_colorbar_set_ticks(ctypes.c_void_p(18), ctypes.byref(ticks)) _, raw_ticks = raw.dvz_colorbar_set_ticks.calls[-1] assert raw_ticks._obj is ticks with pytest.raises(TypeError, match='labels must be omitted'): facade.dvz_colorbar_set_ticks(ctypes.c_void_p(18), ctypes.byref(ticks), ['unused']) def test_non_contiguous_visual_data_is_copied_during_call(fake_facade): raw, facade = fake_facade data = np.arange(12, dtype=np.float32).reshape(3, 4)[:, ::2] observed = {} def inspect_copy(_visual, _attr_name, pointer, item_count): observed['item_count'] = item_count copied = np.ctypeslib.as_array((ctypes.c_float * data.size).from_address(pointer.value)) observed['data'] = copied.reshape(data.shape).copy() observed['pointer'] = pointer.value return 17 inspect_copy.argtypes = raw.dvz_visual_set_data.argtypes inspect_copy.restype = raw.dvz_visual_set_data.restype inspect_copy.__doc__ = raw.dvz_visual_set_data.__doc__ inspect_copy.calls = [] raw.dvz_visual_set_data = inspect_copy facade.dvz_visual_set_data(ctypes.c_void_p(3), 'position', data) assert observed['item_count'] == 3 assert observed['pointer'] != data.ctypes.data np.testing.assert_array_equal(observed['data'], np.ascontiguousarray(data)) def test_scene_buffer_set_data_infers_nbytes(fake_facade): raw, facade = fake_facade data = bytearray(b'abcd') facade.dvz_scene_buffer_set_data(ctypes.c_void_p(4), data) _, pointer, byte_size = raw.dvz_scene_buffer_set_data.calls[-1] assert pointer.value is not None assert byte_size == 4 def test_index_data_requires_declared_dtype(fake_facade): raw, facade = fake_facade indices = np.array([0, 2, 1], dtype=np.uint32) facade.dvz_visual_set_index_data(ctypes.c_void_p(5), indices) _, pointer, index_count = raw.dvz_visual_set_index_data.calls[-1] assert ctypes.cast(pointer, ctypes.c_void_p).value == indices.ctypes.data assert index_count == 3 with pytest.raises(ValueError, match='dtype uint32'): facade.dvz_visual_set_index_data(ctypes.c_void_p(5), indices.astype(np.uint16)) def test_shared_count_array_facade_validates_all_arrays(fake_facade): raw, facade = fake_facade starts = np.array([0.0, 1.0, 2.0], dtype=np.float64) ends = starts + 0.8 values = np.array([0.5, 0.9, 0.7], dtype=np.float64) facade.dvz_bars_set_intervals(ctypes.c_void_p(19), starts, ends, values) bars, starts_ptr, ends_ptr, values_ptr, count = raw.dvz_bars_set_intervals.calls[-1] assert bars.value == 19 assert starts_ptr.contents.value == starts[0] assert ends_ptr.contents.value == ends[0] assert values_ptr.contents.value == values[0] assert count == 3 with pytest.raises(ValueError, match='values count 2 does not match count 3'): facade.dvz_bars_set_intervals( ctypes.c_void_p(19), starts, ends, values[:2], ) assert len(raw.dvz_bars_set_intervals.calls) == 1 def test_plot_array_facade_requires_float64(fake_facade): _raw, facade = fake_facade with pytest.raises(ValueError, match='dtype float64'): facade.dvz_band_set_center( ctypes.c_void_p(20), np.array([0.0, 1.0], dtype=np.float32), np.array([0.5, 0.75], dtype=np.float64), ) def test_view_capture_rgba_returns_numpy_array(fake_facade): raw, facade = fake_facade canvas = ctypes.c_void_p(123) def view_canvas(_view): return canvas def framebuffer_size(_view, out_width, out_height): out_width._obj.value = 3 out_height._obj.value = 2 def capture_into(_canvas, width, height, out_rgba, out_size): assert _canvas == canvas assert width == 3 assert height == 2 assert out_size == 24 data = np.ctypeslib.as_array(out_rgba, shape=(out_size,)) data[:] = np.arange(out_size, dtype=np.uint8) return 0 raw.dvz_view_canvas = view_canvas raw.dvz_view_framebuffer_size = framebuffer_size raw.dvz_canvas_capture_rgba_into = capture_into rgba = facade.dvz_view_capture_rgba(ctypes.c_void_p(11)) assert rgba.shape == (2, 3, 4) assert rgba.dtype == np.uint8 np.testing.assert_array_equal(rgba.ravel(), np.arange(24, dtype=np.uint8)) def test_view_capture_rgba_raises_on_missing_canvas(fake_facade): raw, facade = fake_facade raw.dvz_view_canvas = lambda _view: None with pytest.raises(RuntimeError, match='no canvas'): facade.dvz_view_capture_rgba(ctypes.c_void_p(12)) def test_view_capture_rgba_raises_on_capture_failure(fake_facade): raw, facade = fake_facade raw.dvz_view_canvas = lambda _view: ctypes.c_void_p(123) def framebuffer_size(_view, out_width, out_height): out_width._obj.value = 1 out_height._obj.value = 1 raw.dvz_view_framebuffer_size = framebuffer_size raw.dvz_canvas_capture_rgba_into = lambda *_args: -7 with pytest.raises(RuntimeError, match='code -7'): facade.dvz_view_capture_rgba(ctypes.c_void_p(13)) def test_sampled_field_from_rgba_array_infers_desc_and_upload_view(fake_facade): raw, facade = fake_facade rgba = np.arange(3 * 4 * 4, dtype=np.uint8).reshape(3, 4, 4) assert facade.dvz_sampled_field_from_array(ctypes.c_void_p(21), rgba) == 17 scene, desc_ptr = raw.dvz_sampled_field.calls[-1] desc = desc_ptr._obj assert scene.value == 21 assert desc.dim == raw.DVZ_FIELD_DIM_2D assert desc.format == raw.DVZ_FIELD_FORMAT_RGBA8_UNORM assert desc.semantic == raw.DVZ_FIELD_SEMANTIC_COLOR assert desc.color_role == raw.DVZ_COLOR_ROLE_SRGB_COLOR assert (desc.width, desc.height, desc.depth) == (4, 3, 1) field, view_ptr = raw.dvz_sampled_field_set_data.calls[-1] view = view_ptr._obj assert field == 17 assert view.data == rgba.ctypes.data assert view.bytes_per_row == rgba.strides[0] assert view.rows_per_image == 0 def test_sampled_field_from_scalar_3d_array_uses_volume_layout(fake_facade): raw, facade = fake_facade volume = np.zeros((5, 3, 4), dtype=np.float32) facade.dvz_sampled_field_from_array(ctypes.c_void_p(22), volume) _, desc_ptr = raw.dvz_sampled_field.calls[-1] desc = desc_ptr._obj assert desc.dim == raw.DVZ_FIELD_DIM_3D assert desc.format == raw.DVZ_FIELD_FORMAT_R32_FLOAT assert desc.semantic == raw.DVZ_FIELD_SEMANTIC_SCALAR assert desc.color_role == raw.DVZ_COLOR_ROLE_DATA assert (desc.width, desc.height, desc.depth) == (4, 3, 5) _, view_ptr = raw.dvz_sampled_field_set_data.calls[-1] view = view_ptr._obj assert view.bytes_per_row == volume.strides[1] assert view.rows_per_image == 3 def test_sampled_field_from_non_contiguous_array_copies_before_upload(fake_facade): raw, facade = fake_facade data = np.arange(24, dtype=np.float32).reshape(3, 8)[:, ::2] facade.dvz_sampled_field_from_array(ctypes.c_void_p(23), data) _, desc_ptr = raw.dvz_sampled_field.calls[-1] desc = desc_ptr._obj assert (desc.width, desc.height, desc.depth) == (4, 3, 1) _, view_ptr = raw.dvz_sampled_field_set_data.calls[-1] view = view_ptr._obj assert view.data != data.ctypes.data uploaded = np.ctypeslib.as_array((ctypes.c_float * data.size).from_address(view.data)) np.testing.assert_array_equal(uploaded.reshape(data.shape), np.ascontiguousarray(data)) def test_sampled_field_rejects_unsupported_shape_without_raw_call(fake_facade): raw, facade = fake_facade with pytest.raises(ValueError, match='unsupported sampled-field dtype/shape'): facade.dvz_sampled_field_from_array(ctypes.c_void_p(24), np.zeros((3, 4), np.float64)) assert raw.dvz_sampled_field.calls == [] def test_sampled_field_update_from_array_builds_region_and_view(fake_facade): raw, facade = fake_facade patch = np.arange(12 * 12, dtype=np.float32).reshape(12, 12) field = ctypes.c_void_p(31) assert facade.dvz_sampled_field_update_from_array(field, patch, offset=(4, 5)) is field field_arg, region, view_ptr = raw.dvz_sampled_field_update_region.calls[-1] view = view_ptr._obj assert field_arg is field assert (region.x, region.y, region.z) == (4, 5, 0) assert (region.width, region.height, region.depth) == (12, 12, 1) assert view.data == patch.ctypes.data assert view.bytes_per_row == patch.strides[0] assert view.rows_per_image == 0 def test_sampled_field_update_from_non_contiguous_array_copies_patch(fake_facade): raw, facade = fake_facade patch = np.arange(8 * 6, dtype=np.float32).reshape(6, 8)[:, ::2] facade.dvz_sampled_field_update_from_array(ctypes.c_void_p(32), patch, offset=(1, 2, 0)) _, region, view_ptr = raw.dvz_sampled_field_update_region.calls[-1] view = view_ptr._obj assert (region.width, region.height, region.depth) == (4, 6, 1) assert view.data != patch.ctypes.data uploaded = np.ctypeslib.as_array((ctypes.c_float * patch.size).from_address(view.data)) np.testing.assert_array_equal(uploaded.reshape(patch.shape), np.ascontiguousarray(patch)) def test_sampled_field_update_rejects_mismatched_extent_without_raw_call(fake_facade): raw, facade = fake_facade with pytest.raises(ValueError, match='does not match array-derived extent'): facade.dvz_sampled_field_update_from_array( ctypes.c_void_p(33), np.zeros((3, 4), dtype=np.float32), offset=(0, 0), extent=(5, 3), ) assert raw.dvz_sampled_field_update_region.calls == [] def test_top_level_datoviz_resolves_facade_lazily(fake_facade): raw, _facade = fake_facade import datoviz assert datoviz.dvz_visual_set_data(ctypes.c_void_p(6), 'color', np.zeros((1, 4), np.uint8)) == 17 assert raw.dvz_visual_set_data.calls[-1][1] == b'color'