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

bblanchon / ArduinoJson / 5313785987

pending completion
5313785987

push

github

bblanchon
Extract `arrayEquals()` and `objectEquals()`

28 of 28 new or added lines in 4 files covered. (100.0%)

3362 of 3383 relevant lines covered (99.38%)

6246.1 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,
18
                                  ResourceManager* resources);
19
bool collectionCopy(CollectionData* dst, const CollectionData* src,
20
                    ResourceManager* resources);
21
void collectionRemoveElement(CollectionData* data, size_t index,
22
                             ResourceManager* resources);
23
template <typename T>
24
T parseNumber(const char* s);
25
void slotRelease(VariantSlot* slot, ResourceManager* resources);
26

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

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

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

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

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

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

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

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

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

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

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

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

72
  VariantData* addElement(ResourceManager* resources) {
242✔
73
    auto array = isNull() ? &toArray() : asArray();
242✔
74
    return collectionAddElement(array, resources);
242✔
75
  }
76

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

338
  void removeElement(size_t index, ResourceManager* resources) {
7✔
339
    collectionRemoveElement(asArray(), index, resources);
7✔
340
  }
7✔
341

342
  template <typename TAdaptedString>
343
  void removeMember(TAdaptedString key, ResourceManager* resources) {
13✔
344
    collectionRemoveMember(asObject(), key, resources);
13✔
345
  }
13✔
346

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

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

356
  void setBoolean(bool value, ResourceManager* resources) {
249✔
357
    release(resources);
249✔
358
    setBoolean(value);
249✔
359
  }
249✔
360

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

366
  void setFloat(JsonFloat value, ResourceManager* resources) {
61✔
367
    release(resources);
61✔
368
    setFloat(value);
61✔
369
  }
61✔
370

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

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

383
  template <typename T>
384
  void setInteger(T value, ResourceManager* resources) {
571✔
385
    release(resources);
571✔
386
    setInteger(value);
571✔
387
  }
571✔
388

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

393
  void setNull(ResourceManager* resources) {
66,433✔
394
    release(resources);
66,433✔
395
    setNull();
66,433✔
396
  }
66,433✔
397

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

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

414
  template <typename TAdaptedString>
415
  void setString(TAdaptedString value, ResourceManager* resources) {
65,954✔
416
    setNull(resources);
65,954✔
417

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

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

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

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

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

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

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

453
  CollectionData& toArray(ResourceManager* resources) {
220✔
454
    release(resources);
220✔
455
    return toArray();
220✔
456
  }
457

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

464
  CollectionData& toObject(ResourceManager* resources) {
271✔
465
    release(resources);
271✔
466
    return toObject();
271✔
467
  }
468

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

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

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

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

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

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

507
inline VariantData* variantAddElement(VariantData* var,
78✔
508
                                      ResourceManager* resources) {
509
  if (!var)
78✔
510
    return nullptr;
6✔
511
  return var->addElement(resources);
72✔
512
}
513

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

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

525
inline VariantData* variantGetOrAddElement(VariantData* var, size_t index,
137✔
526
                                           ResourceManager* resources) {
527
  if (!var)
137✔
528
    return nullptr;
1✔
529
  return var->getOrAddElement(index, resources);
136✔
530
}
531

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

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

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

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

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

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

574
inline void variantSetFloat(VariantData* var, JsonFloat value,
63✔
575
                            ResourceManager* resources) {
576
  if (!var)
63✔
577
    return;
2✔
578
  var->setFloat(value, resources);
61✔
579
}
580

581
template <typename T>
582
void variantSetInteger(VariantData* var, T value, ResourceManager* resources) {
590✔
583
  if (!var)
590✔
584
    return;
19✔
585
  var->setInteger(value, resources);
571✔
586
}
587

588
inline void variantSetNull(VariantData* var, ResourceManager* resources) {
419✔
589
  if (!var)
419✔
590
    return;
1✔
591
  var->setNull(resources);
418✔
592
}
593

594
template <typename T>
595
void variantSetRawString(VariantData* var, SerializedValue<T> value,
33✔
596
                         ResourceManager* resources) {
597
  if (!var)
33✔
598
    return;
3✔
599
  var->setRawString(value, resources);
30✔
600
}
601

602
template <typename TAdaptedString>
603
void variantSetString(VariantData* var, TAdaptedString value,
65,960✔
604
                      ResourceManager* resources) {
605
  if (!var)
65,960✔
606
    return;
6✔
607
  var->setString(value, resources);
65,954✔
608
}
609

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

614
inline CollectionData* variantToArray(VariantData* var,
222✔
615
                                      ResourceManager* resources) {
616
  if (!var)
222✔
617
    return 0;
2✔
618
  return &var->toArray(resources);
220✔
619
}
620

621
inline CollectionData* variantToObject(VariantData* var,
273✔
622
                                       ResourceManager* resources) {
623
  if (!var)
273✔
624
    return 0;
2✔
625
  return &var->toObject(resources);
271✔
626
}
627

628
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