Repository navigation
Expand file tree
/
Copy pathtest_payload_codec.py
More file actions
2263 lines (1826 loc) · 86.3 KB
/
Copy pathtest_payload_codec.py
File metadata and controls
2263 lines (1826 loc) · 86.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
"""Tests for payload_codec (host stdlib / child NumPy wire format).
Sections: policy threshold, host pack/unpack, child pack/unpack, round-trips, NaN/missing,
realistic Calc-shaped grids (rectangular 2D). Uneven row lengths raise on split_grid;
the small list path returns them unchanged.
"""
from __future__ import annotations
import array
import ast
import math
import sys
from decimal import Decimal
from pathlib import Path
from unittest.mock import patch
import pytest
from plugin.scripting import payload_codec
from plugin.scripting.payload_codec import (
BINARY_MIN_CELLS,
PAYLOAD_CALC_RANGE,
PAYLOAD_DATAFRAME,
PAYLOAD_MULTI_DATA,
PAYLOAD_SPLIT_GRID,
binary_envelope_skip_reason,
child_pack_result,
child_unpack_data,
child_unpack_split_grid,
describe_wire_value,
host_pack_data,
host_pack_multi_data,
host_unpack_data,
host_unpack_split_grid,
is_dataframe_payload,
is_multi_data,
is_numeric_coercible,
is_numeric_grid,
is_split_grid,
should_use_binary_envelope,
wire_cell_count,
)
from tests.scripting.payload_codec_test_support import (
MIXED_LABEL_GRID,
MIXED_WITH_ZIP,
NUMERIC_4X4,
NUMERIC_AT_THRESHOLD,
NUMERIC_BELOW_THRESHOLD,
pickle5_roundtrip,
rect_shape_for_cell_count,
)
from tests.scripting.serialization_ab_support import cython_accelerator_context
def test_host_module_does_not_import_numpy_at_module_level():
"""Host path must stay NumPy-free at import time (ABI / LO embedded Python)."""
src = Path(payload_codec.__file__).read_text(encoding="utf-8")
tree = ast.parse(src)
for node in tree.body:
if isinstance(node, ast.Import):
for alias in node.names:
assert not alias.name.startswith("numpy"), alias.name
elif isinstance(node, ast.ImportFrom) and node.module:
assert not node.module.startswith("numpy"), node.module
def test_flatten_grid_return_parameterizes_array() -> None:
"""Bare array.array leftovers trip reportMissingTypeArgument."""
from plugin.scripting.payload_codec import _flatten_grid_to_components
assert _flatten_grid_to_components.__annotations__["return"] == "tuple[array.array[float], dict[int, str], list[str], list[int]]"
@pytest.mark.parametrize(
("ncells", "force", "expected"),
[
(BINARY_MIN_CELLS - 1, "auto", False),
(BINARY_MIN_CELLS, "auto", True),
(BINARY_MIN_CELLS - 1, "never", False),
(BINARY_MIN_CELLS - 1, "always", True),
],
)
def test_should_use_binary_envelope_boundary(ncells: int, force: str, expected: bool) -> None:
"""Default policy: below BINARY_MIN_CELLS uses nested lists; at/above uses split_grid when force=auto."""
rows, cols = rect_shape_for_cell_count(ncells)
shape = (rows, cols)
assert should_use_binary_envelope(shape, force=force) is expected
def test_should_use_binary_envelope_1d_boundary() -> None:
assert should_use_binary_envelope((BINARY_MIN_CELLS - 1,), force="auto") is False
assert should_use_binary_envelope((BINARY_MIN_CELLS,), force="auto") is True
def test_binary_envelope_skip_reason_below_threshold() -> None:
"""Policy helper explains why a grid below BINARY_MIN_CELLS skips split_grid."""
n = BINARY_MIN_CELLS - 1
rows, cols = rect_shape_for_cell_count(n)
reason = binary_envelope_skip_reason((rows, cols), force="auto")
assert reason is not None
assert str(BINARY_MIN_CELLS) in reason
def test_host_pack_auto_uses_split_grid_for_large_rect():
"""Auto policy uses split_grid when cell count >= BINARY_MIN_CELLS."""
grid = NUMERIC_AT_THRESHOLD
wire = host_pack_data(grid, force="auto")
assert isinstance(wire, dict)
assert wire["__wa_payload__"] == PAYLOAD_SPLIT_GRID
rows, cols = rect_shape_for_cell_count(BINARY_MIN_CELLS)
assert wire["shape"] == [rows, cols]
def test_host_pack_auto_uses_list_for_3x3():
grid = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
wire = host_pack_data(grid, force="auto")
assert isinstance(wire, list)
assert wire[0][0] == 1.0
def test_round_trip_host_split_grid_child_ndarray():
np = pytest.importorskip("numpy")
grid = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]
wire = host_pack_data(grid, force="always")
assert wire["__wa_payload__"] == PAYLOAD_SPLIT_GRID
arr = child_unpack_data(wire)
assert isinstance(arr, np.ndarray)
assert not isinstance(arr, list) # perf sentinel: pure-numeric split_grid must not regress to list materialization
assert arr.shape == (4, 2)
assert arr[0, 0] == pytest.approx(1.0)
assert arr[3, 1] == pytest.approx(8.0)
def test_round_trip_child_split_grid_host_list():
np = pytest.importorskip("numpy")
arr = np.arange(12, dtype=np.float64).reshape(3, 4)
wire = child_pack_result(arr, force="always")
assert wire["__wa_payload__"] == PAYLOAD_SPLIT_GRID
back = host_unpack_data(wire, as_nested_list=True)
assert len(back) == 3
assert len(back[0]) == 4
assert back[0][0] == pytest.approx(0.0)
assert back[2][3] == pytest.approx(11.0)
def test_column_kinds_from_cell_types():
from plugin.scripting.payload_codec import column_kinds_for_grid
assert column_kinds_for_grid([[100, 541], [101, 547]]) == ["int", "int"]
assert column_kinds_for_grid([[100, 1.5], [101, 2.5]]) == ["int", "float"]
assert column_kinds_for_grid([[1.5, 2.0]]) == ["float", "float"]
assert column_kinds_for_grid([[1, None]]) == ["int", "float"]
assert column_kinds_for_grid([[1, "x"]]) == ["int", "float"]
assert column_kinds_for_grid([["x"], ["y"]]) == ["float"]
def test_uniform_unpack_uses_full_column_kinds_on_wire():
"""Fast decode path must not require a shortened wire tag; column_kinds stays per-column."""
from plugin.scripting.payload_codec import envelope_uniform_column_kind
grid = [[100, 541], [101, 547], [102, 557], [103, 563], [104, 569], [105, 571], [106, 577]]
wire = host_pack_data(grid, force="always")
assert wire["column_kinds"] == ["int", "int"]
assert "uniform_column_kind" not in wire
assert envelope_uniform_column_kind(wire, ncols=2) == "int"
def test_host_unpack_restores_integer_grid():
grid = [[100, 541], [101, 547], [102, 557], [103, 563], [104, 569], [105, 571], [106, 577]]
wire = host_pack_data(grid, force="always")
assert wire["dtype"] == "float64"
assert wire["column_kinds"] == ["int", "int"]
back = host_unpack_data(wire, as_nested_list=True)
assert back == grid
assert all(isinstance(cell, int) for row in back for cell in row)
def test_host_unpack_mixed_int_float_columns():
grid = [[100, 1.5], [101, 2.5], [102, 3.5], [103, 4.5], [104, 5.5]]
wire = host_pack_data(grid, force="always")
assert wire["column_kinds"] == ["int", "float"]
back = host_unpack_data(wire, as_nested_list=True)
assert back[0] == [100, 1.5]
assert isinstance(back[0][0], int)
assert isinstance(back[0][1], float)
assert back[1][0] == 101
def test_child_pack_integer_ndarray_sets_column_kinds():
np = pytest.importorskip("numpy")
from plugin.scripting.payload_codec import child_pack_result
wire = child_pack_result(np.arange(12, dtype=np.int64).reshape(3, 4), force="always")
assert wire["dtype"] == "float64"
assert wire["column_kinds"] == ["int", "int", "int", "int"]
back = host_unpack_data(wire, as_nested_list=True)
assert back[0][0] == 0
assert isinstance(back[0][0], int)
def test_child_pack_bool_ndarray_sets_column_kinds():
np = pytest.importorskip("numpy")
from plugin.scripting.payload_codec import child_pack_split_grid
wire = child_pack_split_grid(np.array([[True, False], [False, True]]))
assert wire["column_kinds"] == ["bool", "bool"]
back = host_unpack_data(wire, as_nested_list=True)
assert back == [[True, False], [False, True]]
def test_split_grid_bool_beside_float_stays_float_in_child():
"""A bool column next to floats stays float64 in the child.
Host unpack restores True. =PY() sees 1.0. == treats True == 1.0, so
this checks type identity.
"""
np = pytest.importorskip("numpy")
grid = [[1.5, True], [2.5, False]]
wire = host_pack_data(grid, force="always")
assert wire["column_kinds"] == ["float", "bool"]
child = child_unpack_data(wire)
assert isinstance(child, np.ndarray)
assert child.dtype == np.float64
assert type(child[0, 1].item()) is float
assert child[0, 1].item() == 1.0
assert type(child[1, 1].item()) is float
assert child[1, 1].item() == 0.0
host = host_unpack_data(wire)
assert host[0][1] is True
assert host[1][1] is False
assert type(host[0][0]) is float
def test_decode_split_grid_buffer_rejects_non_bytes() -> None:
"""bytearray and memoryview are TypeError, including when deal is stripped.
Pack and b64decode hand this helper bytes. Do not coerce a legacy buffer.
"""
payload = array.array("d", [1.0]).tobytes()
base = {
"__wa_payload__": PAYLOAD_SPLIT_GRID,
"dtype": "float64",
"shape": [1],
"strings": {},
}
for raw in (bytearray(payload), memoryview(payload)):
envelope = {**base, "buffer": raw}
with pytest.raises(TypeError, match="bytes-like"):
payload_codec._decode_split_grid_buffer(envelope, 1)
assert payload_codec._decode_split_grid_buffer({**base, "buffer": payload}, 1) == payload
def test_split_grid_mixed_bool_int_column_becomes_int():
"""One column cannot be both bool and int. split_grid promotes to int.
force=never keeps True. The A/B oracle uses ==, and True == 1, so this
checks type and identity.
"""
np = pytest.importorskip("numpy")
grid = [[True], [1]]
wire = host_pack_data(grid, force="always")
assert wire["column_kinds"] == ["int"]
unpacked = host_unpack_split_grid(wire)
assert unpacked == [[1], [1]]
assert type(unpacked[0][0]) is int
assert type(unpacked[1][0]) is int
listed = host_pack_data(grid, force="never")
assert listed[0][0] is True
assert type(listed[1][0]) is int
child = child_unpack_split_grid(wire)
assert child.dtype == np.int64
assert type(child[0, 0].item()) is int
def test_child_unpack_float_frombuffer_is_readonly():
"""Uniform float and mixed numeric kinds are the read-only frombuffer view.
Uniform int and bool allocate via astype and ==, so those arrays are
writable. CalcRange.to_numpy() is the writable copy for =PY().
"""
np = pytest.importorskip("numpy")
float_arr = child_unpack_split_grid(host_pack_data([[1.5, 2.5]], force="always"))
assert float_arr.dtype == np.float64
assert float_arr.flags.writeable is False
with pytest.raises(ValueError, match="read-only"):
float_arr[0, 0] = 9.0
mixed = child_unpack_split_grid(host_pack_data([[1, 1.5]], force="always"))
assert mixed.dtype == np.float64
assert mixed.flags.writeable is False
int_arr = child_unpack_split_grid(host_pack_data([[1, 2]], force="always"))
assert int_arr.flags.writeable is True
bool_arr = child_unpack_split_grid(host_pack_data([[True, False]], force="always"))
assert bool_arr.dtype == np.bool_
assert bool_arr.flags.writeable is True
def test_none_becomes_nan_in_split_grid():
pytest.importorskip("numpy")
wire = host_pack_data([[1.0, None, 3.0]], force="always")
arr = child_unpack_data(wire)
assert arr.shape == (1, 3)
assert math.isnan(float(arr[0, 1]))
def test_child_pack_plain_python_without_numpy() -> None:
"""Plain Python results serialize when NumPy is unavailable.
``sys.modules['numpy'] = None`` is how a later ``import numpy`` fails
(``ModuleNotFoundError``), without intercepting every other import.
"""
result = {"changes": [["pays", "paid"]]}
with patch.dict(sys.modules, {"numpy": None}):
assert child_pack_result(result, force="auto") == result
def test_child_unpack_plain_python_without_numpy() -> None:
"""Inbound lists and dicts materialize when NumPy is unavailable.
A numeric list stays a list: there is no ndarray to build. ``split_grid``
still needs NumPy and is not covered here.
"""
text = [["pays", "paid"]]
numbers = [1.0, 2.0]
with patch.dict(sys.modules, {"numpy": None}):
assert child_unpack_data(text) == text
assert child_unpack_data({"changes": text}) == {"changes": text}
assert child_unpack_data(numbers) == numbers
def test_scalar_egress_stays_json():
wire = child_pack_result(42.5, force="auto")
assert wire == 42.5
def test_is_numeric_grid_rejects_text():
assert is_numeric_grid([1.0, "hello"]) is False
assert is_numeric_grid([[1.0, 2.0], [3.0, 4.0]]) is True
def test_describe_wire_value_split_grid():
wire = host_pack_data([[1.0] * 4 for _ in range(4)], force="always")
desc = describe_wire_value(wire)
assert "split_grid" in desc
assert "shape=[4, 4]" in desc
def test_wire_cell_count_split_grid():
wire = host_pack_data([[1.0] * 4 for _ in range(4)], force="always")
assert wire_cell_count(wire) == 16
def test_child_list_path_array():
pytest.importorskip("numpy")
wire = host_pack_data([1.0, 2.0, 3.0], force="never")
arr = child_unpack_data(wire)
assert list(arr) == pytest.approx([1.0, 2.0, 3.0])
def test_host_pack_split_grid_mixed():
"""Verify that a 2D mixed grid is packed using Split-Grid serialization."""
grid = [
[1.0, "apple", 10.0],
[2.0, "banana", 20.0],
[3.0, "cherry", 30.0],
[4.0, "date", 40.0]
]
# Use force="always" to trigger it regardless of threshold
wire = host_pack_data(grid, force="always")
assert isinstance(wire, dict)
assert wire["__wa_payload__"] == payload_codec.PAYLOAD_SPLIT_GRID
assert wire["shape"] == [4, 3]
assert "strings" in wire
assert wire["strings"] == {
1: "apple",
4: "banana",
7: "cherry",
10: "date",
}
def test_round_trip_split_grid():
"""Verify that split_grid payload round-trips correctly and reconstructs exact values."""
pytest.importorskip("numpy")
grid = [
[1.5, "apple", 10.1],
[2.5, "banana", 20.2],
[3.5, "cherry", None],
[4.5, "", 40.4]
]
wire = host_pack_data(grid, force="always")
reconstructed = child_unpack_data(wire)
assert isinstance(reconstructed, list)
assert len(reconstructed) == 4
assert reconstructed[0] == [1.5, "apple", 10.1]
assert reconstructed[1] == [2.5, "banana", 20.2]
# None/empty cells should round-trip correctly
assert reconstructed[2] == [3.5, "cherry", None]
assert reconstructed[3] == [4.5, "", 40.4]
def test_split_grid_non_2d_fallback():
"""Verify that grids/lists fallback correctly when force="never"."""
# 1D mixed grid fallback
grid_1d = [1.0, "apple", 3.0]
wire_1d = host_pack_data(grid_1d, force="never")
assert isinstance(wire_1d, list)
assert wire_1d == [1.0, "apple", 3.0]
# 2D mixed grid but with force="never"
grid_2d = [
[1.0, "apple"],
[2.0, "banana"]
]
wire_2d = host_pack_data(grid_2d, force="never")
assert isinstance(wire_2d, list)
assert wire_2d == [[1.0, "apple"], [2.0, "banana"]]
def test_round_trip_split_grid_1d():
"""Verify that both numeric and mixed 1D flat lists round-trip flawlessly under split_grid."""
np = pytest.importorskip("numpy")
# Numeric 1D flat list
grid_num_1d = [1.5, 2.5, 3.5, 4.5]
wire_num = host_pack_data(grid_num_1d, force="always")
assert isinstance(wire_num, dict)
assert wire_num["__wa_payload__"] == PAYLOAD_SPLIT_GRID
assert wire_num["shape"] == [4]
# Unpack in child -> should be purely numeric ndarray
child_unpacked_num = child_unpack_data(wire_num)
assert isinstance(child_unpacked_num, np.ndarray)
assert child_unpacked_num.shape == (4,)
assert list(child_unpacked_num) == pytest.approx(grid_num_1d)
# Pack result in child -> should pack 1D array as split_grid
wire_child_num = child_pack_result(child_unpacked_num, force="always")
assert wire_child_num["__wa_payload__"] == PAYLOAD_SPLIT_GRID
assert wire_child_num["shape"] == [4]
# Unpack on host -> should return a flat list
host_unpacked_num = host_unpack_data(wire_child_num, as_nested_list=True)
assert isinstance(host_unpacked_num, list)
assert host_unpacked_num == pytest.approx(grid_num_1d)
# Mixed 1D flat list
grid_mixed_1d = [1.5, "banana", None, 4.5]
wire_mixed = host_pack_data(grid_mixed_1d, force="always")
assert isinstance(wire_mixed, dict)
assert wire_mixed["__wa_payload__"] == PAYLOAD_SPLIT_GRID
assert wire_mixed["shape"] == [4]
assert wire_mixed["strings"] == {1: "banana"}
# Unpack in child -> reconstructed mixed list
child_unpacked_mixed = child_unpack_data(wire_mixed)
assert isinstance(child_unpacked_mixed, list)
assert child_unpacked_mixed == [1.5, "banana", None, 4.5]
# Pack result in child -> pack 1D mixed list
wire_child_mixed = child_pack_result(child_unpacked_mixed, force="always")
assert wire_child_mixed["__wa_payload__"] == PAYLOAD_SPLIT_GRID
assert wire_child_mixed["shape"] == [4]
assert wire_child_mixed["strings"] == {1: "banana"}
# Unpack on host -> flat list.
# A Python None in the list (hole) was packed via split_grid (nan in buffer, no strings entry).
# With the egress policy, host unpack now preserves it as nan (Calc will show error for that slot).
import math
host_unpacked_mixed = host_unpack_data(wire_child_mixed, as_nested_list=True)
assert host_unpacked_mixed[0] == 1.5
assert host_unpacked_mixed[1] == "banana"
assert math.isnan(host_unpacked_mixed[2])
assert host_unpacked_mixed[3] == 4.5
def test_child_unpack_single_entry_auto_scalar_and_integer_coercion():
"""Single-cell floats stay floats. A longer float64 column already did; 1×1 used to become int."""
np = pytest.importorskip("numpy")
# 1. 1-element numeric list representing a whole-number float
wire_int_float = [100000.0]
unpacked_int_float = child_unpack_data(wire_int_float)
assert isinstance(unpacked_int_float, float)
assert unpacked_int_float == 100000.0
# 2. 1-element numeric list representing a real float
wire_real_float = [3.14]
unpacked_real_float = child_unpack_data(wire_real_float)
assert isinstance(unpacked_real_float, float)
assert unpacked_real_float == pytest.approx(3.14)
# 3. 1-element string list
wire_str = ["hello"]
unpacked_str = child_unpack_data(wire_str)
assert isinstance(unpacked_str, str)
assert unpacked_str == "hello"
# 4. 1-element boolean list
wire_bool = [True]
unpacked_bool = child_unpack_data(wire_bool)
assert isinstance(unpacked_bool, bool)
assert unpacked_bool is True
# 5. 1-element numpy array representing a whole-number float
arr_int_float = np.array([100000.0])
unpacked_arr_int_float = child_unpack_data(arr_int_float)
assert isinstance(unpacked_arr_int_float, float)
assert unpacked_arr_int_float == 100000.0
# 6. Multi-element list or 2D list should NOT be unpacked to scalar
assert isinstance(child_unpack_data([100000.0, 200000.0]), np.ndarray)
assert child_unpack_data([[100000.0]]) == [[100000.0]] # 2D list preserved
def test_iter_split_grid_cells_row_major_order() -> None:
"""Row-major (col_idx, flat_idx, val) order for 2D and 1D split-grid flatten iterators."""
from plugin.scripting.payload_codec import _iter_split_grid_cells
grid_2d = [[10, 11, 12], [20, 21, 22]]
assert list(_iter_split_grid_cells(grid_2d, is_2d=True)) == [
(0, 0, 10),
(1, 1, 11),
(2, 2, 12),
(0, 3, 20),
(1, 4, 21),
(2, 5, 22),
]
grid_1d = [10, 11, 12]
assert list(_iter_split_grid_cells(grid_1d, is_2d=False)) == [
(0, 0, 10),
(0, 1, 11),
(0, 2, 12),
]
def test_uneven_row_lengths_rejected_on_split_grid_only() -> None:
"""Split_grid rejects a jagged grid. The small list path returns it unchanged.
=PY pre-normalizes with ensure_rectangular_2d. child_pack treats a
non-rectangular result as a list of rows, not one grid.
"""
jagged = [[1, 2], [3]]
assert host_pack_data(jagged, force="never") == jagged
assert host_pack_data(jagged) == jagged
with pytest.raises(ValueError, match="Uneven row lengths"):
host_pack_data(jagged, force="always")
def test_rectangular_tuple_rows_egress_as_lists() -> None:
"""A rectangular tuple-of-tuples becomes a list-of-lists on the list path.
host_unpack_data would restore tuples that are still tuples on the wire.
This egress path does not send them. Split_grid cannot keep row types.
"""
packed = child_pack_result(((1, 2), (3, 4)), force="never")
assert packed == [[1, 2], [3, 4]]
assert type(packed) is list
assert all(type(row) is list for row in packed)
assert host_unpack_data(packed) == packed
restored = host_unpack_data(((1, 2), (3, 4)))
assert type(restored) is tuple
assert type(restored[0]) is tuple
def test_non_sequence_row_raises_valueerror() -> None:
"""A scalar or string row is ValueError, not TypeError from len().
Deal's pre rejects the grid before the body. The release build strips
deal, and that path used to raise TypeError.
"""
from tests.harness.strip_bundle import expect_pre_or_body
with pytest.raises(ValueError, match="not a list or tuple"):
payload_codec._split_grid_row_width(3)
with pytest.raises(ValueError, match="not a list or tuple"):
payload_codec._validate_rectangular_grid([[1, 2], 3])
with pytest.raises(ValueError, match="not a list or tuple"):
payload_codec._validate_rectangular_grid([[1, 2], "ab"])
for grid in ([[1, 2], 3], [[1, 2], "ab"]):
expect_pre_or_body(lambda grid=grid: host_pack_data(grid), body_exc=ValueError)
expect_pre_or_body(
lambda grid=grid: host_pack_data(grid, force="always"), body_exc=ValueError
)
def test_child_unpack_non_sequence_row_raises_valueerror() -> None:
"""[[1, 2], 3] is ValueError, not TypeError from list(row) or len(row).
_child_unpack_single_data and wire_cell_count have no grid pre, so deal
did not hide the TypeError. is_numeric_grid's pre rejects the grid; the
stripped body must still raise ValueError.
"""
from tests.harness.strip_bundle import expect_pre_or_body
for grid in ([[1, 2], 3], [[1, 2], "ab"]):
with pytest.raises(ValueError, match="not a list or tuple") as caught:
child_unpack_data(grid)
assert type(caught.value) is ValueError
with pytest.raises(ValueError, match="not a list or tuple") as counted:
wire_cell_count(grid)
assert type(counted.value) is ValueError
expect_pre_or_body(lambda grid=grid: is_numeric_grid(grid), body_exc=ValueError)
def test_split_grid_bad_b64_raises_valueerror() -> None:
"""A bad legacy b64 is ValueError, not binascii.Error or UnicodeEncodeError.
What was wrong: both subclass ValueError, and deal.raises matches exact
types, so the failure became RaisesContractError (AssertionError).
"""
import base64
raw = array.array("d", [1.5]).tobytes()
good = base64.b64encode(raw).decode("ascii")
envelope = {
"__wa_payload__": PAYLOAD_SPLIT_GRID,
"shape": [1],
"dtype": "float64",
"column_kinds": ["float"],
"strings": {},
}
good_env = {**envelope, "b64": good}
assert host_unpack_split_grid(good_env) == [1.5]
pytest.importorskip("numpy")
child = child_unpack_split_grid(good_env)
assert float(child[0]) == pytest.approx(1.5)
junk = good[:4] + "!" + good[4:]
for bad in ("!!!!", "abc", junk, "café"):
bad_env = {**envelope, "b64": bad}
for unpack in (host_unpack_split_grid, child_unpack_split_grid):
with pytest.raises(ValueError, match="not valid base64") as caught:
unpack(bad_env)
assert type(caught.value) is ValueError
def test_column_kinds_for_grid_jagged_raises() -> None:
"""The kinds helper used to return [] and hide the flatten ValueError."""
with pytest.raises(ValueError, match="Uneven row lengths"):
payload_codec.column_kinds_for_grid([[1, 2], [3]])
def test_envelope_detectors_reject_bool_shape_dims() -> None:
"""bool is an int subclass. True used to pass as a shape extent."""
from plugin.scripting.payload_codec import is_calc_range_payload
assert is_split_grid(
{
"__wa_payload__": PAYLOAD_SPLIT_GRID,
"shape": [True, 1],
"buffer": b"\x00" * 8,
}
) is False
assert is_calc_range_payload(
{
"__wa_payload__": PAYLOAD_CALC_RANGE,
"shape": [1, False],
"data": [[1.0]],
}
) is False
assert is_split_grid(
{
"__wa_payload__": PAYLOAD_SPLIT_GRID,
"shape": [0, 0],
"buffer": b"",
}
) is True
# --- NaN, empty cells, and inf (realistic Calc / NumPy paths) ---
def test_none_cell_pack_produces_nan_in_buffer() -> None:
"""Calc empty cell (None) encodes as NaN in the split_grid float64 buffer."""
grid = [[1.0, None, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]
wire = host_pack_data(grid, force="always")
assert wire["__wa_payload__"] == PAYLOAD_SPLIT_GRID
import array
buf = array.array("d")
buf.frombytes(wire["buffer"])
assert math.isnan(buf[1])
def test_none_numeric_ingress_child_gets_np_nan() -> None:
"""Numeric-only ingress: empty Calc cells become np.nan in child ndarray, not Python None."""
np = pytest.importorskip("numpy")
grid = [[1.0, None, 3.0, 4.0], [5.0, 6.0, None, 8.0], [9.0, 10.0, 11.0, 12.0]]
arr = child_unpack_data(host_pack_data(grid, force="always"))
assert isinstance(arr, np.ndarray)
assert not isinstance(arr, list) # perf sentinel: pure-numeric split_grid fast path must return ndarray (not list-of-lists from tolist)
assert np.isnan(arr[0, 1])
assert arr[0, 0] == pytest.approx(1.0)
def test_none_mixed_ingress_child_gets_python_none() -> None:
"""Mixed grid ingress: empty cells become None in the nested list (not np.nan)."""
pytest.importorskip("numpy")
grid = [[1.0, None, "label"], [2.0, 3.0, "x"]] * 2 # 12 cells, rectangular
out = child_unpack_data(host_pack_data(grid, force="always"))
assert isinstance(out, list)
assert out[0][1] is None
def test_nan_egress_child_pack_host_unpack() -> None:
"""NumPy result with np.nan: host unpack preserves NaN (it becomes a Calc error on =PYTHON() egress)."""
np = pytest.importorskip("numpy")
import math
wire = child_pack_result(np.array([1.0, np.nan, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]), force="always")
back = host_unpack_data(wire, as_nested_list=True)
assert back[0] == pytest.approx(1.0)
assert math.isnan(back[1])
def test_none_host_egress_round_trip() -> None:
"""Rectangular numeric grid with holes: host pack -> child ndarray (nan) -> host list preserves nan (Calc will show error).
We no longer coerce buffer NaN back to Python None on host unpack. A Calc blank that flows through
a pure-numeric range becomes nan on egress and surfaces as a Calc error (by design).
"""
np = pytest.importorskip("numpy")
import math
grid = [[1.0, None, 3.0, 4.0], [5.0, 6.0, None, 8.0], [9.0, 10.0, 11.0, 12.0]]
wire = host_pack_data(grid, force="always")
arr = child_unpack_data(wire)
assert isinstance(arr, np.ndarray)
back = host_unpack_data(wire, as_nested_list=True)
assert math.isnan(back[0][1])
assert math.isnan(back[1][2])
def test_inf_egress_from_numpy_result() -> None:
"""np.inf in worker results is not collapsed to None on host unpack."""
np = pytest.importorskip("numpy")
vals = [1.0, float("inf"), -float("inf"), 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
wire = child_pack_result(np.array(vals, dtype=np.float64), force="always")
back = host_unpack_data(wire, as_nested_list=True)
assert back[1] == float("inf")
assert back[2] == float("-inf")
def test_pickle5_roundtrip_preserves_nan_buffer() -> None:
"""IPC Pickle5 must preserve raw buffer bytes including NaN slots."""
grid = [[1.0, None, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]
wire = pickle5_roundtrip(host_pack_data(grid, force="always"))
np = pytest.importorskip("numpy")
arr = child_unpack_data(wire)
assert isinstance(arr, np.ndarray)
assert np.isnan(arr[0, 1])
def test_mixed_grid_preserves_zip_code_strings() -> None:
"""Zip-style text must stay in strings map, not be coerced to float."""
wire = host_pack_data(MIXED_WITH_ZIP, force="always")
assert wire["strings"][1] == "02138"
pytest.importorskip("numpy")
out = child_unpack_data(wire)
assert out[0][1] == "02138"
def test_mixed_grid_preserves_non_numeric_string() -> None:
"""Non-coercible text stays a string; numeric-looking text that fails float() is kept."""
grid = [[1.0, "hello", "3.14z", 4.0]] * 3 # 12 cells
wire = host_pack_data(grid, force="always")
assert "hello" in wire["strings"].values()
pytest.importorskip("numpy")
out = child_unpack_data(wire)
assert out[0][1] == "hello"
@pytest.mark.parametrize(
"value, expected",
[
# Pasted ' ' is numeric-coercible for Calc but np.float64 cannot convert it.
pytest.param(" ", " ", id="test_whitespace_only_cell_does_not_crash_child_unpack"),
# Bare '' on mixed split_grid must not raise (Calc usually maps '' to None first).
pytest.param("", "", id="test_empty_string_mixed_split_grid_does_not_crash_child_unpack"),
],
)
def test_whitespace_only_cell_does_not_crash_child_unpack(value, expected) -> None:
pytest.importorskip("numpy")
grid = [[1.0, value, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]]
out = child_unpack_data(host_pack_data(grid, force="always"))
assert isinstance(out, list)
assert out[0][1] == expected
def test_mixed_grid_real_nan_becomes_none_on_child() -> None:
"""Documented: mixed-grid ingress has no blank-vs-NaN wire bit."""
pytest.importorskip("numpy")
grid = [[1.0, float("nan")], ["label", 4.0]]
out = child_unpack_data(host_pack_data(grid, force="always"))
assert out[0][1] is None
assert out[1][0] == "label"
def test_decimal_split_grid_stays_float_not_truncated_int() -> None:
"""stdlib flatten must label Decimal columns float (Cython already did)."""
pytest.importorskip("numpy")
grid = [[Decimal("1.5"), Decimal("2.25")], [Decimal("3.0"), Decimal("4.75")]]
with cython_accelerator_context(enabled=False):
out = child_unpack_data(host_pack_data(grid, force="always"))
assert out[0][0] == pytest.approx(1.5)
assert out[0][1] == pytest.approx(2.25)
def test_decimal_fraction_encoding_ignores_earlier_text() -> None:
"""Decimal and Fraction stay floats after a text cell, same as before one."""
from fractions import Fraction
from plugin.scripting.payload_codec import host_pack_split_grid
def _check() -> None:
after_text = host_unpack_split_grid(
host_pack_split_grid([["02138", Decimal("1.25"), Fraction(1, 4)]])
)
assert after_text[0][0] == "02138"
assert after_text[0][1] == pytest.approx(1.25)
assert after_text[0][2] == pytest.approx(0.25)
assert type(after_text[0][1]) is float
assert type(after_text[0][2]) is float
before_text = host_unpack_split_grid(
host_pack_split_grid([[Decimal("1.25"), Fraction(1, 4), "02138"]])
)
assert before_text[0][0] == pytest.approx(1.25)
assert before_text[0][1] == pytest.approx(0.25)
assert before_text[0][2] == "02138"
assert type(before_text[0][0]) is type(after_text[0][1])
assert type(before_text[0][1]) is type(after_text[0][2])
wire = host_pack_split_grid(
[["label", "x"], [Decimal("1.50"), Fraction(1, 4)]]
)
assert "1.50" not in wire["strings"].values()
assert "1/4" not in wire["strings"].values()
same_col = host_unpack_split_grid(wire)
assert same_col[0] == ["label", "x"]
assert same_col[1][0] == pytest.approx(1.5)
assert same_col[1][1] == pytest.approx(0.25)
assert wire["column_kinds"] == ["float", "float"]
with cython_accelerator_context(enabled=False):
_check()
if payload_codec.fast_flatten_grid_2d is not None:
_check()
class _ObjectKind:
kind = "O"
class _WeirdFloat:
"""dtype.kind 'O' whose float() succeeds. str() is 'W' so a stringify shows up."""
dtype = _ObjectKind()
def __float__(self) -> float:
return 1.5
def __str__(self) -> str:
return "W"
def test_unknown_dtype_kind_promotes_column_like_cython() -> None:
"""kind 'O' that float() accepts is a float column on both packers.
The pure path used to leave the column bool while Cython set float.
"""
from plugin.scripting.payload_codec import column_kinds_for_grid
grid = [[True, 0], [_WeirdFloat(), 0]]
with cython_accelerator_context(enabled=False):
assert column_kinds_for_grid(grid) == ["float", "int"]
if payload_codec.fast_flatten_grid_2d is not None:
assert column_kinds_for_grid(grid) == ["float", "int"]
def test_unknown_dtype_kind_numeric_regardless_of_string_position() -> None:
"""The same object is a float before or after a text cell.
The slow path used to str() an unknown dtype kind, so ['x', Weird()]
became the text 'W' while [Weird(), 'x'] stayed 1.5.
"""
from plugin.scripting.payload_codec import host_pack_split_grid
weird = _WeirdFloat()
def _check() -> None:
before = host_unpack_split_grid(host_pack_split_grid([[weird, "x"]]))
assert before[0][0] == pytest.approx(1.5)
assert type(before[0][0]) is float
assert before[0][1] == "x"
after_wire = host_pack_split_grid([["x", weird]])
assert "W" not in after_wire["strings"].values()
after = host_unpack_split_grid(after_wire)
assert after[0][0] == "x"
assert after[0][1] == pytest.approx(1.5)
assert type(after[0][1]) is float
with cython_accelerator_context(enabled=False):
_check()
if payload_codec.fast_flatten_grid_2d is not None:
_check()
def test_numpy_str_stays_text_on_split_grid() -> None:
"""Unicode scalars stay text. float() must not eat a zip-code-like np.str_."""
np = pytest.importorskip("numpy")
from plugin.scripting.payload_codec import host_pack_split_grid
def _check() -> None:
wire = host_pack_split_grid([[np.str_("02138")]])
assert "02138" in wire["strings"].values()
assert host_unpack_split_grid(wire) == [["02138"]]
with cython_accelerator_context(enabled=False):
_check()
if payload_codec.fast_flatten_grid_2d is not None:
_check()
def test_wide_sheet_above_shape_dim_unpacks() -> None:
"""Pack accepts real sheet widths; unpack must not cap columns at SHAPE_DIM."""
import deal
from plugin.framework.deal_shim import DEAL_MAX_COL_INDEX, DEAL_MAX_SHAPE_DIM
from plugin.scripting.payload_codec import envelope_column_kinds
from tests.harness.strip_bundle import deal_pre_present
ncols = DEAL_MAX_SHAPE_DIM + 1
grid = [[float(i) for i in range(ncols)]]
wire = host_pack_data(grid, force="auto")
assert is_split_grid(wire)
assert wire["shape"] == [1, ncols]
unpacked = host_unpack_data(wire)
assert len(unpacked) == 1
assert len(unpacked[0]) == ncols
assert unpacked[0][0] == pytest.approx(0.0)
assert unpacked[0][-1] == pytest.approx(float(ncols - 1))
np = pytest.importorskip("numpy")
arr = child_unpack_data(wire)
assert isinstance(arr, np.ndarray)
assert arr.shape == (1, ncols)
assert float(arr[0, -1]) == pytest.approx(float(ncols - 1))
cap = DEAL_MAX_COL_INDEX + 1
kinds = envelope_column_kinds({"column_kinds": ["float"] * cap}, ncols=cap)
assert kinds == ["float"] * cap
if deal_pre_present(envelope_column_kinds):
with pytest.raises(deal.PreContractError):
envelope_column_kinds({}, ncols=cap + 1)
def test_bool_cells_round_trip_in_numeric_grid() -> None:
"""Calc booleans in an all-numeric grid become 0.0/1.0 in child ndarray (float64 lane)."""
np = pytest.importorskip("numpy")
grid = [[True, False, 1.0, 2.0], [False, True, 3.0, 4.0], [True, False, 5.0, 6.0]]
arr = child_unpack_data(host_pack_data(grid, force="always"))
assert isinstance(arr, np.ndarray)
assert arr[0, 0] == pytest.approx(1.0)
assert arr[0, 1] == pytest.approx(0.0)
def test_bool_col_11_split_grid_sums() -> None:
"""11 logical cells use split_grid inside calc_range; bools encode as 0/1."""
np = pytest.importorskip("numpy")
from plugin.calc.calc_addin_data import calc_addin_data_to_python, pack_calc_data_for_wire
from plugin.scripting.calc_range import CalcRange, is_calc_range_payload
pattern = (True, True, True, False, True, False, True, False, True, True, False)
# Column range stays N×1 under the shape-preserving contract.
uno_col = tuple((v,) for v in pattern)
wire = pack_calc_data_for_wire(calc_addin_data_to_python(uno_col), force="always")
assert is_calc_range_payload(wire)
assert is_split_grid(wire["data"])
assert wire_cell_count(wire) == 11
assert wire["shape"] == [11, 1]
rng = child_unpack_data(wire)
assert isinstance(rng, CalcRange)
assert rng.shape == (11, 1)
assert float(np.sum(rng)) == pytest.approx(7.0)
def test_large_string_ndarray_packs_instead_of_raising() -> None:
"""A >=100-cell string or object ndarray must not crash the float64 packer."""