首次推送
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
package teamspeak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSplitCommandRows_SingleLine(t *testing.T) {
|
||||
rows := splitCommandRows("clientlist")
|
||||
if len(rows) != 1 || rows[0] != "clientlist" {
|
||||
t.Errorf("unexpected rows: %v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCommandRows_NoArgsNoPipe(t *testing.T) {
|
||||
rows := splitCommandRows("hello key=val")
|
||||
if len(rows) != 1 || rows[0] != "hello key=val" {
|
||||
t.Errorf("unexpected rows: %v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCommandRows_PipeSplitsToPrefixedRows(t *testing.T) {
|
||||
rows := splitCommandRows("notifycliententerview clid=1|clid=2|clid=3")
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("expected 3 rows, got %d: %v", len(rows), rows)
|
||||
}
|
||||
if rows[0] != "notifycliententerview clid=1" {
|
||||
t.Errorf("row[0] = %q", rows[0])
|
||||
}
|
||||
if rows[1] != "notifycliententerview clid=2" {
|
||||
t.Errorf("row[1] = %q", rows[1])
|
||||
}
|
||||
if rows[2] != "notifycliententerview clid=3" {
|
||||
t.Errorf("row[2] = %q", rows[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCommandRows_EmptyPartSkipped(t *testing.T) {
|
||||
// Leading/trailing | in "rest" produces empty parts, which are skipped.
|
||||
rows := splitCommandRows("cmd a=1||b=2")
|
||||
// empty part skipped → 2 rows
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("expected 2 rows, got %d: %v", len(rows), rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitCommandRows_NoNameNoSpace(t *testing.T) {
|
||||
// A string without a space → treated as a single command with no args.
|
||||
rows := splitCommandRows("justcommand")
|
||||
if len(rows) != 1 || rows[0] != "justcommand" {
|
||||
t.Errorf("unexpected: %v", rows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAutoNicknameMatch_ExactMatch(t *testing.T) {
|
||||
if !isAutoNicknameMatch("Bot", "Bot") {
|
||||
t.Error("exact match should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAutoNicknameMatch_NumericSuffix(t *testing.T) {
|
||||
if !isAutoNicknameMatch("Bot", "Bot123") {
|
||||
t.Error("numeric suffix should match")
|
||||
}
|
||||
if !isAutoNicknameMatch("Bot", "Bot1") {
|
||||
t.Error("single digit suffix should match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAutoNicknameMatch_NonNumericSuffix(t *testing.T) {
|
||||
if isAutoNicknameMatch("Bot", "BotX") {
|
||||
t.Error("non-numeric suffix should not match")
|
||||
}
|
||||
if isAutoNicknameMatch("Bot", "Bot1a") {
|
||||
t.Error("alphanumeric suffix should not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAutoNicknameMatch_NoPrefixMatch(t *testing.T) {
|
||||
if isAutoNicknameMatch("Bot", "OtherBot") {
|
||||
t.Error("different prefix should not match")
|
||||
}
|
||||
if isAutoNicknameMatch("Bot", "") {
|
||||
t.Error("empty string should not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTracker_RegisterAndResolve(t *testing.T) {
|
||||
tr := newCommandTracker()
|
||||
|
||||
rc, ch := tr.register()
|
||||
if rc == 0 {
|
||||
t.Error("expected non-zero rc")
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
tr.resolve(rc, nil)
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-ch:
|
||||
if result.Err != nil {
|
||||
t.Errorf("unexpected error: %v", result.Err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("resolve timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTracker_CollectAndResolveWithData(t *testing.T) {
|
||||
tr := newCommandTracker()
|
||||
rc, ch := tr.register()
|
||||
|
||||
tr.collect(map[string]string{"k": "v"})
|
||||
tr.resolve(rc, nil)
|
||||
|
||||
result := <-ch
|
||||
if len(result.Data) != 1 || result.Data[0]["k"] != "v" {
|
||||
t.Errorf("unexpected data: %v", result.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTracker_UnregisterPreventsFire(t *testing.T) {
|
||||
tr := newCommandTracker()
|
||||
rc, ch := tr.register()
|
||||
tr.unregister(rc)
|
||||
// Resolving after unregister should be a no-op.
|
||||
tr.resolve(rc, nil)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Error("channel should not receive after unregister")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandTracker_Reset(t *testing.T) {
|
||||
tr := newCommandTracker()
|
||||
_, _ = tr.register()
|
||||
_, _ = tr.register()
|
||||
tr.reset()
|
||||
|
||||
// After reset, pending map should be empty; new resolve is a no-op.
|
||||
tr.resolve(1, nil)
|
||||
tr.resolve(2, nil)
|
||||
}
|
||||
|
||||
func TestCommandTracker_RCMonotoneIncreasing(t *testing.T) {
|
||||
tr := newCommandTracker()
|
||||
rc1, _ := tr.register()
|
||||
rc2, _ := tr.register()
|
||||
if rc2 <= rc1 {
|
||||
t.Errorf("expected rc2 > rc1, got rc1=%d rc2=%d", rc1, rc2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandThrottle_InitialTokensAllowImmediate(t *testing.T) {
|
||||
th := newCommandThrottle()
|
||||
// Should not block with fresh tokens.
|
||||
ctx := context.Background()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_ = th.wait(ctx)
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("wait blocked unexpectedly on fresh throttle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandThrottle_ContextCancelUnblocks(t *testing.T) {
|
||||
th := newCommandThrottle()
|
||||
// Drain all tokens (default 5, max 8).
|
||||
ctx := context.Background()
|
||||
for range 8 {
|
||||
_ = th.wait(ctx)
|
||||
}
|
||||
|
||||
// Now tokens are exhausted; cancel should unblock.
|
||||
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- th.wait(cancelCtx)
|
||||
}()
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("expected context.Canceled, got %v", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("wait did not unblock after context cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandThrottle_Concurrent(t *testing.T) {
|
||||
th := newCommandThrottle()
|
||||
var count atomic.Int32
|
||||
ctx := context.Background()
|
||||
start := make(chan struct{})
|
||||
|
||||
const goroutines = 5
|
||||
done := make(chan struct{}, goroutines)
|
||||
for range goroutines {
|
||||
go func() {
|
||||
<-start
|
||||
_ = th.wait(ctx)
|
||||
count.Add(1)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
}
|
||||
close(start)
|
||||
|
||||
for range goroutines {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("throttle wait did not complete (got %d/%d)", count.Load(), goroutines)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
if count.Load() != goroutines {
|
||||
t.Errorf("expected %d completions, got %d", goroutines, count.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUint64Value_Valid(t *testing.T) {
|
||||
v, err := parseUint64Value("42")
|
||||
if err != nil || v != 42 {
|
||||
t.Fatalf("expected 42 with nil error, got value=%d err=%v", v, err)
|
||||
}
|
||||
v, err = parseUint64Value("0")
|
||||
if err != nil || v != 0 {
|
||||
t.Fatalf("expected 0 with nil error, got value=%d err=%v", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUint64Value_Invalid_ReturnsError(t *testing.T) {
|
||||
_, err := parseUint64Value("abc")
|
||||
if err == nil {
|
||||
t.Error("expected parse error for invalid input")
|
||||
}
|
||||
_, err = parseUint64Value("")
|
||||
if err == nil {
|
||||
t.Error("expected parse error for empty input")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user