summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--main.go42
2 files changed, 25 insertions, 19 deletions
diff --git a/README.md b/README.md
index 64e85c2..3a05bcf 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/main.go b/main.go
index 8bba841..aede524 100644
--- a/main.go
+++ b/main.go
@@ -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.
115func NewRunner(filePath, envName, stateFile string) (*Runner, error) { 116func 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) {
163func (r *Runner) ExecuteGroup(groupName string) error { 164func (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 {