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

bblanchon / ArduinoJson / 18593280678

17 Oct 2025 12:52PM UTC coverage: 99.417% (+0.02%) from 99.399%
18593280678

push

github

bblanchon
ResourceManager: move out-of-class definitions back in the class

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

5 existing lines in 3 files now uncovered.

3925 of 3948 relevant lines covered (99.42%)

10567.54 hits per line

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

99.36
/src/ArduinoJson/Variant/VariantImpl.hpp
1
// ArduinoJson - https://arduinojson.org
2
// Copyright © 2014-2025, Benoit BLANCHON
3
// MIT License
4

5
#pragma once
6

7
#include <ArduinoJson/Collection/CollectionIterator.hpp>
8
#include <ArduinoJson/Memory/ResourceManager.hpp>
9
#include <ArduinoJson/Misc/SerializedValue.hpp>
10
#include <ArduinoJson/Numbers/convertNumber.hpp>
11
#include <ArduinoJson/Strings/JsonString.hpp>
12
#include <ArduinoJson/Strings/StringAdapters.hpp>
13
#include <ArduinoJson/Variant/VariantData.hpp>
14

15
ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
16

17
// HACK: large functions are implemented in static function to give opportunity
18
// to the compiler to optimize the `this` pointer away.
19
class VariantImpl {
20
 public:
21
  using iterator = CollectionIterator;
22

23
  VariantImpl() : data_(nullptr), resources_(nullptr) {}
559✔
24

25
  VariantImpl(VariantData* data, ResourceManager* resources)
225,463✔
26
      : data_(data), resources_(resources) {}
225,463✔
27

28
  VariantData* getData() const {
150,013✔
29
    return data_;
150,013✔
30
  }
31

32
  ResourceManager* getResourceManager() const {
223,915✔
33
    return resources_;
223,915✔
34
  }
35

36
  template <typename TVisitor>
37
  typename TVisitor::result_type accept(TVisitor& visit) {
5,329✔
38
    return accept(visit, data_, resources_);
5,329✔
39
  }
40

41
  template <typename TVisitor>
42
  static typename TVisitor::result_type accept(TVisitor& visit,
139,510✔
43
                                               VariantData* data,
44
                                               ResourceManager* resources) {
45
    if (!data)
139,510✔
46
      return visit.visit(nullptr);
170✔
47

48
#if ARDUINOJSON_USE_8_BYTE_POOL
49
    auto eightByteValue = getEightByte(data, resources);
139,340✔
50
#endif
51
    switch (data->type) {
139,340✔
52
      case VariantType::Float:
121✔
53
        return visit.visit(data->content.asFloat);
121✔
54

55
#if ARDUINOJSON_USE_DOUBLE
56
      case VariantType::Double:
16✔
57
        return visit.visit(eightByteValue->asDouble);
16✔
58
#endif
59

60
      case VariantType::Array:
982✔
61
        return visit.visitArray(data);
982✔
62

63
      case VariantType::Object:
2,679✔
64
        return visit.visitObject(data);
2,679✔
65

66
      case VariantType::TinyString:
341✔
67
        return visit.visit(JsonString(data->content.asTinyString));
341✔
68

69
      case VariantType::LongString:
1,783✔
70
        return visit.visit(JsonString(data->content.asStringNode->data,
3,566✔
71
                                      data->content.asStringNode->length));
3,566✔
72

73
      case VariantType::RawString:
129✔
74
        return visit.visit(RawString(data->content.asStringNode->data,
258✔
75
                                     data->content.asStringNode->length));
258✔
76

77
      case VariantType::Int32:
1,184✔
78
        return visit.visit(static_cast<JsonInteger>(data->content.asInt32));
1,184✔
79

80
      case VariantType::Uint32:
256✔
81
        return visit.visit(static_cast<JsonUInt>(data->content.asUint32));
256✔
82

83
#if ARDUINOJSON_USE_LONG_LONG
84
      case VariantType::Int64:
7✔
85
        return visit.visit(eightByteValue->asInt64);
7✔
86

87
      case VariantType::Uint64:
8✔
88
        return visit.visit(eightByteValue->asUint64);
8✔
89
#endif
90

91
      case VariantType::Boolean:
607✔
92
        return visit.visit(data->content.asBoolean != 0);
607✔
93

94
      default:
131,227✔
95
        return visit.visit(nullptr);
131,227✔
96
    }
97
  }
98

99
  VariantData* addElement() {
611✔
100
    if (!isArray())
611✔
101
      return nullptr;
8✔
102
    return addElement(data_, resources_);
603✔
103
  }
104

105
  static VariantData* addElement(VariantData*, ResourceManager*);
106

107
  template <typename TAdaptedString>
108
  VariantData* addMember(TAdaptedString key) {
109
    if (!isObject())
110
      return nullptr;
111
    return addMember(key, data_, resources_);
112
  }
113

114
  template <typename TAdaptedString>
115
  static VariantData* addMember(TAdaptedString key, VariantData*,
116
                                ResourceManager*);
117

118
  VariantData* addPair(VariantData** value) {
119
    if (isNull())
120
      return nullptr;
121
    return addPair(value, data_, resources_);
122
  }
123

124
  static VariantData* addPair(VariantData** value, VariantData*,
125
                              ResourceManager*);
126

127
  template <typename T>
128
  bool addValue(const T& value) {
66,686✔
129
    if (!isArray())
66,686✔
130
      return false;
9✔
131
    return addValue(value, data_, resources_);
66,677✔
132
  }
133

134
  template <typename T>
135
  static bool addValue(const T& value, VariantData*, ResourceManager*);
136

137
  bool asBoolean() const {
1,254✔
138
    return asBoolean(data_, resources_);
1,254✔
139
  }
140

141
  static bool asBoolean(VariantData* data, ResourceManager* resources) {
1,254✔
142
    if (!data)
1,254✔
143
      return false;
699✔
144

145
#if ARDUINOJSON_USE_8_BYTE_POOL
146
    auto eightByteValue = getEightByte(data, resources);
555✔
147
#endif
148
    switch (data->type) {
555✔
149
      case VariantType::Boolean:
305✔
150
        return data->content.asBoolean;
305✔
151
      case VariantType::Uint32:
6✔
152
      case VariantType::Int32:
153
        return data->content.asUint32 != 0;
6✔
154
      case VariantType::Float:
2✔
155
        return data->content.asFloat != 0;
2✔
156
#if ARDUINOJSON_USE_DOUBLE
157
      case VariantType::Double:
4✔
158
        return eightByteValue->asDouble != 0;
4✔
159
#endif
160
      case VariantType::Null:
6✔
161
        return false;
6✔
162
#if ARDUINOJSON_USE_LONG_LONG
163
      case VariantType::Uint64:
2✔
164
      case VariantType::Int64:
165
        return eightByteValue->asUint64 != 0;
2✔
166
#endif
167
      default:
230✔
168
        return true;
230✔
169
    }
170
  }
171

172
  template <typename T>
173
  T asFloat() const {
55✔
174
    return asFloat<T>(data_, resources_);
55✔
175
  }
176

177
  template <typename T>
178
  static T asFloat(VariantData* data, ResourceManager* resources) {
55✔
179
    if (!data)
55✔
180
      return 0.0;
1✔
181

182
    static_assert(is_floating_point<T>::value, "T must be a floating point");
183
#if ARDUINOJSON_USE_8_BYTE_POOL
184
    auto eightByteValue = getEightByte(data, resources);
54✔
185
#endif
186
    const char* str = nullptr;
54✔
187
    switch (data->type) {
54✔
188
      case VariantType::Boolean:
2✔
189
        return static_cast<T>(data->content.asBoolean);
2✔
190
      case VariantType::Uint32:
2✔
191
        return static_cast<T>(data->content.asUint32);
2✔
192
      case VariantType::Int32:
4✔
193
        return static_cast<T>(data->content.asInt32);
4✔
194
#if ARDUINOJSON_USE_LONG_LONG
195
      case VariantType::Uint64:
1✔
196
        return static_cast<T>(eightByteValue->asUint64);
1✔
197
      case VariantType::Int64:
1✔
198
        return static_cast<T>(eightByteValue->asInt64);
1✔
199
#endif
200
      case VariantType::TinyString:
2✔
201
        str = data->content.asTinyString;
2✔
202
        break;
2✔
203
      case VariantType::LongString:
1✔
204
        str = data->content.asStringNode->data;
1✔
205
        break;
1✔
206
      case VariantType::Float:
18✔
207
        return static_cast<T>(data->content.asFloat);
18✔
208
#if ARDUINOJSON_USE_DOUBLE
209
      case VariantType::Double:
21✔
210
        return static_cast<T>(eightByteValue->asDouble);
21✔
211
#endif
212
      default:
2✔
213
        return 0.0;
2✔
214
    }
215

216
    ARDUINOJSON_ASSERT(str != nullptr);
217
    return parseNumber<T>(str);
3✔
218
  }
219

220
  template <typename T>
221
  T asIntegral() const {
203✔
222
    return asIntegral<T>(data_, resources_);
203✔
223
  }
224

225
  template <typename T>
226
  static T asIntegral(VariantData* data, ResourceManager* resources) {
203✔
227
    if (!data)
203✔
228
      return 0;
5✔
229

230
    static_assert(is_integral<T>::value, "T must be an integral type");
231
#if ARDUINOJSON_USE_8_BYTE_POOL
232
    auto eightByteValue = getEightByte(data, resources);
198✔
233
#endif
234
    const char* str = nullptr;
198✔
235
    switch (data->type) {
198✔
236
      case VariantType::Boolean:
2✔
237
        return data->content.asBoolean;
2✔
238
      case VariantType::Uint32:
66✔
239
        return convertNumber<T>(data->content.asUint32);
66✔
240
      case VariantType::Int32:
83✔
241
        return convertNumber<T>(data->content.asInt32);
83✔
242
#if ARDUINOJSON_USE_LONG_LONG
243
      case VariantType::Uint64:
10✔
244
        return convertNumber<T>(eightByteValue->asUint64);
10✔
245
      case VariantType::Int64:
11✔
246
        return convertNumber<T>(eightByteValue->asInt64);
11✔
247
#endif
248
      case VariantType::TinyString:
3✔
249
        str = data->content.asTinyString;
3✔
250
        break;
3✔
251
      case VariantType::LongString:
6✔
252
        str = data->content.asStringNode->data;
6✔
253
        break;
6✔
254
      case VariantType::Float:
5✔
255
        return convertNumber<T>(data->content.asFloat);
5✔
256
#if ARDUINOJSON_USE_DOUBLE
257
      case VariantType::Double:
10✔
258
        return convertNumber<T>(eightByteValue->asDouble);
10✔
259
#endif
260
      default:
2✔
261
        return 0;
2✔
262
    }
263

264
    ARDUINOJSON_ASSERT(str != nullptr);
265
    return parseNumber<T>(str);
9✔
266
  }
267

268
  iterator at(size_t index) const;
269

270
  iterator createIterator() const {
1,512✔
271
    if (!isCollection())
1,512✔
272
      return iterator();
10✔
273
    return createIterator(data_, resources_);
1,502✔
274
  }
275

276
  static iterator createIterator(VariantData*, ResourceManager*);
277

278
#if ARDUINOJSON_USE_8_BYTE_POOL
279
  static const EightByteValue* getEightByte(VariantData* data,
140,295✔
280
                                            ResourceManager* resources) {
281
    ARDUINOJSON_ASSERT(data != nullptr);
282
    ARDUINOJSON_ASSERT(resources != nullptr);
283
    return data->type & VariantTypeBits::EightByteBit
140,295✔
284
               ? resources->getEightByte(data->content.asSlotId)
140,295✔
285
               : 0;
140,295✔
286
  }
287
#endif
288

289
  VariantData* getOrAddElement(size_t index);
290

291
  VariantData* getElement(size_t index) const;
292

293
  template <typename TAdaptedString>
294
  VariantData* getMember(TAdaptedString key) const {
2,663✔
295
    if (!isObject())
2,663✔
296
      return nullptr;
57✔
297
    return getMember(key, data_, resources_);
2,606✔
298
  }
299

300
  template <typename TAdaptedString>
301
  static VariantData* getMember(TAdaptedString key, VariantData*,
302
                                ResourceManager*);
303

304
  template <typename TAdaptedString>
305
  VariantData* getOrAddMember(TAdaptedString key) {
950✔
306
    if (!isObject())
950✔
307
      return nullptr;
3✔
308
    return getOrAddMember(key, data_, resources_);
947✔
309
  }
310

311
  template <typename TAdaptedString>
312
  static VariantData* getOrAddMember(TAdaptedString key, VariantData*,
313
                                     ResourceManager*);
314

315
  bool isArray() const {
67,791✔
316
    return type() == VariantType::Array;
67,791✔
317
  }
318

319
  bool isCollection() const {
1,932✔
320
    return type() & VariantTypeBits::CollectionMask;
1,932✔
321
  }
322

323
  template <typename T>
324
  bool isInteger() const {
167✔
325
    return isInteger<T>(data_, resources_);
167✔
326
  }
327

328
  template <typename T>
329
  static bool isInteger(VariantData* data, ResourceManager* resources) {
167✔
330
    if (!data)
167✔
331
      return false;
16✔
332

333
#if ARDUINOJSON_USE_LONG_LONG
334
    auto eightByteValue = getEightByte(data, resources);
148✔
335
#else
336
    (void)resources;
337
#endif
338
    switch (data->type) {
151✔
339
      case VariantType::Uint32:
11✔
340
        return canConvertNumber<T>(data->content.asUint32);
11✔
341

342
      case VariantType::Int32:
65✔
343
        return canConvertNumber<T>(data->content.asInt32);
65✔
344

345
#if ARDUINOJSON_USE_LONG_LONG
346
      case VariantType::Uint64:
3✔
347
        return canConvertNumber<T>(eightByteValue->asUint64);
3✔
348

349
      case VariantType::Int64:
4✔
350
        return canConvertNumber<T>(eightByteValue->asInt64);
4✔
351
#endif
352

353
      default:
68✔
354
        return false;
68✔
355
    }
356
  }
357

358
  bool isNull() const {
2,911✔
359
    return type() == VariantType::Null;
2,911✔
360
  }
361

362
  bool isObject() const {
3,690✔
363
    return type() == VariantType::Object;
3,690✔
364
  }
365

366
  size_t nesting() const;
367

368
  void removeElement(size_t index);
369

370
  void removeElement(CollectionIterator it) {
22✔
371
    removeOne(it);
22✔
372
  }
22✔
373

374
  template <typename TAdaptedString>
375
  void removeMember(TAdaptedString key) {
31✔
376
    removePair(findKey(key));
31✔
377
  }
31✔
378

379
  void removeMember(CollectionIterator it) {
4✔
380
    removePair(it);
4✔
381
  }
4✔
382

383
  bool setBoolean(bool value) {
250✔
384
    if (!data_)
250✔
385
      return false;
2✔
386
    clear(data_, resources_);
248✔
387
    data_->setBoolean(value);
248✔
388
    return true;
248✔
389
  }
390

391
  template <typename T>
392
  bool setFloat(T value) {
73✔
393
    if (!data_)
73✔
394
      return false;
1✔
395
    clear(data_, resources_);
72✔
396
    return setFloat(value, data_, resources_);
72✔
397
  }
398

399
  template <typename T>
400
  static enable_if_t<sizeof(T) == 4, bool> setFloat(T value, VariantData* data,
90✔
401
                                                    ResourceManager*) {
402
    ARDUINOJSON_ASSERT(data != nullptr);
403
    ARDUINOJSON_ASSERT(data->type == VariantType::Null);
404
    data->type = VariantType::Float;
90✔
405
    data->content.asFloat = value;
90✔
406
    return true;
90✔
407
  }
408

409
  template <typename T>
410
  static enable_if_t<sizeof(T) == 8, bool> setFloat(
79✔
411
      T value, VariantData* data, ResourceManager* resources) {
412
    ARDUINOJSON_ASSERT(data != nullptr);
413
    ARDUINOJSON_ASSERT(data->type == VariantType::Null);
414
    ARDUINOJSON_ASSERT(resources != nullptr);
415

416
    float valueAsFloat = static_cast<float>(value);
79✔
417

418
#if ARDUINOJSON_USE_DOUBLE
419
    if (value == valueAsFloat) {
77✔
420
      data->type = VariantType::Float;
32✔
421
      data->content.asFloat = valueAsFloat;
32✔
422
    } else {
423
      auto slot = resources->allocEightByte();
45✔
424
      if (!slot)
45✔
425
        return false;
3✔
426
      data->type = VariantType::Double;
42✔
427
      data->content.asSlotId = slot.id();
42✔
428
      slot->asDouble = value;
42✔
429
    }
430
#else
431
    data->type = VariantType::Float;
2✔
432
    data->content.asFloat = valueAsFloat;
2✔
433
#endif
434
    return true;
76✔
435
  }
436

437
  template <typename T>
438
  bool setInteger(T value) {
1,433✔
439
    if (!data_)
1,433✔
440
      return false;
16✔
441
    clear(data_, resources_);
1,417✔
442
    return setInteger(value, data_, resources_);
1,417✔
443
  }
444

445
  template <typename T>
446
  static enable_if_t<is_signed<T>::value, bool> setInteger(
1,548✔
447
      T value, VariantData* data, ResourceManager* resources) {
448
    ARDUINOJSON_ASSERT(data != nullptr);
449
    ARDUINOJSON_ASSERT(data->type == VariantType::Null);
450
    ARDUINOJSON_ASSERT(resources != nullptr);
451

452
    if (canConvertNumber<int32_t>(value)) {
1,548✔
453
      data->type = VariantType::Int32;
1,531✔
454
      data->content.asInt32 = static_cast<int32_t>(value);
1,531✔
455
    }
456
#if ARDUINOJSON_USE_LONG_LONG
457
    else {
458
      auto slot = resources->allocEightByte();
17✔
459
      if (!slot)
17✔
460
        return false;
3✔
461
      data->type = VariantType::Int64;
14✔
462
      data->content.asSlotId = slot.id();
14✔
463
      slot->asInt64 = value;
14✔
464
    }
465
#else
466
    (void)resources;
467
#endif
468
    return true;
1,545✔
469
  }
470

471
  template <typename T>
472
  static enable_if_t<is_unsigned<T>::value, bool> setInteger(
2,299✔
473
      T value, VariantData* data, ResourceManager* resources) {
474
    ARDUINOJSON_ASSERT(data != nullptr);
475
    ARDUINOJSON_ASSERT(data->type == VariantType::Null);
476
    ARDUINOJSON_ASSERT(resources != nullptr);
477

478
    if (canConvertNumber<uint32_t>(value)) {
2,299✔
479
      data->type = VariantType::Uint32;
2,282✔
480
      data->content.asUint32 = static_cast<uint32_t>(value);
2,282✔
481
    }
482
#if ARDUINOJSON_USE_LONG_LONG
483
    else {
484
      auto slot = resources->allocEightByte();
16✔
485
      if (!slot)
16✔
486
        return false;
3✔
487
      data->type = VariantType::Uint64;
13✔
488
      data->content.asSlotId = slot.id();
13✔
489
      slot->asUint64 = value;
13✔
490
    }
491
#else
492
    (void)resources;
493
#endif
494
    return true;
2,296✔
495
  }
496

497
  template <typename T>
498
  void setRawString(SerializedValue<T> value) {
36✔
499
    if (!data_)
36✔
500
      return;
2✔
501
    clear(data_, resources_);
34✔
502
    auto dup = resources_->saveString(adaptString(value.data(), value.size()));
34✔
503
    if (dup)
34✔
504
      data_->setRawString(dup);
30✔
505
  }
506

507
  template <typename TAdaptedString>
508
  bool setString(TAdaptedString value) {
66,121✔
509
    if (!data_)
66,121✔
510
      return false;
5✔
511
    clear(data_, resources_);
66,116✔
512
    return setString(value, data_, resources_);
66,116✔
513
  }
514

515
  template <typename TAdaptedString>
516
  static bool setString(TAdaptedString value, VariantData* data,
67,026✔
517
                        ResourceManager* resources) {
518
    ARDUINOJSON_ASSERT(data != nullptr);
519
    ARDUINOJSON_ASSERT(data->type == VariantType::Null);
520
    ARDUINOJSON_ASSERT(resources != nullptr);
521

522
    if (value.isNull())
67,026✔
523
      return false;
65,553✔
524

525
    if (isTinyString(value, value.size())) {
1,473✔
526
      data->setTinyString(value);
412✔
527
      return true;
412✔
528
    }
529

530
    auto dup = resources->saveString(value);
1,061✔
531
    if (dup) {
1,061✔
532
      data->setLongString(dup);
1,049✔
533
      return true;
1,049✔
534
    }
535

536
    return false;
12✔
537
  }
538

539
  size_t size() const {
369✔
540
    if (!isCollection())
369✔
541
      return 0;
23✔
542

543
    return size(data_, resources_);
346✔
544
  }
545

546
  static size_t size(VariantData* data, ResourceManager* resources) {
365✔
547
    ARDUINOJSON_ASSERT(data != nullptr);
548
    ARDUINOJSON_ASSERT(data->isCollection());
549
    ARDUINOJSON_ASSERT(resources != nullptr);
550

551
    size_t n = 0;
365✔
552
    for (auto it = createIterator(data, resources); !it.done();
132,494✔
553
         it.next(resources))
132,129✔
554
      n++;
132,129✔
555

556
    if (data->type == VariantType::Object) {
365✔
557
      ARDUINOJSON_ASSERT((n % 2) == 0);
558
      n /= 2;
267✔
559
    }
560

561
    return n;
365✔
562
  }
563

564
  bool toArray() {
78✔
565
    if (!data_)
78✔
UNCOV
566
      return false;
×
567
    clear(data_, resources_);
78✔
568
    data_->toArray();
78✔
569
    return true;
78✔
570
  }
571

572
  bool toObject() {
109✔
573
    if (!data_)
109✔
UNCOV
574
      return false;
×
575
    clear(data_, resources_);
109✔
576
    data_->toObject();
109✔
577
    return true;
109✔
578
  }
579

580
  VariantType type() const {
76,324✔
581
    return data_ ? data_->type : VariantType::Null;
76,324✔
582
  }
583

584
  // Release the resources used by this variant and set it to null.
585
  void clear() {
48✔
586
    if (data_)
48✔
587
      clear(data_, resources_);
47✔
588
  }
48✔
589

590
  static void clear(VariantData* data, ResourceManager* resources) {
68,531✔
591
    ARDUINOJSON_ASSERT(data != nullptr);
592
    ARDUINOJSON_ASSERT(resources != nullptr);
593

594
    if (data->type & VariantTypeBits::OwnedStringBit)
68,531✔
595
      resources->dereferenceString(data->content.asStringNode->data);
45✔
596

597
#if ARDUINOJSON_USE_8_BYTE_POOL
598
    if (data->type & VariantTypeBits::EightByteBit)
68,531✔
599
      resources->freeEightByte(data->content.asSlotId);
3✔
600
#endif
601

602
    if (data->type & VariantTypeBits::CollectionMask)
68,531✔
603
      empty(data, resources);
17✔
604

605
    data->type = VariantType::Null;
68,531✔
606
  }
68,531✔
607

608
  void empty() {
51✔
609
    if (!isCollection())
51✔
610
      return;
2✔
611
    empty(data_, resources_);
49✔
612
  }
613

614
  static void empty(VariantData*, ResourceManager*);
615

616
 private:
617
  VariantData* data_;
618
  ResourceManager* resources_;
619

620
  template <typename TAdaptedString>
621
  iterator findKey(TAdaptedString key) const {
31✔
622
    if (!isObject())
31✔
623
      return iterator();
3✔
624
    return findKey(key, data_, resources_);
28✔
625
  }
626

627
  template <typename TAdaptedString>
628
  static iterator findKey(TAdaptedString key, VariantData*, ResourceManager*);
629

630
  static void appendOne(Slot<VariantData> slot, VariantData*, ResourceManager*);
631

632
  static void appendPair(Slot<VariantData> key, Slot<VariantData> value,
633
                         VariantData*, ResourceManager*);
634

635
  void removeOne(iterator it);
636
  void removePair(iterator it);
637

638
  static void freeVariant(Slot<VariantData> slot, ResourceManager* resources) {
368✔
639
    clear(slot.ptr(), resources);
368✔
640
    resources->freeVariant(slot);
368✔
641
  }
368✔
642

643
  Slot<VariantData> getPreviousSlot(VariantData*) const;
644
};
645

646
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