diff --git a/src/helpers/report-builder.cjs b/src/helpers/report-builder.cjs index f74624b..3e27936 100644 --- a/src/helpers/report-builder.cjs +++ b/src/helpers/report-builder.cjs @@ -409,6 +409,8 @@ class ReportBuilder extends ReportBuilderBase { const missingConfigByFile = new Map(); for (const [, { data: detail }] of this._data.details) { + this.#applyDefaultTaxonomy(detail); + const { status, retries } = detail; if (status === 'passed') { @@ -449,6 +451,28 @@ class ReportBuilder extends ReportBuilderBase { return this; } + #applyDefaultTaxonomy(detail) { + if (detail.location?.file != null) { + return; + } + + const { type, tool } = this.#reportConfiguration.getDefaultTaxonomy(); + + if (type == null && tool == null) { + return; + } + + detail.taxonomy ??= {}; + + if (type != null) { + detail.taxonomy.type ??= type; + } + + if (tool != null) { + detail.taxonomy.tool ??= tool; + } + } + #logMissingConfigWarnings(missingConfigByFile) { if (missingConfigByFile.size === 0) { return; diff --git a/src/helpers/report-configuration.cjs b/src/helpers/report-configuration.cjs index 057ce74..2a57b1f 100644 --- a/src/helpers/report-configuration.cjs +++ b/src/helpers/report-configuration.cjs @@ -103,6 +103,13 @@ class ReportConfiguration { return this.#reportConfigurationPath; } + getDefaultTaxonomy() { + return { + type: this.#reportConfiguration.type?.toLowerCase(), + tool: this.#reportConfiguration.tool + }; + } + getTaxonomy(filePath) { filePath = makeRelativeFilePath(filePath); @@ -124,12 +131,9 @@ class ReportConfiguration { } } - const { - type: defaultType, - tool: defaultTool - } = this.#reportConfiguration; + const { type: defaultType, tool: defaultTool } = this.getDefaultTaxonomy(); - metadata.type = metadata.type ?? defaultType?.toLowerCase(); + metadata.type = metadata.type ?? defaultType; metadata.tool = metadata.tool ?? defaultTool; return metadata; diff --git a/src/helpers/report-configuration.d.cts b/src/helpers/report-configuration.d.cts new file mode 100644 index 0000000..a74bcd6 --- /dev/null +++ b/src/helpers/report-configuration.d.cts @@ -0,0 +1,17 @@ +import type { Logger } from './report-builder.js'; + +export interface Taxonomy { + type?: string; + tool?: string; +} + +export declare class ReportConfiguration { + constructor(path?: string, logger?: Pick); + + getPath(): string | undefined; + getDefaultTaxonomy(): Taxonomy; + getTaxonomy(filePath: string): Taxonomy; + hasTaxonomy(filePath: string): boolean; + ignoreFilePath(filePath: string): boolean; + toJSON(): Record; +} diff --git a/test/integration/report-validation.test.js b/test/integration/report-validation.test.js index 7357cd4..5ff4dcc 100644 --- a/test/integration/report-validation.test.js +++ b/test/integration/report-validation.test.js @@ -6,6 +6,7 @@ import { getOperatingSystemType } from '../../src/helpers/system.cjs'; import { hasContext } from '../../src/helpers/github.cjs'; import { latestReportVersion } from '../../src/helpers/schema.cjs'; import { Report } from '../../src/helpers/report.cjs'; +import { ReportBuilder } from '../../src/helpers/report-builder.cjs'; import { testReportLatestPartial as testReportLatestPartialJest } from './data/validation/test-report-jest.js'; import { testReportLatestPartial as testReportLatestPartialMocha } from './data/validation/test-report-mocha.js'; import { testReportLatestPartial as testReportLatestPartialNodeTest } from './data/validation/test-report-node.js'; @@ -61,6 +62,28 @@ const reportTests = [{ }]; describe('report validation', () => { + it('applies available defaults to details without locations', () => { + const warnings = []; + const logger = { + error: () => {}, + info: () => {}, + location: () => {}, + warning: message => warnings.push(message) + }; + const builder = new ReportBuilder('node', logger, { reportWriter: () => {} }); + const detail = builder.getDetail('locationless').setPassed(); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ tool: 'Test Tooling' }); + expect(warnings).to.deep.equal([ + '1 test missing taxonomy fields: type (1).', + 'Affected files: 1:', + '- unknown location (1 test)', + 'Check d2l-test-reporting.config.json to configure missing taxonomy fields.' + ]); + }); + for (const reportTest of reportTests) { describe(reportTest.name, () => { it('exists', () => { diff --git a/test/unit/report-builder.test.js b/test/unit/report-builder.test.js index 79fcf96..757439e 100644 --- a/test/unit/report-builder.test.js +++ b/test/unit/report-builder.test.js @@ -638,6 +638,84 @@ describe('report builder', () => { }); }); + describe('taxonomy', () => { + it('applies defaults to a detail without a location', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit', tool: 'Test Reporting' }); + }); + + it('does not override file taxonomy with defaults', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + detail.setLocationFile('test/example.test.js'); + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit', tool: 'Test Reporting' }); + }); + + it('applies available default taxonomy fields', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ type: 'unit' })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ type: 'unit' }); + }); + + it('applies a tool-only default', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ tool: 'Test Reporting' })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ tool: 'Test Reporting' }); + }); + + it('preserves existing taxonomy fields', () => { + const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); + const detail = builder.getDetail('test'); + + detail.data.taxonomy = { type: 'custom', tool: 'custom' }; + builder.finalize(); + + expect(detail.data.taxonomy).to.deep.equal({ + type: 'custom', + tool: 'custom' + }); + }); + + it('does not create taxonomy without defaults', () => { + mock.method(fs, 'readFileSync', () => JSON.stringify({ + overrides: [{ pattern: '**', type: 'unit', tool: 'Test Reporting' }] + })); + + const builder = new ReportBuilder('mocha', noopLogger, { + reportConfigurationPath: './d2l-test-reporting.config.json', + reportWriter: () => { } + }); + const detail = builder.getDetail('test'); + + builder.finalize(); + + expect(detail.data).to.not.have.property('taxonomy'); + }); + }); + describe('ignore', () => { it('false without config', () => { const builder = new ReportBuilder('mocha', noopLogger, { reportWriter: () => { } }); diff --git a/test/unit/report-configuration.test.js b/test/unit/report-configuration.test.js index d8b8c82..c629472 100644 --- a/test/unit/report-configuration.test.js +++ b/test/unit/report-configuration.test.js @@ -141,6 +141,12 @@ describe('report configuration', () => { expect(() => new ReportConfiguration(configPath, logger)).to.throw('Unable to read/parse'); }); + + it('when the default configuration is unparseable', () => { + mock.method(fs, 'readFileSync', () => 'not json'); + + expect(() => new ReportConfiguration(undefined, logger)).to.throw('Unable to read/parse'); + }); }); it('empty without config file', () => { @@ -152,6 +158,12 @@ describe('report configuration', () => { }); }); + it('reports the resolved configuration path', () => { + const config = loadConfig({ type: 'integration', tool: 'Test Reporting' }); + + expect(config.getPath()).to.equal('d2l-test-reporting.config.json'); + }); + describe('default logger', () => { const legacyConfig = { type: 'integration', @@ -172,6 +184,28 @@ describe('report configuration', () => { }); describe('taxonomy', () => { + describe('defaults', () => { + it('lowercases type and preserves tool', () => { + const config = loadConfig({ type: 'UI', tool: 'My Tool' }); + + expect(config.getDefaultTaxonomy()).to.deep.equal({ + type: 'ui', + tool: 'My Tool' + }); + }); + + it('omits absent values', () => { + const config = loadConfig({ + overrides: [{ pattern: '**', type: 'unit', tool: 'Test Reporting' }] + }); + + expect(config.getDefaultTaxonomy()).to.deep.equal({ + type: undefined, + tool: undefined + }); + }); + }); + it('lowercases type', () => { const config = loadConfig({ type: 'UI', tool: 'My Tool' }); @@ -198,6 +232,22 @@ describe('report configuration', () => { }); }); + it('inherits missing fields from defaults', () => { + const config = loadConfig({ + type: 'integration', + tool: 'Default Tool', + overrides: [{ + pattern: '**/special.test.js', + type: 'UI' + }] + }); + + expect(config.getTaxonomy('test/special.test.js')).to.deep.equal({ + type: 'ui', + tool: 'Default Tool' + }); + }); + it('normalizes leading ./', () => { const config = loadConfig({ type: 'integration', diff --git a/test/unit/report.test.js b/test/unit/report.test.js index d36954d..92bd98f 100644 --- a/test/unit/report.test.js +++ b/test/unit/report.test.js @@ -407,6 +407,33 @@ const testReportOldV3ConfigOnly = { describe('report', () => { afterEach(() => mock.reset()); + describe('LMS information', () => { + const lmsInfo = { + buildNumber: '20.26.9.12345', + instanceUrl: 'https://example.brightspace.com' + }; + const reports = [{ + name: 'v1', + report: testReportV1Full + }, { + name: 'v2', + report: testReportV2Full + }, { + name: 'v3', + report: testReportLatestFull + }]; + + for (const { name, report: reportData } of reports) { + it(`adds LMS information to ${name} reports`, () => { + mock.method(fs, 'readFileSync', () => JSON.stringify(reportData)); + + const report = new Report(testReportPath, { lmsInfo }); + + expect(report.toJSON().summary.lms).to.deep.equal(lmsInfo); + }); + } + }); + describe(`legacy (v1, upgrades to v${latestReportVersion})`, () => { const testReportCurrentVersion = 1;