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

facet-rs / facet / 19774894729

28 Nov 2025 10:22PM UTC coverage: 59.711% (-0.6%) from 60.346%
19774894729

push

github

fasterthanlime
Add DynamicValue support for deserializing into facet_value::Value

This adds support for deserializing JSON (and potentially other formats)
into facet_value::Value without format crates needing to depend on facet-value.

Key changes:
- Add Def::DynamicValue variant with vtable for building dynamic values
- Implement Facet trait for Value in facet-value
- Extend Partial to handle DynamicValue scalars via set_into_dynamic_value
- Add deserialize_dynamic_value handler in facet-json

Currently supports scalar values (null, bool, numbers, strings).
Arrays and objects are stubbed out with TODO errors.

451 of 922 new or added lines in 20 files covered. (48.92%)

29 existing lines in 3 files now uncovered.

16657 of 27896 relevant lines covered (59.71%)

153.76 hits per line

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

69.25
/facet-value/src/object.rs
1
//! Object (map) value type.
2

3
#[cfg(feature = "alloc")]
4
use alloc::alloc::{Layout, alloc, dealloc, realloc};
5
#[cfg(feature = "alloc")]
6
use alloc::borrow::ToOwned;
7
#[cfg(feature = "alloc")]
8
use alloc::collections::BTreeMap;
9
use core::fmt::{self, Debug, Formatter};
10
use core::hash::{Hash, Hasher};
11
use core::iter::FromIterator;
12
use core::ops::{Index, IndexMut};
13
use core::{cmp, mem, ptr};
14

15
#[cfg(feature = "std")]
16
use std::collections::HashMap;
17

18
use crate::string::VString;
19
use crate::value::{TypeTag, Value};
20

21
/// A key-value pair.
22
#[repr(C)]
23
struct KeyValuePair {
24
    key: VString,
25
    value: Value,
26
}
27

28
/// Header for heap-allocated objects.
29
#[repr(C, align(8))]
30
struct ObjectHeader {
31
    /// Number of key-value pairs
32
    len: usize,
33
    /// Capacity
34
    cap: usize,
35
    // Array of KeyValuePair follows immediately after
36
}
37

38
/// An object (map) value.
39
///
40
/// `VObject` is an ordered map of string keys to `Value`s.
41
/// It preserves insertion order and uses linear search for lookups.
42
/// This is efficient for small objects (which are common in JSON).
43
#[repr(transparent)]
44
#[derive(Clone)]
45
pub struct VObject(pub(crate) Value);
46

47
impl VObject {
48
    fn layout(cap: usize) -> Layout {
96✔
49
        Layout::new::<ObjectHeader>()
96✔
50
            .extend(Layout::array::<KeyValuePair>(cap).unwrap())
96✔
51
            .unwrap()
96✔
52
            .0
96✔
53
            .pad_to_align()
96✔
54
    }
96✔
55

56
    #[cfg(feature = "alloc")]
57
    fn alloc(cap: usize) -> *mut ObjectHeader {
26✔
58
        unsafe {
59
            let layout = Self::layout(cap);
26✔
60
            let ptr = alloc(layout).cast::<ObjectHeader>();
26✔
61
            (*ptr).len = 0;
26✔
62
            (*ptr).cap = cap;
26✔
63
            ptr
26✔
64
        }
65
    }
26✔
66

67
    #[cfg(feature = "alloc")]
68
    fn realloc_ptr(ptr: *mut ObjectHeader, new_cap: usize) -> *mut ObjectHeader {
22✔
69
        unsafe {
70
            let old_cap = (*ptr).cap;
22✔
71
            let old_layout = Self::layout(old_cap);
22✔
72
            let new_layout = Self::layout(new_cap);
22✔
73
            let new_ptr =
22✔
74
                realloc(ptr.cast::<u8>(), old_layout, new_layout.size()).cast::<ObjectHeader>();
22✔
75
            (*new_ptr).cap = new_cap;
22✔
76
            new_ptr
22✔
77
        }
78
    }
22✔
79

80
    #[cfg(feature = "alloc")]
81
    fn dealloc_ptr(ptr: *mut ObjectHeader) {
26✔
82
        unsafe {
26✔
83
            let cap = (*ptr).cap;
26✔
84
            let layout = Self::layout(cap);
26✔
85
            dealloc(ptr.cast::<u8>(), layout);
26✔
86
        }
26✔
87
    }
26✔
88

89
    fn header(&self) -> &ObjectHeader {
438✔
90
        unsafe { &*(self.0.heap_ptr() as *const ObjectHeader) }
438✔
91
    }
438✔
92

93
    fn header_mut(&mut self) -> &mut ObjectHeader {
84✔
94
        unsafe { &mut *(self.0.heap_ptr_mut() as *mut ObjectHeader) }
84✔
95
    }
84✔
96

97
    fn items_ptr(&self) -> *const KeyValuePair {
95✔
98
        unsafe { (self.header() as *const ObjectHeader).add(1).cast() }
95✔
99
    }
95✔
100

101
    fn items_ptr_mut(&mut self) -> *mut KeyValuePair {
85✔
102
        // Use heap_ptr_mut directly to preserve mutable provenance
103
        unsafe { (self.0.heap_ptr_mut() as *mut ObjectHeader).add(1).cast() }
85✔
104
    }
85✔
105

106
    fn items(&self) -> &[KeyValuePair] {
95✔
107
        unsafe { core::slice::from_raw_parts(self.items_ptr(), self.len()) }
95✔
108
    }
95✔
109

110
    fn items_mut(&mut self) -> &mut [KeyValuePair] {
1✔
111
        unsafe { core::slice::from_raw_parts_mut(self.items_ptr_mut(), self.len()) }
1✔
112
    }
1✔
113

114
    /// Creates a new empty object.
115
    #[cfg(feature = "alloc")]
116
    #[must_use]
117
    pub fn new() -> Self {
24✔
118
        Self::with_capacity(0)
24✔
119
    }
24✔
120

121
    /// Creates a new object with the specified capacity.
122
    #[cfg(feature = "alloc")]
123
    #[must_use]
124
    pub fn with_capacity(cap: usize) -> Self {
26✔
125
        unsafe {
126
            let ptr = Self::alloc(cap);
26✔
127
            VObject(Value::new_ptr(ptr.cast(), TypeTag::Object))
26✔
128
        }
129
    }
26✔
130

131
    /// Returns the number of entries.
132
    #[must_use]
133
    pub fn len(&self) -> usize {
218✔
134
        self.header().len
218✔
135
    }
218✔
136

137
    /// Returns `true` if the object is empty.
138
    #[must_use]
139
    pub fn is_empty(&self) -> bool {
69✔
140
        self.len() == 0
69✔
141
    }
69✔
142

143
    /// Returns the capacity.
144
    #[must_use]
145
    pub fn capacity(&self) -> usize {
42✔
146
        self.header().cap
42✔
147
    }
42✔
148

149
    /// Reserves capacity for at least `additional` more entries.
150
    #[cfg(feature = "alloc")]
151
    pub fn reserve(&mut self, additional: usize) {
42✔
152
        let current_cap = self.capacity();
42✔
153
        let desired_cap = self
42✔
154
            .len()
42✔
155
            .checked_add(additional)
42✔
156
            .expect("capacity overflow");
42✔
157

158
        if current_cap >= desired_cap {
42✔
159
            return;
20✔
160
        }
22✔
161

162
        let new_cap = cmp::max(current_cap * 2, desired_cap.max(4));
22✔
163

164
        unsafe {
22✔
165
            let new_ptr = Self::realloc_ptr(self.0.heap_ptr().cast(), new_cap);
22✔
166
            self.0.set_ptr(new_ptr.cast());
22✔
167
        }
22✔
168
    }
42✔
169

170
    /// Finds the index of a key.
171
    fn find_key(&self, key: &str) -> Option<usize> {
70✔
172
        self.items().iter().position(|kv| kv.key.as_str() == key)
70✔
173
    }
70✔
174

175
    /// Gets a value by key.
176
    #[must_use]
177
    pub fn get(&self, key: &str) -> Option<&Value> {
22✔
178
        self.find_key(key).map(|i| &self.items()[i].value)
22✔
179
    }
22✔
180

181
    /// Gets a mutable value by key.
182
    pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
×
183
        self.find_key(key).map(|i| &mut self.items_mut()[i].value)
×
184
    }
×
185

186
    /// Gets a key-value pair by key.
187
    #[must_use]
188
    pub fn get_key_value(&self, key: &str) -> Option<(&VString, &Value)> {
×
189
        self.find_key(key).map(|i| {
×
190
            let kv = &self.items()[i];
×
191
            (&kv.key, &kv.value)
×
192
        })
×
193
    }
×
194

195
    /// Returns `true` if the object contains the key.
196
    #[must_use]
197
    pub fn contains_key(&self, key: &str) -> bool {
4✔
198
        self.find_key(key).is_some()
4✔
199
    }
4✔
200

201
    /// Inserts a key-value pair. Returns the old value if the key existed.
202
    #[cfg(feature = "alloc")]
203
    pub fn insert(&mut self, key: impl Into<VString>, value: impl Into<Value>) -> Option<Value> {
43✔
204
        let key = key.into();
43✔
205
        let value = value.into();
43✔
206

207
        if let Some(i) = self.find_key(key.as_str()) {
43✔
208
            // Key exists, replace value
209
            Some(mem::replace(&mut self.items_mut()[i].value, value))
1✔
210
        } else {
211
            // New key
212
            self.reserve(1);
42✔
213
            unsafe {
42✔
214
                let len = self.header().len;
42✔
215
                let ptr = self.items_ptr_mut().add(len);
42✔
216
                ptr.write(KeyValuePair { key, value });
42✔
217
                self.header_mut().len = len + 1;
42✔
218
            }
42✔
219
            None
42✔
220
        }
221
    }
43✔
222

223
    /// Removes a key-value pair. Returns the value if the key existed.
224
    pub fn remove(&mut self, key: &str) -> Option<Value> {
1✔
225
        self.remove_entry(key).map(|(_, v)| v)
1✔
226
    }
1✔
227

228
    /// Removes and returns a key-value pair.
229
    pub fn remove_entry(&mut self, key: &str) -> Option<(VString, Value)> {
1✔
230
        let idx = self.find_key(key)?;
1✔
231
        let len = self.len();
1✔
232

233
        unsafe {
234
            let ptr = self.items_ptr_mut().add(idx);
1✔
235
            let kv = ptr.read();
1✔
236

237
            // Shift remaining elements
238
            if idx < len - 1 {
1✔
239
                ptr::copy(ptr.add(1), ptr, len - idx - 1);
1✔
240
            }
1✔
241

242
            self.header_mut().len = len - 1;
1✔
243
            Some((kv.key, kv.value))
1✔
244
        }
245
    }
1✔
246

247
    /// Clears the object.
248
    pub fn clear(&mut self) {
26✔
249
        while !self.is_empty() {
67✔
250
            unsafe {
41✔
251
                let len = self.header().len;
41✔
252
                self.header_mut().len = len - 1;
41✔
253
                let ptr = self.items_ptr_mut().add(len - 1);
41✔
254
                ptr::drop_in_place(ptr);
41✔
255
            }
41✔
256
        }
257
    }
26✔
258

259
    /// Returns an iterator over keys.
260
    pub fn keys(&self) -> impl Iterator<Item = &VString> {
1✔
261
        self.items().iter().map(|kv| &kv.key)
1✔
262
    }
1✔
263

264
    /// Returns an iterator over values.
265
    pub fn values(&self) -> impl Iterator<Item = &Value> {
×
266
        self.items().iter().map(|kv| &kv.value)
×
267
    }
×
268

269
    /// Returns an iterator over mutable values.
270
    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Value> {
×
271
        self.items_mut().iter_mut().map(|kv| &mut kv.value)
×
272
    }
×
273

274
    /// Returns an iterator over key-value pairs.
275
    pub fn iter(&self) -> Iter<'_> {
1✔
276
        Iter {
1✔
277
            inner: self.items().iter(),
1✔
278
        }
1✔
279
    }
1✔
280

281
    /// Returns an iterator over mutable key-value pairs.
282
    pub fn iter_mut(&mut self) -> IterMut<'_> {
×
283
        IterMut {
×
284
            inner: self.items_mut().iter_mut(),
×
285
        }
×
286
    }
×
287

288
    /// Shrinks the capacity to match the length.
289
    #[cfg(feature = "alloc")]
290
    pub fn shrink_to_fit(&mut self) {
×
291
        let len = self.len();
×
292
        let cap = self.capacity();
×
293

294
        if len < cap {
×
295
            unsafe {
×
296
                let new_ptr = Self::realloc_ptr(self.0.heap_ptr().cast(), len);
×
297
                self.0.set_ptr(new_ptr.cast());
×
298
            }
×
299
        }
×
300
    }
×
301

302
    pub(crate) fn clone_impl(&self) -> Value {
1✔
303
        let mut new = VObject::with_capacity(self.len());
1✔
304
        for kv in self.items() {
1✔
305
            new.insert(kv.key.clone(), kv.value.clone());
1✔
306
        }
1✔
307
        new.0
1✔
308
    }
1✔
309

310
    pub(crate) fn drop_impl(&mut self) {
26✔
311
        self.clear();
26✔
312
        unsafe {
26✔
313
            Self::dealloc_ptr(self.0.heap_ptr().cast());
26✔
314
        }
26✔
315
    }
26✔
316
}
317

318
// === Iterators ===
319

320
/// Iterator over `(&VString, &Value)` pairs.
321
pub struct Iter<'a> {
322
    inner: core::slice::Iter<'a, KeyValuePair>,
323
}
324

325
impl<'a> Iterator for Iter<'a> {
326
    type Item = (&'a VString, &'a Value);
327

328
    fn next(&mut self) -> Option<Self::Item> {
2✔
329
        self.inner.next().map(|kv| (&kv.key, &kv.value))
2✔
330
    }
2✔
331

332
    fn size_hint(&self) -> (usize, Option<usize>) {
×
333
        self.inner.size_hint()
×
334
    }
×
335
}
336

337
impl ExactSizeIterator for Iter<'_> {}
338

339
/// Iterator over `(&VString, &mut Value)` pairs.
340
pub struct IterMut<'a> {
341
    inner: core::slice::IterMut<'a, KeyValuePair>,
342
}
343

344
impl<'a> Iterator for IterMut<'a> {
345
    type Item = (&'a VString, &'a mut Value);
346

347
    fn next(&mut self) -> Option<Self::Item> {
×
348
        self.inner.next().map(|kv| (&kv.key, &mut kv.value))
×
349
    }
×
350

351
    fn size_hint(&self) -> (usize, Option<usize>) {
×
352
        self.inner.size_hint()
×
353
    }
×
354
}
355

356
impl ExactSizeIterator for IterMut<'_> {}
357

358
/// Iterator over owned `(VString, Value)` pairs.
359
pub struct ObjectIntoIter {
360
    object: VObject,
361
}
362

363
impl Iterator for ObjectIntoIter {
364
    type Item = (VString, Value);
365

366
    fn next(&mut self) -> Option<Self::Item> {
×
367
        if self.object.is_empty() {
×
368
            None
×
369
        } else {
370
            // Remove from the front to preserve order
371
            let key = self.object.items()[0].key.as_str().to_owned();
×
372
            self.object.remove_entry(&key)
×
373
        }
374
    }
×
375

376
    fn size_hint(&self) -> (usize, Option<usize>) {
×
377
        let len = self.object.len();
×
378
        (len, Some(len))
×
379
    }
×
380
}
381

382
impl ExactSizeIterator for ObjectIntoIter {}
383

384
impl IntoIterator for VObject {
385
    type Item = (VString, Value);
386
    type IntoIter = ObjectIntoIter;
387

388
    fn into_iter(self) -> Self::IntoIter {
×
389
        ObjectIntoIter { object: self }
×
390
    }
×
391
}
392

393
impl<'a> IntoIterator for &'a VObject {
394
    type Item = (&'a VString, &'a Value);
395
    type IntoIter = Iter<'a>;
396

397
    fn into_iter(self) -> Self::IntoIter {
×
398
        self.iter()
×
399
    }
×
400
}
401

402
impl<'a> IntoIterator for &'a mut VObject {
403
    type Item = (&'a VString, &'a mut Value);
404
    type IntoIter = IterMut<'a>;
405

406
    fn into_iter(self) -> Self::IntoIter {
×
407
        self.iter_mut()
×
408
    }
×
409
}
410

411
// === Index ===
412

413
impl Index<&str> for VObject {
414
    type Output = Value;
415

416
    fn index(&self, key: &str) -> &Value {
17✔
417
        self.get(key).expect("key not found")
17✔
418
    }
17✔
419
}
420

421
impl IndexMut<&str> for VObject {
422
    fn index_mut(&mut self, key: &str) -> &mut Value {
×
423
        self.get_mut(key).expect("key not found")
×
424
    }
×
425
}
426

427
// === Comparison ===
428

429
impl PartialEq for VObject {
430
    fn eq(&self, other: &Self) -> bool {
1✔
431
        if self.len() != other.len() {
1✔
432
            return false;
×
433
        }
1✔
434
        for (k, v) in self.iter() {
1✔
435
            if other.get(k.as_str()) != Some(v) {
1✔
436
                return false;
×
437
            }
1✔
438
        }
439
        true
1✔
440
    }
1✔
441
}
442

443
impl Eq for VObject {}
444

445
impl Hash for VObject {
446
    fn hash<H: Hasher>(&self, state: &mut H) {
×
447
        // Hash length and then each key-value pair
448
        // Note: This doesn't depend on order, which is correct for map semantics
449
        self.len().hash(state);
×
450

451
        // Sum hashes to make order-independent (XOR is order-independent)
452
        let mut total: u64 = 0;
×
453
        for (k, _v) in self.iter() {
×
454
            // Simple hash combining for each pair
455
            let mut kh: u64 = 0;
×
456
            for byte in k.as_bytes() {
×
457
                kh = kh.wrapping_mul(31).wrapping_add(*byte as u64);
×
458
            }
×
459
            // Just XOR the key hash contribution
460
            total ^= kh;
×
461
        }
462
        total.hash(state);
×
463
    }
×
464
}
465

466
impl Debug for VObject {
467
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
×
468
        f.debug_map().entries(self.iter()).finish()
×
469
    }
×
470
}
471

472
impl Default for VObject {
473
    fn default() -> Self {
×
474
        Self::new()
×
475
    }
×
476
}
477

478
// === FromIterator / Extend ===
479

480
#[cfg(feature = "alloc")]
481
impl<K: Into<VString>, V: Into<Value>> FromIterator<(K, V)> for VObject {
482
    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
1✔
483
        let iter = iter.into_iter();
1✔
484
        let (lower, _) = iter.size_hint();
1✔
485
        let mut obj = VObject::with_capacity(lower);
1✔
486
        for (k, v) in iter {
3✔
487
            obj.insert(k, v);
2✔
488
        }
2✔
489
        obj
1✔
490
    }
1✔
491
}
492

493
#[cfg(feature = "alloc")]
494
impl<K: Into<VString>, V: Into<Value>> Extend<(K, V)> for VObject {
495
    fn extend<I: IntoIterator<Item = (K, V)>>(&mut self, iter: I) {
×
496
        let iter = iter.into_iter();
×
497
        let (lower, _) = iter.size_hint();
×
498
        self.reserve(lower);
×
499
        for (k, v) in iter {
×
500
            self.insert(k, v);
×
501
        }
×
502
    }
×
503
}
504

505
// === From implementations ===
506

507
#[cfg(feature = "std")]
508
impl<K: Into<VString>, V: Into<Value>> From<HashMap<K, V>> for VObject {
509
    fn from(map: HashMap<K, V>) -> Self {
×
510
        map.into_iter().collect()
×
511
    }
×
512
}
513

514
#[cfg(feature = "alloc")]
515
impl<K: Into<VString>, V: Into<Value>> From<BTreeMap<K, V>> for VObject {
516
    fn from(map: BTreeMap<K, V>) -> Self {
×
517
        map.into_iter().collect()
×
518
    }
×
519
}
520

521
// === Value conversions ===
522

523
impl AsRef<Value> for VObject {
524
    fn as_ref(&self) -> &Value {
×
525
        &self.0
×
526
    }
×
527
}
528

529
impl AsMut<Value> for VObject {
530
    fn as_mut(&mut self) -> &mut Value {
×
531
        &mut self.0
×
532
    }
×
533
}
534

535
impl From<VObject> for Value {
536
    fn from(obj: VObject) -> Self {
17✔
537
        obj.0
17✔
538
    }
17✔
539
}
540

541
impl VObject {
542
    /// Converts this VObject into a Value, consuming self.
543
    #[inline]
NEW
544
    pub fn into_value(self) -> Value {
×
NEW
545
        self.0
×
NEW
546
    }
×
547
}
548

549
#[cfg(test)]
550
mod tests {
551
    use super::*;
552

553
    #[test]
554
    fn test_new() {
1✔
555
        let obj = VObject::new();
1✔
556
        assert!(obj.is_empty());
1✔
557
        assert_eq!(obj.len(), 0);
1✔
558
    }
1✔
559

560
    #[test]
561
    fn test_insert_get() {
1✔
562
        let mut obj = VObject::new();
1✔
563
        obj.insert("name", Value::from("Alice"));
1✔
564
        obj.insert("age", Value::from(30));
1✔
565

566
        assert_eq!(obj.len(), 2);
1✔
567
        assert!(obj.contains_key("name"));
1✔
568
        assert!(obj.contains_key("age"));
1✔
569
        assert!(!obj.contains_key("email"));
1✔
570

571
        assert_eq!(
1✔
572
            obj.get("name").unwrap().as_string().unwrap().as_str(),
1✔
573
            "Alice"
574
        );
575
        assert_eq!(
1✔
576
            obj.get("age").unwrap().as_number().unwrap().to_i64(),
1✔
577
            Some(30)
578
        );
579
    }
1✔
580

581
    #[test]
582
    fn test_insert_replace() {
1✔
583
        let mut obj = VObject::new();
1✔
584
        assert!(obj.insert("key", Value::from(1)).is_none());
1✔
585
        assert!(obj.insert("key", Value::from(2)).is_some());
1✔
586
        assert_eq!(obj.len(), 1);
1✔
587
        assert_eq!(
1✔
588
            obj.get("key").unwrap().as_number().unwrap().to_i64(),
1✔
589
            Some(2)
590
        );
591
    }
1✔
592

593
    #[test]
594
    fn test_remove() {
1✔
595
        let mut obj = VObject::new();
1✔
596
        obj.insert("a", Value::from(1));
1✔
597
        obj.insert("b", Value::from(2));
1✔
598
        obj.insert("c", Value::from(3));
1✔
599

600
        let removed = obj.remove("b");
1✔
601
        assert!(removed.is_some());
1✔
602
        assert_eq!(obj.len(), 2);
1✔
603
        assert!(!obj.contains_key("b"));
1✔
604
    }
1✔
605

606
    #[test]
607
    fn test_clone() {
1✔
608
        let mut obj = VObject::new();
1✔
609
        obj.insert("key", Value::from("value"));
1✔
610

611
        let obj2 = obj.clone();
1✔
612
        assert_eq!(obj, obj2);
1✔
613
    }
1✔
614

615
    #[test]
616
    fn test_iter() {
1✔
617
        let mut obj = VObject::new();
1✔
618
        obj.insert("a", Value::from(1));
1✔
619
        obj.insert("b", Value::from(2));
1✔
620

621
        let keys: Vec<_> = obj.keys().map(|k| k.as_str()).collect();
2✔
622
        assert_eq!(keys, vec!["a", "b"]);
1✔
623
    }
1✔
624

625
    #[test]
626
    fn test_collect() {
1✔
627
        let obj: VObject = vec![("a", Value::from(1)), ("b", Value::from(2))]
1✔
628
            .into_iter()
1✔
629
            .collect();
1✔
630
        assert_eq!(obj.len(), 2);
1✔
631
    }
1✔
632

633
    #[test]
634
    fn test_index() {
1✔
635
        let mut obj = VObject::new();
1✔
636
        obj.insert("key", Value::from(42));
1✔
637

638
        assert_eq!(obj["key"].as_number().unwrap().to_i64(), Some(42));
1✔
639
    }
1✔
640
}
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