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

realm / realm-core / github_pull_request_275914

25 Sep 2023 03:10PM UTC coverage: 92.915% (+1.7%) from 91.215%
github_pull_request_275914

Pull #6073

Evergreen

jedelbo
Merge tag 'v13.21.0' into next-major

"Feature/Bugfix release"
Pull Request #6073: Merge next-major

96928 of 177706 branches covered (0.0%)

8324 of 8714 new or added lines in 122 files covered. (95.52%)

181 existing lines in 28 files now uncovered.

247505 of 266379 relevant lines covered (92.91%)

7164945.17 hits per line

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

77.04
/src/realm/to_json.cpp
1
/*************************************************************************
2
 *
3
 * Copyright 2016 Realm Inc.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 * http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 **************************************************************************/
18

19
#include <realm/group.hpp>
20
#include <realm/dictionary.hpp>
21
#include <realm/list.hpp>
22
#include <realm/set.hpp>
23
#include <external/json/json.hpp>
24
#include "realm/util/base64.hpp"
25

26
namespace realm {
27

28
void Group::schema_to_json(std::ostream& out, std::map<std::string, std::string>* opt_renames) const
29
{
3✔
30
    check_attached();
3✔
31

32
    std::map<std::string, std::string> renames;
3✔
33
    if (opt_renames) {
3✔
NEW
34
        renames = *opt_renames;
×
NEW
35
    }
×
36

37
    out << "[" << std::endl;
3✔
38

39
    auto keys = get_table_keys();
3✔
40
    int sz = int(keys.size());
3✔
41
    for (int i = 0; i < sz; ++i) {
9✔
42
        auto key = keys[i];
6✔
43
        ConstTableRef table = get_table(key);
6✔
44

45
        table->schema_to_json(out, renames);
6✔
46
        if (i < sz - 1)
6✔
47
            out << ",";
3✔
48
        out << std::endl;
6✔
49
    }
6✔
50

51
    out << "]" << std::endl;
3✔
52
}
3✔
53

54
void Group::to_json(std::ostream& out, size_t link_depth, std::map<std::string, std::string>* opt_renames,
55
                    JSONOutputMode output_mode) const
56
{
9✔
57
    check_attached();
9✔
58

59
    std::map<std::string, std::string> renames;
9✔
60
    if (opt_renames) {
9✔
NEW
61
        renames = *opt_renames;
×
NEW
62
    }
×
63

64
    out << "{" << std::endl;
9✔
65

66
    auto keys = get_table_keys();
9✔
67
    bool first = true;
9✔
68
    for (size_t i = 0; i < keys.size(); ++i) {
18✔
69
        auto key = keys[i];
9✔
70
        StringData name = get_table_name(key);
9✔
71
        if (renames[name] != "")
9✔
NEW
72
            name = renames[name];
×
73

74
        ConstTableRef table = get_table(key);
9✔
75

76
        if (!table->is_embedded()) {
9✔
77
            if (!first)
9✔
NEW
78
                out << ",";
×
79
            out << "\"" << name << "\"";
9✔
80
            out << ":";
9✔
81
            table->to_json(out, link_depth, renames, output_mode);
9✔
82
            out << std::endl;
9✔
83
            first = false;
9✔
84
        }
9✔
85
    }
9✔
86

87
    out << "}" << std::endl;
9✔
88
}
9✔
89

90
void Table::to_json(std::ostream& out, size_t link_depth, const std::map<std::string, std::string>& renames,
91
                    JSONOutputMode output_mode) const
92
{
147✔
93
    // Represent table as list of objects
94
    out << "[";
147✔
95
    bool first = true;
147✔
96

97
    for (auto& obj : *this) {
453✔
98
        if (first) {
453✔
99
            first = false;
147✔
100
        }
147✔
101
        else {
306✔
102
            out << ",";
306✔
103
        }
306✔
104
        obj.to_json(out, link_depth, renames, output_mode);
453✔
105
    }
453✔
106

107
    out << "]";
147✔
108
}
147✔
109

110
using Json = nlohmann::json;
111

112
template <typename T>
113
void Dictionary::insert_json(const std::string& key, const T& value)
114
{
24✔
115
    const Json& j = value;
24✔
116
    switch (j.type()) {
24✔
NEW
117
        case Json::value_t::null:
✔
NEW
118
            insert(key, Mixed());
×
NEW
119
            break;
×
120
        case Json::value_t::string:
6✔
121
            insert(key, j.get<std::string>());
6✔
122
            break;
6✔
NEW
123
        case Json::value_t::boolean:
✔
NEW
124
            insert(key, j.get<bool>());
×
NEW
125
            break;
×
NEW
126
        case Json::value_t::number_integer:
✔
127
        case Json::value_t::number_unsigned:
12✔
128
            insert(key, j.get<int64_t>());
12✔
129
            break;
12✔
NEW
130
        case Json::value_t::number_float:
✔
NEW
131
            insert(key, j.get<double>());
×
NEW
132
            break;
×
NEW
133
        case Json::value_t::object: {
✔
NEW
134
            insert_collection(key, CollectionType::Dictionary);
×
NEW
135
            auto dict = get_dictionary(key);
×
NEW
136
            for (auto [k, v] : j.items()) {
×
NEW
137
                dict->insert_json(k, v);
×
NEW
138
            }
×
NEW
139
            break;
×
NEW
140
        }
×
141
        case Json::value_t::array: {
6✔
142
            insert_collection(key, CollectionType::List);
6✔
143
            auto list = get_list(key);
6✔
144
            for (auto&& elem : value) {
18✔
145
                list->add_json(elem);
18✔
146
            }
18✔
147
            break;
6✔
NEW
148
        }
×
NEW
149
        case Json::value_t::discarded:
✔
NEW
150
            REALM_TERMINATE("should never see discarded");
×
151
    }
24✔
152
}
24✔
153

154
template <typename T>
155
void Lst<Mixed>::add_json(const T& value)
156
{
36✔
157
    const Json& j = value;
36✔
158
    size_t sz = size();
36✔
159
    switch (j.type()) {
36✔
NEW
160
        case Json::value_t::null:
✔
NEW
161
            insert(sz, Mixed());
×
NEW
162
            break;
×
163
        case Json::value_t::string:
6✔
164
            insert(sz, j.get<std::string>());
6✔
165
            break;
6✔
NEW
166
        case Json::value_t::boolean:
✔
NEW
167
            insert(sz, j.get<bool>());
×
NEW
168
            break;
×
NEW
169
        case Json::value_t::number_integer:
✔
NEW
170
        case Json::value_t::number_unsigned:
✔
NEW
171
            insert(sz, j.get<int64_t>());
×
NEW
172
            break;
×
173
        case Json::value_t::number_float:
18✔
174
            insert(sz, j.get<double>());
18✔
175
            break;
18✔
176
        case Json::value_t::object: {
12✔
177
            insert_collection(sz, CollectionType::Dictionary);
12✔
178
            auto dict = get_dictionary(sz);
12✔
179
            for (auto [k, v] : j.items()) {
24✔
180
                dict->insert_json(k, v);
24✔
181
            }
24✔
182
            break;
12✔
NEW
183
        }
×
NEW
184
        case Json::value_t::array: {
✔
NEW
185
            insert_collection(sz, CollectionType::List);
×
NEW
186
            auto list = get_list(sz);
×
NEW
187
            for (auto&& elem : j) {
×
NEW
188
                list->add_json(elem);
×
NEW
189
            }
×
NEW
190
            break;
×
NEW
191
        }
×
NEW
192
        case Json::value_t::discarded:
✔
NEW
193
            REALM_TERMINATE("should never see discarded");
×
194
    }
36✔
195
}
36✔
196

197
Obj& Obj::set_json(ColKey col_key, StringData json)
198
{
6✔
199
    auto j = Json::parse(std::string_view(json.data(), json.size()), nullptr, false);
6✔
200
    switch (j.type()) {
6✔
NEW
201
        case Json::value_t::null:
✔
NEW
202
            set(col_key, Mixed());
×
NEW
203
            break;
×
NEW
204
        case Json::value_t::string:
✔
NEW
205
            set(col_key, Mixed(j.get<std::string>()));
×
NEW
206
            break;
×
NEW
207
        case Json::value_t::boolean:
✔
NEW
208
            set(col_key, Mixed(j.get<bool>()));
×
NEW
209
            break;
×
NEW
210
        case Json::value_t::number_integer:
✔
NEW
211
        case Json::value_t::number_unsigned:
✔
NEW
212
            set(col_key, Mixed(j.get<int64_t>()));
×
NEW
213
            break;
×
NEW
214
        case Json::value_t::number_float:
✔
NEW
215
            set(col_key, Mixed(j.get<double>()));
×
NEW
216
            break;
×
NEW
217
        case Json::value_t::object: {
✔
NEW
218
            set_collection(col_key, CollectionType::Dictionary);
×
NEW
219
            Dictionary dict(*this, col_key);
×
NEW
220
            for (auto [k, v] : j.items()) {
×
NEW
221
                dict.insert_json(k, v);
×
NEW
222
            }
×
NEW
223
            break;
×
NEW
224
        }
×
225
        case Json::value_t::array: {
6✔
226
            set_collection(col_key, CollectionType::List);
6✔
227
            Lst<Mixed> list(*this, col_key);
6✔
228
            list.clear();
6✔
229
            for (auto&& elem : j) {
18✔
230
                list.add_json(elem);
18✔
231
            }
18✔
232
        } break;
6✔
NEW
233
        case Json::value_t::discarded:
✔
NEW
234
            throw InvalidArgument(ErrorCodes::MalformedJson, "Illegal json");
×
235
    }
6✔
236

237
    return *this;
6✔
238
}
6✔
239

240
void Obj::to_json(std::ostream& out, size_t link_depth, const std::map<std::string, std::string>& renames,
241
                  std::vector<ObjLink>& followed, JSONOutputMode output_mode) const
242
{
924✔
243
    followed.push_back(get_link());
924✔
244
    size_t new_depth = link_depth == not_found ? not_found : link_depth - 1;
783✔
245
    StringData name = "_key";
924✔
246
    bool prefixComma = false;
924✔
247
    if (renames.count(name))
924✔
NEW
248
        name = renames.at(name);
×
249
    out << "{";
924✔
250
    if (output_mode == output_mode_json) {
924✔
251
        prefixComma = true;
660✔
252
        out << "\"" << name << "\":" << this->m_key.value;
660✔
253
    }
660✔
254

255
    auto col_keys = m_table->get_column_keys();
924✔
256
    for (auto ck : col_keys) {
4,413✔
257
        name = m_table->get_column_name(ck);
4,413✔
258
        auto type = ck.get_type();
4,413✔
259
        if (type == col_type_LinkList)
4,413✔
260
            type = col_type_Link;
204✔
261
        if (renames.count(name))
4,413✔
262
            name = renames.at(name);
81✔
263

264
        if (prefixComma)
4,413✔
265
            out << ",";
4,149✔
266
        out << "\"" << name << "\":";
4,413✔
267
        prefixComma = true;
4,413✔
268

269
        TableRef target_table;
4,413✔
270
        ColKey pk_col_key;
4,413✔
271
        if (type == col_type_Link) {
4,413✔
272
            target_table = get_target_table(ck);
690✔
273
            pk_col_key = target_table->get_primary_key_column();
690✔
274
        }
690✔
275

276
        auto print_link = [&](const Mixed& val) {
645✔
277
            REALM_ASSERT(val.is_type(type_Link, type_TypedLink));
645✔
278
            TableRef tt = target_table;
645✔
279
            auto obj_key = val.get<ObjKey>();
645✔
280
            std::string table_info;
645✔
281
            std::string table_info_close;
645✔
282
            if (!tt) {
645✔
283
                // It must be a typed link
284
                tt = m_table->get_parent_group()->get_table(val.get_link().get_table_key());
63✔
285
                pk_col_key = tt->get_primary_key_column();
63✔
286
                if (output_mode == output_mode_xjson_plus) {
63✔
287
                    table_info = std::string("{ \"$link\": ");
15✔
288
                    table_info_close = " }";
15✔
289
                }
15✔
290

291
                table_info += std::string("{ \"table\": \"") + std::string(tt->get_name()) + "\", \"key\": ";
63✔
292
                table_info_close += " }";
63✔
293
            }
63✔
294
            if (pk_col_key && output_mode != output_mode_json) {
645✔
295
                out << table_info;
156✔
296
                tt->get_primary_key(obj_key).to_json(out, output_mode_xjson);
156✔
297
                out << table_info_close;
156✔
298
            }
156✔
299
            else {
489✔
300
                ObjLink link(tt->get_key(), obj_key);
489✔
301
                if (obj_key.is_unresolved()) {
489✔
NEW
302
                    out << "null";
×
NEW
303
                    return;
×
NEW
304
                }
×
305
                if (!tt->is_embedded()) {
489✔
306
                    if (link_depth == 0) {
453✔
307
                        out << table_info << obj_key.value << table_info_close;
261✔
308
                        return;
261✔
309
                    }
261✔
310
                    if ((link_depth == realm::npos &&
192✔
311
                         std::find(followed.begin(), followed.end(), link) != followed.end())) {
81✔
312
                        // We have detected a cycle in links
313
                        out << "{ \"table\": \"" << tt->get_name() << "\", \"key\": " << obj_key.value << " }";
9✔
314
                        return;
9✔
315
                    }
9✔
316
                }
219✔
317

318
                tt->get_object(obj_key).to_json(out, new_depth, renames, followed, output_mode);
219✔
319
            }
219✔
320
        };
645✔
321

322
        if (ck.is_collection()) {
4,413✔
323
            auto collection = get_collection_ptr(ck);
885✔
324
            collection->to_json(out, link_depth, output_mode, print_link);
885✔
325
        }
885✔
326
        else {
3,528✔
327
            auto val = get_any(ck);
3,528✔
328
            if (!val.is_null()) {
3,528✔
329
                if (type == col_type_Link) {
3,285✔
330
                    std::string close_string;
288✔
331
                    bool is_embedded = target_table->is_embedded();
288✔
332
                    bool link_depth_reached = !is_embedded && (link_depth == 0);
288✔
333

334
                    if (output_mode == output_mode_xjson_plus) {
288✔
335
                        out << "{ " << (is_embedded ? "\"$embedded" : "\"$link") << "\": ";
9✔
336
                        close_string += "}";
12✔
337
                    }
12✔
338
                    if ((link_depth_reached && output_mode == output_mode_json) ||
288✔
339
                        output_mode == output_mode_xjson_plus) {
198✔
340
                        out << "{ \"table\": \"" << target_table->get_name() << "\", "
198✔
341
                            << (is_embedded ? "\"value" : "\"key") << "\": ";
195✔
342
                        close_string += "}";
198✔
343
                    }
198✔
344

345
                    print_link(val);
288✔
346
                    out << close_string;
288✔
347
                }
288✔
348
                else if (val.is_type(type_TypedLink)) {
2,997✔
349
                    print_link(val);
12✔
350
                }
12✔
351
                else if (val.is_type(type_Dictionary)) {
2,985✔
352
                    DummyParent parent(m_table, val.get_ref());
6✔
353
                    Dictionary dict(parent, 0);
6✔
354
                    dict.to_json(out, link_depth, output_mode, print_link);
6✔
355
                }
6✔
356
                else if (val.is_type(type_Set)) {
2,979✔
357
                    DummyParent parent(this->get_table(), val.get_ref());
6✔
358
                    Set<Mixed> set(parent, 0);
6✔
359
                    set.to_json(out, link_depth, output_mode, print_link);
6✔
360
                }
6✔
361
                else if (val.is_type(type_List)) {
2,973✔
362
                    DummyParent parent(m_table, val.get_ref());
9✔
363
                    Lst<Mixed> list(parent, 0);
9✔
364
                    list.to_json(out, link_depth, output_mode, print_link);
9✔
365
                }
9✔
366
                else {
2,964✔
367
                    val.to_json(out, output_mode);
2,964✔
368
                }
2,964✔
369
            }
3,285✔
370
            else {
243✔
371
                out << "null";
243✔
372
            }
243✔
373
        }
3,528✔
374
    }
4,413✔
375
    out << "}";
924✔
376
    followed.pop_back();
924✔
377
}
924✔
378

379
namespace {
380
const char to_be_escaped[] = "\"\n\r\t\f\\\b";
381
const char encoding[] = "\"nrtf\\b";
382

383
template <class T>
384
inline void out_floats(std::ostream& out, T value)
385
{
270✔
386
    std::streamsize old = out.precision();
270✔
387
    out.precision(std::numeric_limits<T>::digits10 + 1);
270✔
388
    out << std::scientific << value;
270✔
389
    out.precision(old);
270✔
390
}
270✔
391

392
void out_string(std::ostream& out, std::string str)
393
{
1,464✔
394
    size_t p = str.find_first_of(to_be_escaped);
1,464✔
395
    while (p != std::string::npos) {
1,485✔
396
        char c = str[p];
21✔
397
        auto found = strchr(to_be_escaped, c);
21✔
398
        REALM_ASSERT(found);
21✔
399
        out << str.substr(0, p) << '\\' << encoding[found - to_be_escaped];
21✔
400
        str = str.substr(p + 1);
21✔
401
        p = str.find_first_of(to_be_escaped);
21✔
402
    }
21✔
403
    out << str;
1,464✔
404
}
1,464✔
405

406
void out_binary(std::ostream& out, BinaryData bin)
407
{
135✔
408
    const char* start = bin.data();
135✔
409
    const size_t len = bin.size();
135✔
410
    std::string encode_buffer;
135✔
411
    encode_buffer.resize(util::base64_encoded_size(len));
135✔
412
    util::base64_encode(start, len, encode_buffer.data(), encode_buffer.size());
135✔
413
    out << encode_buffer;
135✔
414
}
135✔
415
} // anonymous namespace
416

417

418
void Mixed::to_xjson(std::ostream& out) const noexcept
419
{
2,205✔
420
    switch (get_type()) {
2,205✔
421
        case type_Int:
660✔
422
            out << "{\"$numberLong\": \"";
660✔
423
            out << int_val;
660✔
424
            out << "\"}";
660✔
425
            break;
660✔
426
        case type_Bool:
90✔
427
            out << (bool_val ? "true" : "false");
48✔
428
            break;
90✔
429
        case type_Float:
90✔
430
            out << "{\"$numberDouble\": \"";
90✔
431
            out_floats<float>(out, float_val);
90✔
432
            out << "\"}";
90✔
433
            break;
90✔
434
        case type_Double:
90✔
435
            out << "{\"$numberDouble\": \"";
90✔
436
            out_floats<double>(out, double_val);
90✔
437
            out << "\"}";
90✔
438
            break;
90✔
439
        case type_String: {
822✔
440
            out << "\"";
822✔
441
            out_string(out, string_val);
822✔
442
            out << "\"";
822✔
443
            break;
822✔
NEW
444
        }
×
445
        case type_Binary: {
90✔
446
            out << "{\"$binary\": {\"base64\": \"";
90✔
447
            out_binary(out, binary_val);
90✔
448
            out << "\", \"subType\": \"00\"}}";
90✔
449
            break;
90✔
NEW
450
        }
×
451
        case type_Timestamp: {
93✔
452
            out << "{\"$date\": {\"$numberLong\": \"";
93✔
453
            int64_t timeMillis = date_val.get_seconds() * 1000 + date_val.get_nanoseconds() / 1000000;
93✔
454
            out << timeMillis;
93✔
455
            out << "\"}}";
93✔
456
            break;
93✔
NEW
457
        }
×
458
        case type_Decimal:
90✔
459
            out << "{\"$numberDecimal\": \"";
90✔
460
            out << decimal_val;
90✔
461
            out << "\"}";
90✔
462
            break;
90✔
463
        case type_ObjectId:
90✔
464
            out << "{\"$oid\": \"";
90✔
465
            out << id_val;
90✔
466
            out << "\"}";
90✔
467
            break;
90✔
468
        case type_UUID:
90✔
469
            out << "{\"$binary\": {\"base64\": \"";
90✔
470
            out << uuid_val.to_base64();
90✔
471
            out << "\", \"subType\": \"04\"}}";
90✔
472
            break;
90✔
473

NEW
474
        case type_TypedLink: {
✔
NEW
475
            Mixed val(get<ObjLink>().get_obj_key());
×
NEW
476
            val.to_xjson(out);
×
NEW
477
            break;
×
NEW
478
        }
×
NEW
479
        case type_Link:
✔
NEW
480
        case type_LinkList:
✔
NEW
481
        case type_Mixed:
✔
NEW
482
            break;
×
483
    }
2,205✔
484
}
2,205✔
485

486
void Mixed::to_xjson_plus(std::ostream& out) const noexcept
487
{
1,041✔
488

489
    // Special case for outputing a typedLink, otherwise just us out_mixed_xjson
490
    if (is_type(type_TypedLink)) {
1,041✔
NEW
491
        auto link = get<ObjLink>();
×
NEW
492
        out << "{ \"$link\": { \"table\": \"" << link.get_table_key() << "\", \"key\": ";
×
NEW
493
        Mixed val(link.get_obj_key());
×
NEW
494
        val.to_xjson(out);
×
NEW
495
        out << "}}";
×
NEW
496
        return;
×
NEW
497
    }
×
498

499
    to_xjson(out);
1,041✔
500
}
1,041✔
501

502
void Mixed::to_json(std::ostream& out, JSONOutputMode output_mode) const noexcept
503
{
4,059✔
504
    if (is_null()) {
4,059✔
505
        out << "null";
36✔
506
        return;
36✔
507
    }
36✔
508
    switch (output_mode) {
4,023✔
509
        case output_mode_xjson: {
1,164✔
510
            to_xjson(out);
1,164✔
511
            return;
1,164✔
NEW
512
        }
×
513
        case output_mode_xjson_plus: {
1,041✔
514
            to_xjson_plus(out);
1,041✔
515
            return;
1,041✔
NEW
516
        }
×
517
        case output_mode_json: {
1,818✔
518
            switch (get_type()) {
1,818✔
519
                case type_Int:
642✔
520
                    out << int_val;
642✔
521
                    break;
642✔
522
                case type_Bool:
54✔
523
                    out << (bool_val ? "true" : "false");
27✔
524
                    break;
54✔
525
                case type_Float:
45✔
526
                    out_floats<float>(out, float_val);
45✔
527
                    break;
45✔
528
                case type_Double:
45✔
529
                    out_floats<double>(out, double_val);
45✔
530
                    break;
45✔
531
                case type_String: {
642✔
532
                    out << "\"";
642✔
533
                    out_string(out, string_val);
642✔
534
                    out << "\"";
642✔
535
                    break;
642✔
NEW
536
                }
×
537
                case type_Binary: {
45✔
538
                    out << "\"";
45✔
539
                    out_binary(out, binary_val);
45✔
540
                    out << "\"";
45✔
541
                    break;
45✔
NEW
542
                }
×
543
                case type_Timestamp:
48✔
544
                    out << "\"";
48✔
545
                    out << date_val;
48✔
546
                    out << "\"";
48✔
547
                    break;
48✔
548
                case type_Decimal:
45✔
549
                    out << "\"";
45✔
550
                    out << decimal_val;
45✔
551
                    out << "\"";
45✔
552
                    break;
45✔
553
                case type_ObjectId:
207✔
554
                    out << "\"";
207✔
555
                    out << id_val;
207✔
556
                    out << "\"";
207✔
557
                    break;
207✔
558
                case type_UUID:
45✔
559
                    out << "\"";
45✔
560
                    out << uuid_val;
45✔
561
                    out << "\"";
45✔
562
                    break;
45✔
NEW
563
                case type_TypedLink:
✔
NEW
564
                    out << "\"";
×
NEW
565
                    out << link_val;
×
NEW
566
                    out << "\"";
×
NEW
567
                    break;
×
NEW
568
                case type_Link:
✔
NEW
569
                case type_LinkList:
✔
NEW
570
                case type_Mixed:
✔
NEW
571
                    break;
×
572
            }
1,818✔
573
        }
1,818✔
574
    }
4,023✔
575
}
4,023✔
576

577
} // namespace realm
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