|
20 | 20 | PREDBAT_KERNEL_REQUIRED=1 is set (CI) in which case it fails. |
21 | 21 | """ |
22 | 22 |
|
| 23 | +import array |
23 | 24 | import copy |
| 25 | +import ctypes |
| 26 | +import gc |
24 | 27 | import os |
25 | 28 | import random |
26 | 29 | import subprocess |
@@ -380,6 +383,51 @@ def dual_run(name, my_predbat, pv_step, pv10_step, load_step, load10_step, charg |
380 | 383 | return failed |
381 | 384 |
|
382 | 385 |
|
| 386 | +def run_marshalling_tests(): |
| 387 | + """Check the ctypes buffer helpers, returns True on failure. |
| 388 | +
|
| 389 | + double_array/int32_array build their buffers with from_buffer, which returns a view over an |
| 390 | + array.array rather than a copy. If ctypes did not keep the backing object alive the kernel would |
| 391 | + read freed memory - silently, and only sometimes - so that guarantee is asserted here rather than |
| 392 | + assumed, along with the values surviving the round trip. |
| 393 | + """ |
| 394 | + print("**** Running kernel marshalling tests ****") |
| 395 | + failed = False |
| 396 | + |
| 397 | + # The typecode chosen for the backing array must match the ctypes element exactly. from_buffer |
| 398 | + # only checks the buffer is large enough, so a wider backing type is accepted and then read as |
| 399 | + # interleaved garbage - a silent corruption rather than an exception. |
| 400 | + for name, typecode, ctype in (("DOUBLE_TYPECODE", prediction_kernel.DOUBLE_TYPECODE, ctypes.c_double), ("INT32_TYPECODE", prediction_kernel.INT32_TYPECODE, ctypes.c_int32)): |
| 401 | + if typecode is not None and array.array(typecode).itemsize != ctypes.sizeof(ctype): |
| 402 | + print("ERROR: {} is '{}' with itemsize {} but the ctypes element is {} bytes".format(name, typecode, array.array(typecode).itemsize, ctypes.sizeof(ctype))) |
| 403 | + failed = True |
| 404 | + |
| 405 | + for name, builder, values, typecode in (("double_array", prediction_kernel.double_array, [0.0, -1.5, 3.25, 1e6], prediction_kernel.DOUBLE_TYPECODE), ("int32_array", prediction_kernel.int32_array, [0, -7, 42, 100000], prediction_kernel.INT32_TYPECODE)): |
| 406 | + # Build from a temporary so the source list/array is unreferenced by the time it is read |
| 407 | + buffer = builder(list(values)) |
| 408 | + gc.collect() |
| 409 | + got = [buffer[i] for i in range(len(values))] |
| 410 | + if got != values: |
| 411 | + print("ERROR: {} round trip expected {} but got {}".format(name, values, got)) |
| 412 | + failed = True |
| 413 | + # Only the from_buffer path holds a view that needs its backing kept alive; the fallback |
| 414 | + # copies the values, so it has nothing to retain and is safe without _objects. Truthiness |
| 415 | + # rather than "is not None": an empty _objects would mean nothing is retained, which is just |
| 416 | + # as unsafe as the attribute being absent. |
| 417 | + if typecode is not None and not getattr(buffer, "_objects", None): |
| 418 | + print("ERROR: {} did not retain its backing buffer - the kernel could read freed memory".format(name)) |
| 419 | + failed = True |
| 420 | + |
| 421 | + empty = builder([]) |
| 422 | + if len(empty) != 0: |
| 423 | + print("ERROR: {}([]) should be empty, got length {}".format(name, len(empty))) |
| 424 | + failed = True |
| 425 | + |
| 426 | + if not failed: |
| 427 | + print("PASS") |
| 428 | + return failed |
| 429 | + |
| 430 | + |
383 | 431 | def run_edge_case_tests(my_predbat): |
384 | 432 | """Deterministic scenarios pinning each kernel branch, returns True on failure""" |
385 | 433 | failed = False |
@@ -840,7 +888,8 @@ def run_kernel_parity_tests(my_predbat): |
840 | 888 |
|
841 | 889 | state = snapshot_scenario_state(my_predbat) |
842 | 890 | try: |
843 | | - failed = run_edge_case_tests(my_predbat) |
| 891 | + failed = run_marshalling_tests() |
| 892 | + failed |= run_edge_case_tests(my_predbat) |
844 | 893 | if not failed: |
845 | 894 | failed |= run_random_sweep_tests(my_predbat) |
846 | 895 | if not failed: |
|
0 commit comments