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

bblanchon / ArduinoJson / 5088490945

pending completion
5088490945

push

github

Benoit Blanchon
Implement `JsonArray` from `JsonVariant`

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

3316 of 3336 relevant lines covered (99.4%)

6937.22 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
  VariantData* addElement(MemoryPool* pool) {
66,067✔
72
    auto array = isNull() ? &toArray() : asArray();
66,067✔
73
    return collectionAddElement(array, pool);
66,067✔
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() {
66,548✔
93
    return isArray() ? &content_.asCollection : 0;
66,548✔
94
  }
95

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

100
  const CollectionData* asCollection() const {
67,928✔
101
    return isCollection() ? &content_.asCollection : 0;
67,928✔
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,761✔
146
    return isObject() ? &content_.asCollection : 0;
2,761✔
147
  }
148

149
  const CollectionData* asObject() const {
2,169✔
150
    return const_cast<VariantData*>(this)->asObject();
2,169✔
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,152✔
218
    auto object = asObject();
2,152✔
219
    if (!object)
2,152✔
220
      return nullptr;
44✔
221
    return slotData(object->get(key));
2,108✔
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 {
66,644✔
259
    return (flags_ & VALUE_IS_ARRAY) != 0;
66,644✔
260
  }
261

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

266
  bool isCollection() const {
68,040✔
267
    return (flags_ & COLLECTION_MASK) != 0;
68,040✔
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 {
67,515✔
289
    return type() == VALUE_IS_NULL;
67,515✔
290
  }
291

292
  bool isObject() const {
3,111✔
293
    return (flags_ & VALUE_IS_OBJECT) != 0;
3,111✔
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 {
41✔
301
    switch (type()) {
41✔
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:
13✔
306
      case VALUE_IS_ARRAY:
307
        return content_.asCollection.memoryUsage();
13✔
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
  void removeElement(size_t index, MemoryPool* pool) {
10✔
338
    collectionRemoveElement(asArray(), index, pool);
10✔
339
  }
10✔
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 {
112✔
443
    return isCollection() ? content_.asCollection.size() : 0;
112✔
444
  }
445

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

452
  CollectionData& toArray(MemoryPool* pool) {
221✔
453
    release(pool);
221✔
454
    return toArray();
221✔
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 {
207,004✔
469
    return flags_ & VALUE_MASK;
207,004✔
470
  }
471

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

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

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

490
template <typename TVisitor>
491
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) {
66,076✔
507
  if (!var)
66,076✔
508
    return nullptr;
9✔
509
  return var->addElement(pool);
66,067✔
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,158✔
518
  if (!var)
2,158✔
519
    return 0;
14✔
520
  return var->getMember(key);
2,144✔
521
}
522

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

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

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

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

550
inline void variantRemoveElement(VariantData* var, size_t index,
12✔
551
                                 MemoryPool* pool) {
552
  if (!var)
12✔
553
    return;
2✔
554
  var->removeElement(index, pool);
10✔
555
}
556

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

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

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

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

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

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

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

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

611
inline CollectionData* variantToArray(VariantData* var, MemoryPool* pool) {
224✔
612
  if (!var)
224✔
613
    return 0;
3✔
614
  return &var->toArray(pool);
221✔
615
}
616

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

623
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