|
5 | 5 | "encoding/json"
|
6 | 6 | "fmt"
|
7 | 7 | "reflect"
|
| 8 | + "regexp" |
8 | 9 | "strings"
|
9 | 10 |
|
10 | 11 | "github.com/gosimple/slug"
|
@@ -168,11 +169,13 @@ func GetExecutorResult(r interface{}) map[string]interface{} {
|
168 | 169 | }
|
169 | 170 |
|
170 | 171 | type UserExecutor struct {
|
171 |
| - Executor string `json:"executor" yaml:"executor"` |
172 |
| - Input H `json:"input" yaml:"input"` |
173 |
| - RawTestSteps []json.RawMessage `json:"steps" yaml:"steps"` |
174 |
| - Output json.RawMessage `json:"output" yaml:"output"` |
175 |
| - Filename string `json:"-" yaml:"-"` |
| 172 | + Executor string |
| 173 | + Input H `json:"input" yaml:"input"` |
| 174 | + TestSteps []json.RawMessage `json:"steps" yaml:"steps"` |
| 175 | + Raw []byte `json:"-" yaml:"-"` // the raw file content of the executor |
| 176 | + RawInputs []byte `json:"-" yaml:"-"` |
| 177 | + Filename string `json:"-" yaml:"-"` |
| 178 | + Output json.RawMessage `json:"output" yaml:"output"` |
176 | 179 | }
|
177 | 180 |
|
178 | 181 | // Run is not implemented on user executor
|
@@ -202,34 +205,66 @@ func (ux UserExecutor) ZeroValueResult() interface{} {
|
202 | 205 |
|
203 | 206 | func (v *Venom) RunUserExecutor(ctx context.Context, runner ExecutorRunner, tcIn *TestCase, tsIn *TestStepResult, step TestStep) (interface{}, error) {
|
204 | 207 | vrs := tcIn.TestSuiteVars.Clone()
|
205 |
| - uxIn := runner.GetExecutor().(UserExecutor) |
206 |
| - |
207 |
| - for k, va := range uxIn.Input { |
208 |
| - if strings.HasPrefix(k, "input.") { |
209 |
| - // do not reinject input.vars from parent user executor if exists |
210 |
| - continue |
211 |
| - } else if !strings.HasPrefix(k, "venom") { |
212 |
| - if vl, ok := step[k]; ok && vl != "" { // value from step |
213 |
| - vrs.AddWithPrefix("input", k, vl) |
214 |
| - } else { // default value from executor |
215 |
| - vrs.AddWithPrefix("input", k, va) |
| 208 | + ux := runner.GetExecutor().(UserExecutor) |
| 209 | + var tsVars map[string]string |
| 210 | + newUX := UserExecutor{} |
| 211 | + var err error |
| 212 | + |
| 213 | + // process inputs |
| 214 | + if len(ux.RawInputs) != 0 { |
| 215 | + tsVars, err = DumpString(vrs) |
| 216 | + if err != nil { |
| 217 | + return nil, errors.Wrapf(err, "error processing executor inputs: unable to dump testsuite vars") |
| 218 | + } |
| 219 | + |
| 220 | + interpolatedInput, err := interpolate.Do(string(ux.RawInputs), tsVars) |
| 221 | + if err != nil { |
| 222 | + return nil, errors.Wrapf(err, "unable to interpolate executor inputs %q", ux.Executor) |
| 223 | + } |
| 224 | + |
| 225 | + err = yaml.Unmarshal([]byte(interpolatedInput), &newUX) |
| 226 | + if err != nil { |
| 227 | + return nil, errors.Wrapf(err, "unable to unmarshal inputs for executor %q - raw interpolated:\n%v", ux.Executor, string(interpolatedInput)) |
| 228 | + } |
| 229 | + |
| 230 | + for k, va := range newUX.Input { |
| 231 | + if strings.HasPrefix(k, "input.") { |
| 232 | + // do not reinject input.vars from parent user executor if exists |
| 233 | + continue |
| 234 | + } else if !strings.HasPrefix(k, "venom") { |
| 235 | + if vl, ok := step[k]; ok && vl != "" { // value from step |
| 236 | + vrs.AddWithPrefix("input", k, vl) |
| 237 | + } else { // default value from executor |
| 238 | + vrs.AddWithPrefix("input", k, va) |
| 239 | + } |
| 240 | + } else { |
| 241 | + vrs.Add(k, va) |
216 | 242 | }
|
217 |
| - } else { |
218 |
| - vrs.Add(k, va) |
| 243 | + } |
| 244 | + tsVars, err = DumpString(vrs) |
| 245 | + if err != nil { |
| 246 | + return nil, errors.Wrapf(err, "error processing executor inputs: unable to dump testsuite vars") |
219 | 247 | }
|
220 | 248 | }
|
221 |
| - // reload the user executor with the interpolated vars |
222 |
| - _, exe, err := v.GetExecutorRunner(ctx, step, vrs) |
| 249 | + |
| 250 | + interpolatedFull, err := interpolate.Do(string(ux.Raw), tsVars) |
| 251 | + if err != nil { |
| 252 | + return nil, errors.Wrapf(err, "unable to interpolate executor %q", ux.Executor) |
| 253 | + } |
| 254 | + // quote any remaining template expressions to ensure proper YAML parsing |
| 255 | + sanitized := quoteTemplateExpressions([]byte(interpolatedFull)) |
| 256 | + |
| 257 | + err = yaml.Unmarshal([]byte(sanitized), &newUX) |
223 | 258 | if err != nil {
|
224 |
| - return nil, errors.Wrapf(err, "unable to reload executor") |
| 259 | + return nil, errors.Wrapf(err, "unable to unmarshal executor %q - raw interpolated :\n%v", ux.Executor, string(sanitized)) |
225 | 260 | }
|
226 |
| - ux := exe.GetExecutor().(UserExecutor) |
| 261 | + ux.Output = newUX.Output |
227 | 262 |
|
228 | 263 | tc := &TestCase{
|
229 | 264 | TestCaseInput: TestCaseInput{
|
230 | 265 | Name: ux.Executor,
|
231 |
| - RawTestSteps: ux.RawTestSteps, |
232 | 266 | Vars: vrs,
|
| 267 | + RawTestSteps: newUX.TestSteps, |
233 | 268 | },
|
234 | 269 | number: tcIn.number,
|
235 | 270 | TestSuiteVars: tcIn.TestSuiteVars,
|
@@ -283,7 +318,7 @@ func (v *Venom) RunUserExecutor(ctx context.Context, runner ExecutorRunner, tcIn
|
283 | 318 | }
|
284 | 319 |
|
285 | 320 | if len(tsIn.Errors) > 0 {
|
286 |
| - return outputResult, fmt.Errorf("failed") |
| 321 | + return outputResult, fmt.Errorf("executor %q failed - raw interpolated:\n%v", ux.Executor, string(sanitized)) |
287 | 322 | }
|
288 | 323 |
|
289 | 324 | // here, we have the user executor results.
|
@@ -332,3 +367,15 @@ func (v *Venom) RunUserExecutor(ctx context.Context, runner ExecutorRunner, tcIn
|
332 | 367 | }
|
333 | 368 | return result, nil
|
334 | 369 | }
|
| 370 | + |
| 371 | +// quoteTemplateExpressions adds double quotes around template expressions in YAML content. |
| 372 | +// It specifically targets expressions that follow a colon and whitespace like 'key: {{.variable}}' |
| 373 | +// and are not already enclosed in quotes. This ensures proper YAML parsing of template variables. |
| 374 | +func quoteTemplateExpressions(content []byte) []byte { |
| 375 | + // First capture group matches everything up to the colon, checking the last non-whitespace |
| 376 | + // character isn't a quote (to skip JSON keys) |
| 377 | + re := regexp.MustCompile(`(?m)(^.*[^"\s][\s]*)(:\s+)({{.*?}})(.*?)(?:\s*)$`) |
| 378 | + |
| 379 | + // Put quotes around the template expression and what follows it |
| 380 | + return re.ReplaceAll(content, []byte(`$1$2"$3$4"`)) |
| 381 | +} |
0 commit comments