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

vortex-data / vortex / 16758771979

05 Aug 2025 06:58PM UTC coverage: 83.78% (+0.2%) from 83.546%
16758771979

Pull #4114

github

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

259 of 311 new or added lines in 1 file covered. (83.28%)

19 existing lines in 1 file now uncovered.

47434 of 56617 relevant lines covered (83.78%)

487525.95 hits per line

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

83.11
/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_error::{VortexUnwrap, vortex_panic};
23
use vortex_mask::Mask;
24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

760
    let len = array.len();
4,221✔
761
    if len < 4 {
4,221✔
762
        return; // Need enough elements for meaningful boolean patterns
663✔
763
    }
3,558✔
764

765
    // Create two boolean arrays with different patterns
766
    let mask1_pattern: Vec<bool> = (0..len).map(|i| i % 2 == 0).collect();
1,650,953✔
767
    let mask2_pattern: Vec<bool> = (0..len).map(|i| i % 3 == 0).collect();
1,650,953✔
768

769
    let mask1 = BoolArray::from_iter(mask1_pattern);
3,558✔
770
    let mask2 = BoolArray::from_iter(mask2_pattern);
3,558✔
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)) = (
3,558✔
774
        and(mask1.as_ref(), mask2.as_ref()),
3,558✔
775
        invert(mask1.as_ref()),
3,558✔
776
        invert(mask2.as_ref()),
3,558✔
777
    ) {
778
        let not_a_and_b = invert(&a_and_b).vortex_unwrap();
3,558✔
779
        let not_a_or_not_b = or(&not_a, &not_b).vortex_unwrap();
3,558✔
780

781
        assert_eq!(
3,558✔
782
            not_a_and_b.len(),
3,558✔
783
            not_a_or_not_b.len(),
3,558✔
NEW
784
            "De Morgan's law results should have same length"
×
785
        );
786

787
        for i in 0..not_a_and_b.len().min(20) {
33,711✔
788
            let left = not_a_and_b.scalar_at(i).vortex_unwrap();
33,711✔
789
            let right = not_a_or_not_b.scalar_at(i).vortex_unwrap();
33,711✔
790
            assert_eq!(
33,711✔
791
                left, right,
NEW
792
                "De Morgan's first law failed at index {i}: \
×
NEW
793
                 NOT(A AND B) = {left:?}, (NOT A) OR (NOT B) = {right:?}"
×
794
            );
795
        }
NEW
796
    }
×
797

798
    // Test second De Morgan's law: NOT(A OR B) = (NOT A) AND (NOT B)
799
    if let (Ok(a_or_b), Ok(not_a), Ok(not_b)) = (
3,558✔
800
        or(mask1.as_ref(), mask2.as_ref()),
3,558✔
801
        invert(mask1.as_ref()),
3,558✔
802
        invert(mask2.as_ref()),
3,558✔
803
    ) {
804
        let not_a_or_b = invert(&a_or_b).vortex_unwrap();
3,558✔
805
        let not_a_and_not_b = and(&not_a, &not_b).vortex_unwrap();
3,558✔
806

807
        for i in 0..not_a_or_b.len().min(20) {
33,711✔
808
            let left = not_a_or_b.scalar_at(i).vortex_unwrap();
33,711✔
809
            let right = not_a_and_not_b.scalar_at(i).vortex_unwrap();
33,711✔
810
            assert_eq!(
33,711✔
811
                left, right,
NEW
812
                "De Morgan's second law failed at index {i}: \
×
NEW
813
                 NOT(A OR B) = {left:?}, (NOT A) AND (NOT B) = {right:?}"
×
814
            );
815
        }
NEW
816
    }
×
817
}
4,221✔
818

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

837
    use crate::compute::{min_max, sum};
838

839
    let len = array.len();
4,221✔
840
    if len < 5 {
4,221✔
841
        return; // Need enough elements for meaningful slice
707✔
842
    }
3,514✔
843

844
    // Define slice bounds
845
    let start = 1;
3,514✔
846
    let end = (len - 1).min(start + 10); // Take up to 10 elements
3,514✔
847

848
    // Get sliced array and canonical slice
849
    let sliced = array.slice(start, end).vortex_unwrap();
3,514✔
850
    let canonical = array.to_canonical().vortex_unwrap();
3,514✔
851
    let canonical_sliced = canonical.as_ref().slice(start, end).vortex_unwrap();
3,514✔
852

853
    // Test null count through invalid_count
854
    if let (Ok(slice_null_count), Ok(canonical_null_count)) =
3,514✔
855
        (sliced.invalid_count(), canonical_sliced.invalid_count())
3,514✔
856
    {
857
        assert_eq!(
3,514✔
858
            slice_null_count, canonical_null_count,
NEW
859
            "null_count on sliced array should match canonical. \
×
NEW
860
             Sliced: {slice_null_count}, Canonical: {canonical_null_count}"
×
861
        );
NEW
862
    }
×
863

864
    // Test sum for numeric types
865
    if !matches!(array.dtype(), DType::Primitive(..)) {
3,514✔
866
        return;
684✔
867
    }
2,830✔
868

869
    if let (Ok(slice_sum), Ok(canonical_sum)) = (sum(&sliced), sum(&canonical_sliced)) {
2,830✔
870
        // Compare sum scalars
871
        assert_eq!(
2,830✔
872
            slice_sum, canonical_sum,
NEW
873
            "sum on sliced array should match canonical. \
×
NEW
874
                 Sliced: {slice_sum:?}, Canonical: {canonical_sum:?}"
×
875
        );
NEW
876
    }
×
877

878
    // Test min_max
879
    if let (Ok(slice_minmax), Ok(canonical_minmax)) = (min_max(&sliced), min_max(&canonical_sliced))
2,830✔
880
    {
881
        match (slice_minmax, canonical_minmax) {
2,830✔
882
            (Some(s_result), Some(c_result)) => {
2,791✔
883
                assert_eq!(
2,791✔
884
                    s_result.min, c_result.min,
NEW
885
                    "min on sliced array should match canonical. \
×
NEW
886
                         Sliced: {:?}, Canonical: {:?}",
×
887
                    s_result.min, c_result.min
888
                );
889
                assert_eq!(
2,791✔
890
                    s_result.max, c_result.max,
NEW
891
                    "max on sliced array should match canonical. \
×
NEW
892
                         Sliced: {:?}, Canonical: {:?}",
×
893
                    s_result.max, c_result.max
894
                );
895
            }
896
            (None, None) => {} // Both empty, OK
39✔
NEW
897
            _ => vortex_panic!("min_max results don't match"),
×
898
        }
NEW
899
    }
×
900
}
4,221✔
901

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

920
    use crate::compute::cast;
921

922
    let len = array.len();
4,221✔
923
    if len < 5 {
4,221✔
924
        return; // Need at least 5 elements for meaningful slice
707✔
925
    }
3,514✔
926

927
    // Define slice bounds
928
    let start = 2;
3,514✔
929
    let end = 7.min(len - 2).max(start + 1); // Ensure we have at least 1 element
3,514✔
930

931
    // Get canonical form of the original array
932
    let canonical = array.to_canonical().vortex_unwrap();
3,514✔
933

934
    // Choose appropriate target dtype based on the array's type
935
    let target_dtypes = match array.dtype() {
3,514✔
936
        DType::Null => vec![],
3✔
937
        DType::Bool(nullability) => vec![
10✔
938
            DType::Primitive(PType::U8, *nullability),
10✔
939
            DType::Primitive(PType::I32, *nullability),
10✔
940
        ],
941
        DType::Primitive(ptype, nullability) => {
2,830✔
942
            let mut targets = vec![];
2,830✔
943
            // Test nullability changes
944
            let opposite_nullability = match nullability {
2,830✔
945
                Nullability::NonNullable => Nullability::Nullable,
2,216✔
946
                Nullability::Nullable => Nullability::NonNullable,
614✔
947
            };
948
            targets.push(DType::Primitive(*ptype, opposite_nullability));
2,830✔
949

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

1036
    // Test each target dtype
1037
    for target_dtype in target_dtypes {
12,091✔
1038
        // Slice the array
1039
        let sliced = array.slice(start, end).vortex_unwrap();
8,577✔
1040

1041
        // Try to cast the sliced array
1042
        let slice_then_cast = match cast(&sliced, &target_dtype) {
8,577✔
1043
            Ok(result) => result,
8,200✔
1044
            Err(_) => continue, // Skip if cast fails
377✔
1045
        };
1046

1047
        // Verify against canonical form
1048
        assert_eq!(
8,200✔
1049
            slice_then_cast.len(),
8,200✔
1050
            end - start,
8,200✔
NEW
1051
            "Sliced and casted array should have length {}, but has {}",
×
NEW
1052
            end - start,
×
NEW
1053
            slice_then_cast.len()
×
1054
        );
1055

1056
        // Compare each value against the canonical form
1057
        for i in 0..slice_then_cast.len() {
20,239✔
1058
            let slice_cast_val = slice_then_cast.scalar_at(i).vortex_unwrap();
20,239✔
1059

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

1063
            // Cast the canonical scalar to the target dtype
1064
            let expected_val = match canonical_val.cast(&target_dtype) {
20,239✔
1065
                Ok(val) => val,
19,702✔
1066
                Err(_) => {
1067
                    // If scalar cast fails, we can't compare - skip this target dtype
1068
                    // This can happen for some type conversions that aren't supported at scalar level
1069
                    break;
537✔
1070
                }
1071
            };
1072

1073
            assert_eq!(
19,702✔
1074
                slice_cast_val,
1075
                expected_val,
NEW
1076
                "Cast of sliced array produced incorrect value at index {i}. \
×
NEW
1077
                 Got: {slice_cast_val:?}, Expected: {expected_val:?} \
×
NEW
1078
                 (canonical value at index {}: {canonical_val:?})\n\
×
NEW
1079
                 This likely indicates the array encoding doesn't preserve offset information during cast.",
×
NEW
1080
                start + i
×
1081
            );
1082
        }
1083

1084
        // Also test the other way: cast then slice
1085
        let casted = match cast(array, &target_dtype) {
8,200✔
1086
            Ok(result) => result,
7,583✔
1087
            Err(_) => continue, // Skip if cast fails
617✔
1088
        };
1089
        let cast_then_slice = casted.slice(start, end).vortex_unwrap();
7,583✔
1090

1091
        // Verify the two approaches produce identical results
1092
        assert_eq!(
7,583✔
1093
            slice_then_cast.len(),
7,583✔
1094
            cast_then_slice.len(),
7,583✔
NEW
1095
            "Slice-then-cast and cast-then-slice should produce arrays of the same length"
×
1096
        );
1097

1098
        for i in 0..slice_then_cast.len() {
20,043✔
1099
            let slice_cast_val = slice_then_cast.scalar_at(i).vortex_unwrap();
20,043✔
1100
            let cast_slice_val = cast_then_slice.scalar_at(i).vortex_unwrap();
20,043✔
1101
            assert_eq!(
20,043✔
1102
                slice_cast_val, cast_slice_val,
NEW
1103
                "Slice-then-cast and cast-then-slice produced different values at index {i}. \
×
NEW
1104
                 Slice-then-cast: {slice_cast_val:?}, Cast-then-slice: {cast_slice_val:?}"
×
1105
            );
1106
        }
1107
    }
1108
}
4,221✔
1109

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

1159
    // Boolean operations
1160
    test_boolean_demorgan_consistency(array);
4,221✔
1161

1162
    // Comparison operations
1163
    test_comparison_inverse_consistency(array);
4,221✔
1164
    test_comparison_symmetry_consistency(array);
4,221✔
1165

1166
    // Aggregate operations
1167
    test_slice_aggregate_consistency(array);
4,221✔
1168

1169
    // Identity operations
1170
    test_filter_identity(array);
4,221✔
1171
    test_mask_identity(array);
4,221✔
1172
    test_take_preserves_properties(array);
4,221✔
1173

1174
    // Ordering and correctness
1175
    test_filter_preserves_order(array);
4,221✔
1176
    test_take_repeated_indices(array);
4,221✔
1177

1178
    // Null handling
1179
    test_mask_filter_null_consistency(array);
4,221✔
1180
    test_nullable_indices_consistency(array);
4,221✔
1181

1182
    // Edge cases
1183
    test_empty_operations_consistency(array);
4,221✔
1184
    test_large_array_consistency(array);
4,221✔
1185
}
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