diff --git a/changelog/unreleased/pull-4318 b/changelog/unreleased/pull-4318 new file mode 100644 index 000000000..198b972d3 --- /dev/null +++ b/changelog/unreleased/pull-4318 @@ -0,0 +1,8 @@ +Bugfix: Correctly clean up status bar output of the `backup` command + +Due to a regression in restic 0.15.2, the status bar of the `backup` command +could leave some output behind. This happened if filenames were printed that +are wider than the current terminal width. This has been fixed. + +https://github.com/restic/restic/issues/4319 +https://github.com/restic/restic/pull/4318 diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 46bfb2868..5b310ec80 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -334,6 +334,21 @@ func wideRune(s string) (wide bool, utfsize uint) { return wide, uint(size) } +func sanitizeLines(lines []string, width int) []string { + // Sanitize lines and truncate them if they're too long. + for i, line := range lines { + line = Quote(line) + if width > 0 { + line = Truncate(line, width-2) + } + if i < len(lines)-1 { // Last line gets no line break. + line += "\n" + } + lines[i] = line + } + return lines +} + // SetStatus updates the status lines. // The lines should not contain newlines; this method adds them. func (t *Terminal) SetStatus(lines []string) { @@ -352,16 +367,7 @@ func (t *Terminal) SetStatus(lines []string) { } } - // Sanitize lines and truncate them if they're too long. - for i, line := range lines { - line = Quote(line) - if width > 0 { - line = Truncate(line, width-2) - } - if i < len(lines)-1 { // Last line gets no line break. - lines[i] = line + "\n" - } - } + sanitizeLines(lines, width) select { case t.status <- status{lines: lines}: diff --git a/internal/ui/termstatus/status_test.go b/internal/ui/termstatus/status_test.go index 9f5e15cb1..b59063076 100644 --- a/internal/ui/termstatus/status_test.go +++ b/internal/ui/termstatus/status_test.go @@ -1,12 +1,54 @@ package termstatus import ( + "bytes" + "context" + "fmt" + "io" "strconv" "testing" rtest "github.com/restic/restic/internal/test" ) +func TestSetStatus(t *testing.T) { + var buf bytes.Buffer + term := New(&buf, io.Discard, false) + + term.canUpdateStatus = true + term.fd = ^uintptr(0) + term.clearCurrentLine = posixClearCurrentLine + term.moveCursorUp = posixMoveCursorUp + + ctx, cancel := context.WithCancel(context.Background()) + go term.Run(ctx) + + const ( + clear = posixControlClearLine + home = posixControlMoveCursorHome + up = posixControlMoveCursorUp + ) + + term.SetStatus([]string{"first"}) + exp := home + clear + "first" + home + + term.SetStatus([]string{"foo", "bar", "baz"}) + exp += home + clear + "foo\n" + home + clear + "bar\n" + + home + clear + "baz" + home + up + up + + term.SetStatus([]string{"quux", "needs\nquote"}) + exp += home + clear + "quux\n" + + home + clear + "\"needs\\nquote\"\n" + + home + clear + home + up + up // Third line implicit. + + cancel() + exp += home + clear + "\n" + home + clear + "\n" + + home + up + up // Status cleared. + + <-term.closed + rtest.Equals(t, exp, buf.String()) +} + func TestQuote(t *testing.T) { for _, c := range []struct { in string @@ -91,3 +133,23 @@ func BenchmarkTruncateUnicode(b *testing.B) { benchmarkTruncate(b, s, w-1) } + +func TestSanitizeLines(t *testing.T) { + var tests = []struct { + input []string + width int + output []string + }{ + {[]string{""}, 80, []string{""}}, + {[]string{"too long test line"}, 10, []string{"too long"}}, + {[]string{"too long test line", "text"}, 10, []string{"too long\n", "text"}}, + {[]string{"too long test line", "second long test line"}, 10, []string{"too long\n", "second l"}}, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s %d", test.input, test.width), func(t *testing.T) { + out := sanitizeLines(test.input, test.width) + rtest.Equals(t, test.output, out) + }) + } +}