Skip to content

Commit e05d89e

Browse files
committed
[air] Add unit tests for the DABs deploy-path helpers
Covers the parts of rundabs.go that don't need a live workspace: - writeBundleProject: databricks.yml + command.sh land, immutable_folder is set, code_source tree is staged, a user command.sh doesn't shadow the generated one, and the convertibility gate fails before any temp dir is created. - newBundleCarrierCommand: forwards air's profile, declares the flags ProcessBundle reads, and seeds bundle-root + direct engine on the context. - bundleTargetsBlock and isRunnableJob. deployBundle / runDeployedJob still need a live deploy+RunNow and stay covered by staging validation. Co-authored-by: Isaac
1 parent f01b540 commit e05d89e

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package aircmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/databricks/cli/bundle/config/resources"
9+
bundleresources "github.com/databricks/cli/bundle/resources"
10+
"github.com/databricks/cli/libs/env"
11+
"github.com/databricks/databricks-sdk-go"
12+
"github.com/databricks/databricks-sdk-go/config"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestWriteBundleProject(t *testing.T) {
18+
cfg := &runConfig{
19+
ExperimentName: "exp",
20+
Command: new("echo hi"),
21+
Compute: &computeConfig{AcceleratorType: "GPU_1xA10", NumAccelerators: 1},
22+
}
23+
24+
root, cleanup, err := writeBundleProject(t.Context(), cfg, "train.yaml")
25+
require.NoError(t, err)
26+
defer cleanup()
27+
28+
// databricks.yml carries the converted job and the appended dev target.
29+
body, err := os.ReadFile(filepath.Join(root, "databricks.yml"))
30+
require.NoError(t, err)
31+
assert.Contains(t, string(body), "ai_runtime_task:")
32+
assert.Contains(t, string(body), "immutable_folder: true")
33+
assert.Contains(t, string(body), "mode: development")
34+
35+
// command.sh holds the run's command verbatim.
36+
script, err := os.ReadFile(filepath.Join(root, bundleCommandScript))
37+
require.NoError(t, err)
38+
assert.Equal(t, "echo hi", string(script))
39+
40+
// cleanup removes the temp root.
41+
cleanup()
42+
_, err = os.Stat(root)
43+
assert.True(t, os.IsNotExist(err))
44+
}
45+
46+
func TestWriteBundleProjectStagesCodeSource(t *testing.T) {
47+
src := t.TempDir()
48+
require.NoError(t, os.WriteFile(filepath.Join(src, "train.py"), []byte("print()"), 0o644))
49+
require.NoError(t, os.MkdirAll(filepath.Join(src, "pkg"), 0o755))
50+
require.NoError(t, os.WriteFile(filepath.Join(src, "pkg", "mod.py"), []byte("x=1"), 0o644))
51+
52+
cfg := &runConfig{
53+
ExperimentName: "exp",
54+
Command: new("python train.py"),
55+
Compute: &computeConfig{AcceleratorType: "GPU_1xA10", NumAccelerators: 1},
56+
CodeSource: &codeSourceConfig{Type: "snapshot", Snapshot: &snapshotSourceConfig{RootPath: src}},
57+
}
58+
59+
root, cleanup, err := writeBundleProject(t.Context(), cfg, "train.yaml")
60+
require.NoError(t, err)
61+
defer cleanup()
62+
63+
// The user's tree is copied into the bundle root alongside our generated files.
64+
assert.FileExists(t, filepath.Join(root, "train.py"))
65+
assert.FileExists(t, filepath.Join(root, "pkg", "mod.py"))
66+
assert.FileExists(t, filepath.Join(root, "databricks.yml"))
67+
assert.FileExists(t, filepath.Join(root, bundleCommandScript))
68+
}
69+
70+
func TestWriteBundleProjectCommandShadowProtection(t *testing.T) {
71+
// A command.sh in the user's tree must not shadow the one we generate from the
72+
// run's command: our write happens after the tree copy.
73+
src := t.TempDir()
74+
require.NoError(t, os.WriteFile(filepath.Join(src, bundleCommandScript), []byte("STALE"), 0o644))
75+
76+
cfg := &runConfig{
77+
ExperimentName: "exp",
78+
Command: new("FRESH"),
79+
Compute: &computeConfig{AcceleratorType: "GPU_1xA10", NumAccelerators: 1},
80+
CodeSource: &codeSourceConfig{Type: "snapshot", Snapshot: &snapshotSourceConfig{RootPath: src}},
81+
}
82+
83+
root, cleanup, err := writeBundleProject(t.Context(), cfg, "train.yaml")
84+
require.NoError(t, err)
85+
defer cleanup()
86+
87+
script, err := os.ReadFile(filepath.Join(root, bundleCommandScript))
88+
require.NoError(t, err)
89+
assert.Equal(t, "FRESH", string(script))
90+
}
91+
92+
func TestWriteBundleProjectRejectsUnconvertible(t *testing.T) {
93+
// A gate failure (here: a $CODE_SOURCE_PATH command) surfaces before any temp
94+
// directory is created.
95+
cfg := &runConfig{
96+
ExperimentName: "exp",
97+
Command: new("cd $CODE_SOURCE_PATH && python train.py"),
98+
Compute: &computeConfig{AcceleratorType: "GPU_1xA10", NumAccelerators: 1},
99+
}
100+
_, _, err := writeBundleProject(t.Context(), cfg, "train.yaml")
101+
require.Error(t, err)
102+
}
103+
104+
func TestBundleTargetsBlock(t *testing.T) {
105+
block := bundleTargetsBlock()
106+
assert.Contains(t, block, "targets:")
107+
assert.Contains(t, block, dabsTarget+":")
108+
assert.Contains(t, block, "mode: development")
109+
assert.Contains(t, block, "default: true")
110+
}
111+
112+
func TestNewBundleCarrierCommand(t *testing.T) {
113+
// Construct the client directly (not via NewWorkspaceClient, which would try to
114+
// resolve "myprofile" from ~/.databrickscfg); the carrier only reads Config.Profile.
115+
w := &databricks.WorkspaceClient{Config: &config.Config{Profile: "myprofile"}}
116+
117+
cmd := newBundleCarrierCommand(t.Context(), w, "/tmp/bundle-root")
118+
119+
// air's active profile is forwarded so the bundle authenticates the same way.
120+
assert.Equal(t, "myprofile", cmd.Flag("profile").Value.String())
121+
122+
// The flags ProcessBundle reads are declared.
123+
for _, name := range []string{"var", "target", "profile", "output"} {
124+
assert.NotNil(t, cmd.Flag(name), "flag %q must be declared", name)
125+
}
126+
127+
// The bundle root and direct engine are seeded on the command's context.
128+
assert.Equal(t, "/tmp/bundle-root", env.Get(cmd.Context(), "DATABRICKS_BUNDLE_ROOT"))
129+
assert.Equal(t, "direct", env.Get(cmd.Context(), "DATABRICKS_BUNDLE_ENGINE"))
130+
}
131+
132+
func TestNewBundleCarrierCommandNoProfile(t *testing.T) {
133+
// With no profile on the client, the profile flag is left empty (the bundle
134+
// falls back to its own default resolution).
135+
w := &databricks.WorkspaceClient{Config: &config.Config{}}
136+
137+
cmd := newBundleCarrierCommand(t.Context(), w, "/tmp/bundle-root")
138+
assert.Empty(t, cmd.Flag("profile").Value.String())
139+
}
140+
141+
func TestIsRunnableJob(t *testing.T) {
142+
assert.True(t, isRunnableJob(bundleresources.Reference{Resource: &resources.Job{}}))
143+
assert.False(t, isRunnableJob(bundleresources.Reference{Resource: &resources.Pipeline{}}))
144+
}

0 commit comments

Comments
 (0)