package render import ( "bytes" "context" "io" "testing" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/databricks/databricks-sdk-go/service/pipelines" "github.com/databricks/databricks-sdk-go/service/serving" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type renderTestOutputTestCase struct { name string bundle *bundle.Bundle diags diag.Diagnostics opts RenderOptions expected string } func TestRenderTextOutput(t *testing.T) { loadingBundle := &bundle.Bundle{ Config: config.Root{ Bundle: config.Bundle{ Name: "test-bundle", Target: "test-target", }, }, } testCases := []renderTestOutputTestCase{ { name: "nil bundle and 1 error", diags: diag.Diagnostics{ { Severity: diag.Error, Summary: "failed to load xxx", }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: failed to load xxx\n" + "\n" + "Found 1 error\n", }, { name: "nil bundle and 1 recommendation", diags: diag.Diagnostics{ { Severity: diag.Recommendation, Summary: "recommendation", }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Recommendation: recommendation\n" + "\n" + "Found 1 recommendation\n", }, { name: "bundle during 'load' and 1 error", bundle: loadingBundle, diags: diag.Errorf("failed to load bundle"), opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: failed to load bundle\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 1 error\n", }, { name: "bundle during 'load' and 1 warning", bundle: loadingBundle, diags: diag.Warningf("failed to load bundle"), opts: RenderOptions{RenderSummaryTable: true}, expected: "Warning: failed to load bundle\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 1 warning\n", }, { name: "bundle during 'load' and 2 warnings", bundle: loadingBundle, diags: diag.Warningf("warning (1)").Extend(diag.Warningf("warning (2)")), opts: RenderOptions{RenderSummaryTable: true}, expected: "Warning: warning (1)\n" + "\n" + "Warning: warning (2)\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 2 warnings\n", }, { name: "bundle during 'load' and 2 errors, 1 warning and 1 recommendation with details", bundle: loadingBundle, diags: diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, Summary: "error (1)", Detail: "detail (1)", Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Error, Summary: "error (2)", Detail: "detail (2)", Locations: []dyn.Location{{File: "foo.py", Line: 2, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Warning, Summary: "warning (3)", Detail: "detail (3)", Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Recommendation, Summary: "recommendation (4)", Detail: "detail (4)", Locations: []dyn.Location{{File: "foo.py", Line: 4, Column: 1}}, }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: error (1)\n" + " in foo.py:1:1\n" + "\n" + "detail (1)\n" + "\n" + "Error: error (2)\n" + " in foo.py:2:1\n" + "\n" + "detail (2)\n" + "\n" + "Warning: warning (3)\n" + " in foo.py:3:1\n" + "\n" + "detail (3)\n" + "\n" + "Recommendation: recommendation (4)\n" + " in foo.py:4:1\n" + "\n" + "detail (4)\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 2 errors, 1 warning, and 1 recommendation\n", }, { name: "bundle during 'load' and 1 error and 1 warning", bundle: loadingBundle, diags: diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, Summary: "error (1)", Detail: "detail (1)", Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Warning, Summary: "warning (2)", Detail: "detail (2)", Locations: []dyn.Location{{File: "foo.py", Line: 2, Column: 1}}, }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: error (1)\n" + " in foo.py:1:1\n" + "\n" + "detail (1)\n" + "\n" + "Warning: warning (2)\n" + " in foo.py:2:1\n" + "\n" + "detail (2)\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 1 error and 1 warning\n", }, { name: "bundle during 'load' and 1 errors, 2 warning and 2 recommendations with details", bundle: loadingBundle, diags: diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, Summary: "error (1)", Detail: "detail (1)", Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Warning, Summary: "warning (2)", Detail: "detail (2)", Locations: []dyn.Location{{File: "foo.py", Line: 2, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Warning, Summary: "warning (3)", Detail: "detail (3)", Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Recommendation, Summary: "recommendation (4)", Detail: "detail (4)", Locations: []dyn.Location{{File: "foo.py", Line: 4, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Recommendation, Summary: "recommendation (5)", Detail: "detail (5)", Locations: []dyn.Location{{File: "foo.py", Line: 5, Column: 1}}, }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: error (1)\n" + " in foo.py:1:1\n" + "\n" + "detail (1)\n" + "\n" + "Warning: warning (2)\n" + " in foo.py:2:1\n" + "\n" + "detail (2)\n" + "\n" + "Warning: warning (3)\n" + " in foo.py:3:1\n" + "\n" + "detail (3)\n" + "\n" + "Recommendation: recommendation (4)\n" + " in foo.py:4:1\n" + "\n" + "detail (4)\n" + "\n" + "Recommendation: recommendation (5)\n" + " in foo.py:5:1\n" + "\n" + "detail (5)\n" + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + "Found 1 error, 2 warnings, and 2 recommendations\n", }, { name: "bundle during 'init'", bundle: &bundle.Bundle{ Config: config.Root{ Bundle: config.Bundle{ Name: "test-bundle", Target: "test-target", }, Workspace: config.Workspace{ Host: "https://localhost/", CurrentUser: &config.User{ User: &iam.User{ UserName: "test-user", }, }, RootPath: "/Users/test-user@databricks.com/.bundle/examples/test-target", }, }, }, diags: nil, opts: RenderOptions{RenderSummaryTable: true}, expected: "Name: test-bundle\n" + "Target: test-target\n" + "Workspace:\n" + " Host: https://localhost/\n" + " User: test-user\n" + " Path: /Users/test-user@databricks.com/.bundle/examples/test-target\n" + "\n" + "Validation OK!\n", }, { name: "nil bundle without summary with 1 error, 1 warning and 1 recommendation", bundle: nil, diags: diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Error, Summary: "error (1)", Detail: "detail (1)", Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Warning, Summary: "warning (2)", Detail: "detail (2)", Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, }, diag.Diagnostic{ Severity: diag.Recommendation, Summary: "recommendation (3)", Detail: "detail (3)", Locations: []dyn.Location{{File: "foo.py", Line: 5, Column: 1}}, }, }, opts: RenderOptions{RenderSummaryTable: false}, expected: "Error: error (1)\n" + " in foo.py:1:1\n" + "\n" + "detail (1)\n" + "\n" + "Warning: warning (2)\n" + " in foo.py:3:1\n" + "\n" + "detail (2)\n" + "\n" + "Recommendation: recommendation (3)\n" + " in foo.py:5:1\n" + "\n" + "detail (3)\n" + "\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { writer := &bytes.Buffer{} err := RenderDiagnostics(writer, tc.bundle, tc.diags, tc.opts) require.NoError(t, err) assert.Equal(t, tc.expected, writer.String()) }) } } type renderDiagnosticsTestCase struct { name string diags diag.Diagnostics expected string } func TestRenderDiagnostics(t *testing.T) { bundle := &bundle.Bundle{} testCases := []renderDiagnosticsTestCase{ { name: "empty diagnostics", diags: diag.Diagnostics{}, expected: "", }, { name: "error with short summary", diags: diag.Diagnostics{ { Severity: diag.Error, Summary: "failed to load xxx", }, }, expected: "Error: failed to load xxx\n\n", }, { name: "error with source location", diags: diag.Diagnostics{ { Severity: diag.Error, Summary: "failed to load xxx", Detail: "'name' is required", Locations: []dyn.Location{{ File: "foo.yaml", Line: 1, Column: 2}}, }, }, expected: "Error: failed to load xxx\n" + " in foo.yaml:1:2\n\n" + "'name' is required\n\n", }, { name: "error with multiple source locations", diags: diag.Diagnostics{ { Severity: diag.Error, Summary: "failed to load xxx", Detail: "'name' is required", Locations: []dyn.Location{ { File: "foo.yaml", Line: 1, Column: 2, }, { File: "bar.yaml", Line: 3, Column: 4, }, }, }, }, expected: "Error: failed to load xxx\n" + " in foo.yaml:1:2\n" + " bar.yaml:3:4\n\n" + "'name' is required\n\n", }, { name: "error with path", diags: diag.Diagnostics{ { Severity: diag.Error, Detail: "'name' is required", Summary: "failed to load xxx", Paths: []dyn.Path{dyn.MustPathFromString("resources.jobs.xxx")}, }, }, expected: "Error: failed to load xxx\n" + " at resources.jobs.xxx\n" + "\n" + "'name' is required\n\n", }, { name: "error with multiple paths", diags: diag.Diagnostics{ { Severity: diag.Error, Detail: "'name' is required", Summary: "failed to load xxx", Paths: []dyn.Path{ dyn.MustPathFromString("resources.jobs.xxx"), dyn.MustPathFromString("resources.jobs.yyy"), dyn.MustPathFromString("resources.jobs.zzz"), }, }, }, expected: "Error: failed to load xxx\n" + " at resources.jobs.xxx\n" + " resources.jobs.yyy\n" + " resources.jobs.zzz\n" + "\n" + "'name' is required\n\n", }, { name: "recommendation with multiple paths and locations", diags: diag.Diagnostics{ { Severity: diag.Recommendation, Summary: "summary", Detail: "detail", Paths: []dyn.Path{ dyn.MustPathFromString("resources.jobs.xxx"), dyn.MustPathFromString("resources.jobs.yyy"), }, Locations: []dyn.Location{ {File: "foo.yaml", Line: 1, Column: 2}, {File: "bar.yaml", Line: 3, Column: 4}, }, }, }, expected: "Recommendation: summary\n" + " at resources.jobs.xxx\n" + " resources.jobs.yyy\n" + " in foo.yaml:1:2\n" + " bar.yaml:3:4\n\n" + "detail\n\n", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { writer := &bytes.Buffer{} err := renderDiagnosticsOnly(writer, bundle, tc.diags) require.NoError(t, err) assert.Equal(t, tc.expected, writer.String()) }) } } func TestRenderSummaryTemplate_nilBundle(t *testing.T) { writer := &bytes.Buffer{} err := renderSummaryHeaderTemplate(writer, nil) require.NoError(t, err) io.WriteString(writer, buildTrailer(nil)) assert.Equal(t, "Validation OK!\n", writer.String()) } func TestRenderSummary(t *testing.T) { ctx := context.Background() // Create a mock bundle with various resources b := &bundle.Bundle{ Config: config.Root{ Bundle: config.Bundle{ Name: "test-bundle", Target: "test-target", }, Workspace: config.Workspace{ Host: "https://mycompany.databricks.com/", }, Resources: config.Resources{ Jobs: map[string]*resources.Job{ "job1": { ID: "1", URL: "https://url1", JobSettings: &jobs.JobSettings{Name: "job1-name"}, }, "job2": { ID: "2", URL: "https://url2", JobSettings: &jobs.JobSettings{Name: "job2-name"}, }, "job3": { ID: "3", URL: "https://url3", // This emulates deleted job }, }, Pipelines: map[string]*resources.Pipeline{ "pipeline2": { ID: "4", // no URL PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline2-name"}, }, "pipeline1": { ID: "3", URL: "https://url3", PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline1-name"}, }, }, Schemas: map[string]*resources.Schema{ "schema1": { ID: "catalog.schema", CreateSchema: &catalog.CreateSchema{ Name: "schema", }, // no URL }, }, ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ "endpoint1": { ID: "7", CreateServingEndpoint: &serving.CreateServingEndpoint{ Name: "my_serving_endpoint", }, URL: "https://url4", }, }, }, }, } writer := &bytes.Buffer{} err := RenderSummary(ctx, writer, b) require.NoError(t, err) expectedSummary := `Name: test-bundle Target: test-target Workspace: Host: https://mycompany.databricks.com/ Resources: Jobs: job1: Name: job1-name URL: https://url1 job2: Name: job2-name URL: https://url2 Model Serving Endpoints: endpoint1: Name: my_serving_endpoint URL: https://url4 Pipelines: pipeline1: Name: pipeline1-name URL: https://url3 pipeline2: Name: pipeline2-name URL: (not deployed) Schemas: schema1: Name: schema URL: (not deployed) ` assert.Equal(t, expectedSummary, writer.String()) }