mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-10 17:43:00 +08:00
feat: enhance sleep functionality with start time support
This commit is contained in:
@@ -1 +1 @@
|
||||
v5.0.0-250808
|
||||
v5.0.0-250809
|
||||
|
||||
@@ -284,8 +284,9 @@ func getSimulationDuration(params []float64) (milliseconds int64) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// sleepStrict sleeps strict duration with given params
|
||||
// startTime is used to correct sleep duration caused by process time
|
||||
// sleepStrict sleeps for strict duration with optional start time correction
|
||||
// If startTime is zero, acts as normal context-aware sleep
|
||||
// If startTime is provided, corrects sleep duration by subtracting elapsed time
|
||||
// ctx allows for cancellation during sleep
|
||||
func sleepStrict(ctx context.Context, startTime time.Time, strictMilliseconds int64) {
|
||||
var elapsed int64
|
||||
|
||||
@@ -15,7 +15,29 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
// ToolSleep implements the sleep tool call.
|
||||
// extractStartTimeMs extracts start_time_ms from MCP request arguments
|
||||
// Returns time.Time (zero if not provided) and any conversion error
|
||||
func extractStartTimeMs(request mcp.CallToolRequest) (time.Time, error) {
|
||||
startTimeMs, ok := request.GetArguments()["start_time_ms"]
|
||||
if !ok || startTimeMs == nil {
|
||||
return time.Time{}, nil // Return zero time for normal sleep
|
||||
}
|
||||
|
||||
var ms int64
|
||||
switch v := startTimeMs.(type) {
|
||||
case float64:
|
||||
ms = int64(v)
|
||||
case int64:
|
||||
ms = v
|
||||
case int:
|
||||
ms = int64(v)
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("invalid start_time_ms type: %T", v)
|
||||
}
|
||||
|
||||
return time.UnixMilli(ms), nil
|
||||
}
|
||||
|
||||
type ToolSleep struct {
|
||||
// Return data fields - these define the structure of data returned by this tool
|
||||
Seconds float64 `json:"seconds" desc:"Duration in seconds that was slept"`
|
||||
@@ -33,6 +55,7 @@ func (t *ToolSleep) Description() string {
|
||||
func (t *ToolSleep) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("seconds", mcp.Description("Number of seconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,16 +93,15 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
milliseconds := int64(actualSeconds * 1000)
|
||||
sleepStrict(ctx, startTime, milliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %v seconds", actualSeconds)
|
||||
returnData := ToolSleep{
|
||||
Seconds: actualSeconds,
|
||||
@@ -91,9 +113,24 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleep) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{
|
||||
"seconds": action.Params,
|
||||
arguments := map[string]any{}
|
||||
|
||||
var seconds float64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
seconds, _ = param.Float64()
|
||||
arguments["seconds"] = seconds
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
seconds = float64(param)
|
||||
arguments["seconds"] = seconds
|
||||
} else if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both seconds and startTime
|
||||
seconds = sleepConfig.Seconds
|
||||
arguments["seconds"] = seconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep params: %v", action.Params)
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
@@ -115,6 +152,7 @@ func (t *ToolSleepMS) Description() string {
|
||||
func (t *ToolSleepMS) Options() []mcp.ToolOption {
|
||||
return []mcp.ToolOption{
|
||||
mcp.WithNumber("milliseconds", mcp.Description("Number of milliseconds to sleep")),
|
||||
mcp.WithNumber("start_time_ms", mcp.Description("Start time as Unix milliseconds for strict sleep calculation")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,16 +190,14 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
return nil, fmt.Errorf("unsupported sleep duration type: %T", v)
|
||||
}
|
||||
|
||||
// Use context-aware sleep instead of blocking time.Sleep
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
// Extract start_time_ms and use sleepStrict for unified sleep logic
|
||||
startTime, err := extractStartTimeMs(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sleepStrict(ctx, startTime, actualMilliseconds)
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %d milliseconds", actualMilliseconds)
|
||||
returnData := ToolSleepMS{
|
||||
Milliseconds: actualMilliseconds,
|
||||
@@ -173,17 +209,24 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
}
|
||||
|
||||
func (t *ToolSleepMS) ConvertActionToCallToolRequest(action option.MobileAction) (mcp.CallToolRequest, error) {
|
||||
arguments := map[string]any{}
|
||||
|
||||
var milliseconds int64
|
||||
if param, ok := action.Params.(json.Number); ok {
|
||||
milliseconds, _ = param.Int64()
|
||||
arguments["milliseconds"] = milliseconds
|
||||
} else if param, ok := action.Params.(int64); ok {
|
||||
milliseconds = param
|
||||
arguments["milliseconds"] = milliseconds
|
||||
} else if sleepConfig, ok := action.Params.(SleepConfig); ok {
|
||||
// When startTime is provided, pass both milliseconds and startTime
|
||||
milliseconds = sleepConfig.Milliseconds
|
||||
arguments["milliseconds"] = milliseconds
|
||||
arguments["start_time_ms"] = sleepConfig.StartTime.UnixMilli()
|
||||
} else {
|
||||
return mcp.CallToolRequest{}, fmt.Errorf("invalid sleep ms params: %v", action.Params)
|
||||
}
|
||||
arguments := map[string]any{
|
||||
"milliseconds": milliseconds,
|
||||
}
|
||||
|
||||
return BuildMCPCallToolRequest(t.Name(), arguments, action), nil
|
||||
}
|
||||
|
||||
|
||||
240
uixt/mcp_tools_utility_test.go
Normal file
240
uixt/mcp_tools_utility_test.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
func TestToolSleep_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleep{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: json.Number("3.5"),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(3.5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: int64(5),
|
||||
},
|
||||
expectedArgs: map[string]any{"seconds": float64(5)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Seconds: 2.5,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"seconds": 2.5,
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_Sleep,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolSleepMS_ConvertActionToCallToolRequest(t *testing.T) {
|
||||
tool := &ToolSleepMS{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
action option.MobileAction
|
||||
expectedArgs map[string]any
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "json.Number parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: json.Number("1500"),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(1500)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "int64 parameter",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: int64(2000),
|
||||
},
|
||||
expectedArgs: map[string]any{"milliseconds": int64(2000)},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "SleepConfig with startTime",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: SleepConfig{
|
||||
StartTime: time.UnixMilli(1691234567890),
|
||||
Milliseconds: 3000,
|
||||
},
|
||||
},
|
||||
expectedArgs: map[string]any{
|
||||
"milliseconds": int64(3000),
|
||||
"start_time_ms": int64(1691234567890),
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid parameter type",
|
||||
action: option.MobileAction{
|
||||
Method: option.ACTION_SleepMS,
|
||||
Params: "invalid",
|
||||
},
|
||||
expectedArgs: nil,
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request, err := tool.ConvertActionToCallToolRequest(tt.action)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
args := request.GetArguments()
|
||||
for key, expectedValue := range tt.expectedArgs {
|
||||
assert.Equal(t, expectedValue, args[key], "Argument %s mismatch", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSleepStrictTiming(t *testing.T) {
|
||||
// Test that strict sleep properly adjusts for elapsed time
|
||||
startTime := time.Now()
|
||||
|
||||
// Simulate some processing time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test sleepStrict with the start time
|
||||
testStart := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // 200ms total duration
|
||||
actualElapsed := time.Since(testStart)
|
||||
|
||||
// Should sleep approximately 150ms (200ms - 50ms already elapsed)
|
||||
// Allow some tolerance for timing variations
|
||||
expectedSleep := 150 * time.Millisecond
|
||||
assert.Greater(t, actualElapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, actualElapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepCancellation(t *testing.T) {
|
||||
// Test that sleep respects context cancellation
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Cancel after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 500) // Try to sleep 500ms
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled after ~50ms, not sleep full 500ms
|
||||
assert.Less(t, elapsed, 200*time.Millisecond, "Sleep was not properly cancelled")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithZeroTime(t *testing.T) {
|
||||
// Test sleepStrict behaves like normal sleep when startTime is zero
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, time.Time{}, 100) // 100ms, no start time
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should sleep full duration
|
||||
expectedSleep := 100 * time.Millisecond
|
||||
assert.Greater(t, elapsed, expectedSleep/2, "Sleep too short")
|
||||
assert.Less(t, elapsed, expectedSleep*2, "Sleep too long")
|
||||
}
|
||||
|
||||
func TestSleepStrictWithPastStartTime(t *testing.T) {
|
||||
// Test sleepStrict skips sleep when elapsed time exceeds duration
|
||||
startTime := time.Now().Add(-300 * time.Millisecond) // 300ms ago
|
||||
ctx := context.Background()
|
||||
|
||||
start := time.Now()
|
||||
sleepStrict(ctx, startTime, 200) // Want 200ms total, but 300ms already elapsed
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should skip sleep entirely
|
||||
assert.Less(t, elapsed, 50*time.Millisecond, "Should have skipped sleep")
|
||||
}
|
||||
|
||||
func TestJsonNumberHandling(t *testing.T) {
|
||||
// Test that json.Number is correctly handled in different scenarios
|
||||
|
||||
// Test float json.Number
|
||||
floatNumber := json.Number("3.14")
|
||||
floatVal, err := floatNumber.Float64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3.14, floatVal)
|
||||
|
||||
// Test int json.Number
|
||||
intNumber := json.Number("1500")
|
||||
intVal, err := intNumber.Int64()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1500), intVal)
|
||||
|
||||
// Test invalid json.Number
|
||||
invalidNumber := json.Number("invalid")
|
||||
_, err = invalidNumber.Float64()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user