|
| 1 | +import pytest |
| 2 | +import pythonmonkey as pm |
| 3 | +import gc |
| 4 | +import numpy, array, struct |
| 5 | + |
| 6 | +def test_py_buffer_to_js_typed_array(): |
| 7 | + # JS TypedArray/ArrayBuffer should coerce to Python memoryview type |
| 8 | + def assert_js_to_py_memoryview(buf: memoryview): |
| 9 | + assert type(buf) is memoryview |
| 10 | + assert None == buf.obj # https://docs.python.org/3.9/c-api/buffer.html#c.Py_buffer.obj |
| 11 | + assert 2 * 4 == buf.nbytes # 2 elements * sizeof(int32_t) |
| 12 | + assert "02000000ffffffff" == buf.hex() # native (little) endian |
| 13 | + buf1 = pm.eval("new Int32Array([2,-1])") |
| 14 | + buf2 = pm.eval("new Int32Array([2,-1]).buffer") |
| 15 | + assert_js_to_py_memoryview(buf1) |
| 16 | + assert_js_to_py_memoryview(buf2) |
| 17 | + assert [2, -1] == buf1.tolist() |
| 18 | + assert [2, 0, 0, 0, 255, 255, 255, 255] == buf2.tolist() |
| 19 | + assert -1 == buf1[1] |
| 20 | + assert 255 == buf2[7] |
| 21 | + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): |
| 22 | + buf1[2] |
| 23 | + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): |
| 24 | + buf2[8] |
| 25 | + del buf1, buf2 |
| 26 | + |
| 27 | + # test element value ranges |
| 28 | + buf3 = pm.eval("new Uint8Array(1)") |
| 29 | + with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): |
| 30 | + buf3[0] = 256 |
| 31 | + with pytest.raises(ValueError, match="memoryview: invalid value for format 'B'"): |
| 32 | + buf3[0] = -1 |
| 33 | + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): # no automatic resize |
| 34 | + buf3[1] = 0 |
| 35 | + del buf3 |
| 36 | + |
| 37 | + # Python buffers should coerce to JS TypedArray |
| 38 | + # and the typecode maps to TypedArray subtype (Uint8Array, Float64Array, ...) |
| 39 | + assert True == pm.eval("(arr)=>arr instanceof Uint8Array")( bytearray([1,2,3]) ) |
| 40 | + assert True == pm.eval("(arr)=>arr instanceof Uint8Array")( numpy.array([1], dtype=numpy.uint8) ) |
| 41 | + assert True == pm.eval("(arr)=>arr instanceof Uint16Array")( numpy.array([1], dtype=numpy.uint16) ) |
| 42 | + assert True == pm.eval("(arr)=>arr instanceof Uint32Array")( numpy.array([1], dtype=numpy.uint32) ) |
| 43 | + assert True == pm.eval("(arr)=>arr instanceof BigUint64Array")( numpy.array([1], dtype=numpy.uint64) ) |
| 44 | + assert True == pm.eval("(arr)=>arr instanceof Int8Array")( numpy.array([1], dtype=numpy.int8) ) |
| 45 | + assert True == pm.eval("(arr)=>arr instanceof Int16Array")( numpy.array([1], dtype=numpy.int16) ) |
| 46 | + assert True == pm.eval("(arr)=>arr instanceof Int32Array")( numpy.array([1], dtype=numpy.int32) ) |
| 47 | + assert True == pm.eval("(arr)=>arr instanceof BigInt64Array")( numpy.array([1], dtype=numpy.int64) ) |
| 48 | + assert True == pm.eval("(arr)=>arr instanceof Float32Array")( numpy.array([1], dtype=numpy.float32) ) |
| 49 | + assert True == pm.eval("(arr)=>arr instanceof Float64Array")( numpy.array([1], dtype=numpy.float64) ) |
| 50 | + assert pm.eval("new Uint8Array([1])").format == "B" |
| 51 | + assert pm.eval("new Uint16Array([1])").format == "H" |
| 52 | + assert pm.eval("new Uint32Array([1])").format == "I" # FIXME (Tom Tang): this is "L" on 32-bit systems |
| 53 | + assert pm.eval("new BigUint64Array([1n])").format == "Q" |
| 54 | + assert pm.eval("new Int8Array([1])").format == "b" |
| 55 | + assert pm.eval("new Int16Array([1])").format == "h" |
| 56 | + assert pm.eval("new Int32Array([1])").format == "i" |
| 57 | + assert pm.eval("new BigInt64Array([1n])").format == "q" |
| 58 | + assert pm.eval("new Float32Array([1])").format == "f" |
| 59 | + assert pm.eval("new Float64Array([1])").format == "d" |
| 60 | + |
| 61 | + # not enough bytes to populate an element of the TypedArray |
| 62 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: buffer length for BigInt64Array should be a multiple of 8"): |
| 63 | + pm.eval("(arr) => new BigInt64Array(arr.buffer)")(array.array('i', [-11111111])) |
| 64 | + |
| 65 | + # TypedArray with `byteOffset` and `length` |
| 66 | + arr1 = array.array('i', [-11111111, 22222222, -33333333, 44444444]) |
| 67 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): |
| 68 | + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ -4)")(arr1) |
| 69 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: start offset of Int32Array should be a multiple of 4"): |
| 70 | + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 1)")(arr1) |
| 71 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: size of buffer is too small for Int32Array with byteOffset"): |
| 72 | + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 20)")(arr1) |
| 73 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: invalid or out-of-range index"): |
| 74 | + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ -1)")(arr1) |
| 75 | + with pytest.raises(pm.SpiderMonkeyError, match="RangeError: attempting to construct out-of-bounds Int32Array on ArrayBuffer"): |
| 76 | + pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 4)")(arr1) |
| 77 | + arr2 = pm.eval("(arr) => new Int32Array(arr.buffer, /*byteOffset*/ 4, /*length*/ 2)")(arr1) |
| 78 | + assert 2 * 4 == arr2.nbytes # 2 elements * sizeof(int32_t) |
| 79 | + assert [22222222, -33333333] == arr2.tolist() |
| 80 | + assert "8e155301ab5f03fe" == arr2.hex() # native (little) endian |
| 81 | + assert 22222222 == arr2[0] # offset 1 int32 |
| 82 | + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): |
| 83 | + arr2[2] |
| 84 | + arr3 = pm.eval("(arr) => new Int32Array(arr.buffer, 16 /* byteOffset */)")(arr1) # empty Int32Array |
| 85 | + assert 0 == arr3.nbytes |
| 86 | + del arr3 |
| 87 | + |
| 88 | + # test GC |
| 89 | + del arr1 |
| 90 | + gc.collect(), pm.collect() |
| 91 | + gc.collect(), pm.collect() |
| 92 | + # TODO (Tom Tang): the 0th element in the underlying buffer is still accessible after GC, even is not referenced by the JS TypedArray with byteOffset |
| 93 | + del arr2 |
| 94 | + |
| 95 | + # mutation |
| 96 | + mut_arr_original = bytearray(4) |
| 97 | + pm.eval(""" |
| 98 | + (/* @type Uint8Array */ arr) => { |
| 99 | + // 2.25 in float32 little endian |
| 100 | + arr[2] = 0x10 |
| 101 | + arr[3] = 0x40 |
| 102 | + } |
| 103 | + """)(mut_arr_original) |
| 104 | + assert 0x10 == mut_arr_original[2] |
| 105 | + assert 0x40 == mut_arr_original[3] |
| 106 | + # mutation to a different TypedArray accessing the same underlying data block will also change the original buffer |
| 107 | + def do_mutation(mut_arr_js): |
| 108 | + assert 2.25 == mut_arr_js[0] |
| 109 | + mut_arr_js[0] = 225.50048828125 # float32 little endian: 0x 20 80 61 43 |
| 110 | + assert "20806143" == mut_arr_original.hex() |
| 111 | + assert 225.50048828125 == array.array("f", mut_arr_original)[0] |
| 112 | + mut_arr_new = pm.eval(""" |
| 113 | + (/* @type Uint8Array */ arr, do_mutation) => { |
| 114 | + const mut_arr_js = new Float32Array(arr.buffer) |
| 115 | + do_mutation(mut_arr_js) |
| 116 | + return arr |
| 117 | + } |
| 118 | + """)(mut_arr_original, do_mutation) |
| 119 | + assert [0x20, 0x80, 0x61, 0x43] == mut_arr_new.tolist() |
| 120 | + |
| 121 | + # simple 1-D numpy array should just work as well |
| 122 | + numpy_int16_array = numpy.array([0, 1, 2, 3], dtype=numpy.int16) |
| 123 | + assert "0,1,2,3" == pm.eval("(typedArray) => typedArray.toString()")(numpy_int16_array) |
| 124 | + assert 3.0 == pm.eval("(typedArray) => typedArray[3]")(numpy_int16_array) |
| 125 | + assert True == pm.eval("(typedArray) => typedArray instanceof Int16Array")(numpy_int16_array) |
| 126 | + numpy_memoryview = pm.eval("(typedArray) => typedArray")(numpy_int16_array) |
| 127 | + assert 2 == numpy_memoryview[2] |
| 128 | + assert 4 * 2 == numpy_memoryview.nbytes # 4 elements * sizeof(int16_t) |
| 129 | + assert "h" == numpy_memoryview.format # the type code for int16 is 'h', see https://docs.python.org/3.9/library/array.html |
| 130 | + with pytest.raises(IndexError, match="index out of bounds on dimension 1"): |
| 131 | + numpy_memoryview[4] |
| 132 | + |
| 133 | + # can work for empty Python buffer |
| 134 | + def assert_empty_py_buffer(buf, type: str): |
| 135 | + assert 0 == pm.eval("(typedArray) => typedArray.length")(buf) |
| 136 | + assert None == pm.eval("(typedArray) => typedArray[0]")(buf) # `undefined` |
| 137 | + assert True == pm.eval("(typedArray) => typedArray instanceof "+type)(buf) |
| 138 | + assert_empty_py_buffer(bytearray(b''), "Uint8Array") |
| 139 | + assert_empty_py_buffer(numpy.array([], dtype=numpy.uint64), "BigUint64Array") |
| 140 | + assert_empty_py_buffer(array.array('d', []), "Float64Array") |
| 141 | + |
| 142 | + # can work for empty TypedArray |
| 143 | + def assert_empty_typedarray(buf: memoryview, typecode: str): |
| 144 | + assert typecode == buf.format |
| 145 | + assert struct.calcsize(typecode) == buf.itemsize |
| 146 | + assert 0 == buf.nbytes |
| 147 | + assert "" == buf.hex() |
| 148 | + assert b"" == buf.tobytes() |
| 149 | + assert [] == buf.tolist() |
| 150 | + buf.release() |
| 151 | + assert_empty_typedarray(pm.eval("new BigInt64Array()"), "q") |
| 152 | + assert_empty_typedarray(pm.eval("new Float32Array(new ArrayBuffer(4), 4 /*byteOffset*/)"), "f") |
| 153 | + assert_empty_typedarray(pm.eval("(arr)=>arr")( bytearray([]) ), "B") |
| 154 | + assert_empty_typedarray(pm.eval("(arr)=>arr")( numpy.array([], dtype=numpy.uint16) ),"H") |
| 155 | + assert_empty_typedarray(pm.eval("(arr)=>arr")( array.array("d", []) ),"d") |
| 156 | + |
| 157 | + # can work for empty ArrayBuffer |
| 158 | + def assert_empty_arraybuffer(buf): |
| 159 | + assert "B" == buf.format |
| 160 | + assert 1 == buf.itemsize |
| 161 | + assert 0 == buf.nbytes |
| 162 | + assert "" == buf.hex() |
| 163 | + assert b"" == buf.tobytes() |
| 164 | + assert [] == buf.tolist() |
| 165 | + buf.release() |
| 166 | + assert_empty_arraybuffer(pm.eval("new ArrayBuffer()")) |
| 167 | + assert_empty_arraybuffer(pm.eval("new Uint8Array().buffer")) |
| 168 | + assert_empty_arraybuffer(pm.eval("new Float64Array().buffer")) |
| 169 | + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( bytearray([]) )) |
| 170 | + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( pm.eval("(arr)=>arr.buffer")(bytearray()) )) |
| 171 | + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( numpy.array([], dtype=numpy.uint64) )) |
| 172 | + assert_empty_arraybuffer(pm.eval("(arr)=>arr.buffer")( array.array("d", []) )) |
| 173 | + |
| 174 | + # TODO (Tom Tang): shared ArrayBuffer should be disallowed |
| 175 | + # pm.eval("new WebAssembly.Memory({ initial: 1, maximum: 1, shared: true }).buffer") |
| 176 | + |
| 177 | + # TODO (Tom Tang): once a JS ArrayBuffer is transferred to a worker thread, it should be invalidated in Python-land as well |
| 178 | + |
| 179 | + # TODO (Tom Tang): error for detached ArrayBuffer, or should it be considered as empty? |
| 180 | + |
| 181 | + # should error on immutable Python buffers |
| 182 | + # Note: Python `bytes` type must be converted to a (mutable) `bytearray` because there's no such a concept of read-only ArrayBuffer in JS |
| 183 | + with pytest.raises(BufferError, match="Object is not writable."): |
| 184 | + pm.eval("(typedArray) => {}")(b'') |
| 185 | + immutable_numpy_array = numpy.arange(10) |
| 186 | + immutable_numpy_array.setflags(write=False) |
| 187 | + with pytest.raises(ValueError, match="buffer source array is read-only"): |
| 188 | + pm.eval("(typedArray) => {}")(immutable_numpy_array) |
| 189 | + |
| 190 | + # buffer should be in C order (row major) |
| 191 | + fortran_order_arr = numpy.array([[1, 2], [3, 4]], order="F") # 1-D array is always considered C-contiguous because it doesn't matter if it's row or column major in 1-D |
| 192 | + with pytest.raises(ValueError, match="ndarray is not C-contiguous"): |
| 193 | + pm.eval("(typedArray) => {}")(fortran_order_arr) |
| 194 | + |
| 195 | + # disallow multidimensional array |
| 196 | + numpy_2d_array = numpy.array([[1, 2], [3, 4]], order="C") |
| 197 | + with pytest.raises(BufferError, match="multidimensional arrays are not allowed"): |
| 198 | + pm.eval("(typedArray) => {}")(numpy_2d_array) |
0 commit comments