mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 18:08:54 +00:00
Retire legacy maestro per-platform contexts from cancel-e2e-runs and document the canonical PR, master, and CMT status context mapping. Wire Detox + Maestro (PR/main/CMT) into Test System IO with a single commit status (e2e/mobile; CMT uses e2e/compatibility-matrix-testing) and groups mobile-pr / mobile-main / cmt-mobile. Upload shard results with correct framework labels, merge Jest shards into TSIO Detox shape, stub missing/dead shards as failures, and post channel summaries after status settles. Matterwick MASTER is treated as MAIN for mobile-main routing. Also harden report/fail paths, stage Maestro screenshots per flow for TSIO galleries, fix a few Detox setup/testID issues, and skip known platform flakes so the pipeline stays actionable while TSIO lands.
176 lines
5.1 KiB
JavaScript
176 lines
5.1 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/* eslint-disable no-console, no-process-env */
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const {parseMaestroReport} = require('./maestro_report');
|
|
const {createTestCycle, createTestExecutions} = require('./test_cases');
|
|
|
|
const MM_T_TAG = /^MM-T\d+(?:_\d+)?$/;
|
|
|
|
function normalizeFlowPath(flowPath) {
|
|
return String(flowPath || '').replace(/\\/g, '/');
|
|
}
|
|
|
|
function readTagsFromFlowYaml(content) {
|
|
const tags = [];
|
|
let inTags = false;
|
|
|
|
for (const line of content.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith('tags:')) {
|
|
inTags = true;
|
|
continue;
|
|
}
|
|
|
|
if (inTags) {
|
|
if (trimmed.startsWith('- ')) {
|
|
const tag = trimmed.slice(2).split(/\s+/)[0];
|
|
if (MM_T_TAG.test(tag)) {
|
|
tags.push(tag);
|
|
}
|
|
} else if (trimmed && !line.startsWith(' ')) {
|
|
inTags = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return tags;
|
|
}
|
|
|
|
function resolveMaestroFlowsRoots(repoRoot) {
|
|
const candidates = [
|
|
path.join(repoRoot, 'e2e/maestro/flows'),
|
|
path.join(repoRoot, 'detox/maestro/flows'),
|
|
path.join(repoRoot, 'maestro/flows'),
|
|
];
|
|
return candidates.filter((dir) => fs.existsSync(dir));
|
|
}
|
|
|
|
function buildFlowTagsMap(repoRoot) {
|
|
const map = {};
|
|
|
|
for (const flowsRoot of resolveMaestroFlowsRoots(repoRoot)) {
|
|
const walk = (dir) => {
|
|
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walk(fullPath);
|
|
continue;
|
|
}
|
|
|
|
if (!entry.name.endsWith('.yml')) {
|
|
continue;
|
|
}
|
|
|
|
const tags = readTagsFromFlowYaml(fs.readFileSync(fullPath, 'utf-8'));
|
|
if (!tags.length) {
|
|
continue;
|
|
}
|
|
|
|
const relativePath = normalizeFlowPath(path.relative(repoRoot, fullPath));
|
|
const underFlows = normalizeFlowPath(path.relative(flowsRoot, fullPath));
|
|
map[relativePath] = tags;
|
|
|
|
// Alias keys so JUnit `file=` values from either layout still resolve.
|
|
map[underFlows] = tags;
|
|
map[`flows/${underFlows}`] = tags;
|
|
map[entry.name] = tags;
|
|
}
|
|
};
|
|
|
|
walk(flowsRoot);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
function extractTestCaseKey(tag) {
|
|
const match = tag.match(/(MM-T\d+)/);
|
|
return match ? match[0] : null;
|
|
}
|
|
|
|
function maestroSummaryToAllTests(summary, repoRoot) {
|
|
const tagsByFlow = buildFlowTagsMap(repoRoot);
|
|
let incrementalDuration = 0;
|
|
const tests = [];
|
|
|
|
for (const flow of summary.flows) {
|
|
const normalizedFile = normalizeFlowPath(flow.file);
|
|
const basename = path.basename(normalizedFile);
|
|
const tags = tagsByFlow[normalizedFile] ||
|
|
tagsByFlow[normalizedFile.split('flows/').pop()] ||
|
|
tagsByFlow[`flows/${normalizedFile.split('flows/').pop()}`] ||
|
|
tagsByFlow[basename] ||
|
|
[];
|
|
|
|
const keys = [...new Set(
|
|
tags.map(extractTestCaseKey).filter(Boolean),
|
|
)];
|
|
|
|
if (!keys.length) {
|
|
console.log(`Maestro Zephyr: skipping flow without MM-T tag (${flow.name || normalizedFile})`);
|
|
continue;
|
|
}
|
|
|
|
let state = 'failed';
|
|
if (flow.status === 'passed') {
|
|
state = 'passed';
|
|
} else if (flow.status === 'skipped') {
|
|
state = 'skipped';
|
|
}
|
|
const durationMs = Math.round((flow.time || 0) * 1000);
|
|
incrementalDuration += durationMs;
|
|
|
|
for (const key of keys) {
|
|
tests.push({
|
|
name: `${key} - ${basename || flow.name}`,
|
|
time: durationMs,
|
|
incrementalDuration,
|
|
state,
|
|
pass: state === 'passed',
|
|
fail: state === 'failed',
|
|
pending: state === 'skipped',
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
start: summary.stats.start,
|
|
end: summary.stats.end,
|
|
tests,
|
|
};
|
|
}
|
|
|
|
async function saveMaestroTestCases(xmlPath, repoRoot = path.join(__dirname, '../..')) {
|
|
const summary = parseMaestroReport(xmlPath);
|
|
if (!summary) {
|
|
console.log('Maestro Zephyr: no report to save');
|
|
return null;
|
|
}
|
|
|
|
const allTests = maestroSummaryToAllTests(summary, repoRoot);
|
|
if (!allTests.tests.length) {
|
|
console.log('Maestro Zephyr: no MM-T tagged flows found in report');
|
|
return null;
|
|
}
|
|
|
|
const testCycle = await createTestCycle(allTests.start, allTests.end, {framework: 'Maestro'});
|
|
if (!testCycle?.key) {
|
|
console.log('Maestro Zephyr: failed to create test cycle');
|
|
return null;
|
|
}
|
|
|
|
await createTestExecutions(allTests, testCycle, {framework: 'Maestro'});
|
|
return testCycle;
|
|
}
|
|
|
|
module.exports = {
|
|
buildFlowTagsMap,
|
|
maestroSummaryToAllTests,
|
|
saveMaestroTestCases,
|
|
};
|