Skip to content

Commit c1c203f

Browse files
committed
feat: adaptive processing of request body format and floating-point decimal point
1 parent af78568 commit c1c203f

File tree

7 files changed

+158
-45
lines changed

7 files changed

+158
-45
lines changed

cmd/sponge/commands/perftest/common/common.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package common
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"math/rand"
@@ -9,6 +10,99 @@ import (
910
"time"
1011
)
1112

13+
const (
14+
BodyTypeJSON = "application/json"
15+
BodyTypeForm = "application/x-www-form-urlencoded"
16+
BodyTypeText = "text/plain"
17+
)
18+
19+
// nolint
20+
func ParseHTTPParams(method string, headers []string, body string, bodyFile string) ([]byte, map[string]string, error) {
21+
var bodyType string
22+
var bodyBytes []byte
23+
headerMap := make(map[string]string)
24+
for _, h := range headers {
25+
kvs := strings.SplitN(h, ":", 2)
26+
if len(kvs) == 2 {
27+
key := trimString(kvs[0])
28+
value := trimString(kvs[1])
29+
headerMap[key] = value
30+
}
31+
}
32+
33+
if strings.ToUpper(method) == "GET" {
34+
return bodyBytes, headerMap, nil
35+
}
36+
37+
if body != "" {
38+
body = strings.Trim(body, "'")
39+
bodyBytes = []byte(body)
40+
} else {
41+
if bodyFile != "" {
42+
data, err := os.ReadFile(bodyFile)
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
bodyBytes = data
47+
}
48+
}
49+
if len(bodyBytes) == 0 {
50+
return bodyBytes, headerMap, nil
51+
}
52+
53+
for k, v := range headerMap {
54+
if strings.ToLower(k) == "content-type" && v != "" {
55+
bodyType = strings.ToLower(v)
56+
}
57+
}
58+
59+
switch bodyType {
60+
case BodyTypeJSON:
61+
ok, err := isValidJSON(bodyBytes)
62+
if err != nil {
63+
return nil, nil, err
64+
}
65+
if !ok {
66+
return nil, nil, fmt.Errorf("invalid JSON format")
67+
}
68+
return bodyBytes, headerMap, nil
69+
70+
case BodyTypeForm:
71+
if bytes.Count(bodyBytes, []byte("=")) == 0 {
72+
return nil, nil, fmt.Errorf("invalid body form data format")
73+
}
74+
return bodyBytes, headerMap, nil
75+
76+
case BodyTypeText:
77+
return bodyBytes, headerMap, nil
78+
79+
default:
80+
if bodyType != "" {
81+
return bodyBytes, headerMap, nil
82+
}
83+
84+
ok, err := isValidJSON(bodyBytes)
85+
if err == nil && ok {
86+
headerMap["Content-Type"] = BodyTypeJSON
87+
return bodyBytes, headerMap, nil
88+
}
89+
90+
equalNun := bytes.Count(bodyBytes, []byte("="))
91+
andNun := bytes.Count(bodyBytes, []byte("&"))
92+
if equalNun == andNun+1 {
93+
headerMap["Content-Type"] = BodyTypeForm
94+
} else {
95+
headerMap["Content-Type"] = BodyTypeText
96+
}
97+
98+
return bodyBytes, headerMap, nil
99+
}
100+
}
101+
102+
func trimString(s string) string {
103+
return strings.Trim(s, " \t\r\n\"'")
104+
}
105+
12106
// CheckBodyParam checks if the body parameter is provided in JSON or file format.
13107
func CheckBodyParam(bodyJSON string, bodyFile string) (string, error) {
14108
var body []byte

cmd/sponge/commands/perftest/common/progress_bar.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ func (b *Bar) shouldDraw() bool {
5959
now := time.Now().UnixNano()
6060
intervalNano := b.updateInterval.Nanoseconds()
6161

62-
//for {
6362
lastNano := b.lastDrawNano.Load()
6463
if now-lastNano < intervalNano {
6564
// Too close to the last draw, skip
@@ -71,7 +70,6 @@ func (b *Bar) shouldDraw() bool {
7170
return true
7271
}
7372
return false
74-
//}
7573
}
7674

7775
// draw renders the bar in the terminal.

cmd/sponge/commands/perftest/http/http1.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ func PerfTestHTTPCMD() *cobra.Command {
5858
SilenceErrors: true,
5959
SilenceUsage: true,
6060
RunE: func(cmd *cobra.Command, args []string) error {
61-
bodyData, err := common.CheckBodyParam(body, bodyFile)
61+
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
6262
if err != nil {
6363
return err
6464
}
6565

6666
params := &HTTPReqParams{
6767
URL: targetURL,
6868
Method: method,
69-
Headers: headers,
70-
Body: bodyData,
69+
Headers: headerMap,
70+
Body: bodyBytes,
7171
version: "HTTP/1.1",
7272
}
7373

cmd/sponge/commands/perftest/http/http2.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ func PerfTestHTTP2CMD() *cobra.Command {
5858
SilenceErrors: true,
5959
SilenceUsage: true,
6060
RunE: func(cmd *cobra.Command, args []string) error {
61-
bodyData, err := common.CheckBodyParam(body, bodyFile)
61+
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
6262
if err != nil {
6363
return err
6464
}
6565

6666
params := &HTTPReqParams{
6767
URL: targetURL,
6868
Method: method,
69-
Headers: headers,
70-
Body: bodyData,
69+
Headers: headerMap,
70+
Body: bodyBytes,
7171
version: "HTTP/2",
7272
}
7373

cmd/sponge/commands/perftest/http/http3.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,16 @@ func PerfTestHTTP3CMD() *cobra.Command {
6060
SilenceErrors: true,
6161
SilenceUsage: true,
6262
RunE: func(cmd *cobra.Command, args []string) error {
63-
bodyData, err := common.CheckBodyParam(body, bodyFile)
63+
bodyBytes, headerMap, err := common.ParseHTTPParams(method, headers, body, bodyFile)
6464
if err != nil {
6565
return err
6666
}
6767

6868
params := &HTTPReqParams{
6969
URL: targetURL,
7070
Method: method,
71-
Headers: headers,
72-
Body: bodyData,
71+
Headers: headerMap,
72+
Body: bodyBytes,
7373
version: "HTTP/3",
7474
}
7575

cmd/sponge/commands/perftest/http/run.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ type Result struct {
251251
type HTTPReqParams struct {
252252
URL string
253253
Method string
254-
Headers []string
255-
Body string
254+
Headers map[string]string
255+
Body []byte
256256

257257
version string
258258
}
@@ -263,20 +263,14 @@ func buildRequest(params *HTTPReqParams) (*http.Request, error) {
263263

264264
reqMethod := strings.ToUpper(params.Method)
265265
if reqMethod == "POST" || reqMethod == "PUT" || reqMethod == "PATCH" || reqMethod == "DELETE" {
266-
body := bytes.NewReader([]byte(params.Body))
266+
body := bytes.NewReader(params.Body)
267267
req, err = http.NewRequest(reqMethod, params.URL, body)
268-
if err == nil {
269-
req.Header.Set("Content-Type", "application/json")
270-
}
271268
} else {
272269
req, err = http.NewRequest(reqMethod, params.URL, nil)
273270
}
274271

275-
for _, h := range params.Headers {
276-
kvs := strings.SplitN(h, ":", 2)
277-
if len(kvs) == 2 {
278-
req.Header.Set(strings.TrimSpace(kvs[0]), strings.TrimSpace(kvs[1]))
279-
}
272+
for k, v := range params.Headers {
273+
req.Header.Set(k, v)
280274
}
281275

282276
return req, err

cmd/sponge/commands/perftest/http/stats.go

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package http
33
import (
44
"bytes"
55
"context"
6+
"crypto/tls"
67
"encoding/json"
78
"fmt"
89
"math"
910
"net/http"
1011
"os"
1112
"path/filepath"
1213
"sort"
14+
"strconv"
1315
"time"
1416

1517
"github.com/fatih/color"
@@ -129,27 +131,27 @@ func (c *statsCollector) toStatistics(totalTime time.Duration, totalRequests uin
129131
p95 = percentile(0.95)
130132
}
131133

132-
var errors []string
134+
errors := []string{}
133135
for errStr := range c.errSet {
134136
errors = append(errors, errStr)
135137
}
136138

137139
body := params.Body
138140
if len(body) > 300 {
139-
body = body[:300] + "..."
141+
body = append(body[:293], []byte(" ......")...)
140142
}
141143

142144
return &Statistics{
143145
URL: params.URL,
144146
Method: params.Method,
145-
Body: body,
147+
Body: string(body),
146148

147149
TotalRequests: totalRequests,
148150
Errors: errors,
149151
SuccessCount: c.successCount,
150152
ErrorCount: c.errorCount,
151-
TotalTime: totalTime.Seconds(),
152-
QPS: float64(c.successCount) / totalTime.Seconds(),
153+
TotalDuration: math.Round(totalTime.Seconds()*100) / 100,
154+
QPS: math.Round(float64(c.successCount)/totalTime.Seconds()*10) / 10,
153155

154156
AvgLatency: convertToMilliseconds(avg),
155157
P25Latency: convertToMilliseconds(p25),
@@ -158,20 +160,37 @@ func (c *statsCollector) toStatistics(totalTime time.Duration, totalRequests uin
158160
MinLatency: convertToMilliseconds(minLatency),
159161
MaxLatency: convertToMilliseconds(maxLatency),
160162

161-
TotalSent: float64(c.totalReqBytes),
162-
TotalReceived: float64(c.totalRespBytes),
163+
TotalSent: c.totalReqBytes,
164+
TotalReceived: c.totalRespBytes,
163165
StatusCodes: c.statusCodeSet,
164166
}
165167
}
166168

169+
// convert float64 to string with specified precision, automatically process the last 0
170+
func float64ToString(f float64, precision int) string {
171+
if precision == 0 {
172+
return strconv.FormatInt(int64(f), 10)
173+
}
174+
if precision >= 1 && precision <= 6 {
175+
factor := math.Pow10(precision)
176+
rounded := math.Round(f*factor) / factor
177+
return strconv.FormatFloat(rounded, 'f', precision, 64)
178+
}
179+
return strconv.FormatFloat(f, 'f', -1, 64)
180+
}
181+
182+
func float64ToStringNoRound(f float64) string {
183+
return strconv.FormatFloat(f, 'f', -1, 64)
184+
}
185+
167186
func (c *statsCollector) printReport(totalDuration time.Duration, totalRequests uint64, params *HTTPReqParams) (*Statistics, error) {
168187
fmt.Printf("\n========== %s Performance Test Report ==========\n\n", params.version)
169188
if c.successCount == 0 {
170189
_, _ = color.New(color.Bold).Println("[Requests]")
171190
fmt.Printf(" • %-19s%d\n", "Total Requests:", totalRequests)
172191
fmt.Printf(" • %-19s%d%s\n", "Successful:", 0, color.RedString(" (0%)"))
173192
fmt.Printf(" • %-19s%d%s\n", "Failed:", c.errorCount, color.RedString(" ✗"))
174-
fmt.Printf(" • %-19s%.2fs\n\n", "Total Duration:", totalDuration.Seconds())
193+
fmt.Printf(" • %-19s%s s\n\n", "Total Duration:", float64ToString(totalDuration.Seconds(), 2))
175194

176195
if len(c.statusCodeSet) > 0 {
177196
printStatusCodeSet(c.statusCodeSet)
@@ -196,27 +215,28 @@ func (c *statsCollector) printReport(totalDuration time.Duration, totalRequests
196215
if st.SuccessCount == 0 {
197216
successStr += color.RedString(" (0%)")
198217
} else {
199-
successStr += color.YellowString(" (%d%%)", int(float64(st.SuccessCount)/float64(st.TotalRequests)*100))
218+
percentage := float64ToString(float64(st.SuccessCount)/float64(st.TotalRequests)*100, 1)
219+
successStr += color.YellowString(" (%s%%)", percentage)
200220
}
201221
failureStr += color.RedString(" ✗")
202222
}
203223
}
204224
fmt.Println(successStr)
205225
fmt.Println(failureStr)
206-
fmt.Printf(" • %-19s%.2fs\n", "Total Duration:", st.TotalTime)
207-
fmt.Printf(" • %-19s%.2f req/sec\n\n", "Throughput (QPS):", st.QPS)
226+
fmt.Printf(" • %-19s%s s\n", "Total Duration:", float64ToStringNoRound(st.TotalDuration))
227+
fmt.Printf(" • %-19s%s req/sec\n\n", "Throughput (QPS):", float64ToStringNoRound(st.QPS))
208228

209229
_, _ = color.New(color.Bold).Println("[Latency]")
210-
fmt.Printf(" • %-19s%.2f ms\n", "Average:", st.AvgLatency)
211-
fmt.Printf(" • %-19s%.2f ms\n", "Minimum:", st.MinLatency)
212-
fmt.Printf(" • %-19s%.2f ms\n", "Maximum:", st.MaxLatency)
213-
fmt.Printf(" • %-19s%.2f ms\n", "P25:", st.P25Latency)
214-
fmt.Printf(" • %-19s%.2f ms\n", "P50:", st.P50Latency)
215-
fmt.Printf(" • %-19s%.2f ms\n\n", "P95:", st.P95Latency)
230+
fmt.Printf(" • %-19s%s ms\n", "Average:", float64ToStringNoRound(st.AvgLatency))
231+
fmt.Printf(" • %-19s%s ms\n", "Minimum:", float64ToStringNoRound(st.MinLatency))
232+
fmt.Printf(" • %-19s%s ms\n", "Maximum:", float64ToStringNoRound(st.MaxLatency))
233+
fmt.Printf(" • %-19s%s ms\n", "P25:", float64ToStringNoRound(st.P25Latency))
234+
fmt.Printf(" • %-19s%s ms\n", "P50:", float64ToStringNoRound(st.P50Latency))
235+
fmt.Printf(" • %-19s%s ms\n\n", "P95:", float64ToStringNoRound(st.P95Latency))
216236

217237
_, _ = color.New(color.Bold).Println("[Data Transfer]")
218-
fmt.Printf(" • %-19s%.0f Bytes\n", "Sent:", st.TotalSent)
219-
fmt.Printf(" • %-19s%.0f Bytes\n\n", "Received:", st.TotalReceived)
238+
fmt.Printf(" • %-19s%d Bytes\n", "Sent:", st.TotalSent)
239+
fmt.Printf(" • %-19s%d Bytes\n\n", "Received:", st.TotalReceived)
220240

221241
if len(c.statusCodeSet) > 0 {
222242
printStatusCodeSet(st.StatusCodes)
@@ -262,7 +282,7 @@ type Statistics struct {
262282
Body string `json:"body"` // request body (JSON)
263283

264284
TotalRequests uint64 `json:"total_requests"` // total requests
265-
TotalTime float64 `json:"total_time"` // seconds
285+
TotalDuration float64 `json:"total_duration"` // total duration (seconds)
266286
SuccessCount uint64 `json:"success_count"` // successful requests (status code 2xx)
267287
ErrorCount uint64 `json:"error_count"` // failed requests (status code not 2xx)
268288
Errors []string `json:"errors"` // error details
@@ -275,10 +295,12 @@ type Statistics struct {
275295
MinLatency float64 `json:"min_latency"` // minimum latency (ms)
276296
MaxLatency float64 `json:"max_latency"` // maximum latency (ms)
277297

278-
TotalSent float64 `json:"total_sent"` // total sent (bytes)
279-
TotalReceived float64 `json:"total_received"` // total received (bytes)
298+
TotalSent int64 `json:"total_sent"` // total sent (bytes)
299+
TotalReceived int64 `json:"total_received"` // total received (bytes)
280300

281301
StatusCodes map[int]int64 `json:"status_codes"` // status code distribution (count)
302+
303+
CreatedAt time.Time `json:"created_at"` // created time
282304
}
283305

284306
// Save saves the statistics data to a JSON file.
@@ -485,6 +507,7 @@ func (spc *statsPrometheusCollector) PushToPrometheusAsync(ctx context.Context,
485507
func (spc *statsPrometheusCollector) PushToServer(ctx context.Context, pushURL string, elapsed time.Duration, httpReqParams *HTTPReqParams, id int64) error {
486508
statistics := spc.statsCollector.toStatistics(elapsed, spc.statsCollector.successCount+spc.statsCollector.errorCount, httpReqParams)
487509
statistics.PerfTestID = id
510+
statistics.CreatedAt = time.Now()
488511

489512
_, err := postWithContext(ctx, pushURL, statistics)
490513
return err
@@ -510,7 +533,11 @@ func postWithContext(ctx context.Context, url string, data *Statistics) (*http.R
510533

511534
req.Header.Set("Content-Type", "application/json")
512535

513-
client := &http.Client{}
536+
client := &http.Client{
537+
Transport: &http.Transport{
538+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Skip certificate validation
539+
},
540+
}
514541
resp, err := client.Do(req)
515542
if err != nil {
516543
return nil, err

0 commit comments

Comments
 (0)