gotest.tools/gotestsum@v1.11.0/testjson/dotformat.go (about)

     1  package testjson
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	"golang.org/x/term"
    13  	"gotest.tools/gotestsum/internal/dotwriter"
    14  	"gotest.tools/gotestsum/internal/log"
    15  )
    16  
    17  func dotsFormatV1(out io.Writer) EventFormatter {
    18  	buf := bufio.NewWriter(out)
    19  	// nolint:errcheck
    20  	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
    21  		pkg := exec.Package(event.Package)
    22  		switch {
    23  		case event.PackageEvent():
    24  			return nil
    25  		case event.Action == ActionRun && pkg.Total == 1:
    26  			buf.WriteString("[" + RelativePackagePath(event.Package) + "]")
    27  			return buf.Flush()
    28  		}
    29  		buf.WriteString(fmtDot(event))
    30  		return buf.Flush()
    31  	})
    32  }
    33  
    34  func fmtDot(event TestEvent) string {
    35  	withColor := colorEvent(event)
    36  	switch event.Action {
    37  	case ActionPass:
    38  		return withColor("·")
    39  	case ActionFail:
    40  		return withColor("✖")
    41  	case ActionSkip:
    42  		return withColor("↷")
    43  	}
    44  	return ""
    45  }
    46  
    47  type dotFormatter struct {
    48  	pkgs      map[string]*dotLine
    49  	order     []string
    50  	writer    *dotwriter.Writer
    51  	opts      FormatOptions
    52  	termWidth int
    53  }
    54  
    55  type dotLine struct {
    56  	runes      int
    57  	builder    *strings.Builder
    58  	lastUpdate time.Time
    59  }
    60  
    61  func (l *dotLine) update(dot string) {
    62  	if dot == "" {
    63  		return
    64  	}
    65  	l.builder.WriteString(dot)
    66  	l.runes++
    67  }
    68  
    69  // checkWidth marks the line as full when the width of the line hits the
    70  // terminal width.
    71  func (l *dotLine) checkWidth(prefix, terminal int) {
    72  	if prefix+l.runes >= terminal {
    73  		l.builder.WriteString("\n" + strings.Repeat(" ", prefix))
    74  		l.runes = 0
    75  	}
    76  }
    77  
    78  func newDotFormatter(out io.Writer, opts FormatOptions) EventFormatter {
    79  	w, _, err := term.GetSize(int(os.Stdout.Fd()))
    80  	if err != nil || w == 0 {
    81  		log.Warnf("Failed to detect terminal width for dots format, error: %v", err)
    82  		return dotsFormatV1(out)
    83  	}
    84  	return &dotFormatter{
    85  		pkgs:      make(map[string]*dotLine),
    86  		writer:    dotwriter.New(out),
    87  		termWidth: w,
    88  		opts:      opts,
    89  	}
    90  }
    91  
    92  func (d *dotFormatter) Format(event TestEvent, exec *Execution) error {
    93  	if d.pkgs[event.Package] == nil {
    94  		d.pkgs[event.Package] = &dotLine{builder: new(strings.Builder)}
    95  		d.order = append(d.order, event.Package)
    96  	}
    97  	line := d.pkgs[event.Package]
    98  	line.lastUpdate = event.Time
    99  
   100  	if !event.PackageEvent() {
   101  		line.update(fmtDot(event))
   102  	}
   103  	switch event.Action {
   104  	case ActionOutput, ActionBench:
   105  		return nil
   106  	}
   107  
   108  	// Add an empty header to work around incorrect line counting
   109  	fmt.Fprint(d.writer, "\n\n")
   110  
   111  	sort.Slice(d.order, d.orderByLastUpdated)
   112  	for _, pkg := range d.order {
   113  		if d.opts.HideEmptyPackages && exec.Package(pkg).IsEmpty() {
   114  			continue
   115  		}
   116  
   117  		line := d.pkgs[pkg]
   118  		pkgname := RelativePackagePath(pkg) + " "
   119  		prefix := fmtDotElapsed(exec.Package(pkg))
   120  		line.checkWidth(len(prefix+pkgname), d.termWidth)
   121  		fmt.Fprintf(d.writer, prefix+pkgname+line.builder.String()+"\n")
   122  	}
   123  	PrintSummary(d.writer, exec, SummarizeNone)
   124  	return d.writer.Flush()
   125  }
   126  
   127  // orderByLastUpdated so that the most recently updated packages move to the
   128  // bottom of the list, leaving completed package in the same order at the top.
   129  func (d *dotFormatter) orderByLastUpdated(i, j int) bool {
   130  	return d.pkgs[d.order[i]].lastUpdate.Before(d.pkgs[d.order[j]].lastUpdate)
   131  }
   132  
   133  func fmtDotElapsed(p *Package) string {
   134  	f := func(v string) string {
   135  		return fmt.Sprintf(" %5s ", v)
   136  	}
   137  
   138  	elapsed := p.Elapsed()
   139  	switch {
   140  	case p.cached:
   141  		return f("🖴 ")
   142  	case elapsed <= 0:
   143  		return f("")
   144  	case elapsed >= time.Hour:
   145  		return f("⏳ ")
   146  	case elapsed < time.Second:
   147  		return f(elapsed.String())
   148  	}
   149  
   150  	const maxWidth = 7
   151  	var steps = []time.Duration{
   152  		time.Millisecond,
   153  		10 * time.Millisecond,
   154  		100 * time.Millisecond,
   155  		time.Second,
   156  		10 * time.Second,
   157  		time.Minute,
   158  		10 * time.Minute,
   159  	}
   160  
   161  	for _, trunc := range steps {
   162  		r := f(elapsed.Truncate(trunc).String())
   163  		if len(r) <= maxWidth {
   164  			return r
   165  		}
   166  	}
   167  	return f("")
   168  }