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

bblanchon / ArduinoJson / 5082307777

pending completion
5082307777

push

github

Benoit Blanchon
Move all functions from `VariantFunctions.hpp` to ``VariantData.hpp`

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

3317 of 3336 relevant lines covered (99.43%)

6264.05 hits per line

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

99.23
/src/ArduinoJson/Variant/VariantData.hpp
1
// ArduinoJson - https://arduinojson.org
2
// Copyright © 2014-2023, Benoit BLANCHON
3
// MIT License
4

5
#pragma once
6

7
#include <ArduinoJson/Memory/StringNode.hpp>
8
#include <ArduinoJson/Misc/SerializedValue.hpp>
9
#include <ArduinoJson/Numbers/convertNumber.hpp>
10
#include <ArduinoJson/Strings/JsonString.hpp>
11
#include <ArduinoJson/Strings/StringAdapters.hpp>
12
#include <ArduinoJson/Variant/VariantContent.hpp>
13
#include <ArduinoJson/Variant/VariantSlot.hpp>
14

15
ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
16

17
VariantData* collectionAddElement(CollectionData* array, MemoryPool* pool);
18
bool collectionCopy(CollectionData* dst, const CollectionData* src,
19
                    MemoryPool* pool);
20
void collectionRemoveElement(CollectionData* data, size_t index,
21
                             MemoryPool* pool);
22
template <typename T>
23
T parseNumber(const char* s);
24
void slotRelease(VariantSlot* slot, MemoryPool* pool);
25

26
class VariantData {
27
  VariantContent content_;  // must be first to allow cast from array to variant
28
  uint8_t flags_;
29

30
 public:
31
  VariantData() : flags_(VALUE_IS_NULL) {}
2,082✔
32

33
  template <typename TVisitor>
34
  typename TVisitor::result_type accept(TVisitor& visitor) const {
137,781✔
35
    switch (type()) {
137,781✔
36
      case VALUE_IS_FLOAT:
135✔
37
        return visitor.visitFloat(content_.asFloat);
135✔
38

39
      case VALUE_IS_ARRAY:
897✔
40
        return visitor.visitArray(content_.asCollection);
1,107✔
41

42
      case VALUE_IS_OBJECT:
2,588✔
43
        return visitor.visitObject(content_.asCollection);
2,588✔
44

45
      case VALUE_IS_LINKED_STRING:
670✔
46
        return visitor.visitString(content_.asLinkedString,
670✔
47
                                   strlen(content_.asLinkedString));
670✔
48

49
      case VALUE_IS_OWNED_STRING:
282✔
50
        return visitor.visitString(content_.asOwnedString->data,
282✔
51
                                   content_.asOwnedString->length);
282✔
52

53
      case VALUE_IS_RAW_STRING:
55✔
54
        return visitor.visitRawString(content_.asOwnedString->data,
55✔
55
                                      content_.asOwnedString->length);
55✔
56

57
      case VALUE_IS_SIGNED_INTEGER:
1,041✔
58
        return visitor.visitSignedInteger(content_.asSignedInteger);
1,041✔
59

60
      case VALUE_IS_UNSIGNED_INTEGER:
257✔
61
        return visitor.visitUnsignedInteger(content_.asUnsignedInteger);
257✔
62

63
      case VALUE_IS_BOOLEAN:
616✔
64
        return visitor.visitBoolean(content_.asBoolean != 0);
616✔
65

66
      default:
131,240✔
67
        return visitor.visitNull();
131,240✔
68
    }
69
  }
70

71
  inline VariantData* addElement(MemoryPool* pool) {
242✔
72
    auto array = isNull() ? &toArray() : asArray();
242✔
73
    return collectionAddElement(array, pool);
242✔
74
  }
75

76
  bool asBoolean() const {
554✔
77
    switch (type()) {
554✔
78
      case VALUE_IS_BOOLEAN:
307✔
79
        return content_.asBoolean;
307✔
80
      case VALUE_IS_SIGNED_INTEGER:
7✔
81
      case VALUE_IS_UNSIGNED_INTEGER:
82
        return content_.asUnsignedInteger != 0;
7✔
83
      case VALUE_IS_FLOAT:
5✔
84
        return content_.asFloat != 0;
5✔
85
      case VALUE_IS_NULL:
6✔
86
        return false;
6✔
87
      default:
229✔
88
        return true;
229✔
89
    }
90
  }
91

92
  CollectionData* asArray() {
706✔
93
    return isArray() ? &content_.asCollection : 0;
706✔
94
  }
95

96
  const CollectionData* asArray() const {
459✔
97
    return const_cast<VariantData*>(this)->asArray();
459✔
98
  }
99

100
  const CollectionData* asCollection() const {
67,927✔
101
    return isCollection() ? &content_.asCollection : 0;
67,927✔
102
  }
103

104
  template <typename T>
105
  T asFloat() const {
117✔
106
    static_assert(is_floating_point<T>::value, "T must be a floating point");
107
    switch (type()) {
117✔
108
      case VALUE_IS_BOOLEAN:
2✔
109
        return static_cast<T>(content_.asBoolean);
2✔
110
      case VALUE_IS_UNSIGNED_INTEGER:
3✔
111
        return static_cast<T>(content_.asUnsignedInteger);
3✔
112
      case VALUE_IS_SIGNED_INTEGER:
6✔
113
        return static_cast<T>(content_.asSignedInteger);
6✔
114
      case VALUE_IS_LINKED_STRING:
1✔
115
      case VALUE_IS_OWNED_STRING:
116
        return parseNumber<T>(content_.asOwnedString->data);
1✔
117
      case VALUE_IS_FLOAT:
103✔
118
        return static_cast<T>(content_.asFloat);
103✔
119
      default:
2✔
120
        return 0;
2✔
121
    }
122
  }
123

124
  template <typename T>
125
  T asIntegral() const {
210✔
126
    static_assert(is_integral<T>::value, "T must be an integral type");
127
    switch (type()) {
210✔
128
      case VALUE_IS_BOOLEAN:
2✔
129
        return content_.asBoolean;
2✔
130
      case VALUE_IS_UNSIGNED_INTEGER:
87✔
131
        return convertNumber<T>(content_.asUnsignedInteger);
87✔
132
      case VALUE_IS_SIGNED_INTEGER:
84✔
133
        return convertNumber<T>(content_.asSignedInteger);
84✔
134
      case VALUE_IS_LINKED_STRING:
6✔
135
        return parseNumber<T>(content_.asLinkedString);
6✔
136
      case VALUE_IS_OWNED_STRING:
2✔
137
        return parseNumber<T>(content_.asOwnedString->data);
2✔
138
      case VALUE_IS_FLOAT:
19✔
139
        return convertNumber<T>(content_.asFloat);
19✔
140
      default:
10✔
141
        return 0;
10✔
142
    }
143
  }
144

145
  CollectionData* asObject() {
2,744✔
146
    return isObject() ? &content_.asCollection : 0;
2,744✔
147
  }
148

149
  const CollectionData* asObject() const {
2,152✔
150
    return const_cast<VariantData*>(this)->asObject();
2,152✔
151
  }
152

153
  JsonString asRawString() const {
4✔
154
    switch (type()) {
4✔
155
      case VALUE_IS_RAW_STRING:
4✔
156
        return JsonString(content_.asOwnedString->data,
4✔
157
                          content_.asOwnedString->length, JsonString::Copied);
4✔
158
      default:
×
159
        return JsonString();
×
160
    }
161
  }
162

163
  JsonString asString() const {
444✔
164
    switch (type()) {
444✔
165
      case VALUE_IS_LINKED_STRING:
30✔
166
        return JsonString(content_.asLinkedString, JsonString::Linked);
30✔
167
      case VALUE_IS_OWNED_STRING:
76✔
168
        return JsonString(content_.asOwnedString->data,
76✔
169
                          content_.asOwnedString->length, JsonString::Copied);
76✔
170
      default:
338✔
171
        return JsonString();
338✔
172
    }
173
  }
174

175
  bool copyFrom(const VariantData* src, MemoryPool* pool) {
71✔
176
    release(pool);
71✔
177
    if (!src) {
71✔
178
      setNull();
4✔
179
      return true;
4✔
180
    }
181
    switch (src->type()) {
67✔
182
      case VALUE_IS_ARRAY:
12✔
183
        return collectionCopy(&toArray(), src->asArray(), pool);
12✔
184
      case VALUE_IS_OBJECT:
17✔
185
        return collectionCopy(&toObject(), src->asObject(), pool);
17✔
186
      case VALUE_IS_OWNED_STRING: {
11✔
187
        auto str = adaptString(src->asString());
11✔
188
        auto dup = pool->saveString(str);
11✔
189
        if (!dup)
11✔
190
          return false;
1✔
191
        setOwnedString(dup);
10✔
192
        return true;
10✔
193
      }
194
      case VALUE_IS_RAW_STRING: {
4✔
195
        auto str = adaptString(src->asRawString());
4✔
196
        auto dup = pool->saveString(str);
4✔
197
        if (!dup)
4✔
198
          return false;
1✔
199
        setRawString(dup);
3✔
200
        return true;
3✔
201
      }
202
      default:
23✔
203
        content_ = src->content_;
23✔
204
        flags_ = src->flags_;
23✔
205
        return true;
23✔
206
    }
207
  }
208

209
  VariantData* getElement(size_t index) const {
424✔
210
    auto array = asArray();
424✔
211
    if (!array)
424✔
212
      return nullptr;
15✔
213
    return slotData(array->get(index));
409✔
214
  }
215

216
  template <typename TAdaptedString>
217
  VariantData* getMember(TAdaptedString key) const {
2,135✔
218
    auto object = asObject();
2,135✔
219
    if (!object)
2,135✔
220
      return nullptr;
44✔
221
    return slotData(object->get(key));
2,091✔
222
  }
223

224
  VariantData* getOrAddElement(size_t index, MemoryPool* pool) {
136✔
225
    auto array = isNull() ? &toArray() : asArray();
136✔
226
    if (!array)
136✔
227
      return nullptr;
1✔
228
    VariantSlot* slot = array->head();
135✔
229
    while (slot && index > 0) {
145✔
230
      slot = slot->next();
10✔
231
      index--;
10✔
232
    }
233
    if (!slot)
135✔
234
      index++;
101✔
235
    while (index > 0) {
249✔
236
      slot = new (pool) VariantSlot();
228✔
237
      if (!slot)
114✔
238
        return nullptr;
×
239
      array->add(slot);
114✔
240
      index--;
114✔
241
    }
242
    return slot->data();
135✔
243
  }
244

245
  template <typename TAdaptedString>
246
  VariantData* getOrAddMember(TAdaptedString key, MemoryPool* pool) {
799✔
247
    if (key.isNull())
799✔
248
      return nullptr;
1✔
249
    auto obj = isNull() ? &toObject() : asObject();
798✔
250
    if (!obj)
798✔
251
      return nullptr;
1✔
252
    auto slot = obj->get(key);
797✔
253
    if (slot)
797✔
254
      return slot->data();
33✔
255
    return collectionAddMember(obj, key, pool);
764✔
256
  }
257

258
  bool isArray() const {
802✔
259
    return (flags_ & VALUE_IS_ARRAY) != 0;
802✔
260
  }
261

262
  bool isBoolean() const {
35✔
263
    return type() == VALUE_IS_BOOLEAN;
35✔
264
  }
265

266
  bool isCollection() const {
67,966✔
267
    return (flags_ & COLLECTION_MASK) != 0;
67,966✔
268
  }
269

270
  bool isFloat() const {
402✔
271
    return (flags_ & NUMBER_BIT) != 0;
402✔
272
  }
273

274
  template <typename T>
275
  bool isInteger() const {
123✔
276
    switch (type()) {
123✔
277
      case VALUE_IS_UNSIGNED_INTEGER:
12✔
278
        return canConvertNumber<T>(content_.asUnsignedInteger);
12✔
279

280
      case VALUE_IS_SIGNED_INTEGER:
57✔
281
        return canConvertNumber<T>(content_.asSignedInteger);
57✔
282

283
      default:
54✔
284
        return false;
54✔
285
    }
286
  }
287

288
  bool isNull() const {
1,690✔
289
    return type() == VALUE_IS_NULL;
1,690✔
290
  }
291

292
  bool isObject() const {
3,094✔
293
    return (flags_ & VALUE_IS_OBJECT) != 0;
3,094✔
294
  }
295

296
  bool isString() const {
62✔
297
    return type() == VALUE_IS_LINKED_STRING || type() == VALUE_IS_OWNED_STRING;
62✔
298
  }
299

300
  size_t memoryUsage() const {
34✔
301
    switch (type()) {
34✔
302
      case VALUE_IS_OWNED_STRING:
10✔
303
      case VALUE_IS_RAW_STRING:
304
        return sizeofString(content_.asOwnedString->length);
10✔
305
      case VALUE_IS_OBJECT:
6✔
306
      case VALUE_IS_ARRAY:
307
        return content_.asCollection.memoryUsage();
6✔
308
      default:
18✔
309
        return 0;
18✔
310
    }
311
  }
312

313
  void movePointers(ptrdiff_t variantDistance) {
22✔
314
    if (flags_ & COLLECTION_MASK)
22✔
315
      content_.asCollection.movePointers(variantDistance);
10✔
316
  }
22✔
317

318
  size_t nesting() const {
21✔
319
    auto collection = asCollection();
21✔
320
    if (!collection)
21✔
321
      return 0;
5✔
322

323
    size_t maxChildNesting = 0;
16✔
324
    for (const VariantSlot* s = collection->head(); s; s = s->next()) {
22✔
325
      size_t childNesting = s->data()->nesting();
6✔
326
      if (childNesting > maxChildNesting)
6✔
327
        maxChildNesting = childNesting;
4✔
328
    }
329
    return maxChildNesting + 1;
16✔
330
  }
331

332
  void operator=(const VariantData& src) {
16✔
333
    content_ = src.content_;
16✔
334
    flags_ = uint8_t((flags_ & OWNED_KEY_BIT) | (src.flags_ & ~OWNED_KEY_BIT));
16✔
335
  }
16✔
336

337
  inline void removeElement(size_t index, MemoryPool* pool) {
7✔
338
    collectionRemoveElement(asArray(), index, pool);
7✔
339
  }
7✔
340

341
  template <typename TAdaptedString>
342
  void removeMember(TAdaptedString key, MemoryPool* pool) {
13✔
343
    collectionRemoveMember(asObject(), key, pool);
13✔
344
  }
13✔
345

346
  void reset() {
1,763✔
347
    flags_ = VALUE_IS_NULL;
1,763✔
348
  }
1,763✔
349

350
  void setBoolean(bool value) {
337✔
351
    setType(VALUE_IS_BOOLEAN);
337✔
352
    content_.asBoolean = value;
337✔
353
  }
337✔
354

355
  void setBoolean(bool value, MemoryPool* pool) {
249✔
356
    release(pool);
249✔
357
    setBoolean(value);
249✔
358
  }
249✔
359

360
  void setFloat(JsonFloat value) {
231✔
361
    setType(VALUE_IS_FLOAT);
231✔
362
    content_.asFloat = value;
231✔
363
  }
231✔
364

365
  void setFloat(JsonFloat value, MemoryPool* pool) {
61✔
366
    release(pool);
61✔
367
    setFloat(value);
61✔
368
  }
61✔
369

370
  template <typename T>
371
  typename enable_if<is_signed<T>::value>::type setInteger(T value) {
723✔
372
    setType(VALUE_IS_SIGNED_INTEGER);
723✔
373
    content_.asSignedInteger = value;
723✔
374
  }
723✔
375

376
  template <typename T>
377
  typename enable_if<is_unsigned<T>::value>::type setInteger(T value) {
2,313✔
378
    setType(VALUE_IS_UNSIGNED_INTEGER);
2,313✔
379
    content_.asUnsignedInteger = static_cast<JsonUInt>(value);
2,313✔
380
  }
2,313✔
381

382
  template <typename T>
383
  void setInteger(T value, MemoryPool* pool) {
571✔
384
    release(pool);
571✔
385
    setInteger(value);
571✔
386
  }
571✔
387

388
  void setNull() {
66,444✔
389
    setType(VALUE_IS_NULL);
66,444✔
390
  }
66,444✔
391

392
  void setNull(MemoryPool* pool) {
66,433✔
393
    release(pool);
66,433✔
394
    setNull();
66,433✔
395
  }
66,433✔
396

397
  void setRawString(StringNode* s) {
30✔
398
    ARDUINOJSON_ASSERT(s);
399
    setType(VALUE_IS_RAW_STRING);
30✔
400
    content_.asOwnedString = s;
30✔
401
  }
30✔
402

403
  template <typename T>
404
  void setRawString(SerializedValue<T> value, MemoryPool* pool) {
30✔
405
    release(pool);
30✔
406
    auto dup = pool->saveString(adaptString(value.data(), value.size()));
30✔
407
    if (dup)
30✔
408
      setRawString(dup);
27✔
409
    else
410
      setNull();
3✔
411
  }
30✔
412

413
  template <typename TAdaptedString>
414
  void setString(TAdaptedString value, MemoryPool* pool) {
65,954✔
415
    setNull(pool);
65,954✔
416

417
    if (value.isNull())
65,954✔
418
      return;
65,553✔
419

420
    if (value.isLinked()) {
401✔
421
      setLinkedString(value.data());
300✔
422
      return;
300✔
423
    }
424

425
    auto dup = pool->saveString(value);
101✔
426
    if (dup)
101✔
427
      setOwnedString(dup);
98✔
428
  }
429

430
  void setLinkedString(const char* s) {
300✔
431
    ARDUINOJSON_ASSERT(s);
432
    setType(VALUE_IS_LINKED_STRING);
300✔
433
    content_.asLinkedString = s;
300✔
434
  }
300✔
435

436
  void setOwnedString(StringNode* s) {
469✔
437
    ARDUINOJSON_ASSERT(s);
438
    setType(VALUE_IS_OWNED_STRING);
469✔
439
    content_.asOwnedString = s;
469✔
440
  }
469✔
441

442
  size_t size() const {
39✔
443
    return isCollection() ? content_.asCollection.size() : 0;
39✔
444
  }
445

446
  CollectionData& toArray() {
1,217✔
447
    setType(VALUE_IS_ARRAY);
1,217✔
448
    content_.asCollection.clear();
1,217✔
449
    return content_.asCollection;
1,217✔
450
  }
451

452
  CollectionData& toArray(MemoryPool* pool) {
220✔
453
    release(pool);
220✔
454
    return toArray();
220✔
455
  }
456

457
  CollectionData& toObject() {
1,269✔
458
    setType(VALUE_IS_OBJECT);
1,269✔
459
    content_.asCollection.clear();
1,269✔
460
    return content_.asCollection;
1,269✔
461
  }
462

463
  CollectionData& toObject(MemoryPool* pool) {
271✔
464
    release(pool);
271✔
465
    return toObject();
271✔
466
  }
467

468
  uint8_t type() const {
141,172✔
469
    return flags_ & VALUE_MASK;
141,172✔
470
  }
471

472
 private:
473
  void release(MemoryPool* pool) {
67,906✔
474
    if (flags_ & OWNED_VALUE_BIT)
67,906✔
475
      pool->dereferenceString(content_.asOwnedString->data);
11✔
476

477
    auto c = asCollection();
67,906✔
478
    if (c) {
67,906✔
479
      for (auto slot = c->head(); slot; slot = slot->next())
29✔
480
        slotRelease(slot, pool);
14✔
481
    }
482
  }
67,906✔
483

484
  void setType(uint8_t t) {
73,333✔
485
    flags_ &= OWNED_KEY_BIT;
73,333✔
486
    flags_ |= t;
73,333✔
487
  }
73,333✔
488
};
489

490
template <typename TVisitor>
491
inline typename TVisitor::result_type variantAccept(const VariantData* var,
5,534✔
492
                                                    TVisitor& visitor) {
493
  if (var != 0)
5,534✔
494
    return var->accept(visitor);
5,377✔
495
  else
496
    return visitor.visitNull();
157✔
497
}
498

499
inline bool variantCopyFrom(VariantData* dst, const VariantData* src,
75✔
500
                            MemoryPool* pool) {
501
  if (!dst)
75✔
502
    return false;
4✔
503
  return dst->copyFrom(src, pool);
71✔
504
}
505

506
inline VariantData* variantAddElement(VariantData* var, MemoryPool* pool) {
248✔
507
  if (!var)
248✔
508
    return nullptr;
6✔
509
  return var->addElement(pool);
242✔
510
}
511

512
inline VariantData* variantGetElement(const VariantData* var, size_t index) {
429✔
513
  return var != 0 ? var->getElement(index) : 0;
429✔
514
}
515

516
template <typename TAdaptedString>
517
VariantData* variantGetMember(const VariantData* var, TAdaptedString key) {
2,140✔
518
  if (!var)
2,140✔
519
    return 0;
13✔
520
  return var->getMember(key);
2,127✔
521
}
522

523
inline NO_INLINE VariantData* variantGetOrAddElement(VariantData* var,
137✔
524
                                                     size_t index,
525
                                                     MemoryPool* pool) {
526
  if (!var)
137✔
527
    return nullptr;
1✔
528
  return var->getOrAddElement(index, pool);
136✔
529
}
530

531
template <typename TAdaptedString>
532
VariantData* variantGetOrAddMember(VariantData* var, TAdaptedString key,
804✔
533
                                   MemoryPool* pool) {
534
  if (!var)
804✔
535
    return nullptr;
5✔
536
  return var->getOrAddMember(key, pool);
799✔
537
}
538

539
inline bool variantIsNull(const VariantData* var) {
1,237✔
540
  if (!var)
1,237✔
541
    return true;
726✔
542
  return var->isNull();
511✔
543
}
544

545
inline size_t variantNesting(const VariantData* var) {
18✔
546
  if (!var)
18✔
547
    return 0;
3✔
548
  return var->nesting();
15✔
549
}
550

551
inline void variantRemoveElement(VariantData* var, size_t index,
8✔
552
                                 MemoryPool* pool) {
553
  if (!var)
8✔
554
    return;
1✔
555
  var->removeElement(index, pool);
7✔
556
}
557

558
template <typename TAdaptedString>
559
void variantRemoveMember(VariantData* var, TAdaptedString key,
14✔
560
                         MemoryPool* pool) {
561
  if (!var)
14✔
562
    return;
1✔
563
  var->removeMember(key, pool);
13✔
564
}
565

566
inline void variantSetBoolean(VariantData* var, bool value, MemoryPool* pool) {
253✔
567
  if (!var)
253✔
568
    return;
4✔
569
  var->setBoolean(value, pool);
249✔
570
}
571

572
inline void variantSetFloat(VariantData* var, JsonFloat value,
63✔
573
                            MemoryPool* pool) {
574
  if (!var)
63✔
575
    return;
2✔
576
  var->setFloat(value, pool);
61✔
577
}
578

579
template <typename T>
580
inline void variantSetInteger(VariantData* var, T value, MemoryPool* pool) {
590✔
581
  if (!var)
590✔
582
    return;
19✔
583
  var->setInteger(value, pool);
571✔
584
}
585

586
inline void variantSetNull(VariantData* var, MemoryPool* pool) {
419✔
587
  if (!var)
419✔
588
    return;
1✔
589
  var->setNull(pool);
418✔
590
}
591

592
template <typename T>
593
inline void variantSetRawString(VariantData* var, SerializedValue<T> value,
33✔
594
                                MemoryPool* pool) {
595
  if (!var)
33✔
596
    return;
3✔
597
  var->setRawString(value, pool);
30✔
598
}
599

600
template <typename TAdaptedString>
601
inline void variantSetString(VariantData* var, TAdaptedString value,
65,960✔
602
                             MemoryPool* pool) {
603
  if (!var)
65,960✔
604
    return;
6✔
605
  var->setString(value, pool);
65,954✔
606
}
607

608
inline size_t variantSize(const VariantData* var) {
30✔
609
  return var != 0 ? var->size() : 0;
30✔
610
}
611

612
inline CollectionData* variantToArray(VariantData* var, MemoryPool* pool) {
222✔
613
  if (!var)
222✔
614
    return 0;
2✔
615
  return &var->toArray(pool);
220✔
616
}
617

618
inline CollectionData* variantToObject(VariantData* var, MemoryPool* pool) {
273✔
619
  if (!var)
273✔
620
    return 0;
2✔
621
  return &var->toObject(pool);
271✔
622
}
623

624
ARDUINOJSON_END_PRIVATE_NAMESPACE
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

© 2025 Coveralls, Inc