首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
@@ -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")
}
}