Repository: codecov/uploader
Pull request #309 (2021-08-16 14:58:52)

Better provider detection
Fixes: https://github.com/codecov/uploader/issues/23


Patch:
diff --git a/bin/codecov.ts b/bin/codecov.ts
index a1e8c0a..386aff2 100755
--- a/bin/codecov.ts
+++ b/bin/codecov.ts
@@ -29,5 +29,5 @@ main(realArgs)
     logError(error)
     const end = Date.now()
     verbose(`End of uploader: ${end - start} milliseconds`, argv.verbose)
-    process.exit(argv.nonZero ? -1 : 0)
+    process.exit(Boolean(realArgs.nonZero) ? -1 : 0)
   })
diff --git a/src/ci_providers/provider_local.ts b/src/ci_providers/provider_local.ts
index 177636b..72b8929 100644
--- a/src/ci_providers/provider_local.ts
+++ b/src/ci_providers/provider_local.ts
@@ -1,9 +1,10 @@
-import childprocess from 'child_process'
 import { parseSlug } from '../helpers/git'
+import { isProgramInstalled, runExternalProgram } from '../helpers/util'
 import { IServiceParams, UploaderEnvs, UploaderInputs } from '../types'
 
+// This provider requires git to be installed
 export function detect(envs: UploaderEnvs): boolean {
-  return !envs.CI
+  return isProgramInstalled('git')
 }
 
 function _getBuild(inputs: UploaderInputs): string {
@@ -22,10 +23,7 @@ function _getBranch(inputs: UploaderInputs): string {
     return args.branch
   }
   try {
-    const branchName = childprocess
-      .spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
-      .stdout.toString()
-      .trimRight()
+    const branchName = runExternalProgram('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
     return branchName
   } catch (error) {
     throw new Error(
@@ -61,10 +59,7 @@ function _getSHA(inputs: UploaderInputs) {
     return args.sha
   }
   try {
-    const sha = childprocess
-      .spawnSync('git', ['rev-parse', 'HEAD'])
-      .stdout.toString()
-      .trimRight()
+    const sha = runExternalProgram('git', ['rev-parse', 'HEAD'])
     return sha
   } catch (error) {
     throw new Error(`There was an error getting the commit SHA from git: ${error}`)
@@ -77,10 +72,7 @@ function _getSlug(inputs: UploaderInputs): string {
     return args.slug
   }
   try {
-    const slug = childprocess
-      .spawnSync('git', ['config', '--get', 'remote.origin.url'])
-      .stdout.toString()
-      .trimRight()
+    const slug = runExternalProgram('git', ['config', '--get', 'remote.origin.url'])
     return parseSlug(slug)
   } catch (error) {
     throw new Error(`There was an error getting the slug from git: ${error}`)
diff --git a/src/helpers/provider.ts b/src/helpers/provider.ts
new file mode 100644
index 0000000..8684271
--- /dev/null
+++ b/src/helpers/provider.ts
@@ -0,0 +1,65 @@
+import providers from '../ci_providers'
+import { info, logError, verbose } from '../helpers/logger'
+import { IServiceParams, UploaderInputs } from '../types'
+
+export function detectProvider(inputs: UploaderInputs): IServiceParams {
+  const { args, environment } = inputs
+  let serviceParams: IServiceParams | undefined
+
+  //   check if we have a complete set of manual overrides (slug, SHA)
+  if (args.sha && args.slug) {
+    // We have the needed args for a manual override
+    info(
+      `Using manual override from args. CI Provider detection will not be ran.`,
+    )
+    serviceParams = {
+      branch: '',
+      build: '',
+      buildURL: '',
+      commit: args.sha,
+      job: '',
+      pr: 0,
+      service: '',
+      slug: args.slug,
+    }
+    return serviceParams
+  }
+
+  //   if not, loop though all providers
+  try {
+    serviceParams = walkProviders(inputs)
+    if (serviceParams !== undefined) {
+      return serviceParams
+    }
+  } catch (error) {
+    //   if fails, display message explaining failure, and explaining that SHA and slug need to be set as args
+    if (serviceParams !== undefined) {
+      logError(`Errow detecting repos setting using git: ${error}`)
+    }
+  }
+  throw new Error(
+    '\nUnable to detect service, please specify sha and slug manually.\nYou can do this by passing the values with the `-S` and `-r` flags.\nSee the `-h` flag for more details.',
+  )
+}
+
+export function walkProviders(
+  inputs: UploaderInputs,
+): IServiceParams {
+  for (const provider of providers) {
+    if (provider.detect(inputs.environment)) {
+      info(`Detected ${provider.getServiceName()} as the CI provider.`)
+      verbose(
+        '-> Using the following env variables:',
+        Boolean(inputs.args.verbose),
+      )
+      for (const envVarName of provider.getEnvVarNames()) {
+        verbose(
+          `     ${envVarName}: ${inputs.environment[envVarName]}`,
+          Boolean(inputs.args.verbose),
+        )
+      }
+      return provider.getServiceParams(inputs)
+    }
+  }
+  throw new Error(`Unable to detect provider.`)
+}
diff --git a/src/helpers/util.ts b/src/helpers/util.ts
new file mode 100644
index 0000000..9a30d97
--- /dev/null
+++ b/src/helpers/util.ts
@@ -0,0 +1,16 @@
+import childprocess from 'child_process'
+
+export function isProgramInstalled(programName: string): boolean {
+  return !childprocess.spawnSync(programName).error
+}
+
+export function runExternalProgram(
+  programName: string,
+  optionalArguments: string[] = [],
+): string {
+  const result = childprocess.spawnSync(programName, optionalArguments)
+  if (result.error) {
+    throw new Error(`Error running external program: ${result.error}`)
+  }
+  return result.stdout.toString().trimRight()
+}
diff --git a/src/index.ts b/src/index.ts
index 1d44ef6..9f59ece 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,9 +3,9 @@ import { UploaderArgs, UploaderInputs } from './types'
 import zlib from 'zlib'
 import { version } from '../package.json'
 import * as validateHelpers from './helpers/validate'
+import { detectProvider } from './helpers/provider'
 import * as webHelpers from './helpers/web'
 import { info, verbose } from './helpers/logger'
-import providers from './ci_providers'
 import { getToken } from './helpers/token'
 import {
   coverageFilePatterns,
@@ -149,7 +149,8 @@ export async function main(
 
   // Remove invalid and duplicate file paths
   coverageFilePaths = [... new Set(coverageFilePaths.filter(file => {
-    return fileExists(args.dir || projectRoot, file)
+    return validateHelpers.validateFileNamePath(file) &&
+      fileExists(args.dir || projectRoot, file)
   }))]
 
   if (coverageFilePaths.length > 0) {
@@ -213,26 +214,7 @@ export async function main(
 
   // == Step 7: determine CI provider
 
-  // Determine CI provider
-  let serviceParams
-  for (const provider of providers) {
-    if (provider.detect(envs)) {
-      info(`Detected ${provider.getServiceName()} as the CI provider.`)
-      verbose('-> Using the following env variables:', Boolean(args.verbose))
-      for (const envVarName of provider.getEnvVarNames()) {
-        verbose(
-          `     ${envVarName}: ${envs[envVarName]}`,
-          Boolean(args.verbose),
-        )
-      }
-      serviceParams = provider.getServiceParams(inputs)
-      break
-    }
-  }
-
-  if (serviceParams === undefined) {
-    throw new Error('Unable to detect service, please specify manually.')
-  }
+const serviceParams = detectProvider(inputs)
 
   // == Step 8: either upload or dry-run
 


Test patch:
diff --git a/test/helpers/provider.test.ts b/test/helpers/provider.test.ts
new file mode 100644
index 0000000..f67d009
--- /dev/null
+++ b/test/helpers/provider.test.ts
@@ -0,0 +1,86 @@
+import childProcess from 'child_process'
+import td from 'testdouble'
+import { detectProvider, walkProviders } from '../../src/helpers/provider'
+import { IServiceParams, UploaderInputs } from '../../src/types'
+
+describe('detectProvider()', () => {
+  afterEach(() => {
+    td.reset()
+    jest.clearAllMocks()
+  })
+
+  it('can detect a manual override', () => {
+    const inputs: UploaderInputs = {
+      args: {
+        sha: '1234566',
+        slug: 'fakeOrg/fakeRepo',
+      },
+      environment: {},
+    }
+    const expectedOutput: IServiceParams = {
+      branch: '',
+      build: '',
+      buildURL: '',
+      commit: '1234566',
+      job: '',
+      pr: 0,
+      service: '',
+      slug: 'fakeOrg/fakeRepo',
+    }
+    expect(detectProvider(inputs)).toEqual(expectedOutput)
+  })
+
+  it('will throw if unable to detect', () => {
+    jest.spyOn(console, 'error').mockImplementation(() => {})
+    const spawnSync = td.replace(childProcess, 'spawnSync')
+    td.when(spawnSync('git')).thenReturn({
+      error: 'Git is not installed!',
+    })
+    const inputs: UploaderInputs = {
+      args: {},
+      environment: {},
+    }
+    expect(()  => detectProvider(inputs)).toThrowError(/Unable to detect service, please specify sha and slug manually/)
+  })
+})
+
+describe('walkProviders()', () => {
+  afterEach(() => {
+    td.reset()
+  })
+
+  it('will throw if unable to detect', () => {
+    const spawnSync = td.replace(childProcess, 'spawnSync')
+    td.when(spawnSync('git')).thenReturn({
+      error: 'Git is not installed!',
+    })
+    const inputs: UploaderInputs = {
+      args: {},
+      environment: {},
+    }
+    expect(()  => walkProviders(inputs)).toThrowError(/Unable to detect provider./)
+
+  })
+
+  it('will return serviceParams if able to detect', () => {
+    const inputs: UploaderInputs = {
+      args: {},
+      environment: {
+        CI: 'true',
+        CIRCLECI: 'true',
+      },
+    }
+    const expectedOutput: IServiceParams = {
+      branch: '',
+      build: '',
+      buildURL: '',
+      commit: '',
+      job: '',
+      pr: 0,
+      service: 'circleci',
+      slug: 'undefined/undefined',
+    }
+    expect(walkProviders(inputs)).toEqual(expectedOutput)
+  })
+})
+
diff --git a/test/helpers/util.test.ts b/test/helpers/util.test.ts
new file mode 100644
index 0000000..5f92966
--- /dev/null
+++ b/test/helpers/util.test.ts
@@ -0,0 +1,42 @@
+import childProcess from 'child_process'
+import td from 'testdouble'
+import { isProgramInstalled , runExternalProgram} from '../../src/helpers/util'
+
+describe('isProgramInstalled()', () => {
+    afterEach(() => {
+        td.reset()
+    })
+
+    it('should throw an error when program is not found', () => {
+        const spawnSync = td.replace(childProcess, 'spawnSync')
+        td.when(spawnSync('fooProg')).thenReturn({
+          error: 'Huh?',
+        })
+        expect(isProgramInstalled('fooProg')).toBeFalsy()
+
+    })
+})
+
+describe('runExternalProgram()', () => {
+    afterEach(() => {
+        td.reset()
+    })
+
+    it('should throw an error when program is not found', () => {
+        const spawnSync = td.replace(childProcess, 'spawnSync')
+        td.when(spawnSync('ls', ['-geewhiz'])).thenReturn({
+          error: 'Huh?',
+        })
+        expect(() => runExternalProgram('ls', ['-geewhiz'])).toThrowError(/Huh\?/)
+
+    })
+
+    it('should throw an error when program returns an error', () => {
+        const spawnSync = td.replace(childProcess, 'spawnSync')
+        td.when(spawnSync('ls', ['-geewhiz'])).thenReturn({
+          stdout: 'I am output',
+        })
+        expect(runExternalProgram('ls', ['-geewhiz'])).toEqual("I am output")
+
+    })
+})
\ No newline at end of file
diff --git a/test/providers/provider_local.test.ts b/test/providers/provider_local.test.ts
index 2dbbcb7..9aaeb78 100644
--- a/test/providers/provider_local.test.ts
+++ b/test/providers/provider_local.test.ts
@@ -10,7 +10,11 @@ describe('Local Params', () => {
   })
 
   describe('detect()', () => {
-    it('does not run with the CI env variable', () => {
+    it('does not run with git not installed', () => {
+      const spawnSync = td.replace(childProcess, 'spawnSync')
+      td.when(spawnSync('git')).thenReturn({
+        error: 'Git is not installed!',
+      })
       const inputs: UploaderInputs = {
         args: {
           tag: '',
@@ -26,7 +30,7 @@ describe('Local Params', () => {
       expect(detected).toBeFalsy()
     })
 
-    it('does run without the CI env variable', () => {
+    it('does run with git installed', () => {
       const inputs: UploaderInputs = {
         args: {
           tag: '',