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

realm / realm-core / jorgen.edelbo_402

21 Aug 2024 11:10AM UTC coverage: 91.054% (-0.03%) from 91.085%
jorgen.edelbo_402

Pull #7803

Evergreen

jedelbo
Small fix to Table::typed_write

When writing the realm to a new file from a write transaction,
the Table may be COW so that the top ref is changed. So don't
use the ref that is present in the group when the operation starts.
Pull Request #7803: Feature/string compression

103494 of 181580 branches covered (57.0%)

1929 of 1999 new or added lines in 46 files covered. (96.5%)

695 existing lines in 51 files now uncovered.

220142 of 241772 relevant lines covered (91.05%)

7344461.76 hits per line

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

80.89
/src/realm/replication.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/replication.hpp>
20

21
#include <realm/list.hpp>
22
#include <realm/path.hpp>
23
#include <iostream>
24

25
using namespace realm;
26
using namespace realm::util;
27
using LogLevel = util::Logger::Level;
28

29
const char* Replication::history_type_name(int type)
30
{
18✔
31
    switch (type) {
18✔
32
        case hist_None:
✔
33
            return "None";
×
34
        case hist_OutOfRealm:
✔
35
            return "Local out of Realm";
×
36
        case hist_InRealm:
12✔
37
            return "Local in-Realm";
12✔
38
        case hist_SyncClient:
6✔
39
            return "SyncClient";
6✔
40
        case hist_SyncServer:
✔
41
            return "SyncServer";
×
42
        default:
✔
43
            return "Unknown";
×
44
    }
18✔
45
}
18✔
46

47
void Replication::initialize(DB&)
48
{
72,126✔
49
    // Nothing needs to be done here
50
}
72,126✔
51

52
void Replication::do_initiate_transact(Group&, version_type, bool)
53
{
627,471✔
54
    char* data = m_stream.get_data();
627,471✔
55
    size_t size = m_stream.get_size();
627,471✔
56
    m_encoder.set_buffer(data, data + size);
627,471✔
57
    m_most_recently_created_object.clear();
627,471✔
58
}
627,471✔
59

60
Replication::version_type Replication::prepare_commit(version_type orig_version)
61
{
601,539✔
62
    char* data = m_stream.get_data();
601,539✔
63
    size_t size = m_encoder.write_position() - data;
601,539✔
64
    version_type new_version = prepare_changeset(data, size, orig_version); // Throws
601,539✔
65
    return new_version;
601,539✔
66
}
601,539✔
67

68
void Replication::add_class(TableKey table_key, StringData name, Table::Type type)
69
{
165,924✔
70
    if (auto logger = would_log(LogLevel::debug)) {
165,924✔
71
        if (type == Table::Type::Embedded) {
28,056✔
72
            logger->log(LogCategory::object, LogLevel::debug, "Add %1 class '%2'", type, name);
3,306✔
73
        }
3,306✔
74
        else {
24,750✔
75
            logger->log(LogCategory::object, LogLevel::debug, "Add class '%1'", name);
24,750✔
76
        }
24,750✔
77
    }
28,056✔
78
    unselect_all();
165,924✔
79
    m_encoder.insert_group_level_table(table_key); // Throws
165,924✔
80
}
165,924✔
81

82
void Replication::add_class_with_primary_key(TableKey tk, StringData name, DataType pk_type, StringData pk_name, bool,
83
                                             Table::Type table_type)
84
{
109,785✔
85
    if (auto logger = would_log(LogLevel::debug)) {
109,785✔
86
        logger->log(LogCategory::object, LogLevel::debug, "Add %1 class '%2' with primary key property '%3' of %4",
74,004✔
87
                    table_type, Group::table_name_to_class_name(name), pk_name, pk_type);
74,004✔
88
    }
74,004✔
89
    REALM_ASSERT(table_type != Table::Type::Embedded);
109,785✔
90
    unselect_all();
109,785✔
91
    m_encoder.insert_group_level_table(tk); // Throws
109,785✔
92
}
109,785✔
93

94
void Replication::erase_class(TableKey tk, StringData table_name, size_t)
95
{
1,569✔
96
    if (auto logger = would_log(LogLevel::debug)) {
1,569✔
97
        logger->log(LogCategory::object, LogLevel::debug, "Remove class '%1'",
×
98
                    Group::table_name_to_class_name(table_name));
×
99
    }
×
100
    unselect_all();
1,569✔
101
    m_encoder.erase_class(tk); // Throws
1,569✔
102
}
1,569✔
103

104
void Replication::insert_column(const Table* t, ColKey col_key, DataType type, StringData col_name,
105
                                Table* target_table)
106
{
574,797✔
107
    if (auto logger = would_log(LogLevel::debug)) {
574,797✔
108
        const char* collection_type = "";
286,863✔
109
        if (col_key.is_collection()) {
286,863✔
110
            if (col_key.is_list()) {
107,874✔
111
                collection_type = "list ";
39,114✔
112
            }
39,114✔
113
            else if (col_key.is_dictionary()) {
68,760✔
114
                collection_type = "dictionary ";
35,298✔
115
            }
35,298✔
116
            else {
33,462✔
117
                collection_type = "set ";
33,462✔
118
            }
33,462✔
119
        }
107,874✔
120
        if (target_table) {
286,863✔
121
            logger->log(LogCategory::object, LogLevel::debug, "On class '%1': Add property '%2' %3linking '%4'",
15,138✔
122
                        t->get_class_name(), col_name, collection_type, target_table->get_class_name());
15,138✔
123
        }
15,138✔
124
        else {
271,725✔
125
            logger->log(LogCategory::object, LogLevel::debug, "On class '%1': Add property '%2' %3of %4",
271,725✔
126
                        t->get_class_name(), col_name, collection_type, type);
271,725✔
127
        }
271,725✔
128
    }
286,863✔
129
    select_table(t);                  // Throws
574,797✔
130
    m_encoder.insert_column(col_key); // Throws
574,797✔
131
}
574,797✔
132

133
void Replication::erase_column(const Table* t, ColKey col_key)
134
{
4,242✔
135
    if (auto logger = would_log(LogLevel::debug)) {
4,242✔
136
        logger->log(LogCategory::object, LogLevel::debug, "On class '%1': Remove property '%2'", t->get_class_name(),
60✔
137
                    t->get_column_name(col_key));
60✔
138
    }
60✔
139
    select_table(t);                 // Throws
4,242✔
140
    m_encoder.erase_column(col_key); // Throws
4,242✔
141
}
4,242✔
142

143
void Replication::track_new_object(ObjKey key)
144
{
5,296,050✔
145
    m_selected_obj = key;
5,296,050✔
146
    m_selected_collection = CollectionId();
5,296,050✔
147
    m_newly_created_object = true;
5,296,050✔
148

149
    auto table_index = m_selected_table->get_index_in_group();
5,296,050✔
150
    if (table_index >= m_most_recently_created_object.size()) {
5,296,050✔
151
        if (table_index >= m_most_recently_created_object.capacity())
282,162✔
152
            m_most_recently_created_object.reserve(table_index * 2);
148,626✔
153
        m_most_recently_created_object.resize(table_index + 1);
282,162✔
154
    }
282,162✔
155
    m_most_recently_created_object[table_index] = m_selected_obj;
5,296,050✔
156
}
5,296,050✔
157

158
void Replication::create_object(const Table* t, GlobalKey id)
159
{
4,888,449✔
160
    if (auto logger = would_log(LogLevel::debug)) {
4,888,449✔
161
        logger->log(LogCategory::object, LogLevel::debug, "Create object '%1'", t->get_class_name());
26,010✔
162
    }
26,010✔
163
    select_table(t);                              // Throws
4,888,449✔
164
    m_encoder.create_object(id.get_local_key(0)); // Throws
4,888,449✔
165
    track_new_object(id.get_local_key(0));        // Throws
4,888,449✔
166
}
4,888,449✔
167

168
void Replication::create_object_with_primary_key(const Table* t, ObjKey key, Mixed pk)
169
{
360,354✔
170
    if (auto logger = would_log(LogLevel::debug)) {
360,354✔
171
        logger->log(LogCategory::object, LogLevel::debug, "Create object '%1' with primary key %2",
118,881✔
172
                    t->get_class_name(), pk);
118,881✔
173
    }
118,881✔
174
    select_table(t);              // Throws
360,354✔
175
    m_encoder.create_object(key); // Throws
360,354✔
176
    track_new_object(key);
360,354✔
177
}
360,354✔
178

179
void Replication::create_linked_object(const Table* t, ObjKey key)
180
{
46,533✔
181
    select_table(t);       // Throws
46,533✔
182
    track_new_object(key); // Throws
46,533✔
183
    // Does not need to encode anything as embedded tables can't be observed
184
}
46,533✔
185

186
void Replication::remove_object(const Table* t, ObjKey key)
187
{
4,936,050✔
188
    if (auto logger = would_log(LogLevel::debug)) {
4,936,050✔
189
        if (t->is_embedded()) {
7,512✔
190
            logger->log(LogCategory::object, LogLevel::debug, "Remove embedded object '%1'", t->get_class_name());
3,657✔
191
        }
3,657✔
192
        else if (t->get_primary_key_column()) {
3,855✔
193
            logger->log(LogCategory::object, LogLevel::debug, "Remove object '%1' with primary key %2",
3,672✔
194
                        t->get_class_name(), t->get_primary_key(key));
3,672✔
195
        }
3,672✔
196
        else {
183✔
197
            logger->log(LogCategory::object, LogLevel::debug, "Remove object '%1'[%2]", t->get_class_name(), key);
183✔
198
        }
183✔
199
    }
7,512✔
200
    select_table(t);              // Throws
4,936,050✔
201
    m_encoder.remove_object(key); // Throws
4,936,050✔
202
}
4,936,050✔
203

204
void Replication::do_select_table(const Table* table)
205
{
1,024,890✔
206
    m_encoder.select_table(table->get_key()); // Throws
1,024,890✔
207
    m_selected_table = table;
1,024,890✔
208
    m_selected_collection = CollectionId();
1,024,890✔
209
    m_selected_obj = ObjKey();
1,024,890✔
210
}
1,024,890✔
211

212
void Replication::do_select_obj(ObjKey key)
213
{
10,673,580✔
214
    m_selected_obj = key;
10,673,580✔
215
    m_selected_collection = CollectionId();
10,673,580✔
216

217
    auto table_index = m_selected_table->get_index_in_group();
10,673,580✔
218
    if (table_index < m_most_recently_created_object.size()) {
10,673,580✔
219
        m_newly_created_object = m_most_recently_created_object[table_index] == key;
1,032,960✔
220
    }
1,032,960✔
221
    else {
9,640,620✔
222
        m_newly_created_object = false;
9,640,620✔
223
    }
9,640,620✔
224

225
    if (auto logger = would_log(LogLevel::debug)) {
10,673,580✔
226
        auto class_name = m_selected_table->get_class_name();
115,434✔
227
        if (m_selected_table->get_primary_key_column()) {
115,434✔
228
            auto pk = m_selected_table->get_primary_key(key);
62,607✔
229
            logger->log(LogCategory::object, LogLevel::debug, "Mutating object '%1' with primary key %2", class_name,
62,607✔
230
                        pk);
62,607✔
231
        }
62,607✔
232
        else if (m_selected_table->is_embedded()) {
52,827✔
233
            auto obj = m_selected_table->get_object(key);
27,795✔
234
            logger->log(LogCategory::object, LogLevel::debug, "Mutating object '%1' with path '%2'", class_name,
27,795✔
235
                        obj.get_id());
27,795✔
236
        }
27,795✔
237
        else {
25,032✔
238
            logger->log(LogCategory::object, LogLevel::debug, "Mutating anonymous object '%1'[%2]", class_name, key);
25,032✔
239
        }
25,032✔
240
    }
115,434✔
241
}
10,673,580✔
242

243
void Replication::do_select_collection(const CollectionBase& coll)
244
{
260,982✔
245
    select_table(coll.get_table().unchecked_ptr());
260,982✔
246
    ColKey col_key = coll.get_col_key();
260,982✔
247
    ObjKey key = coll.get_owner_key();
260,982✔
248
    auto path = coll.get_stable_path();
260,982✔
249

250
    if (select_obj(key)) {
260,982✔
251
        m_encoder.select_collection(col_key, key, path); // Throws
150,333✔
252
    }
150,333✔
253
    m_selected_collection = CollectionId(coll.get_table()->get_key(), key, std::move(path));
260,982✔
254
}
260,982✔
255

256
void Replication::do_set(const Table* t, ColKey col_key, ObjKey key, _impl::Instruction variant)
257
{
18,982,674✔
258
    if (variant != _impl::Instruction::instr_SetDefault) {
18,982,674✔
259
        select_table(t); // Throws
18,982,305✔
260
        if (select_obj(key)) {
18,982,305✔
261
            m_encoder.modify_object(col_key, key); // Throws
13,122,228✔
262
        }
13,122,228✔
263
    }
18,982,305✔
264
}
18,982,674✔
265

266
void Replication::set(const Table* t, ColKey col_key, ObjKey key, Mixed value, _impl::Instruction variant)
267
{
18,973,836✔
268
    do_set(t, col_key, key, variant); // Throws
18,973,836✔
269
    if (auto logger = would_log(LogLevel::trace)) {
18,973,836✔
270
        if (col_key.get_type() == col_type_Link && value.is_type(type_Link)) {
30!
UNCOV
271
            auto target_table = t->get_opposite_table(col_key);
×
UNCOV
272
            if (target_table->is_embedded()) {
×
273
                logger->log(LogCategory::object, LogLevel::trace, "   Creating embedded object '%1' in '%2'",
×
274
                            target_table->get_class_name(), t->get_column_name(col_key));
×
275
            }
×
276
            else if (target_table->get_primary_key_column()) {
×
277
                auto link = value.get<ObjKey>();
×
278
                auto pk = target_table->get_primary_key(link);
×
279
                logger->log(LogCategory::object, LogLevel::trace,
×
280
                            "   Linking object '%1' with primary key %2 from '%3'", target_table->get_class_name(),
×
281
                            pk, t->get_column_name(col_key));
×
282
            }
×
283
            else {
×
284
                logger->log(LogCategory::object, LogLevel::trace, "   Linking object '%1'[%2] from '%3'",
×
285
                            target_table->get_class_name(), key, t->get_column_name(col_key));
×
286
            }
×
287
        }
×
288
        else {
30✔
289
            logger->log(LogCategory::object, LogLevel::trace, "   Set '%1' to %2", t->get_column_name(col_key),
30✔
290
                        value.to_string(util::Logger::max_width_of_value));
30✔
291
        }
30✔
292
    }
30✔
293
}
18,973,836✔
294

295
void Replication::nullify_link(const Table* t, ColKey col_key, ObjKey key)
296
{
771✔
297
    select_table(t); // Throws
771✔
298
    if (select_obj(key)) {
771✔
299
        m_encoder.modify_object(col_key, key); // Throws
753✔
300
    }
753✔
301
    if (auto logger = would_log(LogLevel::trace)) {
771✔
UNCOV
302
        logger->log(LogCategory::object, LogLevel::trace, "   Nullify '%1'", t->get_column_name(col_key));
×
303
    }
×
304
}
771✔
305

306
void Replication::add_int(const Table* t, ColKey col_key, ObjKey key, int_fast64_t value)
307
{
8,481✔
308
    do_set(t, col_key, key); // Throws
8,481✔
309
    if (auto logger = get_logger()) {
8,481✔
310
        logger->log(LogCategory::object, LogLevel::trace, "   Adding %1 to '%2'", value, t->get_column_name(col_key));
96✔
311
    }
96✔
312
}
8,481✔
313

314
Path Replication::get_prop_name(Path&& path) const
UNCOV
315
{
×
316
    auto col_key = path[0].get_col_key();
×
317
    auto prop_name = m_selected_table->get_column_name(col_key);
×
318
    path[0] = PathElement(prop_name);
×
319
    return std::move(path);
×
320
}
×
321

322
void Replication::log_collection_operation(const char* operation, const CollectionBase& collection, Mixed value,
323
                                           Mixed index) const
324
{
1,487,439✔
325
    auto logger = would_log(LogLevel::trace);
1,487,439✔
326
    if (REALM_LIKELY(!logger))
1,487,439✔
327
        return;
1,487,427✔
328

329
    auto path = collection.get_short_path();
12✔
330
    auto col_key = path[0].get_col_key();
12✔
331
    auto prop_name = m_selected_table->get_column_name(col_key);
12✔
332
    path[0] = PathElement(prop_name);
12✔
333
    std::string position;
12✔
334
    if (!index.is_null()) {
12✔
335
        position = util::format(" at position %1", index);
12✔
336
    }
12✔
337
    if (Table::is_link_type(col_key.get_type()) && value.is_type(type_Link)) {
12!
UNCOV
338
        auto target_table = m_selected_table->get_opposite_table(col_key);
×
UNCOV
339
        if (target_table->is_embedded()) {
×
340
            logger->log(LogCategory::object, LogLevel::trace, "   %1 embedded object '%2' in %3%4 ", operation,
×
341
                        target_table->get_class_name(), path, position);
×
342
        }
×
343
        else if (target_table->get_primary_key_column()) {
×
344
            auto link = value.get<ObjKey>();
×
345
            auto pk = target_table->get_primary_key(link);
×
346
            logger->log(LogCategory::object, LogLevel::trace, "   %1 object '%2' with primary key %3 in %4%5",
×
347
                        operation, target_table->get_class_name(), pk, path, position);
×
348
        }
×
349
        else {
×
350
            auto link = value.get<ObjKey>();
×
351
            logger->log(LogCategory::object, LogLevel::trace, "   %1 object '%2'[%3] in %4%5", operation,
×
352
                        target_table->get_class_name(), link, path, position);
×
353
        }
×
354
    }
×
355
    else {
12✔
356
        logger->log(LogCategory::object, LogLevel::trace, "   %1 %2 in %3%4", operation,
12✔
357
                    value.to_string(util::Logger::max_width_of_value), path, position);
12✔
358
    }
12✔
359
}
12✔
360

361
void Replication::list_insert(const CollectionBase& list, size_t list_ndx, Mixed value, size_t)
362
{
1,337,733✔
363
    if (select_collection(list)) {                                   // Throws
1,337,733✔
364
        m_encoder.collection_insert(list.translate_index(list_ndx)); // Throws
399,465✔
365
    }
399,465✔
366
    log_collection_operation("Insert", list, value, int64_t(list_ndx));
1,337,733✔
367
}
1,337,733✔
368

369
void Replication::list_set(const CollectionBase& list, size_t list_ndx, Mixed value)
370
{
49,488✔
371
    if (select_collection(list)) {                                // Throws
49,488✔
372
        m_encoder.collection_set(list.translate_index(list_ndx)); // Throws
33,918✔
373
    }
33,918✔
374
    log_collection_operation("Set", list, value, int64_t(list_ndx));
49,488✔
375
}
49,488✔
376

377
void Replication::list_erase(const CollectionBase& list, size_t link_ndx)
378
{
213,210✔
379
    if (select_collection(list)) {                                  // Throws
213,210✔
380
        m_encoder.collection_erase(list.translate_index(link_ndx)); // Throws
76,833✔
381
    }
76,833✔
382
    if (auto logger = would_log(LogLevel::trace)) {
213,210✔
UNCOV
383
        logger->log(LogCategory::object, LogLevel::trace, "   Erase '%1' at position %2",
×
UNCOV
384
                    get_prop_name(list.get_short_path()), link_ndx);
×
385
    }
×
386
}
213,210✔
387

388
void Replication::list_move(const CollectionBase& list, size_t from_link_ndx, size_t to_link_ndx)
389
{
1,758✔
390
    if (select_collection(list)) {                                                                         // Throws
1,758✔
391
        m_encoder.collection_move(list.translate_index(from_link_ndx), list.translate_index(to_link_ndx)); // Throws
972✔
392
    }
972✔
393
    if (auto logger = would_log(LogLevel::trace)) {
1,758✔
UNCOV
394
        logger->log(LogCategory::object, LogLevel::trace, "   Move %1 to %2 in '%3'", from_link_ndx, to_link_ndx,
×
UNCOV
395
                    get_prop_name(list.get_short_path()));
×
396
    }
×
397
}
1,758✔
398

399
void Replication::set_insert(const CollectionBase& set, size_t set_ndx, Mixed value)
400
{
63,624✔
401
    Replication::list_insert(set, set_ndx, value, 0); // Throws
63,624✔
402
}
63,624✔
403

404
void Replication::set_erase(const CollectionBase& set, size_t set_ndx, Mixed)
405
{
12,522✔
406
    Replication::list_erase(set, set_ndx); // Throws
12,522✔
407
}
12,522✔
408

409
void Replication::set_clear(const CollectionBase& set)
410
{
2,418✔
411
    Replication::list_clear(set); // Throws
2,418✔
412
}
2,418✔
413

414
void Replication::list_clear(const CollectionBase& list)
415
{
9,171✔
416
    if (select_collection(list)) {               // Throws
9,171✔
417
        m_encoder.collection_clear(list.size()); // Throws
7,641✔
418
    }
7,641✔
419
    if (auto logger = would_log(LogLevel::trace)) {
9,171✔
UNCOV
420
        logger->log(LogCategory::object, LogLevel::trace, "   Clear '%1'", get_prop_name(list.get_short_path()));
×
UNCOV
421
    }
×
422
}
9,171✔
423

424
void Replication::link_list_nullify(const Lst<ObjKey>& list, size_t link_ndx)
425
{
2,136✔
426
    if (select_collection(list)) { // Throws
2,136✔
427
        m_encoder.collection_erase(link_ndx);
1,956✔
428
    }
1,956✔
429
    if (auto logger = would_log(LogLevel::trace)) {
2,136✔
UNCOV
430
        logger->log(LogCategory::object, LogLevel::trace, "   Nullify '%1' position %2",
×
UNCOV
431
                    m_selected_table->get_column_name(list.get_col_key()), link_ndx);
×
UNCOV
432
    }
×
433
}
2,136✔
434

435
void Replication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
436
{
90,387✔
437
    if (select_collection(dict)) { // Throws
90,387✔
438
        m_encoder.collection_insert(ndx);
46,407✔
439
    }
46,407✔
440
    log_collection_operation("Insert", dict, value, key);
90,387✔
441
}
90,387✔
442

443
void Replication::dictionary_set(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
444
{
9,837✔
445
    if (select_collection(dict)) { // Throws
9,837✔
446
        m_encoder.collection_set(ndx);
8,277✔
447
    }
8,277✔
448
    log_collection_operation("Set", dict, value, key);
9,837✔
449
}
9,837✔
450

451
void Replication::dictionary_erase(const CollectionBase& dict, size_t ndx, Mixed key)
452
{
12,300✔
453
    if (select_collection(dict)) { // Throws
12,300✔
454
        m_encoder.collection_erase(ndx);
11,334✔
455
    }
11,334✔
456
    if (auto logger = would_log(LogLevel::trace)) {
12,300✔
UNCOV
457
        logger->log(LogCategory::object, LogLevel::trace, "   Erase %1 from '%2'", key,
×
UNCOV
458
                    get_prop_name(dict.get_short_path()));
×
UNCOV
459
    }
×
460
}
12,300✔
461

462
void Replication::dictionary_clear(const CollectionBase& dict)
463
{
2,856✔
464
    if (select_collection(dict)) { // Throws
2,856✔
465
        m_encoder.collection_clear(dict.size());
2,604✔
466
    }
2,604✔
467
    if (auto logger = would_log(LogLevel::trace)) {
2,856✔
UNCOV
468
        logger->log(LogCategory::object, LogLevel::trace, "   Clear '%1'", get_prop_name(dict.get_short_path()));
×
UNCOV
469
    }
×
470
}
2,856✔
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