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

vortex-data / vortex / 16759415741

05 Aug 2025 07:30PM UTC coverage: 83.808% (+0.3%) from 83.546%
16759415741

Pull #4114

github

web-flow
Merge 2364f9d74 into 03508f9eb
Pull Request #4114: chore: improve consistency tests

258 of 313 new or added lines in 1 file covered. (82.43%)

1 existing line in 1 file now uncovered.

47451 of 56619 relevant lines covered (83.81%)

515530.45 hits per line

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

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

4
//! # Array Consistency Tests
5
//!
6
//! This module contains tests that verify consistency between related compute operations
7
//! on Vortex arrays. These tests ensure that different ways of achieving the same result
8
//! produce identical outputs.
9
//!
10
//! ## Test Categories
11
//!
12
//! - **Filter/Take Consistency**: Verifies that filtering with a mask produces the same
13
//!   result as taking with the indices where the mask is true.
14
//! - **Mask Composition**: Ensures that applying multiple masks sequentially produces
15
//!   the same result as applying a combined mask.
16
//! - **Identity Operations**: Tests that operations with identity inputs (all-true masks,
17
//!   sequential indices) preserve the original array.
18
//! - **Null Handling**: Verifies consistent behavior when operations introduce or
19
//!   interact with null values.
20
//! - **Edge Cases**: Tests empty arrays, single elements, and boundary conditions.
21

22
use vortex_dtype::DType;
23
use vortex_error::{VortexUnwrap, vortex_panic};
24
use vortex_mask::Mask;
25

26
use crate::arrays::{BoolArray, PrimitiveArray};
27
use crate::compute::{filter, mask, take};
28
use crate::{Array, IntoArray};
29

30
/// Tests that filter and take operations produce consistent results.
31
///
32
/// # Invariant
33
/// `filter(array, mask)` should equal `take(array, indices_where_mask_is_true)`
34
///
35
/// # Test Details
36
/// - Creates a mask that keeps elements where index % 3 != 1
37
/// - Applies filter with this mask
38
/// - Creates indices array containing positions where mask is true
39
/// - Applies take with these indices
40
/// - Verifies both results are identical
41
fn test_filter_take_consistency(array: &dyn Array) {
4,221✔
42
    let len = array.len();
4,221✔
43
    if len == 0 {
4,221✔
44
        return;
1✔
45
    }
4,220✔
46

47
    // Create a test mask (keep elements where index % 3 != 1)
48
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 3 != 1).collect();
1,651,964✔
49
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern.clone())).vortex_unwrap();
4,220✔
50

51
    // Filter the array
52
    let filtered = filter(array, &mask).vortex_unwrap();
4,220✔
53

54
    // Create indices where mask is true
55
    let indices: Vec<u64> = mask_pattern
4,220✔
56
        .iter()
4,220✔
57
        .enumerate()
4,220✔
58
        .filter_map(|(i, &v)| v.then_some(i as u64))
1,651,964✔
59
        .collect();
4,220✔
60
    let indices_array = PrimitiveArray::from_iter(indices).into_array();
4,220✔
61

62
    // Take using those indices
63
    let taken = take(array, &indices_array).vortex_unwrap();
4,220✔
64

65
    // Results should be identical
66
    assert_eq!(
4,220✔
67
        filtered.len(),
4,220✔
68
        taken.len(),
4,220✔
69
        "Filter and take should produce arrays of the same length. \
×
70
         Filtered length: {}, Taken length: {}",
×
71
        filtered.len(),
×
72
        taken.len()
×
73
    );
74

75
    for i in 0..filtered.len() {
1,100,809✔
76
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
1,100,809✔
77
        let taken_val = taken.scalar_at(i).vortex_unwrap();
1,100,809✔
78
        assert_eq!(
1,100,809✔
79
            filtered_val, taken_val,
80
            "Filter and take produced different values at index {i}. \
×
81
             Filtered value: {filtered_val:?}, Taken value: {taken_val:?}"
×
82
        );
83
    }
84
}
4,221✔
85

86
/// Tests that double masking is consistent with combined mask.
87
///
88
/// # Invariant
89
/// `mask(mask(array, mask1), mask2)` should equal `mask(array, mask1 | mask2)`
90
///
91
/// # Test Details
92
/// - Creates two masks: mask1 (every 3rd element) and mask2 (every 2nd element)
93
/// - Applies masks sequentially: first mask1, then mask2 on the result
94
/// - Creates a combined mask using OR operation (element is masked if either mask is true)
95
/// - Applies the combined mask directly to the original array
96
/// - Verifies both approaches produce identical results
97
///
98
/// # Why This Matters
99
/// This test ensures that mask operations compose correctly, which is critical for
100
/// complex query operations that may apply multiple filters.
101
fn test_double_mask_consistency(array: &dyn Array) {
4,221✔
102
    let len = array.len();
4,221✔
103
    if len == 0 {
4,221✔
104
        return;
1✔
105
    }
4,220✔
106

107
    // Create two different mask patterns
108
    let mask1_pattern: Vec<bool> = (0..len).map(|i| i % 3 == 0).collect();
1,651,964✔
109
    let mask2_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
1,651,964✔
110

111
    let mask1 = Mask::try_from(&BoolArray::from_iter(mask1_pattern.clone())).vortex_unwrap();
4,220✔
112
    let mask2 = Mask::try_from(&BoolArray::from_iter(mask2_pattern.clone())).vortex_unwrap();
4,220✔
113

114
    // Apply masks sequentially
115
    let first_masked = mask(array, &mask1).vortex_unwrap();
4,220✔
116
    let double_masked = mask(&first_masked, &mask2).vortex_unwrap();
4,220✔
117

118
    // Create combined mask (OR operation - element is masked if EITHER mask is true)
119
    let combined_pattern: Vec<bool> = mask1_pattern
4,220✔
120
        .iter()
4,220✔
121
        .zip(mask2_pattern.iter())
4,220✔
122
        .map(|(&a, &b)| a || b)
1,651,964✔
123
        .collect();
4,220✔
124
    let combined_mask = Mask::try_from(&BoolArray::from_iter(combined_pattern)).vortex_unwrap();
4,220✔
125

126
    // Apply combined mask directly
127
    let directly_masked = mask(array, &combined_mask).vortex_unwrap();
4,220✔
128

129
    // Results should be identical
130
    assert_eq!(
4,220✔
131
        double_masked.len(),
4,220✔
132
        directly_masked.len(),
4,220✔
133
        "Sequential masking and combined masking should produce arrays of the same length. \
×
134
         Sequential length: {}, Combined length: {}",
×
135
        double_masked.len(),
×
136
        directly_masked.len()
×
137
    );
138

139
    for i in 0..double_masked.len() {
1,651,964✔
140
        let double_val = double_masked.scalar_at(i).vortex_unwrap();
1,651,964✔
141
        let direct_val = directly_masked.scalar_at(i).vortex_unwrap();
1,651,964✔
142
        assert_eq!(
1,651,964✔
143
            double_val, direct_val,
144
            "Sequential masking and combined masking produced different values at index {i}. \
×
145
             Sequential masking value: {double_val:?}, Combined masking value: {direct_val:?}\n\
×
146
             This likely indicates an issue with how masks are composed in the array implementation."
×
147
        );
148
    }
149
}
4,221✔
150

151
/// Tests that filtering with an all-true mask preserves the array.
152
///
153
/// # Invariant
154
/// `filter(array, all_true_mask)` should equal `array`
155
///
156
/// # Test Details
157
/// - Creates a mask with all elements set to true
158
/// - Applies filter with this mask
159
/// - Verifies the result is identical to the original array
160
///
161
/// # Why This Matters
162
/// This is an identity operation that should be optimized in implementations
163
/// to avoid unnecessary copying.
164
fn test_filter_identity(array: &dyn Array) {
4,221✔
165
    let len = array.len();
4,221✔
166
    if len == 0 {
4,221✔
167
        return;
1✔
168
    }
4,220✔
169

170
    let all_true_mask = Mask::new_true(len);
4,220✔
171
    let filtered = filter(array, &all_true_mask).vortex_unwrap();
4,220✔
172

173
    // Filtered array should be identical to original
174
    assert_eq!(
4,220✔
175
        filtered.len(),
4,220✔
176
        array.len(),
4,220✔
177
        "Filtering with all-true mask should preserve array length. \
×
178
         Original length: {}, Filtered length: {}",
×
179
        array.len(),
×
180
        filtered.len()
×
181
    );
182

183
    for i in 0..len {
1,651,964✔
184
        let original_val = array.scalar_at(i).vortex_unwrap();
1,651,964✔
185
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
1,651,964✔
186
        assert_eq!(
1,651,964✔
187
            filtered_val, original_val,
188
            "Filtering with all-true mask should preserve all values. \
×
189
             Value at index {i} changed from {original_val:?} to {filtered_val:?}"
×
190
        );
191
    }
192
}
4,221✔
193

194
/// Tests that masking with an all-false mask preserves values while making them nullable.
195
///
196
/// # Invariant
197
/// `mask(array, all_false_mask)` should have same values as `array` but with nullable type
198
///
199
/// # Test Details
200
/// - Creates a mask with all elements set to false (no elements are nullified)
201
/// - Applies mask operation
202
/// - Verifies all values are preserved but the array type becomes nullable
203
///
204
/// # Why This Matters
205
/// Masking always produces a nullable array, even when no values are actually masked.
206
/// This test ensures the type system handles this correctly.
207
fn test_mask_identity(array: &dyn Array) {
4,221✔
208
    let len = array.len();
4,221✔
209
    if len == 0 {
4,221✔
210
        return;
1✔
211
    }
4,220✔
212

213
    let all_false_mask = Mask::new_false(len);
4,220✔
214
    let masked = mask(array, &all_false_mask).vortex_unwrap();
4,220✔
215

216
    // Masked array should have same values (just nullable)
217
    assert_eq!(
4,220✔
218
        masked.len(),
4,220✔
219
        array.len(),
4,220✔
220
        "Masking with all-false mask should preserve array length. \
×
221
         Original length: {}, Masked length: {}",
×
222
        array.len(),
×
223
        masked.len()
×
224
    );
225

226
    assert!(
4,220✔
227
        masked.dtype().is_nullable(),
4,220✔
228
        "Mask operation should always produce a nullable array, but dtype is {:?}",
×
229
        masked.dtype()
×
230
    );
231

232
    for i in 0..len {
1,651,964✔
233
        let original_val = array.scalar_at(i).vortex_unwrap();
1,651,964✔
234
        let masked_val = masked.scalar_at(i).vortex_unwrap();
1,651,964✔
235
        let expected_val = original_val.clone().into_nullable();
1,651,964✔
236
        assert_eq!(
1,651,964✔
237
            masked_val, expected_val,
238
            "Masking with all-false mask should preserve values (as nullable). \
×
239
             Value at index {i}: original = {original_val:?}, masked = {masked_val:?}, expected = {expected_val:?}"
×
240
        );
241
    }
242
}
4,221✔
243

244
/// Tests that slice and filter with contiguous mask produce same results.
245
///
246
/// # Invariant
247
/// `filter(array, contiguous_true_mask)` should equal `slice(array, start, end)`
248
///
249
/// # Test Details
250
/// - Creates a mask that is true only for indices 1, 2, and 3
251
/// - Filters the array with this mask
252
/// - Slices the array from index 1 to 4
253
/// - Verifies both operations produce identical results
254
///
255
/// # Why This Matters
256
/// When a filter mask represents a contiguous range, it should be equivalent to
257
/// a slice operation. Some implementations may optimize this case.
258
fn test_slice_filter_consistency(array: &dyn Array) {
4,221✔
259
    let len = array.len();
4,221✔
260
    if len < 4 {
4,221✔
261
        return; // Need at least 4 elements for meaningful test
663✔
262
    }
3,558✔
263

264
    // Create a contiguous mask (true from index 1 to 3)
265
    let mut mask_pattern = vec![false; len];
3,558✔
266
    mask_pattern[1..4.min(len)].fill(true);
3,558✔
267

268
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,558✔
269
    let filtered = filter(array, &mask).vortex_unwrap();
3,558✔
270

271
    // Slice should produce the same result
272
    let sliced = array.slice(1, 4.min(len)).vortex_unwrap();
3,558✔
273

274
    assert_eq!(
3,558✔
275
        filtered.len(),
3,558✔
276
        sliced.len(),
3,558✔
277
        "Filter with contiguous mask and slice should produce same length. \
×
278
         Filtered length: {}, Sliced length: {}",
×
279
        filtered.len(),
×
280
        sliced.len()
×
281
    );
282

283
    for i in 0..filtered.len() {
10,674✔
284
        let filtered_val = filtered.scalar_at(i).vortex_unwrap();
10,674✔
285
        let sliced_val = sliced.scalar_at(i).vortex_unwrap();
10,674✔
286
        assert_eq!(
10,674✔
287
            filtered_val, sliced_val,
288
            "Filter with contiguous mask and slice produced different values at index {i}. \
×
289
             Filtered value: {filtered_val:?}, Sliced value: {sliced_val:?}"
×
290
        );
291
    }
292
}
4,221✔
293

294
/// Tests that take with sequential indices equals slice.
295
///
296
/// # Invariant
297
/// `take(array, [1, 2, 3, ...])` should equal `slice(array, 1, n)`
298
///
299
/// # Test Details
300
/// - Creates indices array with sequential values [1, 2, 3]
301
/// - Takes elements at these indices
302
/// - Slices array from index 1 to 4
303
/// - Verifies both operations produce identical results
304
///
305
/// # Why This Matters
306
/// Sequential takes are a common pattern that can be optimized to slice operations.
307
fn test_take_slice_consistency(array: &dyn Array) {
4,221✔
308
    let len = array.len();
4,221✔
309
    if len < 3 {
4,221✔
310
        return; // Need at least 3 elements
509✔
311
    }
3,712✔
312

313
    // Take indices [1, 2, 3]
314
    let end = 4.min(len);
3,712✔
315
    let indices = PrimitiveArray::from_iter((1..end).map(|i| i as u64)).into_array();
10,982✔
316
    let taken = take(array, &indices).vortex_unwrap();
3,712✔
317

318
    // Slice from 1 to end
319
    let sliced = array.slice(1, end).vortex_unwrap();
3,712✔
320

321
    assert_eq!(
3,712✔
322
        taken.len(),
3,712✔
323
        sliced.len(),
3,712✔
324
        "Take with sequential indices and slice should produce same length. \
×
325
         Taken length: {}, Sliced length: {}",
×
326
        taken.len(),
×
327
        sliced.len()
×
328
    );
329

330
    for i in 0..taken.len() {
10,982✔
331
        let taken_val = taken.scalar_at(i).vortex_unwrap();
10,982✔
332
        let sliced_val = sliced.scalar_at(i).vortex_unwrap();
10,982✔
333
        assert_eq!(
10,982✔
334
            taken_val, sliced_val,
335
            "Take with sequential indices and slice produced different values at index {i}. \
×
336
             Taken value: {taken_val:?}, Sliced value: {sliced_val:?}"
×
337
        );
338
    }
339
}
4,221✔
340

341
/// Tests that filter preserves relative ordering
342
fn test_filter_preserves_order(array: &dyn Array) {
4,221✔
343
    let len = array.len();
4,221✔
344
    if len < 4 {
4,221✔
345
        return;
663✔
346
    }
3,558✔
347

348
    // Create a mask that selects elements at indices 0, 2, 3
349
    let mask_pattern: Vec<bool> = (0..len).map(|i| i == 0 || i == 2 || i == 3).collect();
1,650,953✔
350
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,558✔
351

352
    let filtered = filter(array, &mask).vortex_unwrap();
3,558✔
353

354
    // Verify the filtered array contains the right elements in order
355
    assert_eq!(filtered.len(), 3.min(len));
3,558✔
356
    if len >= 4 {
3,558✔
357
        assert_eq!(
3,558✔
358
            filtered.scalar_at(0).vortex_unwrap(),
3,558✔
359
            array.scalar_at(0).vortex_unwrap()
3,558✔
360
        );
361
        assert_eq!(
3,558✔
362
            filtered.scalar_at(1).vortex_unwrap(),
3,558✔
363
            array.scalar_at(2).vortex_unwrap()
3,558✔
364
        );
365
        assert_eq!(
3,558✔
366
            filtered.scalar_at(2).vortex_unwrap(),
3,558✔
367
            array.scalar_at(3).vortex_unwrap()
3,558✔
368
        );
369
    }
×
370
}
4,221✔
371

372
/// Tests that take with repeated indices works correctly
373
fn test_take_repeated_indices(array: &dyn Array) {
4,221✔
374
    let len = array.len();
4,221✔
375
    if len == 0 {
4,221✔
376
        return;
1✔
377
    }
4,220✔
378

379
    // Take the first element three times
380
    let indices = PrimitiveArray::from_iter([0u64, 0, 0]).into_array();
4,220✔
381
    let taken = take(array, &indices).vortex_unwrap();
4,220✔
382

383
    assert_eq!(taken.len(), 3);
4,220✔
384
    for i in 0..3 {
16,880✔
385
        assert_eq!(
12,660✔
386
            taken.scalar_at(i).vortex_unwrap(),
12,660✔
387
            array.scalar_at(0).vortex_unwrap()
12,660✔
388
        );
389
    }
390
}
4,221✔
391

392
/// Tests mask and filter interaction with nulls
393
fn test_mask_filter_null_consistency(array: &dyn Array) {
4,221✔
394
    let len = array.len();
4,221✔
395
    if len < 3 {
4,221✔
396
        return;
509✔
397
    }
3,712✔
398

399
    // First mask some elements
400
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
1,651,415✔
401
    let mask_array = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
3,712✔
402
    let masked = mask(array, &mask_array).vortex_unwrap();
3,712✔
403

404
    // Then filter to remove the nulls
405
    let filter_pattern: Vec<bool> = (0..len).map(|i| i % 2 != 0).collect();
1,651,415✔
406
    let filter_mask = Mask::try_from(&BoolArray::from_iter(filter_pattern)).vortex_unwrap();
3,712✔
407
    let filtered = filter(&masked, &filter_mask).vortex_unwrap();
3,712✔
408

409
    // This should be equivalent to directly filtering the original array
410
    let direct_filtered = filter(array, &filter_mask).vortex_unwrap();
3,712✔
411

412
    assert_eq!(filtered.len(), direct_filtered.len());
3,712✔
413
    for i in 0..filtered.len() {
824,528✔
414
        assert_eq!(
824,528✔
415
            filtered.scalar_at(i).vortex_unwrap(),
824,528✔
416
            direct_filtered.scalar_at(i).vortex_unwrap()
824,528✔
417
        );
418
    }
419
}
4,221✔
420

421
/// Tests that empty operations are consistent
422
fn test_empty_operations_consistency(array: &dyn Array) {
4,221✔
423
    let len = array.len();
4,221✔
424

425
    // Empty filter
426
    let empty_filter = filter(array, &Mask::new_false(len)).vortex_unwrap();
4,221✔
427
    assert_eq!(empty_filter.len(), 0);
4,221✔
428
    assert_eq!(empty_filter.dtype(), array.dtype());
4,221✔
429

430
    // Empty take
431
    let empty_indices =
4,221✔
432
        PrimitiveArray::empty::<u64>(vortex_dtype::Nullability::NonNullable).into_array();
4,221✔
433
    let empty_take = take(array, &empty_indices).vortex_unwrap();
4,221✔
434
    assert_eq!(empty_take.len(), 0);
4,221✔
435
    assert_eq!(empty_take.dtype(), array.dtype());
4,221✔
436

437
    // Empty slice (if array is non-empty)
438
    if len > 0 {
4,221✔
439
        let empty_slice = array.slice(0, 0).vortex_unwrap();
4,220✔
440
        assert_eq!(empty_slice.len(), 0);
4,220✔
441
        assert_eq!(empty_slice.dtype(), array.dtype());
4,220✔
442
    }
1✔
443
}
4,221✔
444

445
/// Tests that take preserves array properties
446
fn test_take_preserves_properties(array: &dyn Array) {
4,221✔
447
    let len = array.len();
4,221✔
448
    if len == 0 {
4,221✔
449
        return;
1✔
450
    }
4,220✔
451

452
    // Take all elements in original order
453
    let indices = PrimitiveArray::from_iter((0..len).map(|i| i as u64)).into_array();
1,651,964✔
454
    let taken = take(array, &indices).vortex_unwrap();
4,220✔
455

456
    // Should be identical to original
457
    assert_eq!(taken.len(), array.len());
4,220✔
458
    assert_eq!(taken.dtype(), array.dtype());
4,220✔
459
    for i in 0..len {
1,651,964✔
460
        assert_eq!(
1,651,964✔
461
            taken.scalar_at(i).vortex_unwrap(),
1,651,964✔
462
            array.scalar_at(i).vortex_unwrap()
1,651,964✔
463
        );
464
    }
465
}
4,221✔
466

467
/// Tests consistency with nullable indices.
468
///
469
/// # Invariant
470
/// `take(array, [Some(0), None, Some(2)])` should produce `[array[0], null, array[2]]`
471
///
472
/// # Test Details
473
/// - Creates an indices array with null at position 1: `[Some(0), None, Some(2)]`
474
/// - Takes elements using these indices
475
/// - Verifies that:
476
///   - Position 0 contains the value from array index 0
477
///   - Position 1 contains null
478
///   - Position 2 contains the value from array index 2
479
///   - The result array has nullable type
480
///
481
/// # Why This Matters
482
/// Nullable indices are a powerful feature that allows introducing nulls during
483
/// a take operation, which is useful for outer joins and similar operations.
484
fn test_nullable_indices_consistency(array: &dyn Array) {
4,221✔
485
    let len = array.len();
4,221✔
486
    if len < 3 {
4,221✔
487
        return; // Need at least 3 elements to test indices 0 and 2
509✔
488
    }
3,712✔
489

490
    // Create nullable indices where some indices are null
491
    let indices = PrimitiveArray::from_option_iter([Some(0u64), None, Some(2u64)]).into_array();
3,712✔
492

493
    let taken = take(array, &indices).vortex_unwrap();
3,712✔
494

495
    // Result should have nulls where indices were null
496
    assert_eq!(
3,712✔
497
        taken.len(),
3,712✔
498
        3,
499
        "Take with nullable indices should produce array of length 3, got {}",
×
500
        taken.len()
×
501
    );
502

503
    assert!(
3,712✔
504
        taken.dtype().is_nullable(),
3,712✔
505
        "Take with nullable indices should produce nullable array, but dtype is {:?}",
×
506
        taken.dtype()
×
507
    );
508

509
    // Check first element (from index 0)
510
    let expected_0 = array.scalar_at(0).vortex_unwrap().into_nullable();
3,712✔
511
    let actual_0 = taken.scalar_at(0).vortex_unwrap();
3,712✔
512
    assert_eq!(
3,712✔
513
        actual_0, expected_0,
514
        "Take with nullable indices: element at position 0 should be from array index 0. \
×
515
         Expected: {expected_0:?}, Actual: {actual_0:?}"
×
516
    );
517

518
    // Check second element (should be null)
519
    let actual_1 = taken.scalar_at(1).vortex_unwrap();
3,712✔
520
    assert!(
3,712✔
521
        actual_1.is_null(),
3,712✔
522
        "Take with nullable indices: element at position 1 should be null, but got {actual_1:?}"
×
523
    );
524

525
    // Check third element (from index 2)
526
    let expected_2 = array.scalar_at(2).vortex_unwrap().into_nullable();
3,712✔
527
    let actual_2 = taken.scalar_at(2).vortex_unwrap();
3,712✔
528
    assert_eq!(
3,712✔
529
        actual_2, expected_2,
530
        "Take with nullable indices: element at position 2 should be from array index 2. \
×
531
         Expected: {expected_2:?}, Actual: {actual_2:?}"
×
532
    );
533
}
4,221✔
534

535
/// Tests large array consistency
536
fn test_large_array_consistency(array: &dyn Array) {
4,221✔
537
    let len = array.len();
4,221✔
538
    if len < 1000 {
4,221✔
539
        return;
3,377✔
540
    }
844✔
541

542
    // Test with every 10th element
543
    let indices: Vec<u64> = (0..len).step_by(10).map(|i| i as u64).collect();
162,520✔
544
    let indices_array = PrimitiveArray::from_iter(indices).into_array();
844✔
545
    let taken = take(array, &indices_array).vortex_unwrap();
844✔
546

547
    // Create equivalent filter mask
548
    let mask_pattern: Vec<bool> = (0..len).map(|i| i % 10 == 0).collect();
1,624,592✔
549
    let mask = Mask::try_from(&BoolArray::from_iter(mask_pattern)).vortex_unwrap();
844✔
550
    let filtered = filter(array, &mask).vortex_unwrap();
844✔
551

552
    // Results should match
553
    assert_eq!(taken.len(), filtered.len());
844✔
554
    for i in 0..taken.len() {
162,520✔
555
        assert_eq!(
162,520✔
556
            taken.scalar_at(i).vortex_unwrap(),
162,520✔
557
            filtered.scalar_at(i).vortex_unwrap()
162,520✔
558
        );
559
    }
560
}
4,221✔
561

562
/// Tests that comparison operations follow inverse relationships.
563
///
564
/// # Invariants
565
/// - `compare(array, value, Eq)` is the inverse of `compare(array, value, NotEq)`
566
/// - `compare(array, value, Gt)` is the inverse of `compare(array, value, Lte)`
567
/// - `compare(array, value, Lt)` is the inverse of `compare(array, value, Gte)`
568
///
569
/// # Test Details
570
/// - Creates comparison results for each operator
571
/// - Verifies that inverse operations produce opposite boolean values
572
/// - Tests with multiple scalar values to ensure consistency
573
///
574
/// # Why This Matters
575
/// Comparison operations must maintain logical consistency across encodings.
576
/// This test catches bugs where an encoding might implement one comparison
577
/// correctly but fail on its logical inverse.
578
fn test_comparison_inverse_consistency(array: &dyn Array) {
4,221✔
579
    use vortex_dtype::DType;
580

581
    use crate::compute::{Operator, compare, invert};
582

583
    let len = array.len();
4,221✔
584
    if len == 0 {
4,221✔
585
        return;
1✔
586
    }
4,220✔
587

588
    // Skip non-comparable types
589
    match array.dtype() {
4,220✔
590
        DType::Null | DType::Extension(_) => return,
312✔
591
        DType::Struct(..) | DType::List(..) => return,
10✔
592
        _ => {}
3,898✔
593
    }
594

595
    // Get a test value from the middle of the array
596
    let test_scalar = match array.scalar_at(len / 2) {
3,898✔
597
        Ok(s) => s,
3,898✔
NEW
598
        Err(_) => return,
×
599
    };
600

601
    // Test Eq vs NotEq
602
    let const_array = crate::arrays::ConstantArray::new(test_scalar, len);
3,898✔
603
    if let (Ok(eq_result), Ok(neq_result)) = (
3,594✔
604
        compare(array, const_array.as_ref(), Operator::Eq),
3,898✔
605
        compare(array, const_array.as_ref(), Operator::NotEq),
3,898✔
606
    ) {
607
        let inverted_eq = invert(&eq_result).vortex_unwrap();
3,594✔
608

609
        assert_eq!(
3,594✔
610
            inverted_eq.len(),
3,594✔
611
            neq_result.len(),
3,594✔
NEW
612
            "Inverted Eq should have same length as NotEq"
×
613
        );
614

615
        for i in 0..inverted_eq.len() {
1,417,880✔
616
            let inv_val = inverted_eq.scalar_at(i).vortex_unwrap();
1,417,880✔
617
            let neq_val = neq_result.scalar_at(i).vortex_unwrap();
1,417,880✔
618
            assert_eq!(
1,417,880✔
619
                inv_val, neq_val,
NEW
620
                "At index {i}: NOT(Eq) should equal NotEq. \
×
NEW
621
                 NOT(Eq) = {inv_val:?}, NotEq = {neq_val:?}"
×
622
            );
623
        }
624
    }
304✔
625

626
    // Test Gt vs Lte
627
    if let (Ok(gt_result), Ok(lte_result)) = (
3,898✔
628
        compare(array, const_array.as_ref(), Operator::Gt),
3,898✔
629
        compare(array, const_array.as_ref(), Operator::Lte),
3,898✔
630
    ) {
631
        let inverted_gt = invert(&gt_result).vortex_unwrap();
3,898✔
632

633
        for i in 0..inverted_gt.len() {
1,592,604✔
634
            let inv_val = inverted_gt.scalar_at(i).vortex_unwrap();
1,592,604✔
635
            let lte_val = lte_result.scalar_at(i).vortex_unwrap();
1,592,604✔
636
            assert_eq!(
1,592,604✔
637
                inv_val, lte_val,
NEW
638
                "At index {i}: NOT(Gt) should equal Lte. \
×
NEW
639
                 NOT(Gt) = {inv_val:?}, Lte = {lte_val:?}"
×
640
            );
641
        }
NEW
642
    }
×
643

644
    // Test Lt vs Gte
645
    if let (Ok(lt_result), Ok(gte_result)) = (
3,898✔
646
        compare(array, const_array.as_ref(), Operator::Lt),
3,898✔
647
        compare(array, const_array.as_ref(), Operator::Gte),
3,898✔
648
    ) {
649
        let inverted_lt = invert(&lt_result).vortex_unwrap();
3,898✔
650

651
        for i in 0..inverted_lt.len() {
1,592,604✔
652
            let inv_val = inverted_lt.scalar_at(i).vortex_unwrap();
1,592,604✔
653
            let gte_val = gte_result.scalar_at(i).vortex_unwrap();
1,592,604✔
654
            assert_eq!(
1,592,604✔
655
                inv_val, gte_val,
NEW
656
                "At index {i}: NOT(Lt) should equal Gte. \
×
NEW
657
                 NOT(Lt) = {inv_val:?}, Gte = {gte_val:?}"
×
658
            );
659
        }
NEW
660
    }
×
661
}
4,221✔
662

663
/// Tests that comparison operations maintain proper symmetry relationships.
664
///
665
/// # Invariants
666
/// - `compare(array, value, Gt)` should equal `compare_scalar_array(value, array, Lt)`
667
/// - `compare(array, value, Lt)` should equal `compare_scalar_array(value, array, Gt)`
668
/// - `compare(array, value, Eq)` should equal `compare_scalar_array(value, array, Eq)`
669
///
670
/// # Test Details
671
/// - Compares array-scalar operations with their symmetric scalar-array versions
672
/// - Verifies that ordering relationships are properly reversed
673
/// - Tests equality which should be symmetric
674
///
675
/// # Why This Matters
676
/// Ensures that comparison operations maintain mathematical ordering properties
677
/// regardless of operand order.
678
fn test_comparison_symmetry_consistency(array: &dyn Array) {
4,221✔
679
    use vortex_dtype::DType;
680

681
    use crate::compute::{Operator, compare};
682

683
    let len = array.len();
4,221✔
684
    if len == 0 {
4,221✔
685
        return;
1✔
686
    }
4,220✔
687

688
    // Skip non-comparable types
689
    match array.dtype() {
4,220✔
690
        DType::Null | DType::Extension(_) => return,
312✔
691
        DType::Struct(..) | DType::List(..) => return,
10✔
692
        _ => {}
3,898✔
693
    }
694

695
    // Get test values
696
    let test_scalar = match array.scalar_at(len / 2) {
3,898✔
697
        Ok(s) => s,
3,898✔
NEW
698
        Err(_) => return,
×
699
    };
700

701
    // Create a constant array with the test scalar for reverse comparison
702
    let const_array = crate::arrays::ConstantArray::new(test_scalar, len);
3,898✔
703

704
    // Test Gt vs Lt symmetry
705
    if let (Ok(arr_gt_scalar), Ok(scalar_lt_arr)) = (
3,898✔
706
        compare(array, const_array.as_ref(), Operator::Gt),
3,898✔
707
        compare(const_array.as_ref(), array, Operator::Lt),
3,898✔
708
    ) {
709
        assert_eq!(
3,898✔
710
            arr_gt_scalar.len(),
3,898✔
711
            scalar_lt_arr.len(),
3,898✔
NEW
712
            "Symmetric comparisons should have same length"
×
713
        );
714

715
        for i in 0..arr_gt_scalar.len() {
1,592,604✔
716
            let arr_gt = arr_gt_scalar.scalar_at(i).vortex_unwrap();
1,592,604✔
717
            let scalar_lt = scalar_lt_arr.scalar_at(i).vortex_unwrap();
1,592,604✔
718
            assert_eq!(
1,592,604✔
719
                arr_gt, scalar_lt,
NEW
720
                "At index {i}: (array > scalar) should equal (scalar < array). \
×
NEW
721
                 array > scalar = {arr_gt:?}, scalar < array = {scalar_lt:?}"
×
722
            );
723
        }
NEW
724
    }
×
725

726
    // Test Eq symmetry
727
    if let (Ok(arr_eq_scalar), Ok(scalar_eq_arr)) = (
3,594✔
728
        compare(array, const_array.as_ref(), Operator::Eq),
3,898✔
729
        compare(const_array.as_ref(), array, Operator::Eq),
3,898✔
730
    ) {
731
        for i in 0..arr_eq_scalar.len() {
1,417,880✔
732
            let arr_eq = arr_eq_scalar.scalar_at(i).vortex_unwrap();
1,417,880✔
733
            let scalar_eq = scalar_eq_arr.scalar_at(i).vortex_unwrap();
1,417,880✔
734
            assert_eq!(
1,417,880✔
735
                arr_eq, scalar_eq,
NEW
736
                "At index {i}: (array == scalar) should equal (scalar == array). \
×
NEW
737
                 array == scalar = {arr_eq:?}, scalar == array = {scalar_eq:?}"
×
738
            );
739
        }
740
    }
304✔
741
}
4,221✔
742

743
/// Tests that boolean operations follow De Morgan's laws.
744
///
745
/// # Invariants
746
/// - `NOT(A AND B)` equals `(NOT A) OR (NOT B)`
747
/// - `NOT(A OR B)` equals `(NOT A) AND (NOT B)`
748
///
749
/// # Test Details
750
/// - If the array is boolean, uses it directly for testing boolean operations
751
/// - Creates two boolean masks from patterns based on the array
752
/// - Computes AND/OR operations and their inversions
753
/// - Verifies De Morgan's laws hold for all elements
754
///
755
/// # Why This Matters
756
/// Boolean operations must maintain logical consistency across encodings.
757
/// This test catches bugs where encodings might optimize boolean operations
758
/// incorrectly, breaking fundamental logical properties.
759
fn test_boolean_demorgan_consistency(array: &dyn Array) {
4,221✔
760
    use crate::compute::{and, invert, or};
761

762
    if !matches!(array.dtype(), DType::Bool(_)) {
4,221✔
763
        return;
4,208✔
764
    }
13✔
765

766
    let mask = {
13✔
767
        let mask_pattern: Vec<bool> = (0..array.len()).map(|i| i % 3 == 0).collect();
6,052✔
768
        BoolArray::from_iter(mask_pattern)
13✔
769
    };
770
    let mask = mask.as_ref();
13✔
771

772
    // Test first De Morgan's law: NOT(A AND B) = (NOT A) OR (NOT B)
773
    if let (Ok(a_and_b), Ok(not_a), Ok(not_b)) = (and(array, mask), invert(array), invert(mask)) {
13✔
774
        let not_a_and_b = invert(&a_and_b).vortex_unwrap();
13✔
775
        let not_a_or_not_b = or(&not_a, &not_b).vortex_unwrap();
13✔
776

777
        assert_eq!(
13✔
778
            not_a_and_b.len(),
13✔
779
            not_a_or_not_b.len(),
13✔
NEW
780
            "De Morgan's law results should have same length"
×
781
        );
782

783
        for i in 0..not_a_and_b.len() {
6,052✔
784
            let left = not_a_and_b.scalar_at(i).vortex_unwrap();
6,052✔
785
            let right = not_a_or_not_b.scalar_at(i).vortex_unwrap();
6,052✔
786
            assert_eq!(
6,052✔
787
                left, right,
NEW
788
                "De Morgan's first law failed at index {i}: \
×
NEW
789
                 NOT(A AND B) = {left:?}, (NOT A) OR (NOT B) = {right:?}"
×
790
            );
791
        }
NEW
792
    }
×
793

794
    // Test second De Morgan's law: NOT(A OR B) = (NOT A) AND (NOT B)
795
    if let (Ok(a_or_b), Ok(not_a), Ok(not_b)) = (or(array, mask), invert(array), invert(mask)) {
13✔
796
        let not_a_or_b = invert(&a_or_b).vortex_unwrap();
13✔
797
        let not_a_and_not_b = and(&not_a, &not_b).vortex_unwrap();
13✔
798

799
        for i in 0..not_a_or_b.len() {
6,052✔
800
            let left = not_a_or_b.scalar_at(i).vortex_unwrap();
6,052✔
801
            let right = not_a_and_not_b.scalar_at(i).vortex_unwrap();
6,052✔
802
            assert_eq!(
6,052✔
803
                left, right,
NEW
804
                "De Morgan's second law failed at index {i}: \
×
NEW
805
                 NOT(A OR B) = {left:?}, (NOT A) AND (NOT B) = {right:?}"
×
806
            );
807
        }
NEW
808
    }
×
809
}
4,221✔
810

811
/// Tests that slice and aggregate operations produce consistent results.
812
///
813
/// # Invariants
814
/// - Aggregating a sliced array should equal aggregating the corresponding
815
///   elements from the canonical form
816
/// - This applies to sum, count, min/max, and other aggregate functions
817
///
818
/// # Test Details
819
/// - Slices the array and computes aggregates
820
/// - Compares against aggregating the canonical form's slice
821
/// - Tests multiple aggregate functions where applicable
822
///
823
/// # Why This Matters
824
/// Aggregate operations on sliced arrays must produce correct results
825
/// regardless of the underlying encoding's offset handling.
826
fn test_slice_aggregate_consistency(array: &dyn Array) {
4,221✔
827
    use vortex_dtype::DType;
828

829
    use crate::compute::{min_max, nan_count, sum};
830

831
    let len = array.len();
4,221✔
832
    if len < 5 {
4,221✔
833
        return; // Need enough elements for meaningful slice
707✔
834
    }
3,514✔
835

836
    // Define slice bounds
837
    let start = 1;
3,514✔
838
    let end = (len - 1).min(start + 10); // Take up to 10 elements
3,514✔
839

840
    // Get sliced array and canonical slice
841
    let sliced = array.slice(start, end).vortex_unwrap();
3,514✔
842
    let canonical = array.to_canonical().vortex_unwrap();
3,514✔
843
    let canonical_sliced = canonical.as_ref().slice(start, end).vortex_unwrap();
3,514✔
844

845
    // Test null count through invalid_count
846
    if let (Ok(slice_null_count), Ok(canonical_null_count)) =
3,514✔
847
        (sliced.invalid_count(), canonical_sliced.invalid_count())
3,514✔
848
    {
849
        assert_eq!(
3,514✔
850
            slice_null_count, canonical_null_count,
NEW
851
            "null_count on sliced array should match canonical. \
×
NEW
852
             Sliced: {slice_null_count}, Canonical: {canonical_null_count}"
×
853
        );
NEW
854
    }
×
855

856
    // Test sum for numeric types
857
    if !matches!(array.dtype(), DType::Primitive(..)) {
3,514✔
858
        return;
684✔
859
    }
2,830✔
860

861
    if let (Ok(slice_sum), Ok(canonical_sum)) = (sum(&sliced), sum(&canonical_sliced)) {
2,830✔
862
        // Compare sum scalars
863
        assert_eq!(
2,830✔
864
            slice_sum, canonical_sum,
NEW
865
            "sum on sliced array should match canonical. \
×
NEW
866
                 Sliced: {slice_sum:?}, Canonical: {canonical_sum:?}"
×
867
        );
NEW
868
    }
×
869

870
    // Test min_max
871
    if let (Ok(slice_minmax), Ok(canonical_minmax)) = (min_max(&sliced), min_max(&canonical_sliced))
2,830✔
872
    {
873
        match (slice_minmax, canonical_minmax) {
2,830✔
874
            (Some(s_result), Some(c_result)) => {
2,791✔
875
                assert_eq!(
2,791✔
876
                    s_result.min, c_result.min,
NEW
877
                    "min on sliced array should match canonical. \
×
NEW
878
                         Sliced: {:?}, Canonical: {:?}",
×
879
                    s_result.min, c_result.min
880
                );
881
                assert_eq!(
2,791✔
882
                    s_result.max, c_result.max,
NEW
883
                    "max on sliced array should match canonical. \
×
NEW
884
                         Sliced: {:?}, Canonical: {:?}",
×
885
                    s_result.max, c_result.max
886
                );
887
            }
888
            (None, None) => {} // Both empty, OK
39✔
NEW
889
            _ => vortex_panic!("min_max results don't match"),
×
890
        }
NEW
891
    }
×
892

893
    // Test nan_count for floating point types
894
    if matches!(
2,293✔
895
        array.dtype(),
2,830✔
896
        DType::Primitive(
897
            vortex_dtype::PType::F16 | vortex_dtype::PType::F32 | vortex_dtype::PType::F64,
898
            _
899
        )
900
    ) {
901
        if let (Ok(slice_nan_count), Ok(canonical_nan_count)) =
537✔
902
            (nan_count(&sliced), nan_count(&canonical_sliced))
537✔
903
        {
904
            assert_eq!(
537✔
905
                slice_nan_count, canonical_nan_count,
NEW
906
                "nan_count on sliced array should match canonical. \
×
NEW
907
                 Sliced: {slice_nan_count}, Canonical: {canonical_nan_count}"
×
908
            );
NEW
909
        }
×
910
    }
2,293✔
911
}
4,221✔
912

913
/// Tests that cast operations preserve array properties when sliced.
914
///
915
/// # Invariant
916
/// `cast(slice(array, start, end), dtype)` should equal `slice(cast(array, dtype), start, end)`
917
///
918
/// # Test Details
919
/// - Slices the array from index 2 to 7 (or len-2 if smaller)
920
/// - Casts the sliced array to a different type
921
/// - Compares against the canonical form of the array (without slicing or casting the canonical form)
922
/// - Verifies both approaches produce identical results
923
///
924
/// # Why This Matters
925
/// This test specifically catches bugs where encodings (like RunEndArray) fail to preserve
926
/// offset information during cast operations. Such bugs can lead to incorrect data being
927
/// returned after casting a sliced array.
928
fn test_cast_slice_consistency(array: &dyn Array) {
4,221✔
929
    use vortex_dtype::{DType, Nullability, PType};
930

931
    use crate::compute::cast;
932

933
    let len = array.len();
4,221✔
934
    if len < 5 {
4,221✔
935
        return; // Need at least 5 elements for meaningful slice
707✔
936
    }
3,514✔
937

938
    // Define slice bounds
939
    let start = 2;
3,514✔
940
    let end = 7.min(len - 2).max(start + 1); // Ensure we have at least 1 element
3,514✔
941

942
    // Get canonical form of the original array
943
    let canonical = array.to_canonical().vortex_unwrap();
3,514✔
944

945
    // Choose appropriate target dtype based on the array's type
946
    let target_dtypes = match array.dtype() {
3,514✔
947
        DType::Null => vec![],
3✔
948
        DType::Bool(nullability) => vec![
10✔
949
            DType::Primitive(PType::U8, *nullability),
10✔
950
            DType::Primitive(PType::I32, *nullability),
10✔
951
        ],
952
        DType::Primitive(ptype, nullability) => {
2,830✔
953
            let mut targets = vec![];
2,830✔
954
            // Test nullability changes
955
            let opposite_nullability = match nullability {
2,830✔
956
                Nullability::NonNullable => Nullability::Nullable,
2,216✔
957
                Nullability::Nullable => Nullability::NonNullable,
614✔
958
            };
959
            targets.push(DType::Primitive(*ptype, opposite_nullability));
2,830✔
960

961
            // Test widening casts
962
            match ptype {
2,830✔
963
                PType::U8 => {
152✔
964
                    targets.push(DType::Primitive(PType::U16, *nullability));
152✔
965
                    targets.push(DType::Primitive(PType::I16, *nullability));
152✔
966
                }
152✔
967
                PType::U16 => {
190✔
968
                    targets.push(DType::Primitive(PType::U32, *nullability));
190✔
969
                    targets.push(DType::Primitive(PType::I32, *nullability));
190✔
970
                }
190✔
971
                PType::U32 => {
267✔
972
                    targets.push(DType::Primitive(PType::U64, *nullability));
267✔
973
                    targets.push(DType::Primitive(PType::I64, *nullability));
267✔
974
                }
267✔
975
                PType::U64 => {
228✔
976
                    targets.push(DType::Primitive(PType::F64, *nullability));
228✔
977
                }
228✔
978
                PType::I8 => {
38✔
979
                    targets.push(DType::Primitive(PType::I16, *nullability));
38✔
980
                    targets.push(DType::Primitive(PType::F32, *nullability));
38✔
981
                }
38✔
982
                PType::I16 => {
115✔
983
                    targets.push(DType::Primitive(PType::I32, *nullability));
115✔
984
                    targets.push(DType::Primitive(PType::F32, *nullability));
115✔
985
                }
115✔
986
                PType::I32 => {
1,034✔
987
                    targets.push(DType::Primitive(PType::I64, *nullability));
1,034✔
988
                    targets.push(DType::Primitive(PType::F64, *nullability));
1,034✔
989
                }
1,034✔
990
                PType::I64 => {
269✔
991
                    targets.push(DType::Primitive(PType::F64, *nullability));
269✔
992
                }
269✔
NEW
993
                PType::F16 => {
×
NEW
994
                    targets.push(DType::Primitive(PType::F32, *nullability));
×
NEW
995
                }
×
996
                PType::F32 => {
344✔
997
                    targets.push(DType::Primitive(PType::F64, *nullability));
344✔
998
                    targets.push(DType::Primitive(PType::I32, *nullability));
344✔
999
                }
344✔
1000
                PType::F64 => {
193✔
1001
                    targets.push(DType::Primitive(PType::I64, *nullability));
193✔
1002
                }
193✔
1003
            }
1004
            targets
2,830✔
1005
        }
1006
        DType::Utf8(nullability) => {
238✔
1007
            let opposite = match nullability {
238✔
1008
                Nullability::NonNullable => Nullability::Nullable,
160✔
1009
                Nullability::Nullable => Nullability::NonNullable,
78✔
1010
            };
1011
            vec![DType::Utf8(opposite), DType::Binary(*nullability)]
238✔
1012
        }
1013
        DType::Binary(nullability) => {
3✔
1014
            let opposite = match nullability {
3✔
1015
                Nullability::NonNullable => Nullability::Nullable,
2✔
1016
                Nullability::Nullable => Nullability::NonNullable,
1✔
1017
            };
1018
            vec![
3✔
1019
                DType::Binary(opposite),
3✔
1020
                DType::Utf8(*nullability), // May fail if not valid UTF-8
3✔
1021
            ]
1022
        }
1023
        DType::Decimal(decimal_type, nullability) => {
268✔
1024
            let opposite = match nullability {
268✔
1025
                Nullability::NonNullable => Nullability::Nullable,
191✔
1026
                Nullability::Nullable => Nullability::NonNullable,
77✔
1027
            };
1028
            vec![DType::Decimal(*decimal_type, opposite)]
268✔
1029
        }
1030
        DType::Struct(fields, nullability) => {
4✔
1031
            let opposite = match nullability {
4✔
1032
                Nullability::NonNullable => Nullability::Nullable,
4✔
NEW
1033
                Nullability::Nullable => Nullability::NonNullable,
×
1034
            };
1035
            vec![DType::Struct(fields.clone(), opposite)]
4✔
1036
        }
1037
        DType::List(element_type, nullability) => {
3✔
1038
            let opposite = match nullability {
3✔
1039
                Nullability::NonNullable => Nullability::Nullable,
3✔
NEW
1040
                Nullability::Nullable => Nullability::NonNullable,
×
1041
            };
1042
            vec![DType::List(element_type.clone(), opposite)]
3✔
1043
        }
1044
        DType::Extension(_) => vec![], // Extension types typically only cast to themselves
155✔
1045
    };
1046

1047
    // Test each target dtype
1048
    for target_dtype in target_dtypes {
12,091✔
1049
        // Slice the array
1050
        let sliced = array.slice(start, end).vortex_unwrap();
8,577✔
1051

1052
        // Try to cast the sliced array
1053
        let slice_then_cast = match cast(&sliced, &target_dtype) {
8,577✔
1054
            Ok(result) => result,
8,200✔
1055
            Err(_) => continue, // Skip if cast fails
377✔
1056
        };
1057

1058
        // Verify against canonical form
1059
        assert_eq!(
8,200✔
1060
            slice_then_cast.len(),
8,200✔
1061
            end - start,
8,200✔
NEW
1062
            "Sliced and casted array should have length {}, but has {}",
×
NEW
1063
            end - start,
×
NEW
1064
            slice_then_cast.len()
×
1065
        );
1066

1067
        // Compare each value against the canonical form
1068
        for i in 0..slice_then_cast.len() {
20,239✔
1069
            let slice_cast_val = slice_then_cast.scalar_at(i).vortex_unwrap();
20,239✔
1070

1071
            // Get the corresponding value from the canonical array (adjusted for slice offset)
1072
            let canonical_val = canonical.as_ref().scalar_at(start + i).vortex_unwrap();
20,239✔
1073

1074
            // Cast the canonical scalar to the target dtype
1075
            let expected_val = match canonical_val.cast(&target_dtype) {
20,239✔
1076
                Ok(val) => val,
19,702✔
1077
                Err(_) => {
1078
                    // If scalar cast fails, we can't compare - skip this target dtype
1079
                    // This can happen for some type conversions that aren't supported at scalar level
1080
                    break;
537✔
1081
                }
1082
            };
1083

1084
            assert_eq!(
19,702✔
1085
                slice_cast_val,
1086
                expected_val,
NEW
1087
                "Cast of sliced array produced incorrect value at index {i}. \
×
NEW
1088
                 Got: {slice_cast_val:?}, Expected: {expected_val:?} \
×
NEW
1089
                 (canonical value at index {}: {canonical_val:?})\n\
×
NEW
1090
                 This likely indicates the array encoding doesn't preserve offset information during cast.",
×
NEW
1091
                start + i
×
1092
            );
1093
        }
1094

1095
        // Also test the other way: cast then slice
1096
        let casted = match cast(array, &target_dtype) {
8,200✔
1097
            Ok(result) => result,
7,583✔
1098
            Err(_) => continue, // Skip if cast fails
617✔
1099
        };
1100
        let cast_then_slice = casted.slice(start, end).vortex_unwrap();
7,583✔
1101

1102
        // Verify the two approaches produce identical results
1103
        assert_eq!(
7,583✔
1104
            slice_then_cast.len(),
7,583✔
1105
            cast_then_slice.len(),
7,583✔
NEW
1106
            "Slice-then-cast and cast-then-slice should produce arrays of the same length"
×
1107
        );
1108

1109
        for i in 0..slice_then_cast.len() {
20,043✔
1110
            let slice_cast_val = slice_then_cast.scalar_at(i).vortex_unwrap();
20,043✔
1111
            let cast_slice_val = cast_then_slice.scalar_at(i).vortex_unwrap();
20,043✔
1112
            assert_eq!(
20,043✔
1113
                slice_cast_val, cast_slice_val,
NEW
1114
                "Slice-then-cast and cast-then-slice produced different values at index {i}. \
×
NEW
1115
                 Slice-then-cast: {slice_cast_val:?}, Cast-then-slice: {cast_slice_val:?}"
×
1116
            );
1117
        }
1118
    }
1119
}
4,221✔
1120

1121
/// Run all consistency tests on an array.
1122
///
1123
/// This function executes a comprehensive suite of consistency tests that verify
1124
/// the correctness of compute operations on Vortex arrays.
1125
///
1126
/// # Test Suite Overview
1127
///
1128
/// ## Core Operation Consistency
1129
/// - **Filter/Take**: Verifies `filter(array, mask)` equals `take(array, true_indices)`
1130
/// - **Mask Composition**: Ensures sequential masks equal combined masks
1131
/// - **Slice/Filter**: Checks contiguous filters equal slice operations
1132
/// - **Take/Slice**: Validates sequential takes equal slice operations
1133
/// - **Cast/Slice**: Ensures cast operations preserve sliced array properties
1134
///
1135
/// ## Boolean Operations
1136
/// - **De Morgan's Laws**: Verifies boolean operations follow logical laws
1137
///
1138
/// ## Comparison Operations
1139
/// - **Inverse Relationships**: Verifies logical inverses (Eq/NotEq, Gt/Lte, Lt/Gte)
1140
/// - **Symmetry**: Ensures proper ordering relationships when operands are swapped
1141
///
1142
/// ## Aggregate Operations
1143
/// - **Slice/Aggregate**: Verifies aggregates on sliced arrays match canonical
1144
///
1145
/// ## Identity Operations
1146
/// - **Filter Identity**: All-true mask preserves the array
1147
/// - **Mask Identity**: All-false mask preserves values (as nullable)
1148
/// - **Take Identity**: Taking all indices preserves the array
1149
///
1150
/// ## Edge Cases
1151
/// - **Empty Operations**: Empty filters, takes, and slices behave correctly
1152
/// - **Single Element**: Operations work with single-element arrays
1153
/// - **Repeated Indices**: Take with duplicate indices works correctly
1154
///
1155
/// ## Null Handling
1156
/// - **Nullable Indices**: Null indices produce null values
1157
/// - **Mask/Filter Interaction**: Masking then filtering behaves predictably
1158
///
1159
/// ## Large Arrays
1160
/// - **Performance**: Operations scale correctly to large arrays (1000+ elements)
1161
/// ```
1162
pub fn test_array_consistency(array: &dyn Array) {
4,221✔
1163
    // Core operation consistency
1164
    test_filter_take_consistency(array);
4,221✔
1165
    test_double_mask_consistency(array);
4,221✔
1166
    test_slice_filter_consistency(array);
4,221✔
1167
    test_take_slice_consistency(array);
4,221✔
1168
    test_cast_slice_consistency(array);
4,221✔
1169

1170
    // Boolean operations
1171
    test_boolean_demorgan_consistency(array);
4,221✔
1172

1173
    // Comparison operations
1174
    test_comparison_inverse_consistency(array);
4,221✔
1175
    test_comparison_symmetry_consistency(array);
4,221✔
1176

1177
    // Aggregate operations
1178
    test_slice_aggregate_consistency(array);
4,221✔
1179

1180
    // Identity operations
1181
    test_filter_identity(array);
4,221✔
1182
    test_mask_identity(array);
4,221✔
1183
    test_take_preserves_properties(array);
4,221✔
1184

1185
    // Ordering and correctness
1186
    test_filter_preserves_order(array);
4,221✔
1187
    test_take_repeated_indices(array);
4,221✔
1188

1189
    // Null handling
1190
    test_mask_filter_null_consistency(array);
4,221✔
1191
    test_nullable_indices_consistency(array);
4,221✔
1192

1193
    // Edge cases
1194
    test_empty_operations_consistency(array);
4,221✔
1195
    test_large_array_consistency(array);
4,221✔
1196
}
4,221✔
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