@sasjs/cli 6.8.1 → 6.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/flow/execute.d.ts +38 -0
- package/build/commands/flow/execute.js +80 -1
- package/build/commands/flow/flowCommand.d.ts +2 -2
- package/build/commands/flow/flowCommand.js +14 -1
- package/build/commands/flow/spec/flow.spec.js +103 -3
- package/build/commands/flow/spec/flowCommand.spec.js +49 -0
- package/build/commands/fs/fsCommand.js +13 -7
- package/build/commands/fs/spec/fsCommand.spec.js +120 -0
- package/build/commands/job/internal/execute/viya.js +60 -41
- package/build/commands/job/spec/execute.spec.js +88 -0
- package/build/types/command/returnCode.d.ts +2 -1
- package/build/types/command/returnCode.js +5 -1
- package/build/utils/logcheck.d.ts +12 -1
- package/build/utils/logcheck.js +72 -35
- package/build/utils/spec/logcheck.spec.js +106 -0
- package/package.json +1 -1
|
@@ -1,3 +1,41 @@
|
|
|
1
1
|
import { AuthConfig, Target } from '@sasjs/utils';
|
|
2
2
|
import SASjs from '@sasjs/adapter/node';
|
|
3
|
+
import { FlowWave } from '../../types';
|
|
4
|
+
/**
|
|
5
|
+
* A flow that finished without every job succeeding.
|
|
6
|
+
*
|
|
7
|
+
* It carries the jobs that did not, so the command can say which ones rather
|
|
8
|
+
* than reporting a JavaScript fault two frames later - which is what happened
|
|
9
|
+
* when `execute()` resolved `false` instead and the command called `.replace`
|
|
10
|
+
* on it.
|
|
11
|
+
*
|
|
12
|
+
* The marker flag rather than `instanceof`: this package compiles to es5, where
|
|
13
|
+
* a subclass of a built-in loses its prototype chain and `instanceof` answers
|
|
14
|
+
* false in the built artefact.
|
|
15
|
+
*/
|
|
16
|
+
export declare class FlowExecutionError extends Error {
|
|
17
|
+
readonly flowFailure = true;
|
|
18
|
+
readonly csvFilePath?: string;
|
|
19
|
+
/**
|
|
20
|
+
* @param message what did not succeed.
|
|
21
|
+
* @param csvFilePath the flow results file, which records every job and its
|
|
22
|
+
* outcome. It is the diagnostic for a failed run, so it belongs in the
|
|
23
|
+
* message: without it the failure path says less than the success path,
|
|
24
|
+
* which prints the same path.
|
|
25
|
+
*/
|
|
26
|
+
constructor(message: string, csvFilePath?: string);
|
|
27
|
+
}
|
|
28
|
+
export declare const isFlowFailure: (error: unknown) => error is FlowExecutionError;
|
|
29
|
+
/**
|
|
30
|
+
* Names the jobs that did not succeed, so the message a pipeline sees is about
|
|
31
|
+
* the flow rather than about the code that reported it.
|
|
32
|
+
*
|
|
33
|
+
* A job with no status never ran - its flow was failed by a predecessor - and is
|
|
34
|
+
* left out. The test is against `undefined` rather than truthiness: a status
|
|
35
|
+
* constant added later that happens to be falsy would otherwise be dropped from
|
|
36
|
+
* the message, and the message is the whole diagnosis.
|
|
37
|
+
*/
|
|
38
|
+
export declare const describeFailedJobs: (flows: {
|
|
39
|
+
[key: string]: FlowWave;
|
|
40
|
+
}) => string;
|
|
3
41
|
export declare function execute(target: Target, sasjs: SASjs, authConfig: AuthConfig, source: string, logFolder?: string, csvFile?: string, streamLog?: boolean): Promise<unknown>;
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __extends = (this && this.__extends) || (function () {
|
|
3
|
+
var extendStatics = function (d, b) {
|
|
4
|
+
extendStatics = Object.setPrototypeOf ||
|
|
5
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
6
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
7
|
+
return extendStatics(d, b);
|
|
8
|
+
};
|
|
9
|
+
return function (d, b) {
|
|
10
|
+
if (typeof b !== "function" && b !== null)
|
|
11
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
12
|
+
extendStatics(d, b);
|
|
13
|
+
function __() { this.constructor = d; }
|
|
14
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
15
|
+
};
|
|
16
|
+
})();
|
|
2
17
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
18
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
19
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -50,10 +65,71 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
50
65
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
66
|
};
|
|
52
67
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
68
|
+
exports.describeFailedJobs = exports.isFlowFailure = exports.FlowExecutionError = void 0;
|
|
53
69
|
exports.execute = execute;
|
|
54
70
|
var displayResult_1 = require("../../utils/displayResult");
|
|
55
71
|
var examples_1 = __importDefault(require("./internal/examples"));
|
|
56
72
|
var internal_1 = require("./internal");
|
|
73
|
+
var flow_1 = require("../../types/flow");
|
|
74
|
+
/**
|
|
75
|
+
* A flow that finished without every job succeeding.
|
|
76
|
+
*
|
|
77
|
+
* It carries the jobs that did not, so the command can say which ones rather
|
|
78
|
+
* than reporting a JavaScript fault two frames later - which is what happened
|
|
79
|
+
* when `execute()` resolved `false` instead and the command called `.replace`
|
|
80
|
+
* on it.
|
|
81
|
+
*
|
|
82
|
+
* The marker flag rather than `instanceof`: this package compiles to es5, where
|
|
83
|
+
* a subclass of a built-in loses its prototype chain and `instanceof` answers
|
|
84
|
+
* false in the built artefact.
|
|
85
|
+
*/
|
|
86
|
+
var FlowExecutionError = /** @class */ (function (_super) {
|
|
87
|
+
__extends(FlowExecutionError, _super);
|
|
88
|
+
/**
|
|
89
|
+
* @param message what did not succeed.
|
|
90
|
+
* @param csvFilePath the flow results file, which records every job and its
|
|
91
|
+
* outcome. It is the diagnostic for a failed run, so it belongs in the
|
|
92
|
+
* message: without it the failure path says less than the success path,
|
|
93
|
+
* which prints the same path.
|
|
94
|
+
*/
|
|
95
|
+
function FlowExecutionError(message, csvFilePath) {
|
|
96
|
+
var _this = _super.call(this, csvFilePath
|
|
97
|
+
? "".concat(message, " The flow results were written to ").concat(csvFilePath, ".")
|
|
98
|
+
: message) || this;
|
|
99
|
+
_this.flowFailure = true;
|
|
100
|
+
_this.name = 'FlowExecutionError';
|
|
101
|
+
_this.csvFilePath = csvFilePath;
|
|
102
|
+
return _this;
|
|
103
|
+
}
|
|
104
|
+
return FlowExecutionError;
|
|
105
|
+
}(Error));
|
|
106
|
+
exports.FlowExecutionError = FlowExecutionError;
|
|
107
|
+
var isFlowFailure = function (error) {
|
|
108
|
+
return (error === null || error === void 0 ? void 0 : error.flowFailure) === true;
|
|
109
|
+
};
|
|
110
|
+
exports.isFlowFailure = isFlowFailure;
|
|
111
|
+
/**
|
|
112
|
+
* Names the jobs that did not succeed, so the message a pipeline sees is about
|
|
113
|
+
* the flow rather than about the code that reported it.
|
|
114
|
+
*
|
|
115
|
+
* A job with no status never ran - its flow was failed by a predecessor - and is
|
|
116
|
+
* left out. The test is against `undefined` rather than truthiness: a status
|
|
117
|
+
* constant added later that happens to be falsy would otherwise be dropped from
|
|
118
|
+
* the message, and the message is the whole diagnosis.
|
|
119
|
+
*/
|
|
120
|
+
var describeFailedJobs = function (flows) {
|
|
121
|
+
var failed = Object.values(flows).flatMap(function (flow) {
|
|
122
|
+
return flow.jobs
|
|
123
|
+
.filter(function (job) {
|
|
124
|
+
return job.status !== undefined && job.status !== flow_1.FlowWaveJobStatus.Success;
|
|
125
|
+
})
|
|
126
|
+
.map(function (job) { return "".concat(job.location, " (").concat(job.status, ")"); });
|
|
127
|
+
});
|
|
128
|
+
return failed.length
|
|
129
|
+
? "The flow did not complete with all jobs successful: ".concat(failed.join(', '), ".")
|
|
130
|
+
: 'The flow did not complete with all jobs successful.';
|
|
131
|
+
};
|
|
132
|
+
exports.describeFailedJobs = describeFailedJobs;
|
|
57
133
|
function execute(target_1, sasjs_1, authConfig_1, source_1, logFolder_1, csvFile_1) {
|
|
58
134
|
return __awaiter(this, arguments, void 0, function (target, sasjs, authConfig, source, logFolder, csvFile, streamLog) {
|
|
59
135
|
var _this = this;
|
|
@@ -146,10 +222,13 @@ function execute(target_1, sasjs_1, authConfig_1, source_1, logFolder_1, csvFile
|
|
|
146
222
|
reject(flowStatus.message);
|
|
147
223
|
_b = (0, internal_1.allFlowsCompleted)(flows), completed = _b.completed, completedWithAllSuccess = _b.completedWithAllSuccess;
|
|
148
224
|
if (completed) {
|
|
225
|
+
// a flow that did not succeed is a failure of the flow, not of the CLI:
|
|
226
|
+
// reject with what did not run, so the command can report it and exit
|
|
227
|
+
// with a code that means "the flow failed"
|
|
149
228
|
if (completedWithAllSuccess)
|
|
150
229
|
resolve(csvFileRealPath);
|
|
151
230
|
else
|
|
152
|
-
|
|
231
|
+
reject(new FlowExecutionError((0, exports.describeFailedJobs)(flows), csvFileRealPath));
|
|
153
232
|
}
|
|
154
233
|
return [2 /*return*/];
|
|
155
234
|
}
|
|
@@ -4,6 +4,6 @@ import { ReturnCode } from '../../types/command';
|
|
|
4
4
|
import { TargetCommand } from '../../types/command/targetCommand';
|
|
5
5
|
export declare class FlowCommand extends TargetCommand {
|
|
6
6
|
constructor(args: string[]);
|
|
7
|
-
execute(): Promise<ReturnCode>;
|
|
8
|
-
executeFlow(target: Target, sasjs: SASjs, authConfig: AuthConfig): Promise<ReturnCode>;
|
|
7
|
+
execute(): Promise<ReturnCode.Success | ReturnCode.InvalidCommand | ReturnCode.InternalError | ReturnCode.LogCheckError | ReturnCode.FlowError>;
|
|
8
|
+
executeFlow(target: Target, sasjs: SASjs, authConfig: AuthConfig): Promise<ReturnCode.Success | ReturnCode.InternalError | ReturnCode.LogCheckError | ReturnCode.FlowError>;
|
|
9
9
|
}
|
|
@@ -67,7 +67,8 @@ var FlowSubCommand;
|
|
|
67
67
|
})(FlowSubCommand || (FlowSubCommand = {}));
|
|
68
68
|
var syntax = 'flow <subCommand> [options]';
|
|
69
69
|
var usage = 'sasjs flow <execute> --source <flowPath> [options]';
|
|
70
|
-
var description = 'Performs operations on flows of jobs
|
|
70
|
+
var description = 'Performs operations on flows of jobs.\n' +
|
|
71
|
+
'Exits non-zero when a flow does not complete: 6 when a job did not succeed, 2 when the flow could not be executed at all.';
|
|
71
72
|
var examples = [
|
|
72
73
|
{
|
|
73
74
|
command: 'sasjs flow execute --source /local/flow.json --logFolder /local/log/folder --csvFile /local/some.csv --target myTarget --streamLog',
|
|
@@ -163,6 +164,14 @@ var FlowCommand = /** @class */ (function (_super) {
|
|
|
163
164
|
return [4 /*yield*/, (0, execute_1.execute)(target, sasjs, authConfig, source, logFolder, csvFile, streamLog)
|
|
164
165
|
.then(function (csvFilePath) {
|
|
165
166
|
var _a;
|
|
167
|
+
// execute() resolves a path or rejects. Anything else is a contract
|
|
168
|
+
// violation, and reporting it as a successful flow would hide it - the
|
|
169
|
+
// shape that used to arrive here was `false` for a failed flow, which is
|
|
170
|
+
// what turned a failure into a TypeError
|
|
171
|
+
if (typeof csvFilePath !== 'string') {
|
|
172
|
+
(0, utils_1.displayError)(new Error("The flow reported completion without a CSV path (resolved ".concat(typeof csvFilePath, ").")), 'An error has occurred when executing this flow.');
|
|
173
|
+
return command_1.ReturnCode.InternalError;
|
|
174
|
+
}
|
|
166
175
|
var pathSepRegExp = new RegExp(path_1.default.sep.replace(/\\/g, '\\\\'), 'g');
|
|
167
176
|
(_a = process.logger) === null || _a === void 0 ? void 0 : _a.info("CSV file located at: ".concat(csvFilePath.replace(pathSepRegExp, '/')));
|
|
168
177
|
return command_1.ReturnCode.Success;
|
|
@@ -170,6 +179,10 @@ var FlowCommand = /** @class */ (function (_super) {
|
|
|
170
179
|
.catch(function (err) {
|
|
171
180
|
if ((0, logcheck_1.isLogCheckFailure)(err))
|
|
172
181
|
return command_1.ReturnCode.LogCheckError;
|
|
182
|
+
if ((0, execute_1.isFlowFailure)(err)) {
|
|
183
|
+
(0, utils_1.displayError)(err, 'The flow did not complete successfully.');
|
|
184
|
+
return command_1.ReturnCode.FlowError;
|
|
185
|
+
}
|
|
173
186
|
(0, utils_1.displayError)(err, 'An error has occurred when executing this flow.');
|
|
174
187
|
return command_1.ReturnCode.InternalError;
|
|
175
188
|
})];
|
|
@@ -207,9 +207,104 @@ describe('sasjs flow', function () {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
}); });
|
|
210
|
+
it('rejects with the failing job rather than resolving a value', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
211
|
+
var flows, error;
|
|
212
|
+
return __generator(this, function (_a) {
|
|
213
|
+
switch (_a.label) {
|
|
214
|
+
case 0:
|
|
215
|
+
flows = {
|
|
216
|
+
flow1: {
|
|
217
|
+
jobs: [{ location: 'goodJob' }, { location: 'badJob' }],
|
|
218
|
+
predecessors: []
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
setupMocks(flows, 'badJob');
|
|
222
|
+
return [4 /*yield*/, executeFlowWrapper().catch(function (thrown) { return thrown; })
|
|
223
|
+
// the marker, not `instanceof`: the built artefact is es5, where an Error
|
|
224
|
+
// subclass loses its prototype chain, so the assertion has to be the one
|
|
225
|
+
// that holds in both places
|
|
226
|
+
];
|
|
227
|
+
case 1:
|
|
228
|
+
error = _a.sent();
|
|
229
|
+
// the marker, not `instanceof`: the built artefact is es5, where an Error
|
|
230
|
+
// subclass loses its prototype chain, so the assertion has to be the one
|
|
231
|
+
// that holds in both places
|
|
232
|
+
expect((0, execute_1.isFlowFailure)(error)).toBe(true);
|
|
233
|
+
expect(error.message).toEqual('The flow did not complete with all jobs successful: badJob (failure).');
|
|
234
|
+
return [2 /*return*/];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}); });
|
|
238
|
+
it('rejects with a flow error a caller can recognise', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
239
|
+
var flows;
|
|
240
|
+
return __generator(this, function (_a) {
|
|
241
|
+
switch (_a.label) {
|
|
242
|
+
case 0:
|
|
243
|
+
flows = {
|
|
244
|
+
flow1: { jobs: [{ location: 'badJob' }], predecessors: [] }
|
|
245
|
+
};
|
|
246
|
+
setupMocks(flows, 'badJob');
|
|
247
|
+
return [4 /*yield*/, expect(executeFlowWrapper()).rejects.toSatisfy(execute_1.isFlowFailure)];
|
|
248
|
+
case 1:
|
|
249
|
+
_a.sent();
|
|
250
|
+
return [2 /*return*/];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}); });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe('FlowExecutionError', function () {
|
|
257
|
+
it('carries the flow results path, and says where they are', function () {
|
|
258
|
+
var error = new execute_1.FlowExecutionError('The flow did not complete with all jobs successful: /b (failure).', '/project/sasjsresults/flow-results.csv');
|
|
259
|
+
expect(error.csvFilePath).toEqual('/project/sasjsresults/flow-results.csv');
|
|
260
|
+
expect(error.message).toEqual('The flow did not complete with all jobs successful: /b (failure). ' +
|
|
261
|
+
'The flow results were written to /project/sasjsresults/flow-results.csv.');
|
|
262
|
+
expect((0, execute_1.isFlowFailure)(error)).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
it('says nothing about a path it was not given', function () {
|
|
265
|
+
expect(new execute_1.FlowExecutionError('The flow failed.').message).toEqual('The flow failed.');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('describeFailedJobs', function () {
|
|
269
|
+
it('names every job that did not succeed, across flows', function () {
|
|
270
|
+
var flows = {
|
|
271
|
+
flow1: {
|
|
272
|
+
jobs: [
|
|
273
|
+
{ location: '/a', status: flow_1.FlowWaveJobStatus.Success },
|
|
274
|
+
{ location: '/b', status: flow_1.FlowWaveJobStatus.Failure }
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
flow2: {
|
|
278
|
+
jobs: [{ location: '/c', status: flow_1.FlowWaveJobStatus.Failure }]
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /b (failure), /c (failure).');
|
|
282
|
+
});
|
|
283
|
+
it('leaves out jobs that did not run', function () {
|
|
284
|
+
var flows = {
|
|
285
|
+
flow1: {
|
|
286
|
+
jobs: [
|
|
287
|
+
{ location: '/never-ran' },
|
|
288
|
+
{ location: '/b', status: flow_1.FlowWaveJobStatus.Failure }
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /b (failure).');
|
|
293
|
+
});
|
|
294
|
+
it('names a job whose status is falsy rather than dropping it', function () {
|
|
295
|
+
// a status constant added later could be falsy; truthiness would silently
|
|
296
|
+
// omit that job from the only diagnosis the caller gets
|
|
297
|
+
var flows = {
|
|
298
|
+
flow1: { jobs: [{ location: '/zero', status: 0 }] }
|
|
299
|
+
};
|
|
300
|
+
expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful: /zero (0).');
|
|
301
|
+
});
|
|
302
|
+
it('states the failure without a list when no job carries a status', function () {
|
|
303
|
+
var flows = { flow1: { jobs: [{ location: '/a' }] } };
|
|
304
|
+
expect((0, execute_1.describeFailedJobs)(flows)).toEqual('The flow did not complete with all jobs successful.');
|
|
210
305
|
});
|
|
211
306
|
});
|
|
212
|
-
var setupMocks = function (flows) {
|
|
307
|
+
var setupMocks = function (flows, failingJob) {
|
|
213
308
|
jest.restoreAllMocks();
|
|
214
309
|
jest.mock('../internal/validateParams');
|
|
215
310
|
jest.mock('../internal/executeFlow');
|
|
@@ -223,9 +318,14 @@ var setupMocks = function (flows) {
|
|
|
223
318
|
jest.spyOn(executeFlowModule, 'executeFlow').mockImplementation(function (flow) { return __awaiter(void 0, void 0, void 0, function () {
|
|
224
319
|
return __generator(this, function (_a) {
|
|
225
320
|
flow.execution = 'started';
|
|
226
|
-
flow.jobs.forEach(function (job) {
|
|
321
|
+
flow.jobs.forEach(function (job) {
|
|
322
|
+
job.status =
|
|
323
|
+
job.location === failingJob
|
|
324
|
+
? flow_1.FlowWaveJobStatus.Failure
|
|
325
|
+
: flow_1.FlowWaveJobStatus.Success;
|
|
326
|
+
});
|
|
227
327
|
return [2 /*return*/, {
|
|
228
|
-
jobStatus:
|
|
328
|
+
jobStatus: !failingJob,
|
|
229
329
|
flowStatus: { terminate: false, message: '' }
|
|
230
330
|
}];
|
|
231
331
|
});
|
|
@@ -95,6 +95,7 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
|
95
95
|
};
|
|
96
96
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
97
97
|
var executeModule = __importStar(require("../execute"));
|
|
98
|
+
var execute_1 = require("../execute");
|
|
98
99
|
var flowCommand_1 = require("../flowCommand");
|
|
99
100
|
var utils_1 = require("@sasjs/utils");
|
|
100
101
|
var configUtils = __importStar(require("../../../utils/config"));
|
|
@@ -264,6 +265,54 @@ describe('FlowCommand', function () {
|
|
|
264
265
|
}
|
|
265
266
|
});
|
|
266
267
|
}); });
|
|
268
|
+
it('should return the flow code when the flow did not succeed', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
269
|
+
var command, _a;
|
|
270
|
+
return __generator(this, function (_b) {
|
|
271
|
+
switch (_b.label) {
|
|
272
|
+
case 0:
|
|
273
|
+
jest
|
|
274
|
+
.spyOn(executeModule, 'execute')
|
|
275
|
+
.mockImplementation(function () {
|
|
276
|
+
return Promise.reject(new execute_1.FlowExecutionError('badJob (failure)'));
|
|
277
|
+
});
|
|
278
|
+
command = new flowCommand_1.FlowCommand(__spreadArray(__spreadArray([], __read(defaultArgs), false), [
|
|
279
|
+
'flow',
|
|
280
|
+
'execute',
|
|
281
|
+
'--source',
|
|
282
|
+
source
|
|
283
|
+
], false));
|
|
284
|
+
_a = expect;
|
|
285
|
+
return [4 /*yield*/, command.execute()];
|
|
286
|
+
case 1:
|
|
287
|
+
_a.apply(void 0, [_b.sent()]).toEqual(command_1.ReturnCode.FlowError);
|
|
288
|
+
return [2 /*return*/];
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}); });
|
|
292
|
+
it('should treat a resolve without a CSV path as an internal error', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
293
|
+
var command, _a;
|
|
294
|
+
return __generator(this, function (_b) {
|
|
295
|
+
switch (_b.label) {
|
|
296
|
+
case 0:
|
|
297
|
+
// execute() resolves a path or rejects; anything else is a contract
|
|
298
|
+
// violation, and must not be reported as a green flow
|
|
299
|
+
jest
|
|
300
|
+
.spyOn(executeModule, 'execute')
|
|
301
|
+
.mockImplementation(function () { return Promise.resolve(false); });
|
|
302
|
+
command = new flowCommand_1.FlowCommand(__spreadArray(__spreadArray([], __read(defaultArgs), false), [
|
|
303
|
+
'flow',
|
|
304
|
+
'execute',
|
|
305
|
+
'--source',
|
|
306
|
+
source
|
|
307
|
+
], false));
|
|
308
|
+
_a = expect;
|
|
309
|
+
return [4 /*yield*/, command.execute()];
|
|
310
|
+
case 1:
|
|
311
|
+
_a.apply(void 0, [_b.sent()]).toEqual(command_1.ReturnCode.InternalError);
|
|
312
|
+
return [2 /*return*/];
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}); });
|
|
267
316
|
it('should log the error and return the error code when execution is unsuccessful', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
268
317
|
var args, command, returnCode;
|
|
269
318
|
return __generator(this, function (_a) {
|
|
@@ -332,17 +332,23 @@ var FSCommand = /** @class */ (function (_super) {
|
|
|
332
332
|
return __generator(this, function (_c) {
|
|
333
333
|
switch (_c.label) {
|
|
334
334
|
case 0: return [4 /*yield*/, (0, utils_1.saveLog)(log, path_1.default.join(outputFolder, 'deploy.log'), '', false)
|
|
335
|
-
//
|
|
336
|
-
// the deploy whose record someone needs to read
|
|
335
|
+
// After the log file is written: a deploy whose log fails the check is
|
|
336
|
+
// the deploy whose record someone needs to read. Fatal, because a
|
|
337
|
+
// deploy that aborted still has to fail the build - reporting it and
|
|
338
|
+
// then printing "successfully deployed" is the bug in sasjs/cli#1535.
|
|
337
339
|
];
|
|
338
340
|
case 1:
|
|
339
341
|
_c.sent();
|
|
340
|
-
//
|
|
341
|
-
// the deploy whose record someone needs to read
|
|
342
|
-
|
|
342
|
+
// After the log file is written: a deploy whose log fails the check is
|
|
343
|
+
// the deploy whose record someone needs to read. Fatal, because a
|
|
344
|
+
// deploy that aborted still has to fail the build - reporting it and
|
|
345
|
+
// then printing "successfully deployed" is the bug in sasjs/cli#1535.
|
|
346
|
+
return [4 /*yield*/, (0, logcheck_1.checkLog)(log, 'fs deploy', { fatal: true })];
|
|
343
347
|
case 2:
|
|
344
|
-
//
|
|
345
|
-
// the deploy whose record someone needs to read
|
|
348
|
+
// After the log file is written: a deploy whose log fails the check is
|
|
349
|
+
// the deploy whose record someone needs to read. Fatal, because a
|
|
350
|
+
// deploy that aborted still has to fail the build - reporting it and
|
|
351
|
+
// then printing "successfully deployed" is the bug in sasjs/cli#1535.
|
|
346
352
|
_c.sent();
|
|
347
353
|
return [2 /*return*/];
|
|
348
354
|
}
|
|
@@ -103,6 +103,8 @@ var FileModule = __importStar(require("@sasjs/utils/file"));
|
|
|
103
103
|
var configUtils = __importStar(require("../../../utils/config"));
|
|
104
104
|
var setConstantsUtils = __importStar(require("../../../utils/setConstants"));
|
|
105
105
|
var executeCodeModule = __importStar(require("../internal/executeCode"));
|
|
106
|
+
var logcheckModule = __importStar(require("../../../utils/logcheck"));
|
|
107
|
+
var logcheck_1 = require("../../../utils/logcheck");
|
|
106
108
|
var defaultArgs = ['node', 'sasjs', 'fs'];
|
|
107
109
|
var target = new utils_1.Target({
|
|
108
110
|
name: 'test',
|
|
@@ -227,6 +229,28 @@ describe('FSCommand', function () {
|
|
|
227
229
|
}
|
|
228
230
|
});
|
|
229
231
|
}); });
|
|
232
|
+
it('should return the logcheck code when the deploy log fails the check', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
233
|
+
var returnCode;
|
|
234
|
+
return __generator(this, function (_a) {
|
|
235
|
+
switch (_a.label) {
|
|
236
|
+
case 0:
|
|
237
|
+
jest
|
|
238
|
+
.spyOn(logcheckModule, 'checkLog')
|
|
239
|
+
.mockImplementation(function () {
|
|
240
|
+
return Promise.reject(new logcheck_1.LogCheckFailure('2 errors'));
|
|
241
|
+
});
|
|
242
|
+
return [4 /*yield*/, executeCommandWrapper([
|
|
243
|
+
'deploy',
|
|
244
|
+
'localFolder',
|
|
245
|
+
'remoteFolder'
|
|
246
|
+
])];
|
|
247
|
+
case 1:
|
|
248
|
+
returnCode = _a.sent();
|
|
249
|
+
expect(returnCode).toEqual(command_1.ReturnCode.LogCheckError);
|
|
250
|
+
return [2 /*return*/];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}); });
|
|
230
254
|
});
|
|
231
255
|
describe('sync', function () {
|
|
232
256
|
beforeEach(function () {
|
|
@@ -335,6 +359,102 @@ describe('FSCommand', function () {
|
|
|
335
359
|
}
|
|
336
360
|
});
|
|
337
361
|
}); });
|
|
362
|
+
it('should return the logcheck code when the sync log fails the check', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
363
|
+
var returnCode;
|
|
364
|
+
return __generator(this, function (_a) {
|
|
365
|
+
switch (_a.label) {
|
|
366
|
+
case 0:
|
|
367
|
+
jest
|
|
368
|
+
.spyOn(logcheckModule, 'checkLog')
|
|
369
|
+
.mockImplementation(function () {
|
|
370
|
+
return Promise.reject(new logcheck_1.LogCheckFailure('1 error'));
|
|
371
|
+
});
|
|
372
|
+
jest.spyOn(FSModule, 'extractHashArray').mockImplementationOnce(function () { return []; });
|
|
373
|
+
return [4 /*yield*/, executeCommandWrapper([
|
|
374
|
+
'sync',
|
|
375
|
+
'-l',
|
|
376
|
+
'localFolder',
|
|
377
|
+
'-r',
|
|
378
|
+
'remoteFolder'
|
|
379
|
+
])];
|
|
380
|
+
case 1:
|
|
381
|
+
returnCode = _a.sent();
|
|
382
|
+
expect(returnCode).toEqual(command_1.ReturnCode.LogCheckError);
|
|
383
|
+
return [2 /*return*/];
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}); });
|
|
387
|
+
});
|
|
388
|
+
describe('log check mode', function () {
|
|
389
|
+
beforeEach(function () {
|
|
390
|
+
process.logger = new utils_1.Logger(utils_1.LogLevel.Off);
|
|
391
|
+
jest.spyOn(process.logger, 'info');
|
|
392
|
+
setupMocks();
|
|
393
|
+
jest
|
|
394
|
+
.spyOn(FSModule, 'generateCompileProgram')
|
|
395
|
+
.mockImplementation(function () { return Promise.resolve(''); });
|
|
396
|
+
jest
|
|
397
|
+
.spyOn(FileModule, 'createFile')
|
|
398
|
+
.mockImplementation(function () { return Promise.resolve(); });
|
|
399
|
+
jest
|
|
400
|
+
.spyOn(executeCodeModule, 'executeCode')
|
|
401
|
+
.mockImplementation(function () { return Promise.resolve({ log: '' }); });
|
|
402
|
+
jest
|
|
403
|
+
.spyOn(logcheckModule, 'checkLog')
|
|
404
|
+
.mockImplementation(function () { return Promise.resolve(); });
|
|
405
|
+
});
|
|
406
|
+
it('should switch the log check on for --logcheck', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
407
|
+
var spy;
|
|
408
|
+
return __generator(this, function (_a) {
|
|
409
|
+
switch (_a.label) {
|
|
410
|
+
case 0:
|
|
411
|
+
spy = jest.spyOn(logcheckModule, 'setLogCheckMode');
|
|
412
|
+
return [4 /*yield*/, executeCommandWrapper([
|
|
413
|
+
'deploy',
|
|
414
|
+
'localFolder',
|
|
415
|
+
'remoteFolder',
|
|
416
|
+
'--logcheck'
|
|
417
|
+
])];
|
|
418
|
+
case 1:
|
|
419
|
+
_a.sent();
|
|
420
|
+
expect(spy).toHaveBeenCalledWith('on');
|
|
421
|
+
return [2 /*return*/];
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}); });
|
|
425
|
+
it('should switch the log check off for --no-logcheck', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
426
|
+
var spy;
|
|
427
|
+
return __generator(this, function (_a) {
|
|
428
|
+
switch (_a.label) {
|
|
429
|
+
case 0:
|
|
430
|
+
spy = jest.spyOn(logcheckModule, 'setLogCheckMode');
|
|
431
|
+
return [4 /*yield*/, executeCommandWrapper([
|
|
432
|
+
'deploy',
|
|
433
|
+
'localFolder',
|
|
434
|
+
'remoteFolder',
|
|
435
|
+
'--no-logcheck'
|
|
436
|
+
])];
|
|
437
|
+
case 1:
|
|
438
|
+
_a.sent();
|
|
439
|
+
expect(spy).toHaveBeenCalledWith('off');
|
|
440
|
+
return [2 /*return*/];
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}); });
|
|
444
|
+
it('should leave the log check on auto when no flag is given', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
445
|
+
var spy;
|
|
446
|
+
return __generator(this, function (_a) {
|
|
447
|
+
switch (_a.label) {
|
|
448
|
+
case 0:
|
|
449
|
+
spy = jest.spyOn(logcheckModule, 'setLogCheckMode');
|
|
450
|
+
return [4 /*yield*/, executeCommandWrapper(['deploy', 'localFolder', 'remoteFolder'])];
|
|
451
|
+
case 1:
|
|
452
|
+
_a.sent();
|
|
453
|
+
expect(spy).toHaveBeenCalledWith('auto');
|
|
454
|
+
return [2 /*return*/];
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}); });
|
|
338
458
|
});
|
|
339
459
|
describe('invalid subCommand', function () {
|
|
340
460
|
it('should return ReturnCode.InvalidCommand when subCommand is unknown', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
@@ -68,7 +68,7 @@ var utils_4 = require("../utils");
|
|
|
68
68
|
*/
|
|
69
69
|
function executeJobViya(sasjs, authConfig, jobPath, target, waitForJob, output, logFile, statusFile, ignoreWarnings, source, streamLog) {
|
|
70
70
|
return __awaiter(this, void 0, void 0, function () {
|
|
71
|
-
var pollOptions, result, startTime, contextName, macroVars, submittedJob, endTime, outputJson, currentDirPath, outputPath, folderPath, parentFolderPath, logObj, logUrl, logCount, logData,
|
|
71
|
+
var pollOptions, result, startTime, contextName, macroVars, submittedJob, endTime, wantsVerdict, outputJson, currentDirPath, outputPath, folderPath, parentFolderPath, error_1, logObj, logUrl, logCount, logData, error_2;
|
|
72
72
|
var _this = this;
|
|
73
73
|
var _a, _b;
|
|
74
74
|
return __generator(this, function (_c) {
|
|
@@ -176,15 +176,18 @@ function executeJobViya(sasjs, authConfig, jobPath, target, waitForJob, output,
|
|
|
176
176
|
_c.sent();
|
|
177
177
|
_c.label = 8;
|
|
178
178
|
case 8:
|
|
179
|
-
if (!(submittedJob && submittedJob.links)) return [3 /*break*/,
|
|
179
|
+
if (!(submittedJob && submittedJob.links)) return [3 /*break*/, 28];
|
|
180
180
|
if (!result)
|
|
181
181
|
result = true;
|
|
182
|
-
|
|
183
|
-
_c.label = 9;
|
|
182
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
184
183
|
case 9:
|
|
185
|
-
_c.
|
|
184
|
+
wantsVerdict = _c.sent();
|
|
185
|
+
if (!output) return [3 /*break*/, 18];
|
|
186
|
+
_c.label = 10;
|
|
187
|
+
case 10:
|
|
188
|
+
_c.trys.push([10, 17, , 18]);
|
|
186
189
|
outputJson = JSON.stringify(submittedJob, null, 2);
|
|
187
|
-
if (!(typeof output === 'string')) return [3 /*break*/,
|
|
190
|
+
if (!(typeof output === 'string')) return [3 /*break*/, 15];
|
|
188
191
|
currentDirPath = path_1.default.isAbsolute(output)
|
|
189
192
|
? ''
|
|
190
193
|
: process.projectDir;
|
|
@@ -195,55 +198,71 @@ function executeJobViya(sasjs, authConfig, jobPath, target, waitForJob, output,
|
|
|
195
198
|
folderPath.pop();
|
|
196
199
|
parentFolderPath = folderPath.join(path_1.default.sep);
|
|
197
200
|
return [4 /*yield*/, (0, utils_2.folderExists)(parentFolderPath)];
|
|
198
|
-
case 10:
|
|
199
|
-
if (!!(_c.sent())) return [3 /*break*/, 12];
|
|
200
|
-
return [4 /*yield*/, (0, utils_2.createFolder)(parentFolderPath)];
|
|
201
201
|
case 11:
|
|
202
|
+
if (!!(_c.sent())) return [3 /*break*/, 13];
|
|
203
|
+
return [4 /*yield*/, (0, utils_2.createFolder)(parentFolderPath)];
|
|
204
|
+
case 12:
|
|
202
205
|
_c.sent();
|
|
203
|
-
_c.label =
|
|
204
|
-
case
|
|
205
|
-
case
|
|
206
|
+
_c.label = 13;
|
|
207
|
+
case 13: return [4 /*yield*/, (0, utils_2.createFile)(outputPath, outputJson)];
|
|
208
|
+
case 14:
|
|
206
209
|
_c.sent();
|
|
207
210
|
(0, displayResult_1.displaySuccess)("Output saved to: ".concat(outputPath));
|
|
208
|
-
return [3 /*break*/,
|
|
209
|
-
case
|
|
211
|
+
return [3 /*break*/, 16];
|
|
212
|
+
case 15:
|
|
210
213
|
if (output) {
|
|
211
214
|
// log job object to the console
|
|
212
215
|
;
|
|
213
216
|
(process.logger || console).log(outputJson);
|
|
214
217
|
}
|
|
215
|
-
_c.label =
|
|
216
|
-
case
|
|
217
|
-
|
|
218
|
+
_c.label = 16;
|
|
219
|
+
case 16:
|
|
220
|
+
result = submittedJob;
|
|
221
|
+
return [3 /*break*/, 18];
|
|
222
|
+
case 17:
|
|
223
|
+
error_1 = _c.sent();
|
|
224
|
+
// handle use case when submitted job can't be parsed
|
|
225
|
+
result = false;
|
|
226
|
+
(0, displayResult_1.displayError)(null, 'An error has occurred when parsing an output of the job.');
|
|
227
|
+
return [3 /*break*/, 18];
|
|
228
|
+
case 18:
|
|
229
|
+
if (!(!pollOptions.streamLog && (Boolean(logFile) || wantsVerdict))) return [3 /*break*/, 27];
|
|
218
230
|
logObj = submittedJob.links.find(function (link) { return link.rel === 'log' && link.method === 'GET'; });
|
|
219
|
-
if (!logObj) return [3 /*break*/,
|
|
231
|
+
if (!logObj) return [3 /*break*/, 26];
|
|
232
|
+
_c.label = 19;
|
|
233
|
+
case 19:
|
|
234
|
+
_c.trys.push([19, 24, , 25]);
|
|
220
235
|
logUrl = target.serverUrl + logObj.href;
|
|
221
236
|
logCount = submittedJob.logStatistics.lineCount;
|
|
222
237
|
return [4 /*yield*/, (0, fetchLogFileContent_1.fetchLogFileContent)(sasjs, authConfig.access_token, logUrl, logCount)];
|
|
223
|
-
case
|
|
238
|
+
case 20:
|
|
224
239
|
logData = _c.sent();
|
|
240
|
+
if (!logFile) return [3 /*break*/, 22];
|
|
225
241
|
return [4 /*yield*/, (0, utils_4.saveLog)(logData, logFile, jobPath)];
|
|
226
|
-
case
|
|
242
|
+
case 21:
|
|
227
243
|
_c.sent();
|
|
228
|
-
|
|
229
|
-
case
|
|
244
|
+
_c.label = 22;
|
|
245
|
+
case 22: return [4 /*yield*/, (0, logcheck_1.checkLog)(typeof logData === 'string' ? logData : (0, utils_1.parseLogLines)(logData), jobPath)];
|
|
246
|
+
case 23:
|
|
230
247
|
_c.sent();
|
|
231
|
-
|
|
232
|
-
case
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// A log-check failure is the verdict the caller asked for, not a parse
|
|
238
|
-
// error: swallowing it here printed "Logcheck failed" and then exited 0,
|
|
239
|
-
// which is the green pipeline the feature exists to prevent.
|
|
240
|
-
if ((0, logcheck_1.isLogCheckFailure)(error_1))
|
|
241
|
-
throw error_1;
|
|
242
|
-
// handle use case when submitted job can't be parsed
|
|
248
|
+
return [3 /*break*/, 25];
|
|
249
|
+
case 24:
|
|
250
|
+
error_2 = _c.sent();
|
|
251
|
+
// the verdict the caller asked for, not a failed fetch
|
|
252
|
+
if ((0, logcheck_1.isLogCheckFailure)(error_2))
|
|
253
|
+
throw error_2;
|
|
243
254
|
result = false;
|
|
244
|
-
(0, displayResult_1.displayError)(null, 'An error has occurred when
|
|
245
|
-
return [3 /*break*/,
|
|
246
|
-
case
|
|
255
|
+
(0, displayResult_1.displayError)(null, 'An error has occurred when retrieving the job log.');
|
|
256
|
+
return [3 /*break*/, 25];
|
|
257
|
+
case 25: return [3 /*break*/, 27];
|
|
258
|
+
case 26:
|
|
259
|
+
// A log was asked for and the job carries none. Staying quiet here is
|
|
260
|
+
// the same "flag that does nothing" this block exists to remove: it
|
|
261
|
+
// reaches the end with the job's own status and exits 0.
|
|
262
|
+
result = false;
|
|
263
|
+
(0, displayResult_1.displayError)(null, 'An error has occurred: the job returned no log to check or save.');
|
|
264
|
+
_c.label = 27;
|
|
265
|
+
case 27:
|
|
247
266
|
// wait for job state and return status(0,1,2) based on it
|
|
248
267
|
if (waitForJob && submittedJob.state) {
|
|
249
268
|
switch (submittedJob.state) {
|
|
@@ -259,13 +278,13 @@ function executeJobViya(sasjs, authConfig, jobPath, target, waitForJob, output,
|
|
|
259
278
|
break;
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
|
-
return [3 /*break*/,
|
|
263
|
-
case
|
|
281
|
+
return [3 /*break*/, 29];
|
|
282
|
+
case 28:
|
|
264
283
|
if (result) {
|
|
265
284
|
(0, utils_3.terminateProcess)(command_1.ReturnCode.InternalError);
|
|
266
285
|
}
|
|
267
|
-
_c.label =
|
|
268
|
-
case
|
|
286
|
+
_c.label = 29;
|
|
287
|
+
case 29:
|
|
269
288
|
(_b = process.logger) === null || _b === void 0 ? void 0 : _b.info("This job was executed in ".concat((endTime - startTime) / 1000, " seconds"));
|
|
270
289
|
return [2 /*return*/, result];
|
|
271
290
|
}
|
|
@@ -82,6 +82,8 @@ var configUtils = __importStar(require("../../../utils/config"));
|
|
|
82
82
|
var logcheckModule = __importStar(require("../../../utils/logcheck"));
|
|
83
83
|
var fetchLogFileContentModule = __importStar(require("../../shared/fetchLogFileContent"));
|
|
84
84
|
var logcheck_1 = require("../../../utils/logcheck");
|
|
85
|
+
var jobUtils = __importStar(require("../internal/utils"));
|
|
86
|
+
var displayResultModule = __importStar(require("../../../utils/displayResult"));
|
|
85
87
|
var execute_1 = require("../internal/execute");
|
|
86
88
|
var mocks_1 = require("./mocks");
|
|
87
89
|
var sasjs = new node_1.default();
|
|
@@ -194,6 +196,92 @@ describe('executeJobViya', function () {
|
|
|
194
196
|
}
|
|
195
197
|
});
|
|
196
198
|
}); });
|
|
199
|
+
it('fetches the log under --logcheck even when no log file was named', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
200
|
+
var saveSpy, fetchSpy, failure;
|
|
201
|
+
return __generator(this, function (_a) {
|
|
202
|
+
switch (_a.label) {
|
|
203
|
+
case 0:
|
|
204
|
+
// otherwise the flag prints nothing and exits 0, which reads as a clean log
|
|
205
|
+
jest.spyOn(logcheckModule, 'logCheckWantsVerdict').mockResolvedValue(true);
|
|
206
|
+
saveSpy = jest.spyOn(jobUtils, 'saveLog').mockResolvedValue(undefined);
|
|
207
|
+
jest.spyOn(sasjs, 'startComputeJob').mockResolvedValue({
|
|
208
|
+
job: {
|
|
209
|
+
state: 'completed',
|
|
210
|
+
links: [{ rel: 'log', method: 'GET', href: '/log' }],
|
|
211
|
+
logStatistics: { lineCount: 5 }
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
fetchSpy = jest
|
|
215
|
+
.spyOn(fetchLogFileContentModule, 'fetchLogFileContent')
|
|
216
|
+
.mockResolvedValue('ERROR: deliberate');
|
|
217
|
+
failure = new logcheck_1.LogCheckFailure('SAS log check failed for test/job');
|
|
218
|
+
jest.spyOn(logcheckModule, 'checkLog').mockRejectedValue(failure);
|
|
219
|
+
return [4 /*yield*/, expect((0, execute_1.executeJobViya)(sasjs, mocks_1.mockAuthConfig, 'test/job', target, true, false, undefined, undefined, false, undefined, false)).rejects.toBe(failure)];
|
|
220
|
+
case 1:
|
|
221
|
+
_a.sent();
|
|
222
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
223
|
+
// the log was fetched for the verdict, not asked for: writing it would
|
|
224
|
+
// leave a file in the project directory that nobody requested
|
|
225
|
+
expect(saveSpy).not.toHaveBeenCalled();
|
|
226
|
+
return [2 /*return*/];
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}); });
|
|
230
|
+
it('names the log fetch, not the job output, when the log cannot be retrieved', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
231
|
+
var checkSpy, displaySpy, result;
|
|
232
|
+
return __generator(this, function (_a) {
|
|
233
|
+
switch (_a.label) {
|
|
234
|
+
case 0:
|
|
235
|
+
jest.spyOn(logcheckModule, 'logCheckWantsVerdict').mockResolvedValue(true);
|
|
236
|
+
jest.spyOn(sasjs, 'startComputeJob').mockResolvedValue({
|
|
237
|
+
job: {
|
|
238
|
+
state: 'completed',
|
|
239
|
+
links: [{ rel: 'log', method: 'GET', href: '/log' }],
|
|
240
|
+
logStatistics: { lineCount: 5 }
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
jest
|
|
244
|
+
.spyOn(fetchLogFileContentModule, 'fetchLogFileContent')
|
|
245
|
+
.mockRejectedValue(new Error('502 Bad Gateway'));
|
|
246
|
+
checkSpy = jest.spyOn(logcheckModule, 'checkLog');
|
|
247
|
+
displaySpy = jest.spyOn(displayResultModule, 'displayError');
|
|
248
|
+
return [4 /*yield*/, (0, execute_1.executeJobViya)(sasjs, mocks_1.mockAuthConfig, 'test/job', target, false, false, undefined, undefined, false, undefined, false)];
|
|
249
|
+
case 1:
|
|
250
|
+
result = _a.sent();
|
|
251
|
+
expect(result).toBe(false);
|
|
252
|
+
expect(checkSpy).not.toHaveBeenCalled();
|
|
253
|
+
expect(displaySpy).toHaveBeenCalledWith(null, 'An error has occurred when retrieving the job log.');
|
|
254
|
+
return [2 /*return*/];
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}); });
|
|
258
|
+
it('fails a verdict asked for from a job that carries no log link', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
259
|
+
var fetchSpy, checkSpy, displaySpy, result;
|
|
260
|
+
return __generator(this, function (_a) {
|
|
261
|
+
switch (_a.label) {
|
|
262
|
+
case 0:
|
|
263
|
+
jest.spyOn(logcheckModule, 'logCheckWantsVerdict').mockResolvedValue(true);
|
|
264
|
+
jest.spyOn(sasjs, 'startComputeJob').mockResolvedValue({
|
|
265
|
+
job: {
|
|
266
|
+
state: 'completed',
|
|
267
|
+
links: [],
|
|
268
|
+
logStatistics: { lineCount: 0 }
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
fetchSpy = jest.spyOn(fetchLogFileContentModule, 'fetchLogFileContent');
|
|
272
|
+
checkSpy = jest.spyOn(logcheckModule, 'checkLog');
|
|
273
|
+
displaySpy = jest.spyOn(displayResultModule, 'displayError');
|
|
274
|
+
return [4 /*yield*/, (0, execute_1.executeJobViya)(sasjs, mocks_1.mockAuthConfig, 'test/job', target, false, false, undefined, undefined, false, undefined, false)];
|
|
275
|
+
case 1:
|
|
276
|
+
result = _a.sent();
|
|
277
|
+
expect(result).toBe(false);
|
|
278
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
279
|
+
expect(checkSpy).not.toHaveBeenCalled();
|
|
280
|
+
expect(displaySpy).toHaveBeenCalledWith(null, 'An error has occurred: the job returned no log to check or save.');
|
|
281
|
+
return [2 /*return*/];
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}); });
|
|
197
285
|
});
|
|
198
286
|
describe('executeJobSasjs', function () {
|
|
199
287
|
beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
@@ -13,6 +13,10 @@ var ReturnCode;
|
|
|
13
13
|
ReturnCode[ReturnCode["AlreadyInitialised"] = 4] = "AlreadyInitialised";
|
|
14
14
|
// `logcheck` found issues at or above the configured failure policy. Kept
|
|
15
15
|
// apart from LintError so a pipeline can tell a code-quality failure from a
|
|
16
|
-
// run
|
|
16
|
+
// run SAS complained about.
|
|
17
17
|
ReturnCode[ReturnCode["LogCheckError"] = 5] = "LogCheckError";
|
|
18
|
+
// A flow finished without every job succeeding. Distinct from InternalError,
|
|
19
|
+
// which also covers a bad source file or an unreachable server, so a pipeline
|
|
20
|
+
// can tell "the flow failed" from "the CLI could not run the flow".
|
|
21
|
+
ReturnCode[ReturnCode["FlowError"] = 6] = "FlowError";
|
|
18
22
|
})(ReturnCode || (exports.ReturnCode = ReturnCode = {}));
|
|
@@ -32,6 +32,15 @@ export declare class LogCheckFailure extends Error {
|
|
|
32
32
|
readonly logCheckFailure = true;
|
|
33
33
|
constructor(message: string);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a log reaching this command has to be fetched at all.
|
|
37
|
+
*
|
|
38
|
+
* Reporting is free once a command owns a log, but a command that never
|
|
39
|
+
* receives one - a Viya job run without `--log` - has to ask for it. It asks
|
|
40
|
+
* when the verdict counts: `--logcheck`, or `logCheck.autoCheck` in the project
|
|
41
|
+
* config. A check nobody asked for is not worth an extra request.
|
|
42
|
+
*/
|
|
43
|
+
export declare const logCheckWantsVerdict: () => Promise<boolean>;
|
|
35
44
|
export declare const isLogCheckFailure: (error: unknown) => error is LogCheckFailure;
|
|
36
45
|
/**
|
|
37
46
|
* Reports the issues in a log a command just received, and throws when the
|
|
@@ -41,4 +50,6 @@ export declare const isLogCheckFailure: (error: unknown) => error is LogCheckFai
|
|
|
41
50
|
* have their own success output, and a check that announces itself on every
|
|
42
51
|
* green run is noise that gets switched off.
|
|
43
52
|
*/
|
|
44
|
-
export declare const checkLog: (logText: string | undefined | null, label: string
|
|
53
|
+
export declare const checkLog: (logText: string | undefined | null, label: string, options?: {
|
|
54
|
+
fatal?: boolean;
|
|
55
|
+
}) => Promise<void>;
|
package/build/utils/logcheck.js
CHANGED
|
@@ -90,7 +90,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
90
90
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
91
|
};
|
|
92
92
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
-
exports.checkLog = exports.isLogCheckFailure = exports.LogCheckFailure = exports.logCheckModeOf = exports.getLogCheckMode = exports.setLogCheckMode = void 0;
|
|
93
|
+
exports.checkLog = exports.isLogCheckFailure = exports.logCheckWantsVerdict = exports.LogCheckFailure = exports.logCheckModeOf = exports.getLogCheckMode = exports.setLogCheckMode = void 0;
|
|
94
94
|
var logs_1 = require("@sasjs/utils/logs");
|
|
95
95
|
var chalk_1 = __importDefault(require("chalk"));
|
|
96
96
|
var collectLogs_1 = require("../commands/logcheck/collectLogs");
|
|
@@ -135,6 +135,36 @@ var LogCheckFailure = /** @class */ (function (_super) {
|
|
|
135
135
|
return LogCheckFailure;
|
|
136
136
|
}(Error));
|
|
137
137
|
exports.LogCheckFailure = LogCheckFailure;
|
|
138
|
+
/**
|
|
139
|
+
* Whether a log reaching this command has to be fetched at all.
|
|
140
|
+
*
|
|
141
|
+
* Reporting is free once a command owns a log, but a command that never
|
|
142
|
+
* receives one - a Viya job run without `--log` - has to ask for it. It asks
|
|
143
|
+
* when the verdict counts: `--logcheck`, or `logCheck.autoCheck` in the project
|
|
144
|
+
* config. A check nobody asked for is not worth an extra request.
|
|
145
|
+
*/
|
|
146
|
+
var logCheckWantsVerdict = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
147
|
+
var _a;
|
|
148
|
+
return __generator(this, function (_b) {
|
|
149
|
+
switch (_b.label) {
|
|
150
|
+
case 0:
|
|
151
|
+
if (mode === 'off')
|
|
152
|
+
return [2 /*return*/, false];
|
|
153
|
+
if (mode === 'on')
|
|
154
|
+
return [2 /*return*/, true];
|
|
155
|
+
_b.label = 1;
|
|
156
|
+
case 1:
|
|
157
|
+
_b.trys.push([1, 3, , 4]);
|
|
158
|
+
return [4 /*yield*/, (0, logCheckConfig_1.readLogCheckConfig)()];
|
|
159
|
+
case 2: return [2 /*return*/, (_b.sent()).autoCheck === true];
|
|
160
|
+
case 3:
|
|
161
|
+
_a = _b.sent();
|
|
162
|
+
return [2 /*return*/, false];
|
|
163
|
+
case 4: return [2 /*return*/];
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}); };
|
|
167
|
+
exports.logCheckWantsVerdict = logCheckWantsVerdict;
|
|
138
168
|
var isLogCheckFailure = function (error) {
|
|
139
169
|
return (error === null || error === void 0 ? void 0 : error.logCheckFailure) === true;
|
|
140
170
|
};
|
|
@@ -147,43 +177,50 @@ exports.isLogCheckFailure = isLogCheckFailure;
|
|
|
147
177
|
* have their own success output, and a check that announces itself on every
|
|
148
178
|
* green run is noise that gets switched off.
|
|
149
179
|
*/
|
|
150
|
-
var checkLog = function (
|
|
151
|
-
var
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
180
|
+
var checkLog = function (logText_1, label_1) {
|
|
181
|
+
var args_1 = [];
|
|
182
|
+
for (var _i = 2; _i < arguments.length; _i++) {
|
|
183
|
+
args_1[_i - 2] = arguments[_i];
|
|
184
|
+
}
|
|
185
|
+
return __awaiter(void 0, __spreadArray([logText_1, label_1], __read(args_1), false), void 0, function (logText, label, options) {
|
|
186
|
+
var config, error_1, _a, issues, suppressed, counts, failing, totals, failure;
|
|
187
|
+
var _b;
|
|
188
|
+
if (options === void 0) { options = {}; }
|
|
189
|
+
return __generator(this, function (_c) {
|
|
190
|
+
switch (_c.label) {
|
|
191
|
+
case 0:
|
|
192
|
+
if (mode === 'off' || !logText)
|
|
193
|
+
return [2 /*return*/];
|
|
194
|
+
_c.label = 1;
|
|
195
|
+
case 1:
|
|
196
|
+
_c.trys.push([1, 3, , 4]);
|
|
197
|
+
return [4 /*yield*/, (0, logCheckConfig_1.readLogCheckConfig)()];
|
|
198
|
+
case 2:
|
|
199
|
+
config = _c.sent();
|
|
200
|
+
return [3 /*break*/, 4];
|
|
201
|
+
case 3:
|
|
202
|
+
error_1 = _c.sent();
|
|
203
|
+
// An automatic check must never break the command it is riding on: a
|
|
204
|
+
// config it cannot read is worth a warning, not a failed run.
|
|
205
|
+
(_b = process.logger) === null || _b === void 0 ? void 0 : _b.warn("Skipping the SAS log check: ".concat(error_1.message));
|
|
157
206
|
return [2 /*return*/];
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return [2 /*return*/];
|
|
171
|
-
case 4:
|
|
172
|
-
_a = (0, collectLogs_1.partitionIssues)((0, logs_1.parseLog)(logText), config.ignoreCodes), issues = _a.issues, suppressed = _a.suppressed;
|
|
173
|
-
if (!issues.length)
|
|
207
|
+
case 4:
|
|
208
|
+
_a = (0, collectLogs_1.partitionIssues)((0, logs_1.parseLog)(logText), config.ignoreCodes), issues = _a.issues, suppressed = _a.suppressed;
|
|
209
|
+
if (!issues.length)
|
|
210
|
+
return [2 /*return*/];
|
|
211
|
+
counts = (0, collectLogs_1.countIssues)(issues);
|
|
212
|
+
failing = options.fatal === true || mode === 'on' || config.autoCheck === true;
|
|
213
|
+
totals = __assign(__assign({}, counts), { suppressed: suppressed.length, logs: 1, skipped: 0 });
|
|
214
|
+
failure = failing ? (0, processLogCheck_1.describeFailure)(totals, config) : undefined;
|
|
215
|
+
renderInline(label, issues, counts, suppressed.length, config, failure);
|
|
216
|
+
if (failure) {
|
|
217
|
+
throw new LogCheckFailure("SAS log check failed for ".concat(label, ": ").concat(failure));
|
|
218
|
+
}
|
|
174
219
|
return [2 /*return*/];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
totals = __assign(__assign({}, counts), { suppressed: suppressed.length, logs: 1, skipped: 0 });
|
|
178
|
-
failure = failing ? (0, processLogCheck_1.describeFailure)(totals, config) : undefined;
|
|
179
|
-
renderInline(label, issues, counts, suppressed.length, config, failure);
|
|
180
|
-
if (failure) {
|
|
181
|
-
throw new LogCheckFailure("SAS log check failed for ".concat(label, ": ").concat(failure));
|
|
182
|
-
}
|
|
183
|
-
return [2 /*return*/];
|
|
184
|
-
}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
185
222
|
});
|
|
186
|
-
}
|
|
223
|
+
};
|
|
187
224
|
exports.checkLog = checkLog;
|
|
188
225
|
/**
|
|
189
226
|
* The inline report: the heading, the issues, and the verdict when there is
|
|
@@ -208,6 +208,19 @@ describe('checkLog', function () {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
}); });
|
|
211
|
+
it('fails a command that cannot succeed on a bad log, whatever the config says', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
212
|
+
return __generator(this, function (_a) {
|
|
213
|
+
switch (_a.label) {
|
|
214
|
+
case 0: return [4 /*yield*/, withProject()];
|
|
215
|
+
case 1:
|
|
216
|
+
_a.sent();
|
|
217
|
+
return [4 /*yield*/, expect((0, logcheck_1.checkLog)(logWithError, 'fs deploy', { fatal: true })).rejects.toBeInstanceOf(logcheck_1.LogCheckFailure)];
|
|
218
|
+
case 2:
|
|
219
|
+
_a.sent();
|
|
220
|
+
return [2 /*return*/];
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}); });
|
|
211
224
|
it('does not fail on a note under the default policy', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
212
225
|
return __generator(this, function (_a) {
|
|
213
226
|
switch (_a.label) {
|
|
@@ -269,6 +282,99 @@ describe('checkLog', function () {
|
|
|
269
282
|
});
|
|
270
283
|
}); });
|
|
271
284
|
});
|
|
285
|
+
describe('logCheckWantsVerdict', function () {
|
|
286
|
+
var originalProjectDir = process.projectDir;
|
|
287
|
+
var withProject = function (logCheck) { return __awaiter(void 0, void 0, void 0, function () {
|
|
288
|
+
var folder;
|
|
289
|
+
return __generator(this, function (_a) {
|
|
290
|
+
switch (_a.label) {
|
|
291
|
+
case 0: return [4 /*yield*/, tempFolder()];
|
|
292
|
+
case 1:
|
|
293
|
+
folder = _a.sent();
|
|
294
|
+
process.projectDir = folder;
|
|
295
|
+
if (!(logCheck !== undefined)) return [3 /*break*/, 3];
|
|
296
|
+
return [4 /*yield*/, fs_1.promises.writeFile(path_1.default.join(folder, 'sasjsconfig.json'), JSON.stringify({ logCheck: logCheck }))];
|
|
297
|
+
case 2:
|
|
298
|
+
_a.sent();
|
|
299
|
+
_a.label = 3;
|
|
300
|
+
case 3: return [2 /*return*/];
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}); };
|
|
304
|
+
afterEach(function () {
|
|
305
|
+
process.projectDir = originalProjectDir;
|
|
306
|
+
(0, logcheck_1.setLogCheckMode)('auto');
|
|
307
|
+
});
|
|
308
|
+
it('is true under --logcheck and false under --no-logcheck', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
309
|
+
var _a, _b;
|
|
310
|
+
return __generator(this, function (_c) {
|
|
311
|
+
switch (_c.label) {
|
|
312
|
+
case 0: return [4 /*yield*/, withProject()];
|
|
313
|
+
case 1:
|
|
314
|
+
_c.sent();
|
|
315
|
+
(0, logcheck_1.setLogCheckMode)('on');
|
|
316
|
+
_a = expect;
|
|
317
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
318
|
+
case 2:
|
|
319
|
+
_a.apply(void 0, [_c.sent()]).toBe(true);
|
|
320
|
+
(0, logcheck_1.setLogCheckMode)('off');
|
|
321
|
+
_b = expect;
|
|
322
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
323
|
+
case 3:
|
|
324
|
+
_b.apply(void 0, [_c.sent()]).toBe(false);
|
|
325
|
+
return [2 /*return*/];
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}); });
|
|
329
|
+
it('follows autoCheck when the mode is auto', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
330
|
+
var _a, _b, _c;
|
|
331
|
+
return __generator(this, function (_d) {
|
|
332
|
+
switch (_d.label) {
|
|
333
|
+
case 0: return [4 /*yield*/, withProject({ autoCheck: true })];
|
|
334
|
+
case 1:
|
|
335
|
+
_d.sent();
|
|
336
|
+
_a = expect;
|
|
337
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
338
|
+
case 2:
|
|
339
|
+
_a.apply(void 0, [_d.sent()]).toBe(true);
|
|
340
|
+
return [4 /*yield*/, withProject()];
|
|
341
|
+
case 3:
|
|
342
|
+
_d.sent();
|
|
343
|
+
_b = expect;
|
|
344
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
345
|
+
case 4:
|
|
346
|
+
_b.apply(void 0, [_d.sent()]).toBe(false);
|
|
347
|
+
return [4 /*yield*/, withProject({ autoCheck: false })];
|
|
348
|
+
case 5:
|
|
349
|
+
_d.sent();
|
|
350
|
+
_c = expect;
|
|
351
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
352
|
+
case 6:
|
|
353
|
+
_c.apply(void 0, [_d.sent()]).toBe(false);
|
|
354
|
+
return [2 /*return*/];
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}); });
|
|
358
|
+
it('asks for no log when the config cannot be read', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
359
|
+
var folder, _a;
|
|
360
|
+
return __generator(this, function (_b) {
|
|
361
|
+
switch (_b.label) {
|
|
362
|
+
case 0: return [4 /*yield*/, tempFolder()];
|
|
363
|
+
case 1:
|
|
364
|
+
folder = _b.sent();
|
|
365
|
+
process.projectDir = folder;
|
|
366
|
+
return [4 /*yield*/, fs_1.promises.writeFile(path_1.default.join(folder, 'sasjsconfig.json'), '{ "logCheck": ')];
|
|
367
|
+
case 2:
|
|
368
|
+
_b.sent();
|
|
369
|
+
_a = expect;
|
|
370
|
+
return [4 /*yield*/, (0, logcheck_1.logCheckWantsVerdict)()];
|
|
371
|
+
case 3:
|
|
372
|
+
_a.apply(void 0, [_b.sent()]).toBe(false);
|
|
373
|
+
return [2 /*return*/];
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}); });
|
|
377
|
+
});
|
|
272
378
|
describe('isLogCheckFailure', function () {
|
|
273
379
|
it('recognises only its own error', function () {
|
|
274
380
|
expect((0, logcheck_1.isLogCheckFailure)(new logcheck_1.LogCheckFailure('x'))).toBe(true);
|
package/package.json
CHANGED