Merge branch 'main' into robertbrignull/move_language_pack

This commit is contained in:
Robert 2024-01-29 16:34:13 +00:00 коммит произвёл GitHub
Родитель e70b083828 5367bdea9c
Коммит a228dba0ee
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
17 изменённых файлов: 699 добавлений и 169 удалений

Просмотреть файл

@ -5,6 +5,7 @@
- Enable collection of telemetry for the `codeQL.addingDatabases.addDatabaseSourceToWorkspace` setting. [#3238](https://github.com/github/vscode-codeql/pull/3238)
- In the CodeQL model editor, you can now select individual method rows and save changes to only the selected rows, instead of having to save the entire library model. [#3156](https://github.com/github/vscode-codeql/pull/3156)
- If you run a query without having selected a database, we show a more intuitive prompt to help you select a database. [#3214](https://github.com/github/vscode-codeql/pull/3214)
- Error messages returned from the CodeQL CLI are now less verbose and more user-friendly. [#3259](https://github.com/github/vscode-codeql/pull/3259)
- The UI for browsing and running CodeQL tests has moved to use VS Code's built-in test UI. This makes the CodeQL test UI more consistent with the test UIs for other languages.
This change means that this extension no longer depends on the "Test Explorer UI" and "Test Adapter Converter" extensions. You can uninstall those two extensions if they are
not being used by any other extensions you may have installed.

Просмотреть файл

@ -0,0 +1,102 @@
import { asError, getErrorMessage } from "../common/helpers-pure";
// https://docs.github.com/en/code-security/codeql-cli/using-the-advanced-functionality-of-the-codeql-cli/exit-codes
const EXIT_CODE_USER_ERROR = 2;
const EXIT_CODE_CANCELLED = 98;
export class ExitCodeError extends Error {
constructor(public readonly exitCode: number | null) {
super(`Process exited with code ${exitCode}`);
}
}
export class CliError extends Error {
constructor(
message: string,
public readonly stderr: string | undefined,
public readonly cause: Error,
public readonly commandDescription: string,
public readonly commandArgs: string[],
) {
super(message);
}
}
export function getCliError(
e: unknown,
stderr: string | undefined,
commandDescription: string,
commandArgs: string[],
): CliError {
const error = asError(e);
if (!(error instanceof ExitCodeError) || !stderr) {
return formatCliErrorFallback(
error,
stderr,
commandDescription,
commandArgs,
);
}
switch (error.exitCode) {
case EXIT_CODE_USER_ERROR: {
// This is an error that we should try to format nicely
const fatalErrorIndex = stderr.lastIndexOf("A fatal error occurred: ");
if (fatalErrorIndex !== -1) {
return new CliError(
stderr.slice(fatalErrorIndex),
stderr,
error,
commandDescription,
commandArgs,
);
}
break;
}
case EXIT_CODE_CANCELLED: {
const cancellationIndex = stderr.lastIndexOf(
"Computation was cancelled: ",
);
if (cancellationIndex !== -1) {
return new CliError(
stderr.slice(cancellationIndex),
stderr,
error,
commandDescription,
commandArgs,
);
}
break;
}
}
return formatCliErrorFallback(error, stderr, commandDescription, commandArgs);
}
function formatCliErrorFallback(
error: Error,
stderr: string | undefined,
commandDescription: string,
commandArgs: string[],
): CliError {
if (stderr) {
return new CliError(
stderr,
undefined,
error,
commandDescription,
commandArgs,
);
}
return new CliError(
getErrorMessage(error),
undefined,
error,
commandDescription,
commandArgs,
);
}

Просмотреть файл

@ -35,6 +35,7 @@ import { LINE_ENDINGS, splitStreamAtSeparators } from "../common/split-stream";
import type { Position } from "../query-server/messages";
import { LOGGING_FLAGS } from "./cli-command";
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
import { ExitCodeError, getCliError } from "./cli-errors";
/**
* The version of the SARIF format that we are using.
@ -420,7 +421,9 @@ export class CodeQLCliServer implements Disposable {
stderrBuffers.push(newData);
});
// Listen for process exit.
process.addListener("close", (code) => reject(code));
process.addListener("close", (code) =>
reject(new ExitCodeError(code)),
);
// Write the command followed by a null terminator.
process.stdin.write(JSON.stringify(args), "utf8");
process.stdin.write(this.nullBuffer);
@ -436,19 +439,18 @@ export class CodeQLCliServer implements Disposable {
} catch (err) {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error code or nodejs error)
const newError =
stderrBuffers.length === 0
? new Error(
`${description} failed with args:${EOL} ${argsString}${EOL}${err}`,
)
: new Error(
`${description} failed with args:${EOL} ${argsString}${EOL}${Buffer.concat(
stderrBuffers,
).toString("utf8")}`,
);
newError.stack += getErrorStack(err);
throw newError;
const cliError = getCliError(
err,
stderrBuffers.length > 0
? Buffer.concat(stderrBuffers).toString("utf8")
: undefined,
description,
args,
);
cliError.stack += getErrorStack(err);
throw cliError;
} finally {
if (!silent) {
void this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));

Просмотреть файл

@ -13,6 +13,8 @@ import { redactableError } from "../../common/errors";
import { UserCancellationException } from "./progress";
import { telemetryListener } from "./telemetry";
import type { AppTelemetry } from "../telemetry";
import { CliError } from "../../codeql-cli/cli-errors";
import { EOL } from "os";
/**
* Create a command manager for VSCode, wrapping registerCommandWithErrorHandling
@ -62,6 +64,16 @@ export function registerCommandWithErrorHandling(
} else {
void showAndLogWarningMessage(logger, errorMessage.fullMessage);
}
} else if (e instanceof CliError) {
const fullMessage = `${e.commandDescription} failed with args:${EOL} ${e.commandArgs.join(" ")}${EOL}${
e.stderr ?? e.cause
}`;
void showAndLogExceptionWithTelemetry(logger, telemetry, errorMessage, {
fullMessage,
extraTelemetryProperties: {
command: commandId,
},
});
} else {
// Include the full stack in the error log only.
const fullMessage = errorMessage.fullMessageWithStack;

Просмотреть файл

@ -158,6 +158,7 @@ export interface VariantAnalysisSubmission {
// unclear what it will look like in the future.
export interface VariantAnalysisQueries {
language: QueryLanguage;
count: number;
}
export async function isVariantAnalysisComplete(

Просмотреть файл

@ -405,6 +405,7 @@ export class VariantAnalysisManager
? undefined
: {
language: qlPackDetails.language,
count: qlPackDetails.queryFiles.length,
};
const variantAnalysisSubmission: VariantAnalysisSubmission = {

Просмотреть файл

@ -218,9 +218,15 @@ export class VariantAnalysisView
}
private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
return variantAnalysis
? `${variantAnalysis.query.name} - Variant Analysis Results`
: `Variant Analysis ${this.variantAnalysisId} - Results`;
if (!variantAnalysis) {
return `Variant Analysis ${this.variantAnalysisId} - Results`;
}
if (variantAnalysis.queries) {
return `Variant Analysis using multiple queries - Results`;
} else {
return `${variantAnalysis.query.name} - Variant Analysis Results`;
}
}
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {

Просмотреть файл

@ -20,6 +20,7 @@ import { findMatchingOptions } from "./options";
import { SuggestBoxItem } from "./SuggestBoxItem";
import { LabelText } from "./LabelText";
import type { Diagnostic } from "./diagnostics";
import { useOpenKey } from "./useOpenKey";
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
width: 430px;
@ -152,6 +153,7 @@ export const SuggestBox = <
const focus = useFocus(context);
const role = useRole(context, { role: "listbox" });
const dismiss = useDismiss(context);
const openKey = useOpenKey(context);
const listNav = useListNavigation(context, {
listRef,
activeIndex,
@ -161,7 +163,7 @@ export const SuggestBox = <
});
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
[focus, role, dismiss, listNav],
[focus, role, dismiss, openKey, listNav],
);
const handleInput = useCallback(

Просмотреть файл

@ -0,0 +1,26 @@
import { renderHook } from "@testing-library/react";
import { useEffectEvent } from "../useEffectEvent";
describe("useEffectEvent", () => {
it("does not change reference when changing the callback function", () => {
const callback1 = jest.fn();
const callback2 = jest.fn();
const { result, rerender } = renderHook(
(callback) => useEffectEvent(callback),
{
initialProps: callback1,
},
);
const callbackResult = result.current;
rerender();
expect(result.current).toBe(callbackResult);
rerender(callback2);
expect(result.current).toBe(callbackResult);
});
});

Просмотреть файл

@ -0,0 +1,194 @@
import type { KeyboardEvent } from "react";
import { renderHook } from "@testing-library/react";
import type { FloatingContext } from "@floating-ui/react";
import { mockedObject } from "../../../../../test/mocked-object";
import { useOpenKey } from "../useOpenKey";
describe("useOpenKey", () => {
const onOpenChange = jest.fn();
beforeEach(() => {
onOpenChange.mockReset();
});
const render = ({ open }: { open: boolean }) => {
const context = mockedObject<FloatingContext>({
open,
onOpenChange,
});
const { result } = renderHook(() => useOpenKey(context));
expect(result.current).toEqual({
reference: {
onKeyDown: expect.any(Function),
},
});
const onKeyDown = result.current.reference?.onKeyDown;
if (!onKeyDown) {
throw new Error("onKeyDown is undefined");
}
return {
onKeyDown,
};
};
const mockKeyboardEvent = ({
key = "",
altKey = false,
ctrlKey = false,
metaKey = false,
shiftKey = false,
preventDefault = jest.fn(),
}: Partial<KeyboardEvent>) =>
mockedObject<KeyboardEvent>({
key,
altKey,
ctrlKey,
metaKey,
shiftKey,
preventDefault,
});
const pressKey = (event: Parameters<typeof mockKeyboardEvent>[0]) => {
const { onKeyDown } = render({
open: false,
});
const keyboardEvent = mockKeyboardEvent(event);
onKeyDown(keyboardEvent);
return {
onKeyDown,
keyboardEvent,
};
};
it("opens when pressing Ctrl + Space and it is closed", () => {
const { keyboardEvent } = pressKey({
key: " ",
ctrlKey: true,
});
expect(keyboardEvent.preventDefault).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledTimes(1);
expect(onOpenChange).toHaveBeenCalledWith(true, keyboardEvent);
});
it("does not open when pressing Ctrl + Space and it is open", () => {
const { onKeyDown } = render({
open: true,
});
// Do not mock any properties to ensure that none of them are used.
const keyboardEvent = mockedObject<KeyboardEvent>({});
onKeyDown(keyboardEvent);
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Cmd + Space", () => {
pressKey({
key: " ",
metaKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + Shift + Space", () => {
pressKey({
key: " ",
ctrlKey: true,
shiftKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + Alt + Space", () => {
pressKey({
key: " ",
ctrlKey: true,
altKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + Cmd + Space", () => {
pressKey({
key: " ",
ctrlKey: true,
metaKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + Shift + Alt + Space", () => {
pressKey({
key: " ",
ctrlKey: true,
altKey: true,
shiftKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Space", () => {
pressKey({
key: " ",
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + Tab", () => {
pressKey({
key: "Tab",
ctrlKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not open when pressing Ctrl + a letter", () => {
pressKey({
key: "a",
ctrlKey: true,
});
expect(onOpenChange).not.toHaveBeenCalled();
});
it("does not change reference when the context changes", () => {
const context = mockedObject<FloatingContext>({
open: false,
onOpenChange,
});
const { result, rerender } = renderHook((context) => useOpenKey(context), {
initialProps: context,
});
const firstOnKeyDown = result.current.reference?.onKeyDown;
expect(firstOnKeyDown).toBeDefined();
rerender(
mockedObject<FloatingContext>({
open: true,
onOpenChange: jest.fn(),
}),
);
const secondOnKeyDown = result.current.reference?.onKeyDown;
// test that useEffectEvent is used correctly and the reference doesn't change
expect(secondOnKeyDown).toBe(firstOnKeyDown);
});
});

Просмотреть файл

@ -0,0 +1,23 @@
import { useCallback, useInsertionEffect, useRef } from "react";
// Copy of https://github.com/floating-ui/floating-ui/blob/5d025db1167e0bc13e7d386d7df2498b9edf2f8a/packages/react/src/hooks/utils/useEffectEvent.ts
// since it's not exported
/**
* Creates a reference to a callback that will never change in value. This will ensure that when a callback gets changed,
* no new reference to the callback will be created and thus no unnecessary re-renders will be triggered.
*
* @param callback The callback to call when the event is triggered.
*/
export function useEffectEvent<T extends (...args: any[]) => any>(callback: T) {
const ref = useRef<T>(callback);
useInsertionEffect(() => {
ref.current = callback;
});
return useCallback<(...args: Parameters<T>) => ReturnType<T>>(
(...args) => ref.current(...args),
[],
) as T;
}

Просмотреть файл

@ -0,0 +1,45 @@
import type { KeyboardEvent } from "react";
import { useMemo } from "react";
import type {
ElementProps,
FloatingContext,
ReferenceType,
} from "@floating-ui/react";
import { isReactEvent } from "@floating-ui/react/utils";
import { useEffectEvent } from "./useEffectEvent";
/**
* Open the floating element when Ctrl+Space is pressed.
*/
export const useOpenKey = <RT extends ReferenceType = ReferenceType>(
context: FloatingContext<RT>,
): ElementProps => {
const { open, onOpenChange } = context;
const openOnOpenKey = useEffectEvent(
(event: KeyboardEvent<Element> | KeyboardEvent) => {
if (open) {
return;
}
if (
event.key === " " &&
event.ctrlKey &&
!event.altKey &&
!event.metaKey &&
!event.shiftKey
) {
event.preventDefault();
onOpenChange(true, isReactEvent(event) ? event.nativeEvent : event);
}
},
);
return useMemo((): ElementProps => {
return {
reference: {
onKeyDown: openOnOpenKey,
},
};
}, [openOnOpenKey]);
};

Просмотреть файл

@ -21,6 +21,7 @@ import {
defaultFilterSortState,
filterAndSortRepositoriesWithResults,
} from "../../variant-analysis/shared/variant-analysis-filter-sort";
import { ViewTitle } from "../common";
type VariantAnalysisHeaderProps = {
variantAnalysis: VariantAnalysis;
@ -50,6 +51,29 @@ const Row = styled.div`
align-items: center;
`;
const QueryInfo = ({
variantAnalysis,
onOpenQueryFileClick,
onViewQueryTextClick,
}: {
variantAnalysis: VariantAnalysis;
onOpenQueryFileClick: () => void;
onViewQueryTextClick: () => void;
}) => {
if (variantAnalysis.queries) {
return <ViewTitle>{variantAnalysis.queries?.count} queries</ViewTitle>;
} else {
return (
<QueryDetails
queryName={variantAnalysis.query.name}
queryFileName={basename(variantAnalysis.query.filePath)}
onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick}
/>
);
}
};
export const VariantAnalysisHeader = ({
variantAnalysis,
repositoryStates,
@ -117,9 +141,8 @@ export const VariantAnalysisHeader = ({
return (
<Container>
<Row>
<QueryDetails
queryName={variantAnalysis.query.name}
queryFileName={basename(variantAnalysis.query.filePath)}
<QueryInfo
variantAnalysis={variantAnalysis}
onOpenQueryFileClick={onOpenQueryFileClick}
onViewQueryTextClick={onViewQueryTextClick}
/>

Просмотреть файл

@ -0,0 +1,80 @@
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
type DynamicProperties<T extends object> = {
[P in keyof T]?: () => T[P];
};
type MockedObjectOptions<T extends object> = {
/**
* Properties for which the given method should be called when accessed.
* The method should return the value to be returned when the property is accessed.
* Methods which are explicitly defined in `methods` will take precedence over
* dynamic properties.
*/
dynamicProperties?: DynamicProperties<T>;
};
export function mockedObject<T extends object>(
props: DeepPartial<T>,
{ dynamicProperties }: MockedObjectOptions<T> = {},
): T {
return new Proxy<T>({} as unknown as T, {
get: (_target, prop) => {
if (prop in props) {
return (props as any)[prop];
}
if (dynamicProperties && prop in dynamicProperties) {
return (dynamicProperties as any)[prop]();
}
// The `then` method is accessed by `Promise.resolve` to check if the object is a thenable.
// We don't want to throw an error when this happens.
if (prop === "then") {
return undefined;
}
// The `asymmetricMatch` is accessed by jest to check if the object is a matcher.
// We don't want to throw an error when this happens.
if (prop === "asymmetricMatch") {
return undefined;
}
// The `Symbol.iterator` is accessed by jest to check if the object is iterable.
// We don't want to throw an error when this happens.
if (prop === Symbol.iterator) {
return undefined;
}
// The `$$typeof` is accessed by jest to check if the object is a React element.
// We don't want to throw an error when this happens.
if (prop === "$$typeof") {
return undefined;
}
// The `nodeType` and `tagName` are accessed by jest to check if the object is a DOM node.
// We don't want to throw an error when this happens.
if (prop === "nodeType" || prop === "tagName") {
return undefined;
}
// The `@@__IMMUTABLE_ITERABLE__@@` and variants are accessed by jest to check if the object is an
// immutable object (from Immutable.js).
// We don't want to throw an error when this happens.
if (prop.toString().startsWith("@@__IMMUTABLE_")) {
return undefined;
}
// The `Symbol.toStringTag` is accessed by jest.
// We don't want to throw an error when this happens.
if (prop === Symbol.toStringTag) {
return "MockedObject";
}
throw new Error(`Method ${String(prop)} not mocked`);
},
});
}

Просмотреть файл

@ -0,0 +1,93 @@
import {
CliError,
ExitCodeError,
getCliError,
} from "../../../src/codeql-cli/cli-errors";
import { EOL } from "os";
describe("getCliError", () => {
it("returns an error with an unknown error", () => {
const error = new Error("foo");
expect(getCliError(error, undefined, "bar", ["baz"])).toEqual(
new CliError("foo", undefined, error, "bar", ["baz"]),
);
});
it("returns an error with an unknown error with stderr", () => {
const error = new Error("foo");
expect(getCliError(error, "Something failed", "bar", ["baz"])).toEqual(
new CliError("Something failed", "Something failed", error, "bar", [
"baz",
]),
);
});
it("returns an error with an unknown error with stderr", () => {
const error = new Error("foo");
expect(getCliError(error, "Something failed", "bar", ["baz"])).toEqual(
new CliError("Something failed", "Something failed", error, "bar", [
"baz",
]),
);
});
it("returns an error with an exit code error with unhandled exit code", () => {
const error = new ExitCodeError(99); // OOM
expect(getCliError(error, "OOM!", "bar", ["baz"])).toEqual(
new CliError("OOM!", "OOM!", error, "bar", ["baz"]),
);
});
it("returns an error with an exit code error with handled exit code without string", () => {
const error = new ExitCodeError(2);
expect(getCliError(error, "Something happened!", "bar", ["baz"])).toEqual(
new CliError("Something happened!", "Something happened!", error, "bar", [
"baz",
]),
);
});
it("returns an error with a user code error with identifying string", () => {
const error = new ExitCodeError(2);
const stderr = `Something happened!${EOL}A fatal error occurred: The query did not run successfully.${EOL}The correct columns were not present.`;
expect(getCliError(error, stderr, "bar", ["baz"])).toEqual(
new CliError(
`A fatal error occurred: The query did not run successfully.${EOL}The correct columns were not present.`,
stderr,
error,
"bar",
["baz"],
),
);
});
it("returns an error with a user code error with cancelled string", () => {
const error = new ExitCodeError(2);
const stderr = `Running query...${EOL}Something is happening...${EOL}Computation was cancelled: Cancelled by user`;
expect(getCliError(error, stderr, "bar", ["baz"])).toEqual(
new CliError(stderr, stderr, error, "bar", ["baz"]),
);
});
it("returns an error with a cancelled error with identifying string", () => {
const error = new ExitCodeError(98);
const stderr = `Running query...${EOL}Something is happening...${EOL}Computation was cancelled: Cancelled by user`;
expect(getCliError(error, stderr, "bar", ["baz"])).toEqual(
new CliError(
"Computation was cancelled: Cancelled by user",
stderr,
error,
"bar",
["baz"],
),
);
});
});

Просмотреть файл

@ -3,7 +3,7 @@ import {
writeQueryHistoryToFile,
} from "../../../../../src/query-history/store/query-history-store";
import { join } from "path";
import { writeFileSync, mkdirpSync, writeFile } from "fs-extra";
import { writeFileSync, mkdirpSync } from "fs-extra";
import type { InitialQueryInfo } from "../../../../../src/query-results";
import { LocalQueryInfo } from "../../../../../src/query-results";
import type { QueryWithResults } from "../../../../../src/run-queries-shared";
@ -13,22 +13,18 @@ import type { DatabaseInfo } from "../../../../../src/common/interface-types";
import type { CancellationTokenSource } from "vscode";
import { Uri } from "vscode";
import { tmpDir } from "../../../../../src/tmp-dir";
import type { VariantAnalysisHistoryItem } from "../../../../../src/query-history/variant-analysis-history-item";
import type { QueryHistoryInfo } from "../../../../../src/query-history/query-history-info";
import { createMockVariantAnalysisHistoryItem } from "../../../../factories/query-history/variant-analysis-history-item";
import { nanoid } from "nanoid";
import type {
QueryHistoryDto,
QueryHistoryItemDto,
} from "../../../../../src/query-history/store/query-history-dto";
import { mapQueryHistoryToDto } from "../../../../../src/query-history/store/query-history-domain-mapper";
describe("write and read", () => {
let infoSuccessRaw: LocalQueryInfo;
let infoSuccessInterpreted: LocalQueryInfo;
let infoEarlyFailure: LocalQueryInfo;
let infoLateFailure: LocalQueryInfo;
let infoInProgress: LocalQueryInfo;
let variantAnalysis1: VariantAnalysisHistoryItem;
let variantAnalysis2: VariantAnalysisHistoryItem;
let allHistory: QueryHistoryInfo[];
let allHistoryDtos: QueryHistoryItemDto[];
let expectedHistory: QueryHistoryInfo[];
let queryPath: string;
let cnt = 0;
@ -36,23 +32,23 @@ describe("write and read", () => {
beforeEach(() => {
queryPath = join(Uri.file(tmpDir.name).fsPath, `query-${cnt++}`);
infoSuccessRaw = createMockFullQueryInfo(
const infoSuccessRaw = createMockFullQueryInfo(
"a",
createMockQueryWithResults(`${queryPath}-a`, false, "/a/b/c/a"),
);
infoSuccessInterpreted = createMockFullQueryInfo(
const infoSuccessInterpreted = createMockFullQueryInfo(
"b",
createMockQueryWithResults(`${queryPath}-b`, true, "/a/b/c/b"),
);
infoEarlyFailure = createMockFullQueryInfo("c", undefined, true);
infoLateFailure = createMockFullQueryInfo(
const infoEarlyFailure = createMockFullQueryInfo("c", undefined, true);
const infoLateFailure = createMockFullQueryInfo(
"d",
createMockQueryWithResults(`${queryPath}-c`, false, "/a/b/c/d"),
);
infoInProgress = createMockFullQueryInfo("e");
const infoInProgress = createMockFullQueryInfo("e");
variantAnalysis1 = createMockVariantAnalysisHistoryItem({});
variantAnalysis2 = createMockVariantAnalysisHistoryItem({});
const variantAnalysis1 = createMockVariantAnalysisHistoryItem({});
const variantAnalysis2 = createMockVariantAnalysisHistoryItem({});
allHistory = [
infoSuccessRaw,
@ -64,6 +60,8 @@ describe("write and read", () => {
variantAnalysis2,
];
allHistoryDtos = mapQueryHistoryToDto(allHistory);
// the expected results only contains the history with completed queries
expectedHistory = [
infoSuccessRaw,
@ -139,54 +137,50 @@ describe("write and read", () => {
it("should remove remote queries from the history", async () => {
const path = join(tmpDir.name, "query-history-with-remote.json");
await writeFile(
path,
JSON.stringify({
version: 2,
queries: [
...allHistory,
{
t: "remote",
status: "InProgress",
completed: false,
queryId: nanoid(),
remoteQuery: {
queryName: "query-name",
queryFilePath: "query-file.ql",
queryText: "select 1",
language: "javascript",
controllerRepository: {
owner: "github",
name: "vscode-codeql-integration-tests",
},
executionStartTime: Date.now(),
actionsWorkflowRunId: 1,
repositoryCount: 0,
writeRawQueryHistory(path, {
version: 2,
queries: [
...allHistoryDtos,
{
t: "remote",
status: "InProgress",
completed: false,
queryId: nanoid(),
remoteQuery: {
queryName: "query-name",
queryFilePath: "query-file.ql",
queryText: "select 1",
language: "javascript",
controllerRepository: {
owner: "github",
name: "vscode-codeql-integration-tests",
},
executionStartTime: Date.now(),
actionsWorkflowRunId: 1,
repositoryCount: 0,
},
{
t: "remote",
status: "Completed",
completed: true,
queryId: nanoid(),
remoteQuery: {
queryName: "query-name",
queryFilePath: "query-file.ql",
queryText: "select 1",
language: "javascript",
controllerRepository: {
owner: "github",
name: "vscode-codeql-integration-tests",
},
executionStartTime: Date.now(),
actionsWorkflowRunId: 1,
repositoryCount: 0,
} as unknown as QueryHistoryItemDto,
{
t: "remote",
status: "Completed",
completed: true,
queryId: nanoid(),
remoteQuery: {
queryName: "query-name",
queryFilePath: "query-file.ql",
queryText: "select 1",
language: "javascript",
controllerRepository: {
owner: "github",
name: "vscode-codeql-integration-tests",
},
executionStartTime: Date.now(),
actionsWorkflowRunId: 1,
repositoryCount: 0,
},
],
}),
"utf8",
);
} as unknown as QueryHistoryItemDto,
],
});
const actual = await readQueryHistoryFromFile(path);
expect(actual.length).toEqual(expectedHistory.length);
@ -194,14 +188,10 @@ describe("write and read", () => {
it("should handle an invalid query history version", async () => {
const badPath = join(tmpDir.name, "bad-query-history.json");
writeFileSync(
badPath,
JSON.stringify({
version: 3,
queries: allHistory,
}),
"utf8",
);
writeRawQueryHistory(badPath, {
version: 3,
queries: allHistoryDtos,
});
const allHistoryActual = await readQueryHistoryFromFile(badPath);
// version number is invalid. Should return an empty array.
@ -274,4 +264,8 @@ describe("write and read", () => {
return result;
}
function writeRawQueryHistory(path: string, queryHistory: QueryHistoryDto) {
writeFileSync(path, JSON.stringify(queryHistory), "utf8");
}
});

Просмотреть файл

@ -2,86 +2,11 @@ import type { QuickPickItem, window, Uri } from "vscode";
import type { DatabaseItem } from "../../../src/databases/local-databases";
import type { Octokit } from "@octokit/rest";
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
import type { DeepPartial } from "../../mocked-object";
import { mockedObject } from "../../mocked-object";
type DynamicProperties<T extends object> = {
[P in keyof T]?: () => T[P];
};
type MockedObjectOptions<T extends object> = {
/**
* Properties for which the given method should be called when accessed.
* The method should return the value to be returned when the property is accessed.
* Methods which are explicitly defined in `methods` will take precedence over
* dynamic properties.
*/
dynamicProperties?: DynamicProperties<T>;
};
export function mockedObject<T extends object>(
props: DeepPartial<T>,
{ dynamicProperties }: MockedObjectOptions<T> = {},
): T {
return new Proxy<T>({} as unknown as T, {
get: (_target, prop) => {
if (prop in props) {
return (props as any)[prop];
}
if (dynamicProperties && prop in dynamicProperties) {
return (dynamicProperties as any)[prop]();
}
// The `then` method is accessed by `Promise.resolve` to check if the object is a thenable.
// We don't want to throw an error when this happens.
if (prop === "then") {
return undefined;
}
// The `asymmetricMatch` is accessed by jest to check if the object is a matcher.
// We don't want to throw an error when this happens.
if (prop === "asymmetricMatch") {
return undefined;
}
// The `Symbol.iterator` is accessed by jest to check if the object is iterable.
// We don't want to throw an error when this happens.
if (prop === Symbol.iterator) {
return undefined;
}
// The `$$typeof` is accessed by jest to check if the object is a React element.
// We don't want to throw an error when this happens.
if (prop === "$$typeof") {
return undefined;
}
// The `nodeType` and `tagName` are accessed by jest to check if the object is a DOM node.
// We don't want to throw an error when this happens.
if (prop === "nodeType" || prop === "tagName") {
return undefined;
}
// The `@@__IMMUTABLE_ITERABLE__@@` and variants are accessed by jest to check if the object is an
// immutable object (from Immutable.js).
// We don't want to throw an error when this happens.
if (prop.toString().startsWith("@@__IMMUTABLE_")) {
return undefined;
}
// The `Symbol.toStringTag` is accessed by jest.
// We don't want to throw an error when this happens.
if (prop === Symbol.toStringTag) {
return "MockedObject";
}
throw new Error(`Method ${String(prop)} not mocked`);
},
});
}
export { mockedObject };
export type { DeepPartial };
export function mockedOctokitFunction<
Namespace extends keyof Octokit["rest"],