Skip to content

Commit 79ad6ba

Browse files
chore(ci): e2e report improve (#2355)
Description Improve the E2E messenger report generated by CI and reorganize the report scripts so the same logic is easier to follow. User-facing changes: Add a Reason column to the failed-tests thread table and rename Test group to Tests. Link cluster names in failed-tests thread replies to the matching workflow/job URL. Hide the Errors column in the main test-results table when there are no Ginkgo errors. Calculate Success Rate from executed specs only: passed / (passed + failed + errors). Add table header emojis to the main report. Use the full Ginkgo Failure.Message text as the failed-test reason. Stop wrapping *exec.ExitError with %w in test/e2e/internal/framework/ssh.go so Ginkgo no longer dumps Go internals into the report when an SSH command fails. Capture Ginkgo stdout/stderr in the reusable pipeline and parse it as a fallback report source so suite-level failures such as SynchronizedBeforeSuite are still surfaced when the JSON report is missing or contains only setup failure data. Internal refactors (no behavior change, covered by existing tests): Move the shared withTempDir / createCore jest helpers into shared/test-utils.js to remove duplication across three test files. Extract escapeRegExp in shared/report-model.js; consolidate buildStatusMessage into a status → template lookup table; remove a redundant ternary in buildClusterStatus. Unify the Ginkgo source lookup (findGinkgoReport / findGinkgoOutput → one findGinkgoSource) and call parseGinkgoFile once with the descriptor picked up front. Iterate over a list of required keys in requireClusterReportConfig instead of three identical if blocks. Pre-filter cluster reports once by storage key and merge renderClusterFailuresSection / renderMissingReportsSection into a single renderBulletSection helper. Output stays byte-identical; verified by existing messenger-report tests. Pass the loop credentials object as-is to postToLoopApi so the two call sites collapse to one-liners. --------- Signed-off-by: Nikita Korolev <nikita.korolev@flant.com>
1 parent 7903b08 commit 79ad6ba

11 files changed

Lines changed: 871 additions & 325 deletions

File tree

.github/scripts/js/e2e/report/cluster-report.js

Lines changed: 112 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
const fs = require("fs");
1414

1515
const { findSingleMatchingFile } = require("./shared/fs-utils");
16-
const { parseGinkgoReport } = require("./shared/ginkgo-report-utils");
16+
const {
17+
parseGinkgoOutput,
18+
parseGinkgoReport,
19+
} = require("./shared/ginkgo-report-utils");
1720
const {
1821
archivedReportPattern,
1922
buildClusterStatus,
2023
buildReportSummary,
2124
buildTestStatus,
25+
ginkgoOutputPattern,
2226
reportFileName,
2327
zeroMetrics,
2428
} = require("./shared/report-model");
@@ -83,17 +87,13 @@ function readClusterReportConfigFromEnv(env = process.env) {
8387
};
8488
}
8589

86-
function requireClusterReportConfig(config) {
87-
if (!config.storageType) {
88-
throw new Error("buildClusterReport requires storageType");
89-
}
90+
const requiredClusterReportConfigKeys = ["storageType", "reportsDir", "reportFile"];
9091

91-
if (!config.reportsDir) {
92-
throw new Error("buildClusterReport requires reportsDir");
93-
}
94-
95-
if (!config.reportFile) {
96-
throw new Error("buildClusterReport requires reportFile");
92+
function requireClusterReportConfig(config) {
93+
for (const key of requiredClusterReportConfigKeys) {
94+
if (!config[key]) {
95+
throw new Error(`buildClusterReport requires ${key}`);
96+
}
9797
}
9898

9999
return { ...config };
@@ -168,42 +168,110 @@ async function readStageJobUrlsFromApi(github, context, config, core) {
168168
return stageJobUrls;
169169
}
170170

171-
function findGinkgoReport(config) {
172-
const rawReportPattern = archivedReportPattern(config.storageType);
171+
/**
172+
* Builds a parsed-report payload used as a placeholder when no source data
173+
* is available, so the downstream report builder can keep working with a
174+
* uniform shape.
175+
*
176+
* @param {string} source Source label to record on the placeholder.
177+
* @returns {{
178+
* metrics: ReturnType<typeof zeroMetrics>,
179+
* failedTests: string[],
180+
* failedTestDetails: Array<{name: string, reason: string}>,
181+
* startedAt: null,
182+
* source: string,
183+
* }} Empty parsed-report payload.
184+
*/
185+
function emptyParsedReport(source) {
186+
return {
187+
metrics: zeroMetrics(),
188+
failedTests: [],
189+
failedTestDetails: [],
190+
startedAt: null,
191+
source,
192+
};
193+
}
194+
195+
const ginkgoJsonSource = {
196+
label: "Ginkgo JSON report",
197+
okSource: "ginkgo-json",
198+
invalidSource: "ginkgo-json-invalid",
199+
parse: parseGinkgoReport,
200+
pattern: archivedReportPattern,
201+
};
202+
203+
const ginkgoOutputSource = {
204+
label: "Ginkgo output log",
205+
okSource: "ginkgo-output",
206+
invalidSource: "ginkgo-output-invalid",
207+
parse: parseGinkgoOutput,
208+
pattern: ginkgoOutputPattern,
209+
};
173210

211+
/**
212+
* @typedef {Object} GinkgoSourceDescriptor
213+
* @property {string} label Human-readable source name for log lines and warnings.
214+
* @property {string} okSource Source tag stored on a successful parse result.
215+
* @property {string} invalidSource Source tag stored when parsing fails.
216+
* @property {function(string): {
217+
* metrics: ReturnType<typeof zeroMetrics>,
218+
* failedTests: string[],
219+
* failedTestDetails: Array<{name: string, reason: string}>,
220+
* startedAt: string|null,
221+
* }} parse Parser function for the source content.
222+
* @property {function(string): RegExp} pattern Builds the file-name regex for the source.
223+
*/
224+
225+
/**
226+
* Locates a single Ginkgo source file (JSON report or stdout fallback log)
227+
* for the configured storage type. Throws when more than one matching file
228+
* exists; returns null when none is found.
229+
*
230+
* @param {ClusterReportConfig} config Resolved cluster report config.
231+
* @param {GinkgoSourceDescriptor} source Source descriptor.
232+
* @returns {string|null} Path to the source file, or null when none exists.
233+
*/
234+
function findGinkgoSource(config, source) {
174235
return findSingleMatchingFile(
175236
config.reportsDir,
176-
rawReportPattern,
177-
"Ginkgo JSON report"
237+
source.pattern(config.storageType),
238+
source.label
178239
);
179240
}
180241

181-
function parseGinkgoReportFile(rawReportPath, core) {
182-
if (!rawReportPath) {
183-
return {
184-
metrics: zeroMetrics(),
185-
failedTests: [],
186-
startedAt: null,
187-
source: "empty",
188-
};
242+
/**
243+
* Reads and parses a Ginkgo source file (JSON report or stdout log) using
244+
* the provided source descriptor. Returns an empty placeholder when the
245+
* file path is missing or the parser throws, so the caller always receives
246+
* a consistent parsed-report shape.
247+
*
248+
* @param {string|null} filePath Path to the source file, or null/empty.
249+
* @param {ClusterReportCore} core GitHub Actions core API.
250+
* @param {GinkgoSourceDescriptor} source Source descriptor.
251+
* @returns {{
252+
* metrics: ReturnType<typeof zeroMetrics>,
253+
* failedTests: string[],
254+
* failedTestDetails: Array<{name: string, reason: string}>,
255+
* startedAt: string|null,
256+
* source: string,
257+
* }} Parsed report payload with a source tag.
258+
*/
259+
function parseGinkgoFile(filePath, core, source) {
260+
if (!filePath) {
261+
return emptyParsedReport("empty");
189262
}
190263

191-
core.info(`Found Ginkgo JSON report: ${rawReportPath}`);
264+
core.info(`Found ${source.label}: ${filePath}`);
192265
try {
193266
return {
194-
...parseGinkgoReport(fs.readFileSync(rawReportPath, "utf8")),
195-
source: "ginkgo-json",
267+
...source.parse(fs.readFileSync(filePath, "utf8")),
268+
source: source.okSource,
196269
};
197270
} catch (error) {
198271
core.warning(
199-
`Unable to parse Ginkgo JSON report ${rawReportPath}: ${error.message}`
272+
`Unable to parse ${source.label} ${filePath}: ${error.message}`
200273
);
201-
return {
202-
metrics: zeroMetrics(),
203-
failedTests: [],
204-
startedAt: null,
205-
source: "ginkgo-json-invalid",
206-
};
274+
return emptyParsedReport(source.invalidSource);
207275
}
208276
}
209277

@@ -213,7 +281,7 @@ function buildReportPayload({
213281
fallbackWorkflowRunUrl,
214282
branchName,
215283
parsedReport,
216-
rawReportPath,
284+
sourcePath,
217285
}) {
218286
const clusterStatus = buildClusterStatus(config.stageResults);
219287
const testStatus = buildTestStatus(
@@ -251,7 +319,8 @@ function buildReportPayload({
251319
startedAt: parsedReport.startedAt,
252320
metrics: parsedReport.metrics,
253321
failedTests: parsedReport.failedTests,
254-
sourceReport: rawReportPath,
322+
failedTestDetails: parsedReport.failedTestDetails,
323+
sourceReport: sourcePath,
255324
reportSource: parsedReport.source,
256325
};
257326
}
@@ -317,22 +386,27 @@ async function buildClusterReport({ core, context, github, config } = {}) {
317386

318387
const fallbackWorkflowRunUrl = getWorkflowRunUrl(context);
319388
const branchName = getBranchName(context);
320-
const rawReportPath = findGinkgoReport(resolvedConfig);
389+
const rawReportPath = findGinkgoSource(resolvedConfig, ginkgoJsonSource);
390+
const outputPath = rawReportPath
391+
? null
392+
: findGinkgoSource(resolvedConfig, ginkgoOutputSource);
393+
const sourcePath = rawReportPath || outputPath;
394+
const sourceDescriptor = rawReportPath ? ginkgoJsonSource : ginkgoOutputSource;
321395

322396
if (!rawReportPath) {
323397
core.warning(
324398
`Ginkgo JSON report was not found for ${resolvedConfig.storageType} under ${resolvedConfig.reportsDir}`
325399
);
326400
}
327401

328-
const parsedReport = parseGinkgoReportFile(rawReportPath, core);
402+
const parsedReport = parseGinkgoFile(sourcePath, core, sourceDescriptor);
329403
const report = buildReportPayload({
330404
config: resolvedConfig,
331405
context,
332406
fallbackWorkflowRunUrl,
333407
branchName,
334408
parsedReport,
335-
rawReportPath,
409+
sourcePath,
336410
});
337411

338412
try {

0 commit comments

Comments
 (0)