Files

1155 lines
26 KiB
Go
Raw Permalink Normal View History

//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")
}
}