Add test coverage

This commit is contained in:
Daisuke Maki 2026-02-17 07:00:32 +09:00
parent e1164500e6
commit 4c439c71a8
4 changed files with 498 additions and 0 deletions

67
caret_test.go Normal file
View file

@ -0,0 +1,67 @@
package peco
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCaretInitialPos(t *testing.T) {
t.Parallel()
var c Caret
require.Equal(t, 0, c.Pos())
}
func TestCaretSetPos(t *testing.T) {
t.Parallel()
var c Caret
c.SetPos(5)
require.Equal(t, 5, c.Pos())
c.SetPos(0)
require.Equal(t, 0, c.Pos())
c.SetPos(100)
require.Equal(t, 100, c.Pos())
}
func TestCaretMove(t *testing.T) {
t.Parallel()
var c Caret
c.SetPos(5)
c.Move(3)
require.Equal(t, 8, c.Pos())
c.Move(-2)
require.Equal(t, 6, c.Pos())
// Move to negative territory
c.Move(-10)
require.Equal(t, -4, c.Pos())
}
func TestCaretMoveFromZero(t *testing.T) {
t.Parallel()
var c Caret
c.Move(1)
require.Equal(t, 1, c.Pos())
c.Move(-1)
require.Equal(t, 0, c.Pos())
}
func TestCaretMultipleMoves(t *testing.T) {
t.Parallel()
var c Caret
for i := 0; i < 10; i++ {
c.Move(1)
}
require.Equal(t, 10, c.Pos())
for i := 0; i < 5; i++ {
c.Move(-1)
}
require.Equal(t, 5, c.Pos())
}

View file

@ -0,0 +1,61 @@
package buffer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetLineListBuf(t *testing.T) {
t.Parallel()
buf := GetLineListBuf()
require.NotNil(t, buf)
require.Equal(t, 0, len(buf))
require.Equal(t, DefaultFilterBufSize, cap(buf))
}
func TestReleaseAndGetLineListBuf(t *testing.T) {
t.Parallel()
// Get a buffer, use it, release it, get it back
buf := GetLineListBuf()
require.NotNil(t, buf)
// Release it back to the pool
ReleaseLineListBuf(buf)
// Get another one — may or may not be the same one (pool behavior is not guaranteed)
buf2 := GetLineListBuf()
require.NotNil(t, buf2)
require.Equal(t, 0, len(buf2))
}
func TestReleaseLineListBufNil(t *testing.T) {
t.Parallel()
// Should not panic
ReleaseLineListBuf(nil)
}
func TestReleaseLineListBufResetsLength(t *testing.T) {
t.Parallel()
buf := GetLineListBuf()
// Simulate usage — we can't easily append real line.Line values
// without importing the line package's internals, but we can verify
// that the pool resets the slice length to 0 on release.
// The ReleaseLineListBuf function does: l = l[0:0]
// This is verified by the pool returning zero-length slices.
ReleaseLineListBuf(buf)
buf2 := GetLineListBuf()
require.Equal(t, 0, len(buf2))
}
func TestMultipleGetReleaseCycles(t *testing.T) {
t.Parallel()
for i := 0; i < 10; i++ {
buf := GetLineListBuf()
require.NotNil(t, buf)
require.Equal(t, 0, len(buf))
ReleaseLineListBuf(buf)
}
}

173
internal/util/util_test.go Normal file
View file

@ -0,0 +1,173 @@
package util
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestCaseInsensitiveIndexFunc(t *testing.T) {
t.Parallel()
tests := []struct {
name string
r rune
matchR rune
matches bool
}{
{"same lowercase", 'a', 'a', true},
{"upper matches lower", 'a', 'A', true},
{"lower matches upper", 'A', 'a', true},
{"different chars", 'a', 'b', false},
{"unicode same", 'ä', 'Ä', true},
{"digit matches itself", '1', '1', true},
{"digit vs letter", '1', 'a', false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fn := CaseInsensitiveIndexFunc(tt.r)
require.Equal(t, tt.matches, fn(tt.matchR))
})
}
}
func TestContainsUpper(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected bool
}{
{"all lowercase", "hello", false},
{"all uppercase", "HELLO", true},
{"mixed case", "hEllo", true},
{"empty string", "", false},
{"numbers only", "12345", false},
{"with uppercase at end", "hellO", true},
{"with uppercase at start", "Hello", true},
{"unicode lowercase", "こんにちは", false},
{"special chars", "!@#$%", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, ContainsUpper(tt.input))
})
}
}
func TestStripANSISequence(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{"no ANSI", "hello", "hello"},
{"empty string", "", ""},
{"bold", "\x1B[1mhello\x1B[0m", "hello"},
{"color red", "\x1B[31mred text\x1B[0m", "red text"},
{"multiple sequences", "\x1B[1m\x1B[31mhello\x1B[0m", "hello"},
{"color with semicolon", "\x1B[1;31mbold red\x1B[0m", "bold red"},
{"mixed content", "before\x1B[32mgreen\x1B[0mafter", "beforegreenafter"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, StripANSISequence(tt.input))
})
}
}
// Mock error types for testing error interface checks
type mockIgnorableError struct {
ignorable bool
msg string
}
func (e *mockIgnorableError) Error() string { return e.msg }
func (e *mockIgnorableError) Ignorable() bool { return e.ignorable }
type mockCollectResultsError struct {
collect bool
msg string
}
func (e *mockCollectResultsError) Error() string { return e.msg }
func (e *mockCollectResultsError) CollectResults() bool { return e.collect }
type mockExitStatusError struct {
status int
msg string
}
func (e *mockExitStatusError) Error() string { return e.msg }
func (e *mockExitStatusError) ExitStatus() int { return e.status }
func TestIsIgnorableError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expected bool
}{
{"ignorable true", &mockIgnorableError{ignorable: true, msg: "test"}, true},
{"ignorable false", &mockIgnorableError{ignorable: false, msg: "test"}, false},
{"non-ignorable error", errors.New("plain error"), false},
{"wrapped ignorable", fmt.Errorf("wrapper: %w", &mockIgnorableError{ignorable: true, msg: "inner"}), true},
{"wrapped non-ignorable", fmt.Errorf("wrapper: %w", errors.New("plain")), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, IsIgnorableError(tt.err))
})
}
}
func TestIsCollectResultsError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expected bool
}{
{"collect true", &mockCollectResultsError{collect: true, msg: "test"}, true},
{"collect false", &mockCollectResultsError{collect: false, msg: "test"}, false},
{"plain error", errors.New("plain error"), false},
{"wrapped collect", fmt.Errorf("wrapper: %w", &mockCollectResultsError{collect: true, msg: "inner"}), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.expected, IsCollectResultsError(tt.err))
})
}
}
func TestGetExitStatus(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
expectedStatus int
expectedFound bool
}{
{"exit status 0", &mockExitStatusError{status: 0, msg: "test"}, 0, true},
{"exit status 1", &mockExitStatusError{status: 1, msg: "test"}, 1, true},
{"exit status 42", &mockExitStatusError{status: 42, msg: "test"}, 42, true},
{"plain error", errors.New("plain"), 1, false},
{"wrapped exit status", fmt.Errorf("wrapper: %w", &mockExitStatusError{status: 2, msg: "inner"}), 2, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
status, found := GetExitStatus(tt.err)
require.Equal(t, tt.expectedStatus, status)
require.Equal(t, tt.expectedFound, found)
})
}
}

197
query_test.go Normal file
View file

@ -0,0 +1,197 @@
package peco
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestQuerySetAndString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
}{
{"empty string", ""},
{"ascii", "hello"},
{"unicode", "こんにちは"},
{"mixed", "hello世界"},
{"with spaces", "hello world"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var q Query
q.Set(tt.input)
require.Equal(t, tt.input, q.String())
})
}
}
func TestQueryLen(t *testing.T) {
t.Parallel()
var q Query
require.Equal(t, 0, q.Len())
q.Set("hello")
require.Equal(t, 5, q.Len())
// Unicode: each character is one rune
q.Set("こんにちは")
require.Equal(t, 5, q.Len())
}
func TestQueryReset(t *testing.T) {
t.Parallel()
var q Query
q.Set("hello")
require.Equal(t, 5, q.Len())
q.Reset()
require.Equal(t, 0, q.Len())
require.Equal(t, "", q.String())
}
func TestQuerySaveAndRestore(t *testing.T) {
t.Parallel()
var q Query
q.Set("original")
q.SaveQuery()
// After save, query is cleared
require.Equal(t, "", q.String())
// Setting new query while saved
q.Set("temporary")
require.Equal(t, "temporary", q.String())
// Restore brings back the saved query
q.RestoreSavedQuery()
require.Equal(t, "original", q.String())
}
func TestQuerySaveAndRestoreEmpty(t *testing.T) {
t.Parallel()
var q Query
// Save an empty query
q.SaveQuery()
require.Equal(t, "", q.String())
q.Set("something")
q.RestoreSavedQuery()
require.Equal(t, "", q.String())
}
func TestQueryDeleteRange(t *testing.T) {
t.Parallel()
tests := []struct {
name string
initial string
start int
end int
expected string
}{
{"delete middle", "abcdef", 2, 4, "abef"},
{"delete from start", "abcdef", 0, 3, "def"},
{"delete to end", "abcdef", 3, 6, "abc"},
{"delete all", "abcdef", 0, 6, ""},
{"delete single char", "abcdef", 2, 3, "abdef"},
{"start is -1 (no-op)", "abcdef", -1, 3, "abcdef"},
{"start > end (no-op)", "abcdef", 4, 2, "abcdef"},
{"end beyond length (clamped)", "abcdef", 4, 100, "abcd"},
{"unicode delete", "あいうえお", 1, 3, "あえお"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var q Query
q.Set(tt.initial)
q.DeleteRange(tt.start, tt.end)
require.Equal(t, tt.expected, q.String())
})
}
}
func TestQueryRuneSlice(t *testing.T) {
t.Parallel()
var q Query
q.Set("hello")
runes := q.RuneSlice()
require.Equal(t, []rune("hello"), runes)
// Verify it's a copy - modifying the returned slice shouldn't affect the query
runes[0] = 'H'
require.Equal(t, "hello", q.String())
}
func TestQueryRuneSliceEmpty(t *testing.T) {
t.Parallel()
var q Query
runes := q.RuneSlice()
require.Empty(t, runes)
}
func TestQueryRuneAt(t *testing.T) {
t.Parallel()
var q Query
q.Set("hello")
require.Equal(t, 'h', q.RuneAt(0))
require.Equal(t, 'e', q.RuneAt(1))
require.Equal(t, 'o', q.RuneAt(4))
}
func TestQueryRuneAtUnicode(t *testing.T) {
t.Parallel()
var q Query
q.Set("あいう")
require.Equal(t, 'あ', q.RuneAt(0))
require.Equal(t, 'い', q.RuneAt(1))
require.Equal(t, 'う', q.RuneAt(2))
}
func TestQueryInsertAt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
initial string
ch rune
where int
expected string
}{
{"insert at beginning", "hello", 'X', 0, "Xhello"},
{"insert at end (append)", "hello", 'X', 5, "helloX"},
{"insert in middle", "hello", 'X', 2, "heXllo"},
{"insert into empty", "", 'X', 0, "X"},
{"insert unicode", "hello", 'あ', 2, "heあllo"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var q Query
q.Set(tt.initial)
q.InsertAt(tt.ch, tt.where)
require.Equal(t, tt.expected, q.String())
})
}
}
func TestQueryMultipleInserts(t *testing.T) {
t.Parallel()
var q Query
// Build "abc" by inserting one character at a time
q.InsertAt('a', 0)
q.InsertAt('b', 1)
q.InsertAt('c', 2)
require.Equal(t, "abc", q.String())
// Insert at beginning
q.InsertAt('0', 0)
require.Equal(t, "0abc", q.String())
// Insert in middle
q.InsertAt('X', 2)
require.Equal(t, "0aXbc", q.String())
}