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

realm / realm-core / github_pull_request_281750

30 Oct 2023 03:37PM UTC coverage: 90.528% (-1.0%) from 91.571%
github_pull_request_281750

Pull #6073

Evergreen

jedelbo
Log free space and history sizes when opening file
Pull Request #6073: Merge next-major

95488 of 175952 branches covered (0.0%)

8973 of 12277 new or added lines in 149 files covered. (73.09%)

622 existing lines in 51 files now uncovered.

233503 of 257934 relevant lines covered (90.53%)

6533720.56 hits per line

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

85.79
/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
#include <realm/util/logger.hpp>
21

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

26
using namespace realm;
27
using namespace realm::util;
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
{
138,750✔
49
    // Nothing needs to be done here
68,217✔
50
}
138,750✔
51

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

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

67
void Replication::add_class(TableKey table_key, StringData name, Table::Type type)
68
{
201,189✔
69
    if (auto logger = get_logger()) {
201,189✔
70
        if (type == Table::Type::Embedded) {
161,154✔
71
            logger->log(LogCategory::object, util::Logger::Level::debug, "Add %1 class '%2'", type, name);
18,993✔
72
        }
18,993✔
73
        else {
142,161✔
74
            logger->log(LogCategory::object, util::Logger::Level::debug, "Add class '%1'", name);
142,161✔
75
        }
142,161✔
76
    }
161,154✔
77
    unselect_all();
201,189✔
78
    m_encoder.insert_group_level_table(table_key); // Throws
201,189✔
79
}
201,189✔
80

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

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

104

105
void Replication::insert_column(const Table* t, ColKey col_key, DataType type, StringData col_name,
106
                                Table* target_table)
107
{
745,545✔
108
    if (auto logger = get_logger()) {
745,545✔
109
        const char* collection_type = "";
692,688✔
110
        if (col_key.is_collection()) {
692,688✔
111
            if (col_key.is_list()) {
209,505✔
112
                collection_type = "list ";
94,662✔
113
            }
94,662✔
114
            else if (col_key.is_dictionary()) {
114,843✔
115
                collection_type = "dictionary ";
52,848✔
116
            }
52,848✔
117
            else {
61,995✔
118
                collection_type = "set ";
61,995✔
119
            }
61,995✔
120
        }
209,505✔
121
        if (target_table) {
692,688✔
122
            logger->log(LogCategory::object, util::Logger::Level::debug,
75,321✔
123
                        "On class '%1': Add property '%2' %3linking '%4'", t->get_class_name(), col_name,
75,321✔
124
                        collection_type, target_table->get_class_name());
75,321✔
125
        }
75,321✔
126
        else {
617,367✔
127
            logger->log(LogCategory::object, util::Logger::Level::debug, "On class '%1': Add property '%2' %3of %4",
617,367✔
128
                        t->get_class_name(), col_name, collection_type, type);
617,367✔
129
        }
617,367✔
130
    }
692,688✔
131
    select_table(t);                  // Throws
745,545✔
132
    m_encoder.insert_column(col_key); // Throws
745,545✔
133
}
745,545✔
134

135
void Replication::erase_column(const Table* t, ColKey col_key)
136
{
525✔
137
    if (auto logger = get_logger()) {
525✔
138
        logger->log(LogCategory::object, util::Logger::Level::debug, "On class '%1': Remove property '%2'",
438✔
139
                    t->get_class_name(), t->get_column_name(col_key));
438✔
140
    }
438✔
141
    select_table(t);                 // Throws
525✔
142
    m_encoder.erase_column(col_key); // Throws
525✔
143
}
525✔
144

145
void Replication::create_object(const Table* t, GlobalKey id)
146
{
4,311,522✔
147
    if (auto logger = get_logger()) {
4,311,522✔
148
        logger->log(LogCategory::object, util::Logger::Level::debug, "Create object '%1'", t->get_class_name());
2,634,477✔
149
    }
2,634,477✔
150
    select_table(t);                              // Throws
4,311,522✔
151
    m_encoder.create_object(id.get_local_key(0)); // Throws
4,311,522✔
152
}
4,311,522✔
153

154
void Replication::create_object_with_primary_key(const Table* t, ObjKey key, Mixed pk)
155
{
445,803✔
156
    if (auto logger = get_logger()) {
445,803✔
157
        logger->log(LogCategory::object, util::Logger::Level::debug, "Create object '%1' with primary key %2",
190,647✔
158
                    t->get_class_name(), pk);
190,647✔
159
    }
190,647✔
160
    select_table(t);              // Throws
445,803✔
161
    m_encoder.create_object(key); // Throws
445,803✔
162
}
445,803✔
163

164
void Replication::remove_object(const Table* t, ObjKey key)
165
{
5,007,459✔
166
    if (auto logger = get_logger()) {
5,007,459✔
167
        if (t->is_embedded()) {
2,519,376✔
168
            logger->log(LogCategory::object, util::Logger::Level::debug, "Remove embedded object '%1'",
8,853✔
169
                        t->get_class_name());
8,853✔
170
        }
8,853✔
171
        else if (t->get_primary_key_column()) {
2,510,523✔
172
            logger->log(LogCategory::object, util::Logger::Level::debug, "Remove object '%1' with primary key %2",
102,975✔
173
                        t->get_class_name(), t->get_primary_key(key));
102,975✔
174
        }
102,975✔
175
        else {
2,407,548✔
176
            logger->log(LogCategory::object, util::Logger::Level::debug, "Remove object '%1'[%2]",
2,407,548✔
177
                        t->get_class_name(), key);
2,407,548✔
178
        }
2,407,548✔
179
    }
2,519,376✔
180
    select_table(t);              // Throws
5,007,459✔
181
    m_encoder.remove_object(key); // Throws
5,007,459✔
182
}
5,007,459✔
183

184
inline void Replication::select_obj(ObjKey key)
185
{
16,742,355✔
186
    if (key == m_selected_obj) {
16,742,355✔
187
        return;
2,950,059✔
188
    }
2,950,059✔
189
    if (auto logger = get_logger()) {
13,792,296✔
190
        if (logger->would_log(util::Logger::Level::debug)) {
2,981,265✔
191
            auto class_name = m_selected_table->get_class_name();
54✔
192
            if (m_selected_table->get_primary_key_column()) {
54✔
193
                auto pk = m_selected_table->get_primary_key(key);
36✔
194
                logger->log(LogCategory::object, util::Logger::Level::debug,
36✔
195
                            "Mutating object '%1' with primary key %2", class_name, pk);
36✔
196
            }
36✔
197
            else if (m_selected_table->is_embedded()) {
18✔
NEW
198
                auto obj = m_selected_table->get_object(key);
×
NEW
199
                logger->log(LogCategory::object, util::Logger::Level::debug, "Mutating object '%1' with path '%2'",
×
NEW
200
                            class_name, obj.get_id());
×
NEW
201
            }
×
202
            else {
18✔
203
                logger->log(LogCategory::object, util::Logger::Level::debug, "Mutating anonymous object '%1'[%2]",
18✔
204
                            class_name, key);
18✔
205
            }
18✔
206
        }
54✔
207
    }
2,981,265✔
208
    m_selected_obj = key;
13,792,296✔
209
}
13,792,296✔
210

211
void Replication::do_set(const Table* t, ColKey col_key, ObjKey key, _impl::Instruction variant)
212
{
16,472,313✔
213
    if (variant != _impl::Instruction::instr_SetDefault) {
16,472,313✔
214
        select_table(t); // Throws
16,470,669✔
215
        select_obj(key);
16,470,669✔
216
        m_encoder.modify_object(col_key, key); // Throws
16,470,669✔
217
    }
16,470,669✔
218
}
16,472,313✔
219

220
void Replication::set(const Table* t, ColKey col_key, ObjKey key, Mixed value, _impl::Instruction variant)
221
{
16,460,406✔
222
    do_set(t, col_key, key, variant); // Throws
16,460,406✔
223
    if (auto logger = get_logger()) {
16,460,406✔
224
        if (logger->would_log(util::Logger::Level::trace)) {
3,132,732✔
225
            if (col_key.get_type() == col_type_Link && value.is_type(type_Link)) {
36!
NEW
226
                auto target_table = t->get_opposite_table(col_key);
×
NEW
227
                if (target_table->is_embedded()) {
×
NEW
228
                    logger->log(LogCategory::object, util::Logger::Level::trace,
×
NEW
229
                                "   Creating embedded object '%1' in '%2'", target_table->get_class_name(),
×
NEW
230
                                t->get_column_name(col_key));
×
NEW
231
                }
×
NEW
232
                else if (target_table->get_primary_key_column()) {
×
NEW
233
                    auto link = value.get<ObjKey>();
×
NEW
234
                    auto pk = target_table->get_primary_key(link);
×
NEW
235
                    logger->log(LogCategory::object, util::Logger::Level::trace,
×
NEW
236
                                "   Linking object '%1' with primary key %2 from '%3'",
×
NEW
237
                                target_table->get_class_name(), pk, t->get_column_name(col_key));
×
NEW
238
                }
×
NEW
239
                else {
×
NEW
240
                    logger->log(LogCategory::object, util::Logger::Level::trace,
×
NEW
241
                                "   Linking object '%1'[%2] from '%3'", target_table->get_class_name(), key,
×
NEW
242
                                t->get_column_name(col_key));
×
NEW
243
                }
×
NEW
244
            }
×
245
            else {
36✔
246
                logger->log(LogCategory::object, util::Logger::Level::trace, "   Set '%1' to %2",
36✔
247
                            t->get_column_name(col_key), value.to_string(util::Logger::max_width_of_value));
36✔
248
            }
36✔
249
        }
36✔
250
    }
3,132,732✔
251
}
16,460,406✔
252

253
void Replication::nullify_link(const Table* t, ColKey col_key, ObjKey key)
254
{
759✔
255
    select_table(t); // Throws
759✔
256
    select_obj(key);
759✔
257
    m_encoder.modify_object(col_key, key); // Throws
759✔
258
    if (auto logger = get_logger()) {
759✔
259
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Nullify '%1'", t->get_column_name(col_key));
669✔
260
    }
669✔
261
}
759✔
262

263
void Replication::add_int(const Table* t, ColKey col_key, ObjKey key, int_fast64_t value)
264
{
10,743✔
265
    do_set(t, col_key, key); // Throws
10,743✔
266
    if (auto logger = get_logger()) {
10,743✔
267
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Adding %1 to '%2'", value,
90✔
268
                    t->get_column_name(col_key));
90✔
269
    }
90✔
270
}
10,743✔
271

272

273
Path Replication::get_prop_name(Path&& path) const
274
{
32,307✔
275
    Path ret(std::move(path));
32,307✔
276
    auto col_key = ret[0].get_col_key();
32,307✔
277
    auto prop_name = m_selected_table->get_column_name(col_key);
32,307✔
278
    ret[0] = PathElement(prop_name);
32,307✔
279
    return ret;
32,307✔
280
}
32,307✔
281

282
void Replication::log_collection_operation(const char* operation, const CollectionBase& collection, Mixed value,
283
                                           Mixed index) const
284
{
18✔
285
    auto logger = get_logger();
18✔
286
    auto path = collection.get_short_path();
18✔
287
    auto col_key = path[0].get_col_key();
18✔
288
    auto prop_name = m_selected_table->get_column_name(col_key);
18✔
289
    path[0] = PathElement(prop_name);
18✔
290
    std::string position;
18✔
291
    if (!index.is_null()) {
18✔
292
        position = util::format(" at position %1", index);
12✔
293
    }
12✔
294
    if (Table::is_link_type(col_key.get_type()) && value.is_type(type_Link)) {
18!
NEW
295
        auto target_table = m_selected_table->get_opposite_table(col_key);
×
NEW
296
        if (target_table->is_embedded()) {
×
NEW
297
            logger->log(LogCategory::object, util::Logger::Level::trace, "   %1 embedded object '%2' in %3%4 ",
×
NEW
298
                        operation, target_table->get_class_name(), path, position);
×
NEW
299
        }
×
NEW
300
        else if (target_table->get_primary_key_column()) {
×
NEW
301
            auto link = value.get<ObjKey>();
×
NEW
302
            auto pk = target_table->get_primary_key(link);
×
NEW
303
            logger->log(LogCategory::object, util::Logger::Level::trace,
×
NEW
304
                        "   %1 object '%2' with primary key %3 in %4%5", operation, target_table->get_class_name(),
×
NEW
305
                        pk, path, position);
×
NEW
306
        }
×
NEW
307
        else {
×
NEW
308
            auto link = value.get<ObjKey>();
×
NEW
309
            logger->log(LogCategory::object, util::Logger::Level::trace, "   %1 object '%2'[%3] in %4%5", operation,
×
NEW
310
                        target_table->get_class_name(), link, path, position);
×
NEW
311
        }
×
NEW
312
    }
×
313
    else {
18✔
314
        logger->log(LogCategory::object, util::Logger::Level::trace, "   %1 %2 in %3%4", operation,
18✔
315
                    value.to_string(util::Logger::max_width_of_value), path, position);
18✔
316
    }
18✔
317
}
18✔
318
void Replication::list_insert(const CollectionBase& list, size_t list_ndx, Mixed value, size_t)
319
{
1,167,087✔
320
    select_collection(list);                                     // Throws
1,167,087✔
321
    m_encoder.collection_insert(list.translate_index(list_ndx)); // Throws
1,167,087✔
322
    if (auto logger = get_logger()) {
1,167,087✔
323
        if (logger->would_log(util::Logger::Level::trace)) {
518,130✔
324
            log_collection_operation("Insert", list, value, int64_t(list_ndx));
6✔
325
        }
6✔
326
    }
518,130✔
327
}
1,167,087✔
328

329
void Replication::list_set(const CollectionBase& list, size_t list_ndx, Mixed value)
330
{
41,457✔
331
    select_collection(list);                                  // Throws
41,457✔
332
    m_encoder.collection_set(list.translate_index(list_ndx)); // Throws
41,457✔
333
    if (auto logger = get_logger()) {
41,457✔
334
        if (logger->would_log(util::Logger::Level::trace)) {
8,736✔
NEW
335
            log_collection_operation("Set", list, value, int64_t(list_ndx));
×
NEW
336
        }
×
337
    }
8,736✔
338
}
41,457✔
339

340
void Replication::list_erase(const CollectionBase& list, size_t link_ndx)
341
{
210,228✔
342
    select_collection(list);                                    // Throws
210,228✔
343
    m_encoder.collection_erase(list.translate_index(link_ndx)); // Throws
210,228✔
344
    if (auto logger = get_logger()) {
210,228✔
345
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Erase '%1' at position %2",
6,546✔
346
                    get_prop_name(list.get_short_path()), link_ndx);
6,546✔
347
    }
6,546✔
348
}
210,228✔
349

350
void Replication::list_move(const CollectionBase& list, size_t from_link_ndx, size_t to_link_ndx)
351
{
1,638✔
352
    select_collection(list);                                                                           // Throws
1,638✔
353
    m_encoder.collection_move(list.translate_index(from_link_ndx), list.translate_index(to_link_ndx)); // Throws
1,638✔
354
    if (auto logger = get_logger()) {
1,638✔
355
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Move %1 to %2 in '%3'", from_link_ndx,
1,500✔
356
                    to_link_ndx, get_prop_name(list.get_short_path()));
1,500✔
357
    }
1,500✔
358
}
1,638✔
359

360
void Replication::set_insert(const CollectionBase& set, size_t set_ndx, Mixed value)
361
{
60,951✔
362
    select_collection(set);               // Throws
60,951✔
363
    m_encoder.collection_insert(set_ndx); // Throws
60,951✔
364
    if (auto logger = get_logger()) {
60,951✔
365
        if (logger->would_log(util::Logger::Level::trace)) {
60,078✔
366
            log_collection_operation("Insert", set, value, Mixed());
6✔
367
        }
6✔
368
    }
60,078✔
369
}
60,951✔
370

371
void Replication::set_erase(const CollectionBase& set, size_t set_ndx, Mixed value)
372
{
11,538✔
373
    select_collection(set);              // Throws
11,538✔
374
    m_encoder.collection_erase(set_ndx); // Throws
11,538✔
375
    if (auto logger = get_logger()) {
11,538✔
376
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Erase %1 from '%2'", value,
11,412✔
377
                    get_prop_name(set.get_short_path()));
11,412✔
378
    }
11,412✔
379
}
11,538✔
380

381
void Replication::set_clear(const CollectionBase& set)
382
{
2,412✔
383
    select_collection(set);                 // Throws
2,412✔
384
    m_encoder.collection_clear(set.size()); // Throws
2,412✔
385
    if (auto logger = get_logger()) {
2,412✔
386
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Clear '%1'",
2,346✔
387
                    get_prop_name(set.get_short_path()));
2,346✔
388
    }
2,346✔
389
}
2,412✔
390

391
void Replication::do_select_table(const Table* table)
392
{
1,390,290✔
393
    m_encoder.select_table(table->get_key()); // Throws
1,390,290✔
394
    m_selected_table = table;
1,390,290✔
395
    m_selected_list = CollectionId();
1,390,290✔
396
    m_selected_obj = ObjKey();
1,390,290✔
397
}
1,390,290✔
398

399
void Replication::do_select_collection(const CollectionBase& list)
400
{
272,316✔
401
    select_table(list.get_table().unchecked_ptr());
272,316✔
402
    ColKey col_key = list.get_col_key();
272,316✔
403
    ObjKey key = list.get_owner_key();
272,316✔
404
    auto path = list.get_stable_path();
272,316✔
405

137,820✔
406
    select_obj(key);
272,316✔
407

137,820✔
408
    m_encoder.select_collection(col_key, key, path); // Throws
272,316✔
409
    m_selected_list = CollectionId(list.get_table()->get_key(), key, std::move(path));
272,316✔
410
}
272,316✔
411

412
void Replication::list_clear(const CollectionBase& list)
413
{
4,584✔
414
    select_collection(list);           // Throws
4,584✔
415
    m_encoder.collection_clear(list.size()); // Throws
4,584✔
416
    if (auto logger = get_logger()) {
4,584✔
417
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Clear '%1'",
3,909✔
418
                    get_prop_name(list.get_short_path()));
3,909✔
419
    }
3,909✔
420
}
4,584✔
421

422
void Replication::link_list_nullify(const Lst<ObjKey>& list, size_t link_ndx)
423
{
1,716✔
424
    select_collection(list);
1,716✔
425
    m_encoder.collection_erase(link_ndx);
1,716✔
426
    if (auto logger = get_logger()) {
1,716✔
427
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Nullify '%1' position %2",
1,656✔
428
                    m_selected_table->get_column_name(list.get_col_key()), link_ndx);
1,656✔
429
    }
1,656✔
430
}
1,716✔
431

432
void Replication::dictionary_insert(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
433
{
83,382✔
434
    select_collection(dict);
83,382✔
435
    m_encoder.collection_insert(ndx);
83,382✔
436
    if (auto logger = get_logger()) {
83,382✔
437
        if (logger->would_log(util::Logger::Level::trace)) {
70,116✔
438
            log_collection_operation("Insert", dict, value, key);
6✔
439
        }
6✔
440
    }
70,116✔
441
}
83,382✔
442

443
void Replication::dictionary_set(const CollectionBase& dict, size_t ndx, Mixed key, Mixed value)
444
{
8,268✔
445
    select_collection(dict);
8,268✔
446
    m_encoder.collection_set(ndx);
8,268✔
447
    if (auto logger = get_logger()) {
8,268✔
448
        if (logger->would_log(util::Logger::Level::trace)) {
8,154✔
NEW
449
            log_collection_operation("Set", dict, value, key);
×
NEW
450
        }
×
451
    }
8,154✔
452
}
8,268✔
453

454
void Replication::dictionary_erase(const CollectionBase& dict, size_t ndx, Mixed key)
455
{
12,126✔
456
    select_collection(dict);
12,126✔
457
    m_encoder.collection_erase(ndx);
12,126✔
458
    if (auto logger = get_logger()) {
12,126✔
459
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Erase %1 from '%2'", key,
4,290✔
460
                    get_prop_name(dict.get_short_path()));
4,290✔
461
    }
4,290✔
462
}
12,126✔
463

464
void Replication::dictionary_clear(const CollectionBase& dict)
465
{
2,340✔
466
    select_collection(dict);
2,340✔
467
    m_encoder.collection_clear(dict.size());
2,340✔
468
    if (auto logger = get_logger()) {
2,340✔
469
        logger->log(LogCategory::object, util::Logger::Level::trace, "   Clear '%1'",
2,304✔
470
                    get_prop_name(dict.get_short_path()));
2,304✔
471
    }
2,304✔
472
}
2,340✔
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