diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1b3b61..2f81c99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive + lfs: true - name: Install system dependencies run: | diff --git a/AGENTS.md b/AGENTS.md index a71b1cb..b5e655b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,9 +26,11 @@ pathrex/ │ │ └── inmemory.rs # InMemory marker, InMemoryBuilder, InMemoryGraph │ └── formats/ │ ├── mod.rs # FormatError enum, re-exports -│ └── csv.rs # Csv — CSV → Edge iterator (CsvConfig, ColumnSpec) +│ ├── csv.rs # Csv — CSV → Edge iterator (CsvConfig, ColumnSpec) +│ └── mm.rs # MatrixMarket directory loader (vertices.txt, edges.txt, *.txt) ├── tests/ -│ └── inmemory_tests.rs # Integration tests for InMemoryBuilder / InMemoryGraph +│ ├── inmemory_tests.rs # Integration tests for InMemoryBuilder / InMemoryGraph +│ └── mm_tests.rs # Integration tests for MatrixMarket format ├── deps/ │ └── LAGraph/ # Git submodule (SparseLinearAlgebra/LAGraph) └── .github/workflows/ci.yml # CI: build GraphBLAS + LAGraph, cargo build & test @@ -178,18 +180,26 @@ pub trait Backend { ### InMemoryBuilder / InMemoryGraph [`InMemoryBuilder`](src/graph/inmemory.rs:35) is the primary `GraphBuilder` implementation. -It collects edges in RAM, then [`build()`](src/graph/inmemory.rs:110) calls +It collects edges in RAM, then [`build()`](src/graph/inmemory.rs:131) calls GraphBLAS to create one `GrB_Matrix` per label via COO format, wraps each in an -`LAGraph_Graph`, and returns an [`InMemoryGraph`](src/graph/inmemory.rs:153). +`LAGraph_Graph`, and returns an [`InMemoryGraph`](src/graph/inmemory.rs:173). Multiple CSV sources can be chained with repeated `.load()` calls; all edges are merged into a single graph. +**Node ID representation:** Internally, `InMemoryBuilder` uses `HashMap` for +`id_to_node` (changed from `Vec` to support sparse/pre-assigned IDs from MatrixMarket). +The [`set_node_map()`](src/graph/inmemory.rs:67) method allows bulk-installing a node mapping, +which is used by the MatrixMarket loader. + ### Format parsers -[`Csv`](src/formats/csv.rs:52) is the only built-in parser. It yields -`Iterator>` and is directly pluggable into -`GraphBuilder::load()` via its `GraphSource` impl. +Two built-in parsers are available: + +#### CSV format + +[`Csv`](src/formats/csv.rs:52) yields `Iterator>` and is +directly pluggable into `GraphBuilder::load()` via its `GraphSource` impl. Configuration is via [`CsvConfig`](src/formats/csv.rs:17): @@ -204,6 +214,26 @@ Configuration is via [`CsvConfig`](src/formats/csv.rs:17): [`ColumnSpec`](src/formats/csv.rs:11) is either `Index(usize)` or `Name(String)`. Name-based lookup requires `has_header: true`. +#### MatrixMarket directory format + +[`MatrixMarket`](src/formats/mm.rs:160) loads an edge-labeled graph from a directory with: + +- `vertices.txt` — one line per node: ` <1-based-index>` on disk; [`get_node_id`](src/graph/mod.rs:199) returns the matching **0-based** matrix index +- `edges.txt` — one line per label: ` <1-based-index>` (selects `n.txt`) +- `.txt` — MatrixMarket adjacency matrix for label with index `n` + +The loader uses [`LAGraph_MMRead`](src/lagraph_sys.rs) to parse each `.txt` file into a +`GrB_Matrix`, then wraps it in an `LAGraph_Graph`. Vertex indices from `vertices.txt` are +converted to 0-based and installed via [`InMemoryBuilder::set_node_map()`](src/graph/inmemory.rs:67). + +Helper functions: + +- [`load_mm_file(path)`](src/formats/mm.rs:64) — reads a single MatrixMarket file into a + `GrB_Matrix`. +- [`parse_index_map(path)`](src/formats/mm.rs) — parses ` ` lines; indices must be **>= 1** and **unique** within the file. + +`MatrixMarket` implements `GraphSource` in [`src/graph/inmemory.rs`](src/graph/inmemory.rs): `vertices.txt` maps are converted from 1-based file indices to 0-based matrix ids before [`set_node_map`](src/graph/inmemory.rs:67); `edges.txt` indices are unchanged for `n.txt` lookup. + ### FFI layer [`lagraph_sys`](src/lagraph_sys.rs) exposes raw C bindings for GraphBLAS and diff --git a/Cargo.toml b/Cargo.toml index 70180c8..4b2d184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,8 @@ thiserror = "1.0" [features] regenerate-bindings = ["bindgen"] +[dev-dependencies] +tempfile = "3" + [build-dependencies] bindgen = { version = "0.71", optional = true } diff --git a/src/formats/mm.rs b/src/formats/mm.rs new file mode 100644 index 0000000..3679bd5 --- /dev/null +++ b/src/formats/mm.rs @@ -0,0 +1,268 @@ +//! MatrixMarket directory format loader. +//! +//! An edge-labeled graph is stored in a directory with the following layout: +//! +//! ```text +//! / +//! vertices.txt — one line per node: ` <1-based-index>` +//! edges.txt — one line per label: ` <1-based-index>` +//! 1.txt — MM adjacency matrix for the label with index 1 +//! 2.txt — MM adjacency matrix for the label with index 2 +//! … +//! ``` +//! +//! # Example +//! +//! ```no_run +//! use pathrex::graph::{Graph, InMemory, GraphDecomposition}; +//! use pathrex::formats::mm::MatrixMarket; +//! +//! let graph = Graph::::try_from( +//! MatrixMarket::from_dir("path/to/graph/dir") +//! ).unwrap(); +//! println!("Nodes: {}", graph.num_nodes()); +//! ``` + +use std::collections::HashMap; +use std::ffi::CString; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::os::fd::IntoRawFd; +use std::path::{Path, PathBuf}; + +use crate::formats::FormatError; +use crate::graph::{GraphError, ensure_grb_init}; +use crate::la_ok; +use crate::lagraph_sys::{FILE, GrB_Matrix, LAGraph_MMRead}; + +/// Read a single MatrixMarket file and return the raw [`GrB_Matrix`]. +pub fn load_mm_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + + ensure_grb_init().map_err(|e| match e { + GraphError::LAGraph(info, msg) => FormatError::MatrixMarket { + code: info, + message: msg, + }, + _ => FormatError::MatrixMarket { + code: crate::lagraph_sys::GrB_Info::GrB_PANIC, + message: "Failed to initialize GraphBLAS".to_string(), + }, + })?; + + let file = File::open(path)?; + let fd = file.into_raw_fd(); + + let c_mode = CString::new("r").unwrap(); + let f = unsafe { libc::fdopen(fd, c_mode.as_ptr()) }; + if f.is_null() { + unsafe { libc::close(fd) }; + return Err(std::io::Error::last_os_error().into()); + } + + let mut matrix: GrB_Matrix = std::ptr::null_mut(); + + let err = la_ok!(LAGraph_MMRead(&mut matrix, f as *mut FILE)); + unsafe { libc::fclose(f) }; + + match err { + Ok(_) => Ok(matrix), + Err(GraphError::LAGraph(info, msg)) => Err(FormatError::MatrixMarket { + code: info, + message: msg, + }), + _ => unreachable!("should be either mm read error or ok"), + } +} + +/// Parse a ` ` mapping file. +/// +/// Throws error on non-positive or duplicate indicies +pub(crate) fn parse_index_map( + path: &Path, +) -> Result<(HashMap, HashMap), FormatError> { + let file_name = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.display().to_string()); + + let reader = BufReader::new(File::open(path)?); + let mut by_idx: HashMap = HashMap::new(); + let mut by_name: HashMap = HashMap::new(); + + for (line_no, line) in reader.lines().enumerate() { + let line = line?; + let line = line.trim(); + if line.is_empty() { + continue; + } + + let (name, idx_str) = + line.rsplit_once(char::is_whitespace) + .ok_or_else(|| FormatError::InvalidFormat { + file: file_name.clone(), + line: line_no + 1, + reason: "expected ' ' but found no whitespace".into(), + })?; + + let idx: usize = idx_str + .trim() + .parse() + .map_err(|_| FormatError::InvalidFormat { + file: file_name.clone(), + line: line_no + 1, + reason: format!( + "index '{}' is not a valid positive integer", + idx_str.trim() + ), + })?; + + if idx == 0 { + return Err(FormatError::InvalidFormat { + file: file_name.clone(), + line: line_no + 1, + reason: "index must be a positive integer (>= 1)".into(), + }); + } + + let name = name.trim().to_owned(); + if by_idx.insert(idx, name.clone()).is_some() { + return Err(FormatError::InvalidFormat { + file: file_name.clone(), + line: line_no + 1, + reason: format!("duplicate index {idx}"), + }); + } + by_name.insert(name, idx); + } + + Ok((by_idx, by_name)) +} + +/// A MatrixMarket directory data source. +/// +/// Reads the graph from a directory that contains: +/// - `vertices.txt` — ` <1-based-index>` mapping +/// - `edges.txt` — ` <1-based-index>` mapping +/// - `.txt` — one MM adjacency matrix per label index +/// # Example +/// +/// ```no_run +/// use pathrex::graph::{Graph, InMemory, GraphDecomposition}; +/// use pathrex::formats::mm::MatrixMarket; +/// +/// let graph = Graph::::try_from( +/// MatrixMarket::from_dir("path/to/graph/dir") +/// ).unwrap(); +/// println!("Nodes: {}", graph.num_nodes()); +/// ``` +pub struct MatrixMarket { + pub(crate) dir: PathBuf, +} + +impl MatrixMarket { + /// Create a `MatrixMarket` source that will load from `dir`. + pub fn from_dir(dir: impl Into) -> Self { + Self { dir: dir.into() } + } + + pub(crate) fn mm_path(&self, idx: usize) -> PathBuf { + self.dir.join(format!("{}.txt", idx)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::TempDir; + + fn write_file(dir: &Path, name: &str, content: &str) { + let mut f = File::create(dir.join(name)).unwrap(); + f.write_all(content.as_bytes()).unwrap(); + } + + #[test] + fn test_parse_index_map_basic() { + let tmp = TempDir::new().unwrap(); + write_file( + tmp.path(), + "vertices.txt", + " 1\n 2\n<1940> 3\n", + ); + let (by_idx, by_name) = parse_index_map(&tmp.path().join("vertices.txt")).unwrap(); + assert_eq!(by_idx[&1], ""); + assert_eq!(by_idx[&2], ""); + assert_eq!(by_idx[&3], "<1940>"); + assert_eq!(by_name[""], 1); + assert_eq!(by_name[""], 2); + } + + #[test] + fn test_parse_index_map_rejects_zero_index() { + let tmp = TempDir::new().unwrap(); + write_file(tmp.path(), "v.txt", " 0\n"); + let err = parse_index_map(&tmp.path().join("v.txt")).unwrap_err(); + assert!(matches!(err, FormatError::InvalidFormat { .. })); + } + + #[test] + fn test_parse_index_map_rejects_duplicate_index() { + let tmp = TempDir::new().unwrap(); + write_file(tmp.path(), "v.txt", " 1\n 1\n"); + let err = parse_index_map(&tmp.path().join("v.txt")).unwrap_err(); + assert!(matches!(err, FormatError::InvalidFormat { .. })); + } + + #[test] + fn test_parse_index_map_empty_lines_ignored() { + let tmp = TempDir::new().unwrap(); + write_file(tmp.path(), "edges.txt", "\n 1\n\n 2\n"); + let (by_idx, _) = parse_index_map(&tmp.path().join("edges.txt")).unwrap(); + assert_eq!(by_idx.len(), 2); + assert_eq!(by_idx[&1], ""); + assert_eq!(by_idx[&2], ""); + } + + #[test] + fn test_parse_index_map_bad_index_returns_error() { + let tmp = TempDir::new().unwrap(); + write_file(tmp.path(), "bad.txt", " notanumber\n"); + let err = parse_index_map(&tmp.path().join("bad.txt")).unwrap_err(); + assert!( + matches!(err, FormatError::InvalidFormat { .. }), + "expected InvalidFormat, got {:?}", + err + ); + } + + #[test] + fn test_parse_index_map_missing_whitespace_returns_error() { + let tmp = TempDir::new().unwrap(); + write_file(tmp.path(), "bad.txt", "nospacehere\n"); + let err = parse_index_map(&tmp.path().join("bad.txt")).unwrap_err(); + assert!(matches!(err, FormatError::InvalidFormat { .. })); + } + + #[test] + fn test_load_nonexistent_mm_file_returns_io_error() { + let result = load_mm_file("/nonexistent/path/to/file.txt"); + assert!( + matches!(result, Err(FormatError::Io(_))), + "expected Io error for missing file, got: {:?}", + result + ); + } + + #[test] + fn test_from_dir_stores_path() { + let src = MatrixMarket::from_dir("/some/path"); + assert_eq!(src.dir, PathBuf::from("/some/path")); + } + + #[test] + fn test_mm_path() { + let src = MatrixMarket::from_dir("/graph"); + assert_eq!(src.mm_path(3), PathBuf::from("/graph/3.txt")); + } +} diff --git a/src/formats/mod.rs b/src/formats/mod.rs index 4aafb79..5ca0cea 100644 --- a/src/formats/mod.rs +++ b/src/formats/mod.rs @@ -14,11 +14,15 @@ //! ``` pub mod csv; +pub mod mm; pub use csv::Csv; +pub use mm::MatrixMarket; use thiserror::Error; +use crate::lagraph_sys::GrB_Info; + /// Unified error type for all format parsing operations. #[derive(Error, Debug)] pub enum FormatError { @@ -33,4 +37,16 @@ pub enum FormatError { /// An I/O error occurred while reading the data source. #[error("I/O error: {0}")] Io(#[from] std::io::Error), + + /// [`LAGraph_MMRead`](crate::lagraph_sys::LAGraph_MMRead) returned a + /// non-zero info code while reading a MatrixMarket file. + #[error("MatrixMarket read error (code {code}): {message}")] + MatrixMarket { code: GrB_Info, message: String }, + + #[error("Invalid format in '{file}' at line {line}: {reason}")] + InvalidFormat { + file: String, + line: usize, + reason: String, + }, } diff --git a/src/graph/inmemory.rs b/src/graph/inmemory.rs index d31916c..103e5fe 100644 --- a/src/graph/inmemory.rs +++ b/src/graph/inmemory.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use std::{collections::HashMap, io::Read}; -use crate::formats::Csv; +use crate::formats::mm::{load_mm_file, parse_index_map}; +use crate::formats::{Csv, MatrixMarket}; use crate::{ graph::GraphSource, lagraph_sys::{GrB_Index, GrB_Matrix, GrB_Matrix_free, LAGraph_Kind}, @@ -33,9 +34,10 @@ impl Backend for InMemory { /// Accumulates edges in RAM and compiles them into an [`InMemoryGraph`]. #[derive(Default)] pub struct InMemoryBuilder { - node_to_id: HashMap, - id_to_node: Vec, - label_buffers: HashMap>, + node_to_id: HashMap, + id_to_node: HashMap, + next_id: usize, + label_buffers: HashMap>, prebuilt: HashMap, } @@ -43,22 +45,41 @@ impl InMemoryBuilder { pub fn new() -> Self { Self { node_to_id: HashMap::new(), - id_to_node: Vec::new(), + id_to_node: HashMap::new(), + next_id: 0, label_buffers: HashMap::new(), prebuilt: HashMap::new(), } } - fn insert_node(&mut self, node: &str) -> u64 { + fn insert_node(&mut self, node: &str) -> usize { if let Some(&id) = self.node_to_id.get(node) { return id; } - let id = self.id_to_node.len() as u64; - self.id_to_node.push(node.to_owned()); + let id = self.next_id; + self.next_id += 1; + self.id_to_node.insert(id, node.to_owned()); self.node_to_id.insert(node.to_owned(), id); id } + /// Bulk-install the node mapping. Replaces any previously inserted nodes. + pub fn set_node_map( + &mut self, + by_idx: HashMap, + by_name: HashMap, + ) { + self.id_to_node = by_idx; + self.node_to_id = by_name; + self.next_id = self + .id_to_node + .keys() + .copied() + .max() + .map(|m| m + 1) + .unwrap_or(0); + } + pub fn push_edge(&mut self, edge: Edge) -> Result<(), GraphError> { let src = self.insert_node(&edge.source); let tgt = self.insert_node(&edge.target); @@ -110,7 +131,13 @@ impl GraphBuilder for InMemoryBuilder { fn build(self) -> Result { ensure_grb_init()?; - let n = self.id_to_node.len() as GrB_Index; + let n: GrB_Index = self + .id_to_node + .keys() + .copied() + .max() + .map(|m| m + 1) + .unwrap_or(0) as GrB_Index; let mut graphs: HashMap> = HashMap::with_capacity(self.label_buffers.len() + self.prebuilt.len()); @@ -120,8 +147,8 @@ impl GraphBuilder for InMemoryBuilder { } for (label, pairs) in &self.label_buffers { - let rows: Vec = pairs.iter().map(|(r, _)| *r).collect(); - let cols: Vec = pairs.iter().map(|(_, c)| *c).collect(); + let rows: Vec = pairs.iter().map(|(r, _)| *r as GrB_Index).collect(); + let cols: Vec = pairs.iter().map(|(_, c)| *c as GrB_Index).collect(); let vals: Vec = vec![true; pairs.len()]; let lg = LagraphGraph::from_coo( @@ -135,14 +162,8 @@ impl GraphBuilder for InMemoryBuilder { graphs.insert(label.clone(), Arc::new(lg)); } - let node_to_id: HashMap = self - .node_to_id - .into_iter() - .map(|(k, v)| (k, v as usize)) - .collect(); - Ok(InMemoryGraph { - node_to_id, + node_to_id: self.node_to_id, id_to_node: self.id_to_node, graphs, }) @@ -152,7 +173,7 @@ impl GraphBuilder for InMemoryBuilder { /// Immutable, read-only Boolean-decomposed graph backed by LAGraph graphs. pub struct InMemoryGraph { node_to_id: HashMap, - id_to_node: Vec, + id_to_node: HashMap, graphs: HashMap>, } @@ -171,7 +192,7 @@ impl GraphDecomposition for InMemoryGraph { } fn get_node_name(&self, mapped_id: usize) -> Option { - self.id_to_node.get(mapped_id).cloned() + self.id_to_node.get(&mapped_id).cloned() } fn num_nodes(&self) -> usize { @@ -191,6 +212,29 @@ impl GraphSource for Csv { } } +impl GraphSource for MatrixMarket { + fn apply_to(self, mut builder: InMemoryBuilder) -> Result { + let vertices_path = self.dir.join("vertices.txt"); + let (vert_by_idx, vert_by_name) = parse_index_map(&vertices_path)?; + let vert_by_idx = + vert_by_idx.into_iter().map(|(i, n)| (i - 1, n)).collect(); + let vert_by_name = + vert_by_name.into_iter().map(|(n, i)| (n, i - 1)).collect(); + + let (edge_by_idx, _) = parse_index_map(&self.dir.join("edges.txt"))?; + + builder.set_node_map(vert_by_idx, vert_by_name); + + for (idx, label) in edge_by_idx { + let path = self.mm_path(idx); + let matrix = load_mm_file(&path)?; + builder.push_grb_matrix(label, matrix)?; + } + + Ok(builder) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 62e1667..d096b6f 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -109,7 +109,7 @@ impl LagraphGraph { Self::new(matrix, kind) } - pub fn check_graph(&self) -> Result<(), GraphError> { + pub fn check_graph(&self) -> Result<(), GraphError> { la_ok!(LAGraph_CheckGraph(self.inner)) } } diff --git a/tests/mm_tests.rs b/tests/mm_tests.rs new file mode 100644 index 0000000..9d1ea99 --- /dev/null +++ b/tests/mm_tests.rs @@ -0,0 +1,201 @@ +use pathrex::formats::mm::MatrixMarket; +use pathrex::graph::{Backend, Graph, GraphDecomposition, GraphSource, InMemory, InMemoryGraph}; + +const GRAPH_DIR: &str = "tests/testdata/mm_graph"; +static INMEMORY_GRAPH: std::sync::LazyLock = + std::sync::LazyLock::new(|| load_graph_from_mm::(GRAPH_DIR)); + +fn load_graph_from_mm(dir: &str) -> B::Graph +where + MatrixMarket: GraphSource, +{ + let mm = MatrixMarket::from_dir(dir); + Graph::::try_from(mm).expect("Failed to load graph") +} + +#[test] +fn test_load_mm_graph_basic() { + let graph = &INMEMORY_GRAPH; + let expected_nodes_number = 24225; + + assert_eq!(graph.num_nodes(), expected_nodes_number); +} + +#[test] +fn test_mm_graph_node_mapping() { + let graph = &INMEMORY_GRAPH; + + let test_nodes = vec![ + ("", 0), + ("<1940>", 1), + ("", 2), + ("", 3), + ]; + + for (name, expected_id) in test_nodes { + let id = graph.get_node_id(name).expect("Node should exist"); + assert_eq!(id, expected_id); + + let retrieved_name = graph + .get_node_name(id) + .expect("ID should map back to a name"); + assert_eq!(retrieved_name, name); + } +} + +#[test] +fn test_mm_graph_edge_labels() { + let graph = &INMEMORY_GRAPH; + + let expected_labels = vec![ + "", + "", + "", + "", + "", + "", + "", + "", + "", + ]; + + for label in expected_labels { + let result = graph.get_graph(label); + assert!(result.is_ok()); + } +} + +#[test] +fn test_mm_graph_nonexistent_label() { + let graph = &INMEMORY_GRAPH; + + let result = graph.get_graph(""); + assert!(result.is_err()); +} + +#[test] +fn test_mm_graph_nonexistent_node() { + let graph = &INMEMORY_GRAPH; + + assert!(graph.get_node_id("").is_none()); + + assert!(graph.get_node_name(999999).is_none()); +} + +#[test] +fn test_mm_graph_specific_nodes_exist() { + let graph = &INMEMORY_GRAPH; + + let nodes_to_check = vec![ + "", + "", + "", + "", + "<1940>", + "<1950>", + "<1960>", + "", + "", + ]; + + for node in nodes_to_check { + assert!(graph.get_node_id(node).is_some()); + } +} + +#[test] +fn test_mm_graph_matrix_dimensions() { + let graph = &INMEMORY_GRAPH; + + let expected_labels = vec![ + "", + "", + "", + "", + "", + "", + "", + "", + "", + ]; + + for label in expected_labels { + let matrix = graph + .get_graph(label) + .expect(&format!("Should have matrix for label {}", label)); + matrix + .check_graph() + .expect(&format!("Matrix for {} should be valid", label)); + } +} + +#[test] +fn test_mm_graph_load_from_nonexistent_dir() { + let mm = MatrixMarket::from_dir("tests/testdata/nonexistent_dir"); + let result = Graph::::try_from(mm); + + assert!( + result.is_err(), + "Loading from nonexistent directory should fail" + ); +} + +#[test] +fn test_mm_graph_edge_label_mapping() { + let mm = MatrixMarket::from_dir("tests/testdata/mm_graph"); + let graph = Graph::::try_from(mm).expect("Failed to load graph"); + + // Test that edge labels map correctly to their index files + // From edges.txt: + // 1 -> 1.txt + // 2 -> 2.txt + // 3 -> 3.txt + // etc. + + let label_to_file = vec![ + ("", "1.txt"), + ("", "2.txt"), + ("", "3.txt"), + ("", "4.txt"), + ("", "5.txt"), + ("", "6.txt"), + ("", "7.txt"), + ("", "8.txt"), + ("", "9.txt"), + ]; + + for (label, _file) in label_to_file { + let result = graph.get_graph(label); + assert!( + result.is_ok(), + "Label {} should be loaded from its corresponding matrix file", + label + ); + } +} + +#[test] +fn test_mm_graph_handles_large_indices() { + let mm = MatrixMarket::from_dir("tests/testdata/mm_graph"); + let graph = Graph::::try_from(mm).expect("Failed to load graph"); + + let num_nodes = graph.num_nodes(); + + let high_index = num_nodes - 1; + let name = graph.get_node_name(high_index); + assert!( + name.is_some(), + "Should be able to retrieve node at last matrix index {}", + high_index + ); +} + +#[test] +fn test_mm_graph_empty_label_handling() { + let mm = MatrixMarket::from_dir("tests/testdata/mm_graph"); + let graph = Graph::::try_from(mm).expect("Failed to load graph"); + + // Test that empty string label is handled correctly + let result = graph.get_graph(""); + assert!(result.is_err(), "Empty label should not exist in the graph"); +} diff --git a/tests/testdata/mm_graph/1.txt b/tests/testdata/mm_graph/1.txt new file mode 100644 index 0000000..07f9f9f --- /dev/null +++ b/tests/testdata/mm_graph/1.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e114b66c664e918db9c0028b807bf71489823c59f2343c869247fd95ffd2fea +size 177568 diff --git a/tests/testdata/mm_graph/2.txt b/tests/testdata/mm_graph/2.txt new file mode 100644 index 0000000..c5396bd --- /dev/null +++ b/tests/testdata/mm_graph/2.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad246a187678aaca451a5ee25b5d93a0a5b6d2750d25d48216f2925b9d7a8856 +size 362093 diff --git a/tests/testdata/mm_graph/3.txt b/tests/testdata/mm_graph/3.txt new file mode 100644 index 0000000..748983c --- /dev/null +++ b/tests/testdata/mm_graph/3.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f02d2650d78733ff138fcab4729e5b884f4b05ef8b8d5048b872a84166c34a5 +size 1901 diff --git a/tests/testdata/mm_graph/4.txt b/tests/testdata/mm_graph/4.txt new file mode 100644 index 0000000..6c47e14 --- /dev/null +++ b/tests/testdata/mm_graph/4.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b29aac24c99d3c8352611e028fad339b185f7f9659982707b86df51c617c4c75 +size 27389 diff --git a/tests/testdata/mm_graph/5.txt b/tests/testdata/mm_graph/5.txt new file mode 100644 index 0000000..744a21d --- /dev/null +++ b/tests/testdata/mm_graph/5.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:637d470d35589c61f00193a84daeffa7077f915a8dec87f60976e998ce0f8c07 +size 13510 diff --git a/tests/testdata/mm_graph/6.txt b/tests/testdata/mm_graph/6.txt new file mode 100644 index 0000000..3b27bc1 --- /dev/null +++ b/tests/testdata/mm_graph/6.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b892febf7dbb7cd216e2a9a820a6e5df896fbef00d790f26f6249c6a9ae4c7d +size 895 diff --git a/tests/testdata/mm_graph/7.txt b/tests/testdata/mm_graph/7.txt new file mode 100644 index 0000000..85c0207 --- /dev/null +++ b/tests/testdata/mm_graph/7.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f226d0f9ef8fdd563796608a786d9b7c7170032d492a9f23c56f2aeeab083c39 +size 79217 diff --git a/tests/testdata/mm_graph/8.txt b/tests/testdata/mm_graph/8.txt new file mode 100644 index 0000000..74ea982 --- /dev/null +++ b/tests/testdata/mm_graph/8.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:182207280048c1eeeda6565ec3eb02af076be85df5798b5c41403f4d46f8439d +size 180 diff --git a/tests/testdata/mm_graph/9.txt b/tests/testdata/mm_graph/9.txt new file mode 100644 index 0000000..ed0f398 --- /dev/null +++ b/tests/testdata/mm_graph/9.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d7759cc89917da2541cd459c203b312ca37fb97ba694e200703e4bbf7d3169a +size 248 diff --git a/tests/testdata/mm_graph/edges.txt b/tests/testdata/mm_graph/edges.txt new file mode 100644 index 0000000..7e785c6 --- /dev/null +++ b/tests/testdata/mm_graph/edges.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:466ef6e13673ac095b86ce9043d544f1415efcbe51fc1d74927b67e7265bb282 +size 110 diff --git a/tests/testdata/mm_graph/vertices.txt b/tests/testdata/mm_graph/vertices.txt new file mode 100644 index 0000000..c659e80 --- /dev/null +++ b/tests/testdata/mm_graph/vertices.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99af2476c08f57dd2f7e37b09ae3b56ee684cff7131eeabe7fb31e2b973ee8c4 +size 596769