Skip to content

Commit 7e4d314

Browse files
Merge pull request #71 from pcoop/branch1
Better error handling
2 parents 7957551 + 476878f commit 7e4d314

File tree

7 files changed

+221
-11
lines changed

7 files changed

+221
-11
lines changed

__tests__/api/BundleDeploy/BundleDeployer.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,19 @@ describe("BundleDeployer01", () => {
133133
expect(listSpy).toHaveBeenCalledTimes(2);
134134
expect(submitSpy).toHaveBeenCalledTimes(1);
135135
});
136+
it("should failover to JESMSGLG if SYSTSPRT not found", async () => {
137+
138+
const createSpy = jest.spyOn(ZosmfSession, "createBasicZosmfSession").mockImplementationOnce(() => ({}));
139+
const listSpy = jest.spyOn(List, "allMembers").mockImplementationOnce(() => ( { val: "DFHDPLOY" }))
140+
.mockImplementationOnce(() => ( { val: "EYU9ABSI" }));
141+
const submitSpy = jest.spyOn(SubmitJobs, "submitJclString").mockImplementationOnce(() =>
142+
[{ddName: "JESMSGLG", stepName: "DFHDPLOY"}] );
143+
await runDeployTestWithError();
144+
145+
expect(createSpy).toHaveBeenCalledTimes(1);
146+
expect(listSpy).toHaveBeenCalledTimes(2);
147+
expect(submitSpy).toHaveBeenCalledTimes(1);
148+
});
136149
it("should tolerate empty output from DFHDPLOY", async () => {
137150

138151
const createSpy = jest.spyOn(ZosmfSession, "createBasicZosmfSession").mockImplementationOnce(() => ({}));

__tests__/api/BundleDeploy/__snapshots__/BundleDeployer.test.ts.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`BundleDeployer01 should complain if DFHDPLOY ends with an error 1`] = `
44

55
exports[`BundleDeployer01 should complain if DFHDPLOY ends with an error 2`] = `"DFHDPLOY stopped processing due to an error."`;
66

7-
exports[`BundleDeployer01 should complain if SYSTSPRT not found 1`] = `"SYSTSPRT output from DFHDPLOY not found. Most recent status update: 'Submitting DFHDPLOY JCL for the DEPLOY action'."`;
7+
exports[`BundleDeployer01 should complain if SYSTSPRT not found 1`] = `"SYSTSPRT and JESMSGLG output from DFHDPLOY not found. Most recent status update: 'Submitting DFHDPLOY JCL for the DEPLOY action'."`;
88

99
exports[`BundleDeployer01 should complain if cicshlq not found 1`] = `"Validation of --cicshlq dataset failed: Injected CICSHLQ error"`;
1010

@@ -30,6 +30,10 @@ exports[`BundleDeployer01 should deploy successfully 1`] = `"Bundle deployment s
3030

3131
exports[`BundleDeployer01 should fail with overlong jobcard 1`] = `"--jobcard parameter section cannot be split into 72 character lines: '// 73CHARS=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1'."`;
3232

33+
exports[`BundleDeployer01 should failover to JESMSGLG if SYSTSPRT not found 1`] = `"undefined"`;
34+
35+
exports[`BundleDeployer01 should failover to JESMSGLG if SYSTSPRT not found 2`] = `"DFHDPLOY command completed in error without generating SYSTSPRT output."`;
36+
3337
exports[`BundleDeployer01 should generate deploy JCL for AVAILABLE 1`] = `
3438
"//DFHDPLOY JOB DFHDPLOY,CLASS=A,MSGCLASS=X,TIME=NOLIMIT
3539
//DFHDPLOY EXEC PGM=DFHDPLOY,REGION=100M

__tests__/api/BundlePush/BundlePusher.test.ts

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ const DEFAULT_PARAMTERS: IHandlerParameters = {
3030
response: {
3131
data: {
3232
setMessage: jest.fn((setMsgArgs) => {
33-
expect("" + setMsgArgs).toMatchSnapshot();
33+
throw new Error("Unexpected use of setMessage in Mock: " + setMsgArgs.toString());
3434
}),
3535
setObj: jest.fn((setObjArgs) => {
36-
expect(setObjArgs).toMatchSnapshot();
36+
throw new Error("Unexpected use of setObj in Mock: " + setObjArgs.toString());
3737
})
3838
},
3939
console: {
4040
log: jest.fn((logs) => {
41-
expect("" + logs).toMatchSnapshot();
41+
consoleText += logs.toString();
4242
}),
4343
error: jest.fn((errors) => {
44-
expect("" + errors).toMatchSnapshot();
44+
throw new Error("Unexpected use of error log in Mock: " + errors.toString());
4545
}),
4646
errorHeader: jest.fn(() => undefined)
4747
},
@@ -53,6 +53,7 @@ const DEFAULT_PARAMTERS: IHandlerParameters = {
5353
definition: PushBundleDefinition.PushBundleDefinition,
5454
fullDefinition: PushBundleDefinition.PushBundleDefinition,
5555
};
56+
let consoleText = "";
5657

5758
// Initialise xml2json before mocking anything
5859
const parser = require("xml2json");
@@ -87,6 +88,7 @@ describe("BundlePusher01", () => {
8788
}
8889
});
8990
uploadSpy = jest.spyOn(Upload, "dirToUSSDirRecursive").mockImplementation(() => ({}));
91+
consoleText = "";
9092
});
9193
afterEach(() => {
9294
jest.restoreAllMocks();
@@ -323,6 +325,51 @@ describe("BundlePusher01", () => {
323325
expect(readSpy).toHaveBeenCalledTimes(1);
324326
expect(uploadSpy).toHaveBeenCalledTimes(1);
325327
});
328+
it("should handle custom bundle id", async () => {
329+
uploadSpy.mockImplementationOnce(() => { throw new Error("Injected upload error"); });
330+
readSpy = jest.spyOn(fs, "readFileSync").mockImplementation((data: string) => {
331+
if (data.indexOf("cics.xml") > -1) {
332+
return "<manifest xmlns=\"http://www.ibm.com/xmlns/prod/cics/bundle\" id=\"InjectedBundleId\" ></manifest>";
333+
}
334+
});
335+
336+
await runPushTestWithError("__tests__/__resources__/ExampleBundle01", false,
337+
"A problem occurred uploading the bundle to the remote directory '/u/ThisDoesNotExist/InjectedBundleId_1.0.0'. Problem is: Injected upload error");
338+
339+
expect(zosMFSpy).toHaveBeenCalledTimes(1);
340+
expect(sshSpy).toHaveBeenCalledTimes(1);
341+
expect(listSpy).toHaveBeenCalledTimes(1);
342+
expect(createSpy).toHaveBeenCalledTimes(1);
343+
expect(shellSpy).toHaveBeenCalledTimes(0);
344+
expect(membersSpy).toHaveBeenCalledTimes(0);
345+
expect(submitSpy).toHaveBeenCalledTimes(0);
346+
expect(existsSpy).toHaveBeenCalledTimes(1);
347+
expect(readSpy).toHaveBeenCalledTimes(1);
348+
expect(uploadSpy).toHaveBeenCalledTimes(1);
349+
});
350+
it("should handle custom bundle id and version", async () => {
351+
uploadSpy.mockImplementationOnce(() => { throw new Error("Injected upload error"); });
352+
readSpy = jest.spyOn(fs, "readFileSync").mockImplementation((data: string) => {
353+
if (data.indexOf("cics.xml") > -1) {
354+
return "<manifest xmlns=\"http://www.ibm.com/xmlns/prod/cics/bundle\" id=\"InjectedBundleId\" " +
355+
" bundleMajorVer=\"33\" bundleMinorVer=\"22\" bundleMicroVer=\"11\"></manifest>";
356+
}
357+
});
358+
359+
await runPushTestWithError("__tests__/__resources__/ExampleBundle01", false,
360+
"A problem occurred uploading the bundle to the remote directory '/u/ThisDoesNotExist/InjectedBundleId_33.22.11'. Problem is: Injected upload error");
361+
362+
expect(zosMFSpy).toHaveBeenCalledTimes(1);
363+
expect(sshSpy).toHaveBeenCalledTimes(1);
364+
expect(listSpy).toHaveBeenCalledTimes(1);
365+
expect(createSpy).toHaveBeenCalledTimes(1);
366+
expect(shellSpy).toHaveBeenCalledTimes(0);
367+
expect(membersSpy).toHaveBeenCalledTimes(0);
368+
expect(submitSpy).toHaveBeenCalledTimes(0);
369+
expect(existsSpy).toHaveBeenCalledTimes(1);
370+
expect(readSpy).toHaveBeenCalledTimes(1);
371+
expect(uploadSpy).toHaveBeenCalledTimes(1);
372+
});
326373
it("should handle error with remote npm install", async () => {
327374
shellSpy.mockImplementation((session: any, cmd: string) => {
328375
if (cmd.indexOf("npm install") > -1) {
@@ -355,6 +402,40 @@ describe("BundlePusher01", () => {
355402
expect(readSpy).toHaveBeenCalledTimes(1);
356403
expect(uploadSpy).toHaveBeenCalledTimes(1);
357404
});
405+
it("should handle failure of remote npm install", async () => {
406+
shellSpy.mockImplementation((session: any, cmd: string, dir: string, stdoutHandler: (data: string) => void) => {
407+
if (cmd.indexOf("npm install") > -1) {
408+
stdoutHandler("Injected stdout error message");
409+
}
410+
else {
411+
return true;
412+
}
413+
});
414+
existsSpy.mockImplementation((data: string) => {
415+
if (data.indexOf(".zosattributes") > -1) {
416+
return false;
417+
}
418+
if (data.indexOf("package.json") > -1) {
419+
return true;
420+
}
421+
});
422+
423+
await runPushTestWithError("__tests__/__resources__/ExampleBundle01", false,
424+
"A problem occurred attempting to run 'npm install' in remote directory '/u/ThisDoesNotExist/12345678'. " +
425+
"Problem is: The output from the remote command implied that an error occurred.");
426+
427+
expect(consoleText).toContain("Injected stdout error message");
428+
expect(zosMFSpy).toHaveBeenCalledTimes(1);
429+
expect(sshSpy).toHaveBeenCalledTimes(1);
430+
expect(createSpy).toHaveBeenCalledTimes(1);
431+
expect(listSpy).toHaveBeenCalledTimes(1);
432+
expect(shellSpy).toHaveBeenCalledTimes(1);
433+
expect(membersSpy).toHaveBeenCalledTimes(0);
434+
expect(submitSpy).toHaveBeenCalledTimes(0);
435+
expect(existsSpy).toHaveBeenCalledTimes(2);
436+
expect(readSpy).toHaveBeenCalledTimes(1);
437+
expect(uploadSpy).toHaveBeenCalledTimes(1);
438+
});
358439
it("should handle error with remote bundle deploy", async () => {
359440
submitSpy.mockImplementationOnce(() => { throw new Error("Injected deploy error"); });
360441

@@ -388,6 +469,41 @@ describe("BundlePusher01", () => {
388469
expect(readSpy).toHaveBeenCalledTimes(1);
389470
expect(uploadSpy).toHaveBeenCalledTimes(1);
390471
});
472+
it("should run to completion with verbose output", async () => {
473+
const parms = getCommonParmsForPushTests();
474+
parms.arguments.verbose = true;
475+
shellSpy.mockImplementation((session: any, cmd: string, dir: string, stdoutHandler: (data: string) => void) => {
476+
stdoutHandler("Injected stdout shell message");
477+
});
478+
existsSpy.mockImplementation((data: string) => {
479+
if (data.indexOf(".zosattributes") > -1) {
480+
return false;
481+
}
482+
if (data.indexOf("package.json") > -1) {
483+
return true;
484+
}
485+
});
486+
487+
await runPushTest("__tests__/__resources__/ExampleBundle01", false, "PUSH operation completed.", parms);
488+
489+
expect(consoleText).toContain("Making remote bundle directory");
490+
expect(consoleText).toContain("Accessing contents of remote bundle directory");
491+
expect(consoleText).toContain("Uploading the bundle to the remote bundle directory");
492+
expect(consoleText).toContain("Running npm install for the remote bundle");
493+
expect(consoleText).toContain("Injected stdout shell message");
494+
expect(consoleText).toContain("Deploying the bundle to CICS");
495+
expect(consoleText).toContain("Deployed existing bundle to CICS");
496+
expect(zosMFSpy).toHaveBeenCalledTimes(1);
497+
expect(sshSpy).toHaveBeenCalledTimes(1);
498+
expect(listSpy).toHaveBeenCalledTimes(1);
499+
expect(createSpy).toHaveBeenCalledTimes(1);
500+
expect(shellSpy).toHaveBeenCalledTimes(1);
501+
expect(membersSpy).toHaveBeenCalledTimes(2);
502+
expect(submitSpy).toHaveBeenCalledTimes(1);
503+
expect(existsSpy).toHaveBeenCalledTimes(2);
504+
expect(readSpy).toHaveBeenCalledTimes(1);
505+
expect(uploadSpy).toHaveBeenCalledTimes(1);
506+
});
391507
});
392508

393509
async function runPushTestWithError(localBundleDir: string, overwrite: boolean, errorText: string, parmsIn?: IHandlerParameters) {
@@ -411,8 +527,14 @@ async function runPushTestWithError(localBundleDir: string, overwrite: boolean,
411527
expect(err.message).toContain(errorText);
412528
}
413529

414-
async function runPushTest(localBundleDir: string, overwrite: boolean, expectedResponse: string) {
415-
const parms = getCommonParmsForPushTests();
530+
async function runPushTest(localBundleDir: string, overwrite: boolean, expectedResponse: string, parmsIn?: IHandlerParameters) {
531+
let parms: IHandlerParameters;
532+
if (parmsIn === undefined) {
533+
parms = getCommonParmsForPushTests();
534+
}
535+
else {
536+
parms = parmsIn;
537+
}
416538
parms.arguments.overwrite = overwrite;
417539

418540
let err: Error;

src/api/BundleContent/Bundle.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ export class Bundle {
110110
this.manifest.setBundleId(id);
111111
}
112112

113+
/**
114+
* Returns the Bundle's version value.
115+
* @returns {string}
116+
* @throws ImperativeError
117+
* @memberof Bundle
118+
*/
119+
public getVersion(): string {
120+
return this.manifest.getBundleVersion();
121+
}
122+
113123
/**
114124
* Set the Bundle's version number.
115125
* @param {number} majorVersion - The major version number.
@@ -245,12 +255,18 @@ export class Bundle {
245255
}
246256

247257
const contents = "" +
258+
"#z/OS File Attributes Document\n" +
259+
"#-----------------------------\n" +
260+
"# This document specfies the encodings for the files within\n" +
261+
"# the project in a form that is compatible with the\n" +
262+
"# 'Zowe files upload dir-to-uss' command.\n" +
263+
"#\n" +
248264
"# Don't upload node_modules\n" +
249265
"node_modules -\n" +
250266
"# Don't upload things that start with dots\n" +
251267
".* -\n" +
252268
"\n" +
253-
"# Upload images in binary\n" +
269+
"# Upload the following file types in binary\n" +
254270
"*.jpg binary binary\n" +
255271
"*.png binary binary\n" +
256272
"*.gif binary binary\n" +

src/api/BundleContent/Manifest.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,29 @@ export class Manifest {
237237
this.manifestAsJson.manifest.id = mangledId;
238238
}
239239

240+
/**
241+
* Returns the Bundle's version number.
242+
*
243+
* @returns {string}
244+
* @throws ImperativeError
245+
* @memberof Manifest
246+
*/
247+
public getBundleVersion(): string {
248+
if (this.manifestAsJson.manifest.bundleMajorVer === undefined) {
249+
this.manifestAsJson.manifest.bundleMajorVer = 1;
250+
}
251+
if (this.manifestAsJson.manifest.bundleMinorVer === undefined) {
252+
this.manifestAsJson.manifest.bundleMinorVer = 0;
253+
}
254+
if (this.manifestAsJson.manifest.bundleMicroVer === undefined) {
255+
this.manifestAsJson.manifest.bundleMicroVer = 0;
256+
}
257+
258+
return this.manifestAsJson.manifest.bundleMajorVer + "." +
259+
this.manifestAsJson.manifest.bundleMinorVer + "." +
260+
this.manifestAsJson.manifest.bundleMicroVer;
261+
}
262+
240263
/**
241264
* Set the Bundle's version number.
242265
* @param {number} majorVersion - The major version number.

src/api/BundleDeploy/BundleDeployer.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,17 @@ export class BundleDeployer {
343343
}
344344
}
345345

346+
// If we haven't found SYSTSPRT then echo JESMSGLG instead
347+
for (const file of spoolOutput) {
348+
if (file.ddName === "JESMSGLG") {
349+
status.stageName = TaskStage.FAILED;
350+
// log the error output to the console
351+
this.params.response.console.log(file.data);
352+
throw new Error("DFHDPLOY command completed in error without generating SYSTSPRT output.");
353+
}
354+
}
355+
346356
status.stageName = TaskStage.FAILED;
347-
throw new Error("SYSTSPRT output from DFHDPLOY not found. Most recent status update: '" + status.statusMessage + "'.");
357+
throw new Error("SYSTSPRT and JESMSGLG output from DFHDPLOY not found. Most recent status update: '" + status.statusMessage + "'.");
348358
}
349359
}

src/api/BundlePush/BundlePusher.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class BundlePusher {
4444
this.localDirectory = localDirectory;
4545
this.validateParameters();
4646

47-
// Construct a bundledir from the targetdir and bundle name
47+
// Set an initial bundledir value for validation purposes (we'll replace it with a better value shortly)
4848
this.params.arguments.bundledir = this.path.posix.join(this.params.arguments.targetdir, this.params.arguments.name);
4949
}
5050

@@ -59,6 +59,12 @@ export class BundlePusher {
5959
const bundle = new Bundle(this.localDirectory, true, true);
6060
bundle.validate();
6161

62+
// If the bundle has an id, use it in the target directory name
63+
if (bundle.getId() !== undefined) {
64+
this.params.arguments.bundledir = this.path.posix.join(this.params.arguments.targetdir, bundle.getId()) +
65+
"_" + bundle.getVersion();
66+
}
67+
6268
// Create a zOSMF session
6369
const zosMFSession = await this.createZosMFSession();
6470
// Create an SSH session
@@ -243,6 +249,11 @@ export class BundlePusher {
243249
}
244250

245251
private sshOutput(data: string) {
252+
// If verbose output is requested then log SSH output directly to the console
253+
if (this.params.arguments.verbose) {
254+
this.params.response.console.log(data);
255+
}
256+
246257
this.sshOutputText += data;
247258
}
248259

@@ -287,6 +298,17 @@ export class BundlePusher {
287298
try {
288299
this.sshOutputText = "";
289300
const shell = await Shell.executeSshCwd(sshSession, sshCommand, directory, this.sshOutput.bind(this));
301+
302+
// Did the SSH command work? It's unclear how to tell, but for starters let's look for the word
303+
// 'error' in the output text.
304+
if (this.sshOutputText.toUpperCase().indexOf("ERROR ") > -1) {
305+
// if we've not already logged the output, log it now
306+
if (this.params.arguments.verbose !== true)
307+
{
308+
this.params.response.console.log(this.sshOutputText);
309+
}
310+
throw new Error("The output from the remote command implied that an error occurred.");
311+
}
290312
}
291313
catch (error) {
292314
throw new Error("A problem occurred attempting to run '" + sshCommand + "' in remote directory '" + directory +
@@ -330,7 +352,7 @@ export class BundlePusher {
330352
this.progressBar.statusMessage = status;
331353

332354
if (this.params.arguments.verbose) {
333-
this.params.response.console.log(status);
355+
this.params.response.console.log(status + "\n");
334356
}
335357
}
336358
}

0 commit comments

Comments
 (0)