diff --git a/packages/zosfiles/CHANGELOG.md b/packages/zosfiles/CHANGELOG.md index c3d2d68ec..186ef4334 100644 --- a/packages/zosfiles/CHANGELOG.md +++ b/packages/zosfiles/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Zowe z/OS files SDK package will be documented in this file. +## Recent Changes + +- 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) + ## `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) 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 bccc37a43..27652eea7 100644 --- a/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts +++ b/packages/zosfiles/__tests__/__system__/methods/copy/Copy.system.test.ts @@ -22,6 +22,7 @@ import { ITestEnvironment } from "../../../../../../__tests__/__src__/environmen import { tmpdir } from "os"; import path = require("path"); import * as fs from "fs"; +import { List } from "@zowe/zos-files-for-zowe-sdk"; let REAL_SESSION: Session; let REAL_TARGET_SESSION: Session; @@ -29,6 +30,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"; @@ -44,6 +47,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`; }); @@ -891,10 +896,18 @@ describe("Copy", () => { }); 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 = { @@ -903,22 +916,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() => { 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 e6de696ef..ea3ef2049 100644 --- a/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts +++ b/packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts @@ -807,6 +807,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 +827,12 @@ describe("Copy", () => { spacu: "TRK" }; + const dataSetPOCYL = { + dsname: poDataSetName, + dsorg: "PO", + spacu: "CYL" + }; + beforeEach(() => { getDatasetSpy.mockClear(); listDatasetSpy.mockClear(); @@ -842,9 +849,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 +861,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 +883,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 +897,7 @@ describe("Copy", () => { }); try { - response = await Copy.dataSetCrossLPAR( + await Copy.dataSetCrossLPAR( dummySession, { dsn: psDataSetName }, { "from-dataset": { dsn: dataSetPS.dsname }, replace: false}, @@ -909,6 +908,7 @@ describe("Copy", () => { caughtError = e; } + expect(caughtError).toBeDefined(); expect(listDatasetSpy).toHaveBeenCalledTimes(2); expect(getDatasetSpy).toHaveBeenCalledTimes(1); expect(uploadDatasetSpy).toHaveBeenCalledTimes(0); @@ -959,9 +959,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 +1062,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/src/methods/copy/Copy.ts b/packages/zosfiles/src/methods/copy/Copy.ts index 3d943655f..7e5d91d90 100644 --- a/packages/zosfiles/src/methods/copy/Copy.ts +++ b/packages/zosfiles/src/methods/copy/Copy.ts @@ -272,7 +272,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 +316,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 +346,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 +419,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"; } }