diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | main.go | 42 |
2 files changed, 25 insertions, 19 deletions
| @@ -61,6 +61,8 @@ This hierarchy (System > .env > YAML > State) is designed for **dynamic runtime | |||
| 61 | * `-req`: Comma-separated list of request names to execute. | 61 | * `-req`: Comma-separated list of request names to execute. |
| 62 | * `-group`: The name of a request group to execute. | 62 | * `-group`: The name of a request group to execute. |
| 63 | * `-headers`: Show response headers in the output. | 63 | * `-headers`: Show response headers in the output. |
| 64 | * `-timeout`: Request timeout duration (default: 10s). | ||
| 65 | * `-state`: Path to state file. | ||
| 64 | 66 | ||
| 65 | ## Core Concepts | 67 | ## Core Concepts |
| 66 | 68 | ||
| @@ -79,6 +79,7 @@ func main() { | |||
| 79 | reqNames := flag.String("req", "", "Comma-separated list of request names to execute") | 79 | reqNames := flag.String("req", "", "Comma-separated list of request names to execute") |
| 80 | groupName := flag.String("group", "", "Group to execute") | 80 | groupName := flag.String("group", "", "Group to execute") |
| 81 | showHeaders := flag.Bool("headers", false, "Display response headers") | 81 | showHeaders := flag.Bool("headers", false, "Display response headers") |
| 82 | timeout := flag.Duration("timeout", 10*time.Second, "Request timeout duration") | ||
| 82 | flag.Parse() | 83 | flag.Parse() |
| 83 | 84 | ||
| 84 | if filePath == "" { | 85 | if filePath == "" { |
| @@ -87,7 +88,7 @@ func main() { | |||
| 87 | os.Exit(1) | 88 | os.Exit(1) |
| 88 | } | 89 | } |
| 89 | 90 | ||
| 90 | runner, err := NewRunner(filePath, envName, statePath) | 91 | runner, err := NewRunner(filePath, envName, statePath, *timeout) |
| 91 | if err != nil { | 92 | if err != nil { |
| 92 | log.Fatalf("Error: %v", err) | 93 | log.Fatalf("Error: %v", err) |
| 93 | } | 94 | } |
| @@ -112,19 +113,19 @@ func main() { | |||
| 112 | } | 113 | } |
| 113 | 114 | ||
| 114 | // NewRunner initializes a new Hepi runner. | 115 | // NewRunner initializes a new Hepi runner. |
| 115 | func NewRunner(filePath, envName, stateFile string) (*Runner, error) { | 116 | func NewRunner(filePath, envName, stateFile string, timeout time.Duration) (*Runner, error) { |
| 116 | data, err := os.ReadFile(filePath) | 117 | data, err := os.ReadFile(filePath) |
| 117 | if err != nil { | 118 | if err != nil { |
| 118 | return nil, fmt.Errorf("failed to read file: %w", err) | 119 | return nil, fmt.Errorf("%sfailed to read file: %w%s", colorRed, err, colorReset) |
| 119 | } | 120 | } |
| 120 | 121 | ||
| 121 | var config Config | 122 | var config Config |
| 122 | if err := yaml.Unmarshal(data, &config); err != nil { | 123 | if err := yaml.Unmarshal(data, &config); err != nil { |
| 123 | return nil, fmt.Errorf("failed to parse YAML: %w", err) | 124 | return nil, fmt.Errorf("%sfailed to parse YAML: %w%s", colorRed, err, colorReset) |
| 124 | } | 125 | } |
| 125 | 126 | ||
| 126 | if config.Environments.Kind != yaml.MappingNode { | 127 | if config.Environments.Kind != yaml.MappingNode { |
| 127 | return nil, fmt.Errorf("environments must be a mapping") | 128 | return nil, fmt.Errorf("%senvironments must be a mapping%s", colorRed, colorReset) |
| 128 | } | 129 | } |
| 129 | 130 | ||
| 130 | selectedEnvName := envName | 131 | selectedEnvName := envName |
| @@ -138,14 +139,14 @@ func NewRunner(filePath, envName, stateFile string) (*Runner, error) { | |||
| 138 | availableEnvs = append(availableEnvs, name) | 139 | availableEnvs = append(availableEnvs, name) |
| 139 | if name == envName { | 140 | if name == envName { |
| 140 | if err := config.Environments.Content[i+1].Decode(&selectedEnv); err != nil { | 141 | if err := config.Environments.Content[i+1].Decode(&selectedEnv); err != nil { |
| 141 | return nil, fmt.Errorf("failed to decode environment %q: %w", envName, err) | 142 | return nil, fmt.Errorf("%sfailed to decode environment %q: %w%s", colorRed, envName, err, colorReset) |
| 142 | } | 143 | } |
| 143 | found = true | 144 | found = true |
| 144 | break | 145 | break |
| 145 | } | 146 | } |
| 146 | } | 147 | } |
| 147 | if !found { | 148 | if !found { |
| 148 | return nil, fmt.Errorf("environment %q not found\nAvailable environments:\n- %s", envName, strings.Join(availableEnvs, "\n- ")) | 149 | return nil, fmt.Errorf("%senvironment %q not found\nAvailable environments:\n- %s%s", colorRed, envName, strings.Join(availableEnvs, "\n- "), colorReset) |
| 149 | } | 150 | } |
| 150 | } | 151 | } |
| 151 | 152 | ||
| @@ -155,7 +156,7 @@ func NewRunner(filePath, envName, stateFile string) (*Runner, error) { | |||
| 155 | Environment: selectedEnv, | 156 | Environment: selectedEnv, |
| 156 | State: loadState(selectedEnvName, stateFile), | 157 | State: loadState(selectedEnvName, stateFile), |
| 157 | StateFile: stateFile, | 158 | StateFile: stateFile, |
| 158 | HTTPClient: &http.Client{Timeout: 10 * time.Second}, | 159 | HTTPClient: &http.Client{Timeout: timeout}, |
| 159 | }, nil | 160 | }, nil |
| 160 | } | 161 | } |
| 161 | 162 | ||
| @@ -163,7 +164,7 @@ func NewRunner(filePath, envName, stateFile string) (*Runner, error) { | |||
| 163 | func (r *Runner) ExecuteGroup(groupName string) error { | 164 | func (r *Runner) ExecuteGroup(groupName string) error { |
| 164 | group, ok := r.Config.Groups[groupName] | 165 | group, ok := r.Config.Groups[groupName] |
| 165 | if !ok { | 166 | if !ok { |
| 166 | return fmt.Errorf("group %q not found", groupName) | 167 | return fmt.Errorf("%sgroup %q not found%s", colorRed, groupName, colorReset) |
| 167 | } | 168 | } |
| 168 | 169 | ||
| 169 | for _, reqName := range group { | 170 | for _, reqName := range group { |
| @@ -187,7 +188,7 @@ func (r *Runner) ExecuteRequests(reqNames string) error { | |||
| 187 | 188 | ||
| 188 | requestsNode := r.Config.Requests | 189 | requestsNode := r.Config.Requests |
| 189 | if requestsNode.Kind != yaml.MappingNode { | 190 | if requestsNode.Kind != yaml.MappingNode { |
| 190 | return fmt.Errorf("requests must be a mapping") | 191 | return fmt.Errorf("%srequests must be a mapping%s", colorRed, colorReset) |
| 191 | } | 192 | } |
| 192 | 193 | ||
| 193 | for i := 0; i < len(requestsNode.Content); i += 2 { | 194 | for i := 0; i < len(requestsNode.Content); i += 2 { |
| @@ -202,12 +203,12 @@ func (r *Runner) ExecuteRequests(reqNames string) error { | |||
| 202 | 203 | ||
| 203 | var req Request | 204 | var req Request |
| 204 | if err := valNode.Decode(&req); err != nil { | 205 | if err := valNode.Decode(&req); err != nil { |
| 205 | return fmt.Errorf("failed to decode request %q: %w", name, err) | 206 | return fmt.Errorf("%sfailed to decode request %q: %w%s", colorRed, name, err, colorReset) |
| 206 | } | 207 | } |
| 207 | 208 | ||
| 208 | fmt.Printf("\n%s--- %s[%s]%s %s ---%s\n", colorBold, colorCyan, name, colorReset, req.Description, colorReset) | 209 | fmt.Printf("\n%s--- %s[%s]%s %s ---%s\n", colorBold, colorCyan, name, colorReset, req.Description, colorReset) |
| 209 | if err := r.executeRequest(name, req); err != nil { | 210 | if err := r.executeRequest(name, req); err != nil { |
| 210 | log.Printf("Warning: request %q failed: %v", name, err) | 211 | return err |
| 211 | } | 212 | } |
| 212 | } | 213 | } |
| 213 | 214 | ||
| @@ -218,7 +219,7 @@ func (r *Runner) ExecuteRequests(reqNames string) error { | |||
| 218 | } | 219 | } |
| 219 | } | 220 | } |
| 220 | if len(missing) > 0 { | 221 | if len(missing) > 0 { |
| 221 | return fmt.Errorf("requests not found: %s", strings.Join(missing, ", ")) | 222 | return fmt.Errorf("%srequests not found: %s%s", colorRed, strings.Join(missing, ", "), colorReset) |
| 222 | } | 223 | } |
| 223 | 224 | ||
| 224 | return nil | 225 | return nil |
| @@ -231,7 +232,7 @@ func (r *Runner) executeRequest(name string, req Request) error { | |||
| 231 | if req.Params != nil { | 232 | if req.Params != nil { |
| 232 | u, err := url.Parse(rawURL) | 233 | u, err := url.Parse(rawURL) |
| 233 | if err != nil { | 234 | if err != nil { |
| 234 | return fmt.Errorf("failed to parse URL %q: %w", rawURL, err) | 235 | return fmt.Errorf("%sfailed to parse URL %q: %w%s", colorRed, rawURL, err, colorReset) |
| 235 | } | 236 | } |
| 236 | q := u.Query() | 237 | q := u.Query() |
| 237 | params := r.substituteMap(req.Params) | 238 | params := r.substituteMap(req.Params) |
| @@ -281,13 +282,13 @@ func (r *Runner) executeRequest(name string, req Request) error { | |||
| 281 | substitutedPath := r.substitute(path) | 282 | substitutedPath := r.substitute(path) |
| 282 | file, err := os.Open(substitutedPath) | 283 | file, err := os.Open(substitutedPath) |
| 283 | if err != nil { | 284 | if err != nil { |
| 284 | return fmt.Errorf("failed to open file %q: %w", substitutedPath, err) | 285 | return fmt.Errorf("%sfailed to open file %q: %w%s", colorRed, substitutedPath, err, colorReset) |
| 285 | } | 286 | } |
| 286 | defer file.Close() | 287 | defer file.Close() |
| 287 | 288 | ||
| 288 | part, err := writer.CreateFormFile(field, substitutedPath) | 289 | part, err := writer.CreateFormFile(field, substitutedPath) |
| 289 | if err != nil { | 290 | if err != nil { |
| 290 | return fmt.Errorf("failed to create form file for %q: %w", field, err) | 291 | return fmt.Errorf("%sfailed to create form file for %q: %w%s", colorRed, field, err, colorReset) |
| 291 | } | 292 | } |
| 292 | _, _ = io.Copy(part, file) | 293 | _, _ = io.Copy(part, file) |
| 293 | } | 294 | } |
| @@ -307,7 +308,7 @@ func (r *Runner) executeRequest(name string, req Request) error { | |||
| 307 | 308 | ||
| 308 | httpReq, err := http.NewRequest(req.Method, rawURL, bodyReader) | 309 | httpReq, err := http.NewRequest(req.Method, rawURL, bodyReader) |
| 309 | if err != nil { | 310 | if err != nil { |
| 310 | return fmt.Errorf("failed to create HTTP request: %w", err) | 311 | return fmt.Errorf("%sfailed to create HTTP request: %w%s", colorRed, err, colorReset) |
| 311 | } | 312 | } |
| 312 | 313 | ||
| 313 | if contentType != "" { | 314 | if contentType != "" { |
| @@ -321,7 +322,10 @@ func (r *Runner) executeRequest(name string, req Request) error { | |||
| 321 | startTime := time.Now() | 322 | startTime := time.Now() |
| 322 | resp, err := r.HTTPClient.Do(httpReq) | 323 | resp, err := r.HTTPClient.Do(httpReq) |
| 323 | if err != nil { | 324 | if err != nil { |
| 324 | return fmt.Errorf("request failed: %w", err) | 325 | if os.IsTimeout(err) { |
| 326 | return fmt.Errorf("%srequest timed out after %v%s", colorRed, r.HTTPClient.Timeout, colorReset) | ||
| 327 | } | ||
| 328 | return fmt.Errorf("%srequest failed: %w%s", colorRed, err, colorReset) | ||
| 325 | } | 329 | } |
| 326 | duration := time.Since(startTime) | 330 | duration := time.Since(startTime) |
| 327 | defer resp.Body.Close() | 331 | defer resp.Body.Close() |
| @@ -344,7 +348,7 @@ func (r *Runner) executeRequest(name string, req Request) error { | |||
| 344 | 348 | ||
| 345 | respData, err := io.ReadAll(resp.Body) | 349 | respData, err := io.ReadAll(resp.Body) |
| 346 | if err != nil { | 350 | if err != nil { |
| 347 | return fmt.Errorf("failed to read response body: %w", err) | 351 | return fmt.Errorf("%sfailed to read response body: %w%s", colorRed, err, colorReset) |
| 348 | } | 352 | } |
| 349 | 353 | ||
| 350 | if len(respData) > 0 { | 354 | if len(respData) > 0 { |
