Skip to content

Commit 8b13f5d

Browse files
authored
fix: Fuzzer reference compare results in null when comparing to null (#2676)
1 parent 9f99583 commit 8b13f5d

File tree

6 files changed

+160
-52
lines changed

6 files changed

+160
-52
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ arrow-buffer = { workspace = true }
2222
arrow-ord = { workspace = true }
2323
futures-util = { workspace = true }
2424
libfuzzer-sys = { workspace = true }
25+
thiserror = { workspace = true }
2526
tokio = { workspace = true, features = ["full"] }
2627
vortex-array = { workspace = true, features = ["arbitrary"] }
2728
vortex-btrblocks = { workspace = true }

fuzz/fuzz_targets/array_ops.rs

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![no_main]
2+
#![allow(clippy::unwrap_used)]
23

34
use libfuzzer_sys::{Corpus, fuzz_target};
45
use vortex_array::aliases::hash_set::HashSet;
@@ -12,7 +13,8 @@ use vortex_array::compute::{
1213
use vortex_array::vtable::EncodingVTable;
1314
use vortex_array::{Array, ArrayRef};
1415
use vortex_btrblocks::BtrBlocksCompressor;
15-
use vortex_error::VortexUnwrap;
16+
use vortex_error::{VortexUnwrap, vortex_panic};
17+
use vortex_fuzz::error::{VortexFuzzError, VortexFuzzResult};
1618
use vortex_fuzz::{Action, FuzzArrayAction, sort_canonical_array};
1719
use vortex_scalar::Scalar;
1820

@@ -25,18 +27,18 @@ fuzz_target!(|fuzz_action: FuzzArrayAction| -> Corpus {
2527
current_array = BtrBlocksCompressor
2628
.compress(current_array.to_canonical().vortex_unwrap().as_ref())
2729
.vortex_unwrap();
28-
assert_array_eq(&expected.array(), &current_array, i);
30+
assert_array_eq(&expected.array(), &current_array, i).unwrap();
2931
}
3032
Action::Slice(range) => {
3133
current_array = slice(&current_array, range.start, range.end).vortex_unwrap();
32-
assert_array_eq(&expected.array(), &current_array, i);
34+
assert_array_eq(&expected.array(), &current_array, i).unwrap();
3335
}
3436
Action::Take(indices) => {
3537
if indices.is_empty() {
3638
return Corpus::Reject;
3739
}
3840
current_array = take(&current_array, &indices).vortex_unwrap();
39-
assert_array_eq(&expected.array(), &current_array, i);
41+
assert_array_eq(&expected.array(), &current_array, i).unwrap();
4042
}
4143
Action::SearchSorted(s, side) => {
4244
// TODO(robert): Ideally we'd preserve the encoding perfectly but this is close enough
@@ -53,20 +55,26 @@ fuzz_target!(|fuzz_action: FuzzArrayAction| -> Corpus {
5355
{
5456
sorted = BtrBlocksCompressor.compress(&sorted).vortex_unwrap();
5557
}
56-
assert_search_sorted(sorted, s, side, expected.search(), i)
58+
assert_search_sorted(sorted, s, side, expected.search(), i).unwrap()
5759
}
5860
Action::Filter(mask) => {
5961
current_array = filter(&current_array, &mask).vortex_unwrap();
60-
assert_array_eq(&expected.array(), &current_array, i);
62+
assert_array_eq(&expected.array(), &current_array, i).unwrap();
6163
}
6264
Action::Compare(v, op) => {
63-
current_array = compare(
65+
let compare_result = compare(
6466
&current_array,
65-
&ConstantArray::new(v, current_array.len()).into_array(),
67+
&ConstantArray::new(v.clone(), current_array.len()).into_array(),
6668
op,
6769
)
6870
.vortex_unwrap();
69-
assert_array_eq(&expected.array(), &current_array, i);
71+
if let Err(e) = assert_array_eq(&expected.array(), &compare_result, i) {
72+
vortex_panic!(
73+
"Failed to compare {}with {op} {v}\nError: {e}",
74+
current_array.tree_display()
75+
)
76+
}
77+
current_array = compare_result;
7078
}
7179
}
7280
}
@@ -79,38 +87,47 @@ fn assert_search_sorted(
7987
side: SearchSortedSide,
8088
expected: SearchResult,
8189
step: usize,
82-
) {
90+
) -> VortexFuzzResult<()> {
8391
let search_result = search_sorted(&array, s.clone(), side).vortex_unwrap();
84-
assert_eq!(
85-
expected,
86-
search_result,
87-
"Expected to find {s}({}) at {expected} in {} from {side} but instead found it at {search_result} in step {step}",
88-
s.dtype(),
89-
array.tree_display()
90-
);
92+
if search_result != expected {
93+
Err(VortexFuzzError::SearchSortedError(
94+
s,
95+
expected,
96+
array.to_array(),
97+
side,
98+
search_result,
99+
step,
100+
))
101+
} else {
102+
Ok(())
103+
}
91104
}
92105

93106
// TODO(ngates): this is horrific... we should have an array_equals compute function?
94-
fn assert_array_eq(lhs: &dyn Array, rhs: &dyn Array, step: usize) {
95-
assert_eq!(
96-
lhs.len(),
97-
rhs.len(),
98-
"LHS len {} != RHS len {}, lhs is {} rhs is {} in step {step}",
99-
lhs.len(),
100-
rhs.len(),
101-
lhs.tree_display(),
102-
rhs.tree_display()
103-
);
107+
fn assert_array_eq(lhs: &ArrayRef, rhs: &ArrayRef, step: usize) -> VortexFuzzResult<()> {
108+
if lhs.len() != rhs.len() {
109+
return Err(VortexFuzzError::LengthMismatch(
110+
lhs.len(),
111+
rhs.len(),
112+
lhs.to_array(),
113+
rhs.to_array(),
114+
step,
115+
));
116+
}
104117
for idx in 0..lhs.len() {
105118
let l = scalar_at(lhs, idx).vortex_unwrap();
106119
let r = scalar_at(rhs, idx).vortex_unwrap();
107120

108-
assert_eq!(
109-
l,
110-
r,
111-
"{l} != {r} at index {idx}, lhs is {} rhs is {} in step {step}",
112-
lhs.tree_display(),
113-
rhs.tree_display()
114-
);
121+
if l != r {
122+
return Err(VortexFuzzError::ArrayNotEqual(
123+
l,
124+
r,
125+
idx,
126+
lhs.to_array(),
127+
rhs.to_array(),
128+
step,
129+
));
130+
}
115131
}
132+
Ok(())
116133
}

fuzz/src/compare.rs

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1+
use std::fmt::Debug;
2+
use std::ops::Deref;
3+
4+
use arrow_buffer::BooleanBuffer;
15
use vortex_array::accessor::ArrayAccessor;
26
use vortex_array::arrays::BoolArray;
37
use vortex_array::compute::{Operator, scalar_at, scalar_cmp};
8+
use vortex_array::validity::Validity;
49
use vortex_array::{Array, ArrayRef, ToCanonical};
5-
use vortex_dtype::{DType, match_each_native_ptype};
6-
use vortex_error::VortexResult;
10+
use vortex_dtype::{DType, NativePType, match_each_native_ptype};
11+
use vortex_error::{VortexExpect, VortexResult};
712
use vortex_scalar::Scalar;
813

914
pub fn compare_canonical_array(
1015
array: &dyn Array,
1116
value: &Scalar,
1217
operator: Operator,
1318
) -> VortexResult<ArrayRef> {
19+
if value.is_null() {
20+
return Ok(
21+
BoolArray::new(BooleanBuffer::new_unset(array.len()), Validity::AllInvalid)
22+
.into_array(),
23+
);
24+
}
25+
1426
match array.dtype() {
1527
DType::Bool(_) => {
16-
let bool = value.as_bool().value();
28+
let bool = value
29+
.as_bool()
30+
.value()
31+
.vortex_expect("nulls handled before");
1732
Ok(compare_to(
1833
array
1934
.to_bool()?
@@ -29,8 +44,8 @@ pub fn compare_canonical_array(
2944
let primitive = value.as_primitive();
3045
let primitive_array = array.to_primitive()?;
3146
match_each_native_ptype!(p, |$P| {
32-
let pval = primitive.typed_value::<$P>();
33-
Ok(compare_to(
47+
let pval = primitive.typed_value::<$P>().vortex_expect("nulls handled before");
48+
Ok(compare_native_ptype(
3449
primitive_array
3550
.as_slice::<$P>()
3651
.iter()
@@ -43,20 +58,26 @@ pub fn compare_canonical_array(
4358
})
4459
}
4560
DType::Utf8(_) => array.to_varbinview()?.with_iterator(|iter| {
46-
let utf8_value = value.as_utf8().value();
61+
let utf8_value = value
62+
.as_utf8()
63+
.value()
64+
.vortex_expect("nulls handled before");
4765
compare_to(
4866
iter.map(|v| v.map(|b| unsafe { str::from_utf8_unchecked(b) })),
49-
utf8_value.as_deref(),
67+
utf8_value.deref(),
5068
operator,
5169
)
5270
}),
5371
DType::Binary(_) => array.to_varbinview()?.with_iterator(|iter| {
54-
let binary_value = value.as_binary().value();
72+
let binary_value = value
73+
.as_binary()
74+
.value()
75+
.vortex_expect("nulls handled before");
5576
compare_to(
5677
// Don't understand the lifetime problem here but identity map makes it go away
5778
#[allow(clippy::map_identity)]
5879
iter.map(|v| v),
59-
binary_value.as_deref(),
80+
binary_value.deref(),
6081
operator,
6182
)
6283
}),
@@ -75,18 +96,38 @@ pub fn compare_canonical_array(
7596
}
7697
}
7798

78-
fn compare_to<T: PartialOrd + PartialEq>(
99+
fn compare_to<T: PartialOrd + PartialEq + Debug>(
100+
values: impl Iterator<Item = Option<T>>,
101+
cmp_value: T,
102+
operator: Operator,
103+
) -> ArrayRef {
104+
BoolArray::from_iter(values.map(|val| {
105+
val.map(|v| match operator {
106+
Operator::Eq => v == cmp_value,
107+
Operator::NotEq => v != cmp_value,
108+
Operator::Gt => v > cmp_value,
109+
Operator::Gte => v >= cmp_value,
110+
Operator::Lt => v < cmp_value,
111+
Operator::Lte => v <= cmp_value,
112+
})
113+
}))
114+
.into_array()
115+
}
116+
117+
fn compare_native_ptype<T: NativePType>(
79118
values: impl Iterator<Item = Option<T>>,
80-
value: Option<T>,
119+
cmp_value: T,
81120
operator: Operator,
82121
) -> ArrayRef {
83-
BoolArray::from_iter(values.map(|v| match operator {
84-
Operator::Eq => v == value,
85-
Operator::NotEq => v != value,
86-
Operator::Gt => v > value,
87-
Operator::Gte => v >= value,
88-
Operator::Lt => v < value,
89-
Operator::Lte => v <= value,
122+
BoolArray::from_iter(values.map(|val| {
123+
val.map(|v| match operator {
124+
Operator::Eq => v.is_eq(cmp_value),
125+
Operator::NotEq => !v.is_eq(cmp_value),
126+
Operator::Gt => v.is_gt(cmp_value),
127+
Operator::Gte => v.is_ge(cmp_value),
128+
Operator::Lt => v.is_lt(cmp_value),
129+
Operator::Lte => v.is_le(cmp_value),
130+
})
90131
}))
91132
.into_array()
92133
}

fuzz/src/error.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use std::fmt;
2+
use std::fmt::{Debug, Display, Formatter};
3+
4+
use vortex_array::ArrayRef;
5+
use vortex_array::compute::{SearchResult, SearchSortedSide};
6+
use vortex_error::VortexError;
7+
use vortex_scalar::Scalar;
8+
9+
fn tree_display(arr: &ArrayRef) -> impl Display {
10+
arr.tree_display()
11+
}
12+
13+
#[derive(thiserror::Error)]
14+
pub enum VortexFuzzError {
15+
#[error("Expected to find {0} at {1} in {tree} from {3} but instead found it at {4} in step {5}", tree = tree_display(.2))]
16+
SearchSortedError(
17+
Scalar,
18+
SearchResult,
19+
ArrayRef,
20+
SearchSortedSide,
21+
SearchResult,
22+
usize,
23+
),
24+
25+
#[error("{0} != {1} at index {2}, lhs is {lhs_tree} rhs is {rhs_tree} in step {5}",lhs_tree = tree_display(.3), rhs_tree = tree_display(.4))]
26+
ArrayNotEqual(Scalar, Scalar, usize, ArrayRef, ArrayRef, usize),
27+
28+
#[error("LHS len {0} != RHS len {1}, lhs is {lhs_tree} rhs is {rhs_tree} in step {4}", lhs_tree = tree_display(.2), rhs_tree = tree_display(.3))]
29+
LengthMismatch(usize, usize, ArrayRef, ArrayRef, usize),
30+
31+
#[error(transparent)]
32+
VortexError(
33+
#[from]
34+
#[backtrace]
35+
VortexError,
36+
),
37+
}
38+
39+
impl Debug for VortexFuzzError {
40+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
41+
Display::fmt(self, f)
42+
}
43+
}
44+
45+
pub type VortexFuzzResult<T> = Result<T, VortexFuzzError>;

fuzz/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
#![feature(error_generic_member_access)]
2+
13
mod compare;
4+
pub mod error;
25
mod filter;
36
mod search_sorted;
47
mod slice;

0 commit comments

Comments
 (0)