@zohodesk/testinglibrary 0.0.83-n20-experimental → 0.0.85-n20-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/build/core/playwright/builtInFixtures/context.js +27 -7
- package/build/core/playwright/constants/failureTagConstants.js +5 -2
- package/build/core/playwright/constants/logCollectorConstants.js +1 -4
- package/build/core/playwright/helpers/failureTagClassifier.js +176 -76
- package/build/core/playwright/helpers/logCollector.js +10 -4
- package/build/core/playwright/helpers/runtimeLogPath.js +61 -0
- package/build/core/playwright/helpers/runtimeLogReader.js +25 -16
- package/build/core/playwright/patterns/casePatterns.js +11 -0
- package/build/core/playwright/patterns/index.js +7 -0
- package/build/core/playwright/patterns/toolPatterns.js +1 -3
- package/build/core/playwright/setup/qc-custom-reporter.js +15 -3
- package/build/test/core/playwright/helpers/__tests__/failureTagClassifier.test.js +274 -28
- package/build/test/core/playwright/helpers/__tests__/logCollector.test.js +26 -12
- package/build/test/core/playwright/helpers/__tests__/runtimeLogReader.test.js +85 -25
- package/npm-shrinkwrap.json +1191 -1179
- package/package.json +1 -1
- package/unit_reports/unit-report.html +1 -1
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
5
|
+
var _os = _interopRequireDefault(require("os"));
|
|
6
|
+
var _path = _interopRequireDefault(require("path"));
|
|
4
7
|
var toolPatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/toolPatterns"));
|
|
8
|
+
var casePatterns = _interopRequireWildcard(require("../../../../../core/playwright/patterns/casePatterns"));
|
|
9
|
+
var _runtimeLogPath = require("../../../../../core/playwright/helpers/runtimeLogPath");
|
|
5
10
|
var _failureTagClassifier = require("../../../../../core/playwright/helpers/failureTagClassifier");
|
|
11
|
+
var _failureTagConstants = require("../../../../../core/playwright/constants/failureTagConstants");
|
|
6
12
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const TEST_TITLE = 'environment-abort-test';
|
|
14
|
+
let tempDir;
|
|
15
|
+
function writeRuntimeLog(events, {
|
|
16
|
+
workerIndex = 0,
|
|
17
|
+
retry = 0,
|
|
18
|
+
title = TEST_TITLE
|
|
19
|
+
} = {}) {
|
|
20
|
+
const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(tempDir, {
|
|
21
|
+
workerIndex,
|
|
22
|
+
retry,
|
|
23
|
+
title
|
|
24
|
+
});
|
|
25
|
+
_fs.default.mkdirSync(_path.default.dirname(file), {
|
|
26
|
+
recursive: true
|
|
27
|
+
});
|
|
28
|
+
_fs.default.writeFileSync(file, `${events.map(e => JSON.stringify(e)).join('\n')}\n`);
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
function failingDetail(events, options = {}) {
|
|
32
|
+
writeRuntimeLog(events, options);
|
|
16
33
|
return {
|
|
17
34
|
status: 'unexpected',
|
|
18
35
|
results: [{
|
|
19
36
|
status: 'failed',
|
|
20
|
-
|
|
37
|
+
workerIndex: options.workerIndex ?? 0,
|
|
38
|
+
retry: options.retry ?? 0
|
|
21
39
|
}]
|
|
22
40
|
};
|
|
23
41
|
}
|
|
@@ -35,12 +53,21 @@ function failingDetailWithStdio({
|
|
|
35
53
|
}]
|
|
36
54
|
};
|
|
37
55
|
}
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
tempDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'failure-tag-'));
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
_fs.default.rmSync(tempDir, {
|
|
61
|
+
recursive: true,
|
|
62
|
+
force: true
|
|
63
|
+
});
|
|
64
|
+
});
|
|
38
65
|
describe('classifyFailureTags', () => {
|
|
39
66
|
test('returns flaky and skips log analysis for flaky outcome', () => {
|
|
40
67
|
const tags = (0, _failureTagClassifier.classifyFailureTags)([{
|
|
41
68
|
status: 'flaky',
|
|
42
69
|
results: []
|
|
43
|
-
}]);
|
|
70
|
+
}], TEST_TITLE, tempDir);
|
|
44
71
|
expect(tags).toEqual(['FLAKY']);
|
|
45
72
|
});
|
|
46
73
|
test('tags environment when a document abort is present', () => {
|
|
@@ -49,7 +76,7 @@ describe('classifyFailureTags', () => {
|
|
|
49
76
|
url: 'https://app/events/list/commentsubtabevent',
|
|
50
77
|
failure: 'net::ERR_ABORTED',
|
|
51
78
|
resourceType: 'document'
|
|
52
|
-
}])]);
|
|
79
|
+
}])], TEST_TITLE, tempDir);
|
|
53
80
|
expect(tags).toEqual(['ENVIRONMENT']);
|
|
54
81
|
});
|
|
55
82
|
test('tags environment for a network/cert failure', () => {
|
|
@@ -58,7 +85,9 @@ describe('classifyFailureTags', () => {
|
|
|
58
85
|
url: 'https://js/gshandler.js',
|
|
59
86
|
failure: 'net::ERR_CERT_COMMON_NAME_INVALID',
|
|
60
87
|
resourceType: 'script'
|
|
61
|
-
}]
|
|
88
|
+
}], {
|
|
89
|
+
title: 'cert-test'
|
|
90
|
+
})], 'cert-test', tempDir);
|
|
62
91
|
expect(tags).toEqual(['ENVIRONMENT']);
|
|
63
92
|
});
|
|
64
93
|
test('tags code when an app undefined-property error is present', () => {
|
|
@@ -66,7 +95,9 @@ describe('classifyFailureTags', () => {
|
|
|
66
95
|
kind: 'pageerror',
|
|
67
96
|
name: 'TypeError',
|
|
68
97
|
message: "Cannot read properties of undefined (reading 'id')"
|
|
69
|
-
}]
|
|
98
|
+
}], {
|
|
99
|
+
title: 'code-test'
|
|
100
|
+
})], 'code-test', tempDir);
|
|
70
101
|
expect(tags).toEqual(['CODE']);
|
|
71
102
|
});
|
|
72
103
|
test('does not tag a non-document (xhr) abort as environment', () => {
|
|
@@ -75,27 +106,45 @@ describe('classifyFailureTags', () => {
|
|
|
75
106
|
url: 'https://app/api/tickets',
|
|
76
107
|
failure: 'net::ERR_ABORTED',
|
|
77
108
|
resourceType: 'xhr'
|
|
78
|
-
}]
|
|
109
|
+
}], {
|
|
110
|
+
title: 'xhr-test'
|
|
111
|
+
})], 'xhr-test', tempDir);
|
|
79
112
|
expect(tags).toEqual([]);
|
|
80
113
|
});
|
|
81
|
-
test('tags
|
|
114
|
+
test('tags case and code when stderr reports DATA_GENERATION_ERROR', () => {
|
|
82
115
|
const detail = failingDetailWithStdio({
|
|
83
116
|
stderr: [{
|
|
84
117
|
text: 'status : DATA_GENERATION_ERROR\n generator: comments'
|
|
85
118
|
}]
|
|
86
119
|
});
|
|
87
|
-
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
88
|
-
expect(tags).toEqual(['
|
|
120
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
121
|
+
expect(tags).toEqual(['CASE', 'CODE']);
|
|
89
122
|
});
|
|
90
|
-
test('tags tool for
|
|
123
|
+
test('tags tool for a known tool pattern in base64 stdout', () => {
|
|
91
124
|
const detail = failingDetailWithStdio({
|
|
92
125
|
stdout: [{
|
|
93
|
-
buffer: Buffer.from('Error
|
|
126
|
+
buffer: Buffer.from('Error during login').toString('base64')
|
|
94
127
|
}]
|
|
95
128
|
});
|
|
96
|
-
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
129
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
97
130
|
expect(tags).toEqual(['TOOL']);
|
|
98
131
|
});
|
|
132
|
+
test('tags case and code when stderr matches a case pattern', () => {
|
|
133
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
134
|
+
try {
|
|
135
|
+
casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
|
|
136
|
+
const detail = failingDetailWithStdio({
|
|
137
|
+
stderr: [{
|
|
138
|
+
text: 'Error: expect(received).toBe(expected)'
|
|
139
|
+
}]
|
|
140
|
+
});
|
|
141
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
142
|
+
expect(tags).toEqual(['CASE', 'CODE']);
|
|
143
|
+
} finally {
|
|
144
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
145
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
99
148
|
test('does not tag tool for benign stdout/stderr', () => {
|
|
100
149
|
const detail = failingDetailWithStdio({
|
|
101
150
|
stdout: [{
|
|
@@ -103,7 +152,7 @@ describe('classifyFailureTags', () => {
|
|
|
103
152
|
}],
|
|
104
153
|
stderr: []
|
|
105
154
|
});
|
|
106
|
-
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
155
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
107
156
|
expect(tags).toEqual([]);
|
|
108
157
|
});
|
|
109
158
|
test('does not throw for malformed stdio chunk entries', () => {
|
|
@@ -113,8 +162,8 @@ describe('classifyFailureTags', () => {
|
|
|
113
162
|
}],
|
|
114
163
|
stderr: [false]
|
|
115
164
|
});
|
|
116
|
-
expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail])).not.toThrow();
|
|
117
|
-
expect((0, _failureTagClassifier.classifyFailureTags)([detail])).toEqual([]);
|
|
165
|
+
expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).not.toThrow();
|
|
166
|
+
expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).toEqual([]);
|
|
118
167
|
});
|
|
119
168
|
test('leaves untagged when no environment or code pattern is present', () => {
|
|
120
169
|
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
@@ -129,10 +178,200 @@ describe('classifyFailureTags', () => {
|
|
|
129
178
|
location: {
|
|
130
179
|
url: 'https://app/page'
|
|
131
180
|
}
|
|
132
|
-
}]
|
|
181
|
+
}], {
|
|
182
|
+
title: 'neutral-test'
|
|
183
|
+
})], 'neutral-test', tempDir);
|
|
133
184
|
expect(tags).toEqual([]);
|
|
134
185
|
});
|
|
135
186
|
});
|
|
187
|
+
describe('classifyFailureDetails', () => {
|
|
188
|
+
test('records the matching pattern for environment failures', () => {
|
|
189
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetail([{
|
|
190
|
+
kind: 'requestfailed',
|
|
191
|
+
url: 'https://app/events/list/commentsubtabevent',
|
|
192
|
+
failure: 'net::ERR_ABORTED',
|
|
193
|
+
resourceType: 'document'
|
|
194
|
+
}])], TEST_TITLE, tempDir);
|
|
195
|
+
expect(details.tags).toEqual(['ENVIRONMENT']);
|
|
196
|
+
expect(details.matches).toHaveLength(1);
|
|
197
|
+
expect(details.matches[0]).toMatchObject({
|
|
198
|
+
tag: 'ENVIRONMENT',
|
|
199
|
+
pattern: '^requestfailed document\\b[\\s\\S]*ERR_ABORTED',
|
|
200
|
+
source: 'runtime-log'
|
|
201
|
+
});
|
|
202
|
+
expect(details.matches[0].text).toContain('ERR_ABORTED');
|
|
203
|
+
});
|
|
204
|
+
test('records the matching pattern for code failures from events', () => {
|
|
205
|
+
const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([{
|
|
206
|
+
kind: 'pageerror',
|
|
207
|
+
name: 'TypeError',
|
|
208
|
+
message: "Cannot read properties of undefined (reading 'id')"
|
|
209
|
+
}]);
|
|
210
|
+
expect(details.tags).toEqual(['CODE']);
|
|
211
|
+
expect(details.matches[0]).toMatchObject({
|
|
212
|
+
tag: 'CODE',
|
|
213
|
+
pattern: 'Cannot read properties of (undefined|null)',
|
|
214
|
+
source: 'runtime-log'
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
test('records flaky outcome without a regex pattern', () => {
|
|
218
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([{
|
|
219
|
+
status: 'flaky',
|
|
220
|
+
results: []
|
|
221
|
+
}], TEST_TITLE, tempDir);
|
|
222
|
+
expect(details.tags).toEqual(['FLAKY']);
|
|
223
|
+
expect(details.matches[0]).toEqual({
|
|
224
|
+
tag: 'FLAKY',
|
|
225
|
+
pattern: null,
|
|
226
|
+
source: 'retry-outcome',
|
|
227
|
+
text: null
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
test('records tool pattern from stdio', () => {
|
|
231
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetailWithStdio({
|
|
232
|
+
stderr: [{
|
|
233
|
+
text: 'Error during login for actor admin'
|
|
234
|
+
}]
|
|
235
|
+
})], TEST_TITLE, tempDir);
|
|
236
|
+
expect(details.tags).toEqual(['TOOL']);
|
|
237
|
+
expect(details.matches[0]).toMatchObject({
|
|
238
|
+
tag: 'TOOL',
|
|
239
|
+
pattern: 'Error during login',
|
|
240
|
+
source: 'stdio'
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
test('records case pattern from stdio and implies code', () => {
|
|
244
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
245
|
+
try {
|
|
246
|
+
casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
|
|
247
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetailWithStdio({
|
|
248
|
+
stderr: [{
|
|
249
|
+
text: 'Error: expect(received).toBe(expected)'
|
|
250
|
+
}]
|
|
251
|
+
})], TEST_TITLE, tempDir);
|
|
252
|
+
expect(details.tags).toEqual(['CASE', 'CODE']);
|
|
253
|
+
expect(details.matches).toHaveLength(2);
|
|
254
|
+
expect(details.matches[0]).toMatchObject({
|
|
255
|
+
tag: 'CASE',
|
|
256
|
+
pattern: 'expect\\(received\\)\\.toBe',
|
|
257
|
+
source: 'stdio'
|
|
258
|
+
});
|
|
259
|
+
expect(details.matches[1]).toMatchObject({
|
|
260
|
+
tag: 'CODE',
|
|
261
|
+
pattern: null,
|
|
262
|
+
source: 'case-implied'
|
|
263
|
+
});
|
|
264
|
+
expect(details.matches[1].text).toContain('expect(received).toBe(expected)');
|
|
265
|
+
} finally {
|
|
266
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
267
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
test('keeps runtime-log code match when case also matches', () => {
|
|
271
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
272
|
+
try {
|
|
273
|
+
casePatterns.CASE_PATTERNS.push(/expect\(received\)\.toBe/i);
|
|
274
|
+
writeRuntimeLog([{
|
|
275
|
+
kind: 'pageerror',
|
|
276
|
+
name: 'TypeError',
|
|
277
|
+
message: "Cannot read properties of undefined (reading 'id')"
|
|
278
|
+
}], {
|
|
279
|
+
title: 'case-with-runtime-code'
|
|
280
|
+
});
|
|
281
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([{
|
|
282
|
+
status: 'unexpected',
|
|
283
|
+
results: [{
|
|
284
|
+
status: 'failed',
|
|
285
|
+
workerIndex: 0,
|
|
286
|
+
retry: 0,
|
|
287
|
+
stderr: [{
|
|
288
|
+
text: 'Error: expect(received).toBe(expected)'
|
|
289
|
+
}]
|
|
290
|
+
}]
|
|
291
|
+
}], 'case-with-runtime-code', tempDir);
|
|
292
|
+
expect(details.tags).toEqual(['CASE', 'CODE']);
|
|
293
|
+
expect(details.matches).toHaveLength(2);
|
|
294
|
+
expect(details.matches.find(match => match.tag === 'CASE')).toMatchObject({
|
|
295
|
+
source: 'stdio'
|
|
296
|
+
});
|
|
297
|
+
expect(details.matches.find(match => match.tag === 'CODE')).toMatchObject({
|
|
298
|
+
source: 'runtime-log',
|
|
299
|
+
pattern: 'Cannot read properties of (undefined|null)'
|
|
300
|
+
});
|
|
301
|
+
} finally {
|
|
302
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
303
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
test('implies code from case when no runtime log is present', () => {
|
|
307
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
308
|
+
try {
|
|
309
|
+
casePatterns.CASE_PATTERNS.push(/Timed out \d+ms waiting for/i);
|
|
310
|
+
const details = (0, _failureTagClassifier.classifyFailureDetails)([failingDetailWithStdio({
|
|
311
|
+
stderr: [{
|
|
312
|
+
text: 'Timed out 5000ms waiting for locator("#save")'
|
|
313
|
+
}]
|
|
314
|
+
})], TEST_TITLE, tempDir);
|
|
315
|
+
expect(details.tags).toEqual(['CASE', 'CODE']);
|
|
316
|
+
expect(details.matches.find(match => match.tag === 'CODE')).toMatchObject({
|
|
317
|
+
source: 'case-implied',
|
|
318
|
+
pattern: null
|
|
319
|
+
});
|
|
320
|
+
} finally {
|
|
321
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
322
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
describe('failure tag metadata attachment', () => {
|
|
327
|
+
test('builds a small JSON attachment when tags are present', () => {
|
|
328
|
+
const details = (0, _failureTagClassifier.classifyFailureDetailsFromEvents)([{
|
|
329
|
+
kind: 'pageerror',
|
|
330
|
+
name: 'TypeError',
|
|
331
|
+
message: "Cannot read properties of undefined (reading 'id')"
|
|
332
|
+
}]);
|
|
333
|
+
const attachment = (0, _failureTagClassifier.buildFailureTagMetadataAttachment)(details);
|
|
334
|
+
expect(attachment).toMatchObject({
|
|
335
|
+
name: _failureTagConstants.FAILURE_TAG_METADATA_ATTACHMENT,
|
|
336
|
+
contentType: 'application/json'
|
|
337
|
+
});
|
|
338
|
+
expect(JSON.parse(attachment.body.toString('utf8'))).toEqual(details);
|
|
339
|
+
});
|
|
340
|
+
test('returns null when no tags are present', () => {
|
|
341
|
+
expect((0, _failureTagClassifier.buildFailureTagMetadataAttachment)({
|
|
342
|
+
tags: [],
|
|
343
|
+
matches: []
|
|
344
|
+
})).toBeNull();
|
|
345
|
+
});
|
|
346
|
+
test('replaces an existing metadata attachment', () => {
|
|
347
|
+
const first = (0, _failureTagClassifier.buildFailureTagMetadataAttachment)({
|
|
348
|
+
tags: ['CODE'],
|
|
349
|
+
matches: [{
|
|
350
|
+
tag: 'CODE',
|
|
351
|
+
pattern: 'old',
|
|
352
|
+
source: 'runtime-log',
|
|
353
|
+
text: 'old'
|
|
354
|
+
}]
|
|
355
|
+
});
|
|
356
|
+
const second = (0, _failureTagClassifier.buildFailureTagMetadataAttachment)({
|
|
357
|
+
tags: ['TOOL'],
|
|
358
|
+
matches: [{
|
|
359
|
+
tag: 'TOOL',
|
|
360
|
+
pattern: 'new',
|
|
361
|
+
source: 'stdio',
|
|
362
|
+
text: 'new'
|
|
363
|
+
}]
|
|
364
|
+
});
|
|
365
|
+
const merged = (0, _failureTagClassifier.upsertFailureTagMetadataAttachment)([{
|
|
366
|
+
name: 'screenshot',
|
|
367
|
+
path: '/tmp/a.png'
|
|
368
|
+
}, first], JSON.parse(second.body.toString('utf8')));
|
|
369
|
+
expect(merged).toHaveLength(2);
|
|
370
|
+
expect(merged[0].name).toBe('screenshot');
|
|
371
|
+
expect(merged[1].name).toBe(_failureTagConstants.FAILURE_TAG_METADATA_ATTACHMENT);
|
|
372
|
+
expect(JSON.parse(merged[1].body.toString('utf8')).matches[0].pattern).toBe('new');
|
|
373
|
+
});
|
|
374
|
+
});
|
|
136
375
|
describe('mergeFailureTags', () => {
|
|
137
376
|
test('places suffixed failure tags first, then existing tags', () => {
|
|
138
377
|
expect((0, _failureTagClassifier.mergeFailureTags)(['@comments', '@calls'], ['CODE', 'ENVIRONMENT'])).toEqual(['CODE_failure', 'ENVIRONMENT_failure', '@comments', '@calls']);
|
|
@@ -143,25 +382,32 @@ describe('mergeFailureTags', () => {
|
|
|
143
382
|
test('does not duplicate an already-present failure tag', () => {
|
|
144
383
|
expect((0, _failureTagClassifier.mergeFailureTags)(['CODE_failure'], ['CODE'])).toEqual(['CODE_failure']);
|
|
145
384
|
});
|
|
385
|
+
test('formats case and code failure tags with suffix', () => {
|
|
386
|
+
expect((0, _failureTagClassifier.mergeFailureTags)(['@smoke'], ['CASE', 'CODE'])).toEqual(['CASE_failure', 'CODE_failure', '@smoke']);
|
|
387
|
+
});
|
|
146
388
|
});
|
|
147
389
|
describe('regex safety', () => {
|
|
148
390
|
test('resets stateful regex for tool pattern checks', () => {
|
|
149
391
|
const originalToolPatterns = toolPatterns.TOOL_PATTERNS.slice();
|
|
392
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
150
393
|
try {
|
|
151
394
|
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
152
395
|
toolPatterns.TOOL_PATTERNS.push(/DATA_GENERATION_ERROR/g);
|
|
396
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
153
397
|
const detail = failingDetailWithStdio({
|
|
154
398
|
stderr: [{
|
|
155
399
|
text: 'status : DATA_GENERATION_ERROR'
|
|
156
400
|
}]
|
|
157
401
|
});
|
|
158
|
-
const first = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
159
|
-
const second = (0, _failureTagClassifier.classifyFailureTags)([detail]);
|
|
402
|
+
const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
403
|
+
const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
160
404
|
expect(first).toEqual(['TOOL']);
|
|
161
405
|
expect(second).toEqual(['TOOL']);
|
|
162
406
|
} finally {
|
|
163
407
|
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
164
408
|
toolPatterns.TOOL_PATTERNS.push(...originalToolPatterns);
|
|
409
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
410
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
165
411
|
}
|
|
166
412
|
});
|
|
167
413
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
5
|
+
var _os = _interopRequireDefault(require("os"));
|
|
6
|
+
var _path = _interopRequireDefault(require("path"));
|
|
3
7
|
var _logCollector = require("../../../../../core/playwright/helpers/logCollector");
|
|
4
8
|
function makeEmitter() {
|
|
5
9
|
const handlers = {};
|
|
@@ -100,19 +104,29 @@ describe('LogCollector baseline-noise stripping', () => {
|
|
|
100
104
|
name: 'TypeError'
|
|
101
105
|
});
|
|
102
106
|
});
|
|
103
|
-
test('
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
test('flushes captured events to a jsonl file', () => {
|
|
108
|
+
const flushDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'log-collector-flush-'));
|
|
109
|
+
try {
|
|
110
|
+
page.emit('pageerror', {
|
|
111
|
+
name: 'TypeError',
|
|
112
|
+
message: "Cannot read properties of undefined (reading 'id')",
|
|
113
|
+
stack: 'TypeError: ...'
|
|
114
|
+
});
|
|
115
|
+
const filePath = collector.flushToFile(flushDir, 'w0-test-0');
|
|
116
|
+
const contents = _fs.default.readFileSync(filePath, 'utf8');
|
|
117
|
+
expect(filePath).toContain('w0-test-0.jsonl');
|
|
118
|
+
expect(contents).toContain('"kind":"pageerror"');
|
|
119
|
+
expect(contents).toContain("Cannot read properties of undefined (reading 'id')");
|
|
120
|
+
expect(contents.endsWith('\n')).toBe(true);
|
|
121
|
+
} finally {
|
|
122
|
+
_fs.default.rmSync(flushDir, {
|
|
123
|
+
recursive: true,
|
|
124
|
+
force: true
|
|
125
|
+
});
|
|
126
|
+
}
|
|
113
127
|
});
|
|
114
|
-
test('returns null from
|
|
115
|
-
expect(collector.
|
|
128
|
+
test('returns null from flushToFile when no events were captured', () => {
|
|
129
|
+
expect(collector.flushToFile('/tmp', 'empty')).toBeNull();
|
|
116
130
|
});
|
|
117
131
|
test('keeps a document-navigation abort (real environment signal)', () => {
|
|
118
132
|
context.emit('requestfailed', failedRequest('https://app/agent/events/list/commentsubtabevent', 'net::ERR_ABORTED', 'document'));
|
|
@@ -1,41 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
5
|
+
var _os = _interopRequireDefault(require("os"));
|
|
6
|
+
var _path = _interopRequireDefault(require("path"));
|
|
7
|
+
var _runtimeLogPath = require("../../../../../core/playwright/helpers/runtimeLogPath");
|
|
4
8
|
var _runtimeLogReader = require("../../../../../core/playwright/helpers/runtimeLogReader");
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
const TEST_TITLE = 'runtime-log-reader-test';
|
|
10
|
+
let tempDir;
|
|
11
|
+
function writeRuntimeLog(events, {
|
|
12
|
+
workerIndex = 0,
|
|
13
|
+
retry = 0,
|
|
14
|
+
title = TEST_TITLE
|
|
15
|
+
} = {}) {
|
|
16
|
+
const file = (0, _runtimeLogPath.resolveRuntimeLogFilePath)(tempDir, {
|
|
17
|
+
workerIndex,
|
|
18
|
+
retry,
|
|
19
|
+
title
|
|
20
|
+
});
|
|
21
|
+
_fs.default.mkdirSync(_path.default.dirname(file), {
|
|
22
|
+
recursive: true
|
|
23
|
+
});
|
|
24
|
+
_fs.default.writeFileSync(file, `${events.map(e => JSON.stringify(e)).join('\n')}\n`);
|
|
25
|
+
return file;
|
|
7
26
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tempDir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), 'runtime-log-reader-'));
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
_fs.default.rmSync(tempDir, {
|
|
32
|
+
recursive: true,
|
|
33
|
+
force: true
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('readRuntimeLogEventsForResult', () => {
|
|
37
|
+
test('reads project-side runtime log for a failing result', () => {
|
|
38
|
+
const events = [{
|
|
11
39
|
kind: 'pageerror',
|
|
12
40
|
message: 'inline boom'
|
|
13
41
|
}];
|
|
42
|
+
writeRuntimeLog(events);
|
|
14
43
|
const result = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
body: jsonlBody(inlineEvents)
|
|
19
|
-
}]
|
|
44
|
+
status: 'failed',
|
|
45
|
+
workerIndex: 0,
|
|
46
|
+
retry: 0
|
|
20
47
|
};
|
|
21
|
-
expect((0, _runtimeLogReader.
|
|
48
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, TEST_TITLE, tempDir)).toEqual(events);
|
|
22
49
|
});
|
|
23
50
|
test('skips malformed jsonl lines', () => {
|
|
24
|
-
const
|
|
51
|
+
const file = writeRuntimeLog([]);
|
|
52
|
+
_fs.default.writeFileSync(file, [JSON.stringify({
|
|
25
53
|
kind: 'console',
|
|
26
54
|
text: 'ok'
|
|
27
55
|
}), 'not-json', JSON.stringify({
|
|
28
56
|
kind: 'pageerror',
|
|
29
57
|
message: 'err'
|
|
30
|
-
})].join('\n');
|
|
58
|
+
})].join('\n') + '\n');
|
|
59
|
+
const result = {
|
|
60
|
+
status: 'failed',
|
|
61
|
+
workerIndex: 0,
|
|
62
|
+
retry: 0
|
|
63
|
+
};
|
|
64
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, TEST_TITLE, tempDir)).toEqual([{
|
|
65
|
+
kind: 'console',
|
|
66
|
+
text: 'ok'
|
|
67
|
+
}, {
|
|
68
|
+
kind: 'pageerror',
|
|
69
|
+
message: 'err'
|
|
70
|
+
}]);
|
|
71
|
+
});
|
|
72
|
+
test('returns an empty array when the runtime log file is missing', () => {
|
|
73
|
+
const result = {
|
|
74
|
+
status: 'failed',
|
|
75
|
+
workerIndex: 0,
|
|
76
|
+
retry: 0
|
|
77
|
+
};
|
|
78
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, 'missing-test', tempDir)).toEqual([]);
|
|
79
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(null, TEST_TITLE, tempDir)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
test('returns an empty array when logsDir is missing', () => {
|
|
31
82
|
const result = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
body: Buffer.from(`${raw}\n`)
|
|
36
|
-
}]
|
|
83
|
+
status: 'failed',
|
|
84
|
+
workerIndex: 0,
|
|
85
|
+
retry: 0
|
|
37
86
|
};
|
|
38
|
-
expect((0, _runtimeLogReader.
|
|
87
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, TEST_TITLE)).toEqual([]);
|
|
88
|
+
expect((0, _runtimeLogReader.readRuntimeLogEventsForResult)(result, TEST_TITLE, undefined)).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('readRuntimeLogEvents', () => {
|
|
92
|
+
test('reads events from a jsonl file path', () => {
|
|
93
|
+
const file = writeRuntimeLog([{
|
|
94
|
+
kind: 'console',
|
|
95
|
+
text: 'ok'
|
|
96
|
+
}, {
|
|
97
|
+
kind: 'pageerror',
|
|
98
|
+
message: 'err'
|
|
99
|
+
}]);
|
|
100
|
+
expect((0, _runtimeLogReader.readRuntimeLogEvents)(file)).toEqual([{
|
|
39
101
|
kind: 'console',
|
|
40
102
|
text: 'ok'
|
|
41
103
|
}, {
|
|
@@ -43,10 +105,8 @@ describe('readRuntimeLogEventsFromResult', () => {
|
|
|
43
105
|
message: 'err'
|
|
44
106
|
}]);
|
|
45
107
|
});
|
|
46
|
-
test('returns an empty array
|
|
47
|
-
expect((0, _runtimeLogReader.
|
|
48
|
-
|
|
49
|
-
})).toEqual([]);
|
|
50
|
-
expect((0, _runtimeLogReader.readRuntimeLogEventsFromResult)(null)).toEqual([]);
|
|
108
|
+
test('returns an empty array for a missing file', () => {
|
|
109
|
+
expect((0, _runtimeLogReader.readRuntimeLogEvents)(null)).toEqual([]);
|
|
110
|
+
expect((0, _runtimeLogReader.readRuntimeLogEvents)(_path.default.join(tempDir, 'missing.jsonl'))).toEqual([]);
|
|
51
111
|
});
|
|
52
112
|
});
|