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

zbraniecki / icu4x / 6815798908

09 Nov 2023 05:17PM CUT coverage: 72.607% (-2.4%) from 75.01%
6815798908

push

github

web-flow
Implement `Any/BufferProvider` for some smart pointers (#4255)

Allows storing them as a `Box<dyn Any/BufferProvider>` without using a
wrapper type that implements the trait.

44281 of 60987 relevant lines covered (72.61%)

201375.86 hits per line

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

95.48
/components/datetime/src/pattern/reference/parser.rs
1
// This file is part of ICU4X. For terms of use, please see the file
11✔
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4

5
use super::{
6
    super::error::PatternError,
7
    super::{GenericPatternItem, PatternItem},
8
    GenericPattern, Pattern,
9
};
10
use crate::fields::FieldSymbol;
11
use alloc::string::String;
12
use alloc::vec;
13
use alloc::vec::Vec;
14
use core::convert::{TryFrom, TryInto};
15

16
#[derive(Debug, PartialEq)]
×
17
enum Segment {
18
    Symbol { symbol: FieldSymbol, length: u8 },
×
19
    Literal { literal: String, quoted: bool },
×
20
}
21

22
#[derive(Debug)]
×
23
pub struct Parser<'p> {
24
    source: &'p str,
×
25
    state: Segment,
×
26
}
27

28
impl<'p> Parser<'p> {
29
    pub fn new(source: &'p str) -> Self {
757✔
30
        Self {
757✔
31
            source,
32
            state: Segment::Literal {
757✔
33
                literal: String::new(),
757✔
34
                quoted: false,
35
            },
36
        }
37
    }
757✔
38

39
    fn handle_quoted_literal(
3,745✔
40
        &mut self,
41
        ch: char,
42
        chars: &mut core::iter::Peekable<core::str::Chars>,
43
        result: &mut Vec<PatternItem>,
44
    ) -> Result<bool, PatternError> {
45
        if ch == '\'' {
3,837✔
46
            match (&mut self.state, chars.peek() == Some(&'\'')) {
92✔
47
                (
48
                    Segment::Literal {
49
                        ref mut literal, ..
10✔
50
                    },
51
                    true,
52
                ) => {
53
                    literal.push('\'');
10✔
54
                    chars.next();
10✔
55
                }
56
                (Segment::Literal { ref mut quoted, .. }, false) => {
77✔
57
                    *quoted = !*quoted;
77✔
58
                }
77✔
59
                (Segment::Symbol { symbol, length }, true) => {
4✔
60
                    result.push((*symbol, *length).try_into()?);
4✔
61
                    self.state = Segment::Literal {
4✔
62
                        literal: String::from(ch),
4✔
63
                        quoted: false,
64
                    };
65
                    chars.next();
4✔
66
                }
67
                (Segment::Symbol { symbol, length }, false) => {
1✔
68
                    result.push((*symbol, *length).try_into()?);
1✔
69
                    self.state = Segment::Literal {
1✔
70
                        literal: String::new(),
1✔
71
                        quoted: true,
72
                    };
73
                }
1✔
74
            }
75
            Ok(true)
92✔
76
        } else if let Segment::Literal {
3,653✔
77
            ref mut literal,
142✔
78
            quoted: true,
79
        } = self.state
80
        {
81
            literal.push(ch);
142✔
82
            Ok(true)
142✔
83
        } else {
84
            Ok(false)
3,511✔
85
        }
86
    }
3,745✔
87

88
    fn handle_generic_quoted_literal(
182✔
89
        &mut self,
90
        ch: char,
91
        chars: &mut core::iter::Peekable<core::str::Chars>,
92
    ) -> Result<bool, PatternError> {
93
        if ch == '\'' {
219✔
94
            match (&mut self.state, chars.peek() == Some(&'\'')) {
37✔
95
                (
96
                    Segment::Literal {
97
                        ref mut literal, ..
8✔
98
                    },
99
                    true,
100
                ) => {
101
                    literal.push('\'');
8✔
102
                    chars.next();
8✔
103
                }
104
                (Segment::Literal { ref mut quoted, .. }, false) => {
29✔
105
                    *quoted = !*quoted;
29✔
106
                }
29✔
107
                _ => unreachable!("Generic pattern has no symbols."),
×
108
            }
109
            Ok(true)
37✔
110
        } else if let Segment::Literal {
145✔
111
            ref mut literal,
38✔
112
            quoted: true,
113
        } = self.state
114
        {
115
            literal.push(ch);
38✔
116
            Ok(true)
38✔
117
        } else {
118
            Ok(false)
107✔
119
        }
120
    }
182✔
121

122
    fn collect_segment(state: Segment, result: &mut Vec<PatternItem>) -> Result<(), PatternError> {
1,914✔
123
        match state {
1,914✔
124
            Segment::Symbol { symbol, length } => {
691✔
125
                result.push((symbol, length).try_into()?);
691✔
126
            }
127
            Segment::Literal { quoted, .. } if quoted => {
1,223✔
128
                return Err(PatternError::UnclosedLiteral);
2✔
129
            }
130
            Segment::Literal { literal, .. } => {
1,221✔
131
                result.extend(literal.chars().map(PatternItem::from));
1,221✔
132
            }
1,221✔
133
        }
134
        Ok(())
1,909✔
135
    }
1,914✔
136

137
    fn collect_generic_segment(
103✔
138
        state: Segment,
139
        result: &mut Vec<GenericPatternItem>,
140
    ) -> Result<(), PatternError> {
141
        match state {
103✔
142
            Segment::Literal { quoted, .. } if quoted => {
103✔
143
                return Err(PatternError::UnclosedLiteral);
1✔
144
            }
145
            Segment::Literal { literal, .. } => {
102✔
146
                if !literal.is_empty() {
102✔
147
                    result.extend(literal.chars().map(GenericPatternItem::from))
29✔
148
                }
149
            }
102✔
150
            _ => unreachable!("Generic pattern has no symbols."),
×
151
        }
152
        Ok(())
102✔
153
    }
103✔
154

155
    pub fn parse(mut self) -> Result<Vec<PatternItem>, PatternError> {
704✔
156
        let mut chars = self.source.chars().peekable();
704✔
157
        let mut result = vec![];
704✔
158

159
        while let Some(ch) = chars.next() {
4,449✔
160
            if !self.handle_quoted_literal(ch, &mut chars, &mut result)? {
3,745✔
161
                if let Ok(new_symbol) = FieldSymbol::try_from(ch) {
3,511✔
162
                    match self.state {
2,861✔
163
                        Segment::Symbol {
164
                            ref symbol,
1,658✔
165
                            ref mut length,
1,658✔
166
                        } if new_symbol == *symbol => {
1,658✔
167
                            *length += 1;
1,651✔
168
                        }
1,651✔
169
                        segment => {
1,210✔
170
                            Self::collect_segment(segment, &mut result)?;
1,210✔
171
                            self.state = Segment::Symbol {
1,210✔
172
                                symbol: new_symbol,
1,210✔
173
                                length: 1,
174
                            };
175
                        }
1,210✔
176
                    }
177
                } else {
178
                    match self.state {
650✔
179
                        Segment::Symbol { symbol, length } => {
514✔
180
                            result.push((symbol, length).try_into()?);
514✔
181
                            self.state = Segment::Literal {
514✔
182
                                literal: String::from(ch),
514✔
183
                                quoted: false,
184
                            };
185
                        }
514✔
186
                        Segment::Literal {
187
                            ref mut literal, ..
136✔
188
                        } => literal.push(ch),
136✔
189
                    }
190
                }
191
            }
192
        }
193

194
        Self::collect_segment(self.state, &mut result)?;
704✔
195

196
        Ok(result)
699✔
197
    }
704✔
198

199
    pub fn parse_generic(mut self) -> Result<Vec<GenericPatternItem>, PatternError> {
53✔
200
        let mut chars = self.source.chars().peekable();
53✔
201
        let mut result = vec![];
53✔
202

203
        while let Some(ch) = chars.next() {
230✔
204
            if !self.handle_generic_quoted_literal(ch, &mut chars)? {
182✔
205
                if ch == '{' {
157✔
206
                    Self::collect_generic_segment(self.state, &mut result)?;
55✔
207

208
                    let ch = chars.next().ok_or(PatternError::UnclosedPlaceholder)?;
55✔
209
                    let idx = ch
54✔
210
                        .to_digit(10)
211
                        .ok_or(PatternError::UnknownSubstitution(ch))?
55✔
212
                        as u8;
213
                    result.push(GenericPatternItem::Placeholder(idx));
53✔
214
                    let ch = chars.next().ok_or(PatternError::UnclosedPlaceholder)?;
53✔
215
                    if ch != '}' {
52✔
216
                        return Err(PatternError::UnclosedPlaceholder);
2✔
217
                    }
218
                    self.state = Segment::Literal {
50✔
219
                        literal: String::new(),
50✔
220
                        quoted: false,
221
                    };
222
                } else if let Segment::Literal {
52✔
223
                    ref mut literal, ..
52✔
224
                } = self.state
225
                {
226
                    literal.push(ch);
52✔
227
                } else {
228
                    unreachable!()
×
229
                }
230
            }
231
        }
232

233
        Self::collect_generic_segment(self.state, &mut result)?;
48✔
234

235
        Ok(result)
47✔
236
    }
53✔
237

238
    pub fn parse_placeholders(
25✔
239
        self,
240
        replacements: Vec<Pattern>,
241
    ) -> Result<Vec<PatternItem>, PatternError> {
242
        let generic_items = self.parse_generic()?;
25✔
243

244
        let gp = GenericPattern::from(generic_items);
19✔
245
        Ok(gp.combined(replacements)?.items.to_vec())
19✔
246
    }
25✔
247
}
248

249
#[cfg(test)]
250
mod tests {
251
    use super::*;
252
    use crate::fields::{self, FieldLength};
253
    use crate::pattern::reference::Pattern;
254

255
    #[test]
256
    fn pattern_parse_simple() {
2✔
257
        let samples = [
2✔
258
            (
1✔
259
                "dd/MM/y",
260
                vec![
2✔
261
                    (fields::Day::DayOfMonth.into(), FieldLength::TwoDigit).into(),
1✔
262
                    '/'.into(),
1✔
263
                    (fields::Month::Format.into(), FieldLength::TwoDigit).into(),
1✔
264
                    '/'.into(),
1✔
265
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
266
                ],
267
            ),
268
            (
1✔
269
                "HH:mm:ss",
270
                vec![
2✔
271
                    (fields::Hour::H23.into(), FieldLength::TwoDigit).into(),
1✔
272
                    ':'.into(),
1✔
273
                    (FieldSymbol::Minute, FieldLength::TwoDigit).into(),
1✔
274
                    ':'.into(),
1✔
275
                    (fields::Second::Second.into(), FieldLength::TwoDigit).into(),
1✔
276
                ],
277
            ),
278
            (
1✔
279
                "y年M月d日",
280
                vec![
2✔
281
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
282
                    '年'.into(),
1✔
283
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
284
                    '月'.into(),
1✔
285
                    (fields::Day::DayOfMonth.into(), FieldLength::One).into(),
1✔
286
                    '日'.into(),
1✔
287
                ],
288
            ),
289
            (
1✔
290
                "HH:mm:ss.SS",
291
                vec![
2✔
292
                    (fields::Hour::H23.into(), FieldLength::TwoDigit).into(),
1✔
293
                    ':'.into(),
1✔
294
                    (FieldSymbol::Minute, FieldLength::TwoDigit).into(),
1✔
295
                    ':'.into(),
1✔
296
                    (fields::Second::Second.into(), FieldLength::TwoDigit).into(),
1✔
297
                    '.'.into(),
1✔
298
                    (
1✔
299
                        fields::Second::FractionalSecond.into(),
1✔
300
                        FieldLength::Fixed(2),
1✔
301
                    )
302
                        .into(),
303
                ],
304
            ),
305
        ];
×
306

307
        for (string, items) in samples {
5✔
308
            assert_eq!(
4✔
309
                string.parse::<Pattern>().expect("Parsing pattern failed."),
4✔
310
                Pattern::from(items)
4✔
311
            );
312
        }
4✔
313
    }
2✔
314

315
    fn str2pis(input: &str) -> Vec<PatternItem> {
31✔
316
        input.chars().map(Into::into).collect()
31✔
317
    }
31✔
318

319
    #[test]
320
    fn pattern_parse_literals() {
2✔
321
        let samples = [
2✔
322
            ("", ""),
1✔
323
            (" ", " "),
1✔
324
            ("  ", "  "),
1✔
325
            (" żółć ", " żółć "),
1✔
326
            ("''", "'"),
1✔
327
            (" ''", " '"),
1✔
328
            (" '' ", " ' "),
1✔
329
            ("''''", "''"),
1✔
330
            (" '' '' ", " ' ' "),
1✔
331
            ("ż'ół'ć", "żółć"),
1✔
332
            ("ż'ó''ł'ć", "żó'łć"),
1✔
333
            (" 'Ymd' ", " Ymd "),
1✔
334
            ("الأسبوع", "الأسبوع"),
1✔
335
        ];
336

337
        for (string, pattern) in samples {
1✔
338
            assert_eq!(
13✔
339
                Parser::new(string)
13✔
340
                    .parse()
341
                    .expect("Parsing pattern failed."),
342
                str2pis(pattern),
13✔
343
            );
344

345
            assert_eq!(
13✔
346
                Parser::new(string)
13✔
347
                    .parse_placeholders(vec![])
26✔
348
                    .expect("Parsing pattern failed."),
349
                str2pis(pattern),
13✔
350
            );
351
        }
352

353
        let broken = [(" 'foo ", PatternError::UnclosedLiteral)];
1✔
354

355
        for (string, error) in broken {
1✔
356
            assert_eq!(Parser::new(string).parse(), Err(error),);
1✔
357
        }
358
    }
2✔
359

360
    #[test]
361
    fn pattern_parse_symbols() {
2✔
362
        let samples = [
1✔
363
            (
1✔
364
                "y",
365
                vec![(fields::Year::Calendar.into(), FieldLength::One).into()],
1✔
366
            ),
367
            (
1✔
368
                "yy",
369
                vec![(fields::Year::Calendar.into(), FieldLength::TwoDigit).into()],
1✔
370
            ),
371
            (
1✔
372
                "yyy",
373
                vec![(fields::Year::Calendar.into(), FieldLength::Abbreviated).into()],
1✔
374
            ),
375
            (
1✔
376
                "yyyy",
377
                vec![(fields::Year::Calendar.into(), FieldLength::Wide).into()],
1✔
378
            ),
379
            (
1✔
380
                "yyyyy",
381
                vec![(fields::Year::Calendar.into(), FieldLength::Narrow).into()],
1✔
382
            ),
383
            (
1✔
384
                "yyyyyy",
385
                vec![(fields::Year::Calendar.into(), FieldLength::Six).into()],
1✔
386
            ),
387
            (
1✔
388
                "yM",
389
                vec![
2✔
390
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
391
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
392
                ],
393
            ),
394
            (
1✔
395
                "y ",
396
                vec![
2✔
397
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
398
                    ' '.into(),
1✔
399
                ],
400
            ),
401
            (
1✔
402
                "y M",
403
                vec![
2✔
404
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
405
                    ' '.into(),
1✔
406
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
407
                ],
408
            ),
409
            (
1✔
410
                "hh''a",
411
                vec![
2✔
412
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
413
                    '\''.into(),
1✔
414
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
415
                ],
416
            ),
417
            (
1✔
418
                "hh''b",
419
                vec![
2✔
420
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
421
                    '\''.into(),
1✔
422
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
423
                ],
424
            ),
425
            (
1✔
426
                "y'My'M",
427
                vec![
2✔
428
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
429
                    'M'.into(),
1✔
430
                    'y'.into(),
1✔
431
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
432
                ],
433
            ),
434
            (
1✔
435
                "y 'My' M",
436
                vec![
2✔
437
                    (fields::Year::Calendar.into(), FieldLength::One).into(),
1✔
438
                    ' '.into(),
1✔
439
                    'M'.into(),
1✔
440
                    'y'.into(),
1✔
441
                    ' '.into(),
1✔
442
                    (fields::Month::Format.into(), FieldLength::One).into(),
1✔
443
                ],
444
            ),
445
            (
1✔
446
                " 'r'. 'y'. ",
447
                vec![
2✔
448
                    ' '.into(),
1✔
449
                    'r'.into(),
1✔
450
                    '.'.into(),
1✔
451
                    ' '.into(),
1✔
452
                    'y'.into(),
1✔
453
                    '.'.into(),
1✔
454
                    ' '.into(),
1✔
455
                ],
456
            ),
457
            (
1✔
458
                "hh 'o''clock' a",
459
                vec![
2✔
460
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
461
                    ' '.into(),
1✔
462
                    'o'.into(),
1✔
463
                    '\''.into(),
1✔
464
                    'c'.into(),
1✔
465
                    'l'.into(),
1✔
466
                    'o'.into(),
1✔
467
                    'c'.into(),
1✔
468
                    'k'.into(),
1✔
469
                    ' '.into(),
1✔
470
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
471
                ],
472
            ),
473
            (
1✔
474
                "hh 'o''clock' b",
475
                vec![
2✔
476
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
477
                    ' '.into(),
1✔
478
                    'o'.into(),
1✔
479
                    '\''.into(),
1✔
480
                    'c'.into(),
1✔
481
                    'l'.into(),
1✔
482
                    'o'.into(),
1✔
483
                    'c'.into(),
1✔
484
                    'k'.into(),
1✔
485
                    ' '.into(),
1✔
486
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
487
                ],
488
            ),
489
            (
1✔
490
                "hh''a",
491
                vec![
2✔
492
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
493
                    '\''.into(),
1✔
494
                    (fields::DayPeriod::AmPm.into(), FieldLength::One).into(),
1✔
495
                ],
496
            ),
497
            (
1✔
498
                "hh''b",
499
                vec![
2✔
500
                    (fields::Hour::H12.into(), FieldLength::TwoDigit).into(),
1✔
501
                    '\''.into(),
1✔
502
                    (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(),
1✔
503
                ],
504
            ),
505
            (
1✔
506
                "z",
507
                vec![(fields::TimeZone::LowerZ.into(), FieldLength::One).into()],
1✔
508
            ),
509
            (
1✔
510
                "Z",
511
                vec![(fields::TimeZone::UpperZ.into(), FieldLength::One).into()],
1✔
512
            ),
513
            (
1✔
514
                "O",
515
                vec![(fields::TimeZone::UpperO.into(), FieldLength::One).into()],
1✔
516
            ),
517
            (
1✔
518
                "v",
519
                vec![(fields::TimeZone::LowerV.into(), FieldLength::One).into()],
1✔
520
            ),
521
            (
1✔
522
                "V",
523
                vec![(fields::TimeZone::UpperV.into(), FieldLength::One).into()],
1✔
524
            ),
525
            (
1✔
526
                "x",
527
                vec![(fields::TimeZone::LowerX.into(), FieldLength::One).into()],
1✔
528
            ),
529
            (
1✔
530
                "X",
531
                vec![(fields::TimeZone::UpperX.into(), FieldLength::One).into()],
1✔
532
            ),
533
        ];
×
534

535
        for (string, pattern) in samples {
26✔
536
            assert_eq!(
25✔
537
                Parser::new(string)
25✔
538
                    .parse()
539
                    .expect("Parsing pattern failed."),
540
                pattern,
541
            );
542
        }
25✔
543

544
        let broken = [
1✔
545
            (
1✔
546
                "yyyyyyy",
547
                PatternError::FieldLengthInvalid(FieldSymbol::Year(fields::Year::Calendar)),
1✔
548
            ),
549
            (
1✔
550
                "hh:mm:ss.SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS",
551
                PatternError::FieldLengthInvalid(FieldSymbol::Second(fields::Second::FractionalSecond)),
1✔
552
            ),
553
        ];
554

555
        for (string, error) in broken {
1✔
556
            assert_eq!(Parser::new(string).parse(), Err(error),);
2✔
557
        }
558
    }
2✔
559

560
    #[test]
561
    fn pattern_parse_placeholders() {
2✔
562
        let samples = [
2✔
563
            ("{0}", vec![Pattern::from("ONE")], str2pis("ONE")),
1✔
564
            (
1✔
565
                "{0}{1}",
566
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
567
                str2pis("ONETWO"),
1✔
568
            ),
×
569
            (
1✔
570
                "{0} 'at' {1}",
571
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
572
                str2pis("ONE at TWO"),
1✔
573
            ),
×
574
            (
1✔
575
                "{0}'at'{1}",
576
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
577
                str2pis("ONEatTWO"),
1✔
578
            ),
×
579
            (
1✔
580
                "'{0}' 'at' '{1}'",
581
                vec![Pattern::from("ONE"), Pattern::from("TWO")],
1✔
582
                str2pis("{0} at {1}"),
1✔
583
            ),
×
584
        ];
×
585

586
        for (string, replacements, pattern) in samples {
6✔
587
            assert_eq!(
5✔
588
                Parser::new(string)
5✔
589
                    .parse_placeholders(replacements)
5✔
590
                    .expect("Parsing pattern failed."),
591
                pattern,
592
            );
593
        }
5✔
594

595
        let broken = [
1✔
596
            ("{0}", vec![], PatternError::UnknownSubstitution('0')),
1✔
597
            ("{a}", vec![], PatternError::UnknownSubstitution('a')),
1✔
598
            ("{", vec![], PatternError::UnclosedPlaceholder),
1✔
599
            (
1✔
600
                "{0",
601
                vec![Pattern::from(vec![])],
1✔
602
                PatternError::UnclosedPlaceholder,
1✔
603
            ),
604
            (
1✔
605
                "{01",
606
                vec![Pattern::from(vec![])],
1✔
607
                PatternError::UnclosedPlaceholder,
1✔
608
            ),
609
            (
1✔
610
                "{00}",
611
                vec![Pattern::from(vec![])],
1✔
612
                PatternError::UnclosedPlaceholder,
1✔
613
            ),
614
            (
1✔
615
                "'{00}",
616
                vec![Pattern::from(vec![])],
1✔
617
                PatternError::UnclosedLiteral,
1✔
618
            ),
619
        ];
×
620

621
        for (string, replacements, error) in broken {
8✔
622
            assert_eq!(
7✔
623
                Parser::new(string).parse_placeholders(replacements),
7✔
624
                Err(error),
7✔
625
            );
626
        }
7✔
627
    }
2✔
628
}
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