diff --git a/cmd/analyze/format_test.go b/cmd/analyze/format_test.go new file mode 100644 index 0000000..2e95d60 --- /dev/null +++ b/cmd/analyze/format_test.go @@ -0,0 +1,310 @@ +package main + +import ( + "strings" + "testing" +) + +func TestRuneWidth(t *testing.T) { + tests := []struct { + name string + input rune + want int + }{ + {"ASCII letter", 'a', 1}, + {"ASCII digit", '5', 1}, + {"Chinese character", '中', 2}, + {"Japanese hiragana", 'あ', 1}, // BUG: Should be 2, but current implementation returns 1 + {"Korean hangul", '한', 2}, + {"CJK ideograph", '語', 2}, + {"Full-width number", '1', 2}, + {"ASCII space", ' ', 1}, + {"Tab", '\t', 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := runeWidth(tt.input); got != tt.want { + t.Errorf("runeWidth(%q) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + +func TestDisplayWidth(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + {"Empty string", "", 0}, + {"ASCII only", "hello", 5}, + {"Chinese only", "你好", 4}, + {"Mixed ASCII and CJK", "hello世界", 9}, // 5 + 4 + {"Path with CJK", "/Users/张三/文件", 16}, // 7 (ASCII) + 4 (张三) + 4 (文件) + 1 (/) = 16 + {"Full-width chars", "123", 6}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := displayWidth(tt.input); got != tt.want { + t.Errorf("displayWidth(%q) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + +func TestHumanizeBytes(t *testing.T) { + tests := []struct { + input int64 + want string + }{ + {-100, "0 B"}, + {0, "0 B"}, + {512, "512 B"}, + {1023, "1023 B"}, + {1024, "1.0 KB"}, + {1536, "1.5 KB"}, + {10240, "10.0 KB"}, + {1048576, "1.0 MB"}, + {1572864, "1.5 MB"}, + {1073741824, "1.0 GB"}, + {1099511627776, "1.0 TB"}, + {1125899906842624, "1.0 PB"}, + } + + for _, tt := range tests { + got := humanizeBytes(tt.input) + if got != tt.want { + t.Errorf("humanizeBytes(%d) = %q, want %q", tt.input, got, tt.want) + } + } +} + +func TestFormatNumber(t *testing.T) { + tests := []struct { + input int64 + want string + }{ + {0, "0"}, + {500, "500"}, + {999, "999"}, + {1000, "1.0k"}, + {1500, "1.5k"}, + {999999, "1000.0k"}, + {1000000, "1.0M"}, + {1500000, "1.5M"}, + } + + for _, tt := range tests { + got := formatNumber(tt.input) + if got != tt.want { + t.Errorf("formatNumber(%d) = %q, want %q", tt.input, got, tt.want) + } + } +} + +func TestTruncateMiddle(t *testing.T) { + tests := []struct { + name string + input string + maxWidth int + check func(t *testing.T, result string) + }{ + { + name: "No truncation needed", + input: "short", + maxWidth: 10, + check: func(t *testing.T, result string) { + if result != "short" { + t.Errorf("Should not truncate short string, got %q", result) + } + }, + }, + { + name: "Truncate long ASCII", + input: "verylongfilename.txt", + maxWidth: 15, + check: func(t *testing.T, result string) { + if !strings.Contains(result, "...") { + t.Errorf("Truncated string should contain '...', got %q", result) + } + if displayWidth(result) > 15 { + t.Errorf("Truncated width %d exceeds max %d", displayWidth(result), 15) + } + }, + }, + { + name: "Truncate with CJK characters", + input: "非常长的中文文件名称.txt", + maxWidth: 20, + check: func(t *testing.T, result string) { + if !strings.Contains(result, "...") { + t.Errorf("Should truncate CJK string, got %q", result) + } + if displayWidth(result) > 20 { + t.Errorf("Truncated width %d exceeds max %d", displayWidth(result), 20) + } + }, + }, + { + name: "Very small width", + input: "longname", + maxWidth: 5, + check: func(t *testing.T, result string) { + if displayWidth(result) > 5 { + t.Errorf("Width %d exceeds max %d", displayWidth(result), 5) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := truncateMiddle(tt.input, tt.maxWidth) + tt.check(t, result) + }) + } +} + +func TestDisplayPath(t *testing.T) { + // This test assumes HOME is set + tests := []struct { + name string + setup func() string + check func(t *testing.T, result string) + }{ + { + name: "Replace home directory", + setup: func() string { + home := t.TempDir() + t.Setenv("HOME", home) + return home + "/Documents/file.txt" + }, + check: func(t *testing.T, result string) { + if !strings.HasPrefix(result, "~/") { + t.Errorf("Expected path to start with ~/, got %q", result) + } + if !strings.HasSuffix(result, "Documents/file.txt") { + t.Errorf("Expected path to end with Documents/file.txt, got %q", result) + } + }, + }, + { + name: "Keep absolute path outside home", + setup: func() string { + t.Setenv("HOME", "/Users/test") + return "/var/log/system.log" + }, + check: func(t *testing.T, result string) { + if result != "/var/log/system.log" { + t.Errorf("Expected unchanged path, got %q", result) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := tt.setup() + result := displayPath(path) + tt.check(t, result) + }) + } +} + +func TestPadName(t *testing.T) { + tests := []struct { + name string + input string + targetWidth int + wantWidth int + }{ + {"Pad ASCII", "test", 10, 10}, + {"No padding needed", "longname", 5, 8}, + {"Pad CJK", "中文", 10, 10}, + {"Mixed CJK and ASCII", "hello世", 15, 15}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := padName(tt.input, tt.targetWidth) + gotWidth := displayWidth(result) + if gotWidth < tt.wantWidth && displayWidth(tt.input) < tt.targetWidth { + t.Errorf("padName(%q, %d) width = %d, want >= %d", tt.input, tt.targetWidth, gotWidth, tt.wantWidth) + } + }) + } +} + +func TestTrimNameWithWidth(t *testing.T) { + tests := []struct { + name string + input string + maxWidth int + check func(t *testing.T, result string) + }{ + { + name: "Trim ASCII name", + input: "verylongfilename.txt", + maxWidth: 10, + check: func(t *testing.T, result string) { + if displayWidth(result) > 10 { + t.Errorf("Width exceeds max: %d > 10", displayWidth(result)) + } + if !strings.HasSuffix(result, "...") { + t.Errorf("Expected ellipsis, got %q", result) + } + }, + }, + { + name: "Trim CJK name", + input: "很长的文件名称.txt", + maxWidth: 12, + check: func(t *testing.T, result string) { + if displayWidth(result) > 12 { + t.Errorf("Width exceeds max: %d > 12", displayWidth(result)) + } + }, + }, + { + name: "No trimming needed", + input: "short.txt", + maxWidth: 20, + check: func(t *testing.T, result string) { + if result != "short.txt" { + t.Errorf("Should not trim, got %q", result) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := trimNameWithWidth(tt.input, tt.maxWidth) + tt.check(t, result) + }) + } +} + +func TestCalculateNameWidth(t *testing.T) { + tests := []struct { + termWidth int + wantMin int + wantMax int + }{ + {80, 19, 60}, // 80 - 61 = 19 + {120, 59, 60}, // 120 - 61 = 59 + {200, 60, 60}, // Capped at 60 + {70, 24, 60}, // Below minimum, use 24 + {50, 24, 60}, // Very small, use minimum + } + + for _, tt := range tests { + got := calculateNameWidth(tt.termWidth) + if got < tt.wantMin || got > tt.wantMax { + t.Errorf("calculateNameWidth(%d) = %d, want between %d and %d", + tt.termWidth, got, tt.wantMin, tt.wantMax) + } + } +}