diff --git a/schemas/report/v4.json b/schemas/report/v4.json index d7f00e7..a6ac675 100644 --- a/schemas/report/v4.json +++ b/schemas/report/v4.json @@ -110,7 +110,6 @@ "details": { "type": "array", "minItems": 1, - "uniqueItems": true, "items": { "type": "object", "unevaluatedProperties": false, diff --git a/src/helpers/report.cjs b/src/helpers/report.cjs index fe07197..b269874 100644 --- a/src/helpers/report.cjs +++ b/src/helpers/report.cjs @@ -31,6 +31,26 @@ const getReportVersion = (report) => { } }; +const validateReportV4Details = (report, dataVar) => { + const detailIndices = new Map(); + + for (const [index, detail] of report.details.entries()) { + const key = detail.testId === undefined ? + `detail:${JSON.stringify(detail)}` : + `testId:${detail.testId}`; + const existingIndex = detailIndices.get(key); + + if (existingIndex !== undefined) { + throw new Error( + `[${dataVar}].details: must NOT have duplicate items ` + + `(items ## ${existingIndex} and ${index} are identical)` + ); + } + + detailIndices.set(key, index); + } +}; + const validateReport = (report, dataVar = 'report') => { const reportVersion = getReportVersion(report); let errors; @@ -59,6 +79,10 @@ const validateReport = (report, dataVar = 'report') => { errors = validateReportV4Ajv.errors; } + if (errors === undefined || errors.length === 0) { + validateReportV4Details(report, dataVar); + } + break; default: throw new Error(`Unknown report version '${reportVersion}'`); diff --git a/test/unit/report.test.js b/test/unit/report.test.js index 92bd98f..603ddf8 100644 --- a/test/unit/report.test.js +++ b/test/unit/report.test.js @@ -403,6 +403,14 @@ const testReportOldV3ConfigOnly = { ...testReportOldV3Full, details: testDetailsOldV3ConfigOnly }; +const testReportV4Full = { + ...testReportLatestFull, + version: 4, + details: testDetailsLatest.map((detail, index) => ({ + ...detail, + testId: `test-id-${index}` + })) +}; describe('report', () => { afterEach(() => mock.reset()); @@ -840,6 +848,47 @@ describe('report', () => { }); }); + describe('v4', () => { + it('allows otherwise identical details with distinct test IDs', () => { + const detail = testDetailsLatest[0]; + const reportData = { + ...testReportV4Full, + details: [{ ...detail, testId: 'test-id-1' }, { ...detail, testId: 'test-id-2' }] + }; + + mock.method(fs, 'readFileSync', () => JSON.stringify(reportData)); + + expect(() => new Report(testReportPath)).to.not.throw(); + }); + + it('rejects duplicate test IDs', () => { + const detail = testDetailsLatest[0]; + const reportData = { + ...testReportV4Full, + details: [ + { ...detail, testId: 'duplicate-test-id' }, + { ...detail, duration: { final: 238, total: 550 }, testId: 'duplicate-test-id' } + ] + }; + + mock.method(fs, 'readFileSync', () => JSON.stringify(reportData)); + + expect(() => new Report(testReportPath)).to.throw(/must NOT have duplicate items/); + }); + + it('rejects duplicate details without test IDs', () => { + const detail = testDetailsLatest[0]; + const reportData = { + ...testReportV4Full, + details: [detail, detail] + }; + + mock.method(fs, 'readFileSync', () => JSON.stringify(reportData)); + + expect(() => new Report(testReportPath)).to.throw(/must NOT have duplicate items/); + }); + }); + describe(`legacy (old v3, cleans to v${latestReportVersion})`, () => { it('cleans config to configuration', () => { mock.method(fs, 'readFileSync', () => JSON.stringify(testReportOldV3ConfigOnly));