Add64 checked currentNum > max only after already mutating currentNum and currentBytes, so a single call that overshoots max returned an error but left the bar stuck: currentNum frozen above max, percent stuck above 100%, and every later Add/Set call failing with the same error forever, since the top-of-function currentNum < max guard could never pass again. Check the bound before mutating state instead, so an overshooting call is rejected as a whole and leaves currentNum/currentBytes/currentPercent exactly as they were. The now-unreachable post-hoc check is removed. Add TestAddExceedsMaxDoesNotCorruptState covering the corrupted-state case and that the bar keeps accepting valid progress afterwards.
1373 lines
38 KiB
Go
1373 lines
38 KiB
Go
package progressbar
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/chengxilo/virtualterm"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
termWidth = func(w io.Writer) (int, error) {
|
|
return 0, os.ErrPermission
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func BenchmarkRender(b *testing.B) {
|
|
bar := NewOptions64(100000000,
|
|
OptionSetWriter(os.Stderr),
|
|
OptionShowIts(),
|
|
)
|
|
for i := 0; i < b.N; i++ {
|
|
bar.Add(1)
|
|
}
|
|
}
|
|
|
|
func ExampleProgressBar() {
|
|
bar := New(100)
|
|
bar.Add(10)
|
|
// Output:
|
|
// 10% |████ | [0s:0s]
|
|
}
|
|
|
|
func ExampleProgressBar_Set() {
|
|
bar := New(100)
|
|
bar.Set(10)
|
|
// Output:
|
|
// 10% |████ | [0s:0s]
|
|
}
|
|
|
|
func ExampleProgressBar_Set64() {
|
|
bar := New(100)
|
|
bar.Set64(10)
|
|
// Output:
|
|
// 10% |████ | [0s:0s]
|
|
}
|
|
|
|
func ExampleProgressBar_basic() {
|
|
bar := NewOptions(100, OptionSetWidth(10))
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
// Output:
|
|
// 10% |█ | [1s:9s]
|
|
}
|
|
|
|
func ExampleProgressBar_invisible() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionSetRenderBlankState(true), OptionSetVisibility(false))
|
|
bar.Reset()
|
|
fmt.Println("hello, world")
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
// Output:
|
|
// hello, world
|
|
}
|
|
|
|
func ExampleOptionThrottle() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionThrottle(100*time.Millisecond))
|
|
bar.Reset()
|
|
bar.Add(5)
|
|
time.Sleep(150 * time.Millisecond)
|
|
bar.Add(5)
|
|
bar.Add(10)
|
|
// Output:
|
|
// 10% |█ | [0s:1s]
|
|
}
|
|
|
|
func ExampleOptionClearOnFinish() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionClearOnFinish())
|
|
bar.Reset()
|
|
bar.Finish()
|
|
fmt.Println("Finished")
|
|
// Output:
|
|
// Finished
|
|
}
|
|
|
|
func TestSpinnerClearOnFinish(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1, OptionSetWidth(100), OptionShowCount(), OptionShowBytes(true), OptionShowIts(), OptionClearOnFinish(), OptionSetWriter(&buf))
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
time.Sleep(1 * time.Second)
|
|
bar.Finish()
|
|
result, _ := virtualterm.Process(buf.String())
|
|
expect := " "
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func ExampleProgressBar_Finish() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionShowCount(), OptionShowBytes(true), OptionShowIts())
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Finish()
|
|
// Output:
|
|
// 100% |██████████| (100/100 B, 100 B/s, 100 it/s)
|
|
}
|
|
|
|
func TestSpinnerFinish(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1, OptionSetWidth(100), OptionShowCount(), OptionShowBytes(true), OptionShowIts(), OptionSetWriter(&buf))
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
time.Sleep(1 * time.Second)
|
|
bar.Finish()
|
|
result, err := virtualterm.Process(buf.String())
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
// the "\r \r"
|
|
expect := "| (10 B, 5 B/s, 5 it/s) [2s] "
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func Example_xOutOfY() {
|
|
bar := NewOptions(100, OptionSetPredictTime(true))
|
|
|
|
for i := 0; i < 100; i++ {
|
|
bar.Add(1)
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func ExampleOptionShowIts_count() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionShowIts(), OptionShowCount())
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
// Output:
|
|
// 10% |█ | (10/100, 10 it/s) [1s:9s]
|
|
}
|
|
|
|
func ExampleOptionShowIts() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionShowIts(), OptionSetPredictTime(false))
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
// Output:
|
|
// 10% |█ | (10 it/s)
|
|
}
|
|
|
|
func ExampleOptionShowCount_minuscule() {
|
|
bar := NewOptions(10000, OptionSetWidth(10), OptionShowCount(), OptionSetPredictTime(false))
|
|
bar.Add(1)
|
|
// Output:
|
|
// 0% | | (1/10000)
|
|
}
|
|
|
|
func ExampleOptionSetPredictTime() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionSetPredictTime(false))
|
|
_ = bar.Add(10)
|
|
// Output:
|
|
// 10% |█ |
|
|
}
|
|
|
|
func ExampleOptionShowDescriptionAtLineEnd() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionShowDescriptionAtLineEnd(), OptionSetDescription("hello"))
|
|
_ = bar.Add(10)
|
|
// Output:
|
|
// 10% |█ | [0s:0s] hello
|
|
}
|
|
|
|
func ExampleOptionShowDescriptionAtLineEnd_spinner() {
|
|
bar := NewOptions(-1, OptionSetWidth(10), OptionShowDescriptionAtLineEnd(), OptionSetDescription("hello"))
|
|
_ = bar.Add(1)
|
|
// Output:
|
|
// | [0s] hello
|
|
}
|
|
|
|
func ExampleDefault() {
|
|
bar := Default(100)
|
|
for i := 0; i < 50; i++ {
|
|
bar.Add(1)
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
// Output:
|
|
//
|
|
}
|
|
|
|
func ExampleProgressBar_ChangeMax() {
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionSetPredictTime(false))
|
|
bar.ChangeMax(50)
|
|
bar.Add(50)
|
|
// Output:
|
|
// 100% |██████████|
|
|
}
|
|
|
|
func ExampleProgressBar_AddMax() {
|
|
bar := NewOptions(50, OptionSetWidth(10), OptionSetPredictTime(false))
|
|
bar.AddMax(50)
|
|
bar.Add(100)
|
|
// Output:
|
|
// 100% |██████████|
|
|
}
|
|
|
|
func ExampleOptionShowIts_spinner() {
|
|
/*
|
|
Spinner test with iteration count and iteration rate
|
|
*/
|
|
vt := virtualterm.NewDefault()
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSetWriter(&vt),
|
|
)
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(5)
|
|
bar.lock.Lock()
|
|
s, err := vt.String()
|
|
bar.lock.Unlock()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Print(s)
|
|
// Output:
|
|
// - (5/-, 5 it/s) [1s]
|
|
}
|
|
|
|
func TestSpinnerType(t *testing.T) {
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetDescription("indeterminate spinner"),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSpinnerType(9),
|
|
)
|
|
bar.Reset()
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(120 * time.Millisecond)
|
|
err := bar.Add(1)
|
|
if err != nil {
|
|
t.Errorf("Successfully tested one spinner option can be used.")
|
|
}
|
|
}
|
|
if false {
|
|
t.Errorf("error")
|
|
}
|
|
}
|
|
|
|
func TestSpinnerCustom(t *testing.T) {
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetDescription("indeterminate spinner"),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSpinnerCustom([]string{"🐰", "🐰", "🥕", "🥕"}),
|
|
)
|
|
bar.Reset()
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(120 * time.Millisecond)
|
|
err := bar.Add(1)
|
|
if err != nil {
|
|
t.Errorf("Successfully tested one spinner option can be used.")
|
|
}
|
|
}
|
|
if false {
|
|
t.Errorf("error")
|
|
}
|
|
}
|
|
|
|
func TestSpinnerTypeAndCustom(t *testing.T) {
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetDescription("indeterminate spinner"),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSpinnerCustom([]string{"🐰", "🐰", "🥕", "🥕"}),
|
|
OptionSpinnerType(9),
|
|
)
|
|
bar.Reset()
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(120 * time.Millisecond)
|
|
err := bar.Add(1)
|
|
if err == nil {
|
|
t.Errorf("Successfully tested both spinner options cannot be used together.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_IsFinished(t *testing.T) {
|
|
isCalled := false
|
|
bar := NewOptions(72, OptionOnCompletion(func() {
|
|
isCalled = true
|
|
}))
|
|
|
|
// Test1: If bar is not fully completed.
|
|
bar.Add(5)
|
|
if bar.IsFinished() || isCalled {
|
|
t.Errorf("Successfully tested bar is not yet finished.")
|
|
}
|
|
|
|
// Test2: Bar fully completed.
|
|
bar.Add(67)
|
|
if !bar.IsFinished() || !isCalled {
|
|
t.Errorf("Successfully tested bar is finished.")
|
|
}
|
|
|
|
// Test3: If increases maximum bytes error should be thrown and
|
|
// bar finished will remain false.
|
|
bar.Reset()
|
|
err := bar.Add(73)
|
|
if err == nil || bar.IsFinished() {
|
|
t.Errorf("Successfully got error when bytes increases max bytes, bar finished: %v", bar.IsFinished())
|
|
}
|
|
}
|
|
|
|
func ExampleOptionShowBytes_spinner() {
|
|
/*
|
|
Spinner test with iterations and count
|
|
*/
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionShowBytes(true),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
// since 10 is the width and we don't know the max bytes
|
|
// it will do a infinite scrolling.
|
|
bar.Add(11)
|
|
bar.lock.Lock()
|
|
result, _ := virtualterm.Process(buf.String())
|
|
bar.lock.Unlock()
|
|
fmt.Print(result)
|
|
// Output:
|
|
// - (11 B/s) [1s]
|
|
}
|
|
|
|
func TestBarSlowAdd(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionShowIts(), OptionSetWriter(&buf))
|
|
bar.Reset()
|
|
time.Sleep(3 * time.Second)
|
|
bar.Add(1)
|
|
if !strings.Contains(buf.String(), "1%") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
if !strings.Contains(buf.String(), "20 it/min") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
if !strings.Contains(buf.String(), "[3s:") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
// Output:
|
|
// 1% | | (20 it/min) [3s:4m57s]
|
|
}
|
|
|
|
func TestBarSmallBytes(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions64(100000000, OptionShowBytes(true), OptionShowCount(), OptionSetWidth(10), OptionSetWriter(&buf))
|
|
for i := 1; i < 10; i++ {
|
|
time.Sleep(100 * time.Millisecond)
|
|
bar.Add(1000)
|
|
}
|
|
if !strings.Contains(buf.String(), "9.0 kB/100 MB") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
for i := 1; i < 10; i++ {
|
|
time.Sleep(10 * time.Millisecond)
|
|
bar.Add(1000000)
|
|
}
|
|
if !strings.Contains(buf.String(), "9.0/100 MB") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestBarFastBytes(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions64(1e8, OptionShowBytes(true), OptionShowCount(), OptionSetWidth(10), OptionSetWriter(&buf))
|
|
bar.StartWithoutRender()
|
|
time.Sleep(time.Millisecond)
|
|
bar.Add(1e7)
|
|
if !strings.Contains(buf.String(), " GB/s)") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestBar(t *testing.T) {
|
|
bar := New(0)
|
|
if err := bar.Add(1); err == nil {
|
|
t.Error("should have an error for 0 bar")
|
|
}
|
|
bar = New(10)
|
|
if err := bar.Add(11); err == nil {
|
|
t.Error("should have an error for adding > bar")
|
|
}
|
|
}
|
|
|
|
func TestAddExceedsMaxDoesNotCorruptState(t *testing.T) {
|
|
bar := NewOptions(100, OptionSetWriter(io.Discard))
|
|
|
|
if err := bar.Add(95); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if err := bar.Add(10); err == nil {
|
|
t.Error("should have an error for adding past max")
|
|
}
|
|
|
|
// A rejected Add must leave state exactly as it was before the call,
|
|
// not partially applied, so the bar is not left stuck above 100%.
|
|
if bar.state.currentNum != 95 {
|
|
t.Errorf("expected currentNum to stay at 95 after a rejected Add, got %d", bar.state.currentNum)
|
|
}
|
|
if bar.state.currentPercent != 95 {
|
|
t.Errorf("expected currentPercent to stay at 95 after a rejected Add, got %d", bar.state.currentPercent)
|
|
}
|
|
|
|
// The bar must still accept further, non-overshooting progress afterwards.
|
|
if err := bar.Add(5); err != nil {
|
|
t.Errorf("bar should still accept progress after a rejected Add, got error: %v", err)
|
|
}
|
|
if bar.state.currentNum != 100 {
|
|
t.Errorf("expected currentNum to reach 100, got %d", bar.state.currentNum)
|
|
}
|
|
}
|
|
|
|
func TestState(t *testing.T) {
|
|
bar := NewOptions(100, OptionSetWidth(10))
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
s := bar.State()
|
|
if s.CurrentPercent != 0.1 {
|
|
t.Error(s)
|
|
}
|
|
}
|
|
|
|
func TestStartingBytes(t *testing.T) {
|
|
bar := NewOptions64(100, OptionSetWidth(10), OptionSetStartingBytes(80))
|
|
s := bar.State()
|
|
if s.CurrentBytes != 80 {
|
|
t.Errorf("expected CurrentBytes 80, got %v", s.CurrentBytes)
|
|
}
|
|
if s.CurrentPercent != 0.8 {
|
|
t.Errorf("expected CurrentPercent 0.8, got %v", s.CurrentPercent)
|
|
}
|
|
|
|
// Transfer 10 more over ~0.2s: the rate must reflect only those 10 units,
|
|
// not the 80 seeded ones. Without the offset the fallback rate would be
|
|
// 90/0.2 = 450/s; with it, ~50/s.
|
|
bar = NewOptions64(100, OptionSetWidth(10), OptionSetStartingBytes(80), OptionSetWriter(io.Discard))
|
|
bar.Add(0) // start the clock at the seeded position
|
|
time.Sleep(200 * time.Millisecond)
|
|
bar.Add(10)
|
|
s = bar.State()
|
|
if rate := s.KBsPerSecond * 1024; rate > 200 {
|
|
t.Errorf("rate should exclude seeded bytes, got %v/s", rate)
|
|
}
|
|
}
|
|
|
|
func ExampleOptionSetRenderBlankState() {
|
|
NewOptions(10, OptionSetWidth(10), OptionSetRenderBlankState(true))
|
|
// Output:
|
|
// 0% | | [0s:0s]
|
|
}
|
|
|
|
func TestBasicSets(t *testing.T) {
|
|
b := NewOptions(
|
|
999,
|
|
OptionSetWidth(888),
|
|
OptionSetRenderBlankState(true),
|
|
OptionSetWriter(io.Discard), // suppressing output for this test
|
|
)
|
|
|
|
tc := b.config
|
|
|
|
if tc.max != 999 {
|
|
t.Errorf("Expected %s to be %d, instead I got %d\n%+v", "max", 999, tc.max, b)
|
|
}
|
|
|
|
if tc.width != 888 {
|
|
t.Errorf("Expected %s to be %d, instead I got %d\n%+v", "width", 999, tc.max, b)
|
|
}
|
|
|
|
if !tc.renderWithBlankState {
|
|
t.Errorf("Expected %s to be %t, instead I got %t\n%+v", "renderWithBlankState", true, tc.renderWithBlankState, b)
|
|
}
|
|
}
|
|
|
|
func TestOptionSetTheme(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
10,
|
|
OptionSetTheme(
|
|
Theme{Saucer: "#", SaucerPadding: "-",
|
|
BarStart: ">", BarEnd: "<"}),
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
bar.RenderBlank()
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "0% >----------<"
|
|
if !strings.Contains(result, expect) {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
buf.Reset()
|
|
|
|
bar.Add(5)
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "50% >#####-----< [0s:0s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
buf.Reset()
|
|
|
|
bar.Finish()
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "100% >##########<"
|
|
if !strings.Contains(result, expect) {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestOptionSetThemeFilled(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
10,
|
|
OptionSetTheme(
|
|
Theme{Saucer: "#", SaucerPadding: "-",
|
|
BarStart: ">", BarStartFilled: "]",
|
|
BarEnd: "<", BarEndFilled: "["}),
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
bar.RenderBlank()
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "0% >----------<"
|
|
if !strings.Contains(result, expect) {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
buf.Reset()
|
|
|
|
bar.Add(5)
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "50% ]#####-----< [0s:0s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
buf.Reset()
|
|
|
|
bar.Finish()
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "100% ]##########["
|
|
if !strings.Contains(result, expect) {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
// TestOptionSetPredictTime ensures that when predict time is turned off, the progress
|
|
// bar is showing the total steps completed of the given max, otherwise the predicted
|
|
// time in seconds is specified.
|
|
func TestOptionSetPredictTime(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
10,
|
|
OptionSetPredictTime(false),
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
|
|
_ = bar.Add(2)
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "20% |██ |"
|
|
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
|
|
bar.Reset()
|
|
bar.config.predictTime = true
|
|
buf.Reset()
|
|
|
|
_ = bar.Add(7)
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "70% |███████ | [0s:0s]"
|
|
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestOptionSetElapsedTime_spinner(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSetElapsedTime(false),
|
|
)
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(5)
|
|
bar.lock.Lock()
|
|
result, err := virtualterm.Process(buf.String())
|
|
bar.lock.Unlock()
|
|
result = strings.TrimSpace(result)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expect := "- (5/-, 5 it/s)"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestOptionSetElapsedTime(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
10,
|
|
OptionSetElapsedTime(false),
|
|
OptionSetPredictTime(false),
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
|
|
_ = bar.Add(2)
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "20% |██ |"
|
|
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
|
|
bar.Reset()
|
|
bar.config.elapsedTime = true
|
|
buf.Reset()
|
|
|
|
_ = bar.Add(7)
|
|
result = strings.TrimSpace(buf.String())
|
|
expect = "70% |███████ | [0s]"
|
|
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestShowElapsedTimeOnFinish(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(10,
|
|
OptionShowElapsedTimeOnFinish(),
|
|
OptionSetWidth(10),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
bar.Reset()
|
|
time.Sleep(3 * time.Second)
|
|
bar.Add(10)
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "100% |██████████| [3s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestSpinnerState(t *testing.T) {
|
|
bar := NewOptions(
|
|
-1,
|
|
OptionSetWidth(100),
|
|
)
|
|
bar.Reset()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
|
|
state := bar.State()
|
|
if state.Max != -1 {
|
|
t.Errorf("Max mismatched gotMax %d wantMax %d", state.Max, -1)
|
|
}
|
|
if state.CurrentNum != 10 {
|
|
t.Errorf("Number mismatched gotNum %d wantNum %d", state.CurrentNum, 10)
|
|
}
|
|
if state.CurrentBytes != 10.0 {
|
|
t.Errorf("Number of bytes mismatched gotBytes %f wantBytes %f", state.CurrentBytes, 10.0)
|
|
}
|
|
if state.CurrentPercent != 0.1 {
|
|
t.Errorf("Percent of bar mismatched got %f want %f", state.CurrentPercent, 0.1)
|
|
}
|
|
|
|
kbPerSec := fmt.Sprintf("%2.2f", state.KBsPerSecond)
|
|
if kbPerSec != "0.01" {
|
|
t.Errorf("Speed mismatched got %s want %s", kbPerSec, "0.01")
|
|
}
|
|
}
|
|
|
|
func TestReaderToBuffer(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
urlToGet := "https://dl.google.com/go/go1.14.1.src.tar.gz"
|
|
req, err := http.NewRequest("GET", urlToGet, nil)
|
|
assert.Nil(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
assert.Nil(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
buf := new(bytes.Buffer)
|
|
bar := NewOptions(int(resp.ContentLength), OptionShowBytes(true), OptionShowCount())
|
|
out := io.MultiWriter(buf, bar)
|
|
_, err = io.Copy(out, resp.Body)
|
|
assert.Nil(t, err)
|
|
|
|
md5, err := md5sum(buf)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "d441819a800f8c90825355dfbede7266", md5)
|
|
}
|
|
|
|
func TestReaderToFile(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
urlToGet := "https://dl.google.com/go/go1.14.1.src.tar.gz"
|
|
req, err := http.NewRequest("GET", urlToGet, nil)
|
|
assert.Nil(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
assert.Nil(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
f, err := os.CreateTemp("", "progressbar_testfile")
|
|
if err != nil {
|
|
t.Fatal()
|
|
}
|
|
defer os.Remove(f.Name())
|
|
defer f.Close()
|
|
|
|
realStdout := os.Stdout
|
|
defer func() { os.Stdout = realStdout }()
|
|
r, fakeStdout, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Stdout = fakeStdout
|
|
|
|
bar := DefaultBytes(resp.ContentLength)
|
|
out := io.MultiWriter(f, bar)
|
|
_, err = io.Copy(out, resp.Body)
|
|
assert.Nil(t, err)
|
|
f.Sync()
|
|
f.Seek(0, 0)
|
|
|
|
if err := fakeStdout.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := r.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, "", string(b))
|
|
|
|
md5, err := md5sum(f)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "d441819a800f8c90825355dfbede7266", md5)
|
|
}
|
|
|
|
func TestReaderToFileUnknownLength(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
urlToGet := "https://dl.google.com/go/go1.14.1.src.tar.gz"
|
|
req, err := http.NewRequest("GET", urlToGet, nil)
|
|
assert.Nil(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
assert.Nil(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
f, err := os.CreateTemp("", "progressbar_testfile")
|
|
if err != nil {
|
|
t.Fatal()
|
|
}
|
|
defer os.Remove(f.Name())
|
|
defer f.Close()
|
|
|
|
realStdout := os.Stdout
|
|
defer func() { os.Stdout = realStdout }()
|
|
r, fakeStdout, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Stdout = fakeStdout
|
|
|
|
bar := DefaultBytes(-1, " downloading")
|
|
out := io.MultiWriter(f, bar)
|
|
_, err = io.Copy(out, resp.Body)
|
|
assert.Nil(t, err)
|
|
f.Sync()
|
|
f.Seek(0, 0)
|
|
|
|
if err := fakeStdout.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b, err := io.ReadAll(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := r.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, "", string(b))
|
|
|
|
md5, err := md5sum(f)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, "d441819a800f8c90825355dfbede7266", md5)
|
|
}
|
|
|
|
func TestConcurrency(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
1000,
|
|
OptionSetWriter(&buf),
|
|
)
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 900; i++ {
|
|
wg.Add(1)
|
|
go func(b *ProgressBar, wg *sync.WaitGroup) {
|
|
bar.Add(1)
|
|
wg.Done()
|
|
}(bar, &wg)
|
|
}
|
|
wg.Wait()
|
|
result := bar.state.currentNum
|
|
expect := int64(900)
|
|
assert.Equal(t, expect, result)
|
|
}
|
|
|
|
func TestIterationNames(t *testing.T) {
|
|
b := Default(20)
|
|
tc := b.config
|
|
|
|
// Checking for the default iterations per second or "it/s"
|
|
if tc.iterationString != "it" {
|
|
t.Errorf("Expected %s to be %s, instead I got %s", "iterationString", "it", tc.iterationString)
|
|
}
|
|
|
|
// Change the default "it/s" to provide context, downloads per second or "dl/s"
|
|
b = NewOptions(20, OptionSetItsString("dl"))
|
|
tc = b.config
|
|
|
|
if tc.iterationString != "dl" {
|
|
t.Errorf("Expected %s to be %s, instead I got %s", "iterationString", "dl", tc.iterationString)
|
|
}
|
|
}
|
|
|
|
func md5sum(r io.Reader) (string, error) {
|
|
hash := md5.New()
|
|
_, err := io.Copy(hash, r)
|
|
return hex.EncodeToString(hash.Sum(nil)), err
|
|
}
|
|
|
|
func TestProgressBar_Describe(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionSetWriter(&buf))
|
|
bar.Describe("performing axial adjustments")
|
|
bar.Add(10)
|
|
result := buf.String()
|
|
expect := "" +
|
|
"\rperforming axial adjustments 0% | | [0s:0s]" +
|
|
"\r \r" +
|
|
"\rperforming axial adjustments 10% |█ | [0s:0s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestProgressBar_DescribeAfterFinish(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100,
|
|
OptionSetDescription("Uploading files"),
|
|
OptionShowCount(),
|
|
OptionSetWriter(&buf),
|
|
OptionSetWidth(10),
|
|
)
|
|
for i := 0; i < 100; i++ {
|
|
bar.Add(1)
|
|
}
|
|
bar.Describe("Upload complete")
|
|
result := buf.String()
|
|
assert.Contains(t, result, "Upload complete")
|
|
}
|
|
|
|
func TestRenderBlankStateWithThrottle(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100, OptionSetWidth(10), OptionSetRenderBlankState(true), OptionThrottle(time.Millisecond), OptionSetWriter(&buf))
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "0% | | [0s:0s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestOptionFullWidth(t *testing.T) {
|
|
var tests = []struct {
|
|
opts []Option
|
|
expected string
|
|
}{
|
|
{ // 1
|
|
[]Option{},
|
|
"" +
|
|
"\r 10% |██████ | [1s:9s]" +
|
|
"\r \r" +
|
|
"\r 100% |██████████████████████████████████████████████████████████████| ",
|
|
},
|
|
{ // 2
|
|
[]Option{OptionSetDescription("Progress:")},
|
|
"" +
|
|
"\rProgress: 10% |█████ | [1s:9s]" +
|
|
"\r \r" +
|
|
"\rProgress: 100% |█████████████████████████████████████████████████████| ",
|
|
},
|
|
{ // 3
|
|
[]Option{OptionSetDescription("<1/5>"), OptionShowDescriptionAtLineEnd()},
|
|
"" +
|
|
"\r 10% |█████ | [1s:9s] <1/5>" +
|
|
"\r \r" +
|
|
"\r 100% |████████████████████████████████████████████████████████| <1/5>",
|
|
},
|
|
{ // 4
|
|
[]Option{OptionSetPredictTime(false)},
|
|
"" +
|
|
"\r 10% |██████ | " +
|
|
"\r \r" +
|
|
"\r 100% |█████████████████████████████████████████████████████████████████████| ",
|
|
},
|
|
{ // 5
|
|
[]Option{OptionSetPredictTime(false), OptionShowElapsedTimeOnFinish()},
|
|
"" +
|
|
"\r 10% |██████ | " +
|
|
"\r \r" +
|
|
"\r 100% |████████████████████████████████████████████████████████████████| [2s] ",
|
|
},
|
|
{ // 6
|
|
[]Option{OptionSetPredictTime(false), OptionSetElapsedTime(false)},
|
|
"" +
|
|
"\r 10% |██████ | " +
|
|
"\r \r" +
|
|
"\r 100% |█████████████████████████████████████████████████████████████████████| ",
|
|
},
|
|
{ // 7
|
|
[]Option{OptionShowIts()},
|
|
"" +
|
|
"\r 10% |█████ | (10 it/s) [1s:9s]" +
|
|
"\r \r" +
|
|
"\r 100% |█████████████████████████████████████████████████████| (50 it/s)",
|
|
},
|
|
{ // 8
|
|
[]Option{OptionShowCount()},
|
|
"" +
|
|
"\r 10% |█████ | (10/100) [1s:9s]" +
|
|
"\r \r" +
|
|
"\r 100% |█████████████████████████████████████████████████████| (100/100)",
|
|
},
|
|
{ // 9
|
|
[]Option{OptionShowIts(), OptionShowCount(), OptionShowElapsedTimeOnFinish()},
|
|
"" +
|
|
"\r 10% |████ | (10/100, 10 it/s) [1s:9s]" +
|
|
"\r \r" +
|
|
"\r 100% |████████████████████████████████████████████| (100/100, 50 it/s) [2s]",
|
|
},
|
|
{ // 10
|
|
[]Option{OptionSetDescription("Progress:"), OptionShowIts(), OptionShowCount()},
|
|
"" +
|
|
"\rProgress: 10% |███ | (10/100, 10 it/s) [1s:9s]" +
|
|
"\r \r" +
|
|
"\rProgress: 100% |███████████████████████████████████| (100/100, 50 it/s)",
|
|
},
|
|
{ // 11
|
|
[]Option{OptionSetDescription("<3/5>"), OptionShowIts(), OptionShowCount(), OptionShowElapsedTimeOnFinish(), OptionShowDescriptionAtLineEnd()},
|
|
"" +
|
|
"\r 10% |███ | (10/100, 10 it/s) [1s:9s] <3/5>" +
|
|
"\r \r" +
|
|
"\r 100% |██████████████████████████████████████| (100/100, 50 it/s) [2s] <3/5>",
|
|
},
|
|
{ // 12
|
|
[]Option{OptionShowIts(), OptionShowCount(), OptionSetPredictTime(false)},
|
|
"" +
|
|
"\r 10% |█████ | (10/100, 10 it/s) " +
|
|
"\r \r" +
|
|
"\r 100% |███████████████████████████████████████████████████| (100/100, 50 it/s) ",
|
|
},
|
|
{ // 13
|
|
[]Option{OptionShowIts(), OptionShowCount(), OptionSetPredictTime(false), OptionShowElapsedTimeOnFinish()},
|
|
"" +
|
|
"\r 10% |████ | (10/100, 10 it/s) " +
|
|
"\r \r" +
|
|
"\r 100% |██████████████████████████████████████████████| (100/100, 50 it/s) [2s] ",
|
|
},
|
|
{ // 14
|
|
[]Option{OptionShowIts(), OptionShowCount(), OptionSetPredictTime(false), OptionSetElapsedTime(false)},
|
|
"" +
|
|
"\r 10% |█████ | (10/100, 10 it/s) " +
|
|
"\r \r" +
|
|
"\r 100% |███████████████████████████████████████████████████| (100/100, 50 it/s) ",
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
test := test
|
|
t.Run(fmt.Sprintf("%d", i+1), func(t *testing.T) {
|
|
t.Parallel()
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100, append(test.opts, []Option{OptionFullWidth(), OptionSetWriter(&buf)}...)...)
|
|
bar.StartWithoutRender()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(90)
|
|
assert.Equal(t, test.expected, buf.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFixedWidthBarFitsTerminal(t *testing.T) {
|
|
oldTermWidth := termWidth
|
|
termWidth = func(w io.Writer) (int, error) {
|
|
return 50, nil
|
|
}
|
|
defer func() {
|
|
termWidth = oldTermWidth
|
|
}()
|
|
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(
|
|
100,
|
|
OptionSetWriter(&buf),
|
|
OptionSetDescription("really-long-file-name.bin "),
|
|
OptionSetWidth(20),
|
|
OptionShowCount(),
|
|
OptionSetPredictTime(false),
|
|
)
|
|
bar.Add(1)
|
|
|
|
assert.LessOrEqual(t, getStringWidth(bar.config, bar.String()), 50)
|
|
assert.NotContains(t, bar.String(), "| |")
|
|
}
|
|
|
|
func TestHumanizeBytesSI(t *testing.T) {
|
|
amount, suffix := humanizeBytes(float64(12.34)*1000*1000, false)
|
|
assert.Equal(t, "12 MB", fmt.Sprintf("%s%s", amount, suffix))
|
|
|
|
amount, suffix = humanizeBytes(float64(56.78)*1000*1000*1000, false)
|
|
assert.Equal(t, "57 GB", fmt.Sprintf("%s%s", amount, suffix))
|
|
}
|
|
|
|
func TestHumanizeBytesIEC(t *testing.T) {
|
|
amount, suffix := humanizeBytes(float64(12.34)*1024*1024, true)
|
|
assert.Equal(t, "12 MiB", fmt.Sprintf("%s%s", amount, suffix))
|
|
|
|
amount, suffix = humanizeBytes(float64(56.78)*1024*1024*1024, true)
|
|
assert.Equal(t, "57 GiB", fmt.Sprintf("%s%s", amount, suffix))
|
|
}
|
|
|
|
func TestProgressBar_StartWithoutRender(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(100, OptionSetWriter(&buf))
|
|
time.Sleep(1 * time.Second)
|
|
bar.StartWithoutRender()
|
|
time.Sleep(1 * time.Second)
|
|
bar.Add(10)
|
|
result := strings.TrimSpace(buf.String())
|
|
expect := "10% |████ | [1s:9s]"
|
|
if result != expect {
|
|
t.Errorf("Render miss-match\nResult: '%s'\nExpect: '%s'\n%+v", result, expect, bar)
|
|
}
|
|
}
|
|
|
|
func TestOptionSetSpinnerChangeInterval(t *testing.T) {
|
|
interval := 1000 * time.Millisecond
|
|
vt := virtualterm.NewDefault()
|
|
actuals := make([]string, 0, 8)
|
|
expecteds := []string{
|
|
"◐ test [0s]",
|
|
"◓ test [1s]",
|
|
"◑ test [2s]",
|
|
"◒ test [3s]",
|
|
"◐ test [4s]",
|
|
"◓ test [5s]",
|
|
"◑ test [6s]",
|
|
"◒ test [7s]",
|
|
}
|
|
bar := NewOptions(-1,
|
|
OptionSetDescription("test"),
|
|
OptionSpinnerType(7),
|
|
OptionSetWriter(&vt),
|
|
OptionSetSpinnerChangeInterval(interval))
|
|
bar.Add(1)
|
|
for i := 0; i < 8; i++ {
|
|
bar.lock.Lock()
|
|
s, _ := vt.String()
|
|
bar.lock.Unlock()
|
|
s = strings.TrimSpace(s)
|
|
actuals = append(actuals, s)
|
|
// sleep 50 ms more to make sure to go to next interval each time
|
|
time.Sleep(1050 * time.Millisecond)
|
|
}
|
|
for i := range actuals {
|
|
assert.Equal(t, expecteds[i], actuals[i])
|
|
}
|
|
}
|
|
|
|
func TestOptionSetSpinnerChangeIntervalZero(t *testing.T) {
|
|
vt := virtualterm.NewDefault()
|
|
bar := NewOptions(-1,
|
|
OptionSetDescription("test"),
|
|
OptionSpinnerType(7),
|
|
OptionSetWriter(&vt),
|
|
OptionSetSpinnerChangeInterval(0))
|
|
actuals := make([]string, 0, 5)
|
|
expected := []string{
|
|
"◐ test [0s]",
|
|
"◓ test [1s]",
|
|
"◑ test [2s]",
|
|
"◒ test [3s]",
|
|
"◐ test [4s]",
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
bar.Add(1)
|
|
bar.lock.Lock()
|
|
s, _ := vt.String()
|
|
bar.lock.Unlock()
|
|
_ = strings.TrimSpace(s)
|
|
}
|
|
for i := range actuals {
|
|
assert.Equal(t, expected[i], actuals[i])
|
|
}
|
|
}
|
|
|
|
func TestOptionShowTotalFalseDeterminate(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions64(
|
|
100000000,
|
|
OptionShowBytes(true),
|
|
OptionShowCount(),
|
|
OptionSetWidth(10),
|
|
OptionShowTotalBytes(false),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
|
|
bar.Add(10000)
|
|
if !strings.Contains(buf.String(), "10 kB, ") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestOptionShowTotalFalseIndeterminate(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetDescription("indeterminate spinner"),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSpinnerType(9),
|
|
OptionShowTotalBytes(false),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
bar.Add(10)
|
|
if !strings.Contains(buf.String(), "10, ") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestOptionShowTotalTrueIndeterminate(t *testing.T) {
|
|
buf := strings.Builder{}
|
|
bar := NewOptions(-1,
|
|
OptionSetWidth(10),
|
|
OptionSetDescription("indeterminate spinner"),
|
|
OptionShowIts(),
|
|
OptionShowCount(),
|
|
OptionSpinnerType(9),
|
|
OptionShowTotalBytes(true),
|
|
OptionSetWriter(&buf),
|
|
)
|
|
bar.Add(10)
|
|
if !strings.Contains(buf.String(), "10/-, ") {
|
|
t.Errorf("wrong string: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func freeTestHTTPAddr(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen for free port: %v", err)
|
|
}
|
|
defer listener.Close()
|
|
|
|
return listener.Addr().String()
|
|
}
|
|
|
|
func getHTTPWithRetry(t *testing.T, url string) *http.Response {
|
|
t.Helper()
|
|
|
|
var lastErr error
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
resp, err := http.Get(url)
|
|
if err == nil {
|
|
return resp
|
|
}
|
|
lastErr = err
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
t.Fatalf("GET %s: %v", url, lastErr)
|
|
return nil
|
|
}
|
|
|
|
func TestStartHTTPServer(t *testing.T) {
|
|
bar := Default(10, "test")
|
|
bar.Add(1)
|
|
|
|
hostPort := freeTestHTTPAddr(t)
|
|
svr := bar.StartHTTPServer(hostPort)
|
|
|
|
// check plain text
|
|
resp := getHTTPWithRetry(t, fmt.Sprintf("http://%s/desc", hostPort))
|
|
got, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if string(got) != "1/10, 10.00%, 0s left" {
|
|
t.Errorf("wrong string: %s", string(got))
|
|
}
|
|
|
|
// check json
|
|
resp = getHTTPWithRetry(t, fmt.Sprintf("http://%s/state", hostPort))
|
|
got, err = io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
var result State
|
|
err = json.Unmarshal(got, &result)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if result.Max != bar.State().Max || result.CurrentNum != bar.State().CurrentNum {
|
|
t.Errorf("wrong state: %v", result)
|
|
}
|
|
|
|
// shutdown server
|
|
err = svr.Shutdown(context.Background())
|
|
if err != nil {
|
|
t.Errorf("shutdown server failed: %v", err)
|
|
}
|
|
|
|
// start new bar server
|
|
bar = Default(10, "test")
|
|
bar.Add(1)
|
|
svr = bar.StartHTTPServer(hostPort)
|
|
err = svr.Close()
|
|
if err != nil {
|
|
t.Errorf("shutdown server failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTrimRateSamples(t *testing.T) {
|
|
base := time.Now()
|
|
times := []time.Time{
|
|
base.Add(-10 * time.Second),
|
|
base.Add(-5 * time.Second),
|
|
base.Add(-2 * time.Second),
|
|
base,
|
|
}
|
|
rates := []float64{1, 2, 3, 4}
|
|
|
|
clone := func() ([]float64, []time.Time) {
|
|
return append([]float64{}, rates...), append([]time.Time{}, times...)
|
|
}
|
|
|
|
// A 6s window keeps only the samples collected within the last 6 seconds.
|
|
r, tt := clone()
|
|
gotRates, gotTimes := trimRateSamples(r, tt, 6*time.Second, base)
|
|
if len(gotRates) != 3 || gotRates[0] != 2 {
|
|
t.Errorf("window trim: expected 3 samples starting at 2, got %v", gotRates)
|
|
}
|
|
if len(gotTimes) != len(gotRates) {
|
|
t.Errorf("window trim: rates/times length mismatch: %d vs %d", len(gotRates), len(gotTimes))
|
|
}
|
|
|
|
// A window shorter than the gap between samples keeps only the newest sample.
|
|
r, tt = clone()
|
|
gotRates, _ = trimRateSamples(r, tt, time.Nanosecond, base)
|
|
if len(gotRates) != 1 || gotRates[0] != 4 {
|
|
t.Errorf("tiny window: expected only the newest sample 4, got %v", gotRates)
|
|
}
|
|
|
|
// Without a window, at most maxLegacyRateSamples samples are kept.
|
|
manyRates := make([]float64, maxLegacyRateSamples+5)
|
|
manyTimes := make([]time.Time, maxLegacyRateSamples+5)
|
|
for i := range manyRates {
|
|
manyRates[i] = float64(i)
|
|
manyTimes[i] = base.Add(time.Duration(i) * time.Second)
|
|
}
|
|
gotRates, _ = trimRateSamples(manyRates, manyTimes, 0, base)
|
|
if len(gotRates) != maxLegacyRateSamples {
|
|
t.Errorf("legacy trim: expected %d samples, got %d", maxLegacyRateSamples, len(gotRates))
|
|
}
|
|
if gotRates[0] != 5 {
|
|
t.Errorf("legacy trim: expected to keep the newest samples, first kept = %v", gotRates[0])
|
|
}
|
|
}
|
|
|
|
func TestRateAveragingWindow(t *testing.T) {
|
|
forceSamples := func(bar *ProgressBar, n int) {
|
|
for i := 0; i < n; i++ {
|
|
// backdate the counter so each Add records a new rate sample
|
|
bar.state.counterTime = time.Now().Add(-time.Second)
|
|
if err := bar.Add(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Without a window the number of retained rate samples is capped.
|
|
legacy := NewOptions(1000000, OptionSetWriter(io.Discard))
|
|
forceSamples(legacy, maxLegacyRateSamples+15)
|
|
if got := len(legacy.state.counterLastTenRates); got != maxLegacyRateSamples {
|
|
t.Errorf("legacy: expected %d retained rate samples, got %d", maxLegacyRateSamples, got)
|
|
}
|
|
|
|
// With a window covering the whole test, every sample is retained, so the
|
|
// count exceeds the legacy cap.
|
|
windowed := NewOptions(1000000, OptionSetWriter(io.Discard), OptionSetRateAveragingWindow(time.Hour))
|
|
forceSamples(windowed, maxLegacyRateSamples+15)
|
|
if got := len(windowed.state.counterLastTenRates); got <= maxLegacyRateSamples {
|
|
t.Errorf("windowed: expected more than %d retained samples, got %d", maxLegacyRateSamples, got)
|
|
}
|
|
}
|