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

dance858 / PSLP / 20194694571

13 Dec 2025 04:23PM UTC coverage: 88.341% (+0.6%) from 87.707%
20194694571

Pull #23

github

web-flow
Merge 5f71f389e into c9f5db0c7
Pull Request #23: Added test fix col pos inf

1226 of 1383 branches covered (88.65%)

Branch coverage included in aggregate %.

26 of 28 new or added lines in 1 file covered. (92.86%)

36 existing lines in 2 files now uncovered.

3790 of 4295 relevant lines covered (88.24%)

3400.78 hits per line

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

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

19
#include "Matrix.h"
20
#include "Debugger.h"
21
#include "Memory_wrapper.h"
22
#include "Numerics.h"
23
#include "RowColViews.h"
24
#include "glbopts.h"
25
#include "stdlib.h"
26
#include "string.h"
27

28
Matrix *matrix_new(const double *Ax, const int *Ai, const int *Ap, int n_rows,
34✔
29
                   int n_cols, int nnz)
30
{
31
    DEBUG(ASSERT_NO_ZEROS_D(Ax, nnz));
34✔
32
    Matrix *A = matrix_alloc(n_rows, n_cols, nnz);
34✔
33
    RETURN_PTR_IF_NULL(A, NULL);
34✔
34
    int offset, i, row_size, row_alloc;
35

36
    offset = 0;
34✔
37
    for (i = 0; i < n_rows; ++i)
2,189✔
38
    {
39
        A->p[i].start = Ap[i] + offset;
2,155✔
40
        memcpy(A->x + A->p[i].start, Ax + Ap[i],
2,155✔
41
               (Ap[i + 1] - Ap[i]) * sizeof(double));
2,155✔
42

43
        memcpy(A->i + A->p[i].start, Ai + Ap[i], (Ap[i + 1] - Ap[i]) * sizeof(int));
2,155✔
44

45
        A->p[i].end = Ap[i + 1] + offset;
2,155✔
46
        row_size = A->p[i].end - A->p[i].start;
2,155✔
47
        row_alloc = calc_memory_row(row_size, EXTRA_ROW_SPACE, EXTRA_MEMORY_RATIO);
2,155✔
48
        offset += row_alloc - row_size;
2,155✔
49
    }
50

51
    A->p[n_rows].start = Ap[n_rows] + offset;
34✔
52
    A->p[n_rows].end = A->p[n_rows].start;
34✔
53

54
    return A;
34✔
55
}
56

57
// needed for the transpose function
58
Matrix *matrix_alloc(int n_rows, int n_cols, int nnz)
448✔
59
{
60
    Matrix *A = (Matrix *) ps_malloc(1, sizeof(Matrix));
448✔
61
    RETURN_PTR_IF_NULL(A, NULL);
448✔
62

63
    A->m = n_rows;
448✔
64
    A->n = n_cols;
448✔
65
    A->nnz = nnz;
448✔
66
    A->n_alloc = calc_memory(nnz, n_rows, EXTRA_ROW_SPACE, EXTRA_MEMORY_RATIO);
448✔
67

68
#ifdef TESTING
69
    A->i = (int *) ps_calloc(A->n_alloc, sizeof(int));
448✔
70
    A->p = (RowRange *) ps_calloc(n_rows + 1, sizeof(RowRange));
448✔
71
    A->x = (double *) ps_calloc(A->n_alloc, sizeof(double));
448✔
72
#else
73
    A->i = (int *) ps_malloc(A->n_alloc, sizeof(int));
74
    A->p = (RowRange *) ps_malloc(n_rows + 1, sizeof(RowRange));
75
    A->x = (double *) ps_malloc(A->n_alloc, sizeof(double));
76
#endif
77

78
    if (!A->i || !A->p || !A->x)
448✔
79
    {
80
        free_matrix(A);
×
81
        return NULL;
×
82
    }
83

84
    return A;
448✔
85
}
86

87
Matrix *matrix_new_no_extra_space(const double *Ax, const int *Ai, const int *Ap,
90✔
88
                                  int n_rows, int n_cols, int nnz)
89
{
90
    Matrix *A = (Matrix *) ps_malloc(1, sizeof(Matrix));
90✔
91
    RETURN_PTR_IF_NULL(A, NULL);
90✔
92

93
    A->m = n_rows;
90✔
94
    A->n = n_cols;
90✔
95
    A->nnz = nnz;
90✔
96
    A->n_alloc = nnz;
90✔
97
    A->i = (int *) ps_malloc(A->n_alloc, sizeof(int));
90✔
98
    A->p = (RowRange *) ps_malloc(n_rows + 1, sizeof(RowRange));
90✔
99
    A->x = (double *) ps_malloc(A->n_alloc, sizeof(double));
90✔
100

101
    if (!A->i || !A->p || !A->x)
90✔
102
    {
103
        free_matrix(A);
×
104
        return NULL;
×
105
    }
106

107
    memcpy(A->x, Ax, nnz * sizeof(double));
90✔
108
    memcpy(A->i, Ai, nnz * sizeof(int));
90✔
109

110
    for (int i = 0; i <= n_rows; ++i)
583✔
111
    {
112
        A->p[i].start = Ap[i];
493✔
113
        A->p[i].end = Ap[i + 1];
493✔
114
    }
115

116
    return A;
90✔
117
}
118

119
Matrix *transpose(const Matrix *A, int *work_n_cols)
414✔
120
{
121
    Matrix *AT = matrix_alloc(A->n, A->m, A->nnz);
414✔
122
    RETURN_PTR_IF_NULL(AT, NULL);
414✔
123
    int i, j, start;
124
    int *count = work_n_cols;
414✔
125
    memset(count, 0, A->n * sizeof(int));
414✔
126

127
    // -------------------------------------------------------------------
128
    //  compute nnz in each column of A
129
    // -------------------------------------------------------------------
130
    for (i = 0; i < A->m; ++i)
2,294✔
131
    {
132
        for (j = A->p[i].start; j < A->p[i].end; ++j)
7,918✔
133
        {
134
            count[A->i[j]]++;
6,038✔
135
        }
136
    }
137
    // ------------------------------------------------------------------
138
    //  compute row pointers, taking the extra space into account
139
    // ------------------------------------------------------------------
140
    AT->p[0].start = 0;
414✔
141
    for (i = 0; i < A->n; ++i)
2,575✔
142
    {
143
        start = AT->p[i].start;
2,161✔
144
        AT->p[i].end = start + count[i];
2,161✔
145
        AT->p[i + 1].start =
2,161✔
146
            start + calc_memory_row(count[i], EXTRA_ROW_SPACE, EXTRA_MEMORY_RATIO);
2,161✔
147
        count[i] = start;
2,161✔
148
    }
149

150
    AT->p[A->n].start = AT->n_alloc;
414✔
151
    AT->p[A->n].end = AT->n_alloc;
414✔
152

153
    // ------------------------------------------------------------------
154
    //  fill transposed matrix (this is a bottleneck)
155
    // ------------------------------------------------------------------
156
    for (i = 0; i < A->m; ++i)
2,294✔
157
    {
158
        for (j = A->p[i].start; j < A->p[i].end; j++)
7,918✔
159
        {
160
            AT->x[count[A->i[j]]] = A->x[j];
6,038✔
161
            AT->i[count[A->i[j]]] = i;
6,038✔
162
            count[A->i[j]]++;
6,038✔
163
        }
164
    }
165

166
    return AT;
414✔
167
}
168

169
int calc_memory(int nnz, int n_rows, int extra_row_space, double memory_ratio)
448✔
170
{
171
    return (int) (nnz * memory_ratio) + n_rows * extra_row_space;
448✔
172
}
173

174
int calc_memory_row(int size, int extra_row_space, double memory_ratio)
5,039✔
175
{
176
    return (int) (size * memory_ratio) + extra_row_space;
5,039✔
177
}
178

179
void free_matrix(Matrix *A)
538✔
180
{
181
    if (A)
538✔
182
    {
183
        PS_FREE(A->i);
538✔
184
        PS_FREE(A->p);
538✔
185
        PS_FREE(A->x);
538✔
186
    }
187

188
    PS_FREE(A);
538✔
189
}
538✔
190

191
void remove_extra_space(Matrix *A, const int *row_sizes, const int *col_sizes,
195✔
192
                        bool remove_all, int *col_idxs_map)
193
{
194
    int i, j, start, end, len, row_alloc, curr, n_deleted_rows, col_count;
195
    double extra_row_space = (remove_all) ? 0.0 : EXTRA_ROW_SPACE;
195✔
196
    double extra_mem_ratio = (remove_all) ? 1.0 : EXTRA_MEMORY_RATIO;
195✔
197
    curr = 0;
195✔
198
    n_deleted_rows = 0;
195✔
199

200
    // --------------------------------------------------------------------------
201
    // loop through the rows and remove redundant space, including inactive
202
    // rows.
203
    // --------------------------------------------------------------------------
204
    for (i = 0; i < A->m; ++i)
1,105✔
205
    {
206
        if (row_sizes[i] == SIZE_INACTIVE_ROW)
910✔
207
        {
208
            n_deleted_rows++;
187✔
209
            continue;
187✔
210
        }
211

212
        start = A->p[i].start;
723✔
213
        end = A->p[i].end;
723✔
214
        len = end - start;
723✔
215
        row_alloc = calc_memory_row(len, extra_row_space, extra_mem_ratio);
723✔
216
        memmove(A->x + curr, A->x + start, len * sizeof(double));
723✔
217
        memmove(A->i + curr, A->i + start, len * sizeof(int));
723✔
218
        A->p[i - n_deleted_rows].start = curr;
723✔
219
        A->p[i - n_deleted_rows].end = curr + len;
723✔
220
        curr += row_alloc;
723✔
221
    }
222

223
    A->m -= n_deleted_rows;
195✔
224
    A->p[A->m].start = curr;
195✔
225
    A->p[A->m].end = curr;
195✔
226

227
    // shrink size
228
    A->x = realloc(A->x, curr * sizeof(double));
195✔
229
    A->i = (int *) ps_realloc(A->i, curr, sizeof(int));
195✔
230
    A->p = (RowRange *) ps_realloc(A->p, A->m + 1, sizeof(RowRange));
195✔
231

232
    // -------------------------------------------------------------------------
233
    //                      compute new column indices
234
    // -------------------------------------------------------------------------
235
    col_count = 0;
195✔
236
    for (i = 0; i < A->n; ++i)
1,124✔
237
    {
238
        if (col_sizes[i] == SIZE_INACTIVE_COL)
929✔
239
        {
240
            col_idxs_map[i] = -1;
194✔
241
        }
242
        else
243
        {
244
            col_idxs_map[i] = (col_count++);
735✔
245
        }
246
    }
247
    A->n = col_count;
195✔
248

249
    // -------------------------------------------------------------------------
250
    //                        update column indices
251
    // -------------------------------------------------------------------------
252
    for (i = 0; i < A->m; ++i)
918✔
253
    {
254
        for (j = A->p[i].start; j < A->p[i].end; ++j)
2,822✔
255
        {
256
            A->i[j] = col_idxs_map[A->i[j]];
2,099✔
257
        }
258
    }
259
}
195✔
260

261
bool shift_row(Matrix *A, int row, int extra_space, int max_shift)
28✔
262
{
263
    int left, right, missing_space, remaining_shifts, left_shifts;
264
    int right_shifts, space_left, space_right, n_move_right, n_move_left;
265
    int next_start, next_end;
266
    bool shift_left;
267
    RowRange *row_r = A->p;
28✔
268
    left = row;
28✔
269
    right = row + 1;
28✔
270
    remaining_shifts = max_shift;
28✔
271
    left_shifts = 0;
28✔
272
    right_shifts = 0;
28✔
273
    missing_space = extra_space - (row_r[right].start - row_r[row].end);
28✔
274

275
    if (missing_space <= 0)
28✔
276
    {
277
        return true;
2✔
278
    }
279

280
    // ------------------------------------------------------------------------
281
    // compute the new start index for row 'row' and a lower bound on the start
282
    // index for the the first active row following row 'row'.
283
    // ------------------------------------------------------------------------
284
    while (missing_space > 0)
84✔
285
    {
286
        if (left == 0 && right == A->m)
65✔
287
        {
UNCOV
288
            return false;
×
289
        }
290

291
        // space_left is the number of steps we can shift 'left' row to the
292
        // left without overwriting row 'left - 1'.
293
        // space_right is the number of steps we can shift 'right' row to
294
        // the right without overwriting row 'right + 1'.
295
        assert(left >= 0 && right <= A->m);
65✔
296
        space_left = (left == 0) ? 0 : row_r[left].start - row_r[left - 1].end;
65✔
297
        space_right =
65✔
298
            (right == A->m) ? 0 : row_r[right + 1].start - row_r[right].end;
65✔
299
        assert(space_left >= 0 && space_right >= 0);
65✔
300

301
        // number of elements that must be moved left resp. right if we shift
302
        // in a certain direction
303
        n_move_right = row_r[right].end - row_r[right].start;
65✔
304
        n_move_left = row_r[left].end - row_r[left].start;
65✔
305

306
        // decide which direction to shift
307
        if (left == 0)
65✔
308
        {
309
            if (right != A->m && n_move_right <= remaining_shifts)
7✔
310
            {
311
                shift_left = false;
5✔
312
            }
313
            else
314
            {
315
                return false;
2✔
316
            }
317
        }
318
        else if (right == A->m)
58✔
319
        {
320
            if (left != 0 && n_move_left <= remaining_shifts)
5✔
321
            {
322
                shift_left = true;
4✔
323
            }
324
            else
325
            {
326
                return false;
1✔
327
            }
328
        }
329
        else if (n_move_left == 0)
53✔
330
        {
UNCOV
331
            shift_left = true;
×
332
        }
333
        else if (n_move_right == 0)
53✔
334
        {
335
            shift_left = false;
11✔
336
        }
337
        else if (n_move_left <= remaining_shifts &&
42✔
338
                 (space_left / (double) (n_move_left) >=
37✔
339
                  space_right / (double) (n_move_right)))
37✔
340
        {
341
            shift_left = true;
16✔
342
        }
343
        else if (n_move_right <= remaining_shifts)
26✔
344
        {
345
            shift_left = false;
22✔
346
        }
347
        else
348
        {
349
            return false;
4✔
350
        }
351

352
        assert(!(shift_left && left == 0) && !(!shift_left && right == A->m));
58✔
353

354
        if (shift_left)
58✔
355
        {
356
            left_shifts = MIN(missing_space, space_left);
20✔
357
            missing_space -= left_shifts;
20✔
358
            remaining_shifts -= n_move_left;
20✔
359
            left -= 1;
20✔
360
        }
361
        else
362
        {
363
            right_shifts = MIN(missing_space, space_right);
38✔
364
            missing_space -= right_shifts;
38✔
365
            remaining_shifts -= n_move_right;
38✔
366
            right += 1;
38✔
367
        }
368
    }
369
    assert(remaining_shifts >= 0);
19✔
370

371
    // ------------------------------------------------------------------------
372
    //                 execute total left shift
373
    // ------------------------------------------------------------------------
374
    next_start = row_r[left + 1].start - left_shifts;
19✔
375
    for (; left < row; left++)
39✔
376
    {
377
        int len = row_r[left + 1].end - row_r[left + 1].start;
20✔
378
        if (len > 0)
20✔
379
        {
380
            memmove(A->x + next_start, A->x + row_r[left + 1].start,
20✔
381
                    len * sizeof(double));
382
            memmove(A->i + next_start, A->i + row_r[left + 1].start,
20✔
383
                    len * sizeof(int));
384
        }
385
        row_r[left + 1].start = next_start;
20✔
386
        row_r[left + 1].end = next_start + len;
20✔
387
        next_start += len;
20✔
388
    }
389

390
    // ------------------------------------------------------------------------
391
    //                 execute total right shift
392
    // ------------------------------------------------------------------------
393
    next_end = row_r[right - 1].end + right_shifts;
19✔
394
    for (; right > row + 1; right--)
56✔
395
    {
396
        int len = row_r[right - 1].end - row_r[right - 1].start;
37✔
397
        if (len > 0)
37✔
398
        {
399
            memmove(A->x + next_end - len, A->x + row_r[right - 1].start,
25✔
400
                    len * sizeof(double));
401
            memmove(A->i + next_end - len, A->i + row_r[right - 1].start,
25✔
402
                    len * sizeof(int));
403
        }
404
        row_r[right - 1].start = next_end - len;
37✔
405
        row_r[right - 1].end = next_end;
37✔
406
        next_end = row_r[right - 1].start;
37✔
407
    }
408

409
    assert(row_r[row + 1].start - row_r[row].end == extra_space);
19✔
410
    return true;
19✔
411
}
412

413
void print_row_starts(const RowRange *row_ranges, size_t len)
×
414
{
415
    for (size_t i = 0; i < len; ++i)
×
416
    {
417
        printf("%d ", row_ranges[i].start);
×
418
    }
UNCOV
419
    printf("\n");
×
UNCOV
420
}
×
421

422
double insert_or_update_coeff(Matrix *A, int row, int col, double val, int *row_size)
60✔
423
{
424
    int i, start, end, insertion;
425
    double old_val = 0.0;
60✔
426
    start = A->p[row].start;
60✔
427
    end = A->p[row].end;
60✔
428
    insertion = end;
60✔
429

430
    // -----------------------------------------------------------------
431
    //             find where it should be inserted
432
    // -----------------------------------------------------------------
433
    for (i = start; i < end; ++i)
132✔
434
    {
435
        if (A->i[i] >= col)
128✔
436
        {
437
            insertion = i;
56✔
438
            break;
56✔
439
        }
440
    }
441

442
    // -----------------------------------------------------------------
443
    // Insert the new value if it is nonzero. If it exists or should be
444
    // inserted in the end, we don't need to shift values.
445
    // -----------------------------------------------------------------
446
    if (ABS(val) > ZERO_TOL)
60✔
447
    {
448
        // assert(!IS_ZERO_FEAS_TOL(val));
449
        if (insertion == end)
50✔
450
        {
451
            A->x[insertion] = val;
4✔
452
            A->i[insertion] = col;
4✔
453
            A->p[row].end += 1;
4✔
454
            A->nnz += 1;
4✔
455
            *row_size += 1;
4✔
456
        }
457
        else if (A->i[insertion] == col)
46✔
458
        {
459
            old_val = A->x[insertion];
18✔
460
            A->x[insertion] = val;
18✔
461
        }
462
        else
463
        {
464
            memmove(A->x + insertion + 1, A->x + insertion,
28✔
465
                    (end - insertion) * sizeof(double));
28✔
466
            memmove(A->i + insertion + 1, A->i + insertion,
28✔
467
                    (end - insertion) * sizeof(int));
28✔
468

469
            // insert new value
470
            A->x[insertion] = val;
28✔
471
            A->i[insertion] = col;
28✔
472
            A->p[row].end += 1;
28✔
473
            A->nnz += 1;
28✔
474
            *row_size += 1;
28✔
475
        }
476
    }
477
    // if the new value is zero, we just have to shift
478
    else
479
    {
480
        // we only expect that the new value is zero if the coefficient
481
        // already exists
482
        assert(A->i[insertion] == col);
10✔
483

484
        // we only have to shift values if the zero is not in the end
485
        if (insertion != end - 1)
10✔
486
        {
487
            memmove(A->x + insertion, A->x + insertion + 1,
6✔
488
                    (end - insertion - 1) * sizeof(double));
6✔
489
            memmove(A->i + insertion, A->i + insertion + 1,
6✔
490
                    (end - insertion - 1) * sizeof(int));
6✔
491
        }
492

493
        A->p[row].end -= 1;
10✔
494
        A->nnz -= 1;
10✔
495
        *row_size -= 1;
10✔
496
    }
497

498
    assert(A->p[row].end <= A->p[row + 1].start);
60✔
499
    return old_val;
60✔
500
}
501

502
void remove_coeff(RowView *row, int col)
33✔
503
{
504
    int shift = 0;
33✔
505
    int len = *row->len;
33✔
506
    for (int i = 0; i < len; ++i)
185✔
507
    {
508
        if (row->cols[i] == col)
152✔
509
        {
510
            shift = 1;
33✔
511
        }
512

513
        row->vals[i] = row->vals[i + shift];
152✔
514
        row->cols[i] = row->cols[i + shift];
152✔
515
    }
516

517
    assert(shift != 0);
33✔
518
    (*row->range).end -= 1;
33✔
519
    *row->len -= 1;
33✔
520
}
33✔
521

522
void count_rows(const Matrix *A, int *row_sizes)
180✔
523
{
524
    for (int i = 0; i < A->m; ++i)
1,025✔
525
    {
526
        row_sizes[i] = A->p[i].end - A->p[i].start;
845✔
527
    }
528
}
180✔
529

530
#ifdef TESTING
531
// Function to create a random CSR matrix
532
Matrix *random_matrix_new(int n_rows, int n_cols, double density)
2✔
533
{
534
    // allocate memory
535
    int n_alloc_nnz = (int) (density * n_rows * n_cols);
2✔
536
    double *Ax = (double *) ps_malloc(n_alloc_nnz, sizeof(double));
2✔
537
    int *Ai = (int *) ps_malloc(n_alloc_nnz, sizeof(int));
2✔
538
    int *Ap = (int *) ps_malloc(n_rows + 1, sizeof(int));
2✔
539
    if (!Ax || !Ai || !Ap)
2✔
540
    {
541
        PS_FREE(Ax);
×
542
        PS_FREE(Ai);
×
UNCOV
543
        PS_FREE(Ap);
×
UNCOV
544
        return NULL;
×
545
    }
546

547
    // Initialize random number generator
548
    srand(1);
2✔
549

550
    int nnz_count = 0; // Counter for nonzero elements
2✔
551
    Ap[0] = 0;
2✔
552

553
    for (int i = 0; i < n_rows; ++i)
2,002✔
554
    {
555
        int row_nnz = 0;
2,000✔
556

557
        // Randomly determine the number of nonzeros in this row
558
        for (int j = 0; j < n_cols; ++j)
3,999,508✔
559
        {
560
            if ((double) rand() / RAND_MAX < density)
3,997,510✔
561
            {
562
                if (nnz_count >= n_alloc_nnz)
400,002✔
563
                {
564
                    break;
2✔
565
                }
566

567
                Ax[nnz_count] = ((double) (rand() - rand()) / RAND_MAX) * 20.0;
400,000✔
568
                Ai[nnz_count] = j;
400,000✔
569
                ++nnz_count;
400,000✔
570
                ++row_nnz;
400,000✔
571
            }
572
        }
573
        Ap[i + 1] = Ap[i] + row_nnz;
2,000✔
574
    }
575

576
    // create matrix in modified CSR format
577
    Matrix *A = matrix_new(Ax, Ai, Ap, n_rows, n_cols, nnz_count);
2✔
578
    PS_FREE(Ax);
2✔
579
    PS_FREE(Ai);
2✔
580
    PS_FREE(Ap);
2✔
581

582
    return A;
2✔
583
}
584

585
void replace_row_A(Matrix *A, int row, double ratio, double *new_vals, int *cols_new,
42✔
586
                   int new_len)
587
{
588
    int i, len, start, n_new_elements;
589
    len = A->p[row].end - A->p[row].start;
42✔
590
    n_new_elements = new_len - len;
42✔
591

592
    // potentially shift row to get extra space
593
    if (n_new_elements > 0)
42✔
594
    {
595
        assert(shift_row(A, row, n_new_elements, 2000));
8✔
596
    }
597

598
    // replace the row
599
    start = A->p[row].start;
42✔
600
    for (i = 0; i < new_len; ++i)
8,078✔
601
    {
602
        A->x[start + i] = ratio * new_vals[i];
8,036✔
603
        A->i[start + i] = cols_new[i];
8,036✔
604
    }
605
    A->p[row].end = A->p[row].start + new_len;
42✔
606
    assert(A->p[row].end <= A->p[row + 1].start);
42✔
607
}
42✔
608

609
#endif
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