Skip to content

Commit ad8e61c

Browse files
authored
Fix ability to import the CLI repository as module (#1671)
## Changes While investigating #1629, I found that Go doesn't allow characters outside the set documented at https://pkg.go.dev/golang.org/x/mod/module#CheckFilePath. To fix this, I changed the relevant test case to create the fixtures it needs instead of loading it from the `testdata` directory (in `renderer_test.go`). Some test cases in `config_test.go` depended on templated paths without needing to do so. In the process of fixing this, I refactored these tests slightly to reduce dependencies between them. This change also adds a test case to ensure that all files in the repository are allowed to be part of a module (per the earlier `CheckFilePath` function). Fixes #1629. ## Tests I manually confirmed I could import the repository as a Go module.
1 parent a38e16c commit ad8e61c

File tree

16 files changed

+296
-75
lines changed

16 files changed

+296
-75
lines changed

internal/testutil/copy.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package testutil
2+
3+
import (
4+
"io"
5+
"io/fs"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// CopyDirectory copies the contents of a directory to another directory.
14+
// The destination directory is created if it does not exist.
15+
func CopyDirectory(t *testing.T, src, dst string) {
16+
err := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
17+
if err != nil {
18+
return err
19+
}
20+
21+
rel, err := filepath.Rel(src, path)
22+
require.NoError(t, err)
23+
24+
if d.IsDir() {
25+
return os.MkdirAll(filepath.Join(dst, rel), 0755)
26+
}
27+
28+
// Copy the file to the temporary directory
29+
in, err := os.Open(path)
30+
if err != nil {
31+
return err
32+
}
33+
34+
defer in.Close()
35+
36+
out, err := os.Create(filepath.Join(dst, rel))
37+
if err != nil {
38+
return err
39+
}
40+
41+
defer out.Close()
42+
43+
_, err = io.Copy(out, in)
44+
return err
45+
})
46+
47+
require.NoError(t, err)
48+
}

libs/template/config_test.go

+92-59
Original file line numberDiff line numberDiff line change
@@ -3,110 +3,153 @@ package template
33
import (
44
"context"
55
"fmt"
6+
"path/filepath"
67
"testing"
78
"text/template"
89

9-
"github.com/databricks/cli/cmd/root"
1010
"github.com/databricks/cli/libs/jsonschema"
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313
)
1414

15-
func testConfig(t *testing.T) *config {
16-
c, err := newConfig(context.Background(), "./testdata/config-test-schema/test-schema.json")
17-
require.NoError(t, err)
18-
return c
19-
}
20-
2115
func TestTemplateConfigAssignValuesFromFile(t *testing.T) {
22-
c := testConfig(t)
16+
testDir := "./testdata/config-assign-from-file"
2317

24-
err := c.assignValuesFromFile("./testdata/config-assign-from-file/config.json")
25-
assert.NoError(t, err)
18+
ctx := context.Background()
19+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
20+
require.NoError(t, err)
2621

27-
assert.Equal(t, int64(1), c.values["int_val"])
28-
assert.Equal(t, float64(2), c.values["float_val"])
29-
assert.Equal(t, true, c.values["bool_val"])
30-
assert.Equal(t, "hello", c.values["string_val"])
22+
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
23+
if assert.NoError(t, err) {
24+
assert.Equal(t, int64(1), c.values["int_val"])
25+
assert.Equal(t, float64(2), c.values["float_val"])
26+
assert.Equal(t, true, c.values["bool_val"])
27+
assert.Equal(t, "hello", c.values["string_val"])
28+
}
3129
}
3230

33-
func TestTemplateConfigAssignValuesFromFileForInvalidIntegerValue(t *testing.T) {
34-
c := testConfig(t)
31+
func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *testing.T) {
32+
testDir := "./testdata/config-assign-from-file"
3533

36-
err := c.assignValuesFromFile("./testdata/config-assign-from-file-invalid-int/config.json")
37-
assert.EqualError(t, err, "failed to load config from file ./testdata/config-assign-from-file-invalid-int/config.json: failed to parse property int_val: cannot convert \"abc\" to an integer")
38-
}
34+
ctx := context.Background()
35+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
36+
require.NoError(t, err)
3937

40-
func TestTemplateConfigAssignValuesFromFileDoesNotOverwriteExistingConfigs(t *testing.T) {
41-
c := testConfig(t)
4238
c.values = map[string]any{
4339
"string_val": "this-is-not-overwritten",
4440
}
4541

46-
err := c.assignValuesFromFile("./testdata/config-assign-from-file/config.json")
47-
assert.NoError(t, err)
42+
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
43+
if assert.NoError(t, err) {
44+
assert.Equal(t, int64(1), c.values["int_val"])
45+
assert.Equal(t, float64(2), c.values["float_val"])
46+
assert.Equal(t, true, c.values["bool_val"])
47+
assert.Equal(t, "this-is-not-overwritten", c.values["string_val"])
48+
}
49+
}
50+
51+
func TestTemplateConfigAssignValuesFromFileForInvalidIntegerValue(t *testing.T) {
52+
testDir := "./testdata/config-assign-from-file-invalid-int"
53+
54+
ctx := context.Background()
55+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
56+
require.NoError(t, err)
4857

49-
assert.Equal(t, int64(1), c.values["int_val"])
50-
assert.Equal(t, float64(2), c.values["float_val"])
51-
assert.Equal(t, true, c.values["bool_val"])
52-
assert.Equal(t, "this-is-not-overwritten", c.values["string_val"])
58+
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
59+
assert.EqualError(t, err, fmt.Sprintf("failed to load config from file %s: failed to parse property int_val: cannot convert \"abc\" to an integer", filepath.Join(testDir, "config.json")))
5360
}
5461

5562
func TestTemplateConfigAssignValuesFromFileFiltersPropertiesNotInTheSchema(t *testing.T) {
56-
c := testConfig(t)
63+
testDir := "./testdata/config-assign-from-file-unknown-property"
5764

58-
err := c.assignValuesFromFile("./testdata/config-assign-from-file-unknown-property/config.json")
65+
ctx := context.Background()
66+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
67+
require.NoError(t, err)
68+
69+
err = c.assignValuesFromFile(filepath.Join(testDir, "config.json"))
5970
assert.NoError(t, err)
6071

6172
// assert only the known property is loaded
6273
assert.Len(t, c.values, 1)
6374
assert.Equal(t, "i am a known property", c.values["string_val"])
6475
}
6576

66-
func TestTemplateConfigAssignDefaultValues(t *testing.T) {
67-
c := testConfig(t)
77+
func TestTemplateConfigAssignValuesFromDefaultValues(t *testing.T) {
78+
testDir := "./testdata/config-assign-from-default-value"
6879

6980
ctx := context.Background()
70-
ctx = root.SetWorkspaceClient(ctx, nil)
71-
helpers := loadHelpers(ctx)
72-
r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", t.TempDir())
81+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
82+
require.NoError(t, err)
83+
84+
r, err := newRenderer(ctx, nil, nil, "./testdata/empty/template", "./testdata/empty/library", t.TempDir())
7385
require.NoError(t, err)
7486

7587
err = c.assignDefaultValues(r)
76-
assert.NoError(t, err)
88+
if assert.NoError(t, err) {
89+
assert.Equal(t, int64(123), c.values["int_val"])
90+
assert.Equal(t, float64(123), c.values["float_val"])
91+
assert.Equal(t, true, c.values["bool_val"])
92+
assert.Equal(t, "hello", c.values["string_val"])
93+
}
94+
}
95+
96+
func TestTemplateConfigAssignValuesFromTemplatedDefaultValues(t *testing.T) {
97+
testDir := "./testdata/config-assign-from-templated-default-value"
98+
99+
ctx := context.Background()
100+
c, err := newConfig(ctx, filepath.Join(testDir, "schema.json"))
101+
require.NoError(t, err)
102+
103+
r, err := newRenderer(ctx, nil, nil, filepath.Join(testDir, "template/template"), filepath.Join(testDir, "template/library"), t.TempDir())
104+
require.NoError(t, err)
77105

78-
assert.Len(t, c.values, 2)
79-
assert.Equal(t, "my_file", c.values["string_val"])
80-
assert.Equal(t, int64(123), c.values["int_val"])
106+
// Note: only the string value is templated.
107+
// The JSON schema package doesn't allow using a string default for integer types.
108+
err = c.assignDefaultValues(r)
109+
if assert.NoError(t, err) {
110+
assert.Equal(t, int64(123), c.values["int_val"])
111+
assert.Equal(t, float64(123), c.values["float_val"])
112+
assert.Equal(t, true, c.values["bool_val"])
113+
assert.Equal(t, "world", c.values["string_val"])
114+
}
81115
}
82116

83117
func TestTemplateConfigValidateValuesDefined(t *testing.T) {
84-
c := testConfig(t)
118+
ctx := context.Background()
119+
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
120+
require.NoError(t, err)
121+
85122
c.values = map[string]any{
86123
"int_val": 1,
87124
"float_val": 1.0,
88125
"bool_val": false,
89126
}
90127

91-
err := c.validate()
128+
err = c.validate()
92129
assert.EqualError(t, err, "validation for template input parameters failed. no value provided for required property string_val")
93130
}
94131

95132
func TestTemplateConfigValidateTypeForValidConfig(t *testing.T) {
96-
c := testConfig(t)
133+
ctx := context.Background()
134+
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
135+
require.NoError(t, err)
136+
97137
c.values = map[string]any{
98138
"int_val": 1,
99139
"float_val": 1.1,
100140
"bool_val": true,
101141
"string_val": "abcd",
102142
}
103143

104-
err := c.validate()
144+
err = c.validate()
105145
assert.NoError(t, err)
106146
}
107147

108148
func TestTemplateConfigValidateTypeForUnknownField(t *testing.T) {
109-
c := testConfig(t)
149+
ctx := context.Background()
150+
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
151+
require.NoError(t, err)
152+
110153
c.values = map[string]any{
111154
"unknown_prop": 1,
112155
"int_val": 1,
@@ -115,20 +158,23 @@ func TestTemplateConfigValidateTypeForUnknownField(t *testing.T) {
115158
"string_val": "abcd",
116159
}
117160

118-
err := c.validate()
161+
err = c.validate()
119162
assert.EqualError(t, err, "validation for template input parameters failed. property unknown_prop is not defined in the schema")
120163
}
121164

122165
func TestTemplateConfigValidateTypeForInvalidType(t *testing.T) {
123-
c := testConfig(t)
166+
ctx := context.Background()
167+
c, err := newConfig(ctx, "testdata/config-test-schema/test-schema.json")
168+
require.NoError(t, err)
169+
124170
c.values = map[string]any{
125171
"int_val": "this-should-be-an-int",
126172
"float_val": 1.1,
127173
"bool_val": true,
128174
"string_val": "abcd",
129175
}
130176

131-
err := c.validate()
177+
err = c.validate()
132178
assert.EqualError(t, err, "validation for template input parameters failed. incorrect type for property int_val: expected type integer, but value is \"this-should-be-an-int\"")
133179
}
134180

@@ -224,19 +270,6 @@ func TestTemplateEnumValidation(t *testing.T) {
224270
assert.NoError(t, c.validate())
225271
}
226272

227-
func TestAssignDefaultValuesWithTemplatedDefaults(t *testing.T) {
228-
c := testConfig(t)
229-
ctx := context.Background()
230-
ctx = root.SetWorkspaceClient(ctx, nil)
231-
helpers := loadHelpers(ctx)
232-
r, err := newRenderer(ctx, nil, helpers, "./testdata/templated-defaults/template", "./testdata/templated-defaults/library", t.TempDir())
233-
require.NoError(t, err)
234-
235-
err = c.assignDefaultValues(r)
236-
assert.NoError(t, err)
237-
assert.Equal(t, "my_file", c.values["string_val"])
238-
}
239-
240273
func TestTemplateSchemaErrorsWithEmptyDescription(t *testing.T) {
241274
_, err := newConfig(context.Background(), "./testdata/config-test-schema/invalid-test-schema.json")
242275
assert.EqualError(t, err, "template property property-without-description is missing a description")

libs/template/renderer_test.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
bundleConfig "github.com/databricks/cli/bundle/config"
1717
"github.com/databricks/cli/bundle/phases"
1818
"github.com/databricks/cli/cmd/root"
19+
"github.com/databricks/cli/internal/testutil"
1920
"github.com/databricks/cli/libs/diag"
2021
"github.com/databricks/cli/libs/tags"
2122
"github.com/databricks/databricks-sdk-go"
@@ -655,15 +656,27 @@ func TestRendererFileTreeRendering(t *testing.T) {
655656
func TestRendererSubTemplateInPath(t *testing.T) {
656657
ctx := context.Background()
657658
ctx = root.SetWorkspaceClient(ctx, nil)
658-
tmpDir := t.TempDir()
659659

660-
helpers := loadHelpers(ctx)
661-
r, err := newRenderer(ctx, nil, helpers, "./testdata/template-in-path/template", "./testdata/template-in-path/library", tmpDir)
660+
// Copy the template directory to a temporary directory where we can safely include a templated file path.
661+
// These paths include characters that are forbidden in Go modules, so we can't use the testdata directory.
662+
// Also see https://github.com/databricks/cli/pull/1671.
663+
templateDir := t.TempDir()
664+
testutil.CopyDirectory(t, "./testdata/template-in-path", templateDir)
665+
666+
// Use a backtick-quoted string; double quotes are a reserved character for Windows paths:
667+
// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file.
668+
testutil.Touch(t, filepath.Join(templateDir, "template/{{template `dir_name`}}/{{template `file_name`}}"))
669+
670+
tmpDir := t.TempDir()
671+
r, err := newRenderer(ctx, nil, nil, filepath.Join(templateDir, "template"), filepath.Join(templateDir, "library"), tmpDir)
662672
require.NoError(t, err)
663673

664674
err = r.walk()
665675
require.NoError(t, err)
666676

667-
assert.Equal(t, filepath.Join(tmpDir, "my_directory", "my_file"), r.files[0].DstPath().absPath())
668-
assert.Equal(t, "my_directory/my_file", r.files[0].DstPath().relPath)
677+
if assert.Len(t, r.files, 2) {
678+
f := r.files[1]
679+
assert.Equal(t, filepath.Join(tmpDir, "my_directory", "my_file"), f.DstPath().absPath())
680+
assert.Equal(t, "my_directory/my_file", f.DstPath().relPath)
681+
}
669682
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"properties": {
3+
"int_val": {
4+
"type": "integer",
5+
"description": "This is an integer value",
6+
"default": 123
7+
},
8+
"float_val": {
9+
"type": "number",
10+
"description": "This is a float value",
11+
"default": 123
12+
},
13+
"bool_val": {
14+
"type": "boolean",
15+
"description": "This is a boolean value",
16+
"default": true
17+
},
18+
"string_val": {
19+
"type": "string",
20+
"description": "This is a string value",
21+
"default": "hello"
22+
}
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"properties": {
3+
"int_val": {
4+
"type": "integer",
5+
"description": "This is an integer value"
6+
},
7+
"float_val": {
8+
"type": "number",
9+
"description": "This is a float value"
10+
},
11+
"bool_val": {
12+
"type": "boolean",
13+
"description": "This is a boolean value"
14+
},
15+
"string_val": {
16+
"type": "string",
17+
"description": "This is a string value"
18+
}
19+
}
20+
}

0 commit comments

Comments
 (0)