326 lines
7.6 KiB
Go
326 lines
7.6 KiB
Go
package sglang
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
// TestClientConfig tests ClientConfig validation
|
|
func TestClientConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config ClientConfig
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid config",
|
|
config: ClientConfig{
|
|
Endpoint: "grpc://localhost:20000",
|
|
TokenizerPath: "/path/to/tokenizer",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing endpoint",
|
|
config: ClientConfig{
|
|
Endpoint: "",
|
|
TokenizerPath: "/path/to/tokenizer",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing tokenizer path",
|
|
config: ClientConfig{
|
|
Endpoint: "grpc://localhost:20000",
|
|
TokenizerPath: "",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "both missing",
|
|
config: ClientConfig{
|
|
Endpoint: "",
|
|
TokenizerPath: "",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := NewClient(tt.config)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestChatMessageTypes tests ChatMessage struct and its variants
|
|
func TestChatMessageTypes(t *testing.T) {
|
|
msg := ChatMessage{
|
|
Role: "user",
|
|
Content: "Hello",
|
|
}
|
|
|
|
if msg.Role != "user" {
|
|
t.Errorf("Expected role 'user', got '%s'", msg.Role)
|
|
}
|
|
if msg.Content != "Hello" {
|
|
t.Errorf("Expected content 'Hello', got '%s'", msg.Content)
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionRequestValidation tests ChatCompletionRequest validation
|
|
func TestChatCompletionRequestValidation(t *testing.T) {
|
|
// Test valid request
|
|
req := ChatCompletionRequest{
|
|
Model: "default",
|
|
Messages: []ChatMessage{
|
|
{Role: "user", Content: "test"},
|
|
},
|
|
Stream: false,
|
|
}
|
|
|
|
if req.Model == "" {
|
|
t.Error("Expected model to be set")
|
|
}
|
|
|
|
if len(req.Messages) == 0 {
|
|
t.Error("Expected messages to be non-empty")
|
|
}
|
|
|
|
if req.Messages[0].Role != "user" {
|
|
t.Errorf("Expected first message role 'user', got '%s'", req.Messages[0].Role)
|
|
}
|
|
}
|
|
|
|
// TestClientClose tests that Close can be called multiple times safely
|
|
func TestClientClose(t *testing.T) {
|
|
// Create a mock client (note: in real tests, you might want to skip this
|
|
// if it requires actual server connection)
|
|
config := ClientConfig{
|
|
Endpoint: "grpc://localhost:20000",
|
|
TokenizerPath: "/path/to/tokenizer",
|
|
}
|
|
|
|
// Skip if connection fails (expected in unit test environment)
|
|
client, err := NewClient(config)
|
|
if err != nil {
|
|
t.Skip("Skipping client close test: server not available")
|
|
}
|
|
|
|
// First close should succeed
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("First Close() failed: %v", err)
|
|
}
|
|
|
|
// Second close should also succeed (idempotent)
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("Second Close() failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestChatCompletionResponseTypes tests response type structures
|
|
func TestChatCompletionResponseTypes(t *testing.T) {
|
|
resp := ChatCompletionResponse{
|
|
ID: "test-id",
|
|
Model: "default",
|
|
Created: 1234567890,
|
|
Choices: []Choice{
|
|
{
|
|
Message: Message{
|
|
Role: "assistant",
|
|
Content: "Hello",
|
|
},
|
|
FinishReason: "stop",
|
|
},
|
|
},
|
|
Usage: Usage{
|
|
PromptTokens: 10,
|
|
CompletionTokens: 20,
|
|
TotalTokens: 30,
|
|
},
|
|
}
|
|
|
|
if resp.ID != "test-id" {
|
|
t.Errorf("Expected ID 'test-id', got '%s'", resp.ID)
|
|
}
|
|
|
|
if len(resp.Choices) != 1 {
|
|
t.Errorf("Expected 1 choice, got %d", len(resp.Choices))
|
|
}
|
|
|
|
if resp.Choices[0].Message.Content != "Hello" {
|
|
t.Errorf("Expected content 'Hello', got '%s'", resp.Choices[0].Message.Content)
|
|
}
|
|
|
|
if resp.Usage.TotalTokens != 30 {
|
|
t.Errorf("Expected total tokens 30, got %d", resp.Usage.TotalTokens)
|
|
}
|
|
}
|
|
|
|
// TestStreamingResponseTypes tests streaming response structures
|
|
func TestStreamingResponseTypes(t *testing.T) {
|
|
chunk := ChatCompletionStreamResponse{
|
|
ID: "stream-id",
|
|
Created: 1234567890,
|
|
Choices: []StreamChoice{
|
|
{
|
|
Index: 0,
|
|
Delta: MessageDelta{
|
|
Content: "Hello",
|
|
},
|
|
FinishReason: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
if chunk.ID != "stream-id" {
|
|
t.Errorf("Expected ID 'stream-id', got '%s'", chunk.ID)
|
|
}
|
|
|
|
if len(chunk.Choices) == 0 {
|
|
t.Error("Expected at least one choice")
|
|
}
|
|
|
|
if chunk.Choices[0].Delta.Content != "Hello" {
|
|
t.Errorf("Expected delta content 'Hello', got '%s'", chunk.Choices[0].Delta.Content)
|
|
}
|
|
}
|
|
|
|
// TestToolCallStructure tests Tool and ToolCall structures
|
|
func TestToolCallStructure(t *testing.T) {
|
|
tool := Tool{
|
|
Type: "function",
|
|
Function: Function{
|
|
Name: "get_weather",
|
|
Description: "Get the weather",
|
|
Parameters: map[string]interface{}{
|
|
"location": "string",
|
|
},
|
|
},
|
|
}
|
|
|
|
if tool.Type != "function" {
|
|
t.Errorf("Expected tool type 'function', got '%s'", tool.Type)
|
|
}
|
|
|
|
if tool.Function.Name != "get_weather" {
|
|
t.Errorf("Expected function name 'get_weather', got '%s'", tool.Function.Name)
|
|
}
|
|
|
|
toolCall := ToolCall{
|
|
ID: "call-123",
|
|
Type: "function",
|
|
Function: FunctionCall{
|
|
Name: "get_weather",
|
|
Arguments: `{"location": "San Francisco"}`,
|
|
},
|
|
}
|
|
|
|
if toolCall.ID != "call-123" {
|
|
t.Errorf("Expected tool call ID 'call-123', got '%s'", toolCall.ID)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentClientOperations tests thread safety
|
|
// This is a basic test that just verifies concurrent calls don't panic
|
|
func TestConcurrentClientOperations(t *testing.T) {
|
|
config := ClientConfig{
|
|
Endpoint: "grpc://localhost:20000",
|
|
TokenizerPath: "/path/to/tokenizer",
|
|
}
|
|
|
|
client, err := NewClient(config)
|
|
if err != nil {
|
|
t.Skip("Skipping concurrent operations test: server not available")
|
|
}
|
|
defer client.Close()
|
|
|
|
// Try concurrent Close calls (should not panic or race)
|
|
done := make(chan bool, 2)
|
|
|
|
go func() {
|
|
client.Close()
|
|
done <- true
|
|
}()
|
|
|
|
go func() {
|
|
client.Close()
|
|
done <- true
|
|
}()
|
|
|
|
<-done
|
|
<-done
|
|
}
|
|
|
|
// BenchmarkChatCompletionRequest benchmarks request creation
|
|
func BenchmarkChatCompletionRequest(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
_ = ChatCompletionRequest{
|
|
Model: "default",
|
|
Messages: []ChatMessage{
|
|
{Role: "user", Content: "test message"},
|
|
},
|
|
Stream: false,
|
|
Temperature: floatPtr(0.7),
|
|
MaxCompletionTokens: intPtr(100),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions for benchmarks
|
|
func floatPtr(f float32) *float32 {
|
|
return &f
|
|
}
|
|
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
// TestContextCancellation tests that cancelled context is handled gracefully.
|
|
//
|
|
// NOTE: Currently, the FFI layer is blocking and doesn't actively monitor context cancellation.
|
|
// This test verifies that the client at least returns an error rather than panicking or
|
|
// hanging indefinitely when a pre-cancelled context is passed.
|
|
//
|
|
// Future: When FFI supports context cancellation (via signals or async operations),
|
|
// this test should be updated to assert that the error is context.Canceled or wrapped
|
|
// context cancellation error.
|
|
func TestContextCancellation(t *testing.T) {
|
|
config := ClientConfig{
|
|
Endpoint: "grpc://localhost:20000",
|
|
TokenizerPath: "/path/to/tokenizer",
|
|
}
|
|
|
|
client, err := NewClient(config)
|
|
if err != nil {
|
|
t.Skip("Skipping context cancellation test: server not available")
|
|
}
|
|
defer client.Close()
|
|
|
|
// Create a pre-cancelled context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
req := ChatCompletionRequest{
|
|
Model: "default",
|
|
Messages: []ChatMessage{
|
|
{Role: "user", Content: "test"},
|
|
},
|
|
}
|
|
|
|
// Attempt request with cancelled context
|
|
// Since FFI is blocking, we expect either:
|
|
// 1. An error from the server/network
|
|
// 2. The call to complete normally (FFI doesn't check context)
|
|
// What we DON'T expect is a panic or indefinite hang
|
|
_, err = client.CreateChatCompletion(ctx, req)
|
|
if err != nil {
|
|
t.Logf("Request with cancelled context returned error: %v", err)
|
|
} else {
|
|
t.Logf("Request with cancelled context completed (FFI may not support context cancellation)")
|
|
}
|
|
}
|