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

tensorchord / envd / 13723197293

07 Mar 2025 02:47PM UTC coverage: 42.527% (+0.6%) from 41.895%
13723197293

push

github

web-flow
feat: support uv (#1990)

* feat: support uv

Signed-off-by: Keming <kemingyang@tensorchord.ai>

* fix ci

Signed-off-by: Keming <kemingyang@tensorchord.ai>

---------

Signed-off-by: Keming <kemingyang@tensorchord.ai>

44 of 52 new or added lines in 6 files covered. (84.62%)

13 existing lines in 2 files now uncovered.

5113 of 12023 relevant lines covered (42.53%)

158.62 hits per line

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

39.36
/pkg/progress/progressui/display.go
1
// Copyright 2023 The envd Authors
2
// Copyright 2023 The buildkit Authors
3
//
4
// Licensed under the Apache License, Version 2.0 (the "License");
5
// you may not use this file except in compliance with the License.
6
// You may obtain a copy of the License at
7
//
8
//      http://www.apache.org/licenses/LICENSE-2.0
9
//
10
// Unless required by applicable law or agreed to in writing, software
11
// distributed under the License is distributed on an "AS IS" BASIS,
12
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
// See the License for the specific language governing permissions and
14
// limitations under the License.
15

16
package progressui
17

18
import (
19
        "bytes"
20
        "container/ring"
21
        "context"
22
        "fmt"
23
        "io"
24
        "os"
25
        "sort"
26
        "strconv"
27
        "strings"
28
        "time"
29

30
        "github.com/containerd/console"
31
        "github.com/moby/buildkit/client"
32
        "github.com/morikuni/aec"
33
        "github.com/opencontainers/go-digest"
34
        "github.com/tonistiigi/units"
35
        "github.com/tonistiigi/vt100"
36
        "golang.org/x/time/rate"
37
)
38

39
func DisplaySolveStatus(ctx context.Context, phase string, c console.Console, w io.Writer, ch chan *client.SolveStatus) ([]client.VertexWarning, error) {
27✔
40
        modeConsole := c != nil
27✔
41

27✔
42
        disp := &display{c: c, phase: phase}
27✔
43
        printer := &textMux{w: w}
27✔
44

27✔
45
        if disp.phase == "" {
27✔
46
                disp.phase = "Building"
×
47
        }
×
48

49
        t := newTrace(w, modeConsole)
27✔
50

27✔
51
        tickerTimeout := 150 * time.Millisecond
27✔
52
        displayTimeout := 100 * time.Millisecond
27✔
53

27✔
54
        if v := os.Getenv("TTY_DISPLAY_RATE"); v != "" {
27✔
55
                if r, err := strconv.ParseInt(v, 10, 64); err == nil {
×
56
                        tickerTimeout = time.Duration(r) * time.Millisecond
×
57
                        displayTimeout = time.Duration(r) * time.Millisecond
×
58
                }
×
59
        }
60

61
        var done bool
27✔
62
        ticker := time.NewTicker(tickerTimeout)
27✔
63
        // implemented as closure because "ticker" can change
27✔
64
        defer func() {
54✔
65
                ticker.Stop()
27✔
66
        }()
27✔
67

68
        displayLimiter := rate.NewLimiter(rate.Every(displayTimeout), 1)
27✔
69

27✔
70
        var height int
27✔
71
        width, _ := disp.getSize()
27✔
72
        for {
6,732✔
73
                select {
6,705✔
74
                case <-ctx.Done():
×
75
                        return nil, ctx.Err()
×
76
                case <-ticker.C:
2,418✔
77
                case ss, ok := <-ch:
4,287✔
78
                        if ok {
8,547✔
79
                                t.update(ss, width)
4,260✔
80
                        } else {
4,287✔
81
                                done = true
27✔
82
                        }
27✔
83
                }
84

85
                if modeConsole {
6,705✔
86
                        width, height = disp.getSize()
×
87
                        if done {
×
88
                                disp.print(t.displayInfo(), width, height, true)
×
89
                                t.printErrorLogs(c)
×
90
                                return t.warnings(), nil
×
91
                        }
×
92
                        if displayLimiter.Allow() {
×
93
                                ticker.Stop()
×
94
                                ticker = time.NewTicker(tickerTimeout)
×
95
                                disp.print(t.displayInfo(), width, height, false)
×
96
                        }
×
97
                } else {
6,705✔
98
                        if done || displayLimiter.Allow() {
9,695✔
99
                                printer.print(t)
2,990✔
100
                                if done {
3,017✔
101
                                        t.printErrorLogs(w)
27✔
102
                                        return t.warnings(), nil
27✔
103
                                }
27✔
104
                                ticker.Stop()
2,963✔
105
                                ticker = time.NewTicker(tickerTimeout)
2,963✔
106
                        }
107
                }
108
        }
109
}
110

111
const termHeight = 6
112
const termPad = 10
113

114
type displayInfo struct {
115
        startTime      time.Time
116
        jobs           []*job
117
        countTotal     int
118
        countCompleted int
119
}
120

121
type job struct {
122
        intervals   []interval
123
        isCompleted bool
124
        name        string
125
        status      string
126
        hasError    bool
127
        isCanceled  bool
128
        vertex      *vertex
129
        showTerm    bool
130
}
131

132
type trace struct {
133
        w             io.Writer
134
        startTime     *time.Time
135
        localTimeDiff time.Duration
136
        vertices      []*vertex
137
        byDigest      map[digest.Digest]*vertex
138
        updates       map[digest.Digest]struct{}
139
        modeConsole   bool
140
        groups        map[string]*vertexGroup // group id -> group
141
}
142

143
type vertex struct {
144
        *client.Vertex
145

146
        statuses []*status
147
        byID     map[string]*status
148
        indent   string
149
        index    int
150

151
        logs          [][]byte
152
        logsPartial   bool
153
        logsOffset    int
154
        logsBuffer    *ring.Ring // stores last logs to print them on error
155
        prev          *client.Vertex
156
        events        []string
157
        lastBlockTime *time.Time
158
        count         int
159
        statusUpdates map[string]struct{}
160

161
        warnings   []client.VertexWarning
162
        warningIdx int
163

164
        jobs      []*job
165
        jobCached bool
166

167
        term      *vt100.VT100
168
        termBytes int
169
        termCount int
170

171
        // Interval start time in unix nano -> interval. Using a map ensures
172
        // that updates for the same interval overwrite their previous updates.
173
        intervals       map[int64]interval
174
        mergedIntervals []interval
175

176
        // whether the vertex should be hidden due to being in a progress group
177
        // that doesn't have any non-weak members that have started
178
        hidden bool
179
}
180

181
// nolint:unparam
182
func (v *vertex) update(c int) {
5,028✔
183
        if v.count == 0 {
5,937✔
184
                now := time.Now()
909✔
185
                v.lastBlockTime = &now
909✔
186
        }
909✔
187
        v.count += c
5,028✔
188
}
189

190
func (v *vertex) mostRecentInterval() *interval {
18,565✔
191
        if v.isStarted() {
37,130✔
192
                ival := v.mergedIntervals[len(v.mergedIntervals)-1]
18,565✔
193
                return &ival
18,565✔
194
        }
18,565✔
195
        return nil
×
196
}
197

198
func (v *vertex) isStarted() bool {
25,451✔
199
        return len(v.mergedIntervals) > 0
25,451✔
200
}
25,451✔
201

202
func (v *vertex) isCompleted() bool {
7,328✔
203
        if ival := v.mostRecentInterval(); ival != nil {
14,656✔
204
                return ival.stop != nil
7,328✔
205
        }
7,328✔
206
        return false
×
207
}
208

209
type vertexGroup struct {
210
        *vertex
211
        subVtxs map[digest.Digest]client.Vertex
212
}
213

214
func (vg *vertexGroup) refresh() (changed, newlyStarted, newlyRevealed bool) {
×
215
        newVtx := *vg.Vertex
×
216
        newVtx.Cached = true
×
217
        alreadyStarted := vg.isStarted()
×
218
        wasHidden := vg.hidden
×
219
        for _, subVtx := range vg.subVtxs {
×
220
                if subVtx.Started != nil {
×
221
                        newInterval := interval{
×
222
                                start: subVtx.Started,
×
223
                                stop:  subVtx.Completed,
×
224
                        }
×
225
                        prevInterval := vg.intervals[subVtx.Started.UnixNano()]
×
226
                        if !newInterval.isEqual(prevInterval) {
×
227
                                changed = true
×
228
                        }
×
229
                        if !alreadyStarted {
×
230
                                newlyStarted = true
×
231
                        }
×
232
                        vg.intervals[subVtx.Started.UnixNano()] = newInterval
×
233

×
234
                        if !subVtx.ProgressGroup.Weak {
×
235
                                vg.hidden = false
×
236
                        }
×
237
                }
238

239
                // Group is considered cached iff all subvtxs are cached
240
                newVtx.Cached = newVtx.Cached && subVtx.Cached
×
241

×
242
                // Group error is set to the first error found in subvtxs, if any
×
243
                if newVtx.Error == "" {
×
244
                        newVtx.Error = subVtx.Error
×
245
                } else {
×
246
                        vg.hidden = false
×
247
                }
×
248
        }
249

250
        if vg.Cached != newVtx.Cached {
×
251
                changed = true
×
252
        }
×
253
        if vg.Error != newVtx.Error {
×
254
                changed = true
×
255
        }
×
256
        vg.Vertex = &newVtx
×
257

×
258
        if !vg.hidden && wasHidden {
×
259
                changed = true
×
260
                newlyRevealed = true
×
261
        }
×
262

263
        var ivals []interval
×
264
        for _, ival := range vg.intervals {
×
265
                ivals = append(ivals, ival)
×
266
        }
×
267
        vg.mergedIntervals = mergeIntervals(ivals)
×
268

×
269
        return changed, newlyStarted, newlyRevealed
×
270
}
271

272
type interval struct {
273
        start *time.Time
274
        stop  *time.Time
275
}
276

277
func (ival interval) duration() time.Duration {
464✔
278
        if ival.start == nil {
464✔
279
                return 0
×
280
        }
×
281
        if ival.stop == nil {
464✔
282
                return time.Since(*ival.start)
×
283
        }
×
284
        return ival.stop.Sub(*ival.start)
464✔
285
}
286

287
func (ival interval) isEqual(other interval) (isEqual bool) {
×
288
        return equalTimes(ival.start, other.start) && equalTimes(ival.stop, other.stop)
×
289
}
×
290

291
func equalTimes(t1, t2 *time.Time) bool {
×
292
        if t2 == nil {
×
293
                return t1 == nil
×
294
        }
×
295
        if t1 == nil {
×
296
                return false
×
297
        }
×
298
        return t1.Equal(*t2)
×
299
}
300

301
// mergeIntervals takes a slice of (start, stop) pairs and returns a slice where
302
// any intervals that overlap in time are combined into a single interval. If an
303
// interval's stop time is nil, it is treated as positive infinity and consumes
304
// any intervals after it. Intervals with nil start times are ignored and not
305
// returned.
306
func mergeIntervals(intervals []interval) []interval {
1,717✔
307
        // remove any intervals that have not started
1,717✔
308
        var filtered []interval
1,717✔
309
        for _, interval := range intervals {
3,670✔
310
                if interval.start != nil {
3,903✔
311
                        filtered = append(filtered, interval)
1,950✔
312
                }
1,950✔
313
        }
314
        intervals = filtered
1,717✔
315

1,717✔
316
        if len(intervals) == 0 {
1,718✔
317
                return nil
1✔
318
        }
1✔
319

320
        // sort intervals by start time
321
        sort.Slice(intervals, func(i, j int) bool {
1,978✔
322
                return intervals[i].start.Before(*intervals[j].start)
262✔
323
        })
262✔
324

325
        var merged []interval
1,716✔
326
        cur := intervals[0]
1,716✔
327
        for i := 1; i < len(intervals); i++ {
1,950✔
328
                next := intervals[i]
234✔
329
                if cur.stop == nil {
235✔
330
                        // if cur doesn't stop, all intervals after it will be merged into it
1✔
331
                        merged = append(merged, cur)
1✔
332
                        return merged
1✔
333
                }
1✔
334
                if cur.stop.Before(*next.start) {
445✔
335
                        // if cur stops before next starts, no intervals after cur will be
212✔
336
                        // merged into it; cur stands on its own
212✔
337
                        merged = append(merged, cur)
212✔
338
                        cur = next
212✔
339
                        continue
212✔
340
                }
341
                if next.stop == nil {
22✔
342
                        // cur and next partially overlap, but next also never stops, so all
1✔
343
                        // subsequent intervals will be merged with both cur and next
1✔
344
                        merged = append(merged, interval{
1✔
345
                                start: cur.start,
1✔
346
                                stop:  nil,
1✔
347
                        })
1✔
348
                        return merged
1✔
349
                }
1✔
350
                if cur.stop.After(*next.stop) || cur.stop.Equal(*next.stop) {
30✔
351
                        // cur fully subsumes next
10✔
352
                        continue
10✔
353
                }
354
                // cur partially overlaps with next, merge them together into cur
355
                cur = interval{
10✔
356
                        start: cur.start,
10✔
357
                        stop:  next.stop,
10✔
358
                }
10✔
359
        }
360
        // append anything we are left with
361
        merged = append(merged, cur)
1,714✔
362
        return merged
1,714✔
363
}
364

365
type status struct {
366
        *client.VertexStatus
367
}
368

369
func newTrace(w io.Writer, modeConsole bool) *trace {
27✔
370
        return &trace{
27✔
371
                byDigest:    make(map[digest.Digest]*vertex),
27✔
372
                updates:     make(map[digest.Digest]struct{}),
27✔
373
                w:           w,
27✔
374
                modeConsole: modeConsole,
27✔
375
                groups:      make(map[string]*vertexGroup),
27✔
376
        }
27✔
377
}
27✔
378

379
func (t *trace) warnings() []client.VertexWarning {
27✔
380
        var out []client.VertexWarning
27✔
381
        for _, v := range t.vertices {
875✔
382
                out = append(out, v.warnings...)
848✔
383
        }
848✔
384
        return out
27✔
385
}
386

387
func (t *trace) triggerVertexEvent(v *client.Vertex) {
2,094✔
388
        if v.Started == nil {
2,896✔
389
                return
802✔
390
        }
802✔
391

392
        var old client.Vertex
1,292✔
393
        vtx := t.byDigest[v.Digest]
1,292✔
394
        if v := vtx.prev; v != nil {
1,736✔
395
                old = *v
444✔
396
        }
444✔
397

398
        changed := false
1,292✔
399
        if v.Digest != old.Digest {
2,140✔
400
                changed = true
848✔
401
        }
848✔
402
        if v.Name != old.Name {
2,140✔
403
                changed = true
848✔
404
        }
848✔
405
        if v.Started != old.Started {
2,584✔
406
                if v.Started != nil && old.Started == nil || !v.Started.Equal(*old.Started) {
2,198✔
407
                        changed = true
906✔
408
                }
906✔
409
        }
410
        if v.Completed != old.Completed && v.Completed != nil {
2,198✔
411
                changed = true
906✔
412
        }
906✔
413
        if v.Cached != old.Cached {
1,754✔
414
                changed = true
462✔
415
        }
462✔
416
        if v.Error != old.Error {
1,292✔
UNCOV
417
                changed = true
×
UNCOV
418
        }
×
419

420
        if changed {
2,584✔
421
                vtx.update(1)
1,292✔
422
                t.updates[v.Digest] = struct{}{}
1,292✔
423
        }
1,292✔
424

425
        t.byDigest[v.Digest].prev = v
1,292✔
426
}
427

428
func (t *trace) update(s *client.SolveStatus, termWidth int) {
4,260✔
429
        seenGroups := make(map[string]struct{})
4,260✔
430
        var groups []string
4,260✔
431
        for _, v := range s.Vertexes {
6,354✔
432
                if t.startTime == nil {
2,233✔
433
                        t.startTime = v.Started
139✔
434
                }
139✔
435
                if v.ProgressGroup != nil {
2,094✔
436
                        group, ok := t.groups[v.ProgressGroup.Id]
×
437
                        if !ok {
×
438
                                group = &vertexGroup{
×
439
                                        vertex: &vertex{
×
440
                                                Vertex: &client.Vertex{
×
441
                                                        Digest: digest.Digest(v.ProgressGroup.Id),
×
442
                                                        Name:   v.ProgressGroup.Name,
×
443
                                                },
×
444
                                                byID:          make(map[string]*status),
×
445
                                                statusUpdates: make(map[string]struct{}),
×
446
                                                intervals:     make(map[int64]interval),
×
447
                                                hidden:        true,
×
448
                                        },
×
449
                                        subVtxs: make(map[digest.Digest]client.Vertex),
×
450
                                }
×
451
                                if t.modeConsole {
×
452
                                        group.term = vt100.NewVT100(termHeight, termWidth-termPad)
×
453
                                }
×
454
                                t.groups[v.ProgressGroup.Id] = group
×
455
                                t.byDigest[group.Digest] = group.vertex
×
456
                        }
457
                        if _, ok := seenGroups[v.ProgressGroup.Id]; !ok {
×
458
                                groups = append(groups, v.ProgressGroup.Id)
×
459
                                seenGroups[v.ProgressGroup.Id] = struct{}{}
×
460
                        }
×
461
                        group.subVtxs[v.Digest] = *v
×
462
                        t.byDigest[v.Digest] = group.vertex
×
463
                        continue
×
464
                }
465
                prev, ok := t.byDigest[v.Digest]
2,094✔
466
                if !ok {
2,942✔
467
                        t.byDigest[v.Digest] = &vertex{
848✔
468
                                byID:          make(map[string]*status),
848✔
469
                                statusUpdates: make(map[string]struct{}),
848✔
470
                                intervals:     make(map[int64]interval),
848✔
471
                        }
848✔
472
                        if t.modeConsole {
848✔
473
                                t.byDigest[v.Digest].term = vt100.NewVT100(termHeight, termWidth-termPad)
×
474
                        }
×
475
                }
476
                t.triggerVertexEvent(v)
2,094✔
477
                if v.Started != nil && (prev == nil || !prev.isStarted()) {
2,942✔
478
                        if t.localTimeDiff == 0 {
873✔
479
                                t.localTimeDiff = time.Since(*v.Started)
25✔
480
                        }
25✔
481
                        t.vertices = append(t.vertices, t.byDigest[v.Digest])
848✔
482
                }
483
                // allow a duplicate initial vertex that shouldn't reset state
484
                if !(prev != nil && prev.isStarted() && v.Started == nil) {
4,188✔
485
                        t.byDigest[v.Digest].Vertex = v
2,094✔
486
                }
2,094✔
487
                if v.Started != nil {
3,386✔
488
                        t.byDigest[v.Digest].intervals[v.Started.UnixNano()] = interval{
1,292✔
489
                                start: v.Started,
1,292✔
490
                                stop:  v.Completed,
1,292✔
491
                        }
1,292✔
492
                        var ivals []interval
1,292✔
493
                        for _, ival := range t.byDigest[v.Digest].intervals {
2,738✔
494
                                ivals = append(ivals, ival)
1,446✔
495
                        }
1,446✔
496
                        t.byDigest[v.Digest].mergedIntervals = mergeIntervals(ivals)
1,292✔
497
                }
498
                t.byDigest[v.Digest].jobCached = false
2,094✔
499
        }
500
        for _, groupID := range groups {
4,260✔
501
                group := t.groups[groupID]
×
502
                changed, newlyStarted, newlyRevealed := group.refresh()
×
503
                if newlyStarted {
×
504
                        if t.localTimeDiff == 0 {
×
505
                                t.localTimeDiff = time.Since(*group.mergedIntervals[0].start)
×
506
                        }
×
507
                }
508
                if group.hidden {
×
509
                        continue
×
510
                }
511
                if newlyRevealed {
×
512
                        t.vertices = append(t.vertices, group.vertex)
×
513
                }
×
514
                if changed {
×
515
                        group.update(1)
×
516
                        t.updates[group.Digest] = struct{}{}
×
517
                }
×
518
                group.jobCached = false
×
519
        }
520
        for _, s := range s.Statuses {
4,901✔
521
                v, ok := t.byDigest[s.Vertex]
641✔
522
                if !ok {
641✔
523
                        continue // shouldn't happen
×
524
                }
525
                v.jobCached = false
641✔
526
                prev, ok := v.byID[s.ID]
641✔
527
                if !ok {
954✔
528
                        v.byID[s.ID] = &status{VertexStatus: s}
313✔
529
                }
313✔
530
                if s.Started != nil && (prev == nil || prev.Started == nil) {
954✔
531
                        v.statuses = append(v.statuses, v.byID[s.ID])
313✔
532
                }
313✔
533
                v.byID[s.ID].VertexStatus = s
641✔
534
                v.statusUpdates[s.ID] = struct{}{}
641✔
535
                t.updates[v.Digest] = struct{}{}
641✔
536
                v.update(1)
641✔
537
        }
538
        for _, w := range s.Warnings {
4,260✔
539
                v, ok := t.byDigest[w.Vertex]
×
540
                if !ok {
×
541
                        continue // shouldn't happen
×
542
                }
543
                v.warnings = append(v.warnings, *w)
×
544
                v.update(1)
×
545
        }
546
        for _, l := range s.Logs {
7,355✔
547
                v, ok := t.byDigest[l.Vertex]
3,095✔
548
                if !ok {
3,095✔
549
                        continue // shouldn't happen
×
550
                }
551
                v.jobCached = false
3,095✔
552
                if v.term != nil {
3,095✔
553
                        if v.term.Width != termWidth {
×
554
                                v.term.Resize(termHeight, termWidth-termPad)
×
555
                        }
×
556
                        v.termBytes += len(l.Data)
×
557
                        // nolint
×
558
                        v.term.Write(l.Data) // error unhandled on purpose. don't trust vt100
×
559
                }
560
                i := 0
3,095✔
561
                complete := split(l.Data, byte('\n'), func(dt []byte) {
12,380✔
562
                        if v.logsPartial && len(v.logs) != 0 && i == 0 {
10,419✔
563
                                v.logs[len(v.logs)-1] = append(v.logs[len(v.logs)-1], dt...)
1,134✔
564
                        } else {
9,285✔
565
                                ts := time.Duration(0)
8,151✔
566
                                if ival := v.mostRecentInterval(); ival != nil {
16,302✔
567
                                        ts = l.Timestamp.Sub(*ival.start)
8,151✔
568
                                }
8,151✔
569
                                prec := 1
8,151✔
570
                                sec := ts.Seconds()
8,151✔
571
                                if sec < 10 {
15,358✔
572
                                        prec = 3
7,207✔
573
                                } else if sec < 100 {
9,095✔
574
                                        prec = 2
944✔
575
                                }
944✔
576
                                v.logs = append(v.logs, []byte(fmt.Sprintf("#%d %s %s", v.index, fmt.Sprintf("%.[2]*[1]f", sec, prec), dt)))
8,151✔
577
                        }
578
                        i++
9,285✔
579
                })
580
                v.logsPartial = !complete
3,095✔
581
                t.updates[v.Digest] = struct{}{}
3,095✔
582
                v.update(1)
3,095✔
583
        }
584
}
585

586
func (t *trace) printErrorLogs(f io.Writer) {
27✔
587
        for _, v := range t.vertices {
875✔
588
                if v.Error != "" && !strings.HasSuffix(v.Error, context.Canceled.Error()) {
848✔
UNCOV
589
                        fmt.Fprintln(f, "------")
×
UNCOV
590
                        fmt.Fprintf(f, " > %s:\n", v.Name)
×
UNCOV
591
                        // tty keeps original logs
×
UNCOV
592
                        for _, l := range v.logs {
×
593
                                // nolint
×
594
                                f.Write(l)
×
595
                                fmt.Fprintln(f)
×
596
                        }
×
597
                        // printer keeps last logs buffer
UNCOV
598
                        if v.logsBuffer != nil {
×
599
                                for i := 0; i < v.logsBuffer.Len(); i++ {
×
600
                                        if v.logsBuffer.Value != nil {
×
601
                                                fmt.Fprintln(f, string(v.logsBuffer.Value.([]byte)))
×
602
                                        }
×
603
                                        v.logsBuffer = v.logsBuffer.Next()
×
604
                                }
605
                        }
UNCOV
606
                        fmt.Fprintln(f, "------")
×
607
                }
608
        }
609
}
610

611
func (t *trace) displayInfo() (d displayInfo) {
×
612
        d.startTime = time.Now()
×
613
        if t.startTime != nil {
×
614
                d.startTime = t.startTime.Add(t.localTimeDiff)
×
615
        }
×
616
        d.countTotal = len(t.byDigest)
×
617
        for _, v := range t.byDigest {
×
618
                if v.ProgressGroup != nil || v.hidden {
×
619
                        // don't count vtxs in a group, they are merged into a single vtx
×
620
                        d.countTotal--
×
621
                        continue
×
622
                }
623
                if v.isCompleted() {
×
624
                        d.countCompleted++
×
625
                }
×
626
        }
627

628
        for _, v := range t.vertices {
×
629
                if v.jobCached {
×
630
                        d.jobs = append(d.jobs, v.jobs...)
×
631
                        continue
×
632
                }
633
                var jobs []*job
×
634
                j := &job{
×
635
                        name:        strings.Replace(v.Name, "\t", " ", -1),
×
636
                        vertex:      v,
×
637
                        isCompleted: true,
×
638
                }
×
639
                for _, ival := range v.intervals {
×
640
                        j.intervals = append(j.intervals, interval{
×
641
                                start: addTime(ival.start, t.localTimeDiff),
×
642
                                stop:  addTime(ival.stop, t.localTimeDiff),
×
643
                        })
×
644
                        if ival.stop == nil {
×
645
                                j.isCompleted = false
×
646
                        }
×
647
                }
648
                j.intervals = mergeIntervals(j.intervals)
×
649
                if v.Error != "" {
×
650
                        if strings.HasSuffix(v.Error, context.Canceled.Error()) {
×
651
                                j.isCanceled = true
×
652
                                j.name = "CANCELED " + j.name
×
653
                        } else {
×
654
                                j.hasError = true
×
655
                                j.name = "ERROR " + j.name
×
656
                        }
×
657
                }
658
                if v.Cached {
×
659
                        j.name = "CACHED " + j.name
×
660
                }
×
661
                j.name = v.indent + j.name
×
662
                jobs = append(jobs, j)
×
663
                for _, s := range v.statuses {
×
664
                        j := &job{
×
665
                                intervals: []interval{{
×
666
                                        start: addTime(s.Started, t.localTimeDiff),
×
667
                                        stop:  addTime(s.Completed, t.localTimeDiff),
×
668
                                }},
×
669
                                isCompleted: s.Completed != nil,
×
670
                                name:        v.indent + "=> " + s.ID,
×
671
                        }
×
672
                        if s.Total != 0 {
×
673
                                j.status = fmt.Sprintf("%.2f / %.2f", units.Bytes(s.Current), units.Bytes(s.Total))
×
674
                        } else if s.Current != 0 {
×
675
                                j.status = fmt.Sprintf("%.2f", units.Bytes(s.Current))
×
676
                        }
×
677
                        jobs = append(jobs, j)
×
678
                }
679
                for _, w := range v.warnings {
×
680
                        msg := "WARN: " + string(w.Short)
×
681
                        var mostRecentInterval interval
×
682
                        if ival := v.mostRecentInterval(); ival != nil {
×
683
                                mostRecentInterval = *ival
×
684
                        }
×
685
                        j := &job{
×
686
                                intervals: []interval{{
×
687
                                        start: addTime(mostRecentInterval.start, t.localTimeDiff),
×
688
                                        stop:  addTime(mostRecentInterval.stop, t.localTimeDiff),
×
689
                                }},
×
690
                                name:       msg,
×
691
                                isCanceled: true,
×
692
                        }
×
693
                        jobs = append(jobs, j)
×
694
                }
695
                d.jobs = append(d.jobs, jobs...)
×
696
                v.jobs = jobs
×
697
                v.jobCached = true
×
698
        }
699

700
        return d
×
701
}
702

703
func split(dt []byte, sep byte, fn func([]byte)) bool {
3,095✔
704
        if len(dt) == 0 {
3,095✔
705
                return false
×
706
        }
×
707
        for {
14,341✔
708
                if len(dt) == 0 {
13,207✔
709
                        return true
1,961✔
710
                }
1,961✔
711
                idx := bytes.IndexByte(dt, sep)
9,285✔
712
                if idx == -1 {
10,419✔
713
                        fn(dt)
1,134✔
714
                        return false
1,134✔
715
                }
1,134✔
716
                fn(dt[:idx])
8,151✔
717
                dt = dt[idx+1:]
8,151✔
718
        }
719
}
720

721
func addTime(tm *time.Time, d time.Duration) *time.Time {
×
722
        if tm == nil {
×
723
                return nil
×
724
        }
×
725
        t := (*tm).Add(d)
×
726
        return &t
×
727
}
728

729
type display struct {
730
        c         console.Console
731
        phase     string
732
        lineCount int
733
        repeated  bool
734
}
735

736
func (disp *display) getSize() (int, int) {
27✔
737
        width := 80
27✔
738
        height := 10
27✔
739
        if disp.c != nil {
27✔
740
                size, err := disp.c.Size()
×
741
                if err == nil && size.Width > 0 && size.Height > 0 {
×
742
                        width = int(size.Width)
×
743
                        height = int(size.Height)
×
744
                }
×
745
        }
746
        return width, height
27✔
747
}
748

749
func setupTerminals(jobs []*job, height int, all bool) []*job {
×
750
        var candidates []*job
×
751
        numInUse := 0
×
752
        for _, j := range jobs {
×
753
                if j.vertex != nil && j.vertex.termBytes > 0 && !j.isCompleted {
×
754
                        candidates = append(candidates, j)
×
755
                }
×
756
                if !j.isCompleted {
×
757
                        numInUse++
×
758
                }
×
759
        }
760
        sort.Slice(candidates, func(i, j int) bool {
×
761
                idxI := candidates[i].vertex.termBytes + candidates[i].vertex.termCount*50
×
762
                idxJ := candidates[j].vertex.termBytes + candidates[j].vertex.termCount*50
×
763
                return idxI > idxJ
×
764
        })
×
765

766
        numFree := height - 2 - numInUse
×
767
        numToHide := 0
×
768
        termLimit := termHeight + 3
×
769

×
770
        for i := 0; numFree > termLimit && i < len(candidates); i++ {
×
771
                candidates[i].showTerm = true
×
772
                numToHide += candidates[i].vertex.term.UsedHeight()
×
773
                numFree -= termLimit
×
774
        }
×
775

776
        if !all {
×
777
                jobs = wrapHeight(jobs, height-2-numToHide)
×
778
        }
×
779

780
        return jobs
×
781
}
782

783
func (disp *display) print(d displayInfo, width, height int, all bool) {
×
784
        // this output is inspired by Buck
×
785
        d.jobs = setupTerminals(d.jobs, height, all)
×
786
        b := aec.EmptyBuilder
×
787
        for i := 0; i <= disp.lineCount; i++ {
×
788
                b = b.Up(1)
×
789
        }
×
790
        if !disp.repeated {
×
791
                b = b.Down(1)
×
792
        }
×
793
        disp.repeated = true
×
794
        fmt.Fprint(disp.c, b.Column(0).ANSI)
×
795

×
796
        statusStr := ""
×
797
        if d.countCompleted > 0 && d.countCompleted == d.countTotal && all {
×
798
                statusStr = "FINISHED"
×
799
        }
×
800

801
        fmt.Fprint(disp.c, aec.Hide)
×
802
        defer fmt.Fprint(disp.c, aec.Show)
×
803

×
804
        out := fmt.Sprintf("[+] %s %.1fs (%d/%d) %s", disp.phase, time.Since(d.startTime).Seconds(), d.countCompleted, d.countTotal, statusStr)
×
805
        out = align(out, "", width)
×
806
        fmt.Fprintln(disp.c, out)
×
807
        lineCount := 0
×
808
        for _, j := range d.jobs {
×
809
                if len(j.intervals) == 0 {
×
810
                        continue
×
811
                }
812
                var dt float64
×
813
                for _, ival := range j.intervals {
×
814
                        dt += ival.duration().Seconds()
×
815
                }
×
816
                if dt < 0.05 {
×
817
                        dt = 0
×
818
                }
×
819
                pfx := " => "
×
820
                timer := fmt.Sprintf(" %3.1fs\n", dt)
×
821
                status := j.status
×
822
                showStatus := false
×
823

×
824
                left := width - len(pfx) - len(timer) - 1
×
825
                if status != "" {
×
826
                        if left+len(status) > 20 {
×
827
                                showStatus = true
×
828
                                left -= len(status) + 1
×
829
                        }
×
830
                }
831
                if left < 12 { // too small screen to show progress
×
832
                        continue
×
833
                }
834
                name := j.name
×
835
                if len(name) > left {
×
836
                        name = name[:left]
×
837
                }
×
838

839
                out := pfx + name
×
840
                if showStatus {
×
841
                        out += " " + status
×
842
                }
×
843

844
                out = align(out, timer, width)
×
845
                if j.isCompleted {
×
846
                        color := colorRun
×
847
                        if j.isCanceled {
×
848
                                color = colorCancel
×
849
                        } else if j.hasError {
×
850
                                color = colorError
×
851
                        }
×
852
                        out = aec.Apply(out, color)
×
853
                }
854
                fmt.Fprint(disp.c, out)
×
855
                lineCount++
×
856
                if j.showTerm {
×
857
                        term := j.vertex.term
×
858
                        term.Resize(termHeight, width-termPad)
×
859
                        for _, l := range term.Content {
×
860
                                if !isEmpty(l) {
×
861
                                        out := aec.Apply(fmt.Sprintf(" => => # %s\n", string(l)), aec.Faint)
×
862
                                        fmt.Fprint(disp.c, out)
×
863
                                        lineCount++
×
864
                                }
×
865
                        }
866
                        j.vertex.termCount++
×
867
                        j.showTerm = false
×
868
                }
869
        }
870
        // override previous content
871
        if diff := disp.lineCount - lineCount; diff > 0 {
×
872
                for i := 0; i < diff; i++ {
×
873
                        fmt.Fprintln(disp.c, strings.Repeat(" ", width))
×
874
                }
×
875
                fmt.Fprint(disp.c, aec.EmptyBuilder.Up(uint(diff)).Column(0).ANSI)
×
876
        }
877
        disp.lineCount = lineCount
×
878
}
879

880
func isEmpty(l []rune) bool {
×
881
        for _, r := range l {
×
882
                if r != ' ' {
×
883
                        return false
×
884
                }
×
885
        }
886
        return true
×
887
}
888

889
func align(l, r string, w int) string {
×
890
        return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
×
891
}
×
892

893
func wrapHeight(j []*job, limit int) []*job {
×
894
        if limit < 0 {
×
895
                return nil
×
896
        }
×
897
        var wrapped []*job
×
898
        wrapped = append(wrapped, j...)
×
899
        if len(j) > limit {
×
900
                wrapped = wrapped[len(j)-limit:]
×
901

×
902
                // wrap things around if incomplete jobs were cut
×
903
                var invisible []*job
×
904
                for _, j := range j[:len(j)-limit] {
×
905
                        if !j.isCompleted {
×
906
                                invisible = append(invisible, j)
×
907
                        }
×
908
                }
909

910
                if l := len(invisible); l > 0 {
×
911
                        rewrapped := make([]*job, 0, len(wrapped))
×
912
                        for _, j := range wrapped {
×
913
                                if !j.isCompleted || l <= 0 {
×
914
                                        rewrapped = append(rewrapped, j)
×
915
                                }
×
916
                                l--
×
917
                        }
918
                        freespace := len(wrapped) - len(rewrapped)
×
919
                        wrapped = append(invisible[len(invisible)-freespace:], rewrapped...)
×
920
                }
921
        }
922
        return wrapped
×
923
}
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