-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-coverage): implement plugin configuration
- Loading branch information
Showing
7 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { coveragePlugin } from './lib/coverage-plugin'; | ||
|
||
export default coveragePlugin; | ||
export type { CoveragePluginConfig } from './lib/config'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { z } from 'zod'; | ||
|
||
export const coverageTypeSchema = z.enum(['function', 'branch', 'line']); | ||
export type CoverageType = z.infer<typeof coverageTypeSchema>; | ||
|
||
export const coveragePluginConfigSchema = z.object({ | ||
coverageToolCommand: z | ||
.object({ | ||
command: z | ||
.string({ description: 'Command to run coverage tool.' }) | ||
.min(1), | ||
args: z | ||
.array(z.string(), { | ||
description: 'Arguments to be passed to the coverage tool.', | ||
}) | ||
.optional(), | ||
}) | ||
.optional(), | ||
coverageType: z.array(coverageTypeSchema).min(1), | ||
reports: z | ||
.array(z.string().includes('lcov'), { | ||
description: | ||
'Path to all code coverage report files. Only LCOV format is supported for now.', | ||
}) | ||
.min(1), | ||
perfectScoreThreshold: z | ||
.number({ description: 'Score will be 100 for this coverage and above.' }) | ||
.min(1) | ||
.max(100) | ||
.optional(), | ||
}); | ||
|
||
export type CoveragePluginConfig = z.infer<typeof coveragePluginConfigSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import { CoveragePluginConfig, coveragePluginConfigSchema } from './config'; | ||
|
||
describe('coveragePluginConfigSchema', () => { | ||
it('accepts a code coverage configuration with all entities', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['branch', 'function'], | ||
reports: ['coverage/cli/lcov.info'], | ||
coverageToolCommand: { | ||
command: 'npx nx run-many', | ||
args: ['-t', 'test', '--coverage'], | ||
}, | ||
perfectScoreThreshold: 85, | ||
} satisfies CoveragePluginConfig), | ||
).not.toThrow(); | ||
}); | ||
|
||
it('accepts a minimal code coverage configuration', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['line'], | ||
reports: ['coverage/cli/lcov.info'], | ||
} satisfies CoveragePluginConfig), | ||
).not.toThrow(); | ||
}); | ||
|
||
it('throws for no coverage type', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: [], | ||
reports: ['coverage/cli/lcov.info'], | ||
} satisfies CoveragePluginConfig), | ||
).toThrow('too_small'); | ||
}); | ||
|
||
it('throws for no report', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['branch'], | ||
reports: [], | ||
} satisfies CoveragePluginConfig), | ||
).toThrow('too_small'); | ||
}); | ||
|
||
it('throws for unsupported report format', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['line'], | ||
reports: ['coverage/cli/coverage-final.json'], | ||
}), | ||
).toThrow(/Invalid input: must include.+lcov/); | ||
}); | ||
|
||
it('throws for missing command', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['line'], | ||
reports: ['coverage/cli/lcov.info'], | ||
coverageToolCommand: { | ||
args: ['npx', 'nx', 'run-many', '-t', 'test', '--coverage'], | ||
}, | ||
}), | ||
).toThrow('invalid_type'); | ||
}); | ||
|
||
it('throws for invalid score threshold', () => { | ||
expect(() => | ||
coveragePluginConfigSchema.parse({ | ||
coverageType: ['line'], | ||
reports: ['coverage/cli/lcov.info'], | ||
perfectScoreThreshold: 110, | ||
} satisfies CoveragePluginConfig), | ||
).toThrow('too_big'); | ||
}); | ||
}); |
78 changes: 78 additions & 0 deletions
78
packages/plugin-coverage/src/lib/coverage-plugin.integration.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { join } from 'node:path'; | ||
import { describe, expect, it } from 'vitest'; | ||
import { CoveragePluginConfig } from './config'; | ||
import { coveragePlugin } from './coverage-plugin'; | ||
|
||
describe('coveragePluginConfigSchema', () => { | ||
it('should initialise a Code coverage plugin', () => { | ||
expect( | ||
coveragePlugin({ | ||
coverageType: ['function'], | ||
reports: [join('packages', 'plugin-coverage', 'mocks', 'lcov.info')], | ||
}), | ||
).toStrictEqual( | ||
expect.objectContaining({ | ||
slug: 'coverage', | ||
title: 'Code coverage', | ||
audits: expect.any(Array), | ||
}), | ||
); | ||
}); | ||
|
||
it('should generate audits from coverage types', () => { | ||
expect( | ||
coveragePlugin({ | ||
coverageType: ['function', 'branch'], | ||
reports: [join('packages', 'plugin-coverage', 'mocks', 'lcov.info')], | ||
}), | ||
).toStrictEqual( | ||
expect.objectContaining({ | ||
audits: [ | ||
{ | ||
slug: 'function-coverage', | ||
title: 'function coverage', | ||
description: 'function coverage percentage on the project', | ||
}, | ||
expect.objectContaining({ slug: 'branch-coverage' }), | ||
], | ||
}), | ||
); | ||
}); | ||
|
||
it('should assign RunnerConfig when a command is passed', () => { | ||
expect( | ||
coveragePlugin({ | ||
coverageType: ['line'], | ||
reports: [join('packages', 'plugin-coverage', 'mocks', 'lcov.info')], | ||
coverageToolCommand: { | ||
command: 'npm run-many', | ||
args: ['-t', 'test', '--coverage'], | ||
}, | ||
} satisfies CoveragePluginConfig), | ||
).toStrictEqual( | ||
expect.objectContaining({ | ||
slug: 'coverage', | ||
runner: { | ||
command: 'npm run-many', | ||
args: ['-t', 'test', '--coverage'], | ||
outputFile: expect.stringContaining('runner-output.json'), | ||
outputTransform: expect.any(Function), | ||
}, | ||
}), | ||
); | ||
}); | ||
|
||
it('should assign a RunnerFunction when only reports are passed', () => { | ||
expect( | ||
coveragePlugin({ | ||
coverageType: ['line'], | ||
reports: [join('packages', 'plugin-coverage', 'mocks', 'lcov.info')], | ||
} satisfies CoveragePluginConfig), | ||
).toStrictEqual( | ||
expect.objectContaining({ | ||
slug: 'coverage', | ||
runner: expect.any(Function), | ||
}), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { join } from 'node:path'; | ||
import type { | ||
Audit, | ||
PluginConfig, | ||
RunnerConfig, | ||
RunnerFunction, | ||
} from '@code-pushup/models'; | ||
import { pluginWorkDir } from '@code-pushup/utils'; | ||
import { name, version } from '../../package.json'; | ||
import { CoveragePluginConfig, coveragePluginConfigSchema } from './config'; | ||
import { lcovResultsToAuditOutputs } from './runner/lcov/runner'; | ||
import { applyMaxScoreAboveThreshold } from './utils'; | ||
|
||
export const RUNNER_OUTPUT_PATH = join( | ||
pluginWorkDir('coverage'), | ||
'runner-output.json', | ||
); | ||
|
||
/** | ||
* Instantiates Code PushUp code coverage plugin for core config. | ||
* | ||
* @example | ||
* import coveragePlugin from '@code-pushup/coverage-plugin' | ||
* | ||
* export default { | ||
* // ... core config ... | ||
* plugins: [ | ||
* // ... other plugins ... | ||
* await coveragePlugin({ | ||
* coverageType: ['function', 'line'], | ||
* reports: ['coverage/cli/lcov.info'] | ||
* }) | ||
* ] | ||
* } | ||
* | ||
* @returns Plugin configuration as a promise. | ||
*/ | ||
export function coveragePlugin(config: CoveragePluginConfig): PluginConfig { | ||
const { reports, perfectScoreThreshold, coverageType, coverageToolCommand } = | ||
coveragePluginConfigSchema.parse(config); | ||
|
||
const audits = coverageType.map( | ||
type => | ||
({ | ||
slug: `${type}-coverage`, | ||
title: `${type} coverage`, | ||
description: `${type} coverage percentage on the project`, | ||
} satisfies Audit), | ||
); | ||
|
||
const getAuditOutputs = async () => | ||
perfectScoreThreshold | ||
? applyMaxScoreAboveThreshold( | ||
await lcovResultsToAuditOutputs(reports, coverageType), | ||
perfectScoreThreshold, | ||
) | ||
: await lcovResultsToAuditOutputs(reports, coverageType); | ||
|
||
// if coverage results are provided, only convert them to AuditOutputs | ||
// if not, run coverage command and then run result conversion | ||
const runner: RunnerConfig | RunnerFunction = | ||
coverageToolCommand == null | ||
? getAuditOutputs | ||
: ({ | ||
command: coverageToolCommand.command, | ||
args: coverageToolCommand.args, | ||
outputFile: RUNNER_OUTPUT_PATH, | ||
outputTransform: getAuditOutputs, | ||
} satisfies RunnerConfig); | ||
return { | ||
slug: 'coverage', | ||
title: 'Code coverage', | ||
icon: 'folder-coverage-open', | ||
description: 'Official Code PushUp code coverage plugin', | ||
docsUrl: 'https://www.softwaretestinghelp.com/code-coverage-tutorial/', | ||
packageName: name, | ||
version, | ||
audits, | ||
runner, | ||
} satisfies PluginConfig; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { AuditOutputs } from '@code-pushup/models'; | ||
|
||
/** | ||
* Since more code coverage does not necessarily mean better score, this optional override allows for defining custom coverage goals. | ||
* @param outputs original results | ||
* @param threshold threshold above which the score is to be 1 | ||
* @returns Outputs with overriden score (not value) to 1 if it reached a defined threshold. | ||
*/ | ||
export function applyMaxScoreAboveThreshold( | ||
outputs: AuditOutputs, | ||
threshold: number, | ||
): AuditOutputs { | ||
return outputs.map(output => | ||
output.score >= threshold ? { ...output, score: 1 } : output, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
import type { AuditOutput } from '@code-pushup/models'; | ||
import { applyMaxScoreAboveThreshold } from './utils'; | ||
|
||
describe('applyMaxScoreAboveThreshold', () => { | ||
it('should transform score above threshold to maximum', () => { | ||
expect( | ||
applyMaxScoreAboveThreshold( | ||
[ | ||
{ | ||
slug: 'branch-coverage', | ||
value: 75, | ||
score: 0.75, | ||
} satisfies AuditOutput, | ||
], | ||
0.7, | ||
), | ||
).toEqual([ | ||
{ | ||
slug: 'branch-coverage', | ||
value: 75, | ||
score: 1, | ||
} satisfies AuditOutput, | ||
]); | ||
}); | ||
|
||
it('should leave score below threshold untouched', () => { | ||
expect( | ||
applyMaxScoreAboveThreshold( | ||
[ | ||
{ | ||
slug: 'line-coverage', | ||
value: 60, | ||
score: 0.6, | ||
} satisfies AuditOutput, | ||
], | ||
0.7, | ||
), | ||
).toEqual([ | ||
{ | ||
slug: 'line-coverage', | ||
value: 60, | ||
score: 0.6, | ||
} satisfies AuditOutput, | ||
]); | ||
}); | ||
}); |