Skip to content

Commit fef2f5a

Browse files
committed
chore: split pytest files
1 parent 269c479 commit fef2f5a

File tree

4 files changed

+535
-528
lines changed

4 files changed

+535
-528
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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)

tests/python/test_dicts_lists.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import pythonmonkey as pm
2+
3+
def test_eval_objects():
4+
pyObj = pm.eval("Object({a:1.0})")
5+
assert pyObj == {'a':1.0}
6+
7+
def test_eval_objects_subobjects():
8+
pyObj = pm.eval("Object({a:1.0, b:{c:2.0}})")
9+
10+
assert pyObj['a'] == 1.0
11+
assert pyObj['b'] == {'c': 2.0}
12+
assert pyObj['b']['c'] == 2.0
13+
14+
def test_eval_objects_cycle():
15+
pyObj = pm.eval("Object({a:1.0, b:2.0, recursive: function() { this.recursive = this; return this; }}.recursive())")
16+
17+
assert pyObj['a'] == 1.0
18+
assert pyObj['b'] == 2.0
19+
assert pyObj['recursive'] == pyObj
20+
21+
def test_eval_objects_proxy_get():
22+
f = pm.eval("(obj) => { return obj.a}")
23+
assert f({'a':42.0}) == 42.0
24+
25+
def test_eval_objects_proxy_set():
26+
f = pm.eval("(obj) => { obj.a = 42.0; return;}")
27+
pyObj = {}
28+
f(pyObj)
29+
assert pyObj['a'] == 42.0
30+
31+
def test_eval_objects_proxy_keys():
32+
f = pm.eval("(obj) => { return Object.keys(obj)[0]}")
33+
assert f({'a':42.0}) == 'a'
34+
35+
def test_eval_objects_proxy_delete():
36+
f = pm.eval("(obj) => { delete obj.a }")
37+
pyObj = {'a': 42.0}
38+
f(pyObj)
39+
assert 'a' not in pyObj
40+
41+
def test_eval_objects_proxy_has():
42+
f = pm.eval("(obj) => { return 'a' in obj }")
43+
pyObj = {'a': 42.0}
44+
assert(f(pyObj))
45+
46+
def test_eval_objects_proxy_not_extensible():
47+
assert False == pm.eval("(o) => Object.isExtensible(o)")({})
48+
assert False == pm.eval("(o) => Object.isExtensible(o)")({ "abc": 1 })
49+
assert True == pm.eval("(o) => Object.preventExtensions(o) === o")({})
50+
assert True == pm.eval("(o) => Object.preventExtensions(o) === o")({ "abc": 1 })
51+
52+
def test_eval_objects_proxy_proto():
53+
assert pm.null == pm.eval("(o) => Object.getPrototypeOf(o)")({})
54+
assert pm.null == pm.eval("(o) => Object.getPrototypeOf(o)")({ "abc": 1 })
55+
56+
def test_eval_objects_jsproxy_get():
57+
proxy = pm.eval("({a: 1})")
58+
assert 1.0 == proxy['a']
59+
assert 1.0 == proxy.a
60+
61+
def test_eval_objects_jsproxy_set():
62+
proxy = pm.eval("({a: 1})")
63+
proxy.a = 2.0
64+
assert 2.0 == proxy['a']
65+
proxy['a'] = 3.0
66+
assert 3.0 == proxy.a
67+
proxy.b = 1.0
68+
assert 1.0 == proxy['b']
69+
proxy['b'] = 2.0
70+
assert 2.0 == proxy.b
71+
72+
def test_eval_objects_jsproxy_length():
73+
proxy = pm.eval("({a: 1, b:2})")
74+
assert 2 == len(proxy)
75+
76+
def test_eval_objects_jsproxy_delete():
77+
proxy = pm.eval("({a: 1})")
78+
del proxy.a
79+
assert None == proxy.a
80+
assert None == proxy['a']
81+
82+
def test_eval_objects_jsproxy_compare():
83+
proxy = pm.eval("({a: 1, b:2})")
84+
assert proxy == {'a': 1.0, 'b': 2.0}

0 commit comments

Comments
 (0)