diff --git a/TestResultSummaryService/Database.js b/TestResultSummaryService/Database.js index c7ba5906..7fbf53ae 100644 --- a/TestResultSummaryService/Database.js +++ b/TestResultSummaryService/Database.js @@ -131,7 +131,13 @@ class Database { } // ToDo: impl check can be added once we have impl stored - async getTotals(query) { + /** + * Base function for aggregating data from testResults. + * @param {Object} query - The query object. + * @param {string} type - The type of request (e.g., 'totals', 'rerunDetails', 'jobsDetails'). + * @returns {Object} - The aggregation result. + */ + async testResultsBaseAggregation(query, type) { const url = query.url; const buildName = query.buildName; let id = query.id; @@ -159,27 +165,61 @@ class Database { if (query.platform) buildNameRegex = `${buildNameRegex}${query.platform}`; - const result = await this.aggregate([ - { $match: matchQuery }, - { - $graphLookup: { - from: 'testResults', - startWith: '$_id', - connectFromField: '_id', - connectToField: 'parentId', - as: 'childBuilds', + // Call the routing function to get the specific aggregation + const specificAggregation = this.getSpecificAggregation(type, buildNameRegex, query); + + try { + const result = await this.aggregate([ + { $match: matchQuery }, + { + $graphLookup: { + from: 'testResults', + startWith: '$_id', + connectFromField: '_id', + connectToField: 'parentId', + as: 'childBuilds', + }, }, - }, - { - $project: { - childBuilds: '$childBuilds', + { + $project: { + childBuilds: '$childBuilds', + }, }, - }, + ...specificAggregation, + ]); + return result[0] || {}; + } catch (error) { + console.error('Error:', error); + } + } + + /** + * Routing function to get the specific aggregation based on the type. + * @param {string} type - The type of request (e.g., 'totals', 'rerunDetails', 'jobsDetails'). + * @param {string} buildNameRegex - The build name regex. + * @param {Object} query - The query object. + * @returns {Array} - The specific aggregation steps. + */ + getSpecificAggregation(type, buildNameRegex, query) { + switch (type) { + case 'totals': + return this.getTotalsAggregation(query, buildNameRegex); + case 'rerunDetails': + return this.getRerunDetailsAggregation(); + case 'jobsDetails': + return this.getJobsDetailsAggregation(); + default: + throw new Error(`Unknown type: ${type}`); + } + } + + getTotalsAggregation(query, buildNameRegex) { + return [ { $unwind: '$childBuilds' }, { $match: { 'childBuilds.buildName': { $regex: buildNameRegex } } }, { $group: { - _id: id, + _id: query.id, total: { $sum: '$childBuilds.testSummary.total' }, executed: { $sum: '$childBuilds.testSummary.executed' }, passed: { $sum: '$childBuilds.testSummary.passed' }, @@ -188,8 +228,153 @@ class Database { skipped: { $sum: '$childBuilds.testSummary.skipped' }, }, }, - ]); - return result[0] || {}; + ]; + } + + getRerunDetailsAggregation() { + return [ + { + $addFields: { + manual_rerun_needed_list: { + $filter: { + input: '$childBuilds', + as: 'build', + cond: { + $and: [ + { $in: ['$$build.buildResult', ['UNSTABLE', 'FAILED', 'ABORTED']] }, + { + $or: [ + { + $regexMatch: { input: '$$build.buildName', regex: /_rerun$/ } + }, + { + // If buildName does not end with '_rerun', check if another build with the same name exists that ends with '_rerun' + $and: [ + { $not: [{ $regexMatch: { input: '$$build.buildName', regex: /_rerun$/ } }] }, + { + // Veriy that there is no '_rerun' build for this build + $eq: [ + { + $size: { + $filter: { + input: '$childBuilds', + as: 'internal_build', + cond: { + $and: [ + { + $regexMatch: { + input: '$$internal_build.buildName', + regex: { $concat: ['^', '$$build.buildName'] } + } + }, + { $regexMatch: { input: '$$internal_build.buildName', regex: /_rerun$/ } } + ] + } + } + } + }, + 0 + ] + } + ] + } + ] + } + ] + }, + } + } + }, + }, + { + $addFields: { + manual_rerun_needed: { + $size: + '$manual_rerun_needed_list' + } + }, + }, + { + $addFields: { + tests_needed_manual_rerun: { + $reduce: { + input: '$manual_rerun_needed_list', + initialValue: 0, + in: { + $add: [ + '$$value', + { $ifNull: ['$$this.testSummary.failed', 0] } + ] + } + } + } + } + }, + { + $addFields: { + manual_rerun_needed_regex: { + $concat: [ + '^(', + { + $reduce: { + input: '$manual_rerun_needed_list', + initialValue: '', + in: { + $cond: [ + { $eq: ['$$value', ''] }, // If the first item + { $replaceAll: { input: '$$this.buildName', find: '.', replacement: '\\.' } }, + { + $concat: [ + '$$value', + '|', + { $replaceAll: { input: '$$this.buildName', find: '.', replacement: '\\.' } } + ] + } + ] + } + } + }, + ')$' + ] + } + } + }, + { $unset: ['childBuilds', 'manual_rerun_needed_list'] } + ]; + } + + getJobsDetailsAggregation() { + return [ + { + $addFields: { + job_success_rate: { + $round: [ + { + $multiply: [ + { + $divide: [ + { + $size: { + $filter: { + input: "$childBuilds", + as: "build", + cond: { $eq: ["$$build.buildResult", "SUCCESS"] } + } + } + }, + { $size: "$childBuilds" } + ] + }, + 100 + ] + }, + 2 + ] + } + } + }, + { $unset: ['childBuilds'] } + ]; } async getRootBuildId(id) { diff --git a/TestResultSummaryService/routes/GetFailedTestByMachine.js b/TestResultSummaryService/routes/GetFailedTestByMachine.js new file mode 100644 index 00000000..73cc391d --- /dev/null +++ b/TestResultSummaryService/routes/GetFailedTestByMachine.js @@ -0,0 +1,58 @@ +const { TestResultsDB } = require('../Database'); + +module.exports = async (req, res) => { + try { + const { parentId } = req.query; + + if (!parentId) { + return res.status(400).send({ error: 'parentId is required' }); + } + + const db = new TestResultsDB(); + + // Fetch all tests and their machines for the specified parentId + const buildData = await db.getSpecificData( + { parentId: parentId }, + { tests: 1, machine: 1 } + ); + + if (!buildData || buildData.length === 0) { + return res + .status(404) + .send({ error: 'No builds found for the given parentId' }); + } + + // Initialize a map to store machine names and their respective failed test counts + const failedTestsByMachine = {}; + + // Iterate through all builds and process tests + for (const build of buildData) { + const { tests, machine } = build; + + if (tests && Array.isArray(tests)) { + for (const test of tests) { + if (test.testResult === 'FAILED') { + // Accumulate failure count for the machine + failedTestsByMachine[machine] = + (failedTestsByMachine[machine] || 0) + 1; + } + } + } + } + + // Format the result as an array of dictionaries with "machine" and "failedTest" + const formattedResult = Object.entries(failedTestsByMachine) + .map(([machine, failedTests]) => ({ + machine, + failedTests, + })) + .sort((a, b) => b.failedTests - a.failedTests) // Sort in descending order + .slice(0, 3); // Keep only the top 3 machines + + // Send the formatted result as the response + res.send(formattedResult); + } catch (error) { + console.error('Error processing request:', error); + res.status(500).send({ error: 'Internal Server Error' }); + } +}; diff --git a/TestResultSummaryService/routes/getJobsDetails.js b/TestResultSummaryService/routes/getJobsDetails.js new file mode 100644 index 00000000..7ef0347f --- /dev/null +++ b/TestResultSummaryService/routes/getJobsDetails.js @@ -0,0 +1,18 @@ +const { TestResultsDB } = require('../Database'); + +/** + * getJobsDetails returns jobs details + * @route GET /api/getJobsDetails + * @group Test - Operations about test + * @param {number} id Optional. + * @param {string} url Optional. If provided, it has to be used with buildName and buildNum + * @param {string} buildName Optional. If provided, it has to be used with url and buildNum + * @param {string} buildNum Optional. If provided, it has to be used with url and buildName + * @return {object} {job_success_rate} + */ + +module.exports = async (req, res) => { + const testResultsDB = new TestResultsDB(); + const result = await testResultsDB.testResultsBaseAggregation(req.query, 'jobsDetails'); + res.send(result); +}; diff --git a/TestResultSummaryService/routes/getRerunDetails.js b/TestResultSummaryService/routes/getRerunDetails.js new file mode 100644 index 00000000..ea600159 --- /dev/null +++ b/TestResultSummaryService/routes/getRerunDetails.js @@ -0,0 +1,18 @@ +const { TestResultsDB } = require('../Database'); + +/** + * getRerunDetails returns rerun summary + * @route GET /api/getRerunDetails + * @group Test - Operations about test + * @param {number} id Optional. + * @param {string} url Optional. If provided, it has to be used with buildName and buildNum + * @param {string} buildName Optional. If provided, it has to be used with url and buildNum + * @param {string} buildNum Optional. If provided, it has to be used with url and buildName + * @return {object} {manual_rerun_needed, tests_needed_manual_rerun, manual_rerun_needed_regex} + */ + +module.exports = async (req, res) => { + const testResultsDB = new TestResultsDB(); + const result = await testResultsDB.testResultsBaseAggregation(req.query, 'rerunDetails'); + res.send(result); +}; diff --git a/TestResultSummaryService/routes/getTotals.js b/TestResultSummaryService/routes/getTotals.js index 1fd4c5c4..005b88f6 100644 --- a/TestResultSummaryService/routes/getTotals.js +++ b/TestResultSummaryService/routes/getTotals.js @@ -13,6 +13,6 @@ const { TestResultsDB, ObjectID } = require('../Database'); module.exports = async (req, res) => { const testResultsDB = new TestResultsDB(); - const result = await testResultsDB.getTotals(req.query); + const result = await testResultsDB.testResultsBaseAggregation(req.query, 'totals'); res.send(result); }; diff --git a/TestResultSummaryService/routes/index.js b/TestResultSummaryService/routes/index.js index 7eb38fcc..a0f4ad8c 100644 --- a/TestResultSummaryService/routes/index.js +++ b/TestResultSummaryService/routes/index.js @@ -51,6 +51,9 @@ app.get('/getFeedbackUrl', require('./getFeedbackUrl')); app.get('/rescanBuild', require('./rescanBuild')); app.get('/testParserViaFile', require('./test/testParserViaFile')); app.get('/testParserViaLogStream', require('./test/testParserViaLogStream')); +app.get('/getRerunDetails', require('./getRerunDetails')); +app.get('/getJobsDetails', require('./getJobsDetails')); +app.get('/GetFailedTestByMachine', require('./GetFailedTestByMachine')); app.get('/updateComments', require('./updateComments')); app.get('/updateKeepForever', require('./updateKeepForever')); diff --git a/test-result-summary-client/src/Build/BuildLink.jsx b/test-result-summary-client/src/Build/BuildLink.jsx index 715bc6e6..8699c6de 100644 --- a/test-result-summary-client/src/Build/BuildLink.jsx +++ b/test-result-summary-client/src/Build/BuildLink.jsx @@ -13,7 +13,7 @@ const BuildLink = ({ if (id) { return ( - {label} + {label}
- - - Test Summary - - - Pass Percentage - - - Build Result - - - Build Metadata - - - - + + + + Test Summary + -
- -
- - + + + + Tests Success Rate:{' '} + {passPercentage ? passPercentage.toFixed(2) + '%' @@ -188,7 +190,70 @@ export default class Overview extends Component {
- + + + Test Failures By Machine + + {machineFailures && + machineFailures.length > 0 ? ( + machineFailures.map( + ({ machine, failedTests }) => ( +
+ {machine}: Failures:{' '} + {failedTests} +
+ ) + ) + ) : ( +
No Machine Failures
+ )} + + + + + Additional Metrics + + +
+ + + Manual Rerun Targets Involved: + + {tests_needed_manual_rerun} + +
+
+ + + Job Success Rate: + {job_success_rate + ? job_success_rate.toFixed(2) + + '%' + : 'N/A'} + + +
+ + + + + Build Result +
Build Started at:{' '} {moment(parentBuildInfo.timestamp).format( @@ -211,9 +276,15 @@ export default class Overview extends Component {
- + + + Build Metadata +
- java -version:{' '} + java -version:
{javaVersion}
diff --git a/test-result-summary-client/src/Build/Summary/ResultSummary.jsx b/test-result-summary-client/src/Build/Summary/ResultSummary.jsx index bad283aa..f0da0719 100644 --- a/test-result-summary-client/src/Build/Summary/ResultSummary.jsx +++ b/test-result-summary-client/src/Build/Summary/ResultSummary.jsx @@ -28,6 +28,7 @@ const hcvalues = { export default function ResultSummary() { const location = useLocation(); + const [state, setState] = useState({ selectedPlatforms: [], allPlatforms: [], @@ -38,6 +39,9 @@ export default function ResultSummary() { sdkBuilds: [], buildMap: {}, summary: {}, + machinesData: {}, + rerunSummary: {}, + jobsDetailsSummary: {}, parentBuildInfo: {}, childBuildsResult: 'UNDEFINED', javaVersion: null, @@ -50,6 +54,15 @@ export default function ResultSummary() { // get test summary (i.e., passed, failed, total numbers) const summaryRes = fetchData(`/api/getTotals?id=${parentId}`); + const machinesDataRes = fetchData( + `/api/GetFailedTestByMachine?parentId=${parentId}` + ); + // get rerun summary + const rerunRes = fetchData(`/api/getRerunDetails?id=${parentId}`); + + // get jobs details + const jobsDetailsRes = fetchData(`/api/getJobsDetails?id=${parentId}`); + // get build information const buildInfoRes = fetchData(`/api/getData?_id=${parentId}`); @@ -73,12 +86,16 @@ export default function ResultSummary() { parentId, })}` ); - const [summary, buildInfo, sdkBuilds, builds] = await Promise.all([ - summaryRes, - buildInfoRes, - sdkBuildsRes, - buildsRes, - ]); + const [summary, rerunSummary, jobsDetailsSummary, machinesData, buildInfo, sdkBuilds, builds] = + await Promise.all([ + summaryRes, + rerunRes, + jobsDetailsRes, + machinesDataRes, + buildInfoRes, + sdkBuildsRes, + buildsRes, + ]); const parentBuildInfo = buildInfo[0] || {}; let childBuildsResult = 'UNDEFINED'; let javaVersion = null; @@ -325,6 +342,9 @@ export default function ResultSummary() { ...prevState, buildMap, summary, + machinesData, + rerunSummary, + jobsDetailsSummary, parentBuildInfo, selectedPlatforms: platformOpts, allPlatforms: platformOpts, @@ -350,6 +370,9 @@ export default function ResultSummary() { selectedJdkImpls, allJdkImpls, summary, + machinesData, + rerunSummary, + jobsDetailsSummary, parentBuildInfo, childBuildsResult, sdkBuilds, @@ -366,6 +389,9 @@ export default function ResultSummary() { id={parentId} parentBuildInfo={parentBuildInfo} summary={summary} + machinesData={machinesData} + rerunSummary={rerunSummary} + jobsDetailsSummary={jobsDetailsSummary} childBuildsResult={childBuildsResult} sdkBuilds={sdkBuilds} javaVersion={javaVersion}