Tests: Custom script to run unit tests filtered by code ownership (#111210)
* feat(script): generate a source file x teams manifest from CODEOWNERS * feat(script): unit tests + coverage report only for files owned by team * feat(script): calculate CODEOWNERS metadata * refactor(script): export a pure codeowners manifest generation function * refactor(script): export a pure test coverage by team function * refactor(script): generate raw JSONL codeowners data from Node.js script * feat(script): put codeowners manifest all together in one script * refactor(scripts): group consistently with NPM script name * refactor(scripts): deduplicate constants for file paths etc. * refactor(scripts): make console output cute 💅✨ * refactor(tests): make coverage by "owner" directory more human readable * refactor(scripts): use consistent naming "codeowner" instead of "team" * chore(codeowners): mark DataViz as owners of scripts for now * chore(todo): leave a note where coverage metrics should be emitted later * fix(gitignore): ignore root codeowners-manifest directory not scripts/* * refactor(script): rename manifest to generate for clarity * docs(readme): add a brief README describing new scrips * chore(linter): ignore temporary files in prettier, fix whitespace format * refactor(script): simplify Jest config by using team files list directly * refactor(script): simplify script, partition sourceFiles and testFiles * refactor(script): simplify and parallelize manifest write operations * fix(script): handle errors for JSONL line reader * refactor(script): use Map instead of POJOs * fix(script): handle errors when streaming raw JSONL output * fix(script): add error handling, and use promise API for metadata check * fix(reporter): suppress duplicate Jest CLI coverage report output * refactor(script): simplify with fs promises API for consistency * fix(script): error handling for cp spawn-ed process * refactor(script): use Promise API for mkdir + exists * refactor(script): use fs Promise API * refactor(script): use fs Promise API * fix(script): same allow list for sourceFilter and all Jest config rules Co-authored-by: Paul Marbach <paul.marbach@grafana.com> * fix(script): bust cache when new files are created also --------- Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Paul Marbach
parent
9d60d03d11
commit
70dc9a0027
@@ -0,0 +1,117 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const baseConfig = require('./jest.config.js');
|
||||
|
||||
const CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH = 'codeowners-manifest/filenames-by-team.json';
|
||||
|
||||
const teamName = process.env.TEAM_NAME;
|
||||
if (!teamName) {
|
||||
console.error('ERROR: TEAM_NAME environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = `./coverage/by-team/${createOwnerDirectory(teamName)}`;
|
||||
|
||||
const codeownersFilePath = path.join(__dirname, CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH);
|
||||
|
||||
if (!fs.existsSync(codeownersFilePath)) {
|
||||
console.error(`Codeowners file not found at ${codeownersFilePath} ...`);
|
||||
console.error('Please run: yarn codeowners-manifest first to generate the mapping file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const codeownersData = JSON.parse(fs.readFileSync(codeownersFilePath, 'utf8'));
|
||||
const teamFiles = codeownersData[teamName] || [];
|
||||
|
||||
if (teamFiles.length === 0) {
|
||||
console.error(`ERROR: No files found for team "${teamName}"`);
|
||||
console.error('Available teams:', Object.keys(codeownersData).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceFiles = teamFiles.filter((file) => {
|
||||
const ext = path.extname(file);
|
||||
return (
|
||||
['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&
|
||||
!file.includes('.test.') &&
|
||||
!file.includes('.spec.') &&
|
||||
!file.includes('.story.') &&
|
||||
!file.includes('.gen.ts') &&
|
||||
!file.includes('.d.ts') &&
|
||||
!file.endsWith('/types.ts')
|
||||
);
|
||||
});
|
||||
|
||||
const testFiles = teamFiles.filter((file) => {
|
||||
const ext = path.extname(file);
|
||||
return ['.ts', '.tsx', '.js', '.jsx'].includes(ext) && (file.includes('.test.') || file.includes('.spec.'));
|
||||
});
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
console.log(`No test files found for team ${teamName}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${teamName}.`
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: sourceFiles.map((file) => `<rootDir>/${file}`),
|
||||
coverageReporters: ['none'],
|
||||
coverageDirectory: '/tmp/jest-coverage-ignore',
|
||||
|
||||
coverageProvider: 'v8',
|
||||
reporters: [
|
||||
'default',
|
||||
[
|
||||
'jest-monocart-coverage',
|
||||
{
|
||||
name: `Coverage Report - ${teamName} owned files`,
|
||||
outputDir: outputDir,
|
||||
reports: ['console-summary', 'v8', 'json', 'lcov'],
|
||||
sourceFilter: (coveredFile) => sourceFiles.includes(coveredFile),
|
||||
all: {
|
||||
dir: ['./packages', './public'],
|
||||
filter: (filePath) => {
|
||||
const relativePath = filePath.replace(process.cwd() + '/', '');
|
||||
return sourceFiles.includes(relativePath);
|
||||
},
|
||||
},
|
||||
cleanCache: true,
|
||||
onEnd: (coverageResults) => {
|
||||
console.log(`📄 Coverage report saved to file://${path.resolve(outputDir)}/index.html`);
|
||||
// TODO: Emit coverage metrics https://github.com/grafana/grafana/issues/111208
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
testRegex: undefined,
|
||||
|
||||
testMatch: testFiles.map((file) => `<rootDir>/${file}`),
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a filesystem-safe directory structure for different owner types
|
||||
* @param {string} owner - CODEOWNERS owner (username, team, or email)
|
||||
* @returns {string} Directory path relative to coverage/by-team/
|
||||
*/
|
||||
function createOwnerDirectory(owner) {
|
||||
if (owner.includes('@') && owner.includes('/')) {
|
||||
// Example: @grafana/dataviz-squad
|
||||
const [org, team] = owner.substring(1).split('/');
|
||||
return `teams/${org}/${team}`;
|
||||
} else if (owner.startsWith('@')) {
|
||||
// Example: @jesdavpet
|
||||
return `users/${owner.substring(1)}`;
|
||||
} else {
|
||||
// Example: user@domain.tld
|
||||
const [user, domain] = owner.split('@');
|
||||
return `emails/${user}-at-${domain}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user