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

facet-rs / facet / 19903754435

03 Dec 2025 05:59PM UTC coverage: 59.12% (+0.1%) from 59.013%
19903754435

push

github

fasterthanlime
fix: use strict provenance APIs in facet-value

For inline values (null, booleans, short strings), we pack data directly
into pointer bits - there's no actual memory being pointed to. Previously
we used `bits as *mut u8` which is an integer-to-pointer cast that
violates strict provenance.

Now we use:
- `ptr::without_provenance_mut()` for creating fake pointers from integers
- `.addr()` instead of `as usize` for getting the address

This makes miri happy with `-Zmiri-strict-provenance`.

4 of 7 new or added lines in 1 file covered. (57.14%)

420 existing lines in 9 files now uncovered.

20944 of 35426 relevant lines covered (59.12%)

537.13 hits per line

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

95.38
/facet-macros-impl/src/process_struct.rs
1
use quote::{format_ident, quote, quote_spanned};
2

3
use super::*;
4

5
/// Generates the `::facet::Field` definition `TokenStream` from a `PStructField`.
6
pub(crate) fn gen_field_from_pfield(
2,982✔
7
    field: &PStructField,
2,982✔
8
    struct_name: &Ident,
2,982✔
9
    bgp: &BoundedGenericParams,
2,982✔
10
    base_offset: Option<TokenStream>,
2,982✔
11
    facet_crate: &TokenStream,
2,982✔
12
) -> TokenStream {
2,982✔
13
    let field_name_effective = &field.name.effective;
2,982✔
14
    let field_name_raw = &field.name.raw;
2,982✔
15
    let field_type = &field.ty;
2,982✔
16

17
    let bgp_without_bounds = bgp.display_without_bounds();
2,982✔
18

19
    let doc_lines: Vec<String> = field
2,982✔
20
        .attrs
2,982✔
21
        .doc
2,982✔
22
        .iter()
2,982✔
23
        .map(|doc| doc.as_str().replace("\\\"", "\""))
2,982✔
24
        .collect();
2,982✔
25

26
    // Check for opaque attribute to determine shape_of variant
27
    // (Required at compile time - shape_of requires Facet, shape_of_opaque wraps in Opaque)
28
    let shape_of = if field.attrs.has_builtin("opaque") {
2,982✔
29
        quote! { shape_of_opaque }
29✔
30
    } else {
31
        quote! { shape_of }
2,953✔
32
    };
33

34
    // All attributes go through grammar dispatch
35
    // Note: deserialize_with and serialize_with have been REMOVED from the grammar.
36
    // Use #[facet(proxy = Type)] for custom serialization instead.
37
    let mut attribute_list: Vec<TokenStream> = field
2,982✔
38
        .attrs
2,982✔
39
        .facet
2,982✔
40
        .iter()
2,982✔
41
        .map(|attr| {
2,982✔
42
            let ext_attr = emit_attr_for_field(attr, field_name_raw, field_type, facet_crate);
947✔
43
            quote! { #ext_attr }
947✔
44
        })
947✔
45
        .collect();
2,982✔
46

47
    // Generate proxy conversion function pointers when proxy attribute is present
48
    if let Some(attr) = field
2,982✔
49
        .attrs
2,982✔
50
        .facet
2,982✔
51
        .iter()
2,982✔
52
        .find(|a| a.is_builtin() && a.key_str() == "proxy")
2,982✔
53
    {
31✔
54
        let proxy_type = &attr.args;
31✔
55

31✔
56
        // Generate __proxy_in: converts proxy -> field type via TryFrom
31✔
57
        attribute_list.push(quote! {
31✔
58
            #facet_crate::ExtensionAttr {
31✔
59
                ns: ::core::option::Option::None,
31✔
60
                key: "__proxy_in",
31✔
61
                data: &const {
31✔
62
                    extern crate alloc as __alloc;
31✔
63
                    unsafe fn __proxy_convert_in<'mem>(
31✔
64
                        proxy_ptr: #facet_crate::PtrConst<'mem>,
31✔
65
                        field_ptr: #facet_crate::PtrUninit<'mem>,
31✔
66
                    ) -> ::core::result::Result<#facet_crate::PtrMut<'mem>, __alloc::string::String> {
31✔
67
                        let proxy: #proxy_type = proxy_ptr.read();
31✔
68
                        match <#field_type as ::core::convert::TryFrom<#proxy_type>>::try_from(proxy) {
31✔
69
                            ::core::result::Result::Ok(value) => ::core::result::Result::Ok(field_ptr.put(value)),
31✔
70
                            ::core::result::Result::Err(e) => ::core::result::Result::Err(__alloc::string::ToString::to_string(&e)),
31✔
71
                        }
31✔
72
                    }
31✔
73
                    __proxy_convert_in as #facet_crate::ProxyConvertInFn
31✔
74
                } as *const #facet_crate::ProxyConvertInFn as *const (),
31✔
75
                shape: <() as #facet_crate::Facet>::SHAPE,
31✔
76
            }
31✔
77
        });
31✔
78

31✔
79
        // Generate __proxy_out: converts &field type -> proxy via TryFrom
31✔
80
        attribute_list.push(quote! {
31✔
81
            #facet_crate::ExtensionAttr {
31✔
82
                ns: ::core::option::Option::None,
31✔
83
                key: "__proxy_out",
31✔
84
                data: &const {
31✔
85
                    extern crate alloc as __alloc;
31✔
86
                    unsafe fn __proxy_convert_out<'mem>(
31✔
87
                        field_ptr: #facet_crate::PtrConst<'mem>,
31✔
88
                        proxy_ptr: #facet_crate::PtrUninit<'mem>,
31✔
89
                    ) -> ::core::result::Result<#facet_crate::PtrMut<'mem>, __alloc::string::String> {
31✔
90
                        let field_ref: &#field_type = field_ptr.get();
31✔
91
                        match <#proxy_type as ::core::convert::TryFrom<&#field_type>>::try_from(field_ref) {
31✔
92
                            ::core::result::Result::Ok(proxy) => ::core::result::Result::Ok(proxy_ptr.put(proxy)),
31✔
93
                            ::core::result::Result::Err(e) => ::core::result::Result::Err(__alloc::string::ToString::to_string(&e)),
31✔
94
                        }
31✔
95
                    }
31✔
96
                    __proxy_convert_out as #facet_crate::ProxyConvertOutFn
31✔
97
                } as *const #facet_crate::ProxyConvertOutFn as *const (),
31✔
98
                shape: <() as #facet_crate::Facet>::SHAPE,
31✔
99
            }
31✔
100
        });
31✔
101
    }
2,951✔
102

103
    let maybe_attributes = if attribute_list.is_empty() {
2,982✔
104
        quote! {}
2,160✔
105
    } else {
106
        quote! { .attributes(&const { [#(#attribute_list),*] }) }
822✔
107
    };
108

109
    let maybe_field_doc = if doc_lines.is_empty() {
2,982✔
110
        quote! {}
2,928✔
111
    } else {
112
        quote! { .doc(&[#(#doc_lines),*]) }
54✔
113
    };
114

115
    // Calculate the final offset, incorporating the base_offset if present
116
    let final_offset = match base_offset {
2,982✔
117
        Some(base) => {
115✔
118
            quote! { #base + ::core::mem::offset_of!(#struct_name #bgp_without_bounds, #field_name_raw) }
115✔
119
        }
120
        None => {
121
            quote! { ::core::mem::offset_of!(#struct_name #bgp_without_bounds, #field_name_raw) }
2,867✔
122
        }
123
    };
124

125
    quote! {
2,982✔
126
        {
127
            #facet_crate::Field::builder()
128
                // Use the effective name (after rename rules) for metadata
129
                .name(#field_name_effective)
130
                // Use the raw field name/index TokenStream for shape_of and offset_of
131
                .shape(|| #facet_crate::#shape_of(&|s: &#struct_name #bgp_without_bounds| &s.#field_name_raw))
132
                .offset(#final_offset)
133
                #maybe_attributes
134
                #maybe_field_doc
135
                .build()
136
        }
137
    }
138
}
2,982✔
139

140
/// Processes a regular struct to implement Facet
141
///
142
/// Example input:
143
/// ```rust
144
/// struct Blah {
145
///     foo: u32,
146
///     bar: String,
147
/// }
148
/// ```
149
pub(crate) fn process_struct(parsed: Struct) -> TokenStream {
1,513✔
150
    let ps = PStruct::parse(&parsed); // Use the parsed representation
1,513✔
151

152
    // Emit any collected errors as compile_error! with proper spans
153
    if !ps.container.attrs.errors.is_empty() {
1,513✔
UNCOV
154
        let errors = ps.container.attrs.errors.iter().map(|e| {
×
UNCOV
155
            let msg = &e.message;
×
UNCOV
156
            let span = e.span;
×
UNCOV
157
            quote_spanned! { span => compile_error!(#msg); }
×
UNCOV
158
        });
×
UNCOV
159
        return quote! { #(#errors)* };
×
160
    }
1,513✔
161

162
    let struct_name_ident = format_ident!("{}", ps.container.name);
1,513✔
163
    let struct_name = &ps.container.name;
1,513✔
164
    let struct_name_str = struct_name.to_string();
1,513✔
165

166
    let opaque = ps.container.attrs.has_builtin("opaque");
1,513✔
167

168
    // Get the facet crate path (custom or default ::facet)
169
    let facet_crate = ps.container.attrs.facet_crate();
1,513✔
170

171
    let type_name_fn =
1,513✔
172
        generate_type_name_fn(struct_name, parsed.generics.as_ref(), opaque, &facet_crate);
1,513✔
173

174
    // TODO: I assume the `PrimitiveRepr` is only relevant for enums, and does not need to be preserved?
175
    let repr = match &ps.container.attrs.repr {
1,513✔
176
        PRepr::Transparent => quote! { #facet_crate::Repr::transparent() },
4✔
177
        PRepr::Rust(_) => quote! { #facet_crate::Repr::default() },
1,508✔
178
        PRepr::C(_) => quote! { #facet_crate::Repr::c() },
1✔
179
        PRepr::RustcWillCatch => {
180
            // rustc will emit an error for the invalid repr.
181
            // Return empty TokenStream so we don't add misleading errors.
UNCOV
182
            return quote! {};
×
183
        }
184
    };
185

186
    // Use PStruct for kind and fields
187
    let (kind, fields_vec) = match &ps.kind {
1,513✔
188
        PStructKind::Struct { fields } => {
1,418✔
189
            let kind = quote!(#facet_crate::StructKind::Struct);
1,418✔
190
            let fields_vec = fields
1,418✔
191
                .iter()
1,418✔
192
                .map(|field| {
2,306✔
193
                    gen_field_from_pfield(field, struct_name, &ps.container.bgp, None, &facet_crate)
2,306✔
194
                })
2,306✔
195
                .collect::<Vec<_>>();
1,418✔
196
            (kind, fields_vec)
1,418✔
197
        }
198
        PStructKind::TupleStruct { fields } => {
87✔
199
            let kind = quote!(#facet_crate::StructKind::TupleStruct);
87✔
200
            let fields_vec = fields
87✔
201
                .iter()
87✔
202
                .map(|field| {
110✔
203
                    gen_field_from_pfield(field, struct_name, &ps.container.bgp, None, &facet_crate)
110✔
204
                })
110✔
205
                .collect::<Vec<_>>();
87✔
206
            (kind, fields_vec)
87✔
207
        }
208
        PStructKind::UnitStruct => {
209
            let kind = quote!(#facet_crate::StructKind::Unit);
8✔
210
            (kind, vec![])
8✔
211
        }
212
    };
213

214
    // Still need original AST for where clauses and type params for build_ helpers
215
    let where_clauses_ast = match &parsed.kind {
1,513✔
216
        StructKind::Struct { clauses, .. } => clauses.as_ref(),
1,418✔
217
        StructKind::TupleStruct { clauses, .. } => clauses.as_ref(),
87✔
218
        StructKind::UnitStruct { clauses, .. } => clauses.as_ref(),
8✔
219
    };
220
    let where_clauses = build_where_clauses(
1,513✔
221
        where_clauses_ast,
1,513✔
222
        parsed.generics.as_ref(),
1,513✔
223
        opaque,
1,513✔
224
        &facet_crate,
1,513✔
225
    );
226
    let type_params = build_type_params(parsed.generics.as_ref(), opaque, &facet_crate);
1,513✔
227

228
    // Static decl using PStruct BGP
229
    let static_decl = if ps.container.bgp.params.is_empty() {
1,513✔
230
        generate_static_decl(struct_name, &facet_crate)
1,448✔
231
    } else {
232
        TokenStream::new()
65✔
233
    };
234

235
    // Doc comments from PStruct
236
    let maybe_container_doc = if ps.container.attrs.doc.is_empty() {
1,513✔
237
        quote! {}
1,492✔
238
    } else {
239
        let doc_lines = ps.container.attrs.doc.iter().map(|s| quote!(#s));
95✔
240
        quote! { .doc(&[#(#doc_lines),*]) }
21✔
241
    };
242

243
    // Container attributes - most go through grammar dispatch
244
    // Filter out `invariants` and `crate` since they're handled specially
245
    let container_attributes_tokens = {
1,513✔
246
        let items: Vec<TokenStream> = ps
1,513✔
247
            .container
1,513✔
248
            .attrs
1,513✔
249
            .facet
1,513✔
250
            .iter()
1,513✔
251
            .filter(|attr| {
1,513✔
252
                // invariants is handled specially - it populates vtable.invariants
253
                // crate is handled specially - it sets the facet crate path
254
                !(attr.is_builtin()
73✔
255
                    && (attr.key_str() == "invariants" || attr.key_str() == "crate"))
71✔
256
            })
73✔
257
            .map(|attr| {
1,513✔
258
                let ext_attr = emit_attr(attr, &facet_crate);
67✔
259
                quote! { #ext_attr }
67✔
260
            })
67✔
261
            .collect();
1,513✔
262

263
        if items.is_empty() {
1,513✔
264
            quote! {}
1,454✔
265
        } else {
266
            quote! { .attributes(&const { [#(#items),*] }) }
59✔
267
        }
268
    };
269

270
    // Type tag from PStruct
271
    let type_tag_maybe = {
1,513✔
272
        if let Some(type_tag) = ps.container.attrs.get_builtin_args("type_tag") {
1,513✔
273
            quote! { .type_tag(#type_tag) }
8✔
274
        } else {
275
            quote! {}
1,505✔
276
        }
277
    };
278

279
    // Invariants from PStruct - extract invariant function expressions
280
    let invariant_maybe = {
1,513✔
281
        let invariant_exprs: Vec<&TokenStream> = ps
1,513✔
282
            .container
1,513✔
283
            .attrs
1,513✔
284
            .facet
1,513✔
285
            .iter()
1,513✔
286
            .filter(|attr| attr.is_builtin() && attr.key_str() == "invariants")
1,513✔
287
            .map(|attr| &attr.args)
1,513✔
288
            .collect();
1,513✔
289

290
        if !invariant_exprs.is_empty() {
1,513✔
291
            let tests = invariant_exprs.iter().map(|expr| {
3✔
292
                quote! {
3✔
293
                    if !#expr(value) {
294
                        return false;
295
                    }
296
                }
297
            });
3✔
298

299
            let bgp_display = ps.container.bgp.display_without_bounds();
3✔
300
            quote! {
3✔
301
                unsafe fn invariants<'mem>(value: #facet_crate::PtrConst<'mem>) -> bool {
302
                    let value = value.get::<#struct_name_ident #bgp_display>();
303
                    #(#tests)*
304
                    true
305
                }
306

307
                {
308
                    vtable.invariants = Some(invariants);
309
                }
310
            }
311
        } else {
312
            quote! {}
1,510✔
313
        }
314
    };
315

316
    // Transparent logic using PStruct
317
    let inner_field = if ps.container.attrs.has_builtin("transparent") {
1,513✔
318
        match &ps.kind {
28✔
319
            PStructKind::TupleStruct { fields } => {
28✔
320
                if fields.len() > 1 {
28✔
UNCOV
321
                    return quote! {
×
322
                        compile_error!("Transparent structs must be tuple structs with zero or one field");
323
                    };
324
                }
28✔
325
                fields.first().cloned() // Use first field if it exists, None otherwise (ZST case)
28✔
326
            }
327
            _ => {
UNCOV
328
                return quote! {
×
329
                    compile_error!("Transparent structs must be tuple structs");
330
                };
331
            }
332
        }
333
    } else {
334
        None
1,485✔
335
    };
336

337
    // Add try_from_inner implementation for transparent types
338
    let try_from_inner_code = if ps.container.attrs.has_builtin("transparent") {
1,513✔
339
        if let Some(inner_field) = &inner_field {
28✔
340
            if !inner_field.attrs.has_builtin("opaque") {
28✔
341
                // Transparent struct with one field
342
                let inner_field_type = &inner_field.ty;
26✔
343
                let bgp_without_bounds = ps.container.bgp.display_without_bounds();
26✔
344

345
                quote! {
26✔
346
                    // Define the try_from function for the value vtable
347
                    unsafe fn try_from<'src, 'dst>(
348
                        src_ptr: #facet_crate::PtrConst<'src>,
349
                        src_shape: &'static #facet_crate::Shape,
350
                        dst: #facet_crate::PtrUninit<'dst>
351
                    ) -> Result<#facet_crate::PtrMut<'dst>, #facet_crate::TryFromError> {
352
                        // Try the inner type's try_from function if it exists
353
                        let inner_result = match <#inner_field_type as #facet_crate::Facet>::SHAPE.vtable.try_from {
354
                            Some(inner_try) => unsafe { (inner_try)(src_ptr, src_shape, dst) },
355
                            None => Err(#facet_crate::TryFromError::UnsupportedSourceShape {
356
                                src_shape,
357
                                expected: const { &[ &<#inner_field_type as #facet_crate::Facet>::SHAPE ] },
358
                            })
359
                        };
360

361
                        match inner_result {
362
                            Ok(result) => Ok(result),
363
                            Err(_) => {
364
                                // If inner_try failed, check if source shape is exactly the inner shape
365
                                if src_shape != <#inner_field_type as #facet_crate::Facet>::SHAPE {
366
                                    return Err(#facet_crate::TryFromError::UnsupportedSourceShape {
367
                                        src_shape,
368
                                        expected: const { &[ &<#inner_field_type as #facet_crate::Facet>::SHAPE ] },
369
                                    });
370
                                }
371
                                // Read the inner value and construct the wrapper.
372
                                let inner: #inner_field_type = unsafe { src_ptr.read() };
373
                                Ok(unsafe { dst.put(inner) }) // Construct wrapper
374
                            }
375
                        }
376
                    }
377

378
                    // Define the try_into_inner function for the value vtable
379
                    unsafe fn try_into_inner<'src, 'dst>(
380
                        src_ptr: #facet_crate::PtrMut<'src>,
381
                        dst: #facet_crate::PtrUninit<'dst>
382
                    ) -> Result<#facet_crate::PtrMut<'dst>, #facet_crate::TryIntoInnerError> {
383
                        let wrapper = unsafe { src_ptr.get::<#struct_name_ident #bgp_without_bounds>() };
384
                        Ok(unsafe { dst.put(wrapper.0.clone()) }) // Assume tuple struct field 0
385
                    }
386

387
                    // Define the try_borrow_inner function for the value vtable
388
                    unsafe fn try_borrow_inner<'src>(
389
                        src_ptr: #facet_crate::PtrConst<'src>
390
                    ) -> Result<#facet_crate::PtrConst<'src>, #facet_crate::TryBorrowInnerError> {
391
                        let wrapper = unsafe { src_ptr.get::<#struct_name_ident #bgp_without_bounds>() };
392
                        // Return a pointer to the inner field (field 0 for tuple struct)
393
                        Ok(#facet_crate::PtrConst::new(::core::ptr::NonNull::from(&wrapper.0)))
394
                    }
395

396
                    {
397
                        vtable.try_from = Some(try_from);
398
                        vtable.try_into_inner = Some(try_into_inner);
399
                        vtable.try_borrow_inner = Some(try_borrow_inner);
400
                    }
401
                }
402
            } else {
403
                quote! {} // No try_from can be done for opaque
2✔
404
            }
405
        } else {
406
            // Transparent ZST struct (like struct Unit;)
UNCOV
407
            quote! {
×
408
                // Define the try_from function for the value vtable (ZST case)
409
                unsafe fn try_from<'src, 'dst>(
410
                    src_ptr: #facet_crate::PtrConst<'src>,
411
                    src_shape: &'static #facet_crate::Shape,
412
                    dst: #facet_crate::PtrUninit<'dst>
413
                ) -> Result<#facet_crate::PtrMut<'dst>, #facet_crate::TryFromError> {
414
                    if src_shape.layout.size() == 0 {
415
                         Ok(unsafe { dst.put(#struct_name_ident) }) // Construct ZST
416
                    } else {
417
                        Err(#facet_crate::TryFromError::UnsupportedSourceShape {
418
                            src_shape,
419
                            expected: const { &[ <() as #facet_crate::Facet>::SHAPE ] }, // Expect unit-like shape
420
                        })
421
                    }
422
                }
423

424
                {
425
                    vtable.try_from = Some(try_from);
426
                }
427

428
                // ZSTs cannot be meaningfully borrowed or converted *into* an inner value
429
                // try_into_inner and try_borrow_inner remain None
430
            }
431
        }
432
    } else {
433
        quote! {} // Not transparent
1,485✔
434
    };
435

436
    // Generate the inner shape function for transparent types
437
    let inner_setter = if ps.container.attrs.has_builtin("transparent") {
1,513✔
438
        let inner_shape_val = if let Some(inner_field) = &inner_field {
28✔
439
            let ty = &inner_field.ty;
28✔
440
            if inner_field.attrs.has_builtin("opaque") {
28✔
441
                quote! { <#facet_crate::Opaque<#ty> as #facet_crate::Facet>::SHAPE }
2✔
442
            } else {
443
                quote! { <#ty as #facet_crate::Facet>::SHAPE }
26✔
444
            }
445
        } else {
446
            // Transparent ZST case
UNCOV
447
            quote! { <() as #facet_crate::Facet>::SHAPE }
×
448
        };
449
        quote! { .inner(#inner_shape_val) }
28✔
450
    } else {
451
        quote! {}
1,485✔
452
    };
453

454
    // Generics from PStruct
455
    let facet_bgp = ps
1,513✔
456
        .container
1,513✔
457
        .bgp
1,513✔
458
        .with_lifetime(LifetimeName(format_ident!("__facet")));
1,513✔
459
    let bgp_def = facet_bgp.display_with_bounds();
1,513✔
460
    let bgp_without_bounds = ps.container.bgp.display_without_bounds();
1,513✔
461

462
    let (ty, fields) = if opaque {
1,513✔
463
        (
2✔
464
            quote! {
2✔
465
                .ty(#facet_crate::Type::User(#facet_crate::UserType::Opaque))
2✔
466
            },
2✔
467
            quote! {},
2✔
468
        )
2✔
469
    } else {
470
        (
471
            quote! {
1,511✔
472
                .ty(#facet_crate::Type::User(#facet_crate::UserType::Struct(#facet_crate::StructType::builder()
473
                    .repr(#repr)
474
                    .kind(#kind)
475
                    .fields(fields)
476
                    .build()
477
                )))
478
            },
479
            quote! {
1,511✔
480
                let fields: &'static [#facet_crate::Field] = &const {[#(#fields_vec),*]};
481
            },
482
        )
483
    };
484

485
    // Final quote block using refactored parts
486
    let result = quote! {
1,513✔
487
        #static_decl
488

489
        #[automatically_derived]
490
        unsafe impl #bgp_def #facet_crate::Facet<'__facet> for #struct_name_ident #bgp_without_bounds #where_clauses {
491
            const SHAPE: &'static #facet_crate::Shape = &const {
492
                #fields
493

494
                #facet_crate::Shape::builder_for_sized::<Self>()
495
                    .vtable({
496
                        let mut vtable = #facet_crate::value_vtable!(Self, #type_name_fn);
497
                        #invariant_maybe
498
                        #try_from_inner_code // Use the generated code for transparent types
499
                        vtable
500
                    })
501
                    .type_identifier(#struct_name_str)
502
                    #type_params // Still from parsed.generics
503
                    #ty
504
                    #inner_setter // Use transparency flag from PStruct
505
                    #maybe_container_doc // From ps.container.attrs.doc
506
                    #container_attributes_tokens // From ps.container.attrs.facet
507
                    #type_tag_maybe
508
                    .build()
509
            };
510
        }
511
    };
512

513
    result
1,513✔
514
}
1,513✔
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