Skip to content

Commit

Permalink
Add e2e tests for bundle init flow (#1073)
Browse files Browse the repository at this point in the history
Add smoke tests for bundle init and auth flows.

Also fixes these problems:
- DATABRICKS_CONFIG_FILE env var was not always respected (for example
when creating a new profile)
- bundle file watcher was broken on windows: we were using raw fsPath
with backslashes, which doesn't work for globs
- saveNewProfile was broken on windows: `toISOString` returns values
with characters that aren't allowed in windows paths, resulting in
`ENOENT` errors from `copyFile`
  • Loading branch information
ilia-db authored Feb 21, 2024
1 parent 65f03ee commit efa3c6b
Show file tree
Hide file tree
Showing 22 changed files with 698 additions and 478 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"overrides": [
{
"files": "**/*.test.ts",
"files": ["**/*.test.ts", "**/test/**"],
"rules": {
"no-console": "off"
}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ jobs:
run: yarn run test:cov
working-directory: packages/databricks-vscode

# - name: Integration Tests
# run: yarn run test:integ
# working-directory: packages/databricks-vscode
- name: Integration Tests
run: yarn run test:integ
working-directory: packages/databricks-vscode

- name: Integration Tests SDK wrappers
run: yarn run test:integ:sdk
Expand Down
6 changes: 3 additions & 3 deletions packages/databricks-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"license": "LicenseRef-LICENSE",
"version": "2.0.0",
"engines": {
"vscode": "^1.83.0"
"vscode": "^1.86.0"
},
"categories": [
"Data Science",
Expand Down Expand Up @@ -844,8 +844,8 @@
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vsce": "^2.15.0",
"wdio-video-reporter": "^4.0.5",
"wdio-vscode-service": "^5.2.2",
"wdio-video-reporter": "^5.1.4",
"wdio-vscode-service": "^6.0.2",
"yargs": "^17.7.2"
},
"nyc": {
Expand Down
20 changes: 11 additions & 9 deletions packages/databricks-vscode/src/bundle/BundleFileSet.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Uri} from "vscode";
import {BundleFileSet, getAbsolutePath} from "./BundleFileSet";
import {BundleFileSet, getAbsoluteGlobPath} from "./BundleFileSet";
import {expect} from "chai";
import path from "path";
import * as tmp from "tmp-promise";
Expand All @@ -18,16 +18,18 @@ describe(__filename, async function () {
await tmpdir.cleanup();
});

it("should return the correct absolute path", () => {
it("should return the correct absolute glob path", () => {
const tmpdirUri = Uri.file(tmpdir.path);

expect(getAbsolutePath("test.txt", tmpdirUri).fsPath).to.equal(
path.join(tmpdirUri.fsPath, "test.txt")
let expectedGlob = path.join(tmpdirUri.fsPath, "test.txt");
if (process.platform === "win32") {
expectedGlob = expectedGlob.replace(/\\/g, "/");
}
expect(getAbsoluteGlobPath("test.txt", tmpdirUri)).to.equal(
expectedGlob
);
expect(getAbsoluteGlobPath(Uri.file("test.txt"), tmpdirUri)).to.equal(
expectedGlob
);

expect(
getAbsolutePath(Uri.file("test.txt"), tmpdirUri).fsPath
).to.equal(path.join(tmpdirUri.fsPath, "test.txt"));
});

it("should find the correct root bundle yaml", async () => {
Expand Down
31 changes: 12 additions & 19 deletions packages/databricks-vscode/src/bundle/BundleFileSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function writeBundleYaml(file: Uri, data: BundleSchema) {

export async function getSubProjects(root: Uri) {
const subProjectRoots = await glob.glob(
toGlobPath(getAbsolutePath(subProjectFilePattern, root).fsPath),
getAbsoluteGlobPath(subProjectFilePattern, root),
{nocase: process.platform === "win32"}
);
const normalizedRoot = path.normalize(root.fsPath);
Expand All @@ -40,11 +40,10 @@ export async function getSubProjects(root: Uri) {
});
}

export function getAbsolutePath(path: string | Uri, root: Uri) {
if (typeof path === "string") {
return Uri.joinPath(root, path);
}
return Uri.joinPath(root, path.fsPath);
export function getAbsoluteGlobPath(path: string | Uri, root: Uri): string {
path = typeof path === "string" ? path : path.fsPath;
const uri = Uri.joinPath(root, path);
return toGlobPath(uri.fsPath);
}

function toGlobPath(path: string) {
Expand All @@ -68,9 +67,7 @@ export class BundleFileSet {

async getRootFile() {
const rootFile = await glob.glob(
toGlobPath(
getAbsolutePath(rootFilePattern, this.workspaceRoot).fsPath
),
getAbsoluteGlobPath(rootFilePattern, this.workspaceRoot),
{nocase: process.platform === "win32"}
);
if (rootFile.length !== 1) {
Expand All @@ -83,11 +80,9 @@ export class BundleFileSet {
root?: Uri
): Promise<{relative: Uri; absolute: Uri}[]> {
const subProjectRoots = await glob.glob(
toGlobPath(
getAbsolutePath(
subProjectFilePattern,
root || this.workspaceRoot
).fsPath
getAbsoluteGlobPath(
subProjectFilePattern,
root || this.workspaceRoot
),
{nocase: process.platform === "win32"}
);
Expand Down Expand Up @@ -167,9 +162,7 @@ export class BundleFileSet {
isRootBundleFile(e: Uri) {
return minimatch(
e.fsPath,
toGlobPath(
getAbsolutePath(rootFilePattern, this.workspaceRoot).fsPath
)
getAbsoluteGlobPath(rootFilePattern, this.workspaceRoot)
);
}

Expand All @@ -178,10 +171,10 @@ export class BundleFileSet {
if (includedFilesGlob === undefined) {
return false;
}
includedFilesGlob = getAbsolutePath(
includedFilesGlob = getAbsoluteGlobPath(
includedFilesGlob,
this.workspaceRoot
).fsPath;
);
return minimatch(e.fsPath, toGlobPath(includedFilesGlob));
}

Expand Down
5 changes: 2 additions & 3 deletions packages/databricks-vscode/src/bundle/BundleWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Disposable, EventEmitter, Uri, workspace} from "vscode";
import {BundleFileSet, getAbsolutePath} from "./BundleFileSet";
import {BundleFileSet, getAbsoluteGlobPath} from "./BundleFileSet";
import {WithMutex} from "../locking";
import path from "path";

Expand All @@ -23,8 +23,7 @@ export class BundleWatcher implements Disposable {
constructor(bundleFileSet: BundleFileSet, workspaceUri: Uri) {
this.bundleFileSet = new WithMutex(bundleFileSet);
const yamlWatcher = workspace.createFileSystemWatcher(
getAbsolutePath(path.join("**", "*.{yaml,yml}"), workspaceUri)
.fsPath
getAbsoluteGlobPath(path.join("**", "*.{yaml,yml}"), workspaceUri)
);

this.disposables.push(
Expand Down
16 changes: 16 additions & 0 deletions packages/databricks-vscode/src/configuration/ConnectionCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {ConnectionManager} from "./ConnectionManager";
import {UrlUtils} from "../utils";
import {WorkspaceFsCommands} from "../workspace-fs";
import {ConfigModel} from "./models/ConfigModel";
import {saveNewProfile} from "./LoginWizard";
import {PersonalAccessTokenAuthProvider} from "./auth/AuthProvider";
import {normalizeHost} from "../utils/urlUtils";

function formatQuickPickClusterSize(sizeInMB: number): string {
if (sizeInMB > 1024) {
Expand Down Expand Up @@ -73,6 +76,19 @@ export class ConnectionCommands implements Disposable {
);
}

// This command is not exposed to users.
// We use it to test new profile flow in e2e tests.
async saveNewProfileCommand(name: string) {
const host = this.connectionManager.workspaceClient?.config.host;
const token = this.connectionManager.workspaceClient?.config.token;
if (!host || !token) {
throw new Error("Must login first");
}
const hostUrl = normalizeHost(host);
const provider = new PersonalAccessTokenAuthProvider(hostUrl, token);
await saveNewProfile(name, provider);
}

/**
* Attach to cluster from settings. If attach fails or no cluster is configured
* then show dialog to select (or create) a cluster. The selected cluster is saved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export async function saveNewProfile(
// Create a backup for .databrickscfg
const backup = path.join(
path.dirname(configFilePath),
`.databrickscfg.${new Date().toISOString()}.bak`
`.databrickscfg.${Date.now()}.bak`
);
await copyFile(configFilePath, backup);
window.showInformationMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,7 @@ export class ProfileAuthProvider extends AuthProvider {
public static getSdkConfig(profile: string): Config {
return new Config({
profile: profile,
configFile:
workspaceConfigs.databrickscfgLocation ??
process.env.DATABRICKS_CONFIG_FILE,
configFile: workspaceConfigs.databrickscfgLocation,
env: {},
});
}
Expand Down
5 changes: 5 additions & 0 deletions packages/databricks-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ export async function activate(
"databricks.connection.detachCluster",
connectionCommands.detachClusterCommand(),
connectionCommands
),
telemetry.registerCommand(
"databricks.connection.saveNewProfile",
connectionCommands.saveNewProfileCommand,
connectionCommands
)
);

Expand Down
141 changes: 141 additions & 0 deletions packages/databricks-vscode/src/test/e2e/auth.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import path from "node:path";
import assert from "node:assert";
import * as fs from "fs/promises";
import {
dismissNotifications,
waitForInput,
getViewSection,
waitForLogin,
} from "./utils.ts";
import {CustomTreeSection} from "wdio-vscode-service";

const BUNDLE = `
bundle:
name: hello_test
targets:
dev_test:
mode: development
default: true
workspace:
host: _HOST_
`;

let projectDir: string;
let bundleConfig: string;
let cfgPath: string;
let cfgContent: Buffer;

describe("Configure Databricks Extension", async function () {
this.timeout(3 * 60 * 1000);

before(async function () {
assert(process.env.WORKSPACE_PATH, "WORKSPACE_PATH doesn't exist");
assert(
process.env.DATABRICKS_CONFIG_FILE,
"DATABRICKS_CONFIG_FILE doesn't exist"
);
cfgPath = process.env.DATABRICKS_CONFIG_FILE;
projectDir = process.env.WORKSPACE_PATH;
bundleConfig = path.join(projectDir, "databricks.yml");
cfgContent = await fs.readFile(cfgPath);
});

after(async function () {
await fs.unlink(bundleConfig);
if (cfgContent) {
await fs.writeFile(cfgPath, cfgContent);
}
});

it("should open VSCode and dismiss notifications", async function () {
const workbench = await browser.getWorkbench();
const title = await workbench.getTitleBar().getTitle();
assert(
title.indexOf("[Extension Development Host]") >= 0,
"Unexpected VSCode title"
);
await dismissNotifications();
});

it("should wait for a welcome screen", async () => {
const section = await getViewSection("CONFIGURATION");
const welcomeButtons = await browser.waitUntil(async () => {
const welcome = await section!.findWelcomeContent();
const buttons = await welcome!.getButtons();
if (buttons?.length >= 2) {
return buttons;
}
});
assert(welcomeButtons, "Welcome buttons don't exist");
const initTitle = await welcomeButtons[0].getTitle();
const quickStartTitle = await welcomeButtons[1].getTitle();
assert(
initTitle.toLowerCase().includes("initialize"),
"'initialize` button doesn't exist"
);
assert(
quickStartTitle.toLowerCase().includes("quickstart"),
"'quickstart' button doesn't exist"
);
});

it("should automatically login after detecting bundle configuration", async () => {
assert(process.env.DATABRICKS_HOST, "DATABRICKS_HOST doesn't exist");
await fs.writeFile(
bundleConfig,
BUNDLE.replace("_HOST_", process.env.DATABRICKS_HOST)
);
await waitForLogin("DEFAULT");
});

it("should create new profile", async () => {
// We create a new profile programmatically to avoid leaking tokens through screenshots or video reporters.
// We still trigger similar code path to the UI flow.
await browser.executeWorkbench(async (vscode) => {
await vscode.commands.executeCommand(
"databricks.connection.saveNewProfile",
"NEW_PROFILE"
);
});
});

it("should change profiles", async () => {
const section = (await getViewSection(
"CONFIGURATION"
)) as CustomTreeSection;
assert(section, "CONFIGURATION section doesn't exist");
const signInButton = await browser.waitUntil(
async () => {
const items = await section.getVisibleItems();
for (const item of items) {
const label = await item.getLabel();
if (label.toLowerCase().includes("auth type")) {
return item.getActionButton("Sign in");
}
}
},
{timeout: 10_000}
);
assert(signInButton, "Sign In button doesn't exist");
(await signInButton.elem).click();

const authMethodInput = await waitForInput();
const newProfilePick =
await authMethodInput.findQuickPick("NEW_PROFILE");
assert(
newProfilePick,
"NEW_PROFILE is absent in the quick pick selection"
);
await newProfilePick.select();
await waitForLogin("NEW_PROFILE");
});

it("should pick up new profile after reloading", async () => {
const workbench = await driver.getWorkbench();
const editorView = workbench.getEditorView();
await editorView.closeAllEditors();
await workbench.executeCommand("Developer: Reload Window");
await waitForLogin("NEW_PROFILE");
});
});
Loading

0 comments on commit efa3c6b

Please sign in to comment.