use std::{collections::BTreeSet, fs, path::PathBuf};
use cudf::interop::contract::PreparedTableExport;
use cugraph::{
DeviceArray, EnteredContext, Forward, Graph, GraphBuilder, ResourceHandle, Transposed,
centrality::{CentralityResult, PageRankParams},
community::{LouvainParams, LouvainResult},
cudf_interop::{TableGraphOptions, graph_from_prepared_table_export},
error::Result,
layout::{ForceAtlas2Params, LayoutResult},
random::RngState,
};
use static_assertions::assert_not_impl_any;
fn rust_sources(root: &std::path::Path, out: &mut Vec<PathBuf>) {
for entry in fs::read_dir(root).unwrap_or_else(|error| panic!("read {}: {error}", root.display())) {
let path = entry.expect("source directory entry").path();
if path.is_dir() {
rust_sources(&path, out);
} else if path.extension().is_some_and(|extension| extension == "rs") {
out.push(path);
}
}
}
type DeviceArrayNewI32 = fn(&EnteredContext<'_>, usize) -> Result<DeviceArray<i32>>;
type DeviceArrayFromSliceI32 = fn(&EnteredContext<'_>, &[i32]) -> Result<DeviceArray<i32>>;
type BuildForwardGraph = fn(&GraphBuilder<'_>, &EnteredContext<'_>) -> Result<Graph<Forward>>;
type PageRankEntryPoint = fn(&mut Graph<Transposed>, &EnteredContext<'_>, &PageRankParams) -> Result<CentralityResult>;
type LouvainEntryPoint = fn(&mut Graph<Forward>, &EnteredContext<'_>, &LouvainParams) -> Result<LouvainResult>;
type ForceAtlas2EntryPoint =
fn(&mut Graph<Forward>, &EnteredContext<'_>, &mut RngState, &ForceAtlas2Params) -> Result<LayoutResult>;
type RngStateNew = fn(&EnteredContext<'_>, u64) -> Result<RngState>;
type GraphFromPreparedTableForward = for<'export, 'buffers> fn(
&EnteredContext<'_>,
&'export PreparedTableExport<'buffers>,
&TableGraphOptions,
) -> Result<Graph<Forward>>;
#[test]
fn resource_handle_and_entered_context_are_not_send_or_sync() {
assert_not_impl_any!(ResourceHandle: Send, Sync);
assert_not_impl_any!(EnteredContext<'static>: Send, Sync);
}
fn build_forward(builder: &GraphBuilder<'_>, handle: &EnteredContext<'_>) -> Result<Graph<Forward>> {
builder.build::<Forward>(handle)
}
#[test]
fn gpu_entry_points_require_entered_context() {
let _: DeviceArrayNewI32 = DeviceArray::<i32>::new;
let _: DeviceArrayFromSliceI32 = DeviceArray::<i32>::from_slice;
let _: BuildForwardGraph = build_forward;
let _: PageRankEntryPoint = Graph::<Transposed>::pagerank;
let _: LouvainEntryPoint = Graph::<Forward>::louvain;
let _: ForceAtlas2EntryPoint = Graph::<Forward>::force_atlas2;
let _: RngStateNew = RngState::new;
}
#[test]
fn canonical_cudf_graph_ingress_requires_entered_context() {
let _: GraphFromPreparedTableForward = graph_from_prepared_table_export::<Forward>;
}
#[test]
fn runtime_bound_resource_handle_uses_no_secondary_stream_pool() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let shim = manifest_dir.join("shim").join("runtime_resource_handle.cpp");
let source = fs::read_to_string(&shim).unwrap_or_else(|err| panic!("read {}: {err}", shim.display()));
let normalized_source = source.split_whitespace().collect::<Vec<_>>().join(" ");
assert!(
normalized_source.contains(
"std::make_unique<raft::handle_t>( stream, std::shared_ptr<rmm::cuda_stream_pool>{}, raft::mr::device_resource{rmm::mr::any_device_resource_from_raw(mr)})"
),
"runtime-bound cuGraph handle should pass an empty RAFT stream pool so graph work remains on the runtime stream"
);
assert_eq!(
source.matches("cuda_stream_pool").count(),
1,
"runtime-bound cuGraph handle must not introduce a secondary RAFT stream pool"
);
assert!(
!source.contains("set_current_device_resource"),
"runtime-bound graph work must not swap RMM's process-wide current resource"
);
assert!(
!source.contains("runtime_graph_creation_memory_resource"),
"the legacy global-RMM graph-construction bridge must stay removed"
);
let handle_source = fs::read_to_string(manifest_dir.join("src/handle.rs")).expect("runtime handle source");
assert!(
handle_source.contains("route_current_resource(runtime.stream(), runtime.memory_resource())"),
"runtime-bound handles must register their explicit resource through cuDF's stable current-resource route"
);
}
#[test]
fn every_device_bound_owner_uses_the_typed_destroy_bridge() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let shim = fs::read_to_string(manifest_dir.join("shim/runtime_resource_handle.cpp")).expect("native destroy shim");
assert!(shim.contains("cugraph_rs_destroy_device_resource"));
assert!(shim.contains("CUGRAPH_RS_DESTROY_FATAL_CUDA"));
let direct_void_destructors = [
"cugraph_similarity_result_free(",
"cugraph_vertex_pairs_free(",
"cugraph_rng_state_free(",
"cugraph_core_result_free(",
"cugraph_k_core_result_free(",
"cugraph_degrees_result_free(",
"cugraph_induced_subgraph_result_free(",
"cugraph_layout_result_free(",
"cugraph_hierarchical_clustering_result_free(",
"cugraph_triangle_count_result_free(",
"cugraph_random_walk_result_free(",
"cugraph_sample_result_free(",
"cugraph_clustering_result_free(",
"cugraph_edgelist_free(",
"cugraph_graph_free(",
"cugraph_bfs_predicate_result_free(",
"cugraph_paths_result_free(",
"cugraph_extract_paths_result_free(",
"cugraph_free_resource_handle(",
"cugraph_centrality_result_free(",
"cugraph_edge_centrality_result_free(",
"cugraph_hits_result_free(",
"cugraph_labeling_result_free(",
"cugraph_type_erased_device_array_free(",
];
let mut sources = Vec::new();
rust_sources(&manifest_dir.join("src"), &mut sources);
for source_path in sources {
let source = fs::read_to_string(&source_path).expect("Rust source");
for destructor in direct_void_destructors {
assert!(
!source.contains(destructor),
"{} bypasses typed native teardown through {destructor}",
source_path.display()
);
}
}
}
#[test]
fn direct_void_destructors_are_limited_to_proven_host_metadata() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let expected = BTreeSet::from([
"cugraph_error_free".to_owned(),
"cugraph_sampling_options_free".to_owned(),
"cugraph_type_erased_device_array_view_free".to_owned(),
"cugraph_type_erased_host_array_view_free".to_owned(),
]);
let mut sources = Vec::new();
rust_sources(&manifest_dir.join("src"), &mut sources);
let mut found = BTreeSet::new();
for source_path in sources {
let source = fs::read_to_string(&source_path).expect("Rust source");
for (start, _) in source.match_indices("cugraph_") {
let candidate = &source[start..];
let name_len = candidate
.find(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
.unwrap_or(candidate.len());
let name = &candidate[..name_len];
if name.ends_with("_free") && candidate[name_len..].starts_with('(') {
found.insert(name.to_owned());
}
}
}
assert_eq!(
found, expected,
"a direct upstream void destructor was added or a proven host-metadata exception changed"
);
let upstream = manifest_dir.parent().expect("workspace root").join("cugraph/cpp/src/c_api");
let array_cpp = fs::read_to_string(upstream.join("array.cpp")).expect("upstream array.cpp");
let array_hpp = fs::read_to_string(upstream.join("array.hpp")).expect("upstream array.hpp");
let error_cpp = fs::read_to_string(upstream.join("error.cpp")).expect("upstream error.cpp");
let error_hpp = fs::read_to_string(upstream.join("error.hpp")).expect("upstream error.hpp");
let sampling_cpp = fs::read_to_string(upstream.join("sampling_result.cpp")).expect("upstream sampling_result.cpp");
let sampling_hpp = fs::read_to_string(upstream.join("sampling_common.hpp")).expect("upstream sampling_common.hpp");
assert!(array_cpp.contains("cugraph_type_erased_device_array_view_free"));
assert!(array_cpp.contains("cugraph_type_erased_host_array_view_free"));
assert!(array_hpp.contains("struct cugraph_type_erased_device_array_view_t {\n void* data_;"));
assert!(array_hpp.contains("struct cugraph_type_erased_host_array_view_t {\n std::byte* data_;"));
assert!(error_cpp.contains("cugraph_error_free"));
assert!(error_hpp.contains("struct cugraph_error_t {\n std::string error_message_{};"));
assert!(sampling_cpp.contains("cugraph_sampling_options_free"));
let sampling_options = sampling_hpp
.split_once("struct cugraph_sampling_options_t {")
.and_then(|(_, tail)| tail.split_once("struct sampling_flags_t"))
.map(|(body, _)| body)
.expect("sampling-options definition");
assert!(!sampling_options.contains("device_"));
assert!(!sampling_options.contains("rmm::"));
}
#[test]
fn rust_and_native_destroy_kinds_have_identical_discriminants() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let rust = fs::read_to_string(manifest_dir.join("src/cuda.rs")).expect("Rust destroy kinds");
let native = fs::read_to_string(manifest_dir.join("shim/runtime_resource_handle.h")).expect("native destroy kinds");
let kinds = [
("SimilarityResult", "SIMILARITY_RESULT", 1),
("VertexPairs", "VERTEX_PAIRS", 2),
("RngState", "RNG_STATE", 3),
("CoreResult", "CORE_RESULT", 4),
("KCoreResult", "K_CORE_RESULT", 5),
("DegreesResult", "DEGREES_RESULT", 6),
("InducedSubgraphResult", "INDUCED_SUBGRAPH_RESULT", 7),
("LayoutResult", "LAYOUT_RESULT", 8),
("HierarchicalClusteringResult", "HIERARCHICAL_CLUSTERING_RESULT", 9),
("TriangleCountResult", "TRIANGLE_COUNT_RESULT", 10),
("RandomWalkResult", "RANDOM_WALK_RESULT", 11),
("SampleResult", "SAMPLE_RESULT", 12),
("ClusteringResult", "CLUSTERING_RESULT", 13),
("Edgelist", "EDGELIST", 14),
("Graph", "GRAPH", 15),
("BfsPredicateResult", "BFS_PREDICATE_RESULT", 16),
("PathsResult", "PATHS_RESULT", 17),
("ExtractPathsResult", "EXTRACT_PATHS_RESULT", 18),
("RuntimeResourceHandle", "RUNTIME_RESOURCE_HANDLE", 19),
("ResourceHandle", "RESOURCE_HANDLE", 20),
("CentralityResult", "CENTRALITY_RESULT", 21),
("EdgeCentralityResult", "EDGE_CENTRALITY_RESULT", 22),
("HitsResult", "HITS_RESULT", 23),
("LabelingResult", "LABELING_RESULT", 24),
("DeviceArray", "DEVICE_ARRAY", 26),
];
for (rust_name, native_name, value) in kinds {
assert!(rust.contains(&format!("{rust_name} = {value},")), "missing Rust destroy kind {rust_name}");
assert!(
native.contains(&format!("CUGRAPH_RS_DEVICE_RESOURCE_{native_name} = {value}")),
"missing native destroy kind {native_name}"
);
}
}