File: dotformat.go

package info (click to toggle)
gotestsum 1.8.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,060 kB
  • sloc: sh: 89; makefile: 16
file content (155 lines) | stat: -rw-r--r-- 3,506 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package testjson

import (
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"time"

	"golang.org/x/term"
	"gotest.tools/gotestsum/internal/dotwriter"
	"gotest.tools/gotestsum/internal/log"
)

func dotsFormatV1(event TestEvent, exec *Execution) string {
	pkg := exec.Package(event.Package)
	switch {
	case event.PackageEvent():
		return ""
	case event.Action == ActionRun && pkg.Total == 1:
		return "[" + RelativePackagePath(event.Package) + "]"
	}
	return fmtDot(event)
}

func fmtDot(event TestEvent) string {
	withColor := colorEvent(event)
	switch event.Action {
	case ActionPass:
		return withColor("·")
	case ActionFail:
		return withColor("✖")
	case ActionSkip:
		return withColor("↷")
	}
	return ""
}

type dotFormatter struct {
	pkgs      map[string]*dotLine
	order     []string
	writer    *dotwriter.Writer
	termWidth int
}

type dotLine struct {
	runes      int
	builder    *strings.Builder
	lastUpdate time.Time
}

func (l *dotLine) update(dot string) {
	if dot == "" {
		return
	}
	l.builder.WriteString(dot)
	l.runes++
}

// checkWidth marks the line as full when the width of the line hits the
// terminal width.
func (l *dotLine) checkWidth(prefix, terminal int) {
	if prefix+l.runes >= terminal {
		l.builder.WriteString("\n" + strings.Repeat(" ", prefix))
		l.runes = 0
	}
}

func newDotFormatter(out io.Writer) EventFormatter {
	w, _, err := term.GetSize(int(os.Stdout.Fd()))
	if err != nil || w == 0 {
		log.Warnf("Failed to detect terminal width for dots format, error: %v", err)
		return &formatAdapter{format: dotsFormatV1, out: out}
	}
	return &dotFormatter{
		pkgs:      make(map[string]*dotLine),
		writer:    dotwriter.New(out),
		termWidth: w,
	}
}

func (d *dotFormatter) Format(event TestEvent, exec *Execution) error {
	if d.pkgs[event.Package] == nil {
		d.pkgs[event.Package] = &dotLine{builder: new(strings.Builder)}
		d.order = append(d.order, event.Package)
	}
	line := d.pkgs[event.Package]
	line.lastUpdate = event.Time

	if !event.PackageEvent() {
		line.update(fmtDot(event))
	}
	switch event.Action {
	case ActionOutput, ActionBench:
		return nil
	}

	// Add an empty header to work around incorrect line counting
	fmt.Fprint(d.writer, "\n\n")

	sort.Slice(d.order, d.orderByLastUpdated)
	for _, pkg := range d.order {
		line := d.pkgs[pkg]
		pkgname := RelativePackagePath(pkg) + " "
		prefix := fmtDotElapsed(exec.Package(pkg))
		line.checkWidth(len(prefix+pkgname), d.termWidth)
		fmt.Fprintf(d.writer, prefix+pkgname+line.builder.String()+"\n")
	}
	PrintSummary(d.writer, exec, SummarizeNone)
	return d.writer.Flush()
}

// orderByLastUpdated so that the most recently updated packages move to the
// bottom of the list, leaving completed package in the same order at the top.
func (d *dotFormatter) orderByLastUpdated(i, j int) bool {
	return d.pkgs[d.order[i]].lastUpdate.Before(d.pkgs[d.order[j]].lastUpdate)
}

func fmtDotElapsed(p *Package) string {
	f := func(v string) string {
		return fmt.Sprintf(" %5s ", v)
	}

	elapsed := p.Elapsed()
	switch {
	case p.cached:
		return f("🖴 ")
	case elapsed <= 0:
		return f("")
	case elapsed >= time.Hour:
		return f("⏳ ")
	case elapsed < time.Second:
		return f(elapsed.String())
	}

	const maxWidth = 7
	var steps = []time.Duration{
		time.Millisecond,
		10 * time.Millisecond,
		100 * time.Millisecond,
		time.Second,
		10 * time.Second,
		time.Minute,
		10 * time.Minute,
	}

	for _, trunc := range steps {
		r := f(elapsed.Truncate(trunc).String())
		if len(r) <= maxWidth {
			return r
		}
	}
	return f("")
}