@thecollege/azure-test-track
Version:
Azure DevOps utilities for test plan and test run management
279 lines (235 loc) • 11.6 kB
JavaScript
const fs = require('fs');
const xml2js = require('xml2js');
const logger = require('../lib/logger');
function readAndProcessJUnitXML(filePath) {
logger.info("Reading and processing XML file...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading XML file:", err);
return reject(err);
}
xml2js.parseString(data, (err, result) => {
if (err) {
logger.error("Error parsing XML:", err);
return reject(err);
}
if (!result || !result.testsuites || !result.testsuites.testsuite) {
return resolve([]);
}
const newResultsData = result.testsuites.testsuite.flatMap(suite => {
if (!suite.testcase || !Array.isArray(suite.testcase)) {
return [];
}
return suite.testcase.map(testcase => {
const testName = testcase.$.name;
const testCaseIdMatch = testName.match(/TC_(\d+)/);
const testCaseId = testCaseIdMatch ? parseInt(testCaseIdMatch[1]) : null;
// Extract execution time from the 'time' attribute (in seconds)
const executionTimeInSeconds = testcase.$.time ? parseFloat(testcase.$.time) : 0;
// Convert to milliseconds and round to integer
const executionTime = Math.round(executionTimeInSeconds * 1000);
let outcome = "Passed";
if (testcase.failure || testcase.error) {
outcome = "Failed";
} else if (testcase.skipped) {
outcome = "Skipped";
}
return testCaseId ? { testCaseId, outcome, executionTime } : null;
})
}
).filter(Boolean);
resolve(newResultsData);
});
});
});
}
function readAndProcessJUnitXMLUsingTestInfo(filePath) {
logger.info("Reading and processing XML file using TestCaseId from properties...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading XML file:", err);
return reject(err);
}
xml2js.parseString(data, (err, result) => {
if (err) {
logger.error("Error parsing XML:", err);
return reject(err);
}
if (!result || !result.testsuites || !result.testsuites.testsuite) {
return resolve([]);
}
const newResultsData = result.testsuites.testsuite.flatMap(suite => {
if (!suite.testcase || !Array.isArray(suite.testcase)) {
return [];
}
return suite.testcase.flatMap(testcase => {
// Determine the outcome first
let outcome = "Passed";
if (testcase.failure || testcase.error) {
outcome = "Failed";
} else if (testcase.skipped) {
outcome = "Skipped";
}
// Extract execution time from the 'time' attribute (in seconds)
const executionTimeInSeconds = testcase.$.time ? parseFloat(testcase.$.time) : 0;
// Convert to milliseconds and round to integer
const executionTime = Math.round(executionTimeInSeconds * 1000);
// Extract TestCaseId from properties
const testCaseIds = [];
if (testcase.properties && Array.isArray(testcase.properties)) {
testcase.properties.forEach(propertiesBlock => {
if (propertiesBlock.property && Array.isArray(propertiesBlock.property)) {
propertiesBlock.property.forEach(prop => {
if (prop.$ && (prop.$.name === 'TestCaseId' || prop.$.name === 'testCaseId')) {
const value = prop.$.value;
if (value) {
const testCaseId = parseInt(value);
if (!isNaN(testCaseId)) {
testCaseIds.push(testCaseId);
}
}
}
});
}
});
}
return testCaseIds.map(testCaseId => ({ testCaseId, outcome, executionTime }));
});
}).filter(Boolean);
resolve(newResultsData);
});
});
});
}
function readAndProcessCucumberJSON(filePath) {
logger.info("Reading and processing Cucumber JSON file...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading JSON file:", err);
return reject(err);
}
try {
const parsedData = JSON.parse(data);
const results = parsedData.flatMap(feature => {
if (!feature.elements || !Array.isArray(feature.elements)) {
return [];
}
return feature.elements.map(scenario => {
const scenarioName = scenario.name || "Unnamed Scenario";
const testCaseIdMatch = scenarioName.match(/TC_(\d+)/);
const testCaseId = testCaseIdMatch ? parseInt(testCaseIdMatch[1]) : null;
const statuses = scenario.steps.map(step => step.result?.status || "unknown");
const outcome = statuses.includes("failed") ? "Failed" : "Passed";
return testCaseId ? { testCaseId, outcome } : null;
});
}).filter(Boolean);
resolve(results);
} catch (parseError) {
logger.error("Error parsing JSON:", parseError);
reject(parseError);
}
});
});
}
function readAndProcessPlaywrightJSON(filePath) {
logger.info("Reading and extracting test results...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading JSON file:", err);
return reject(err);
}
try {
const parsedData = JSON.parse(data);
const results = parsedData.suites.flatMap(suite => {
return suite.specs.map(spec => {
const testCaseIdMatch = spec.title.match(/TC_(\d+)/);
const testCaseId = testCaseIdMatch ? parseInt(testCaseIdMatch[1]) : null;
const outcome = spec.tests.every(test =>
test.results.every(result => result.status === "passed")
) ? "Passed" : "Failed";
return testCaseId ? { testCaseId, outcome } : null;
});
}).filter(Boolean);
resolve(results);
} catch (parseError) {
logger.error("Error parsing JSON:", parseError);
reject(parseError);
}
});
});
}
function readAndProcessPlaywrightJSONUsingTestInfo(filePath) {
logger.info("Reading and extracting test results using TestInfo annotations...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading JSON file:", err);
return reject(err);
}
try {
const parsedData = JSON.parse(data);
const results = parsedData.suites.flatMap(suite => {
return suite.specs.flatMap(spec => {
// Determine the outcome based on test results
const outcome = spec.tests.every(test =>
test.results.every(result => result.status === "passed")
) ? "Passed" : "Failed";
// Extract TestCaseId from annotations
const testCaseIds = [];
spec.tests.forEach(test => {
if (test.annotations && Array.isArray(test.annotations)) {
test.annotations.forEach(annotation => {
if (annotation.type === 'TestCaseId' && annotation.description) {
const testCaseId = parseInt(annotation.description);
if (!isNaN(testCaseId) && !testCaseIds.includes(testCaseId)) {
testCaseIds.push(testCaseId);
}
}
});
}
});
// Return one result object per TestCaseId found
return testCaseIds.map(testCaseId => ({ testCaseId, outcome }));
});
}).filter(Boolean);
resolve(results);
} catch (parseError) {
logger.error("Error parsing JSON:", parseError);
reject(parseError);
}
});
});
}
function getJUnitTotalDurationMs(filePath) {
logger.info("Reading testsuites total time from JUnit XML file...");
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) {
logger.error("Error reading XML file:", err);
return reject(err);
}
xml2js.parseString(data, (err, result) => {
if (err) {
logger.error("Error parsing XML:", err);
return reject(err);
}
const time = result?.testsuites?.$?.time;
const totalMs = time ? Math.round(parseFloat(time) * 1000) : 0;
logger.debug("Testsuites total time (ms):", totalMs);
resolve(totalMs);
});
});
});
}
module.exports = {
readAndProcessJUnitXML,
readAndProcessJUnitXMLUsingTestInfo,
readAndProcessCucumberJSON,
readAndProcessPlaywrightJSON,
readAndProcessPlaywrightJSONUsingTestInfo,
getJUnitTotalDurationMs
};