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

systemd / systemd / 20629511025

31 Dec 2025 02:58PM UTC coverage: 72.676% (+0.2%) from 72.439%
20629511025

push

github

web-flow
Support Bash completions for short option group in journalctl (#40214)

Currently, the Bash completions for journalctl tries to match the
previous word _**exactly**_, which leads to the following issue:
`journalctl -u dock` correctly auto completes to `journalctl -u
docker.service`, but `journalctl -eu` provides no completions at all,
which is a shame since I never use the `-u` option alone (almost always
`-eu` or `-efu`, I wish the `-e` option was the default but I digress).

The proposed solution is to assume words that start with only a single
dash and consist of only letters are short option groups and handle them
as if the previous word was the short option using the last character,
e.g. `-efu` -> `-u`.

309992 of 426542 relevant lines covered (72.68%)

1153788.89 hits per line

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

88.46
/src/shared/serialize.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <fcntl.h>
4
#include <unistd.h>
5

6
#include "sd-id128.h"
7

8
#include "alloc-util.h"
9
#include "env-util.h"
10
#include "escape.h"
11
#include "extract-word.h"
12
#include "fd-util.h"
13
#include "fdset.h"
14
#include "fileio.h"
15
#include "format-util.h"
16
#include "hexdecoct.h"
17
#include "image-policy.h"
18
#include "log.h"
19
#include "memfd-util.h"
20
#include "parse-util.h"
21
#include "pidref.h"
22
#include "ratelimit.h"
23
#include "serialize.h"
24
#include "set.h"
25
#include "string-util.h"
26
#include "strv.h"
27
#include "time-util.h"
28

29
int serialize_item(FILE *f, const char *key, const char *value) {
491,024✔
30
        assert(f);
491,024✔
31
        assert(key);
491,024✔
32

33
        if (!value)
491,024✔
34
                return 0;
35

36
        /* Make sure that anything we serialize we can also read back again with read_line() with a maximum line size
37
         * of LONG_LINE_MAX. This is a safety net only. All code calling us should filter this out earlier anyway. */
38
        if (strlen(key) + 1 + strlen(value) + 1 > LONG_LINE_MAX)
422,910✔
39
                return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Attempted to serialize overly long item '%s', refusing.", key);
7✔
40

41
        fputs(key, f);
422,903✔
42
        fputc('=', f);
422,903✔
43
        fputs(value, f);
422,903✔
44
        fputc('\n', f);
422,903✔
45

46
        return 1;
422,903✔
47
}
48

49
int serialize_item_escaped(FILE *f, const char *key, const char *value) {
41,936✔
50
        _cleanup_free_ char *c = NULL;
41,936✔
51

52
        assert(f);
41,936✔
53
        assert(key);
41,936✔
54

55
        if (!value)
41,936✔
56
                return 0;
57

58
        c = xescape(value, " ");
28,620✔
59
        if (!c)
28,620✔
60
                return log_oom();
×
61

62
        return serialize_item(f, key, c);
28,620✔
63
}
64

65
int serialize_item_format(FILE *f, const char *key, const char *format, ...) {
314,575✔
66
        _cleanup_free_ char *allocated = NULL;
314,575✔
67
        char buf[256]; /* Something reasonably short that fits nicely on any stack (i.e. is considerably less
314,575✔
68
                        * than LONG_LINE_MAX (1MiB!) */
69
        const char *b;
314,575✔
70
        va_list ap;
314,575✔
71
        int k;
314,575✔
72

73
        assert(f);
314,575✔
74
        assert(key);
314,575✔
75
        assert(format);
314,575✔
76

77
        /* First, let's try to format this into a stack buffer */
78
        va_start(ap, format);
314,575✔
79
        k = vsnprintf(buf, sizeof(buf), format, ap);
314,575✔
80
        va_end(ap);
314,575✔
81

82
        if (k < 0)
314,575✔
83
                return log_warning_errno(errno, "Failed to serialize item '%s', ignoring: %m", key);
×
84
        if (strlen(key) + 1 + k + 1 > LONG_LINE_MAX) /* See above */
314,575✔
85
                return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Attempted to serialize overly long item '%s', refusing.", key);
×
86

87
        if ((size_t) k < sizeof(buf))
314,575✔
88
                b = buf; /* Yay, it fit! */
89
        else {
90
                /* So the string didn't fit in the short buffer above, but was not above our total limit,
91
                 * hence let's format it via dynamic memory */
92

93
                va_start(ap, format);
×
94
                k = vasprintf(&allocated, format, ap);
×
95
                va_end(ap);
×
96

97
                if (k < 0)
×
98
                        return log_warning_errno(errno, "Failed to serialize item '%s', ignoring: %m", key);
×
99

100
                b = allocated;
×
101
        }
102

103
        fputs(key, f);
314,575✔
104
        fputc('=', f);
314,575✔
105
        fputs(b, f);
314,575✔
106
        fputc('\n', f);
314,575✔
107

108
        return 1;
109
}
110

111
int serialize_fd(FILE *f, FDSet *fds, const char *key, int fd) {
55,434✔
112
        int copy;
55,434✔
113

114
        assert(f);
55,434✔
115
        assert(fds);
55,434✔
116
        assert(key);
55,434✔
117

118
        if (fd < 0)
55,434✔
119
                return 0;
120

121
        copy = fdset_put_dup(fds, fd);
8,101✔
122
        if (copy < 0)
8,101✔
123
                return log_error_errno(copy, "Failed to add file descriptor to serialization set: %m");
×
124

125
        return serialize_item_format(f, key, "%i", copy);
8,101✔
126
}
127

128
int serialize_fd_many(FILE *f, FDSet *fds, const char *key, const int fd_array[], size_t n_fd_array) {
730✔
129
        _cleanup_free_ char *t = NULL;
730✔
130

131
        assert(f);
730✔
132

133
        if (n_fd_array == 0)
730✔
134
                return 0;
135

136
        assert(fd_array);
730✔
137

138
        for (size_t i = 0; i < n_fd_array; i++) {
1,932✔
139
                int copy;
1,202✔
140

141
                if (fd_array[i] < 0)
1,202✔
142
                        return -EBADF;
143

144
                copy = fdset_put_dup(fds, fd_array[i]);
1,202✔
145
                if (copy < 0)
1,202✔
146
                        return log_error_errno(copy, "Failed to add file descriptor to serialization set: %m");
×
147

148
                if (strextendf_with_separator(&t, " ", "%i", copy) < 0)
1,202✔
149
                        return log_oom();
×
150
        }
151

152
        return serialize_item(f, key, t);
730✔
153
}
154

155
int serialize_usec(FILE *f, const char *key, usec_t usec) {
22,267✔
156
        assert(f);
22,267✔
157
        assert(key);
22,267✔
158

159
        if (usec == USEC_INFINITY)
22,267✔
160
                return 0;
161

162
        return serialize_item_format(f, key, USEC_FMT, usec);
5,153✔
163
}
164

165
int serialize_dual_timestamp(FILE *f, const char *key, const dual_timestamp *t) {
141,957✔
166
        assert(f);
141,957✔
167
        assert(key);
141,957✔
168
        assert(t);
141,957✔
169

170
        if (!dual_timestamp_is_set(t))
141,957✔
171
                return 0;
172

173
        return serialize_item_format(f, key, USEC_FMT " " USEC_FMT, t->realtime, t->monotonic);
56,823✔
174
}
175

176
int serialize_strv(FILE *f, const char *key, char * const *l) {
45,469✔
177
        int ret = 0, r;
45,469✔
178

179
        /* Returns the first error, or positive if anything was serialized, 0 otherwise. */
180

181
        assert(f);
45,469✔
182
        assert(key);
45,469✔
183

184
        STRV_FOREACH(i, l) {
72,755✔
185
                r = serialize_item_escaped(f, key, *i);
27,286✔
186
                if ((ret >= 0 && r < 0) ||
27,286✔
187
                    (ret == 0 && r > 0))
27,285✔
188
                        ret = r;
7,244✔
189
        }
190

191
        return ret;
45,469✔
192
}
193

194
int serialize_id128(FILE *f, const char *key, sd_id128_t id) {
18,247✔
195
        assert(f);
18,247✔
196
        assert(key);
18,247✔
197

198
        if (sd_id128_is_null(id))
18,247✔
199
                return 0;
7,934✔
200

201
        return serialize_item_format(f, key, SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(id));
10,313✔
202
}
203

204
int serialize_pidref(FILE *f, FDSet *fds, const char *key, PidRef *pidref) {
13,076✔
205
        int r;
13,076✔
206

207
        assert(f);
13,076✔
208
        assert(fds);
13,076✔
209

210
        if (!pidref_is_set(pidref))
13,076✔
211
                return 0;
212

213
        /* We always serialize the pid separately, to keep downgrades mostly working (older versions will
214
         * deserialize the pid and silently fail to deserialize the pidfd). If we also have a pidfd, we
215
         * serialize both the pid and pidfd, so that we can construct the exact same pidref after
216
         * deserialization (this doesn't work with only the pidfd, as we can't retrieve the original pid
217
         * from the pidfd anymore if the process is reaped). */
218

219
        if (pidref->fd >= 0) {
736✔
220
                int copy = fdset_put_dup(fds, pidref->fd);
736✔
221
                if (copy < 0)
736✔
222
                        return log_error_errno(copy, "Failed to add file descriptor to serialization set: %m");
×
223

224
                r = serialize_item_format(f, key, "@%i:" PID_FMT, copy, pidref->pid);
736✔
225
                if (r < 0)
736✔
226
                        return r;
227
        }
228

229
        return serialize_item_format(f, key, PID_FMT, pidref->pid);
736✔
230
}
231

232
int serialize_ratelimit(FILE *f, const char *key, const RateLimit *rl) {
39,286✔
233
        assert(rl);
39,286✔
234

235
        return serialize_item_format(f, key,
78,572✔
236
                                     USEC_FMT " " USEC_FMT " %u %u",
237
                                     rl->begin,
39,286✔
238
                                     rl->interval,
39,286✔
239
                                     rl->num,
39,286✔
240
                                     rl->burst);
39,286✔
241
}
242

243
int serialize_item_hexmem(FILE *f, const char *key, const void *p, size_t l) {
2,562✔
244
        _cleanup_free_ char *encoded = NULL;
2,562✔
245
        int r;
2,562✔
246

247
        assert(f);
2,562✔
248
        assert(key);
2,562✔
249

250
        if (!p && l > 0)
2,562✔
251
                return -EINVAL;
252

253
        if (l == 0)
2,562✔
254
                return 0;
255

256
        encoded = hexmem(p, l);
1✔
257
        if (!encoded)
1✔
258
                return log_oom_debug();
×
259

260
        r = serialize_item(f, key, encoded);
1✔
261
        if (r < 0)
1✔
262
                return r;
×
263

264
        return 1;
265
}
266

267
int serialize_item_base64mem(FILE *f, const char *key, const void *p, size_t l) {
2,562✔
268
        _cleanup_free_ char *encoded = NULL;
2,562✔
269
        ssize_t len;
2,562✔
270
        int r;
2,562✔
271

272
        assert(f);
2,562✔
273
        assert(key);
2,562✔
274

275
        if (!p && l > 0)
2,562✔
276
                return -EINVAL;
277

278
        if (l == 0)
2,562✔
279
                return 0;
280

281
        len = base64mem(p, l, &encoded);
1✔
282
        if (len <= 0)
1✔
283
                return log_oom_debug();
×
284

285
        r = serialize_item(f, key, encoded);
1✔
286
        if (r < 0)
1✔
287
                return r;
×
288

289
        return 1;
290
}
291

292
int serialize_string_set(FILE *f, const char *key, const Set *s) {
5,122✔
293
        int r;
5,122✔
294

295
        assert(f);
5,122✔
296
        assert(key);
5,122✔
297

298
        if (set_isempty(s))
5,122✔
299
                return 0;
5,122✔
300

301
        /* Serialize as individual items, as each element might contain separators and escapes */
302

303
        const char *e;
1✔
304
        SET_FOREACH(e, s) {
3✔
305
                r = serialize_item(f, key, e);
2✔
306
                if (r < 0)
2✔
307
                        return r;
×
308
        }
309

310
        return 1;
1✔
311
}
312

313
int serialize_image_policy(FILE *f, const char *key, const ImagePolicy *p) {
7,680✔
314
        _cleanup_free_ char *policy = NULL;
7,680✔
315
        int r;
7,680✔
316

317
        assert(f);
7,680✔
318
        assert(key);
7,680✔
319

320
        if (!p)
7,680✔
321
                return 0;
322

323
        r = image_policy_to_string(p, /* simplify= */ false, &policy);
2✔
324
        if (r < 0)
2✔
325
                return r;
326

327
        r = serialize_item(f, key, policy);
2✔
328
        if (r < 0)
2✔
329
                return r;
×
330

331
        return 1;
332
}
333

334
int serialize_bool(FILE *f, const char *key, bool b) {
191,624✔
335
        return serialize_item(f, key, yes_no(b));
354,606✔
336
}
337

338
int serialize_bool_elide(FILE *f, const char *key, bool b) {
130,618✔
339
        return b ? serialize_item(f, key, yes_no(b)) : 0;
130,618✔
340
}
341

342
int deserialize_read_line(FILE *f, char **ret) {
1,789,198✔
343
        _cleanup_free_ char *line = NULL;
1,789,198✔
344
        int r;
1,789,198✔
345

346
        assert(f);
1,789,198✔
347
        assert(ret);
1,789,198✔
348

349
        r = read_stripped_line(f, LONG_LINE_MAX, &line);
1,789,198✔
350
        if (r < 0)
1,789,198✔
351
                return log_error_errno(r, "Failed to read serialization line: %m");
×
352
        if (r == 0) { /* eof */
1,789,198✔
353
                *ret = NULL;
31✔
354
                return 0;
31✔
355
        }
356

357
        if (isempty(line)) { /* End marker */
1,789,167✔
358
                *ret = NULL;
64,369✔
359
                return 0;
64,369✔
360
        }
361

362
        *ret = TAKE_PTR(line);
1,724,798✔
363
        return 1;
1,724,798✔
364
}
365

366
int deserialize_fd(FDSet *fds, const char *value) {
37,386✔
367
        _cleanup_close_ int our_fd = -EBADF;
37,386✔
368
        int parsed_fd;
37,386✔
369

370
        assert(value);
37,386✔
371

372
        parsed_fd = parse_fd(value);
37,386✔
373
        if (parsed_fd < 0)
37,386✔
374
                return log_debug_errno(parsed_fd, "Failed to parse file descriptor serialization: %s", value);
×
375

376
        our_fd = fdset_remove(fds, parsed_fd); /* Take possession of the fd */
37,386✔
377
        if (our_fd < 0)
37,386✔
378
                return log_debug_errno(our_fd, "Failed to acquire fd from serialization fds: %m");
×
379

380
        return TAKE_FD(our_fd);
381
}
382

383
int deserialize_fd_many(FDSet *fds, const char *value, size_t n, int *ret) {
2,046✔
384
        int r, *fd_array = NULL;
2,046✔
385
        size_t m = 0;
2,046✔
386

387
        assert(value);
2,046✔
388

389
        fd_array = new(int, n);
2,046✔
390
        if (!fd_array)
2,046✔
391
                return -ENOMEM;
2,046✔
392

393
        CLEANUP_ARRAY(fd_array, m, close_many_and_free);
4,092✔
394

395
        for (;;) {
10,928✔
396
                _cleanup_free_ char *w = NULL;
4,441✔
397
                int fd;
6,487✔
398

399
                r = extract_first_word(&value, &w, NULL, 0);
6,487✔
400
                if (r < 0)
6,487✔
401
                        return r;
402
                if (r == 0) {
6,487✔
403
                        if (m < n) /* Too few */
2,046✔
404
                                return -EINVAL;
405

406
                        break;
2,046✔
407
                }
408

409
                if (m >= n) /* Too many */
4,441✔
410
                        return -EINVAL;
411

412
                fd = deserialize_fd(fds, w);
4,441✔
413
                if (fd < 0)
4,441✔
414
                        return fd;
415

416
                fd_array[m++] = fd;
4,441✔
417
        }
418

419
        memcpy(ret, fd_array, m * sizeof(int));
2,046✔
420
        fd_array = mfree(fd_array);
2,046✔
421

422
        return 0;
2,046✔
423
}
424

425
int deserialize_strv(const char *value, char ***l) {
82,959✔
426
        ssize_t unescaped_len;
82,959✔
427
        char *unescaped;
82,959✔
428

429
        assert(l);
82,959✔
430
        assert(value);
82,959✔
431

432
        unescaped_len = cunescape(value, 0, &unescaped);
82,959✔
433
        if (unescaped_len < 0)
82,959✔
434
                return unescaped_len;
×
435

436
        return strv_consume(l, unescaped);
82,959✔
437
}
438

439
int deserialize_usec(const char *value, usec_t *ret) {
13,560✔
440
        int r;
13,560✔
441

442
        assert(value);
13,560✔
443
        assert(ret);
13,560✔
444

445
        r = safe_atou64(value, ret);
13,560✔
446
        if (r < 0)
13,560✔
447
                return log_debug_errno(r, "Failed to parse usec value \"%s\": %m", value);
×
448

449
        return 0;
450
}
451

452
int deserialize_dual_timestamp(const char *value, dual_timestamp *ret) {
40,717✔
453
        uint64_t a, b;
40,717✔
454
        int r, pos;
40,717✔
455

456
        assert(value);
40,717✔
457
        assert(ret);
40,717✔
458

459
        pos = strspn(value, WHITESPACE);
40,717✔
460
        if (value[pos] == '-')
40,717✔
461
                return -EINVAL;
40,717✔
462
        pos += strspn(value + pos, DIGITS);
40,716✔
463
        pos += strspn(value + pos, WHITESPACE);
40,716✔
464
        if (value[pos] == '-')
40,716✔
465
                return -EINVAL;
466

467
        r = sscanf(value, "%" PRIu64 "%" PRIu64 "%n", &a, &b, &pos);
40,715✔
468
        if (r != 2)
40,715✔
469
                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
470
                                       "Failed to parse dual timestamp value \"%s\".",
471
                                       value);
472

473
        if (value[pos] != '\0')
40,714✔
474
                /* trailing garbage */
475
                return -EINVAL;
476

477
        *ret = (dual_timestamp) {
40,713✔
478
                .realtime = a,
479
                .monotonic = b,
480
        };
481

482
        return 0;
40,713✔
483
}
484

485
int deserialize_environment(const char *value, char ***list) {
580✔
486
        _cleanup_free_ char *unescaped = NULL;
580✔
487
        ssize_t l;
580✔
488
        int r;
580✔
489

490
        assert(value);
580✔
491
        assert(list);
580✔
492

493
        /* Changes the *environment strv inline. */
494

495
        l = cunescape(value, 0, &unescaped);
580✔
496
        if (l < 0)
580✔
497
                return log_error_errno(l, "Failed to unescape: %m");
2✔
498

499
        r = strv_env_replace_consume(list, TAKE_PTR(unescaped));
578✔
500
        if (r < 0)
578✔
501
                return log_error_errno(r, "Failed to append environment variable: %m");
×
502

503
        return 0;
504
}
505

506
int deserialize_pidref(FDSet *fds, const char *value, PidRef *ret) {
579✔
507
        const char *e;
579✔
508
        int r;
579✔
509

510
        assert(value);
579✔
511
        assert(ret);
579✔
512

513
        e = startswith(value, "@");
579✔
514
        if (e) {
579✔
515
                _cleanup_free_ char *fdstr = NULL, *pidstr = NULL;
579✔
516
                _cleanup_close_ int fd = -EBADF;
579✔
517

518
                r = extract_many_words(&e, ":", /* flags= */ 0, &fdstr, &pidstr);
579✔
519
                if (r < 0)
579✔
520
                        return log_debug_errno(r, "Failed to deserialize pidref '%s': %m", e);
×
521
                if (r == 0)
579✔
522
                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot deserialize pidref from empty string.");
×
523

524
                assert(r <= 2);
579✔
525

526
                fd = deserialize_fd(fds, fdstr);
579✔
527
                if (fd < 0)
579✔
528
                        return fd;
529

530
                /* The serialization format changed after 255.4. In systemd <= 255.4 only pidfd is
531
                 * serialized, but that causes problems when reconstructing pidref (see serialize_pidref for
532
                 * details). After 255.4 the pid is serialized as well even if we have a pidfd, but we still
533
                 * need to support older format as we might be upgrading from a version that still uses the
534
                 * old format. */
535
                if (pidstr) {
579✔
536
                        pid_t pid;
579✔
537

538
                        r = parse_pid(pidstr, &pid);
579✔
539
                        if (r < 0)
579✔
540
                                return log_debug_errno(r, "Failed to parse PID: %s", pidstr);
×
541

542
                        *ret = (PidRef) {
579✔
543
                                .pid = pid,
544
                                .fd = TAKE_FD(fd),
579✔
545
                        };
546
                } else
547
                        r = pidref_set_pidfd_consume(ret, TAKE_FD(fd));
×
548
        } else {
549
                pid_t pid;
×
550

551
                r = parse_pid(value, &pid);
×
552
                if (r < 0)
×
553
                        return log_debug_errno(r, "Failed to parse PID: %s", value);
×
554

555
                r = pidref_set_pid(ret, pid);
×
556
        }
557
        if (r < 0)
579✔
558
                return log_debug_errno(r, "Failed to initialize pidref: %m");
×
559

560
        return 0;
561
}
562

563
void deserialize_ratelimit(RateLimit *rl, const char *name, const char *value) {
28,550✔
564
        usec_t begin, interval;
28,550✔
565
        unsigned num, burst;
28,550✔
566

567
        assert(rl);
28,550✔
568
        assert(name);
28,550✔
569
        assert(value);
28,550✔
570

571
        if (sscanf(value, USEC_FMT " " USEC_FMT " %u %u", &begin, &interval, &num, &burst) != 4)
28,550✔
572
                return log_notice("Failed to parse %s, ignoring: %s", name, value);
×
573

574
        /* Preserve the counter only if the configuration didn't change. */
575
        rl->num = (interval == rl->interval && burst == rl->burst) ? num : 0;
28,550✔
576
        rl->begin = begin;
28,550✔
577
}
578

579
int open_serialization_fd(const char *ident) {
4,782✔
580
        assert(ident);
4,782✔
581

582
        int fd = memfd_new_full(ident, MFD_ALLOW_SEALING);
4,782✔
583
        if (fd < 0)
4,782✔
584
                return fd;
585

586
        log_debug("Serializing %s to memfd.", ident);
4,782✔
587
        return fd;
588
}
589

590
int open_serialization_file(const char *ident, FILE **ret) {
2,687✔
591
        _cleanup_fclose_ FILE *f = NULL;
2,687✔
592
        _cleanup_close_ int fd;
2,687✔
593

594
        assert(ret);
2,687✔
595

596
        fd = open_serialization_fd(ident);
2,687✔
597
        if (fd < 0)
2,687✔
598
                return fd;
599

600
        f = take_fdopen(&fd, "w+");
2,687✔
601
        if (!f)
2,687✔
602
                return -errno;
×
603

604
        *ret = TAKE_PTR(f);
2,687✔
605
        return 0;
2,687✔
606
}
607

608
int finish_serialization_fd(int fd) {
2,095✔
609
        assert(fd >= 0);
2,095✔
610

611
        if (lseek(fd, 0, SEEK_SET) < 0)
2,095✔
612
                return -errno;
×
613

614
        return memfd_set_sealed(fd);
2,095✔
615
}
616

617
int finish_serialization_file(FILE *f) {
2,687✔
618
        int r;
2,687✔
619

620
        assert(f);
2,687✔
621

622
        r = fflush_and_check(f);
2,687✔
623
        if (r < 0)
2,687✔
624
                return r;
625

626
        if (fseeko(f, 0, SEEK_SET) < 0)
2,687✔
627
                return -errno;
×
628

629
        int fd = fileno(f);
2,687✔
630
        if (fd < 0)
2,687✔
631
                return -EBADF;
632

633
        return memfd_set_sealed(fd);
2,687✔
634
}
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