Merge remote-tracking branch 'origin/main' into koesie10/filter-vscode-output
This commit is contained in:
17
.github/workflows/release.yml
vendored
17
.github/workflows/release.yml
vendored
@@ -94,6 +94,23 @@ jobs:
|
||||
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Create sourcemap ZIP file
|
||||
run: |
|
||||
cd dist/vscode-codeql/out
|
||||
zip -r ../../vscode-codeql-sourcemaps.zip *.map
|
||||
|
||||
- name: Upload sourcemap ZIP file
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
if: success()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# Get the `upload_url` from the `create-release` step above.
|
||||
upload_url: ${{ steps.create-release.outputs.upload_url }}
|
||||
asset_path: dist/vscode-codeql-sourcemaps.zip
|
||||
asset_name: ${{ format('vscode-codeql-sourcemaps-{0}.zip', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
###
|
||||
# Do Post release work: version bump and changelog PR
|
||||
# Only do this if we are running from a PR (ie- this is part of the release process)
|
||||
|
||||
@@ -228,6 +228,13 @@ Pre-recorded scenarios are stored in `./src/mocks/scenarios`. However, it's poss
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
|
||||
* Go through all recent PRs and make sure they are properly accounted for.
|
||||
* Make sure all changelog entries have links back to their PR(s) if appropriate.
|
||||
* For picking the new version number, we default to increasing the patch version number, but make our own judgement about whether a change is big enough to warrant a minor version bump. Common reasons for a minor bump could include:
|
||||
* Making substantial new features available to all users. This can include lifting a feature flag.
|
||||
* Breakage in compatibility with recent versions of the CLI.
|
||||
* Minimum required version of VS Code is increased.
|
||||
* New telemetry events are added.
|
||||
* Deprecation or removal of commands.
|
||||
* Accumulation of many changes, none of which are individually big enough to warrant a minor bump, but which together are. This does not include changes which are purely internal to the extension, such as refactoring, or which are only available behind a feature flag.
|
||||
1. Double-check that the node version we're using matches the one used for VS Code. If it doesn't, you will then need to update the node version in the following files:
|
||||
* `.nvmrc` - this will enable `nvm` to automatically switch to the correct node version when you're in the project folder
|
||||
* `.github/workflows/main.yml` - all the "node-version: <version>" settings
|
||||
|
||||
241
extensions/ql-vscode/scripts/source-map.ts
Normal file
241
extensions/ql-vscode/scripts/source-map.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* This scripts helps finding the original source file and line number for a
|
||||
* given file and line number in the compiled extension. It currently only
|
||||
* works with released extensions.
|
||||
*
|
||||
* Usage: npx ts-node scripts/source-map.ts <version-number> <filename>:<line>:<column>
|
||||
* For example: npx ts-node scripts/source-map.ts v1.7.8 "/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131164:13"
|
||||
*
|
||||
* Alternative usage: npx ts-node scripts/source-map.ts <version-number> <multi-line-stacktrace>
|
||||
* For example: npx ts-node scripts/source-map.ts v1.7.8 'Error: Failed to find CodeQL distribution.
|
||||
* at CodeQLCliServer.getCodeQlPath (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131164:13)
|
||||
* at CodeQLCliServer.launchProcess (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131169:24)
|
||||
* at CodeQLCliServer.runCodeQlCliInternal (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131194:24)
|
||||
* at CodeQLCliServer.runJsonCodeQlCliCommand (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131330:20)
|
||||
* at CodeQLCliServer.resolveRam (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:131455:12)
|
||||
* at QueryServerClient2.startQueryServerImpl (/Users/user/.vscode/extensions/github.vscode-codeql-1.7.8/out/extension.js:138618:21)'
|
||||
*/
|
||||
|
||||
import { spawnSync } from "child_process";
|
||||
import { basename, resolve } from "path";
|
||||
import { pathExists, readJSON } from "fs-extra";
|
||||
import { RawSourceMap, SourceMapConsumer } from "source-map";
|
||||
import { Open } from "unzipper";
|
||||
|
||||
if (process.argv.length !== 4) {
|
||||
console.error(
|
||||
"Expected 2 arguments - the version number and the filename:line number",
|
||||
);
|
||||
}
|
||||
|
||||
const stackLineRegex =
|
||||
/at (?<name>.*)? \((?<file>.*):(?<line>\d+):(?<column>\d+)\)/gm;
|
||||
|
||||
const versionNumber = process.argv[2].startsWith("v")
|
||||
? process.argv[2]
|
||||
: `v${process.argv[2]}`;
|
||||
const stacktrace = process.argv[3];
|
||||
|
||||
async function extractSourceMap() {
|
||||
const releaseAssetsDirectory = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"release-assets",
|
||||
versionNumber,
|
||||
);
|
||||
const sourceMapsDirectory = resolve(
|
||||
__dirname,
|
||||
"..",
|
||||
"artifacts",
|
||||
"source-maps",
|
||||
versionNumber,
|
||||
);
|
||||
|
||||
if (!(await pathExists(sourceMapsDirectory))) {
|
||||
console.log("Downloading source maps...");
|
||||
|
||||
const release = runGhJSON<Release>([
|
||||
"release",
|
||||
"view",
|
||||
versionNumber,
|
||||
"--json",
|
||||
"id,name,assets",
|
||||
]);
|
||||
|
||||
const sourcemapAsset = release.assets.find(
|
||||
(asset) => asset.name === `vscode-codeql-sourcemaps-${versionNumber}.zip`,
|
||||
);
|
||||
|
||||
if (sourcemapAsset) {
|
||||
// This downloads a ZIP file of the source maps
|
||||
runGh([
|
||||
"release",
|
||||
"download",
|
||||
versionNumber,
|
||||
"--pattern",
|
||||
sourcemapAsset.name,
|
||||
"--dir",
|
||||
releaseAssetsDirectory,
|
||||
]);
|
||||
|
||||
const file = await Open.file(
|
||||
resolve(releaseAssetsDirectory, sourcemapAsset.name),
|
||||
);
|
||||
await file.extract({ path: sourceMapsDirectory });
|
||||
} else {
|
||||
const workflowRuns = runGhJSON<WorkflowRunListItem[]>([
|
||||
"run",
|
||||
"list",
|
||||
"--workflow",
|
||||
"release.yml",
|
||||
"--branch",
|
||||
versionNumber,
|
||||
"--json",
|
||||
"databaseId,number",
|
||||
]);
|
||||
|
||||
if (workflowRuns.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected exactly one workflow run for ${versionNumber}, got ${workflowRuns.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowRun = workflowRuns[0];
|
||||
|
||||
runGh([
|
||||
"run",
|
||||
"download",
|
||||
workflowRun.databaseId.toString(),
|
||||
"--name",
|
||||
"vscode-codeql-sourcemaps",
|
||||
"--dir",
|
||||
sourceMapsDirectory,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (stacktrace.includes("at")) {
|
||||
const rawSourceMaps = new Map<string, RawSourceMap>();
|
||||
|
||||
const mappedStacktrace = await replaceAsync(
|
||||
stacktrace,
|
||||
stackLineRegex,
|
||||
async (match, name, file, line, column) => {
|
||||
if (!rawSourceMaps.has(file)) {
|
||||
const rawSourceMap: RawSourceMap = await readJSON(
|
||||
resolve(sourceMapsDirectory, `${basename(file)}.map`),
|
||||
);
|
||||
rawSourceMaps.set(file, rawSourceMap);
|
||||
}
|
||||
|
||||
const originalPosition = await SourceMapConsumer.with(
|
||||
rawSourceMaps.get(file) as RawSourceMap,
|
||||
null,
|
||||
async function (consumer) {
|
||||
return consumer.originalPositionFor({
|
||||
line: parseInt(line, 10),
|
||||
column: parseInt(column, 10),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (!originalPosition.source) {
|
||||
return match;
|
||||
}
|
||||
|
||||
const originalFilename = resolve(file, "..", originalPosition.source);
|
||||
|
||||
return `at ${originalPosition.name ?? name} (${originalFilename}:${
|
||||
originalPosition.line
|
||||
}:${originalPosition.column})`;
|
||||
},
|
||||
);
|
||||
|
||||
console.log(mappedStacktrace);
|
||||
} else {
|
||||
// This means it's just a filename:line:column
|
||||
const [filename, line, column] = stacktrace.split(":", 3);
|
||||
|
||||
const fileBasename = basename(filename);
|
||||
|
||||
const sourcemapName = `${fileBasename}.map`;
|
||||
const sourcemapPath = resolve(sourceMapsDirectory, sourcemapName);
|
||||
|
||||
if (!(await pathExists(sourcemapPath))) {
|
||||
throw new Error(`No source map found for ${fileBasename}`);
|
||||
}
|
||||
|
||||
const rawSourceMap: RawSourceMap = await readJSON(sourcemapPath);
|
||||
|
||||
const originalPosition = await SourceMapConsumer.with(
|
||||
rawSourceMap,
|
||||
null,
|
||||
async function (consumer) {
|
||||
return consumer.originalPositionFor({
|
||||
line: parseInt(line, 10),
|
||||
column: parseInt(column, 10),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (!originalPosition.source) {
|
||||
throw new Error(`No source found for ${stacktrace}`);
|
||||
}
|
||||
|
||||
const originalFilename = resolve(filename, "..", originalPosition.source);
|
||||
|
||||
console.log(
|
||||
`${originalFilename}:${originalPosition.line}:${originalPosition.column}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extractSourceMap().catch((e: unknown) => {
|
||||
console.error(e);
|
||||
process.exit(2);
|
||||
});
|
||||
|
||||
function runGh(args: readonly string[]): string {
|
||||
const gh = spawnSync("gh", args);
|
||||
if (gh.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to get the source map for ${versionNumber}: ${gh.stderr}`,
|
||||
);
|
||||
}
|
||||
return gh.stdout.toString("utf-8");
|
||||
}
|
||||
|
||||
function runGhJSON<T>(args: readonly string[]): T {
|
||||
return JSON.parse(runGh(args));
|
||||
}
|
||||
|
||||
type ReleaseAsset = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Release = {
|
||||
id: string;
|
||||
name: string;
|
||||
assets: ReleaseAsset[];
|
||||
};
|
||||
|
||||
type WorkflowRunListItem = {
|
||||
databaseId: number;
|
||||
number: number;
|
||||
};
|
||||
|
||||
async function replaceAsync(
|
||||
str: string,
|
||||
regex: RegExp,
|
||||
replacer: (substring: string, ...args: any[]) => Promise<string>,
|
||||
) {
|
||||
const promises: Array<Promise<string>> = [];
|
||||
str.replace(regex, (match, ...args) => {
|
||||
const promise = replacer(match, ...args);
|
||||
promises.push(promise);
|
||||
return match;
|
||||
});
|
||||
const data = await Promise.all(promises);
|
||||
return str.replace(regex, () => data.shift() as string);
|
||||
}
|
||||
@@ -56,15 +56,6 @@ export class Setting {
|
||||
.getConfiguration(this.parent.qualifiedName)
|
||||
.update(this.name, value, target);
|
||||
}
|
||||
|
||||
inspect<T>(): InspectionResult<T> | undefined {
|
||||
if (this.parent === undefined) {
|
||||
throw new Error("Cannot update the value of a root setting.");
|
||||
}
|
||||
return workspace
|
||||
.getConfiguration(this.parent.qualifiedName)
|
||||
.inspect(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
export interface InspectionResult<T> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[
|
||||
"v2.12.1",
|
||||
"v2.12.2",
|
||||
"v2.11.6",
|
||||
"v2.7.6",
|
||||
"v2.8.5",
|
||||
|
||||
@@ -8,23 +8,20 @@ import { dirname } from "path";
|
||||
import fetch from "node-fetch";
|
||||
import { DB_URL, dbLoc, setStoragePath, storagePath } from "./global.helper";
|
||||
import * as tmp from "tmp";
|
||||
import { getTestSetting } from "../test-config";
|
||||
import { CUSTOM_CODEQL_PATH_SETTING } from "../../../src/config";
|
||||
import { extensions, workspace } from "vscode";
|
||||
|
||||
import baseJestSetup from "../jest.setup";
|
||||
|
||||
export default baseJestSetup;
|
||||
import { ConfigurationTarget, env, extensions, workspace } from "vscode";
|
||||
import { beforeEachAction } from "../test-config";
|
||||
|
||||
// create an extension storage location
|
||||
let removeStorage: tmp.DirResult["removeCallback"] | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set the CLI version here before activation to ensure we don't accidentally try to download a cli
|
||||
await getTestSetting(CUSTOM_CODEQL_PATH_SETTING)?.setInitialTestValue(
|
||||
await beforeEachAction();
|
||||
await CUSTOM_CODEQL_PATH_SETTING.updateValue(
|
||||
process.env.CLI_PATH,
|
||||
ConfigurationTarget.Workspace,
|
||||
);
|
||||
await getTestSetting(CUSTOM_CODEQL_PATH_SETTING)?.setup();
|
||||
|
||||
// ensure the test database is downloaded
|
||||
mkdirpSync(dirname(dbLoc));
|
||||
@@ -78,6 +75,17 @@ beforeAll(async () => {
|
||||
await extensions.getExtension("GitHub.vscode-codeql")?.activate();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(env, "openExternal").mockResolvedValue(false);
|
||||
|
||||
await beforeEachAction();
|
||||
|
||||
await CUSTOM_CODEQL_PATH_SETTING.updateValue(
|
||||
process.env.CLI_PATH,
|
||||
ConfigurationTarget.Workspace,
|
||||
);
|
||||
});
|
||||
|
||||
// ensure extension is cleaned up.
|
||||
afterAll(async () => {
|
||||
// ensure temp directory is cleaned up.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { resolve } from "path";
|
||||
import {
|
||||
authentication,
|
||||
commands,
|
||||
ConfigurationTarget,
|
||||
extensions,
|
||||
QuickPickItem,
|
||||
TextDocument,
|
||||
@@ -12,7 +13,10 @@ import {
|
||||
|
||||
import { CodeQLExtensionInterface } from "../../../../src/extension";
|
||||
import { MockGitHubApiServer } from "../../../../src/mocks/mock-gh-api-server";
|
||||
import { mockConfiguration } from "../../utils/configuration-helpers";
|
||||
import {
|
||||
CANARY_FEATURES,
|
||||
setRemoteControllerRepo,
|
||||
} from "../../../../src/config";
|
||||
|
||||
jest.setTimeout(30_000);
|
||||
|
||||
@@ -36,17 +40,8 @@ describe("Variant Analysis Submission Integration", () => {
|
||||
let showErrorMessageSpy: jest.SpiedFunction<typeof window.showErrorMessage>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockConfiguration({
|
||||
values: {
|
||||
codeQL: {
|
||||
canary: true,
|
||||
},
|
||||
"codeQL.variantAnalysis": {
|
||||
liveResults: true,
|
||||
controllerRepo: "github/vscode-codeql",
|
||||
},
|
||||
},
|
||||
});
|
||||
await CANARY_FEATURES.updateValue(true, ConfigurationTarget.Global);
|
||||
await setRemoteControllerRepo("github/vscode-codeql");
|
||||
|
||||
jest.spyOn(authentication, "getSession").mockResolvedValue({
|
||||
id: "test",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { env } from "vscode";
|
||||
import { jestTestConfigHelper } from "./test-config";
|
||||
import { beforeEachAction } from "./test-config";
|
||||
|
||||
(env as any).openExternal = () => {
|
||||
/**/
|
||||
};
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(env, "openExternal").mockResolvedValue(false);
|
||||
|
||||
export default async function setupEnv() {
|
||||
await jestTestConfigHelper();
|
||||
}
|
||||
await beforeEachAction();
|
||||
});
|
||||
|
||||
@@ -6,8 +6,13 @@ import {
|
||||
QueryHistoryConfigListener,
|
||||
QueryServerConfigListener,
|
||||
} from "../../../src/config";
|
||||
import { vscodeGetConfigurationMock } from "../test-config";
|
||||
|
||||
describe("config listeners", () => {
|
||||
beforeEach(() => {
|
||||
vscodeGetConfigurationMock.mockRestore();
|
||||
});
|
||||
|
||||
interface TestConfig<T> {
|
||||
clazz: new () => ConfigListener;
|
||||
settings: Array<{
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
|
||||
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
|
||||
import { createMockExtensionContext } from "../../../factories/extension-context";
|
||||
import { createDbConfig } from "../../../factories/db-config-factories";
|
||||
import { mockConfiguration } from "../../utils/configuration-helpers";
|
||||
import { setRemoteControllerRepo } from "../../../../src/config";
|
||||
|
||||
describe("db panel rendering nodes", () => {
|
||||
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
|
||||
@@ -50,12 +50,8 @@ describe("db panel rendering nodes", () => {
|
||||
});
|
||||
|
||||
describe("when controller repo is not set", () => {
|
||||
mockConfiguration({
|
||||
values: {
|
||||
"codeQL.variantAnalysis": {
|
||||
controllerRepo: undefined,
|
||||
},
|
||||
},
|
||||
beforeEach(async () => {
|
||||
await setRemoteControllerRepo(undefined);
|
||||
});
|
||||
|
||||
it("should not have any items", async () => {
|
||||
@@ -81,14 +77,8 @@ describe("db panel rendering nodes", () => {
|
||||
});
|
||||
|
||||
describe("when controller repo is set", () => {
|
||||
beforeEach(() => {
|
||||
mockConfiguration({
|
||||
values: {
|
||||
"codeQL.variantAnalysis": {
|
||||
controllerRepo: "github/codeql",
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await setRemoteControllerRepo("github/codeql");
|
||||
});
|
||||
|
||||
it("should render default remote nodes when the config is empty", async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DbTreeViewItem } from "../../../../src/databases/ui/db-tree-view-item";
|
||||
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
|
||||
import { createMockExtensionContext } from "../../../factories/extension-context";
|
||||
import { createDbConfig } from "../../../factories/db-config-factories";
|
||||
import { mockConfiguration } from "../../utils/configuration-helpers";
|
||||
import { setRemoteControllerRepo } from "../../../../src/config";
|
||||
|
||||
describe("db panel", () => {
|
||||
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
|
||||
@@ -40,13 +40,7 @@ describe("db panel", () => {
|
||||
beforeEach(async () => {
|
||||
await ensureDir(workspaceStoragePath);
|
||||
|
||||
mockConfiguration({
|
||||
values: {
|
||||
"codeQL.variantAnalysis": {
|
||||
controllerRepo: "github/codeql",
|
||||
},
|
||||
},
|
||||
});
|
||||
await setRemoteControllerRepo("github/codeql");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { ExtensionApp } from "../../../../src/common/vscode/vscode-app";
|
||||
import { createMockExtensionContext } from "../../../factories/extension-context";
|
||||
import { createDbConfig } from "../../../factories/db-config-factories";
|
||||
import { mockConfiguration } from "../../utils/configuration-helpers";
|
||||
import { setRemoteControllerRepo } from "../../../../src/config";
|
||||
|
||||
describe("db panel selection", () => {
|
||||
const workspaceStoragePath = join(__dirname, "test-workspace-storage");
|
||||
@@ -46,13 +46,7 @@ describe("db panel selection", () => {
|
||||
beforeEach(async () => {
|
||||
await ensureDir(workspaceStoragePath);
|
||||
|
||||
mockConfiguration({
|
||||
values: {
|
||||
"codeQL.variantAnalysis": {
|
||||
controllerRepo: "github/codeql",
|
||||
},
|
||||
},
|
||||
});
|
||||
await setRemoteControllerRepo("github/codeql");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UserCancellationException } from "../../../src/commandRunner";
|
||||
import { ENABLE_TELEMETRY } from "../../../src/config";
|
||||
import * as Config from "../../../src/config";
|
||||
import { createMockExtensionContext } from "./index";
|
||||
import { vscodeGetConfigurationMock } from "../test-config";
|
||||
import { redactableError } from "../../../src/pure/errors";
|
||||
|
||||
// setting preferences can trigger lots of background activity
|
||||
@@ -41,6 +42,8 @@ describe("telemetry reporting", () => {
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
vscodeGetConfigurationMock.mockRestore();
|
||||
|
||||
try {
|
||||
// in case a previous test has accidentally activated this extension,
|
||||
// need to disable it first.
|
||||
|
||||
@@ -1,126 +1,225 @@
|
||||
import {
|
||||
ConfigurationScope,
|
||||
ConfigurationTarget,
|
||||
workspace,
|
||||
WorkspaceConfiguration as VSCodeWorkspaceConfiguration,
|
||||
} from "vscode";
|
||||
import { readFileSync } from "fs-extra";
|
||||
import { join } from "path";
|
||||
import { ConfigurationTarget } from "vscode";
|
||||
import { ALL_SETTINGS, InspectionResult, Setting } from "../../src/config";
|
||||
|
||||
class TestSetting<T> {
|
||||
private initialSettingState: InspectionResult<T> | undefined;
|
||||
function getIn(object: any, path: string): any {
|
||||
const parts = path.split(".");
|
||||
let current = object;
|
||||
for (const part of parts) {
|
||||
current = current[part];
|
||||
if (current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly setting: Setting,
|
||||
private initialTestValue: T | undefined = undefined,
|
||||
) {}
|
||||
function setIn(object: any, path: string, value: any): void {
|
||||
const parts = path.split(".");
|
||||
let current = object;
|
||||
for (const part of parts.slice(0, -1)) {
|
||||
if (current[part] === undefined) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
current[parts[parts.length - 1]] = value;
|
||||
}
|
||||
|
||||
public async get(): Promise<T | undefined> {
|
||||
return this.setting.getValue();
|
||||
interface WorkspaceConfiguration {
|
||||
scope?: ConfigurationTarget;
|
||||
|
||||
get<T>(section: string | undefined, key: string): T | undefined;
|
||||
has(section: string | undefined, key: string): boolean;
|
||||
update(section: string | undefined, key: string, value: unknown): void;
|
||||
}
|
||||
|
||||
class InMemoryConfiguration implements WorkspaceConfiguration {
|
||||
private readonly values: Record<string, unknown> = {};
|
||||
|
||||
public constructor(public readonly scope: ConfigurationTarget) {}
|
||||
|
||||
public get<T>(section: string | undefined, key: string): T | undefined {
|
||||
return getIn(this.values, this.getKey(section, key)) as T | undefined;
|
||||
}
|
||||
|
||||
public async set(
|
||||
value: T | undefined,
|
||||
target: ConfigurationTarget = ConfigurationTarget.Global,
|
||||
): Promise<void> {
|
||||
await this.setting.updateValue(value, target);
|
||||
public has(section: string | undefined, key: string): boolean {
|
||||
return getIn(this.values, this.getKey(section, key)) !== undefined;
|
||||
}
|
||||
|
||||
public async setInitialTestValue(value: T | undefined) {
|
||||
this.initialTestValue = value;
|
||||
public update(
|
||||
section: string | undefined,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void {
|
||||
setIn(this.values, this.getKey(section, key), value);
|
||||
}
|
||||
|
||||
public async initialSetup() {
|
||||
this.initialSettingState = this.setting.inspect();
|
||||
private getKey(section: string | undefined, key: string): string {
|
||||
return section ? `${section}.${key}` : key;
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately it's not well-documented how to check whether we can write to a workspace
|
||||
// configuration. This is the best I could come up with. It only fails for initial test values
|
||||
// which are not undefined.
|
||||
if (this.initialSettingState?.workspaceValue !== undefined) {
|
||||
await this.set(this.initialTestValue, ConfigurationTarget.Workspace);
|
||||
}
|
||||
if (this.initialSettingState?.workspaceFolderValue !== undefined) {
|
||||
await this.set(
|
||||
this.initialTestValue,
|
||||
ConfigurationTarget.WorkspaceFolder,
|
||||
);
|
||||
}
|
||||
class DefaultConfiguration implements WorkspaceConfiguration {
|
||||
private readonly values: Record<string, unknown> = {};
|
||||
|
||||
await this.setup();
|
||||
public constructor(configurations: Record<string, { default: unknown }>) {
|
||||
for (const [section, config] of Object.entries(configurations)) {
|
||||
setIn(this.values, section, config.default);
|
||||
}
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
await this.set(this.initialTestValue, ConfigurationTarget.Global);
|
||||
public get<T>(section: string | undefined, key: string): T | undefined {
|
||||
return getIn(this.values, this.getKey(section, key)) as T | undefined;
|
||||
}
|
||||
|
||||
public async restoreToInitialValues() {
|
||||
const state = this.setting.inspect();
|
||||
public has(section: string | undefined, key: string): boolean {
|
||||
return getIn(this.values, this.getKey(section, key)) !== undefined;
|
||||
}
|
||||
|
||||
// We need to check the state of the setting before we restore it. This is less important for the global
|
||||
// configuration target, but the workspace/workspace folder configuration might not even exist. If they
|
||||
// don't exist, VSCode will error when trying to write the new value (even if that value is undefined).
|
||||
if (state?.globalValue !== this.initialSettingState?.globalValue) {
|
||||
await this.set(
|
||||
this.initialSettingState?.globalValue,
|
||||
ConfigurationTarget.Global,
|
||||
);
|
||||
public update(
|
||||
_section: string | undefined,
|
||||
_key: string,
|
||||
_value: unknown,
|
||||
): void {
|
||||
throw new Error("Cannot update default configuration");
|
||||
}
|
||||
|
||||
private getKey(section: string | undefined, key: string): string {
|
||||
return section ? `${section}.${key}` : key;
|
||||
}
|
||||
}
|
||||
|
||||
class ChainedInMemoryConfiguration {
|
||||
constructor(private readonly configurations: WorkspaceConfiguration[]) {}
|
||||
|
||||
public getConfiguration(target: ConfigurationTarget) {
|
||||
const configuration = this.configurations.find(
|
||||
(configuration) => configuration.scope === target,
|
||||
);
|
||||
|
||||
if (configuration === undefined) {
|
||||
throw new Error(`Unknown configuration target ${target}`);
|
||||
}
|
||||
if (state?.workspaceValue !== this.initialSettingState?.workspaceValue) {
|
||||
await this.set(
|
||||
this.initialSettingState?.workspaceValue,
|
||||
ConfigurationTarget.Workspace,
|
||||
);
|
||||
}
|
||||
if (
|
||||
state?.workspaceFolderValue !==
|
||||
this.initialSettingState?.workspaceFolderValue
|
||||
) {
|
||||
await this.set(
|
||||
this.initialSettingState?.workspaceFolderValue,
|
||||
ConfigurationTarget.WorkspaceFolder,
|
||||
);
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public get<T>(section: string | undefined, key: string): T | undefined {
|
||||
for (const configuration of this.configurations) {
|
||||
if (configuration.has(section, key)) {
|
||||
return configuration.get(section, key);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public has(section: string | undefined, key: string): boolean {
|
||||
return this.configurations.some((configuration) =>
|
||||
configuration.has(section, key),
|
||||
);
|
||||
}
|
||||
|
||||
public update<T>(
|
||||
section: string | undefined,
|
||||
key: string,
|
||||
value: T,
|
||||
target: ConfigurationTarget,
|
||||
): void {
|
||||
const configuration = this.getConfiguration(target);
|
||||
|
||||
configuration.update(section, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Public configuration keys are the ones defined in the package.json.
|
||||
// These keys are documented in the settings page. Other keys are
|
||||
// internal and not documented.
|
||||
const PKG_CONFIGURATION: Record<string, any> =
|
||||
(function initConfigurationKeys() {
|
||||
// Note we are using synchronous file reads here. This is fine because
|
||||
// we are in tests.
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(__dirname, "../../package.json"), "utf-8"),
|
||||
const packageConfiguration: Record<
|
||||
string,
|
||||
{
|
||||
default: any | undefined;
|
||||
}
|
||||
> = (function initConfigurationKeys() {
|
||||
// Note we are using synchronous file reads here. This is fine because
|
||||
// we are in tests.
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(__dirname, "../../package.json"), "utf-8"),
|
||||
);
|
||||
return pkg.contributes.configuration.properties;
|
||||
})();
|
||||
|
||||
export const vsCodeGetConfiguration = workspace.getConfiguration;
|
||||
export let vscodeGetConfigurationMock: jest.SpiedFunction<
|
||||
typeof workspace.getConfiguration
|
||||
>;
|
||||
|
||||
export const beforeEachAction = async () => {
|
||||
const defaultConfiguration = new DefaultConfiguration(packageConfiguration);
|
||||
|
||||
const configuration = new ChainedInMemoryConfiguration([
|
||||
new InMemoryConfiguration(ConfigurationTarget.WorkspaceFolder),
|
||||
new InMemoryConfiguration(ConfigurationTarget.Workspace),
|
||||
new InMemoryConfiguration(ConfigurationTarget.Global),
|
||||
defaultConfiguration,
|
||||
]);
|
||||
|
||||
vscodeGetConfigurationMock = jest
|
||||
.spyOn(workspace, "getConfiguration")
|
||||
.mockImplementation(
|
||||
(
|
||||
section?: string,
|
||||
scope?: ConfigurationScope | null,
|
||||
): VSCodeWorkspaceConfiguration => {
|
||||
if (scope) {
|
||||
throw new Error("Scope is not supported in tests");
|
||||
}
|
||||
|
||||
return {
|
||||
get(key: string, defaultValue?: unknown) {
|
||||
return configuration.get(section, key) ?? defaultValue;
|
||||
},
|
||||
has(key: string) {
|
||||
return configuration.has(section, key);
|
||||
},
|
||||
inspect(_key: string) {
|
||||
throw new Error("inspect is not supported in tests");
|
||||
},
|
||||
async update(
|
||||
key: string,
|
||||
value: unknown,
|
||||
configurationTarget?: ConfigurationTarget | boolean | null,
|
||||
overrideInLanguage?: boolean,
|
||||
) {
|
||||
if (overrideInLanguage) {
|
||||
throw new Error("overrideInLanguage is not supported in tests");
|
||||
}
|
||||
|
||||
function getActualConfigurationTarget(): ConfigurationTarget {
|
||||
if (
|
||||
configurationTarget === undefined ||
|
||||
configurationTarget === null
|
||||
) {
|
||||
return ConfigurationTarget.Global;
|
||||
}
|
||||
if (typeof configurationTarget === "boolean") {
|
||||
return configurationTarget
|
||||
? ConfigurationTarget.Workspace
|
||||
: ConfigurationTarget.Global;
|
||||
}
|
||||
return configurationTarget;
|
||||
}
|
||||
|
||||
const target = getActualConfigurationTarget();
|
||||
|
||||
configuration.update(section, key, value, target);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
return pkg.contributes.configuration.properties;
|
||||
})();
|
||||
|
||||
// The test settings are all settings in ALL_SETTINGS which don't have any children
|
||||
// and are also not hidden settings like the codeQL.canary.
|
||||
const TEST_SETTINGS = ALL_SETTINGS.filter(
|
||||
(setting) =>
|
||||
setting.qualifiedName in PKG_CONFIGURATION && !setting.hasChildren,
|
||||
).map((setting) => new TestSetting(setting));
|
||||
|
||||
export const getTestSetting = (
|
||||
setting: Setting,
|
||||
): TestSetting<unknown> | undefined => {
|
||||
return TEST_SETTINGS.find((testSetting) => testSetting.setting === setting);
|
||||
};
|
||||
|
||||
export const jestTestConfigHelper = async () => {
|
||||
// Read in all current settings
|
||||
await Promise.all(TEST_SETTINGS.map((setting) => setting.initialSetup()));
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset the settings to their initial values before each test
|
||||
await Promise.all(TEST_SETTINGS.map((setting) => setting.setup()));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Restore all settings to their default values after each test suite
|
||||
// Only do this outside of CI since the sometimes hangs on CI.
|
||||
if (process.env.CI !== "true") {
|
||||
await Promise.all(
|
||||
TEST_SETTINGS.map((setting) => setting.restoreToInitialValues()),
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { workspace } from "vscode";
|
||||
|
||||
type MockConfigurationConfig = {
|
||||
values: {
|
||||
[section: string]: {
|
||||
[scope: string]: any | (() => any);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function mockConfiguration(config: MockConfigurationConfig) {
|
||||
const originalGetConfiguration = workspace.getConfiguration;
|
||||
|
||||
jest
|
||||
.spyOn(workspace, "getConfiguration")
|
||||
.mockImplementation((section, scope) => {
|
||||
const configuration = originalGetConfiguration(section, scope);
|
||||
|
||||
return {
|
||||
get(key: string, defaultValue?: unknown) {
|
||||
if (
|
||||
section &&
|
||||
config.values[section] &&
|
||||
config.values[section][key]
|
||||
) {
|
||||
const value = config.values[section][key];
|
||||
return typeof value === "function" ? value() : value;
|
||||
}
|
||||
|
||||
return configuration.get(key, defaultValue);
|
||||
},
|
||||
has(key: string) {
|
||||
return configuration.has(key);
|
||||
},
|
||||
inspect(key: string) {
|
||||
return configuration.inspect(key);
|
||||
},
|
||||
update(
|
||||
key: string,
|
||||
value: unknown,
|
||||
configurationTarget?: boolean,
|
||||
overrideInLanguage?: boolean,
|
||||
) {
|
||||
return configuration.update(
|
||||
key,
|
||||
value,
|
||||
configurationTarget,
|
||||
overrideInLanguage,
|
||||
);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user