Repository: codecov/uploader
Pull request #318 (2021-08-18 15:19:15)

Fix for Bitbucket issue 316
- #316 


Patch:
diff --git a/bin/codecov.ts b/bin/codecov.ts
index 9b39f54..bbb836d 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/helpers/web.ts b/src/helpers/web.ts
index c2e2f2a..50dbb29 100644
--- a/src/helpers/web.ts
+++ b/src/helpers/web.ts
@@ -1,9 +1,9 @@
-import { snakeCase } from "snake-case";
+import { snakeCase } from 'snake-case'
 import superagent from 'superagent'
 
 import { version } from '../../package.json'
-import { IServiceParams, UploaderInputs } from '../types'
-import { info, logError } from './logger'
+import { IServiceParams, UploaderArgs, UploaderInputs } from '../types'
+import { info, logError, verbose } from './logger'
 import * as validateHelpers from './validate'
 import { checkValueType } from './validate'
 
@@ -50,9 +50,7 @@ export async function uploadToCodecovPUT(
 ): Promise<{ status: string; resultURL: string }> {
   info('Uploading...')
 
-  const parts = uploadURL.split('\n')
-  const putURL = parts[1]
-  const codecovResultURL = parts[0]
+  const { putURL, resultURL } = parsePOSTResults(uploadURL)
 
   try {
     const result = await superagent
@@ -63,7 +61,7 @@ export async function uploadToCodecovPUT(
       .set('Content-Encoding', 'gzip')
 
     if (result.status === 200) {
-      return { status: 'success', resultURL: codecovResultURL }
+      return { status: 'success', resultURL }
     }
     throw new Error(`${result.status}, ${result.body}`)
   } catch (error) {
@@ -120,3 +118,26 @@ export function generateQuery(queryParams: IServiceParams): string {
     .map(([key, value]) => `${snakeCase(key)}=${value}`)
     .join('&')
 }
+
+export function parsePOSTResults(uploadURL: string): {
+  putURL: string
+  resultURL: string
+} {
+  // JS for [[:graph:]] https://www.regular-expressions.info/posixbrackets.html
+  const re = /([\x21-\x7E]+)[\r\n]?/gm
+
+  const matches = uploadURL.match(re)
+
+  if (matches === null) {
+    throw new Error(`Parsing results from POST failed: (${uploadURL})`)
+  }
+
+  if (matches?.length !== 2) {
+    throw new Error(`Incorrect number of urls when parsing results from POST: ${matches.length}`)
+  }
+
+  const putURL = matches[1]
+  const resultURL = matches[0].trimEnd() // This match may have trailing 0x0A and 0x0D that must be trimmed
+
+  return { putURL, resultURL }
+}
diff --git a/src/index.ts b/src/index.ts
index 1d44ef6..57d8c62 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,
@@ -213,26 +213,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/helpers/web.test.ts b/test/helpers/web.test.ts
index ec9aeab..9ababdb 100644
--- a/test/helpers/web.test.ts
+++ b/test/helpers/web.test.ts
@@ -1,7 +1,7 @@
 import nock from 'nock'
 
 import { version } from '../../package.json'
-import * as webHelper from '../../src/helpers/web'
+import { generateQuery, getPackage, parsePOSTResults, populateBuildParams, uploadToCodecov, uploadToCodecovPUT } from '../../src/helpers/web'
 import { IServiceParams } from '../../src/types'
 
 describe('Web Helpers', () => {
@@ -33,7 +33,7 @@ describe('Web Helpers', () => {
       .query(true)
       .reply(200, 'testPOSTHTTP')
 
-    const response = await webHelper.uploadToCodecov(
+    const response = await uploadToCodecov(
       uploadURL,
       token,
       query,
@@ -56,7 +56,7 @@ describe('Web Helpers', () => {
       .query(true)
       .reply(200, 'testPOSTHTTPS')
 
-    const response = await webHelper.uploadToCodecov(
+    const response = await uploadToCodecov(
       uploadURL,
       token,
       query,
@@ -68,9 +68,10 @@ describe('Web Helpers', () => {
 
   it('Can PUT to the storage endpoint', async () => {
     jest.spyOn(console, 'log').mockImplementation(() => {})
-    uploadURL = 'https://results.codecov.io\nhttps://codecov.io'
-    const response = await webHelper.uploadToCodecovPUT(uploadURL, uploadFile)
-    expect(response.resultURL).toBe('https://results.codecov.io')
+    uploadURL = `https://results.codecov.io
+    https://codecov.io`
+    const response = await uploadToCodecovPUT(uploadURL, uploadFile)
+    expect(response.resultURL).toEqual('https://results.codecov.io')
   })
 
   it('Can generate query URL', () => {
@@ -87,13 +88,13 @@ describe('Web Helpers', () => {
       pr: 2,
       job: '6',
     }
-    expect(webHelper.generateQuery(queryParams)).toBe(
+    expect(generateQuery(queryParams)).toBe(
       'branch=testBranch&commit=commitSHA&build=4&build_url=https://ci-providor.local/job/xyz&name=testName&tag=tagV1&slug=testOrg/testRepo&service=testingCI&flags=unit,uploader&pr=2&job=6',
     )
   })
 
   it('can populateBuildParams() from args', () => {
-    const result = webHelper.populateBuildParams(
+    const result = populateBuildParams(
       { args: { flags: 'testFlag', tag: 'testTag' }, environment: {} },
       {
         name: '',
@@ -113,7 +114,7 @@ describe('Web Helpers', () => {
   })
 
   it('can populateBuildParams() from args with multiple flags as string', () => {
-    const result = webHelper.populateBuildParams(
+    const result = populateBuildParams(
       { args: { flags: 'testFlag1,testFlag2', tag: 'testTag' }, environment: {} },
       {
         name: '',
@@ -133,7 +134,7 @@ describe('Web Helpers', () => {
   })
 
   it('can populateBuildParams() from args with multiple flags as list', () => {
-    const result = webHelper.populateBuildParams(
+    const result = populateBuildParams(
       { args: { flags: ['testFlag1', 'testFlag2'], tag: 'testTag' }, environment: {} },
       {
         name: '',
@@ -153,12 +154,38 @@ describe('Web Helpers', () => {
   })
 
   it('can getPackage() from source', () => {
-    const result = webHelper.getPackage('github-actions-2.0.0')
+    const result = getPackage('github-actions-2.0.0')
     expect(result).toBe(`github-actions-2.0.0-uploader-${version}`)
   })
 
   it('can getPackage() from no source', () => {
-    const result = webHelper.getPackage('')
+    const result = getPackage('')
     expect(result).toBe(`uploader-${version}`)
   })
+
+  describe('parsePOSTResults()', () => {
+    it('will throw when unable to match', () => {
+      const testURL = `🤨`
+      expect(() => parsePOSTResults(testURL)).toThrowError(/Parsing results from POST failed/)
+    })
+
+    it('will throw when can not match exactly twice', () => {
+      const testURL = `dummyURL`
+      expect(() => parsePOSTResults(testURL)).toThrowError('Incorrect number of urls when parsing results from POST: 1')
+    })
+
+    it('will return an object when parsing correctly and input has multiple linebreaks', () => {
+      const testURL = `dummyURL
+      
+      
+      
+      
+      
+      
+      
+      OtherURL`
+      const expectedResults = { putURL: 'OtherURL', resultURL: 'dummyURL'}
+      expect(parsePOSTResults(testURL)).toEqual(expectedResults)
+    })
+  })
 })
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: '',