Skip to content

Commit 2b4a826

Browse files
samtalkirobertbuessowollemartensson
committed
GH-184: Harden C Data Interface tests
Add deterministic malformed import fuzzing, nested malformed input and export layout checks, a C producer smoke test, repeated GC stress, and an optional PyArrow C Data smoke test with skips when the external dependency is unavailable. Co-authored-by: Robert Buessow <robert.buessow@relational.ai> Co-authored-by: Olle Martensson <olle.martensson@gmail.com> Generated-by: OpenAI Codex
1 parent 775bc7d commit 2b4a826

2 files changed

Lines changed: 310 additions & 0 deletions

File tree

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
2222
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
2323
FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f"
2424
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
25+
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
2526
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
2627
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
2728
PooledArrays = "2dfb63ee-cc39-5dd5-95bd-886bf059d720"

test/cdata.jl

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ function _cdata_release_array(ptr::Ptr{Arrow.ArrowArray})
5454
end
5555

5656
using Dates
57+
using Libdl
58+
using Random
5759

5860
const _CDATA_RELEASE_SCHEMA =
5961
@cfunction(_cdata_release_schema, Cvoid, (Ptr{Arrow.ArrowSchema},))
@@ -242,6 +244,10 @@ function _set_schema!(
242244
return f
243245
end
244246

247+
function _replace_schema!(f::CDataFixture; kwargs...)
248+
return _set_schema!(f; kwargs...)
249+
end
250+
245251
function _set_array!(
246252
f::CDataFixture;
247253
length=f.array[].length,
@@ -270,6 +276,10 @@ function _set_array!(
270276
return f
271277
end
272278

279+
function _replace_array!(f::CDataFixture; len=f.array[].length, kwargs...)
280+
return _set_array!(f; length=len, kwargs...)
281+
end
282+
273283
@testset "Arrow C Data Interface import" begin
274284
@testset "ABI layout" begin
275285
ptr = sizeof(Ptr{Cvoid})
@@ -1148,6 +1158,214 @@ end
11481158
end
11491159
bad(child)
11501160
end
1161+
1162+
@testset "deterministic malformed import fuzz" begin
1163+
iters = something(tryparse(Int, get(ENV, "ARROW_CDATA_FUZZ_ITERS", "64")), 64)
1164+
iters = clamp(iters, 0, 10_000)
1165+
rng = Random.MersenneTwister(0x0cda7a)
1166+
for _ = 1:iters
1167+
f = _primitive_fixture("i", Int32[10, 20, 30])
1168+
case = rand(rng, 1:11)
1169+
if case == 1
1170+
_replace_array!(f; len=Int64(-1))
1171+
elseif case == 2
1172+
_replace_array!(f; offset=Int64(-1))
1173+
elseif case == 3
1174+
_replace_array!(f; null_count=Int64(4))
1175+
elseif case == 4
1176+
_replace_array!(f; n_buffers=Int64(1))
1177+
elseif case == 5
1178+
_replace_array!(f; buffers=Ptr{Ptr{Cvoid}}(C_NULL))
1179+
elseif case == 6
1180+
_replace_schema!(f; format=Cstring(C_NULL))
1181+
elseif case == 7
1182+
_replace_schema!(f; release=Ptr{Cvoid}(C_NULL))
1183+
elseif case == 8
1184+
_replace_array!(f; release=Ptr{Cvoid}(C_NULL))
1185+
elseif case == 9
1186+
fmt = Vector{UInt8}("+l\0")
1187+
push!(f.roots, fmt)
1188+
_replace_schema!(f; format=Cstring(pointer(fmt)))
1189+
elseif case == 10
1190+
offsets = Int32[0, 2, 1, 3]
1191+
bytes = UInt8[0x01, 0x02, 0x03]
1192+
buffers = Ptr{Cvoid}[
1193+
C_NULL,
1194+
Ptr{Cvoid}(pointer(offsets)),
1195+
Ptr{Cvoid}(pointer(bytes)),
1196+
]
1197+
push!(f.roots, offsets)
1198+
push!(f.roots, bytes)
1199+
push!(f.roots, buffers)
1200+
fmt = Vector{UInt8}("z\0")
1201+
push!(f.roots, fmt)
1202+
_replace_schema!(f; format=Cstring(pointer(fmt)))
1203+
_replace_array!(
1204+
f;
1205+
buffers=Ptr{Ptr{Cvoid}}(pointer(buffers)),
1206+
n_buffers=Int64(3),
1207+
)
1208+
else
1209+
fmt = Vector{UInt8}("w:$(Arrow._CDATA_MAX_FIXED_SIZE + rand(rng, 1:8))\0")
1210+
push!(f.roots, fmt)
1211+
_replace_schema!(f; format=Cstring(pointer(fmt)))
1212+
end
1213+
@test_throws ArgumentError Arrow.from_c_data(_schema_ptr(f), _array_ptr(f))
1214+
end
1215+
end
1216+
1217+
@testset "malformed nested inputs" begin
1218+
child = _primitive_fixture("i", Int32[1, 2, 3])
1219+
_replace_schema!(child; release=Ptr{Cvoid}(C_NULL))
1220+
offsets = Int32[0, 1]
1221+
f = _cdata_fixture(
1222+
"+l",
1223+
1,
1224+
Ptr{Cvoid}[C_NULL, Ptr{Cvoid}(pointer(offsets))];
1225+
children=[child],
1226+
)
1227+
push!(f.roots, offsets)
1228+
@test_throws ArgumentError Arrow.from_c_data(_schema_ptr(f), _array_ptr(f))
1229+
1230+
child = _primitive_fixture("i", Int32[1])
1231+
root = _cdata_fixture("+s", 2, Ptr{Cvoid}[C_NULL]; children=[child])
1232+
@test_throws ArgumentError Arrow.from_c_data(_schema_ptr(root), _array_ptr(root))
1233+
1234+
child = _primitive_fixture("i", Int32[])
1235+
f = _cdata_fixture(
1236+
"+w:2",
1237+
1,
1238+
Ptr{Cvoid}[C_NULL];
1239+
offset=Int64(typemax(Int)),
1240+
children=[child],
1241+
)
1242+
@test_throws ArgumentError Arrow.from_c_data(_schema_ptr(f), _array_ptr(f))
1243+
end
1244+
1245+
@testset "C producer smoke" begin
1246+
cc = Sys.which("cc")
1247+
if cc === nothing || Sys.iswindows()
1248+
@test_skip "C producer smoke requires a C compiler"
1249+
else
1250+
c_src = """
1251+
#include <stdint.h>
1252+
struct ArrowSchema {
1253+
const char* format;
1254+
const char* name;
1255+
const char* metadata;
1256+
int64_t flags;
1257+
int64_t n_children;
1258+
struct ArrowSchema** children;
1259+
struct ArrowSchema* dictionary;
1260+
void (*release)(struct ArrowSchema*);
1261+
void* private_data;
1262+
};
1263+
struct ArrowArray {
1264+
int64_t length;
1265+
int64_t null_count;
1266+
int64_t offset;
1267+
int64_t n_buffers;
1268+
int64_t n_children;
1269+
const void** buffers;
1270+
struct ArrowArray** children;
1271+
struct ArrowArray* dictionary;
1272+
void (*release)(struct ArrowArray*);
1273+
void* private_data;
1274+
};
1275+
static const char format[] = "i";
1276+
static const uint8_t validity[] = {0x05};
1277+
static const int32_t data[] = {10, 20, 30};
1278+
static const void* buffers[] = {validity, data};
1279+
static void release_schema(struct ArrowSchema* schema) {
1280+
schema->release = 0;
1281+
}
1282+
static void release_array(struct ArrowArray* array) {
1283+
array->release = 0;
1284+
}
1285+
int make_nullable_int32(struct ArrowSchema* schema, struct ArrowArray* array) {
1286+
if (!schema || !array) return -1;
1287+
schema->format = format;
1288+
schema->name = "";
1289+
schema->metadata = 0;
1290+
schema->flags = 2;
1291+
schema->n_children = 0;
1292+
schema->children = 0;
1293+
schema->dictionary = 0;
1294+
schema->release = release_schema;
1295+
schema->private_data = 0;
1296+
array->length = 3;
1297+
array->null_count = 1;
1298+
array->offset = 0;
1299+
array->n_buffers = 2;
1300+
array->n_children = 0;
1301+
array->buffers = buffers;
1302+
array->children = 0;
1303+
array->dictionary = 0;
1304+
array->release = release_array;
1305+
array->private_data = 0;
1306+
return 0;
1307+
}
1308+
"""
1309+
mktempdir() do dir
1310+
src = joinpath(dir, "producer.c")
1311+
lib = joinpath(dir, "producer.$(Libdl.dlext)")
1312+
write(src, c_src)
1313+
if Sys.isapple()
1314+
run(`$cc -dynamiclib -o $lib $src`)
1315+
else
1316+
run(`$cc -shared -fPIC -o $lib $src`)
1317+
end
1318+
handle = Libdl.dlopen(lib)
1319+
try
1320+
make = Libdl.dlsym(handle, :make_nullable_int32)
1321+
schema = Ref(
1322+
Arrow.ArrowSchema(
1323+
Cstring(C_NULL),
1324+
Cstring(C_NULL),
1325+
Cstring(C_NULL),
1326+
0,
1327+
0,
1328+
Ptr{Ptr{Arrow.ArrowSchema}}(C_NULL),
1329+
Ptr{Arrow.ArrowSchema}(C_NULL),
1330+
Ptr{Cvoid}(C_NULL),
1331+
Ptr{Cvoid}(C_NULL),
1332+
),
1333+
)
1334+
array = Ref(
1335+
Arrow.ArrowArray(
1336+
0,
1337+
0,
1338+
0,
1339+
0,
1340+
0,
1341+
Ptr{Ptr{Cvoid}}(C_NULL),
1342+
Ptr{Ptr{Arrow.ArrowArray}}(C_NULL),
1343+
Ptr{Arrow.ArrowArray}(C_NULL),
1344+
Ptr{Cvoid}(C_NULL),
1345+
Ptr{Cvoid}(C_NULL),
1346+
),
1347+
)
1348+
@test ccall(
1349+
make,
1350+
Cint,
1351+
(Ptr{Arrow.ArrowSchema}, Ptr{Arrow.ArrowArray}),
1352+
Base.unsafe_convert(Ptr{Arrow.ArrowSchema}, schema),
1353+
Base.unsafe_convert(Ptr{Arrow.ArrowArray}, array),
1354+
) == 0
1355+
x = Arrow.from_c_data(
1356+
Base.unsafe_convert(Ptr{Arrow.ArrowSchema}, schema),
1357+
Base.unsafe_convert(Ptr{Arrow.ArrowArray}, array),
1358+
)
1359+
@test isequal(collect(x), Union{Int32,Missing}[10, missing, 30])
1360+
Arrow.release_c_data(x)
1361+
@test schema[].release == C_NULL
1362+
@test array[].release == C_NULL
1363+
finally
1364+
Libdl.dlclose(handle)
1365+
end
1366+
end
1367+
end
1368+
end
11511369
end
11521370

11531371
_schema_ref_ptr(ref::Ref{Arrow.ArrowSchema}) =
@@ -1591,4 +1809,95 @@ end
15911809
@test length(Arrow._CDATA_EXPORT_SCHEMA_OWNERS) == schema_count
15921810
@test length(Arrow._CDATA_EXPORT_ARRAY_OWNERS) == array_count
15931811
end
1812+
1813+
@testset "malformed nested export layouts" begin
1814+
schema_count = length(Arrow._CDATA_EXPORT_SCHEMA_OWNERS)
1815+
array_count = length(Arrow._CDATA_EXPORT_ARRAY_OWNERS)
1816+
1817+
child = Arrow.toarrowvector(Int32[1])
1818+
bad_struct =
1819+
Arrow.Struct{NamedTuple{(:a,),Tuple{Int32}},Tuple{typeof(child)},(:a,)}(
1820+
Arrow.ValidityBitmap(UInt8[], 1, 0, 0),
1821+
(child,),
1822+
2,
1823+
nothing,
1824+
)
1825+
@test_throws ArgumentError Arrow.to_c_data(bad_struct)
1826+
1827+
fixed_child = Arrow.toarrowvector(Int32[1, 2, 3, 4, 5])
1828+
bad_fixed = Arrow.FixedSizeList{NTuple{3,Int32},typeof(fixed_child)}(
1829+
UInt8[],
1830+
Arrow.ValidityBitmap(UInt8[], 1, 0, 0),
1831+
fixed_child,
1832+
2,
1833+
nothing,
1834+
)
1835+
@test_throws ArgumentError Arrow.to_c_data(bad_fixed)
1836+
1837+
list_child = Arrow.toarrowvector(Int32[1])
1838+
bad_list = Arrow.List{Vector{Int32},Int32,typeof(list_child)}(
1839+
UInt8[],
1840+
Arrow.ValidityBitmap(UInt8[], 1, 0, 0),
1841+
Arrow.Offsets(UInt8[], Int32[0, 2]),
1842+
list_child,
1843+
1,
1844+
nothing,
1845+
)
1846+
@test_throws ArgumentError Arrow.to_c_data(bad_list)
1847+
1848+
@test length(Arrow._CDATA_EXPORT_SCHEMA_OWNERS) == schema_count
1849+
@test length(Arrow._CDATA_EXPORT_ARRAY_OWNERS) == array_count
1850+
end
1851+
1852+
@testset "repeated GC stress" begin
1853+
for i = 1:24
1854+
tbl = (
1855+
id=Int32[i, i + 1, i + 2],
1856+
label=["a$(i)", "b$(i)", "c$(i)"],
1857+
flags=Union{Bool,Missing}[true, missing, isodd(i)],
1858+
)
1859+
schema, array = Arrow.to_c_data(tbl)
1860+
imported = _import_exported(schema, array)
1861+
GC.gc(true)
1862+
GC.gc(true)
1863+
@test collect(imported.id) == tbl.id
1864+
@test collect(imported.label) == tbl.label
1865+
@test isequal(collect(imported.flags), tbl.flags)
1866+
Arrow.release_c_data(imported)
1867+
@test schema[].release == C_NULL
1868+
@test array[].release == C_NULL
1869+
end
1870+
end
1871+
1872+
@testset "optional PyArrow C Data smoke" begin
1873+
python = Sys.which("python3")
1874+
if python === nothing
1875+
@test_skip "python3 not available"
1876+
else
1877+
script = """
1878+
try:
1879+
import pyarrow as pa
1880+
except Exception:
1881+
print("skip: pyarrow unavailable")
1882+
raise SystemExit(0)
1883+
arr = pa.array([1, None, 3], type=pa.int32())
1884+
if not hasattr(arr, "__arrow_c_array__"):
1885+
print("skip: pyarrow C array export unavailable")
1886+
raise SystemExit(0)
1887+
if not hasattr(pa.Array, "_import_from_c_capsule"):
1888+
print("skip: pyarrow C capsule import unavailable")
1889+
raise SystemExit(0)
1890+
capsules = arr.__arrow_c_array__()
1891+
out = pa.Array._import_from_c_capsule(*capsules)
1892+
assert out.to_pylist() == [1, None, 3]
1893+
print("ok")
1894+
"""
1895+
out = readchomp(`$python -c $script`)
1896+
if startswith(out, "skip:")
1897+
@test_skip out
1898+
else
1899+
@test out == "ok"
1900+
end
1901+
end
1902+
end
15941903
end

0 commit comments

Comments
 (0)