1155 lines
26 KiB
Go
1155 lines
26 KiB
Go
//go:build linux || freebsd
|
||||
|
|
// +build linux freebsd
|
|||
|
|
|
|||
|
|
package cli
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"errors"
|
|||
|
|
"os"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// parser.go tests
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
func TestParseArgs(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
args []string
|
|||
|
|
want map[string]string
|
|||
|
|
wantErr bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "empty args",
|
|||
|
|
args: []string{},
|
|||
|
|
want: map[string]string{},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "single key-value pair",
|
|||
|
|
args: []string{"--url", "https://example.com/file.zip"},
|
|||
|
|
want: map[string]string{"url": "https://example.com/file.zip"},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "boolean flag",
|
|||
|
|
args: []string{"--verbose"},
|
|||
|
|
want: map[string]string{"verbose": "true"},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "multiple boolean flags",
|
|||
|
|
args: []string{"--verbose", "--debug", "--resume"},
|
|||
|
|
want: map[string]string{
|
|||
|
|
"verbose": "true",
|
|||
|
|
"debug": "true",
|
|||
|
|
"resume": "true",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "mixed boolean and value flags",
|
|||
|
|
args: []string{"--url", "https://example.com/file.zip", "--verbose", "--output", "file.zip"},
|
|||
|
|
want: map[string]string{
|
|||
|
|
"url": "https://example.com/file.zip",
|
|||
|
|
"verbose": "true",
|
|||
|
|
"output": "file.zip",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "flag with equals in value",
|
|||
|
|
args: []string{"--proxy", "http://user:pass@proxy.example.com:8080"},
|
|||
|
|
want: map[string]string{"proxy": "http://user:pass@proxy.example.com:8080"},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "multiple key-value pairs",
|
|||
|
|
args: []string{"--key1", "val1", "--key2", "val2", "--key3", "val3"},
|
|||
|
|
want: map[string]string{
|
|||
|
|
"key1": "val1",
|
|||
|
|
"key2": "val2",
|
|||
|
|
"key3": "val3",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "flag with empty string value",
|
|||
|
|
args: []string{"--output", ""},
|
|||
|
|
want: map[string]string{"output": ""},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got, err := ParseArgs(tt.args)
|
|||
|
|
if (err != nil) != tt.wantErr {
|
|||
|
|
t.Errorf("ParseArgs() error = %v, wantErr %v", err, tt.wantErr)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if len(got) != len(tt.want) {
|
|||
|
|
t.Errorf("ParseArgs() got %v, want %v", got, tt.want)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for k, v := range tt.want {
|
|||
|
|
if got[k] != v {
|
|||
|
|
t.Errorf("ParseArgs()[%q] = %q, want %q", k, got[k], v)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestValidateRequiredFlags(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
flags map[string]string
|
|||
|
|
required []string
|
|||
|
|
wantErr bool
|
|||
|
|
errMsg string
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "all required present",
|
|||
|
|
flags: map[string]string{"url": "http://example.com", "output": "file.zip"},
|
|||
|
|
required: []string{"url", "output"},
|
|||
|
|
wantErr: false,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "single required missing",
|
|||
|
|
flags: map[string]string{"output": "file.zip"},
|
|||
|
|
required: []string{"url"},
|
|||
|
|
wantErr: true,
|
|||
|
|
errMsg: "missing required flags: url",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "multiple required missing",
|
|||
|
|
flags: map[string]string{},
|
|||
|
|
required: []string{"url", "output", "verbose"},
|
|||
|
|
wantErr: true,
|
|||
|
|
errMsg: "missing required flags: url, output, verbose",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "no required flags",
|
|||
|
|
flags: map[string]string{"url": "http://example.com"},
|
|||
|
|
required: []string{},
|
|||
|
|
wantErr: false,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "empty flags map with required",
|
|||
|
|
flags: map[string]string{},
|
|||
|
|
required: []string{"url"},
|
|||
|
|
wantErr: true,
|
|||
|
|
errMsg: "missing required flags: url",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "nil flags map",
|
|||
|
|
flags: nil,
|
|||
|
|
required: []string{"url"},
|
|||
|
|
wantErr: true,
|
|||
|
|
errMsg: "missing required flags: url",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
err := ValidateRequiredFlags(tt.flags, tt.required)
|
|||
|
|
if (err != nil) != tt.wantErr {
|
|||
|
|
t.Errorf("ValidateRequiredFlags() error = %v, wantErr %v", err, tt.wantErr)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if tt.wantErr && err != nil && err.Error() != tt.errMsg {
|
|||
|
|
t.Errorf("ValidateRequiredFlags() error message = %q, want %q", err.Error(), tt.errMsg)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParserGetBool(t *testing.T) {
|
|||
|
|
p := NewParser("test")
|
|||
|
|
p.fs.Bool("verbose", false, "verbose output")
|
|||
|
|
p.fs.Bool("debug", false, "debug mode")
|
|||
|
|
|
|||
|
|
// Parse with verbose=true
|
|||
|
|
if err := p.Parse([]string{"--verbose"}); err != nil {
|
|||
|
|
t.Fatalf("Parse() error = %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if !p.GetBool("verbose") {
|
|||
|
|
t.Error("GetBool(verbose) = false, want true")
|
|||
|
|
}
|
|||
|
|
if p.GetBool("debug") {
|
|||
|
|
t.Error("GetBool(debug) = true, want false (default)")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParserGetString(t *testing.T) {
|
|||
|
|
p := NewParser("test")
|
|||
|
|
p.fs.String("url", "", "URL to download")
|
|||
|
|
p.fs.String("output", "default.txt", "output file")
|
|||
|
|
p.fs.String("proxy", "", "proxy URL")
|
|||
|
|
|
|||
|
|
// Parse with custom values
|
|||
|
|
if err := p.Parse([]string{"--url", "https://example.com", "--output", "file.zip"}); err != nil {
|
|||
|
|
t.Fatalf("Parse() error = %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if got := p.GetString("url"); got != "https://example.com" {
|
|||
|
|
t.Errorf("GetString(url) = %q, want %q", got, "https://example.com")
|
|||
|
|
}
|
|||
|
|
if got := p.GetString("output"); got != "file.zip" {
|
|||
|
|
t.Errorf("GetString(output) = %q, want %q", got, "file.zip")
|
|||
|
|
}
|
|||
|
|
// Unset flag should return default value
|
|||
|
|
if got := p.GetString("proxy"); got != "" {
|
|||
|
|
t.Errorf("GetString(proxy) = %q, want %q (default)", got, "")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParserGetBoolDefaultTrue(t *testing.T) {
|
|||
|
|
p := NewParser("test")
|
|||
|
|
p.fs.Bool("no-color", false, "disable colors")
|
|||
|
|
|
|||
|
|
if err := p.Parse([]string{}); err != nil {
|
|||
|
|
t.Fatalf("Parse() error = %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if p.GetBool("no-color") {
|
|||
|
|
t.Error("GetBool(no-color) = true, want false (default)")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParserGetStringDefault(t *testing.T) {
|
|||
|
|
p := NewParser("test")
|
|||
|
|
p.fs.String("output", "default.txt", "output file")
|
|||
|
|
|
|||
|
|
if err := p.Parse([]string{}); err != nil {
|
|||
|
|
t.Fatalf("Parse() error = %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if got := p.GetString("output"); got != "default.txt" {
|
|||
|
|
t.Errorf("GetString(output) = %q, want %q (default)", got, "default.txt")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParserParse(t *testing.T) {
|
|||
|
|
p := NewParser("test")
|
|||
|
|
p.fs.String("url", "", "URL")
|
|||
|
|
p.fs.Bool("verbose", false, "verbose")
|
|||
|
|
|
|||
|
|
// Valid parse
|
|||
|
|
if err := p.Parse([]string{"--url", "http://example.com", "--verbose"}); err != nil {
|
|||
|
|
t.Errorf("Parse() unexpected error: %v", err)
|
|||
|
|
}
|
|||
|
|
if got := p.GetString("url"); got != "http://example.com" {
|
|||
|
|
t.Errorf("GetString(url) = %q, want %q", got, "http://example.com")
|
|||
|
|
}
|
|||
|
|
if !p.GetBool("verbose") {
|
|||
|
|
t.Error("GetBool(verbose) should be true")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// flags.go tests
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
func TestPrintError(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
err := errors.New("connection refused")
|
|||
|
|
PrintError(&buf, err)
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "Error:") {
|
|||
|
|
t.Errorf("PrintError output = %q, want it to contain 'Error:'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "connection refused") {
|
|||
|
|
t.Errorf("PrintError output = %q, want it to contain 'connection refused'", output)
|
|||
|
|
}
|
|||
|
|
// Should contain the unicode error symbol
|
|||
|
|
if !strings.Contains(output, "✗") {
|
|||
|
|
t.Errorf("PrintError output = %q, want it to contain '✗'", output)
|
|||
|
|
}
|
|||
|
|
// No ANSI codes since buffer is not a terminal
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintError output = %q, should not contain ANSI codes when writing to buffer", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintWarning(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintWarning(&buf, "rate limit exceeded")
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "Warning:") {
|
|||
|
|
t.Errorf("PrintWarning output = %q, want it to contain 'Warning:'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "rate limit exceeded") {
|
|||
|
|
t.Errorf("PrintWarning output = %q, want it to contain 'rate limit exceeded'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "⚠") {
|
|||
|
|
t.Errorf("PrintWarning output = %q, want it to contain '⚠'", output)
|
|||
|
|
}
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintWarning output = %q, should not contain ANSI codes when writing to buffer", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintSuccess(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintSuccess(&buf, "download complete")
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "✓") {
|
|||
|
|
t.Errorf("PrintSuccess output = %q, want it to contain '✓'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "download complete") {
|
|||
|
|
t.Errorf("PrintSuccess output = %q, want it to contain 'download complete'", output)
|
|||
|
|
}
|
|||
|
|
// Success message format is "✓ <msg>\n"
|
|||
|
|
if output != "✓ download complete\n" {
|
|||
|
|
t.Errorf("PrintSuccess output = %q, want %q", output, "✓ download complete\n")
|
|||
|
|
}
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintSuccess output = %q, should not contain ANSI codes when writing to buffer", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintInfo(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintInfo(&buf, "processing file")
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "ℹ") {
|
|||
|
|
t.Errorf("PrintInfo output = %q, want it to contain 'ℹ'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "processing file") {
|
|||
|
|
t.Errorf("PrintInfo output = %q, want it to contain 'processing file'", output)
|
|||
|
|
}
|
|||
|
|
// Info message format is "ℹ <msg>\n"
|
|||
|
|
if output != "ℹ processing file\n" {
|
|||
|
|
t.Errorf("PrintInfo output = %q, want %q", output, "ℹ processing file\n")
|
|||
|
|
}
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintInfo output = %q, should not contain ANSI codes when writing to buffer", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintErrorWithNilError(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintError(&buf, nil)
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "Error:") {
|
|||
|
|
t.Errorf("PrintError with nil output = %q, want it to contain 'Error:'", output)
|
|||
|
|
}
|
|||
|
|
// nil error prints "<nil>"
|
|||
|
|
if !strings.Contains(output, "<nil>") {
|
|||
|
|
t.Errorf("PrintError with nil output = %q, want it to contain '<nil>'", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestFormatError(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
err error
|
|||
|
|
context string
|
|||
|
|
wantMsg string
|
|||
|
|
wantNil bool
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "with context",
|
|||
|
|
err: errors.New("not found"),
|
|||
|
|
context: "downloading file",
|
|||
|
|
wantMsg: "downloading file: not found",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "empty context",
|
|||
|
|
err: errors.New("something went wrong"),
|
|||
|
|
context: "",
|
|||
|
|
wantMsg: "something went wrong",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "nil error with context",
|
|||
|
|
err: nil,
|
|||
|
|
context: "processing",
|
|||
|
|
wantMsg: "processing: %!w(<nil>)",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "nil error empty context",
|
|||
|
|
err: nil,
|
|||
|
|
context: "",
|
|||
|
|
wantMsg: "",
|
|||
|
|
wantNil: true,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got := FormatError(tt.err, tt.context)
|
|||
|
|
if tt.wantNil {
|
|||
|
|
if got != nil {
|
|||
|
|
t.Errorf("FormatError() = %v, want nil", got)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if got == nil {
|
|||
|
|
t.Fatal("FormatError() returned nil, want non-nil")
|
|||
|
|
}
|
|||
|
|
if got.Error() != tt.wantMsg {
|
|||
|
|
t.Errorf("FormatError() = %q, want %q", got.Error(), tt.wantMsg)
|
|||
|
|
}
|
|||
|
|
// Verify error wrapping: if original error is non-nil with non-empty context,
|
|||
|
|
// the original should be accessible via errors.Unwrap
|
|||
|
|
if tt.err != nil && tt.context != "" {
|
|||
|
|
if !errors.Is(got, tt.err) {
|
|||
|
|
t.Errorf("FormatError() should wrap the original error, errors.Is(got, tt.err) = false")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestIndent(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
text string
|
|||
|
|
spaces int
|
|||
|
|
want string
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "single line",
|
|||
|
|
text: "hello",
|
|||
|
|
spaces: 2,
|
|||
|
|
want: " hello",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "multiple lines",
|
|||
|
|
text: "line1\nline2\nline3",
|
|||
|
|
spaces: 4,
|
|||
|
|
want: " line1\n line2\n line3",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "empty string",
|
|||
|
|
text: "",
|
|||
|
|
spaces: 4,
|
|||
|
|
want: "",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "zero spaces",
|
|||
|
|
text: "hello",
|
|||
|
|
spaces: 0,
|
|||
|
|
want: "hello",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "lines with empty lines in between",
|
|||
|
|
text: "line1\n\nline3",
|
|||
|
|
spaces: 2,
|
|||
|
|
want: " line1\n\n line3",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "trailing newline",
|
|||
|
|
text: "hello\n",
|
|||
|
|
spaces: 3,
|
|||
|
|
want: " hello\n",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got := Indent(tt.text, tt.spaces)
|
|||
|
|
if got != tt.want {
|
|||
|
|
t.Errorf("Indent() = %q, want %q", got, tt.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestIsTerminal(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
|
|||
|
|
// A buffer is not a terminal
|
|||
|
|
if IsTerminal(&buf) {
|
|||
|
|
t.Error("IsTerminal(buffer) = true, want false")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestIsTerminalWithFile(t *testing.T) {
|
|||
|
|
// /dev/null is a valid *os.File but not a terminal
|
|||
|
|
f, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Skipf("cannot open %s: %v", os.DevNull, err)
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
|
|||
|
|
// The result depends on the environment (TERM env var, etc.)
|
|||
|
|
// Just verify it doesn't panic and returns a bool
|
|||
|
|
result := IsTerminal(f)
|
|||
|
|
_ = result // should not panic
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintUsage(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintUsage(&buf)
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
sections := []string{
|
|||
|
|
"NAME",
|
|||
|
|
"SYNOPSIS",
|
|||
|
|
"DESCRIPTION",
|
|||
|
|
"TARGET",
|
|||
|
|
"PROTOCOLS",
|
|||
|
|
"EXAMPLES",
|
|||
|
|
}
|
|||
|
|
for _, section := range sections {
|
|||
|
|
if !strings.Contains(output, section) {
|
|||
|
|
t.Errorf("PrintUsage output missing section %q", section)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Should NOT contain ANSI codes since buffer is not a terminal
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintUsage output should not contain ANSI codes when writing to buffer")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestColorize(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
|
|||
|
|
// On a buffer (non-terminal), should return plain text
|
|||
|
|
result := Colorize(&buf, ColorRed, "error text")
|
|||
|
|
if result != "error text" {
|
|||
|
|
t.Errorf("Colorize(buffer) = %q, want %q (plain text)", result, "error text")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// progress.go tests
|
|||
|
|
|
|||
|
|
func TestFormatDuration(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
d time.Duration
|
|||
|
|
want string
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "zero duration",
|
|||
|
|
d: 0,
|
|||
|
|
want: "0s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "seconds only",
|
|||
|
|
d: 30 * time.Second,
|
|||
|
|
want: "30s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "minutes and seconds",
|
|||
|
|
d: 2*time.Minute + 15*time.Second,
|
|||
|
|
want: "2m15s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "hours and minutes",
|
|||
|
|
d: 3*time.Hour + 45*time.Minute,
|
|||
|
|
want: "3h45m",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "exactly one minute",
|
|||
|
|
d: 1 * time.Minute,
|
|||
|
|
want: "1m0s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "exactly one hour",
|
|||
|
|
d: 1 * time.Hour,
|
|||
|
|
want: "1h0m",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "single second",
|
|||
|
|
d: 1 * time.Second,
|
|||
|
|
want: "1s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "seconds rounded down",
|
|||
|
|
d: 59*time.Second + 999*time.Millisecond,
|
|||
|
|
want: "1m0s",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "rounds to nearest second",
|
|||
|
|
d: 500 * time.Millisecond,
|
|||
|
|
want: "1s",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got := formatDuration(tt.d)
|
|||
|
|
if got != tt.want {
|
|||
|
|
t.Errorf("formatDuration(%v) = %q, want %q", tt.d, got, tt.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParseInt(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
s string
|
|||
|
|
want int
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "simple number",
|
|||
|
|
s: "42",
|
|||
|
|
want: 42,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "zero",
|
|||
|
|
s: "0",
|
|||
|
|
want: 0,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "large number",
|
|||
|
|
s: "123456789",
|
|||
|
|
want: 123456789,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "string with trailing non-digits",
|
|||
|
|
s: "80columns",
|
|||
|
|
want: 80,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "empty string",
|
|||
|
|
s: "",
|
|||
|
|
want: 0,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "no digits",
|
|||
|
|
s: "abc",
|
|||
|
|
want: 0,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "mixed with non-digits at start",
|
|||
|
|
s: "abc123",
|
|||
|
|
want: 0,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "negative sign not supported by parseInt",
|
|||
|
|
s: "-5",
|
|||
|
|
want: 0,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "leading zeros",
|
|||
|
|
s: "007",
|
|||
|
|
want: 7,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got := parseInt(tt.s)
|
|||
|
|
if got != tt.want {
|
|||
|
|
t.Errorf("parseInt(%q) = %d, want %d", tt.s, got, tt.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestMax(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
a, b int
|
|||
|
|
want int
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "a greater",
|
|||
|
|
a: 10,
|
|||
|
|
b: 5,
|
|||
|
|
want: 10,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "b greater",
|
|||
|
|
a: 3,
|
|||
|
|
b: 7,
|
|||
|
|
want: 7,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "equal values",
|
|||
|
|
a: 42,
|
|||
|
|
b: 42,
|
|||
|
|
want: 42,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "negative values",
|
|||
|
|
a: -5,
|
|||
|
|
b: -10,
|
|||
|
|
want: -5,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "zero and positive",
|
|||
|
|
a: 0,
|
|||
|
|
b: 100,
|
|||
|
|
want: 100,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
got := max(tt.a, tt.b)
|
|||
|
|
if got != tt.want {
|
|||
|
|
t.Errorf("max(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNewProgressBarWithNilConfig(t *testing.T) {
|
|||
|
|
pb := NewProgressBar(nil)
|
|||
|
|
if pb == nil {
|
|||
|
|
t.Fatal("NewProgressBar(nil) returned nil")
|
|||
|
|
}
|
|||
|
|
if pb.config == nil {
|
|||
|
|
t.Fatal("NewProgressBar(nil) config is nil, should use defaults")
|
|||
|
|
}
|
|||
|
|
if pb.config.Total != -1 {
|
|||
|
|
t.Errorf("default Total = %d, want -1", pb.config.Total)
|
|||
|
|
}
|
|||
|
|
if pb.config.Writer != os.Stderr {
|
|||
|
|
t.Errorf("default Writer should be os.Stderr")
|
|||
|
|
}
|
|||
|
|
if pb.current != 0 {
|
|||
|
|
t.Errorf("current = %d, want 0", pb.current)
|
|||
|
|
}
|
|||
|
|
if pb.barWidth < 20 {
|
|||
|
|
t.Errorf("barWidth = %d, want >= 20", pb.barWidth)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNewProgressBarWithCustomConfig(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
onCompleteCalled := false
|
|||
|
|
onComplete := func() {
|
|||
|
|
onCompleteCalled = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 30,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
OnComplete: onComplete,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
if pb == nil {
|
|||
|
|
t.Fatal("NewProgressBar(cfg) returned nil")
|
|||
|
|
}
|
|||
|
|
if pb.config.Total != 1000 {
|
|||
|
|
t.Errorf("Total = %d, want 1000", pb.config.Total)
|
|||
|
|
}
|
|||
|
|
if pb.config.Width != 30 {
|
|||
|
|
t.Errorf("Width = %d, want 30", pb.config.Width)
|
|||
|
|
}
|
|||
|
|
if pb.config.Writer != &buf {
|
|||
|
|
t.Errorf("Writer mismatch")
|
|||
|
|
}
|
|||
|
|
if pb.barWidth != 30 {
|
|||
|
|
t.Errorf("barWidth = %d, want 30", pb.barWidth)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Call Finish which should trigger OnComplete
|
|||
|
|
pb.Finish()
|
|||
|
|
if !onCompleteCalled {
|
|||
|
|
t.Error("OnComplete was not called")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestNewProgressBarWithWidthZero(t *testing.T) {
|
|||
|
|
// Width 0 should auto-calculate based on terminal width
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 500,
|
|||
|
|
Width: 0,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: os.Stderr,
|
|||
|
|
}
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
if pb.barWidth < 20 {
|
|||
|
|
t.Errorf("auto-calculated barWidth = %d, want >= 20", pb.barWidth)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarUpdateDoesNotPanic(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
if pb == nil {
|
|||
|
|
t.Fatal("NewProgressBar returned nil")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Multiple updates should not panic
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Update panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(0)
|
|||
|
|
pb.Update(100)
|
|||
|
|
pb.Update(500)
|
|||
|
|
pb.Update(1000)
|
|||
|
|
|
|||
|
|
if pb.current != 1000 {
|
|||
|
|
t.Errorf("current = %d, want 1000", pb.current)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarUpdateExceedsTotal(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 100,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Update panicked when exceeding total: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(200)
|
|||
|
|
// current can exceed total, should not panic
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarFinishDoesNotPanic(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Finish panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(800)
|
|||
|
|
pb.Finish()
|
|||
|
|
|
|||
|
|
// After finish, current should equal total
|
|||
|
|
if pb.current != 1000 {
|
|||
|
|
t.Errorf("after Finish, current = %d, want %d (Total)", pb.current, 1000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Output should contain completion message
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "complete") {
|
|||
|
|
t.Errorf("Finish output = %q, want it to contain 'complete'", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarFinishWithUnknownTotal(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
// Total = -1 (unknown)
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: -1,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Finish with unknown total panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(500)
|
|||
|
|
pb.Finish()
|
|||
|
|
|
|||
|
|
// With unknown total, current should stay at the last value (not overwritten)
|
|||
|
|
if pb.current != 500 {
|
|||
|
|
t.Errorf("after Finish with Total=-1, current = %d, want %d", pb.current, 500)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarAddDoesNotPanic(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Add panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Add(100)
|
|||
|
|
pb.Add(200)
|
|||
|
|
pb.Add(300)
|
|||
|
|
|
|||
|
|
if pb.current != 600 {
|
|||
|
|
t.Errorf("after Add calls, current = %d, want 600", pb.current)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarCancelDoesNotPanic(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar.Cancel panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(300)
|
|||
|
|
pb.Cancel()
|
|||
|
|
|
|||
|
|
// Output should contain cancellation message
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "cancelled") {
|
|||
|
|
t.Errorf("Cancel output = %q, want it to contain 'cancelled'", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarWithColors(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 100,
|
|||
|
|
Width: 20,
|
|||
|
|
Colors: true,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("ProgressBar with colors panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
pb.Update(50)
|
|||
|
|
pb.Finish()
|
|||
|
|
|
|||
|
|
// Should not panic with colors enabled even on a buffer
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarWithOnComplete(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
callCount := 0
|
|||
|
|
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 100,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
OnComplete: func() {
|
|||
|
|
callCount++
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
pb.Finish()
|
|||
|
|
|
|||
|
|
if callCount != 1 {
|
|||
|
|
t.Errorf("OnComplete called %d times, want 1", callCount)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Calling Finish again should call OnComplete again
|
|||
|
|
pb.Finish()
|
|||
|
|
if callCount != 2 {
|
|||
|
|
t.Errorf("OnComplete called %d times after second Finish, want 2", callCount)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarUpdateAfterFinish(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 100,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
pb.Finish()
|
|||
|
|
|
|||
|
|
defer func() {
|
|||
|
|
if r := recover(); r != nil {
|
|||
|
|
t.Fatalf("Update after Finish panicked: %v", r)
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
// Update after finish should not panic
|
|||
|
|
pb.Update(999)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDefaultProgressBarConfig(t *testing.T) {
|
|||
|
|
cfg := DefaultProgressBarConfig()
|
|||
|
|
if cfg == nil {
|
|||
|
|
t.Fatal("DefaultProgressBarConfig() returned nil")
|
|||
|
|
}
|
|||
|
|
if cfg.Total != -1 {
|
|||
|
|
t.Errorf("Total = %d, want -1", cfg.Total)
|
|||
|
|
}
|
|||
|
|
if cfg.Width != 0 {
|
|||
|
|
t.Errorf("Width = %d, want 0", cfg.Width)
|
|||
|
|
}
|
|||
|
|
if !cfg.Colors {
|
|||
|
|
t.Error("Colors = false, want true")
|
|||
|
|
}
|
|||
|
|
if cfg.Writer != os.Stderr {
|
|||
|
|
t.Errorf("Writer should be os.Stderr")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintProgressInfo(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintProgressInfo(&buf, "connecting to server")
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "•") {
|
|||
|
|
t.Errorf("PrintProgressInfo output = %q, want it to contain '•'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "connecting to server") {
|
|||
|
|
t.Errorf("PrintProgressInfo output = %q, want it to contain 'connecting to server'", output)
|
|||
|
|
}
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintProgressInfo output should not contain ANSI codes when writing to buffer")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestPrintProgressSuccess(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
PrintProgressSuccess(&buf, "file saved")
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "✓") {
|
|||
|
|
t.Errorf("PrintProgressSuccess output = %q, want it to contain '✓'", output)
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(output, "file saved") {
|
|||
|
|
t.Errorf("PrintProgressSuccess output = %q, want it to contain 'file saved'", output)
|
|||
|
|
}
|
|||
|
|
if strings.Contains(output, "\033[") {
|
|||
|
|
t.Errorf("PrintProgressSuccess output should not contain ANSI codes when writing to buffer")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarRenderRateLimit(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
pb.lastUpdate = time.Now().Add(-100 * time.Millisecond) // Ensure update is not rate-limited
|
|||
|
|
|
|||
|
|
// This should trigger render since enough time passed
|
|||
|
|
pb.Update(500)
|
|||
|
|
|
|||
|
|
// Output should contain bar characters and percentage
|
|||
|
|
output := buf.String()
|
|||
|
|
if !strings.Contains(output, "%") {
|
|||
|
|
t.Errorf("render output = %q, want it to contain '%%'", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestProgressBarRenderRateLimitSkip(t *testing.T) {
|
|||
|
|
var buf bytes.Buffer
|
|||
|
|
cfg := &ProgressBarConfig{
|
|||
|
|
Total: 1000,
|
|||
|
|
Width: 40,
|
|||
|
|
Colors: false,
|
|||
|
|
Writer: &buf,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pb := NewProgressBar(cfg)
|
|||
|
|
pb.lastUpdate = time.Now() // Set lastUpdate to now so the next update is rate-limited
|
|||
|
|
|
|||
|
|
// This should NOT trigger render because of rate limiting
|
|||
|
|
pb.Update(100)
|
|||
|
|
|
|||
|
|
output := buf.String()
|
|||
|
|
if output != "" {
|
|||
|
|
t.Errorf("render should have been skipped due to rate limiting, but output = %q", output)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Integration: parser + flags
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
func TestParseArgsAndValidate(t *testing.T) {
|
|||
|
|
// Integration test: parse args and then validate required flags
|
|||
|
|
args := []string{"--url", "https://example.com", "--output", "file.zip", "--verbose"}
|
|||
|
|
parsed, err := ParseArgs(args)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("ParseArgs() error = %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Validate required fields
|
|||
|
|
if err := ValidateRequiredFlags(parsed, []string{"url"}); err != nil {
|
|||
|
|
t.Errorf("ValidateRequiredFlags() error = %v, want nil", err)
|
|||
|
|
}
|
|||
|
|
if err := ValidateRequiredFlags(parsed, []string{"url", "output"}); err != nil {
|
|||
|
|
t.Errorf("ValidateRequiredFlags() error = %v, want nil", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Missing required flag should fail
|
|||
|
|
if err := ValidateRequiredFlags(parsed, []string{"nonexistent"}); err == nil {
|
|||
|
|
t.Error("ValidateRequiredFlags() should have returned error for missing flag")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Verify parsed values
|
|||
|
|
if parsed["url"] != "https://example.com" {
|
|||
|
|
t.Errorf("parsed url = %q, want %q", parsed["url"], "https://example.com")
|
|||
|
|
}
|
|||
|
|
if parsed["output"] != "file.zip" {
|
|||
|
|
t.Errorf("parsed output = %q, want %q", parsed["output"], "file.zip")
|
|||
|
|
}
|
|||
|
|
if parsed["verbose"] != "true" {
|
|||
|
|
t.Errorf("parsed verbose = %q, want %q", parsed["verbose"], "true")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDefaultProgressBarConfigReturnsNewInstance(t *testing.T) {
|
|||
|
|
// Ensure multiple calls return independent instances
|
|||
|
|
cfg1 := DefaultProgressBarConfig()
|
|||
|
|
cfg2 := DefaultProgressBarConfig()
|
|||
|
|
|
|||
|
|
if cfg1 == cfg2 {
|
|||
|
|
t.Error("DefaultProgressBarConfig() should return new instances each call")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Modifying cfg1 should not affect cfg2
|
|||
|
|
cfg1.Total = 100
|
|||
|
|
if cfg2.Total != -1 {
|
|||
|
|
t.Error("modifying one config should not affect the other")
|
|||
|
|
}
|
|||
|
|
}
|