Skip to content

Commit c84efa9

Browse files
committed
test: regression tests for AwkwardForth VM and libawkward fixes
Covers the behaviorally-verifiable fixes: from_json with a nullable-record schema and missing option-type keys, ArrayBuilder clear-then-reuse, ForthMachine64 with output_initial_size=0 / resize factor 1.0, and Nbit reads with N >= 32. Assisted-by: ClaudeCode:claude-fable-5
1 parent 84fe019 commit c84efa9

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# BSD 3-Clause License; see https://github.com/scikit-hep/awkward/blob/main/LICENSE
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import numpy as np
8+
import pytest
9+
10+
import awkward as ak
11+
12+
forth_only = pytest.mark.skipif(
13+
sys.byteorder == "big",
14+
reason="AwkwardForth not yet supported on big-endian systems",
15+
)
16+
17+
18+
def test_from_json_nullable_record_missing_option_key():
19+
# A nullable record (type ["object", "null"]) with a missing option-type
20+
# key used to raise a spurious "JSON schema mismatch" because the
21+
# instruction stack was left unbalanced (switch fall-through in
22+
# nulls_for_optiontype). It should null-fill the missing key, exactly like
23+
# the non-nullable record does.
24+
schema = {
25+
"type": "array",
26+
"items": {
27+
"type": ["object", "null"],
28+
"properties": {
29+
"x": {"type": "integer"},
30+
"y": {"type": ["integer", "null"]},
31+
},
32+
},
33+
}
34+
out = ak.from_json('[{"x": 1}, null, {"x": 2, "y": 3}]', schema=schema)
35+
assert out.tolist() == [{"x": 1, "y": None}, None, {"x": 2, "y": 3}]
36+
37+
38+
def test_from_json_nonnullable_record_missing_option_key():
39+
# The non-nullable counterpart already worked; keep it as a guard.
40+
schema = {
41+
"type": "array",
42+
"items": {
43+
"type": "object",
44+
"properties": {
45+
"x": {"type": "integer"},
46+
"y": {"type": ["integer", "null"]},
47+
},
48+
},
49+
}
50+
out = ak.from_json('[{"x": 1}, {"x": 2, "y": 3}]', schema=schema)
51+
assert out.tolist() == [{"x": 1, "y": None}, {"x": 2, "y": 3}]
52+
53+
54+
def test_arraybuilder_clear_then_reuse():
55+
# RecordBuilder::clear() used to clear keys_/pointers_ while keeping
56+
# contents_, leaving the parallel arrays out of sync (out-of-bounds reads)
57+
# and length_ == -1 (ValueError from __len__). clear() must leave a
58+
# consistent, empty-but-structured state that can be reused.
59+
builder = ak.ArrayBuilder()
60+
with builder.record():
61+
builder.field("x").integer(1)
62+
builder.field("y").real(2.5)
63+
64+
ext = builder._layout
65+
assert len(ext) == 1
66+
67+
ext.clear()
68+
# No ValueError, length resets cleanly, form stays intact.
69+
assert len(ext) == 0
70+
form_after_clear = ext.form()
71+
assert "RecordArray" in form_after_clear
72+
73+
# Reuse after clear must succeed and produce a consistent array.
74+
with builder.record():
75+
builder.field("x").integer(7)
76+
builder.field("y").real(8.5)
77+
assert len(ext) == 1
78+
assert builder.snapshot().tolist() == [{"x": 7, "y": 8.5}]
79+
80+
81+
@forth_only
82+
def test_forthmachine_output_initial_size_zero():
83+
# maybe_resize() used to loop forever when output_initial_size == 0
84+
# (reservation stayed 0). It must grow and run to completion.
85+
vm = ak.forth.ForthMachine64(
86+
"output out int64 5 0 do i out <- stack loop",
87+
output_initial_size=0,
88+
)
89+
vm.run({})
90+
assert vm.output("out").tolist() == [0, 1, 2, 3, 4]
91+
92+
93+
@forth_only
94+
def test_forthmachine_output_resize_factor_one():
95+
# A resize factor of exactly 1.0 also failed to grow geometrically.
96+
vm = ak.forth.ForthMachine64(
97+
"output out int64 5 0 do i out <- stack loop",
98+
output_initial_size=1,
99+
output_resize_factor=1.0,
100+
)
101+
vm.run({})
102+
assert vm.output("out").tolist() == [0, 1, 2, 3, 4]
103+
104+
105+
@forth_only
106+
@pytest.mark.parametrize(
107+
("nbits", "nbytes"),
108+
[(32, 4), (33, 5), (40, 5), (64, 8)],
109+
)
110+
def test_forthmachine_nbit_mask_large_widths(nbits, nbytes):
111+
# mask = (1 << bit_width) - 1 shifted an int literal -> UB/wrong for
112+
# bit_width >= 31. Reading all-ones bytes must yield exactly nbits set.
113+
expected = (1 << nbits) - 1
114+
vm = ak.forth.ForthMachine64(
115+
f"input data output out uint64 1 data #{nbits}bit-> out",
116+
output_initial_size=64,
117+
)
118+
data = np.frombuffer(b"\xff" * nbytes, dtype=np.uint8)
119+
vm.run({"data": data})
120+
(result,) = vm.output("out").tolist()
121+
assert (result & 0xFFFFFFFFFFFFFFFF) == (expected & 0xFFFFFFFFFFFFFFFF)

0 commit comments

Comments
 (0)