• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16632364072

30 Jul 2025 07:52PM UTC coverage: 83.037% (+0.005%) from 83.032%
16632364072

Pull #4069

github

web-flow
Merge b1378ea5b into 6d9481860
Pull Request #4069: [chore] improve binary_numeric conformance

163 of 208 new or added lines in 10 files covered. (78.37%)

13 existing lines in 1 file now uncovered.

46028 of 55431 relevant lines covered (83.04%)

451047.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.64
/vortex-array/src/compute/conformance/binary_numeric.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
//! # Binary Numeric Conformance Tests
5
//!
6
//! This module provides conformance testing for binary numeric operations on Vortex arrays.
7
//! It ensures that all numeric array encodings produce identical results when performing
8
//! arithmetic operations (add, subtract, multiply, divide).
9
//!
10
//! ## Test Strategy
11
//!
12
//! For each array encoding, we test:
13
//! 1. All binary numeric operators against a constant scalar value
14
//! 2. Both left-hand and right-hand side operations (e.g., array + 1 and 1 + array)
15
//! 3. That results match the canonical primitive array implementation
16
//!
17
//! ## Supported Operations
18
//!
19
//! - Addition (`+`)
20
//! - Subtraction (`-`)
21
//! - Reverse Subtraction (scalar - array)
22
//! - Multiplication (`*`)
23
//! - Division (`/`)
24
//! - Reverse Division (scalar / array)
25

26
use itertools::Itertools;
27
use num_traits::Num;
28
use vortex_dtype::NativePType;
29
use vortex_error::{VortexExpect, VortexUnwrap, vortex_err, vortex_panic};
30
use vortex_scalar::{NumericOperator, PrimitiveScalar, Scalar};
31

32
use crate::arrays::ConstantArray;
33
use crate::compute::numeric::numeric;
34
use crate::{Array, ArrayRef, IntoArray, ToCanonical};
35

36
fn to_vec_of_scalar(array: &dyn Array) -> Vec<Scalar> {
74,186✔
37
    // Not fast, but obviously correct
38
    (0..array.len())
74,186✔
39
        .map(|index| array.scalar_at(index))
2,107,148✔
40
        .try_collect()
74,186✔
41
        .vortex_unwrap()
74,186✔
42
}
74,186✔
43

44
/// Tests binary numeric operations for conformance across array encodings.
45
///
46
/// # Type Parameters
47
///
48
/// * `T` - The native numeric type (e.g., i32, f64) that the array contains
49
///
50
/// # Arguments
51
///
52
/// * `array` - The array to test, which should contain numeric values of type `T`
53
///
54
/// # Test Details
55
///
56
/// This function:
57
/// 1. Canonicalizes the input array to primitive form to get expected values
58
/// 2. Tests all binary numeric operators against a constant value of 1
59
/// 3. Verifies results match the expected primitive array computation
60
/// 4. Tests both array-operator-scalar and scalar-operator-array forms
61
/// 5. Gracefully skips operations that would cause overflow/underflow
62
///
63
/// # Panics
64
///
65
/// Panics if:
66
/// - The array cannot be converted to primitive form
67
/// - Results don't match expected values (for operations that don't overflow)
68
fn test_binary_numeric_conformance<T: NativePType + Num + Copy>(array: ArrayRef)
1,991✔
69
where
1,991✔
70
    Scalar: From<T>,
1,991✔
71
{
72
    // First test with the standard scalar value of 1
73
    test_standard_binary_numeric::<T>(array.clone());
1,991✔
74

75
    // Then test edge cases if we have enough data
76
    if array.len() >= 5 {
1,991✔
77
        test_binary_numeric_edge_cases(array);
1,915✔
78
    }
1,915✔
79
}
1,991✔
80

81
fn test_standard_binary_numeric<T: NativePType + Num + Copy>(array: ArrayRef)
1,991✔
82
where
1,991✔
83
    Scalar: From<T>,
1,991✔
84
{
85
    let canonicalized_array = array
1,991✔
86
        .to_primitive()
1,991✔
87
        .vortex_expect("Failed to canonicalize array to primitive form for binary numeric test");
1,991✔
88
    let original_values = to_vec_of_scalar(&canonicalized_array.into_array());
1,991✔
89

90
    let one = T::from(1)
1,991✔
91
        .ok_or_else(|| vortex_err!("could not convert 1 into array native type"))
1,991✔
92
        .vortex_unwrap();
1,991✔
93
    let scalar_one = Scalar::from(one).cast(array.dtype()).vortex_unwrap();
1,991✔
94

95
    let operators: [NumericOperator; 6] = [
1,991✔
96
        NumericOperator::Add,
1,991✔
97
        NumericOperator::Sub,
1,991✔
98
        NumericOperator::RSub,
1,991✔
99
        NumericOperator::Mul,
1,991✔
100
        NumericOperator::Div,
1,991✔
101
        NumericOperator::RDiv,
1,991✔
102
    ];
1,991✔
103

104
    for operator in operators {
13,937✔
105
        // Test array operator scalar (e.g., array + 1)
106
        let result = numeric(
11,946✔
107
            &array,
11,946✔
108
            &ConstantArray::new(scalar_one.clone(), array.len()).into_array(),
11,946✔
109
            operator,
11,946✔
110
        );
111

112
        // Skip this operator if the entire operation fails
113
        // This can happen for some edge cases in specific encodings
114
        let Ok(result) = result else {
11,946✔
115
            continue;
839✔
116
        };
117

118
        let actual_values = to_vec_of_scalar(&result);
11,107✔
119

120
        // Check each element for overflow/underflow
121
        let expected_results: Vec<Option<Scalar>> = original_values
11,107✔
122
            .iter()
11,107✔
123
            .map(|x| {
283,793✔
124
                x.as_primitive()
283,793✔
125
                    .checked_binary_numeric(&scalar_one.as_primitive(), operator)
283,793✔
126
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
283,793✔
127
            })
283,793✔
128
            .collect();
11,107✔
129

130
        // For elements that didn't overflow, check they match
131
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
283,793✔
132
            if let Some(expected_value) = expected {
283,793✔
133
                assert_eq!(
283,793✔
134
                    actual,
135
                    expected_value,
NEW
136
                    "Binary numeric operation failed for encoding {} at index {}: \
×
NEW
137
                     ({array:?})[{idx}] {operator:?} {scalar_one} \
×
NEW
138
                     expected {expected_value:?}, got {actual:?}",
×
NEW
139
                    array.encoding_id(),
×
140
                    idx,
141
                );
142
            } else {
143
                // For overflow elements, verify that the operation would indeed overflow
144
                // by trying it on the scalar value
NEW
145
                let original_value = &original_values[idx];
×
NEW
146
                let overflow_check = original_value
×
NEW
147
                    .as_primitive()
×
NEW
UNCOV
148
                    .checked_binary_numeric(&scalar_one.as_primitive(), operator);
×
NEW
UNCOV
149
                assert!(
×
NEW
150
                    overflow_check.is_none(),
×
NEW
151
                    "Expected overflow at index {idx} for operation {original_value} {operator:?} {scalar_one} but overflow check returned {overflow_check:?}"
×
152
                );
153
            }
154
        }
155

156
        // Test scalar operator array (e.g., 1 + array)
157
        let result = numeric(
11,107✔
158
            &ConstantArray::new(scalar_one.clone(), array.len()).into_array(),
11,107✔
159
            &array,
11,107✔
160
            operator,
11,107✔
161
        );
162

163
        // Skip this operator if the entire operation fails
164
        let Ok(result) = result else {
11,107✔
165
            continue;
763✔
166
        };
167

168
        let actual_values = to_vec_of_scalar(&result);
10,344✔
169

170
        // Check each element for overflow/underflow
171
        let expected_results: Vec<Option<Scalar>> = original_values
10,344✔
172
            .iter()
10,344✔
173
            .map(|x| {
260,348✔
174
                scalar_one
260,348✔
175
                    .as_primitive()
260,348✔
176
                    .checked_binary_numeric(&x.as_primitive(), operator)
260,348✔
177
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
260,348✔
178
            })
260,348✔
179
            .collect();
10,344✔
180

181
        // For elements that didn't overflow, check they match
182
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
260,348✔
183
            if let Some(expected_value) = expected {
260,348✔
184
                assert_eq!(
260,348✔
185
                    actual,
186
                    expected_value,
NEW
187
                    "Binary numeric operation failed for encoding {} at index {}: \
×
NEW
188
                     {scalar_one} {operator:?} ({array:?})[{idx}] \
×
NEW
189
                     expected {expected_value:?}, got {actual:?}",
×
NEW
190
                    array.encoding_id(),
×
191
                    idx,
192
                );
193
            } else {
194
                // For overflow elements, verify that the operation would indeed overflow
195
                // by trying it on the scalar value
NEW
196
                let original_value = &original_values[idx];
×
NEW
197
                let overflow_check = scalar_one
×
NEW
198
                    .as_primitive()
×
NEW
199
                    .checked_binary_numeric(&original_value.as_primitive(), operator);
×
NEW
200
                assert!(
×
NEW
201
                    overflow_check.is_none(),
×
NEW
202
                    "Expected overflow at index {idx} for operation {scalar_one} {operator:?} {original_value} but overflow check returned {overflow_check:?}"
×
203
                );
204
            }
205
        }
206
    }
207
}
1,991✔
208

209
/// Entry point for binary numeric conformance testing for any array type.
210
///
211
/// This function automatically detects the array's numeric type and runs
212
/// the appropriate tests. It's designed to be called from rstest parameterized
213
/// tests without requiring explicit type parameters.
214
///
215
/// # Example
216
///
217
/// ```ignore
218
/// #[rstest]
219
/// #[case::i32_array(create_i32_array())]
220
/// #[case::f64_array(create_f64_array())]
221
/// fn test_my_encoding_binary_numeric(#[case] array: MyArray) {
222
///     test_binary_numeric_array(array.into_array());
223
/// }
224
/// ```
225
pub fn test_binary_numeric_array(array: ArrayRef) {
1,991✔
226
    use vortex_dtype::PType;
227

228
    match array.dtype() {
1,991✔
229
        vortex_dtype::DType::Primitive(ptype, _) => match ptype {
1,991✔
230
            PType::I8 => test_binary_numeric_conformance::<i8>(array),
38✔
231
            PType::I16 => test_binary_numeric_conformance::<i16>(array),
38✔
232
            PType::I32 => test_binary_numeric_conformance::<i32>(array),
539✔
233
            PType::I64 => test_binary_numeric_conformance::<i64>(array),
193✔
234
            PType::U8 => test_binary_numeric_conformance::<u8>(array),
76✔
235
            PType::U16 => test_binary_numeric_conformance::<u16>(array),
76✔
236
            PType::U32 => test_binary_numeric_conformance::<u32>(array),
305✔
237
            PType::U64 => test_binary_numeric_conformance::<u64>(array),
190✔
NEW
238
            PType::F16 => {
×
NEW
239
                // F16 not supported in num-traits, skip
×
NEW
240
                eprintln!("Skipping f16 binary numeric tests (not supported)");
×
NEW
241
            }
×
242
            PType::F32 => test_binary_numeric_conformance::<f32>(array),
268✔
243
            PType::F64 => test_binary_numeric_conformance::<f64>(array),
268✔
244
        },
245
        _ => {
NEW
246
            vortex_panic!(
×
NEW
247
                "Binary numeric tests are only supported for primitive numeric types, got {:?}",
×
NEW
248
                array.dtype()
×
249
            );
250
        }
251
    }
252
}
1,991✔
253

254
/// Tests binary numeric operations with edge case scalar values.
255
///
256
/// This function tests operations with scalar values:
257
/// - Zero (identity for addition/subtraction, absorbing for multiplication)
258
/// - Negative one (tests signed arithmetic)
259
/// - Maximum value (tests overflow behavior)
260
/// - Minimum value (tests underflow behavior)
261
fn test_binary_numeric_edge_cases(array: ArrayRef) {
1,915✔
262
    use vortex_dtype::PType;
263

264
    match array.dtype() {
1,915✔
265
        vortex_dtype::DType::Primitive(ptype, _) => match ptype {
1,915✔
266
            PType::I8 => test_binary_numeric_edge_cases_for_type::<i8>(array),
38✔
267
            PType::I16 => test_binary_numeric_edge_cases_for_type::<i16>(array),
38✔
268
            PType::I32 => test_binary_numeric_edge_cases_for_type::<i32>(array),
463✔
269
            PType::I64 => test_binary_numeric_edge_cases_for_type::<i64>(array),
193✔
270
            PType::U8 => test_binary_numeric_edge_cases_unsigned::<u8>(array),
76✔
271
            PType::U16 => test_binary_numeric_edge_cases_unsigned::<u16>(array),
76✔
272
            PType::U32 => test_binary_numeric_edge_cases_unsigned::<u32>(array),
305✔
273
            PType::U64 => test_binary_numeric_edge_cases_unsigned::<u64>(array),
190✔
NEW
274
            PType::F16 => {
×
NEW
275
                eprintln!("Skipping f16 edge case tests (not supported)");
×
NEW
276
            }
×
277
            PType::F32 => test_binary_numeric_edge_cases_float::<f32>(array),
268✔
278
            PType::F64 => test_binary_numeric_edge_cases_float::<f64>(array),
268✔
279
        },
280
        _ => {
NEW
281
            vortex_panic!(
×
NEW
282
                "Binary numeric edge case tests are only supported for primitive numeric types"
×
283
            );
284
        }
285
    }
286
}
1,915✔
287

288
fn test_binary_numeric_edge_cases_for_type<T>(array: ArrayRef)
732✔
289
where
732✔
290
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Bounded + num_traits::Signed,
732✔
291
    Scalar: From<T>,
732✔
292
{
293
    // Test with zero
294
    test_binary_numeric_with_scalar(array.clone(), T::zero());
732✔
295

296
    // Test with -1
297
    test_binary_numeric_with_scalar(array.clone(), -T::one());
732✔
298

299
    // Test with max value
300
    test_binary_numeric_with_scalar(array.clone(), T::max_value());
732✔
301

302
    // Test with min value
303
    test_binary_numeric_with_scalar(array, T::min_value());
732✔
304
}
732✔
305

306
fn test_binary_numeric_edge_cases_unsigned<T>(array: ArrayRef)
647✔
307
where
647✔
308
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Bounded,
647✔
309
    Scalar: From<T>,
647✔
310
{
311
    // Test with zero
312
    test_binary_numeric_with_scalar(array.clone(), T::zero());
647✔
313

314
    // Test with max value
315
    test_binary_numeric_with_scalar(array.clone(), T::max_value());
647✔
316

317
    // Test with min value (0 for unsigned)
318
    test_binary_numeric_with_scalar(array, T::min_value());
647✔
319
}
647✔
320

321
fn test_binary_numeric_edge_cases_float<T>(array: ArrayRef)
536✔
322
where
536✔
323
    T: NativePType + Num + Copy + std::fmt::Debug + num_traits::Float,
536✔
324
    Scalar: From<T>,
536✔
325
{
326
    // Test with zero
327
    test_binary_numeric_with_scalar(array.clone(), T::zero());
536✔
328

329
    // Test with -1
330
    test_binary_numeric_with_scalar(array.clone(), -T::one());
536✔
331

332
    // Test with max value
333
    test_binary_numeric_with_scalar(array.clone(), T::max_value());
536✔
334

335
    // Test with min value
336
    test_binary_numeric_with_scalar(array.clone(), T::min_value());
536✔
337

338
    // Test with small positive value
339
    test_binary_numeric_with_scalar(array.clone(), T::epsilon());
536✔
340

341
    // Test with special float values (NaN, Infinity)
342
    test_binary_numeric_with_scalar(array.clone(), T::nan());
536✔
343
    test_binary_numeric_with_scalar(array.clone(), T::infinity());
536✔
344
    test_binary_numeric_with_scalar(array, T::neg_infinity());
536✔
345
}
536✔
346

347
fn test_binary_numeric_with_scalar<T>(array: ArrayRef, scalar_value: T)
9,157✔
348
where
9,157✔
349
    T: NativePType + Num + Copy + std::fmt::Debug,
9,157✔
350
    Scalar: From<T>,
9,157✔
351
{
352
    let canonicalized_array = array
9,157✔
353
        .to_primitive()
9,157✔
354
        .vortex_expect("Failed to canonicalize array to primitive form for binary numeric test");
9,157✔
355
    let original_values = to_vec_of_scalar(&canonicalized_array.into_array());
9,157✔
356

357
    let scalar = Scalar::from(scalar_value)
9,157✔
358
        .cast(array.dtype())
9,157✔
359
        .vortex_unwrap();
9,157✔
360

361
    // Only test operators that make sense for the given scalar
362
    let operators = if scalar_value == T::zero() {
9,157✔
363
        // Skip division by zero
364
        vec![
2,562✔
365
            NumericOperator::Add,
2,562✔
366
            NumericOperator::Sub,
2,562✔
367
            NumericOperator::RSub,
2,562✔
368
            NumericOperator::Mul,
2,562✔
369
        ]
370
    } else {
371
        vec![
6,595✔
372
            NumericOperator::Add,
6,595✔
373
            NumericOperator::Sub,
6,595✔
374
            NumericOperator::RSub,
6,595✔
375
            NumericOperator::Mul,
6,595✔
376
            NumericOperator::Div,
6,595✔
377
            NumericOperator::RDiv,
6,595✔
378
        ]
379
    };
380

381
    for operator in operators {
58,975✔
382
        // Test array operator scalar
383
        let result = numeric(
49,818✔
384
            &array,
49,818✔
385
            &ConstantArray::new(scalar.clone(), array.len()).into_array(),
49,818✔
386
            operator,
49,818✔
387
        );
388

389
        // Skip if the entire operation fails
390
        if result.is_err() {
49,818✔
391
            continue;
8,231✔
392
        }
41,587✔
393

394
        let result = result.vortex_unwrap();
41,587✔
395
        let actual_values = to_vec_of_scalar(&result);
41,587✔
396

397
        // Check each element for overflow/underflow
398
        let expected_results: Vec<Option<Scalar>> = original_values
41,587✔
399
            .iter()
41,587✔
400
            .map(|x| {
1,242,655✔
401
                x.as_primitive()
1,242,655✔
402
                    .checked_binary_numeric(&scalar.as_primitive(), operator)
1,242,655✔
403
                    .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
1,242,655✔
404
            })
1,242,655✔
405
            .collect();
41,587✔
406

407
        // For elements that didn't overflow, check they match
408
        for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
1,242,655✔
409
            if let Some(expected_value) = expected {
1,242,655✔
410
                assert_eq!(
1,242,655✔
411
                    actual,
412
                    expected_value,
NEW
UNCOV
413
                    "Binary numeric operation failed for encoding {} at index {} with scalar {:?}: \
×
NEW
UNCOV
414
                     ({array:?})[{idx}] {operator:?} {scalar} \
×
NEW
UNCOV
415
                     expected {expected_value:?}, got {actual:?}",
×
NEW
UNCOV
416
                    array.encoding_id(),
×
417
                    idx,
418
                    scalar_value,
419
                );
420
            } else {
421
                // For overflow elements, verify that the operation would indeed overflow
422
                // by trying it on the scalar value
NEW
UNCOV
423
                let original_value = &original_values[idx];
×
NEW
UNCOV
424
                let overflow_check = original_value
×
NEW
UNCOV
425
                    .as_primitive()
×
NEW
UNCOV
426
                    .checked_binary_numeric(&scalar.as_primitive(), operator);
×
NEW
UNCOV
427
                assert!(
×
NEW
UNCOV
428
                    overflow_check.is_none(),
×
NEW
UNCOV
429
                    "Expected overflow at index {idx} for operation {original_value} {operator:?} {scalar} but overflow check returned {overflow_check:?}"
×
430
                );
431
            }
432
        }
433
    }
434
}
9,157✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc