Merge pull request #2036 from github/shati-elena/create-skeleton
Introduce command for creating skeleton QL packs
This commit is contained in:
@@ -9,6 +9,8 @@ import {
|
||||
showAndLogInformationMessage,
|
||||
isLikelyDatabaseRoot,
|
||||
showAndLogExceptionWithTelemetry,
|
||||
isFolderAlreadyInWorkspace,
|
||||
showBinaryChoiceDialog,
|
||||
} from "./helpers";
|
||||
import { ProgressCallback, withProgress } from "./commandRunner";
|
||||
import {
|
||||
@@ -23,6 +25,7 @@ import { asError, getErrorMessage } from "./pure/helpers-pure";
|
||||
import { QueryRunner } from "./queryRunner";
|
||||
import { pathsEqual } from "./pure/files";
|
||||
import { redactableError } from "./pure/errors";
|
||||
import { isCodespacesTemplate } from "./config";
|
||||
|
||||
/**
|
||||
* databases.ts
|
||||
@@ -621,9 +624,38 @@ export class DatabaseManager extends DisposableObject {
|
||||
await this.addDatabaseItem(progress, token, databaseItem);
|
||||
await this.addDatabaseSourceArchiveFolder(databaseItem);
|
||||
|
||||
if (isCodespacesTemplate()) {
|
||||
await this.createSkeletonPacks(databaseItem);
|
||||
}
|
||||
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
public async createSkeletonPacks(databaseItem: DatabaseItem) {
|
||||
if (databaseItem === undefined) {
|
||||
void this.logger.log(
|
||||
"Could not create QL pack because no database is selected. Please add a database.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (databaseItem.language === "") {
|
||||
void this.logger.log(
|
||||
"Could not create skeleton QL pack because the selected database's language is not set.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const folderName = `codeql-custom-queries-${databaseItem.language}`;
|
||||
if (isFolderAlreadyInWorkspace(folderName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showBinaryChoiceDialog(
|
||||
`We've noticed you don't have a QL pack downloaded to analyze this database. Can we set up a ${databaseItem.language} query pack for you`,
|
||||
);
|
||||
}
|
||||
|
||||
private async reregisterDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
|
||||
@@ -255,6 +255,15 @@ export function getOnDiskWorkspaceFolders() {
|
||||
return diskWorkspaceFolders;
|
||||
}
|
||||
|
||||
/** Check if folder is already present in workspace */
|
||||
export function isFolderAlreadyInWorkspace(folderName: string) {
|
||||
const workspaceFolders = workspace.workspaceFolders || [];
|
||||
|
||||
return !!workspaceFolders.find(
|
||||
(workspaceFolder) => workspaceFolder.name === folderName,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a utility method to invoke a function only if a minimum time interval has elapsed since
|
||||
* the last invocation of that function.
|
||||
|
||||
@@ -108,7 +108,7 @@ describe("config listeners", () => {
|
||||
await wait();
|
||||
const newValue = listener[setting.property as keyof typeof listener];
|
||||
expect(newValue).toEqual(setting.values[1]);
|
||||
expect(onDidChangeConfiguration).toHaveBeenCalledTimes(1);
|
||||
expect(onDidChangeConfiguration).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../../../src/archive-filesystem-provider";
|
||||
import { testDisposeHandler } from "../test-dispose-handler";
|
||||
import { QueryRunner } from "../../../src/queryRunner";
|
||||
import * as helpers from "../../../src/helpers";
|
||||
|
||||
describe("databases", () => {
|
||||
const MOCK_DB_OPTIONS: FullDatabaseOptions = {
|
||||
@@ -34,6 +35,11 @@ describe("databases", () => {
|
||||
let registerSpy: jest.Mock<Promise<void>, []>;
|
||||
let deregisterSpy: jest.Mock<Promise<void>, []>;
|
||||
let resolveDatabaseSpy: jest.Mock<Promise<DbInfo>, []>;
|
||||
let logSpy: jest.Mock<any, []>;
|
||||
|
||||
let showBinaryChoiceDialogSpy: jest.SpiedFunction<
|
||||
typeof helpers.showBinaryChoiceDialog
|
||||
>;
|
||||
|
||||
let dir: tmp.DirResult;
|
||||
|
||||
@@ -44,6 +50,13 @@ describe("databases", () => {
|
||||
registerSpy = jest.fn(() => Promise.resolve(undefined));
|
||||
deregisterSpy = jest.fn(() => Promise.resolve(undefined));
|
||||
resolveDatabaseSpy = jest.fn(() => Promise.resolve({} as DbInfo));
|
||||
logSpy = jest.fn(() => {
|
||||
/* */
|
||||
});
|
||||
|
||||
showBinaryChoiceDialogSpy = jest
|
||||
.spyOn(helpers, "showBinaryChoiceDialog")
|
||||
.mockResolvedValue(true);
|
||||
|
||||
databaseManager = new DatabaseManager(
|
||||
{
|
||||
@@ -66,9 +79,7 @@ describe("databases", () => {
|
||||
resolveDatabase: resolveDatabaseSpy,
|
||||
} as unknown as CodeQLCliServer,
|
||||
{
|
||||
log: () => {
|
||||
/**/
|
||||
},
|
||||
log: logSpy,
|
||||
} as unknown as Logger,
|
||||
);
|
||||
|
||||
@@ -122,29 +133,31 @@ describe("databases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should rename a db item and emit an event", async () => {
|
||||
const mockDbItem = createMockDB();
|
||||
const onDidChangeDatabaseItem = jest.fn();
|
||||
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
|
||||
await (databaseManager as any).addDatabaseItem(
|
||||
{} as ProgressCallback,
|
||||
{} as CancellationToken,
|
||||
mockDbItem,
|
||||
);
|
||||
describe("renameDatabaseItem", () => {
|
||||
it("should rename a db item and emit an event", async () => {
|
||||
const mockDbItem = createMockDB();
|
||||
const onDidChangeDatabaseItem = jest.fn();
|
||||
databaseManager.onDidChangeDatabaseItem(onDidChangeDatabaseItem);
|
||||
await (databaseManager as any).addDatabaseItem(
|
||||
{} as ProgressCallback,
|
||||
{} as CancellationToken,
|
||||
mockDbItem,
|
||||
);
|
||||
|
||||
await databaseManager.renameDatabaseItem(mockDbItem, "new name");
|
||||
await databaseManager.renameDatabaseItem(mockDbItem, "new name");
|
||||
|
||||
expect(mockDbItem.name).toBe("new name");
|
||||
expect(updateSpy).toBeCalledWith("databaseList", [
|
||||
{
|
||||
options: { ...MOCK_DB_OPTIONS, displayName: "new name" },
|
||||
uri: dbLocationUri().toString(true),
|
||||
},
|
||||
]);
|
||||
expect(mockDbItem.name).toBe("new name");
|
||||
expect(updateSpy).toBeCalledWith("databaseList", [
|
||||
{
|
||||
options: { ...MOCK_DB_OPTIONS, displayName: "new name" },
|
||||
uri: dbLocationUri().toString(true),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(onDidChangeDatabaseItem).toBeCalledWith({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Rename,
|
||||
expect(onDidChangeDatabaseItem).toBeCalledWith({
|
||||
item: undefined,
|
||||
kind: DatabaseEventKind.Rename,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -287,7 +300,10 @@ describe("databases", () => {
|
||||
|
||||
describe("resolveSourceFile", () => {
|
||||
it("should fail to resolve when not a uri", () => {
|
||||
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
Uri.parse("file:/sourceArchive-uri/"),
|
||||
);
|
||||
(db as any)._contents.sourceArchiveUri = undefined;
|
||||
expect(() => db.resolveSourceFile("abc")).toThrowError(
|
||||
"Scheme is missing",
|
||||
@@ -295,7 +311,10 @@ describe("databases", () => {
|
||||
});
|
||||
|
||||
it("should fail to resolve when not a file uri", () => {
|
||||
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
Uri.parse("file:/sourceArchive-uri/"),
|
||||
);
|
||||
(db as any)._contents.sourceArchiveUri = undefined;
|
||||
expect(() => db.resolveSourceFile("http://abc")).toThrowError(
|
||||
"Invalid uri scheme",
|
||||
@@ -304,14 +323,20 @@ describe("databases", () => {
|
||||
|
||||
describe("no source archive", () => {
|
||||
it("should resolve undefined", () => {
|
||||
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
Uri.parse("file:/sourceArchive-uri/"),
|
||||
);
|
||||
(db as any)._contents.sourceArchiveUri = undefined;
|
||||
const resolved = db.resolveSourceFile(undefined);
|
||||
expect(resolved.toString(true)).toBe(dbLocationUri().toString(true));
|
||||
});
|
||||
|
||||
it("should resolve an empty file", () => {
|
||||
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
Uri.parse("file:/sourceArchive-uri/"),
|
||||
);
|
||||
(db as any)._contents.sourceArchiveUri = undefined;
|
||||
const resolved = db.resolveSourceFile("file:");
|
||||
expect(resolved.toString()).toBe("file:///");
|
||||
@@ -321,6 +346,7 @@ describe("databases", () => {
|
||||
describe("zipped source archive", () => {
|
||||
it("should encode a source archive url", () => {
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: "sourceArchive-uri",
|
||||
pathWithinSourceArchive: "def",
|
||||
@@ -340,6 +366,7 @@ describe("databases", () => {
|
||||
|
||||
it("should encode a source archive url with trailing slash", () => {
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: "sourceArchive-uri",
|
||||
pathWithinSourceArchive: "def/",
|
||||
@@ -359,6 +386,7 @@ describe("databases", () => {
|
||||
|
||||
it("should encode an empty source archive url", () => {
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
encodeSourceArchiveUri({
|
||||
sourceArchiveZipPath: "sourceArchive-uri",
|
||||
pathWithinSourceArchive: "def",
|
||||
@@ -372,26 +400,35 @@ describe("databases", () => {
|
||||
});
|
||||
|
||||
it("should handle an empty file", () => {
|
||||
const db = createMockDB(Uri.parse("file:/sourceArchive-uri/"));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
Uri.parse("file:/sourceArchive-uri/"),
|
||||
);
|
||||
const resolved = db.resolveSourceFile("");
|
||||
expect(resolved.toString()).toBe("file:///sourceArchive-uri/");
|
||||
});
|
||||
});
|
||||
|
||||
it("should get the primary language", async () => {
|
||||
resolveDatabaseSpy.mockResolvedValue({
|
||||
languages: ["python"],
|
||||
} as unknown as DbInfo);
|
||||
const result = await (databaseManager as any).getPrimaryLanguage("hucairz");
|
||||
expect(result).toBe("python");
|
||||
});
|
||||
describe("getPrimaryLanguage", () => {
|
||||
it("should get the primary language", async () => {
|
||||
resolveDatabaseSpy.mockResolvedValue({
|
||||
languages: ["python"],
|
||||
} as unknown as DbInfo);
|
||||
const result = await (databaseManager as any).getPrimaryLanguage(
|
||||
"hucairz",
|
||||
);
|
||||
expect(result).toBe("python");
|
||||
});
|
||||
|
||||
it("should handle missing the primary language", async () => {
|
||||
resolveDatabaseSpy.mockResolvedValue({
|
||||
languages: [],
|
||||
} as unknown as DbInfo);
|
||||
const result = await (databaseManager as any).getPrimaryLanguage("hucairz");
|
||||
expect(result).toBe("");
|
||||
it("should handle missing the primary language", async () => {
|
||||
resolveDatabaseSpy.mockResolvedValue({
|
||||
languages: [],
|
||||
} as unknown as DbInfo);
|
||||
const result = await (databaseManager as any).getPrimaryLanguage(
|
||||
"hucairz",
|
||||
);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAffectedByTest", () => {
|
||||
@@ -409,12 +446,17 @@ describe("databases", () => {
|
||||
});
|
||||
|
||||
it("should return true for testproj database in test directory", async () => {
|
||||
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(projectPath),
|
||||
);
|
||||
expect(await db.isAffectedByTest(directoryPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-existent test directory", async () => {
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(join(dir.name, "non-existent/non-existent.testproj")),
|
||||
);
|
||||
@@ -428,6 +470,7 @@ describe("databases", () => {
|
||||
await fs.writeFile(anotherProjectPath, "");
|
||||
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(anotherProjectPath),
|
||||
);
|
||||
@@ -441,6 +484,7 @@ describe("databases", () => {
|
||||
await fs.writeFile(anotherProjectPath, "");
|
||||
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(anotherProjectPath),
|
||||
);
|
||||
@@ -448,20 +492,32 @@ describe("databases", () => {
|
||||
});
|
||||
|
||||
it("should return false for testproj database for prefix directory", async () => {
|
||||
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(projectPath),
|
||||
);
|
||||
// /d is a prefix of /dir/dir.testproj, but
|
||||
// /dir/dir.testproj is not under /d
|
||||
expect(await db.isAffectedByTest(join(directoryPath, "d"))).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for testproj database for test file", async () => {
|
||||
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(projectPath),
|
||||
);
|
||||
expect(await db.isAffectedByTest(qlFilePath)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-existent test file", async () => {
|
||||
const otherTestFile = join(directoryPath, "other-test.ql");
|
||||
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(projectPath),
|
||||
);
|
||||
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -470,6 +526,7 @@ describe("databases", () => {
|
||||
await fs.writeFile(anotherProjectPath, "");
|
||||
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(anotherProjectPath),
|
||||
);
|
||||
@@ -480,7 +537,11 @@ describe("databases", () => {
|
||||
const otherTestFile = join(dir.name, "test.ql");
|
||||
await fs.writeFile(otherTestFile, "");
|
||||
|
||||
const db = createMockDB(sourceLocationUri(), Uri.file(projectPath));
|
||||
const db = createMockDB(
|
||||
MOCK_DB_OPTIONS,
|
||||
sourceLocationUri(),
|
||||
Uri.file(projectPath),
|
||||
);
|
||||
expect(await db.isAffectedByTest(otherTestFile)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -524,7 +585,46 @@ describe("databases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSkeletonPacks", () => {
|
||||
let mockDbItem: DatabaseItemImpl;
|
||||
|
||||
describe("when the language is set", () => {
|
||||
it("should offer the user to set up a skeleton QL pack", async () => {
|
||||
const options: FullDatabaseOptions = {
|
||||
dateAdded: 123,
|
||||
ignoreSourceArchive: false,
|
||||
language: "ruby",
|
||||
};
|
||||
mockDbItem = createMockDB(options);
|
||||
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
|
||||
expect(showBinaryChoiceDialogSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the language is not set", () => {
|
||||
it("should fail gracefully", async () => {
|
||||
mockDbItem = createMockDB();
|
||||
await (databaseManager as any).createSkeletonPacks(mockDbItem);
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
"Could not create skeleton QL pack because the selected database's language is not set.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the databaseItem is not set", () => {
|
||||
it("should fail gracefully", async () => {
|
||||
await (databaseManager as any).createSkeletonPacks(undefined);
|
||||
expect(logSpy).toHaveBeenCalledWith(
|
||||
"Could not create QL pack because no database is selected. Please add a database.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockDB(
|
||||
mockDbOptions = MOCK_DB_OPTIONS,
|
||||
// source archive location must be a real(-ish) location since
|
||||
// tests will add this to the workspace location
|
||||
sourceArchiveUri = sourceLocationUri(),
|
||||
@@ -536,7 +636,7 @@ describe("databases", () => {
|
||||
sourceArchiveUri,
|
||||
datasetUri: databaseUri,
|
||||
} as DatabaseContents,
|
||||
MOCK_DB_OPTIONS,
|
||||
mockDbOptions,
|
||||
() => void 0,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
SecretStorageChangeEvent,
|
||||
Uri,
|
||||
window,
|
||||
workspace,
|
||||
WorkspaceFolder,
|
||||
} from "vscode";
|
||||
import { dump } from "js-yaml";
|
||||
import * as tmp from "tmp";
|
||||
@@ -19,6 +21,7 @@ import { DirResult } from "tmp";
|
||||
import {
|
||||
getInitialQueryContents,
|
||||
InvocationRateLimiter,
|
||||
isFolderAlreadyInWorkspace,
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder,
|
||||
showBinaryChoiceDialog,
|
||||
@@ -533,3 +536,21 @@ describe("walkDirectory", () => {
|
||||
expect(files.sort()).toEqual([file1, file2, file3, file4, file5, file6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFolderAlreadyInWorkspace", () => {
|
||||
beforeEach(() => {
|
||||
const folders = [
|
||||
{ name: "/first/path" },
|
||||
{ name: "/second/path" },
|
||||
] as WorkspaceFolder[];
|
||||
|
||||
jest.spyOn(workspace, "workspaceFolders", "get").mockReturnValue(folders);
|
||||
});
|
||||
it("should return true if the folder is already in the workspace", () => {
|
||||
expect(isFolderAlreadyInWorkspace("/first/path")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the folder is not in the workspace", () => {
|
||||
expect(isFolderAlreadyInWorkspace("/third/path")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user