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

paulmthompson / WhiskerToolbox / 18389801194

09 Oct 2025 09:35PM UTC coverage: 71.943% (+0.1%) from 71.826%
18389801194

push

github

paulmthompson
add correlation matrix to filtering interface

207 of 337 new or added lines in 5 files covered. (61.42%)

867 existing lines in 31 files now uncovered.

49964 of 69449 relevant lines covered (71.94%)

1103.53 hits per line

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

69.68
/src/DataManager/Lines/Line_Data.cpp
1
#include "Line_Data.hpp"
2

3
#include "CoreGeometry/points.hpp"
4
#include "utils/map_timeseries.hpp"
5
#include "Entity/EntityRegistry.hpp"
6

7
#include <cmath>
8
#include <iostream>
9
#include <ranges>
10

11
// ========== Constructors ==========
12

13
LineData::LineData(std::map<TimeFrameIndex, std::vector<Line2D>> const & data) {
41✔
14
    // Convert old format to new format
15
    for (auto const & [time, lines] : data) {
96✔
16
        _data[time].reserve(lines.size());
55✔
17
        for (auto const & line : lines) {
118✔
18
            _data[time].emplace_back(line, 0); // EntityId will be 0 initially
63✔
19
        }
20
    }
21
}
41✔
22

23
// ========== Setters ==========
24

25
bool LineData::clearAtTime(TimeFrameIndex const time, bool notify) {
8✔
26
    auto it = _data.find(time);
8✔
27
    if (it != _data.end()) {
8✔
28
        _data.erase(it);
8✔
29
        if (notify) {
8✔
30
            notifyObservers();
1✔
31
        }
32
        return true;
8✔
33
    }
34
    return false;
×
35
}
36

37
bool LineData::clearAtTime(TimeFrameIndex const time, int const line_id, bool notify) {
×
38
    auto it = _data.find(time);
×
39
    if (it != _data.end() && static_cast<size_t>(line_id) < it->second.size()) {
×
40
        it->second.erase(it->second.begin() + static_cast<long int>(line_id));
×
41
        if (it->second.empty()) {
×
42
            _data.erase(it);
×
43
        }
44
        if (notify) {
×
45
            notifyObservers();
×
46
        }
47
        return true;
×
48
    }
49
    return false;
×
50
}
51

52
void LineData::addAtTime(TimeFrameIndex const time, std::vector<float> const & x, std::vector<float> const & y, bool notify) {
622✔
53
    Line2D new_line;
622✔
54
    for (size_t i = 0; i < std::min(x.size(), y.size()); ++i) {
6,818✔
55
        new_line.push_back(Point2D<float>{x[i], y[i]});
6,196✔
56
    }
57
    
58
    int const local_index = static_cast<int>(_data[time].size());
622✔
59
    EntityId entity_id = 0;
622✔
60
    if (_identity_registry) {
622✔
61
        entity_id = _identity_registry->ensureId(_identity_data_key, EntityKind::LineEntity, time, local_index);
32✔
62
    }
63
    
64
    _data[time].emplace_back(std::move(new_line), entity_id);
622✔
65

66
    if (notify) {
622✔
67
        notifyObservers();
242✔
68
    }
69
}
1,244✔
70

71
void LineData::addAtTime(TimeFrameIndex const time, std::vector<Point2D<float>> const & line, bool notify) {
102✔
72
    addAtTime(time, Line2D(line), notify);
102✔
73
}
102✔
74

75
void LineData::addAtTime(TimeFrameIndex const time, Line2D const & line, bool notify) {
2,485✔
76
    int const local_index = static_cast<int>(_data[time].size());
2,485✔
77
    EntityId entity_id = 0;
2,485✔
78
    if (_identity_registry) {
2,485✔
79
        entity_id = _identity_registry->ensureId(_identity_data_key, EntityKind::LineEntity, time, local_index);
2,006✔
80
    }
81
    
82
    _data[time].emplace_back(line, entity_id);
2,485✔
83

84
    if (notify) {
2,485✔
85
        notifyObservers();
351✔
86
    }
87
}
2,485✔
88

89
void LineData::addPointToLine(TimeFrameIndex const time, int const line_id, Point2D<float> point, bool notify) {
×
90
    if (static_cast<size_t>(line_id) < _data[time].size()) {
×
91
        _data[time][static_cast<size_t>(line_id)].line.push_back(point);
×
92
    } else {
93
        std::cerr << "LineData::addPointToLine: line_id out of range" << std::endl;
×
94
        EntityId entity_id = 0;
×
95
        if (_identity_registry) {
×
96
            int const local_index = static_cast<int>(_data[time].size());
×
97
            entity_id = _identity_registry->ensureId(_identity_data_key, EntityKind::LineEntity, time, local_index);
×
98
        }
99
        _data[time].emplace_back(Line2D{point}, entity_id);
×
100
    }
101

102
    if (notify) {
×
103
        notifyObservers();
×
104
    }
105
}
×
106

107
void LineData::addPointToLineInterpolate(TimeFrameIndex const time, int const line_id, Point2D<float> point, bool notify) {
×
108
    if (static_cast<size_t>(line_id) >= _data[time].size()) {
×
109
        std::cerr << "LineData::addPointToLineInterpolate: line_id out of range" << std::endl;
×
110
        EntityId entity_id = 0;
×
111
        if (_identity_registry) {
×
112
            int const local_index = static_cast<int>(_data[time].size());
×
113
            entity_id = _identity_registry->ensureId(_identity_data_key, EntityKind::LineEntity, time, local_index);
×
114
        }
115
        _data[time].emplace_back(Line2D{}, entity_id);
×
116
    }
117

118
    Line2D & line = _data[time][static_cast<size_t>(line_id)].line;
×
119
    if (!line.empty()) {
×
120
        Point2D<float> const last_point = line.back();
×
121
        float const distance = std::sqrt(std::pow(point.x - last_point.x, 2.0f) + std::pow(point.y - last_point.y, 2.0f));
×
122
        int const n = static_cast<int>(distance / 2.0f);
×
123
        for (int i = 1; i <= n; ++i) {
×
124
            float const t = static_cast<float>(i) / static_cast<float>(n + 1);
×
125
            float const interp_x = last_point.x + t * (point.x - last_point.x);
×
126
            float const interp_y = last_point.y + t * (point.y - last_point.y);
×
127
            line.push_back(Point2D<float>{interp_x, interp_y});
×
128
        }
129
    }
130
    line.push_back(point);
×
131
    // Note: smooth_line function needs to be implemented or included
132
    // smooth_line(line);
133

134
    if (notify) {
×
135
        notifyObservers();
×
136
    }
137
}
×
138

139
void LineData::addLineEntryAtTime(TimeFrameIndex const time, Line2D const & line, EntityId entity_id, bool notify) {
6✔
140
    _data[time].emplace_back(line, entity_id);
6✔
141

142
    if (notify) {
6✔
143
        notifyObservers();
×
144
    }
145
}
6✔
146

147
// ========== Getters ==========
148

149
std::vector<Line2D> const & LineData::getAtTime(TimeFrameIndex const time) const {
445✔
150
    // This method needs to return a reference to a vector of Line2D objects
151
    // Since we now store LineEntry objects, we need to create a temporary vector
152
    // We'll use a member variable to store the converted data to maintain reference stability
153
    
154
    auto it = _data.find(time);
445✔
155
    if (it == _data.end()) {
445✔
156
        return _empty;
42✔
157
    }
158
    
159
    // Use a mutable member variable to store the converted lines
160
    // This is not thread-safe but maintains API compatibility
161
    _temp_lines.clear();
403✔
162
    _temp_lines.reserve(it->second.size());
403✔
163
    for (auto const & entry : it->second) {
962✔
164
        _temp_lines.push_back(entry.line);
559✔
165
    }
166
    
167
    return _temp_lines;
403✔
168
}
169

170
std::vector<Line2D> const & LineData::getAtTime(TimeFrameIndex const time, 
205✔
171
                                                TimeFrame const * source_timeframe,
172
                                                TimeFrame const * line_timeframe) const {
173
    // Convert time if needed
174
    TimeFrameIndex converted_time = time;
205✔
175
    if (source_timeframe && line_timeframe && source_timeframe != line_timeframe) {
205✔
176
        auto time_value = source_timeframe->getTimeAtIndex(time);
×
177
        converted_time = line_timeframe->getIndexAtTime(static_cast<float>(time_value));
×
178
    }
179
    
180
    return getAtTime(converted_time);
410✔
181
}
182

183
std::vector<EntityId> const & LineData::getEntityIdsAtTime(TimeFrameIndex const time) const {
1,304✔
184
    // Similar to getAtTime, we need to create a temporary vector
185
    auto it = _data.find(time);
1,304✔
186
    if (it == _data.end()) {
1,304✔
187
        return _empty_entity_ids;
10✔
188
    }
189
    
190
    _temp_entity_ids.clear();
1,294✔
191
    _temp_entity_ids.reserve(it->second.size());
1,294✔
192
    for (auto const & entry : it->second) {
3,823✔
193
        _temp_entity_ids.push_back(entry.entity_id);
2,529✔
194
    }
195
    
196
    return _temp_entity_ids;
1,294✔
197
}
198

199
std::vector<EntityId> const & LineData::getEntityIdsAtTime(TimeFrameIndex const time,
137✔
200
                                                           TimeFrame const * source_timeframe,
201
                                                           TimeFrame const * line_timeframe) const {
202
    // Convert time if needed
203
    TimeFrameIndex converted_time = time;
137✔
204
    if (source_timeframe && line_timeframe && source_timeframe != line_timeframe) {
137✔
205
        auto time_value = source_timeframe->getTimeAtIndex(time);
×
206
        converted_time = line_timeframe->getIndexAtTime(static_cast<float>(time_value));
×
207
    }
208
    
209
    return getEntityIdsAtTime(converted_time);
274✔
210
}
211

212
std::vector<EntityId> LineData::getAllEntityIds() const {
50✔
213
    std::vector<EntityId> out;
50✔
214
    for (auto const & [t, entries] : _data) {
1,543✔
215
        (void)t;
216
        for (auto const & entry : entries) {
4,448✔
217
            out.push_back(entry.entity_id);
2,955✔
218
        }
219
    }
220
    return out;
50✔
221
}
×
222

223
// ========== Entity Lookup Methods ==========
224

225
std::optional<Line2D> LineData::getLineByEntityId(EntityId entity_id) const {
4,042✔
226
    if (!_identity_registry) {
4,042✔
227
        return std::nullopt;
7✔
228
    }
229
    
230
    auto descriptor = _identity_registry->get(entity_id);
4,035✔
231
    if (!descriptor || descriptor->kind != EntityKind::LineEntity || descriptor->data_key != _identity_data_key) {
4,035✔
232
        return std::nullopt;
×
233
    }
234
    
235
    TimeFrameIndex const time{descriptor->time_value};
4,035✔
236
    int const local_index = descriptor->local_index;
4,035✔
237
    
238
    auto time_it = _data.find(time);
4,035✔
239
    if (time_it == _data.end()) {
4,035✔
240
        return std::nullopt;
×
241
    }
242
    
243
    if (local_index < 0 || static_cast<size_t>(local_index) >= time_it->second.size()) {
4,035✔
244
        return std::nullopt;
×
245
    }
246
    
247
    return time_it->second[static_cast<size_t>(local_index)].line;
4,035✔
248
}
4,035✔
249

UNCOV
250
std::optional<std::reference_wrapper<Line2D>> LineData::getMutableLineByEntityId(EntityId entity_id) {
×
UNCOV
251
    if (!_identity_registry) {
×
UNCOV
252
        return std::nullopt;
×
253
    }
254
    
UNCOV
255
    auto descriptor = _identity_registry->get(entity_id);
×
UNCOV
256
    if (!descriptor || descriptor->kind != EntityKind::LineEntity || descriptor->data_key != _identity_data_key) {
×
257
        return std::nullopt;
×
258
    }
259
    
UNCOV
260
    TimeFrameIndex const time{descriptor->time_value};
×
UNCOV
261
    int const local_index = descriptor->local_index;
×
262
    
UNCOV
263
    auto time_it = _data.find(time);
×
UNCOV
264
    if (time_it == _data.end()) {
×
UNCOV
265
        return std::nullopt;
×
266
    }
267
    
UNCOV
268
    if (local_index < 0 || static_cast<size_t>(local_index) >= time_it->second.size()) {
×
UNCOV
269
        return std::nullopt;
×
270
    }
271
    
UNCOV
272
    return std::ref(time_it->second[static_cast<size_t>(local_index)].line);
×
UNCOV
273
}
×
274

275
std::optional<std::pair<TimeFrameIndex, int>> LineData::getTimeAndIndexByEntityId(EntityId entity_id) const {
338✔
276
    if (!_identity_registry) {
338✔
277
        return std::nullopt;
7✔
278
    }
279
    
280
    auto descriptor = _identity_registry->get(entity_id);
331✔
281
    if (!descriptor || descriptor->kind != EntityKind::LineEntity || descriptor->data_key != _identity_data_key) {
331✔
UNCOV
282
        return std::nullopt;
×
283
    }
284
    
285
    TimeFrameIndex const time{descriptor->time_value};
331✔
286
    int const local_index = descriptor->local_index;
331✔
287
    
288
    // Verify the time and index are valid
289
    auto time_it = _data.find(time);
331✔
290
    if (time_it == _data.end() || local_index < 0 || static_cast<size_t>(local_index) >= time_it->second.size()) {
331✔
UNCOV
291
        return std::nullopt;
×
292
    }
293
    
294
    return std::make_pair(time, local_index);
331✔
295
}
331✔
296

297
std::vector<std::pair<EntityId, Line2D>> LineData::getLinesByEntityIds(std::vector<EntityId> const & entity_ids) const {
5✔
298
    std::vector<std::pair<EntityId, Line2D>> results;
5✔
299
    results.reserve(entity_ids.size());
5✔
300
    
301
    for (EntityId const entity_id : entity_ids) {
17✔
302
        auto line = getLineByEntityId(entity_id);
12✔
303
        if (line.has_value()) {
12✔
304
            results.emplace_back(entity_id, std::move(line.value()));
7✔
305
        }
306
    }
12✔
307
    
308
    return results;
5✔
309
}
×
310

311
std::vector<std::tuple<EntityId, TimeFrameIndex, int>> LineData::getTimeInfoByEntityIds(std::vector<EntityId> const & entity_ids) const {
4✔
312
    std::vector<std::tuple<EntityId, TimeFrameIndex, int>> results;
4✔
313
    results.reserve(entity_ids.size());
4✔
314
    
315
    for (EntityId const entity_id : entity_ids) {
13✔
316
        auto time_info = getTimeAndIndexByEntityId(entity_id);
9✔
317
        if (time_info.has_value()) {
9✔
318
            results.emplace_back(entity_id, time_info->first, time_info->second);
4✔
319
        }
320
    }
321
    
322
    return results;
4✔
UNCOV
323
}
×
324

325
// ========== Image Size ==========
326

UNCOV
327
void LineData::changeImageSize(ImageSize const & image_size)
×
328
{
UNCOV
329
    if (_image_size.width == -1 || _image_size.height == -1) {
×
330
        std::cout << "No size set for current image. "
UNCOV
331
                  << " Please set a valid image size before trying to scale" << std::endl;
×
332
    }
333

UNCOV
334
    if (_image_size.width == image_size.width && _image_size.height == image_size.height) {
×
335
        std::cout << "Image size is the same. No need to scale" << std::endl;
×
336
        return;
×
337
    }
338

UNCOV
339
    float const scale_x = static_cast<float>(image_size.width) / static_cast<float>(_image_size.width);
×
340
    float const scale_y = static_cast<float>(image_size.height) / static_cast<float>(_image_size.height);
×
341

UNCOV
342
    for (auto & [time, entries] : _data) {
×
UNCOV
343
        for (auto & entry : entries) {
×
UNCOV
344
            for (auto & point : entry.line) {
×
UNCOV
345
                point.x *= scale_x;
×
UNCOV
346
                point.y *= scale_y;
×
347
            }
348
        }
349
    }
UNCOV
350
    _image_size = image_size;
×
351
}
352

353
void LineData::setIdentityContext(std::string const & data_key, EntityRegistry * registry) {
333✔
354
    _identity_data_key = data_key;
333✔
355
    _identity_registry = registry;
333✔
356
}
333✔
357

358
void LineData::rebuildAllEntityIds() {
333✔
359
    if (!_identity_registry) {
333✔
UNCOV
360
        for (auto & [t, entries] : _data) {
×
UNCOV
361
            for (auto & entry : entries) {
×
UNCOV
362
                entry.entity_id = 0;
×
363
            }
364
        }
UNCOV
365
        return;
×
366
    }
367
    
368
    for (auto & [t, entries] : _data) {
1,263✔
369
        for (int i = 0; i < static_cast<int>(entries.size()); ++i) {
2,149✔
370
            entries[static_cast<size_t>(i)].entity_id = _identity_registry->ensureId(_identity_data_key, EntityKind::LineEntity, t, i);
1,219✔
371
        }
372
    }
373
}
374

375
// ========== Copy and Move ==========
376

377
std::size_t LineData::copyTo(LineData& target, TimeFrameInterval const & interval, bool notify) const {
8✔
378
    if (interval.start > interval.end) {
8✔
379
        std::cerr << "LineData::copyTo: interval start (" << interval.start.getValue() 
1✔
380
                  << ") must be <= interval end (" << interval.end.getValue() << ")" << std::endl;
1✔
381
        return 0;
1✔
382
    }
383

384
    std::size_t total_lines_copied = 0;
7✔
385

386
    // Iterate through all times in the source data within the interval
387
    for (auto const & [time, entries] : _data) {
27✔
388
        if (time >= interval.start && time <= interval.end && !entries.empty()) {
20✔
389
            for (auto const& entry : entries) {
21✔
390
                target.addAtTime(time, entry.line, false); // Don't notify for each operation
13✔
391
                total_lines_copied++;
13✔
392
            }
393
        }
394
    }
395

396
    // Notify observer only once at the end if requested
397
    if (notify && total_lines_copied > 0) {
7✔
398
        target.notifyObservers();
5✔
399
    }
400

401
    return total_lines_copied;
7✔
402
}
403

404
std::size_t LineData::copyTo(LineData& target, std::vector<TimeFrameIndex> const& times, bool notify) const {
1✔
405
    std::size_t total_lines_copied = 0;
1✔
406

407
    // Copy lines for each specified time
408
    for (TimeFrameIndex const time : times) {
3✔
409
        auto it = _data.find(time);
2✔
410
        if (it != _data.end() && !it->second.empty()) {
2✔
411
            for (auto const& entry : it->second) {
5✔
412
                target.addAtTime(time, entry.line, false); // Don't notify for each operation
3✔
413
                total_lines_copied++;
3✔
414
            }
415
        }
416
    }
417

418
    // Notify observer only once at the end if requested
419
    if (notify && total_lines_copied > 0) {
1✔
420
        target.notifyObservers();
1✔
421
    }
422

423
    return total_lines_copied;
1✔
424
}
425

426
std::size_t LineData::moveTo(LineData& target, TimeFrameInterval const & interval, bool notify) {
4✔
427
    if (interval.start > interval.end) {
4✔
UNCOV
428
        std::cerr << "LineData::moveTo: interval start (" << interval.start.getValue() 
×
UNCOV
429
                  << ") must be <= interval end (" << interval.end.getValue() << ")" << std::endl;
×
UNCOV
430
        return 0;
×
431
    }
432

433
    std::size_t total_lines_moved = 0;
4✔
434
    std::vector<TimeFrameIndex> times_to_clear;
4✔
435

436
    // First, copy all lines in the interval to target
437
    for (auto const & [time, entries] : _data) {
13✔
438
        if (time >= interval.start && time <= interval.end && !entries.empty()) {
9✔
439
            for (auto const& entry : entries) {
12✔
440
                target.addAtTime(time, entry.line, false); // Don't notify for each operation
7✔
441
                total_lines_moved++;
7✔
442
            }
443
            times_to_clear.push_back(time);
5✔
444
        }
445
    }
446

447
    // Then, clear all the times from source
448
    for (TimeFrameIndex const time : times_to_clear) {
9✔
449
        (void)clearAtTime(time, false); // Don't notify for each operation
5✔
450
    }
451

452
    // Notify observers only once at the end if requested
453
    if (notify && total_lines_moved > 0) {
4✔
454
        target.notifyObservers();
3✔
455
        notifyObservers();
3✔
456
    }
457

458
    return total_lines_moved;
4✔
459
}
4✔
460

461
std::size_t LineData::moveTo(LineData& target, std::vector<TimeFrameIndex> const & times, bool notify) {
2✔
462
    std::size_t total_lines_moved = 0;
2✔
463
    std::vector<TimeFrameIndex> times_to_clear;
2✔
464

465
    // First, copy lines for each specified time to target
466
    for (TimeFrameIndex const time : times) {
6✔
467
        auto it = _data.find(time);
4✔
468
        if (it != _data.end() && !it->second.empty()) {
4✔
469
            for (auto const& entry : it->second) {
4✔
470
                target.addAtTime(time, entry.line, false); // Don't notify for each operation
2✔
471
                total_lines_moved++;
2✔
472
            }
473
            times_to_clear.push_back(time);
2✔
474
        }
475
    }
476

477
    // Then, clear all the times from source
478
    for (TimeFrameIndex const time : times_to_clear) {
4✔
479
        (void)clearAtTime(time, false); // Don't notify for each operation
2✔
480
    }
481

482
    // Notify observers only once at the end if requested
483
    if (notify && total_lines_moved > 0) {
2✔
484
        target.notifyObservers();
1✔
485
        notifyObservers();
1✔
486
    }
487

488
    return total_lines_moved;
2✔
489
}
2✔
490

491
std::size_t LineData::copyLinesByEntityIds(LineData & target, std::vector<EntityId> const & entity_ids, bool notify) {
5✔
492
    std::size_t total_lines_copied = 0;
5✔
493
    
494
    // Iterate through all data to find matching EntityIds
495
    for (auto const & [time, entries] : _data) {
17✔
496
        for (auto const & entry : entries) {
28✔
497
            // Check if this entry's EntityId is in the list to copy
498
            if (std::ranges::find(entity_ids, entry.entity_id) != entity_ids.end()) {
16✔
499
                target.addAtTime(time, entry.line, false); // Don't notify for each operation
6✔
500
                total_lines_copied++;
6✔
501
            }
502
        }
503
    }
504
    
505
    // Notify observer only once at the end if requested
506
    if (notify && total_lines_copied > 0) {
5✔
507
        target.notifyObservers();
3✔
508
    }
509
    
510
    return total_lines_copied;
5✔
511
}
512

513
std::size_t LineData::moveLinesByEntityIds(LineData & target, std::vector<EntityId> const & entity_ids, bool notify) {
4✔
514
    std::size_t total_lines_moved = 0;
4✔
515
    std::vector<std::pair<TimeFrameIndex, size_t>> entries_to_remove;
4✔
516
    
517
    // First, copy all matching lines to target and collect removal information
518
    for (auto const & [time, entries] : _data) {
16✔
519
        for (size_t i = 0; i < entries.size(); ++i) {
28✔
520
            auto const & entry = entries[i];
16✔
521
            // Check if this entry's EntityId is in the list to move
522
            if (std::ranges::find(entity_ids, entry.entity_id) != entity_ids.end()) {
16✔
523
                // For move operations, preserve the original entity ID
524
                target.addLineEntryAtTime(time, entry.line, entry.entity_id, false);
6✔
525
                entries_to_remove.emplace_back(time, i);
6✔
526
                total_lines_moved++;
6✔
527
            }
528
        }
529
    }
530
    
531
    // Then, remove the moved entries from source (in reverse order to maintain indices)
532
    std::ranges::sort(entries_to_remove, 
4✔
533
                      [](auto const & a, auto const & b) {
3✔
534
                          if (a.first != b.first) return a.first > b.first; // Sort by time descending
3✔
535
                          return a.second > b.second; // Then by index descending
2✔
536
                      });
537
    
538
    for (auto const & [time, index] : entries_to_remove) {
10✔
539
        auto it = _data.find(time);
6✔
540
        if (it != _data.end() && index < it->second.size()) {
6✔
541
            it->second.erase(it->second.begin() + static_cast<long>(index));
6✔
542
            if (it->second.empty()) {
6✔
543
                _data.erase(it);
3✔
544
            }
545
        }
546
    }
547
    
548
    // Notify observers only once at the end if requested
549
    if (notify && total_lines_moved > 0) {
4✔
550
        target.notifyObservers();
3✔
551
        notifyObservers();
3✔
552
    }
553
    
554
    return total_lines_moved;
4✔
555
}
4✔
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