Add unit tests and test tasks
- Add main_test.go with tests for formatContextInfo, stripANSI, StatusInput parsing, getGitInfo, and getGiteaStatus - Add test, test:cover, test:fixture tasks to Taskfile - 45% code coverage
This commit is contained in:
38
Taskfile.yml
38
Taskfile.yml
@ -15,34 +15,44 @@ tasks:
|
|||||||
build:
|
build:
|
||||||
desc: Build with stripped symbols
|
desc: Build with stripped symbols
|
||||||
cmds:
|
cmds:
|
||||||
- go build -ldflags="{{.LDFLAGS}}" -o {{.BINARY}} .
|
- go build -ldflags="{{.LDFLAGS}}" -o bin/{{.BINARY}} .
|
||||||
sources:
|
sources:
|
||||||
- "*.go"
|
- "*.go"
|
||||||
- go.mod
|
- go.mod
|
||||||
- go.sum
|
- go.sum
|
||||||
generates:
|
generates:
|
||||||
- "{{.BINARY}}"
|
- "bin/{{.BINARY}}"
|
||||||
|
|
||||||
build:debug:
|
build:debug:
|
||||||
desc: Build with debug symbols
|
desc: Build with debug symbols
|
||||||
cmds:
|
cmds:
|
||||||
- go build -o {{.BINARY}} .
|
- go build -o bin/{{.BINARY}} .
|
||||||
|
|
||||||
run:
|
run:
|
||||||
desc: Run with test fixture
|
desc: Run with test fixture
|
||||||
deps: [build]
|
deps: [build]
|
||||||
cmds:
|
cmds:
|
||||||
- cat test/fixture.json | ./{{.BINARY}}
|
- cat test/fixture.json | ./bin/{{.BINARY}}
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
desc: Run Go unit tests
|
||||||
|
cmds:
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
test:cover:
|
||||||
|
desc: Run tests with coverage
|
||||||
|
cmds:
|
||||||
|
- go test -cover ./...
|
||||||
|
|
||||||
|
test:fixture:
|
||||||
desc: Run with test fixture and show output
|
desc: Run with test fixture and show output
|
||||||
deps: [build]
|
deps: [build]
|
||||||
cmds:
|
cmds:
|
||||||
- echo "=== Output ==="
|
- echo "=== Output ==="
|
||||||
- cat test/fixture.json | ./{{.BINARY}}
|
- cat test/fixture.json | ./bin/{{.BINARY}}
|
||||||
- echo ""
|
- echo ""
|
||||||
- echo "=== Timing (single run) ==="
|
- echo "=== Timing (single run) ==="
|
||||||
- time sh -c 'cat test/fixture.json | ./{{.BINARY}} > /dev/null'
|
- time sh -c 'cat test/fixture.json | ./bin/{{.BINARY}} > /dev/null'
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
desc: Benchmark Go vs Shell (100 runs)
|
desc: Benchmark Go vs Shell (100 runs)
|
||||||
@ -50,10 +60,10 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- |
|
- |
|
||||||
echo "=== Pure Go (100 runs) ==="
|
echo "=== Pure Go (100 runs) ==="
|
||||||
time for i in $(seq 1 100); do cat test/fixture.json | ./{{.BINARY}} >/dev/null; done
|
time for i in $(seq 1 100); do cat test/fixture.json | ./bin/{{.BINARY}} >/dev/null; done
|
||||||
echo ""
|
# echo ""
|
||||||
echo "=== Shell (100 runs) ==="
|
# echo "=== Shell (100 runs) ==="
|
||||||
time for i in $(seq 1 100); do cat test/fixture.json | ./statusline.sh >/dev/null; done
|
# time for i in $(seq 1 100); do cat test/fixture.json | ./statusline.sh >/dev/null; done
|
||||||
silent: false
|
silent: false
|
||||||
|
|
||||||
bench:go:
|
bench:go:
|
||||||
@ -62,7 +72,7 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- |
|
- |
|
||||||
echo "=== Pure Go (100 runs) ==="
|
echo "=== Pure Go (100 runs) ==="
|
||||||
time for i in $(seq 1 100); do cat test/fixture.json | ./{{.BINARY}} >/dev/null; done
|
time for i in $(seq 1 100); do cat test/fixture.json | ./bin/{{.BINARY}} >/dev/null; done
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
desc: Run go mod tidy
|
desc: Run go mod tidy
|
||||||
@ -92,18 +102,18 @@ tasks:
|
|||||||
clean:
|
clean:
|
||||||
desc: Remove built binary
|
desc: Remove built binary
|
||||||
cmds:
|
cmds:
|
||||||
- rm -f {{.BINARY}}
|
- rm -rf bin/
|
||||||
|
|
||||||
size:
|
size:
|
||||||
desc: Show binary size
|
desc: Show binary size
|
||||||
deps: [build]
|
deps: [build]
|
||||||
cmds:
|
cmds:
|
||||||
- ls -lh {{.BINARY}}
|
- ls -lh bin/{{.BINARY}}
|
||||||
|
|
||||||
install:
|
install:
|
||||||
desc: Install to ~/.claude/
|
desc: Install to ~/.claude/
|
||||||
deps: [build]
|
deps: [build]
|
||||||
cmds:
|
cmds:
|
||||||
- mkdir -p ~/.claude
|
- mkdir -p ~/.claude
|
||||||
- cp {{.BINARY}} ~/.claude/{{.BINARY}}
|
- cp bin/{{.BINARY}} ~/.claude/{{.BINARY}}
|
||||||
- echo "Installed to ~/.claude/{{.BINARY}}"
|
- echo "Installed to ~/.claude/{{.BINARY}}"
|
||||||
|
|||||||
205
main_test.go
Normal file
205
main_test.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatContextInfo_NilUsage(t *testing.T) {
|
||||||
|
result := formatContextInfo(200000, nil)
|
||||||
|
expected := "0/200k"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("formatContextInfo(200000, nil) = %q, want %q", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatContextInfo_WithUsage(t *testing.T) {
|
||||||
|
usage := &struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
CacheCreationTokens int `json:"cache_creation_input_tokens"`
|
||||||
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
|
}{
|
||||||
|
InputTokens: 8500,
|
||||||
|
CacheCreationTokens: 5000,
|
||||||
|
CacheReadInputTokens: 2000,
|
||||||
|
}
|
||||||
|
result := formatContextInfo(200000, usage)
|
||||||
|
expected := "15k/200k"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("formatContextInfo(200000, usage) = %q, want %q", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatContextInfo_SmallValues(t *testing.T) {
|
||||||
|
usage := &struct {
|
||||||
|
InputTokens int `json:"input_tokens"`
|
||||||
|
CacheCreationTokens int `json:"cache_creation_input_tokens"`
|
||||||
|
CacheReadInputTokens int `json:"cache_read_input_tokens"`
|
||||||
|
}{
|
||||||
|
InputTokens: 500,
|
||||||
|
CacheCreationTokens: 0,
|
||||||
|
CacheReadInputTokens: 0,
|
||||||
|
}
|
||||||
|
result := formatContextInfo(100000, usage)
|
||||||
|
expected := "0k/100k"
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("formatContextInfo(100000, usage) = %q, want %q", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripANSI(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no ansi",
|
||||||
|
input: "hello world",
|
||||||
|
expected: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single color",
|
||||||
|
input: "\033[32mgreen\033[0m",
|
||||||
|
expected: "green",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple colors",
|
||||||
|
input: "\033[31mred\033[0m \033[32mgreen\033[0m",
|
||||||
|
expected: "red green",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bold",
|
||||||
|
input: "\033[1mbold\033[0m",
|
||||||
|
expected: "bold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex",
|
||||||
|
input: "\033[32m●\033[0m \033[35mOpus\033[0m \033[1;32m➜\033[0m",
|
||||||
|
expected: "● Opus ➜",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := stripANSI(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusInputParsing(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"model": {"display_name": "Opus 4.5"},
|
||||||
|
"workspace": {"current_dir": "/root/projects/statusline"},
|
||||||
|
"context_window": {
|
||||||
|
"context_window_size": 200000,
|
||||||
|
"current_usage": {
|
||||||
|
"input_tokens": 5000,
|
||||||
|
"cache_creation_input_tokens": 1000,
|
||||||
|
"cache_read_input_tokens": 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
var data StatusInput
|
||||||
|
err := json.Unmarshal([]byte(jsonData), &data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Model.DisplayName != "Opus 4.5" {
|
||||||
|
t.Errorf("Model.DisplayName = %q, want %q", data.Model.DisplayName, "Opus 4.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Workspace.CurrentDir != "/root/projects/statusline" {
|
||||||
|
t.Errorf("Workspace.CurrentDir = %q, want %q", data.Workspace.CurrentDir, "/root/projects/statusline")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ContextWindow.ContextWindowSize != 200000 {
|
||||||
|
t.Errorf("ContextWindow.ContextWindowSize = %d, want %d", data.ContextWindow.ContextWindowSize, 200000)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ContextWindow.CurrentUsage == nil {
|
||||||
|
t.Fatal("ContextWindow.CurrentUsage is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ContextWindow.CurrentUsage.InputTokens != 5000 {
|
||||||
|
t.Errorf("CurrentUsage.InputTokens = %d, want %d", data.ContextWindow.CurrentUsage.InputTokens, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusInputParsing_NilUsage(t *testing.T) {
|
||||||
|
jsonData := `{
|
||||||
|
"model": {"display_name": "Sonnet"},
|
||||||
|
"workspace": {"current_dir": "/tmp"},
|
||||||
|
"context_window": {
|
||||||
|
"context_window_size": 100000
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
var data StatusInput
|
||||||
|
err := json.Unmarshal([]byte(jsonData), &data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ContextWindow.CurrentUsage != nil {
|
||||||
|
t.Errorf("ContextWindow.CurrentUsage should be nil, got %+v", data.ContextWindow.CurrentUsage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGitInfo_CurrentRepo(t *testing.T) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("Could not get working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := getGitInfo(cwd)
|
||||||
|
|
||||||
|
// Should return something like " git:(master)" or " git:(master) ✗"
|
||||||
|
if result == "" {
|
||||||
|
t.Skip("Not in a git repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(result, "git:(") {
|
||||||
|
t.Errorf("getGitInfo(%q) = %q, expected to contain 'git:('", cwd, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGitInfo_NonRepo(t *testing.T) {
|
||||||
|
result := getGitInfo("/tmp")
|
||||||
|
|
||||||
|
// /tmp is unlikely to be a git repo
|
||||||
|
if result != "" && !contains(result, "git:(") {
|
||||||
|
t.Errorf("getGitInfo(/tmp) = %q, expected empty or valid git info", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGiteaStatus(t *testing.T) {
|
||||||
|
result := getGiteaStatus()
|
||||||
|
|
||||||
|
// Should return either green or red dot
|
||||||
|
greenDot := green + "●" + reset
|
||||||
|
redDot := red + "●" + reset
|
||||||
|
|
||||||
|
if result != greenDot && result != redDot {
|
||||||
|
t.Errorf("getGiteaStatus() = %q, expected green or red dot", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsHelper(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user