diff --git a/.github/scripts/check-tests.mjs b/.github/scripts/check-tests.mjs new file mode 100644 index 0000000000..047dce0678 --- /dev/null +++ b/.github/scripts/check-tests.mjs @@ -0,0 +1,70 @@ +import fs from 'fs'; +import path from 'path'; +import util from 'util'; +import { exec } from 'child_process'; +import { glob } from "glob"; +import parser from '@babel/parser'; +import traverse from '@babel/traverse'; + +const readFileAsync = util.promisify(fs.readFile); +const execAsync = util.promisify(exec); + +const filterAsync = async (asyncPredicate, arr) => { + const filterResults = await Promise.all(arr.map(async item => ({ + item, + shouldKeep: await asyncPredicate(item) + }))); + + return filterResults.filter(({shouldKeep}) => shouldKeep).map(({item}) => item); +} + +// Function to check if a file has a function declaration, function expression, object method or class +const hasFunctionOrClass = async filePath => { + const code = await readFileAsync(filePath); + const ast = parser.parse(code, {sourceType: 'module', plugins: ['typescript']}); + + let hasFunctionOrClass = false; + traverse(ast, { + enter(path) { + if ( + path.isFunctionDeclaration() || + path.isFunctionExpression() || + path.isObjectMethod() || + path.isClassDeclaration() + ) { + hasFunctionOrClass = true; + path.stop(); // stop traversing as soon as we found a function or class + } + }, + }); + + return hasFunctionOrClass; +} + +const program = async () => { + + // Run a git command to get a list of all files in the commit + const changedFilesCommand = "git diff --name-only --diff-filter=d origin/master...HEAD"; + const changedFiles = await execAsync(changedFilesCommand).then(({stdout}) => stdout.toString().trim().split('\n')); + + // Get all .spec.ts and .test.ts files from the packages + const specAndTestTsFiles = await glob('../../packages/*/**/__test__/*.{spec,test}.ts'); + const specAndTestTsFilesNames = specAndTestTsFiles.map(file => path.parse(file).name.replace(/\.(test|spec)/, '')); + + // Filter out the .ts and .vue files from the changed files, .ts files with any kind of function declaration or class + const changedVueFiles = changedFiles.filter(file => file.endsWith('.vue')); + const changedTsFilesWithFunction = await filterAsync(async file => file.endsWith('.ts') && await hasFunctionOrClass(file), changedFiles); + + + // For each .ts or .vue file, check if there's a corresponding .test.ts or .spec.ts file in the repository + changedVueFiles.concat(changedTsFilesWithFunction).forEach(async file => { + const fileName = path.parse(file).name; + + if (!specAndTestTsFilesNames.includes(fileName)) { + console.error(`No corresponding test file for: ${file}`); + process.exit(1); + } + }); +}; + +program(); diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 60f945f3f5..0fddeed6ae 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -1,6 +1,9 @@ { "dependencies": { - "semver": "^7.3.8", - "conventional-changelog-cli": "^2.2.2" + "@babel/parser": "^7.22.5", + "@babel/traverse": "^7.22.5", + "conventional-changelog-cli": "^2.2.2", + "glob": "^10.2.7", + "semver": "^7.3.8" } } diff --git a/.github/workflows/check-tests.yml b/.github/workflows/check-tests.yml new file mode 100644 index 0000000000..b89003c306 --- /dev/null +++ b/.github/workflows/check-tests.yml @@ -0,0 +1,23 @@ +name: Check Test Files + +on: + pull_request: + branches: + - master + +jobs: + check-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use Node.js + - uses: actions/setup-node@v3 + with: + node-version: 18.x + - run: npm install --prefix=.github/scripts --no-package-lock + + - name: Check for test files + run: node .github/actions/check-tests.mjs