@sasjs/cli 6.6.0 → 6.8.0
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/README.md +2 -1
- package/build/commands/flow/flowCommand.js +11 -1
- package/build/commands/flow/internal/executeFlow.js +44 -19
- package/build/commands/flow/internal/saveLog.d.ts +12 -1
- package/build/commands/flow/internal/saveLog.js +2 -2
- package/build/commands/flow/internal/spec/executeFlow.spec.js +44 -1
- package/build/commands/flow/spec/flowCommand.spec.js +44 -0
- package/build/commands/help/help.js +10 -0
- package/build/commands/index.d.ts +1 -0
- package/build/commands/index.js +3 -1
- package/build/commands/job/internal/execute/sas9.js +16 -13
- package/build/commands/job/internal/execute/sasjs.js +12 -9
- package/build/commands/job/internal/execute/viya.js +41 -36
- package/build/commands/job/jobCommand.js +13 -0
- package/build/commands/job/spec/executeLogOrder.spec.d.ts +1 -0
- package/build/commands/job/spec/executeLogOrder.spec.js +119 -0
- package/build/commands/job/spec/jobCommand.spec.js +32 -0
- package/build/commands/logcheck/collectLogs.d.ts +60 -0
- package/build/commands/logcheck/collectLogs.js +386 -0
- package/build/commands/logcheck/logCheckConfig.d.ts +83 -0
- package/build/commands/logcheck/logCheckConfig.js +343 -0
- package/build/commands/logcheck/logcheckCommand.d.ts +6 -0
- package/build/commands/logcheck/logcheckCommand.js +200 -0
- package/build/commands/logcheck/processLogCheck.d.ts +44 -0
- package/build/commands/logcheck/processLogCheck.js +242 -0
- package/build/commands/logcheck/renderLogCheck.d.ts +129 -0
- package/build/commands/logcheck/renderLogCheck.js +424 -0
- package/build/commands/logcheck/spec/collectLogs.spec.d.ts +1 -0
- package/build/commands/logcheck/spec/collectLogs.spec.js +477 -0
- package/build/commands/logcheck/spec/logCheckConfig.spec.d.ts +1 -0
- package/build/commands/logcheck/spec/logCheckConfig.spec.js +384 -0
- package/build/commands/logcheck/spec/logcheckCommand.spec.d.ts +1 -0
- package/build/commands/logcheck/spec/logcheckCommand.spec.js +360 -0
- package/build/commands/logcheck/spec/processLogCheck.spec.d.ts +1 -0
- package/build/commands/logcheck/spec/processLogCheck.spec.js +354 -0
- package/build/commands/logcheck/spec/renderLogCheck.spec.d.ts +1 -0
- package/build/commands/logcheck/spec/renderLogCheck.spec.js +426 -0
- package/build/commands/logcheck/types.d.ts +106 -0
- package/build/commands/logcheck/types.js +14 -0
- package/build/commands/request/request.js +66 -27
- package/build/commands/request/requestCommand.js +11 -1
- package/build/commands/request/spec/requestCommand.spec.js +40 -0
- package/build/commands/run/run.d.ts +1 -3
- package/build/commands/run/run.js +20 -5
- package/build/commands/run/runCommand.js +9 -0
- package/build/commands/run/spec/runCommand.spec.js +50 -0
- package/build/types/command/commandAliases.js +1 -0
- package/build/types/command/commandFactory.js +2 -0
- package/build/types/command/returnCode.d.ts +2 -1
- package/build/types/command/returnCode.js +4 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.js +1 -0
- package/build/utils/logcheck.d.ts +44 -0
- package/build/utils/logcheck.js +224 -0
- package/build/utils/spec/logcheck.spec.d.ts +1 -0
- package/build/utils/spec/logcheck.spec.js +297 -0
- package/package.json +3 -3
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
var renderLogCheck_1 = require("../renderLogCheck");
|
|
15
|
+
var types_1 = require("../types");
|
|
16
|
+
var config = types_1.defaultLogCheckConfig;
|
|
17
|
+
var issue = function (overrides) {
|
|
18
|
+
if (overrides === void 0) { overrides = {}; }
|
|
19
|
+
return (__assign({ line: 11, endLine: 11, column: 4, endColumn: 20, severity: 'error', message: 'Syntax error, expecting one of the following' }, overrides));
|
|
20
|
+
};
|
|
21
|
+
var log = function (overrides) {
|
|
22
|
+
if (overrides === void 0) { overrides = {}; }
|
|
23
|
+
return (__assign({ filePath: '/project/sasjsresults/run.log', displayPath: 'sasjsresults/run.log', issues: [], suppressed: [], counts: { errors: 0, warnings: 0, notes: 0 } }, overrides));
|
|
24
|
+
};
|
|
25
|
+
var summary = function (overrides) {
|
|
26
|
+
if (overrides === void 0) { overrides = {}; }
|
|
27
|
+
return (__assign({ logs: [], skipped: [], missing: [], unreadable: [], usedDefaultInput: false, searched: ['sasjsresults'], totals: {
|
|
28
|
+
errors: 0,
|
|
29
|
+
warnings: 0,
|
|
30
|
+
notes: 0,
|
|
31
|
+
suppressed: 0,
|
|
32
|
+
logs: 0,
|
|
33
|
+
skipped: 0
|
|
34
|
+
} }, overrides));
|
|
35
|
+
};
|
|
36
|
+
var setupLogger = function () {
|
|
37
|
+
var logger = {
|
|
38
|
+
log: jest.fn(),
|
|
39
|
+
table: jest.fn(),
|
|
40
|
+
info: jest.fn(),
|
|
41
|
+
warn: jest.fn(),
|
|
42
|
+
error: jest.fn(),
|
|
43
|
+
success: jest.fn()
|
|
44
|
+
};
|
|
45
|
+
process.logger = logger;
|
|
46
|
+
return {
|
|
47
|
+
logger: logger,
|
|
48
|
+
output: function () { return logger.log.mock.calls.map(function (call) { return call[0]; }).join('\n'); }
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
describe('renderHuman', function () {
|
|
52
|
+
afterEach(function () {
|
|
53
|
+
process.logger = undefined;
|
|
54
|
+
});
|
|
55
|
+
it('prints a file table, the issues and the verdict', function () {
|
|
56
|
+
var output = setupLogger().output;
|
|
57
|
+
(0, renderLogCheck_1.renderHuman)(summary({
|
|
58
|
+
logs: [
|
|
59
|
+
log({
|
|
60
|
+
issues: [issue()],
|
|
61
|
+
counts: { errors: 1, warnings: 0, notes: 0 }
|
|
62
|
+
})
|
|
63
|
+
],
|
|
64
|
+
totals: {
|
|
65
|
+
errors: 1,
|
|
66
|
+
warnings: 0,
|
|
67
|
+
notes: 0,
|
|
68
|
+
suppressed: 0,
|
|
69
|
+
logs: 1,
|
|
70
|
+
skipped: 0
|
|
71
|
+
},
|
|
72
|
+
failure: '1 error'
|
|
73
|
+
}), config);
|
|
74
|
+
var printed = output();
|
|
75
|
+
expect(printed).toContain('SASjs logcheck');
|
|
76
|
+
expect(printed).toContain('sasjsresults/run.log');
|
|
77
|
+
expect(printed).toContain('FAIL');
|
|
78
|
+
expect(printed).toContain('12:5');
|
|
79
|
+
expect(printed).toContain('Syntax error, expecting one of the following');
|
|
80
|
+
expect(printed).toContain('1 error in 1 log');
|
|
81
|
+
expect(printed).toContain('Logcheck failed: 1 error');
|
|
82
|
+
});
|
|
83
|
+
it('says so when the policy is not to fail', function () {
|
|
84
|
+
var output = setupLogger().output;
|
|
85
|
+
(0, renderLogCheck_1.renderHuman)(summary({
|
|
86
|
+
logs: [
|
|
87
|
+
log({
|
|
88
|
+
issues: [issue()],
|
|
89
|
+
counts: { errors: 1, warnings: 0, notes: 0 }
|
|
90
|
+
})
|
|
91
|
+
],
|
|
92
|
+
totals: {
|
|
93
|
+
errors: 1,
|
|
94
|
+
warnings: 0,
|
|
95
|
+
notes: 0,
|
|
96
|
+
suppressed: 0,
|
|
97
|
+
logs: 1,
|
|
98
|
+
skipped: 0
|
|
99
|
+
}
|
|
100
|
+
}), __assign(__assign({}, config), { failOn: 'never' }));
|
|
101
|
+
expect(output()).toContain('Logcheck passed (fail-on: never)');
|
|
102
|
+
});
|
|
103
|
+
it('reports the suppressed issues as a count, with the codes', function () {
|
|
104
|
+
var output = setupLogger().output;
|
|
105
|
+
(0, renderLogCheck_1.renderHuman)(summary({
|
|
106
|
+
logs: [
|
|
107
|
+
log({
|
|
108
|
+
issues: [issue()],
|
|
109
|
+
suppressed: [issue({ code: '49-169' })],
|
|
110
|
+
counts: { errors: 1, warnings: 0, notes: 0 }
|
|
111
|
+
})
|
|
112
|
+
],
|
|
113
|
+
totals: {
|
|
114
|
+
errors: 1,
|
|
115
|
+
warnings: 0,
|
|
116
|
+
notes: 0,
|
|
117
|
+
suppressed: 1,
|
|
118
|
+
logs: 1,
|
|
119
|
+
skipped: 0
|
|
120
|
+
}
|
|
121
|
+
}), __assign(__assign({}, config), { ignoreCodes: ['49-169'] }));
|
|
122
|
+
expect(output()).toContain('1 issue suppressed by message code (49-169)');
|
|
123
|
+
});
|
|
124
|
+
it('names a missing path, and hints when it was the default', function () {
|
|
125
|
+
var output = setupLogger().output;
|
|
126
|
+
(0, renderLogCheck_1.renderHuman)(summary({
|
|
127
|
+
missing: ['sasjsresults'],
|
|
128
|
+
usedDefaultInput: true,
|
|
129
|
+
failure: '1 path not found'
|
|
130
|
+
}), config);
|
|
131
|
+
var printed = output();
|
|
132
|
+
expect(printed).toContain('Path not found: sasjsresults');
|
|
133
|
+
expect(printed).toContain('nothing has run yet, or pass a path');
|
|
134
|
+
expect(printed).not.toContain('No logs found at the path searched.');
|
|
135
|
+
});
|
|
136
|
+
it('reports a folder it could not read, and why', function () {
|
|
137
|
+
var output = setupLogger().output;
|
|
138
|
+
(0, renderLogCheck_1.renderHuman)(summary({
|
|
139
|
+
unreadable: [
|
|
140
|
+
{
|
|
141
|
+
displayPath: 'sasjsresults/locked',
|
|
142
|
+
reason: 'could not be listed (EACCES)'
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
failure: '1 folder could not be read'
|
|
146
|
+
}), config);
|
|
147
|
+
var printed = output();
|
|
148
|
+
expect(printed).toContain('Could not read: sasjsresults/locked');
|
|
149
|
+
expect(printed).toContain('could not be listed (EACCES)');
|
|
150
|
+
expect(printed).toContain('Logcheck failed: 1 folder could not be read');
|
|
151
|
+
});
|
|
152
|
+
it('says when a folder holds nothing to check', function () {
|
|
153
|
+
var output = setupLogger().output;
|
|
154
|
+
(0, renderLogCheck_1.renderHuman)(summary(), config);
|
|
155
|
+
expect(output()).toContain('No logs found at the path searched.');
|
|
156
|
+
});
|
|
157
|
+
it('counts the skipped files, and lists them only when asked', function () {
|
|
158
|
+
var output = setupLogger().output;
|
|
159
|
+
var withSkips = summary({
|
|
160
|
+
logs: [log({ counts: { errors: 0, warnings: 0, notes: 0 } })],
|
|
161
|
+
skipped: [
|
|
162
|
+
{ displayPath: 'sasjsresults/build.log', reason: 'not a SAS log' }
|
|
163
|
+
],
|
|
164
|
+
totals: {
|
|
165
|
+
errors: 0,
|
|
166
|
+
warnings: 0,
|
|
167
|
+
notes: 0,
|
|
168
|
+
suppressed: 0,
|
|
169
|
+
logs: 1,
|
|
170
|
+
skipped: 1
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
(0, renderLogCheck_1.renderHuman)(withSkips, config);
|
|
174
|
+
expect(output()).toContain('1 file skipped');
|
|
175
|
+
expect(output()).not.toContain('SKIPPED FILE');
|
|
176
|
+
var verbose = setupLogger();
|
|
177
|
+
(0, renderLogCheck_1.renderHuman)(withSkips, config, true);
|
|
178
|
+
expect(verbose.output()).toContain('SKIPPED FILE');
|
|
179
|
+
expect(verbose.output()).toContain('sasjsresults/build.log');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('fileFails', function () {
|
|
183
|
+
it('fails a log carrying an error under the default policy', function () {
|
|
184
|
+
expect((0, renderLogCheck_1.fileFails)({ errors: 1, warnings: 0, notes: 0 }, config)).toBe(true);
|
|
185
|
+
expect((0, renderLogCheck_1.fileFails)({ errors: 0, warnings: 4, notes: 4 }, config)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
it('follows the policy for warnings and notes', function () {
|
|
188
|
+
var warning = { errors: 0, warnings: 1, notes: 0 };
|
|
189
|
+
var note = { errors: 0, warnings: 0, notes: 1 };
|
|
190
|
+
expect((0, renderLogCheck_1.fileFails)(warning, __assign(__assign({}, config), { failOn: 'warning' }))).toBe(true);
|
|
191
|
+
expect((0, renderLogCheck_1.fileFails)(warning, __assign(__assign({}, config), { failOn: 'note' }))).toBe(true);
|
|
192
|
+
expect((0, renderLogCheck_1.fileFails)(note, __assign(__assign({}, config), { failOn: 'warning' }))).toBe(false);
|
|
193
|
+
expect((0, renderLogCheck_1.fileFails)(note, __assign(__assign({}, config), { failOn: 'note' }))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it('fails nothing under the never policy', function () {
|
|
196
|
+
expect((0, renderLogCheck_1.fileFails)({ errors: 9, warnings: 9, notes: 9 }, __assign(__assign({}, config), { failOn: 'never' }))).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
it('does not apply the run-wide warnings budget to one log', function () {
|
|
199
|
+
expect((0, renderLogCheck_1.fileFails)({ errors: 0, warnings: 3, notes: 0 }, __assign(__assign({}, config), { maxWarnings: 1 }))).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('columns', function () {
|
|
203
|
+
it('pads each column to the widest cell and underlines the head', function () {
|
|
204
|
+
var lines = (0, renderLogCheck_1.columns)([
|
|
205
|
+
['a.log', '3'],
|
|
206
|
+
['much-longer-name.log', '12']
|
|
207
|
+
], ['FILE', 'ERRORS']);
|
|
208
|
+
expect(lines).toEqual([
|
|
209
|
+
'FILE ERRORS',
|
|
210
|
+
'-------------------- ------',
|
|
211
|
+
'a.log 3',
|
|
212
|
+
'much-longer-name.log 12'
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
it('measures colour codes as no width at all', function () {
|
|
216
|
+
var coloured = '\u001b[31mred\u001b[39m';
|
|
217
|
+
var lines = (0, renderLogCheck_1.columns)([[coloured]], ['HEADER']);
|
|
218
|
+
expect(lines[1]).toEqual('------');
|
|
219
|
+
});
|
|
220
|
+
it('indents every line it returns', function () {
|
|
221
|
+
var lines = (0, renderLogCheck_1.columns)([['a']], ['A'], ' ', ' ');
|
|
222
|
+
expect(lines.every(function (line) { return line.startsWith(' '); })).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('truncate', function () {
|
|
226
|
+
it('leaves a short message alone', function () {
|
|
227
|
+
expect((0, renderLogCheck_1.truncate)('Syntax error')).toEqual('Syntax error');
|
|
228
|
+
});
|
|
229
|
+
it('collapses the whitespace SAS wrapped', function () {
|
|
230
|
+
expect((0, renderLogCheck_1.truncate)('a message\n wrapped by SAS')).toEqual('a message wrapped by SAS');
|
|
231
|
+
});
|
|
232
|
+
it('cuts a long message on a word boundary', function () {
|
|
233
|
+
var message = 'word '.repeat(40).trim();
|
|
234
|
+
var cut = (0, renderLogCheck_1.truncate)(message);
|
|
235
|
+
expect(cut.length).toBeLessThanOrEqual(110);
|
|
236
|
+
expect(cut.endsWith('...')).toBe(true);
|
|
237
|
+
expect(cut).not.toContain(' ');
|
|
238
|
+
expect(message.startsWith(cut.replace('...', '').trim())).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
describe('severityLabel', function () {
|
|
242
|
+
it('states the severity in words', function () {
|
|
243
|
+
expect((0, renderLogCheck_1.severityLabel)('error')).toEqual('error');
|
|
244
|
+
expect((0, renderLogCheck_1.severityLabel)('warning')).toEqual('warning');
|
|
245
|
+
expect((0, renderLogCheck_1.severityLabel)('note')).toEqual('note');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('lineLabel', function () {
|
|
249
|
+
it('is one-based, and names the range a message was wrapped over', function () {
|
|
250
|
+
expect((0, renderLogCheck_1.lineLabel)(issue())).toEqual('12:5');
|
|
251
|
+
expect((0, renderLogCheck_1.lineLabel)(issue({ endLine: 14 }))).toEqual('12-15:5');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('summarise', function () {
|
|
255
|
+
it('states what was found and over how many logs', function () {
|
|
256
|
+
expect((0, renderLogCheck_1.summarise)(summary({
|
|
257
|
+
totals: {
|
|
258
|
+
errors: 2,
|
|
259
|
+
warnings: 1,
|
|
260
|
+
notes: 0,
|
|
261
|
+
suppressed: 0,
|
|
262
|
+
logs: 2,
|
|
263
|
+
skipped: 0
|
|
264
|
+
}
|
|
265
|
+
}), 2)).toEqual('2 errors, 1 warning in 2 logs');
|
|
266
|
+
});
|
|
267
|
+
it('says so when there is nothing to report', function () {
|
|
268
|
+
expect((0, renderLogCheck_1.summarise)(summary({
|
|
269
|
+
totals: {
|
|
270
|
+
errors: 0,
|
|
271
|
+
warnings: 0,
|
|
272
|
+
notes: 0,
|
|
273
|
+
suppressed: 0,
|
|
274
|
+
logs: 1,
|
|
275
|
+
skipped: 0
|
|
276
|
+
}
|
|
277
|
+
}), 1)).toEqual('No issues in 1 log');
|
|
278
|
+
});
|
|
279
|
+
it('names the files it read out of the files it saw', function () {
|
|
280
|
+
expect((0, renderLogCheck_1.summarise)(summary({
|
|
281
|
+
totals: {
|
|
282
|
+
errors: 1,
|
|
283
|
+
warnings: 0,
|
|
284
|
+
notes: 0,
|
|
285
|
+
suppressed: 0,
|
|
286
|
+
logs: 1,
|
|
287
|
+
skipped: 2
|
|
288
|
+
}
|
|
289
|
+
}), 1)).toEqual('1 error in 1 of 3 files');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
describe('toJson', function () {
|
|
293
|
+
it('states line and column from one', function () {
|
|
294
|
+
var json = (0, renderLogCheck_1.toJson)(summary({
|
|
295
|
+
logs: [
|
|
296
|
+
log({
|
|
297
|
+
issues: [issue({ code: '22-322' })],
|
|
298
|
+
counts: { errors: 1, warnings: 0, notes: 0 }
|
|
299
|
+
})
|
|
300
|
+
]
|
|
301
|
+
}), config);
|
|
302
|
+
expect(json.logs[0].issues[0]).toEqual({
|
|
303
|
+
line: 12,
|
|
304
|
+
endLine: 12,
|
|
305
|
+
column: 5,
|
|
306
|
+
endColumn: 21,
|
|
307
|
+
severity: 'error',
|
|
308
|
+
code: '22-322',
|
|
309
|
+
message: 'Syntax error, expecting one of the following'
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
it('leaves out a code an issue does not carry', function () {
|
|
313
|
+
var json = (0, renderLogCheck_1.toJson)(summary({ logs: [log({ issues: [issue()] })] }), config);
|
|
314
|
+
expect(json.logs[0].issues[0]).not.toHaveProperty('code');
|
|
315
|
+
});
|
|
316
|
+
it('lists a folder it could not read', function () {
|
|
317
|
+
var json = (0, renderLogCheck_1.toJson)(summary({
|
|
318
|
+
unreadable: [
|
|
319
|
+
{ displayPath: 'logs/locked', reason: 'could not be listed' }
|
|
320
|
+
]
|
|
321
|
+
}), config);
|
|
322
|
+
expect(json.unreadable).toEqual([
|
|
323
|
+
{ folder: 'logs/locked', reason: 'could not be listed' }
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
it('carries the counts, the policy and the verdict', function () {
|
|
327
|
+
var json = (0, renderLogCheck_1.toJson)(summary({
|
|
328
|
+
logs: [
|
|
329
|
+
log({
|
|
330
|
+
issues: [issue()],
|
|
331
|
+
counts: { errors: 1, warnings: 0, notes: 0 },
|
|
332
|
+
skippedReason: undefined
|
|
333
|
+
})
|
|
334
|
+
],
|
|
335
|
+
skipped: [{ displayPath: 'build.log', reason: 'not a SAS log' }],
|
|
336
|
+
totals: {
|
|
337
|
+
errors: 1,
|
|
338
|
+
warnings: 0,
|
|
339
|
+
notes: 0,
|
|
340
|
+
suppressed: 0,
|
|
341
|
+
logs: 1,
|
|
342
|
+
skipped: 1
|
|
343
|
+
},
|
|
344
|
+
failure: '1 error'
|
|
345
|
+
}), __assign(__assign({}, config), { maxWarnings: 2 }));
|
|
346
|
+
expect(json.summary).toEqual({
|
|
347
|
+
logs: 1,
|
|
348
|
+
skipped: 1,
|
|
349
|
+
errors: 1,
|
|
350
|
+
warnings: 0,
|
|
351
|
+
notes: 0,
|
|
352
|
+
suppressed: 0,
|
|
353
|
+
failed: true,
|
|
354
|
+
failure: '1 error'
|
|
355
|
+
});
|
|
356
|
+
expect(json.config.maxWarnings).toEqual(2);
|
|
357
|
+
expect(json.skipped).toEqual([
|
|
358
|
+
{ file: 'build.log', reason: 'not a SAS log' }
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
describe('toAnnotations', function () {
|
|
363
|
+
it('writes one workflow command per issue, with one-based positions', function () {
|
|
364
|
+
var annotations = (0, renderLogCheck_1.toAnnotations)(summary({
|
|
365
|
+
logs: [
|
|
366
|
+
log({
|
|
367
|
+
issues: [
|
|
368
|
+
issue({ code: '22-322' }),
|
|
369
|
+
issue({ severity: 'warning', message: 'Symbol not resolved' }),
|
|
370
|
+
issue({ severity: 'note', message: 'Division by zero' })
|
|
371
|
+
]
|
|
372
|
+
})
|
|
373
|
+
]
|
|
374
|
+
}));
|
|
375
|
+
expect(annotations).toEqual([
|
|
376
|
+
'::error file=sasjsresults/run.log,line=12,col=5,endLine=12,endColumn=21,title=SAS ERROR 22-322::Syntax error, expecting one of the following',
|
|
377
|
+
'::warning file=sasjsresults/run.log,line=12,col=5,endLine=12,endColumn=21,title=SAS WARNING::Symbol not resolved',
|
|
378
|
+
'::notice file=sasjsresults/run.log,line=12,col=5,endLine=12,endColumn=21,title=SAS NOTE::Division by zero'
|
|
379
|
+
]);
|
|
380
|
+
});
|
|
381
|
+
it('escapes what a workflow command reads as syntax', function () {
|
|
382
|
+
var annotations = (0, renderLogCheck_1.toAnnotations)(summary({
|
|
383
|
+
logs: [
|
|
384
|
+
log({
|
|
385
|
+
displayPath: 'logs/with,comma.log',
|
|
386
|
+
issues: [issue({ message: '100% wrong\nsecond line' })]
|
|
387
|
+
})
|
|
388
|
+
]
|
|
389
|
+
}));
|
|
390
|
+
expect(annotations[0]).toContain('file=logs/with%2Ccomma.log');
|
|
391
|
+
expect(annotations[0]).toContain('::100%25 wrong%0Asecond line');
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
describe('renderLogCheck', function () {
|
|
395
|
+
afterEach(function () {
|
|
396
|
+
jest.restoreAllMocks();
|
|
397
|
+
process.logger = undefined;
|
|
398
|
+
});
|
|
399
|
+
it('writes JSON to stdout, and nothing through the logger', function () {
|
|
400
|
+
var logger = setupLogger().logger;
|
|
401
|
+
var write = jest
|
|
402
|
+
.spyOn(process.stdout, 'write')
|
|
403
|
+
.mockImplementation(function () { return true; });
|
|
404
|
+
(0, renderLogCheck_1.renderLogCheck)(summary(), config, { format: 'json' });
|
|
405
|
+
expect(write).toHaveBeenCalled();
|
|
406
|
+
expect(JSON.parse(String(write.mock.calls[0][0])).tool).toEqual('sasjs logcheck');
|
|
407
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
it('writes annotations to stdout, and nothing when there are none', function () {
|
|
410
|
+
var write = jest
|
|
411
|
+
.spyOn(process.stdout, 'write')
|
|
412
|
+
.mockImplementation(function () { return true; });
|
|
413
|
+
(0, renderLogCheck_1.renderLogCheck)(summary({ logs: [log({ issues: [issue()] })] }), config, {
|
|
414
|
+
format: 'annotations'
|
|
415
|
+
});
|
|
416
|
+
expect(String(write.mock.calls[0][0])).toContain('::error file=');
|
|
417
|
+
write.mockClear();
|
|
418
|
+
(0, renderLogCheck_1.renderLogCheck)(summary(), config, { format: 'annotations' });
|
|
419
|
+
expect(write).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
it('prints the human report by default', function () {
|
|
422
|
+
var output = setupLogger().output;
|
|
423
|
+
(0, renderLogCheck_1.renderLogCheck)(summary(), config);
|
|
424
|
+
expect(output()).toContain('SASjs logcheck');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { LogIssue } from '@sasjs/utils/logs';
|
|
2
|
+
/**
|
|
3
|
+
* When a log check fails. `error` is the default: an ERROR is SAS refusing to
|
|
4
|
+
* do something, and a gate red on day one gets switched off, so the notable
|
|
5
|
+
* notes legacy code produces in bulk are reported but do not fail the run.
|
|
6
|
+
*/
|
|
7
|
+
export type FailOn = 'error' | 'warning' | 'note' | 'never';
|
|
8
|
+
/** How the result is presented. Only `human` carries colour. */
|
|
9
|
+
export type OutputFormat = 'human' | 'json' | 'annotations';
|
|
10
|
+
/**
|
|
11
|
+
* The policy for a log check: what fails it, and what is suppressed. The
|
|
12
|
+
* defaults are set here, a `logCheck` section in `sasjsconfig.json` may
|
|
13
|
+
* override them, and the flags on the command line override both.
|
|
14
|
+
*/
|
|
15
|
+
export interface LogCheckConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Lowest severity that fails the check - see {@link FailOn}.
|
|
18
|
+
*
|
|
19
|
+
* `never` does not disable `maxWarnings`: a budget is a specific
|
|
20
|
+
* instruction and still fails the run when it is exceeded.
|
|
21
|
+
*/
|
|
22
|
+
failOn: FailOn;
|
|
23
|
+
/**
|
|
24
|
+
* Number of warnings tolerated before the check fails, whatever `failOn`
|
|
25
|
+
* says. Undefined means no budget, which is not the same as zero.
|
|
26
|
+
*/
|
|
27
|
+
maxWarnings?: number;
|
|
28
|
+
/**
|
|
29
|
+
* SAS message codes to suppress, e.g. `49-169` for the note about a
|
|
30
|
+
* quoted string being truncated. The code is the stable identifier SAS
|
|
31
|
+
* gives a message, which is what makes it the right thing to key on.
|
|
32
|
+
*/
|
|
33
|
+
ignoreCodes: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Log paths to leave out, as a plain substring (`sasjsresults/legacy`) or a
|
|
36
|
+
* glob (`**/build*.log`). Matched against the path shown in the report.
|
|
37
|
+
*/
|
|
38
|
+
ignorePaths: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Folder to check when no path is given on the command line. Resolved
|
|
41
|
+
* against the project folder.
|
|
42
|
+
*/
|
|
43
|
+
folder?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the commands that receive a SAS log (`run`, `request`, `job
|
|
46
|
+
* execute`, `flow execute`) fail on it as well as reporting it. Off by
|
|
47
|
+
* default: those commands' exit codes are already load-bearing in pipelines,
|
|
48
|
+
* and a job that prints a notable note is not a failed job. `--logcheck` and
|
|
49
|
+
* `--no-logcheck` override it per run.
|
|
50
|
+
*/
|
|
51
|
+
autoCheck?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare const defaultLogCheckConfig: LogCheckConfig;
|
|
54
|
+
/** Counts of the issues a log carries, after suppression. */
|
|
55
|
+
export interface IssueCounts {
|
|
56
|
+
errors: number;
|
|
57
|
+
warnings: number;
|
|
58
|
+
notes: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const emptyCounts: () => IssueCounts;
|
|
61
|
+
/**
|
|
62
|
+
* One log as the check found it. A log that could not be read, is too large,
|
|
63
|
+
* or is not a SAS log carries a `skippedReason` and no issues - it is still
|
|
64
|
+
* part of the report, because a file the user asked about that was quietly
|
|
65
|
+
* dropped is worse than one reported as skipped.
|
|
66
|
+
*/
|
|
67
|
+
export interface ScannedLog {
|
|
68
|
+
/** Absolute path, as it was read. */
|
|
69
|
+
filePath: string;
|
|
70
|
+
/** Path as shown to the user, relative to the project folder where possible. */
|
|
71
|
+
displayPath: string;
|
|
72
|
+
/** Issues that survive the suppression rules. */
|
|
73
|
+
issues: LogIssue[];
|
|
74
|
+
/** Issues removed by `ignoreCodes`, reported only as a count. */
|
|
75
|
+
suppressed: LogIssue[];
|
|
76
|
+
counts: IssueCounts;
|
|
77
|
+
/** Set when the file was not checked at all. */
|
|
78
|
+
skippedReason?: string;
|
|
79
|
+
}
|
|
80
|
+
/** A file the check did not look inside, with the reason it did not. */
|
|
81
|
+
export interface SkippedFile {
|
|
82
|
+
displayPath: string;
|
|
83
|
+
reason: string;
|
|
84
|
+
}
|
|
85
|
+
/** Everything one run of the check produced, before any rendering. */
|
|
86
|
+
export interface LogCheckSummary {
|
|
87
|
+
logs: ScannedLog[];
|
|
88
|
+
skipped: SkippedFile[];
|
|
89
|
+
/** Paths the user named that do not exist. */
|
|
90
|
+
missing: string[];
|
|
91
|
+
/** Folders the check could not list, so their logs were never looked at. */
|
|
92
|
+
unreadable: SkippedFile[];
|
|
93
|
+
/** Whether the paths were defaulted rather than named by the user. */
|
|
94
|
+
usedDefaultInput: boolean;
|
|
95
|
+
/** Folders and files that were searched, as shown in the report. */
|
|
96
|
+
searched: string[];
|
|
97
|
+
totals: IssueCounts & {
|
|
98
|
+
suppressed: number;
|
|
99
|
+
/** Logs actually parsed. */
|
|
100
|
+
logs: number;
|
|
101
|
+
/** Files that were passed over, for whatever reason. */
|
|
102
|
+
skipped: number;
|
|
103
|
+
};
|
|
104
|
+
/** Why the check failed, or undefined when it passed. */
|
|
105
|
+
failure?: string;
|
|
106
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emptyCounts = exports.defaultLogCheckConfig = void 0;
|
|
4
|
+
exports.defaultLogCheckConfig = {
|
|
5
|
+
failOn: 'error',
|
|
6
|
+
ignoreCodes: [],
|
|
7
|
+
ignorePaths: []
|
|
8
|
+
};
|
|
9
|
+
var emptyCounts = function () { return ({
|
|
10
|
+
errors: 0,
|
|
11
|
+
warnings: 0,
|
|
12
|
+
notes: 0
|
|
13
|
+
}); };
|
|
14
|
+
exports.emptyCounts = emptyCounts;
|
|
@@ -35,6 +35,31 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
35
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
39
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
40
|
+
if (!m) return o;
|
|
41
|
+
var i = m.call(o), r, ar = [], e;
|
|
42
|
+
try {
|
|
43
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
44
|
+
}
|
|
45
|
+
catch (error) { e = { error: error }; }
|
|
46
|
+
finally {
|
|
47
|
+
try {
|
|
48
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
49
|
+
}
|
|
50
|
+
finally { if (e) throw e.error; }
|
|
51
|
+
}
|
|
52
|
+
return ar;
|
|
53
|
+
};
|
|
54
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
55
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
56
|
+
if (ar || !(i in from)) {
|
|
57
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
58
|
+
ar[i] = from[i];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
62
|
+
};
|
|
38
63
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
64
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
65
|
};
|
|
@@ -145,7 +170,7 @@ function runSasJob(target, isLocal, sasJobLocation, dataFilePath, configFilePath
|
|
|
145
170
|
case 4:
|
|
146
171
|
_a.sent();
|
|
147
172
|
_a.label = 5;
|
|
148
|
-
case 5: return [4 /*yield*/, saveLogFile(sasjs, sasJobLocation, logFile, jobPath)];
|
|
173
|
+
case 5: return [4 /*yield*/, saveLogFile(sasjs, sasJobLocation, logFile, jobPath, true)];
|
|
149
174
|
case 6:
|
|
150
175
|
_a.sent();
|
|
151
176
|
result = true;
|
|
@@ -170,33 +195,47 @@ function runSasJob(target, isLocal, sasJobLocation, dataFilePath, configFilePath
|
|
|
170
195
|
});
|
|
171
196
|
});
|
|
172
197
|
}
|
|
173
|
-
var saveLogFile = function (
|
|
174
|
-
var
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
var saveLogFile = function (sasjs_1, sasJobLocation_1, logFile_1, jobPath_1) {
|
|
199
|
+
var args_1 = [];
|
|
200
|
+
for (var _i = 4; _i < arguments.length; _i++) {
|
|
201
|
+
args_1[_i - 4] = arguments[_i];
|
|
202
|
+
}
|
|
203
|
+
return __awaiter(void 0, __spreadArray([sasjs_1, sasJobLocation_1, logFile_1, jobPath_1], __read(args_1), false), void 0, function (sasjs, sasJobLocation, logFile, jobPath, check) {
|
|
204
|
+
var sasRequests, currentRequestLog, logPath, timestamp, filename;
|
|
205
|
+
if (check === void 0) { check = false; }
|
|
206
|
+
return __generator(this, function (_a) {
|
|
207
|
+
switch (_a.label) {
|
|
208
|
+
case 0:
|
|
209
|
+
sasRequests = sasjs.getSasRequests();
|
|
210
|
+
currentRequestLog = sasRequests.find(function (x) { return x.serviceLink === sasJobLocation; });
|
|
211
|
+
if (!(currentRequestLog && jobPath)) return [3 /*break*/, 5];
|
|
212
|
+
if (!!logFile) return [3 /*break*/, 2];
|
|
213
|
+
return [4 /*yield*/, (0, utils_2.isSASjsProject)()];
|
|
214
|
+
case 1:
|
|
215
|
+
logPath = (_a.sent())
|
|
216
|
+
? path_1.default.join(process.sasjsConstants.buildDestinationResultsFolder, 'requests')
|
|
217
|
+
: process.projectDir;
|
|
218
|
+
timestamp = (0, utils_1.generateTimestamp)();
|
|
219
|
+
filename = sasJobLocation.split('/').pop() // always use forward slash in SAS Drive
|
|
220
|
+
;
|
|
221
|
+
logFile = path_1.default.join(logPath, "".concat(filename, "-").concat(timestamp, ".log"));
|
|
222
|
+
_a.label = 2;
|
|
223
|
+
case 2: return [4 /*yield*/, (0, utils_2.saveLog)(currentRequestLog.logFile, logFile, jobPath || '', false)
|
|
224
|
+
// after the write, not before: a request that fails the check is the one
|
|
225
|
+
// whose log is wanted on disk
|
|
226
|
+
];
|
|
227
|
+
case 3:
|
|
228
|
+
_a.sent();
|
|
229
|
+
if (!check) return [3 /*break*/, 5];
|
|
230
|
+
return [4 /*yield*/, (0, utils_2.checkLog)(currentRequestLog.logFile, sasJobLocation)];
|
|
231
|
+
case 4:
|
|
232
|
+
_a.sent();
|
|
233
|
+
_a.label = 5;
|
|
234
|
+
case 5: return [2 /*return*/];
|
|
235
|
+
}
|
|
236
|
+
});
|
|
198
237
|
});
|
|
199
|
-
}
|
|
238
|
+
};
|
|
200
239
|
/**
|
|
201
240
|
* Writes output to the file
|
|
202
241
|
* @param outputPathParam path to output file
|