From 5990573ccdb8e5c967c41514b7f0f6a064cca136 Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
Date: Mon, 14 Jan 2019 16:12:39 +0000
Subject: [PATCH] accounting: fix layout of stats - fixes #2910

This fixes several things wrong with the layout of the stats.

Transfers which haven't started are printed in the same format as
those which have so the stats with `--progress` don't show horrible
artifacts.

Checkers and transfers now get a ": checkers" and ": transfers" label
on the end of the stats line.  Transfers will have the transfer stats
when the transfer has started instead of this.

There was a bug in the routine which shortened the file names (it
always produces strings 1 too long).  This is now fixed with a test.

The formatting string was wrong with a fixed width of 45 - this is now
replaces with the value of `--stats-file-name-length`.

This also meant that there were unecessary leading spaces in the file
names.  So the default `--stats-file-name-length` was raised to 45
from 40.
---
 fs/accounting/accounting.go      | 39 ++++++++++++++++-----------
 fs/accounting/accounting_test.go | 45 ++++++++++++++++++++++++++++++++
 fs/accounting/stats.go           |  4 +--
 fs/accounting/stringset.go       | 13 +++++++--
 fs/config.go                     |  2 +-
 5 files changed, 83 insertions(+), 20 deletions(-)

diff --git a/fs/accounting/accounting.go b/fs/accounting/accounting.go
index ef12c286e..adad7f280 100644
--- a/fs/accounting/accounting.go
+++ b/fs/accounting/accounting.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"sync"
 	"time"
+	"unicode/utf8"
 
 	"github.com/ncw/rclone/fs"
 	"github.com/ncw/rclone/fs/asyncreader"
@@ -243,6 +244,24 @@ func (acc *Account) eta() (etaDuration time.Duration, ok bool) {
 	return eta(acc.bytes, acc.size, acc.avg)
 }
 
+// shortenName shortens in to size runes long
+// If size <= 0 then in is left untouched
+func shortenName(in string, size int) string {
+	if size <= 0 {
+		return in
+	}
+	if utf8.RuneCountInString(in) <= size {
+		return in
+	}
+	name := []rune(in)
+	size-- // don't count elipsis rune
+	suffixLength := size / 2
+	prefixLength := size - suffixLength
+	suffixStart := len(name) - suffixLength
+	name = append(append(name[:prefixLength], '…'), name[suffixStart:]...)
+	return string(name)
+}
+
 // String produces stats for this file
 func (acc *Account) String() string {
 	a, b := acc.progress()
@@ -257,16 +276,6 @@ func (acc *Account) String() string {
 		}
 	}
 
-	name := []rune(acc.name)
-	if fs.Config.StatsFileNameLength > 0 {
-		if len(name) > fs.Config.StatsFileNameLength {
-			suffixLength := fs.Config.StatsFileNameLength / 2
-			prefixLength := fs.Config.StatsFileNameLength - suffixLength
-			suffixStart := len(name) - suffixLength
-			name = append(append(name[:prefixLength], '…'), name[suffixStart:]...)
-		}
-	}
-
 	if fs.Config.DataRateUnit == "bits" {
 		cur = cur * 8
 	}
@@ -276,11 +285,11 @@ func (acc *Account) String() string {
 		percentageDone = int(100 * float64(a) / float64(b))
 	}
 
-	done := fmt.Sprintf("%2d%% /%s", percentageDone, fs.SizeSuffix(b))
-
-	return fmt.Sprintf("%45s: %s, %s/s, %s",
-		string(name),
-		done,
+	return fmt.Sprintf("%*s:%3d%% /%s, %s/s, %s",
+		fs.Config.StatsFileNameLength,
+		shortenName(acc.name, fs.Config.StatsFileNameLength),
+		percentageDone,
+		fs.SizeSuffix(b),
 		fs.SizeSuffix(cur),
 		etas,
 	)
diff --git a/fs/accounting/accounting_test.go b/fs/accounting/accounting_test.go
index b197a4a7d..0767745f7 100644
--- a/fs/accounting/accounting_test.go
+++ b/fs/accounting/accounting_test.go
@@ -2,10 +2,12 @@ package accounting
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"strings"
 	"testing"
+	"unicode/utf8"
 
 	"github.com/ncw/rclone/fs"
 	"github.com/ncw/rclone/fs/asyncreader"
@@ -208,3 +210,46 @@ func TestAccountMaxTransfer(t *testing.T) {
 	assert.Equal(t, ErrorMaxTransferLimitReached, err)
 	assert.True(t, fserrors.IsFatalError(err))
 }
+
+func TestShortenName(t *testing.T) {
+	for _, test := range []struct {
+		in   string
+		size int
+		want string
+	}{
+		{"", 0, ""},
+		{"abcde", 10, "abcde"},
+		{"abcde", 0, "abcde"},
+		{"abcde", -1, "abcde"},
+		{"abcde", 5, "abcde"},
+		{"abcde", 4, "ab…e"},
+		{"abcde", 3, "a…e"},
+		{"abcde", 2, "a…"},
+		{"abcde", 1, "…"},
+		{"abcdef", 6, "abcdef"},
+		{"abcdef", 5, "ab…ef"},
+		{"abcdef", 4, "ab…f"},
+		{"abcdef", 3, "a…f"},
+		{"abcdef", 2, "a…"},
+		{"áßcdèf", 1, "…"},
+		{"áßcdè", 5, "áßcdè"},
+		{"áßcdè", 4, "áß…è"},
+		{"áßcdè", 3, "á…è"},
+		{"áßcdè", 2, "á…"},
+		{"áßcdè", 1, "…"},
+		{"áßcdèł", 6, "áßcdèł"},
+		{"áßcdèł", 5, "áß…èł"},
+		{"áßcdèł", 4, "áß…ł"},
+		{"áßcdèł", 3, "á…ł"},
+		{"áßcdèł", 2, "á…"},
+		{"áßcdèł", 1, "…"},
+	} {
+		t.Run(fmt.Sprintf("in=%q, size=%d", test.in, test.size), func(t *testing.T) {
+			got := shortenName(test.in, test.size)
+			assert.Equal(t, test.want, got)
+			if test.size > 0 {
+				assert.True(t, utf8.RuneCountInString(got) <= test.size, "too big")
+			}
+		})
+	}
+}
diff --git a/fs/accounting/stats.go b/fs/accounting/stats.go
index 54c8597d4..56010fbeb 100644
--- a/fs/accounting/stats.go
+++ b/fs/accounting/stats.go
@@ -92,8 +92,8 @@ type StatsInfo struct {
 // NewStats cretates an initialised StatsInfo
 func NewStats() *StatsInfo {
 	return &StatsInfo{
-		checking:     newStringSet(fs.Config.Checkers),
-		transferring: newStringSet(fs.Config.Transfers),
+		checking:     newStringSet(fs.Config.Checkers, "checking"),
+		transferring: newStringSet(fs.Config.Transfers, "transferring"),
 		start:        time.Now(),
 		inProgress:   newInProgress(),
 	}
diff --git a/fs/accounting/stringset.go b/fs/accounting/stringset.go
index e65be6011..71744035d 100644
--- a/fs/accounting/stringset.go
+++ b/fs/accounting/stringset.go
@@ -1,21 +1,26 @@
 package accounting
 
 import (
+	"fmt"
 	"sort"
 	"strings"
 	"sync"
+
+	"github.com/ncw/rclone/fs"
 )
 
 // stringSet holds a set of strings
 type stringSet struct {
 	mu    sync.RWMutex
 	items map[string]struct{}
+	name  string
 }
 
 // newStringSet creates a new empty string set of capacity size
-func newStringSet(size int) *stringSet {
+func newStringSet(size int, name string) *stringSet {
 	return &stringSet{
 		items: make(map[string]struct{}, size),
+		name:  name,
 	}
 }
 
@@ -57,7 +62,11 @@ func (ss *stringSet) Strings() []string {
 		if acc := Stats.inProgress.get(name); acc != nil {
 			out = acc.String()
 		} else {
-			out = name
+			out = fmt.Sprintf("%*s: %s",
+				fs.Config.StatsFileNameLength,
+				shortenName(name, fs.Config.StatsFileNameLength),
+				ss.name,
+			)
 		}
 		strings = append(strings, " * "+out)
 	}
diff --git a/fs/config.go b/fs/config.go
index 3499a9fec..53e6835d1 100644
--- a/fs/config.go
+++ b/fs/config.go
@@ -110,7 +110,7 @@ func NewConfig() *ConfigInfo {
 	c.BufferSize = SizeSuffix(16 << 20)
 	c.UserAgent = "rclone/" + Version
 	c.StreamingUploadCutoff = SizeSuffix(100 * 1024)
-	c.StatsFileNameLength = 40
+	c.StatsFileNameLength = 45
 	c.AskPassword = true
 	c.TPSLimitBurst = 1
 	c.MaxTransfer = -1