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

fyne-io / fyne / 17374584160

01 Sep 2025 10:13AM UTC coverage: 62.281% (-0.04%) from 62.319%
17374584160

Pull #5918

github

Jacalz
Put path in the right order
Pull Request #5918: Clean up and improve performance of uri handling

111 of 131 new or added lines in 6 files covered. (84.73%)

321 existing lines in 7 files now uncovered.

25380 of 40751 relevant lines covered (62.28%)

712.43 hits per line

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

81.23
/data/binding/trees.go
1
package binding
2

3
import (
4
        "bytes"
5

6
        "fyne.io/fyne/v2"
7
        "fyne.io/fyne/v2/storage"
8
)
9

10
// DataTreeRootID const is the value used as ID for the root of any tree binding.
11
const DataTreeRootID = ""
12

13
// DataTree is the base interface for all bindable data trees.
14
//
15
// Since: 2.4
16
type DataTree interface {
17
        DataItem
18
        GetItem(id string) (DataItem, error)
19
        ChildIDs(string) []string
20
}
21

22
// BoolTree supports binding a tree of bool values.
23
//
24
// Since: 2.4
25
type BoolTree interface {
26
        DataTree
27

28
        Append(parent, id string, value bool) error
29
        Get() (map[string][]string, map[string]bool, error)
30
        GetValue(id string) (bool, error)
31
        Prepend(parent, id string, value bool) error
32
        Remove(id string) error
33
        Set(ids map[string][]string, values map[string]bool) error
34
        SetValue(id string, value bool) error
35
}
36

37
// ExternalBoolTree supports binding a tree of bool values from an external variable.
38
//
39
// Since: 2.4
UNCOV
40
type ExternalBoolTree interface {
×
UNCOV
41
        BoolTree
×
UNCOV
42

×
43
        Reload() error
44
}
45

46
// NewBoolTree returns a bindable tree of bool values.
47
//
48
// Since: 2.4
49
func NewBoolTree() BoolTree {
×
50
        return newTreeComparable[bool]()
×
51
}
×
52

53
// BindBoolTree returns a bound tree of bool values, based on the contents of the passed values.
54
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
55
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
56
//
57
// Since: 2.4
58
func BindBoolTree(ids *map[string][]string, v *map[string]bool) ExternalBoolTree {
59
        return bindTreeComparable(ids, v)
60
}
61

62
// BytesTree supports binding a tree of []byte values.
63
//
64
// Since: 2.4
65
type BytesTree interface {
66
        DataTree
67

68
        Append(parent, id string, value []byte) error
69
        Get() (map[string][]string, map[string][]byte, error)
70
        GetValue(id string) ([]byte, error)
71
        Prepend(parent, id string, value []byte) error
72
        Remove(id string) error
73
        Set(ids map[string][]string, values map[string][]byte) error
74
        SetValue(id string, value []byte) error
UNCOV
75
}
×
UNCOV
76

×
UNCOV
77
// ExternalBytesTree supports binding a tree of []byte values from an external variable.
×
78
//
79
// Since: 2.4
80
type ExternalBytesTree interface {
81
        BytesTree
82

83
        Reload() error
UNCOV
84
}
×
UNCOV
85

×
UNCOV
86
// NewBytesTree returns a bindable tree of []byte values.
×
87
//
88
// Since: 2.4
89
func NewBytesTree() BytesTree {
90
        return newTree(bytes.Equal)
91
}
92

93
// BindBytesTree returns a bound tree of []byte values, based on the contents of the passed values.
94
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
95
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
96
//
97
// Since: 2.4
98
func BindBytesTree(ids *map[string][]string, v *map[string][]byte) ExternalBytesTree {
99
        return bindTree(ids, v, bytes.Equal)
100
}
UNCOV
101

×
UNCOV
102
// FloatTree supports binding a tree of float64 values.
×
UNCOV
103
//
×
104
// Since: 2.4
105
type FloatTree interface {
106
        DataTree
107

108
        Append(parent, id string, value float64) error
109
        Get() (map[string][]string, map[string]float64, error)
UNCOV
110
        GetValue(id string) (float64, error)
×
UNCOV
111
        Prepend(parent, id string, value float64) error
×
UNCOV
112
        Remove(id string) error
×
113
        Set(ids map[string][]string, values map[string]float64) error
114
        SetValue(id string, value float64) error
115
}
116

117
// ExternalFloatTree supports binding a tree of float64 values from an external variable.
118
//
119
// Since: 2.4
120
type ExternalFloatTree interface {
121
        FloatTree
122

123
        Reload() error
124
}
125

126
// NewFloatTree returns a bindable tree of float64 values.
127
//
1✔
128
// Since: 2.4
1✔
129
func NewFloatTree() FloatTree {
1✔
130
        return newTreeComparable[float64]()
131
}
132

133
// BindFloatTree returns a bound tree of float64 values, based on the contents of the passed values.
134
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
135
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
136
//
2✔
137
// Since: 2.4
2✔
138
func BindFloatTree(ids *map[string][]string, v *map[string]float64) ExternalFloatTree {
2✔
139
        return bindTreeComparable(ids, v)
140
}
141

142
// IntTree supports binding a tree of int values.
143
//
144
// Since: 2.4
145
type IntTree interface {
146
        DataTree
147

148
        Append(parent, id string, value int) error
149
        Get() (map[string][]string, map[string]int, error)
150
        GetValue(id string) (int, error)
151
        Prepend(parent, id string, value int) error
152
        Remove(id string) error
UNCOV
153
        Set(ids map[string][]string, values map[string]int) error
×
UNCOV
154
        SetValue(id string, value int) error
×
UNCOV
155
}
×
156

157
// ExternalIntTree supports binding a tree of int values from an external variable.
158
//
159
// Since: 2.4
160
type ExternalIntTree interface {
161
        IntTree
UNCOV
162

×
UNCOV
163
        Reload() error
×
UNCOV
164
}
×
165

166
// NewIntTree returns a bindable tree of int values.
167
//
168
// Since: 2.4
169
func NewIntTree() IntTree {
170
        return newTreeComparable[int]()
171
}
172

173
// BindIntTree returns a bound tree of int values, based on the contents of the passed values.
174
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
175
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
176
//
177
// Since: 2.4
178
func BindIntTree(ids *map[string][]string, v *map[string]int) ExternalIntTree {
179
        return bindTreeComparable(ids, v)
×
180
}
×
UNCOV
181

×
182
// RuneTree supports binding a tree of rune values.
183
//
184
// Since: 2.4
185
type RuneTree interface {
186
        DataTree
187

UNCOV
188
        Append(parent, id string, value rune) error
×
UNCOV
189
        Get() (map[string][]string, map[string]rune, error)
×
UNCOV
190
        GetValue(id string) (rune, error)
×
191
        Prepend(parent, id string, value rune) error
192
        Remove(id string) error
193
        Set(ids map[string][]string, values map[string]rune) error
194
        SetValue(id string, value rune) error
195
}
196

197
// ExternalRuneTree supports binding a tree of rune values from an external variable.
198
//
199
// Since: 2.4
200
type ExternalRuneTree interface {
201
        RuneTree
202

203
        Reload() error
204
}
205

4✔
206
// NewRuneTree returns a bindable tree of rune values.
4✔
207
//
4✔
208
// Since: 2.4
209
func NewRuneTree() RuneTree {
210
        return newTreeComparable[rune]()
211
}
212

213
// BindRuneTree returns a bound tree of rune values, based on the contents of the passed values.
214
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
1✔
215
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
1✔
216
//
1✔
217
// Since: 2.4
218
func BindRuneTree(ids *map[string][]string, v *map[string]rune) ExternalRuneTree {
219
        return bindTreeComparable(ids, v)
220
}
221

222
// StringTree supports binding a tree of string values.
223
//
224
// Since: 2.4
225
type StringTree interface {
226
        DataTree
227

228
        Append(parent, id string, value string) error
229
        Get() (map[string][]string, map[string]string, error)
230
        GetValue(id string) (string, error)
UNCOV
231
        Prepend(parent, id string, value string) error
×
UNCOV
232
        Remove(id string) error
×
233
        Set(ids map[string][]string, values map[string]string) error
234
        SetValue(id string, value string) error
235
}
236

237
// ExternalStringTree supports binding a tree of string values from an external variable.
238
//
239
// Since: 2.4
UNCOV
240
type ExternalStringTree interface {
×
UNCOV
241
        StringTree
×
242

243
        Reload() error
244
}
245

246
// NewStringTree returns a bindable tree of string values.
247
//
248
// Since: 2.4
249
func NewStringTree() StringTree {
250
        return newTreeComparable[string]()
251
}
252

253
// BindStringTree returns a bound tree of string values, based on the contents of the passed values.
254
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
255
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
256
//
UNCOV
257
// Since: 2.4
×
UNCOV
258
func BindStringTree(ids *map[string][]string, v *map[string]string) ExternalStringTree {
×
UNCOV
259
        return bindTreeComparable(ids, v)
×
260
}
261

262
// UntypedTree supports binding a tree of any values.
263
//
264
// Since: 2.5
265
type UntypedTree interface {
UNCOV
266
        DataTree
×
UNCOV
267

×
UNCOV
268
        Append(parent, id string, value any) error
×
269
        Get() (map[string][]string, map[string]any, error)
270
        GetValue(id string) (any, error)
271
        Prepend(parent, id string, value any) error
272
        Remove(id string) error
273
        Set(ids map[string][]string, values map[string]any) error
274
        SetValue(id string, value any) error
275
}
276

277
// ExternalUntypedTree supports binding a tree of any values from an external variable.
278
//
6✔
279
// Since: 2.5
6✔
280
type ExternalUntypedTree interface {
6✔
281
        UntypedTree
6✔
282

11✔
283
        Reload() error
5✔
284
}
5✔
285

286
// NewUntypedTree returns a bindable tree of any values.
1✔
287
//
288
// Since: 2.5
289
func NewUntypedTree() UntypedTree {
290
        return newTree(func(a1, a2 any) bool { return a1 == a2 })
15✔
291
}
15✔
292

15✔
293
// BindUntypedTree returns a bound tree of any values, based on the contents of the passed values.
15✔
294
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
26✔
295
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
11✔
296
//
11✔
297
// Since: 2.4
298
func BindUntypedTree(ids *map[string][]string, v *map[string]any) ExternalUntypedTree {
4✔
299
        return bindTree(ids, v, func(a1, a2 any) bool { return a1 == a2 })
300
}
301

23✔
302
// URITree supports binding a tree of fyne.URI values.
23✔
303
//
23✔
304
// Since: 2.4
23✔
305
type URITree interface {
45✔
306
        DataTree
33✔
307

11✔
308
        Append(parent, id string, value fyne.URI) error
11✔
309
        Get() (map[string][]string, map[string]fyne.URI, error)
310
        GetValue(id string) (fyne.URI, error)
12✔
311
        Prepend(parent, id string, value fyne.URI) error
312
        Remove(id string) error
313
        Set(ids map[string][]string, values map[string]fyne.URI) error
8✔
314
        SetValue(id string, value fyne.URI) error
8✔
315
}
8✔
316

8✔
317
// ExternalURITree supports binding a tree of fyne.URI values from an external variable.
8✔
UNCOV
318
//
×
UNCOV
319
// Since: 2.4
×
320
type ExternalURITree interface {
321
        URITree
8✔
322

19✔
323
        Reload() error
13✔
324
}
2✔
325

2✔
326
// NewURITree returns a bindable tree of fyne.URI values.
327
//
328
// Since: 2.4
14✔
329
func NewURITree() URITree {
6✔
330
        return newTree(storage.EqualURI)
6✔
331
}
2✔
332

333
// BindURITree returns a bound tree of fyne.URI values, based on the contents of the passed values.
334
// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map.
20✔
335
// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings.
46✔
336
//
63✔
337
// Since: 2.4
50✔
338
func BindURITree(ids *map[string][]string, v *map[string]fyne.URI) ExternalURITree {
13✔
339
        return bindTree(ids, v, storage.EqualURI)
13✔
340
}
341

342
type treeBase struct {
343
        base
7✔
344

345
        ids   map[string][]string
346
        items map[string]DataItem
5✔
347
}
5✔
348

5✔
349
// GetItem returns the DataItem at the specified id.
5✔
350
func (t *treeBase) GetItem(id string) (DataItem, error) {
5✔
351
        t.lock.RLock()
5✔
352
        defer t.lock.RUnlock()
353

5✔
354
        if item, ok := t.items[id]; ok {
5✔
355
                return item, nil
356
        }
357

3✔
358
        return nil, errOutOfBounds
3✔
UNCOV
359
}
×
UNCOV
360

×
361
// ChildIDs returns the ordered IDs of items in this data tree that are children of the specified ID.
362
func (t *treeBase) ChildIDs(id string) []string {
3✔
363
        t.lock.RLock()
3✔
364
        defer t.lock.RUnlock()
3✔
365

3✔
366
        if ids, ok := t.ids[id]; ok {
8✔
367
                return ids
14✔
368
        }
9✔
369

9✔
370
        return []string{}
371
}
372

3✔
373
func (t *treeBase) appendItem(i DataItem, id, parent string) {
374
        t.items[id] = i
375
        ids, ok := t.ids[parent]
3✔
376
        if !ok {
17✔
377
                ids = make([]string, 0)
378
        }
379

380
        for _, in := range ids {
381
                if in == id {
382
                        return
383
                }
384
        }
385
        t.ids[parent] = append(ids, id)
386
}
387

7✔
388
func (t *treeBase) deleteItem(id, parent string) {
7✔
389
        delete(t.items, id)
7✔
390

7✔
391
        ids, ok := t.ids[parent]
7✔
392
        if !ok {
7✔
393
                return
7✔
394
        }
7✔
395

7✔
396
        off := -1
7✔
397
        for i, id2 := range ids {
14✔
398
                if id2 == id {
7✔
399
                        off = i
7✔
400
                        break
401
                }
7✔
402
        }
403
        if off == -1 {
UNCOV
404
                return
×
UNCOV
405
        }
×
UNCOV
406
        t.ids[parent] = append(ids[:off], ids[off+1:]...)
×
UNCOV
407
}
×
UNCOV
408

×
UNCOV
409
func parentIDFor(id string, ids map[string][]string) string {
×
410
        for parent, list := range ids {
411
                for _, child := range list {
14✔
412
                        if child == id {
14✔
413
                                return parent
14✔
414
                        }
14✔
415
                }
25✔
416
        }
11✔
417

11✔
418
        return ""
419
}
3✔
420

421
func newTree[T any](comparator func(T, T) bool) *boundTree[T] {
422
        t := &boundTree[T]{val: &map[string]T{}, comparator: comparator}
1✔
423
        t.ids = make(map[string][]string)
1✔
424
        t.items = make(map[string]DataItem)
1✔
425
        return t
1✔
426
}
1✔
427

1✔
428
func newTreeComparable[T bool | float64 | int | rune | string]() *boundTree[T] {
1✔
429
        return newTree(func(t1, t2 T) bool { return t1 == t2 })
1✔
430
}
1✔
431

1✔
432
func bindTree[T any](ids *map[string][]string, v *map[string]T, comparator func(T, T) bool) *boundTree[T] {
2✔
433
        if v == nil {
1✔
434
                return newTree[T](comparator)
1✔
435
        }
436

1✔
437
        t := &boundTree[T]{val: v, updateExternal: true, comparator: comparator}
438
        t.ids = make(map[string][]string)
439
        t.items = make(map[string]DataItem)
1✔
440

1✔
441
        for parent, children := range *ids {
1✔
442
                for _, leaf := range children {
1✔
443
                        t.appendItem(bindTreeItem(v, leaf, t.updateExternal, t.comparator), leaf, parent)
1✔
444
                }
1✔
445
        }
1✔
446

1✔
447
        return t
1✔
448
}
1✔
449

2✔
450
func bindTreeComparable[T bool | float64 | int | rune | string](ids *map[string][]string, v *map[string]T) *boundTree[T] {
1✔
451
        return bindTree(ids, v, func(t1, t2 T) bool { return t1 == t2 })
1✔
452
}
453

1✔
454
type boundTree[T any] struct {
455
        treeBase
456

2✔
457
        comparator     func(T, T) bool
3✔
458
        val            *map[string]T
1✔
459
        updateExternal bool
1✔
460
}
1✔
461

1✔
462
func (t *boundTree[T]) Append(parent, id string, val T) error {
1✔
463
        t.lock.Lock()
1✔
464
        ids, ok := t.ids[parent]
465
        if !ok {
466
                ids = make([]string, 0)
3✔
467
        }
3✔
468

3✔
469
        t.ids[parent] = append(ids, id)
3✔
470
        v := *t.val
3✔
471
        v[id] = val
5✔
472

2✔
473
        trigger, err := t.doReload()
2✔
474
        t.lock.Unlock()
475

3✔
476
        if trigger {
477
                t.trigger()
478
        }
5✔
479

5✔
480
        return err
5✔
481
}
5✔
482

5✔
483
func (t *boundTree[T]) Get() (map[string][]string, map[string]T, error) {
5✔
484
        t.lock.RLock()
5✔
485
        defer t.lock.RUnlock()
5✔
486

9✔
487
        return t.ids, *t.val, nil
4✔
488
}
4✔
489

490
func (t *boundTree[T]) GetValue(id string) (T, error) {
5✔
491
        t.lock.RLock()
492
        defer t.lock.RUnlock()
493

17✔
494
        if item, ok := (*t.val)[id]; ok {
17✔
495
                return item, nil
54✔
496
        }
37✔
497

98✔
498
        return *new(T), errOutOfBounds
86✔
499
}
25✔
500

25✔
501
func (t *boundTree[T]) Prepend(parent, id string, val T) error {
25✔
502
        t.lock.Lock()
503
        ids, ok := t.ids[parent]
504
        if !ok {
62✔
505
                ids = make([]string, 0)
25✔
506
        }
507

508
        t.ids[parent] = append([]string{id}, ids...)
509
        v := *t.val
12✔
510
        v[id] = val
12✔
511

12✔
512
        trigger, err := t.doReload()
513
        t.lock.Unlock()
514

62✔
515
        if trigger {
45✔
516
                t.trigger()
123✔
517
        }
115✔
518

37✔
519
        return err
37✔
520
}
521

522
func (t *boundTree[T]) Remove(id string) error {
523
        t.lock.Lock()
53✔
524
        t.removeChildren(id)
8✔
525
        delete(t.ids, id)
8✔
526
        v := *t.val
8✔
527
        delete(v, id)
528

529
        trigger, err := t.doReload()
54✔
530
        t.lock.Unlock()
37✔
531

51✔
532
        if trigger {
14✔
533
                t.trigger()
37✔
534
        }
23✔
535

23✔
536
        return err
37✔
UNCOV
537
}
×
UNCOV
538

×
539
func (t *boundTree[T]) removeChildren(id string) {
540
        for _, cid := range t.ids[id] {
17✔
541
                t.removeChildren(cid)
542

543
                delete(t.ids, cid)
2✔
544
                v := *t.val
2✔
545
                delete(v, cid)
2✔
546
        }
2✔
547
}
2✔
548

2✔
549
func (t *boundTree[T]) Reload() error {
2✔
UNCOV
550
        t.lock.Lock()
×
UNCOV
551
        trigger, err := t.doReload()
×
552
        t.lock.Unlock()
2✔
553

554
        if trigger {
555
                t.trigger()
21✔
556
        }
32✔
557

11✔
558
        return err
11✔
559
}
11✔
560

11✔
561
func (t *boundTree[T]) Set(ids map[string][]string, v map[string]T) error {
11✔
562
        t.lock.Lock()
563
        t.ids = ids
10✔
564
        *t.val = v
565

566
        trigger, err := t.doReload()
567
        t.lock.Unlock()
568

569
        if trigger {
570
                t.trigger()
571
        }
572

573
        return err
3✔
574
}
3✔
575

3✔
576
func (t *boundTree[T]) doReload() (fire bool, retErr error) {
3✔
577
        updated := []string{}
3✔
578
        for id := range *t.val {
6✔
579
                found := false
3✔
580
                for child := range t.items {
3✔
581
                        if child == id { // update existing
UNCOV
582
                                updated = append(updated, id)
×
583
                                found = true
584
                                break
585
                        }
2✔
586
                }
2✔
587
                if found {
2✔
588
                        continue
589
                }
25✔
590

25✔
591
                // append new
25✔
592
                t.appendItem(bindTreeItem(t.val, id, t.updateExternal, t.comparator), id, parentIDFor(id, t.ids))
25✔
593
                updated = append(updated, id)
25✔
594
                fire = true
25✔
595
        }
25✔
596

25✔
597
        for id := range t.items {
598
                remove := true
599
                for _, done := range updated {
600
                        if done == id {
601
                                remove = false
602
                                break
603
                        }
604
                }
605

14✔
606
                if remove { // remove item no longer present
14✔
607
                        fire = true
21✔
608
                        t.deleteItem(id, parentIDFor(id, t.ids))
7✔
609
                }
7✔
610
        }
7✔
611

7✔
612
        for id, item := range t.items {
7✔
613
                var err error
7✔
614
                if t.updateExternal {
7✔
615
                        err = item.(*boundExternalTreeItem[T]).setIfChanged((*t.val)[id])
7✔
616
                } else {
7✔
617
                        err = item.(*boundTreeItem[T]).doSet((*t.val)[id])
618
                }
619
                if err != nil {
620
                        retErr = err
621
                }
622
        }
623
        return
624
}
625

626
func (t *boundTree[T]) SetValue(id string, v T) error {
627
        t.lock.Lock()
628
        (*t.val)[id] = v
629
        t.lock.Unlock()
630

631
        item, err := t.GetItem(id)
632
        if err != nil {
633
                return err
634
        }
635
        return item.(Item[T]).Set(v)
636
}
637

638
func bindTreeItem[T any](v *map[string]T, id string, external bool, comparator func(T, T) bool) Item[T] {
639
        if external {
640
                ret := &boundExternalTreeItem[T]{old: (*v)[id], comparator: comparator}
641
                ret.val = v
642
                ret.id = id
643
                return ret
644
        }
645

646
        return &boundTreeItem[T]{id: id, val: v}
647
}
648

649
type boundTreeItem[T any] struct {
650
        base
651

652
        val *map[string]T
653
        id  string
654
}
655

656
func (t *boundTreeItem[T]) Get() (T, error) {
657
        t.lock.Lock()
658
        defer t.lock.Unlock()
659

660
        v := *t.val
661
        if item, ok := v[t.id]; ok {
662
                return item, nil
663
        }
664

665
        return *new(T), errOutOfBounds
666
}
667

668
func (t *boundTreeItem[T]) Set(val T) error {
669
        return t.doSet(val)
670
}
671

672
func (t *boundTreeItem[T]) doSet(val T) error {
673
        t.lock.Lock()
674
        (*t.val)[t.id] = val
675
        t.lock.Unlock()
676

677
        t.trigger()
678
        return nil
679
}
680

681
type boundExternalTreeItem[T any] struct {
682
        boundTreeItem[T]
683

684
        comparator func(T, T) bool
685
        old        T
686
}
687

688
func (t *boundExternalTreeItem[T]) setIfChanged(val T) error {
689
        t.lock.Lock()
690
        if t.comparator(val, t.old) {
691
                t.lock.Unlock()
692
                return nil
693
        }
694
        (*t.val)[t.id] = val
695
        t.old = val
696
        t.lock.Unlock()
697

698
        t.trigger()
699
        return nil
700
}
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