@zohodesk/unit-testing-framework 0.0.30-experimental → 0.0.32-experimental
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 -0
- package/build/src/runner/__tests__/jest-runner.test.js +115 -0
- package/build/src/runner/jest-runner.js +135 -15
- package/build/src/runner/runner-base.js +3 -3
- package/build/src/utils/__tests__/pipeline-summary-directory.test.js +248 -0
- package/build/src/utils/__tests__/pipeline-summary.test.js +473 -0
- package/build/src/utils/pipeline-summary.js +359 -0
- package/package.json +1 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PipelineSummaryGenerator = exports.ERROR_CATEGORIES = void 0;
|
|
7
|
+
exports.createPipelineSummary = createPipelineSummary;
|
|
8
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
9
|
+
var _path = _interopRequireDefault(require("path"));
|
|
10
|
+
var _logger = require("./logger.js");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
/**
|
|
13
|
+
* Error Categories
|
|
14
|
+
* - 'code': Issues in test/application code
|
|
15
|
+
* - 'environment': Environment setup, dependencies, configuration issues
|
|
16
|
+
* - 'flaky': Intermittent test failures
|
|
17
|
+
* - 'tool': Tool execution failures (jest, npm, etc.)
|
|
18
|
+
*/
|
|
19
|
+
const ERROR_CATEGORIES = exports.ERROR_CATEGORIES = {
|
|
20
|
+
CODE: 'code',
|
|
21
|
+
ENVIRONMENT: 'environment',
|
|
22
|
+
FLAKY: 'flaky',
|
|
23
|
+
TOOL: 'tool'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pipeline Summary Generator
|
|
28
|
+
* Creates standardized JSON responses for pipeline stages
|
|
29
|
+
*/
|
|
30
|
+
class PipelineSummaryGenerator {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.stageName = options.stageName || 'unknown';
|
|
33
|
+
this.outputPath = options.outputPath || null;
|
|
34
|
+
this.webUrl = options.webUrl || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a SUCCESS response
|
|
39
|
+
* @param {Object} params - Response parameters
|
|
40
|
+
* @param {string} params.message - Custom message
|
|
41
|
+
* @param {string} params.webUrl - Optional URL override
|
|
42
|
+
* @returns {Object} Success response object
|
|
43
|
+
*/
|
|
44
|
+
success(params = {}) {
|
|
45
|
+
const response = {
|
|
46
|
+
status: 'SUCCESS',
|
|
47
|
+
message: params.message || `${this.stageName} completed successfully`,
|
|
48
|
+
web_url: params.webUrl || this.webUrl || ''
|
|
49
|
+
};
|
|
50
|
+
this._logResponse(response);
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a FAILURE response with error details
|
|
56
|
+
* @param {Object} params - Failure parameters
|
|
57
|
+
* @param {string} params.message - Custom message for the stage
|
|
58
|
+
* @param {string} params.errorCode - Custom error code
|
|
59
|
+
* @param {string} params.errorCategory - One of: 'code', 'environment', 'flaky', 'tool'
|
|
60
|
+
* @param {string} params.errorMessage - Detailed error message
|
|
61
|
+
* @param {Array} params.errors - List of detailed errors
|
|
62
|
+
* @param {Array} params.result - Optional result data
|
|
63
|
+
* @returns {Object} Failure response object
|
|
64
|
+
*/
|
|
65
|
+
failure(params = {}) {
|
|
66
|
+
const {
|
|
67
|
+
message = `${this.stageName} failed`,
|
|
68
|
+
errorCode = 'UNKNOWN_ERROR',
|
|
69
|
+
errorCategory = ERROR_CATEGORIES.TOOL,
|
|
70
|
+
errorMessage = 'An unexpected error occurred',
|
|
71
|
+
errors = [],
|
|
72
|
+
result = []
|
|
73
|
+
} = params;
|
|
74
|
+
if (!this._isValidErrorCategory(errorCategory)) {
|
|
75
|
+
_logger.Logger.warn(`Invalid error category: ${errorCategory}. Using 'tool' as default.`);
|
|
76
|
+
}
|
|
77
|
+
const response = {
|
|
78
|
+
status: 'FAILURE',
|
|
79
|
+
message,
|
|
80
|
+
web_url: this.webUrl || '',
|
|
81
|
+
error: {
|
|
82
|
+
code: errorCode,
|
|
83
|
+
category: errorCategory,
|
|
84
|
+
message: errorMessage,
|
|
85
|
+
errors: Array.isArray(errors) ? errors : [errors]
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
89
|
+
response.result = result;
|
|
90
|
+
}
|
|
91
|
+
this._logResponse(response);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle test failures
|
|
97
|
+
* @param {Object} params - Test failure parameters
|
|
98
|
+
* @param {Object} params.testResults - Jest test results object
|
|
99
|
+
* @param {string} params.message - Custom message
|
|
100
|
+
* @returns {Object} Failure response object
|
|
101
|
+
*/
|
|
102
|
+
testFailure(params = {}) {
|
|
103
|
+
const {
|
|
104
|
+
testResults,
|
|
105
|
+
message
|
|
106
|
+
} = params;
|
|
107
|
+
if (!testResults) {
|
|
108
|
+
return this.failure({
|
|
109
|
+
errorCode: 'TEST_FAILURE',
|
|
110
|
+
errorCategory: ERROR_CATEGORIES.CODE,
|
|
111
|
+
errorMessage: message || 'Tests failed'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const errors = [];
|
|
115
|
+
const failedTests = testResults.testResults || [];
|
|
116
|
+
for (const testFile of failedTests) {
|
|
117
|
+
if (testFile.numFailingTests > 0) {
|
|
118
|
+
for (const assertion of testFile.assertionResults || []) {
|
|
119
|
+
if (assertion.status === 'failed') {
|
|
120
|
+
errors.push({
|
|
121
|
+
test: assertion.fullName || assertion.title,
|
|
122
|
+
file: testFile.name,
|
|
123
|
+
message: assertion.failureMessages?.[0] || 'Test assertion failed'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return this.failure({
|
|
130
|
+
message: message || `${testResults.numFailedTests} test(s) failed out of ${testResults.numTotalTests}`,
|
|
131
|
+
errorCode: 'TEST_FAILURE',
|
|
132
|
+
errorCategory: ERROR_CATEGORIES.CODE,
|
|
133
|
+
errorMessage: `${testResults.numFailedTests} test assertion(s) failed`,
|
|
134
|
+
errors: errors.length > 0 ? errors : ['Test failures detected'],
|
|
135
|
+
result: [{
|
|
136
|
+
totalTests: testResults.numTotalTests,
|
|
137
|
+
failedTests: testResults.numFailedTests,
|
|
138
|
+
passedTests: testResults.numPassedTests,
|
|
139
|
+
skippedTests: testResults.numSkippedTests
|
|
140
|
+
}]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle tool execution errors
|
|
146
|
+
* @param {Object} params - Tool error parameters
|
|
147
|
+
* @param {string} params.toolName - Name of the tool (jest, npm, etc.)
|
|
148
|
+
* @param {Error|string} params.error - Error object or message
|
|
149
|
+
* @param {string} params.exitCode - Exit code from tool
|
|
150
|
+
* @returns {Object} Failure response object
|
|
151
|
+
*/
|
|
152
|
+
toolError(params = {}) {
|
|
153
|
+
const {
|
|
154
|
+
toolName = 'unknown',
|
|
155
|
+
error,
|
|
156
|
+
exitCode
|
|
157
|
+
} = params;
|
|
158
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
159
|
+
const errorStack = error instanceof Error ? error.stack : null;
|
|
160
|
+
const errors = [{
|
|
161
|
+
tool: toolName,
|
|
162
|
+
message: errorMessage,
|
|
163
|
+
exitCode: exitCode || 'unknown'
|
|
164
|
+
}];
|
|
165
|
+
if (errorStack) {
|
|
166
|
+
errors[0].stack = errorStack;
|
|
167
|
+
}
|
|
168
|
+
return this.failure({
|
|
169
|
+
message: `${toolName} execution failed`,
|
|
170
|
+
errorCode: `${toolName.toUpperCase()}_EXECUTION_ERROR`,
|
|
171
|
+
errorCategory: ERROR_CATEGORIES.TOOL,
|
|
172
|
+
errorMessage: `Failed to execute ${toolName}: ${errorMessage}`,
|
|
173
|
+
errors
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle environment errors
|
|
179
|
+
* @param {Object} params - Environment error parameters
|
|
180
|
+
* @param {string} params.issue - Description of the environment issue
|
|
181
|
+
* @param {string} params.requirement - What was required
|
|
182
|
+
* @param {string} params.suggestion - Suggested fix
|
|
183
|
+
* @returns {Object} Failure response object
|
|
184
|
+
*/
|
|
185
|
+
environmentError(params = {}) {
|
|
186
|
+
const {
|
|
187
|
+
issue = 'Environment configuration issue',
|
|
188
|
+
requirement = null,
|
|
189
|
+
suggestion = null
|
|
190
|
+
} = params;
|
|
191
|
+
const errors = [{
|
|
192
|
+
issue,
|
|
193
|
+
...(requirement && {
|
|
194
|
+
requirement
|
|
195
|
+
}),
|
|
196
|
+
...(suggestion && {
|
|
197
|
+
suggestion
|
|
198
|
+
})
|
|
199
|
+
}];
|
|
200
|
+
return this.failure({
|
|
201
|
+
message: 'Environment setup failed',
|
|
202
|
+
errorCode: 'ENV_SETUP_ERROR',
|
|
203
|
+
errorCategory: ERROR_CATEGORIES.ENVIRONMENT,
|
|
204
|
+
errorMessage: issue,
|
|
205
|
+
errors
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handle flaky test failures
|
|
211
|
+
* @param {Object} params - Flaky test parameters
|
|
212
|
+
* @param {Array} params.testNames - Names of flaky tests
|
|
213
|
+
* @param {number} params.attemptNumber - Current attempt number
|
|
214
|
+
* @param {number} params.maxAttempts - Max retry attempts
|
|
215
|
+
* @returns {Object} Failure response object
|
|
216
|
+
*/
|
|
217
|
+
flakyTestFailure(params = {}) {
|
|
218
|
+
const {
|
|
219
|
+
testNames = [],
|
|
220
|
+
attemptNumber = 1,
|
|
221
|
+
maxAttempts = 3,
|
|
222
|
+
message
|
|
223
|
+
} = params;
|
|
224
|
+
return this.failure({
|
|
225
|
+
message: message || `Flaky test failure (attempt ${attemptNumber}/${maxAttempts})`,
|
|
226
|
+
errorCode: 'FLAKY_TEST_FAILURE',
|
|
227
|
+
errorCategory: ERROR_CATEGORIES.FLAKY,
|
|
228
|
+
errorMessage: `${testNames.length} test(s) failed intermittently`,
|
|
229
|
+
errors: testNames.map(name => ({
|
|
230
|
+
test: name,
|
|
231
|
+
type: 'flaky',
|
|
232
|
+
attemptNumber
|
|
233
|
+
})),
|
|
234
|
+
result: [{
|
|
235
|
+
attemptNumber,
|
|
236
|
+
maxAttempts,
|
|
237
|
+
flakyTestCount: testNames.length
|
|
238
|
+
}]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Save response to JSON file
|
|
244
|
+
* @param {Object} response - Response object to save
|
|
245
|
+
* @param {string} outputPath - Optional path override
|
|
246
|
+
* @returns {boolean} Success status
|
|
247
|
+
*/
|
|
248
|
+
saveToFile(response, outputPath = null) {
|
|
249
|
+
const filePath = outputPath || this.outputPath;
|
|
250
|
+
if (!filePath) {
|
|
251
|
+
_logger.Logger.warn('No output path specified for pipeline summary');
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const dir = _path.default.dirname(filePath);
|
|
256
|
+
|
|
257
|
+
// ── Create directory if it doesn't exist ──────────────────────
|
|
258
|
+
if (!_fs.default.existsSync(dir)) {
|
|
259
|
+
try {
|
|
260
|
+
_fs.default.mkdirSync(dir, {
|
|
261
|
+
recursive: true
|
|
262
|
+
});
|
|
263
|
+
_logger.Logger.info(`Created directory: ${dir}`);
|
|
264
|
+
} catch (mkdirErr) {
|
|
265
|
+
if (mkdirErr.code === 'EACCES') {
|
|
266
|
+
_logger.Logger.error(`Permission denied creating directory: ${dir}. ` + `Check folder permissions.`);
|
|
267
|
+
} else if (mkdirErr.code === 'EROFS') {
|
|
268
|
+
_logger.Logger.error(`Read-only file system: ${dir}. ` + `Cannot create directory on read-only filesystem.`);
|
|
269
|
+
} else {
|
|
270
|
+
_logger.Logger.error(`Failed to create directory ${dir}: ${mkdirErr.message}`);
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Verify directory is writable ─────────────────────────────
|
|
277
|
+
try {
|
|
278
|
+
_fs.default.accessSync(dir, _fs.default.constants.W_OK);
|
|
279
|
+
} catch (accessErr) {
|
|
280
|
+
_logger.Logger.error(`Directory not writable: ${dir}. ` + `Check folder permissions.`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Write the summary file ───────────────────────────────────
|
|
285
|
+
const jsonContent = JSON.stringify(response, null, 2);
|
|
286
|
+
_fs.default.writeFileSync(filePath, jsonContent, 'utf-8');
|
|
287
|
+
|
|
288
|
+
// ── Verify file was written ──────────────────────────────────
|
|
289
|
+
if (!_fs.default.existsSync(filePath)) {
|
|
290
|
+
_logger.Logger.error(`File was not created: ${filePath}. ` + `Unknown file system error.`);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
_logger.Logger.info(`✓ Pipeline summary saved to: ${filePath}`);
|
|
294
|
+
return true;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
// ── Handle unexpected errors ─────────────────────────────────
|
|
297
|
+
if (err.code === 'ENOSPC') {
|
|
298
|
+
_logger.Logger.error(`No space left on device. ` + `Cannot save pipeline summary to: ${outputPath || this.outputPath}`);
|
|
299
|
+
} else if (err.code === 'ENOTDIR') {
|
|
300
|
+
_logger.Logger.error(`Invalid path: ${outputPath || this.outputPath}. ` + `A parent component is not a directory.`);
|
|
301
|
+
} else {
|
|
302
|
+
_logger.Logger.error(`Failed to save pipeline summary: ${err.message}. ` + `Path: ${outputPath || this.outputPath}`);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Save response and return both
|
|
310
|
+
* @param {Object} response - Response object
|
|
311
|
+
* @param {string} outputPath - Optional path override
|
|
312
|
+
* @returns {Object} { response, saved: boolean }
|
|
313
|
+
*/
|
|
314
|
+
saveAndReturn(response, outputPath = null) {
|
|
315
|
+
const saved = this.saveToFile(response, outputPath);
|
|
316
|
+
return {
|
|
317
|
+
response,
|
|
318
|
+
saved
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validate error category
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_isValidErrorCategory(category) {
|
|
327
|
+
return Object.values(ERROR_CATEGORIES).includes(category);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Log response for debugging
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
_logResponse(response) {
|
|
335
|
+
if (response.status === 'SUCCESS') {
|
|
336
|
+
_logger.Logger.info(`✓ Stage '${this.stageName}' completed: ${response.message}`);
|
|
337
|
+
} else {
|
|
338
|
+
_logger.Logger.error(`✗ Stage '${this.stageName}' failed: ${response.error.category} - ${response.error.code}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Factory function for quick usage
|
|
345
|
+
* @param {string} stageName - Name of the pipeline stage
|
|
346
|
+
* @param {Object} options - Additional options
|
|
347
|
+
* @returns {PipelineSummaryGenerator}
|
|
348
|
+
*/
|
|
349
|
+
exports.PipelineSummaryGenerator = PipelineSummaryGenerator;
|
|
350
|
+
function createPipelineSummary(stageName, options = {}) {
|
|
351
|
+
return new PipelineSummaryGenerator({
|
|
352
|
+
stageName,
|
|
353
|
+
...options
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Export error categories for external use
|
|
359
|
+
*/
|