Skip to content

Commit cd0185a

Browse files
authored
chore: split pytest files
Merge pull request #66 from Distributive-Network/Xmader/chore/pytest
2 parents 8fca917 + 675cb21 commit cd0185a

File tree

7 files changed

+964
-947
lines changed

7 files changed

+964
-947
lines changed

tests/python/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
import pythonmonkey as pm
3+
import gc
4+
5+
# This is run at the end of each test function
6+
@pytest.fixture(scope="function", autouse=True)
7+
def teardown_function():
8+
"""
9+
Forcing garbage collection (twice) whenever a test function finishes,
10+
to locate GC-related errors
11+
"""
12+
gc.collect(), pm.collect()
13+
gc.collect(), pm.collect()

tests/python/test_bigints.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import pytest
2+
import pythonmonkey as pm
3+
import random
4+
5+
def test_eval_numbers_bigints():
6+
def test_bigint(py_number: int):
7+
js_number = pm.eval(f'{repr(py_number)}n')
8+
assert py_number == js_number
9+
10+
test_bigint(0)
11+
test_bigint(1)
12+
test_bigint(-1)
13+
14+
# CPython would reuse the objects for small ints in range [-5, 256]
15+
# Making sure we don't do any changes on them
16+
def test_cached_int_object(py_number):
17+
# type is still int
18+
assert type(py_number) == int
19+
assert type(py_number) != pm.bigint
20+
test_bigint(py_number)
21+
assert type(py_number) == int
22+
assert type(py_number) != pm.bigint
23+
# the value doesn't change
24+
# TODO (Tom Tang): Find a way to create a NEW int object with the same value, because int literals also reuse the cached int objects
25+
for _ in range(2):
26+
test_cached_int_object(0) # _PyLong_FromByteArray reuses the int 0 object,
27+
# see https://github.com/python/cpython/blob/3.9/Objects/longobject.c#L862
28+
for i in range(10):
29+
test_cached_int_object(random.randint(-5, 256))
30+
31+
test_bigint(18014398509481984) # 2**54
32+
test_bigint(-18014398509481984) # -2**54
33+
test_bigint(18446744073709551615) # 2**64-1
34+
test_bigint(18446744073709551616) # 2**64
35+
test_bigint(-18446744073709551617) # -2**64-1
36+
37+
limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376
38+
# = 2**300
39+
for i in range(10):
40+
py_number = random.randint(-limit, limit)
41+
test_bigint(py_number)
42+
43+
# TODO (Tom Tang): test -0 (negative zero)
44+
# There's no -0 in both Python int and JS BigInt,
45+
# but this could be possible in JS BigInt's internal representation as it uses a sign bit flag.
46+
# On the other hand, Python int uses `ob_size` 0 for 0, >0 for positive values, <0 for negative values
47+
48+
def test_eval_boxed_numbers_bigints():
49+
def test_boxed_bigint(py_number: int):
50+
# `BigInt()` can only be called without `new`
51+
# https://tc39.es/ecma262/#sec-bigint-constructor
52+
js_number = pm.eval(f'new Object({repr(py_number)}n)')
53+
assert py_number == js_number
54+
55+
test_boxed_bigint(0)
56+
test_boxed_bigint(1)
57+
test_boxed_bigint(-1)
58+
59+
limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376
60+
# = 2**300
61+
for i in range(10):
62+
py_number = random.randint(-limit, limit)
63+
test_boxed_bigint(py_number)
64+
65+
def test_eval_functions_bigints():
66+
ident = pm.eval("(a) => { return a }")
67+
add = pm.eval("(a, b) => { return a + b }")
68+
69+
int1 = random.randint(-1000000,1000000)
70+
bigint1 = pm.bigint(int1)
71+
assert int1 == bigint1
72+
73+
# should return pm.bigint
74+
assert type(ident(bigint1)) == pm.bigint
75+
assert ident(bigint1) is not bigint1
76+
# should return float (because JS number is float64)
77+
assert type(ident(int1)) == float
78+
assert ident(int1) == ident(bigint1)
79+
80+
# should raise exception on ints > (2^53-1), or < -(2^53-1)
81+
def not_raise(num):
82+
ident(num)
83+
def should_raise(num):
84+
with pytest.raises(OverflowError, match="Use pythonmonkey.bigint instead"):
85+
ident(num)
86+
not_raise(9007199254740991) # 2**53-1, 0x433_FFFFFFFFFFFFF in float64
87+
should_raise(9007199254740992) # 2**53, 0x434_0000000000000 in float64
88+
should_raise(9007199254740993) # 2**53+1, NOT 0x434_0000000000001 (2**53+2)
89+
not_raise(-9007199254740991) # -(2**53-1)
90+
should_raise(-9007199254740992) # -(2**53)
91+
should_raise(-9007199254740993) # -(2**53+1)
92+
93+
# should also raise exception on large integers (>=2**53) that can be exactly represented by a float64
94+
# in our current implementation
95+
should_raise(9007199254740994) # 2**53+2, 0x434_0000000000001 in float64
96+
should_raise(2**61+2**9) # 0x43C_0000000000001 in float64
97+
98+
# should raise "Use pythonmonkey.bigint" instead of `PyLong_AsLongLong`'s "OverflowError: int too big to convert" on ints larger than 64bits
99+
should_raise(2**65)
100+
should_raise(-2**65)
101+
not_raise(pm.bigint(2**65))
102+
not_raise(pm.bigint(-2**65))
103+
104+
# should raise JS error when mixing a BigInt with a number in arithmetic operations
105+
def should_js_error(a, b):
106+
with pytest.raises(pm.SpiderMonkeyError, match="can't convert BigInt to number"):
107+
add(a, b)
108+
should_js_error(pm.bigint(0), 0)
109+
should_js_error(pm.bigint(1), 2)
110+
should_js_error(3, pm.bigint(4))
111+
should_js_error(-5, pm.bigint(6))
112+
113+
assert add(pm.bigint(0), pm.bigint(0)) == 0
114+
assert add(pm.bigint(1), pm.bigint(0)) == 1
115+
assert add(pm.bigint(1), pm.bigint(2)) == 3
116+
assert add(pm.bigint(-1), pm.bigint(1)) == 0
117+
assert add(pm.bigint(2**60), pm.bigint(0)) == 1152921504606846976
118+
assert add(pm.bigint(2**65), pm.bigint(-2**65-1)) == -1
119+
120+
# fuzztest
121+
limit = 2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376 # 2**300
122+
for i in range(10):
123+
num1 = random.randint(-limit, limit)
124+
num2 = random.randint(-limit, limit)
125+
assert add(pm.bigint(num1), pm.bigint(num2)) == num1+num2
126+
127+
def test_eval_functions_bigint_factorial():
128+
factorial = pm.eval("(num) => {let r = 1n; for(let i = 0n; i<num; i++){r *= num - i}; return r}")
129+
assert factorial(pm.bigint(1)) == 1
130+
assert factorial(pm.bigint(18)) == 6402373705728000
131+
assert factorial(pm.bigint(19)) == 121645100408832000 # > Number.MAX_SAFE_INTEGER
132+
assert factorial(pm.bigint(21)) == 51090942171709440000 # > 64 bit int
133+
assert factorial(pm.bigint(35)) == 10333147966386144929666651337523200000000 # > 128 bit
134+
135+
def test_eval_functions_bigint_crc32():
136+
crc_table_at = pm.eval("""
137+
// translated from https://rosettacode.org/wiki/CRC-32#Python
138+
const crc_table = (function create_table() {
139+
const a = []
140+
for (let i = 0n; i < 256n; i++) {
141+
let k = i
142+
for (let j = 0n; j < 8n; j++) {
143+
// must use bigint here as js number is trimmed to int32 in bitwise operations
144+
if (k & 1n) k ^= 0x1db710640n
145+
k >>= 1n
146+
}
147+
a.push(k)
148+
}
149+
return a
150+
})();
151+
(n) => crc_table[n]
152+
""")
153+
assert type(crc_table_at(1)) == pm.bigint
154+
assert crc_table_at(0) == 0
155+
assert crc_table_at(1) == 1996959894
156+
assert crc_table_at(255) == 755167117 # last item
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)

0 commit comments

Comments
 (0)