progress: show multiple downloads as a bar table
lineMany crammed every download onto one line as name+percent, dropping the speed column that answers "which one is stalled" and producing ugly double brackets for names like "[Gecko]". render N>1 as a redrawn block: one row each (name, bar, percent, DL speed, ETA/stalled), a header with the active count and totals, falling back to a single summary line when the table would overflow the window. one download keeps its dashboard.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package progress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -112,6 +113,100 @@ func TestLineOneCNSD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBarOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s download.Stat
|
||||
want string
|
||||
}{
|
||||
{"empty", download.Stat{Status: download.Active, Total: 100, Completed: 0}, "|------------|"},
|
||||
{"half", download.Stat{Status: download.Active, Total: 100, Completed: 50}, "|######------|"},
|
||||
{"full by complete", download.Stat{Status: download.Complete}, "|############|"},
|
||||
{"full by seeding", download.Stat{Status: download.Seeding}, "|############|"},
|
||||
{"unknown total is empty", download.Stat{Status: download.Active, Total: -1, Completed: 9}, "|------------|"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := barOf(tc.s); got != tc.want {
|
||||
t.Errorf("%s: barOf = %q, want %q", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPctField(t *testing.T) {
|
||||
if got := pctField(download.Stat{Status: download.Active, Total: 100, Completed: 50}); got != " 50%" {
|
||||
t.Errorf("active 50%% = %q, want ' 50%%'", got)
|
||||
}
|
||||
if got := pctField(download.Stat{Status: download.Seeding}); got != "100%" {
|
||||
t.Errorf("seeding = %q, want 100%%", got)
|
||||
}
|
||||
if got := pctField(download.Stat{Status: download.Active, Total: -1}); got != " --" {
|
||||
t.Errorf("unknown total = %q, want ' --'", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRowMetric: the two trailing fields per download state.
|
||||
func TestRowMetric(t *testing.T) {
|
||||
r := newReporter(true)
|
||||
if m, tail := r.rowMetric(download.Stat{Status: download.Active, Total: 100, Completed: 50}, 10, 0); !strings.HasPrefix(m, "DL:") || tail == "" {
|
||||
t.Errorf("active downloading: metric=%q tail=%q, want DL:… + an ETA", m, tail)
|
||||
}
|
||||
if m, tail := r.rowMetric(download.Stat{Status: download.Seeding}, 0, 128<<10); m != "seeding" || !strings.HasPrefix(tail, "UL:") {
|
||||
t.Errorf("seeding: metric=%q tail=%q, want seeding + UL:…", m, tail)
|
||||
}
|
||||
if m, _ := r.rowMetric(download.Stat{Status: download.Complete}, 0, 0); m != "done" {
|
||||
t.Errorf("complete metric=%q, want done", m)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlockTableAndFallback: several downloads render as header+rows when they
|
||||
// fit, and collapse to a single summary line when the window is too short.
|
||||
func TestBlockTableAndFallback(t *testing.T) {
|
||||
r := newReporter(true)
|
||||
stats := []download.Stat{
|
||||
{ID: "a", Name: "a", Status: download.Active, Total: 100, Completed: 50},
|
||||
{ID: "b", Name: "b", Status: download.Active, Total: 100, Completed: 10},
|
||||
}
|
||||
r.height = 24
|
||||
if got := r.block(stats); len(got) != 3 { // header + 2 rows
|
||||
t.Errorf("table block = %d lines, want 3:\n%v", len(got), got)
|
||||
}
|
||||
r.height = 2 // header + 2 rows = 3 > 2
|
||||
if got := r.block(stats); len(got) != 1 {
|
||||
t.Errorf("overflow block = %d lines, want 1 summary:\n%v", len(got), got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderRedraw: the TTY path draws the block, then on the next frame moves
|
||||
// the cursor up over the previous frame and clears it before repainting.
|
||||
func TestRenderRedraw(t *testing.T) {
|
||||
snap := []download.Stat{
|
||||
{ID: "a", Name: "alpha", IsBT: true, Status: download.Active, Total: 100, Completed: 50},
|
||||
{ID: "b", Name: "beta", IsBT: true, Status: download.Seeding, Total: 100, Completed: 100, Uploaded: 200},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
r := &Reporter{
|
||||
snap: func() []download.Stat { return snap },
|
||||
human: true, w: &buf, tty: true, width: 200, height: 24,
|
||||
win: map[string]*speedWindow{},
|
||||
}
|
||||
r.render(false)
|
||||
first := buf.String()
|
||||
for _, want := range []string{"2 active", "alpha", "seeding", "|"} {
|
||||
if !strings.Contains(first, want) {
|
||||
t.Fatalf("first frame missing %q:\n%q", want, first)
|
||||
}
|
||||
}
|
||||
buf.Reset()
|
||||
r.render(false)
|
||||
second := buf.String()
|
||||
if !strings.Contains(second, "\x1b[2A") { // up prevLines-1 = 3-1
|
||||
t.Errorf("second frame should move cursor up 2 lines:\n%q", second)
|
||||
}
|
||||
if !strings.Contains(second, "\x1b[J") {
|
||||
t.Errorf("second frame should clear to end of screen:\n%q", second)
|
||||
}
|
||||
}
|
||||
|
||||
// TestShortNameRune ensures CJK names are truncated on runes, not bytes.
|
||||
func TestShortNameRune(t *testing.T) {
|
||||
name := strings.Repeat("あ", 30) // 30 runes, 90 bytes
|
||||
|
||||
Reference in New Issue
Block a user