From 5c59d41da6268ae763b6495ee9db244ac4991de9 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 23 Sep 2024 16:24:55 -0400 Subject: [PATCH 01/62] wip: ErrorCorrelator facility and work on error handling Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 107 ++++++++++++++++++ .../zowe-explorer-api/src/utils/Singleton.ts | 22 ++++ packages/zowe-explorer-api/src/utils/index.ts | 1 + .../src/trees/uss/UssFSProvider.ts | 5 + 4 files changed, 135 insertions(+) create mode 100644 packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts create mode 100644 packages/zowe-explorer-api/src/utils/Singleton.ts diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts new file mode 100644 index 000000000..12641d4c7 --- /dev/null +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -0,0 +1,107 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { ImperativeError } from "@zowe/imperative"; +import { Singleton } from "./Singleton"; +import { Gui } from "../globals"; + +type ErrorMatch = string | RegExp; + +interface ErrorCorrelation { + errorCode?: string; + matches: ErrorMatch | ErrorMatch[]; + details: string; + tips?: string[]; +} + +interface NetworkErrorInfo { + errorCode?: string; + fullError?: string; + tips?: string[]; +} + +export class NetworkError extends ImperativeError { + public constructor(msg: string, public info?: NetworkErrorInfo) { + super({ msg }); + } +} + +export enum ZoweExplorerApiType { + Mvs = "mvs", + Jes = "jes", + Uss = "uss", + Command = "cmd", + /* errors that match all API types */ + All = "all", +} + +export type ApiErrors = Partial>; + +export class ErrorCorrelations {} + +export class ErrorCorrelator extends Singleton { + private errorMatches: Map = new Map([ + [ + "zosmf", + { + [ZoweExplorerApiType.Mvs]: [ + { + errorCode: "403", + matches: [/Client is not authorized for file access\.$/], + details: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + tips: [], + }, + ], + [ZoweExplorerApiType.Uss]: [ + { + errorCode: "403", + matches: [/Client is not authorized for file access\.$/], + details: "Insufficient write permissions for this file. The file may be read-only or locked.", + tips: [], + }, + ], + }, + ], + ]); + + public constructor() { + super(); + } + + public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): NetworkError { + if (!this.errorMatches.has(profileType)) { + return new NetworkError(errorDetails); + } + + for (const apiError of [...this.errorMatches.get(profileType)[api], ...(this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? [])]) { + for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { + if (errorDetails.match(match)) { + return new NetworkError(apiError.details, { errorCode: apiError.errorCode, fullError: errorDetails, tips: apiError?.tips }); + } + } + } + + return new NetworkError(errorDetails); + } + + public async translateAndDisplayError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): Promise { + const error = this.correlateError(api, profileType, errorDetails); + const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} (Error Code ${error.info?.errorCode})`, { + items: ["Retry", "More info"], + }); + + if (userSelection === "More info" && error.info?.fullError) { + Gui.errorMessage(error.info.fullError); + } + + return userSelection; + } +} diff --git a/packages/zowe-explorer-api/src/utils/Singleton.ts b/packages/zowe-explorer-api/src/utils/Singleton.ts new file mode 100644 index 000000000..4f98b7402 --- /dev/null +++ b/packages/zowe-explorer-api/src/utils/Singleton.ts @@ -0,0 +1,22 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +export abstract class Singleton { + private static instances: Map = new Map(); + + protected constructor() {} + public static getInstance(this: new () => T): T { + if (!Singleton.instances.has(this)) { + Singleton.instances.set(this, new this()); + } + return Singleton.instances.get(this) as T; + } +} diff --git a/packages/zowe-explorer-api/src/utils/index.ts b/packages/zowe-explorer-api/src/utils/index.ts index a89e6c2f3..bb4931a17 100644 --- a/packages/zowe-explorer-api/src/utils/index.ts +++ b/packages/zowe-explorer-api/src/utils/index.ts @@ -12,6 +12,7 @@ import { IZoweTreeNode } from "../tree"; import { workspace } from "vscode"; +export * from "./ErrorCorrelator"; export * from "./Poller"; export * from "./FileManagement"; diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index d2a830f85..0b9483247 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -14,6 +14,7 @@ import * as vscode from "vscode"; import { BaseProvider, BufferBuilder, + ErrorCorrelator, FsAbstractUtils, imperative, Gui, @@ -23,6 +24,7 @@ import { ZosEncoding, ZoweScheme, UriFsInfo, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; import { USSFileStructure } from "./USSFileStructure"; @@ -391,6 +393,9 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }); } catch (err) { statusMsg.dispose(); + if (err instanceof imperative.ImperativeError) { + await ErrorCorrelator.getInstance().translateAndDisplayError(ZoweExplorerApiType.Uss, entry.metadata.profile.type, err.message); + } throw err; } From 96de43be1c1dd17b72e53f71766e4ba08145c003 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 25 Sep 2024 15:00:20 -0400 Subject: [PATCH 02/62] chore: add typedoc to ErrorCorrelation; details -> summary Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 12641d4c7..53c62e5f3 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -16,9 +16,25 @@ import { Gui } from "../globals"; type ErrorMatch = string | RegExp; interface ErrorCorrelation { + /** + * An optional error code returned from the server. + * @type {string} + */ errorCode?: string; + /** + * One or more patterns to check for within the error message. + * @type {ErrorMatch | ErrorMatch[]} + */ matches: ErrorMatch | ErrorMatch[]; - details: string; + /** + * Human-readable, brief summary of the error that was encountered. + * @type {string} + */ + summary: string; + /** + * Troubleshooting tips for end users that encounter the given error. + * @type {string[]} + */ tips?: string[]; } @@ -56,7 +72,7 @@ export class ErrorCorrelator extends Singleton { { errorCode: "403", matches: [/Client is not authorized for file access\.$/], - details: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", tips: [], }, ], @@ -64,7 +80,7 @@ export class ErrorCorrelator extends Singleton { { errorCode: "403", matches: [/Client is not authorized for file access\.$/], - details: "Insufficient write permissions for this file. The file may be read-only or locked.", + summary: "Insufficient write permissions for this file. The file may be read-only or locked.", tips: [], }, ], @@ -84,7 +100,7 @@ export class ErrorCorrelator extends Singleton { for (const apiError of [...this.errorMatches.get(profileType)[api], ...(this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? [])]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { if (errorDetails.match(match)) { - return new NetworkError(apiError.details, { errorCode: apiError.errorCode, fullError: errorDetails, tips: apiError?.tips }); + return new NetworkError(apiError.summary, { errorCode: apiError.errorCode, fullError: errorDetails, tips: apiError?.tips }); } } } From 17f1beaf421af67c2bca5c8766c640836b1f617b Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 26 Sep 2024 08:47:37 -0400 Subject: [PATCH 03/62] wip: Error prompts and webview PoC Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 20 ++++++--- .../src/trees/uss/UssFSProvider.ts | 13 +++++- .../webviews/src/troubleshoot-error/App.tsx | 44 +++++++++++++++++++ .../components/ErrorInfo.tsx | 34 ++++++++++++++ .../src/troubleshoot-error/index.html | 16 +++++++ .../webviews/src/troubleshoot-error/index.tsx | 15 +++++++ 6 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 53c62e5f3..e41beb1d9 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -41,12 +41,13 @@ interface ErrorCorrelation { interface NetworkErrorInfo { errorCode?: string; fullError?: string; + summary: string; tips?: string[]; } export class NetworkError extends ImperativeError { - public constructor(msg: string, public info?: NetworkErrorInfo) { - super({ msg }); + public constructor(public info: NetworkErrorInfo) { + super({ msg: info.summary }); } } @@ -94,18 +95,23 @@ export class ErrorCorrelator extends Singleton { public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): NetworkError { if (!this.errorMatches.has(profileType)) { - return new NetworkError(errorDetails); + return new NetworkError({ summary: errorDetails }); } for (const apiError of [...this.errorMatches.get(profileType)[api], ...(this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? [])]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { if (errorDetails.match(match)) { - return new NetworkError(apiError.summary, { errorCode: apiError.errorCode, fullError: errorDetails, tips: apiError?.tips }); + return new NetworkError({ + errorCode: apiError.errorCode, + fullError: errorDetails, + summary: apiError.summary, + tips: apiError?.tips, + }); } } } - return new NetworkError(errorDetails); + return new NetworkError({ summary: errorDetails }); } public async translateAndDisplayError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): Promise { @@ -115,7 +121,9 @@ export class ErrorCorrelator extends Singleton { }); if (userSelection === "More info" && error.info?.fullError) { - Gui.errorMessage(error.info.fullError); + return Gui.errorMessage(error.info.fullError, { + items: ["Troubleshoot"], + }); } return userSelection; diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 0b9483247..32fe7d1a1 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -394,7 +394,18 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { - await ErrorCorrelator.getInstance().translateAndDisplayError(ZoweExplorerApiType.Uss, entry.metadata.profile.type, err.message); + const userSelection = await ErrorCorrelator.getInstance().translateAndDisplayError( + ZoweExplorerApiType.Uss, + entry.metadata.profile.type, + err.message + ); + + switch (userSelection) { + case "Retry": + break; + case "Troubleshoot": + break; + } } throw err; } diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx new file mode 100644 index 000000000..4ee7ff5da --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx @@ -0,0 +1,44 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { useEffect, useState } from "preact/hooks"; +import { JSXInternal } from "preact/src/jsx"; +import { isSecureOrigin } from "../utils"; +import { ErrorInfo, ErrorInfoProps, isNetworkErrorInfo } from "./components/ErrorInfo"; + +export function App(): JSXInternal.Element { + const [errorInfo, setErrorInfo] = useState(); + + useEffect(() => { + window.addEventListener("message", (event) => { + if (!isSecureOrigin(event.origin)) { + return; + } + + if (!event.data) { + return; + } + + const errorInfo = event.data["error"]; + + if (isNetworkErrorInfo(errorInfo)) { + setErrorInfo(errorInfo); + } + }); + }, []); + + return ( +
+

Troubleshooting

+ {errorInfo ? : null} +
+ ); +} diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx new file mode 100644 index 000000000..d9d596345 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -0,0 +1,34 @@ +import { VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; +import { NetworkError } from "@zowe/zowe-explorer-api"; + +export type ErrorInfoProps = { + error: NetworkError; +}; + +export const isNetworkErrorInfo = (val: any): val is ErrorInfoProps => { + return val?.["info"] != null && val.info["summary"] != null; +}; + +const TipList = ({ tips }: { tips: string[] }) => { + return ( +
+

Tips

+
    + {tips.map((tip) => ( +
  • {tip}
  • + ))} +
+
+ ); +}; + +export const ErrorInfo = ({ error }: ErrorInfoProps) => { + return ( +
+ {error.info.summary} + Error Details + + {error.info.tips ? : null} +
+ ); +}; diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html new file mode 100644 index 000000000..9beb19e39 --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.html @@ -0,0 +1,16 @@ + + + + + + + + Troubleshoot Error + + +
+ + + diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx new file mode 100644 index 000000000..748009dcd --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/index.tsx @@ -0,0 +1,15 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { render } from "preact"; +import { App } from "./App"; + +render(, document.getElementById("webviewRoot")!); From a9ec1b77c7b3f5f67e04eaf6a7ac6997741dbcf1 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 27 Sep 2024 09:40:07 -0400 Subject: [PATCH 04/62] feat: Troubleshoot webview, move PersistentVSCodeAPI Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 94 ++++++++++++++++--- .../src/trees/shared/SharedInit.ts | 13 +++ .../src/trees/uss/UssFSProvider.ts | 10 +- .../src/utils/TroubleshootError.ts | 50 ++++++++++ .../components => }/PersistentVSCodeAPI.ts | 0 .../src/webviews/src/edit-history/App.tsx | 2 +- .../PersistentTable/PersistentDataPanel.tsx | 2 +- .../PersistentAddNewHistoryItemButton.tsx | 2 +- .../PersistentClearAllButton.tsx | 2 +- .../PersistentDeleteSelectedButton.tsx | 2 +- .../PersistentRefreshButton.tsx | 2 +- .../webviews/src/troubleshoot-error/App.tsx | 8 +- .../components/ErrorInfo.tsx | 38 +++++++- 13 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 packages/zowe-explorer/src/utils/TroubleshootError.ts rename packages/zowe-explorer/src/webviews/src/{edit-history/components => }/PersistentVSCodeAPI.ts (100%) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index e41beb1d9..6f149cd01 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -12,7 +12,11 @@ import { ImperativeError } from "@zowe/imperative"; import { Singleton } from "./Singleton"; import { Gui } from "../globals"; +import { commands } from "vscode"; +/** + * Error match type (substring of error, or regular expression to match against error text) + */ type ErrorMatch = string | RegExp; interface ErrorCorrelation { @@ -38,13 +42,19 @@ interface ErrorCorrelation { tips?: string[]; } -interface NetworkErrorInfo { - errorCode?: string; +interface NetworkErrorInfo extends Omit { + /** + * The full error details sent by the server. + * @type {string} + */ fullError?: string; - summary: string; - tips?: string[]; } +/** + * Network error wrapper around the `ImperativeError` class. + * + * Used to cache the error info such as tips, the match that was encountered and the full error message. + */ export class NetworkError extends ImperativeError { public constructor(public info: NetworkErrorInfo) { super({ msg: info.summary }); @@ -62,8 +72,6 @@ export enum ZoweExplorerApiType { export type ApiErrors = Partial>; -export class ErrorCorrelations {} - export class ErrorCorrelator extends Singleton { private errorMatches: Map = new Map([ [ @@ -71,18 +79,25 @@ export class ErrorCorrelator extends Singleton { { [ZoweExplorerApiType.Mvs]: [ { - errorCode: "403", + errorCode: "500", matches: [/Client is not authorized for file access\.$/], summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", - tips: [], + tips: [ + "Check that your user or group has the appropriate permissions for this data set.", + "Ensure that the data set is not opened within a mainframe editor tool.", + ], }, ], [ZoweExplorerApiType.Uss]: [ { - errorCode: "403", + errorCode: "500", matches: [/Client is not authorized for file access\.$/], summary: "Insufficient write permissions for this file. The file may be read-only or locked.", - tips: [], + tips: [ + "Check that your user or group has the appropriate permissions for this file.", + "Ensure that the file is not in use and locked by another process on the mainframe.", + "Consider using the Edit Attributes feature with this file to update its permissions.", + ], }, ], }, @@ -93,6 +108,29 @@ export class ErrorCorrelator extends Singleton { super(); } + /** + * Adds a new error correlation to the map of error matches. + * + * @param api The API type that corresponds with the error + * @param profileType A profile type that the error occurs within + * @param correlation The correlation info (summary, tips, etc.) + */ + public addCorrelation(api: ZoweExplorerApiType, profileType: string, correlation: ErrorCorrelation): void { + const existingMatches = this.errorMatches.get(profileType); + this.errorMatches.set(profileType, { + ...(existingMatches ?? {}), + [api]: [...(existingMatches?.[api] ?? []), correlation].filter(Boolean), + }); + } + + /** + * Attempt to correlate the error details to an error contributed to the `errorMatches` map. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary + */ public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): NetworkError { if (!this.errorMatches.has(profileType)) { return new NetworkError({ summary: errorDetails }); @@ -114,16 +152,44 @@ export class ErrorCorrelator extends Singleton { return new NetworkError({ summary: errorDetails }); } - public async translateAndDisplayError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): Promise { + /** + * Translates a detailed error message to a user-friendly summary. + * Full error details are available through the "More info" dialog option. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @param allowRetry Whether to allow retrying the action + * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") + */ + public async translateAndDisplayError( + api: ZoweExplorerApiType, + profileType: string, + errorDetails: string, + opts?: { allowRetry?: boolean; stackTrace?: string } + ): Promise { const error = this.correlateError(api, profileType, errorDetails); const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} (Error Code ${error.info?.errorCode})`, { - items: ["Retry", "More info"], + items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), }); + // If the user selected "More info", show the full error details in a dialog, + // containing "Show log" and "Troubleshoot" dialog options if (userSelection === "More info" && error.info?.fullError) { - return Gui.errorMessage(error.info.fullError, { - items: ["Troubleshoot"], + const secondDialogSelection = await Gui.errorMessage(error.info.fullError, { + items: ["Show log", "Troubleshoot"], }); + + switch (secondDialogSelection) { + // Reveal the output channel when the "Show log" option is selected + case "Show log": + return commands.executeCommand("zowe.revealOutputChannel"); + // Show the troubleshooting webview when the "Troubleshoot" option is selected + case "Troubleshoot": + return commands.executeCommand("zowe.troubleshootError", error, opts?.stackTrace); + default: + return; + } } return userSelection; diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index 08ddcfce8..cfc2ea097 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -13,6 +13,7 @@ import * as vscode from "vscode"; import { FileManagement, Gui, + NetworkError, IZoweTree, IZoweTreeNode, TableViewProvider, @@ -46,6 +47,7 @@ import { SharedContext } from "./SharedContext"; import { TreeViewUtils } from "../../utils/TreeViewUtils"; import { CertificateWizard } from "../../utils/CertificateWizard"; import { ZosConsoleViewProvider } from "../../zosconsole/ZosConsolePanel"; +import { TroubleshootError } from "../../utils/TroubleshootError"; export class SharedInit { public static registerRefreshCommand( @@ -296,6 +298,17 @@ export class SharedInit { return LocalFileManagement.fileSelectedToCompare; }) ); + context.subscriptions.push( + vscode.commands.registerCommand("zowe.revealOutputChannel", (): void => { + ZoweLogger.zeOutputChannel.show(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + "zowe.troubleshootError", + (error: NetworkError, stackTrace?: string) => new TroubleshootError(context, { error, stackTrace }) + ) + ); context.subscriptions.push( vscode.commands.registerCommand("zowe.placeholderCommand", () => { // This command does nothing, its here to let us disable individual items in the tree view diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 32fe7d1a1..57a38cb5f 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -394,17 +394,19 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { + ZoweLogger.error(err.message); const userSelection = await ErrorCorrelator.getInstance().translateAndDisplayError( ZoweExplorerApiType.Uss, entry.metadata.profile.type, - err.message + err.message, + { allowRetry: true, stackTrace: err.stack } ); switch (userSelection) { case "Retry": - break; - case "Troubleshoot": - break; + return this.uploadEntry(entry, content, options); + default: + throw err; } } throw err; diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts new file mode 100644 index 000000000..278bc2d03 --- /dev/null +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -0,0 +1,50 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { NetworkError, WebView } from "@zowe/zowe-explorer-api"; +import { ExtensionContext, l10n } from "vscode"; + +type TroubleshootData = { + error: NetworkError; + stackTrace?: string; +}; + +export class TroubleshootError extends WebView { + public constructor(context: ExtensionContext, public errorData: TroubleshootData) { + super(l10n.t("Troubleshoot Error"), "troubleshoot-error", context, { + onDidReceiveMessage: (message: object) => this.onDidReceiveMessage(message), + }); + } + + public async onDidReceiveMessage(message: object): Promise { + if (!("command" in message)) { + return; + } + + switch (message.command) { + case "ready": + await this.setErrorData(this.errorData); + } + } + + /** + * Propagate error data to the webview + * + * @param errorData Error and stack trace + * @returns Whether Zowe Explorer successfully sent the data to the webview + */ + public async setErrorData(errorData: TroubleshootData): Promise { + return this.panel.webview.postMessage({ + error: errorData.error, + stackTrace: errorData.stackTrace, + }); + } +} diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts b/packages/zowe-explorer/src/webviews/src/PersistentVSCodeAPI.ts similarity index 100% rename from packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentVSCodeAPI.ts rename to packages/zowe-explorer/src/webviews/src/PersistentVSCodeAPI.ts diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx index 592923663..bf49e28c5 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx @@ -14,7 +14,7 @@ import { VSCodeDivider, VSCodePanels, VSCodePanelTab } from "@vscode/webview-ui- import { JSXInternal } from "preact/src/jsx"; import { isSecureOrigin } from "../utils"; import PersistentDataPanel from "./components/PersistentTable/PersistentDataPanel"; -import PersistentVSCodeAPI from "./components/PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI" import PersistentManagerHeader from "./components/PersistentManagerHeader/PersistentManagerHeader"; export function App(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx index 675793884..eb10885ca 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentTable/PersistentDataPanel.tsx @@ -18,7 +18,7 @@ import { panelId } from "../../types"; import PersistentToolBar from "../PersistentToolBar/PersistentToolBar"; import PersistentTableData from "./PersistentTableData"; import PersistentDataGridHeaders from "./PersistentDataGridHeaders"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; export default function PersistentDataPanel({ type }: Readonly<{ type: Readonly }>): JSXInternal.Element { const [data, setData] = useState<{ [type: string]: { [property: string]: string[] } }>({ ds: {}, uss: {}, jobs: {} }); diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx index cf9628d45..87905863a 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentAddNewHistoryItemButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as nls from "@vscode/l10n"; export default function PersistentAddNewHistoryItemButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx index 943fd88d7..a42c487f1 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentClearAllButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as nls from "@vscode/l10n"; export default function PersistentClearAllButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx index 3299e683c..cb6ab88cf 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentDeleteSelectedButton.tsx @@ -1,7 +1,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as nls from "@vscode/l10n"; export default function PersistentDeleteSelectedButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx index 5e6b99eb6..c44ddcd4c 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/components/PersistentToolBar/PersistentRefreshButton.tsx @@ -12,7 +12,7 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; import { JSXInternal } from "preact/src/jsx"; import { useDataPanelContext } from "../PersistentUtils"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; +import PersistentVSCodeAPI from "../../../PersistentVSCodeAPI"; import * as nls from "@vscode/l10n"; export default function PersistentRefreshButton(): JSXInternal.Element { diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx index 4ee7ff5da..a04051eb5 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx @@ -12,7 +12,8 @@ import { useEffect, useState } from "preact/hooks"; import { JSXInternal } from "preact/src/jsx"; import { isSecureOrigin } from "../utils"; -import { ErrorInfo, ErrorInfoProps, isNetworkErrorInfo } from "./components/ErrorInfo"; +import { ErrorInfo, ErrorInfoProps, isNetworkError } from "./components/ErrorInfo"; +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; export function App(): JSXInternal.Element { const [errorInfo, setErrorInfo] = useState(); @@ -29,10 +30,11 @@ export function App(): JSXInternal.Element { const errorInfo = event.data["error"]; - if (isNetworkErrorInfo(errorInfo)) { - setErrorInfo(errorInfo); + if (isNetworkError(errorInfo)) { + setErrorInfo({ error: errorInfo, stackTrace: event.data?.stackTrace }); } }); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ command: "ready" }); }, []); return ( diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index d9d596345..6a5cf6169 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -3,9 +3,10 @@ import { NetworkError } from "@zowe/zowe-explorer-api"; export type ErrorInfoProps = { error: NetworkError; + stackTrace?: string; }; -export const isNetworkErrorInfo = (val: any): val is ErrorInfoProps => { +export const isNetworkError = (val: any): val is NetworkError => { return val?.["info"] != null && val.info["summary"] != null; }; @@ -22,13 +23,42 @@ const TipList = ({ tips }: { tips: string[] }) => { ); }; -export const ErrorInfo = ({ error }: ErrorInfoProps) => { +export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { return (
- {error.info.summary} - Error Details +

Error details

+

+ Code: + {error.info.errorCode ?? "Not available"} +

+

+ + Description:
+
+ {error.info.summary} +

+ + Full error message + {error.info.tips ? : null} + +

Additional resources

+
); }; From 2b1083cf588982bdd45a47948721de0e749216ee Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 1 Oct 2024 14:45:08 -0400 Subject: [PATCH 05/62] refactor: Move TipList into component file, troubleshoot format Signed-off-by: Trae Yelovich --- .../components/ErrorInfo.tsx | 22 ++++++------------- .../troubleshoot-error/components/TipList.tsx | 12 ++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 6a5cf6169..65d941609 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -1,5 +1,6 @@ import { VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; import { NetworkError } from "@zowe/zowe-explorer-api"; +import { TipList } from "./TipList"; export type ErrorInfoProps = { error: NetworkError; @@ -10,19 +11,6 @@ export const isNetworkError = (val: any): val is NetworkError => { return val?.["info"] != null && val.info["summary"] != null; }; -const TipList = ({ tips }: { tips: string[] }) => { - return ( -
-

Tips

-
    - {tips.map((tip) => ( -
  • {tip}
  • - ))} -
-
- ); -}; - export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { return (
@@ -38,7 +26,7 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { {error.info.summary}

- Full error message + Full error message: {error.info.tips ? : null} @@ -46,8 +34,12 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {

Additional resources

  • - Open Mainframe Project Slack + GitHub +
  • +
  • + Slack: Open Mainframe Project
  • +
  • Zowe Docs
  • diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx new file mode 100644 index 000000000..bf8ce815d --- /dev/null +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/TipList.tsx @@ -0,0 +1,12 @@ +export const TipList = ({ tips }: { tips: string[] }) => { + return ( +
    +

    Tips

    +
      + {tips.map((tip) => ( +
    • {tip}
    • + ))} +
    +
    + ); +}; From 3a414ffa19c820aeedc70de6ec68f9a16f965365 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 3 Oct 2024 16:19:10 -0400 Subject: [PATCH 06/62] wip: set up test cases and rename function Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 28 +++++++++++++++++++ .../src/utils/ErrorCorrelator.ts | 2 +- .../src/trees/uss/UssFSProvider.ts | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts new file mode 100644 index 000000000..5c549a77c --- /dev/null +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -0,0 +1,28 @@ +import { ErrorCorrelator, ZoweExplorerApiType } from "../../../src/"; + +describe("addCorrelation", () => { + it("adds a correlation for the given API and profile type", () => { + const fakeErrorSummary = "Example error summary for the correlator"; + ErrorCorrelator.getInstance().addCorrelation(ZoweExplorerApiType.Mvs, "zosmf", { + errorCode: "403", + summary: fakeErrorSummary, + matches: ["Specific sequence 1234 encountered"], + }); + expect( + (ErrorCorrelator.getInstance() as any).errorMatches.get("zosmf")[ZoweExplorerApiType.Mvs].find((err) => err.summary === fakeErrorSummary) + ).not.toBe(null); + }); +}); + +describe("correlateError", () => { + it("correctly correlates an error in the list of error matches", () => {}); + it("returns a generic NetworkError with the full error details if no matches are found", () => {}); +}); + +describe("displayError", () => { + it("calls correlateError to get an error correlation", () => {}); + it("presents an additional dialog when the user selects 'More info'", () => {}); + it("opens the Zowe Explorer output channel when the user selects 'Show log'", () => {}); + it("opens the troubleshoot webview if the user selects 'Troubleshoot'", () => {}); + it("returns 'Retry' whenever the user selects 'Retry'", () => {}); +}); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 6f149cd01..3b4a411a6 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -162,7 +162,7 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async translateAndDisplayError( + public async displayError( api: ZoweExplorerApiType, profileType: string, errorDetails: string, diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 57a38cb5f..6256c2980 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -395,7 +395,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().translateAndDisplayError( + const userSelection = await ErrorCorrelator.getInstance().displayError( ZoweExplorerApiType.Uss, entry.metadata.profile.type, err.message, From 3f0eff1b40c98d328b238f85b05628782574500c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 7 Oct 2024 10:05:37 -0400 Subject: [PATCH 07/62] tests: impl ErrorCorrelator.displayError test cases Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 75 +++++++++++++++++-- .../src/utils/ErrorCorrelator.ts | 3 +- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 5c549a77c..cc507e025 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -1,4 +1,16 @@ -import { ErrorCorrelator, ZoweExplorerApiType } from "../../../src/"; +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + +import { ErrorCorrelator, Gui, NetworkError, ZoweExplorerApiType } from "../../../src/"; +import { commands } from "vscode"; describe("addCorrelation", () => { it("adds a correlation for the given API and profile type", () => { @@ -20,9 +32,60 @@ describe("correlateError", () => { }); describe("displayError", () => { - it("calls correlateError to get an error correlation", () => {}); - it("presents an additional dialog when the user selects 'More info'", () => {}); - it("opens the Zowe Explorer output channel when the user selects 'Show log'", () => {}); - it("opens the troubleshoot webview if the user selects 'Troubleshoot'", () => {}); - it("returns 'Retry' whenever the user selects 'Retry'", () => {}); + it("calls correlateError to get an error correlation", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error" })); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + }); + it("presents an additional dialog when the user selects 'More info'", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce(undefined); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + }); + it("opens the Zowe Explorer output channel when the user selects 'Show log'", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Show log"); + const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.revealOutputChannel"); + executeCommandMock.mockRestore(); + }); + it("opens the troubleshoot webview if the user selects 'Troubleshoot'", async () => { + const networkError = new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" }); + const correlateErrorMock = jest.spyOn(ErrorCorrelator.prototype, "correlateError").mockReturnValueOnce(networkError); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Troubleshoot"); + const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); + expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", networkError, undefined); + executeCommandMock.mockRestore(); + }); + it("returns 'Retry' whenever the user selects 'Retry'", async () => { + const correlateErrorMock = jest + .spyOn(ErrorCorrelator.prototype, "correlateError") + .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Retry"); + const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details", { + allowRetry: true, + }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["Retry", "More info"] }); + expect(userResponse).toBe("Retry"); + }); }); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 3b4a411a6..c75c8bdc9 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -169,7 +169,8 @@ export class ErrorCorrelator extends Singleton { opts?: { allowRetry?: boolean; stackTrace?: string } ): Promise { const error = this.correlateError(api, profileType, errorDetails); - const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} (Error Code ${error.info?.errorCode})`, { + const errorCodeStr = error.info?.errorCode ? `(Error Code ${error.info.errorCode})` : ""; + const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), }); From 590c1246f7ac9e305ea455876d8771afde9f7879 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 7 Oct 2024 10:15:28 -0400 Subject: [PATCH 08/62] tests: ErrorCorrelator.correlateError test cases Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index cc507e025..5bdefb504 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -27,8 +27,30 @@ describe("addCorrelation", () => { }); describe("correlateError", () => { - it("correctly correlates an error in the list of error matches", () => {}); - it("returns a generic NetworkError with the full error details if no matches are found", () => {}); + it("correctly correlates an error in the list of error matches", () => { + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "zosmf", "Client is not authorized for file access.") + ).toStrictEqual( + new NetworkError({ + errorCode: "500", + summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + tips: [ + "Check that your user or group has the appropriate permissions for this data set.", + "Ensure that the data set is not opened within a mainframe editor tool.", + ], + }) + ); + }); + it("returns a generic NetworkError if no matches are available for the given profile type", () => { + expect(ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "nonsense", "Some error details")).toStrictEqual( + new NetworkError({ summary: "Some error details" }) + ); + }); + it("returns a generic NetworkError with the full error details if no matches are found", () => { + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "zosmf", "A cryptic error with no available match") + ).toStrictEqual(new NetworkError({ summary: "A cryptic error with no available match" })); + }); }); describe("displayError", () => { From b44ce2d6e555633a9a625db5a1539a474f5a0935 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 7 Oct 2024 15:45:05 -0400 Subject: [PATCH 09/62] wip: Add more error correlations; test data set error handling Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 30 +++++++++++++++++-- .../src/trees/dataset/DatasetFSProvider.ts | 19 ++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index c75c8bdc9..65a56cc96 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -80,18 +80,23 @@ export class ErrorCorrelator extends Singleton { [ZoweExplorerApiType.Mvs]: [ { errorCode: "500", - matches: [/Client is not authorized for file access\.$/], + matches: ["Client is not authorized for file access.", /An I\/O abend was trapped\.(.+?)\n(.+?)__code=0x0913/], summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", tips: [ "Check that your user or group has the appropriate permissions for this data set.", "Ensure that the data set is not opened within a mainframe editor tool.", ], }, + { + matches: ["ISPF LMINIT - data set not found."], + summary: "The specified data set cannot be found. Perhaps the data set name or member name was incorrectly specified.", + tips: ["Ensure that the data set and/or member name is correct and try again."], + }, ], [ZoweExplorerApiType.Uss]: [ { errorCode: "500", - matches: [/Client is not authorized for file access\.$/], + matches: ["Client is not authorized for file access."], summary: "Insufficient write permissions for this file. The file may be read-only or locked.", tips: [ "Check that your user or group has the appropriate permissions for this file.", @@ -99,6 +104,27 @@ export class ErrorCorrelator extends Singleton { "Consider using the Edit Attributes feature with this file to update its permissions.", ], }, + { + matches: ["File not found."], + summary: "The specified UNIX file cannot be found. Perhaps the folder or file path was incorrectly specified.", + tips: ["Ensure that the UNIX folder or file path is correct and try again."], + }, + ], + [ZoweExplorerApiType.Jes]: [ + { + matches: ["No job found for reference:"], + summary: "The job modification request specified a job that does not exist.", + tips: [], + }, + { + matches: ["Submit input data does not start with a slash"], + summary: "The first character for the submitted job is invalid - expected a slash.", + tips: ["Ensure that the input data set or file contains EBCDIC data"], + }, + { + matches: ["Job input was not recognized by system as a job"], + summary: "The job was submitted without a job statement or with unrecognized (non-JCL) content.", + }, ], }, ], diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index bf6ddc145..2f32db2c1 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -26,6 +26,9 @@ import { ZoweScheme, UriFsInfo, FileEntry, + imperative, + ErrorCorrelator, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; @@ -440,6 +443,22 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem }); } catch (err) { statusMsg.dispose(); + if (err instanceof imperative.ImperativeError) { + ZoweLogger.error(err.message); + const userSelection = await ErrorCorrelator.getInstance().displayError( + ZoweExplorerApiType.Mvs, + entry.metadata.profile.type, + err.message, + { allowRetry: true, stackTrace: err.stack } + ); + + switch (userSelection) { + case "Retry": + return this.uploadEntry(parent, entry, content, forceUpload); + default: + throw err; + } + } throw err; } statusMsg.dispose(); From 4dc0a25f4df1ed1f541994747e38e7ada262a53f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 7 Oct 2024 16:40:14 -0400 Subject: [PATCH 10/62] wip(ErrorCorrelator): collapsible error section, Copy Details btn Signed-off-by: Trae Yelovich --- .../components/ErrorInfo.tsx | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 65d941609..a5fc78e3f 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -1,6 +1,7 @@ -import { VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; +import { VSCodeButton, VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; import { NetworkError } from "@zowe/zowe-explorer-api"; import { TipList } from "./TipList"; +import { useState } from "preact/hooks"; export type ErrorInfoProps = { error: NetworkError; @@ -12,6 +13,7 @@ export const isNetworkError = (val: any): val is NetworkError => { }; export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { + const [errorDisplayed, setErrorDisplayed] = useState(false); return (

    Error details

    @@ -25,9 +27,32 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { {error.info.summary}

    - - Full error message: - +
    + setErrorDisplayed((prev) => !prev)} + > + + + {errorDisplayed ? ( + + ) : ( + + )} +   Full error summary + + + Copy details + + + + +
    {error.info.tips ? : null} From 0a2d4fa9a41f349bd0c11a773f95d6a9846e6c74 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 8 Oct 2024 09:35:29 -0400 Subject: [PATCH 11/62] copy details button, fix summary toggle state Signed-off-by: Trae Yelovich --- .../zowe-explorer/src/utils/TroubleshootError.ts | 15 ++++++++++++++- .../troubleshoot-error/components/ErrorInfo.tsx | 13 ++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index 278bc2d03..661f29e60 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -10,7 +10,7 @@ */ import { NetworkError, WebView } from "@zowe/zowe-explorer-api"; -import { ExtensionContext, l10n } from "vscode"; +import { env, ExtensionContext, l10n } from "vscode"; type TroubleshootData = { error: NetworkError; @@ -32,6 +32,19 @@ export class TroubleshootError extends WebView { switch (message.command) { case "ready": await this.setErrorData(this.errorData); + break; + case "copy": + await env.clipboard.writeText( + this.errorData.error.stack + ? `Error details:\n\n${this.errorData.error.info.fullError}\n\nStack trace:\n${this.errorData.error.stack.replace( + /(.+?)\n/, + "" + )}` + : `Error details:\n\n${this.errorData.error.info.fullError}\n\n(No stack trace available)` + ); + break; + default: + break; } } diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index a5fc78e3f..2db6058f3 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -2,6 +2,7 @@ import { VSCodeButton, VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui- import { NetworkError } from "@zowe/zowe-explorer-api"; import { TipList } from "./TipList"; import { useState } from "preact/hooks"; +import PersistentVSCodeAPI from "../../PersistentVSCodeAPI"; export type ErrorInfoProps = { error: NetworkError; @@ -42,7 +43,17 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {   Full error summary - Copy details + { + e.stopImmediatePropagation(); + PersistentVSCodeAPI.getVSCodeAPI().postMessage({ + command: "copy", + }); + }} + > + Copy details + From 0c490f6e0361af38e790e69c891d3132c03f5e8c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 8 Oct 2024 12:22:15 -0400 Subject: [PATCH 12/62] update copied content for copy details button Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/utils/TroubleshootError.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index 661f29e60..f1d16d4bc 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -36,11 +36,8 @@ export class TroubleshootError extends WebView { case "copy": await env.clipboard.writeText( this.errorData.error.stack - ? `Error details:\n\n${this.errorData.error.info.fullError}\n\nStack trace:\n${this.errorData.error.stack.replace( - /(.+?)\n/, - "" - )}` - : `Error details:\n\n${this.errorData.error.info.fullError}\n\n(No stack trace available)` + ? `Error details:\n${this.errorData.error.info.fullError}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` + : `Error details:\n${this.errorData.error.info.fullError}` ); break; default: From dbfa1f36c470e669e3bdd68356fa46cc054651a7 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 9 Oct 2024 15:42:23 -0400 Subject: [PATCH 13/62] feat: support template args in error summaries Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 65a56cc96..de342beec 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -13,6 +13,7 @@ import { ImperativeError } from "@zowe/imperative"; import { Singleton } from "./Singleton"; import { Gui } from "../globals"; import { commands } from "vscode"; +import Mustache = require("mustache"); /** * Error match type (substring of error, or regular expression to match against error text) @@ -126,6 +127,22 @@ export class ErrorCorrelator extends Singleton { summary: "The job was submitted without a job statement or with unrecognized (non-JCL) content.", }, ], + [ZoweExplorerApiType.All]: [ + { + errorCode: "401", + matches: ["Token is not valid or expired"], + summary: + // eslint-disable-next-line max-len + "Your connection is no longer active for profile {{profileName}}. Please log in to an authentication service to restore the connection.", + }, + { + errorCode: "401", + matches: ["Username or password are not valid or expired", "All configured authentication methods failed"], + summary: + // eslint-disable-next-line max-len + "Invalid credentials for profile {{profileName}}. Please ensure the username and password are valid or this may lead to a lock-out.", + }, + ], }, ], ]); @@ -157,18 +174,21 @@ export class ErrorCorrelator extends Singleton { * @param errorDetails The full error details (usually `error.message`) * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary */ - public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string): NetworkError { + public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string, templateArgs?: Record): NetworkError { if (!this.errorMatches.has(profileType)) { return new NetworkError({ summary: errorDetails }); } - for (const apiError of [...this.errorMatches.get(profileType)[api], ...(this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? [])]) { + for (const apiError of [ + ...this.errorMatches.get(profileType)[api], + ...(api === ZoweExplorerApiType.All ? [] : this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? []), + ]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { if (errorDetails.match(match)) { return new NetworkError({ errorCode: apiError.errorCode, fullError: errorDetails, - summary: apiError.summary, + summary: templateArgs ? Mustache.render(apiError.summary, templateArgs) : apiError.summary, tips: apiError?.tips, }); } From 0f42997e32c0774bdc08105e9ae31e8b8c5227b6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 9 Oct 2024 15:46:28 -0400 Subject: [PATCH 14/62] wip: update AuthUtils.errorHandling to use correlator Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/utils/AuthUtils.ts | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index cbfcabbeb..6a64a3889 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -11,7 +11,7 @@ import * as util from "util"; import * as vscode from "vscode"; -import { imperative, Gui, MainframeInteraction, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ZoweLogger } from "../tools/ZoweLogger"; @@ -22,7 +22,7 @@ export class AuthUtils { * @param {label} - additional information such as profile name, credentials, messageID etc * @param {moreInfo} - additional/customized error messages *************************************************************************************************************/ - public static async errorHandling(errorDetails: Error | string, label?: string, moreInfo?: string): Promise { + public static async errorHandling(errorDetails: Error | string, label?: string, moreInfo?: string | imperative.IProfileLoaded): Promise { // Use util.inspect instead of JSON.stringify to handle circular references // eslint-disable-next-line @typescript-eslint/restrict-template-expressions ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null })); @@ -45,18 +45,9 @@ export class AuthUtils { httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || imperativeError.message.includes("All configured authentication methods failed") ) { - const errMsg = vscode.l10n.t({ - message: - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - args: [label], - comment: ["Label"], - }); - const errToken = vscode.l10n.t({ - message: - // eslint-disable-next-line max-len - "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", - args: [label], - comment: ["Label"], + const profile = await Constants.PROFILES_CACHE.loadNamedProfile(label); + const correlation = ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.All, profile.type, imperativeError.message, { + profileName: label, }); if (label.includes("[")) { label = label.substring(0, label.indexOf(" [")).trim(); @@ -68,7 +59,7 @@ export class AuthUtils { if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { const message = vscode.l10n.t("Log in to Authentication Service"); - Gui.showMessage(errToken, { items: [message] }).then(async (selection) => { + Gui.showMessage(correlation.message, { items: [message] }).then(async (selection) => { if (selection) { await Constants.PROFILES_CACHE.ssoLogin(null, label); } @@ -77,7 +68,7 @@ export class AuthUtils { } } const checkCredsButton = vscode.l10n.t("Update Credentials"); - await Gui.errorMessage(errMsg, { + await Gui.errorMessage(correlation.message, { items: [checkCredsButton], vsCodeOpts: { modal: true }, }).then(async (selection) => { @@ -93,13 +84,9 @@ export class AuthUtils { if (errorDetails.toString().includes("Could not find profile")) { return; } - if (moreInfo === undefined) { - moreInfo = errorDetails.toString().includes("Error") ? "" : "Error: "; - } else { - moreInfo += " "; - } + // Try to keep message readable since VS Code doesn't support newlines in error messages - Gui.errorMessage(moreInfo + errorDetails.toString().replace(/\n/g, " | ")); + Gui.errorMessage(errorDetails.toString().replace(/\n/g, " | ")); } /** From 5c5e963945a8937929fe638186212a5ae3f84df3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 10 Oct 2024 16:34:25 -0400 Subject: [PATCH 15/62] wip: update AuthUtils.errorHandling signature and update calls Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 2 +- .../__mocks__/mockUtils.ts | 11 ++ .../src/commands/MvsCommandHandler.ts | 6 +- .../src/commands/TsoCommandHandler.ts | 6 +- .../src/commands/UnixCommandHandler.ts | 7 +- .../src/configuration/Profiles.ts | 6 +- .../src/trees/dataset/DatasetActions.ts | 27 ++-- .../src/trees/dataset/DatasetTree.ts | 4 +- .../src/trees/dataset/ZoweDatasetNode.ts | 7 +- .../zowe-explorer/src/trees/job/JobActions.ts | 6 +- .../zowe-explorer/src/trees/job/JobTree.ts | 2 +- .../src/trees/job/ZoweJobNode.ts | 10 +- .../zowe-explorer/src/trees/uss/USSActions.ts | 8 +- .../zowe-explorer/src/trees/uss/USSTree.ts | 4 +- .../src/trees/uss/ZoweUSSNode.ts | 14 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 122 +++++++++++------- .../zowe-explorer/src/utils/ProfilesUtils.ts | 2 +- 17 files changed, 147 insertions(+), 97 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index de342beec..8eaa6fde2 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -20,7 +20,7 @@ import Mustache = require("mustache"); */ type ErrorMatch = string | RegExp; -interface ErrorCorrelation { +export interface ErrorCorrelation { /** * An optional error code returned from the server. * @type {string} diff --git a/packages/zowe-explorer-ftp-extension/__mocks__/mockUtils.ts b/packages/zowe-explorer-ftp-extension/__mocks__/mockUtils.ts index 250c61e8c..06a03e3e0 100644 --- a/packages/zowe-explorer-ftp-extension/__mocks__/mockUtils.ts +++ b/packages/zowe-explorer-ftp-extension/__mocks__/mockUtils.ts @@ -1,2 +1,13 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + // Idea is borrowed from: https://github.com/kulshekhar/ts-jest/blob/master/src/util/testing.ts export const mocked = any>(fn: T): jest.Mock> => fn as any; diff --git a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts index b93823343..2dc2fda7b 100644 --- a/packages/zowe-explorer/src/commands/MvsCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/MvsCommandHandler.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { Validation, imperative, IZoweTreeNode, Gui } from "@zowe/zowe-explorer-api"; +import { Validation, imperative, IZoweTreeNode, Gui, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; import { ZoweLogger } from "../tools/ZoweLogger"; import { Profiles } from "../configuration/Profiles"; @@ -126,7 +126,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } @@ -215,7 +215,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { } } } catch (error) { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } this.history.addSearchHistory(command); } diff --git a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts index 2f380f2df..e799c297e 100644 --- a/packages/zowe-explorer/src/commands/TsoCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/TsoCommandHandler.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zostso from "@zowe/zos-tso-for-zowe-sdk"; -import { Gui, Validation, imperative, IZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { Gui, Validation, imperative, IZoweTreeNode, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; import { ZoweLogger } from "../tools/ZoweLogger"; import { Profiles } from "../configuration/Profiles"; @@ -134,7 +134,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } @@ -229,7 +229,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { ZoweLogger.error(message); Gui.errorMessage(message); } else { - await AuthUtils.errorHandling(error, profile.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } diff --git a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts index f9518df97..41ab43b05 100644 --- a/packages/zowe-explorer/src/commands/UnixCommandHandler.ts +++ b/packages/zowe-explorer/src/commands/UnixCommandHandler.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosuss from "@zowe/zos-uss-for-zowe-sdk"; import { ZoweCommandProvider } from "./ZoweCommandProvider"; -import { Gui, IZoweTreeNode, imperative } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweTreeNode, ZoweExplorerApiType, imperative } from "@zowe/zowe-explorer-api"; import { Profiles } from "../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../extending/ZoweExplorerApiRegister"; import { ZoweLogger } from "../tools/ZoweLogger"; @@ -165,7 +165,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { }) ); } else { - await AuthUtils.errorHandling(error, this.serviceProf.name); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: this.serviceProf }); } } } @@ -354,7 +354,6 @@ export class UnixCommandHandler extends ZoweCommandProvider { private async issueCommand(profile: imperative.IProfileLoaded, command: string, cwd: string): Promise { ZoweLogger.trace("UnixCommandHandler.issueCommand called."); - const profName = this.sshProfile !== undefined ? this.sshProfile.name : profile.name; try { if (command) { const user: string = profile.profile.user; @@ -379,7 +378,7 @@ export class UnixCommandHandler extends ZoweCommandProvider { this.serviceProf = undefined; } } catch (error) { - await AuthUtils.errorHandling(error, profName); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile }); } } } diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index de3fd1b59..778f7abb9 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -110,7 +110,7 @@ export class Profiles extends ProfilesCache { await Profiles.getInstance().ssoLogin(null, theProfile.name); theProfile = Profiles.getInstance().loadNamedProfile(theProfile.name); } catch (error) { - await AuthUtils.errorHandling(error, theProfile.name, error.message); + await AuthUtils.errorHandling(error, { profile: theProfile.name }); return profileStatus; } } else if (!usingTokenAuth && (!theProfile.profile.user || !theProfile.profile.password)) { @@ -123,7 +123,7 @@ export class Profiles extends ProfilesCache { try { values = await Profiles.getInstance().promptCredentials(theProfile); } catch (error) { - await AuthUtils.errorHandling(error, theProfile.name, error.message); + await AuthUtils.errorHandling(error, { profile: theProfile }); return profileStatus; } if (values) { @@ -748,7 +748,7 @@ export class Profiles extends ProfilesCache { comment: [`The profile name`], }) ); - await AuthUtils.errorHandling(error, theProfile.name); + await AuthUtils.errorHandling(error, { profile: theProfile }); filteredProfile = { status: "inactive", name: theProfile.name, diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts index ab8919fdd..cf93e6755 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts @@ -246,7 +246,7 @@ export class DatasetActions { const errorMsg = vscode.l10n.t("Error encountered when creating data set."); ZoweLogger.error(errorMsg + JSON.stringify(err)); if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), errorMsg); + await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: errorMsg }); } throw new Error(err); } @@ -382,7 +382,7 @@ export class DatasetActions { await ZoweExplorerApiRegister.getMvsApi(profile).allocateLikeDataSet(newDSName.toUpperCase(), likeDSName); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, newDSName, vscode.l10n.t("Unable to create data set.")); + await AuthUtils.errorHandling(err, { profile, dsName: newDSName, scenario: vscode.l10n.t("Unable to create data set.") }); } throw err; } @@ -438,7 +438,7 @@ export class DatasetActions { Gui.reportProgress(progress, value.length, index, "Uploading"); const response = await DatasetActions.uploadFile(node, item.fsPath); if (!response?.success) { - await AuthUtils.errorHandling(response?.commandResponse, node.getProfileName(), response?.commandResponse); + await AuthUtils.errorHandling(response?.commandResponse, { profile: node.getProfile() }); break; } index++; @@ -476,7 +476,7 @@ export class DatasetActions { responseTimeout: prof.profile?.responseTimeout, }); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { profile: node.getProfile() }); } } @@ -674,7 +674,7 @@ export class DatasetActions { }); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, label, vscode.l10n.t("Unable to create member.")); + await AuthUtils.errorHandling(err, { parentDsName: label, scenario: vscode.l10n.t("Unable to create member.") }); } throw err; } @@ -878,7 +878,7 @@ export class DatasetActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), vscode.l10n.t("Unable to list attributes.")); + await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: vscode.l10n.t("Unable to list attributes.") }); } throw err; } @@ -1012,7 +1012,7 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, sessProfileName, vscode.l10n.t("Job submission failed.")); + await AuthUtils.errorHandling(error, { profile: sessProfile, scenario: vscode.l10n.t("Job submission failed.") }); } } } else { @@ -1135,7 +1135,7 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, sesName, vscode.l10n.t("Job submission failed.")); + await AuthUtils.errorHandling(error, { profile: sessProfile, scenario: vscode.l10n.t("Job submission failed.") }); } } } @@ -1192,7 +1192,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { profile: node.getProfile() }); } throw err; } @@ -1276,7 +1276,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { profile: node.getProfile() }); } } } @@ -1293,7 +1293,7 @@ export class DatasetActions { await node.getChildren(); datasetProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { profile: node.getProfile() }); } } @@ -1712,7 +1712,10 @@ export class DatasetActions { } } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, DatasetUtils.getNodeLabels(node).dataSetName, vscode.l10n.t("Unable to copy data set.")); + await AuthUtils.errorHandling(error, { + dsName: DatasetUtils.getNodeLabels(node).dataSetName, + scenario: vscode.l10n.t("Unable to copy data set."), + }); } } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index b99db6fd3..629e03d3f 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -432,7 +432,7 @@ export class DatasetTree extends ZoweTreeProvider implemen if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { profile }); } } // Creates ZoweDatasetNode to track new session and pushes it to mSessionNodes @@ -1058,7 +1058,7 @@ export class DatasetTree extends ZoweTreeProvider implemen response = await this.getChildren(sessionNode); }); } catch (err) { - await AuthUtils.errorHandling(err, String(node.label)); + await AuthUtils.errorHandling(err, { profile: node.getProfile() }); } if (response.length === 0) { return; diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 527d27226..466b10039 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -589,7 +589,10 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod responses.push(await ZoweExplorerApiRegister.getMvsApi(profile).allMembers(this.label as string, options)); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from MVS list API")); + const updated = await AuthUtils.errorHandling(error, { + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from MVS list API"), + }); AuthUtils.syncSessionNode((prof) => ZoweExplorerApiRegister.getMvsApi(prof), this.getSessionNode(), updated && this); return; } @@ -627,7 +630,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod datasetProvider.addFileHistory(`[${this.getProfileName()}]: ${this.label as string}`); } } catch (err) { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { profile: this.getProfile() }); throw err; } } diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 920f21d80..b61d8199d 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -54,7 +54,7 @@ export class JobActions { }) ); } catch (error) { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { profile: job.getProfile() }); } } @@ -307,7 +307,7 @@ export class JobActions { vscode.l10n.t("jobActions.modifyCommand.apiNonExisting", "Not implemented yet for profile of type: ") + job.getProfile().type ); } else { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { profile: job.getProfile() }); } } } @@ -343,7 +343,7 @@ export class JobActions { }) ); } else { - await AuthUtils.errorHandling(error, job.getProfile().name); + await AuthUtils.errorHandling(error, { profile: job.getProfile() }); } } } diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index 0262ab49e..a7a31a960 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -193,7 +193,7 @@ export class JobTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 0daea1bab..4cfb377e1 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -396,7 +396,10 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { }, []); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from JES list API")); + const updated = await AuthUtils.errorHandling(error, { + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from JES list API"), + }); AuthUtils.syncSessionNode((profile) => ZoweExplorerApiRegister.getJesApi(profile), this.getSessionNode(), updated && this); return; } @@ -413,7 +416,10 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { // see an issue #845 for the details spools = spools.filter((item) => !(item.id === undefined && item.ddname === undefined && item.stepname === undefined)); } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from JES list API")); + const updated = await AuthUtils.errorHandling(error, { + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from JES list API"), + }); AuthUtils.syncSessionNode((profile) => ZoweExplorerApiRegister.getJesApi(profile), this.getSessionNode(), updated && this); return; } diff --git a/packages/zowe-explorer/src/trees/uss/USSActions.ts b/packages/zowe-explorer/src/trees/uss/USSActions.ts index 0f89d1be0..ba6cf8c26 100644 --- a/packages/zowe-explorer/src/trees/uss/USSActions.ts +++ b/packages/zowe-explorer/src/trees/uss/USSActions.ts @@ -99,7 +99,7 @@ export class USSActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, node.getProfileName(), vscode.l10n.t("Unable to create node:")); + await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: vscode.l10n.t("Unable to create node:") }); } throw err; } @@ -117,7 +117,7 @@ export class USSActions { await node.getChildren(); ussFileProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, node.getProfileName()); + await AuthUtils.errorHandling(err, { profile: node.getProfile() }); } } @@ -188,7 +188,7 @@ export class USSActions { const ussName = `${node.fullPath}/${localFileName}`; await ZoweExplorerApiRegister.getUssApi(node.getProfile()).putContent(filePath, ussName, { binary: true }); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { profile: node.getProfile() }); } } @@ -213,7 +213,7 @@ export class USSActions { } await ZoweExplorerApiRegister.getUssApi(prof).putContent(doc.fileName, ussName, options); } catch (e) { - await AuthUtils.errorHandling(e, node.getProfileName()); + await AuthUtils.errorHandling(e, { profile: node.getProfile() }); } } diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index f0098ac76..35cc53eac 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -333,7 +333,7 @@ export class USSTree extends ZoweTreeProvider implements Types this.updateFavorites(); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, originalNode.getProfileName(), vscode.l10n.t("Unable to rename node:")); + await AuthUtils.errorHandling(err, { profile: originalNode.getProfile(), scenario: vscode.l10n.t("Unable to rename node:") }); } throw err; } @@ -486,7 +486,7 @@ export class USSTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, profile.name); + await AuthUtils.errorHandling(err, { profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index 78503884f..df808a10a 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -540,7 +540,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ussFileProvider.getTreeView().reveal(this, { select: true, focus: true, expand: false }); } } catch (err) { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { profile: this.getProfile() }); throw err; } } @@ -581,7 +581,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }) ); } else { - await AuthUtils.errorHandling(err, this.getProfileName()); + await AuthUtils.errorHandling(err, { profile: this.getProfile() }); } } } @@ -654,7 +654,10 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ); } } catch (error) { - await AuthUtils.errorHandling(error, this.label.toString(), vscode.l10n.t("Error uploading files")); + await AuthUtils.errorHandling(error, { + profile: this.getProfile(), + scenario: vscode.l10n.t("Error uploading files"), + }); } } @@ -680,7 +683,10 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { return await UssFSProvider.instance.listFiles(profile, this.resourceUri); } } catch (error) { - const updated = await AuthUtils.errorHandling(error, this.getProfileName(), vscode.l10n.t("Retrieving response from USS list API")); + const updated = await AuthUtils.errorHandling(error, { + profile: this.getProfile(), + scenario: vscode.l10n.t("Retrieving response from USS list API"), + }); AuthUtils.syncSessionNode((prof) => ZoweExplorerApiRegister.getUssApi(prof), this.getSessionNode(), updated && this); return { success: false, commandResponse: null }; } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 9917a1fe9..38d22a730 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -11,82 +11,104 @@ import * as util from "util"; import * as vscode from "vscode"; -import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; +import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, NetworkError } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders"; +export interface ErrorContext { + apiType?: ZoweExplorerApiType; + profile?: string | imperative.IProfileLoaded; + scenario?: string; + [key: string]: any; +} + export class AuthUtils { + public static async promptForAuthentication( + imperativeError: imperative.ImperativeError, + correlation: NetworkError, + profile: imperative.IProfileLoaded + ): Promise { + if (imperativeError.mDetails.additionalDetails) { + const tokenError: string = imperativeError.mDetails.additionalDetails; + const isTokenAuth = await AuthUtils.isUsingTokenAuth(profile.name); + + if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { + const message = vscode.l10n.t("Log in to Authentication Service"); + const success = Gui.showMessage(correlation.message, { items: [message] }).then(async (selection) => { + if (selection) { + return Constants.PROFILES_CACHE.ssoLogin(null, profile.name); + } + }); + return success; + } + } + const checkCredsButton = vscode.l10n.t("Update Credentials"); + const creds = await Gui.errorMessage(correlation.message, { + items: [checkCredsButton], + vsCodeOpts: { modal: true }, + }).then(async (selection) => { + if (selection !== checkCredsButton) { + Gui.showMessage(vscode.l10n.t("Operation Cancelled")); + return; + } + return Constants.PROFILES_CACHE.promptCredentials(profile.name, true); + }); + return creds != null ? true : false; + } + + public static async openConfigForMissingHostname(profile: imperative.IProfileLoaded): Promise { + const mProfileInfo = await Constants.PROFILES_CACHE.getProfileInfo(); + Gui.errorMessage(vscode.l10n.t("Required parameter 'host' must not be blank.")); + const profAllAttrs = mProfileInfo.getAllProfiles(); + for (const prof of profAllAttrs) { + if (prof.profName === profile?.name) { + const filePath = prof.profLoc.osLoc[0]; + await Constants.PROFILES_CACHE.openConfigFile(filePath); + } + } + } + /************************************************************************************************************* * Error Handling * @param {errorDetails} - string or error object * @param {label} - additional information such as profile name, credentials, messageID etc * @param {moreInfo} - additional/customized error messages *************************************************************************************************************/ - public static async errorHandling(errorDetails: Error | string, label?: string, moreInfo?: string | imperative.IProfileLoaded): Promise { + public static async errorHandling(errorDetails: Error | string, moreInfo?: ErrorContext): Promise { // Use util.inspect instead of JSON.stringify to handle circular references // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null })); + ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, moreInfo }, { depth: null })); + + const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; + const correlation = ErrorCorrelator.getInstance().correlateError( + moreInfo?.apiType ?? ZoweExplorerApiType.All, + profile?.type, + typeof errorDetails === "string" ? errorDetails : errorDetails.message, + { + profileName: profile?.name, + } + ); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { const imperativeError: imperative.ImperativeError = errorDetails as imperative.ImperativeError; const httpErrorCode = Number(imperativeError.mDetails.errorCode); // open config file for missing hostname error if (imperativeError.toString().includes("hostname")) { - const mProfileInfo = await Constants.PROFILES_CACHE.getProfileInfo(); - Gui.errorMessage(vscode.l10n.t("Required parameter 'host' must not be blank.")); - const profAllAttrs = mProfileInfo.getAllProfiles(); - for (const prof of profAllAttrs) { - if (prof.profName === label.trim()) { - const filePath = prof.profLoc.osLoc[0]; - await Constants.PROFILES_CACHE.openConfigFile(filePath); - return false; - } - } + await AuthUtils.openConfigForMissingHostname(profile); + return false; } else if ( - httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || - imperativeError.message.includes("All configured authentication methods failed") + profile != null && + (httpErrorCode === imperative.RestConstants.HTTP_STATUS_401 || + imperativeError.message.includes("All configured authentication methods failed")) ) { - const profile = await Constants.PROFILES_CACHE.loadNamedProfile(label); - const correlation = ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.All, profile.type, imperativeError.message, { - profileName: label, - }); - if (label.includes("[")) { - label = label.substring(0, label.indexOf(" [")).trim(); - } - - if (imperativeError.mDetails.additionalDetails) { - const tokenError: string = imperativeError.mDetails.additionalDetails; - const isTokenAuth = await AuthUtils.isUsingTokenAuth(label); - - if (tokenError.includes("Token is not valid or expired.") || isTokenAuth) { - const message = vscode.l10n.t("Log in to Authentication Service"); - const success = Gui.showMessage(correlation.message, { items: [message] }).then(async (selection) => { - if (selection) { - return Constants.PROFILES_CACHE.ssoLogin(null, label); - } - }); - return success; - } - } - const checkCredsButton = vscode.l10n.t("Update Credentials"); - await Gui.errorMessage(correlation.message, { - items: [checkCredsButton], - vsCodeOpts: { modal: true }, - }).then(async (selection) => { - if (selection !== checkCredsButton) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); - return; - } - return Constants.PROFILES_CACHE.promptCredentials(label.trim(), true); - }); - return creds != null ? true : false; + return AuthUtils.promptForAuthentication(imperativeError, correlation, profile); } } if (errorDetails.toString().includes("Could not find profile")) { return false; } // Try to keep message readable since VS Code doesn't support newlines in error messages - Gui.errorMessage(errorDetails.toString().replace(/\n/g, " | ")); + Gui.errorMessage(correlation.message.replace(/\n/g, " | ")); return false; } diff --git a/packages/zowe-explorer/src/utils/ProfilesUtils.ts b/packages/zowe-explorer/src/utils/ProfilesUtils.ts index 9e1a87b56..056d4b1ec 100644 --- a/packages/zowe-explorer/src/utils/ProfilesUtils.ts +++ b/packages/zowe-explorer/src/utils/ProfilesUtils.ts @@ -547,7 +547,7 @@ export class ProfilesUtils { ZoweLogger.info(vscode.l10n.t("Zowe Profiles initialized successfully.")); } catch (err) { if (err instanceof imperative.ImperativeError) { - await AuthUtils.errorHandling(err, undefined, err.mDetails.causeErrors); + await AuthUtils.errorHandling(err, { scenario: err.mDetails.causeErrors }); } else { ZoweLogger.error(err); errorCallback(err.message); From 93760092f535a39a670063c48e2ea17ac782a616 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 10 Oct 2024 16:44:10 -0400 Subject: [PATCH 16/62] pass template args from error context, add mvs error correlation Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts | 9 +++++++++ .../zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts | 2 ++ packages/zowe-explorer/src/utils/AuthUtils.ts | 1 + 3 files changed, 12 insertions(+) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 8eaa6fde2..4d82b9a88 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -93,6 +93,15 @@ export class ErrorCorrelator extends Singleton { summary: "The specified data set cannot be found. Perhaps the data set name or member name was incorrectly specified.", tips: ["Ensure that the data set and/or member name is correct and try again."], }, + { + matches: ["Qualifiers cannot be longer than 8 characters."], + summary: "The given data set name/pattern {{dsName}} has a qualifier longer than 8 characters.", + tips: [ + "Each qualifier in a data set can have at most 8 characters. ".concat( + "Ensure that the given name or pattern has 8 characters or less in each qualifier." + ), + ], + }, ], [ZoweExplorerApiType.Uss]: [ { diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 466b10039..f8ee3d474 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -26,6 +26,7 @@ import { ZoweScheme, PdsEntry, FsDatasetsUtils, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { DatasetFSProvider } from "./DatasetFSProvider"; import { SharedUtils } from "../shared/SharedUtils"; @@ -590,6 +591,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } } catch (error) { const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, profile: this.getProfile(), scenario: vscode.l10n.t("Retrieving response from MVS list API"), }); diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 38d22a730..bd7f0043f 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -87,6 +87,7 @@ export class AuthUtils { typeof errorDetails === "string" ? errorDetails : errorDetails.message, { profileName: profile?.name, + ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), } ); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { From 3976e68148366ffd1bda0b56f0dd4ac620172df1 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 10 Oct 2024 16:49:52 -0400 Subject: [PATCH 17/62] wip: separate function to display correlation Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 28 ++++++++++++++----- packages/zowe-explorer/src/utils/AuthUtils.ts | 4 +-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 4d82b9a88..8ee5a09dd 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -217,13 +217,7 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayError( - api: ZoweExplorerApiType, - profileType: string, - errorDetails: string, - opts?: { allowRetry?: boolean; stackTrace?: string } - ): Promise { - const error = this.correlateError(api, profileType, errorDetails); + public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean; stackTrace?: string }): Promise { const errorCodeStr = error.info?.errorCode ? `(Error Code ${error.info.errorCode})` : ""; const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), @@ -250,4 +244,24 @@ export class ErrorCorrelator extends Singleton { return userSelection; } + + /** + * Translates a detailed error message to a user-friendly summary. + * Full error details are available through the "More info" dialog option. + * + * @param api The API type where the error was encountered + * @param profileType The profile type in use + * @param errorDetails The full error details (usually `error.message`) + * @param allowRetry Whether to allow retrying the action + * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") + */ + public async displayError( + api: ZoweExplorerApiType, + profileType: string, + errorDetails: string, + opts?: { allowRetry?: boolean; stackTrace?: string } + ): Promise { + const error = this.correlateError(api, profileType, errorDetails); + return this.displayCorrelatedError(error, opts); + } } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index bd7f0043f..3a86da0d4 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -108,8 +108,8 @@ export class AuthUtils { if (errorDetails.toString().includes("Could not find profile")) { return false; } - // Try to keep message readable since VS Code doesn't support newlines in error messages - Gui.errorMessage(correlation.message.replace(/\n/g, " | ")); + + ErrorCorrelator.getInstance().displayCorrelatedError(correlation); return false; } From 01f5ae5e4a5882911cc0a12add3fe308dac7dade Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 11 Oct 2024 15:43:29 -0400 Subject: [PATCH 18/62] wip: add params to AuthUtils.errorHandling for correlator Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 5 ++ .../__tests__/__unit__/extension.unit.test.ts | 2 + .../src/commands/ZoweCommandProvider.ts | 5 +- .../src/configuration/Constants.ts | 2 +- .../src/configuration/Profiles.ts | 2 +- .../src/trees/ZoweTreeProvider.ts | 3 +- .../src/trees/dataset/DatasetActions.ts | 49 ++++++++++++++----- .../src/trees/dataset/DatasetTree.ts | 5 +- .../src/trees/dataset/ZoweDatasetNode.ts | 8 ++- .../zowe-explorer/src/trees/job/JobActions.ts | 20 ++++---- .../zowe-explorer/src/trees/job/JobTree.ts | 4 +- .../src/trees/job/ZoweJobNode.ts | 4 +- .../zowe-explorer/src/trees/uss/USSActions.ts | 14 ++++-- .../zowe-explorer/src/trees/uss/USSTree.ts | 9 +++- .../src/trees/uss/ZoweUSSNode.ts | 7 ++- 15 files changed, 96 insertions(+), 43 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 8ee5a09dd..062ce4d43 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -135,6 +135,11 @@ export class ErrorCorrelator extends Singleton { matches: ["Job input was not recognized by system as a job"], summary: "The job was submitted without a job statement or with unrecognized (non-JCL) content.", }, + { + errorCode: "400", + matches: ["Value of jobid query parameter is not valid"], + summary: "The given Job ID is invalid. Please verify that the job ID is correct and try again.", + }, ], [ZoweExplorerApiType.All]: [ { diff --git a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts index 02844adc8..ec013bc09 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extension.unit.test.ts @@ -242,6 +242,8 @@ async function createGlobalMocks() { "zowe.compareWithSelected", "zowe.compareWithSelectedReadOnly", "zowe.compareFileStarted", + "zowe.revealOutputChannel", + "zowe.troubleshootError", "zowe.placeholderCommand", ], }; diff --git a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts index d2ac576dc..7db19f3cf 100644 --- a/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts +++ b/packages/zowe-explorer/src/commands/ZoweCommandProvider.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { IZoweTreeNode, PersistenceSchemaEnum, Validation } from "@zowe/zowe-explorer-api"; +import { IZoweTreeNode, PersistenceSchemaEnum, Validation, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZowePersistentFilters } from "../tools/ZowePersistentFilters"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedContext } from "../trees/shared/SharedContext"; @@ -75,7 +75,8 @@ export class ZoweCommandProvider { "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", args: [profile.name], comment: ["Profile name"], - }) + }), + { apiType: ZoweExplorerApiType.Command, profile } ); } else if (profileStatus.status === "active") { if ( diff --git a/packages/zowe-explorer/src/configuration/Constants.ts b/packages/zowe-explorer/src/configuration/Constants.ts index 24a965867..33e640626 100644 --- a/packages/zowe-explorer/src/configuration/Constants.ts +++ b/packages/zowe-explorer/src/configuration/Constants.ts @@ -16,7 +16,7 @@ import { imperative, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; import type { Profiles } from "./Profiles"; export class Constants { - public static readonly COMMAND_COUNT = 99; + public static readonly COMMAND_COUNT = 101; public static readonly MAX_SEARCH_HISTORY = 5; public static readonly MAX_FILE_HISTORY = 10; public static readonly MS_PER_SEC = 1000; diff --git a/packages/zowe-explorer/src/configuration/Profiles.ts b/packages/zowe-explorer/src/configuration/Profiles.ts index 778f7abb9..54c9f4745 100644 --- a/packages/zowe-explorer/src/configuration/Profiles.ts +++ b/packages/zowe-explorer/src/configuration/Profiles.ts @@ -110,7 +110,7 @@ export class Profiles extends ProfilesCache { await Profiles.getInstance().ssoLogin(null, theProfile.name); theProfile = Profiles.getInstance().loadNamedProfile(theProfile.name); } catch (error) { - await AuthUtils.errorHandling(error, { profile: theProfile.name }); + await AuthUtils.errorHandling(error, { profile: theProfile }); return profileStatus; } } else if (!usingTokenAuth && (!theProfile.profile.user || !theProfile.profile.password)) { diff --git a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts index 5a2a99a44..66bd183a8 100644 --- a/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts +++ b/packages/zowe-explorer/src/trees/ZoweTreeProvider.ts @@ -241,7 +241,8 @@ export class ZoweTreeProvider { "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", args: [profile.name], comment: ["Profile name"], - }) + }), + { profile } ); } else if (profileStatus.status === "active") { if ( diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts index cf93e6755..542f3c32e 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import * as path from "path"; -import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types, FsAbstractUtils, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweDatasetTreeNode, Validation, Types, FsAbstractUtils, ZoweScheme, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { DatasetUtils } from "./DatasetUtils"; import { DatasetFSProvider } from "./DatasetFSProvider"; @@ -246,7 +246,7 @@ export class DatasetActions { const errorMsg = vscode.l10n.t("Error encountered when creating data set."); ZoweLogger.error(errorMsg + JSON.stringify(err)); if (err instanceof Error) { - await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: errorMsg }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile(), scenario: errorMsg }); } throw new Error(err); } @@ -382,7 +382,12 @@ export class DatasetActions { await ZoweExplorerApiRegister.getMvsApi(profile).allocateLikeDataSet(newDSName.toUpperCase(), likeDSName); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, { profile, dsName: newDSName, scenario: vscode.l10n.t("Unable to create data set.") }); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + profile, + dsName: newDSName, + scenario: vscode.l10n.t("Unable to create data set."), + }); } throw err; } @@ -438,7 +443,10 @@ export class DatasetActions { Gui.reportProgress(progress, value.length, index, "Uploading"); const response = await DatasetActions.uploadFile(node, item.fsPath); if (!response?.success) { - await AuthUtils.errorHandling(response?.commandResponse, { profile: node.getProfile() }); + await AuthUtils.errorHandling(response?.commandResponse, { + apiType: ZoweExplorerApiType.Mvs, + profile: node.getProfile(), + }); break; } index++; @@ -476,7 +484,7 @@ export class DatasetActions { responseTimeout: prof.profile?.responseTimeout, }); } catch (e) { - await AuthUtils.errorHandling(e, { profile: node.getProfile() }); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } @@ -674,7 +682,11 @@ export class DatasetActions { }); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, { parentDsName: label, scenario: vscode.l10n.t("Unable to create member.") }); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + parentDsName: label, + scenario: vscode.l10n.t("Unable to create member."), + }); } throw err; } @@ -878,7 +890,11 @@ export class DatasetActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: vscode.l10n.t("Unable to list attributes.") }); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Mvs, + profile: node.getProfile(), + scenario: vscode.l10n.t("Unable to list attributes."), + }); } throw err; } @@ -1012,7 +1028,11 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, { profile: sessProfile, scenario: vscode.l10n.t("Job submission failed.") }); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + profile: sessProfile, + scenario: vscode.l10n.t("Job submission failed."), + }); } } } else { @@ -1135,7 +1155,11 @@ export class DatasetActions { ); } catch (error) { if (error instanceof Error) { - await AuthUtils.errorHandling(error, { profile: sessProfile, scenario: vscode.l10n.t("Job submission failed.") }); + await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, + profile: sessProfile, + scenario: vscode.l10n.t("Job submission failed."), + }); } } } @@ -1192,7 +1216,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, { profile: node.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } throw err; } @@ -1276,7 +1300,7 @@ export class DatasetActions { }) ); } else { - await AuthUtils.errorHandling(err, { profile: node.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } } @@ -1293,7 +1317,7 @@ export class DatasetActions { await node.getChildren(); datasetProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, { profile: node.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } } @@ -1713,6 +1737,7 @@ export class DatasetActions { } catch (error) { if (error instanceof Error) { await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Mvs, dsName: DatasetUtils.getNodeLabels(node).dataSetName, scenario: vscode.l10n.t("Unable to copy data set."), }); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index 629e03d3f..bb1e622f7 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -24,6 +24,7 @@ import { ZosEncoding, FsAbstractUtils, DatasetMatch, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { ZoweDatasetNode } from "./ZoweDatasetNode"; import { DatasetFSProvider } from "./DatasetFSProvider"; @@ -432,7 +433,7 @@ export class DatasetTree extends ZoweTreeProvider implemen if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, { profile }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile }); } } // Creates ZoweDatasetNode to track new session and pushes it to mSessionNodes @@ -1058,7 +1059,7 @@ export class DatasetTree extends ZoweTreeProvider implemen response = await this.getChildren(sessionNode); }); } catch (err) { - await AuthUtils.errorHandling(err, { profile: node.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: node.getProfile() }); } if (response.length === 0) { return; diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index f8ee3d474..0e355fd49 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -240,7 +240,11 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod // Throws reject if the Zowe command does not throw an error but does not succeed // The dataSetsMatchingPattern API may return success=false and apiResponse=[] when no data sets found if (!response.success && !(Array.isArray(response.apiResponse) && response.apiResponse.length === 0)) { - await AuthUtils.errorHandling(vscode.l10n.t("The response from Zowe CLI was not successful")); + await AuthUtils.errorHandling(new imperative.ImperativeError({ msg: response.commandResponse }), { + apiType: ZoweExplorerApiType.Mvs, + profile: cachedProfile, + scenario: vscode.l10n.t("The response from Zowe CLI was not successful"), + }); return []; } @@ -632,7 +636,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod datasetProvider.addFileHistory(`[${this.getProfileName()}]: ${this.label as string}`); } } catch (err) { - await AuthUtils.errorHandling(err, { profile: this.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Mvs, profile: this.getProfile() }); throw err; } } diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index b61d8199d..10e364095 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; -import { Gui, IZoweJobTreeNode, Sorting, Types } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweJobTreeNode, Sorting, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweJobNode } from "./ZoweJobNode"; import { JobTree } from "./JobTree"; import { JobUtils } from "./JobUtils"; @@ -54,7 +54,7 @@ export class JobActions { }) ); } catch (error) { - await AuthUtils.errorHandling(error, { profile: job.getProfile() }); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: job.getProfile() }); } } @@ -120,7 +120,7 @@ export class JobActions { if (deletionErrors.length) { const errorMessages = deletionErrors.map((error) => error.message).join(", "); const userMessage = `There were errors during jobs deletion: ${errorMessages}`; - await AuthUtils.errorHandling(userMessage); + await AuthUtils.errorHandling(userMessage, { apiType: ZoweExplorerApiType.Jes }); } } @@ -137,7 +137,7 @@ export class JobActions { try { await jobsProvider.addSession({ sessionName: sessionName.trim() }); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionNode.getProfile() }); return; } sessionNode = jobsProvider.mSessionNodes.find((jobNode) => jobNode.label.toString().trim() === sessionName.trim()); @@ -145,7 +145,7 @@ export class JobActions { try { jobsProvider.refreshElement(sessionNode); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionNode.getProfile() }); return; } sessionNode.searchId = jobId; @@ -183,7 +183,7 @@ export class JobActions { } } } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes }); } } @@ -227,7 +227,7 @@ export class JobActions { } } } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes }); } } @@ -270,7 +270,7 @@ export class JobActions { const jclDoc = await vscode.workspace.openTextDocument({ language: "jcl", content: jobJcl }); await Gui.showTextDocument(jclDoc, { preview: false }); } catch (error) { - await AuthUtils.errorHandling(error); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: job.getProfile() }); } } @@ -307,7 +307,7 @@ export class JobActions { vscode.l10n.t("jobActions.modifyCommand.apiNonExisting", "Not implemented yet for profile of type: ") + job.getProfile().type ); } else { - await AuthUtils.errorHandling(error, { profile: job.getProfile() }); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: job.getProfile() }); } } } @@ -343,7 +343,7 @@ export class JobActions { }) ); } else { - await AuthUtils.errorHandling(error, { profile: job.getProfile() }); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Command, profile: job.getProfile() }); } } } diff --git a/packages/zowe-explorer/src/trees/job/JobTree.ts b/packages/zowe-explorer/src/trees/job/JobTree.ts index a7a31a960..d15120922 100644 --- a/packages/zowe-explorer/src/trees/job/JobTree.ts +++ b/packages/zowe-explorer/src/trees/job/JobTree.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { IJob } from "@zowe/zos-jobs-for-zowe-sdk"; -import { Gui, Validation, imperative, IZoweJobTreeNode, PersistenceSchemaEnum, Poller, Types } from "@zowe/zowe-explorer-api"; +import { Gui, Validation, imperative, IZoweJobTreeNode, PersistenceSchemaEnum, Poller, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { ZoweJobNode } from "./ZoweJobNode"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; @@ -193,7 +193,7 @@ export class JobTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, { profile }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Jes, profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 4cfb377e1..1dfcdff51 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -12,7 +12,7 @@ import * as vscode from "vscode"; import * as zosjobs from "@zowe/zos-jobs-for-zowe-sdk"; import * as path from "path"; -import { FsJobsUtils, imperative, IZoweJobTreeNode, Sorting, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { FsJobsUtils, imperative, IZoweJobTreeNode, Sorting, ZoweExplorerApiType, ZoweScheme, ZoweTreeNode } from "@zowe/zowe-explorer-api"; import { JobFSProvider } from "./JobFSProvider"; import { JobUtils } from "./JobUtils"; import { Constants } from "../../configuration/Constants"; @@ -397,6 +397,7 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { } } catch (error) { const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Jes, profile: this.getProfile(), scenario: vscode.l10n.t("Retrieving response from JES list API"), }); @@ -417,6 +418,7 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { spools = spools.filter((item) => !(item.id === undefined && item.ddname === undefined && item.stepname === undefined)); } catch (error) { const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Jes, profile: this.getProfile(), scenario: vscode.l10n.t("Retrieving response from JES list API"), }); diff --git a/packages/zowe-explorer/src/trees/uss/USSActions.ts b/packages/zowe-explorer/src/trees/uss/USSActions.ts index ba6cf8c26..cd3ba527f 100644 --- a/packages/zowe-explorer/src/trees/uss/USSActions.ts +++ b/packages/zowe-explorer/src/trees/uss/USSActions.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; import * as fs from "fs"; import * as path from "path"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { Gui, imperative, IZoweUSSTreeNode, Types } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, IZoweUSSTreeNode, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { isBinaryFileSync } from "isbinaryfile"; import { USSAttributeView } from "./USSAttributeView"; import { USSFileStructure } from "./USSFileStructure"; @@ -99,7 +99,11 @@ export class USSActions { } } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, { profile: node.getProfile(), scenario: vscode.l10n.t("Unable to create node:") }); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Uss, + profile: node.getProfile(), + scenario: vscode.l10n.t("Unable to create node:"), + }); } throw err; } @@ -117,7 +121,7 @@ export class USSActions { await node.getChildren(); ussFileProvider.refreshElement(node); } catch (err) { - await AuthUtils.errorHandling(err, { profile: node.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } @@ -188,7 +192,7 @@ export class USSActions { const ussName = `${node.fullPath}/${localFileName}`; await ZoweExplorerApiRegister.getUssApi(node.getProfile()).putContent(filePath, ussName, { binary: true }); } catch (e) { - await AuthUtils.errorHandling(e, { profile: node.getProfile() }); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } @@ -213,7 +217,7 @@ export class USSActions { } await ZoweExplorerApiRegister.getUssApi(prof).putContent(doc.fileName, ussName, options); } catch (e) { - await AuthUtils.errorHandling(e, { profile: node.getProfile() }); + await AuthUtils.errorHandling(e, { apiType: ZoweExplorerApiType.Uss, profile: node.getProfile() }); } } diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index 35cc53eac..aa6f87401 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -20,6 +20,7 @@ import { Types, Validation, ZosEncoding, + ZoweExplorerApiType, ZoweScheme, } from "@zowe/zowe-explorer-api"; import { UssFSProvider } from "./UssFSProvider"; @@ -333,7 +334,11 @@ export class USSTree extends ZoweTreeProvider implements Types this.updateFavorites(); } catch (err) { if (err instanceof Error) { - await AuthUtils.errorHandling(err, { profile: originalNode.getProfile(), scenario: vscode.l10n.t("Unable to rename node:") }); + await AuthUtils.errorHandling(err, { + apiType: ZoweExplorerApiType.Uss, + profile: originalNode.getProfile(), + scenario: vscode.l10n.t("Unable to rename node:"), + }); } throw err; } @@ -486,7 +491,7 @@ export class USSTree extends ZoweTreeProvider implements Types if (err.toString().includes("hostname")) { ZoweLogger.error(err); } else { - await AuthUtils.errorHandling(err, { profile }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile }); } } // Creates ZoweNode to track new session and pushes it to mSessionNodes diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index df808a10a..986f02bbb 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -25,6 +25,7 @@ import { UssDirectory, FsAbstractUtils, MainframeInteraction, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { USSUtils } from "./USSUtils"; import { Constants } from "../../configuration/Constants"; @@ -540,7 +541,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ussFileProvider.getTreeView().reveal(this, { select: true, focus: true, expand: false }); } } catch (err) { - await AuthUtils.errorHandling(err, { profile: this.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: this.getProfile() }); throw err; } } @@ -581,7 +582,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }) ); } else { - await AuthUtils.errorHandling(err, { profile: this.getProfile() }); + await AuthUtils.errorHandling(err, { apiType: ZoweExplorerApiType.Uss, profile: this.getProfile() }); } } } @@ -655,6 +656,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } } catch (error) { await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Uss, profile: this.getProfile(), scenario: vscode.l10n.t("Error uploading files"), }); @@ -684,6 +686,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } } catch (error) { const updated = await AuthUtils.errorHandling(error, { + apiType: ZoweExplorerApiType.Uss, profile: this.getProfile(), scenario: vscode.l10n.t("Retrieving response from USS list API"), }); From 27a21db3ddeaa77aabd256d537371556b98604ce Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 14 Oct 2024 11:34:56 -0400 Subject: [PATCH 19/62] tests: Resolve failing test cases Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 2 +- .../commands/MvsCommandHandler.unit.test.ts | 2 +- .../commands/TsoCommandHandler.unit.test.ts | 2 +- .../commands/UnixCommandHandler.unit.test.ts | 2 +- .../commands/ZoweCommandProvider.unit.test.ts | 5 +- .../configuration/Profiles.unit.test.ts | 7 ++- .../DatasetActions.extended.unit.test.ts | 4 +- .../trees/dataset/DatasetActions.unit.test.ts | 23 ++++--- .../dataset/ZoweDatasetNode.unit.test.ts | 2 +- .../trees/job/JobActions.unit.test.ts | 19 ++++-- .../trees/uss/ZoweUSSNode.unit.test.ts | 4 +- .../__unit__/utils/ProfilesUtils.unit.test.ts | 61 +++++++++---------- .../zowe-explorer/src/trees/job/JobActions.ts | 4 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 1 - 14 files changed, 72 insertions(+), 66 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 062ce4d43..893807be3 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -194,7 +194,7 @@ export class ErrorCorrelator extends Singleton { } for (const apiError of [ - ...this.errorMatches.get(profileType)[api], + ...(this.errorMatches.get(profileType)?.[api] ?? []), ...(api === ZoweExplorerApiType.All ? [] : this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? []), ]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts index 66ac72ebb..64bff921f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/MvsCommandHandler.unit.test.ts @@ -280,7 +280,7 @@ describe("mvsCommandActions unit testing", () => { }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("tests the issueMvsCommand function user escapes the quick pick box", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts index 88d79146f..98beadfa3 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/TsoCommandHandler.unit.test.ts @@ -279,7 +279,7 @@ describe("TsoCommandHandler unit testing", () => { }); expect(showInputBox.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("tests the issueTsoCommand function user escapes the quick pick box", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts index 76faf5128..f1a0830b7 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/UnixCommandHandler.unit.test.ts @@ -406,7 +406,7 @@ describe("UnixCommand Actions Unit Testing", () => { }); expect(showInputBox.mock.calls.length).toBe(2); expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("Error: fake testError"); + expect(showErrorMessage.mock.calls[0][0]).toEqual("fake testError"); }); it("If nothing is entered in the inputbox of path", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts index e9f787026..733d2a601 100644 --- a/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/commands/ZoweCommandProvider.unit.test.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { ProfilesCache, ZoweTreeNode } from "@zowe/zowe-explorer-api"; +import { ProfilesCache, ZoweExplorerApiType, ZoweTreeNode } from "@zowe/zowe-explorer-api"; import { createIProfile, createISession } from "../../__mocks__/mockCreators/shared"; import { ZoweCommandProvider } from "../../../src/commands/ZoweCommandProvider"; import { Profiles } from "../../../src/configuration/Profiles"; @@ -85,7 +85,8 @@ describe("ZoweCommandProvider Unit Tests - function checkCurrentProfile", () => expect(errorHandlingSpy).toHaveBeenCalledWith( "Profile Name " + globalMocks.testProfile.name + - " is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct." + " is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.", + { apiType: ZoweExplorerApiType.Command, profile: globalMocks.testProfile } ); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts index 813d265b7..4ffd55400 100644 --- a/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/configuration/Profiles.unit.test.ts @@ -891,13 +891,14 @@ describe("Profiles Unit Tests - function validateProfile", () => { throw testError; }); const errorHandlingSpy = jest.spyOn(AuthUtils, "errorHandling"); - await Profiles.getInstance().validateProfiles({ + const profile = { name: "test1", message: "", type: "", failNotFound: false, - }); - expect(errorHandlingSpy).toHaveBeenCalledWith(testError, "test1"); + }; + await Profiles.getInstance().validateProfiles(profile); + expect(errorHandlingSpy).toHaveBeenCalledWith(testError, { profile }); }); it("should return an object with profile validation status of 'unverified' from session status if validated profiles doesn't exist", async () => { createBlockMocks(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts index dc3bb0048..4fca29287 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.extended.unit.test.ts @@ -19,6 +19,7 @@ import { SharedActions } from "../../../../src/trees/shared/SharedActions"; import { Profiles } from "../../../../src/configuration/Profiles"; import { ZoweDatasetNode } from "../../../../src/trees/dataset/ZoweDatasetNode"; import { AuthUtils } from "../../../../src/utils/AuthUtils"; +import { ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; async function createGlobalMocks() { const newMocks = { @@ -241,8 +242,7 @@ describe("mvsNodeActions", () => { jest.spyOn(mockMvsApi2, "putContents").mockRejectedValue(testError); const errHandlerSpy = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); await DatasetActions.uploadDialog(node, testTree); - - expect(errHandlerSpy).toHaveBeenCalledWith(testError, "sestest"); + expect(errHandlerSpy).toHaveBeenCalledWith(testError, { apiType: ZoweExplorerApiType.Mvs, profile: globalMocks.profileOne }); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts index dcfa23e15..633b3bd42 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts @@ -11,7 +11,7 @@ import * as vscode from "vscode"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; -import { Gui, imperative, Validation, Types } from "@zowe/zowe-explorer-api"; +import { Gui, imperative, Validation, Types, ZoweExplorerApiType } from "@zowe/zowe-explorer-api"; import { DatasetFSProvider } from "../../../../src/trees/dataset/DatasetFSProvider"; import { bindMvsApi, createMvsApi } from "../../../__mocks__/mockCreators/api"; import { @@ -206,7 +206,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { }); mocked(vscode.window.showInputBox).mockResolvedValue("testMember"); - mocked(zosfiles.Upload.bufferToDataSet).mockRejectedValueOnce(Error("test")); + mocked(zosfiles.Upload.bufferToDataSet).mockRejectedValueOnce(Error("Error when uploading to data set")); try { await DatasetActions.createMember(parent, blockMocks.testDatasetTree); @@ -214,7 +214,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { // Prevent exception from failing test } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Unable to create member. Error: test"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error when uploading to data set", { items: ["More info"] }); mocked(zosfiles.Upload.bufferToDataSet).mockReset(); }); it("Checking of attempt to create member without name", async () => { @@ -769,9 +769,9 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { }); mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); - jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("")); + jest.spyOn(DatasetFSProvider.instance, "delete").mockRejectedValueOnce(Error("Deletion error")); await expect(DatasetActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow(""); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Deletion error", { items: ["More info"] }); }); it("Checking Favorite PDS dataset deletion", async () => { createGlobalMocks(); @@ -1123,9 +1123,9 @@ describe("Dataset Actions Unit Tests - Function showAttributes", () => { await expect(DatasetActions.showAttributes(node, blockMocks.testDatasetTree)).rejects.toEqual( Error("No matching names found for query: AUSER.A1557332.A996850.TEST1") ); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith( - "Unable to list attributes. Error: No matching names found for query: AUSER.A1557332.A996850.TEST1" - ); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("No matching names found for query: AUSER.A1557332.A996850.TEST1", { + items: ["More info"], + }); expect(mocked(vscode.window.createWebviewPanel)).not.toHaveBeenCalled(); }); }); @@ -2395,7 +2395,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error encountered when creating data set. Error: Generic Error"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Generic Error", { items: ["More info"] }); expect(mocked(vscode.workspace.getConfiguration)).toHaveBeenLastCalledWith(Constants.SETTINGS_DS_DEFAULT_PS); expect(createDataSetSpy).toHaveBeenCalledWith(zosfiles.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, "TEST", { alcunit: "CYL", @@ -2897,7 +2897,10 @@ describe("Dataset Actions Unit Tests - Function allocateLike", () => { } expect(errorHandlingSpy).toHaveBeenCalledTimes(1); - expect(errorHandlingSpy).toHaveBeenCalledWith(errorMessage, "test", "Unable to create data set."); + expect(errorHandlingSpy).toHaveBeenCalledWith( + errorMessage, + expect.objectContaining({ apiType: ZoweExplorerApiType.Mvs, scenario: "Unable to create data set." }) + ); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts index db0ca1f12..ba20a0e39 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts @@ -516,7 +516,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error: testError"); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("testError", { items: ["More info"] }); }); it("Checking of opening for PDS Member", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts index ee043a6f0..c9ba0a76c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobActions.unit.test.ts @@ -41,6 +41,7 @@ import { JobActions } from "../../../../src/trees/job/JobActions"; import { DatasetActions } from "../../../../src/trees/dataset/DatasetActions"; import { Definitions } from "../../../../src/configuration/Definitions"; import { SpoolUtils } from "../../../../src/utils/SpoolUtils"; +import { AuthUtils } from "../../../../src/utils/AuthUtils"; const activeTextEditorDocument = jest.fn(); @@ -415,8 +416,14 @@ describe("Jobs Actions Unit Tests - Function downloadJcl", () => { }); it("Checking failed attempt to download Job JCL", async () => { createGlobalMocks(); - await JobActions.downloadJcl(undefined as any); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); + const showTextDocumentMock = jest.spyOn(vscode.workspace, "openTextDocument").mockImplementationOnce(() => { + throw new Error(); + }); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); + await JobActions.downloadJcl({ getProfile: jest.fn(), job: createIJobObject() } as any); + expect(showTextDocumentMock).toHaveBeenCalled(); + expect(errorHandlingMock).toHaveBeenCalled(); + errorHandlingMock.mockRestore(); }); }); @@ -1043,6 +1050,7 @@ describe("focusing on a job in the tree view", () => { const existingJobSession = createJobSessionNode(session, profile); const datasetSessionName = existingJobSession.label as string; const jobTree = createTreeView(); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); const jobTreeProvider = createJobsTree(session, submittedJob, profile, jobTree); jobTreeProvider.mSessionNodes.push(existingJobSession); const testError = new Error("focusOnJob failed"); @@ -1053,8 +1061,7 @@ describe("focusing on a job in the tree view", () => { await JobActions.focusOnJob(jobTreeProvider, datasetSessionName, submittedJob.jobid); // assert expect(mocked(jobTreeProvider.refreshElement)).toHaveBeenCalledWith(existingJobSession); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain(testError.message); + expect(errorHandlingMock).toHaveBeenCalled(); }); it("should handle error adding a new tree view session", async () => { // arrange @@ -1064,6 +1071,7 @@ describe("focusing on a job in the tree view", () => { const newJobSession = createJobSessionNode(session, profile); const datasetSessionName = newJobSession.label as string; const jobTree = createTreeView(); + const errorHandlingMock = jest.spyOn(AuthUtils, "errorHandling").mockImplementation(); const jobTreeProvider = createJobsTree(session, submittedJob, profile, jobTree); const testError = new Error("focusOnJob failed"); jest.spyOn(jobTreeProvider, "addSession").mockRejectedValueOnce(testError); @@ -1071,8 +1079,7 @@ describe("focusing on a job in the tree view", () => { await JobActions.focusOnJob(jobTreeProvider, datasetSessionName, submittedJob.jobid); // assert expect(mocked(jobTreeProvider.addSession)).toHaveBeenCalledWith({ sessionName: datasetSessionName }); - expect(mocked(Gui.errorMessage)).toHaveBeenCalled(); - expect(mocked(Gui.errorMessage).mock.calls[0][0]).toContain(testError.message); + expect(errorHandlingMock).toHaveBeenCalled(); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts index 34e76bc09..7ca12bfab 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts @@ -856,9 +856,7 @@ describe("ZoweUSSNode Unit Tests - Function node.getChildren()", () => { await blockMocks.childNode.getChildren(); expect(globalMocks.showErrorMessage.mock.calls.length).toEqual(1); - expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual( - "Retrieving response from USS list API Error: Throwing an error to check error handling for unit tests!" - ); + expect(globalMocks.showErrorMessage.mock.calls[0][0]).toEqual("Throwing an error to check error handling for unit tests!"); }); it("Tests that when passing a session node that is not dirty the node.getChildren() method is exited early", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index c276a01e3..bb1fdefb2 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -91,12 +91,11 @@ describe("ProfilesUtils unit tests", () => { it("should log error details", async () => { createBlockMocks(); const errorDetails = new Error("i haz error"); - const label = "test"; - const moreInfo = "Task failed successfully"; - await AuthUtils.errorHandling(errorDetails, label, moreInfo); - expect(Gui.errorMessage).toHaveBeenCalledWith(moreInfo + ` Error: ${errorDetails.message}`); + const scenario = "Task failed successfully"; + await AuthUtils.errorHandling(errorDetails, { scenario }); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["More info"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `${errorDetails.toString()}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null }) + `${errorDetails.toString()}\n` + util.inspect({ errorDetails, moreInfo: { scenario } }, { depth: null }) ); }); @@ -108,13 +107,12 @@ describe("ProfilesUtils unit tests", () => { msg: "Circular reference", causeErrors: errorJson, }); - const label = "test"; - const moreInfo = "Task failed successfully"; - await AuthUtils.errorHandling(errorDetails, label, moreInfo as unknown as string); + const scenario = "Task failed successfully"; + await AuthUtils.errorHandling(errorDetails, { scenario }); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - expect(Gui.errorMessage).toHaveBeenCalledWith((`${moreInfo} ` + errorDetails) as any); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["More info"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, label, moreInfo }, { depth: null }) + `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, moreInfo: { scenario } }, { depth: null }) ); }); @@ -123,9 +121,8 @@ describe("ProfilesUtils unit tests", () => { msg: "Invalid hostname", errorCode: 404 as unknown as string, }); - const label = "test"; - const moreInfo = "Task failed successfully"; - const spyOpenConfigFile = jest.fn(); + const scenario = "Task failed successfully"; + const openConfigForMissingHostnameMock = jest.spyOn(AuthUtils, "openConfigForMissingHostname"); Object.defineProperty(Constants, "PROFILES_CACHE", { value: { getProfileInfo: () => ({ @@ -139,12 +136,12 @@ describe("ProfilesUtils unit tests", () => { }, ], }), - openConfigFile: spyOpenConfigFile, + openConfigFile: jest.fn(), }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); - expect(spyOpenConfigFile).toHaveBeenCalledTimes(1); + await AuthUtils.errorHandling(errorDetails, { scenario }); + expect(openConfigForMissingHostnameMock).toHaveBeenCalled(); }); it("should handle error for invalid credentials and prompt for authentication", async () => { @@ -153,21 +150,21 @@ describe("ProfilesUtils unit tests", () => { errorCode: 401 as unknown as string, additionalDetails: "Authentication is not valid or expired.", }); - const label = "test"; - const moreInfo = "Task failed successfully"; + const scenario = "Task failed successfully"; const showMessageSpy = jest.spyOn(Gui, "errorMessage").mockImplementation(() => Promise.resolve("Update Credentials")); const promptCredsSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { promptCredentials: promptCredsSpy, getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => [], }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario }); expect(showMessageSpy).toHaveBeenCalledTimes(1); expect(promptCredsSpy).toHaveBeenCalledTimes(1); showMessageSpy.mockClear(); @@ -176,25 +173,25 @@ describe("ProfilesUtils unit tests", () => { it("should handle token error and proceed to login", async () => { const errorDetails = new imperative.ImperativeError({ msg: "Invalid credentials", - errorCode: 401 as unknown as string, + errorCode: "401", additionalDetails: "Token is not valid or expired.", }); - const label = "test"; - const moreInfo = "Task failed successfully"; + const scenario = "Task failed successfully"; const showErrorSpy = jest.spyOn(Gui, "errorMessage"); const showMessageSpy = jest.spyOn(Gui, "showMessage").mockImplementation(() => Promise.resolve("selection")); const ssoLoginSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => ["tokenValue"], ssoLogin: ssoLoginSpy, }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario }); expect(showMessageSpy).toHaveBeenCalledTimes(1); expect(ssoLoginSpy).toHaveBeenCalledTimes(1); expect(showErrorSpy).not.toHaveBeenCalled(); @@ -205,10 +202,9 @@ describe("ProfilesUtils unit tests", () => { it("should handle credential error and no selection made for update", async () => { const errorDetails = new imperative.ImperativeError({ msg: "Invalid credentials", - errorCode: String(401), - additionalDetails: "Authentication failed.", + errorCode: "401", + additionalDetails: "All configured authentication methods failed", }); - const label = "test"; const moreInfo = "Task failed successfully"; Object.defineProperty(vscode, "env", { value: { @@ -219,20 +215,21 @@ describe("ProfilesUtils unit tests", () => { const showErrorSpy = jest.spyOn(Gui, "errorMessage").mockResolvedValue(undefined); const showMsgSpy = jest.spyOn(Gui, "showMessage"); const promptCredentialsSpy = jest.fn(); + const profile = { type: "zosmf" } as any; Object.defineProperty(Constants, "PROFILES_CACHE", { value: { promptCredentials: promptCredentialsSpy, getProfileInfo: profileInfoMock, - getLoadedProfConfig: () => ({ type: "zosmf" }), + getLoadedProfConfig: () => profile, getDefaultProfile: () => ({}), getSecurePropsForProfile: () => [], }, configurable: true, }); - await AuthUtils.errorHandling(errorDetails, label, moreInfo); + await AuthUtils.errorHandling(errorDetails, { profile, scenario: moreInfo }); expect(showErrorSpy).toHaveBeenCalledTimes(1); expect(promptCredentialsSpy).not.toHaveBeenCalled(); - expect(showMsgSpy).toHaveBeenCalledWith("Operation Cancelled"); + expect(showMsgSpy).not.toHaveBeenCalledWith("Operation Cancelled"); showErrorSpy.mockClear(); showMsgSpy.mockClear(); promptCredentialsSpy.mockClear(); @@ -680,7 +677,7 @@ describe("ProfilesUtils unit tests", () => { await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); expect(readConfigFromDiskSpy).toHaveBeenCalledTimes(1); - expect(Gui.errorMessage).toHaveBeenCalledWith(expect.stringContaining(testError.message)); + expect(Gui.errorMessage).toHaveBeenCalledWith(testError.message, { items: ["More info"] }); }); it("should handle JSON parse error thrown on read config from disk", async () => { diff --git a/packages/zowe-explorer/src/trees/job/JobActions.ts b/packages/zowe-explorer/src/trees/job/JobActions.ts index 10e364095..83140dd79 100644 --- a/packages/zowe-explorer/src/trees/job/JobActions.ts +++ b/packages/zowe-explorer/src/trees/job/JobActions.ts @@ -137,7 +137,7 @@ export class JobActions { try { await jobsProvider.addSession({ sessionName: sessionName.trim() }); } catch (error) { - await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionNode.getProfile() }); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionName }); return; } sessionNode = jobsProvider.mSessionNodes.find((jobNode) => jobNode.label.toString().trim() === sessionName.trim()); @@ -145,7 +145,7 @@ export class JobActions { try { jobsProvider.refreshElement(sessionNode); } catch (error) { - await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionNode.getProfile() }); + await AuthUtils.errorHandling(error, { apiType: ZoweExplorerApiType.Jes, profile: sessionName }); return; } sessionNode.searchId = jobId; diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 3a86da0d4..05c45fe45 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -49,7 +49,6 @@ export class AuthUtils { vsCodeOpts: { modal: true }, }).then(async (selection) => { if (selection !== checkCredsButton) { - Gui.showMessage(vscode.l10n.t("Operation Cancelled")); return; } return Constants.PROFILES_CACHE.promptCredentials(profile.name, true); From 5186e842f49f80a3b558b9eda5a4e075c9fca477 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 14 Oct 2024 13:43:20 -0400 Subject: [PATCH 20/62] refactor: Use API type, then profile type for narrowing Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 16 ++++++- .../src/utils/ErrorCorrelator.ts | 43 +++++++++++++------ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 5bdefb504..67134d7d8 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -13,7 +13,7 @@ import { ErrorCorrelator, Gui, NetworkError, ZoweExplorerApiType } from "../../. import { commands } from "vscode"; describe("addCorrelation", () => { - it("adds a correlation for the given API and profile type", () => { + it("adds a correlation for the given API and existing profile type", () => { const fakeErrorSummary = "Example error summary for the correlator"; ErrorCorrelator.getInstance().addCorrelation(ZoweExplorerApiType.Mvs, "zosmf", { errorCode: "403", @@ -21,7 +21,19 @@ describe("addCorrelation", () => { matches: ["Specific sequence 1234 encountered"], }); expect( - (ErrorCorrelator.getInstance() as any).errorMatches.get("zosmf")[ZoweExplorerApiType.Mvs].find((err) => err.summary === fakeErrorSummary) + (ErrorCorrelator.getInstance() as any).errorMatches.get(ZoweExplorerApiType.Mvs)["zosmf"].find((err) => err.summary === fakeErrorSummary) + ).not.toBe(null); + }); + it("adds a correlation for the given API and new profile type", () => { + const fakeErrorSummary = "Example error summary for the correlator"; + ErrorCorrelator.getInstance().addCorrelation(ZoweExplorerApiType.Mvs, "fake-type", { + errorCode: "403", + summary: fakeErrorSummary, + matches: ["Specific sequence 5678 encountered"], + }); + expect( + (ErrorCorrelator.getInstance() as any).errorMatches + .get(ZoweExplorerApiType.Mvs)["fake-type"].find((err) => err.summary === fakeErrorSummary) ).not.toBe(null); }); }); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 893807be3..1d5043e6c 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -71,14 +71,15 @@ export enum ZoweExplorerApiType { All = "all", } -export type ApiErrors = Partial>; +export type ErrorsForApiType = Map; +export type ApiErrors = Record; export class ErrorCorrelator extends Singleton { - private errorMatches: Map = new Map([ + private errorMatches: ErrorsForApiType = new Map([ [ - "zosmf", + ZoweExplorerApiType.Mvs, { - [ZoweExplorerApiType.Mvs]: [ + zosmf: [ { errorCode: "500", matches: ["Client is not authorized for file access.", /An I\/O abend was trapped\.(.+?)\n(.+?)__code=0x0913/], @@ -103,7 +104,12 @@ export class ErrorCorrelator extends Singleton { ], }, ], - [ZoweExplorerApiType.Uss]: [ + }, + ], + [ + ZoweExplorerApiType.Uss, + { + zosmf: [ { errorCode: "500", matches: ["Client is not authorized for file access."], @@ -120,7 +126,12 @@ export class ErrorCorrelator extends Singleton { tips: ["Ensure that the UNIX folder or file path is correct and try again."], }, ], - [ZoweExplorerApiType.Jes]: [ + }, + ], + [ + ZoweExplorerApiType.Jes, + { + zosmf: [ { matches: ["No job found for reference:"], summary: "The job modification request specified a job that does not exist.", @@ -141,7 +152,12 @@ export class ErrorCorrelator extends Singleton { summary: "The given Job ID is invalid. Please verify that the job ID is correct and try again.", }, ], - [ZoweExplorerApiType.All]: [ + }, + ], + [ + ZoweExplorerApiType.All, + { + any: [ { errorCode: "401", matches: ["Token is not valid or expired"], @@ -173,10 +189,10 @@ export class ErrorCorrelator extends Singleton { * @param correlation The correlation info (summary, tips, etc.) */ public addCorrelation(api: ZoweExplorerApiType, profileType: string, correlation: ErrorCorrelation): void { - const existingMatches = this.errorMatches.get(profileType); - this.errorMatches.set(profileType, { + const existingMatches = this.errorMatches.get(api); + this.errorMatches.set(api, { ...(existingMatches ?? {}), - [api]: [...(existingMatches?.[api] ?? []), correlation].filter(Boolean), + [profileType]: [...(existingMatches?.[profileType] ?? []), correlation].filter(Boolean), }); } @@ -189,13 +205,14 @@ export class ErrorCorrelator extends Singleton { * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary */ public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string, templateArgs?: Record): NetworkError { - if (!this.errorMatches.has(profileType)) { + if (!this.errorMatches.has(api)) { return new NetworkError({ summary: errorDetails }); } for (const apiError of [ - ...(this.errorMatches.get(profileType)?.[api] ?? []), - ...(api === ZoweExplorerApiType.All ? [] : this.errorMatches.get(profileType)[ZoweExplorerApiType.All] ?? []), + ...(this.errorMatches.get(api)?.[profileType] ?? []), + ...(this.errorMatches.get(api)?.any ?? []), + ...this.errorMatches.get(ZoweExplorerApiType.All).any, ]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { if (errorDetails.match(match)) { From 53e95185f8c67fd9560a0b8893e43914211a7280 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 14:02:02 -0400 Subject: [PATCH 21/62] wip: Prompt for creds when opening DS Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 60 +++++++++++-------- .../src/trees/dataset/DatasetFSProvider.ts | 29 ++++++--- .../src/trees/uss/UssFSProvider.ts | 3 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 5 +- .../src/utils/TroubleshootError.ts | 4 +- .../components/ErrorInfo.tsx | 8 +-- 6 files changed, 63 insertions(+), 46 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 1d5043e6c..d12b8bc02 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -43,12 +43,15 @@ export interface ErrorCorrelation { tips?: string[]; } -interface NetworkErrorInfo extends Omit { - /** - * The full error details sent by the server. - * @type {string} - */ - fullError?: string; +export interface NetworkErrorProps { + errorCode?: string; + correlation?: ErrorCorrelation; + error?: ImperativeError | string; +} + +export interface CorrelateErrorOpts { + profileType?: string; + templateArgs?: Record; } /** @@ -57,8 +60,8 @@ interface NetworkErrorInfo extends Omit { * Used to cache the error info such as tips, the match that was encountered and the full error message. */ export class NetworkError extends ImperativeError { - public constructor(public info: NetworkErrorInfo) { - super({ msg: info.summary }); + public constructor(public properties: NetworkErrorProps) { + super(properties?.error instanceof ImperativeError ? properties.error.mDetails : { msg: properties.error }); } } @@ -204,13 +207,14 @@ export class ErrorCorrelator extends Singleton { * @param errorDetails The full error details (usually `error.message`) * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary */ - public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string, templateArgs?: Record): NetworkError { + public correlateError(api: ZoweExplorerApiType, error: ImperativeError | string, opts?: CorrelateErrorOpts): NetworkError { + const errorDetails = error instanceof ImperativeError ? error.message : error; if (!this.errorMatches.has(api)) { - return new NetworkError({ summary: errorDetails }); + return new NetworkError({ error }); } for (const apiError of [ - ...(this.errorMatches.get(api)?.[profileType] ?? []), + ...(opts?.profileType ? this.errorMatches.get(api)?.[opts.profileType] ?? [] : []), ...(this.errorMatches.get(api)?.any ?? []), ...this.errorMatches.get(ZoweExplorerApiType.All).any, ]) { @@ -218,15 +222,17 @@ export class ErrorCorrelator extends Singleton { if (errorDetails.match(match)) { return new NetworkError({ errorCode: apiError.errorCode, - fullError: errorDetails, - summary: templateArgs ? Mustache.render(apiError.summary, templateArgs) : apiError.summary, - tips: apiError?.tips, + error, + correlation: { + ...apiError, + summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, + }, }); } } } - return new NetworkError({ summary: errorDetails }); + return new NetworkError({ error }); } /** @@ -239,18 +245,21 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean; stackTrace?: string }): Promise { - const errorCodeStr = error.info?.errorCode ? `(Error Code ${error.info.errorCode})` : ""; + public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean }): Promise { + const errorCodeStr = error.properties.errorCode ? `(Error Code ${error.properties.errorCode})` : ""; const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), }); // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - if (userSelection === "More info" && error.info?.fullError) { - const secondDialogSelection = await Gui.errorMessage(error.info.fullError, { - items: ["Show log", "Troubleshoot"], - }); + if (userSelection === "More info" && error.properties?.error) { + const secondDialogSelection = await Gui.errorMessage( + error.properties.error instanceof ImperativeError ? error.properties.error.message : error.properties.error, + { + items: ["Show log", "Troubleshoot"], + } + ); switch (secondDialogSelection) { // Reveal the output channel when the "Show log" option is selected @@ -258,7 +267,7 @@ export class ErrorCorrelator extends Singleton { return commands.executeCommand("zowe.revealOutputChannel"); // Show the troubleshooting webview when the "Troubleshoot" option is selected case "Troubleshoot": - return commands.executeCommand("zowe.troubleshootError", error, opts?.stackTrace); + return commands.executeCommand("zowe.troubleshootError", error, error.stack); default: return; } @@ -279,11 +288,10 @@ export class ErrorCorrelator extends Singleton { */ public async displayError( api: ZoweExplorerApiType, - profileType: string, - errorDetails: string, - opts?: { allowRetry?: boolean; stackTrace?: string } + errorDetails: string | ImperativeError, + opts?: { allowRetry?: boolean; profileType: string; stackTrace?: string } ): Promise { - const error = this.correlateError(api, profileType, errorDetails); + const error = this.correlateError(api, errorDetails, { profileType: opts.profileType }); return this.displayCorrelatedError(error, opts); } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 2f32db2c1..3fbb6ad5f 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -36,6 +36,7 @@ import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister import { ZoweLogger } from "../../tools/ZoweLogger"; import * as dayjs from "dayjs"; import { DatasetUtils } from "./DatasetUtils"; +import { AuthUtils } from "../../utils/AuthUtils"; export class DatasetFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: DatasetFSProvider; @@ -338,13 +339,24 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const bufBuilder = new BufferBuilder(); const metadata = file.metadata ?? this._getInfoFromUri(uri); const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); + + let resp; + try { + resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + } catch (err) { + const credsUpdated = await AuthUtils.errorHandling(err, { + profile: metadata.profile + }); + if (credsUpdated) { + return this.fetchDatasetAtUri(uri, options); + } + } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); if (options?.isConflict) { @@ -447,9 +459,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ZoweLogger.error(err.message); const userSelection = await ErrorCorrelator.getInstance().displayError( ZoweExplorerApiType.Mvs, - entry.metadata.profile.type, err.message, - { allowRetry: true, stackTrace: err.stack } + { allowRetry: true, profileType: entry.metadata.profile.type, stackTrace: err.stack } ); switch (userSelection) { diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 6256c2980..49463e0ec 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -397,9 +397,8 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv ZoweLogger.error(err.message); const userSelection = await ErrorCorrelator.getInstance().displayError( ZoweExplorerApiType.Uss, - entry.metadata.profile.type, err.message, - { allowRetry: true, stackTrace: err.stack } + { allowRetry: true, profileType: entry.metadata.profile.type, stackTrace: err.stack } ); switch (userSelection) { diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 05c45fe45..bf27205c3 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -82,10 +82,9 @@ export class AuthUtils { const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; const correlation = ErrorCorrelator.getInstance().correlateError( moreInfo?.apiType ?? ZoweExplorerApiType.All, - profile?.type, typeof errorDetails === "string" ? errorDetails : errorDetails.message, { - profileName: profile?.name, + profileType: profile?.type, ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), } ); @@ -108,7 +107,7 @@ export class AuthUtils { return false; } - ErrorCorrelator.getInstance().displayCorrelatedError(correlation); + await ErrorCorrelator.getInstance().displayCorrelatedError(correlation); return false; } diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index f1d16d4bc..292691378 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -36,8 +36,8 @@ export class TroubleshootError extends WebView { case "copy": await env.clipboard.writeText( this.errorData.error.stack - ? `Error details:\n${this.errorData.error.info.fullError}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` - : `Error details:\n${this.errorData.error.info.fullError}` + ? `Error details:\n${this.errorData.error.message}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` + : `Error details:\n${this.errorData.error.message}` ); break; default: diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 2db6058f3..039341b63 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -20,13 +20,13 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {

    Error details

    Code: - {error.info.errorCode ?? "Not available"} + {error.errorCode ?? "Not available"}

    Description:
    - {error.info.summary} + {error.properties.correlation?.summary}

    {
    - {error.info.tips ? : null} + {error.properties.correlation?.tips ? : null}

    Additional resources

      From 41d058ca1dd90e3bb7b70dea1f9e5c35edbf3826 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 16 Oct 2024 14:57:50 -0400 Subject: [PATCH 22/62] fix(api): Fix profile references being lost when cache is refreshed (#3248) * fix(api): Fix profile references being lost when cache is refreshed Signed-off-by: Timothy Johnson * fix: Pass profile instead of profile name for updating creds Signed-off-by: Trae Yelovich --------- Signed-off-by: Timothy Johnson Signed-off-by: Trae Yelovich Co-authored-by: Trae Yelovich --- .../profiles/ProfilesCache.unit.test.ts | 15 +++++ .../__unit__/tree/ZoweTreeNode.unit.test.ts | 57 ++++--------------- .../src/profiles/ProfilesCache.ts | 3 +- .../src/tree/ZoweTreeNode.ts | 19 +------ .../src/trees/dataset/ZoweDatasetNode.ts | 4 -- .../src/trees/job/ZoweJobNode.ts | 4 -- .../src/trees/uss/ZoweUSSNode.ts | 4 -- packages/zowe-explorer/src/utils/AuthUtils.ts | 2 +- 8 files changed, 31 insertions(+), 77 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts index dbfd4a0cd..fcb8af9e3 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ProfilesCache.unit.test.ts @@ -317,6 +317,21 @@ describe("ProfilesCache", () => { expect(profCache.getAllTypes()).toEqual([...profileTypes, "ssh", "base"]); }); + it("should refresh profile data for existing profile and keep object reference", async () => { + const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); + const profInfoSpy = jest.spyOn(profCache, "getProfileInfo").mockResolvedValue(createProfInfoMock([lpar1Profile, zftpProfile])); + await profCache.refresh(fakeApiRegister as unknown as Types.IApiRegisterClient); + expect(profCache.allProfiles.length).toEqual(2); + expect(profCache.allProfiles[0]).toMatchObject(lpar1Profile); + const oldZosmfProfile = profCache.allProfiles[0]; + const newZosmfProfile = { ...lpar1Profile, profile: lpar2Profile.profile }; + profInfoSpy.mockResolvedValue(createProfInfoMock([newZosmfProfile, zftpProfile])); + await profCache.refresh(fakeApiRegister as unknown as Types.IApiRegisterClient); + expect(profCache.allProfiles.length).toEqual(2); + expect(profCache.allProfiles[0]).toMatchObject(newZosmfProfile); + expect(oldZosmfProfile.profile).toEqual(newZosmfProfile.profile); + }); + it("should refresh profile data for and merge tokens with base profile", async () => { const profCache = new ProfilesCache(fakeLogger as unknown as imperative.Logger); jest.spyOn(profCache, "getProfileInfo").mockResolvedValue( diff --git a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts index c8a0ae561..dd438f559 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts @@ -48,14 +48,14 @@ describe("ZoweTreeNode", () => { it("getProfile should return profile of current node", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); - node.setProfileToChoice("myProfile" as unknown as imperative.IProfileLoaded); - expect(node.getProfile()).toBe("myProfile"); + node.setProfileToChoice({ name: "myProfile" } as unknown as imperative.IProfileLoaded); + expect(node.getProfile()).toEqual({ name: "myProfile" }); }); it("getProfile should return profile of parent node", () => { - const parentNode = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, "parentProfile"); + const parentNode = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { name: "parentProfile" }); const node = makeNode("test", vscode.TreeItemCollapsibleState.None, parentNode); - expect(node.getProfile()).toBe("parentProfile"); + expect(node.getProfile()).toEqual({ name: "parentProfile" }); }); it("getProfileName should return profile name of current node", () => { @@ -82,50 +82,13 @@ describe("ZoweTreeNode", () => { }); it("setProfileToChoice should update properties on existing profile object", () => { - const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { - name: "oldProfile", - profile: { host: "example.com" }, - }); + const origProfile: Partial = { name: "oldProfile", profile: { host: "example.com" } }; + const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, origProfile); node.setProfileToChoice({ name: "newProfile", profile: { host: "example.com", port: 443 } } as unknown as imperative.IProfileLoaded); - // Profile name should not change but properties should - expect(node.getProfileName()).toBe("oldProfile"); - expect(node.getProfile().profile?.port).toBeDefined(); - }); - - it("setProfileToChoice should update profile for associated FSProvider entry", () => { - const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); - node.resourceUri = vscode.Uri.file(__dirname); - const fsEntry = { - metadata: { - profile: { name: "oldProfile" }, - }, - }; - node.setProfileToChoice( - { name: "newProfile" } as unknown as imperative.IProfileLoaded, - { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider - ); + // Profile properties should be updated in original reference expect(node.getProfileName()).toBe("newProfile"); - expect(fsEntry.metadata.profile.name).toBe("newProfile"); - }); - - it("setProfileToChoice should update child nodes with the new profile", () => { - const node = makeNode("test", vscode.TreeItemCollapsibleState.Expanded, undefined); - const nodeChild = makeNode("child", vscode.TreeItemCollapsibleState.None, undefined); - node.children = [nodeChild as any]; - const setProfileToChoiceChildMock = jest.spyOn(nodeChild, "setProfileToChoice").mockImplementation(); - const fsEntry = { - metadata: { - profile: { name: "oldProfile" }, - }, - }; - const mockNewProfile = { name: "newProfile" } as unknown as imperative.IProfileLoaded; - const mockProvider = { - lookup: jest.fn().mockReturnValue(fsEntry), - } as unknown as BaseProvider; - node.setProfileToChoice(mockNewProfile, mockProvider); - expect(node.getProfileName()).toBe("newProfile"); - expect(setProfileToChoiceChildMock).toHaveBeenCalledWith(mockNewProfile, mockProvider); + expect(node.getProfile().profile?.port).toBeDefined(); + expect(origProfile.name).toBe("newProfile"); + expect(origProfile.profile?.port).toBeDefined(); }); }); diff --git a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts index 6645767c6..f6a933f2b 100644 --- a/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts +++ b/packages/zowe-explorer-api/src/profiles/ProfilesCache.ts @@ -184,7 +184,8 @@ export class ProfilesCache { } // Step 3: Update allProfiles list - tmpAllProfiles.push(profileFix); + const existingProfile = this.allProfiles.find((tmpProf) => tmpProf.name === prof.profName && tmpProf.type === prof.profType); + tmpAllProfiles.push(existingProfile ? Object.assign(existingProfile, profileFix) : profileFix); } allProfiles.push(...tmpAllProfiles); this.profilesByType.set(type, tmpAllProfiles); diff --git a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts index bdcc061f9..4d2f463de 100644 --- a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts @@ -100,22 +100,9 @@ export class ZoweTreeNode extends vscode.TreeItem { * * @param {imperative.IProfileLoaded} The profile you will set the node to use */ - public setProfileToChoice(aProfile: imperative.IProfileLoaded, fsProvider?: BaseProvider): void { - if (this.profile == null) { - this.profile = aProfile; - } else { - // Don't reassign profile, we want to keep object reference shared across nodes - this.profile.profile = aProfile.profile; - } - if (this.resourceUri != null) { - const fsEntry = fsProvider?.lookup(this.resourceUri, true); - if (fsEntry != null) { - fsEntry.metadata.profile = aProfile; - } - } - for (const child of this.children) { - (child as unknown as ZoweTreeNode).setProfileToChoice(aProfile, fsProvider); - } + public setProfileToChoice(aProfile: imperative.IProfileLoaded): void { + // Don't reassign profile directly, we want to keep object reference shared across nodes + this.profile = Object.assign(this.profile ?? {}, aProfile); } /** * Sets the session for this node to the one chosen in parameters. diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 0e355fd49..2a34f5bd8 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -194,10 +194,6 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return dsEntry.stats; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, DatasetFSProvider.instance); - } - /** * Retrieves child nodes of this ZoweDatasetNode * diff --git a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts index 1dfcdff51..b92236b06 100644 --- a/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts +++ b/packages/zowe-explorer/src/trees/job/ZoweJobNode.ts @@ -262,10 +262,6 @@ export class ZoweJobNode extends ZoweTreeNode implements IZoweJobTreeNode { return this.children; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, JobFSProvider.instance); - } - public static sortJobs(sortOpts: Sorting.NodeSort): (x: IZoweJobTreeNode, y: IZoweJobTreeNode) => number { return (x, y) => { const sortLessThan = sortOpts.direction == Sorting.SortDirection.Ascending ? -1 : 1; diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index 986f02bbb..97fafb3b0 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -161,10 +161,6 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { ussEntry.attributes = { ...ussEntry.attributes, ...attributes }; } - public setProfileToChoice(profile: imperative.IProfileLoaded): void { - super.setProfileToChoice(profile, UssFSProvider.instance); - } - public get onUpdate(): vscode.Event { return this.onUpdateEmitter.event; } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index bf27205c3..78838de97 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -51,7 +51,7 @@ export class AuthUtils { if (selection !== checkCredsButton) { return; } - return Constants.PROFILES_CACHE.promptCredentials(profile.name, true); + return Constants.PROFILES_CACHE.promptCredentials(profile, true); }); return creds != null ? true : false; } From 42fe233ef9e8c360eee0cf58b4ad95efbc3301cf Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 15:35:57 -0400 Subject: [PATCH 23/62] Revert "wip: Prompt for creds when opening DS" This reverts commit 53e95185f8c67fd9560a0b8893e43914211a7280. Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 60 ++++++++----------- .../src/trees/dataset/DatasetFSProvider.ts | 29 +++------ .../src/trees/uss/UssFSProvider.ts | 3 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 5 +- .../src/utils/TroubleshootError.ts | 4 +- .../components/ErrorInfo.tsx | 8 +-- 6 files changed, 46 insertions(+), 63 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index d12b8bc02..1d5043e6c 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -43,15 +43,12 @@ export interface ErrorCorrelation { tips?: string[]; } -export interface NetworkErrorProps { - errorCode?: string; - correlation?: ErrorCorrelation; - error?: ImperativeError | string; -} - -export interface CorrelateErrorOpts { - profileType?: string; - templateArgs?: Record; +interface NetworkErrorInfo extends Omit { + /** + * The full error details sent by the server. + * @type {string} + */ + fullError?: string; } /** @@ -60,8 +57,8 @@ export interface CorrelateErrorOpts { * Used to cache the error info such as tips, the match that was encountered and the full error message. */ export class NetworkError extends ImperativeError { - public constructor(public properties: NetworkErrorProps) { - super(properties?.error instanceof ImperativeError ? properties.error.mDetails : { msg: properties.error }); + public constructor(public info: NetworkErrorInfo) { + super({ msg: info.summary }); } } @@ -207,14 +204,13 @@ export class ErrorCorrelator extends Singleton { * @param errorDetails The full error details (usually `error.message`) * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary */ - public correlateError(api: ZoweExplorerApiType, error: ImperativeError | string, opts?: CorrelateErrorOpts): NetworkError { - const errorDetails = error instanceof ImperativeError ? error.message : error; + public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string, templateArgs?: Record): NetworkError { if (!this.errorMatches.has(api)) { - return new NetworkError({ error }); + return new NetworkError({ summary: errorDetails }); } for (const apiError of [ - ...(opts?.profileType ? this.errorMatches.get(api)?.[opts.profileType] ?? [] : []), + ...(this.errorMatches.get(api)?.[profileType] ?? []), ...(this.errorMatches.get(api)?.any ?? []), ...this.errorMatches.get(ZoweExplorerApiType.All).any, ]) { @@ -222,17 +218,15 @@ export class ErrorCorrelator extends Singleton { if (errorDetails.match(match)) { return new NetworkError({ errorCode: apiError.errorCode, - error, - correlation: { - ...apiError, - summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, - }, + fullError: errorDetails, + summary: templateArgs ? Mustache.render(apiError.summary, templateArgs) : apiError.summary, + tips: apiError?.tips, }); } } } - return new NetworkError({ error }); + return new NetworkError({ summary: errorDetails }); } /** @@ -245,21 +239,18 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean }): Promise { - const errorCodeStr = error.properties.errorCode ? `(Error Code ${error.properties.errorCode})` : ""; + public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean; stackTrace?: string }): Promise { + const errorCodeStr = error.info?.errorCode ? `(Error Code ${error.info.errorCode})` : ""; const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), }); // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - if (userSelection === "More info" && error.properties?.error) { - const secondDialogSelection = await Gui.errorMessage( - error.properties.error instanceof ImperativeError ? error.properties.error.message : error.properties.error, - { - items: ["Show log", "Troubleshoot"], - } - ); + if (userSelection === "More info" && error.info?.fullError) { + const secondDialogSelection = await Gui.errorMessage(error.info.fullError, { + items: ["Show log", "Troubleshoot"], + }); switch (secondDialogSelection) { // Reveal the output channel when the "Show log" option is selected @@ -267,7 +258,7 @@ export class ErrorCorrelator extends Singleton { return commands.executeCommand("zowe.revealOutputChannel"); // Show the troubleshooting webview when the "Troubleshoot" option is selected case "Troubleshoot": - return commands.executeCommand("zowe.troubleshootError", error, error.stack); + return commands.executeCommand("zowe.troubleshootError", error, opts?.stackTrace); default: return; } @@ -288,10 +279,11 @@ export class ErrorCorrelator extends Singleton { */ public async displayError( api: ZoweExplorerApiType, - errorDetails: string | ImperativeError, - opts?: { allowRetry?: boolean; profileType: string; stackTrace?: string } + profileType: string, + errorDetails: string, + opts?: { allowRetry?: boolean; stackTrace?: string } ): Promise { - const error = this.correlateError(api, errorDetails, { profileType: opts.profileType }); + const error = this.correlateError(api, profileType, errorDetails); return this.displayCorrelatedError(error, opts); } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 3fbb6ad5f..2f32db2c1 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -36,7 +36,6 @@ import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister import { ZoweLogger } from "../../tools/ZoweLogger"; import * as dayjs from "dayjs"; import { DatasetUtils } from "./DatasetUtils"; -import { AuthUtils } from "../../utils/AuthUtils"; export class DatasetFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: DatasetFSProvider; @@ -339,24 +338,13 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const bufBuilder = new BufferBuilder(); const metadata = file.metadata ?? this._getInfoFromUri(uri); const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - - let resp; - try { - resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); - } catch (err) { - const credsUpdated = await AuthUtils.errorHandling(err, { - profile: metadata.profile - }); - if (credsUpdated) { - return this.fetchDatasetAtUri(uri, options); - } - } + const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); if (options?.isConflict) { @@ -459,8 +447,9 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ZoweLogger.error(err.message); const userSelection = await ErrorCorrelator.getInstance().displayError( ZoweExplorerApiType.Mvs, + entry.metadata.profile.type, err.message, - { allowRetry: true, profileType: entry.metadata.profile.type, stackTrace: err.stack } + { allowRetry: true, stackTrace: err.stack } ); switch (userSelection) { diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 49463e0ec..6256c2980 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -397,8 +397,9 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv ZoweLogger.error(err.message); const userSelection = await ErrorCorrelator.getInstance().displayError( ZoweExplorerApiType.Uss, + entry.metadata.profile.type, err.message, - { allowRetry: true, profileType: entry.metadata.profile.type, stackTrace: err.stack } + { allowRetry: true, stackTrace: err.stack } ); switch (userSelection) { diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 78838de97..12ebee84f 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -82,9 +82,10 @@ export class AuthUtils { const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; const correlation = ErrorCorrelator.getInstance().correlateError( moreInfo?.apiType ?? ZoweExplorerApiType.All, + profile?.type, typeof errorDetails === "string" ? errorDetails : errorDetails.message, { - profileType: profile?.type, + profileName: profile?.name, ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), } ); @@ -107,7 +108,7 @@ export class AuthUtils { return false; } - await ErrorCorrelator.getInstance().displayCorrelatedError(correlation); + ErrorCorrelator.getInstance().displayCorrelatedError(correlation); return false; } diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index 292691378..f1d16d4bc 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -36,8 +36,8 @@ export class TroubleshootError extends WebView { case "copy": await env.clipboard.writeText( this.errorData.error.stack - ? `Error details:\n${this.errorData.error.message}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` - : `Error details:\n${this.errorData.error.message}` + ? `Error details:\n${this.errorData.error.info.fullError}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` + : `Error details:\n${this.errorData.error.info.fullError}` ); break; default: diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 039341b63..2db6058f3 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -20,13 +20,13 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {

      Error details

      Code: - {error.errorCode ?? "Not available"} + {error.info.errorCode ?? "Not available"}

      Description:
      - {error.properties.correlation?.summary} + {error.info.summary}

      {
      - {error.properties.correlation?.tips ? : null} + {error.info.tips ? : null}

      Additional resources

        From fdc6874c5ac55a873fd86a7cf89894f40170f6f3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 15:47:54 -0400 Subject: [PATCH 24/62] refactor: Add back error correlator changes Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 60 +++++++++++-------- .../src/trees/dataset/DatasetFSProvider.ts | 11 ++-- .../src/trees/uss/UssFSProvider.ts | 11 ++-- packages/zowe-explorer/src/utils/AuthUtils.ts | 5 +- .../src/utils/TroubleshootError.ts | 4 +- .../components/ErrorInfo.tsx | 8 +-- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 1d5043e6c..d12b8bc02 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -43,12 +43,15 @@ export interface ErrorCorrelation { tips?: string[]; } -interface NetworkErrorInfo extends Omit { - /** - * The full error details sent by the server. - * @type {string} - */ - fullError?: string; +export interface NetworkErrorProps { + errorCode?: string; + correlation?: ErrorCorrelation; + error?: ImperativeError | string; +} + +export interface CorrelateErrorOpts { + profileType?: string; + templateArgs?: Record; } /** @@ -57,8 +60,8 @@ interface NetworkErrorInfo extends Omit { * Used to cache the error info such as tips, the match that was encountered and the full error message. */ export class NetworkError extends ImperativeError { - public constructor(public info: NetworkErrorInfo) { - super({ msg: info.summary }); + public constructor(public properties: NetworkErrorProps) { + super(properties?.error instanceof ImperativeError ? properties.error.mDetails : { msg: properties.error }); } } @@ -204,13 +207,14 @@ export class ErrorCorrelator extends Singleton { * @param errorDetails The full error details (usually `error.message`) * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary */ - public correlateError(api: ZoweExplorerApiType, profileType: string, errorDetails: string, templateArgs?: Record): NetworkError { + public correlateError(api: ZoweExplorerApiType, error: ImperativeError | string, opts?: CorrelateErrorOpts): NetworkError { + const errorDetails = error instanceof ImperativeError ? error.message : error; if (!this.errorMatches.has(api)) { - return new NetworkError({ summary: errorDetails }); + return new NetworkError({ error }); } for (const apiError of [ - ...(this.errorMatches.get(api)?.[profileType] ?? []), + ...(opts?.profileType ? this.errorMatches.get(api)?.[opts.profileType] ?? [] : []), ...(this.errorMatches.get(api)?.any ?? []), ...this.errorMatches.get(ZoweExplorerApiType.All).any, ]) { @@ -218,15 +222,17 @@ export class ErrorCorrelator extends Singleton { if (errorDetails.match(match)) { return new NetworkError({ errorCode: apiError.errorCode, - fullError: errorDetails, - summary: templateArgs ? Mustache.render(apiError.summary, templateArgs) : apiError.summary, - tips: apiError?.tips, + error, + correlation: { + ...apiError, + summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, + }, }); } } } - return new NetworkError({ summary: errorDetails }); + return new NetworkError({ error }); } /** @@ -239,18 +245,21 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean; stackTrace?: string }): Promise { - const errorCodeStr = error.info?.errorCode ? `(Error Code ${error.info.errorCode})` : ""; + public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean }): Promise { + const errorCodeStr = error.properties.errorCode ? `(Error Code ${error.properties.errorCode})` : ""; const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), }); // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - if (userSelection === "More info" && error.info?.fullError) { - const secondDialogSelection = await Gui.errorMessage(error.info.fullError, { - items: ["Show log", "Troubleshoot"], - }); + if (userSelection === "More info" && error.properties?.error) { + const secondDialogSelection = await Gui.errorMessage( + error.properties.error instanceof ImperativeError ? error.properties.error.message : error.properties.error, + { + items: ["Show log", "Troubleshoot"], + } + ); switch (secondDialogSelection) { // Reveal the output channel when the "Show log" option is selected @@ -258,7 +267,7 @@ export class ErrorCorrelator extends Singleton { return commands.executeCommand("zowe.revealOutputChannel"); // Show the troubleshooting webview when the "Troubleshoot" option is selected case "Troubleshoot": - return commands.executeCommand("zowe.troubleshootError", error, opts?.stackTrace); + return commands.executeCommand("zowe.troubleshootError", error, error.stack); default: return; } @@ -279,11 +288,10 @@ export class ErrorCorrelator extends Singleton { */ public async displayError( api: ZoweExplorerApiType, - profileType: string, - errorDetails: string, - opts?: { allowRetry?: boolean; stackTrace?: string } + errorDetails: string | ImperativeError, + opts?: { allowRetry?: boolean; profileType: string; stackTrace?: string } ): Promise { - const error = this.correlateError(api, profileType, errorDetails); + const error = this.correlateError(api, errorDetails, { profileType: opts.profileType }); return this.displayCorrelatedError(error, opts); } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 2f32db2c1..78c509391 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -445,12 +445,11 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().displayError( - ZoweExplorerApiType.Mvs, - entry.metadata.profile.type, - err.message, - { allowRetry: true, stackTrace: err.stack } - ); + const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, err.message, { + allowRetry: true, + profileType: entry.metadata.profile.type, + stackTrace: err.stack, + }); switch (userSelection) { case "Retry": diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 6256c2980..9358fa8f8 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -395,12 +395,11 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().displayError( - ZoweExplorerApiType.Uss, - entry.metadata.profile.type, - err.message, - { allowRetry: true, stackTrace: err.stack } - ); + const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Uss, err.message, { + allowRetry: true, + profileType: entry.metadata.profile.type, + stackTrace: err.stack, + }); switch (userSelection) { case "Retry": diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 12ebee84f..78838de97 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -82,10 +82,9 @@ export class AuthUtils { const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; const correlation = ErrorCorrelator.getInstance().correlateError( moreInfo?.apiType ?? ZoweExplorerApiType.All, - profile?.type, typeof errorDetails === "string" ? errorDetails : errorDetails.message, { - profileName: profile?.name, + profileType: profile?.type, ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), } ); @@ -108,7 +107,7 @@ export class AuthUtils { return false; } - ErrorCorrelator.getInstance().displayCorrelatedError(correlation); + await ErrorCorrelator.getInstance().displayCorrelatedError(correlation); return false; } diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index f1d16d4bc..292691378 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -36,8 +36,8 @@ export class TroubleshootError extends WebView { case "copy": await env.clipboard.writeText( this.errorData.error.stack - ? `Error details:\n${this.errorData.error.info.fullError}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` - : `Error details:\n${this.errorData.error.info.fullError}` + ? `Error details:\n${this.errorData.error.message}\nStack trace:\n${this.errorData.error.stack.replace(/(.+?)\n/, "")}` + : `Error details:\n${this.errorData.error.message}` ); break; default: diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 2db6058f3..e94ca6530 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -20,13 +20,13 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {

        Error details

        Code: - {error.info.errorCode ?? "Not available"} + {error.errorCode ?? "Not available"}

        Description:
        - {error.info.summary} + {error.properties.correlation?.summary}

        {
        - {error.info.tips ? : null} + {error.properties.correlation?.tips ? : null}

        Additional resources

          From e342fecae3eb767d354ba52f53739c626923557c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 16:10:05 -0400 Subject: [PATCH 25/62] refactor: fix tests to handle new format Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 65 ++++++++++--------- .../src/utils/ErrorCorrelator.ts | 15 +++-- .../src/trees/dataset/DatasetFSProvider.ts | 2 +- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 67134d7d8..5945a80ec 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -33,7 +33,8 @@ describe("addCorrelation", () => { }); expect( (ErrorCorrelator.getInstance() as any).errorMatches - .get(ZoweExplorerApiType.Mvs)["fake-type"].find((err) => err.summary === fakeErrorSummary) + .get(ZoweExplorerApiType.Mvs) + ["fake-type"].find((err) => err.summary === fakeErrorSummary) ).not.toBe(null); }); }); @@ -41,27 +42,32 @@ describe("addCorrelation", () => { describe("correlateError", () => { it("correctly correlates an error in the list of error matches", () => { expect( - ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "zosmf", "Client is not authorized for file access.") + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "Client is not authorized for file access.", { + profileType: "zosmf", + }) ).toStrictEqual( new NetworkError({ - errorCode: "500", - summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", - tips: [ - "Check that your user or group has the appropriate permissions for this data set.", - "Ensure that the data set is not opened within a mainframe editor tool.", - ], + correlation: { + errorCode: "500", + summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", + tips: [ + "Check that your user or group has the appropriate permissions for this data set.", + "Ensure that the data set is not opened within a mainframe editor tool.", + ], + }, + error: "Client is not authorized for file access.", }) ); }); it("returns a generic NetworkError if no matches are available for the given profile type", () => { - expect(ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "nonsense", "Some error details")).toStrictEqual( - new NetworkError({ summary: "Some error details" }) - ); + expect( + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "nonsense" }) + ).toStrictEqual(new NetworkError({ error: "Some error details" })); }); it("returns a generic NetworkError with the full error details if no matches are found", () => { expect( - ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "zosmf", "A cryptic error with no available match") - ).toStrictEqual(new NetworkError({ summary: "A cryptic error with no available match" })); + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "A cryptic error with no available match", { profileType: "zosmf" }) + ).toStrictEqual(new NetworkError({ error: "A cryptic error with no available match" })); }); }); @@ -69,56 +75,57 @@ describe("displayError", () => { it("calls correlateError to get an error correlation", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error" })); + .mockReturnValueOnce(new NetworkError({ error: "Summary of network error" })); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); }); it("presents an additional dialog when the user selects 'More info'", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce(undefined); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); }); it("opens the Zowe Explorer output channel when the user selects 'Show log'", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Show log"); const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); expect(executeCommandMock).toHaveBeenCalledWith("zowe.revealOutputChannel"); executeCommandMock.mockRestore(); }); it("opens the troubleshoot webview if the user selects 'Troubleshoot'", async () => { - const networkError = new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" }); + const networkError = new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" }); const correlateErrorMock = jest.spyOn(ErrorCorrelator.prototype, "correlateError").mockReturnValueOnce(networkError); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Troubleshoot"); const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); - expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", networkError, undefined); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", networkError, networkError.stack); executeCommandMock.mockRestore(); }); it("returns 'Retry' whenever the user selects 'Retry'", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ summary: "Summary of network error", fullError: "This is the full error message" })); + .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Retry"); - const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "zosmf", "Some error details", { + const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { allowRetry: true, + profileType: "zosmf", }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "zosmf", "Some error details"); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["Retry", "More info"] }); expect(userResponse).toBe("Retry"); }); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index d12b8bc02..69b643abd 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -45,7 +45,7 @@ export interface ErrorCorrelation { export interface NetworkErrorProps { errorCode?: string; - correlation?: ErrorCorrelation; + correlation?: Omit; error?: ImperativeError | string; } @@ -247,9 +247,14 @@ export class ErrorCorrelator extends Singleton { */ public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean }): Promise { const errorCodeStr = error.properties.errorCode ? `(Error Code ${error.properties.errorCode})` : ""; - const userSelection = await Gui.errorMessage(`${error.mDetails.msg.trim()} ${errorCodeStr}`.trim(), { - items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), - }); + const userSelection = await Gui.errorMessage( + `${ + error.properties.correlation ? error.properties.correlation.summary.trim() : error.properties?.error.toString() + } ${errorCodeStr}`.trim(), + { + items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), + } + ); // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options @@ -289,7 +294,7 @@ export class ErrorCorrelator extends Singleton { public async displayError( api: ZoweExplorerApiType, errorDetails: string | ImperativeError, - opts?: { allowRetry?: boolean; profileType: string; stackTrace?: string } + opts?: { allowRetry?: boolean; profileType?: string; stackTrace?: string } ): Promise { const error = this.correlateError(api, errorDetails, { profileType: opts.profileType }); return this.displayCorrelatedError(error, opts); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 78c509391..5dc601035 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -445,7 +445,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem statusMsg.dispose(); if (err instanceof imperative.ImperativeError) { ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, err.message, { + const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, err, { allowRetry: true, profileType: entry.metadata.profile.type, stackTrace: err.stack, From 2b2ee7b4071ae91915a74dd883bb76ddb34a8189 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 17:04:05 -0400 Subject: [PATCH 26/62] tests: TroubleshootError webview class Signed-off-by: Trae Yelovich --- .../utils/TroubleshootError.unit.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts new file mode 100644 index 000000000..8c17447d6 --- /dev/null +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -0,0 +1,48 @@ +import { env, ExtensionContext } from "vscode"; +import { TroubleshootError } from "../../../src/utils/TroubleshootError"; +import { NetworkError } from "@zowe/zowe-explorer-api"; + +describe("TroubleshootError", () => { + function getGlobalMocks() { + const context = { + extensionPath: "/a/b/c/zowe-explorer", + subscriptions: [], + } as unknown as ExtensionContext; + const errorData = { + error: new NetworkError({ error: "test error" }), + stackTrace: "test stack trace", + }; + const troubleshootError = new TroubleshootError(context, errorData); + + return { + context, + errorData, + troubleshootError, + }; + } + describe("onDidReceiveMessage", () => { + it("handles copy command", async () => { + const { errorData, troubleshootError } = getGlobalMocks(); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + await troubleshootError.onDidReceiveMessage({ command: "copy" }); + expect(writeTextMock).toHaveBeenCalledWith( + `Error details:\n${errorData.error.message}\nStack trace:\n${errorData.error.stack.replace(/(.+?)\n/, "")}` + ); + }); + it("handles ready command", async () => { + const { troubleshootError } = getGlobalMocks(); + const sendErrorDataSpy = jest.spyOn(troubleshootError, "sendErrorData"); + await troubleshootError.onDidReceiveMessage({ command: "ready" }); + expect(sendErrorDataSpy).toHaveBeenCalledWith(troubleshootError.errorData); + }); + }); + + describe("sendErrorData", () => { + it("sends error data to the webview", async () => { + const { errorData, troubleshootError } = getGlobalMocks(); + const postMessageSpy = jest.spyOn(troubleshootError.panel.webview, "postMessage"); + await troubleshootError.sendErrorData(errorData); + expect(postMessageSpy).toHaveBeenCalledWith(errorData); + }); + }); +}); From 971a44e954fef6d2b174f15f86165592702fd476 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 17:04:25 -0400 Subject: [PATCH 27/62] refactor: rename TroubleshootError.setErrorData -> sendErrorData Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/utils/TroubleshootError.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index 292691378..9a75b971e 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -31,7 +31,7 @@ export class TroubleshootError extends WebView { switch (message.command) { case "ready": - await this.setErrorData(this.errorData); + await this.sendErrorData(this.errorData); break; case "copy": await env.clipboard.writeText( @@ -51,7 +51,7 @@ export class TroubleshootError extends WebView { * @param errorData Error and stack trace * @returns Whether Zowe Explorer successfully sent the data to the webview */ - public async setErrorData(errorData: TroubleshootData): Promise { + public async sendErrorData(errorData: TroubleshootData): Promise { return this.panel.webview.postMessage({ error: errorData.error, stackTrace: errorData.stackTrace, From 6dbc5195b3ce2bfc05f6fac3ab7f346f27c9f3fe Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 17:12:28 -0400 Subject: [PATCH 28/62] remaining TroubleshootError cases, add log for unknown cmd Signed-off-by: Trae Yelovich --- .../utils/TroubleshootError.unit.test.ts | 18 +++++++++++++++++- .../src/utils/TroubleshootError.ts | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts index 8c17447d6..c24826b87 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -1,6 +1,7 @@ import { env, ExtensionContext } from "vscode"; import { TroubleshootError } from "../../../src/utils/TroubleshootError"; import { NetworkError } from "@zowe/zowe-explorer-api"; +import { ZoweLogger } from "../../../src/tools/ZoweLogger"; describe("TroubleshootError", () => { function getGlobalMocks() { @@ -21,7 +22,7 @@ describe("TroubleshootError", () => { }; } describe("onDidReceiveMessage", () => { - it("handles copy command", async () => { + it("handles copy command for error with stack trace", async () => { const { errorData, troubleshootError } = getGlobalMocks(); const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); await troubleshootError.onDidReceiveMessage({ command: "copy" }); @@ -29,12 +30,27 @@ describe("TroubleshootError", () => { `Error details:\n${errorData.error.message}\nStack trace:\n${errorData.error.stack.replace(/(.+?)\n/, "")}` ); }); + + it("handles copy command for error without stack trace", async () => { + const { errorData, troubleshootError } = getGlobalMocks(); + Object.defineProperty(errorData.error, "stack", { value: undefined }); + const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); + await troubleshootError.onDidReceiveMessage({ command: "copy" }); + expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${errorData.error.message}`); + }); + it("handles ready command", async () => { const { troubleshootError } = getGlobalMocks(); const sendErrorDataSpy = jest.spyOn(troubleshootError, "sendErrorData"); await troubleshootError.onDidReceiveMessage({ command: "ready" }); expect(sendErrorDataSpy).toHaveBeenCalledWith(troubleshootError.errorData); }); + it("handles an unrecognized command", async () => { + const { troubleshootError } = getGlobalMocks(); + const debugSpy = jest.spyOn(ZoweLogger, "debug"); + await troubleshootError.onDidReceiveMessage({ command: "unknown" }); + expect(debugSpy).toHaveBeenCalledWith("[TroubleshootError] Unknown command: unknown"); + }); }); describe("sendErrorData", () => { diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index 9a75b971e..f7e3f33ac 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -11,6 +11,7 @@ import { NetworkError, WebView } from "@zowe/zowe-explorer-api"; import { env, ExtensionContext, l10n } from "vscode"; +import { ZoweLogger } from "../tools/ZoweLogger"; type TroubleshootData = { error: NetworkError; @@ -41,6 +42,7 @@ export class TroubleshootError extends WebView { ); break; default: + ZoweLogger.debug(`[TroubleshootError] Unknown command: ${message.command as string}`); break; } } From 9c93f4611990c3fec942685a4990dc49aa6e56a0 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 18:32:48 -0400 Subject: [PATCH 29/62] refactor: NetworkError -> CorrelatedError; cleanup class, fix type guard Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 69 ++++++++------ .../src/utils/ErrorCorrelator.ts | 89 ++++++++++++------- .../utils/TroubleshootError.unit.test.ts | 15 +++- .../src/trees/shared/SharedInit.ts | 4 +- packages/zowe-explorer/src/utils/AuthUtils.ts | 4 +- .../src/utils/TroubleshootError.ts | 4 +- .../webviews/src/troubleshoot-error/App.tsx | 4 +- .../components/ErrorInfo.tsx | 8 +- 8 files changed, 121 insertions(+), 76 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 5945a80ec..fa3e35980 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { ErrorCorrelator, Gui, NetworkError, ZoweExplorerApiType } from "../../../src/"; +import { ErrorCorrelator, Gui, CorrelatedError, ZoweExplorerApiType } from "../../../src/"; import { commands } from "vscode"; describe("addCorrelation", () => { @@ -46,7 +46,7 @@ describe("correlateError", () => { profileType: "zosmf", }) ).toStrictEqual( - new NetworkError({ + new CorrelatedError({ correlation: { errorCode: "500", summary: "Insufficient write permissions for this data set. The data set may be read-only or locked.", @@ -55,19 +55,19 @@ describe("correlateError", () => { "Ensure that the data set is not opened within a mainframe editor tool.", ], }, - error: "Client is not authorized for file access.", + initialError: "Client is not authorized for file access.", }) ); }); - it("returns a generic NetworkError if no matches are available for the given profile type", () => { + it("returns a generic CorrelatedError if no matches are available for the given profile type", () => { expect( - ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "nonsense" }) - ).toStrictEqual(new NetworkError({ error: "Some error details" })); + ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "nonsense" }) + ).toStrictEqual(new CorrelatedError({ initialError: "This is the full error message" })); }); - it("returns a generic NetworkError with the full error details if no matches are found", () => { + it("returns a generic CorrelatedError with the full error details if no matches are found", () => { expect( ErrorCorrelator.getInstance().correlateError(ZoweExplorerApiType.Mvs, "A cryptic error with no available match", { profileType: "zosmf" }) - ).toStrictEqual(new NetworkError({ error: "A cryptic error with no available match" })); + ).toStrictEqual(new CorrelatedError({ initialError: "A cryptic error with no available match" })); }); }); @@ -75,58 +75,71 @@ describe("displayError", () => { it("calls correlateError to get an error correlation", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ error: "Summary of network error" })); + .mockReturnValueOnce(new CorrelatedError({ initialError: "Summary of network error" })); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); }); it("presents an additional dialog when the user selects 'More info'", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce(undefined); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); }); it("opens the Zowe Explorer output channel when the user selects 'Show log'", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Show log"); const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); expect(executeCommandMock).toHaveBeenCalledWith("zowe.revealOutputChannel"); executeCommandMock.mockRestore(); }); it("opens the troubleshoot webview if the user selects 'Troubleshoot'", async () => { - const networkError = new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" }); - const correlateErrorMock = jest.spyOn(ErrorCorrelator.prototype, "correlateError").mockReturnValueOnce(networkError); + const error = new CorrelatedError({ + correlation: { summary: "Summary of network error" }, + initialError: "This is the full error message", + }); + const correlateErrorMock = jest.spyOn(ErrorCorrelator.getInstance(), "correlateError").mockReturnValueOnce(error); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("More info").mockResolvedValueOnce("Troubleshoot"); const executeCommandMock = jest.spyOn(commands, "executeCommand").mockImplementation(); - await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); + await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["More info"] }); expect(errorMessageMock).toHaveBeenCalledWith("This is the full error message", { items: ["Show log", "Troubleshoot"] }); - expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", networkError, networkError.stack); + expect(executeCommandMock).toHaveBeenCalledWith("zowe.troubleshootError", error, error.stack); executeCommandMock.mockRestore(); }); +}); + +describe("displayCorrelatedError", () => { it("returns 'Retry' whenever the user selects 'Retry'", async () => { - const correlateErrorMock = jest - .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new NetworkError({ correlation: { summary: "Summary of network error" }, error: "This is the full error message" })); + const error = new CorrelatedError({ + correlation: { summary: "Summary of network error" }, + initialError: "This is the full error message", + }); + const correlateErrorMock = jest.spyOn(ErrorCorrelator.getInstance(), "correlateError").mockReturnValueOnce(error); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Retry"); - const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "Some error details", { + const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { + additionalContext: "Some additional context", allowRetry: true, profileType: "zosmf", }); - expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "Some error details", { profileType: "zosmf" }); - expect(errorMessageMock).toHaveBeenCalledWith("Summary of network error", { items: ["Retry", "More info"] }); + expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); + expect(errorMessageMock).toHaveBeenCalledWith("Some additional context: Summary of network error", { items: ["Retry", "More info"] }); expect(userResponse).toBe("Retry"); }); }); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 69b643abd..4d6ff2495 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -9,11 +9,11 @@ * */ -import { ImperativeError } from "@zowe/imperative"; import { Singleton } from "./Singleton"; import { Gui } from "../globals"; import { commands } from "vscode"; import Mustache = require("mustache"); +import { ImperativeError } from "@zowe/imperative"; /** * Error match type (substring of error, or regular expression to match against error text) @@ -43,10 +43,10 @@ export interface ErrorCorrelation { tips?: string[]; } -export interface NetworkErrorProps { +export interface CorrelatedErrorProps { errorCode?: string; correlation?: Omit; - error?: ImperativeError | string; + initialError: Error | string; } export interface CorrelateErrorOpts { @@ -54,14 +54,43 @@ export interface CorrelateErrorOpts { templateArgs?: Record; } +export interface DisplayErrorOpts extends CorrelateErrorOpts { + additionalContext?: string; + allowRetry?: boolean; +} + +export interface DisplayCorrelatedErrorOpts extends Omit {} + /** - * Network error wrapper around the `ImperativeError` class. + * Representation of the given error as a correlated error (wrapper around the `Error` class). * * Used to cache the error info such as tips, the match that was encountered and the full error message. */ -export class NetworkError extends ImperativeError { - public constructor(public properties: NetworkErrorProps) { - super(properties?.error instanceof ImperativeError ? properties.error.mDetails : { msg: properties.error }); +export class CorrelatedError { + public constructor(public properties: CorrelatedErrorProps) { + if (properties.initialError instanceof Error) { + // preserve stack from initial error + } + } + + public get stack(): string | undefined { + return this.initial instanceof Error ? this.initial.stack : undefined; + } + + public get errorCode(): string | undefined { + return this.initial instanceof ImperativeError ? this.initial.errorCode : this.properties.errorCode ?? undefined; + } + + public get message(): string { + if (this.properties.correlation) { + return this.properties.correlation.summary; + } + + return this.properties.initialError instanceof Error ? this.properties.initialError.message : this.properties.initialError; + } + + public get initial(): Error | string { + return this.properties.initialError; } } @@ -205,12 +234,12 @@ export class ErrorCorrelator extends Singleton { * @param api The API type where the error was encountered * @param profileType The profile type in use * @param errorDetails The full error details (usually `error.message`) - * @returns A matching `NetworkError`, or a generic `NetworkError` with the full error details as the summary + * @returns A matching `CorrelatedError`, or a generic `CorrelatedError` with the full error details as the summary */ - public correlateError(api: ZoweExplorerApiType, error: ImperativeError | string, opts?: CorrelateErrorOpts): NetworkError { - const errorDetails = error instanceof ImperativeError ? error.message : error; + public correlateError(api: ZoweExplorerApiType, error: string | Error, opts?: CorrelateErrorOpts): CorrelatedError { + const errorDetails = error instanceof Error ? error.message : error; if (!this.errorMatches.has(api)) { - return new NetworkError({ error }); + return new CorrelatedError({ initialError: error }); } for (const apiError of [ @@ -219,10 +248,10 @@ export class ErrorCorrelator extends Singleton { ...this.errorMatches.get(ZoweExplorerApiType.All).any, ]) { for (const match of Array.isArray(apiError.matches) ? apiError.matches : [apiError.matches]) { - if (errorDetails.match(match)) { - return new NetworkError({ + if (errorDetails.toString().match(match)) { + return new CorrelatedError({ errorCode: apiError.errorCode, - error, + initialError: error, correlation: { ...apiError, summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, @@ -232,7 +261,7 @@ export class ErrorCorrelator extends Singleton { } } - return new NetworkError({ error }); + return new CorrelatedError({ initialError: error }); } /** @@ -245,12 +274,10 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayCorrelatedError(error: NetworkError, opts?: { allowRetry?: boolean }): Promise { - const errorCodeStr = error.properties.errorCode ? `(Error Code ${error.properties.errorCode})` : ""; + public async displayCorrelatedError(error: CorrelatedError, opts?: DisplayCorrelatedErrorOpts): Promise { + const errorCodeStr = error.properties.errorCode ? ` (Error Code ${error.properties.errorCode})` : ""; const userSelection = await Gui.errorMessage( - `${ - error.properties.correlation ? error.properties.correlation.summary.trim() : error.properties?.error.toString() - } ${errorCodeStr}`.trim(), + `${opts?.additionalContext ? opts.additionalContext + ": " : ""}${error.message}${errorCodeStr}`.trim(), { items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), } @@ -258,13 +285,11 @@ export class ErrorCorrelator extends Singleton { // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - if (userSelection === "More info" && error.properties?.error) { - const secondDialogSelection = await Gui.errorMessage( - error.properties.error instanceof ImperativeError ? error.properties.error.message : error.properties.error, - { - items: ["Show log", "Troubleshoot"], - } - ); + if (userSelection === "More info") { + const fullErrorMsg = error.initial instanceof Error ? error.initial.message : error.initial; + const secondDialogSelection = await Gui.errorMessage(fullErrorMsg, { + items: ["Show log", "Troubleshoot"], + }); switch (secondDialogSelection) { // Reveal the output channel when the "Show log" option is selected @@ -291,12 +316,8 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayError( - api: ZoweExplorerApiType, - errorDetails: string | ImperativeError, - opts?: { allowRetry?: boolean; profileType?: string; stackTrace?: string } - ): Promise { - const error = this.correlateError(api, errorDetails, { profileType: opts.profileType }); - return this.displayCorrelatedError(error, opts); + public async displayError(api: ZoweExplorerApiType, errorDetails: string | Error, opts?: DisplayErrorOpts): Promise { + const error = this.correlateError(api, errorDetails, { profileType: opts?.profileType, templateArgs: opts?.templateArgs }); + return this.displayCorrelatedError(error, { additionalContext: opts?.additionalContext, allowRetry: opts?.allowRetry }); } } diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts index c24826b87..ec0a48555 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -1,6 +1,17 @@ +/** + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + * + */ + import { env, ExtensionContext } from "vscode"; import { TroubleshootError } from "../../../src/utils/TroubleshootError"; -import { NetworkError } from "@zowe/zowe-explorer-api"; +import { CorrelatedError } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../../../src/tools/ZoweLogger"; describe("TroubleshootError", () => { @@ -10,7 +21,7 @@ describe("TroubleshootError", () => { subscriptions: [], } as unknown as ExtensionContext; const errorData = { - error: new NetworkError({ error: "test error" }), + error: new CorrelatedError({ error: "test error" }), stackTrace: "test stack trace", }; const troubleshootError = new TroubleshootError(context, errorData); diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index b55e17979..fdc8106f1 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -13,7 +13,7 @@ import * as vscode from "vscode"; import { FileManagement, Gui, - NetworkError, + CorrelatedError, IZoweTree, IZoweTreeNode, TableViewProvider, @@ -286,7 +286,7 @@ export class SharedInit { context.subscriptions.push( vscode.commands.registerCommand( "zowe.troubleshootError", - (error: NetworkError, stackTrace?: string) => new TroubleshootError(context, { error, stackTrace }) + (error: CorrelatedError, stackTrace?: string) => new TroubleshootError(context, { error, stackTrace }) ) ); context.subscriptions.push( diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 78838de97..903fc969b 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -11,7 +11,7 @@ import * as util from "util"; import * as vscode from "vscode"; -import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, NetworkError } from "@zowe/zowe-explorer-api"; +import { imperative, Gui, MainframeInteraction, IZoweTreeNode, ErrorCorrelator, ZoweExplorerApiType, CorrelatedError } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders"; @@ -26,7 +26,7 @@ export interface ErrorContext { export class AuthUtils { public static async promptForAuthentication( imperativeError: imperative.ImperativeError, - correlation: NetworkError, + correlation: CorrelatedError, profile: imperative.IProfileLoaded ): Promise { if (imperativeError.mDetails.additionalDetails) { diff --git a/packages/zowe-explorer/src/utils/TroubleshootError.ts b/packages/zowe-explorer/src/utils/TroubleshootError.ts index f7e3f33ac..b9e2c2c60 100644 --- a/packages/zowe-explorer/src/utils/TroubleshootError.ts +++ b/packages/zowe-explorer/src/utils/TroubleshootError.ts @@ -9,12 +9,12 @@ * */ -import { NetworkError, WebView } from "@zowe/zowe-explorer-api"; +import { CorrelatedError, WebView } from "@zowe/zowe-explorer-api"; import { env, ExtensionContext, l10n } from "vscode"; import { ZoweLogger } from "../tools/ZoweLogger"; type TroubleshootData = { - error: NetworkError; + error: CorrelatedError; stackTrace?: string; }; diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx index a04051eb5..fcc36e249 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/App.tsx @@ -12,7 +12,7 @@ import { useEffect, useState } from "preact/hooks"; import { JSXInternal } from "preact/src/jsx"; import { isSecureOrigin } from "../utils"; -import { ErrorInfo, ErrorInfoProps, isNetworkError } from "./components/ErrorInfo"; +import { ErrorInfo, ErrorInfoProps, isCorrelatedError } from "./components/ErrorInfo"; import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; export function App(): JSXInternal.Element { @@ -30,7 +30,7 @@ export function App(): JSXInternal.Element { const errorInfo = event.data["error"]; - if (isNetworkError(errorInfo)) { + if (isCorrelatedError(errorInfo)) { setErrorInfo({ error: errorInfo, stackTrace: event.data?.stackTrace }); } }); diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index e94ca6530..98e2571b6 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -1,16 +1,16 @@ import { VSCodeButton, VSCodeDivider, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"; -import { NetworkError } from "@zowe/zowe-explorer-api"; +import { CorrelatedError } from "@zowe/zowe-explorer-api"; import { TipList } from "./TipList"; import { useState } from "preact/hooks"; import PersistentVSCodeAPI from "../../PersistentVSCodeAPI"; export type ErrorInfoProps = { - error: NetworkError; + error: CorrelatedError; stackTrace?: string; }; -export const isNetworkError = (val: any): val is NetworkError => { - return val?.["info"] != null && val.info["summary"] != null; +export const isCorrelatedError = (val: any): val is CorrelatedError => { + return val?.["properties"] != null && val.properties["initialError"] != null; }; export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { From ab4ea58884afb379fb5dec1923b19259e48a4939 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 19:08:03 -0400 Subject: [PATCH 30/62] impl. correlator for FSP fns; update correlator tests Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 1 + .../zowe-explorer-api/src/fs/BaseProvider.ts | 18 ++ .../src/utils/ErrorCorrelator.ts | 3 +- .../utils/TroubleshootError.unit.test.ts | 2 +- .../src/trees/dataset/DatasetFSProvider.ts | 228 +++++++++++----- .../src/trees/job/JobFSProvider.ts | 94 +++++-- .../src/trees/uss/UssFSProvider.ts | 246 ++++++++++++------ 7 files changed, 409 insertions(+), 183 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index fa3e35980..8cf553ad1 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -55,6 +55,7 @@ describe("correlateError", () => { "Ensure that the data set is not opened within a mainframe editor tool.", ], }, + errorCode: "500", initialError: "Client is not authorized for file access.", }) ); diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index 3d099f306..a931b2f09 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -15,6 +15,15 @@ import * as path from "path"; import { FsAbstractUtils } from "./utils"; import { Gui } from "../globals/Gui"; import { ZosEncoding } from "../tree"; +import { ErrorCorrelator, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; + +export interface HandleErrorOpts { + allowRetry?: boolean; + profileType?: string; + apiType?: ZoweExplorerApiType; + templateArgs?: Record; + additionalContext?: string; +} export class BaseProvider { // eslint-disable-next-line no-magic-numbers @@ -417,6 +426,15 @@ export class BaseProvider { return entry; } + protected async _handleError(err: Error, opts?: HandleErrorOpts): Promise { + return ErrorCorrelator.getInstance().displayError(opts?.apiType ?? ZoweExplorerApiType.All, err, { + additionalContext: opts?.additionalContext, + allowRetry: opts?.allowRetry ?? false, + profileType: opts?.profileType ?? "any", + templateArgs: opts?.templateArgs, + }); + } + protected _lookupAsDirectory(uri: vscode.Uri, silent: boolean): DirEntry { const entry = this.lookup(uri, silent); if (entry != null && !FsAbstractUtils.isDirectoryEntry(entry) && !silent) { diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 4d6ff2495..4b4464563 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -253,8 +253,9 @@ export class ErrorCorrelator extends Singleton { errorCode: apiError.errorCode, initialError: error, correlation: { - ...apiError, + errorCode: apiError.errorCode, summary: opts?.templateArgs ? Mustache.render(apiError.summary, opts.templateArgs) : apiError.summary, + tips: apiError.tips, }, }); } diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts index ec0a48555..b2948297c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -38,7 +38,7 @@ describe("TroubleshootError", () => { const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); await troubleshootError.onDidReceiveMessage({ command: "copy" }); expect(writeTextMock).toHaveBeenCalledWith( - `Error details:\n${errorData.error.message}\nStack trace:\n${errorData.error.stack.replace(/(.+?)\n/, "")}` + `Error details:\n${errorData.error.message}\nStack trace:\n${errorData.error.stack?.replace(/(.+?)\n/, "")}` ); }); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 5dc601035..0363e84cb 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -26,8 +26,6 @@ import { ZoweScheme, UriFsInfo, FileEntry, - imperative, - ErrorCorrelator, ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; @@ -93,13 +91,26 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem // Locate the resource using the profile in the given URI. let resp; const isPdsMember = !FsDatasetsUtils.isPdsEntry(entry) && (entry as DsEntry).isMember; - if (isPdsMember) { - // PDS member - const pds = this._lookupParentDirectory(uri); - resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true }); - } else { - // Data Set - resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); + try { + if (isPdsMember) { + // PDS member + const pds = this._lookupParentDirectory(uri); + resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true }); + } else { + // Data Set + resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); + } + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to get stats for data set {0}", uri.path), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + }); + if (userResponse === "retry") { + return this.stat(uri); + } + return entry; } // Attempt to parse a successful API response and update the data set's cached stats. @@ -134,14 +145,25 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ), ]; - if (mvsApi.dataSetsMatchingPattern) { - datasetResponses.push(await mvsApi.dataSetsMatchingPattern(dsPatterns)); - } else { - for (const dsp of dsPatterns) { - datasetResponses.push(await mvsApi.dataSet(dsp)); + try { + if (mvsApi.dataSetsMatchingPattern) { + datasetResponses.push(await mvsApi.dataSetsMatchingPattern(dsPatterns)); + } else { + for (const dsp of dsPatterns) { + datasetResponses.push(await mvsApi.dataSet(dsp)); + } + } + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to list datasets"), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: this._getInfoFromUri(uri).profile?.type, + }); + if (userResponse === "Retry") { + return this.fetchEntriesForProfile(uri, uriInfo, pattern); } } - for (const resp of datasetResponses) { for (const ds of resp.apiResponse?.items ?? resp.apiResponse ?? []) { let tempEntry = profileEntry.entries.get(ds.dsname); @@ -172,7 +194,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } private async fetchEntriesForDataset(entry: PdsEntry, uri: vscode.Uri, uriInfo: UriFsInfo): Promise { - const members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); + let members: IZosFilesResponse; + try { + members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to list dataset members"), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + }); + if (userResponse === "Retry") { + return this.fetchEntriesForDataset(entry, uri, uriInfo); + } + } const pdsExtension = DatasetUtils.getExtension(entry.name); for (const ds of members.apiResponse?.items || []) { @@ -191,12 +226,13 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem try { entry = this.lookup(uri, false) as PdsEntry | DsEntry; } catch (err) { - if (!(err instanceof vscode.FileSystemError)) { - throw err; - } - - if (err.code !== "FileNotFound") { - throw err; + if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { + await this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to locate data set {0}", uri.path), + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + }); + return undefined; } } @@ -206,14 +242,27 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const uriPath = uri.path.substring(uriInfo.slashAfterProfilePos + 1).split("/"); const pdsMember = uriPath.length === 2; if (!entryExists) { - const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(entryIsDir ? uriPath[0] : path.parse(uriPath[0]).name, { - attributes: true, - }); - if (resp.success) { - const dsorg: string = resp.apiResponse?.items?.[0]?.dsorg; - entryIsDir = pdsMember ? false : dsorg?.startsWith("PO") ?? false; - } else { - throw vscode.FileSystemError.FileNotFound(uri); + try { + const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(entryIsDir ? uriPath[0] : path.parse(uriPath[0]).name, { + attributes: true, + }); + if (resp.success) { + const dsorg: string = resp.apiResponse?.items?.[0]?.dsorg; + entryIsDir = pdsMember ? false : dsorg?.startsWith("PO") ?? false; + } else { + throw vscode.FileSystemError.FileNotFound(uri); + } + } catch (err) { + const userResponse = await this._handleError(err, { + allowRetry: true, + additionalContext: vscode.l10n.t("Failed to list data set {0}", uri.path), + apiType: ZoweExplorerApiType.Mvs, + profileType: uriInfo.profile?.type, + }); + if (userResponse === "Retry") { + return this.fetchDataset(uri, uriInfo); + } + return undefined; } } @@ -290,7 +339,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } /** - * Creates a directory entry in the provider at the given URI. + * Creates a local directory entry in the provider at the given URI. * @param uri The URI that represents a new directory path */ public createDirectory(uri: vscode.Uri): void { @@ -338,13 +387,31 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const bufBuilder = new BufferBuilder(); const metadata = file.metadata ?? this._getInfoFromUri(uri); const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); + let resp: IZosFilesResponse; + try { + resp = await ZoweExplorerApiRegister.getMvsApi(metadata.profile).getContents(metadata.dsName, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to get contents for {0}", + args: [uri.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: this._getInfoFromUri(uri).profile?.type, + }); + if (userResponse === "Retry") { + return this.fetchDatasetAtUri(uri, options); + } + return; + } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); if (options?.isConflict) { @@ -379,7 +446,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { - throw err; + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to read {0}", + args: [uri.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: this._getInfoFromUri(uri).profile?.type, + }); + if (userResponse === "Retry") { + return this.readFile(uri); + } + return; } // check if parent directory exists; if not, do a remote lookup @@ -443,21 +523,6 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem }); } catch (err) { statusMsg.dispose(); - if (err instanceof imperative.ImperativeError) { - ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, err, { - allowRetry: true, - profileType: entry.metadata.profile.type, - stackTrace: err.stack, - }); - - switch (userSelection) { - case "Retry": - return this.uploadEntry(parent, entry, content, forceUpload); - default: - throw err; - } - } throw err; } statusMsg.dispose(); @@ -532,7 +597,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { - throw err; + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to save {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.writeFile(uri, content, options); + } + return; } entry.data = content; @@ -577,13 +655,19 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem responseTimeout: entry.metadata.profile.profile?.responseTimeout, }); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Deleting {0} failed due to API error: {1}", - args: [entry.metadata.path, err.message], - comment: ["File path", "Error message"], - }) - ); + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.delete(uri, _options); + } return; } @@ -617,13 +701,19 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ); } } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Renaming {0} failed due to API error: {1}", - args: [oldName, err.message], - comment: ["File name", "Error message"], - }) - ); + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to rename {0}", + args: [oldName], + comment: ["Data set name"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: entry.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.rename(oldUri, newUri, options); + } return; } diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index 3228c65f1..99e81311c 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -25,6 +25,7 @@ import { ZoweScheme, FsJobsUtils, FsAbstractUtils, + ZoweExplorerApiType, } from "@zowe/zowe-explorer-api"; import { IJob, IJobFile } from "@zowe/zos-jobs-for-zowe-sdk"; import { Profiles } from "../../configuration/Profiles"; @@ -86,33 +87,40 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const results: [string, vscode.FileType][] = []; const jesApi = ZoweExplorerApiRegister.getJesApi(uriInfo.profile); - if (FsAbstractUtils.isFilterEntry(fsEntry)) { - if (!jesApi.getJobsByParameters) { - throw new Error(vscode.l10n.t("Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.")); - } - - const jobFiles = await jesApi.getJobsByParameters({ - owner: fsEntry.filter["owner"] ?? "*", - status: fsEntry.filter["status"] ?? "*", - prefix: fsEntry.filter["prefix"] ?? "*", - }); - for (const job of jobFiles) { - if (!fsEntry.entries.has(job.jobid)) { - const newJob = new JobEntry(job.jobid); - newJob.job = job; - fsEntry.entries.set(job.jobid, newJob); + try { + if (FsAbstractUtils.isFilterEntry(fsEntry)) { + const jobFiles = await jesApi.getJobsByParameters({ + owner: fsEntry.filter["owner"] ?? "*", + status: fsEntry.filter["status"] ?? "*", + prefix: fsEntry.filter["prefix"] ?? "*", + }); + for (const job of jobFiles) { + if (!fsEntry.entries.has(job.jobid)) { + const newJob = new JobEntry(job.jobid); + newJob.job = job; + fsEntry.entries.set(job.jobid, newJob); + } } - } - } else if (FsJobsUtils.isJobEntry(fsEntry)) { - const spoolFiles = await jesApi.getSpoolFiles(fsEntry.job.jobname, fsEntry.job.jobid); - for (const spool of spoolFiles) { - const spoolName = FsJobsUtils.buildUniqueSpoolName(spool); - if (!fsEntry.entries.has(spoolName)) { - const newSpool = new SpoolEntry(spoolName); - newSpool.spool = spool; - fsEntry.entries.set(spoolName, newSpool); + } else if (FsJobsUtils.isJobEntry(fsEntry)) { + const spoolFiles = await jesApi.getSpoolFiles(fsEntry.job.jobname, fsEntry.job.jobid); + for (const spool of spoolFiles) { + const spoolName = FsJobsUtils.buildUniqueSpoolName(spool); + if (!fsEntry.entries.has(spoolName)) { + const newSpool = new SpoolEntry(spoolName); + newSpool.spool = spool; + fsEntry.entries.set(spoolName, newSpool); + } } } + } catch (err) { + const userResponse = await this._handleError(err, { + allowRetry: true, + apiType: ZoweExplorerApiType.Jes, + profileType: uriInfo.profile?.type, + }); + if (userResponse === "Retry") { + return this.readDirectory(uri); + } } for (const entry of fsEntry.entries) { @@ -189,7 +197,6 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv const bufBuilder = new BufferBuilder(); const jesApi = ZoweExplorerApiRegister.getJesApi(spoolEntry.metadata.profile); - if (jesApi.downloadSingleSpool) { await jesApi.downloadSingleSpool({ jobFile: spoolEntry.spool, @@ -217,7 +224,23 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv public async readFile(uri: vscode.Uri): Promise { const spoolEntry = this._lookupAsFile(uri) as SpoolEntry; if (!spoolEntry.wasAccessed) { - await this.fetchSpoolAtUri(uri); + try { + await this.fetchSpoolAtUri(uri); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to get contents for {0}", + args: [spoolEntry.name], + comment: "Spool name", + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Jes, + profileType: spoolEntry.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.readFile(uri); + } + } spoolEntry.wasAccessed = true; } @@ -284,11 +307,26 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv } const parent = this._lookupParentDirectory(uri, false); - const profInfo = FsAbstractUtils.getInfoForUri(uri, Profiles.getInstance()); if (options.deleteRemote) { - await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); + try { + await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete job {0}", + args: [entry.job.jobname], + comment: "Job name", + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Jes, + profileType: profInfo.profile.type, + }); + if (userResponse === "Retry") { + return this.delete(uri, options); + } + } } parent.entries.delete(entry.name); this._fireSoon({ type: vscode.FileChangeType.Deleted, uri }); diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 9358fa8f8..d2fb0453f 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -14,7 +14,6 @@ import * as vscode from "vscode"; import { BaseProvider, BufferBuilder, - ErrorCorrelator, FsAbstractUtils, imperative, Gui, @@ -122,7 +121,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const oldInfo = this._getInfoFromUri(oldUri); - await ussApi.move(oldInfo.path, info.path); + try { + await ussApi.move(oldInfo.path, info.path); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ message: "Failed to move {0}", args: [oldInfo.path], comment: "File path" }), + apiType: ZoweExplorerApiType.Uss, + allowRetry: true, + profileType: info.profile.type, + }); + if (userResponse === "Retry") { + return this.move(oldUri, newUri); + } + return false; + } await this._relocateEntry(oldUri, newUri, info.path); return true; } @@ -130,15 +142,24 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv public async listFiles(profile: imperative.IProfileLoaded, uri: vscode.Uri, keepRelative: boolean = false): Promise { const queryParams = new URLSearchParams(uri.query); const ussPath = queryParams.has("searchPath") ? queryParams.get("searchPath") : uri.path.substring(uri.path.indexOf("/", 1)); - if (ussPath.length === 0) { - throw new imperative.ImperativeError({ - msg: vscode.l10n.t("Could not list USS files: Empty path provided in URI"), + let response: IZosFilesResponse; + try { + response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); + // If request was successful, create directories for the path if it doesn't exist + if (response.success && !keepRelative && response.apiResponse.items?.[0]?.mode?.startsWith("d") && !this.exists(uri)) { + await vscode.workspace.fs.createDirectory(uri); + } + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t("Failed to list {0}", err.message), + apiType: ZoweExplorerApiType.Uss, + allowRetry: true, + profileType: profile.type, }); - } - const response = await ZoweExplorerApiRegister.getUssApi(profile).fileList(ussPath); - // If request was successful, create directories for the path if it doesn't exist - if (response.success && !keepRelative && response.apiResponse.items?.[0]?.mode?.startsWith("d") && !this.exists(uri)) { - await vscode.workspace.fs.createDirectory(uri); + if (userResponse === "Retry") { + return this.listFiles(profile, uri, keepRelative); + } + return { success: false, commandResponse: err.message }; } return { @@ -183,7 +204,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } const fileList = entryExists ? await this.listFiles(entry.metadata.profile, uri) : resp; - for (const item of fileList.apiResponse.items) { + for (const item of fileList.apiResponse?.items ?? []) { const itemName = item.name as string; const isDirectory = item.mode?.startsWith("d") ?? false; @@ -263,13 +284,31 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const filePath = uri.path.substring(uriInfo.slashAfterProfilePos); const metadata = file.metadata; const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; - const resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { - binary: file.encoding?.kind === "binary", - encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, - responseTimeout: metadata.profile.profile?.responseTimeout, - returnEtag: true, - stream: bufBuilder, - }); + let resp: IZosFilesResponse; + try { + resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { + binary: file.encoding?.kind === "binary", + encoding: file.encoding?.kind === "other" ? file.encoding.codepage : profileEncoding, + responseTimeout: metadata.profile.profile?.responseTimeout, + returnEtag: true, + stream: bufBuilder, + }); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to get contents for {0}", + args: [filePath], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.fetchFileAtUri(uri, options); + } + return; + } await this.autoDetectEncoding(file as UssFile); const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); @@ -296,17 +335,34 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv return; } - const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); - if (ussApi.getTag != null) { - const taggedEncoding = await ussApi.getTag(entry.metadata.path); - if (taggedEncoding === "binary" || taggedEncoding === "mixed") { - entry.encoding = { kind: "binary" }; - } else if (taggedEncoding !== "untagged") { - entry.encoding = { kind: "other", codepage: taggedEncoding }; + try { + const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); + if (ussApi.getTag != null) { + const taggedEncoding = await ussApi.getTag(entry.metadata.path); + if (taggedEncoding === "binary" || taggedEncoding === "mixed") { + entry.encoding = { kind: "binary" }; + } else if (taggedEncoding !== "untagged") { + entry.encoding = { kind: "other", codepage: taggedEncoding }; + } + } else { + const isBinary = await ussApi.isFileTagBinOrAscii(entry.metadata.path); + entry.encoding = isBinary ? { kind: "binary" } : undefined; } - } else { - const isBinary = await ussApi.isFileTagBinOrAscii(entry.metadata.path); - entry.encoding = isBinary ? { kind: "binary" } : undefined; + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to detect encoding for {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: entry.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.autoDetectEncoding(entry); + } + return; } } @@ -393,21 +449,6 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }); } catch (err) { statusMsg.dispose(); - if (err instanceof imperative.ImperativeError) { - ZoweLogger.error(err.message); - const userSelection = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Uss, err.message, { - allowRetry: true, - profileType: entry.metadata.profile.type, - stackTrace: err.stack, - }); - - switch (userSelection) { - case "Retry": - return this.uploadEntry(entry, content, options); - default: - throw err; - } - } throw err; } @@ -482,7 +523,15 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { // Some unknown error happened, don't update the entry - throw err; + const userResponse = await this._handleError(err, { + apiType: ZoweExplorerApiType.Uss, + allowRetry: true, + profileType: parentDir.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.writeFile(uri, content, options); + } + return; } entry.data = content; @@ -535,13 +584,19 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getUssApi(entry.metadata.profile).rename(entry.metadata.path, newPath); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Renaming {0} failed due to API error: {1}", - args: [entry.metadata.path, err.message], - comment: ["File path", "Error message"], - }) - ); + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to rename {0}", + args: [entry.metadata.path], + comment: ["File path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: entry.metadata.profile?.type, + }); + if (userResponse === "Retry") { + return this.rename(oldUri, newUri, options); + } return; } @@ -571,13 +626,19 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entryToDelete instanceof UssDirectory ); } catch (err) { - await Gui.errorMessage( - vscode.l10n.t({ - message: "Deleting {0} failed due to API error: {1}", - args: [entryToDelete.metadata.path, err.message], - comment: ["File name", "Error message"], - }) - ); + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to delete {0}", + args: [entryToDelete.metadata.path], + comment: ["File name"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: parent.metadata.profile.type, + }); + if (userResponse === "Retry") { + return this.delete(uri, _options); + } return; } @@ -653,35 +714,52 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const fileName = this.buildFileName(fileList, path.basename(sourceInfo.path)); const outputPath = path.posix.join(destInfo.path, fileName); - if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { - await api.copy(outputPath, { - from: sourceInfo.path, - recursive: options.tree.type === USSFileStructure.UssFileType.Directory, - overwrite: options.overwrite ?? true, - }); - } else if (options.tree.type === USSFileStructure.UssFileType.Directory) { - // Not all APIs respect the recursive option, so it's best to - // create a directory and copy recursively to avoid missing any files/folders - await api.create(outputPath, "directory"); - if (options.tree.children) { - for (const child of options.tree.children) { - await this.copyTree( - child.localUri, - vscode.Uri.from({ - scheme: ZoweScheme.USS, - path: path.posix.join(destInfo.profile.name, outputPath, child.baseName), - }), - { ...options, tree: child } - ); + try { + if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { + await api.copy(outputPath, { + from: sourceInfo.path, + recursive: options.tree.type === USSFileStructure.UssFileType.Directory, + overwrite: options.overwrite ?? true, + }); + } else if (options.tree.type === USSFileStructure.UssFileType.Directory) { + // Not all APIs respect the recursive option, so it's best to + // create a directory and copy recursively to avoid missing any files/folders + await api.create(outputPath, "directory"); + if (options.tree.children) { + for (const child of options.tree.children) { + await this.copyTree( + child.localUri, + vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: path.posix.join(destInfo.profile.name, outputPath, child.baseName), + }), + { ...options, tree: child } + ); + } + } + } else { + const fileEntry = this.lookup(source); + if (!fileEntry.wasAccessed) { + // must fetch contents of file first before pasting in new path + await this.readFile(source); } + await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); } - } else { - const fileEntry = this.lookup(source); - if (!fileEntry.wasAccessed) { - // must fetch contents of file first before pasting in new path - await this.readFile(source); + } catch (err) { + const userResponse = await this._handleError(err, { + additionalContext: vscode.l10n.t({ + message: "Failed to copy {0} to {1}", + args: [source.path, destination.path, err.message], + comment: ["Source path", "Destination path"], + }), + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: destInfo.profile.type, + }); + if (userResponse === "Retry") { + await this.copyTree(source, destination, options); } - await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); + return; } } From a3167c99bfad3bf06f3fc4107842626dc4ee16ff Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 16 Oct 2024 19:22:11 -0400 Subject: [PATCH 31/62] tests: resolve failing test cases Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 56 ++----------------- .../trees/job/JobFSProvider.unit.test.ts | 15 ----- .../trees/uss/UssFSProvider.unit.test.ts | 48 ++++------------ .../utils/TroubleshootError.unit.test.ts | 34 +++++------ 4 files changed, 33 insertions(+), 120 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 2af16a725..002d9c7b0 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -314,23 +314,6 @@ describe("readFile", () => { remoteLookupForResourceMock.mockRestore(); }); - it("throws an error if the entry does not exist and the error is not FileNotFound", async () => { - const _lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockImplementationOnce((uri) => { - throw FileSystemError.FileIsADirectory(uri as Uri); - }); - - let err; - try { - await DatasetFSProvider.instance.readFile(testUris.ps); - } catch (error) { - err = error; - expect(err.code).toBe("FileIsADirectory"); - } - expect(err).toBeDefined(); - expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); - _lookupAsFileMock.mockRestore(); - }); - it("calls fetchDatasetAtUri if the entry has not yet been accessed", async () => { const _lookupAsFileMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupAsFile") @@ -445,39 +428,6 @@ describe("writeFile", () => { lookupMock.mockRestore(); }); - it("throws an error when there is an error unrelated to etag", async () => { - const mockMvsApi = { - uploadFromBuffer: jest.fn().mockImplementation(() => { - throw new Error("Unknown error on remote system"); - }), - }; - const disposeMock = jest.fn(); - const setStatusBarMsg = jest.spyOn(Gui, "setStatusBarMessage").mockReturnValueOnce({ dispose: disposeMock }); - const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); - const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; - const sessionEntry = { ...testEntries.session }; - sessionEntry.entries.set("USER.DATA.PS", psEntry); - const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); - const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(psEntry); - const newContents = new Uint8Array([3, 6, 9]); - await expect(DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true })).rejects.toThrow( - "Unknown error on remote system" - ); - - expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); - expect(setStatusBarMsg).toHaveBeenCalled(); - expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { - binary: false, - encoding: undefined, - etag: testEntries.ps.etag, - returnEtag: true, - }); - expect(disposeMock).toHaveBeenCalled(); - setStatusBarMsg.mockRestore(); - mvsApiMock.mockRestore(); - lookupMock.mockRestore(); - }); - it("calls _handleConflict when there is an e-tag error", async () => { const mockMvsApi = { uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), @@ -890,7 +840,9 @@ describe("delete", () => { expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); expect(_fireSoonMock).toHaveBeenCalled(); - expect(errorMsgMock).toHaveBeenCalledWith("Deleting /USER.DATA.PS failed due to API error: Data set does not exist on remote"); + expect(errorMsgMock).toHaveBeenCalledWith("Failed to delete /USER.DATA.PS: Data set does not exist on remote", { + items: ["Retry", "More info"], + }); expect(fakeSession.entries.has(fakePs.name)).toBe(true); mvsApiMock.mockRestore(); errorMsgMock.mockRestore(); @@ -972,7 +924,7 @@ describe("rename", () => { .mockReturnValueOnce({ ...testEntries.session }); await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); - expect(errMsgSpy).toHaveBeenCalledWith("Renaming USER.DATA.PDS failed due to API error: could not upload data set"); + expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename USER.DATA.PDS: could not upload data set", { items: ["Retry", "More info"] }); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); _lookupParentDirectoryMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts index 09fb65c8a..1cb1d12dd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts @@ -105,21 +105,6 @@ describe("refreshSpool", () => { }); describe("readDirectory", () => { - it("throws an error if getJobsByParameters does not exist", async () => { - const mockJesApi = {}; - const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); - const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce({ - ...testEntries.session, - filter: { ...testEntries.session.filter, owner: "USER", prefix: "JOB*", status: "*" }, - entries: new Map(), - } as any); - await expect(JobFSProvider.instance.readDirectory(testUris.session)).rejects.toThrow( - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API." - ); - expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.session, false); - jesApiMock.mockRestore(); - }); - it("calls getJobsByParameters to list jobs under a session", async () => { const fakeJob2 = { ...createIJobObject(), jobid: "JOB3456" }; const mockJesApi = { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 446f20b28..1d020ded8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -10,7 +10,7 @@ */ import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri, workspace } from "vscode"; -import { BaseProvider, DirEntry, FileEntry, Gui, UssDirectory, UssFile, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { BaseProvider, DirEntry, FileEntry, Gui, UssDirectory, UssFile, ZoweExplorerApiType, ZoweScheme } from "@zowe/zowe-explorer-api"; import { Profiles } from "../../../../src/configuration/Profiles"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; @@ -142,17 +142,6 @@ describe("move", () => { }); describe("listFiles", () => { - it("throws an error when called with a URI with an empty path", async () => { - await expect( - UssFSProvider.instance.listFiles( - testProfile, - Uri.from({ - scheme: ZoweScheme.USS, - path: "", - }) - ) - ).rejects.toThrow("Could not list USS files: Empty path provided in URI"); - }); it("removes '.', '..', and '...' from IZosFilesResponse items when successful", async () => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ fileList: jest.fn().mockResolvedValueOnce({ @@ -618,27 +607,6 @@ describe("writeFile", () => { ussApiMock.mockRestore(); }); - it("throws an error when there is an error unrelated to etag", async () => { - const mockUssApi = { - uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Unknown error on remote system")), - }; - const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); - const folder = { - ...testEntries.folder, - entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), - }; - const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); - const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); - const newContents = new Uint8Array([3, 6, 9]); - await expect(UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true })).rejects.toThrow( - "Unknown error on remote system" - ); - - lookupParentDirMock.mockRestore(); - ussApiMock.mockRestore(); - autoDetectEncodingMock.mockRestore(); - }); - it("calls _handleConflict when there is an etag error", async () => { const mockUssApi = { uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), @@ -873,7 +841,7 @@ describe("rename", () => { expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); expect(folderEntry.metadata.path).toBe("/aFolder"); expect(sessionEntry.entries.has("aFolder2")).toBe(false); - expect(errMsgSpy).toHaveBeenCalledWith("Renaming /aFolder failed due to API error: could not upload file"); + expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename /aFolder: could not upload file", { items: ["Retry", "More info"] }); lookupMock.mockRestore(); ussApiMock.mockRestore(); @@ -908,15 +876,21 @@ describe("delete", () => { parent: sesEntry, parentUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest" }), }); - const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); - const deleteMock = jest.fn().mockRejectedValueOnce(new Error("insufficient permissions")); + const exampleError = new Error("insufficient permissions"); + const deleteMock = jest.fn().mockRejectedValueOnce(exampleError); jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ delete: deleteMock, } as any); + const handleErrorMock = jest.spyOn((BaseProvider as any).prototype, "_handleError"); await UssFSProvider.instance.delete(testUris.file, { recursive: false }); expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); - expect(errorMsgMock).toHaveBeenCalledWith("Deleting /aFile.txt failed due to API error: insufficient permissions"); + expect(handleErrorMock).toHaveBeenCalledWith(exampleError, { + additionalContext: "Failed to delete /aFile.txt", + allowRetry: true, + apiType: ZoweExplorerApiType.Uss, + profileType: testEntries.file.metadata.profile.type, + }); expect(sesEntry.entries.has("aFile.txt")).toBe(true); expect(sesEntry.size).toBe(1); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts index b2948297c..a65d96bfd 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TroubleshootError.unit.test.ts @@ -13,6 +13,7 @@ import { env, ExtensionContext } from "vscode"; import { TroubleshootError } from "../../../src/utils/TroubleshootError"; import { CorrelatedError } from "@zowe/zowe-explorer-api"; import { ZoweLogger } from "../../../src/tools/ZoweLogger"; +import { MockedProperty } from "../../__mocks__/mockUtils"; describe("TroubleshootError", () => { function getGlobalMocks() { @@ -20,34 +21,33 @@ describe("TroubleshootError", () => { extensionPath: "/a/b/c/zowe-explorer", subscriptions: [], } as unknown as ExtensionContext; - const errorData = { - error: new CorrelatedError({ error: "test error" }), - stackTrace: "test stack trace", - }; - const troubleshootError = new TroubleshootError(context, errorData); + const error = new Error("test error"); + error.stack = "test stack trace"; + const correlatedError = new CorrelatedError({ initialError: error }); + const troubleshootError = new TroubleshootError(context, { error: correlatedError, stackTrace: "test stack trace" }); return { context, - errorData, + error, + correlatedError, troubleshootError, }; } describe("onDidReceiveMessage", () => { it("handles copy command for error with stack trace", async () => { - const { errorData, troubleshootError } = getGlobalMocks(); + const { error, troubleshootError } = getGlobalMocks(); const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); await troubleshootError.onDidReceiveMessage({ command: "copy" }); - expect(writeTextMock).toHaveBeenCalledWith( - `Error details:\n${errorData.error.message}\nStack trace:\n${errorData.error.stack?.replace(/(.+?)\n/, "")}` - ); + expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${error.message}\nStack trace:\n${error.stack?.replace(/(.+?)\n/, "")}`); }); it("handles copy command for error without stack trace", async () => { - const { errorData, troubleshootError } = getGlobalMocks(); - Object.defineProperty(errorData.error, "stack", { value: undefined }); + const { error, troubleshootError } = getGlobalMocks(); + const errorProp = new MockedProperty(error, "stack", { value: undefined }); const writeTextMock = jest.spyOn(env.clipboard, "writeText").mockImplementation(); await troubleshootError.onDidReceiveMessage({ command: "copy" }); - expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${errorData.error.message}`); + expect(writeTextMock).toHaveBeenCalledWith(`Error details:\n${error.message}`); + errorProp[Symbol.dispose](); }); it("handles ready command", async () => { @@ -56,6 +56,7 @@ describe("TroubleshootError", () => { await troubleshootError.onDidReceiveMessage({ command: "ready" }); expect(sendErrorDataSpy).toHaveBeenCalledWith(troubleshootError.errorData); }); + it("handles an unrecognized command", async () => { const { troubleshootError } = getGlobalMocks(); const debugSpy = jest.spyOn(ZoweLogger, "debug"); @@ -66,10 +67,11 @@ describe("TroubleshootError", () => { describe("sendErrorData", () => { it("sends error data to the webview", async () => { - const { errorData, troubleshootError } = getGlobalMocks(); + const { correlatedError, troubleshootError } = getGlobalMocks(); const postMessageSpy = jest.spyOn(troubleshootError.panel.webview, "postMessage"); - await troubleshootError.sendErrorData(errorData); - expect(postMessageSpy).toHaveBeenCalledWith(errorData); + const data = { error: correlatedError, stackTrace: correlatedError.stack }; + await troubleshootError.sendErrorData(data); + expect(postMessageSpy).toHaveBeenCalledWith(data); }); }); }); From b416fa10a83614bb47c0dba42c44a4227f7030ab Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 17 Oct 2024 10:01:41 -0400 Subject: [PATCH 32/62] refactor: throw errs instead of return; update tests Signed-off-by: Trae Yelovich --- .../utils/ErrorCorrelator.unit.test.ts | 6 ++-- .../zowe-explorer-api/src/fs/BaseProvider.ts | 4 +-- .../src/utils/ErrorCorrelator.ts | 30 +++++++++++++++---- .../dataset/DatasetFSProvider.unit.test.ts | 7 +++-- .../trees/uss/UssFSProvider.unit.test.ts | 6 ++-- .../src/trees/dataset/DatasetFSProvider.ts | 28 ++++++++--------- .../src/trees/job/JobFSProvider.ts | 9 ++++-- .../src/trees/uss/UssFSProvider.ts | 26 ++++++++-------- .../src/webviews/src/edit-history/App.tsx | 2 +- 9 files changed, 72 insertions(+), 46 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 8cf553ad1..5262fd995 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -127,20 +127,20 @@ describe("displayError", () => { }); describe("displayCorrelatedError", () => { - it("returns 'Retry' whenever the user selects 'Retry'", async () => { + it("returns 'Retry' for the userResponse whenever the user selects 'Retry'", async () => { const error = new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message", }); const correlateErrorMock = jest.spyOn(ErrorCorrelator.getInstance(), "correlateError").mockReturnValueOnce(error); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce("Retry"); - const userResponse = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { + const handledErrorInfo = await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { additionalContext: "Some additional context", allowRetry: true, profileType: "zosmf", }); expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(errorMessageMock).toHaveBeenCalledWith("Some additional context: Summary of network error", { items: ["Retry", "More info"] }); - expect(userResponse).toBe("Retry"); + expect(handledErrorInfo.userResponse).toBe("Retry"); }); }); diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index a931b2f09..eb8e23250 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -15,7 +15,7 @@ import * as path from "path"; import { FsAbstractUtils } from "./utils"; import { Gui } from "../globals/Gui"; import { ZosEncoding } from "../tree"; -import { ErrorCorrelator, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; +import { ErrorCorrelator, HandledErrorInfo, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; export interface HandleErrorOpts { allowRetry?: boolean; @@ -426,7 +426,7 @@ export class BaseProvider { return entry; } - protected async _handleError(err: Error, opts?: HandleErrorOpts): Promise { + protected async _handleError(err: Error, opts?: HandleErrorOpts): Promise { return ErrorCorrelator.getInstance().displayError(opts?.apiType ?? ZoweExplorerApiType.All, err, { additionalContext: opts?.additionalContext, allowRetry: opts?.allowRetry ?? false, diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 4b4464563..11e4658e5 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -61,26 +61,31 @@ export interface DisplayErrorOpts extends CorrelateErrorOpts { export interface DisplayCorrelatedErrorOpts extends Omit {} +export interface HandledErrorInfo { + correlation: CorrelatedError; + userResponse: string | undefined; +} + /** * Representation of the given error as a correlated error (wrapper around the `Error` class). * * Used to cache the error info such as tips, the match that was encountered and the full error message. */ export class CorrelatedError { + public errorCode?: string; + public constructor(public properties: CorrelatedErrorProps) { if (properties.initialError instanceof Error) { // preserve stack from initial error } + + this.errorCode = this.initial instanceof ImperativeError ? this.initial.errorCode : this.properties.errorCode; } public get stack(): string | undefined { return this.initial instanceof Error ? this.initial.stack : undefined; } - public get errorCode(): string | undefined { - return this.initial instanceof ImperativeError ? this.initial.errorCode : this.properties.errorCode ?? undefined; - } - public get message(): string { if (this.properties.correlation) { return this.properties.correlation.summary; @@ -92,6 +97,16 @@ export class CorrelatedError { public get initial(): Error | string { return this.properties.initialError; } + + public asError(): Error { + const err = new Error(this.message); + err.stack = this.stack; + return err; + } + + public toString(): string { + return this.message; + } } export enum ZoweExplorerApiType { @@ -317,8 +332,11 @@ export class ErrorCorrelator extends Singleton { * @param allowRetry Whether to allow retrying the action * @returns The user selection ("Retry" [if enabled] or "Troubleshoot") */ - public async displayError(api: ZoweExplorerApiType, errorDetails: string | Error, opts?: DisplayErrorOpts): Promise { + public async displayError(api: ZoweExplorerApiType, errorDetails: string | Error, opts?: DisplayErrorOpts): Promise { const error = this.correlateError(api, errorDetails, { profileType: opts?.profileType, templateArgs: opts?.templateArgs }); - return this.displayCorrelatedError(error, { additionalContext: opts?.additionalContext, allowRetry: opts?.allowRetry }); + return { + correlation: error, + userResponse: await this.displayCorrelatedError(error, { additionalContext: opts?.additionalContext, allowRetry: opts?.allowRetry }), + }; } } diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 002d9c7b0..65e974114 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -827,6 +827,7 @@ describe("delete", () => { const fakePs = { ...testEntries.ps }; const fakeSession = { ...testEntries.session, entries: new Map() }; fakeSession.entries.set("USER.DATA.PS", fakePs); + const mockMvsApi = { deleteDataSet: jest.fn().mockRejectedValueOnce(new Error("Data set does not exist on remote")), }; @@ -836,7 +837,7 @@ describe("delete", () => { const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); - await DatasetFSProvider.instance.delete(testUris.ps, { recursive: false }); + await expect(DatasetFSProvider.instance.delete(testUris.ps, { recursive: false })).rejects.toThrow(); expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); expect(_fireSoonMock).toHaveBeenCalled(); @@ -922,7 +923,9 @@ describe("rename", () => { const _lookupParentDirectoryMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); - await DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }); + await expect( + DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }) + ).rejects.toThrow(); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename USER.DATA.PDS: could not upload data set", { items: ["Retry", "More info"] }); _lookupMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 1d020ded8..95a556d4a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -837,7 +837,9 @@ describe("rename", () => { }; (UssFSProvider.instance as any).root.entries.set("sestest", sessionEntry); - await UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }); + await expect( + UssFSProvider.instance.rename(testUris.folder, testUris.folder.with({ path: "/sestest/aFolder2" }), { overwrite: true }) + ).rejects.toThrow(); expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); expect(folderEntry.metadata.path).toBe("/aFolder"); expect(sessionEntry.entries.has("aFolder2")).toBe(false); @@ -882,7 +884,7 @@ describe("delete", () => { delete: deleteMock, } as any); const handleErrorMock = jest.spyOn((BaseProvider as any).prototype, "_handleError"); - await UssFSProvider.instance.delete(testUris.file, { recursive: false }); + await expect(UssFSProvider.instance.delete(testUris.file, { recursive: false })).rejects.toThrow(); expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); expect(handleErrorMock).toHaveBeenCalledWith(exampleError, { diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 0363e84cb..077e0e3ea 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -101,7 +101,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t("Failed to get stats for data set {0}", uri.path), allowRetry: true, apiType: ZoweExplorerApiType.Mvs, @@ -154,7 +154,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list datasets"), allowRetry: true, apiType: ZoweExplorerApiType.Mvs, @@ -198,7 +198,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem try { members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list dataset members"), allowRetry: true, apiType: ZoweExplorerApiType.Mvs, @@ -253,7 +253,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem throw vscode.FileSystemError.FileNotFound(uri); } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { allowRetry: true, additionalContext: vscode.l10n.t("Failed to list data set {0}", uri.path), apiType: ZoweExplorerApiType.Mvs, @@ -397,7 +397,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem stream: bufBuilder, }); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [uri.path], @@ -410,7 +410,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem if (userResponse === "Retry") { return this.fetchDatasetAtUri(uri, options); } - return; + throw correlation.asError(); } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); @@ -446,7 +446,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to read {0}", args: [uri.path], @@ -459,7 +459,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem if (userResponse === "Retry") { return this.readFile(uri); } - return; + throw correlation.asError(); } // check if parent directory exists; if not, do a remote lookup @@ -597,7 +597,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to save {0}", args: [entry.metadata.path], @@ -610,7 +610,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem if (userResponse === "Retry") { return this.writeFile(uri, content, options); } - return; + throw correlation.asError(); } entry.data = content; @@ -655,7 +655,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem responseTimeout: entry.metadata.profile.profile?.responseTimeout, }); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete {0}", args: [entry.metadata.path], @@ -668,7 +668,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem if (userResponse === "Retry") { return this.delete(uri, _options); } - return; + throw correlation.asError(); } parent.entries.delete(entry.name); @@ -701,7 +701,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ); } } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to rename {0}", args: [oldName], @@ -714,7 +714,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem if (userResponse === "Retry") { return this.rename(oldUri, newUri, options); } - return; + throw correlation.asError(); } parentDir.entries.delete(entry.name); diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index 99e81311c..5971342ed 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -113,7 +113,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv } } } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { allowRetry: true, apiType: ZoweExplorerApiType.Jes, profileType: uriInfo.profile?.type, @@ -121,6 +121,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.readDirectory(uri); } + throw correlation.asError(); } for (const entry of fsEntry.entries) { @@ -227,7 +228,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv try { await this.fetchSpoolAtUri(uri); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [spoolEntry.name], @@ -240,6 +241,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.readFile(uri); } + throw correlation.asError(); } spoolEntry.wasAccessed = true; } @@ -313,7 +315,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete job {0}", args: [entry.job.jobname], @@ -326,6 +328,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.delete(uri, options); } + throw correlation.asError(); } } parent.entries.delete(entry.name); diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index d2fb0453f..769e52d30 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -124,7 +124,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ussApi.move(oldInfo.path, info.path); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to move {0}", args: [oldInfo.path], comment: "File path" }), apiType: ZoweExplorerApiType.Uss, allowRetry: true, @@ -133,7 +133,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.move(oldUri, newUri); } - return false; + throw correlation.asError(); } await this._relocateEntry(oldUri, newUri, info.path); return true; @@ -150,7 +150,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv await vscode.workspace.fs.createDirectory(uri); } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list {0}", err.message), apiType: ZoweExplorerApiType.Uss, allowRetry: true, @@ -294,7 +294,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv stream: bufBuilder, }); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [filePath], @@ -307,7 +307,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.fetchFileAtUri(uri, options); } - return; + throw correlation.asError(); } await this.autoDetectEncoding(file as UssFile); @@ -349,7 +349,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entry.encoding = isBinary ? { kind: "binary" } : undefined; } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to detect encoding for {0}", args: [entry.metadata.path], @@ -523,7 +523,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { // Some unknown error happened, don't update the entry - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { apiType: ZoweExplorerApiType.Uss, allowRetry: true, profileType: parentDir.metadata.profile.type, @@ -531,7 +531,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.writeFile(uri, content, options); } - return; + throw correlation.asError(); } entry.data = content; @@ -584,7 +584,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getUssApi(entry.metadata.profile).rename(entry.metadata.path, newPath); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to rename {0}", args: [entry.metadata.path], @@ -597,7 +597,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.rename(oldUri, newUri, options); } - return; + throw correlation.asError(); } parentDir.entries.delete(entry.name); @@ -626,7 +626,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entryToDelete instanceof UssDirectory ); } catch (err) { - const userResponse = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete {0}", args: [entryToDelete.metadata.path], @@ -639,7 +639,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (userResponse === "Retry") { return this.delete(uri, _options); } - return; + throw correlation.asError(); } parent.entries.delete(entryToDelete.name); @@ -746,7 +746,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); } } catch (err) { - const userResponse = await this._handleError(err, { + const { userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to copy {0} to {1}", args: [source.path, destination.path, err.message], diff --git a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx index bf49e28c5..a047014c8 100644 --- a/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx +++ b/packages/zowe-explorer/src/webviews/src/edit-history/App.tsx @@ -14,7 +14,7 @@ import { VSCodeDivider, VSCodePanels, VSCodePanelTab } from "@vscode/webview-ui- import { JSXInternal } from "preact/src/jsx"; import { isSecureOrigin } from "../utils"; import PersistentDataPanel from "./components/PersistentTable/PersistentDataPanel"; -import PersistentVSCodeAPI from "../PersistentVSCodeAPI" +import PersistentVSCodeAPI from "../PersistentVSCodeAPI"; import PersistentManagerHeader from "./components/PersistentManagerHeader/PersistentManagerHeader"; export function App(): JSXInternal.Element { From e89342c33044413b5b3d45d9d6af8ccb707e7ab0 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 18 Oct 2024 08:14:55 -0400 Subject: [PATCH 33/62] fix delete & stat error tests, run prepublish Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 53 +++++-- packages/zowe-explorer/l10n/bundle.l10n.json | 133 ++++++++++++------ packages/zowe-explorer/l10n/poeditor.json | 33 +++-- .../src/trees/dataset/DatasetFSProvider.ts | 2 +- 4 files changed, 158 insertions(+), 63 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 65e974114..a19ef6862 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri } from "vscode"; +import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, TextEditorLineNumbersStyle, Uri } from "vscode"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { DirEntry, @@ -21,6 +21,7 @@ import { FsDatasetsUtils, Gui, PdsEntry, + ZoweExplorerApiType, ZoweScheme, } from "@zowe/zowe-explorer-api"; import { MockedProperty } from "../../../__mocks__/mockUtils"; @@ -655,6 +656,31 @@ describe("stat", () => { lookupParentDirMock.mockRestore(); mvsApiMock.mockRestore(); }); + it("calls handleError if the API response was unsuccessful for remote lookup", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); + const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testEntries.ps.metadata.profile, + }); + const exampleError = new Error("Response unsuccessful"); + const dataSetMock = jest.fn().mockRejectedValue(exampleError); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); + expect(handleErrorMock).toHaveBeenCalledWith(exampleError, { + additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }); + mvsApiMock.mockRestore(); + getInfoForUriMock.mockRestore(); + lookupMock.mockRestore(); + }); }); describe("fetchEntriesForDataset", () => { @@ -828,25 +854,28 @@ describe("delete", () => { const fakeSession = { ...testEntries.session, entries: new Map() }; fakeSession.entries.set("USER.DATA.PS", fakePs); + const sampleError = new Error("Data set does not exist on remote"); const mockMvsApi = { - deleteDataSet: jest.fn().mockRejectedValueOnce(new Error("Data set does not exist on remote")), + deleteDataSet: jest.fn().mockRejectedValueOnce(sampleError), }; const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(fakePs); const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); - const errorMsgMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockResolvedValue(undefined); jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(fakeSession); await expect(DatasetFSProvider.instance.delete(testUris.ps, { recursive: false })).rejects.toThrow(); expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); expect(_fireSoonMock).toHaveBeenCalled(); - expect(errorMsgMock).toHaveBeenCalledWith("Failed to delete /USER.DATA.PS: Data set does not exist on remote", { - items: ["Retry", "More info"], + expect(handleErrorMock).toHaveBeenCalledWith(sampleError, { + additionalContext: "Failed to delete /USER.DATA.PS", + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", }); expect(fakeSession.entries.has(fakePs.name)).toBe(true); mvsApiMock.mockRestore(); - errorMsgMock.mockRestore(); }); }); @@ -912,10 +941,10 @@ describe("rename", () => { it("displays an error message when renaming fails on the remote system", async () => { const oldPds = new PdsEntry("USER.DATA.PDS"); oldPds.metadata = testEntries.pds.metadata; + const sampleError = new Error("could not upload data set"); const mockMvsApi = { - renameDataSet: jest.fn().mockRejectedValueOnce(new Error("could not upload data set")), + renameDataSet: jest.fn().mockRejectedValueOnce(sampleError), }; - const errMsgSpy = jest.spyOn(Gui, "errorMessage"); const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const _lookupMock = jest .spyOn(DatasetFSProvider.instance as any, "lookup") @@ -923,11 +952,17 @@ describe("rename", () => { const _lookupParentDirectoryMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockResolvedValue(undefined); await expect( DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }) ).rejects.toThrow(); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); - expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename USER.DATA.PDS: could not upload data set", { items: ["Retry", "More info"] }); + expect(handleErrorMock).toHaveBeenCalledWith(sampleError, { + additionalContext: "Failed to rename USER.DATA.PDS", + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); _lookupParentDirectoryMock.mockRestore(); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 75ccca5c5..b678280d3 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -48,6 +48,7 @@ "Certificate Key File": "Certificate Key File", "Submit": "Submit", "Cancel": "Cancel", + "Troubleshoot Error": "Troubleshoot Error", "Zowe explorer profiles are being set as unsecured.": "Zowe explorer profiles are being set as unsecured.", "Zowe explorer profiles are being set as secured.": "Zowe explorer profiles are being set as secured.", "Custom credential manager failed to activate": "Custom credential manager failed to activate", @@ -159,14 +160,8 @@ "Enter the path to the certificate key for authenticating the connection.": "Enter the path to the certificate key for authenticating the connection.", "Certificate Keys": "Certificate Keys", "Select Certificate Key": "Select Certificate Key", - "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out./Label": { - "message": "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - "comment": [ - "Label" - ] - }, "Update Credentials": "Update Credentials", + "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection./Profile name": { "message": "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", "comment": [ @@ -209,32 +204,6 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", - "Profile does not exist for this file.": "Profile does not exist for this file.", - "Saving USS file...": "Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", - "comment": [ - "File path", - "Error message" - ] - }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", - "comment": [ - "File name", - "Error message" - ] - }, - "No error details given": "No error details given", - "Error fetching destination {0} for paste action: {1}/USS pathError message": { - "message": "Error fetching destination {0} for paste action: {1}", - "comment": [ - "USS path", - "Error message" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -305,6 +274,55 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Failed to list {0}": "Failed to list {0}", + "Failed to get contents for {0}/File path": { + "message": "Failed to get contents for {0}", + "comment": [ + "File path" + ] + }, + "Failed to detect encoding for {0}/File path": { + "message": "Failed to detect encoding for {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "No error details given": "No error details given", + "Error fetching destination {0} for paste action: {1}/USS pathError message": { + "message": "Error fetching destination {0} for paste action: {1}", + "comment": [ + "USS path", + "Error message" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -490,7 +508,18 @@ "Phase Name": "Phase Name", "Error Details": "Error Details", "Fetching spool file...": "Fetching spool file...", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.", + "Failed to get contents for {0}/Spool name": { + "message": "Failed to get contents for {0}", + "comment": [ + "Spool name" + ] + }, + "Failed to delete job {0}/Job name": { + "message": "Failed to delete job {0}", + "comment": [ + "Job name" + ] + }, "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}/Job name": { "message": "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}", "comment": [ @@ -679,19 +708,39 @@ "Choose the setting location to save the data set template...": "Choose the setting location to save the data set template...", "Save as User setting": "Save as User setting", "Save as Workspace setting": "Save as Workspace setting", + "Failed to get stats for data set {0}/Data set path": { + "message": "Failed to get stats for data set {0}", + "comment": [ + "Data set path" + ] + }, + "Failed to list datasets": "Failed to list datasets", + "Failed to list dataset members": "Failed to list dataset members", + "Failed to locate data set {0}": "Failed to locate data set {0}", + "Failed to list data set {0}": "Failed to list data set {0}", + "Failed to read {0}/File path": { + "message": "Failed to read {0}", + "comment": [ + "File path" + ] + }, "Saving data set...": "Saving data set...", - "Deleting {0} failed due to API error: {1}/File pathError message": { - "message": "Deleting {0} failed due to API error: {1}", + "Failed to save {0}/File path": { + "message": "Failed to save {0}", "comment": [ - "File path", - "Error message" + "File path" ] }, - "Renaming {0} failed due to API error: {1}/File nameError message": { - "message": "Renaming {0} failed due to API error: {1}", + "Failed to delete {0}/File path": { + "message": "Failed to delete {0}", "comment": [ - "File name", - "Error message" + "File path" + ] + }, + "Failed to rename {0}/Data set name": { + "message": "Failed to rename {0}", + "comment": [ + "Data set name" ] }, "Partitioned Data Set: Binary": "Partitioned Data Set: Binary", diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index ca004c054..ee636da9e 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -459,6 +459,7 @@ "Certificate Key File": "", "Submit": "", "Cancel": "", + "Troubleshoot Error": "", "Zowe explorer profiles are being set as unsecured.": "", "Zowe explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", @@ -509,9 +510,8 @@ "Enter the path to the certificate key for authenticating the connection.": "", "Certificate Keys": "", "Select Certificate Key": "", - "Required parameter 'host' must not be blank.": "", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.": "", "Update Credentials": "", + "Required parameter 'host' must not be blank.": "", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "", "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Use the search button to list USS files": "", @@ -529,14 +529,6 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", - "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", - "Profile does not exist for this file.": "", - "Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -565,6 +557,18 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Failed to list {0}": "", + "Failed to get contents for {0}": "", + "Failed to detect encoding for {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -657,7 +661,7 @@ "Phase Name": "", "Error Details": "", "Fetching spool file...": "", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "", + "Failed to delete job {0}": "", "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}": "", "Job {0} was deleted.": "", "Are you sure you want to delete the following {0} items?\nThis will permanently remove the following jobs from your system.\n\n{1}": "", @@ -718,7 +722,14 @@ "Choose the setting location to save the data set template...": "", "Save as User setting": "", "Save as Workspace setting": "", + "Failed to get stats for data set {0}": "", + "Failed to list datasets": "", + "Failed to list dataset members": "", + "Failed to locate data set {0}": "", + "Failed to list data set {0}": "", + "Failed to read {0}": "", "Saving data set...": "", + "Failed to save {0}": "", "Partitioned Data Set: Binary": "", "Partitioned Data Set: C": "", "Partitioned Data Set: Classic": "", diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index ab5c0b7ab..da64c280d 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -102,7 +102,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } catch (err) { const { userResponse } = await this._handleError(err, { - additionalContext: vscode.l10n.t("Failed to get stats for data set {0}", uri.path), + additionalContext: vscode.l10n.t({ message: "Failed to get stats for data set {0}", args: [uri.path], comment: "Data set path" }), allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, From f23f3f449fe9a1a3a017ae6128a89e8a615f7932 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 18 Oct 2024 09:29:00 -0400 Subject: [PATCH 34/62] error handling cases for stat Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 83 ++++++++++++++----- .../src/trees/dataset/DatasetFSProvider.ts | 6 +- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index a19ef6862..2c4a7912d 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -9,7 +9,7 @@ * */ -import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, TextEditorLineNumbersStyle, Uri } from "vscode"; +import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri } from "vscode"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; import { DirEntry, @@ -656,30 +656,67 @@ describe("stat", () => { lookupParentDirMock.mockRestore(); mvsApiMock.mockRestore(); }); - it("calls handleError if the API response was unsuccessful for remote lookup", async () => { - const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); - const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ - isRoot: false, - slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), - profileName: "sestest", - profile: testEntries.ps.metadata.profile, + + describe("error handling", () => { + it("API response was unsuccessful for remote lookup, dialog dismissed", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); + const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testEntries.ps.metadata.profile, + }); + const exampleError = new Error("Response unsuccessful"); + const dataSetMock = jest.fn().mockRejectedValue(exampleError); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + const handleErrorSpy = jest.spyOn(DatasetFSProvider.instance as any, "_handleError"); + await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); + expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, { + additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }); + mvsApiMock.mockRestore(); + getInfoForUriMock.mockRestore(); + lookupMock.mockRestore(); }); - const exampleError = new Error("Response unsuccessful"); - const dataSetMock = jest.fn().mockRejectedValue(exampleError); - const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ - dataSet: dataSetMock, - } as any); - const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); - await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); - expect(handleErrorMock).toHaveBeenCalledWith(exampleError, { - additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - allowRetry: true, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", + + it("API response was unsuccessful for remote lookup, Retry selected", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); + const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testEntries.ps.metadata.profile, + }); + const exampleError = new Error("Response unsuccessful"); + const dataSetMock = jest.fn().mockRejectedValue(exampleError); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSet: dataSetMock, + } as any); + const statSpy = jest + .spyOn(DatasetFSProvider.instance, "stat") + .mockImplementationOnce(DatasetFSProvider.instance.stat) + .mockImplementation(); + const handleErrorSpy = jest + .spyOn(DatasetFSProvider.instance as any, "_handleError") + .mockResolvedValue({ correlation: { asError: jest.fn() } as any, userResponse: "Retry" }); + await DatasetFSProvider.instance.stat(testUris.ps); + expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, { + additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, + allowRetry: true, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }); + expect(statSpy).toHaveBeenCalledTimes(2); + expect(statSpy).toHaveBeenCalledWith(testUris.ps); + mvsApiMock.mockRestore(); + getInfoForUriMock.mockRestore(); + lookupMock.mockRestore(); }); - mvsApiMock.mockRestore(); - getInfoForUriMock.mockRestore(); - lookupMock.mockRestore(); }); }); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index da64c280d..8335d0f9b 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -101,16 +101,16 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); } } catch (err) { - const { userResponse } = await this._handleError(err, { + const { correlation, userResponse } = await this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get stats for data set {0}", args: [uri.path], comment: "Data set path" }), allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, }); - if (userResponse === "retry") { + if (userResponse === "Retry") { return this.stat(uri); } - return entry; + throw correlation.asError(); } // Attempt to parse a successful API response and update the data set's cached stats. From 50e45d4ee2a70b460d6e75294f257e0d8e81e7af Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 18 Oct 2024 11:57:04 -0400 Subject: [PATCH 35/62] refactor _handleError, avoid use of await for errors Signed-off-by: Trae Yelovich --- .../zowe-explorer-api/src/fs/BaseProvider.ts | 30 +++-- .../dataset/DatasetFSProvider.unit.test.ts | 47 ++++---- .../trees/uss/UssFSProvider.unit.test.ts | 14 ++- .../trees/uss/ZoweUSSNode.unit.test.ts | 3 +- .../src/trees/dataset/DatasetFSProvider.ts | 104 +++++++++--------- .../src/trees/job/JobFSProvider.ts | 36 +++--- .../src/trees/uss/UssFSProvider.ts | 95 ++++++++-------- .../src/trees/uss/ZoweUSSNode.ts | 2 +- 8 files changed, 175 insertions(+), 156 deletions(-) diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index 1cb06ac02..a0c4064ff 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -15,10 +15,13 @@ import * as path from "path"; import { FsAbstractUtils } from "./utils"; import { Gui } from "../globals/Gui"; import { ZosEncoding } from "../tree"; -import { ErrorCorrelator, HandledErrorInfo, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; +import { ErrorCorrelator, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; export interface HandleErrorOpts { - allowRetry?: boolean; + retry?: { + fn: (...args: any[]) => any | PromiseLike; + args?: any[]; + }; profileType?: string; apiType?: ZoweExplorerApiType; templateArgs?: Record; @@ -426,13 +429,22 @@ export class BaseProvider { return entry; } - protected async _handleError(err: Error, opts?: HandleErrorOpts): Promise { - return ErrorCorrelator.getInstance().displayError(opts?.apiType ?? ZoweExplorerApiType.All, err, { - additionalContext: opts?.additionalContext, - allowRetry: opts?.allowRetry ?? false, - profileType: opts?.profileType ?? "any", - templateArgs: opts?.templateArgs, - }); + protected _handleError(err: Error, opts?: HandleErrorOpts): void { + ErrorCorrelator.getInstance() + .displayError(opts?.apiType ?? ZoweExplorerApiType.All, err, { + additionalContext: opts?.additionalContext, + allowRetry: opts?.retry?.fn != null, + profileType: opts?.profileType ?? "any", + templateArgs: opts?.templateArgs, + }) + .then(async ({ userResponse }) => { + if (userResponse === "Retry" && opts?.retry?.fn != null) { + await opts.retry.fn(...(opts?.retry.args ?? [])); + } + }) + .catch(() => { + throw err; + }); } protected _lookupAsDirectory(uri: vscode.Uri, silent: boolean): DirEntry { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 2c4a7912d..6a02942f2 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -673,12 +673,11 @@ describe("stat", () => { } as any); const handleErrorSpy = jest.spyOn(DatasetFSProvider.instance as any, "_handleError"); await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); - expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, { + expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, expect.objectContaining({ additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: "zosmf", - }); + })); mvsApiMock.mockRestore(); getInfoForUriMock.mockRestore(); lookupMock.mockRestore(); @@ -705,12 +704,14 @@ describe("stat", () => { .spyOn(DatasetFSProvider.instance as any, "_handleError") .mockResolvedValue({ correlation: { asError: jest.fn() } as any, userResponse: "Retry" }); await DatasetFSProvider.instance.stat(testUris.ps); - expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, { - additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - allowRetry: true, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - }); + expect(handleErrorSpy).toHaveBeenCalledWith( + exampleError, + expect.objectContaining({ + additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); expect(statSpy).toHaveBeenCalledTimes(2); expect(statSpy).toHaveBeenCalledWith(testUris.ps); mvsApiMock.mockRestore(); @@ -905,12 +906,14 @@ describe("delete", () => { expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePs.name, { responseTimeout: undefined }); expect(_lookupMock).toHaveBeenCalledWith(testUris.ps, false); expect(_fireSoonMock).toHaveBeenCalled(); - expect(handleErrorMock).toHaveBeenCalledWith(sampleError, { - additionalContext: "Failed to delete /USER.DATA.PS", - allowRetry: true, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - }); + expect(handleErrorMock).toHaveBeenCalledWith( + sampleError, + expect.objectContaining({ + additionalContext: "Failed to delete /USER.DATA.PS", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); expect(fakeSession.entries.has(fakePs.name)).toBe(true); mvsApiMock.mockRestore(); }); @@ -994,12 +997,14 @@ describe("rename", () => { DatasetFSProvider.instance.rename(testUris.pds, testUris.pds.with({ path: "/USER.DATA.PDS2" }), { overwrite: true }) ).rejects.toThrow(); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PDS", "USER.DATA.PDS2"); - expect(handleErrorMock).toHaveBeenCalledWith(sampleError, { - additionalContext: "Failed to rename USER.DATA.PDS", - allowRetry: true, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - }); + expect(handleErrorMock).toHaveBeenCalledWith( + sampleError, + expect.objectContaining({ + additionalContext: "Failed to rename USER.DATA.PDS", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); _lookupParentDirectoryMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 9b40a7bc6..b7bf87c07 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -912,12 +912,14 @@ describe("delete", () => { await expect(UssFSProvider.instance.delete(testUris.file, { recursive: false })).rejects.toThrow(); expect(getDelInfoMock).toHaveBeenCalledWith(testUris.file); expect(deleteMock).toHaveBeenCalledWith(testEntries.file.metadata.path, false); - expect(handleErrorMock).toHaveBeenCalledWith(exampleError, { - additionalContext: "Failed to delete /aFile.txt", - allowRetry: true, - apiType: ZoweExplorerApiType.Uss, - profileType: testEntries.file.metadata.profile.type, - }); + expect(handleErrorMock).toHaveBeenCalledWith( + exampleError, + expect.objectContaining({ + additionalContext: "Failed to delete /aFile.txt", + apiType: ZoweExplorerApiType.Uss, + profileType: testEntries.file.metadata.profile.type, + }) + ); expect(sesEntry.entries.has("aFile.txt")).toBe(true); expect(sesEntry.size).toBe(1); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts index c3c5186f5..cc35c6faf 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/ZoweUSSNode.unit.test.ts @@ -443,10 +443,9 @@ describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { const newFullPath = "/u/user/newName"; const errMessageMock = jest.spyOn(Gui, "errorMessage").mockImplementation(); - const renameMock = jest.spyOn(UssFSProvider.instance, "rename").mockRejectedValueOnce(new Error("Rename error: file is busy")); + const renameMock = jest.spyOn(vscode.workspace.fs, "rename").mockRejectedValueOnce(new Error("Rename error: file is busy")); await blockMocks.ussDir.rename(newFullPath); - expect(errMessageMock).toHaveBeenCalledWith("Rename error: file is busy"); errMessageMock.mockRestore(); renameMock.mockRestore(); }); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 8335d0f9b..8c3fe2231 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -101,16 +101,16 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); } } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get stats for data set {0}", args: [uri.path], comment: "Data set path" }), - allowRetry: true, + retry: { + fn: this.stat.bind(this), + args: [uri], + }, apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, }); - if (userResponse === "Retry") { - return this.stat(uri); - } - throw correlation.asError(); + throw err; } // Attempt to parse a successful API response and update the data set's cached stats. @@ -154,15 +154,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } } catch (err) { - const { userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list datasets"), - allowRetry: true, + retry: { + fn: this.fetchEntriesForProfile.bind(this), + args: [uri, uriInfo, pattern], + }, apiType: ZoweExplorerApiType.Mvs, profileType: this._getInfoFromUri(uri).profile?.type, }); - if (userResponse === "Retry") { - return this.fetchEntriesForProfile(uri, uriInfo, pattern); - } } for (const resp of datasetResponses) { for (const ds of resp.apiResponse?.items ?? resp.apiResponse ?? []) { @@ -198,15 +198,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem try { members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); } catch (err) { - const { userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list dataset members"), - allowRetry: true, + retry: { + fn: this.fetchEntriesForDataset.bind(this), + args: [entry, uri, uriInfo], + }, apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, }); - if (userResponse === "Retry") { - return this.fetchEntriesForDataset(entry, uri, uriInfo); - } } const pdsExtension = DatasetUtils.getExtension(entry.name); @@ -227,7 +227,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem entry = this.lookup(uri, false) as PdsEntry | DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { - await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t("Failed to locate data set {0}", uri.path), apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, @@ -253,15 +253,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem throw vscode.FileSystemError.FileNotFound(uri); } } catch (err) { - const { userResponse } = await this._handleError(err, { - allowRetry: true, + this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list data set {0}", uri.path), apiType: ZoweExplorerApiType.Mvs, + retry: { + fn: this.fetchDataset.bind(this), + args: [uri, uriInfo], + }, profileType: uriInfo.profile?.type, }); - if (userResponse === "Retry") { - return this.fetchDataset(uri, uriInfo); - } return undefined; } } @@ -397,20 +397,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem stream: bufBuilder, }); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [uri.path], comment: ["File path"], }), - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, + retry: { + fn: this.fetchDatasetAtUri.bind(this), + args: [uri, options], + }, profileType: this._getInfoFromUri(uri).profile?.type, }); - if (userResponse === "Retry") { - return this.fetchDatasetAtUri(uri, options); - } - throw correlation.asError(); + throw err; } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); @@ -446,20 +446,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to read {0}", args: [uri.path], comment: ["File path"], }), - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: this._getInfoFromUri(uri).profile?.type, + retry: { + fn: this.readFile.bind(this), + args: [uri], + }, }); - if (userResponse === "Retry") { - return this.readFile(uri); - } - throw correlation.asError(); + throw err; } // check if parent directory exists; if not, do a remote lookup @@ -597,20 +597,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to save {0}", args: [entry.metadata.path], comment: ["File path"], }), - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: entry.metadata.profile.type, + retry: { + fn: this.writeFile.bind(this), + args: [uri, content, options], + }, }); - if (userResponse === "Retry") { - return this.writeFile(uri, content, options); - } - throw correlation.asError(); + throw err; } entry.data = content; @@ -655,20 +655,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem responseTimeout: entry.metadata.profile.profile?.responseTimeout, }); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete {0}", args: [entry.metadata.path], comment: ["File path"], }), - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: entry.metadata.profile.type, + retry: { + fn: this.delete.bind(this), + args: [uri, _options], + }, }); - if (userResponse === "Retry") { - return this.delete(uri, _options); - } - throw correlation.asError(); + throw err; } parent.entries.delete(entry.name); @@ -701,20 +701,20 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ); } } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to rename {0}", args: [oldName], comment: ["Data set name"], }), - allowRetry: true, apiType: ZoweExplorerApiType.Mvs, profileType: entry.metadata.profile.type, + retry: { + fn: this.rename.bind(this), + args: [oldUri, newUri, options], + }, }); - if (userResponse === "Retry") { - return this.rename(oldUri, newUri, options); - } - throw correlation.asError(); + throw err; } parentDir.entries.delete(entry.name); diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index dc5a40b49..9486a1942 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -116,15 +116,15 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv } } } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { - allowRetry: true, + this._handleError(err, { apiType: ZoweExplorerApiType.Jes, profileType: uriInfo.profile?.type, + retry: { + fn: this.readDirectory.bind(this), + args: [uri], + }, }); - if (userResponse === "Retry") { - return this.readDirectory(uri); - } - throw correlation.asError(); + throw err; } for (const entry of fsEntry.entries) { @@ -233,20 +233,20 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv try { await this.fetchSpoolAtUri(uri); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [spoolEntry.name], comment: "Spool name", }), - allowRetry: true, apiType: ZoweExplorerApiType.Jes, profileType: spoolEntry.metadata.profile.type, + retry: { + fn: this.readFile.bind(this), + args: [uri], + }, }); - if (userResponse === "Retry") { - return this.readFile(uri); - } - throw correlation.asError(); + throw err; } spoolEntry.wasAccessed = true; } @@ -320,20 +320,20 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getJesApi(profInfo.profile).deleteJob(entry.job.jobname, entry.job.jobid); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete job {0}", args: [entry.job.jobname], comment: "Job name", }), - allowRetry: true, apiType: ZoweExplorerApiType.Jes, profileType: profInfo.profile.type, + retry: { + fn: this.delete.bind(this), + args: [uri, options], + }, }); - if (userResponse === "Retry") { - return this.delete(uri, options); - } - throw correlation.asError(); + throw err; } } parent.entries.delete(entry.name); diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 236db72d1..71001d289 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -124,16 +124,16 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ussApi.move(oldInfo.path, info.path); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to move {0}", args: [oldInfo.path], comment: "File path" }), apiType: ZoweExplorerApiType.Uss, - allowRetry: true, + retry: { + fn: this.move.bind(this), + args: [oldUri, newUri], + }, profileType: info.profile.type, }); - if (userResponse === "Retry") { - return this.move(oldUri, newUri); - } - throw correlation.asError(); + throw err; } await this._relocateEntry(oldUri, newUri, info.path); return true; @@ -150,15 +150,15 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv await vscode.workspace.fs.createDirectory(uri); } } catch (err) { - const { userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t("Failed to list {0}", err.message), apiType: ZoweExplorerApiType.Uss, - allowRetry: true, + retry: { + fn: this.listFiles.bind(this), + args: [profile, uri, keepRelative], + }, profileType: profile.type, }); - if (userResponse === "Retry") { - return this.listFiles(profile, uri, keepRelative); - } return { success: false, commandResponse: err.message }; } @@ -285,6 +285,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const metadata = file.metadata; await this.autoDetectEncoding(file as UssFile); const profileEncoding = file.encoding ? null : file.metadata.profile.profile?.encoding; + let resp: IZosFilesResponse; try { resp = await ZoweExplorerApiRegister.getUssApi(metadata.profile).getContents(filePath, { @@ -295,20 +296,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv stream: bufBuilder, }); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to get contents for {0}", args: [filePath], comment: ["File path"], }), - allowRetry: true, + retry: { + fn: this.fetchFileAtUri.bind(this), + args: [uri, options], + }, apiType: ZoweExplorerApiType.Uss, profileType: metadata.profile.type, }); - if (userResponse === "Retry") { - return this.fetchFileAtUri(uri, options); - } - throw correlation.asError(); + throw err; } const data: Uint8Array = bufBuilder.read() ?? new Uint8Array(); @@ -349,20 +350,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entry.encoding = isBinary ? { kind: "binary" } : undefined; } } catch (err) { - const { userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to detect encoding for {0}", args: [entry.metadata.path], comment: ["File path"], }), - allowRetry: true, + retry: { + fn: this.autoDetectEncoding.bind(this), + args: [entry], + }, apiType: ZoweExplorerApiType.Uss, profileType: entry.metadata.profile.type, }); - if (userResponse === "Retry") { - return this.autoDetectEncoding(entry); - } - return; + throw err; } } @@ -523,15 +524,15 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } catch (err) { if (!err.message.includes("Rest API failure with HTTP(S) status 412")) { // Some unknown error happened, don't update the entry - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { apiType: ZoweExplorerApiType.Uss, - allowRetry: true, + retry: { + fn: this.writeFile.bind(this), + args: [uri, content, options], + }, profileType: parentDir.metadata.profile.type, }); - if (userResponse === "Retry") { - return this.writeFile(uri, content, options); - } - throw correlation.asError(); + throw err; } entry.data = content; @@ -584,20 +585,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv try { await ZoweExplorerApiRegister.getUssApi(entry.metadata.profile).rename(entry.metadata.path, newPath); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to rename {0}", args: [entry.metadata.path], comment: ["File path"], }), - allowRetry: true, + retry: { + fn: this.rename.bind(this), + args: [oldUri, newUri, options], + }, apiType: ZoweExplorerApiType.Uss, profileType: entry.metadata.profile?.type, }); - if (userResponse === "Retry") { - return this.rename(oldUri, newUri, options); - } - throw correlation.asError(); + throw err; } parentDir.entries.delete(entry.name); @@ -626,20 +627,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv entryToDelete instanceof UssDirectory ); } catch (err) { - const { correlation, userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to delete {0}", args: [entryToDelete.metadata.path], comment: ["File name"], }), - allowRetry: true, + retry: { + fn: this.delete.bind(this), + args: [uri, _options] + }, apiType: ZoweExplorerApiType.Uss, profileType: parent.metadata.profile.type, }); - if (userResponse === "Retry") { - return this.delete(uri, _options); - } - throw correlation.asError(); + throw err; } parent.entries.delete(entryToDelete.name); @@ -746,20 +747,20 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv await api.uploadFromBuffer(Buffer.from(fileEntry.data), outputPath); } } catch (err) { - const { userResponse } = await this._handleError(err, { + this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to copy {0} to {1}", args: [source.path, destination.path, err.message], comment: ["Source path", "Destination path"], }), - allowRetry: true, + retry: { + fn: this.copyTree.bind(this), + args: [source, destination, options] + }, apiType: ZoweExplorerApiType.Uss, profileType: destInfo.profile.type, }); - if (userResponse === "Retry") { - await this.copyTree(source, destination, options); - } - return; + throw err; } } diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index ce004fc10..b405e9531 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -361,7 +361,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { }); try { - await UssFSProvider.instance.rename(oldUri, newUri, { overwrite: false }); + await vscode.workspace.fs.rename(oldUri, newUri, { overwrite: false }); } catch (err) { Gui.errorMessage(err.message); return; From 746ebdfcc5ac8707822392c6d44ce42065715f7e Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 21 Oct 2024 14:55:07 -0400 Subject: [PATCH 36/62] wip: add coverage to DataSet FSP Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 93 ++++++++++++++++++- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 6a02942f2..79535ac48 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -249,6 +249,22 @@ describe("fetchDatasetAtUri", () => { mvsApiMock.mockRestore(); }); + it("calls _handleError and throws error if API call fails", async () => { + const mockMvsApi = { + getContents: jest.fn().mockRejectedValue(new Error("unknown API error")), + }; + const fakePo = { ...testEntries.ps }; + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + await expect(DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { isConflict: true })).rejects.toThrow(); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); + + lookupAsFileMock.mockRestore(); + mvsApiMock.mockRestore(); + }); + it("calls _updateResourceInEditor if 'editor' is specified", async () => { const contents = "dataset contents"; const mockMvsApi = { @@ -315,6 +331,26 @@ describe("readFile", () => { remoteLookupForResourceMock.mockRestore(); }); + it("calls _handleError and throws error if an unknown error occurred during lookup", async () => { + const _lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockImplementationOnce((uri) => { + throw Error("unknown fs error"); + }); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + + let err; + try { + await DatasetFSProvider.instance.readFile(testUris.ps); + } catch (error) { + err = error; + expect(err.message).toBe("unknown fs error"); + } + expect(err).toBeDefined(); + expect(_lookupAsFileMock).toHaveBeenCalledWith(testUris.ps); + expect(_handleErrorMock).toHaveBeenCalled(); + _lookupAsFileMock.mockRestore(); + _handleErrorMock.mockRestore(); + }); + it("calls fetchDatasetAtUri if the entry has not yet been accessed", async () => { const _lookupAsFileMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupAsFile") @@ -458,6 +494,35 @@ describe("writeFile", () => { lookupMock.mockRestore(); }); + it("calls _handleError when there is an API error", async () => { + const mockMvsApi = { + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure")), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const psEntry = { ...testEntries.ps, metadata: testEntries.ps.metadata } as DsEntry; + const sessionEntry = { ...testEntries.session }; + sessionEntry.entries.set("USER.DATA.PS", psEntry); + const lookupParentDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(sessionEntry); + const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(psEntry); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const newContents = new Uint8Array([3, 6, 9]); + await expect(DatasetFSProvider.instance.writeFile(testUris.ps, newContents, { create: false, overwrite: true })).rejects.toThrow(); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.ps); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving data set..."); + expect(mockMvsApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.ps.name, { + binary: false, + encoding: undefined, + etag: testEntries.ps.etag, + returnEtag: true, + }); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); + mvsApiMock.mockRestore(); + lookupMock.mockRestore(); + }); + it("upload changes to a remote DS even if its not yet in the FSP", async () => { const mockMvsApi = { uploadFromBuffer: jest.fn().mockResolvedValueOnce({ @@ -673,11 +738,14 @@ describe("stat", () => { } as any); const handleErrorSpy = jest.spyOn(DatasetFSProvider.instance as any, "_handleError"); await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); - expect(handleErrorSpy).toHaveBeenCalledWith(exampleError, expect.objectContaining({ - additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - })); + expect(handleErrorSpy).toHaveBeenCalledWith( + exampleError, + expect.objectContaining({ + additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + }) + ); mvsApiMock.mockRestore(); getInfoForUriMock.mockRestore(); lookupMock.mockRestore(); @@ -822,6 +890,21 @@ describe("fetchDataset", () => { }); }); }); + it("calls _handleError whenever an unknown filesystem error occurs", async () => { + const lookupMock = jest.spyOn(DatasetFSProvider.instance, "lookup").mockImplementation(() => { + throw new Error("unknown fs error"); + }); + const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + await (DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { + isRoot: false, + slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); + lookupMock.mockRestore(); + }); }); describe("delete", () => { From 4453ef648b0ad2cf49f9b3aab8ad9a7a97645fd8 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 21 Oct 2024 15:26:57 -0400 Subject: [PATCH 37/62] wip: more ds/uss test cases Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 46 ++++++++++ .../trees/uss/UssFSProvider.unit.test.ts | 86 +++++++++++++++++++ .../src/trees/dataset/DatasetFSProvider.ts | 1 + .../src/trees/uss/UssFSProvider.ts | 6 +- 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 79535ac48..c65f31440 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -811,6 +811,52 @@ describe("fetchEntriesForDataset", () => { expect(allMembersMock).toHaveBeenCalled(); mvsApiMock.mockRestore(); }); + it("calls _handleError in the case of an API error", async () => { + const allMembersMock = jest.fn().mockRejectedValue(new Error("API error")); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + allMembers: allMembersMock, + } as any); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const fakePds = Object.assign(Object.create(Object.getPrototypeOf(testEntries.pds)), testEntries.pds); + await expect( + (DatasetFSProvider.instance as any).fetchEntriesForDataset(fakePds, testUris.pds, { + isRoot: false, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }) + ).rejects.toThrow(); + expect(allMembersMock).toHaveBeenCalled(); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + mvsApiMock.mockRestore(); + }); +}); + +describe("fetchEntriesForProfile", () => { + it("calls _handleError in the case of an API error", async () => { + const dataSetsMatchingPattern = jest.fn().mockRejectedValue(new Error("API error")); + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ + dataSetsMatchingPattern, + } as any); + const fakeSession = Object.assign(Object.create(Object.getPrototypeOf(testEntries.session)), testEntries.session); + const _handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); + const lookupAsDirMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(fakeSession); + await (DatasetFSProvider.instance as any).fetchEntriesForProfile( + testUris.session, + { + isRoot: true, + slashAfterProfilePos: testUris.pds.path.indexOf("/", 1), + profileName: "sestest", + profile: testProfile, + }, + "PUBLIC.*" + ); + expect(_handleErrorMock).toHaveBeenCalled(); + expect(lookupAsDirMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + mvsApiMock.mockRestore(); + }); }); describe("fetchDataset", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index b7bf87c07..cd5bbbeb2 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -139,6 +139,19 @@ describe("move", () => { expect(await UssFSProvider.instance.move(testUris.file, newUri)).toBe(false); expect(errorMsgMock).toHaveBeenCalledWith("The 'move' function is not implemented for this USS API."); }); + it("throws an error if the API request failed", async () => { + getInfoFromUriMock.mockReturnValueOnce({ + // info for new URI + path: "/aFile2.txt", + profile: testProfile, + }); + const move = jest.fn().mockRejectedValue(new Error("error during move")); + const handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ move } as any); + await expect(UssFSProvider.instance.move(testUris.file, newUri)).rejects.toThrow(); + expect(handleErrorMock).toHaveBeenCalled(); + handleErrorMock.mockRestore(); + }); }); describe("listFiles", () => { @@ -181,6 +194,15 @@ describe("listFiles", () => { }, }); }); + it("returns an unsuccessful response if an error occurred", async () => { + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + fileList: jest.fn().mockRejectedValue(new Error("error listing files")), + } as any); + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.listFiles(testProfile, testUris.folder)).rejects.toThrow(); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + }); }); describe("fetchEntries", () => { @@ -311,6 +333,23 @@ describe("fetchFileAtUri", () => { expect(fileEntry.data?.byteLength).toBe(exampleData.length); autoDetectEncodingMock.mockRestore(); }); + it("throws an error if it failed to fetch contents", async () => { + const fileEntry = { ...testEntries.file }; + const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockImplementation(); + jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ + getContents: jest.fn().mockRejectedValue(new Error("error retrieving contents")), + } as any); + + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.fetchFileAtUri(testUris.file)).rejects.toThrow(); + + expect(lookupAsFileMock).toHaveBeenCalledWith(testUris.file); + expect(autoDetectEncodingMock).toHaveBeenCalledWith(fileEntry); + expect(_handleErrorMock).toHaveBeenCalled(); + autoDetectEncodingMock.mockRestore(); + _handleErrorMock.mockRestore(); + }); it("calls getContents to get the data for a file entry with encoding", async () => { const fileEntry = { ...testEntries.file }; const lookupAsFileMock = jest.spyOn((UssFSProvider as any).prototype, "_lookupAsFile").mockReturnValueOnce(fileEntry); @@ -417,6 +456,20 @@ describe("autoDetectEncoding", () => { } as any); }); + it("throws error if getTag call fails", async () => { + getTagMock.mockRejectedValueOnce(new Error("error fetching tag")); + const testEntry = new UssFile("testFile"); + testEntry.metadata = { + path: "/testFile", + profile: testProfile, + }; + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.autoDetectEncoding(testEntry)).rejects.toThrow(); + expect(getTagMock).toHaveBeenCalledTimes(1); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + }); + it("sets encoding if file tagged as binary", async () => { getTagMock.mockResolvedValueOnce("binary"); const testEntry = new UssFile("testFile"); @@ -632,6 +685,39 @@ describe("writeFile", () => { ussApiMock.mockRestore(); }); + it("throws an error when an unknown API error occurs", async () => { + const mockUssApi = { + uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure")), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); + const statusMsgMock = jest.spyOn(Gui, "setStatusBarMessage"); + const folder = { + ...testEntries.folder, + entries: new Map([[testEntries.file.name, { ...testEntries.file }]]), + }; + const lookupParentDirMock = jest.spyOn(UssFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce(folder); + const autoDetectEncodingMock = jest.spyOn(UssFSProvider.instance, "autoDetectEncoding").mockResolvedValue(undefined); + const newContents = new Uint8Array([3, 6, 9]); + const handleConflictMock = jest.spyOn(UssFSProvider.instance as any, "_handleConflict").mockImplementation(); + const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); + await expect(UssFSProvider.instance.writeFile(testUris.file, newContents, { create: false, overwrite: true })).rejects.toThrow(); + + expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.file); + expect(statusMsgMock).toHaveBeenCalledWith("$(sync~spin) Saving USS file..."); + expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(newContents), testEntries.file.metadata.path, { + binary: false, + encoding: undefined, + etag: testEntries.file.etag, + returnEtag: true, + }); + expect(handleConflictMock).not.toHaveBeenCalled(); + expect(_handleErrorMock).toHaveBeenCalled(); + handleConflictMock.mockRestore(); + _handleErrorMock.mockRestore(); + ussApiMock.mockRestore(); + autoDetectEncodingMock.mockRestore(); + }); + it("calls _handleConflict when there is an etag error", async () => { const mockUssApi = { uploadFromBuffer: jest.fn().mockRejectedValueOnce(new Error("Rest API failure with HTTP(S) status 412")), diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 8c3fe2231..b2a2e38a4 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -207,6 +207,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, }); + throw err; } const pdsExtension = DatasetUtils.getExtension(entry.name); diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 71001d289..ff6ac77b4 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -159,7 +159,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, profileType: profile.type, }); - return { success: false, commandResponse: err.message }; + throw err; } return { @@ -635,7 +635,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }), retry: { fn: this.delete.bind(this), - args: [uri, _options] + args: [uri, _options], }, apiType: ZoweExplorerApiType.Uss, profileType: parent.metadata.profile.type, @@ -755,7 +755,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }), retry: { fn: this.copyTree.bind(this), - args: [source, destination, options] + args: [source, destination, options], }, apiType: ZoweExplorerApiType.Uss, profileType: destInfo.profile.type, From 656036cfd329109c38c275e865ec69a9c600f04d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 21 Oct 2024 15:56:13 -0400 Subject: [PATCH 38/62] BaseProvider._handleError test cases Signed-off-by: Trae Yelovich --- .../__unit__/fs/BaseProvider.unit.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts index d5fb25052..cfe6930ae 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/BaseProvider.unit.test.ts @@ -13,6 +13,7 @@ import * as vscode from "vscode"; import { BaseProvider, ConflictViewSelection, DirEntry, FileEntry, ZoweScheme } from "../../../src/fs"; import { Gui } from "../../../src/globals"; import { MockedProperty } from "../../../__mocks__/mockUtils"; +import { ErrorCorrelator, ZoweExplorerApiType } from "../../../src"; function getGlobalMocks(): { [key: string]: any } { return { @@ -542,6 +543,35 @@ describe("_handleConflict", () => { expect(diffOverwriteMock).toHaveBeenCalledWith(globalMocks.testFileUri); }); }); +describe("_handleError", () => { + it("calls ErrorCorrelator.displayError to display error to user - no options provided", async () => { + const displayErrorMock = jest.spyOn(ErrorCorrelator.prototype, "displayError").mockReturnValue(new Promise((res, rej) => {})); + const prov = new (BaseProvider as any)(); + prov._handleError(new Error("example")); + expect(displayErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.All, new Error("example"), { + additionalContext: undefined, + allowRetry: false, + profileType: "any", + templateArgs: undefined, + }); + }); + it("calls ErrorCorrelator.displayError to display error to user - options provided", async () => { + const displayErrorMock = jest.spyOn(ErrorCorrelator.prototype, "displayError").mockReturnValue(new Promise((res, rej) => {})); + const prov = new (BaseProvider as any)(); + prov._handleError(new Error("example"), { + additionalContext: "Failed to list data sets", + apiType: ZoweExplorerApiType.Mvs, + profileType: "zosmf", + templateArgs: {}, + }); + expect(displayErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, new Error("example"), { + additionalContext: "Failed to list data sets", + allowRetry: false, + profileType: "zosmf", + templateArgs: {}, + }); + }); +}); describe("_relocateEntry", () => { it("returns early if the entry does not exist in the file system", () => { From 1884cb58dfb17057383b443e2650a003219e0239 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 21 Oct 2024 16:18:53 -0400 Subject: [PATCH 39/62] jobs test cases for error handling Signed-off-by: Trae Yelovich --- .../trees/job/JobFSProvider.unit.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts index 1d8248c4d..042c3382e 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts @@ -153,6 +153,23 @@ describe("readDirectory", () => { expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(testEntries.job.job?.jobname, testEntries.job.job?.jobid); jesApiMock.mockRestore(); }); + + it("throws error when API error occurs", async () => { + const mockJesApi = { + getSpoolFiles: jest.fn().mockRejectedValue(new Error("Failed to fetch spools")), + }; + const jesApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockJesApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const _handleErrorMock = jest.spyOn(JobFSProvider.instance as any, "_handleError").mockImplementation(); + const lookupAsDirMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValueOnce(fakeJob); + await expect(JobFSProvider.instance.readDirectory(testUris.job)).rejects.toThrow(); + expect(lookupAsDirMock).toHaveBeenCalledWith(testUris.job, false); + expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(testEntries.job.job?.jobname, testEntries.job.job?.jobid); + expect(_handleErrorMock).toHaveBeenCalled(); + jesApiMock.mockRestore(); + _handleErrorMock.mockRestore(); + }); }); describe("updateFilterForUri", () => { @@ -219,6 +236,19 @@ describe("readFile", () => { lookupAsFileMock.mockRestore(); fetchSpoolAtUriMock.mockRestore(); }); + it("throws error if an error occurred while fetching spool", async () => { + const spoolEntry = { ...testEntries.spool }; + const lookupAsFileMock = jest.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(spoolEntry); + const _handleErrorMock = jest.spyOn(JobFSProvider.instance as any, "_handleError").mockImplementation(); + const fetchSpoolAtUriMock = jest + .spyOn(JobFSProvider.instance, "fetchSpoolAtUri") + .mockRejectedValueOnce(new Error("Failed to fetch contents for spool")); + await expect(JobFSProvider.instance.readFile(testUris.spool)).rejects.toThrow(); + expect(_handleErrorMock).toHaveBeenCalled(); + _handleErrorMock.mockRestore(); + lookupAsFileMock.mockRestore(); + fetchSpoolAtUriMock.mockRestore(); + }); }); describe("writeFile", () => { @@ -314,6 +344,25 @@ describe("delete", () => { lookupMock.mockRestore(); lookupParentDirMock.mockRestore(); }); + it("throws an error if an API error occurs during deletion", async () => { + const mockUssApi = { + deleteJob: jest.fn().mockRejectedValue(new Error("Failed to delete job")), + }; + const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getJesApi").mockReturnValueOnce(mockUssApi as any); + const fakeJob = new JobEntry(testEntries.job.name); + fakeJob.job = testEntries.job.job; + const lookupMock = jest.spyOn(JobFSProvider.instance as any, "lookup").mockReturnValueOnce(fakeJob); + const lookupParentDirMock = jest + .spyOn(JobFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }); + await expect(JobFSProvider.instance.delete(testUris.job, { recursive: true, deleteRemote: true })).rejects.toThrow(); + const jobInfo = testEntries.job.job; + expect(jobInfo).not.toBeUndefined(); + expect(mockUssApi.deleteJob).toHaveBeenCalledWith(jobInfo?.jobname || "TESTJOB", jobInfo?.jobid || "JOB12345"); + ussApiMock.mockRestore(); + lookupMock.mockRestore(); + lookupParentDirMock.mockRestore(); + }); it("does not delete a spool from the FSP and remote file system", async () => { const mockUssApi = { From d9597e3d99746e77c9278c5fa6d40a1f7fb39a99 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Mon, 21 Oct 2024 16:30:28 -0400 Subject: [PATCH 40/62] chore: update changelogs Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/CHANGELOG.md | 1 + packages/zowe-explorer/CHANGELOG.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 9fb5d5010..fc1945da7 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Zowe Explorer now includes support for the [VS Code display languages](https://code.visualstudio.com/docs/getstarted/locales) French, German, Japanese, Portuguese, and Spanish. [#3239](https://github.com/zowe/zowe-explorer-vscode/pull/3239) - Localization of strings within the webviews. [#2983](https://github.com/zowe/zowe-explorer-vscode/issues/2983) +- Implemented an error correlation facility for providing user-friendly summaries of API and network errors. Extenders can contribute to the correlator to provide human-readable translations of error messages, as well as tips and additional resources for how to resolve the error. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ## `3.0.1` diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 7c1be70a6..52cf170e5 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Update Zowe SDKs to `8.2.0` to get the latest enhancements from Imperative. - Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175) +- Implemented more user-friendly error messages when encountering API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Added a "Troubleshoot" option for certain errors to provide additional context, tips, and resources for how to resolve them. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ### Bug fixes From 732a083d1cd39256df56ce27e16520315bfda7a3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 22 Oct 2024 15:30:09 -0400 Subject: [PATCH 41/62] expose error correlator in extender API Signed-off-by: Trae Yelovich --- .../src/extend/IApiExplorerExtender.ts | 8 +++++++ .../src/utils/ErrorCorrelator.ts | 15 +++++++++---- .../zowe-explorer-api/src/utils/Singleton.ts | 22 ------------------- .../ZoweExplorerExtender.unit.test.ts | 9 +++++++- .../src/extending/ZoweExplorerExtender.ts | 9 ++++++++ 5 files changed, 36 insertions(+), 27 deletions(-) delete mode 100644 packages/zowe-explorer-api/src/utils/Singleton.ts diff --git a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts index 89e660262..b7dc4a865 100644 --- a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts +++ b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts @@ -11,6 +11,7 @@ import * as imperative from "@zowe/imperative"; import { ProfilesCache } from "../profiles/ProfilesCache"; +import { ErrorCorrelator } from ".."; /** * This interface can be used by other VS Code Extensions to access an alternative @@ -45,4 +46,11 @@ export interface IApiExplorerExtender { * or to create them automatically if it is non-existant. */ initForZowe(type: string, profileTypeConfigurations: imperative.ICommandProfileTypeConfiguration[]): void | Promise; + + /** + * Allows extenders to contribute error correlations, providing user-friendly + * summaries of API or network errors. Also gives extenders the opportunity to + * provide tips or additional resources for errors. + */ + getErrorCorrelator(): ErrorCorrelator; } diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 11e4658e5..eadcce1aa 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -9,7 +9,6 @@ * */ -import { Singleton } from "./Singleton"; import { Gui } from "../globals"; import { commands } from "vscode"; import Mustache = require("mustache"); @@ -121,7 +120,9 @@ export enum ZoweExplorerApiType { export type ErrorsForApiType = Map; export type ApiErrors = Record; -export class ErrorCorrelator extends Singleton { +export class ErrorCorrelator { + private static instance: ErrorCorrelator = null; + private errorMatches: ErrorsForApiType = new Map([ [ ZoweExplorerApiType.Mvs, @@ -224,8 +225,14 @@ export class ErrorCorrelator extends Singleton { ], ]); - public constructor() { - super(); + private constructor() {} + + public static getInstance(): ErrorCorrelator { + if (!ErrorCorrelator.instance) { + ErrorCorrelator.instance = new ErrorCorrelator(); + } + + return ErrorCorrelator.instance; } /** diff --git a/packages/zowe-explorer-api/src/utils/Singleton.ts b/packages/zowe-explorer-api/src/utils/Singleton.ts deleted file mode 100644 index 4f98b7402..000000000 --- a/packages/zowe-explorer-api/src/utils/Singleton.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - * - */ - -export abstract class Singleton { - private static instances: Map = new Map(); - - protected constructor() {} - public static getInstance(this: new () => T): T { - if (!Singleton.instances.has(this)) { - Singleton.instances.set(this, new this()); - } - return Singleton.instances.get(this) as T; - } -} diff --git a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts index ed8411f69..bf526108c 100644 --- a/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/extending/ZoweExplorerExtender.unit.test.ts @@ -23,7 +23,7 @@ import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; import { ZoweLogger } from "../../../src/tools/ZoweLogger"; import { UssFSProvider } from "../../../src/trees/uss/UssFSProvider"; import { ProfilesUtils } from "../../../src/utils/ProfilesUtils"; -import { FileManagement, Gui, ProfilesCache } from "@zowe/zowe-explorer-api"; +import { ErrorCorrelator, FileManagement, Gui, ProfilesCache } from "@zowe/zowe-explorer-api"; import { DatasetTree } from "../../../src/trees/dataset/DatasetTree"; import { USSTree } from "../../../src/trees/uss/USSTree"; import { JobTree } from "../../../src/trees/job/JobTree"; @@ -317,4 +317,11 @@ describe("ZoweExplorerExtender unit tests", () => { addProfTypeToSchema.mockRestore(); }); }); + + describe("getErrorCorrelator", () => { + it("returns the singleton instance of ErrorCorrelator", () => { + const blockMocks = createBlockMocks(); + expect(blockMocks.instTest.getErrorCorrelator()).toBe(ErrorCorrelator.getInstance()); + }); + }); }); diff --git a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts index 0a947aaa4..9e32d2ddf 100644 --- a/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts +++ b/packages/zowe-explorer/src/extending/ZoweExplorerExtender.ts @@ -23,6 +23,7 @@ import { IZoweExplorerTreeApi, imperative, ZoweVsCodeExtension, + ErrorCorrelator, } from "@zowe/zowe-explorer-api"; import { Constants } from "../configuration/Constants"; import { ProfilesUtils } from "../utils/ProfilesUtils"; @@ -139,6 +140,14 @@ export class ZoweExplorerExtender implements IApiExplorerExtender, IZoweExplorer public jobsProvider?: Types.IZoweJobTreeType ) {} + /** + * @implements The {@link IApiExplorerExtender.getErrorCorrelator} function + * @returns Singleton instance of the error correlator + */ + public getErrorCorrelator(): ErrorCorrelator { + return ErrorCorrelator.getInstance(); + } + /** * * @implements IApiExplorerExtender.initForZowe() From afba1921fba83ebb87110c1ad632d694d96093ef Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 22 Oct 2024 15:32:46 -0400 Subject: [PATCH 42/62] chore: address changelog feedback Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/CHANGELOG.md | 2 +- packages/zowe-explorer/CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index fc1945da7..0a5f4877a 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -16,7 +16,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Zowe Explorer now includes support for the [VS Code display languages](https://code.visualstudio.com/docs/getstarted/locales) French, German, Japanese, Portuguese, and Spanish. [#3239](https://github.com/zowe/zowe-explorer-vscode/pull/3239) - Localization of strings within the webviews. [#2983](https://github.com/zowe/zowe-explorer-vscode/issues/2983) -- Implemented an error correlation facility for providing user-friendly summaries of API and network errors. Extenders can contribute to the correlator to provide human-readable translations of error messages, as well as tips and additional resources for how to resolve the error. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Leverage the new error correlation facility to provide user-friendly summaries of API and network errors. Extenders can also contribute to the correlator to provide human-readable translations of error messages, as well as tips and additional resources for how to resolve the error. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ## `3.0.1` diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 52cf170e5..5a5df6f07 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -8,8 +8,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Update Zowe SDKs to `8.2.0` to get the latest enhancements from Imperative. - Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175) -- Implemented more user-friendly error messages when encountering API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) -- Added a "Troubleshoot" option for certain errors to provide additional context, tips, and resources for how to resolve them. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Implemented more user-friendly error messages for API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Use the "Troubleshoot" option for certain errors to obtain additional context, tips, and resources for how to resolve the errors. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) ### Bug fixes From 5ceaf6cf5c6a1f78d054aed37eceff17ddded38d Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 22 Oct 2024 15:38:40 -0400 Subject: [PATCH 43/62] fix circular dep Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts index b7dc4a865..02af80bfb 100644 --- a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts +++ b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts @@ -11,7 +11,7 @@ import * as imperative from "@zowe/imperative"; import { ProfilesCache } from "../profiles/ProfilesCache"; -import { ErrorCorrelator } from ".."; +import { ErrorCorrelator } from "../utils/ErrorCorrelator"; /** * This interface can be used by other VS Code Extensions to access an alternative @@ -49,7 +49,7 @@ export interface IApiExplorerExtender { /** * Allows extenders to contribute error correlations, providing user-friendly - * summaries of API or network errors. Also gives extenders the opportunity to + * summaries of API or network errors. Also gives extenders the opportunity to * provide tips or additional resources for errors. */ getErrorCorrelator(): ErrorCorrelator; From d9e022ad15bad7c8c17667f0709d3b296fcc8de3 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 22 Oct 2024 15:55:26 -0400 Subject: [PATCH 44/62] allow extenders to contribute resources for errors Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts | 9 +++++++++ .../src/troubleshoot-error/components/ErrorInfo.tsx | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index eadcce1aa..bf41fafbf 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -19,6 +19,11 @@ import { ImperativeError } from "@zowe/imperative"; */ type ErrorMatch = string | RegExp; +export interface ExternalResource { + href: string; + title?: string; +} + export interface ErrorCorrelation { /** * An optional error code returned from the server. @@ -40,6 +45,10 @@ export interface ErrorCorrelation { * @type {string[]} */ tips?: string[]; + /** + * Error-specific, external resources for users to help with resolution during troubleshooting. + */ + resources?: ExternalResource[]; } export interface CorrelatedErrorProps { diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index 98e2571b6..e1de90998 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -69,6 +69,11 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => {

          Additional resources

            + {error.properties.correlation?.resources?.map((r) => ( +
          • + {r.title ?? r.href} +
          • + ))}
          • GitHub
          • From 6051d71826fe92b5f8890a6335096f5fcd8421f9 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 13:08:25 -0400 Subject: [PATCH 45/62] handle errors when listing files in virtual workspaces Signed-off-by: Trae Yelovich --- .../__unit__/trees/uss/UssFSProvider.unit.test.ts | 5 ++++- .../zowe-explorer/src/trees/shared/SharedInit.ts | 8 +++++++- .../zowe-explorer/src/trees/uss/UssFSProvider.ts | 14 ++++---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index cd5bbbeb2..171465d32 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -199,7 +199,10 @@ describe("listFiles", () => { fileList: jest.fn().mockRejectedValue(new Error("error listing files")), } as any); const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); - await expect(UssFSProvider.instance.listFiles(testProfile, testUris.folder)).rejects.toThrow(); + expect(await UssFSProvider.instance.listFiles(testProfile, testUris.folder)).toStrictEqual({ + success: false, + commandResponse: "error listing files", + }); expect(_handleErrorMock).toHaveBeenCalled(); _handleErrorMock.mockRestore(); }); diff --git a/packages/zowe-explorer/src/trees/shared/SharedInit.ts b/packages/zowe-explorer/src/trees/shared/SharedInit.ts index fdc8106f1..1a09c800d 100644 --- a/packages/zowe-explorer/src/trees/shared/SharedInit.ts +++ b/packages/zowe-explorer/src/trees/shared/SharedInit.ts @@ -406,7 +406,13 @@ export class SharedInit { (f) => f.uri.scheme === ZoweScheme.DS || f.uri.scheme === ZoweScheme.USS ); for (const folder of newWorkspaces) { - await (folder.uri.scheme === ZoweScheme.DS ? DatasetFSProvider.instance : UssFSProvider.instance).remoteLookupForResource(folder.uri); + try { + await (folder.uri.scheme === ZoweScheme.DS ? DatasetFSProvider.instance : UssFSProvider.instance).remoteLookupForResource(folder.uri); + } catch (err) { + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + } } } diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index ff6ac77b4..41df761a7 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -150,16 +150,10 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv await vscode.workspace.fs.createDirectory(uri); } } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t("Failed to list {0}", err.message), - apiType: ZoweExplorerApiType.Uss, - retry: { - fn: this.listFiles.bind(this), - args: [profile, uri, keepRelative], - }, - profileType: profile.type, - }); - throw err; + if (err instanceof Error) { + ZoweLogger.error(err.message); + } + return { success: false, commandResponse: err instanceof Error ? err.message : JSON.stringify(err) }; } return { From 194dc41b9fcada44758e13be80cadcf42f98bc66 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 13:49:11 -0400 Subject: [PATCH 46/62] remove check for handleError mock in listFiles test Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 171465d32..819e4d720 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -198,13 +198,10 @@ describe("listFiles", () => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ fileList: jest.fn().mockRejectedValue(new Error("error listing files")), } as any); - const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); expect(await UssFSProvider.instance.listFiles(testProfile, testUris.folder)).toStrictEqual({ success: false, commandResponse: "error listing files", }); - expect(_handleErrorMock).toHaveBeenCalled(); - _handleErrorMock.mockRestore(); }); }); From c6c2fe8b0b112241e79d3dc62db85bc162ca09b6 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 15:13:32 -0400 Subject: [PATCH 47/62] skip dialog if no correlation found, fix missing info in webview Signed-off-by: Trae Yelovich --- .../src/utils/ErrorCorrelator.ts | 52 +++++++++---------- packages/zowe-explorer/src/utils/AuthUtils.ts | 12 ++--- .../components/ErrorInfo.tsx | 14 ++--- 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index bf41fafbf..747836bbf 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -81,27 +81,28 @@ export interface HandledErrorInfo { */ export class CorrelatedError { public errorCode?: string; + public message: string; + private wasCorrelated: boolean; public constructor(public properties: CorrelatedErrorProps) { - if (properties.initialError instanceof Error) { - // preserve stack from initial error + this.errorCode = properties.initialError instanceof ImperativeError ? properties.initialError.errorCode : this.properties.errorCode; + this.wasCorrelated = properties.correlation != null; + + if (this.correlationFound) { + this.message = this.properties.correlation.summary; + } else { + this.message = this.properties.initialError instanceof Error ? this.properties.initialError.message : this.properties.initialError; } + } - this.errorCode = this.initial instanceof ImperativeError ? this.initial.errorCode : this.properties.errorCode; + public get correlationFound(): boolean { + return this.wasCorrelated; } public get stack(): string | undefined { return this.initial instanceof Error ? this.initial.stack : undefined; } - public get message(): string { - if (this.properties.correlation) { - return this.properties.correlation.summary; - } - - return this.properties.initialError instanceof Error ? this.properties.initialError.message : this.properties.initialError; - } - public get initial(): Error | string { return this.properties.initialError; } @@ -311,31 +312,30 @@ export class ErrorCorrelator { const userSelection = await Gui.errorMessage( `${opts?.additionalContext ? opts.additionalContext + ": " : ""}${error.message}${errorCodeStr}`.trim(), { - items: [opts?.allowRetry ? "Retry" : undefined, "More info"].filter(Boolean), + items: [opts?.allowRetry ? "Retry" : undefined, error.correlationFound ? "More info" : "Troubleshoot"].filter(Boolean), } ); // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - if (userSelection === "More info") { + let nextSelection: string = error.correlationFound ? undefined : userSelection; + if (error.correlationFound && userSelection === "More info") { const fullErrorMsg = error.initial instanceof Error ? error.initial.message : error.initial; - const secondDialogSelection = await Gui.errorMessage(fullErrorMsg, { + nextSelection = await Gui.errorMessage(fullErrorMsg, { items: ["Show log", "Troubleshoot"], }); - - switch (secondDialogSelection) { - // Reveal the output channel when the "Show log" option is selected - case "Show log": - return commands.executeCommand("zowe.revealOutputChannel"); - // Show the troubleshooting webview when the "Troubleshoot" option is selected - case "Troubleshoot": - return commands.executeCommand("zowe.troubleshootError", error, error.stack); - default: - return; - } } - return userSelection; + switch (nextSelection) { + // Reveal the output channel when the "Show log" option is selected + case "Show log": + return commands.executeCommand("zowe.revealOutputChannel"); + // Show the troubleshooting webview when the "Troubleshoot" option is selected + case "Troubleshoot": + return commands.executeCommand("zowe.troubleshootError", error, error.stack); + default: + return nextSelection; + } } /** diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 2343630e8..c2dbd96c9 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -80,14 +80,10 @@ export class AuthUtils { ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, moreInfo }, { depth: null })); const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; - const correlation = ErrorCorrelator.getInstance().correlateError( - moreInfo?.apiType ?? ZoweExplorerApiType.All, - typeof errorDetails === "string" ? errorDetails : errorDetails.message, - { - profileType: profile?.type, - ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), - } - ); + const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, { + profileType: profile?.type, + ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), + }); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { const imperativeError: imperative.ImperativeError = errorDetails as imperative.ImperativeError; const httpErrorCode = Number(imperativeError.mDetails.errorCode); diff --git a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx index e1de90998..dc2cc66f2 100644 --- a/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx +++ b/packages/zowe-explorer/src/webviews/src/troubleshoot-error/components/ErrorInfo.tsx @@ -23,10 +23,8 @@ export const ErrorInfo = ({ error, stackTrace }: ErrorInfoProps) => { {error.errorCode ?? "Not available"}

            - - Description:
            -
            - {error.properties.correlation?.summary} + Description: + {error.message}

            { />
            - {error.properties.correlation?.tips ? : null} - + {error.properties.correlation?.tips ? ( + <> + + + + ) : null}

            Additional resources

              {error.properties.correlation?.resources?.map((r) => ( From c1a14f4c64be3404deb1fe65eeca9ba376db3d50 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 15:28:54 -0400 Subject: [PATCH 48/62] fix tests, update logic for returning selection Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts | 4 +++- packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts index 5262fd995..43dd68a42 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/utils/ErrorCorrelator.unit.test.ts @@ -76,7 +76,9 @@ describe("displayError", () => { it("calls correlateError to get an error correlation", async () => { const correlateErrorMock = jest .spyOn(ErrorCorrelator.prototype, "correlateError") - .mockReturnValueOnce(new CorrelatedError({ initialError: "Summary of network error" })); + .mockReturnValueOnce( + new CorrelatedError({ correlation: { summary: "Summary of network error" }, initialError: "This is the full error message" }) + ); const errorMessageMock = jest.spyOn(Gui, "errorMessage").mockResolvedValueOnce(undefined); await ErrorCorrelator.getInstance().displayError(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); expect(correlateErrorMock).toHaveBeenCalledWith(ZoweExplorerApiType.Mvs, "This is the full error message", { profileType: "zosmf" }); diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index 747836bbf..fae934c4d 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -88,7 +88,7 @@ export class CorrelatedError { this.errorCode = properties.initialError instanceof ImperativeError ? properties.initialError.errorCode : this.properties.errorCode; this.wasCorrelated = properties.correlation != null; - if (this.correlationFound) { + if (this.wasCorrelated) { this.message = this.properties.correlation.summary; } else { this.message = this.properties.initialError instanceof Error ? this.properties.initialError.message : this.properties.initialError; @@ -318,7 +318,7 @@ export class ErrorCorrelator { // If the user selected "More info", show the full error details in a dialog, // containing "Show log" and "Troubleshoot" dialog options - let nextSelection: string = error.correlationFound ? undefined : userSelection; + let nextSelection = userSelection; if (error.correlationFound && userSelection === "More info") { const fullErrorMsg = error.initial instanceof Error ? error.initial.message : error.initial; nextSelection = await Gui.errorMessage(fullErrorMsg, { From 1c1e0a98482e3e6e20919a1acc8866f3f1439592 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 15:33:45 -0400 Subject: [PATCH 49/62] offer show log opt in first dialog if correlation not found Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts index fae934c4d..37874cda9 100644 --- a/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts +++ b/packages/zowe-explorer-api/src/utils/ErrorCorrelator.ts @@ -312,7 +312,9 @@ export class ErrorCorrelator { const userSelection = await Gui.errorMessage( `${opts?.additionalContext ? opts.additionalContext + ": " : ""}${error.message}${errorCodeStr}`.trim(), { - items: [opts?.allowRetry ? "Retry" : undefined, error.correlationFound ? "More info" : "Troubleshoot"].filter(Boolean), + items: [opts?.allowRetry ? "Retry" : undefined, ...(error.correlationFound ? ["More info"] : ["Show log", "Troubleshoot"])].filter( + Boolean + ), } ); From 3ad3b930c13d6855c3b4eccd326cb1f147e65e90 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 24 Oct 2024 15:54:49 -0400 Subject: [PATCH 50/62] omit profile details from log, update failing tests Signed-off-by: Trae Yelovich --- .../__unit__/trees/dataset/DatasetActions.unit.test.ts | 8 ++++---- .../trees/dataset/ZoweDatasetNode.unit.test.ts | 2 +- .../__unit__/trees/uss/UssFSProvider.unit.test.ts | 2 +- .../__unit__/utils/ProfilesUtils.unit.test.ts | 10 +++++----- packages/zowe-explorer/src/utils/AuthUtils.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts index 1c70721b9..69f799fd6 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetActions.unit.test.ts @@ -213,7 +213,7 @@ describe("Dataset Actions Unit Tests - Function createMember", () => { // Prevent exception from failing test } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error when uploading to data set", { items: ["More info"] }); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Error when uploading to data set", { items: ["Show log", "Troubleshoot"] }); mocked(zosfiles.Upload.bufferToDataSet).mockReset(); }); it("Checking of attempt to create member without name", async () => { @@ -770,7 +770,7 @@ describe("Dataset Actions Unit Tests - Function deleteDataset", () => { mocked(vscode.window.showQuickPick).mockResolvedValueOnce("Delete" as any); jest.spyOn(vscode.workspace.fs, "delete").mockRejectedValueOnce(Error("Deletion error")); await expect(DatasetActions.deleteDataset(node, blockMocks.testDatasetTree)).rejects.toThrow(""); - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Deletion error", { items: ["More info"] }); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Deletion error", { items: ["Show log", "Troubleshoot"] }); }); it("Checking Favorite PDS dataset deletion", async () => { createGlobalMocks(); @@ -1123,7 +1123,7 @@ describe("Dataset Actions Unit Tests - Function showAttributes", () => { Error("No matching names found for query: AUSER.A1557332.A996850.TEST1") ); expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("No matching names found for query: AUSER.A1557332.A996850.TEST1", { - items: ["More info"], + items: ["Show log", "Troubleshoot"], }); expect(mocked(vscode.window.createWebviewPanel)).not.toHaveBeenCalled(); }); @@ -2394,7 +2394,7 @@ describe("Dataset Actions Unit Tests - Function createFile", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Generic Error", { items: ["More info"] }); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("Generic Error", { items: ["Show log", "Troubleshoot"] }); expect(mocked(vscode.workspace.getConfiguration)).toHaveBeenLastCalledWith(Constants.SETTINGS_DS_DEFAULT_PS); expect(createDataSetSpy).toHaveBeenCalledWith(zosfiles.CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, "TEST", { alcunit: "CYL", diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts index 5be608320..0dde39981 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts @@ -520,7 +520,7 @@ describe("ZoweDatasetNode Unit Tests - Function node.openDs()", () => { // do nothing } - expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("testError", { items: ["More info"] }); + expect(mocked(Gui.errorMessage)).toHaveBeenCalledWith("testError", { items: ["Show log", "Troubleshoot"] }); }); it("Checking of opening for PDS Member", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 819e4d720..ec2dfb0aa 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -954,7 +954,7 @@ describe("rename", () => { expect(mockUssApi.rename).toHaveBeenCalledWith("/aFolder", "/aFolder2"); expect(folderEntry.metadata.path).toBe("/aFolder"); expect(sessionEntry.entries.has("aFolder2")).toBe(false); - expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename /aFolder: could not upload file", { items: ["Retry", "More info"] }); + expect(errMsgSpy).toHaveBeenCalledWith("Failed to rename /aFolder: could not upload file", { items: ["Retry", "Show log", "Troubleshoot"] }); lookupMock.mockRestore(); ussApiMock.mockRestore(); diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts index 5a0d13e1c..0ed3cc887 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfilesUtils.unit.test.ts @@ -93,9 +93,9 @@ describe("ProfilesUtils unit tests", () => { const errorDetails = new Error("i haz error"); const scenario = "Task failed successfully"; await AuthUtils.errorHandling(errorDetails, { scenario }); - expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["More info"] }); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["Show log", "Troubleshoot"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `${errorDetails.toString()}\n` + util.inspect({ errorDetails, moreInfo: { scenario } }, { depth: null }) + `${errorDetails.toString()}\n` + util.inspect({ errorDetails, ...{ scenario, profile: undefined } }, { depth: null }) ); }); @@ -110,9 +110,9 @@ describe("ProfilesUtils unit tests", () => { const scenario = "Task failed successfully"; await AuthUtils.errorHandling(errorDetails, { scenario }); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["More info"] }); + expect(Gui.errorMessage).toHaveBeenCalledWith(errorDetails.message, { items: ["Show log", "Troubleshoot"] }); expect(ZoweLogger.error).toHaveBeenCalledWith( - `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, moreInfo: { scenario } }, { depth: null }) + `Error: ${errorDetails.message}\n` + util.inspect({ errorDetails, ...{ scenario, profile: undefined } }, { depth: null }) ); }); @@ -571,7 +571,7 @@ describe("ProfilesUtils unit tests", () => { await ProfilesUtils.initializeZoweProfiles((msg) => ZoweExplorerExtender.showZoweConfigError(msg)); expect(initZoweFolderSpy).toHaveBeenCalledTimes(1); expect(readConfigFromDiskSpy).toHaveBeenCalledTimes(1); - expect(Gui.errorMessage).toHaveBeenCalledWith(testError.message, { items: ["More info"] }); + expect(Gui.errorMessage).toHaveBeenCalledWith(testError.message, { items: ["Show log", "Troubleshoot"] }); }); it("should handle JSON parse error thrown on read config from disk", async () => { diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index c2dbd96c9..2e62cda12 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -77,7 +77,7 @@ export class AuthUtils { public static async errorHandling(errorDetails: Error | string, moreInfo?: ErrorContext): Promise { // Use util.inspect instead of JSON.stringify to handle circular references // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, moreInfo }, { depth: null })); + ZoweLogger.error(`${errorDetails.toString()}\n` + util.inspect({ errorDetails, ...{ ...moreInfo, profile: undefined } }, { depth: null })); const profile = typeof moreInfo.profile === "string" ? Constants.PROFILES_CACHE.loadNamedProfile(moreInfo.profile) : moreInfo?.profile; const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, { From f2eba25fddb66d450ec27c5a3fbde1bce72203ec Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 29 Oct 2024 08:54:55 -0400 Subject: [PATCH 51/62] restore changes to ZoweTreeNode Signed-off-by: Trae Yelovich --- .../src/tree/ZoweTreeNode.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts index 4d2f463de..bdcc061f9 100644 --- a/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/ZoweTreeNode.ts @@ -100,9 +100,22 @@ export class ZoweTreeNode extends vscode.TreeItem { * * @param {imperative.IProfileLoaded} The profile you will set the node to use */ - public setProfileToChoice(aProfile: imperative.IProfileLoaded): void { - // Don't reassign profile directly, we want to keep object reference shared across nodes - this.profile = Object.assign(this.profile ?? {}, aProfile); + public setProfileToChoice(aProfile: imperative.IProfileLoaded, fsProvider?: BaseProvider): void { + if (this.profile == null) { + this.profile = aProfile; + } else { + // Don't reassign profile, we want to keep object reference shared across nodes + this.profile.profile = aProfile.profile; + } + if (this.resourceUri != null) { + const fsEntry = fsProvider?.lookup(this.resourceUri, true); + if (fsEntry != null) { + fsEntry.metadata.profile = aProfile; + } + } + for (const child of this.children) { + (child as unknown as ZoweTreeNode).setProfileToChoice(aProfile, fsProvider); + } } /** * Sets the session for this node to the one chosen in parameters. From 376de3802a681cfb0b736c0ef6e504ff53c05e85 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 29 Oct 2024 08:58:49 -0400 Subject: [PATCH 52/62] move HandleErrorOpts to fs/types/abstract Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/fs/BaseProvider.ts | 13 +------------ packages/zowe-explorer-api/src/fs/types/abstract.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/zowe-explorer-api/src/fs/BaseProvider.ts b/packages/zowe-explorer-api/src/fs/BaseProvider.ts index a0c4064ff..f68682f2d 100644 --- a/packages/zowe-explorer-api/src/fs/BaseProvider.ts +++ b/packages/zowe-explorer-api/src/fs/BaseProvider.ts @@ -10,24 +10,13 @@ */ import * as vscode from "vscode"; -import { DirEntry, FileEntry, IFileSystemEntry, FS_PROVIDER_DELAY, ConflictViewSelection, DeleteMetadata } from "./types"; +import { DirEntry, FileEntry, IFileSystemEntry, FS_PROVIDER_DELAY, ConflictViewSelection, DeleteMetadata, HandleErrorOpts } from "./types"; import * as path from "path"; import { FsAbstractUtils } from "./utils"; import { Gui } from "../globals/Gui"; import { ZosEncoding } from "../tree"; import { ErrorCorrelator, ZoweExplorerApiType } from "../utils/ErrorCorrelator"; -export interface HandleErrorOpts { - retry?: { - fn: (...args: any[]) => any | PromiseLike; - args?: any[]; - }; - profileType?: string; - apiType?: ZoweExplorerApiType; - templateArgs?: Record; - additionalContext?: string; -} - export class BaseProvider { // eslint-disable-next-line no-magic-numbers private readonly FS_PROVIDER_UI_TIMEOUT = 4000; diff --git a/packages/zowe-explorer-api/src/fs/types/abstract.ts b/packages/zowe-explorer-api/src/fs/types/abstract.ts index b26c64ba5..01a71c618 100644 --- a/packages/zowe-explorer-api/src/fs/types/abstract.ts +++ b/packages/zowe-explorer-api/src/fs/types/abstract.ts @@ -13,6 +13,7 @@ import { Duplex } from "stream"; import { IProfileLoaded } from "@zowe/imperative"; import * as vscode from "vscode"; import { ZosEncoding } from "../../tree"; +import { ZoweExplorerApiType } from "../../utils/ErrorCorrelator"; export enum ZoweScheme { DS = "zowe-ds", @@ -147,3 +148,14 @@ export type UriFsInfo = { profileName: string; profile?: IProfileLoaded; }; + +export interface HandleErrorOpts { + retry?: { + fn: (...args: any[]) => any | PromiseLike; + args?: any[]; + }; + profileType?: string; + apiType?: ZoweExplorerApiType; + templateArgs?: Record; + additionalContext?: string; +} From 162836a5f20d91ade0bb6971390f6fe7870eced9 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 29 Oct 2024 08:59:44 -0400 Subject: [PATCH 53/62] remove export from ErrorContext interface Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/utils/AuthUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index 2e62cda12..ace28e1aa 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -16,7 +16,7 @@ import { Constants } from "../configuration/Constants"; import { ZoweLogger } from "../tools/ZoweLogger"; import { SharedTreeProviders } from "../trees/shared/SharedTreeProviders"; -export interface ErrorContext { +interface ErrorContext { apiType?: ZoweExplorerApiType; profile?: string | imperative.IProfileLoaded; scenario?: string; From be3c9492a07a925617e5c5b4303d1b96fa68f322 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 29 Oct 2024 09:05:23 -0400 Subject: [PATCH 54/62] revert changes to ZoweTreeNode tests Signed-off-by: Trae Yelovich --- .../__unit__/tree/ZoweTreeNode.unit.test.ts | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts index dd438f559..c8a0ae561 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/tree/ZoweTreeNode.unit.test.ts @@ -48,14 +48,14 @@ describe("ZoweTreeNode", () => { it("getProfile should return profile of current node", () => { const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); - node.setProfileToChoice({ name: "myProfile" } as unknown as imperative.IProfileLoaded); - expect(node.getProfile()).toEqual({ name: "myProfile" }); + node.setProfileToChoice("myProfile" as unknown as imperative.IProfileLoaded); + expect(node.getProfile()).toBe("myProfile"); }); it("getProfile should return profile of parent node", () => { - const parentNode = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { name: "parentProfile" }); + const parentNode = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, "parentProfile"); const node = makeNode("test", vscode.TreeItemCollapsibleState.None, parentNode); - expect(node.getProfile()).toEqual({ name: "parentProfile" }); + expect(node.getProfile()).toBe("parentProfile"); }); it("getProfileName should return profile name of current node", () => { @@ -82,13 +82,50 @@ describe("ZoweTreeNode", () => { }); it("setProfileToChoice should update properties on existing profile object", () => { - const origProfile: Partial = { name: "oldProfile", profile: { host: "example.com" } }; - const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, origProfile); + const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined, undefined, { + name: "oldProfile", + profile: { host: "example.com" }, + }); node.setProfileToChoice({ name: "newProfile", profile: { host: "example.com", port: 443 } } as unknown as imperative.IProfileLoaded); - // Profile properties should be updated in original reference - expect(node.getProfileName()).toBe("newProfile"); + // Profile name should not change but properties should + expect(node.getProfileName()).toBe("oldProfile"); expect(node.getProfile().profile?.port).toBeDefined(); - expect(origProfile.name).toBe("newProfile"); - expect(origProfile.profile?.port).toBeDefined(); + }); + + it("setProfileToChoice should update profile for associated FSProvider entry", () => { + const node = makeNode("test", vscode.TreeItemCollapsibleState.None, undefined); + node.resourceUri = vscode.Uri.file(__dirname); + const fsEntry = { + metadata: { + profile: { name: "oldProfile" }, + }, + }; + node.setProfileToChoice( + { name: "newProfile" } as unknown as imperative.IProfileLoaded, + { + lookup: jest.fn().mockReturnValue(fsEntry), + } as unknown as BaseProvider + ); + expect(node.getProfileName()).toBe("newProfile"); + expect(fsEntry.metadata.profile.name).toBe("newProfile"); + }); + + it("setProfileToChoice should update child nodes with the new profile", () => { + const node = makeNode("test", vscode.TreeItemCollapsibleState.Expanded, undefined); + const nodeChild = makeNode("child", vscode.TreeItemCollapsibleState.None, undefined); + node.children = [nodeChild as any]; + const setProfileToChoiceChildMock = jest.spyOn(nodeChild, "setProfileToChoice").mockImplementation(); + const fsEntry = { + metadata: { + profile: { name: "oldProfile" }, + }, + }; + const mockNewProfile = { name: "newProfile" } as unknown as imperative.IProfileLoaded; + const mockProvider = { + lookup: jest.fn().mockReturnValue(fsEntry), + } as unknown as BaseProvider; + node.setProfileToChoice(mockNewProfile, mockProvider); + expect(node.getProfileName()).toBe("newProfile"); + expect(setProfileToChoiceChildMock).toHaveBeenCalledWith(mockNewProfile, mockProvider); }); }); From 3befca90f98651c323d50c36f787a14dde16868c Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 29 Oct 2024 13:19:59 -0400 Subject: [PATCH 55/62] make IApiExplorerExtender.getErrorCorrelator optional Signed-off-by: Trae Yelovich --- packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts index 02af80bfb..348c9e773 100644 --- a/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts +++ b/packages/zowe-explorer-api/src/extend/IApiExplorerExtender.ts @@ -52,5 +52,5 @@ export interface IApiExplorerExtender { * summaries of API or network errors. Also gives extenders the opportunity to * provide tips or additional resources for errors. */ - getErrorCorrelator(): ErrorCorrelator; + getErrorCorrelator?(): ErrorCorrelator; } From 7432f4af214ec049584be863fdc4f31829fcb9e8 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Wed, 30 Oct 2024 16:09:17 -0400 Subject: [PATCH 56/62] update command count, run l10n prepublish Signed-off-by: Trae Yelovich --- packages/zowe-explorer/l10n/bundle.l10n.json | 104 +++++++++++++----- packages/zowe-explorer/l10n/poeditor.json | 26 ++++- .../src/configuration/Constants.ts | 2 +- 3 files changed, 100 insertions(+), 32 deletions(-) diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index c6dec33e6..7ac94d643 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -47,6 +47,7 @@ "Certificate Key File": "Certificate Key File", "Submit": "Submit", "Cancel": "Cancel", + "Troubleshoot Error": "Troubleshoot Error", "Zowe explorer profiles are being set as unsecured.": "Zowe explorer profiles are being set as unsecured.", "Zowe explorer profiles are being set as secured.": "Zowe explorer profiles are being set as secured.", "Custom credential manager failed to activate": "Custom credential manager failed to activate", @@ -158,14 +159,8 @@ "Enter the path to the certificate key for authenticating the connection.": "Enter the path to the certificate key for authenticating the connection.", "Certificate Keys": "Certificate Keys", "Select Certificate Key": "Select Certificate Key", - "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out./Label": { - "message": "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.", - "comment": [ - "Label" - ] - }, "Update Credentials": "Update Credentials", + "Required parameter 'host' must not be blank.": "Required parameter 'host' must not be blank.", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection./Profile name": { "message": "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.", "comment": [ @@ -279,21 +274,36 @@ "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Failed to get contents for {0}/File path": { + "message": "Failed to get contents for {0}", + "comment": [ + "File path" + ] + }, + "Failed to detect encoding for {0}/File path": { + "message": "Failed to detect encoding for {0}", + "comment": [ + "File path" + ] + }, "Profile does not exist for this file.": "Profile does not exist for this file.", "Saving USS file...": "Saving USS file...", - "Renaming {0} failed due to API error: {1}/File pathError message": { - "message": "Renaming {0} failed due to API error: {1}", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", "comment": [ - "File path", - "Error message" + "File path" ] }, - "Deleting {0} failed due to API error: {1}/File nameError message": { - "message": "Deleting {0} failed due to API error: {1}", + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", "comment": [ - "File name", - "Error message" + "File name" ] }, "No error details given": "No error details given", @@ -304,6 +314,13 @@ "Error message" ] }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -370,6 +387,12 @@ ] }, "Enter a codepage (e.g., 1047, IBM-1047)": "Enter a codepage (e.g., 1047, IBM-1047)", + "A search must be set for {0} before it can be added to a workspace./Name of USS session": { + "message": "A search must be set for {0} before it can be added to a workspace.", + "comment": [ + "Name of USS session" + ] + }, "Changes in the credential vault detected, refreshing Zowe Explorer.": "Changes in the credential vault detected, refreshing Zowe Explorer.", "Team config file created, refreshing Zowe Explorer.": "Team config file created, refreshing Zowe Explorer.", "Team config file deleted, refreshing Zowe Explorer.": "Team config file deleted, refreshing Zowe Explorer.", @@ -489,7 +512,18 @@ "Phase Name": "Phase Name", "Error Details": "Error Details", "Fetching spool file...": "Fetching spool file...", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.", + "Failed to get contents for {0}/Spool name": { + "message": "Failed to get contents for {0}", + "comment": [ + "Spool name" + ] + }, + "Failed to delete job {0}/Job name": { + "message": "Failed to delete job {0}", + "comment": [ + "Job name" + ] + }, "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}/Job name": { "message": "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}", "comment": [ @@ -678,19 +712,39 @@ "Choose the setting location to save the data set template...": "Choose the setting location to save the data set template...", "Save as User setting": "Save as User setting", "Save as Workspace setting": "Save as Workspace setting", + "Failed to get stats for data set {0}/Data set path": { + "message": "Failed to get stats for data set {0}", + "comment": [ + "Data set path" + ] + }, + "Failed to list datasets": "Failed to list datasets", + "Failed to list dataset members": "Failed to list dataset members", + "Failed to locate data set {0}": "Failed to locate data set {0}", + "Failed to list data set {0}": "Failed to list data set {0}", + "Failed to read {0}/File path": { + "message": "Failed to read {0}", + "comment": [ + "File path" + ] + }, "Saving data set...": "Saving data set...", - "Deleting {0} failed due to API error: {1}/File pathError message": { - "message": "Deleting {0} failed due to API error: {1}", + "Failed to save {0}/File path": { + "message": "Failed to save {0}", "comment": [ - "File path", - "Error message" + "File path" ] }, - "Renaming {0} failed due to API error: {1}/File nameError message": { - "message": "Renaming {0} failed due to API error: {1}", + "Failed to delete {0}/File path": { + "message": "Failed to delete {0}", "comment": [ - "File name", - "Error message" + "File path" + ] + }, + "Failed to rename {0}/Data set name": { + "message": "Failed to rename {0}", + "comment": [ + "Data set name" ] }, "Partitioned Data Set: Binary": "Partitioned Data Set: Binary", diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index b63316cbe..341c85872 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -62,6 +62,9 @@ "addFavorite": { "Add to Favorites": "" }, + "addToWorkspace": { + "Add to Workspace": "" + }, "removeFavProfile": { "Remove profile from Favorites": "" }, @@ -461,6 +464,7 @@ "Certificate Key File": "", "Submit": "", "Cancel": "", + "Troubleshoot Error": "", "Zowe explorer profiles are being set as unsecured.": "", "Zowe explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", @@ -511,9 +515,8 @@ "Enter the path to the certificate key for authenticating the connection.": "", "Certificate Keys": "", "Select Certificate Key": "", - "Required parameter 'host' must not be blank.": "", - "Invalid Credentials for profile '{0}'. Please ensure the username and password are valid or this may lead to a lock-out.": "", "Update Credentials": "", + "Required parameter 'host' must not be blank.": "", "Your connection is no longer active for profile '{0}'. Please log in to an authentication service to restore the connection.": "", "Profile Name {0} is inactive. Please check if your Zowe server is active or if the URL and port in your profile is correct.": "", "Use the search button to list USS files": "", @@ -560,13 +563,16 @@ "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", "The 'move' function is not implemented for this USS API.": "", - "Could not list USS files: Empty path provided in URI": "", + "Failed to move {0}": "", + "Failed to get contents for {0}": "", + "Failed to detect encoding for {0}": "", "Profile does not exist for this file.": "", "Saving USS file...": "", - "Renaming {0} failed due to API error: {1}": "", - "Deleting {0} failed due to API error: {1}": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", "No error details given": "", "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -593,6 +599,7 @@ "Choose encoding for {0}": "", "Current encoding is {0}": "", "Enter a codepage (e.g., 1047, IBM-1047)": "", + "A search must be set for {0} before it can be added to a workspace.": "", "Changes in the credential vault detected, refreshing Zowe Explorer.": "", "Team config file created, refreshing Zowe Explorer.": "", "Team config file deleted, refreshing Zowe Explorer.": "", @@ -659,7 +666,7 @@ "Phase Name": "", "Error Details": "", "Fetching spool file...": "", - "Failed to fetch jobs: getJobsByParameters is not implemented for this session's JES API.": "", + "Failed to delete job {0}": "", "Are you sure you want to delete the following item?\nThis will permanently remove the following job from your system.\n\n{0}": "", "Job {0} was deleted.": "", "Are you sure you want to delete the following {0} items?\nThis will permanently remove the following jobs from your system.\n\n{1}": "", @@ -720,7 +727,14 @@ "Choose the setting location to save the data set template...": "", "Save as User setting": "", "Save as Workspace setting": "", + "Failed to get stats for data set {0}": "", + "Failed to list datasets": "", + "Failed to list dataset members": "", + "Failed to locate data set {0}": "", + "Failed to list data set {0}": "", + "Failed to read {0}": "", "Saving data set...": "", + "Failed to save {0}": "", "Partitioned Data Set: Binary": "", "Partitioned Data Set: C": "", "Partitioned Data Set: Classic": "", diff --git a/packages/zowe-explorer/src/configuration/Constants.ts b/packages/zowe-explorer/src/configuration/Constants.ts index 33e640626..e584d01e9 100644 --- a/packages/zowe-explorer/src/configuration/Constants.ts +++ b/packages/zowe-explorer/src/configuration/Constants.ts @@ -16,7 +16,7 @@ import { imperative, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; import type { Profiles } from "./Profiles"; export class Constants { - public static readonly COMMAND_COUNT = 101; + public static readonly COMMAND_COUNT = 103; public static readonly MAX_SEARCH_HISTORY = 5; public static readonly MAX_FILE_HISTORY = 10; public static readonly MS_PER_SEC = 1000; From 2ff27a6dbc3e0ea0b87c0f08a7b15fe1658c87a4 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 31 Oct 2024 11:59:59 -0400 Subject: [PATCH 57/62] address duplicate errors, pass profileName as template arg Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 7 +--- .../src/trees/dataset/DatasetFSProvider.ts | 42 +++++++------------ .../src/trees/job/JobFSProvider.ts | 3 ++ .../src/trees/uss/UssFSProvider.ts | 7 ++++ packages/zowe-explorer/src/utils/AuthUtils.ts | 3 +- 5 files changed, 30 insertions(+), 32 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index f32b57bc8..6181170f0 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -249,17 +249,14 @@ describe("fetchDatasetAtUri", () => { mvsApiMock.mockRestore(); }); - it("calls _handleError and throws error if API call fails", async () => { + it("returns null if API call fails", async () => { const mockMvsApi = { getContents: jest.fn().mockRejectedValue(new Error("unknown API error")), }; const fakePo = { ...testEntries.ps }; - const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); const lookupAsFileMock = jest.spyOn(DatasetFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(fakePo); const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); - await expect(DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { isConflict: true })).rejects.toThrow(); - expect(handleErrorMock).toHaveBeenCalled(); - handleErrorMock.mockRestore(); + expect(await DatasetFSProvider.instance.fetchDatasetAtUri(testUris.ps, { isConflict: true })).toBe(null); lookupAsFileMock.mockRestore(); mvsApiMock.mockRestore(); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 4c7cf7eeb..543969f04 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -101,15 +101,9 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); } } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t({ message: "Failed to get stats for data set {0}", args: [uri.path], comment: "Data set path" }), - retry: { - fn: this.stat.bind(this), - args: [uri], - }, - apiType: ZoweExplorerApiType.Mvs, - profileType: uriInfo.profile?.type, - }); + if (err instanceof Error) { + ZoweLogger.error(err.message); + } throw err; } @@ -161,7 +155,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem args: [uri, uriInfo, pattern], }, apiType: ZoweExplorerApiType.Mvs, - profileType: this._getInfoFromUri(uri).profile?.type, + profileType: uriInfo.profile?.type, + templateArgs: { profileName: uriInfo.profileName }, }); } for (const resp of datasetResponses) { @@ -206,6 +201,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem }, apiType: ZoweExplorerApiType.Mvs, profileType: uriInfo.profile?.type, + templateArgs: { profileName: uriInfo.profileName }, }); throw err; } @@ -228,12 +224,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem entry = this.lookup(uri, false) as PdsEntry | DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { - this._handleError(err, { - additionalContext: vscode.l10n.t("Failed to locate data set {0}", uri.path), - apiType: ZoweExplorerApiType.Mvs, - profileType: uriInfo.profile?.type, - }); - return undefined; + throw err; } } @@ -266,15 +257,9 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } } } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t("Failed to list data set {0}", uri.path), - apiType: ZoweExplorerApiType.Mvs, - retry: { - fn: this.fetchDataset.bind(this), - args: [uri, uriInfo], - }, - profileType: uriInfo.profile?.type, - }); + if (err instanceof Error) { + ZoweLogger.error(err.message); + } throw err; } } @@ -467,6 +452,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem ds = this._lookupAsFile(uri) as DsEntry; } catch (err) { if (!(err instanceof vscode.FileSystemError) || err.code !== "FileNotFound") { + const uriInfo = this._getInfoFromUri(uri); this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to read {0}", @@ -474,11 +460,12 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem comment: ["File path"], }), apiType: ZoweExplorerApiType.Mvs, - profileType: this._getInfoFromUri(uri).profile?.type, + profileType: uriInfo.profile?.type, retry: { fn: this.readFile.bind(this), args: [uri], }, + templateArgs: { profileName: uriInfo.profile?.name ?? "" }, }); throw err; } @@ -624,6 +611,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem fn: this.writeFile.bind(this), args: [uri, content, options], }, + templateArgs: { profileName: entry.metadata.profile.name ?? "" }, }); throw err; } @@ -682,6 +670,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem fn: this.delete.bind(this), args: [uri, _options], }, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, }); throw err; } @@ -728,6 +717,7 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem fn: this.rename.bind(this), args: [oldUri, newUri, options], }, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, }); throw err; } diff --git a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts index 666179e45..441cf1b10 100644 --- a/packages/zowe-explorer/src/trees/job/JobFSProvider.ts +++ b/packages/zowe-explorer/src/trees/job/JobFSProvider.ts @@ -123,6 +123,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv fn: this.readDirectory.bind(this), args: [uri], }, + templateArgs: { profileName: uriInfo.profile?.name ?? "" }, }); throw err; } @@ -245,6 +246,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv fn: this.readFile.bind(this), args: [uri], }, + templateArgs: { profileName: spoolEntry.metadata.profile?.name ?? "" }, }); throw err; } @@ -330,6 +332,7 @@ export class JobFSProvider extends BaseProvider implements vscode.FileSystemProv fn: this.delete.bind(this), args: [uri, options], }, + templateArgs: { profileName: profInfo.profile.name ?? "" }, }); throw err; } diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 41df761a7..b7d204875 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -132,6 +132,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv args: [oldUri, newUri], }, profileType: info.profile.type, + templateArgs: { profileName: info.profile.name ?? "" }, }); throw err; } @@ -302,6 +303,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, apiType: ZoweExplorerApiType.Uss, profileType: metadata.profile.type, + templateArgs: { profileName: metadata.profile.name }, }); throw err; } @@ -356,6 +358,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, apiType: ZoweExplorerApiType.Uss, profileType: entry.metadata.profile.type, + templateArgs: { profileName: entry.metadata.profile.name ?? "" }, }); throw err; } @@ -525,6 +528,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv args: [uri, content, options], }, profileType: parentDir.metadata.profile.type, + templateArgs: { profileName: parentDir.metadata.profile.name ?? "" }, }); throw err; } @@ -591,6 +595,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, apiType: ZoweExplorerApiType.Uss, profileType: entry.metadata.profile?.type, + templateArgs: { profileName: entry.metadata.profile?.name ?? "" }, }); throw err; } @@ -633,6 +638,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, apiType: ZoweExplorerApiType.Uss, profileType: parent.metadata.profile.type, + templateArgs: { profileName: parent.metadata.profile.name ?? "" }, }); throw err; } @@ -753,6 +759,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv }, apiType: ZoweExplorerApiType.Uss, profileType: destInfo.profile.type, + templateArgs: { profileName: destInfo.profile.name ?? "" }, }); throw err; } diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index ace28e1aa..c07a9c0db 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -83,6 +83,7 @@ export class AuthUtils { const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, { profileType: profile?.type, ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), + templateArgs: { ...(moreInfo.templateArgs ?? {}), profileName: profile?.name ?? "" }, }); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { const imperativeError: imperative.ImperativeError = errorDetails as imperative.ImperativeError; @@ -103,7 +104,7 @@ export class AuthUtils { return false; } - await ErrorCorrelator.getInstance().displayCorrelatedError(correlation); + await ErrorCorrelator.getInstance().displayCorrelatedError(correlation, { templateArgs: { profileName: profile?.name ?? "" } }); return false; } From a3cfc8871f5899428e36a8ff07a6129245fee5ed Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Thu, 31 Oct 2024 12:09:57 -0400 Subject: [PATCH 58/62] resolve failing tests from changes Signed-off-by: Trae Yelovich --- .../dataset/DatasetFSProvider.unit.test.ts | 54 ++----------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index 6181170f0..fc424552a 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -711,7 +711,7 @@ describe("stat", () => { }); describe("error handling", () => { - it("API response was unsuccessful for remote lookup, dialog dismissed", async () => { + it("API response was unsuccessful for remote lookup", async () => { const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ isRoot: false, @@ -724,52 +724,7 @@ describe("stat", () => { const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ dataSet: dataSetMock, } as any); - const handleErrorSpy = jest.spyOn(DatasetFSProvider.instance as any, "_handleError"); await expect(DatasetFSProvider.instance.stat(testUris.ps)).rejects.toThrow(); - expect(handleErrorSpy).toHaveBeenCalledWith( - exampleError, - expect.objectContaining({ - additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - }) - ); - mvsApiMock.mockRestore(); - getInfoForUriMock.mockRestore(); - lookupMock.mockRestore(); - }); - - it("API response was unsuccessful for remote lookup, Retry selected", async () => { - const lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValue(testEntries.ps); - const getInfoForUriMock = jest.spyOn(FsAbstractUtils, "getInfoForUri").mockReturnValue({ - isRoot: false, - slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), - profileName: "sestest", - profile: testEntries.ps.metadata.profile, - }); - const exampleError = new Error("Response unsuccessful"); - const dataSetMock = jest.fn().mockRejectedValue(exampleError); - const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValue({ - dataSet: dataSetMock, - } as any); - const statSpy = jest - .spyOn(DatasetFSProvider.instance, "stat") - .mockImplementationOnce(DatasetFSProvider.instance.stat) - .mockImplementation(); - const handleErrorSpy = jest - .spyOn(DatasetFSProvider.instance as any, "_handleError") - .mockResolvedValue({ correlation: { asError: jest.fn() } as any, userResponse: "Retry" }); - await DatasetFSProvider.instance.stat(testUris.ps); - expect(handleErrorSpy).toHaveBeenCalledWith( - exampleError, - expect.objectContaining({ - additionalContext: `Failed to get stats for data set ${testUris.ps.path}`, - apiType: ZoweExplorerApiType.Mvs, - profileType: "zosmf", - }) - ); - expect(statSpy).toHaveBeenCalledTimes(2); - expect(statSpy).toHaveBeenCalledWith(testUris.ps); mvsApiMock.mockRestore(); getInfoForUriMock.mockRestore(); lookupMock.mockRestore(); @@ -1042,15 +997,12 @@ describe("fetchDataset", () => { const lookupMock = jest.spyOn(DatasetFSProvider.instance, "lookup").mockImplementation(() => { throw new Error("unknown fs error"); }); - const handleErrorMock = jest.spyOn(DatasetFSProvider.instance as any, "_handleError").mockImplementation(); - await (DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { + await expect((DatasetFSProvider.instance as any).fetchDataset(testUris.ps, { isRoot: false, slashAfterProfilePos: testUris.ps.path.indexOf("/", 1), profileName: "sestest", profile: testProfile, - }); - expect(handleErrorMock).toHaveBeenCalled(); - handleErrorMock.mockRestore(); + })).rejects.toThrow(); lookupMock.mockRestore(); }); }); From 80f7be23bd9fd1d5efceb144c01dffb198109248 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 5 Nov 2024 08:29:32 -0500 Subject: [PATCH 59/62] refactor: use optional chaining to spread templateArgs Signed-off-by: Trae Yelovich --- packages/zowe-explorer/src/utils/AuthUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zowe-explorer/src/utils/AuthUtils.ts b/packages/zowe-explorer/src/utils/AuthUtils.ts index c07a9c0db..d648b1a13 100644 --- a/packages/zowe-explorer/src/utils/AuthUtils.ts +++ b/packages/zowe-explorer/src/utils/AuthUtils.ts @@ -83,7 +83,7 @@ export class AuthUtils { const correlation = ErrorCorrelator.getInstance().correlateError(moreInfo?.apiType ?? ZoweExplorerApiType.All, errorDetails, { profileType: profile?.type, ...Object.keys(moreInfo).reduce((all, k) => (typeof moreInfo[k] === "string" ? { ...all, [k]: moreInfo[k] } : all), {}), - templateArgs: { ...(moreInfo.templateArgs ?? {}), profileName: profile?.name ?? "" }, + templateArgs: { profileName: profile?.name ?? "", ...moreInfo?.templateArgs }, }); if (typeof errorDetails !== "string" && (errorDetails as imperative.ImperativeError)?.mDetails !== undefined) { const imperativeError: imperative.ImperativeError = errorDetails as imperative.ImperativeError; From a2f03079f7ce31ec891876a8fe92e7025a569273 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 5 Nov 2024 10:26:53 -0500 Subject: [PATCH 60/62] refactor: use dsName instead of path; rm handling in autoDetectEncoding Signed-off-by: Trae Yelovich --- .../trees/uss/UssFSProvider.unit.test.ts | 3 -- .../src/trees/dataset/DatasetFSProvider.ts | 4 +- .../src/trees/uss/UssFSProvider.ts | 38 +++++-------------- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index ec2dfb0aa..95bb08f83 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -463,11 +463,8 @@ describe("autoDetectEncoding", () => { path: "/testFile", profile: testProfile, }; - const _handleErrorMock = jest.spyOn(UssFSProvider.instance as any, "_handleError").mockImplementation(); await expect(UssFSProvider.instance.autoDetectEncoding(testEntry)).rejects.toThrow(); expect(getTagMock).toHaveBeenCalledTimes(1); - expect(_handleErrorMock).toHaveBeenCalled(); - _handleErrorMock.mockRestore(); }); it("sets encoding if file tagged as binary", async () => { diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 543969f04..3c8746629 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -602,8 +602,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem this._handleError(err, { additionalContext: vscode.l10n.t({ message: "Failed to save {0}", - args: [entry.metadata.path], - comment: ["File path"], + args: [(entry.metadata as DsEntryMetadata).dsName], + comment: ["Data set name"], }), apiType: ZoweExplorerApiType.Mvs, profileType: entry.metadata.profile.type, diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index b7d204875..55a9d38ce 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -332,35 +332,17 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv return; } - try { - const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); - if (ussApi.getTag != null) { - const taggedEncoding = await ussApi.getTag(entry.metadata.path); - if (taggedEncoding === "binary" || taggedEncoding === "mixed") { - entry.encoding = { kind: "binary" }; - } else if (taggedEncoding !== "untagged") { - entry.encoding = { kind: "other", codepage: taggedEncoding }; - } - } else { - const isBinary = await ussApi.isFileTagBinOrAscii(entry.metadata.path); - entry.encoding = isBinary ? { kind: "binary" } : undefined; + const ussApi = ZoweExplorerApiRegister.getUssApi(entry.metadata.profile); + if (ussApi.getTag != null) { + const taggedEncoding = await ussApi.getTag(entry.metadata.path); + if (taggedEncoding === "binary" || taggedEncoding === "mixed") { + entry.encoding = { kind: "binary" }; + } else if (taggedEncoding !== "untagged") { + entry.encoding = { kind: "other", codepage: taggedEncoding }; } - } catch (err) { - this._handleError(err, { - additionalContext: vscode.l10n.t({ - message: "Failed to detect encoding for {0}", - args: [entry.metadata.path], - comment: ["File path"], - }), - retry: { - fn: this.autoDetectEncoding.bind(this), - args: [entry], - }, - apiType: ZoweExplorerApiType.Uss, - profileType: entry.metadata.profile.type, - templateArgs: { profileName: entry.metadata.profile.name ?? "" }, - }); - throw err; + } else { + const isBinary = await ussApi.isFileTagBinOrAscii(entry.metadata.path); + entry.encoding = isBinary ? { kind: "binary" } : undefined; } } From 694ebd5dbfd11d5a52c2c948ce63fbb8b31eaecc Mon Sep 17 00:00:00 2001 From: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:33:43 -0500 Subject: [PATCH 61/62] run package Signed-off-by: Billie Simmons <49491949+JillieBeanSim@users.noreply.github.com> --- packages/zowe-explorer/l10n/bundle.l10n.json | 102 ++++++++----------- packages/zowe-explorer/l10n/poeditor.json | 24 ++--- 2 files changed, 54 insertions(+), 72 deletions(-) diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 7ac94d643..cb9d95139 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -203,6 +203,48 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Failed to get contents for {0}/File path": { + "message": "Failed to get contents for {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "No error details given": "No error details given", + "Error fetching destination {0} for paste action: {1}/USS pathError message": { + "message": "Error fetching destination {0} for paste action: {1}", + "comment": [ + "USS path", + "Error message" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -273,54 +315,6 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Failed to move {0}/File path": { - "message": "Failed to move {0}", - "comment": [ - "File path" - ] - }, - "Failed to get contents for {0}/File path": { - "message": "Failed to get contents for {0}", - "comment": [ - "File path" - ] - }, - "Failed to detect encoding for {0}/File path": { - "message": "Failed to detect encoding for {0}", - "comment": [ - "File path" - ] - }, - "Profile does not exist for this file.": "Profile does not exist for this file.", - "Saving USS file...": "Saving USS file...", - "Failed to rename {0}/File path": { - "message": "Failed to rename {0}", - "comment": [ - "File path" - ] - }, - "Failed to delete {0}/File name": { - "message": "Failed to delete {0}", - "comment": [ - "File name" - ] - }, - "No error details given": "No error details given", - "Error fetching destination {0} for paste action: {1}/USS pathError message": { - "message": "Error fetching destination {0} for paste action: {1}", - "comment": [ - "USS path", - "Error message" - ] - }, - "Failed to copy {0} to {1}/Source pathDestination path": { - "message": "Failed to copy {0} to {1}", - "comment": [ - "Source path", - "Destination path" - ] - }, "{0} location/Node type": { "message": "{0} location", "comment": [ @@ -712,16 +706,8 @@ "Choose the setting location to save the data set template...": "Choose the setting location to save the data set template...", "Save as User setting": "Save as User setting", "Save as Workspace setting": "Save as Workspace setting", - "Failed to get stats for data set {0}/Data set path": { - "message": "Failed to get stats for data set {0}", - "comment": [ - "Data set path" - ] - }, "Failed to list datasets": "Failed to list datasets", "Failed to list dataset members": "Failed to list dataset members", - "Failed to locate data set {0}": "Failed to locate data set {0}", - "Failed to list data set {0}": "Failed to list data set {0}", "Failed to read {0}/File path": { "message": "Failed to read {0}", "comment": [ @@ -729,10 +715,10 @@ ] }, "Saving data set...": "Saving data set...", - "Failed to save {0}/File path": { + "Failed to save {0}/Data set name": { "message": "Failed to save {0}", "comment": [ - "File path" + "Data set name" ] }, "Failed to delete {0}/File path": { diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 341c85872..819384f4c 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -534,6 +534,16 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Failed to get contents for {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -562,17 +572,6 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", - "The 'move' function is not implemented for this USS API.": "", - "Failed to move {0}": "", - "Failed to get contents for {0}": "", - "Failed to detect encoding for {0}": "", - "Profile does not exist for this file.": "", - "Saving USS file...": "", - "Failed to rename {0}": "", - "Failed to delete {0}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", - "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", @@ -727,11 +726,8 @@ "Choose the setting location to save the data set template...": "", "Save as User setting": "", "Save as Workspace setting": "", - "Failed to get stats for data set {0}": "", "Failed to list datasets": "", "Failed to list dataset members": "", - "Failed to locate data set {0}": "", - "Failed to list data set {0}": "", "Failed to read {0}": "", "Saving data set...": "", "Failed to save {0}": "", From ccbafefba35f47d736e85d63eb254a708b34734f Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Tue, 5 Nov 2024 12:00:51 -0500 Subject: [PATCH 62/62] fix: propagate USS listFiles error Signed-off-by: Trae Yelovich --- .../__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts | 5 +---- packages/zowe-explorer/src/trees/uss/UssFSProvider.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 95bb08f83..7d99bf286 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -198,10 +198,7 @@ describe("listFiles", () => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce({ fileList: jest.fn().mockRejectedValue(new Error("error listing files")), } as any); - expect(await UssFSProvider.instance.listFiles(testProfile, testUris.folder)).toStrictEqual({ - success: false, - commandResponse: "error listing files", - }); + await expect(UssFSProvider.instance.listFiles(testProfile, testUris.folder)).rejects.toThrow(); }); }); diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 55a9d38ce..69e6daaa7 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -154,7 +154,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv if (err instanceof Error) { ZoweLogger.error(err.message); } - return { success: false, commandResponse: err instanceof Error ? err.message : JSON.stringify(err) }; + throw err; } return {