haven-cypress-integration 1.6.2 → 2.0.1
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 +224 -120
- package/bin/haven-cypress.js +46 -59
- package/index.js +87 -215
- package/package.json +4 -3
- package/templates/Dockerfile +32 -22
- package/templates/run-filtered.sh +184 -90
- package/templates/syncCypressResults.js +230 -153
|
@@ -1,75 +1,151 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
const
|
|
4
|
-
const glob = require("glob");
|
|
5
|
-
// AWS SDK removed - EC2 instance handles S3 uploads
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
6
4
|
|
|
7
|
-
//
|
|
5
|
+
// Shared paths provided by HAVEN runner
|
|
8
6
|
const automationCasesPath = "/shared/automation-cases.json";
|
|
9
7
|
const postBaseUrlPath = "/shared/result-post-url.txt";
|
|
10
8
|
const triggeredByPath = "/shared/triggered-by.txt";
|
|
11
|
-
const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
|
|
12
9
|
|
|
13
|
-
//
|
|
10
|
+
// Cache for spec file tag mappings
|
|
11
|
+
const specTagsCache = new Map();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a Cypress spec file to extract test title -> automation ID mappings
|
|
15
|
+
* Looks for patterns like: it('Test title', { tags: ['@TC-AUTO-xxx'] }, () => {
|
|
16
|
+
*/
|
|
17
|
+
function parseSpecFileForTags(specFilePath) {
|
|
18
|
+
if (specTagsCache.has(specFilePath)) {
|
|
19
|
+
return specTagsCache.get(specFilePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const titleToAutomationIds = new Map();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync(specFilePath, 'utf-8');
|
|
26
|
+
|
|
27
|
+
// Match it() calls with tags - handles both single and double quotes
|
|
28
|
+
// Pattern: it('title' or it("title", { tags: [...] }
|
|
29
|
+
const itRegex = /it\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*\{\s*tags\s*:\s*\[([^\]]+)\]/g;
|
|
30
|
+
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = itRegex.exec(content)) !== null) {
|
|
33
|
+
const testTitle = match[2];
|
|
34
|
+
const tagsStr = match[3];
|
|
35
|
+
|
|
36
|
+
// Extract TC-AUTO tags from the tags array string
|
|
37
|
+
const automationIds = [];
|
|
38
|
+
const tagMatches = tagsStr.match(/@?TC-AUTO-[\w.-]+/g) || [];
|
|
39
|
+
tagMatches.forEach(tag => {
|
|
40
|
+
automationIds.push(tag.replace(/['"]/g, ''));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (automationIds.length > 0) {
|
|
44
|
+
titleToAutomationIds.set(testTitle, automationIds);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn(`Could not parse spec file ${specFilePath}:`, err.message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
specTagsCache.set(specFilePath, titleToAutomationIds);
|
|
52
|
+
return titleToAutomationIds;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
|
|
14
56
|
let triggeredBy = null;
|
|
15
57
|
try {
|
|
16
58
|
if (fs.existsSync(triggeredByPath)) {
|
|
17
59
|
triggeredBy = fs.readFileSync(triggeredByPath, "utf-8").trim();
|
|
18
60
|
}
|
|
19
61
|
} catch (err) {
|
|
20
|
-
console.warn("
|
|
62
|
+
console.warn("Could not read triggered-by.txt:", err.message);
|
|
21
63
|
}
|
|
22
64
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const formatted = [];
|
|
26
|
-
const foundIds = new Set();
|
|
27
|
-
|
|
65
|
+
function findJsonFiles(dir) {
|
|
66
|
+
const files = [];
|
|
28
67
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (ids.length > 0) automationIds = new Set(ids);
|
|
68
|
+
const entries = fs.readdirSync(dir);
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.endsWith('.json')) {
|
|
71
|
+
files.push(path.join(dir, entry));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
36
74
|
} catch (err) {
|
|
37
|
-
console.
|
|
38
|
-
"⚠️ No valid automation-cases.json found. Using @regression fallback."
|
|
39
|
-
);
|
|
75
|
+
console.error("Failed to read directory:", dir, err.message);
|
|
40
76
|
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
41
79
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
80
|
+
function parseMochawesomeJson(jsonPath) {
|
|
81
|
+
const results = [];
|
|
82
|
+
const foundIds = new Set();
|
|
83
|
+
let combinedResults = { results: [] };
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
// If specific JSON path provided, use it
|
|
86
|
+
if (jsonPath && fs.existsSync(jsonPath)) {
|
|
87
|
+
try {
|
|
88
|
+
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
89
|
+
if (data?.results) {
|
|
90
|
+
combinedResults.results.push(...data.results);
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error("Failed to parse provided JSON:", err.message);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Fallback: scan mochawesome directory
|
|
97
|
+
const mochawesomeDir = path.resolve("results/mochawesome");
|
|
98
|
+
console.log("Looking for mochawesome JSONs in:", `${mochawesomeDir}/*.json`);
|
|
99
|
+
const jsonFiles = findJsonFiles(mochawesomeDir);
|
|
100
|
+
|
|
101
|
+
if (jsonFiles.length === 0) {
|
|
102
|
+
console.error("No mochawesome report files found.");
|
|
103
|
+
return { results: [], foundIds: new Set() };
|
|
104
|
+
}
|
|
50
105
|
|
|
51
|
-
|
|
52
|
-
const combinedResults = { results: [] };
|
|
106
|
+
console.log(`Found ${jsonFiles.length} mochawesome report(s).`);
|
|
53
107
|
|
|
54
|
-
try {
|
|
55
108
|
for (const file of jsonFiles) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
try {
|
|
110
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
111
|
+
if (data?.results) {
|
|
112
|
+
combinedResults.results.push(...data.results);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error("Failed to parse mochawesome JSON:", err.message);
|
|
59
116
|
}
|
|
60
117
|
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error("❌ Failed to parse mochawesome JSON:", err.message);
|
|
63
|
-
process.exit(1);
|
|
64
118
|
}
|
|
65
119
|
|
|
66
|
-
function walkSuites(suite) {
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
function walkSuites(suite, specFilePath) {
|
|
121
|
+
// Get spec file path from suite or use passed-in path
|
|
122
|
+
const currentSpecPath = suite.fullFile || suite.file || specFilePath;
|
|
123
|
+
collectTests(suite.tests, currentSpecPath);
|
|
124
|
+
(suite.suites || []).forEach(s => walkSuites(s, currentSpecPath));
|
|
69
125
|
}
|
|
70
126
|
|
|
71
|
-
function collectTests(tests) {
|
|
127
|
+
function collectTests(tests, specFilePath) {
|
|
128
|
+
// Get tag mappings from spec file if available
|
|
129
|
+
let specTagMap = new Map();
|
|
130
|
+
if (specFilePath) {
|
|
131
|
+
// Try multiple possible paths for the spec file
|
|
132
|
+
const possiblePaths = [
|
|
133
|
+
specFilePath,
|
|
134
|
+
path.resolve(specFilePath),
|
|
135
|
+
path.resolve('/app', specFilePath),
|
|
136
|
+
path.resolve(process.cwd(), specFilePath)
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const tryPath of possiblePaths) {
|
|
140
|
+
if (fs.existsSync(tryPath)) {
|
|
141
|
+
specTagMap = parseSpecFileForTags(tryPath);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
72
147
|
(tests || []).forEach((test) => {
|
|
148
|
+
const testTitle = test.title;
|
|
73
149
|
const titleCombined =
|
|
74
150
|
typeof test.fullTitle === "string"
|
|
75
151
|
? test.fullTitle
|
|
@@ -77,7 +153,38 @@ try {
|
|
|
77
153
|
? test.title.join(" ")
|
|
78
154
|
: test.title;
|
|
79
155
|
|
|
80
|
-
|
|
156
|
+
// Extract automation IDs from title first
|
|
157
|
+
let automationIdMatch = titleCombined.match(/@?TC-AUTO-[\w.-]+/g) || [];
|
|
158
|
+
|
|
159
|
+
// If not found in title, try looking up from spec file tags
|
|
160
|
+
if (automationIdMatch.length === 0 && specTagMap.has(testTitle)) {
|
|
161
|
+
automationIdMatch = specTagMap.get(testTitle);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Also check mochawesome context for automation IDs (added via addContext)
|
|
165
|
+
if (test.context && automationIdMatch.length === 0) {
|
|
166
|
+
const contextArray = Array.isArray(test.context) ? test.context : [test.context];
|
|
167
|
+
for (const ctx of contextArray) {
|
|
168
|
+
if (ctx && ctx.title === 'automationIds' && Array.isArray(ctx.value)) {
|
|
169
|
+
automationIdMatch = ctx.value.filter(id => id.includes('TC-AUTO'));
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
// Handle string context that might contain JSON
|
|
173
|
+
if (typeof ctx === 'string') {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(ctx);
|
|
176
|
+
if (parsed.title === 'automationIds' && Array.isArray(parsed.value)) {
|
|
177
|
+
automationIdMatch = parsed.value.filter(id => id.includes('TC-AUTO'));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Not JSON, check if it's a direct automation ID
|
|
182
|
+
const match = ctx.match(/@?TC-AUTO-[\w.-]+/g);
|
|
183
|
+
if (match) automationIdMatch = match;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
81
188
|
|
|
82
189
|
const status =
|
|
83
190
|
test.state === "passed" || test.pass === true
|
|
@@ -86,140 +193,110 @@ try {
|
|
|
86
193
|
? "skipped"
|
|
87
194
|
: "fail";
|
|
88
195
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
foundIds.add(tag);
|
|
95
|
-
});
|
|
96
|
-
} else {
|
|
97
|
-
allTags
|
|
98
|
-
.filter((tag) => automationIds.has(tag))
|
|
99
|
-
.forEach((tag) => {
|
|
100
|
-
formatted.push({ automation_id: tag, status });
|
|
101
|
-
foundIds.add(tag);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
196
|
+
automationIdMatch.forEach((tag) => {
|
|
197
|
+
const normalizedTag = tag.startsWith('@') ? tag : '@' + tag;
|
|
198
|
+
results.push({ automation_id: normalizedTag, status });
|
|
199
|
+
foundIds.add(normalizedTag.replace(/^@/, ''));
|
|
200
|
+
});
|
|
104
201
|
});
|
|
105
202
|
}
|
|
106
203
|
|
|
107
|
-
(combinedResults.results || []).forEach(walkSuites);
|
|
204
|
+
(combinedResults.results || []).forEach(suite => walkSuites(suite, null));
|
|
205
|
+
return { results, foundIds };
|
|
206
|
+
}
|
|
108
207
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
208
|
+
async function postJson(url, payload) {
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const data = JSON.stringify(payload);
|
|
211
|
+
const urlObj = new URL(url);
|
|
212
|
+
const options = {
|
|
213
|
+
hostname: urlObj.hostname,
|
|
214
|
+
port: urlObj.port || 443,
|
|
215
|
+
path: urlObj.pathname + urlObj.search,
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: {
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
'Content-Length': Buffer.byteLength(data)
|
|
220
|
+
}
|
|
221
|
+
};
|
|
112
222
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
223
|
+
const req = https.request(options, res => {
|
|
224
|
+
let body = '';
|
|
225
|
+
res.on('data', c => body += c);
|
|
226
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
|
|
227
|
+
});
|
|
228
|
+
req.on('error', reject);
|
|
229
|
+
req.write(data);
|
|
230
|
+
req.end();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
116
233
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
234
|
+
async function main() {
|
|
235
|
+
const planId = process.env.PLAN_ID || 'unknown_plan';
|
|
236
|
+
const runId = process.env.RUN_ID || 'unknown_run';
|
|
237
|
+
const testEnvironment = process.env.TEST_ENVIRONMENT || 'QA';
|
|
238
|
+
const jsonReportPath = process.argv[2];
|
|
121
239
|
|
|
122
|
-
|
|
123
|
-
await postSummary(formatted, notFound, planId, runId, testEnvironment);
|
|
124
|
-
})();
|
|
240
|
+
const { results: formattedResults, foundIds } = parseMochawesomeJson(jsonReportPath);
|
|
125
241
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.log(`🔗 Posting to URL: ${postUrl}`);
|
|
242
|
+
console.log(`Looking for results in: ${jsonReportPath || 'results/mochawesome/*.json'}`);
|
|
243
|
+
console.log(`Found ${formattedResults.length} result(s).`);
|
|
129
244
|
|
|
245
|
+
// not_found from automation-cases.json (optional)
|
|
246
|
+
const normalizeId = (id) => id.replace(/^@/, '');
|
|
247
|
+
let notFoundList = [];
|
|
248
|
+
try {
|
|
249
|
+
const automationCases = JSON.parse(fs.readFileSync(automationCasesPath, 'utf-8'));
|
|
250
|
+
const expected = new Set((automationCases || []).map(a => normalizeId(a.automation_id)));
|
|
251
|
+
notFoundList = [...expected].filter(id => !foundIds.has(id));
|
|
252
|
+
} catch { }
|
|
253
|
+
|
|
254
|
+
// Post results
|
|
255
|
+
const resultsUrl = baseUrl;
|
|
130
256
|
const payload = {
|
|
131
257
|
planId,
|
|
132
258
|
runId,
|
|
133
259
|
environment: testEnvironment,
|
|
134
260
|
results: formattedResults,
|
|
135
261
|
not_found: notFoundList,
|
|
136
|
-
triggered_by: triggeredBy
|
|
262
|
+
triggered_by: triggeredBy
|
|
137
263
|
};
|
|
138
|
-
|
|
139
|
-
console.log(
|
|
140
|
-
"📤 Posting result from sync report:",
|
|
141
|
-
JSON.stringify(payload, null, 2)
|
|
142
|
-
);
|
|
264
|
+
console.log('Posting result payload:', JSON.stringify(payload, null, 2));
|
|
265
|
+
console.log(`Posting to URL: ${resultsUrl}`);
|
|
143
266
|
|
|
144
267
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.
|
|
149
|
-
} catch (err) {
|
|
150
|
-
console.error(
|
|
151
|
-
"❌ Failed to post test case results:",
|
|
152
|
-
err.response?.status,
|
|
153
|
-
err.response?.data || err.message
|
|
154
|
-
);
|
|
155
|
-
process.exit(1);
|
|
268
|
+
const res = await postJson(resultsUrl, payload);
|
|
269
|
+
console.log('Post status:', res.statusCode, res.body);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error('Failed to post results:', e.message);
|
|
156
272
|
}
|
|
157
|
-
}
|
|
158
273
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
274
|
+
// Post summary
|
|
275
|
+
const summaryUrl = baseUrl.replace('/api/test-results', '/api/test-run-summary');
|
|
276
|
+
const passedCount = formattedResults.filter(r => r.status === 'pass').length;
|
|
277
|
+
const failedCount = formattedResults.filter(r => r.status === 'fail').length;
|
|
278
|
+
const skippedCount = formattedResults.filter(r => r.status === 'skipped').length;
|
|
279
|
+
const overallStatus = failedCount > 0 ? 'failed' : (formattedResults.length > 0 ? 'completed' : 'no_tests');
|
|
162
280
|
|
|
163
|
-
const
|
|
164
|
-
(await copyLogToShared(planId, runId)) || "Log copy failed or missing";
|
|
165
|
-
|
|
166
|
-
const summaryPayload = {
|
|
167
|
-
runId,
|
|
281
|
+
const summary = {
|
|
168
282
|
planId,
|
|
283
|
+
runId,
|
|
169
284
|
environment: testEnvironment,
|
|
170
|
-
status:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
failed: results.filter((r) => r.status === "fail").length,
|
|
176
|
-
skipped: results.filter((r) => r.status === "skipped").length,
|
|
177
|
-
not_found: notFound.length,
|
|
178
|
-
},
|
|
285
|
+
status: overallStatus,
|
|
286
|
+
total: formattedResults.length,
|
|
287
|
+
passed: passedCount,
|
|
288
|
+
failed: failedCount,
|
|
289
|
+
skipped: skippedCount
|
|
179
290
|
};
|
|
180
|
-
|
|
181
|
-
console.log(
|
|
182
|
-
console.log("📦 Summary payload:", JSON.stringify(summaryPayload, null, 2));
|
|
291
|
+
console.log(`Posting summary to: ${summaryUrl}`);
|
|
292
|
+
console.log('Summary payload:', JSON.stringify(summary, null, 2));
|
|
183
293
|
|
|
184
294
|
try {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error(
|
|
191
|
-
"❌ Failed to post summary:",
|
|
192
|
-
err.response?.data || err.message
|
|
193
|
-
);
|
|
295
|
+
const res = await postJson(summaryUrl, summary);
|
|
296
|
+
console.log('Summary status:', res.statusCode, res.body);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.error('Failed to post summary:', e.message);
|
|
194
299
|
}
|
|
195
300
|
}
|
|
196
301
|
|
|
197
|
-
|
|
198
|
-
const logsPath = "results/logs.txt";
|
|
199
|
-
|
|
200
|
-
if (!fs.existsSync(logsPath)) {
|
|
201
|
-
console.warn("⚠️ No logs.txt file found to copy.");
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const sharedDir = "/shared/test-logs";
|
|
206
|
-
if (!fs.existsSync(sharedDir)) {
|
|
207
|
-
fs.mkdirSync(sharedDir, { recursive: true });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const destPath = path.join(sharedDir, "run_log.txt");
|
|
212
|
-
fs.copyFileSync(logsPath, destPath);
|
|
213
|
-
console.log(`📁 Logs copied to shared volume: ${destPath}`);
|
|
214
|
-
return destPath;
|
|
215
|
-
} catch (err) {
|
|
216
|
-
console.error("❌ Failed to copy log to shared volume:", err.message);
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function computeOverallStatus(results, notFound) {
|
|
222
|
-
if (!results.length && !notFound.length) return "skipped";
|
|
223
|
-
const hasFail = results.some((r) => r.status === "fail");
|
|
224
|
-
return hasFail ? "failed" : "passed";
|
|
225
|
-
}
|
|
302
|
+
main().catch(e => { console.error(e); process.exit(1); });
|