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

vortex-data / vortex / 16374878812

18 Jul 2025 03:54PM UTC coverage: 81.52% (-0.009%) from 81.529%
16374878812

push

github

web-flow
chore: Enable workspace lints on vortex-scan (#3923)

Signed-off-by: Robert Kruszewski <github@robertk.io>

1 of 1 new or added line in 1 file covered. (100.0%)

40 existing lines in 2 files now uncovered.

42091 of 51633 relevant lines covered (81.52%)

171317.78 hits per line

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

85.17
/vortex-array/src/arrays/varbinview/mod.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use std::fmt::{Debug, Formatter};
5
use std::ops::Range;
6

7
use static_assertions::{assert_eq_align, assert_eq_size};
8
use vortex_buffer::{Alignment, Buffer, ByteBuffer};
9
use vortex_dtype::{DType, Nullability};
10
use vortex_error::{VortexResult, VortexUnwrap, vortex_bail, vortex_panic};
11

12
use crate::builders::{ArrayBuilder, VarBinViewBuilder};
13
use crate::stats::{ArrayStats, StatsSetRef};
14
use crate::validity::Validity;
15
use crate::vtable::{
16
    ArrayVTable, CanonicalVTable, NotSupported, VTable, ValidityHelper,
17
    ValidityVTableFromValidityHelper,
18
};
19
use crate::{Canonical, EncodingId, EncodingRef, vtable};
20

21
mod accessor;
22
mod compute;
23
mod ops;
24
mod serde;
25

26
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27
#[repr(C, align(8))]
28
pub struct Inlined {
29
    size: u32,
30
    data: [u8; BinaryView::MAX_INLINED_SIZE],
31
}
32

33
impl Inlined {
34
    fn new<const N: usize>(value: &[u8]) -> Self {
909,978✔
35
        let mut inlined = Self {
909,978✔
36
            size: N.try_into().vortex_unwrap(),
909,978✔
37
            data: [0u8; BinaryView::MAX_INLINED_SIZE],
909,978✔
38
        };
909,978✔
39
        inlined.data[..N].copy_from_slice(&value[..N]);
909,978✔
40
        inlined
909,978✔
41
    }
909,978✔
42

43
    #[inline]
44
    pub fn value(&self) -> &[u8] {
20,528,507✔
45
        &self.data[0..(self.size as usize)]
20,528,507✔
46
    }
20,528,507✔
47
}
48

49
#[derive(Clone, Copy, Debug)]
50
#[repr(C, align(8))]
51
pub struct Ref {
52
    size: u32,
53
    prefix: [u8; 4],
54
    buffer_index: u32,
55
    offset: u32,
56
}
57

58
impl Ref {
59
    pub fn new(size: u32, prefix: [u8; 4], buffer_index: u32, offset: u32) -> Self {
4,175,147✔
60
        Self {
4,175,147✔
61
            size,
4,175,147✔
62
            prefix,
4,175,147✔
63
            buffer_index,
4,175,147✔
64
            offset,
4,175,147✔
65
        }
4,175,147✔
66
    }
4,175,147✔
67

68
    #[inline]
69
    pub fn buffer_index(&self) -> u32 {
14,928,118✔
70
        self.buffer_index
14,928,118✔
71
    }
14,928,118✔
72

73
    #[inline]
74
    pub fn offset(&self) -> u32 {
861,650✔
75
        self.offset
861,650✔
76
    }
861,650✔
77

78
    #[inline]
79
    pub fn prefix(&self) -> &[u8; 4] {
861,650✔
80
        &self.prefix
861,650✔
81
    }
861,650✔
82

83
    #[inline]
84
    pub fn to_range(&self) -> Range<usize> {
15,459,298✔
85
        self.offset as usize..(self.offset + self.size) as usize
15,459,298✔
86
    }
15,459,298✔
87
}
88

89
#[derive(Clone, Copy)]
90
#[repr(C, align(16))]
91
pub union BinaryView {
92
    // Numeric representation. This is logically `u128`, but we split it into the high and low
93
    // bits to preserve the alignment.
94
    le_bytes: [u8; 16],
95

96
    // Inlined representation: strings <= 12 bytes
97
    inlined: Inlined,
98

99
    // Reference type: strings > 12 bytes.
100
    _ref: Ref,
101
}
102

103
assert_eq_size!(BinaryView, [u8; 16]);
104
assert_eq_size!(Inlined, [u8; 16]);
105
assert_eq_size!(Ref, [u8; 16]);
106
assert_eq_align!(BinaryView, u128);
107

108
impl BinaryView {
109
    pub const MAX_INLINED_SIZE: usize = 12;
110

111
    /// Create a view from a value, block and offset
112
    ///
113
    /// Depending on the length of the provided value either a new inlined
114
    /// or a reference view will be constructed.
115
    ///
116
    /// Adapted from arrow-rs <https://github.com/apache/arrow-rs/blob/f4fde769ab6e1a9b75f890b7f8b47bc22800830b/arrow-array/src/builder/generic_bytes_view_builder.rs#L524>
117
    /// Explicitly enumerating inlined view produces code that avoids calling generic `ptr::copy_non_interleave` that's slower than explicit stores
118
    #[inline(never)]
119
    pub fn make_view(value: &[u8], block: u32, offset: u32) -> Self {
4,751,121✔
120
        match value.len() {
4,751,121✔
121
            0 => Self {
667,273✔
122
                inlined: Inlined::new::<0>(value),
667,273✔
123
            },
667,273✔
124
            1 => Self {
2,937✔
125
                inlined: Inlined::new::<1>(value),
2,937✔
126
            },
2,937✔
127
            2 => Self {
3,257✔
128
                inlined: Inlined::new::<2>(value),
3,257✔
129
            },
3,257✔
130
            3 => Self {
9,402✔
131
                inlined: Inlined::new::<3>(value),
9,402✔
132
            },
9,402✔
133
            4 => Self {
4,224✔
134
                inlined: Inlined::new::<4>(value),
4,224✔
135
            },
4,224✔
136
            5 => Self {
14,834✔
137
                inlined: Inlined::new::<5>(value),
14,834✔
138
            },
14,834✔
139
            6 => Self {
14,625✔
140
                inlined: Inlined::new::<6>(value),
14,625✔
141
            },
14,625✔
142
            7 => Self {
12,649✔
143
                inlined: Inlined::new::<7>(value),
12,649✔
144
            },
12,649✔
145
            8 => Self {
14,777✔
146
                inlined: Inlined::new::<8>(value),
14,777✔
147
            },
14,777✔
148
            9 => Self {
10,804✔
149
                inlined: Inlined::new::<9>(value),
10,804✔
150
            },
10,804✔
151
            10 => Self {
53,846✔
152
                inlined: Inlined::new::<10>(value),
53,846✔
153
            },
53,846✔
154
            11 => Self {
52,770✔
155
                inlined: Inlined::new::<11>(value),
52,770✔
156
            },
52,770✔
157
            12 => Self {
48,580✔
158
                inlined: Inlined::new::<12>(value),
48,580✔
159
            },
48,580✔
160
            _ => Self {
3,841,143✔
161
                _ref: Ref::new(
3,841,143✔
162
                    u32::try_from(value.len()).vortex_unwrap(),
3,841,143✔
163
                    value[0..4].try_into().vortex_unwrap(),
3,841,143✔
164
                    block,
3,841,143✔
165
                    offset,
3,841,143✔
166
                ),
3,841,143✔
167
            },
3,841,143✔
168
        }
169
    }
4,751,121✔
170

171
    /// Create a new empty view
172
    #[inline]
173
    pub fn empty_view() -> Self {
1,680✔
174
        Self::new_inlined(&[])
1,680✔
175
    }
1,680✔
176

177
    /// Create a new inlined binary view
178
    #[inline]
179
    pub fn new_inlined(value: &[u8]) -> Self {
1,680✔
180
        assert!(
1,680✔
181
            value.len() <= Self::MAX_INLINED_SIZE,
1,680✔
UNCOV
182
            "expected inlined value to be <= 12 bytes, was {}",
×
UNCOV
183
            value.len()
×
184
        );
185

186
        Self::make_view(value, 0, 0)
1,680✔
187
    }
1,680✔
188

189
    #[inline]
190
    pub fn len(&self) -> u32 {
40,869,431✔
191
        unsafe { self.inlined.size }
40,869,431✔
192
    }
40,869,431✔
193

194
    #[inline]
UNCOV
195
    pub fn is_empty(&self) -> bool {
×
UNCOV
196
        self.len() > 0
×
UNCOV
197
    }
×
198

199
    #[inline]
200
    #[allow(clippy::cast_possible_truncation)]
201
    pub fn is_inlined(&self) -> bool {
39,930,165✔
202
        self.len() <= (Self::MAX_INLINED_SIZE as u32)
39,930,165✔
203
    }
39,930,165✔
204

205
    pub fn as_inlined(&self) -> &Inlined {
20,614,789✔
206
        unsafe { &self.inlined }
20,614,789✔
207
    }
20,614,789✔
208

209
    pub fn as_view(&self) -> &Ref {
28,397,745✔
210
        unsafe { &self._ref }
28,397,745✔
211
    }
28,397,745✔
212

213
    pub fn as_u128(&self) -> u128 {
2,950,582✔
214
        // SAFETY: binary view always safe to read as u128 LE bytes
215
        unsafe { u128::from_le_bytes(self.le_bytes) }
2,950,582✔
216
    }
2,950,582✔
217

218
    /// Shifts the buffer reference by the view by a given offset, useful when merging many
219
    /// varbinview arrays into one.
220
    #[inline(always)]
221
    pub fn offset_view(self, offset: u32) -> Self {
392,933✔
222
        if self.is_inlined() {
392,933✔
223
            self
58,929✔
224
        } else {
225
            // Referencing views must have their buffer_index adjusted with new offsets
226
            let view_ref = self.as_view();
334,004✔
227
            Self {
334,004✔
228
                _ref: Ref::new(
334,004✔
229
                    self.len(),
334,004✔
230
                    *view_ref.prefix(),
334,004✔
231
                    offset + view_ref.buffer_index(),
334,004✔
232
                    view_ref.offset(),
334,004✔
233
                ),
334,004✔
234
            }
334,004✔
235
        }
236
    }
392,933✔
237
}
238

239
impl From<u128> for BinaryView {
UNCOV
240
    fn from(value: u128) -> Self {
×
UNCOV
241
        BinaryView {
×
UNCOV
242
            le_bytes: value.to_le_bytes(),
×
UNCOV
243
        }
×
244
    }
×
245
}
246

247
impl Debug for BinaryView {
248
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
×
UNCOV
249
        let mut s = f.debug_struct("BinaryView");
×
UNCOV
250
        if self.is_inlined() {
×
UNCOV
251
            s.field("inline", &"i".to_string());
×
252
        } else {
×
253
            s.field("ref", &"r".to_string());
×
254
        }
×
255
        s.finish()
×
256
    }
×
257
}
258

259
vtable!(VarBinView);
260

261
impl VTable for VarBinViewVTable {
262
    type Array = VarBinViewArray;
263
    type Encoding = VarBinViewEncoding;
264

265
    type ArrayVTable = Self;
266
    type CanonicalVTable = Self;
267
    type OperationsVTable = Self;
268
    type ValidityVTable = ValidityVTableFromValidityHelper;
269
    type VisitorVTable = Self;
270
    type ComputeVTable = NotSupported;
271
    type EncodeVTable = NotSupported;
272
    type SerdeVTable = Self;
273

274
    fn id(_encoding: &Self::Encoding) -> EncodingId {
73,008✔
275
        EncodingId::new_ref("vortex.varbinview")
73,008✔
276
    }
73,008✔
277

278
    fn encoding(_array: &Self::Array) -> EncodingRef {
16,222✔
279
        EncodingRef::new_ref(VarBinViewEncoding.as_ref())
16,222✔
280
    }
16,222✔
281
}
282

283
#[derive(Clone, Debug)]
284
pub struct VarBinViewArray {
285
    dtype: DType,
286
    buffers: Vec<ByteBuffer>,
287
    views: Buffer<BinaryView>,
288
    validity: Validity,
289
    stats_set: ArrayStats,
290
}
291

292
#[derive(Clone, Debug)]
293
pub struct VarBinViewEncoding;
294

295
impl VarBinViewArray {
296
    pub fn try_new(
19,377✔
297
        views: Buffer<BinaryView>,
19,377✔
298
        buffers: Vec<ByteBuffer>,
19,377✔
299
        dtype: DType,
19,377✔
300
        validity: Validity,
19,377✔
301
    ) -> VortexResult<Self> {
19,377✔
302
        if views.alignment() != Alignment::of::<BinaryView>() {
19,377✔
UNCOV
303
            vortex_bail!("Views must be aligned to a 128 bits");
×
304
        }
19,377✔
305

306
        if !matches!(dtype, DType::Binary(_) | DType::Utf8(_)) {
19,377✔
307
            vortex_bail!(MismatchedTypes: "utf8 or binary", dtype);
×
308
        }
19,377✔
309

310
        if dtype.is_nullable() == (validity == Validity::NonNullable) {
19,377✔
311
            vortex_bail!("incorrect validity {:?}", validity);
×
312
        }
19,377✔
313

314
        Ok(Self {
19,377✔
315
            dtype,
19,377✔
316
            buffers,
19,377✔
317
            views,
19,377✔
318
            validity,
19,377✔
319
            stats_set: Default::default(),
19,377✔
320
        })
19,377✔
321
    }
19,377✔
322

323
    /// Number of raw string data buffers held by this array.
324
    pub fn nbuffers(&self) -> usize {
2,250,487✔
325
        self.buffers.len()
2,250,487✔
326
    }
2,250,487✔
327

328
    /// Access to the primitive views buffer.
329
    ///
330
    /// Variable-sized binary view buffer contain a "view" child array, with 16-byte entries that
331
    /// contain either a pointer into one of the array's owned `buffer`s OR an inlined copy of
332
    /// the string (if the string has 12 bytes or fewer).
333
    #[inline]
334
    pub fn views(&self) -> &Buffer<BinaryView> {
2,374,684✔
335
        &self.views
2,374,684✔
336
    }
2,374,684✔
337

338
    /// Access value bytes at a given index
339
    ///
340
    /// Will return a bytebuffer pointing to the underlying data without performing a copy
341
    #[inline]
342
    pub fn bytes_at(&self, index: usize) -> ByteBuffer {
2,298,602✔
343
        let views = self.views();
2,298,602✔
344
        let view = &views[index];
2,298,602✔
345
        // Expect this to be the common case: strings > 12 bytes.
346
        if !view.is_inlined() {
2,298,602✔
347
            let view_ref = view.as_view();
2,133,047✔
348
            self.buffer(view_ref.buffer_index() as usize)
2,133,047✔
349
                .slice(view_ref.to_range())
2,133,047✔
350
        } else {
351
            // Return access to the range of bytes around it.
352
            views
165,555✔
353
                .clone()
165,555✔
354
                .into_byte_buffer()
165,555✔
355
                .slice_ref(view.as_inlined().value())
165,555✔
356
        }
357
    }
2,298,602✔
358

359
    /// Access one of the backing data buffers.
360
    ///
361
    /// # Panics
362
    ///
363
    /// This method panics if the provided index is out of bounds for the set of buffers provided
364
    /// at construction time.
365
    #[inline]
366
    pub fn buffer(&self, idx: usize) -> &ByteBuffer {
2,221,771✔
367
        if idx >= self.nbuffers() {
2,221,771✔
UNCOV
368
            vortex_panic!(
×
UNCOV
369
                "{idx} buffer index out of bounds, there are {} buffers",
×
UNCOV
370
                self.nbuffers()
×
371
            );
372
        }
2,221,771✔
373
        &self.buffers[idx]
2,221,771✔
374
    }
2,221,771✔
375

376
    /// Iterate over the underlying raw data buffers, not including the views buffer.
377
    #[inline]
378
    pub fn buffers(&self) -> &[ByteBuffer] {
52,497✔
379
        &self.buffers
52,497✔
380
    }
52,497✔
381

382
    /// Accumulate an iterable set of values into our type here.
383
    #[allow(clippy::same_name_method)]
384
    pub fn from_iter<T: AsRef<[u8]>, I: IntoIterator<Item = Option<T>>>(
11✔
385
        iter: I,
11✔
386
        dtype: DType,
11✔
387
    ) -> Self {
11✔
388
        let iter = iter.into_iter();
11✔
389
        let mut builder = VarBinViewBuilder::with_capacity(dtype, iter.size_hint().0);
11✔
390

391
        for item in iter {
2,093✔
392
            match item {
2,082✔
393
                None => builder.append_null(),
7✔
394
                Some(v) => builder.append_value(v),
2,075✔
395
            }
396
        }
397

398
        builder.finish_into_varbinview()
11✔
399
    }
11✔
400

401
    pub fn from_iter_str<T: AsRef<str>, I: IntoIterator<Item = T>>(iter: I) -> Self {
13✔
402
        let iter = iter.into_iter();
13✔
403
        let mut builder = VarBinViewBuilder::with_capacity(
13✔
404
            DType::Utf8(Nullability::NonNullable),
13✔
405
            iter.size_hint().0,
13✔
406
        );
407

408
        for item in iter {
56✔
409
            builder.append_value(item.as_ref());
43✔
410
        }
43✔
411

412
        builder.finish_into_varbinview()
13✔
413
    }
13✔
414

415
    pub fn from_iter_nullable_str<T: AsRef<str>, I: IntoIterator<Item = Option<T>>>(
17✔
416
        iter: I,
17✔
417
    ) -> Self {
17✔
418
        let iter = iter.into_iter();
17✔
419
        let mut builder = VarBinViewBuilder::with_capacity(
17✔
420
            DType::Utf8(Nullability::Nullable),
17✔
421
            iter.size_hint().0,
17✔
422
        );
423

424
        for item in iter {
124✔
425
            match item {
107✔
426
                None => builder.append_null(),
29✔
427
                Some(v) => builder.append_value(v.as_ref()),
78✔
428
            }
429
        }
430

431
        builder.finish_into_varbinview()
17✔
432
    }
17✔
433

434
    pub fn from_iter_bin<T: AsRef<[u8]>, I: IntoIterator<Item = T>>(iter: I) -> Self {
2✔
435
        let iter = iter.into_iter();
2✔
436
        let mut builder = VarBinViewBuilder::with_capacity(
2✔
437
            DType::Binary(Nullability::NonNullable),
2✔
438
            iter.size_hint().0,
2✔
439
        );
440

441
        for item in iter {
6✔
442
            builder.append_value(item.as_ref());
4✔
443
        }
4✔
444

445
        builder.finish_into_varbinview()
2✔
446
    }
2✔
447

UNCOV
448
    pub fn from_iter_nullable_bin<T: AsRef<[u8]>, I: IntoIterator<Item = Option<T>>>(
×
UNCOV
449
        iter: I,
×
UNCOV
450
    ) -> Self {
×
UNCOV
451
        let iter = iter.into_iter();
×
UNCOV
452
        let mut builder = VarBinViewBuilder::with_capacity(
×
UNCOV
453
            DType::Binary(Nullability::Nullable),
×
UNCOV
454
            iter.size_hint().0,
×
455
        );
456

UNCOV
457
        for item in iter {
×
UNCOV
458
            match item {
×
459
                None => builder.append_null(),
×
460
                Some(v) => builder.append_value(v.as_ref()),
×
461
            }
462
        }
463

464
        builder.finish_into_varbinview()
×
465
    }
×
466
}
467

468
impl ArrayVTable<VarBinViewVTable> for VarBinViewVTable {
469
    fn len(array: &VarBinViewArray) -> usize {
2,678,886✔
470
        array.views.len()
2,678,886✔
471
    }
2,678,886✔
472

473
    fn dtype(array: &VarBinViewArray) -> &DType {
206,893✔
474
        &array.dtype
206,893✔
475
    }
206,893✔
476

477
    fn stats(array: &VarBinViewArray) -> StatsSetRef<'_> {
155,761✔
478
        array.stats_set.to_ref(array.as_ref())
155,761✔
479
    }
155,761✔
480
}
481

482
impl ValidityHelper for VarBinViewArray {
483
    fn validity(&self) -> &Validity {
2,513,355✔
484
        &self.validity
2,513,355✔
485
    }
2,513,355✔
486
}
487

488
impl CanonicalVTable<VarBinViewVTable> for VarBinViewVTable {
489
    fn canonicalize(array: &VarBinViewArray) -> VortexResult<Canonical> {
28,241✔
490
        Ok(Canonical::VarBinView(array.clone()))
28,241✔
491
    }
28,241✔
492

493
    fn append_to_builder(
5,283✔
494
        array: &VarBinViewArray,
5,283✔
495
        builder: &mut dyn ArrayBuilder,
5,283✔
496
    ) -> VortexResult<()> {
5,283✔
497
        builder.extend_from_array(array.as_ref())
5,283✔
498
    }
5,283✔
499
}
500

501
impl<'a> FromIterator<Option<&'a [u8]>> for VarBinViewArray {
UNCOV
502
    fn from_iter<T: IntoIterator<Item = Option<&'a [u8]>>>(iter: T) -> Self {
×
UNCOV
503
        Self::from_iter_nullable_bin(iter)
×
UNCOV
504
    }
×
505
}
506

507
impl FromIterator<Option<Vec<u8>>> for VarBinViewArray {
UNCOV
508
    fn from_iter<T: IntoIterator<Item = Option<Vec<u8>>>>(iter: T) -> Self {
×
UNCOV
509
        Self::from_iter_nullable_bin(iter)
×
UNCOV
510
    }
×
511
}
512

513
impl FromIterator<Option<String>> for VarBinViewArray {
UNCOV
514
    fn from_iter<T: IntoIterator<Item = Option<String>>>(iter: T) -> Self {
×
UNCOV
515
        Self::from_iter_nullable_str(iter)
×
UNCOV
516
    }
×
517
}
518

519
impl<'a> FromIterator<Option<&'a str>> for VarBinViewArray {
520
    fn from_iter<T: IntoIterator<Item = Option<&'a str>>>(iter: T) -> Self {
6✔
521
        Self::from_iter_nullable_str(iter)
6✔
522
    }
6✔
523
}
524

525
#[cfg(test)]
526
mod test {
527
    use vortex_scalar::Scalar;
528

529
    use crate::arrays::varbinview::{BinaryView, VarBinViewArray};
530
    use crate::{Array, Canonical, IntoArray};
531

532
    #[test]
533
    pub fn varbin_view() {
1✔
534
        let binary_arr =
1✔
535
            VarBinViewArray::from_iter_str(["hello world", "hello world this is a long string"]);
1✔
536
        assert_eq!(binary_arr.len(), 2);
1✔
537
        assert_eq!(
1✔
538
            binary_arr.scalar_at(0).unwrap(),
1✔
539
            Scalar::from("hello world")
1✔
540
        );
541
        assert_eq!(
1✔
542
            binary_arr.scalar_at(1).unwrap(),
1✔
543
            Scalar::from("hello world this is a long string")
1✔
544
        );
545
    }
1✔
546

547
    #[test]
548
    pub fn slice_array() {
1✔
549
        let binary_arr =
1✔
550
            VarBinViewArray::from_iter_str(["hello world", "hello world this is a long string"])
1✔
551
                .slice(1, 2)
1✔
552
                .unwrap();
1✔
553
        assert_eq!(
1✔
554
            binary_arr.scalar_at(0).unwrap(),
1✔
555
            Scalar::from("hello world this is a long string")
1✔
556
        );
557
    }
1✔
558

559
    #[test]
560
    pub fn flatten_array() {
1✔
561
        let binary_arr = VarBinViewArray::from_iter_str(["string1", "string2"]);
1✔
562

563
        let flattened = binary_arr.to_canonical().unwrap();
1✔
564
        assert!(matches!(flattened, Canonical::VarBinView(_)));
1✔
565

566
        let var_bin = flattened.into_varbinview().unwrap().into_array();
1✔
567
        assert_eq!(var_bin.scalar_at(0).unwrap(), Scalar::from("string1"));
1✔
568
        assert_eq!(var_bin.scalar_at(1).unwrap(), Scalar::from("string2"));
1✔
569
    }
1✔
570

571
    #[test]
572
    pub fn binary_view_size_and_alignment() {
1✔
573
        assert_eq!(size_of::<BinaryView>(), 16);
1✔
574
        assert_eq!(align_of::<BinaryView>(), 16);
1✔
575
    }
1✔
576
}
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