diff --git a/.gitignore b/.gitignore index 9d8191b0e..28edf1369 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ codecov.exe .coverage # File genrated as part of XCode tests coverage-report-test.json + +*.coverage.txt diff --git a/src/helpers/cli.ts b/src/helpers/cli.ts index 312fb52a9..be8480779 100644 --- a/src/helpers/cli.ts +++ b/src/helpers/cli.ts @@ -178,6 +178,7 @@ const args: ICLIArgument[] = [ name: 'feature', type: 'string', description: `Toggle functionalities. Separate multiple ones by comma: -X network,search + -X fixes Enable file fixes to ignore common lines from coverage (e.g. blank lines or empty brackets) -X network Disable uploading the file network -X search Disable searching for coverage files`, }, diff --git a/src/helpers/fixes.ts b/src/helpers/fixes.ts new file mode 100644 index 000000000..6f031d62a --- /dev/null +++ b/src/helpers/fixes.ts @@ -0,0 +1,70 @@ +import fs from 'fs' +import readline from 'readline' + +import { getAllFiles } from './files' +import { UploadLogger } from './logger' + +export const FIXES_HEADER = '# path=fixes\n' + +export async function generateFixes(projectRoot: string): Promise { + // Fake out the UploaderArgs as they are not needed + const allFiles = await getAllFiles(projectRoot, projectRoot, { + flags: '', + slug: '', + upstream: '', + }) + + const allAdjustments: string[] = [] + const EMPTYLINE = /^\s*$/mg + // { or } + const SYNTAXBRACKET = /^\s*[{}]\s*(\/\/.*)?$/m + // [ or ] + const SYNTAXLIST = /^\s*[[\]]\s*(\/\/.*)?$/m + + for (const file of allFiles) { + let lineAdjustments: string[] = [] + + if ( + file.match(/\.c$/) || + file.match(/\.cpp$/) || + file.match(/\.h$/) || + file.match(/\.hpp$/) || + file.match(/\.m$/) || + file.match(/\.swift$/) || + file.match(/\.vala$/) + ) { + lineAdjustments = await getMatchedLines(file, [EMPTYLINE, SYNTAXBRACKET]) + } else if ( + file.match(/\.php$/) + ) { + lineAdjustments = await getMatchedLines(file, [SYNTAXBRACKET, SYNTAXLIST]) + } + + if (lineAdjustments.length > 0) { + UploadLogger.verbose(`Matched file ${file} for adjustments: ${lineAdjustments.join(',')}`) + allAdjustments.push(`${file}:${lineAdjustments.join(',')}\n`) + } + } + return allAdjustments.join('') +} + +async function getMatchedLines(file: string, matchers: RegExp[]): Promise { + const fileStream = fs.createReadStream(file) + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity + }); + + const matchedLines: string[] = [] + let lineNumber = 1 + + for await (const line of rl) { + for (const matcher of matchers) { + if (line.match(matcher)) { + matchedLines.push(lineNumber.toString()) + } + } + lineNumber++ + } + return matchedLines +} diff --git a/src/index.ts b/src/index.ts index 333bb5166..3c721d988 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { removeFile, } from './helpers/files' import { generateCoveragePyFile } from './helpers/coveragepy' +import { generateFixes, FIXES_HEADER } from './helpers/fixes' import { generateGcovCoverageFiles } from './helpers/gcov' import { generateXcodeCoverageFiles } from './helpers/xcode' import { argAsArray } from './helpers/util' @@ -322,6 +323,16 @@ export async function main( uploadFileChunks.push(Buffer.from(MARKER_ENV_END)) } + // Fixes + if (args.feature && args.feature.split(',').includes('fixes') === true) { + info('Generating file fixes...') + const fixes = await generateFixes(projectRoot) + uploadFileChunks.push(Buffer.from(FIXES_HEADER)) + uploadFileChunks.push(Buffer.from(fixes)) + uploadFileChunks.push(Buffer.from(MARKER_ENV_END)) + info('Finished generating file fixes') + } + const uploadFile = Buffer.concat(uploadFileChunks) const gzippedFile = zlib.gzipSync(uploadFile) diff --git a/test/fixtures/fixes/example.php b/test/fixtures/fixes/example.php new file mode 100644 index 000000000..004ce6258 --- /dev/null +++ b/test/fixtures/fixes/example.php @@ -0,0 +1,20 @@ + { + afterEach(() => { + td.reset() + }) + + it('provides no fixes if none are applicable', async () => { + const files = ['package.json'] + td.replace(childProcess, 'spawnSync', () => { + return { + stdout: files.join('\n'), + status: 0, + error: undefined, + } + }) + expect( + await fixesHelpers.generateFixes('.') + ).toBe('') + }) + + it('provides proper fixes for c-like files', async () => { + const files = ['test/fixtures/gcov/main.c'] + td.replace(childProcess, 'spawnSync', () => { + return { + stdout: files.join('\n'), + status: 0, + error: undefined, + } + }) + expect( + await fixesHelpers.generateFixes('.') + ).toBe('test/fixtures/gcov/main.c:2,4,11,12,13,14,16,20\n') + }) + + it('provides proper fixes for php-like files', async () => { + const files = ['test/fixtures/fixes/example.php'] + td.replace(childProcess, 'spawnSync', () => { + return { + stdout: files.join('\n'), + status: 0, + error: undefined, + } + }) + expect( + await fixesHelpers.generateFixes('.') + ).toBe('test/fixtures/fixes/example.php:4,6,11,15,19,20\n') + }) + + it('provides multiple fixes for files', async () => { + const files = ['test/fixtures/fixes/example.php', 'test/fixtures/gcov/main.c'] + td.replace(childProcess, 'spawnSync', () => { + return { + stdout: files.join('\n'), + status: 0, + error: undefined, + } + }) + expect( + await fixesHelpers.generateFixes('.') + ).toBe('test/fixtures/fixes/example.php:4,6,11,15,19,20\ntest/fixtures/gcov/main.c:2,4,11,12,13,14,16,20\n') + }) +}) diff --git a/test/index.test.ts b/test/index.test.ts index 04979a069..fb965a7b8 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -509,4 +509,20 @@ describe('Uploader Core', () => { expect.stringMatching(/<<<<<< network/), ) }) + + it('Can create fixes', async () => { + await app.main({ + name: 'customname', + token: 'abcdefg', + url: 'https://codecov.io', + dryRun: 'true', + feature: 'fixes', + flags: '', + slug: '', + upstream: '' + }) + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/# path=fixes\ntest\/fixtures\/fixes\/example.php:4,6,11,15,19,20\ntest\/fixtures\/gcov\/main.c:2,4,11,12,13,14,16,20/) + ) + }) })