@zohodesk/testinglibrary 0.0.82-n20-experimental → 0.0.84-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/actorContext.js +10 -13
- package/build/core/playwright/builtInFixtures/context.js +19 -19
- package/build/core/playwright/builtInFixtures/page.js +1 -0
- package/build/core/playwright/constants/failureTagConstants.js +19 -0
- package/build/core/playwright/constants/logCollectorConstants.js +15 -0
- package/build/core/playwright/helpers/failureTagClassifier.js +266 -0
- package/build/core/playwright/helpers/logCollector.js +17 -40
- package/build/core/playwright/helpers/runtimeLogPath.js +61 -0
- package/build/core/playwright/helpers/runtimeLogReader.js +54 -0
- package/build/core/playwright/patterns/casePatterns.js +11 -0
- package/build/core/playwright/patterns/codePatterns.js +9 -0
- package/build/core/playwright/patterns/environmentPatterns.js +15 -0
- package/build/core/playwright/patterns/index.js +33 -0
- package/build/core/playwright/patterns/toolPatterns.js +34 -0
- package/build/core/playwright/readConfigFile.js +1 -1
- package/build/core/playwright/setup/custom-reporter.js +1 -16
- package/build/core/playwright/setup/qc-custom-reporter.js +28 -1
- package/build/test/core/playwright/helpers/__tests__/failureTagClassifier.test.js +413 -0
- package/build/test/core/playwright/helpers/__tests__/logCollector.test.js +139 -0
- package/build/test/core/playwright/helpers/__tests__/runtimeLogReader.test.js +112 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/unit_reports/unit-report.html +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.CASE_PATTERNS = void 0;
|
|
7
|
+
// Case patterns: test-case / assertion failures from Playwright output.
|
|
8
|
+
// Matched against the failing result's stdout + stderr.
|
|
9
|
+
const CASE_PATTERNS = exports.CASE_PATTERNS = [
|
|
10
|
+
// Generator
|
|
11
|
+
/GeneratorError: Generator/i, /DataGeneratorConfigurationError: Data Generator configuration is missing for the profile/i, /HTTP error! status:/i, /status\s*:\s*DATA_GENERATION_ERROR/i];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.CODE_PATTERNS = void 0;
|
|
7
|
+
// Code patterns: real application defects surfacing as JS exceptions in the
|
|
8
|
+
// browser. Matched against a flattened text form of each runtime-log event.
|
|
9
|
+
const CODE_PATTERNS = exports.CODE_PATTERNS = [/Cannot read properties of (undefined|null)/i, /\bis not a function\b/i, /\bis not defined\b/i, /\bis not iterable\b/i];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ENVIRONMENT_PATTERNS = void 0;
|
|
7
|
+
// Environment patterns: infra / network / server breakage. Matched against a
|
|
8
|
+
// flattened text form of each runtime-log event (see eventToText).
|
|
9
|
+
const ENVIRONMENT_PATTERNS = exports.ENVIRONMENT_PATTERNS = [
|
|
10
|
+
// Main document navigation was aborted (page never loaded).
|
|
11
|
+
/^requestfailed document\b[\s\S]*ERR_ABORTED/i,
|
|
12
|
+
// Network / DNS / certificate / connection failures.
|
|
13
|
+
/ERR_TUNNEL_CONNECTION_FAILED|ERR_CERT_|ERR_CONNECTION_|ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED|ERR_TIMED_OUT|ERR_ADDRESS_UNREACHABLE/i,
|
|
14
|
+
// Server-side 5xx responses.
|
|
15
|
+
/^response 5\d\d\b/i, /status of 5\d\d/i];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "CASE_PATTERNS", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _casePatterns.CASE_PATTERNS;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "CODE_PATTERNS", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _codePatterns.CODE_PATTERNS;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "ENVIRONMENT_PATTERNS", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _environmentPatterns.ENVIRONMENT_PATTERNS;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "TOOL_PATTERNS", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _toolPatterns.TOOL_PATTERNS;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
var _environmentPatterns = require("./environmentPatterns");
|
|
31
|
+
var _codePatterns = require("./codePatterns");
|
|
32
|
+
var _casePatterns = require("./casePatterns");
|
|
33
|
+
var _toolPatterns = require("./toolPatterns");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.TOOL_PATTERNS = void 0;
|
|
7
|
+
// Tool patterns: failures originating from the test tooling / data-generation
|
|
8
|
+
// layer (not the app under test). Matched against the failing result's
|
|
9
|
+
// stdout + stderr text. Add new patterns here to recognise more tool errors.
|
|
10
|
+
const TOOL_PATTERNS = exports.TOOL_PATTERNS = [/Error during login/i, /fetch failed/i,
|
|
11
|
+
// Configuration
|
|
12
|
+
/Config File Not Exists\. Please provide a/i, /Exception while getting the uat file from the application/i, /There is no beta feature configured with the name/i, /No config mapping found for stage:/i,
|
|
13
|
+
// Edition
|
|
14
|
+
/edition configured\./i, /portal configured in/i, /profile configured in/i,
|
|
15
|
+
// Actor
|
|
16
|
+
/file missing\./i, /Error loading actor configuration from/i, /The actors data is missing\./i, /not found in user pages\./i,
|
|
17
|
+
// Module
|
|
18
|
+
/does not exist\. We have not triggered the execution for this module/i,
|
|
19
|
+
// Auth
|
|
20
|
+
/Error while parsing cookies/i,
|
|
21
|
+
// FileMutex
|
|
22
|
+
/Watch timeout exceeded/i,
|
|
23
|
+
// Runner
|
|
24
|
+
/Method 'run\(\)' must be implemented\./i, /Invalid runner type/i,
|
|
25
|
+
// Process
|
|
26
|
+
/Child Process Exited with Code/i, /Terminating Playwright Process\.\.\./i, /Cleaning up\.\.\./i,
|
|
27
|
+
// TestData
|
|
28
|
+
/Error appending or creating the test data file:/i, /Error\(err\)/i, /Error while deleting the test data file:/i,
|
|
29
|
+
// FeatureFile
|
|
30
|
+
/Error while parsing feature files\. Please fix the above issues before running test cases/i,
|
|
31
|
+
// RPC
|
|
32
|
+
/Error: HTTP /i, /Invalid payload\. It must be a non-null object\./i, /Invalid payload\.arguments\.entityId\./i,
|
|
33
|
+
// Search
|
|
34
|
+
/Invalid or missing data in dataTable/i, /JSONPath query/i];
|
|
@@ -120,7 +120,7 @@ function combineDefaultConfigWithUserConfig(userConfiguration) {
|
|
|
120
120
|
* @property {Array} editionOrder: Order in the form of larger editions in the back. Edition with the most privelages should be last
|
|
121
121
|
* @property {testSetupConfig} testSetup: Specify page and context functions that will be called while intilaizing fixtures.
|
|
122
122
|
* @property {boolean} showCaseTimings: When true, the console reporter prints per-case start/end wall-clock times (h:mm:ss AM/PM) and a case-timings.json sidecar is written to reportPath for the HTML report addon. Default: true.
|
|
123
|
-
* @property {Object} browserLogs: Runtime browser log capture. { mode: 'failure-only' | 'always', limits?: { maxEvents,
|
|
123
|
+
* @property {Object} browserLogs: Runtime browser log capture. { mode: 'failure-only' | 'always', limits?: { maxEvents, ignoreUrlPatterns, ignoreMessagePatterns } }. ignoreMessagePatterns is a list of RegExp matched against console-error text and pageerror messages; matching events are skipped. Default: { mode: 'failure-only' }.
|
|
124
124
|
*/
|
|
125
125
|
|
|
126
126
|
/**
|
|
@@ -128,15 +128,6 @@ class JSONSummaryReporter {
|
|
|
128
128
|
const isFailure = result.status !== 'passed' && result.status !== 'skipped';
|
|
129
129
|
if (isFailure) {
|
|
130
130
|
var _result$error;
|
|
131
|
-
const runtimeAttachment = (result.attachments || []).find(a => a.name === 'runtime-log' && a.body);
|
|
132
|
-
let runtime = null;
|
|
133
|
-
if (runtimeAttachment) {
|
|
134
|
-
try {
|
|
135
|
-
runtime = JSON.parse(runtimeAttachment.body.toString('utf8'));
|
|
136
|
-
} catch {
|
|
137
|
-
// malformed attachment; skip
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
131
|
this._caseTimings.push({
|
|
141
132
|
title: fullTitle,
|
|
142
133
|
fileName,
|
|
@@ -155,13 +146,7 @@ class JSONSummaryReporter {
|
|
|
155
146
|
title: step.title,
|
|
156
147
|
error: (_step$error = step.error) === null || _step$error === void 0 ? void 0 : _step$error.message
|
|
157
148
|
};
|
|
158
|
-
})
|
|
159
|
-
...(runtime ? {
|
|
160
|
-
runtime: {
|
|
161
|
-
summary: runtime.summary,
|
|
162
|
-
logFile: runtime.file
|
|
163
|
-
}
|
|
164
|
-
} : {})
|
|
149
|
+
})
|
|
165
150
|
});
|
|
166
151
|
}
|
|
167
152
|
}
|
|
@@ -10,7 +10,21 @@ var _path = _interopRequireDefault(require("path"));
|
|
|
10
10
|
var _codeFrame = require("@babel/code-frame");
|
|
11
11
|
var _configConstants = _interopRequireDefault(require("../constants/configConstants"));
|
|
12
12
|
var _ConfigurationHelper = require("../configuration/ConfigurationHelper");
|
|
13
|
+
var _failureTagClassifier = require("../helpers/failureTagClassifier");
|
|
13
14
|
const stage = (0, _ConfigurationHelper.getRunStage)();
|
|
15
|
+
|
|
16
|
+
// Walks the built suite tree and appends derived failure tags (CASE_failure, CODE_failure, ENVIRONMENT_failure) into each spec's tags array.
|
|
17
|
+
const applyFailureTags = node => {
|
|
18
|
+
for (const spec of node.specs ?? []) {
|
|
19
|
+
const failureTags = (0, _failureTagClassifier.classifyFailureTags)(spec.tests, spec.title);
|
|
20
|
+
if (failureTags.length > 0) {
|
|
21
|
+
spec.tags = (0, _failureTagClassifier.mergeFailureTags)(spec.tags, failureTags);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const child of node.suites ?? []) {
|
|
25
|
+
applyFailureTags(child);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
14
28
|
class CustomJsonReporter {
|
|
15
29
|
constructor({
|
|
16
30
|
outputFile = 'test-results.json'
|
|
@@ -30,9 +44,21 @@ class CustomJsonReporter {
|
|
|
30
44
|
onTestEnd(test, result) {
|
|
31
45
|
var _result$errors, _result$steps;
|
|
32
46
|
const key = `${test.location.file}:${test.location.line}:${test.title}`;
|
|
47
|
+
const isFailure = !['passed', 'skipped'].includes(result.status);
|
|
48
|
+
let attachments = result.attachments ?? [];
|
|
49
|
+
if (isFailure) {
|
|
50
|
+
const metadata = (0, _failureTagClassifier.classifyFailureDetailsFromResult)({
|
|
51
|
+
status: result.status,
|
|
52
|
+
workerIndex: result.workerIndex,
|
|
53
|
+
retry: result.retry,
|
|
54
|
+
stdout: result.stdout.map(out => stdioEntry(out)),
|
|
55
|
+
stderr: result.stderr.map(err => stdioEntry(err))
|
|
56
|
+
}, test.title);
|
|
57
|
+
attachments = (0, _failureTagClassifier.upsertFailureTagMetadataAttachment)(attachments, metadata);
|
|
58
|
+
}
|
|
33
59
|
const testResult = {
|
|
34
60
|
status: result.status,
|
|
35
|
-
attachments
|
|
61
|
+
attachments,
|
|
36
62
|
startTime: result.startTime,
|
|
37
63
|
retry: result.retry,
|
|
38
64
|
stderr: result.stderr.map(err => stdioEntry(err)),
|
|
@@ -52,6 +78,7 @@ class CustomJsonReporter {
|
|
|
52
78
|
const extracted = suite.suites.map(suite => extractMergedSuite(this.report.config.rootDir, suite, this.testResultsById));
|
|
53
79
|
this.report.suites.push(...extracted);
|
|
54
80
|
});
|
|
81
|
+
this.report.suites.forEach(suite => applyFailureTags(suite));
|
|
55
82
|
const writableStream = _fs.default.createWriteStream(this.outputFile);
|
|
56
83
|
writableStream.write('{\n');
|
|
57
84
|
writableStream.write(` "config": ${JSON.stringify(this.report.config, null, 2)},\n`);
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use strict";
|
|
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"));
|
|
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");
|
|
10
|
+
var _failureTagClassifier = require("../../../../../core/playwright/helpers/failureTagClassifier");
|
|
11
|
+
var _failureTagConstants = require("../../../../../core/playwright/constants/failureTagConstants");
|
|
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); }
|
|
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);
|
|
33
|
+
return {
|
|
34
|
+
status: 'unexpected',
|
|
35
|
+
results: [{
|
|
36
|
+
status: 'failed',
|
|
37
|
+
workerIndex: options.workerIndex ?? 0,
|
|
38
|
+
retry: options.retry ?? 0
|
|
39
|
+
}]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function failingDetailWithStdio({
|
|
43
|
+
stdout = [],
|
|
44
|
+
stderr = []
|
|
45
|
+
}) {
|
|
46
|
+
return {
|
|
47
|
+
status: 'unexpected',
|
|
48
|
+
results: [{
|
|
49
|
+
status: 'failed',
|
|
50
|
+
attachments: [],
|
|
51
|
+
stdout,
|
|
52
|
+
stderr
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
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
|
+
});
|
|
65
|
+
describe('classifyFailureTags', () => {
|
|
66
|
+
test('returns flaky and skips log analysis for flaky outcome', () => {
|
|
67
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([{
|
|
68
|
+
status: 'flaky',
|
|
69
|
+
results: []
|
|
70
|
+
}], TEST_TITLE, tempDir);
|
|
71
|
+
expect(tags).toEqual(['FLAKY']);
|
|
72
|
+
});
|
|
73
|
+
test('tags environment when a document abort is present', () => {
|
|
74
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
75
|
+
kind: 'requestfailed',
|
|
76
|
+
url: 'https://app/events/list/commentsubtabevent',
|
|
77
|
+
failure: 'net::ERR_ABORTED',
|
|
78
|
+
resourceType: 'document'
|
|
79
|
+
}])], TEST_TITLE, tempDir);
|
|
80
|
+
expect(tags).toEqual(['ENVIRONMENT']);
|
|
81
|
+
});
|
|
82
|
+
test('tags environment for a network/cert failure', () => {
|
|
83
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
84
|
+
kind: 'requestfailed',
|
|
85
|
+
url: 'https://js/gshandler.js',
|
|
86
|
+
failure: 'net::ERR_CERT_COMMON_NAME_INVALID',
|
|
87
|
+
resourceType: 'script'
|
|
88
|
+
}], {
|
|
89
|
+
title: 'cert-test'
|
|
90
|
+
})], 'cert-test', tempDir);
|
|
91
|
+
expect(tags).toEqual(['ENVIRONMENT']);
|
|
92
|
+
});
|
|
93
|
+
test('tags code when an app undefined-property error is present', () => {
|
|
94
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
95
|
+
kind: 'pageerror',
|
|
96
|
+
name: 'TypeError',
|
|
97
|
+
message: "Cannot read properties of undefined (reading 'id')"
|
|
98
|
+
}], {
|
|
99
|
+
title: 'code-test'
|
|
100
|
+
})], 'code-test', tempDir);
|
|
101
|
+
expect(tags).toEqual(['CODE']);
|
|
102
|
+
});
|
|
103
|
+
test('does not tag a non-document (xhr) abort as environment', () => {
|
|
104
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
105
|
+
kind: 'requestfailed',
|
|
106
|
+
url: 'https://app/api/tickets',
|
|
107
|
+
failure: 'net::ERR_ABORTED',
|
|
108
|
+
resourceType: 'xhr'
|
|
109
|
+
}], {
|
|
110
|
+
title: 'xhr-test'
|
|
111
|
+
})], 'xhr-test', tempDir);
|
|
112
|
+
expect(tags).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
test('tags case and code when stderr reports DATA_GENERATION_ERROR', () => {
|
|
115
|
+
const detail = failingDetailWithStdio({
|
|
116
|
+
stderr: [{
|
|
117
|
+
text: 'status : DATA_GENERATION_ERROR\n generator: comments'
|
|
118
|
+
}]
|
|
119
|
+
});
|
|
120
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
121
|
+
expect(tags).toEqual(['CASE', 'CODE']);
|
|
122
|
+
});
|
|
123
|
+
test('tags tool for a known tool pattern in base64 stdout', () => {
|
|
124
|
+
const detail = failingDetailWithStdio({
|
|
125
|
+
stdout: [{
|
|
126
|
+
buffer: Buffer.from('Error during login').toString('base64')
|
|
127
|
+
}]
|
|
128
|
+
});
|
|
129
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
130
|
+
expect(tags).toEqual(['TOOL']);
|
|
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
|
+
});
|
|
148
|
+
test('does not tag tool for benign stdout/stderr', () => {
|
|
149
|
+
const detail = failingDetailWithStdio({
|
|
150
|
+
stdout: [{
|
|
151
|
+
text: 'Generated 5 records successfully'
|
|
152
|
+
}],
|
|
153
|
+
stderr: []
|
|
154
|
+
});
|
|
155
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
156
|
+
expect(tags).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
test('does not throw for malformed stdio chunk entries', () => {
|
|
159
|
+
const detail = failingDetailWithStdio({
|
|
160
|
+
stdout: [null, undefined, 42, {
|
|
161
|
+
text: 'still fine'
|
|
162
|
+
}],
|
|
163
|
+
stderr: [false]
|
|
164
|
+
});
|
|
165
|
+
expect(() => (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).not.toThrow();
|
|
166
|
+
expect((0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir)).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
test('leaves untagged when no environment or code pattern is present', () => {
|
|
169
|
+
const tags = (0, _failureTagClassifier.classifyFailureTags)([failingDetail([{
|
|
170
|
+
kind: 'response',
|
|
171
|
+
status: 200,
|
|
172
|
+
method: 'GET',
|
|
173
|
+
url: 'https://app/ok'
|
|
174
|
+
}, {
|
|
175
|
+
kind: 'console',
|
|
176
|
+
level: 'error',
|
|
177
|
+
text: 'Some benign log line',
|
|
178
|
+
location: {
|
|
179
|
+
url: 'https://app/page'
|
|
180
|
+
}
|
|
181
|
+
}], {
|
|
182
|
+
title: 'neutral-test'
|
|
183
|
+
})], 'neutral-test', tempDir);
|
|
184
|
+
expect(tags).toEqual([]);
|
|
185
|
+
});
|
|
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
|
+
});
|
|
375
|
+
describe('mergeFailureTags', () => {
|
|
376
|
+
test('places suffixed failure tags first, then existing tags', () => {
|
|
377
|
+
expect((0, _failureTagClassifier.mergeFailureTags)(['@comments', '@calls'], ['CODE', 'ENVIRONMENT'])).toEqual(['CODE_failure', 'ENVIRONMENT_failure', '@comments', '@calls']);
|
|
378
|
+
});
|
|
379
|
+
test('emits the flaky category as FLAKY without a suffix', () => {
|
|
380
|
+
expect((0, _failureTagClassifier.mergeFailureTags)([], ['FLAKY', 'CODE'])).toEqual(['FLAKY', 'CODE_failure']);
|
|
381
|
+
});
|
|
382
|
+
test('does not duplicate an already-present failure tag', () => {
|
|
383
|
+
expect((0, _failureTagClassifier.mergeFailureTags)(['CODE_failure'], ['CODE'])).toEqual(['CODE_failure']);
|
|
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
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('regex safety', () => {
|
|
390
|
+
test('resets stateful regex for tool pattern checks', () => {
|
|
391
|
+
const originalToolPatterns = toolPatterns.TOOL_PATTERNS.slice();
|
|
392
|
+
const originalCasePatterns = casePatterns.CASE_PATTERNS.slice();
|
|
393
|
+
try {
|
|
394
|
+
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
395
|
+
toolPatterns.TOOL_PATTERNS.push(/DATA_GENERATION_ERROR/g);
|
|
396
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
397
|
+
const detail = failingDetailWithStdio({
|
|
398
|
+
stderr: [{
|
|
399
|
+
text: 'status : DATA_GENERATION_ERROR'
|
|
400
|
+
}]
|
|
401
|
+
});
|
|
402
|
+
const first = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
403
|
+
const second = (0, _failureTagClassifier.classifyFailureTags)([detail], TEST_TITLE, tempDir);
|
|
404
|
+
expect(first).toEqual(['TOOL']);
|
|
405
|
+
expect(second).toEqual(['TOOL']);
|
|
406
|
+
} finally {
|
|
407
|
+
toolPatterns.TOOL_PATTERNS.length = 0;
|
|
408
|
+
toolPatterns.TOOL_PATTERNS.push(...originalToolPatterns);
|
|
409
|
+
casePatterns.CASE_PATTERNS.length = 0;
|
|
410
|
+
casePatterns.CASE_PATTERNS.push(...originalCasePatterns);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|