diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ba62cb4cfc..863d8e52ef 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to the Zowe CLI package will be documented in this file. ## Recent Changes - Enhancement: Added the `--data-set-type` flag to create sequential data set command to allow for creating extended and large formatted sequential data sets. [#2141](https://github.com/zowe/zowe-cli/issues/2141) +- Enhancement: Added `--recordRange` flag to `zowe jobs download output` command to allow users to select a specific range of records to output from a spool file. [#2411](https://github.com/zowe/zowe-cli/pull/2411) +- BugFix: The `zowe zos-files copy data-set` command overwrites the contents of the target data set without user confirmation. A `--safe-replace` option was added which prompts the user to confirm before overwriting the contents of the target data set. [#2369] (https://github.com/zowe/zowe-cli/issues/2369) ## `8.12.0` diff --git a/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap b/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap index 63dd437f15..c432eb3b15 100644 --- a/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__integration__/copy/ds/__snapshots__/cli.files.copy.ds.integration.test.ts.snap @@ -37,6 +37,11 @@ exports[`Copy Data Set should display the help 1`] = ` Specify this option as true if you wish to replace like-named members in the target data set + --safe-replace | --safe-rep | ----sr (boolean) + + Specify this option as true if you wish to replace like-named members or the + content of the target data set. This option will prompt to confirm. + --response-timeout | --rto (number) The maximum amount of time in seconds the z/OSMF Files TSO servlet should run @@ -161,6 +166,11 @@ exports[`Copy Data Set should display the help 1`] = ` $ zowe zos-files copy data-set \\"USER.FROM.SET\\" \\"USER.TO.SET\\" --replace + - Copy the partitioned data set named 'TEST.PDS1' to the + partitioned data set named 'TEST.PDS2': + + $ zowe zos-files copy data-set \\"USER.FROM.SET\\" \\"USER.TO.SET\\" --safe-replace + " `; @@ -170,8 +180,8 @@ exports[`Copy Data Set should display the help in json format 1`] = ` \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: data-set.\\", - \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --safe-replace | --safe-rep | ----sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members or the\\\\n content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n\\" + \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n data-set | ds\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Copy a data set/partitioned data set to another data set/partitioned data set.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-files copy data-set [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n fromDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy from\\\\n\\\\n toDataSetName\\\\t\\\\t (string)\\\\n\\\\n The name of the data set that you want to copy to\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --replace | --rep (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members in the\\\\n target data set\\\\n\\\\n --safe-replace | --safe-rep | ----sr (boolean)\\\\n\\\\n Specify this option as true if you wish to replace like-named members or the\\\\n content of the target data set. This option will prompt to confirm.\\\\n\\\\n --response-timeout | --rto (number)\\\\n\\\\n The maximum amount of time in seconds the z/OSMF Files TSO servlet should run\\\\n before returning a response. Any request exceeding this amount of time will be\\\\n terminated and return an error. Allowed values: 5 - 600\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n member named 'USER.TO.SET(MEM2)':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET(mem2)\\\\\\"\\\\n\\\\n - Copy the data set member named 'USER.FROM.SET(MEM1)' to the\\\\n data set named 'USER.TO.SET':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET(mem1)\\\\\\" \\\\\\"USER.TO.SET\\\\\\"\\\\n\\\\n - Copy the data set named 'USER.FROM.SET' to the data set\\\\n named 'USER.TO.SET' and replace like-named members:\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --replace\\\\n\\\\n - Copy the partitioned data set named 'TEST.PDS1' to the\\\\n partitioned data set named 'TEST.PDS2':\\\\n\\\\n $ zowe zos-files copy data-set \\\\\\"USER.FROM.SET\\\\\\" \\\\\\"USER.TO.SET\\\\\\" --safe-replace\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts index 778b440d72..2975ad8629 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts +++ b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/Ds.handler.unit.test.ts @@ -33,11 +33,24 @@ describe("DsHandler", () => { const fromDataSetName = "ABCD"; const toDataSetName = "EFGH"; + const enq = "SHR"; + const replace = true; + const safeReplace = true; + const responseTimeout: any = undefined; + const commandParameters: any = { arguments: { fromDataSetName, - toDataSetName + toDataSetName, + enq, + replace, + safeReplace, + responseTimeout + + }, + response: { + console: { promptFn: jest.fn() } } }; @@ -49,7 +62,14 @@ describe("DsHandler", () => { expect(copyDatasetSpy).toHaveBeenLastCalledWith( dummySession, { dsn: commandParameters.arguments.toDataSetName }, - { "from-dataset": { dsn: commandParameters.arguments.fromDataSetName } } + { + "from-dataset": { dsn: commandParameters.arguments.fromDataSetName }, + "enq": commandParameters.arguments.enq, + "replace": commandParameters.arguments.replace, + "responseTimeout": commandParameters.arguments.responseTimeout, + "safeReplace": commandParameters.arguments.safeReplace, + "promptFn": expect.any(Function) + } ); expect(response).toBe(defaultReturn); }); @@ -63,11 +83,22 @@ describe("DsHandler", () => { const fromMemberName = "mem1"; const toDataSetName = "EFGH"; const toMemberName = "mem2"; + const enq = "SHR"; + const replace = true; + const safeReplace = true; + const responseTimeout: any = undefined; const commandParameters: any = { arguments: { fromDataSetName: `${fromDataSetName}(${fromMemberName})`, - toDataSetName: `${toDataSetName}(${toMemberName})` + toDataSetName: `${toDataSetName}(${toMemberName})`, + enq, + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } } }; @@ -79,7 +110,14 @@ describe("DsHandler", () => { expect(copyDatasetSpy).toHaveBeenLastCalledWith( dummySession, { dsn: toDataSetName, member: toMemberName }, - { "from-dataset": { dsn: fromDataSetName, member: fromMemberName } } + { + "from-dataset": { dsn: fromDataSetName, member: fromMemberName }, + "enq": commandParameters.arguments.enq, + "replace": commandParameters.arguments.replace, + "responseTimeout": commandParameters.arguments.responseTimeout, + "safeReplace": commandParameters.arguments.safeReplace, + "promptFn": expect.any(Function) + } ); expect(response).toBe(defaultReturn); }); @@ -93,13 +131,20 @@ describe("DsHandler", () => { const toDataSetName = "EFGH"; const enq = "SHR"; const replace = true; + const safeReplace = true; + const responseTimeout: any = undefined; const commandParameters: any = { arguments: { fromDataSetName, toDataSetName, enq, - replace + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } } }; @@ -114,9 +159,84 @@ describe("DsHandler", () => { { "from-dataset": { dsn: commandParameters.arguments.fromDataSetName }, "enq": commandParameters.arguments.enq, - "replace": commandParameters.arguments.replace + "replace": commandParameters.arguments.replace, + "responseTimeout": commandParameters.arguments.responseTimeout, + "safeReplace": commandParameters.arguments.safeReplace, + "promptFn": expect.any(Function) } ); expect(response).toBe(defaultReturn); }); + it("should prompt the user and return true when input is 'y'", async () => { + const handler = new DsHandler(); + + expect(handler).toBeInstanceOf(ZosFilesBaseHandler); + const fromDataSetName = "ABCD"; + const toDataSetName = "EFGH"; + const enq = "SHR"; + const replace = true; + const safeReplace = true; + const responseTimeout: any = undefined; + + const commandParameters: any = { + arguments: { + fromDataSetName, + toDataSetName, + enq, + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } + } + }; + const promptMock = jest.fn(); + promptMock.mockResolvedValue("y"); + + const promptFn = (handler as any)["promptForSafeReplace"]({ prompt: promptMock }); + const result = await promptFn(commandParameters.arguments.toDataSetName); + + expect(promptMock).toHaveBeenCalledWith( + `The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` + + ` Are you sure you want to continue? [y/N]: ` + ); + expect(result).toBe(true); + }); + it("should prompt the user and return true when input is 'N'", async () => { + const handler = new DsHandler(); + + expect(handler).toBeInstanceOf(ZosFilesBaseHandler); + const fromDataSetName = "ABCD"; + const toDataSetName = "EFGH"; + const enq = "SHR"; + const replace = true; + const safeReplace = true; + const responseTimeout: any = undefined; + + const commandParameters: any = { + arguments: { + fromDataSetName, + toDataSetName, + enq, + replace, + safeReplace, + responseTimeout + }, + response: { + console: { promptFn: jest.fn() } + } + }; + const promptMock = jest.fn(); + promptMock.mockResolvedValue("N"); + + const promptFn = (handler as any)["promptForSafeReplace"]({ prompt: promptMock }); + const result = await promptFn(commandParameters.arguments.toDataSetName); + + expect(promptMock).toHaveBeenCalledWith( + `The dataset '${toDataSetName}' exists on the target system. This copy will result in data loss.` + + ` Are you sure you want to continue? [y/N]: ` + ); + expect(result).toBe(false); + }); }); diff --git a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap index cee4e8287e..7e0c8a3fa9 100644 --- a/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap +++ b/packages/cli/__tests__/zosfiles/__unit__/copy/ds/__snapshots__/Ds.definition.unit.test.ts.snap @@ -10,6 +10,15 @@ Array [ "name": "replace", "type": "boolean", }, + Object { + "aliases": Array [ + "safe-rep", + "--sr", + ], + "description": "Specify this option as true if you wish to replace like-named members or the content of the target data set. This option will prompt to confirm.", + "name": "safe-replace", + "type": "boolean", + }, ] `; @@ -35,5 +44,9 @@ Array [ "description": "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace like-named members", "options": "\\"USER.FROM.SET\\" \\"USER.TO.SET\\" --replace", }, + Object { + "description": "Copy the partitioned data set named 'TEST.PDS1' to the partitioned data set named 'TEST.PDS2'", + "options": "\\"USER.FROM.SET\\" \\"USER.TO.SET\\" --safe-replace", + }, ] `; diff --git a/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap b/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap index 6dd3b29ae4..41ee20981f 100644 --- a/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__integration__/download/__snapshots__/cli.zos-jobs.download.output.integration.test.ts.snap @@ -101,6 +101,10 @@ exports[`zos-jobs download output command should display the help 1`] = ` Wait for the job to enter OUTPUT status before completing the command. + --record-range | --rr (string) + + Zero indexed range of records to download from a spool file. (example: 0-100) + ZOSMF CONNECTION OPTIONS ------------------------ @@ -199,12 +203,17 @@ exports[`zos-jobs download output command should display the help 1`] = ` $ zowe zos-jobs download output JOB00234 + - Download the records in the range of 0 to 100 from a job + spool.: + + $ zowe zos-jobs download output --record-range '0-100' + { \\"success\\": true, \\"exitCode\\": 0, \\"message\\": \\"The help was constructed for command: output.\\", - \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\", + \\"stdout\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n --record-range | --rr (string)\\\\n\\\\n Zero indexed range of records to download from a spool file. (example: 0-100)\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n - Download the records in the range of 0 to 100 from a job\\\\n spool.:\\\\n\\\\n $ zowe zos-jobs download output --record-range '0-100'\\\\n\\\\n\\", \\"stderr\\": \\"\\", - \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n\\" + \\"data\\": \\"\\\\n COMMAND NAME\\\\n ------------\\\\n\\\\n output | o\\\\n\\\\n DESCRIPTION\\\\n -----------\\\\n\\\\n Download all job output to a local directory. Each spool DD will be downloaded\\\\n to its own file in the directory.\\\\n\\\\n USAGE\\\\n -----\\\\n\\\\n zowe zos-jobs download output [options]\\\\n\\\\n POSITIONAL ARGUMENTS\\\\n --------------------\\\\n\\\\n jobid\\\\t\\\\t (string)\\\\n\\\\n The z/OS JOBID of the job containing the spool files you want to view. No\\\\n pre-validation of the JOBID is performed.\\\\n\\\\n OPTIONS\\\\n -------\\\\n\\\\n --directory | -d | --dir (string)\\\\n\\\\n The local directory you would like to download the output for the job to.\\\\n\\\\n --extension | -e (string)\\\\n\\\\n A file extension to save the job output with. Defaults to '.txt'.\\\\n\\\\n --omit-jobid-directory | --ojd (boolean)\\\\n\\\\n If specified, job output will be saved directly to the specified directory\\\\n rather than creating a subdirectory named after the ID of the job.\\\\n\\\\n --binary | -b (boolean)\\\\n\\\\n If specified, job output will be downloaded in binary format instead of\\\\n performing text conversion. Conflicts with record.\\\\n\\\\n --record | -r (boolean)\\\\n\\\\n If specified, job output will be downloaded in record format instead of\\\\n performing text conversion. Conflicts with binary.\\\\n\\\\n --encoding | --ec (string)\\\\n\\\\n Download the spool file content with encoding mode, which means that data\\\\n conversion is performed using the file encoding specified.\\\\n\\\\n --wait-for-active | --wfa (boolean)\\\\n\\\\n Wait for the job to enter ACTIVE status before completing the command.\\\\n\\\\n --wait-for-output | --wfo (boolean)\\\\n\\\\n Wait for the job to enter OUTPUT status before completing the command.\\\\n\\\\n --record-range | --rr (string)\\\\n\\\\n Zero indexed range of records to download from a spool file. (example: 0-100)\\\\n\\\\n ZOSMF CONNECTION OPTIONS\\\\n ------------------------\\\\n\\\\n --host | -H (string)\\\\n\\\\n The z/OSMF server host name.\\\\n\\\\n --port | -P (number)\\\\n\\\\n The z/OSMF server port.\\\\n\\\\n Default value: 443\\\\n\\\\n --user | -u (string)\\\\n\\\\n Mainframe (z/OSMF) user name, which can be the same as your TSO login.\\\\n\\\\n --password | --pass | --pw (string)\\\\n\\\\n Mainframe (z/OSMF) password, which can be the same as your TSO password.\\\\n\\\\n --reject-unauthorized | --ru (boolean)\\\\n\\\\n Reject self-signed certificates.\\\\n\\\\n Default value: true\\\\n\\\\n --base-path | --bp (string)\\\\n\\\\n The base path for your API mediation layer instance. Specify this option to\\\\n prepend the base path to all z/OSMF resources when making REST requests. Do not\\\\n specify this option if you are not using an API mediation layer.\\\\n\\\\n --protocol (string)\\\\n\\\\n The protocol used (HTTP or HTTPS)\\\\n\\\\n Default value: https\\\\n Allowed values: http, https\\\\n\\\\n --cert-file (local file path)\\\\n\\\\n The file path to a certificate file to use for authentication\\\\n\\\\n --cert-key-file (local file path)\\\\n\\\\n The file path to a certificate key file to use for authentication\\\\n\\\\n PROFILE OPTIONS\\\\n ---------------\\\\n\\\\n --zosmf-profile | --zosmf-p (string)\\\\n\\\\n The name of a (zosmf) profile to load for this command execution.\\\\n\\\\n --base-profile | --base-p (string)\\\\n\\\\n The name of a (base) profile to load for this command execution.\\\\n\\\\n BASE CONNECTION OPTIONS\\\\n -----------------------\\\\n\\\\n --token-type | --tt (string)\\\\n\\\\n The type of token to get and use for the API. Omit this option to use the\\\\n default token type, which is provided by 'zowe auth login'.\\\\n\\\\n --token-value | --tv (string)\\\\n\\\\n The value of the token to pass to the API.\\\\n\\\\n GLOBAL OPTIONS\\\\n --------------\\\\n\\\\n --show-inputs-only (boolean)\\\\n\\\\n Show command inputs and do not run the command\\\\n\\\\n --response-format-json | --rfj (boolean)\\\\n\\\\n Produce JSON formatted data from a command\\\\n\\\\n --help | -h (boolean)\\\\n\\\\n Display help text\\\\n\\\\n --help-web | --hw (boolean)\\\\n\\\\n Display HTML help in browser\\\\n\\\\n EXAMPLES\\\\n --------\\\\n\\\\n - Download all the output of the job with job ID JOB00234 to\\\\n an automatically generated directory.:\\\\n\\\\n $ zowe zos-jobs download output JOB00234\\\\n\\\\n - Download the records in the range of 0 to 100 from a job\\\\n spool.:\\\\n\\\\n $ zowe zos-jobs download output --record-range '0-100'\\\\n\\\\n\\" }" `; diff --git a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap index f73cf31e05..3eedcb8f07 100644 --- a/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap +++ b/packages/cli/__tests__/zosjobs/__unit__/download/download-output/__snapshots__/Output.definition.unit.test.ts.snap @@ -11,6 +11,10 @@ Object { "description": "Download all the output of the job with job ID JOB00234 to an automatically generated directory.", "options": "JOB00234", }, + Object { + "description": "Download the records in the range of 0 to 100 from a job spool.", + "options": "--record-range '0-100'", + }, ], "name": "output", "options": Array [ @@ -92,6 +96,15 @@ Object { "name": "wait-for-output", "type": "boolean", }, + Object { + "aliases": Array [ + "rr", + ], + "description": "Zero indexed range of records to download from a spool file. (example: 0-100)", + "name": "record-range", + "optional": true, + "type": "string", + }, ], "positionals": Array [ Object { diff --git a/packages/cli/src/zosfiles/-strings-/en.ts b/packages/cli/src/zosfiles/-strings-/en.ts index dad7b32bf6..8649f8866b 100644 --- a/packages/cli/src/zosfiles/-strings-/en.ts +++ b/packages/cli/src/zosfiles/-strings-/en.ts @@ -195,7 +195,9 @@ export default { TODSNAME: "The name of the data set that you want to copy to" }, OPTIONS: { - REPLACE: "Specify this option as true if you wish to replace like-named members in the target data set" + REPLACE: "Specify this option as true if you wish to replace like-named members in the target data set", + SAFE_REPLACE: "Specify this option as true if you wish to replace like-named members or the content of the target data set. " + + "This option will prompt to confirm." }, EXAMPLES: { EX1: "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET'", @@ -204,7 +206,7 @@ export default { EX4: "Copy the data set member named 'USER.FROM.SET(MEM1)' to the data set named 'USER.TO.SET'", EX5: "Copy the data set named 'USER.FROM.SET' to the data set named 'USER.TO.SET' and replace like-named members", EX6: "Copy the partitioned data set named 'TEST.PDS1' to the partitioned data set named 'TEST.PDS2'", - EX7: "Copy the partionted data set named 'EXISTING.PDS' to a non-existent target 'NEW.PDS'" + EX7: "Copy the partitioned data set named 'EXISTING.PDS' to a non-existent target 'NEW.PDS'" } }, DATA_SET_CROSS_LPAR: { diff --git a/packages/cli/src/zosfiles/copy/ds/Ds.definition.ts b/packages/cli/src/zosfiles/copy/ds/Ds.definition.ts index 6324f99352..a3d9cb4b28 100644 --- a/packages/cli/src/zosfiles/copy/ds/Ds.definition.ts +++ b/packages/cli/src/zosfiles/copy/ds/Ds.definition.ts @@ -53,6 +53,12 @@ export const DsDefinition: ICommandDefinition = { aliases: ["rep"], description: strings.OPTIONS.REPLACE, type: "boolean" + }, + { + name: "safe-replace", + aliases: ["safe-rep", "--sr"], + description: strings.OPTIONS.SAFE_REPLACE, + type: "boolean", } ] as ICommandOptionDefinition[]).sort((a, b) => a.name.localeCompare(b.name) @@ -77,6 +83,10 @@ export const DsDefinition: ICommandDefinition = { { description: strings.EXAMPLES.EX5, options: `"USER.FROM.SET" "USER.TO.SET" --replace` + }, + { + description: strings.EXAMPLES.EX6, + options: `"USER.FROM.SET" "USER.TO.SET" --safe-replace` } ] }; diff --git a/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts b/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts index 47a21cfb62..40913a7d5c 100644 --- a/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts +++ b/packages/cli/src/zosfiles/copy/ds/Ds.handler.ts @@ -9,7 +9,7 @@ * */ -import { AbstractSession, IHandlerParameters } from "@zowe/imperative"; +import { AbstractSession, IHandlerParameters, IHandlerResponseConsoleApi } from "@zowe/imperative"; import { Copy, IZosFilesResponse, IDataSet, ICopyDatasetOptions, ZosFilesUtils } from "@zowe/zos-files-for-zowe-sdk"; import { ZosFilesBaseHandler } from "../../ZosFilesBase.handler"; @@ -24,9 +24,21 @@ export default class DsHandler extends ZosFilesBaseHandler { "from-dataset": fromDataSet, enq: commandParameters.arguments.enq, replace: commandParameters.arguments.replace, - responseTimeout: commandParameters.arguments.responseTimeout + responseTimeout: commandParameters.arguments.responseTimeout, + safeReplace: commandParameters.arguments.safeReplace, + promptFn: this.promptForSafeReplace(commandParameters.response.console) }; return Copy.dataSet(session, toDataSet, options); } + + private promptForSafeReplace(console: IHandlerResponseConsoleApi) { + return async (targetDSN: string) => { + const answer: string = await console.prompt( + `The dataset '${targetDSN}' exists on the target system. This copy will result in data loss.` + + ` Are you sure you want to continue? [y/N]: ` + ); + return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"); + }; + } } diff --git a/packages/cli/src/zosjobs/download/download-output/Output.definition.ts b/packages/cli/src/zosjobs/download/download-output/Output.definition.ts index 75e5c89d42..83e59f8b12 100644 --- a/packages/cli/src/zosjobs/download/download-output/Output.definition.ts +++ b/packages/cli/src/zosjobs/download/download-output/Output.definition.ts @@ -86,11 +86,22 @@ export const OutputDefinition: ICommandDefinition = { type: "boolean", conflictsWith: ["wait-for-active"] }, + { + name: "record-range", + aliases: ["rr"], + description: "Zero indexed range of records to download from a spool file. (example: 0-100)", + type: "string", + optional: true + } ] as ICommandOptionDefinition[]), examples: [ { description: "Download all the output of the job with job ID JOB00234 to an automatically generated directory.", options: "JOB00234" + }, + { + description: "Download the records in the range of 0 to 100 from a job spool.", + options: "--record-range '0-100'" } ] }; diff --git a/packages/cli/src/zosjobs/download/download-output/Output.handler.ts b/packages/cli/src/zosjobs/download/download-output/Output.handler.ts index 76725fbba0..f732369f61 100644 --- a/packages/cli/src/zosjobs/download/download-output/Output.handler.ts +++ b/packages/cli/src/zosjobs/download/download-output/Output.handler.ts @@ -37,6 +37,8 @@ export default class OutputHandler extends ZosmfBaseHandler { const encoding: string = this.mArguments.encoding; const waitForActive: boolean = this.mArguments.waitForActive; const waitForOutput: boolean = this.mArguments.waitForOutput; + const recordRange: string = this.mArguments.recordRange; + // Get the job details const job: IJob = await GetJobs.getJob(this.mSession, jobid); const options: IDownloadAllSpoolContentParms = { @@ -49,7 +51,8 @@ export default class OutputHandler extends ZosmfBaseHandler { record, encoding, waitForActive, - waitForOutput + waitForOutput, + recordRange }; // Download 'em all await DownloadJobs.downloadAllSpoolContentCommon(this.mSession, options); diff --git a/packages/imperative/CHANGELOG.md b/packages/imperative/CHANGELOG.md index 059c83a266..6d269b63d1 100644 --- a/packages/imperative/CHANGELOG.md +++ b/packages/imperative/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to the Imperative package will be documented in this file. +## Recent Changes +- Format fix: `DeferredPromise` and `DeferredPromise.unit.test` comment format changed to match standard. + ## `8.11.0` - Enhancement: Added `DeferredPromise` class to Imperative to provide utilities for data synchronization. [#2405](https://github.com/zowe/zowe-cli/pull/2405) diff --git a/packages/imperative/src/utilities/__tests__/DeferredPromise.unit.test.ts b/packages/imperative/src/utilities/__tests__/DeferredPromise.unit.test.ts index 146067c919..7ab1cf1bbe 100644 --- a/packages/imperative/src/utilities/__tests__/DeferredPromise.unit.test.ts +++ b/packages/imperative/src/utilities/__tests__/DeferredPromise.unit.test.ts @@ -1,13 +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. - * - */ +/* +* 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 { DeferredPromise, DeferredPromiseStatus } from "../src/DeferredPromise"; diff --git a/packages/imperative/src/utilities/src/DeferredPromise.ts b/packages/imperative/src/utilities/src/DeferredPromise.ts index 20b8fcfd22..65324d0855 100644 --- a/packages/imperative/src/utilities/src/DeferredPromise.ts +++ b/packages/imperative/src/utilities/src/DeferredPromise.ts @@ -1,13 +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. - * - */ +/* +* 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. +* +*/ /* Status of the deferred promise */ export enum DeferredPromiseStatus { diff --git a/packages/zosfiles/CHANGELOG.md b/packages/zosfiles/CHANGELOG.md index 1f3ec10027..f0779747d1 100644 --- a/packages/zosfiles/CHANGELOG.md +++ b/packages/zosfiles/CHANGELOG.md @@ -5,9 +5,12 @@ All notable changes to the Zowe z/OS files SDK package will be documented in thi ## Recent Changes - BugFix: The `Create.dataSetValidateOptions()` function now correctly handles data set creation when the `dsorg` attribute is set to `PS-L` by automatically updating the `dsntype` attribute to `LARGE`. [#2141](https://github.com/zowe/zowe-cli/issues/2141) +- BugFix: Fixed an issue in the `Copy.dataSetCrossLPAR()` function where the `spacu` attribute of the copied data set was always set to `TRK`, regardless of the source data set's attributes. [#2412](https://github.com/zowe/zowe-cli/issues/2412) +- BugFix: The `Copy.data.set` function now prompts the user to confirm before overwriting the contents of the target data set with the addition of the `--safe-replace` option. [#2369] (https://github.com/zowe/zowe-cli/issues/2369) ## `8.12.0` +## `8.12.0` - Enhancement: The `Copy.dataset` function now creates a new data set if the entered target data set does not exist. [#2349](https://github.com/zowe/zowe-cli/issues/2349) - Enhancement: Added the `maxLength` option to List SDK functions (`allMembers`, `dataSetsMatchingPattern`, `membersMatchingPattern`) to specify the maximum number of items to return. [#2409](https://github.com/zowe/zowe-cli/pull/2409) - Enhancement: Added the `start` option to List SDK functions (`allMembers`, `dataSetsMatchingPattern`, `membersMatchingPattern`) to specify the first data set/member name to return in the response. [#2409](https://github.com/zowe/zowe-cli/pull/2409) diff --git a/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts b/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts index f5a9a83e1c..453a9ea0df 100644 --- a/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts @@ -22,7 +22,8 @@ import { ITestEnvironment } from "../../../../../../__tests__/__src__/environmen import { tmpdir } from "os"; import path = require("path"); import * as fs from "fs"; -import { ZosmfRestClient } from "@zowe/core-for-zowe-sdk"; +import { ZosmfRestClient, List } from "@zowe/core-for-zowe-sdk"; + let REAL_SESSION: Session; let REAL_TARGET_SESSION: Session; @@ -30,6 +31,8 @@ let testEnvironment: ITestEnvironment; let defaultSystem: ITestPropertiesSchema; let defaultTargetSystem: ITestPropertiesSchema; let fromDataSetName: string; +let fromDataSetNameTracks: string; +let fromDataSetNameCylinders: string; let toDataSetName: string; const file1 = "file1"; @@ -45,6 +48,8 @@ describe("Copy", () => { REAL_SESSION = TestEnvironment.createZosmfSession(testEnvironment); REAL_TARGET_SESSION = REAL_SESSION; fromDataSetName = `${defaultSystem.zosmf.user.trim().toUpperCase()}.DATA.ORIGINAL`; + fromDataSetNameTracks = `${defaultSystem.zosmf.user.trim().toUpperCase()}.DATA.TRKORG`; + fromDataSetNameCylinders = `${defaultSystem.zosmf.user.trim().toUpperCase()}.DATA.CYLORG`; toDataSetName = `${defaultSystem.zosmf.user.trim().toUpperCase()}.DATA.COPY`; }); @@ -467,6 +472,87 @@ describe("Copy", () => { expect(contents1.toString()).toEqual(contents2.toString()); }); }); + + describe("Safe replace option", () => { + const promptFn = jest.fn(); + beforeEach(async () => { + try { + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, fromDataSetName); + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, toDataSetName); + await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetName); + await Copy.dataSet( + REAL_SESSION, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName } } + ); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + }); + + it("Should succeed with safe replace option", async () => { + let error; + let response; + let contents1; + let contents2; + promptFn.mockResolvedValue(true); + + try { + response = await Copy.dataSet( + REAL_SESSION, + { dsn: toDataSetName}, + { + "from-dataset": { dsn: fromDataSetName }, + "safeReplace": true, + promptFn + } + ); + contents1 = await Get.dataSet(REAL_SESSION, `${fromDataSetName}`); + contents2 = await Get.dataSet(REAL_SESSION, `${toDataSetName}`); + Imperative.console.info(`Response: ${inspect(response)}`); + } catch (err) { + error = err; + Imperative.console.info(`Error: ${inspect(err)}`); + } + + expect(error).toBeFalsy(); + expect(promptFn).toHaveBeenCalledWith(toDataSetName); + expect(response).toBeTruthy(); + expect(response.success).toBe(true); + expect(response.commandResponse).toContain(ZosFilesMessages.datasetCopiedSuccessfully.message); + + expect(contents1).toBeTruthy(); + expect(contents2).toBeTruthy(); + expect(contents1.toString()).toEqual(contents2.toString()); + }); + + it("Should result in error when safe replace option is selected but the user declines the prompt", async () => { + let error; + let response; + promptFn.mockResolvedValue(false); + + try { + response = await Copy.dataSet( + REAL_SESSION, + { dsn: toDataSetName, member: file2 }, + { + "from-dataset": { dsn: fromDataSetName}, + "safeReplace": true, + promptFn + } + ); + Imperative.console.info(`Response: ${inspect(response)}`); + } catch (err) { + error = err; + Imperative.console.info(`Error: ${inspect(err)}`); + } + + expect(error).toBeTruthy(); + expect(error.message).toContain(ZosFilesMessages.datasetCopiedAborted.message); + expect(response).toBeFalsy(); + }); + + }); describe("responseTimeout option", () => { beforeEach(async () => { try { @@ -633,7 +719,7 @@ describe("Copy", () => { } expect(response?.success).toBeFalsy(); expect(error).toBeDefined(); - expect(error.message).toContain("Data set copied aborted. The existing target data set was not overwritten."); + expect(error.message).toContain("Data set copy aborted. The existing target data set was not overwritten."); }); }); @@ -814,7 +900,7 @@ describe("Copy", () => { } expect(response?.success).toBeFalsy(); expect(error).toBeDefined(); - expect(error.message).toContain("Data set copied aborted. The existing target data set was not overwritten."); + expect(error.message).toContain("Data set copy aborted. The existing target data set was not overwritten."); }); }); @@ -928,15 +1014,23 @@ describe("Copy", () => { } expect(response?.success).toBeFalsy(); expect(error).toBeDefined(); - expect(error.message).toContain("Data set copied aborted. The existing target data set was not overwritten."); + expect(error.message).toContain("Data set copy aborted. The existing target data set was not overwritten."); }); }); describe("Success cases", () => { - it("should copy the source to the destination data set and allocate the dataset", async() => { + it("should copy the source to the destination data set and allocate the dataset - CYLINDERS", async() => { + try { + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, fromDataSetNameCylinders, {alcunit: "CYL"}); + await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetNameCylinders); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + let error: any; let response: IZosFilesResponse | undefined = undefined; let contents: Buffer; + let listAttributes; const TEST_TARGET_SESSION = REAL_TARGET_SESSION; const toDataset: IDataSet = { dsn: toDataSetName, member: file1 }; const fromOptions: IGetOptions = { @@ -945,22 +1039,82 @@ describe("Copy", () => { record: false }; const options: ICrossLparCopyDatasetOptions = { - "from-dataset": { dsn: fromDataSetName }, + "from-dataset": { dsn: fromDataSetNameCylinders }, responseTimeout: 5, replace: false }; const toDataSetString = `${toDataset.dsn}(${toDataset.member})`; try { + listAttributes = (await List.dataSet(REAL_SESSION, fromDataSetNameCylinders, {attributes: true})).apiResponse.items; response = await Copy.dataSetCrossLPAR(REAL_SESSION, toDataset, options, fromOptions, TEST_TARGET_SESSION); contents = await Get.dataSet(TEST_TARGET_SESSION, toDataSetString); } catch (err) { error = err; } + + expect(listAttributes[0].spacu).toEqual("CYLINDERS"); expect(response?.success).toBeTruthy(); expect(error).not.toBeDefined(); expect(response?.errorMessage).not.toBeDefined(); expect(response?.commandResponse).toContain("Data set copied successfully"); expect(contents.toString().trim()).toBe(readFileSync(fileLocation).toString()); + + try { + await Delete.dataSet(REAL_SESSION, fromDataSetNameCylinders); + await Delete.dataSet(REAL_SESSION, toDataSetName); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + }); + + it("should copy the source to the destination data set and allocate the dataset - TRACKS", async() => { + try { + await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_SEQUENTIAL, fromDataSetNameTracks, {alcunit: "TRK"}); + await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetNameTracks); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } + + let error: any; + let response: IZosFilesResponse | undefined = undefined; + let contents: Buffer; + let listAttributes; + const TEST_TARGET_SESSION = REAL_TARGET_SESSION; + + // Append "1" such that it is not an existing data set and thus will reach generateDatasetOptions() within Copy.ts + const toDataset: IDataSet = { dsn: toDataSetName, member: file1 }; + const fromOptions: IGetOptions = { + binary: false, + encoding: undefined, + record: false + }; + const options: ICrossLparCopyDatasetOptions = { + "from-dataset": { dsn: fromDataSetNameTracks }, + responseTimeout: 5, + replace: false + }; + const toDataSetString = `${toDataset.dsn}(${toDataset.member})`; + try { + listAttributes = (await List.dataSet(REAL_SESSION, fromDataSetNameTracks, {attributes: true})).apiResponse.items; + response = await Copy.dataSetCrossLPAR(REAL_SESSION, toDataset, options, fromOptions, TEST_TARGET_SESSION); + contents = await Get.dataSet(TEST_TARGET_SESSION, toDataSetString); + } catch (err) { + error = err; + } + + expect(listAttributes[0].spacu).toEqual("TRACKS"); + expect(response?.success).toBeTruthy(); + expect(error).not.toBeDefined(); + expect(response?.errorMessage).not.toBeDefined(); + expect(response?.commandResponse).toContain("Data set copied successfully"); + expect(contents.toString().trim()).toBe(readFileSync(fileLocation).toString()); + + try { + await Delete.dataSet(REAL_SESSION, fromDataSetNameTracks); + await Delete.dataSet(REAL_SESSION, toDataSetName); + } catch (err) { + Imperative.console.info(`Error: ${inspect(err)}`); + } }); it("should overwrite the destination data set member", async() => { @@ -1043,7 +1197,7 @@ describe("Copy", () => { } expect(response?.success).toBeFalsy(); expect(error).toBeDefined(); - expect(error.message).toContain("Data set copied aborted. The existing target data set was not overwritten."); + expect(error.message).toContain("Data set copy aborted. The existing target data set was not overwritten."); }); }); diff --git a/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts b/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts index e6de696ef0..2ecad85e7a 100644 --- a/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts +++ b/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts @@ -36,6 +36,7 @@ describe("Copy", () => { const toMemberName = "mem2"; const isPDSSpy = jest.spyOn(Copy as any, "isPDS"); let dataSetExistsSpy: jest.SpyInstance; + const promptFn = jest.fn(); beforeEach(() => { copyPDSSpy.mockClear(); @@ -459,6 +460,55 @@ describe("Copy", () => { expect(lastArgumentOfCall).toHaveProperty("replace", false); }); }); + describe("Safe replace option", () => { + it("should not throw error if safeReplace has value of true", async () => { + promptFn.mockResolvedValue(true); + + const response = await Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: true, + promptFn } + ); + + expect(copyExpectStringSpy).toHaveBeenCalledTimes(1); + const argumentsOfCall = copyExpectStringSpy.mock.calls[0]; + const lastArgumentOfCall = argumentsOfCall[argumentsOfCall.length - 1]; + expect(lastArgumentOfCall).toHaveProperty("safeReplace", true); + expect(response).toEqual({success: true, commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message}); + expect(promptFn).toHaveBeenCalledWith(toDataSetName); + }); + + it("should throw error if user declines to replace the dataset", async () => { + promptFn.mockResolvedValue(false); + + await expect(Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: true, + promptFn } + )).rejects.toThrow(new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message })); + + expect(promptFn).toHaveBeenCalledWith(toDataSetName); + }); + + it("should not throw error if safeReplace has value of false", async () => { + await expect(Copy.dataSet( + dummySession, + { dsn: toDataSetName }, + { "from-dataset": { dsn: fromDataSetName }, + safeReplace: false, + } + )).resolves.not.toThrow(); + expect(copyExpectStringSpy).toHaveBeenCalledTimes(1); + const argumentsOfCall = copyExpectStringSpy.mock.calls[0]; + const lastArgumentOfCall = argumentsOfCall[argumentsOfCall.length - 1]; + expect(lastArgumentOfCall).toHaveProperty("safeReplace", false); + }); + + }); describe("Partitioned > Partitioned", () => { let createSpy: jest.SpyInstance; let dataSetExistsSpy: jest.SpyInstance; @@ -807,6 +857,7 @@ describe("Copy", () => { const listAllMembersSpy = jest.spyOn(List, "allMembers"); const createDatasetSpy = jest.spyOn(Create, "dataSet"); const uploadDatasetSpy = jest.spyOn(Upload, "bufferToDataSet"); + const psDataSetName = "TEST.PS.DATA.SET"; const memberName = "mem1"; const poDataSetName = "TEST.PO.DATA.SET"; @@ -826,6 +877,12 @@ describe("Copy", () => { spacu: "TRK" }; + const dataSetPOCYL = { + dsname: poDataSetName, + dsorg: "PO", + spacu: "CYL" + }; + beforeEach(() => { getDatasetSpy.mockClear(); listDatasetSpy.mockClear(); @@ -842,9 +899,6 @@ describe("Copy", () => { describe("Success Scenarios", () => { describe("Sequential > Sequential", () => { it("should send a request", async () => { - let response; - let caughtError; - listDatasetSpy.mockImplementation(async (): Promise => { return { apiResponse: { @@ -857,17 +911,14 @@ describe("Copy", () => { return Buffer.from("123456789abcd"); }); - try { - response = await Copy.dataSetCrossLPAR( - dummySession, - { dsn: psDataSetName }, - { "from-dataset": { dsn: dataSetPS.dsname }, replace: true}, - { }, - dummySession - ); - } catch (e) { - caughtError = e; - } + + const response = await Copy.dataSetCrossLPAR( + dummySession, + { dsn: psDataSetName }, + { "from-dataset": { dsn: dataSetPS.dsname }, replace: true}, + { }, + dummySession + ); expect(response).toEqual({ success: true, @@ -882,9 +933,7 @@ describe("Copy", () => { describe("Sequential > Sequential - no replace", () => { it("should send a request", async () => { - let response; let caughtError; - listDatasetSpy.mockImplementation(async (): Promise => { return { apiResponse: { @@ -898,7 +947,7 @@ describe("Copy", () => { }); try { - response = await Copy.dataSetCrossLPAR( + await Copy.dataSetCrossLPAR( dummySession, { dsn: psDataSetName }, { "from-dataset": { dsn: dataSetPS.dsname }, replace: false}, @@ -909,6 +958,7 @@ describe("Copy", () => { caughtError = e; } + expect(caughtError).toBeDefined(); expect(listDatasetSpy).toHaveBeenCalledTimes(2); expect(getDatasetSpy).toHaveBeenCalledTimes(1); expect(uploadDatasetSpy).toHaveBeenCalledTimes(0); @@ -959,9 +1009,88 @@ describe("Copy", () => { expect(uploadDatasetSpy).toHaveBeenCalledTimes(1); }); + it("should send a request - TRK and validate spacu", async () => { + listDatasetSpy.mockImplementation(async (): Promise => { + return { + apiResponse: { + returnedRows: 1, + items: [dataSetPO], + } + }; + }); + + listAllMembersSpy.mockImplementation(async (): Promise => { + return { + apiResponse: { + returnedRows: 1 + } + }; + }); + + const response = await Copy.dataSetCrossLPAR( + dummySession, + { dsn: poDataSetName, member: memberName }, + { "from-dataset": { dsn: poDataSetName, member: memberName }, replace: true }, + {}, + dummySession); + + // Assertions + expect(response).toEqual({ + success: true, + commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message + }); + + expect(listDatasetSpy).toHaveBeenCalledTimes(2); + expect(listAllMembersSpy).toHaveBeenCalledTimes(1); + expect(listAllMembersSpy.mock.calls[0][2].start).toBe(memberName); + expect(getDatasetSpy).toHaveBeenCalledTimes(1); + expect(uploadDatasetSpy).toHaveBeenCalledTimes(1); + expect(dataSetPO.spacu).toBe("TRK"); + }); + + it("should send a request - CYL and validate spacu", async () => { + listDatasetSpy.mockImplementation(async (): Promise => { + return { + apiResponse: { + returnedRows: 1, + items: [dataSetPOCYL], + } + }; + }); + + listAllMembersSpy.mockImplementation(async (): Promise => { + return { + apiResponse: { + returnedRows: 1 + } + }; + }); + + + const response = await Copy.dataSetCrossLPAR( + dummySession, + { dsn: poDataSetName, member: memberName }, + { "from-dataset": { dsn: poDataSetName, member: memberName }, replace: true }, + {}, + dummySession + ); + + + expect(response).toEqual({ + success: true, + commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message + }); + + expect(listDatasetSpy).toHaveBeenCalledTimes(2); + expect(listAllMembersSpy).toHaveBeenCalledTimes(1); + expect(listAllMembersSpy.mock.calls[0][2].start).toBe(memberName); + expect(getDatasetSpy).toHaveBeenCalledTimes(1); + expect(uploadDatasetSpy).toHaveBeenCalledTimes(1); + expect(dataSetPOCYL.spacu).toBe("CYL"); + }); + describe("Sequential > Member", () => { it("should send a request", async () => { - let response; let caughtError; listDatasetSpy.mockReturnValueOnce({ @@ -983,17 +1112,14 @@ describe("Copy", () => { }; }); - try { - response = await Copy.dataSetCrossLPAR( - dummySession, - { dsn: poDataSetName, member: memberName }, - { "from-dataset": { dsn: psDataSetName }, replace: true}, - { }, - dummySession - ); - } catch (e) { - caughtError = e; - } + const response = await Copy.dataSetCrossLPAR( + dummySession, + { dsn: poDataSetName, member: memberName }, + { "from-dataset": { dsn: psDataSetName }, replace: true}, + { }, + dummySession + ); + expect(response).toEqual({ success: true, diff --git a/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap b/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap index 59245f88cb..61acd4749f 100644 --- a/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap +++ b/packages/zosfiles/__tests__/__unit__/utils/__snapshots__/ZosFilesUtils.unit.test.ts.snap @@ -85,7 +85,7 @@ Object { "message": "Data set allocation aborted. The \\"allocate like\\" data set was not found.", }, "datasetCopiedAborted": Object { - "message": "Data set copied aborted. The existing target data set was not overwritten.", + "message": "Data set copy aborted. The existing target data set was not overwritten.", }, "datasetCopiedAbortedNoPDS": Object { "message": "Data set copied aborted. Copying from a PDS to PDS is not supported when using the 'dsclp' option.", diff --git a/packages/zosfiles/src/constants/ZosFiles.messages.ts b/packages/zosfiles/src/constants/ZosFiles.messages.ts index 33d74079ec..9c4b9d8e7a 100644 --- a/packages/zosfiles/src/constants/ZosFiles.messages.ts +++ b/packages/zosfiles/src/constants/ZosFiles.messages.ts @@ -678,7 +678,7 @@ export const ZosFilesMessages: { [key: string]: IMessageDefinition } = { * @type {IMessageDefinition} */ datasetCopiedAborted: { - message: "Data set copied aborted. The existing target data set was not overwritten." + message: "Data set copy aborted. The existing target data set was not overwritten." }, /** diff --git a/packages/zosfiles/src/methods/copy/Copy.ts b/packages/zosfiles/src/methods/copy/Copy.ts index 3d943655f5..2894cc5c45 100644 --- a/packages/zosfiles/src/methods/copy/Copy.ts +++ b/packages/zosfiles/src/methods/copy/Copy.ts @@ -57,6 +57,7 @@ export class Copy { ): Promise { ImperativeExpect.toBeDefinedAndNonBlank(options["from-dataset"].dsn, "fromDataSetName"); ImperativeExpect.toBeDefinedAndNonBlank(toDataSetName, "toDataSetName"); + const safeReplace: boolean = options.safeReplace; if(options["from-dataset"].dsn === toDataSetName && toMemberName === options["from-dataset"].member) { return { @@ -71,6 +72,15 @@ export class Copy { if (newDataSet) { await Create.dataSetLike(session, toDataSetName, options["from-dataset"].dsn); } + else if(safeReplace) { + if (options.promptFn != null) { + const userResponse = await options.promptFn(toDataSetName); + + if(!userResponse) { + throw new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message }); + } + } + } if(!toMemberName && !options["from-dataset"].member) { const sourceIsPds = await this.isPDS(session, options["from-dataset"].dsn); const targetIsPds = await this.isPDS(session, toDataSetName); @@ -272,7 +282,7 @@ export class Copy { /* * If the source is a PDS and no member was specified then abort the copy. */ - if((sourceDataSetObj.dsorg == "PO" || sourceDataSetObj.dsorg == "POE") && sourceMember == undefined){ + if(sourceDataSetObj.dsorg.startsWith("PO") && sourceMember == undefined){ throw new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAbortedNoPDS.message }); } } @@ -316,7 +326,7 @@ export class Copy { targetDataSetObj = TargetDsList.apiResponse.items[dsnameIndex]; targetFound = true; - if((targetDataSetObj.dsorg == "PO" || targetDataSetObj.dsorg == "POE") && targetMember == undefined) + if(targetDataSetObj.dsorg.startsWith("PO") && targetMember == undefined) { throw new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAbortedTargetNotPDSMember.message }); } @@ -346,11 +356,11 @@ export class Copy { * If this is a PDS but the target is the sequential dataset and does not exist, * create a new sequential dataset with the same parameters as the original PDS. */ - if((createOptions.dsorg == "PO" || createOptions.dsorg == "POE") && targetMember == undefined){ + if(createOptions.dsorg.startsWith("PO") && targetMember == undefined){ createOptions.dsorg ="PS"; createOptions.dirblk = 0; } - else if(targetMember != undefined && (createOptions.dsorg != "PO" && createOptions.dsorg != "POE")) + else if(targetMember != undefined && !createOptions.dsorg.startsWith("PO")) { createOptions.dsorg ="PO"; createOptions.dirblk = 1; @@ -419,21 +429,27 @@ export class Copy { storclass: targetOptions.targetStorageClass, mgntclass: targetOptions.targetManagementClass, dataclass: targetOptions.targetDataClass, - dirblk: parseInt(dsInfo.dsorg == "PO" || dsInfo.dsorg == "POE" ? "10" : "0") + dirblk: parseInt(dsInfo.dsorg.startsWith("PO") ? "10" : "0") })); } /** - * Private function to convert the ALC value from the format returned by the Get() call in to the format used by the Create() call + * Converts the ALC value from the format returned by the Get() call to the format used by the Create() call. + * @param {string} getValue - The ALC value from the Get() call. + * @returns {string} - The ALC value in the format used by the Create() call. */ - private static convertAlcTozOSMF( zosmfValue: string): string { + private static convertAlcTozOSMF(getValue: string): string { /** * Create dataset only accepts tracks or cylinders as allocation units. * When the get() call retreives the dataset info, it will convert size * allocations of the other unit types in to tracks. So we will always * allocate the new target in tracks. */ - return "TRK"; + const alcMap: Record = { + "TRACKS": "TRK", + "CYLINDERS": "CYL" + }; + return alcMap[getValue.toUpperCase()] || "TRK"; } } diff --git a/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts b/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts index b0e2285db6..23d75b7967 100644 --- a/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts +++ b/packages/zosfiles/src/methods/copy/doc/ICopyDatasetOptions.ts @@ -34,4 +34,17 @@ export interface ICopyDatasetOptions extends IZosFilesOptions { * @type {boolean} */ replace?: boolean; + + /** + * Safe replace option + * @type {boolean}; + */ + safeReplace?: boolean; + + /** + * Prompt callback that will be invoked before overwiting a data set. + * @param targetDSN Name of data set that already exists + * @returns True if target data set should be overwritten + */ + promptFn?: (targetDSN: string) => Promise; } diff --git a/packages/zosfiles/src/methods/copy/doc/ICrossLparCopyDatasetOptions.ts b/packages/zosfiles/src/methods/copy/doc/ICrossLparCopyDatasetOptions.ts index 67eb680818..dc954fe028 100644 --- a/packages/zosfiles/src/methods/copy/doc/ICrossLparCopyDatasetOptions.ts +++ b/packages/zosfiles/src/methods/copy/doc/ICrossLparCopyDatasetOptions.ts @@ -44,11 +44,4 @@ export interface ICrossLparCopyDatasetOptions extends ICopyDatasetOptions { * @type {boolean} */ overwrite?: boolean; - - /** - * Prompt callback that will be invoked before overwiting a data set. - * @param targetDSN Name of data set that already exists - * @returns True if target data set should be overwritten - */ - promptFn?: (targetDSN: string) => Promise; } diff --git a/packages/zosjobs/CHANGELOG.md b/packages/zosjobs/CHANGELOG.md index 33947ef779..0adbdd6185 100644 --- a/packages/zosjobs/CHANGELOG.md +++ b/packages/zosjobs/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Zowe z/OS jobs SDK package will be documented in this file. +## Recent Changes + +- Enhancement: Added `--recordRange` flag logic handling to `DownloadJobs.downloadSpoolContentCommon()` to to allow users to select a specific range of records to output from a spool file. [#2411](https://github.com/zowe/zowe-cli/pull/2411) + ## `8.10.2` - BugFix: Check if encoding is set and not empty now works for numeric-only value in encoding (GetJobs.ts). [#2392] (https://github.com/zowe/zowe-cli/pull/2392). diff --git a/packages/zosjobs/__tests__/__system__/DownloadJobs.system.test.ts b/packages/zosjobs/__tests__/__system__/DownloadJobs.system.test.ts index ba34e0f1ed..0d2c767f13 100644 --- a/packages/zosjobs/__tests__/__system__/DownloadJobs.system.test.ts +++ b/packages/zosjobs/__tests__/__system__/DownloadJobs.system.test.ts @@ -57,6 +57,7 @@ describe("Download Jobs - System tests", () => { const job = await SubmitJobs.submitJclNotifyCommon(REAL_SESSION, { jcl: iefbr14JCL }); + testEnvironment.resources.jobs.push(job); jobid = job.jobid; jobname = job.jobname; @@ -289,8 +290,89 @@ describe("Download Jobs - System tests", () => { } } }, LONG_TIMEOUT); - }); + it("should be able to download all DDs from job output with a record range (0-5)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0-5" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(6); + }); + + it("should be able to download all DDs from job output with a record range (2-8)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "2-8" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(7); + }); + + it("should be able to download all DDs from job output with a record range (0-100)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0-100" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString()).toContain("MINUTES EXECUTION TIME"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(16); //only 16 records in spool file + }); + }); describe("Negative tests", () => { let badJobFile: IJobFile; @@ -345,6 +427,101 @@ describe("Download Jobs - System tests", () => { expect(JSON.parse(err.causeErrors).message).toContain("does not contain spool file"); }); + it("should be able to download all DDs from job output with a record range (0-0)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + let err; + let expectedFile; + try{ + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0-0" + }); + expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + } + catch(e){ + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Invalid record range specified: 0-0. Ensure the format is x-y with x < y.'); + expect(expectedFile).toBeUndefined(); + }); + + it("should be able to download all DDs from job output with a record range (2-1)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + let err; + let expectedFile; + try{ + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "2-1" + }); + expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + } + catch(e){ + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Invalid record range specified: 2-1. Ensure the format is x-y with x < y.'); + expect(expectedFile).toBeUndefined(); + }); + + it("should be able to download all DDs from job output with a record range (0 50)", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + let err; + let expectedFile; + try{ + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0 50" + }); + expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + } + catch(e){ + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toEqual('Invalid record range format: 0 50. Expected format is x-y.'); + expect(expectedFile).toBeUndefined(); + }); }); }); @@ -531,5 +708,87 @@ describe("Download Jobs - System tests - Encoded", () => { } } }, LONG_TIMEOUT); + + it("should be able to download all DDs from job output with a record range (0-5) - encoded", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0-5" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(6); + }); + + it("should be able to download all DDs from job output with a record range (2-8) - encoded", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "2-8" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).not.toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(7); + }); + + it("should be able to download all DDs from job output with a record range (0-100) - encoded", async () => { + for (const file of jobFiles) { + if (file.ddname === "JESMSGLG") { + jesJCLJobFile = file; + } + } + + await DownloadJobs.downloadAllSpoolContentCommon(REAL_SESSION, { + outDir: outputDirectory, + jobid, + jobname, + recordRange: "0-100" + }); + const expectedFile = DownloadJobs.getSpoolDownloadFilePath( + { + jobFile: jesJCLJobFile, + omitJobidDirectory: false, + outDir: outputDirectory + } + ); + expect(IO.existsSync(expectedFile)).toEqual(true); + expect(IO.readFileSync(expectedFile).toString()).toBeDefined(); + expect(IO.readFileSync(expectedFile).toString()).toContain("J E S 2 J O B L O G"); + expect(IO.readFileSync(expectedFile).toString()).toContain("0------ JES2 JOB STATISTICS ------"); + expect(IO.readFileSync(expectedFile).toString()).toContain("MINUTES EXECUTION TIME"); + expect(IO.readFileSync(expectedFile).toString().trim().split('\n').length).toEqual(16); //only 16 records in spool file + }); }); }); diff --git a/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts b/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts index 2005f52df9..c0ecdeed28 100644 --- a/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts +++ b/packages/zosjobs/__tests__/__unit__/DownloadJobs.unit.test.ts @@ -473,6 +473,22 @@ describe("DownloadJobs", () => { expect(IO.createDirsSyncFromFilePath).toHaveBeenCalledWith(downloadFilePath); expect(downloadFilePath).not.toContain(spoolParms.jobid); }); + + it("should allow users to call downloadSpoolContentCommon with correct parameters (record range)", async () => { + const jobFile: IJobFile = JSON.parse(JSON.stringify(jobFiles[0])); + const spoolParms: IDownloadSpoolContentParms = { + jobFile: jobFile, + jobid: fakeJobID, + jobname: fakeJobName, + recordRange: "0-100" + }; + const downloadFilePath = DownloadJobs.getSpoolDownloadFilePath(spoolParms); + + await DownloadJobs.downloadSpoolContentCommon(fakeSession, spoolParms); + + expect(IO.createDirsSyncFromFilePath).toHaveBeenCalledWith(downloadFilePath); + expect(downloadFilePath).toContain(DownloadJobs.DEFAULT_JOBS_OUTPUT_DIR); + }); }); }); describe("Error catching - async/ await", () => { @@ -566,6 +582,44 @@ describe("DownloadJobs", () => { }); }); /* eslint-enable jest/no-done-callback */ + + it("should throw error regarding record range on spoolParms (0 100)", async () => { + const jobFile: IJobFile = JSON.parse(JSON.stringify(jobFiles[0])); + const spoolParms: IDownloadSpoolContentParms = { + jobFile: jobFile, + jobid: fakeJobID, + jobname: fakeJobName, + recordRange: "0 100" + }; + let err; + try { + await DownloadJobs.downloadSpoolContentCommon(fakeSession, spoolParms); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toContain(`Invalid record range format: ${spoolParms.recordRange}. Expected format is x-y.`); + }); + + it("should throw error regarding record range on spoolParms (100-0)", async () => { + const jobFile: IJobFile = JSON.parse(JSON.stringify(jobFiles[0])); + const spoolParms: IDownloadSpoolContentParms = { + jobFile: jobFile, + jobid: fakeJobID, + jobname: fakeJobName, + recordRange: "100-0" + }; + let err; + try { + await DownloadJobs.downloadSpoolContentCommon(fakeSession, spoolParms); + } catch (e) { + err = e; + } + + expect(err).toBeDefined(); + expect(err.message).toContain(`Invalid record range specified: ${spoolParms.recordRange}. Ensure the format is x-y with x < y.`); + }); }); describe("Parameter validation tests", () => { diff --git a/packages/zosjobs/src/DownloadJobs.ts b/packages/zosjobs/src/DownloadJobs.ts index 14728422eb..780a2e7ca7 100644 --- a/packages/zosjobs/src/DownloadJobs.ts +++ b/packages/zosjobs/src/DownloadJobs.ts @@ -143,9 +143,30 @@ export class DownloadJobs { parameters += "?fileEncoding=" + parms.encoding; } + const headers = [Headers.TEXT_PLAIN_UTF8]; + + // Handle record range + if (parms.recordRange) { + const recordRangeMatch = parms.recordRange.match(/^(\d+)-(\d+)$/); // Match multi-digit numbers + if (recordRangeMatch) { + const start = parseInt(recordRangeMatch[1], 10); + const end = parseInt(recordRangeMatch[2], 10); + + if (start >= 0 && end > start) { + if (parms.recordRange) { + headers.push({ "X-IBM-Record-Range": `${start}-${end}` }); + } + } else { + throw new Error(`Invalid record range specified: ${parms.recordRange}. Ensure the format is x-y with x < y.`); + } + } else { + throw new Error(`Invalid record range format: ${parms.recordRange}. Expected format is x-y.`); + } + } + const writeStream = parms.stream ?? IO.createWriteStream(file); const normalizeResponseNewLines = !(parms.binary || parms.record); - await ZosmfRestClient.getStreamed(session, JobsConstants.RESOURCE + parameters, [Headers.TEXT_PLAIN_UTF8], writeStream, + await ZosmfRestClient.getStreamed(session, JobsConstants.RESOURCE + parameters, headers, writeStream, normalizeResponseNewLines); } diff --git a/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts b/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts index 13ce91987c..f0968e8af5 100644 --- a/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts +++ b/packages/zosjobs/src/doc/input/IDownloadAllSpoolContentParms.ts @@ -78,6 +78,14 @@ export interface IDownloadAllSpoolContentParms { */ encoding?: string; + /** + * Optional record range + * e.g. 0-100 + * @type {string} + * @memberof IDownloadAllSpoolContentParms + */ + recordRange?: string; + /** * Wait for the job to reach output status */