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)
|
||||
- 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"],
|
||||
|
|
Загрузка…
Ссылка в новой задаче