databricks-cli/bundle/render/render_text_output_test.go

596 lines
15 KiB
Go
Raw Normal View History

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)
_, err = io.WriteString(writer, buildTrailer(nil))
require.NoError(t, err)
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())
}