Repository: codecov/uploader
Pull request #714 (2022-04-13 18:17:50)

feat: add coveragepy xml support
Adds `coveragepy` support, it automagic runs `coverage xml`


Patch:
diff --git a/.gitignore b/.gitignore
index cc09507..9fc97b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,4 @@ codecov.exe
 .DS_Store
 .idea/*
 .dccache
+.coverage
diff --git a/src/helpers/coveragepy.ts b/src/helpers/coveragepy.ts
new file mode 100644
index 0000000..fd507e4
--- /dev/null
+++ b/src/helpers/coveragepy.ts
@@ -0,0 +1,11 @@
+import { isProgramInstalled, runExternalProgram } from "./util"
+import { info } from './logger'
+
+export async function generateCoveragePyFile(): Promise<string> {
+    if (!isProgramInstalled('coverage')) {
+        throw new Error('coveragepy is not installed')
+    }
+
+    info('Running coverage xml...')
+    return runExternalProgram('coverage', ['xml']);
+}
diff --git a/src/index.ts b/src/index.ts
index 050dbab..9573b96 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,6 +22,7 @@ import {
   readCoverageFile,
   removeFile,
 } from './helpers/files'
+import { generateCoveragePyFile } from './helpers/coveragepy'
 import { generateGcovCoverageFiles } from './helpers/gcov'
 import { argAsArray } from './helpers/util'
 
@@ -154,7 +155,7 @@ export async function main(
   // #region == Step 5: select coverage files (search or specify)
 
   let requestedPaths: string[] = []
-  
+
   // Look for files
 
   if (args.gcov) {
@@ -165,7 +166,13 @@ export async function main(
     const gcovLogs = await generateGcovCoverageFiles(projectRoot, gcovInclude, gcovIgnore, gcovArgs)
     UploadLogger.verbose(`${gcovLogs}`)
   }
-  
+
+  try {
+    await generateCoveragePyFile()
+  } catch (error) {
+    UploadLogger.verbose(`Skipping coveragepy conversion: ${error}`)
+  }
+
   let coverageFilePaths: string[] = []
   requestedPaths = argAsArray(args.file)
 
@@ -191,9 +198,9 @@ export async function main(
 
     let coverageFilePathsAfterFilter = coverageFilePaths
 
-    if (coverageFilePaths.length > 0) { 
+    if (coverageFilePaths.length > 0) {
       coverageFilePathsAfterFilter = filterFilesAgainstBlockList(coverageFilePaths, getBlocklist())
-    } 
+    }
 
 
 
@@ -301,7 +308,7 @@ export async function main(
   if (buildParams.slug !== '' && !buildParams.slug?.match(/\//)) {
     logError(`Slug must follow the format of "<owner>/<repo>" or be blank. We detected "${buildParams.slug}"`)
   }
-  
+
   const query = webHelpers.generateQuery(buildParams)
 
   if (args.dryRun) {
diff --git a/src/types.ts b/src/types.ts
index 90799e5..8b98272 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,35 +1,35 @@
 import { Agent } from "https";
 
 export interface UploaderArgs {
-  build?: string // Specify the build number manually
   branch?: string // Specify the branch manually
+  build?: string // Specify the build number manually
+  changelog?: string // Displays the changelog and exits
+  clean?: string // Move discovered coverage reports to the trash
   dir?: string // Directory to search for coverage reports.
+  dryRun?: string // Don't upload files to Codecov
   env?: string // Specify environment variables to be included with this build
-  sha?: string // Specify the commit SHA mannually
+  feature?: string // Toggle features
   file?: string | string[] // Target file(s) to upload
   flags: string | string[] // Flag the upload to group coverage metrics
+  gcov?: string // Run with gcov support
+  gcovArgs?:  string | string[] // Extra arguments to pass to gcov
+  gcovIgnore?:  string | string[] // Paths to ignore during gcov gathering
+  gcovInclude?:  string | string[] // Paths to include during gcov gathering
   name?: string // Custom defined name of the upload. Visible in Codecov UI
   networkFilter?: string // Specify a prefix on the files listed in the network section of the Codecov report. Useful for upload-specific path fixing
   networkPrefix?: string // Specify a prefix on files listed in the network section of the Codecov report. Useful to help resolve path fixing
+  nonZero?: string // Should errors exit with a non-zero (default: false)
   parent?: string // The commit SHA of the parent for which you are uploading coverage.
   pr?: string // Specify the pull request number mannually
-  token?: string // Codecov upload token
-  tag?: string // Specify the git tag
-  verbose?: string // Run with verbose logging
   rootDir?: string // Specify the project root directory when not in a git repo
-  nonZero?: string // Should errors exit with a non-zero (default: false)
-  dryRun?: string // Don't upload files to Codecov
+  sha?: string // Specify the commit SHA mannually
   slug: string // Specify the slug manually
-  url?: string // Change the upload host (Enterprise use)
-  clean?: string // Move discovered coverage reports to the trash
-  feature?: string // Toggle features
-  upstream: string // Upstream proxy to connect to
   source?: string // Track wrappers of the uploader
-  changelog?: string // Displays the changelog and exits
-  gcov?: string // Run with gcov support
-  gcovIgnore?:  string | string[] // Paths to ignore during gcov gathering
-  gcovInclude?:  string | string[] // Paths to include during gcov gathering
-  gcovArgs?:  string | string[] // Extra arguments to pass to gcov
+  tag?: string // Specify the git tag
+  token?: string // Codecov upload token
+  upstream: string // Upstream proxy to connect to
+  url?: string // Change the upload host (Enterprise use)
+  verbose?: string // Run with verbose logging
 }
 
 export type UploaderEnvs = NodeJS.Dict<string>
@@ -70,7 +70,7 @@ export interface IRequestHeaders {
     agent?: Agent
     body?: string | Buffer,
     headers: Record<string, string>
-  } 
+  }
 }
 
 export interface PostResults {
@@ -81,4 +81,4 @@ export interface PostResults {
 export interface PutResults {
   status: string
   resultURL: URL
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/helpers/coveragepy.test.ts b/test/helpers/coveragepy.test.ts
new file mode 100644
index 0000000..884d0e6
--- /dev/null
+++ b/test/helpers/coveragepy.test.ts
@@ -0,0 +1,31 @@
+import td from 'testdouble'
+import childProcess from 'child_process'
+import { generateCoveragePyFile } from '../../src/helpers/coveragepy'
+import { SPAWNPROCESSBUFFERSIZE } from '../../src/helpers/util'
+
+describe('generateCoveragePyFile()', () => {
+    afterEach(() => {
+        td.reset()
+    })
+
+    it('should run when coveragepy is asked for', async () => {
+        const spawnSync = td.replace(childProcess, 'spawnSync')
+        td.when(spawnSync('coverage')).thenReturn({
+            stdout: 'coverage installed',
+            error: null
+        })
+        td.when(spawnSync('coverage', td.matchers.contains('xml'), { maxBuffer: SPAWNPROCESSBUFFERSIZE })).thenReturn({
+            stdout: 'xml',
+            error: null
+        })
+
+        expect(await generateCoveragePyFile()).toBe('xml')
+    })
+
+    it('should return a log when coveragepy is not installed', async () => {
+        const spawnSync = td.replace(childProcess, 'spawnSync')
+        td.when(spawnSync('coverage')).thenReturn({ error: "command not found: coverage" })
+
+        await expect(generateCoveragePyFile()).rejects.toThrowError(/coveragepy is not installed/)
+    })
+})