Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Identical member names - PDS enhancement #2427

Merged
merged 23 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

},
response: {
console: { promptFn: jest.fn() }
console: { promptFn: jest.fn(), promptForLikeNamedMembers: jest.fn() }
}
};

Expand All @@ -68,7 +68,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -98,7 +99,7 @@
responseTimeout
},
response: {
console: { promptFn: jest.fn() }
console: { promptFn: jest.fn(), promptForLikeNamedMembers: jest.fn() }
}
};

Expand All @@ -116,7 +117,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -162,7 +164,8 @@
"replace": commandParameters.arguments.replace,
"responseTimeout": commandParameters.arguments.responseTimeout,
"safeReplace": commandParameters.arguments.safeReplace,
"promptFn": expect.any(Function)
"promptFn": expect.any(Function),
"promptForLikeNamedMembers": expect.any(Function)
}
);
expect(response).toBe(defaultReturn);
Expand Down Expand Up @@ -198,7 +201,7 @@
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.` +
`The dataset '${toDataSetName}' exists on the target system. This copy may result in data loss.` +
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(true);
Expand Down Expand Up @@ -234,7 +237,79 @@
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.` +
`The dataset '${toDataSetName}' exists on the target system. This copy may result in data loss.` +
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(false);
});
it("should prompt the user about duplicate member names 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 = false;
const safeReplace = false;
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 promptForDuplicates = (handler as any)["promptForLikeNamedMembers"]({ prompt: promptMock });
const result = await promptForDuplicates();

expect(promptMock).toHaveBeenCalledWith(
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(true);
});
it("should prompt the user about duplicate member names and return false when input is 'N'", async () => {
const handler = new DsHandler();

expect(handler).toBeInstanceOf(ZosFilesBaseHandler);
const fromDataSetName = "ABCD";
const toDataSetName = "EFGH";
const enq = "SHR";
const replace = false;
const safeReplace = false;
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 promptForDuplicates = (handler as any)["promptForLikeNamedMembers"]({ prompt: promptMock });
const result = await promptForDuplicates();

expect(promptMock).toHaveBeenCalledWith(
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
` Are you sure you want to continue? [y/N]: `
);
expect(result).toBe(false);
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/zosfiles/copy/ds/Ds.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export default class DsHandler extends ZosFilesBaseHandler {
replace: commandParameters.arguments.replace,
responseTimeout: commandParameters.arguments.responseTimeout,
safeReplace: commandParameters.arguments.safeReplace,
promptFn: this.promptForSafeReplace(commandParameters.response.console)
promptFn: this.promptForSafeReplace(commandParameters.response.console),
promptForLikeNamedMembers: this.promptForLikeNamedMembers(commandParameters.response.console)
};

return Copy.dataSet(session, toDataSet, options);
Expand All @@ -35,10 +36,20 @@ export default class DsHandler extends ZosFilesBaseHandler {
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.` +
`The dataset '${targetDSN}' exists on the target system. This copy may result in data loss.` +
` Are you sure you want to continue? [y/N]: `
);
return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
};
}

private promptForLikeNamedMembers(console: IHandlerResponseConsoleApi) {
return async() => {
const answer: string = await console.prompt (
`The source and target data sets have like named member names. The contents of those members will be overwritten.` +
` Are you sure you want to continue? [y/N]: `
)
return answer != null && (answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,40 @@ describe("Copy", () => {
});
});

describe("hasLikeNamedMembers", () => {
beforeEach(async () => {
try {
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, fromDataSetName);
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName);
await Upload.fileToDataset(REAL_SESSION, fileLocation, fromDataSetName);
await Upload.fileToDataset(REAL_SESSION, fileLocation, toDataSetName);
}
catch (err) {
Imperative.console.info(`Error: ${inspect(err)}`);
}
});
afterEach(async () => {
try {
await Delete.dataSet(REAL_SESSION, fromDataSetName);
await Delete.dataSet(REAL_SESSION, toDataSetName);
} catch (err) {
Imperative.console.info(`Error: ${inspect(err)}`);
}
});
it("should return true if the source and target data sets have like-named members", async () => {
const response = await Copy["hasLikeNamedMembers"](REAL_SESSION, fromDataSetName, toDataSetName);
expect(response).toBe(true);
});

it("should return false if the source and target data sets do not have like-named members", async () => {
await Delete.dataSet(REAL_SESSION, toDataSetName);
await Create.dataSet(REAL_SESSION, CreateDataSetTypeEnum.DATA_SET_PARTITIONED, toDataSetName);

const response = await Copy["hasLikeNamedMembers"](REAL_SESSION, fromDataSetName, toDataSetName);
expect(response).toBe(false);
});
});

describe("Data Set Cross LPAR", () => {
describe("Common Failures", () => {
it("should fail if no fromDataSet data set name is supplied", async () => {
Expand Down
117 changes: 115 additions & 2 deletions packages/zosfiles/__tests__/__unit__/methods/copy/Copy.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@
const toDataSetName = "USER.DATA.TO";
const toMemberName = "mem2";
const isPDSSpy = jest.spyOn(Copy as any, "isPDS");
const hasLikeNamedMembers = jest.spyOn(Copy as any, "hasLikeNamedMembers");
let dataSetExistsSpy: jest.SpyInstance;
const promptFn = jest.fn();
const promptForLikeNamedMembers = jest.fn();

beforeEach(() => {
copyPDSSpy.mockClear();
copyExpectStringSpy.mockClear().mockImplementation(async () => { return ""; });
isPDSSpy.mockClear().mockResolvedValue(false);
dataSetExistsSpy = jest.spyOn(Copy as any, "dataSetExists").mockResolvedValue(true);

hasLikeNamedMembers.mockClear().mockResolvedValue(false);
promptForLikeNamedMembers.mockClear();
});
afterAll(() => {
isPDSSpy.mockRestore();
Expand Down Expand Up @@ -619,6 +622,48 @@
commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message
});
});
it("should display a prompt for like named members if there are duplicate member names and --safe-replace and --replace flags are not used", async () => {
hasLikeNamedMembers.mockResolvedValue(true);
promptForLikeNamedMembers.mockClear().mockResolvedValue(true);

const response = await Copy.dataSet(
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
);
expect(promptForLikeNamedMembers).toHaveBeenCalledWith();

})
it("should not display a prompt for like named members if there are no duplicate member names", async () => {
const response = await Copy.dataSet(
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
);

expect(promptForLikeNamedMembers).not.toHaveBeenCalled();
});
it("should throw error if user declines to replace the dataset", async () => {
hasLikeNamedMembers.mockResolvedValue(true);
promptForLikeNamedMembers.mockClear().mockResolvedValue(false);

await expect(Copy.dataSet(
dummySession,
{ dsn: toDataSetName },
{ "from-dataset": { dsn: fromDataSetName },
safeReplace: false,
replace: false,
promptForLikeNamedMembers }
)).rejects.toThrow(new ImperativeError({ msg: ZosFilesMessages.datasetCopiedAborted.message }));

expect(promptForLikeNamedMembers).toHaveBeenCalled();
});
});
it("should return early if the source and target data sets are identical", async () => {
const response = await Copy.dataSet(
Expand Down Expand Up @@ -711,7 +756,7 @@
});
});

describe("Copy Partitioned Data Set", () => {
describe("Partitioned Data Set", () => {
const listAllMembersSpy = jest.spyOn(List, "allMembers");
const downloadAllMembersSpy = jest.spyOn(Download, "allMembers");
const uploadSpy = jest.spyOn(Upload, "streamToDataSet");
Expand All @@ -722,6 +767,11 @@
const readStream = jest.spyOn(IO, "createReadStream");
const rmSync = jest.spyOn(fs, "rmSync");
const listDatasetSpy = jest.spyOn(List, "dataSet");
const hasLikeNamedMembers = jest.spyOn(Copy as any, "hasLikeNamedMembers");

beforeEach(() => {
hasLikeNamedMembers.mockRestore();
});

const dsPO = {
dsname: fromDataSetName,
Expand Down Expand Up @@ -849,6 +899,69 @@
commandResponse: ZosFilesMessages.datasetCopiedSuccessfully.message,
});
});

describe("hasLikeNamedMembers", () => {
const listAllMembersSpy = jest.spyOn(List, "allMembers");

beforeEach(() => {
jest.clearAllMocks();
});
it("should return true if the source and target have like-named members", async () => {
listAllMembersSpy.mockImplementation(async (session, dsName): Promise<any> => {
if (dsName === fromDataSetName) {
return {
apiResponse: {
items: [
{ member: "mem1" },
{ member: "mem2" },
]
}
};
} else if (dsName === toDataSetName) {
return {
apiResponse: {
items: [{ member: "mem1" }]
}
};
}
});

const response = await Copy["hasLikeNamedMembers"](dummySession, fromDataSetName, toDataSetName);
expect(response).toBe(true);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, fromDataSetName);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName);
});
it("should return false if the source and target do not have like-named members", async () => {
const sourceResponse = {
apiResponse: {
items: [
{ member: "mem1" },
{ member: "mem2" },
]
}
};
const targetResponse = {
apiResponse: {
items: [
{ member: "mem3" },
]
}
};
listAllMembersSpy.mockImplementation(async (session, dsName): Promise<any> => {
if (dsName === fromDataSetName) {
return sourceResponse;
} else if (dsName === toDataSetName) {
return targetResponse;
}
});

const response = await Copy["hasLikeNamedMembers"](dummySession, fromDataSetName, toDataSetName);

expect(response).toBe(false);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, fromDataSetName);
expect(listAllMembersSpy).toHaveBeenCalledWith(dummySession, toDataSetName);
});
});
});

describe("Data Set Cross LPAR", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/zosfiles/src/constants/ZosFiles.messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ export const ZosFilesMessages: { [key: string]: IMessageDefinition } = {
message: "Member(s) downloaded successfully."
},


/**
* Message indicating that the member was downloaded successfully
* @type {IMessageDefinition}
Expand Down
Loading
Loading