From bafdc76163a9acbdc9045810e3085f216ab6e071 Mon Sep 17 00:00:00 2001 From: idelafuente-oc Date: Thu, 20 Nov 2025 18:12:22 -0300 Subject: [PATCH 1/5] Adds for each validation --- bundle/config/validate/fast_validate.go | 1 + bundle/config/validate/for_each_task.go | 76 ++++++++ bundle/config/validate/for_each_task_test.go | 190 +++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 bundle/config/validate/for_each_task.go create mode 100644 bundle/config/validate/for_each_task_test.go diff --git a/bundle/config/validate/fast_validate.go b/bundle/config/validate/fast_validate.go index d01eb8c149..d7d2c8eab7 100644 --- a/bundle/config/validate/fast_validate.go +++ b/bundle/config/validate/fast_validate.go @@ -29,6 +29,7 @@ func (f *fastValidate) Apply(ctx context.Context, rb *bundle.Bundle) diag.Diagno // Fast mutators with only in-memory checks JobClusterKeyDefined(), JobTaskClusterSpec(), + ForEachTask(), // Blocking mutators. Deployments will fail if these checks fail. ValidateArtifactPath(), diff --git a/bundle/config/validate/for_each_task.go b/bundle/config/validate/for_each_task.go new file mode 100644 index 0000000000..c1d68be8b5 --- /dev/null +++ b/bundle/config/validate/for_each_task.go @@ -0,0 +1,76 @@ +package validate + +import ( + "context" + "fmt" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/jobs" +) + +// ForEachTask validates constraints for for_each_task configuration +func ForEachTask() bundle.ReadOnlyMutator { + return &forEachTask{} +} + +type forEachTask struct{ bundle.RO } + +func (v *forEachTask) Name() string { + return "validate:for_each_task" +} + +func (v *forEachTask) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + diags := diag.Diagnostics{} + + jobsPath := dyn.NewPath(dyn.Key("resources"), dyn.Key("jobs")) + + for resourceName, job := range b.Config.Resources.Jobs { + resourcePath := jobsPath.Append(dyn.Key(resourceName)) + + for taskIndex, task := range job.Tasks { + taskPath := resourcePath.Append(dyn.Key("tasks"), dyn.Index(taskIndex)) + + if task.ForEachTask != nil { + diags = diags.Extend(validateForEachTask(b, task, taskPath)) + } + } + } + + return diags +} + +func validateForEachTask(b *bundle.Bundle, task jobs.Task, taskPath dyn.Path) diag.Diagnostics { + diags := diag.Diagnostics{} + + if task.MaxRetries != 0 { + diags = diags.Append(invalidRetryFieldDiag(b, task, taskPath, "max_retries", diag.Error)) + } + + if task.MinRetryIntervalMillis != 0 { + diags = diags.Append(invalidRetryFieldDiag(b, task, taskPath, "min_retry_interval_millis", diag.Warning)) + } + + if task.RetryOnTimeout { + diags = diags.Append(invalidRetryFieldDiag(b, task, taskPath, "retry_on_timeout", diag.Warning)) + } + + return diags +} + +func invalidRetryFieldDiag(b *bundle.Bundle, task jobs.Task, taskPath dyn.Path, fieldName string, severity diag.Severity) diag.Diagnostic { + detail := fmt.Sprintf( + "Task %q has %s defined at the parent level, but it uses for_each_task.\n"+ + "When using for_each_task, %s must be defined on the nested task (for_each_task.task.%s), not on the parent task.", + task.TaskKey, fieldName, fieldName, fieldName, + ) + + return diag.Diagnostic{ + Severity: severity, + Summary: fmt.Sprintf("Invalid %s configuration for for_each_task", fieldName), + Detail: detail, + Locations: b.Config.GetLocations(taskPath.String()), + Paths: []dyn.Path{taskPath}, + } +} diff --git a/bundle/config/validate/for_each_task_test.go b/bundle/config/validate/for_each_task_test.go new file mode 100644 index 0000000000..c45aa96633 --- /dev/null +++ b/bundle/config/validate/for_each_task_test.go @@ -0,0 +1,190 @@ +package validate + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/internal/bundletest" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createBundleWithForEachTask(parentTask jobs.Task) *bundle.Bundle { + if parentTask.ForEachTask == nil { + parentTask.ForEachTask = &jobs.ForEachTask{ + Inputs: "[1, 2, 3]", + Task: jobs.Task{ + TaskKey: "child_task", + NotebookTask: &jobs.NotebookTask{ + NotebookPath: "test.py", + }, + }, + } + } + + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job1": { + JobSettings: jobs.JobSettings{ + Name: "My Job", + Tasks: []jobs.Task{parentTask}, + }, + }, + }, + }, + }, + } + + bundletest.SetLocation(b, "resources.jobs.job1.tasks[0]", []dyn.Location{{File: "job.yml", Line: 1, Column: 1}}) + return b +} + +func TestForEachTask_InvalidRetryFields(t *testing.T) { + tests := []struct { + name string + task jobs.Task + expectedSeverity diag.Severity + expectedSummary string + expectedDetail string + }{ + { + name: "max_retries on parent", + task: jobs.Task{ + TaskKey: "parent_task", + MaxRetries: 3, + }, + expectedSeverity: diag.Error, + expectedSummary: "Invalid max_retries configuration for for_each_task", + expectedDetail: "max_retries must be defined on the nested task", + }, + { + name: "min_retry_interval_millis on parent", + task: jobs.Task{ + TaskKey: "parent_task", + MinRetryIntervalMillis: 1000, + }, + expectedSeverity: diag.Warning, + expectedSummary: "Invalid min_retry_interval_millis configuration for for_each_task", + expectedDetail: "min_retry_interval_millis must be defined on the nested task", + }, + { + name: "retry_on_timeout on parent", + task: jobs.Task{ + TaskKey: "parent_task", + RetryOnTimeout: true, + }, + expectedSeverity: diag.Warning, + expectedSummary: "Invalid retry_on_timeout configuration for for_each_task", + expectedDetail: "retry_on_timeout must be defined on the nested task", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(tt.task) + + diags := ForEachTask().Apply(ctx, b) + + require.Len(t, diags, 1) + assert.Equal(t, tt.expectedSeverity, diags[0].Severity) + assert.Equal(t, tt.expectedSummary, diags[0].Summary) + assert.Contains(t, diags[0].Detail, tt.expectedDetail) + }) + } +} + +func TestForEachTask_MultipleRetryFieldsOnParent(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(jobs.Task{ + TaskKey: "parent_task", + MaxRetries: 3, + MinRetryIntervalMillis: 1000, + RetryOnTimeout: true, + }) + + diags := ForEachTask().Apply(ctx, b) + require.Len(t, diags, 3) + + errorCount := 0 + warningCount := 0 + for _, d := range diags { + if d.Severity == diag.Error { + errorCount++ + } else if d.Severity == diag.Warning { + warningCount++ + } + } + assert.Equal(t, 1, errorCount) + assert.Equal(t, 2, warningCount) +} + +func TestForEachTask_ValidConfigurationOnChild(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(jobs.Task{ + TaskKey: "parent_task", + ForEachTask: &jobs.ForEachTask{ + Inputs: "[1, 2, 3]", + Task: jobs.Task{ + TaskKey: "child_task", + MaxRetries: 3, + NotebookTask: &jobs.NotebookTask{ + NotebookPath: "test.py", + }, + }, + }, + }) + + diags := ForEachTask().Apply(ctx, b) + assert.Empty(t, diags) +} + +func TestForEachTask_NoForEachTask(t *testing.T) { + ctx := context.Background() + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job1": { + JobSettings: jobs.JobSettings{ + Name: "My Job", + Tasks: []jobs.Task{ + { + TaskKey: "simple_task", + MaxRetries: 3, + NotebookTask: &jobs.NotebookTask{ + NotebookPath: "test.py", + }, + }, + }, + }, + }, + }, + }, + }, + } + + bundletest.SetLocation(b, "resources.jobs.job1.tasks[0]", []dyn.Location{{File: "job.yml", Line: 1, Column: 1}}) + + diags := ForEachTask().Apply(ctx, b) + assert.Empty(t, diags) +} + +func TestForEachTask_RetryOnTimeoutFalse(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(jobs.Task{ + TaskKey: "parent_task", + RetryOnTimeout: false, + }) + + diags := ForEachTask().Apply(ctx, b) + assert.Empty(t, diags) +} From 1e4193a2b92a1fd4823deebdf5fcf601447b74c2 Mon Sep 17 00:00:00 2001 From: idelafuente-oc Date: Mon, 8 Dec 2025 11:31:36 -0300 Subject: [PATCH 2/5] fix: github action lint --- bundle/config/validate/for_each_task_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bundle/config/validate/for_each_task_test.go b/bundle/config/validate/for_each_task_test.go index c45aa96633..350da09769 100644 --- a/bundle/config/validate/for_each_task_test.go +++ b/bundle/config/validate/for_each_task_test.go @@ -117,9 +117,10 @@ func TestForEachTask_MultipleRetryFieldsOnParent(t *testing.T) { errorCount := 0 warningCount := 0 for _, d := range diags { - if d.Severity == diag.Error { + switch d.Severity { + case diag.Error: errorCount++ - } else if d.Severity == diag.Warning { + case diag.Warning: warningCount++ } } From 77bc751cd648e029ef68453a92fcb4bb817b4eda Mon Sep 17 00:00:00 2001 From: idelafuente-oc Date: Wed, 10 Dec 2025 12:20:43 -0300 Subject: [PATCH 3/5] feat: acceptance tests for for each tasks validation --- .../for_each_task_max_retries/databricks.yml | 16 ++++++++++ .../for_each_task_max_retries/out.test.toml | 5 ++++ .../for_each_task_max_retries/output.txt | 16 ++++++++++ .../validate/for_each_task_max_retries/script | 2 ++ .../for_each_task_max_retries/test.py | 2 ++ .../for_each_task_retry_fields/databricks.yml | 18 +++++++++++ .../for_each_task_retry_fields/out.test.toml | 5 ++++ .../for_each_task_retry_fields/output.txt | 30 +++++++++++++++++++ .../for_each_task_retry_fields/script | 2 ++ .../for_each_task_retry_fields/test.py | 2 ++ .../for_each_task_valid/databricks.yml | 16 ++++++++++ .../for_each_task_valid/out.test.toml | 5 ++++ .../validate/for_each_task_valid/output.txt | 7 +++++ .../validate/for_each_task_valid/script | 2 ++ .../validate/for_each_task_valid/test.py | 2 ++ 15 files changed, 130 insertions(+) create mode 100644 acceptance/bundle/validate/for_each_task_max_retries/databricks.yml create mode 100644 acceptance/bundle/validate/for_each_task_max_retries/out.test.toml create mode 100644 acceptance/bundle/validate/for_each_task_max_retries/output.txt create mode 100644 acceptance/bundle/validate/for_each_task_max_retries/script create mode 100644 acceptance/bundle/validate/for_each_task_max_retries/test.py create mode 100644 acceptance/bundle/validate/for_each_task_retry_fields/databricks.yml create mode 100644 acceptance/bundle/validate/for_each_task_retry_fields/out.test.toml create mode 100644 acceptance/bundle/validate/for_each_task_retry_fields/output.txt create mode 100644 acceptance/bundle/validate/for_each_task_retry_fields/script create mode 100644 acceptance/bundle/validate/for_each_task_retry_fields/test.py create mode 100644 acceptance/bundle/validate/for_each_task_valid/databricks.yml create mode 100644 acceptance/bundle/validate/for_each_task_valid/out.test.toml create mode 100644 acceptance/bundle/validate/for_each_task_valid/output.txt create mode 100644 acceptance/bundle/validate/for_each_task_valid/script create mode 100644 acceptance/bundle/validate/for_each_task_valid/test.py diff --git a/acceptance/bundle/validate/for_each_task_max_retries/databricks.yml b/acceptance/bundle/validate/for_each_task_max_retries/databricks.yml new file mode 100644 index 0000000000..dfe07e4484 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_max_retries/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: "for_each_task_validation" + +resources: + jobs: + test_job: + name: "Test Job" + tasks: + - task_key: "parent_task" + for_each_task: + inputs: "[1, 2, 3]" + task: + task_key: "child_task" + notebook_task: + notebook_path: "test.py" + max_retries: 3 diff --git a/acceptance/bundle/validate/for_each_task_max_retries/out.test.toml b/acceptance/bundle/validate/for_each_task_max_retries/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_max_retries/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/validate/for_each_task_max_retries/output.txt b/acceptance/bundle/validate/for_each_task_max_retries/output.txt new file mode 100644 index 0000000000..d2f057650c --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_max_retries/output.txt @@ -0,0 +1,16 @@ +Error: Invalid max_retries configuration for for_each_task + at resources.jobs.test_job.tasks[0] + in databricks.yml:9:11 + +Task "parent_task" has max_retries defined at the parent level, but it uses for_each_task. +When using for_each_task, max_retries must be defined on the nested task (for_each_task.task.max_retries), not on the parent task. + +Name: for_each_task_validation +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/for_each_task_validation/default + +Found 1 error + +Exit code: 1 diff --git a/acceptance/bundle/validate/for_each_task_max_retries/script b/acceptance/bundle/validate/for_each_task_max_retries/script new file mode 100644 index 0000000000..d286e54863 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_max_retries/script @@ -0,0 +1,2 @@ +#!/bin/bash +errcode $CLI bundle validate diff --git a/acceptance/bundle/validate/for_each_task_max_retries/test.py b/acceptance/bundle/validate/for_each_task_max_retries/test.py new file mode 100644 index 0000000000..4d1620a14e --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_max_retries/test.py @@ -0,0 +1,2 @@ +# Databricks notebook source +print("test") diff --git a/acceptance/bundle/validate/for_each_task_retry_fields/databricks.yml b/acceptance/bundle/validate/for_each_task_retry_fields/databricks.yml new file mode 100644 index 0000000000..69ea50df59 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_retry_fields/databricks.yml @@ -0,0 +1,18 @@ +bundle: + name: "for_each_task_validation" + +resources: + jobs: + test_job: + name: "Test Job" + tasks: + - task_key: "parent_task" + for_each_task: + inputs: "[1, 2, 3]" + task: + task_key: "child_task" + notebook_task: + notebook_path: "test.py" + max_retries: 3 + min_retry_interval_millis: 1000 + retry_on_timeout: true diff --git a/acceptance/bundle/validate/for_each_task_retry_fields/out.test.toml b/acceptance/bundle/validate/for_each_task_retry_fields/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_retry_fields/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/validate/for_each_task_retry_fields/output.txt b/acceptance/bundle/validate/for_each_task_retry_fields/output.txt new file mode 100644 index 0000000000..311d855947 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_retry_fields/output.txt @@ -0,0 +1,30 @@ +Error: Invalid max_retries configuration for for_each_task + at resources.jobs.test_job.tasks[0] + in databricks.yml:9:11 + +Task "parent_task" has max_retries defined at the parent level, but it uses for_each_task. +When using for_each_task, max_retries must be defined on the nested task (for_each_task.task.max_retries), not on the parent task. + +Warning: Invalid min_retry_interval_millis configuration for for_each_task + at resources.jobs.test_job.tasks[0] + in databricks.yml:9:11 + +Task "parent_task" has min_retry_interval_millis defined at the parent level, but it uses for_each_task. +When using for_each_task, min_retry_interval_millis must be defined on the nested task (for_each_task.task.min_retry_interval_millis), not on the parent task. + +Warning: Invalid retry_on_timeout configuration for for_each_task + at resources.jobs.test_job.tasks[0] + in databricks.yml:9:11 + +Task "parent_task" has retry_on_timeout defined at the parent level, but it uses for_each_task. +When using for_each_task, retry_on_timeout must be defined on the nested task (for_each_task.task.retry_on_timeout), not on the parent task. + +Name: for_each_task_validation +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/for_each_task_validation/default + +Found 1 error and 2 warnings + +Exit code: 1 diff --git a/acceptance/bundle/validate/for_each_task_retry_fields/script b/acceptance/bundle/validate/for_each_task_retry_fields/script new file mode 100644 index 0000000000..d286e54863 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_retry_fields/script @@ -0,0 +1,2 @@ +#!/bin/bash +errcode $CLI bundle validate diff --git a/acceptance/bundle/validate/for_each_task_retry_fields/test.py b/acceptance/bundle/validate/for_each_task_retry_fields/test.py new file mode 100644 index 0000000000..4d1620a14e --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_retry_fields/test.py @@ -0,0 +1,2 @@ +# Databricks notebook source +print("test") diff --git a/acceptance/bundle/validate/for_each_task_valid/databricks.yml b/acceptance/bundle/validate/for_each_task_valid/databricks.yml new file mode 100644 index 0000000000..a1dbd9aa56 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_valid/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: "for_each_task_validation" + +resources: + jobs: + test_job: + name: "Test Job" + tasks: + - task_key: "parent_task" + for_each_task: + inputs: "[1, 2, 3]" + task: + task_key: "child_task" + max_retries: 3 + notebook_task: + notebook_path: "test.py" diff --git a/acceptance/bundle/validate/for_each_task_valid/out.test.toml b/acceptance/bundle/validate/for_each_task_valid/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_valid/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/validate/for_each_task_valid/output.txt b/acceptance/bundle/validate/for_each_task_valid/output.txt new file mode 100644 index 0000000000..6503425481 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_valid/output.txt @@ -0,0 +1,7 @@ +Name: for_each_task_validation +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/for_each_task_validation/default + +Validation OK! diff --git a/acceptance/bundle/validate/for_each_task_valid/script b/acceptance/bundle/validate/for_each_task_valid/script new file mode 100644 index 0000000000..c57053d9d0 --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_valid/script @@ -0,0 +1,2 @@ +#!/bin/bash +$CLI bundle validate diff --git a/acceptance/bundle/validate/for_each_task_valid/test.py b/acceptance/bundle/validate/for_each_task_valid/test.py new file mode 100644 index 0000000000..4d1620a14e --- /dev/null +++ b/acceptance/bundle/validate/for_each_task_valid/test.py @@ -0,0 +1,2 @@ +# Databricks notebook source +print("test") From de3dbdaff3f61cbf6278392bc63c9626f54b7a6c Mon Sep 17 00:00:00 2001 From: idelafuente-oc Date: Wed, 10 Dec 2025 12:21:14 -0300 Subject: [PATCH 4/5] refactor: simplifies unit test --- bundle/config/validate/for_each_task_test.go | 147 +++++-------------- 1 file changed, 36 insertions(+), 111 deletions(-) diff --git a/bundle/config/validate/for_each_task_test.go b/bundle/config/validate/for_each_task_test.go index 350da09769..c797db5b2a 100644 --- a/bundle/config/validate/for_each_task_test.go +++ b/bundle/config/validate/for_each_task_test.go @@ -47,88 +47,55 @@ func createBundleWithForEachTask(parentTask jobs.Task) *bundle.Bundle { return b } -func TestForEachTask_InvalidRetryFields(t *testing.T) { - tests := []struct { - name string - task jobs.Task - expectedSeverity diag.Severity - expectedSummary string - expectedDetail string - }{ - { - name: "max_retries on parent", - task: jobs.Task{ - TaskKey: "parent_task", - MaxRetries: 3, - }, - expectedSeverity: diag.Error, - expectedSummary: "Invalid max_retries configuration for for_each_task", - expectedDetail: "max_retries must be defined on the nested task", - }, - { - name: "min_retry_interval_millis on parent", - task: jobs.Task{ - TaskKey: "parent_task", - MinRetryIntervalMillis: 1000, - }, - expectedSeverity: diag.Warning, - expectedSummary: "Invalid min_retry_interval_millis configuration for for_each_task", - expectedDetail: "min_retry_interval_millis must be defined on the nested task", - }, - { - name: "retry_on_timeout on parent", - task: jobs.Task{ - TaskKey: "parent_task", - RetryOnTimeout: true, - }, - expectedSeverity: diag.Warning, - expectedSummary: "Invalid retry_on_timeout configuration for for_each_task", - expectedDetail: "retry_on_timeout must be defined on the nested task", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(tt.task) +func TestForEachTask_MaxRetriesError(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(jobs.Task{ + TaskKey: "parent_task", + MaxRetries: 3, + }) - diags := ForEachTask().Apply(ctx, b) + diags := ForEachTask().Apply(ctx, b) - require.Len(t, diags, 1) - assert.Equal(t, tt.expectedSeverity, diags[0].Severity) - assert.Equal(t, tt.expectedSummary, diags[0].Summary) - assert.Contains(t, diags[0].Detail, tt.expectedDetail) - }) - } + require.Len(t, diags, 1) + assert.Equal(t, diag.Error, diags[0].Severity) + assert.Equal(t, "Invalid max_retries configuration for for_each_task", diags[0].Summary) + assert.Contains(t, diags[0].Detail, `Task "parent_task" has max_retries defined at the parent level`) + assert.Contains(t, diags[0].Detail, "for_each_task.task.max_retries") } -func TestForEachTask_MultipleRetryFieldsOnParent(t *testing.T) { +func TestForEachTask_MinRetryIntervalWarning(t *testing.T) { ctx := context.Background() b := createBundleWithForEachTask(jobs.Task{ TaskKey: "parent_task", - MaxRetries: 3, MinRetryIntervalMillis: 1000, - RetryOnTimeout: true, }) diags := ForEachTask().Apply(ctx, b) - require.Len(t, diags, 3) - - errorCount := 0 - warningCount := 0 - for _, d := range diags { - switch d.Severity { - case diag.Error: - errorCount++ - case diag.Warning: - warningCount++ - } - } - assert.Equal(t, 1, errorCount) - assert.Equal(t, 2, warningCount) + + require.Len(t, diags, 1) + assert.Equal(t, diag.Warning, diags[0].Severity) + assert.Equal(t, "Invalid min_retry_interval_millis configuration for for_each_task", diags[0].Summary) + assert.Contains(t, diags[0].Detail, `Task "parent_task" has min_retry_interval_millis defined at the parent level`) + assert.Contains(t, diags[0].Detail, "for_each_task.task.min_retry_interval_millis") +} + +func TestForEachTask_RetryOnTimeoutWarning(t *testing.T) { + ctx := context.Background() + b := createBundleWithForEachTask(jobs.Task{ + TaskKey: "parent_task", + RetryOnTimeout: true, + }) + + diags := ForEachTask().Apply(ctx, b) + + require.Len(t, diags, 1) + assert.Equal(t, diag.Warning, diags[0].Severity) + assert.Equal(t, "Invalid retry_on_timeout configuration for for_each_task", diags[0].Summary) + assert.Contains(t, diags[0].Detail, `Task "parent_task" has retry_on_timeout defined at the parent level`) + assert.Contains(t, diags[0].Detail, "for_each_task.task.retry_on_timeout") } -func TestForEachTask_ValidConfigurationOnChild(t *testing.T) { +func TestForEachTask_ValidConfiguration(t *testing.T) { ctx := context.Background() b := createBundleWithForEachTask(jobs.Task{ TaskKey: "parent_task", @@ -147,45 +114,3 @@ func TestForEachTask_ValidConfigurationOnChild(t *testing.T) { diags := ForEachTask().Apply(ctx, b) assert.Empty(t, diags) } - -func TestForEachTask_NoForEachTask(t *testing.T) { - ctx := context.Background() - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "job1": { - JobSettings: jobs.JobSettings{ - Name: "My Job", - Tasks: []jobs.Task{ - { - TaskKey: "simple_task", - MaxRetries: 3, - NotebookTask: &jobs.NotebookTask{ - NotebookPath: "test.py", - }, - }, - }, - }, - }, - }, - }, - }, - } - - bundletest.SetLocation(b, "resources.jobs.job1.tasks[0]", []dyn.Location{{File: "job.yml", Line: 1, Column: 1}}) - - diags := ForEachTask().Apply(ctx, b) - assert.Empty(t, diags) -} - -func TestForEachTask_RetryOnTimeoutFalse(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(jobs.Task{ - TaskKey: "parent_task", - RetryOnTimeout: false, - }) - - diags := ForEachTask().Apply(ctx, b) - assert.Empty(t, diags) -} From 6292ae5ab89683fbdd64571775620ea01c9321d2 Mon Sep 17 00:00:00 2001 From: idelafuente-oc Date: Thu, 11 Dec 2025 14:28:33 -0300 Subject: [PATCH 5/5] revert: deletes unit test --- bundle/config/validate/for_each_task_test.go | 116 ------------------- 1 file changed, 116 deletions(-) delete mode 100644 bundle/config/validate/for_each_task_test.go diff --git a/bundle/config/validate/for_each_task_test.go b/bundle/config/validate/for_each_task_test.go deleted file mode 100644 index c797db5b2a..0000000000 --- a/bundle/config/validate/for_each_task_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package validate - -import ( - "context" - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/bundle/internal/bundletest" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/databricks-sdk-go/service/jobs" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func createBundleWithForEachTask(parentTask jobs.Task) *bundle.Bundle { - if parentTask.ForEachTask == nil { - parentTask.ForEachTask = &jobs.ForEachTask{ - Inputs: "[1, 2, 3]", - Task: jobs.Task{ - TaskKey: "child_task", - NotebookTask: &jobs.NotebookTask{ - NotebookPath: "test.py", - }, - }, - } - } - - b := &bundle.Bundle{ - Config: config.Root{ - Resources: config.Resources{ - Jobs: map[string]*resources.Job{ - "job1": { - JobSettings: jobs.JobSettings{ - Name: "My Job", - Tasks: []jobs.Task{parentTask}, - }, - }, - }, - }, - }, - } - - bundletest.SetLocation(b, "resources.jobs.job1.tasks[0]", []dyn.Location{{File: "job.yml", Line: 1, Column: 1}}) - return b -} - -func TestForEachTask_MaxRetriesError(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(jobs.Task{ - TaskKey: "parent_task", - MaxRetries: 3, - }) - - diags := ForEachTask().Apply(ctx, b) - - require.Len(t, diags, 1) - assert.Equal(t, diag.Error, diags[0].Severity) - assert.Equal(t, "Invalid max_retries configuration for for_each_task", diags[0].Summary) - assert.Contains(t, diags[0].Detail, `Task "parent_task" has max_retries defined at the parent level`) - assert.Contains(t, diags[0].Detail, "for_each_task.task.max_retries") -} - -func TestForEachTask_MinRetryIntervalWarning(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(jobs.Task{ - TaskKey: "parent_task", - MinRetryIntervalMillis: 1000, - }) - - diags := ForEachTask().Apply(ctx, b) - - require.Len(t, diags, 1) - assert.Equal(t, diag.Warning, diags[0].Severity) - assert.Equal(t, "Invalid min_retry_interval_millis configuration for for_each_task", diags[0].Summary) - assert.Contains(t, diags[0].Detail, `Task "parent_task" has min_retry_interval_millis defined at the parent level`) - assert.Contains(t, diags[0].Detail, "for_each_task.task.min_retry_interval_millis") -} - -func TestForEachTask_RetryOnTimeoutWarning(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(jobs.Task{ - TaskKey: "parent_task", - RetryOnTimeout: true, - }) - - diags := ForEachTask().Apply(ctx, b) - - require.Len(t, diags, 1) - assert.Equal(t, diag.Warning, diags[0].Severity) - assert.Equal(t, "Invalid retry_on_timeout configuration for for_each_task", diags[0].Summary) - assert.Contains(t, diags[0].Detail, `Task "parent_task" has retry_on_timeout defined at the parent level`) - assert.Contains(t, diags[0].Detail, "for_each_task.task.retry_on_timeout") -} - -func TestForEachTask_ValidConfiguration(t *testing.T) { - ctx := context.Background() - b := createBundleWithForEachTask(jobs.Task{ - TaskKey: "parent_task", - ForEachTask: &jobs.ForEachTask{ - Inputs: "[1, 2, 3]", - Task: jobs.Task{ - TaskKey: "child_task", - MaxRetries: 3, - NotebookTask: &jobs.NotebookTask{ - NotebookPath: "test.py", - }, - }, - }, - }) - - diags := ForEachTask().Apply(ctx, b) - assert.Empty(t, diags) -}