mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
218 lines
5.5 KiB
Go
218 lines
5.5 KiB
Go
package testsuite
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (cc *caseContext) assert(name string, ok bool, detail string) {
|
|
cc.mu.Lock()
|
|
defer cc.mu.Unlock()
|
|
cc.assertions = append(cc.assertions, assertionResult{
|
|
Name: name,
|
|
Passed: ok,
|
|
Detail: detail,
|
|
})
|
|
}
|
|
|
|
func (cc *caseContext) request(ctx context.Context, spec requestSpec) (*responseResult, error) {
|
|
retries := cc.runner.opts.Retries
|
|
if !spec.Retryable {
|
|
retries = 0
|
|
}
|
|
var lastErr error
|
|
for attempt := 1; attempt <= retries+1; attempt++ {
|
|
resp, err := cc.requestOnce(ctx, spec, attempt)
|
|
if err == nil && resp.StatusCode < 500 {
|
|
return resp, nil
|
|
}
|
|
if err != nil {
|
|
lastErr = err
|
|
} else if resp.StatusCode >= 500 {
|
|
lastErr = fmt.Errorf("status=%d", resp.StatusCode)
|
|
}
|
|
if attempt <= retries {
|
|
sleep := time.Duration(300*(1<<(attempt-1))) * time.Millisecond
|
|
time.Sleep(sleep)
|
|
}
|
|
}
|
|
return nil, lastErr
|
|
}
|
|
|
|
func (cc *caseContext) requestOnce(ctx context.Context, spec requestSpec, attempt int) (*responseResult, error) {
|
|
cc.mu.Lock()
|
|
cc.seq++
|
|
seq := cc.seq
|
|
traceID := fmt.Sprintf("ts_%s_%s_%03d", cc.runner.runID, sanitizeID(cc.id), seq)
|
|
cc.traceIDsSet[traceID] = struct{}{}
|
|
cc.mu.Unlock()
|
|
|
|
fullURL, err := withTraceQuery(cc.runner.baseURL+spec.Path, traceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headers := map[string]string{}
|
|
for k, v := range spec.Headers {
|
|
headers[k] = v
|
|
}
|
|
headers["X-Ds2-Test-Trace"] = traceID
|
|
|
|
var bodyBytes []byte
|
|
var bodyAny any
|
|
if spec.Body != nil {
|
|
b, err := json.Marshal(spec.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bodyBytes = b
|
|
bodyAny = spec.Body
|
|
headers["Content-Type"] = "application/json"
|
|
}
|
|
cc.mu.Lock()
|
|
cc.requests = append(cc.requests, requestLog{
|
|
Seq: seq,
|
|
Attempt: attempt,
|
|
TraceID: traceID,
|
|
Method: spec.Method,
|
|
URL: fullURL,
|
|
Headers: headers,
|
|
Body: bodyAny,
|
|
Timestamp: time.Now().Format(time.RFC3339Nano),
|
|
})
|
|
cc.mu.Unlock()
|
|
|
|
reqCtx, cancel := context.WithTimeout(ctx, cc.runner.opts.Timeout)
|
|
defer cancel()
|
|
req, err := http.NewRequestWithContext(reqCtx, spec.Method, fullURL, bytes.NewReader(bodyBytes))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
start := time.Now()
|
|
resp, err := cc.runner.httpClient.Do(req)
|
|
if err != nil {
|
|
cc.mu.Lock()
|
|
cc.responses = append(cc.responses, responseLog{
|
|
Seq: seq,
|
|
Attempt: attempt,
|
|
TraceID: traceID,
|
|
StatusCode: 0,
|
|
DurationMS: time.Since(start).Milliseconds(),
|
|
NetworkErr: err.Error(),
|
|
ReceivedAt: time.Now().Format(time.RFC3339Nano),
|
|
})
|
|
cc.mu.Unlock()
|
|
return nil, err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
cc.mu.Lock()
|
|
cc.responses = append(cc.responses, responseLog{
|
|
Seq: seq,
|
|
Attempt: attempt,
|
|
TraceID: traceID,
|
|
StatusCode: resp.StatusCode,
|
|
Headers: resp.Header,
|
|
BodyText: string(body),
|
|
DurationMS: time.Since(start).Milliseconds(),
|
|
ReceivedAt: time.Now().Format(time.RFC3339Nano),
|
|
})
|
|
|
|
if spec.Stream {
|
|
_, _ = fmt.Fprintf(&cc.streamRaw, "### trace=%s url=%s\n", traceID, fullURL)
|
|
cc.streamRaw.Write(body)
|
|
cc.streamRaw.WriteString("\n\n")
|
|
}
|
|
cc.mu.Unlock()
|
|
|
|
return &responseResult{
|
|
StatusCode: resp.StatusCode,
|
|
Headers: resp.Header,
|
|
Body: body,
|
|
TraceID: traceID,
|
|
URL: fullURL,
|
|
}, nil
|
|
}
|
|
|
|
func (cc *caseContext) flushArtifacts(cs caseResult) error {
|
|
requestPath := filepath.Join(cc.dir, "request.json")
|
|
headersPath := filepath.Join(cc.dir, "response.headers")
|
|
bodyPath := filepath.Join(cc.dir, "response.body")
|
|
streamPath := filepath.Join(cc.dir, "stream.raw")
|
|
assertPath := filepath.Join(cc.dir, "assertions.json")
|
|
metaPath := filepath.Join(cc.dir, "meta.json")
|
|
|
|
if err := writeJSONFile(requestPath, cc.requests); err != nil {
|
|
return err
|
|
}
|
|
respHeaders := make([]map[string]any, 0, len(cc.responses))
|
|
respBodies := make([]map[string]any, 0, len(cc.responses))
|
|
for _, r := range cc.responses {
|
|
respHeaders = append(respHeaders, map[string]any{
|
|
"seq": r.Seq,
|
|
"attempt": r.Attempt,
|
|
"trace_id": r.TraceID,
|
|
"status_code": r.StatusCode,
|
|
"headers": r.Headers,
|
|
})
|
|
respBodies = append(respBodies, map[string]any{
|
|
"seq": r.Seq,
|
|
"attempt": r.Attempt,
|
|
"trace_id": r.TraceID,
|
|
"status_code": r.StatusCode,
|
|
"body_text": r.BodyText,
|
|
"network_error": r.NetworkErr,
|
|
"duration_ms": r.DurationMS,
|
|
})
|
|
}
|
|
if err := writeJSONFile(headersPath, respHeaders); err != nil {
|
|
return err
|
|
}
|
|
if err := writeJSONFile(bodyPath, respBodies); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(streamPath, []byte(cc.streamRaw.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
if err := writeJSONFile(assertPath, cc.assertions); err != nil {
|
|
return err
|
|
}
|
|
meta := map[string]any{
|
|
"case_id": cs.CaseID,
|
|
"trace_id": strings.Join(cs.TraceIDs, ","),
|
|
"attempt": len(cc.responses),
|
|
"duration_ms": cs.DurationMS,
|
|
"status": map[bool]string{true: "passed", false: "failed"}[cs.Passed],
|
|
"status_codes": cs.StatusCodes,
|
|
"assertions": cs.Assertions,
|
|
"artifact_path": cs.ArtifactPath,
|
|
}
|
|
return writeJSONFile(metaPath, meta)
|
|
}
|
|
func (r *Runner) doSimpleJSON(ctx context.Context, method, path string, headers map[string]string, body any) (*responseResult, error) {
|
|
cc := &caseContext{
|
|
runner: r,
|
|
id: "auth_prepare",
|
|
traceIDsSet: map[string]struct{}{},
|
|
}
|
|
return cc.request(ctx, requestSpec{
|
|
Method: method,
|
|
Path: path,
|
|
Headers: headers,
|
|
Body: body,
|
|
Retryable: true,
|
|
})
|
|
}
|