From aff8176c8054cd82bff0245112bea50270c51c93 Mon Sep 17 00:00:00 2001 From: Yota Hamada Date: Wed, 7 Aug 2024 00:14:39 +0900 Subject: [PATCH] refactor tests --- examples/foreach.yaml | 11 + internal/dag/builder.go | 1 + internal/dag/builder_test.go | 796 ++++++------------ internal/dag/definition.go | 2 +- internal/dag/loader.go | 4 +- internal/dag/loader_test.go | 4 +- internal/dag/parser.go | 39 +- internal/dag/testdata/http_executor.yaml | 4 + .../testdata/http_executor_with_config.yaml | 7 + internal/dag/testdata/invalid_env.yaml | 2 + internal/dag/testdata/invalid_params.yaml | 1 + internal/dag/testdata/invalid_schedule.yaml | 5 + internal/dag/testdata/no_command.yaml | 2 + internal/dag/testdata/no_name.yaml | 2 + .../testdata/params_with_complex_values.yaml | 3 + .../testdata/params_with_quoted_values.yaml | 1 + .../testdata/params_with_substitution.yaml | 1 + .../schedule_with_multiple_values.yaml | 10 + internal/dag/testdata/signal_on_stop.yaml | 4 + internal/dag/testdata/valid_command.yaml | 3 + .../dag/testdata/valid_command_in_array.yaml | 3 + .../dag/testdata/valid_command_in_list.yaml | 5 + internal/dag/testdata/valid_env.yaml | 2 + .../dag/testdata/valid_env_substitution.yaml | 2 + .../valid_env_substitution_and_env.yaml | 3 + internal/dag/testdata/valid_schedule.yaml | 4 + internal/dag/testdata/valid_tags.yaml | 4 + internal/dag/testdata/valid_tags_list.yaml | 6 + 28 files changed, 378 insertions(+), 553 deletions(-) create mode 100644 examples/foreach.yaml create mode 100644 internal/dag/testdata/http_executor.yaml create mode 100644 internal/dag/testdata/http_executor_with_config.yaml create mode 100644 internal/dag/testdata/invalid_env.yaml create mode 100644 internal/dag/testdata/invalid_params.yaml create mode 100644 internal/dag/testdata/invalid_schedule.yaml create mode 100644 internal/dag/testdata/no_command.yaml create mode 100644 internal/dag/testdata/no_name.yaml create mode 100644 internal/dag/testdata/params_with_complex_values.yaml create mode 100644 internal/dag/testdata/params_with_quoted_values.yaml create mode 100644 internal/dag/testdata/params_with_substitution.yaml create mode 100644 internal/dag/testdata/schedule_with_multiple_values.yaml create mode 100644 internal/dag/testdata/signal_on_stop.yaml create mode 100644 internal/dag/testdata/valid_command.yaml create mode 100644 internal/dag/testdata/valid_command_in_array.yaml create mode 100644 internal/dag/testdata/valid_command_in_list.yaml create mode 100644 internal/dag/testdata/valid_env.yaml create mode 100644 internal/dag/testdata/valid_env_substitution.yaml create mode 100644 internal/dag/testdata/valid_env_substitution_and_env.yaml create mode 100644 internal/dag/testdata/valid_schedule.yaml create mode 100644 internal/dag/testdata/valid_tags.yaml create mode 100644 internal/dag/testdata/valid_tags_list.yaml diff --git a/examples/foreach.yaml b/examples/foreach.yaml new file mode 100644 index 00000000..c437b106 --- /dev/null +++ b/examples/foreach.yaml @@ -0,0 +1,11 @@ +steps: + - name: foreach + foreach: "[1, 2, 3]" + command: sh + output: "foreach-$1.txt" + script: | + echo "Hello, world! $1" + - name: merge + command: sh + script: | + cat foreach-*.txt diff --git a/internal/dag/builder.go b/internal/dag/builder.go index e89ac2ab..f0840312 100644 --- a/internal/dag/builder.go +++ b/internal/dag/builder.go @@ -82,6 +82,7 @@ var ( errStepCommandMustBeArrayOrString = errors.New( "step command must be an array of strings or a string", ) + errInvalidParamValue = errors.New("invalid parameter value") errCallFunctionNotFound = errors.New( "call must specify a functions that exists", ) diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go index b5325c3c..10fbc37a 100644 --- a/internal/dag/builder_test.go +++ b/internal/dag/builder_test.go @@ -16,422 +16,334 @@ package dag import ( - "fmt" + "errors" "os" "path/filepath" - "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestBuilder_BuildErrors(t *testing.T) { - tests := []struct { - name string - input string - }{ - { - name: "NoName", - input: ` -steps: - - command: echo 1`, - }, - { - name: "NoCommand", - input: ` -steps: - - name: step 1`, - }, - { - name: "InvalidEnv", - input: fmt.Sprintf(` -env: - - VAR: %q`, "`invalid`"), - }, - { - name: "InvalidParams", - input: fmt.Sprintf(`params: %q`, "`invalid`"), - }, - { - name: "InvalidSchedule", - input: `schedule: "1"`, - }, - } +type testCase struct { + Name string + InputFile string + Expected map[string]any + ExpectedErr error +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dg, err := unmarshalData([]byte(tt.input)) - require.NoError(t, err) +type stepTestCase map[string]any - def, err := decode(dg) - require.NoError(t, err) +func readTestFile(t *testing.T, filename string) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join(testdataDir, filename)) + require.NoError(t, err) + return data +} - _, err = new(builder).build(def, nil) - require.Error(t, err) - }) +func runTest(t *testing.T, tc testCase) { + t.Helper() + dag, err := LoadYAML(readTestFile(t, tc.InputFile), buildOpts{}) + + if tc.ExpectedErr != nil { + assert.Error(t, err) + if errs, ok := err.(*errorList); ok && len(*errs) > 0 { + // check if the error is in the list of errors + found := false + for _, e := range *errs { + if errors.Is(e, tc.ExpectedErr) { + found = true + break + } + } + if !found { + t.Errorf("expected error %v, got %v", tc.ExpectedErr, err) + } + } else if !errors.Is(err, tc.ExpectedErr) { + t.Errorf("expected error %v, got %v", tc.ExpectedErr, err) + } + return + } + + require.NoError(t, err) + for k, v := range tc.Expected { + switch k { + case "steps": + stepTestCases := v.([]stepTestCase) + require.Len(t, dag.Steps, len(stepTestCases)) + for i, step := range dag.Steps { + testStep(t, step, stepTestCases[i]) + } + case "env": + for envKey, envVal := range v.(map[string]string) { + assert.Equal(t, envVal, os.Getenv(envKey)) + } + case "tags": + for _, tag := range v.([]string) { + assert.True(t, dag.HasTag(tag)) + } + case "schedule": + schedules := v.(map[string][]string) + for scheduleType, expressions := range schedules { + var actual []Schedule + switch scheduleKey(scheduleType) { + case scheduleKeyStart: + actual = dag.Schedule + case scheduleKeyStop: + actual = dag.StopSchedule + case scheduleKeyRestart: + actual = dag.RestartSchedule + } + assert.Len(t, actual, len(expressions)) + for i, expr := range expressions { + assert.Equal(t, expr, actual[i].Expression) + } + } + } } } -func TestBuilder_BuildEnvs(t *testing.T) { - tests := []struct { - name string - input string - expected map[string]string - }{ +func testStep(t *testing.T, step Step, tc stepTestCase) { + for k, v := range tc { + switch k { + case "name": + assert.Equal(t, v.(string), step.Name) + case "command": + assert.Equal(t, v.(string), step.Command) + case "args": + assert.Equal(t, v.([]string), step.Args) + case "executorConfig": + assert.Equal(t, v.(map[string]any), step.ExecutorConfig.Config) + case "executor": + assert.Equal(t, v.(string), step.ExecutorConfig.Type) + case "signalOnStop": + assert.Equal(t, v.(string), step.SignalOnStop) + } + } +} + +func TestBuilder_Build(t *testing.T) { + tests := []testCase{ { - name: "ValidEnv", - input: ` -env: - "1": "123" -`, - expected: map[string]string{"1": "123"}, + Name: "NoName", + InputFile: "no_name.yaml", + ExpectedErr: errStepNameRequired, }, { - name: "ValidEnvWithSubstitution", - input: ` -env: - VAR: "` + "`echo 1`" + `" -`, - expected: map[string]string{"VAR": "1"}, + Name: "NoCommand", + InputFile: "no_command.yaml", + ExpectedErr: errStepCommandOrCallRequired, }, { - name: "ValidEnvWithSubstitutionAndEnv", - input: ` -env: - - "FOO": "BAR" - - "FOO": "${FOO}:BAZ" - - "FOO": "${FOO}:BAR" - - "FOO": "${FOO}:FOO" -`, - expected: map[string]string{"FOO": "BAR:BAZ:BAR:FOO"}, + Name: "InvalidEnv", + InputFile: "invalid_env.yaml", + ExpectedErr: errInvalidEnvValue, + }, + { + Name: "InvalidParams", + InputFile: "invalid_params.yaml", + ExpectedErr: errInvalidParamValue, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dg, err := unmarshalData([]byte(tt.input)) - require.NoError(t, err) - - def, err := decode(dg) - require.NoError(t, err) - - _, err = new(builder).build(def, nil) - require.NoError(t, err) - - for k, v := range tt.expected { - require.Equal(t, v, os.Getenv(k)) - } - }) - } -} - -func TestBuilder_BuildParams(t *testing.T) { - tests := []struct { - name string - params string - env string - expected map[string]string - }{ { - name: "ValidParams", - params: "x", - expected: map[string]string{ - "1": "x", + Name: "InvalidSchedule", + InputFile: "invalid_schedule.yaml", + ExpectedErr: errInvalidSchedule, + }, + { + Name: "ValidEnv", + InputFile: "valid_env.yaml", + Expected: map[string]any{ + "env": map[string]string{"FOO": "123"}, }, }, { - name: "TwoParams", - params: "x y", - expected: map[string]string{ - "1": "x", - "2": "y", + Name: "ValidEnvWithSubstitution", + InputFile: "valid_env_substitution.yaml", + Expected: map[string]any{ + "env": map[string]string{"VAR": "123"}, }, }, { - name: "ThreeParams", - params: "x yy zzz", - expected: map[string]string{ - "1": "x", - "2": "yy", - "3": "zzz", + Name: "ValidEnvWithSubstitutionAndEnv", + InputFile: "valid_env_substitution_and_env.yaml", + Expected: map[string]any{ + "env": map[string]string{"FOO": "BAR:BAZ:BAR:FOO"}, }, }, { - name: "ParamsWithSubstitution", - params: "x $1", - expected: map[string]string{ - "1": "x", - "2": "x", + Name: "ValidCommand", + InputFile: "valid_command.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "command": "echo", + "args": []string{"1"}, + "name": "step 1", + }, + }, }, }, { - name: "QuotedParams", - params: `x="1" y="2"`, - expected: map[string]string{ - "x": "1", - "y": "2", + Name: "ValidCommandInArray", + InputFile: "valid_command_in_array.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "command": "echo", + "args": []string{"1"}, + "name": "step 1", + }, + }, }, }, { - name: "ComplexParams", - params: "first P1=foo P2=${FOO} P3=`/bin/echo BAR` X=bar Y=${P1} Z=\"A B C\"", - env: "FOO: BAR", - expected: map[string]string{ - "P1": "foo", - "P2": "BAR", - "P3": "BAR", - "X": "bar", - "Y": "foo", - "Z": "A B C", - "1": "first", - "2": `P1=foo`, - "3": `P2=BAR`, - "4": `P3=BAR`, - "5": `X=bar`, - "6": `Y=foo`, - "7": `Z=A B C`, + Name: "ValidCommandInList", + InputFile: "valid_command_in_list.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "command": "echo", + "args": []string{"1"}, + "name": "step 1", + }, + }, }, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var data string - if tt.env != "" { - data = fmt.Sprintf(`env: -- %s -params: %s -`, tt.env, tt.params) - } else { - data = fmt.Sprintf(`params: %s -`, tt.params) - } - dg, err := unmarshalData([]byte(data)) - require.NoError(t, err) - - def, err := decode(dg) - require.NoError(t, err) - - _, err = new(builder).build(def, nil) - require.NoError(t, err) - - for k, v := range tt.expected { - require.Equal(t, v, os.Getenv(k)) - } - }) - } -} - -func TestBuilder_BuildCommand(t *testing.T) { - tests := []struct { - name string - input string - }{ { - name: "ValidCommand", - input: ` -steps: - - name: step1 - command: echo 1`, + Name: "ValidTags", + InputFile: "valid_tags.yaml", + Expected: map[string]any{ + "tags": []string{"daily", "monthly"}, + }, }, { - name: "ValidCommandInArray", - input: ` -steps: - - name: step1 - command: ['echo', '1']`, + Name: "ValidTagsList", + InputFile: "valid_tags_list.yaml", + Expected: map[string]any{ + "tags": []string{"daily", "monthly"}, + }, }, { - name: "ValidCommandInJSONArray", - input: ` -steps: - - name: step1 - command: [echo, 1]`, + Name: "ValidSchedule", + InputFile: "valid_schedule.yaml", + Expected: map[string]any{ + "schedule": map[string][]string{ + "start": {"0 1 * * *"}, + "stop": {"0 2 * * *"}, + "restart": {"0 12 * * *"}, + }, + }, }, { - name: "ValidCommandInYAMLArray", - input: ` -steps: - - name: step1 - command: - - echo - - 1`, + Name: "ScheduleWithMultipleValues", + InputFile: "schedule_with_multiple_values.yaml", + Expected: map[string]any{ + "schedule": map[string][]string{ + "start": { + "0 1 * * *", + "0 18 * * *", + }, + "stop": { + "0 2 * * *", + "0 20 * * *", + }, + "restart": { + "0 12 * * *", + "0 22 * * *", + }, + }, + }, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dg, err := unmarshalData([]byte(tt.input)) - require.NoError(t, err) - - def, err := decode(dg) - require.NoError(t, err) - - dag, err := new(builder).build(def, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if len(dag.Steps) != 1 { - t.Fatalf("expected 1 step, got %d", len(dag.Steps)) - } - - step := dag.Steps[0] - require.Equal(t, "echo", step.Command) - require.Equal(t, []string{"1"}, step.Args) - }) - } -} - -func Test_expandEnv(t *testing.T) { - t.Run("ExpandEnv", func(t *testing.T) { - _ = os.Setenv("FOO", "BAR") - require.Equal(t, expandEnv("${FOO}", false), "BAR") - require.Equal(t, expandEnv("${FOO}", true), "${FOO}") - }) -} - -func TestBuilder_BuildTags(t *testing.T) { - t.Run("ValidTags", func(t *testing.T) { - input := `tags: Daily, Monthly` - expected := []string{"daily", "monthly"} - - m, err := unmarshalData([]byte(input)) - require.NoError(t, err) - - def, err := decode(m) - require.NoError(t, err) - - dg, err := new(builder).build(def, nil) - require.NoError(t, err) - - for _, tag := range expected { - require.True(t, dg.HasTag(tag)) - } - - require.False(t, dg.HasTag("weekly")) - }) -} - -func TestBuilder_BuildSchedule(t *testing.T) { - tests := []struct { - name string - input string - wantErr bool - expected map[string][]string - }{ { - name: "ValidSchedule", - input: ` -schedule: - start: "0 1 * * *" - stop: "0 2 * * *" - restart: "0 12 * * *" -`, - expected: map[string][]string{ - "start": {"0 1 * * *"}, - "stop": {"0 2 * * *"}, - "restart": {"0 12 * * *"}, + Name: "HTTPExecutor", + InputFile: "http_executor.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "executor": "http", + }, + }, }, }, { - name: "OnlyStartSchedule", - input: ` -schedule: - start: "0 1 * * *" -`, - expected: map[string][]string{ - "start": {"0 1 * * *"}, + Name: "HTTPExecutorWithConfig", + InputFile: "http_executor_with_config.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "executor": "http", + "executorConfig": map[string]any{ + "key": "value", + }, + }, + }, }, }, { - name: "OnlyStopSchedule", - input: `schedule: - stop: "0 1 * * *" -`, - expected: map[string][]string{ - "stop": {"0 1 * * *"}, + Name: "SignalOnStop", + InputFile: "signal_on_stop.yaml", + Expected: map[string]any{ + "steps": []stepTestCase{ + { + "signalOnStop": "SIGINT", + }, + }, }, }, { - name: "MultipleSchedules", - input: ` -schedule: - start: - - "0 1 * * *" - - "0 18 * * *" - stop: - - "0 2 * * *" - - "0 20 * * *" -`, - expected: map[string][]string{ - "start": {"0 1 * * *", "0 18 * * *"}, - "stop": {"0 2 * * *", "0 20 * * *"}, + Name: "ParamsWithSubstitution", + InputFile: "params_with_substitution.yaml", + Expected: map[string]any{ + "env": map[string]string{ + "1": "x", + "2": "x", + }, }, }, { - name: "InvalidCronExp", - input: ` -schedule: - stop: "* * * * * * *" -`, - wantErr: true, + Name: "ParamsWithQuotedValues", + InputFile: "params_with_quoted_values.yaml", + Expected: map[string]any{ + "env": map[string]string{ + "x": "a b c", + "y": "d e f", + }, + }, }, { - name: "InvalidKey", - input: ` -schedule: - invalid: "* * * * * * *" -`, - wantErr: true, + Name: "ParamsWithComplexValues", + InputFile: "params_with_complex_values.yaml", + Expected: map[string]any{ + "env": map[string]string{ + "P1": "foo", + "P2": "BAR", + "P3": "BAR", + "X": "bar", + "Y": "foo", + "Z": "A B C", + "1": "first", + "2": `P1=foo`, + "3": `P2=BAR`, + "4": `P3=BAR`, + "5": `X=bar`, + "6": `Y=foo`, + "7": `Z=A B C`, + }, + }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m, err := unmarshalData([]byte(tt.input)) - require.NoError(t, err) - - def, err := decode(m) - require.NoError(t, err) - - dg, err := new(builder).build(def, nil) - - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - for k, v := range tt.expected { - var actual []Schedule - switch scheduleKey(k) { - case scheduleKeyStart: - actual = dg.Schedule - case scheduleKeyStop: - actual = dg.StopSchedule - case scheduleKeyRestart: - actual = dg.RestartSchedule - } - - if len(actual) != len(v) { - t.Errorf("expected %d schedules, got %d", len(v), len(actual)) - } - - for i, s := range actual { - if s.Expression != v[i] { - t.Errorf("expected %s, got %s", v[i], s.Expression) - } - } - } + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + runTest(t, tc) }) } } -func TestLoad(t *testing.T) { +func TestOverrideBaseConfig(t *testing.T) { // Base config has the following values: // MailOn: {Failure: true, Success: false} - t.Run("OverrideBaseConfig", func(t *testing.T) { + t.Run("Override", func(t *testing.T) { baseConfig := filepath.Join(testdataDir, "base.yaml") // Overwrite the base config with the following values: @@ -443,7 +355,7 @@ func TestLoad(t *testing.T) { require.Equal(t, &MailOn{Failure: false, Success: false}, dg.MailOn) require.Equal(t, dg.HistRetentionDays, 7) }) - t.Run("NoOverrideBaseConfig", func(t *testing.T) { + t.Run("WithoutOverride", func(t *testing.T) { baseConfig := filepath.Join(testdataDir, "base.yaml") // no_overwrite.yaml does not have the MailOn key. @@ -455,191 +367,3 @@ func TestLoad(t *testing.T) { require.Equal(t, dg.HistRetentionDays, 30) }) } - -func TestBuilder_BuildExecutor(t *testing.T) { - tests := []struct { - name string - input string - expectedExec string - expectedConfig map[string]any - }{ - { - name: "HTTPExecutor", - input: ` -steps: - - name: S1 - command: echo 1 - executor: http -`, - expectedExec: "http", - expectedConfig: nil, - }, - { - name: "HTTPExecutorWithConfig", - input: ` -steps: - - name: S1 - command: echo 1 - executor: - type: http - config: - key: value -`, - expectedExec: "http", - expectedConfig: map[string]any{ - "key": "value", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - dg, err := unmarshalData([]byte(tt.input)) - require.NoError(t, err) - - def, err := decode(dg) - require.NoError(t, err) - - dag, err := new(builder).build(def, nil) - require.NoError(t, err) - - if len(dag.Steps) != 1 { - t.Errorf("expected 1 step, got %d", len(dag.Steps)) - } - - require.Equal(t, tt.expectedExec, dag.Steps[0].ExecutorConfig.Type) - if tt.expectedConfig != nil { - require.Equal(t, tt.expectedConfig, dag.Steps[0].ExecutorConfig.Config) - } - }) - } -} - -const ( - testSignalOnStop = ` -steps: - - name: "1" - command: "true" - signalOnStop: "SIGINT" -` - testSignalOnStopInvalid = ` -steps: - - name: "1" - command: "true" - signalOnStop: 1000 -` -) - -func TestBuilder_BuildSignalOnStop(t *testing.T) { - t.Run("SignalOnStop", func(t *testing.T) { - ret, err := LoadYAML([]byte(testSignalOnStop)) - require.NoError(t, err) - if len(ret.Steps) != 1 { - t.Fatalf("expected 1 step, got %d", len(ret.Steps)) - } - require.Equal(t, ret.Steps[0].SignalOnStop, "SIGINT") - }) - t.Run("InvalidSignal", func(t *testing.T) { - _, err := LoadYAML([]byte(testSignalOnStopInvalid)) - require.Error(t, err) - }) -} - -func Test_convertMap(t *testing.T) { - t.Run("ValidMap", func(t *testing.T) { - data := map[string]any{ - "key1": "value1", - "map": map[any]any{ - "key2": "value2", - "map": map[any]any{ - "key3": "value3", - }, - }, - } - - err := convertMap(data) - require.NoError(t, err) - - m1 := data["map"] - k1 := reflect.TypeOf(m1).Key().Kind() - require.True(t, k1 == reflect.String) - - m2 := data["map"].(map[string]any)["map"] - k2 := reflect.TypeOf(m2).Key().Kind() - require.True(t, k2 == reflect.String) - - expected := map[string]any{ - "key1": "value1", - "map": map[string]any{ - "key2": "value2", - "map": map[string]any{ - "key3": "value3", - }, - }, - } - require.Equal(t, expected, data) - }) - t.Run("InvalidMap", func(t *testing.T) { - data := map[string]any{ - "key1": "value1", - "map": map[any]any{ - 1: "value2", - }, - } - - err := convertMap(data) - require.Error(t, err) - }) -} - -func Test_evaluateValue(t *testing.T) { - tests := []struct { - name string - input string - expected string - wantErr bool - }{ - { - name: "EnvVar", - input: "${TEST_VAR}", - expected: "test", - }, - { - name: "CommandSubstitution", - input: "`echo test`", - expected: "test", - }, - { - name: "InvalidCommand", - input: "`ech test`", - wantErr: true, - }, - } - - // Set the environment variable for the tests - err := os.Setenv("TEST_VAR", "test") - require.NoError(t, err) - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - r, err := substituteCommands(os.ExpandEnv(tt.input)) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tt.expected, r) - }) - } -} - -func Test_parseParams(t *testing.T) { - t.Run("ParamsWithCommandSubstitution", func(t *testing.T) { - val := "QUESTION=\"what is your favorite activity?\"" - ret, err := parseParamValue(val, true) - require.NoError(t, err) - require.Equal(t, 1, len(ret)) - require.Equal(t, ret[0].name, "QUESTION") - require.Equal(t, ret[0].value, "what is your favorite activity?") - }) -} diff --git a/internal/dag/definition.go b/internal/dag/definition.go index 5a366c65..04c0a4a5 100644 --- a/internal/dag/definition.go +++ b/internal/dag/definition.go @@ -39,7 +39,7 @@ type definition struct { MaxActiveRuns int Params string MaxCleanUpTimeSec *int - Tags string + Tags any } type conditionDef struct { diff --git a/internal/dag/loader.go b/internal/dag/loader.go index 9c720583..f52b36a7 100644 --- a/internal/dag/loader.go +++ b/internal/dag/loader.go @@ -64,7 +64,7 @@ func LoadMetadata(dag string) (*DAG, error) { } // LoadYAML loads config from YAML data. -func LoadYAML(data []byte) (*DAG, error) { +func LoadYAML(data []byte, opts buildOpts) (*DAG, error) { raw, err := unmarshalData(data) if err != nil { return nil, err @@ -75,7 +75,7 @@ func LoadYAML(data []byte) (*DAG, error) { return nil, err } - b := &builder{opts: buildOpts{metadataOnly: false, noEval: true}} + b := &builder{opts: opts} return b.build(def, nil) } diff --git a/internal/dag/loader_test.go b/internal/dag/loader_test.go index a0fbce1b..38cac1c5 100644 --- a/internal/dag/loader_test.go +++ b/internal/dag/loader_test.go @@ -123,7 +123,7 @@ steps: func Test_LoadYAML(t *testing.T) { t.Run("ValidYAMLData", func(t *testing.T) { - ret, err := LoadYAML([]byte(testDAG)) + ret, err := LoadYAML([]byte(testDAG), buildOpts{}) require.NoError(t, err) require.Equal(t, ret.Name, "test DAG") @@ -132,7 +132,7 @@ func Test_LoadYAML(t *testing.T) { require.Equal(t, step.Command, "true") }) t.Run("InvalidYAMLData", func(t *testing.T) { - _, err := LoadYAML([]byte(`invalidyaml`)) + _, err := LoadYAML([]byte(`invalidyaml`), buildOpts{}) require.Error(t, err) }) } diff --git a/internal/dag/parser.go b/internal/dag/parser.go index d339eab9..9f6142b3 100644 --- a/internal/dag/parser.go +++ b/internal/dag/parser.go @@ -223,9 +223,7 @@ func parseParamValue( ) if cmdErr != nil { - return nil, fmt.Errorf( - "error evaluating '%s': %w", value, cmdErr, - ) + return nil, fmt.Errorf("%w: %s", errInvalidParamValue, cmdErr) } } } @@ -251,9 +249,12 @@ func parseKeyValue(m map[any]any, pairs *[]pair) error { return errInvalidKeyType } - val, ok := v.(string) - if !ok { - return errInvalidEnvValue + var val string + switch v := v.(type) { + case string: + val = v + default: + val = fmt.Sprintf("%v", v) } *pairs = append(*pairs, pair{key: key, val: val}) @@ -346,13 +347,27 @@ func parseKey(value any) (string, error) { // parseTags builds a list of tags from the value. // It converts the tags to lowercase and trims the whitespace. -func parseTags(value string) []string { - ret := []string{} +func parseTags(value any) []string { + var ret []string - for _, v := range strings.Split(value, ",") { - tag := strings.ToLower(strings.TrimSpace(v)) - if tag != "" { - ret = append(ret, tag) + switch v := value.(type) { + case string: + for _, v := range strings.Split(v, ",") { + tag := strings.ToLower(strings.TrimSpace(v)) + if tag != "" { + ret = append(ret, tag) + } + } + case []any: + for _, v := range v { + switch v := v.(type) { + case string: + ret = append(ret, strings.ToLower(strings.TrimSpace(v))) + default: + ret = append(ret, strings.ToLower( + strings.TrimSpace(fmt.Sprintf("%v", v))), + ) + } } } diff --git a/internal/dag/testdata/http_executor.yaml b/internal/dag/testdata/http_executor.yaml new file mode 100644 index 00000000..0fc1aec6 --- /dev/null +++ b/internal/dag/testdata/http_executor.yaml @@ -0,0 +1,4 @@ +steps: + - command: GET http://example.com + name: step 1 + executor: http diff --git a/internal/dag/testdata/http_executor_with_config.yaml b/internal/dag/testdata/http_executor_with_config.yaml new file mode 100644 index 00000000..08ecb93c --- /dev/null +++ b/internal/dag/testdata/http_executor_with_config.yaml @@ -0,0 +1,7 @@ +steps: + - command: http://example.com + name: step 1 + executor: + type: http + config: + key: value diff --git a/internal/dag/testdata/invalid_env.yaml b/internal/dag/testdata/invalid_env.yaml new file mode 100644 index 00000000..01d8182d --- /dev/null +++ b/internal/dag/testdata/invalid_env.yaml @@ -0,0 +1,2 @@ +env: + - VAR: "`invalid command`" diff --git a/internal/dag/testdata/invalid_params.yaml b/internal/dag/testdata/invalid_params.yaml new file mode 100644 index 00000000..3f5bbdb2 --- /dev/null +++ b/internal/dag/testdata/invalid_params.yaml @@ -0,0 +1 @@ +params: "`invalid command`" diff --git a/internal/dag/testdata/invalid_schedule.yaml b/internal/dag/testdata/invalid_schedule.yaml new file mode 100644 index 00000000..5dc08851 --- /dev/null +++ b/internal/dag/testdata/invalid_schedule.yaml @@ -0,0 +1,5 @@ +schedule: "1" + +steps: + - command: echo 1 + name: step 1 diff --git a/internal/dag/testdata/no_command.yaml b/internal/dag/testdata/no_command.yaml new file mode 100644 index 00000000..e17d718b --- /dev/null +++ b/internal/dag/testdata/no_command.yaml @@ -0,0 +1,2 @@ +steps: + - name: step 1 diff --git a/internal/dag/testdata/no_name.yaml b/internal/dag/testdata/no_name.yaml new file mode 100644 index 00000000..f706a9d7 --- /dev/null +++ b/internal/dag/testdata/no_name.yaml @@ -0,0 +1,2 @@ +steps: + - command: echo 1 diff --git a/internal/dag/testdata/params_with_complex_values.yaml b/internal/dag/testdata/params_with_complex_values.yaml new file mode 100644 index 00000000..6bddfc74 --- /dev/null +++ b/internal/dag/testdata/params_with_complex_values.yaml @@ -0,0 +1,3 @@ +params: first P1=foo P2=${FOO} P3=`/bin/echo BAR` X=bar Y=${P1} Z="A B C" +env: + - FOO: BAR diff --git a/internal/dag/testdata/params_with_quoted_values.yaml b/internal/dag/testdata/params_with_quoted_values.yaml new file mode 100644 index 00000000..1eed8e5b --- /dev/null +++ b/internal/dag/testdata/params_with_quoted_values.yaml @@ -0,0 +1 @@ +params: x="a b c" y="d e f" diff --git a/internal/dag/testdata/params_with_substitution.yaml b/internal/dag/testdata/params_with_substitution.yaml new file mode 100644 index 00000000..4a649e03 --- /dev/null +++ b/internal/dag/testdata/params_with_substitution.yaml @@ -0,0 +1 @@ +params: "x $1" diff --git a/internal/dag/testdata/schedule_with_multiple_values.yaml b/internal/dag/testdata/schedule_with_multiple_values.yaml new file mode 100644 index 00000000..51191d4c --- /dev/null +++ b/internal/dag/testdata/schedule_with_multiple_values.yaml @@ -0,0 +1,10 @@ +schedule: + start: + - "0 1 * * *" + - "0 18 * * *" + stop: + - "0 2 * * *" + - "0 20 * * *" + restart: + - "0 12 * * *" + - "0 22 * * *" diff --git a/internal/dag/testdata/signal_on_stop.yaml b/internal/dag/testdata/signal_on_stop.yaml new file mode 100644 index 00000000..8658a007 --- /dev/null +++ b/internal/dag/testdata/signal_on_stop.yaml @@ -0,0 +1,4 @@ +steps: + - command: echo 1 + name: step 1 + signalOnStop: SIGINT diff --git a/internal/dag/testdata/valid_command.yaml b/internal/dag/testdata/valid_command.yaml new file mode 100644 index 00000000..057d25f1 --- /dev/null +++ b/internal/dag/testdata/valid_command.yaml @@ -0,0 +1,3 @@ +steps: + - command: echo 1 + name: step 1 diff --git a/internal/dag/testdata/valid_command_in_array.yaml b/internal/dag/testdata/valid_command_in_array.yaml new file mode 100644 index 00000000..58fd4cd2 --- /dev/null +++ b/internal/dag/testdata/valid_command_in_array.yaml @@ -0,0 +1,3 @@ +steps: + - command: [echo, 1] + name: step 1 diff --git a/internal/dag/testdata/valid_command_in_list.yaml b/internal/dag/testdata/valid_command_in_list.yaml new file mode 100644 index 00000000..f8759045 --- /dev/null +++ b/internal/dag/testdata/valid_command_in_list.yaml @@ -0,0 +1,5 @@ +steps: + - command: + - echo + - 1 + name: step 1 diff --git a/internal/dag/testdata/valid_env.yaml b/internal/dag/testdata/valid_env.yaml new file mode 100644 index 00000000..36c286bc --- /dev/null +++ b/internal/dag/testdata/valid_env.yaml @@ -0,0 +1,2 @@ +env: + - "FOO": 123 diff --git a/internal/dag/testdata/valid_env_substitution.yaml b/internal/dag/testdata/valid_env_substitution.yaml new file mode 100644 index 00000000..c129e884 --- /dev/null +++ b/internal/dag/testdata/valid_env_substitution.yaml @@ -0,0 +1,2 @@ +env: + - VAR: "`echo 123`" diff --git a/internal/dag/testdata/valid_env_substitution_and_env.yaml b/internal/dag/testdata/valid_env_substitution_and_env.yaml new file mode 100644 index 00000000..bcd5f44d --- /dev/null +++ b/internal/dag/testdata/valid_env_substitution_and_env.yaml @@ -0,0 +1,3 @@ +env: + - BAR: "`echo BAR`" + - FOO: "${BAR}:BAZ:${BAR}:`echo FOO`" diff --git a/internal/dag/testdata/valid_schedule.yaml b/internal/dag/testdata/valid_schedule.yaml new file mode 100644 index 00000000..5d3c748e --- /dev/null +++ b/internal/dag/testdata/valid_schedule.yaml @@ -0,0 +1,4 @@ +schedule: + start: "0 1 * * *" + stop: "0 2 * * *" + restart: "0 12 * * *" diff --git a/internal/dag/testdata/valid_tags.yaml b/internal/dag/testdata/valid_tags.yaml new file mode 100644 index 00000000..bfa0d53a --- /dev/null +++ b/internal/dag/testdata/valid_tags.yaml @@ -0,0 +1,4 @@ +tags: daily,monthly +steps: + - command: echo 1 + name: step 1 diff --git a/internal/dag/testdata/valid_tags_list.yaml b/internal/dag/testdata/valid_tags_list.yaml new file mode 100644 index 00000000..b2ce36af --- /dev/null +++ b/internal/dag/testdata/valid_tags_list.yaml @@ -0,0 +1,6 @@ +tags: + - daily + - monthly +steps: + - command: echo 1 + name: step 1