import gc
import pytest
import numpy as np
import dlpack_test_module as dtm
try:
import torch
HAS_TORCH = True
except ImportError:
HAS_TORCH = False
torch = None
HAS_CUDA = HAS_TORCH and torch.cuda.is_available()
@pytest.fixture(autouse=True)
def reset_counters():
dtm.reset_counters()
gc.collect()
yield
gc.collect()
class TestCpuImport:
def test_import_numpy_tensor(self):
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
info = dtm.import_tensor(arr)
assert info["shape"] == [2, 3]
assert info["ndim"] == 2
assert info["numel"] == 6
assert info["itemsize"] == 4 assert info["nbytes"] == 24
assert info["is_contiguous"] == True
assert info["is_cpu"] == True
assert info["is_cuda"] == False
def test_import_numpy_zero_copy_ptr(self):
arr = np.arange(16, dtype=np.float32).reshape(4, 4)
np_ptr = arr.__array_interface__["data"][0]
rust_ptr = dtm.get_data_ptr(arr)
assert rust_ptr == np_ptr
def test_import_numpy_1d_tensor(self):
arr = np.array([1, 2, 3, 4, 5], dtype=np.float32)
info = dtm.import_tensor(arr)
assert info["shape"] == [5]
assert info["ndim"] == 1
assert info["numel"] == 5
def test_import_numpy_scalar(self):
arr = np.array(42.0, dtype=np.float32)
info = dtm.import_tensor(arr)
assert info["shape"] == []
assert info["ndim"] == 0
assert info["numel"] == 1
def test_import_numpy_f64_tensor(self):
arr = np.array([1.0, 2.0, 3.0], dtype=np.float64)
info = dtm.import_tensor(arr)
assert info["itemsize"] == 8
def test_import_numpy_int32_tensor(self):
arr = np.array([1, 2, 3], dtype=np.int32)
info = dtm.import_tensor(arr)
assert info["itemsize"] == 4
@pytest.mark.skipif(not HAS_TORCH, reason="PyTorch not available")
def test_import_torch_cpu_tensor(self):
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32)
info = dtm.import_tensor(tensor)
assert info["shape"] == [2, 3]
assert info["is_cpu"] == True
assert info["is_cuda"] == False
@pytest.mark.skipif(not HAS_TORCH, reason="PyTorch not available")
def test_import_torch_transposed_tensor(self):
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32).T
info = dtm.import_tensor(tensor)
assert info["shape"] == [3, 2]
assert info["is_contiguous"] == False
@pytest.mark.skipif(not HAS_TORCH, reason="PyTorch not available")
def test_import_torch_sliced_tensor(self):
tensor = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=torch.float32)
sliced = tensor[:, ::2]
info = dtm.import_tensor(sliced)
assert info["shape"] == [2, 2]
assert info["is_contiguous"] == False
def test_capsule_marked_as_used_after_import(self):
arr = np.array([1, 2, 3], dtype=np.float32)
capsule = arr.__dlpack__()
assert dtm.get_capsule_name(capsule) == "dltensor"
info = dtm.import_from_capsule(capsule)
assert dtm.get_capsule_name(capsule) == "used_dltensor"
assert dtm.is_capsule_consumed(capsule) == True
def test_second_import_of_same_capsule_fails(self):
arr = np.array([1, 2, 3], dtype=np.float32)
capsule = arr.__dlpack__()
info = dtm.import_from_capsule(capsule)
assert info["shape"] == [3]
with pytest.raises(Exception):
dtm.import_from_capsule(capsule)
class TestCpuExport:
def test_export_to_numpy_zero_copy_ptr(self):
capsule = dtm.export_cpu_tensor()
capsule_ptr = dtm.capsule_data_ptr(capsule)
class _CapsuleWrapper:
def __init__(self, cap):
self._cap = cap
def __dlpack__(self):
return self._cap
arr = np.from_dlpack(_CapsuleWrapper(capsule))
np_ptr = arr.__array_interface__["data"][0]
assert np_ptr == capsule_ptr
@pytest.mark.skipif(not HAS_CUDA, reason="CUDA not available")
class TestGpuImport:
def test_import_torch_cuda_tensor(self):
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32, device="cuda:0")
info = dtm.import_tensor(tensor)
assert info["shape"] == [2, 3]
assert info["is_cpu"] == False
assert info["is_cuda"] == True
assert info["device_id"] == 0
def test_import_torch_cuda_1d(self):
tensor = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float32, device="cuda:0")
info = dtm.import_tensor(tensor)
assert info["shape"] == [5]
assert info["is_cuda"] == True
def test_import_torch_cuda_f64(self):
tensor = torch.tensor([1.0, 2.0], dtype=torch.float64, device="cuda:0")
info = dtm.import_tensor(tensor)
assert info["itemsize"] == 8
assert info["is_cuda"] == True
def test_import_torch_cuda_non_contiguous(self):
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float32, device="cuda:0").T
info = dtm.import_tensor(tensor)
assert info["shape"] == [3, 2]
assert info["is_contiguous"] == False
assert info["is_cuda"] == True
def test_cuda_capsule_marked_as_used(self):
tensor = torch.tensor([1, 2, 3], dtype=torch.float32, device="cuda:0")
capsule = tensor.__dlpack__()
assert dtm.get_capsule_name(capsule) == "dltensor"
info = dtm.import_from_capsule(capsule)
assert dtm.get_capsule_name(capsule) == "used_dltensor"
assert info["is_cuda"] == True
class TestMemorySafety:
def test_no_use_after_free_prevented_by_rename(self):
arr = np.array([1, 2, 3], dtype=np.float32)
capsule = arr.__dlpack__()
info1 = dtm.import_from_capsule(capsule)
assert info1["shape"] == [3]
with pytest.raises(Exception):
dtm.import_from_capsule(capsule)
@pytest.mark.skipif(not HAS_TORCH, reason="PyTorch not available")
def test_torch_capsule_protection(self):
tensor = torch.tensor([1, 2, 3], dtype=torch.float32)
capsule = tensor.__dlpack__()
info = dtm.import_from_capsule(capsule)
assert dtm.is_capsule_consumed(capsule) == True
with pytest.raises(Exception):
dtm.import_from_capsule(capsule)
class TestStress:
def test_many_numpy_imports(self):
for i in range(100):
arr = np.random.randn(100, 100).astype(np.float32)
info = dtm.import_tensor(arr)
assert info["shape"] == [100, 100]
@pytest.mark.skipif(not HAS_TORCH, reason="PyTorch not available")
def test_many_torch_imports(self):
for i in range(100):
tensor = torch.randn(100, 100)
info = dtm.import_tensor(tensor)
assert info["shape"] == [100, 100]
@pytest.mark.skipif(not HAS_CUDA, reason="CUDA not available")
def test_many_cuda_imports(self):
for i in range(100):
tensor = torch.randn(100, 100, device="cuda:0")
info = dtm.import_tensor(tensor)
assert info["is_cuda"] == True
gc.collect()
torch.cuda.empty_cache()
def test_large_tensor_numpy(self):
arr = np.random.randn(1000, 1000).astype(np.float32)
info = dtm.import_tensor(arr)
assert info["shape"] == [1000, 1000]
assert info["numel"] == 1000000
@pytest.mark.skipif(not HAS_CUDA, reason="CUDA not available")
def test_large_tensor_cuda(self):
tensor = torch.randn(1000, 1000, device="cuda:0")
info = dtm.import_tensor(tensor)
assert info["shape"] == [1000, 1000]
assert info["is_cuda"] == True
class TestVersionedCpu:
def test_readonly_export_is_versioned_capsule(self):
capsule = dtm.export_cpu_tensor_readonly()
assert dtm.get_capsule_name(capsule) == "dltensor_versioned"
@pytest.mark.skipif(not HAS_TORCH, reason="requires torch")
def test_readonly_capsule_consumed_by_torch(self):
capsule = dtm.export_cpu_tensor_readonly()
t = torch.from_dlpack(capsule)
assert list(t.shape) == [2, 3]
assert t.flatten().tolist() == [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
def test_import_numpy_is_not_readonly(self):
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
assert dtm.import_is_readonly(arr) is False
@pytest.mark.skipif(not HAS_TORCH, reason="requires torch")
def test_import_torch_via_versioned_negotiation(self):
t = torch.arange(6, dtype=torch.float32).reshape(2, 3)
info = dtm.import_tensor(t)
assert info["shape"] == [2, 3]
assert info["is_cpu"] is True
assert dtm.import_is_readonly(t) is False
def test_capsule_data_ptr_versioned(self):
capsule = dtm.export_cpu_tensor_readonly()
ptr = dtm.capsule_data_ptr(capsule)
assert ptr > 0xFFFF
def test_is_capsule_consumed_versioned(self):
cap = dtm.export_cpu_tensor_readonly()
assert dtm.is_capsule_consumed(cap) is False
dtm.import_from_capsule(cap)
assert dtm.is_capsule_consumed(cap) is True
if __name__ == "__main__":
pytest.main([__file__, "-v"])