Files
goget/internal/cli/cli_test.go
T

1155 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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")
}
}