Skip to content

Commit ebc393f

Browse files
committed
Equality functions now return structured errors
1 parent e72c71c commit ebc393f

File tree

7 files changed

+181
-50
lines changed

7 files changed

+181
-50
lines changed

Cargo.toml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ resolver = "2"
33
members = ["algo", "cli", "webgraph"]
44

55
[workspace.dependencies]
6-
webgraph = {path="./webgraph", version="0.3.0"}
7-
webgraph-algo = {path="./algo", version="0.2.0"}
8-
webgraph-cli = {path="./cli", version="0.1.0"}
6+
webgraph = { path = "./webgraph", version = "0.3.0" }
7+
webgraph-algo = { path = "./algo", version = "0.2.0" }
8+
webgraph-cli = { path = "./cli", version = "0.1.0" }
99

1010
card-est-array = "0.1.0"
1111
epserde = "0.8.0"
@@ -35,13 +35,14 @@ serde = { version = "1.0.217", features = ["serde_derive"] }
3535
serde_json = "1.0.137"
3636
zstd = "0.13"
3737
value-traits = "0.1.3"
38+
thiserror = "2.0.12"
3839

3940
[profile.release]
40-
opt-level = 3 # like --release
41+
opt-level = 3 # like --release
4142
#lto = "fat" # Full LTO
42-
overflow-checks = false # Disable integer overflow checks.
43-
debug = true # Include debug info.
44-
debug-assertions = false # Enables debug assertions.
43+
overflow-checks = false # Disable integer overflow checks.
44+
debug = true # Include debug info.
45+
debug-assertions = false # Enables debug assertions.
4546
#codegen-units = 1 # slower compile times, but maybe better perf
4647

4748
[profile.bench]

webgraph/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ predicates.workspace = true
4545
java-properties.workspace = true
4646
sync-cell-slice.workspace = true
4747
arbitrary = { workspace = true, optional = true }
48+
thiserror.workspace = true
4849

4950
libc = "0.2.155"
5051
impl-tools = "0.11.2"

webgraph/src/graphs/csr_graph.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,9 @@ mod test {
374374
let csr = <CsrGraph>::from_seq_graph(&g);
375375
labels::check_impl(&csr);
376376
// We should be able to use eq_sorted
377-
graph::eq(&g, &csr);
377+
assert!(graph::eq(&g, &csr).is_ok());
378378

379-
let csr = CompressedCsrGraph::from_graph(&g);
379+
let _csr = CompressedCsrGraph::from_graph(&g);
380380
/*graph::eq(&g, &csr);
381381
labels::check_impl(&csr);*/
382382
}

webgraph/src/traits/graph.rs

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use impl_tools::autoimpl;
4343
use lender::*;
4444

4545
use super::{
46+
labels::EqError,
4647
lenders::{LenderIntoIter, NodeLabelsLender},
4748
SortedIterator, SortedLender,
4849
};
@@ -62,13 +63,16 @@ struct this_method_cannot_be_called_use_successors_instead;
6263
#[autoimpl(for<S: trait + ?Sized> &S, &mut S, Rc<S>)]
6364
pub trait SequentialGraph: SequentialLabeling<Label = usize> {}
6465

65-
/// Returns true if the two provided graphs with sorted lenders are equal.
66+
/// Checks if the two provided graphs with sorted lenders are equal.
6667
///
6768
/// This associated function can be used to compare graphs with [sorted
6869
/// lenders](crate::lenders::SortedLender), but whose iterators [are not
6970
/// sorted](crate::lenders::SortedIterator). If the graphs are sorted,
7071
/// [`SequentialLabeling::eq_sorted`] should be used instead.
71-
pub fn eq<G0: SequentialGraph, G1: SequentialGraph>(g0: &G0, g1: &G1) -> bool
72+
///
73+
/// If the two graphs are different, an [`EqError`] is returned describing
74+
/// the first difference found.
75+
pub fn eq<G0: SequentialGraph, G1: SequentialGraph>(g0: &G0, g1: &G1) -> Result<(), EqError>
7276
where
7377
for<'a> G0::Lender<'a>: SortedLender,
7478
for<'a> G1::Lender<'a>: SortedLender,
@@ -77,19 +81,20 @@ where
7781
// but due to current limitations of the borrow checker, we would need to
7882
// make G0 and G1 'static.
7983
if g0.num_nodes() != g1.num_nodes() {
80-
return false;
84+
return Err(EqError::NumNodes {
85+
first: g0.num_nodes(),
86+
second: g1.num_nodes(),
87+
});
8188
}
8289
for_!(((node0, succ0), (node1, succ1)) in g0.iter().zip(g1.iter()) {
8390
debug_assert_eq!(node0, node1);
8491
let mut succ0 = succ0.into_iter().collect::<Vec<_>>();
8592
let mut succ1 = succ1.into_iter().collect::<Vec<_>>();
8693
succ0.sort();
8794
succ1.sort();
88-
if succ0 != succ1 {
89-
return false;
90-
}
95+
super::labels::eq_succs(node0, succ0, succ1)?;
9196
});
92-
true
97+
Ok(())
9398
}
9499

95100
/// Convenience type alias for the iterator over the successors of a node
@@ -155,36 +160,35 @@ pub trait RandomAccessGraph: RandomAccessLabeling<Label = usize> + SequentialGra
155160
#[autoimpl(for<S: trait + ?Sized> &S, &mut S, Rc<S>)]
156161
pub trait LabeledSequentialGraph<L>: SequentialLabeling<Label = (usize, L)> {}
157162

158-
/// Returns true if the two provided labeled graphs with sorted lenders are
159-
/// equal.
163+
/// Checks if the two provided labeled graphs with sorted lenders are equal.
160164
///
161165
/// This associated function can be used to compare graphs with [sorted
162166
/// lenders](crate::lenders::SortedLender), but whose iterators [are not
163167
/// sorted](crate::lenders::SortedIterator). If the graphs are sorted,
164168
/// [`SequentialLabeling::eq_sorted`] should be used instead.
169+
///
170+
/// If the two graphs are different, an [`EqError`] is returned describing
171+
/// the first difference found.
165172
pub fn eq_labeled<M, G0: LabeledSequentialGraph<M>, G1: LabeledSequentialGraph<M>>(
166173
g0: &G0,
167174
g1: &G1,
168-
) -> bool
175+
) -> Result<(), EqError>
169176
where
170177
for<'a> G0::Lender<'a>: SortedLender,
171178
for<'a> G1::Lender<'a>: SortedLender,
172-
M: PartialEq,
179+
M: PartialEq + std::fmt::Debug,
173180
{
174181
if g0.num_nodes() != g1.num_nodes() {
175-
return false;
182+
return Err(EqError::NumNodes {
183+
first: g0.num_nodes(),
184+
second: g1.num_nodes(),
185+
});
176186
}
177187
for_!(((node0, succ0), (node1, succ1)) in g0.iter().zip(g1.iter()) {
178188
debug_assert_eq!(node0, node1);
179-
let mut succ0 = succ0.into_iter().collect::<Vec<_>>();
180-
let mut succ1 = succ1.into_iter().collect::<Vec<_>>();
181-
succ0.sort_by_key(|x| x.0);
182-
succ1.sort_by_key(|x| x.0);
183-
if succ0 != succ1 {
184-
return false;
185-
}
189+
super::labels::eq_succs(node0, succ0, succ1)?;
186190
});
187-
true
191+
Ok(())
188192
}
189193

190194
/// A wrapper associating to each successor the label `()`.

webgraph/src/traits/labels.rs

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use impl_tools::autoimpl;
3434
use lender::*;
3535
use rayon::ThreadPool;
3636
use std::rc::Rc;
37+
use thiserror::Error;
3738

3839
use sux::{traits::Succ, utils::FairChunks};
3940

@@ -198,31 +199,105 @@ pub trait SequentialLabeling {
198199
}
199200
}
200201

201-
/// Returns true if the two provided sorted labelings are equal.
202+
/// Error types that can occur during graph equality checking.
203+
#[derive(Error, Debug, Clone, PartialEq, Eq)]
204+
pub enum EqError {
205+
/// The graphs have different numbers of nodes.
206+
#[error("Different number of nodes: {first} != {second}")]
207+
NumNodes { first: usize, second: usize },
208+
209+
/// The graphs have different numbers of arcs.
210+
#[error("Different number of arcs: {first} !={second}")]
211+
NumArcs { first: u64, second: u64 },
212+
213+
/// The graphs have different successors for a specific node.
214+
#[error("Different successors for node {node}: at index {index} {first} != {second}")]
215+
Successors {
216+
node: usize,
217+
index: usize,
218+
first: String,
219+
second: String,
220+
},
221+
222+
/// The graphs have different outdegrees for a specific node.
223+
#[error("Different outdegree for node {node}: {first} != {second}")]
224+
Outdegree {
225+
node: usize,
226+
first: usize,
227+
second: usize,
228+
},
229+
}
230+
231+
#[doc(hidden)]
232+
/// Checks whether two sorted successors lists are identical,
233+
/// returning an appropriate error.
234+
pub fn eq_succs<L: PartialEq + std::fmt::Debug>(
235+
node: usize,
236+
succ0: impl IntoIterator<Item = L>,
237+
succ1: impl IntoIterator<Item = L>,
238+
) -> Result<(), EqError> {
239+
let mut succ0 = succ0.into_iter();
240+
let mut succ1 = succ1.into_iter();
241+
let mut index = 0;
242+
loop {
243+
match (succ0.next(), succ1.next()) {
244+
(None, None) => return Ok(()),
245+
(Some(s0), Some(s1)) => {
246+
if s0 != s1 {
247+
return Err(EqError::Successors {
248+
node,
249+
index,
250+
first: format!("{:?}", s0),
251+
second: format!("{:?}", s1),
252+
});
253+
}
254+
}
255+
(None, Some(_)) => {
256+
return Err(EqError::Outdegree {
257+
node,
258+
first: index,
259+
second: index + 1 + succ1.count(),
260+
});
261+
}
262+
(Some(_), None) => {
263+
return Err(EqError::Outdegree {
264+
node,
265+
first: index + 1 + succ0.count(),
266+
second: index,
267+
});
268+
}
269+
}
270+
index += 1;
271+
}
272+
}
273+
274+
/// Checks if the two provided sorted labelings are equal.
202275
///
203-
/// Since graphs are labelings, this function can also be used
204-
/// to check whether sorted graphs are equal.
276+
/// Since graphs are labelings, this function can also be used to check whether
277+
/// sorted graphs are equal. If the graphs are different, an [`EqError`] is
278+
/// returned describing the first difference found.
205279
pub fn eq_sorted<L0: SequentialLabeling, L1: SequentialLabeling<Label = L0::Label>>(
206280
l0: &L0,
207281
l1: &L1,
208-
) -> bool
282+
) -> Result<(), EqError>
209283
where
210284
for<'a> L0::Lender<'a>: SortedLender,
211285
for<'a> L1::Lender<'a>: SortedLender,
212286
for<'a, 'b> LenderIntoIter<'b, L0::Lender<'a>>: SortedIterator,
213287
for<'a, 'b> LenderIntoIter<'b, L0::Lender<'a>>: SortedIterator,
214-
L0::Label: PartialEq,
288+
L0::Label: PartialEq + std::fmt::Debug,
215289
{
216290
if l0.num_nodes() != l1.num_nodes() {
217-
return false;
291+
return Err(EqError::NumNodes {
292+
first: l0.num_nodes(),
293+
second: l1.num_nodes(),
294+
});
218295
}
219296
for_!(((node0, succ0), (node1, succ1)) in l0.iter().zip(l1.iter()) {
220297
debug_assert_eq!(node0, node1);
221-
if !succ0.into_iter().eq(succ1.into_iter()) {
222-
return false;
223-
}
298+
eq_succs(node0, succ0, succ1)?;
224299
});
225-
true
300+
Ok(())
226301
}
227302

228303
/// Convenience type alias for the iterator over the labels of a node

webgraph/tests/test_labels_graphs.rs

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ use webgraph::graphs::vec_graph::VecGraph;
99
use webgraph::prelude::*;
1010

1111
#[test]
12-
fn test_eq() {
12+
fn test_eq() -> anyhow::Result<()> {
1313
let arcs = vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)];
1414
let g0 = VecGraph::from_arcs(arcs.iter().copied());
1515
let mut g1 = g0.clone();
16-
assert!(labels::eq_sorted(&g0, &g1));
17-
assert!(graph::eq(&g0, &g1));
16+
labels::eq_sorted(&g0, &g1)?;
17+
graph::eq(&g0, &g1)?;
1818
g1.add_arc(0, 3);
19-
assert!(!labels::eq_sorted(&g0, &g1));
20-
assert!(!graph::eq(&g0, &g1));
19+
assert!(labels::eq_sorted(&g0, &g1).is_err());
20+
assert!(graph::eq(&g0, &g1).is_err());
2121

2222
let arcs = vec![
2323
(0, 1, 0),
@@ -29,9 +29,60 @@ fn test_eq() {
2929
];
3030
let g0 = LabeledVecGraph::<usize>::from_arcs(arcs.iter().copied());
3131
let mut g1 = g0.clone();
32-
assert!(labels::eq_sorted(&g0, &g1));
33-
assert!(graph::eq_labeled(&g0, &g1));
32+
labels::eq_sorted(&g0, &g1)?;
33+
graph::eq_labeled(&g0, &g1)?;
3434
g1.add_arc(0, 3, 6);
35-
assert!(!labels::eq_sorted(&g0, &g1));
36-
assert!(!graph::eq_labeled(&g0, &g1));
35+
assert!(labels::eq_sorted(&g0, &g1).is_err());
36+
assert!(graph::eq_labeled(&g0, &g1).is_err());
37+
Ok(())
38+
}
39+
40+
#[test]
41+
fn test_graph_eq_error() -> anyhow::Result<()> {
42+
// Test eq function with different successors
43+
let arcs1 = vec![(0, 0), (0, 2), (1, 2)];
44+
let arcs2 = vec![(0, 0), (0, 1), (1, 2)]; // Different successor for node 0
45+
let g1 = VecGraph::from_arcs(arcs1.iter().copied());
46+
let mut g2 = VecGraph::from_arcs(arcs2.iter().copied());
47+
48+
let result = graph::eq(&g1, &g2);
49+
if let Err(EqError::Successors { node, index, .. }) = result {
50+
assert_eq!(node, 0);
51+
assert_eq!(index, 1);
52+
} else {
53+
panic!("Expected Successors error, got: {:?}", result);
54+
}
55+
56+
g2.add_node(3);
57+
let result = graph::eq(&g1, &g2);
58+
if let Err(EqError::NumNodes {
59+
first: 3,
60+
second: 4,
61+
}) = result
62+
{
63+
} else {
64+
panic!("Expected NumNodes error, got: {:?}", result);
65+
}
66+
67+
// Test eq_labeled function with different labels
68+
let labeled_arcs1 = vec![(0, 1, "a"), (0, 2, "b"), (1, 2, "c")];
69+
let labeled_arcs2 = vec![(0, 1, "a"), (0, 2, "x"), (1, 2, "c")]; // Different label for arc (0,2)
70+
let lg1 = LabeledVecGraph::from_arcs(labeled_arcs1.iter().copied());
71+
let lg2 = LabeledVecGraph::from_arcs(labeled_arcs2.iter().copied());
72+
73+
let result = graph::eq_labeled(&lg1, &lg2);
74+
assert!(result.is_err());
75+
76+
if let Err(EqError::Successors { node, index, .. }) = result {
77+
assert_eq!(node, 0);
78+
assert_eq!(index, 1);
79+
} else {
80+
panic!("Expected Successors error, got: {:?}", result);
81+
}
82+
83+
// Test successful equality
84+
graph::eq(&g1, &g1)?;
85+
86+
graph::eq_labeled(&lg1, &lg1)?;
87+
Ok(())
3788
}

webgraph/tests/test_par_bvcomp.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use std::path::PathBuf;
1111
use anyhow::Result;
1212
use dsi_bitstream::prelude::*;
1313
use dsi_progress_logger::prelude::*;
14-
use lender::*;
1514
use log::info;
1615
use webgraph::prelude::*;
1716

@@ -75,7 +74,7 @@ fn _test_par_bvcomp(basename: &str) -> Result<()> {
7574
.load()?;
7675

7776
info!("Checking that the newly compressed graph is equivalent to the original one...");
78-
assert!(graph::eq(&graph, &comp_graph));
77+
graph::eq(&graph, &comp_graph)?;
7978

8079
let offsets_path = tmp_basename.with_extension(OFFSETS_EXTENSION);
8180
let mut offsets_reader =

0 commit comments

Comments
 (0)