Merge branch 'main' into robertbrignull/move_language_pack
This commit is contained in:
Коммит
a228dba0ee
|
@ -5,6 +5,7 @@
|
||||||
- Enable collection of telemetry for the `codeQL.addingDatabases.addDatabaseSourceToWorkspace` setting. [#3238](https://github.com/github/vscode-codeql/pull/3238)
|
- 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)
|
- 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)
|
- 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.
|
- 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
|
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.
|
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 type { Position } from "../query-server/messages";
|
||||||
import { LOGGING_FLAGS } from "./cli-command";
|
import { LOGGING_FLAGS } from "./cli-command";
|
||||||
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
|
import type { CliFeatures, VersionAndFeatures } from "./cli-version";
|
||||||
|
import { ExitCodeError, getCliError } from "./cli-errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the SARIF format that we are using.
|
* The version of the SARIF format that we are using.
|
||||||
|
@ -420,7 +421,9 @@ export class CodeQLCliServer implements Disposable {
|
||||||
stderrBuffers.push(newData);
|
stderrBuffers.push(newData);
|
||||||
});
|
});
|
||||||
// Listen for process exit.
|
// 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.
|
// Write the command followed by a null terminator.
|
||||||
process.stdin.write(JSON.stringify(args), "utf8");
|
process.stdin.write(JSON.stringify(args), "utf8");
|
||||||
process.stdin.write(this.nullBuffer);
|
process.stdin.write(this.nullBuffer);
|
||||||
|
@ -436,19 +439,18 @@ export class CodeQLCliServer implements Disposable {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Kill the process if it isn't already dead.
|
// Kill the process if it isn't already dead.
|
||||||
this.killProcessIfRunning();
|
this.killProcessIfRunning();
|
||||||
|
|
||||||
// Report the error (if there is a stderr then use that otherwise just report the error code or nodejs error)
|
// Report the error (if there is a stderr then use that otherwise just report the error code or nodejs error)
|
||||||
const newError =
|
const cliError = getCliError(
|
||||||
stderrBuffers.length === 0
|
err,
|
||||||
? new Error(
|
stderrBuffers.length > 0
|
||||||
`${description} failed with args:${EOL} ${argsString}${EOL}${err}`,
|
? Buffer.concat(stderrBuffers).toString("utf8")
|
||||||
)
|
: undefined,
|
||||||
: new Error(
|
description,
|
||||||
`${description} failed with args:${EOL} ${argsString}${EOL}${Buffer.concat(
|
args,
|
||||||
stderrBuffers,
|
);
|
||||||
).toString("utf8")}`,
|
cliError.stack += getErrorStack(err);
|
||||||
);
|
throw cliError;
|
||||||
newError.stack += getErrorStack(err);
|
|
||||||
throw newError;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
void this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
|
void this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { redactableError } from "../../common/errors";
|
||||||
import { UserCancellationException } from "./progress";
|
import { UserCancellationException } from "./progress";
|
||||||
import { telemetryListener } from "./telemetry";
|
import { telemetryListener } from "./telemetry";
|
||||||
import type { AppTelemetry } 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
|
* Create a command manager for VSCode, wrapping registerCommandWithErrorHandling
|
||||||
|
@ -62,6 +64,16 @@ export function registerCommandWithErrorHandling(
|
||||||
} else {
|
} else {
|
||||||
void showAndLogWarningMessage(logger, errorMessage.fullMessage);
|
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 {
|
} else {
|
||||||
// Include the full stack in the error log only.
|
// Include the full stack in the error log only.
|
||||||
const fullMessage = errorMessage.fullMessageWithStack;
|
const fullMessage = errorMessage.fullMessageWithStack;
|
||||||
|
|
|
@ -158,6 +158,7 @@ export interface VariantAnalysisSubmission {
|
||||||
// unclear what it will look like in the future.
|
// unclear what it will look like in the future.
|
||||||
export interface VariantAnalysisQueries {
|
export interface VariantAnalysisQueries {
|
||||||
language: QueryLanguage;
|
language: QueryLanguage;
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isVariantAnalysisComplete(
|
export async function isVariantAnalysisComplete(
|
||||||
|
|
|
@ -405,6 +405,7 @@ export class VariantAnalysisManager
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
language: qlPackDetails.language,
|
language: qlPackDetails.language,
|
||||||
|
count: qlPackDetails.queryFiles.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const variantAnalysisSubmission: VariantAnalysisSubmission = {
|
const variantAnalysisSubmission: VariantAnalysisSubmission = {
|
||||||
|
|
|
@ -218,9 +218,15 @@ export class VariantAnalysisView
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
|
private getTitle(variantAnalysis: VariantAnalysis | undefined): string {
|
||||||
return variantAnalysis
|
if (!variantAnalysis) {
|
||||||
? `${variantAnalysis.query.name} - Variant Analysis Results`
|
return `Variant Analysis ${this.variantAnalysisId} - Results`;
|
||||||
: `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> {
|
private async showDataFlows(dataFlows: DataFlowPaths): Promise<void> {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { findMatchingOptions } from "./options";
|
||||||
import { SuggestBoxItem } from "./SuggestBoxItem";
|
import { SuggestBoxItem } from "./SuggestBoxItem";
|
||||||
import { LabelText } from "./LabelText";
|
import { LabelText } from "./LabelText";
|
||||||
import type { Diagnostic } from "./diagnostics";
|
import type { Diagnostic } from "./diagnostics";
|
||||||
|
import { useOpenKey } from "./useOpenKey";
|
||||||
|
|
||||||
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
|
const Input = styled(VSCodeTextField)<{ $error: boolean }>`
|
||||||
width: 430px;
|
width: 430px;
|
||||||
|
@ -152,6 +153,7 @@ export const SuggestBox = <
|
||||||
const focus = useFocus(context);
|
const focus = useFocus(context);
|
||||||
const role = useRole(context, { role: "listbox" });
|
const role = useRole(context, { role: "listbox" });
|
||||||
const dismiss = useDismiss(context);
|
const dismiss = useDismiss(context);
|
||||||
|
const openKey = useOpenKey(context);
|
||||||
const listNav = useListNavigation(context, {
|
const listNav = useListNavigation(context, {
|
||||||
listRef,
|
listRef,
|
||||||
activeIndex,
|
activeIndex,
|
||||||
|
@ -161,7 +163,7 @@ export const SuggestBox = <
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
|
||||||
[focus, role, dismiss, listNav],
|
[focus, role, dismiss, openKey, listNav],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInput = useCallback(
|
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,
|
defaultFilterSortState,
|
||||||
filterAndSortRepositoriesWithResults,
|
filterAndSortRepositoriesWithResults,
|
||||||
} from "../../variant-analysis/shared/variant-analysis-filter-sort";
|
} from "../../variant-analysis/shared/variant-analysis-filter-sort";
|
||||||
|
import { ViewTitle } from "../common";
|
||||||
|
|
||||||
type VariantAnalysisHeaderProps = {
|
type VariantAnalysisHeaderProps = {
|
||||||
variantAnalysis: VariantAnalysis;
|
variantAnalysis: VariantAnalysis;
|
||||||
|
@ -50,6 +51,29 @@ const Row = styled.div`
|
||||||
align-items: center;
|
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 = ({
|
export const VariantAnalysisHeader = ({
|
||||||
variantAnalysis,
|
variantAnalysis,
|
||||||
repositoryStates,
|
repositoryStates,
|
||||||
|
@ -117,9 +141,8 @@ export const VariantAnalysisHeader = ({
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Row>
|
<Row>
|
||||||
<QueryDetails
|
<QueryInfo
|
||||||
queryName={variantAnalysis.query.name}
|
variantAnalysis={variantAnalysis}
|
||||||
queryFileName={basename(variantAnalysis.query.filePath)}
|
|
||||||
onOpenQueryFileClick={onOpenQueryFileClick}
|
onOpenQueryFileClick={onOpenQueryFileClick}
|
||||||
onViewQueryTextClick={onViewQueryTextClick}
|
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,
|
writeQueryHistoryToFile,
|
||||||
} from "../../../../../src/query-history/store/query-history-store";
|
} from "../../../../../src/query-history/store/query-history-store";
|
||||||
import { join } from "path";
|
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 type { InitialQueryInfo } from "../../../../../src/query-results";
|
||||||
import { LocalQueryInfo } from "../../../../../src/query-results";
|
import { LocalQueryInfo } from "../../../../../src/query-results";
|
||||||
import type { QueryWithResults } from "../../../../../src/run-queries-shared";
|
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 type { CancellationTokenSource } from "vscode";
|
||||||
import { Uri } from "vscode";
|
import { Uri } from "vscode";
|
||||||
import { tmpDir } from "../../../../../src/tmp-dir";
|
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 type { QueryHistoryInfo } from "../../../../../src/query-history/query-history-info";
|
||||||
import { createMockVariantAnalysisHistoryItem } from "../../../../factories/query-history/variant-analysis-history-item";
|
import { createMockVariantAnalysisHistoryItem } from "../../../../factories/query-history/variant-analysis-history-item";
|
||||||
import { nanoid } from "nanoid";
|
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", () => {
|
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 allHistory: QueryHistoryInfo[];
|
||||||
|
let allHistoryDtos: QueryHistoryItemDto[];
|
||||||
let expectedHistory: QueryHistoryInfo[];
|
let expectedHistory: QueryHistoryInfo[];
|
||||||
let queryPath: string;
|
let queryPath: string;
|
||||||
let cnt = 0;
|
let cnt = 0;
|
||||||
|
@ -36,23 +32,23 @@ describe("write and read", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
queryPath = join(Uri.file(tmpDir.name).fsPath, `query-${cnt++}`);
|
queryPath = join(Uri.file(tmpDir.name).fsPath, `query-${cnt++}`);
|
||||||
|
|
||||||
infoSuccessRaw = createMockFullQueryInfo(
|
const infoSuccessRaw = createMockFullQueryInfo(
|
||||||
"a",
|
"a",
|
||||||
createMockQueryWithResults(`${queryPath}-a`, false, "/a/b/c/a"),
|
createMockQueryWithResults(`${queryPath}-a`, false, "/a/b/c/a"),
|
||||||
);
|
);
|
||||||
infoSuccessInterpreted = createMockFullQueryInfo(
|
const infoSuccessInterpreted = createMockFullQueryInfo(
|
||||||
"b",
|
"b",
|
||||||
createMockQueryWithResults(`${queryPath}-b`, true, "/a/b/c/b"),
|
createMockQueryWithResults(`${queryPath}-b`, true, "/a/b/c/b"),
|
||||||
);
|
);
|
||||||
infoEarlyFailure = createMockFullQueryInfo("c", undefined, true);
|
const infoEarlyFailure = createMockFullQueryInfo("c", undefined, true);
|
||||||
infoLateFailure = createMockFullQueryInfo(
|
const infoLateFailure = createMockFullQueryInfo(
|
||||||
"d",
|
"d",
|
||||||
createMockQueryWithResults(`${queryPath}-c`, false, "/a/b/c/d"),
|
createMockQueryWithResults(`${queryPath}-c`, false, "/a/b/c/d"),
|
||||||
);
|
);
|
||||||
infoInProgress = createMockFullQueryInfo("e");
|
const infoInProgress = createMockFullQueryInfo("e");
|
||||||
|
|
||||||
variantAnalysis1 = createMockVariantAnalysisHistoryItem({});
|
const variantAnalysis1 = createMockVariantAnalysisHistoryItem({});
|
||||||
variantAnalysis2 = createMockVariantAnalysisHistoryItem({});
|
const variantAnalysis2 = createMockVariantAnalysisHistoryItem({});
|
||||||
|
|
||||||
allHistory = [
|
allHistory = [
|
||||||
infoSuccessRaw,
|
infoSuccessRaw,
|
||||||
|
@ -64,6 +60,8 @@ describe("write and read", () => {
|
||||||
variantAnalysis2,
|
variantAnalysis2,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
allHistoryDtos = mapQueryHistoryToDto(allHistory);
|
||||||
|
|
||||||
// the expected results only contains the history with completed queries
|
// the expected results only contains the history with completed queries
|
||||||
expectedHistory = [
|
expectedHistory = [
|
||||||
infoSuccessRaw,
|
infoSuccessRaw,
|
||||||
|
@ -139,54 +137,50 @@ describe("write and read", () => {
|
||||||
|
|
||||||
it("should remove remote queries from the history", async () => {
|
it("should remove remote queries from the history", async () => {
|
||||||
const path = join(tmpDir.name, "query-history-with-remote.json");
|
const path = join(tmpDir.name, "query-history-with-remote.json");
|
||||||
await writeFile(
|
writeRawQueryHistory(path, {
|
||||||
path,
|
version: 2,
|
||||||
JSON.stringify({
|
queries: [
|
||||||
version: 2,
|
...allHistoryDtos,
|
||||||
queries: [
|
{
|
||||||
...allHistory,
|
t: "remote",
|
||||||
{
|
status: "InProgress",
|
||||||
t: "remote",
|
completed: false,
|
||||||
status: "InProgress",
|
queryId: nanoid(),
|
||||||
completed: false,
|
remoteQuery: {
|
||||||
queryId: nanoid(),
|
queryName: "query-name",
|
||||||
remoteQuery: {
|
queryFilePath: "query-file.ql",
|
||||||
queryName: "query-name",
|
queryText: "select 1",
|
||||||
queryFilePath: "query-file.ql",
|
language: "javascript",
|
||||||
queryText: "select 1",
|
controllerRepository: {
|
||||||
language: "javascript",
|
owner: "github",
|
||||||
controllerRepository: {
|
name: "vscode-codeql-integration-tests",
|
||||||
owner: "github",
|
|
||||||
name: "vscode-codeql-integration-tests",
|
|
||||||
},
|
|
||||||
executionStartTime: Date.now(),
|
|
||||||
actionsWorkflowRunId: 1,
|
|
||||||
repositoryCount: 0,
|
|
||||||
},
|
},
|
||||||
|
executionStartTime: Date.now(),
|
||||||
|
actionsWorkflowRunId: 1,
|
||||||
|
repositoryCount: 0,
|
||||||
},
|
},
|
||||||
{
|
} as unknown as QueryHistoryItemDto,
|
||||||
t: "remote",
|
{
|
||||||
status: "Completed",
|
t: "remote",
|
||||||
completed: true,
|
status: "Completed",
|
||||||
queryId: nanoid(),
|
completed: true,
|
||||||
remoteQuery: {
|
queryId: nanoid(),
|
||||||
queryName: "query-name",
|
remoteQuery: {
|
||||||
queryFilePath: "query-file.ql",
|
queryName: "query-name",
|
||||||
queryText: "select 1",
|
queryFilePath: "query-file.ql",
|
||||||
language: "javascript",
|
queryText: "select 1",
|
||||||
controllerRepository: {
|
language: "javascript",
|
||||||
owner: "github",
|
controllerRepository: {
|
||||||
name: "vscode-codeql-integration-tests",
|
owner: "github",
|
||||||
},
|
name: "vscode-codeql-integration-tests",
|
||||||
executionStartTime: Date.now(),
|
|
||||||
actionsWorkflowRunId: 1,
|
|
||||||
repositoryCount: 0,
|
|
||||||
},
|
},
|
||||||
|
executionStartTime: Date.now(),
|
||||||
|
actionsWorkflowRunId: 1,
|
||||||
|
repositoryCount: 0,
|
||||||
},
|
},
|
||||||
],
|
} as unknown as QueryHistoryItemDto,
|
||||||
}),
|
],
|
||||||
"utf8",
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const actual = await readQueryHistoryFromFile(path);
|
const actual = await readQueryHistoryFromFile(path);
|
||||||
expect(actual.length).toEqual(expectedHistory.length);
|
expect(actual.length).toEqual(expectedHistory.length);
|
||||||
|
@ -194,14 +188,10 @@ describe("write and read", () => {
|
||||||
|
|
||||||
it("should handle an invalid query history version", async () => {
|
it("should handle an invalid query history version", async () => {
|
||||||
const badPath = join(tmpDir.name, "bad-query-history.json");
|
const badPath = join(tmpDir.name, "bad-query-history.json");
|
||||||
writeFileSync(
|
writeRawQueryHistory(badPath, {
|
||||||
badPath,
|
version: 3,
|
||||||
JSON.stringify({
|
queries: allHistoryDtos,
|
||||||
version: 3,
|
});
|
||||||
queries: allHistory,
|
|
||||||
}),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
|
|
||||||
const allHistoryActual = await readQueryHistoryFromFile(badPath);
|
const allHistoryActual = await readQueryHistoryFromFile(badPath);
|
||||||
// version number is invalid. Should return an empty array.
|
// version number is invalid. Should return an empty array.
|
||||||
|
@ -274,4 +264,8 @@ describe("write and read", () => {
|
||||||
|
|
||||||
return result;
|
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 { DatabaseItem } from "../../../src/databases/local-databases";
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends object
|
import type { DeepPartial } from "../../mocked-object";
|
||||||
? {
|
import { mockedObject } from "../../mocked-object";
|
||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
|
||||||
}
|
|
||||||
: T;
|
|
||||||
|
|
||||||
type DynamicProperties<T extends object> = {
|
export { mockedObject };
|
||||||
[P in keyof T]?: () => T[P];
|
export type { DeepPartial };
|
||||||
};
|
|
||||||
|
|
||||||
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 function mockedOctokitFunction<
|
export function mockedOctokitFunction<
|
||||||
Namespace extends keyof Octokit["rest"],
|
Namespace extends keyof Octokit["rest"],
|
||||||
|
|
Загрузка…
Ссылка в новой задаче