Skip to content

Commit

Permalink
Merge pull request #715 from codecov/feat-xcode
Browse files Browse the repository at this point in the history
Feat xcode
  • Loading branch information
mitchell-codecov authored Apr 20, 2022
2 parents 76f5e03 + 2bea407 commit 12304d1
Show file tree
Hide file tree
Showing 15 changed files with 150 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/ci_providers/provider_azurepipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function _getSHA(inputs: UploaderInputs): string {
if (mergeCommitRegex.exec(mergeCommitMessage)) {
const mergeCommit = mergeCommitMessage.split(' ')[1]
info(` Fixing merge commit SHA ${commit} -> ${mergeCommit}`)
commit = mergeCommit
commit = mergeCommit || ''
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/ci_providers/provider_circleci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function _getSlug(inputs: UploaderInputs): string {
}

if (isSetAndNotEmpty(envs.CIRCLE_REPOSITORY_URL)) {
return `${envs.CIRCLE_REPOSITORY_URL?.split(':')[1].split('.git')[0]}`
return `${envs.CIRCLE_REPOSITORY_URL?.split(':')[1]?.split('.git')[0]}`
}
return slug
}
Expand Down
13 changes: 13 additions & 0 deletions src/helpers/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,19 @@ const args: ICLIArgument[] = [
type: 'string',
description: 'Extra arguments to pass to gcov',
},
{
alias: 'xc',
name: 'xcode',
type: 'boolean',
default: false,
description: 'Run with xcode support',
},
{
alias: 'xp',
name: 'xcodeArchivePath',
type: 'string',
description: 'Specify the xcode archive path. Likely specified as the -resultBundlePath and should end in .xcresult',
},
]

export interface IYargsObject {
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export function fetchGitRoot(): string {
return (
runExternalProgram('git', ['rev-parse', '--show-toplevel'])) || process.cwd()
} catch (error) {
throw new Error('Error fetching git root. Please try using the -R flag.')
throw new Error(`Error fetching git root. Please try using the -R flag. ${error}`)
}
}

Expand Down Expand Up @@ -259,7 +259,7 @@ export function getAllFiles(
if (args.networkPrefix) {
files = files.map(file => String(args.networkPrefix) + file)
}

return files
}

Expand Down Expand Up @@ -348,7 +348,7 @@ export function cleanCoverageFilePaths(projectRoot: string, paths: string[]): st

if (coverageFilePaths.length === 0) {
logError(`None of the following appear to exist as files: ${paths.toString()}`)
throw new Error('Error while cleaning paths. No paths matched existing files!')
throw new Error('Error while cleaning paths. No paths matched existing files!')
}

return coverageFilePaths
Expand Down
8 changes: 4 additions & 4 deletions src/helpers/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ export function parseSlug(slug: string): string {

if (slug.match('http')) {
// Type is http(s)
const phaseOne = slug.split('//')[1].replace('.git', '')
const phaseTwo = phaseOne.split('/')
const phaseOne = slug.split('//')[1]?.replace('.git', '') || ''
const phaseTwo = phaseOne?.split('/') || ''
const cleanSlug = `${phaseTwo[1]}/${phaseTwo[2]}`
return cleanSlug
} else if (slug.match('@')) {
// Type is git
const cleanSlug = slug.split(':')[1].replace('.git', '')
return cleanSlug
const cleanSlug = slug.split(':')[1]?.replace('.git', '')
return cleanSlug || ''
}
throw new Error(`Unable to parse slug URL: ${slug}`)
}
Expand Down
9 changes: 7 additions & 2 deletions src/helpers/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function getPackage(source: string): string {


export async function uploadToCodecovPUT(
putAndResultUrlPair: PostResults,
putAndResultUrlPair: PostResults,
uploadFile: string | Buffer,
args: UploaderArgs
): Promise<PutResults> {
Expand Down Expand Up @@ -133,6 +133,11 @@ export function parsePOSTResults(putAndResultUrlPair: string): PostResults {
)
}

if (matches[0] === undefined || matches[1] === undefined) {
throw new Error(
`Invalid URLs received when parsing results from POST: ${matches[0]},${matches[1]}`
)
}
const resultURL = new URL(matches[0].trimEnd())
const putURL = new URL(matches[1])
// This match may have trailing 0x0A and 0x0D that must be trimmed
Expand Down Expand Up @@ -215,5 +220,5 @@ export function generateRequestHeadersPUT(
}

}

}
63 changes: 63 additions & 0 deletions src/helpers/xcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import fs from 'fs/promises'

import { info, UploadLogger } from '../helpers/logger'
import {
XcodeCoverageFileReport,
XcodeCoverageReport,
} from '../types'
import { isProgramInstalled, runExternalProgram } from "./util"

export async function generateXcodeCoverageFiles(archivePath: string): Promise<string> {
if (!isProgramInstalled('xcrun')) {
throw new Error('xcrun is not installed, cannot process files')
}
info('Running xcode coversion...')

const coverage: XcodeCoverageReport = {}
const report = { coverage: coverage }

getFileList(archivePath).forEach(repoFilePath => {
UploadLogger.verbose(`Converting ${repoFilePath}...`)
const coverageInfo = getCoverageInfo(archivePath, repoFilePath)
const coverageJson = convertCoverage(coverageInfo)
report.coverage[repoFilePath] = coverageJson
})

let pathFilename = archivePath.split('/').pop()
if (pathFilename) {
pathFilename = pathFilename.split('.xcresult')[0]
}
const filename = `./coverage-report-${pathFilename}.json`
UploadLogger.verbose(`Writing coverage to ${filename}`)
await fs.writeFile(filename, JSON.stringify(report))
return filename
}

function getFileList(archivePath: string): string[] {
const fileList = runExternalProgram('xcrun', ['xccov', 'view', '--file-list', '--archive', archivePath]);
return fileList.split('\n').filter(i => i !== '')
}

function getCoverageInfo(archivePath: string, filePath: string): string {
return runExternalProgram('xcrun', ['xccov', 'view', '--archive', archivePath, '--file', filePath])
}

function convertCoverage(coverageInfo: string): XcodeCoverageFileReport {
const coverageInfoArr = coverageInfo.split('\n')
const obj: XcodeCoverageFileReport = {}
coverageInfoArr.forEach(line => {
const [lineNum, lineInfo] = line.split(':')
if (lineNum && Number.isInteger(Number(lineNum))) {
const lineHits = lineInfo?.trimStart().split(' ')[0]?.trim()
if (typeof lineHits !== 'string') {
return
}
if (lineHits === '*') {
obj[String(lineNum.trim())] = null
} else {
obj[String(lineNum.trim())] = lineHits
}
}
})
return obj
}
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './helpers/files'
import { generateCoveragePyFile } from './helpers/coveragepy'
import { generateGcovCoverageFiles } from './helpers/gcov'
import { generateXcodeCoverageFiles } from './helpers/xcode'
import { argAsArray } from './helpers/util'

/**
Expand Down Expand Up @@ -167,6 +168,16 @@ export async function main(
UploadLogger.verbose(`${gcovLogs}`)
}

if (args.xcode) {
if (!args.xcodeArchivePath) {
throw new Error('Please specify xcodeArchivePath to run the Codecov uploader with xcode support')
} else {
const xcodeArchivePath: string = args.xcodeArchivePath
const xcodeLogs = await generateXcodeCoverageFiles(xcodeArchivePath)
UploadLogger.verbose(`${xcodeLogs}`)
}
}

try {
await generateCoveragePyFile()
} catch (error) {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export interface UploaderArgs {
upstream: string // Upstream proxy to connect to
url?: string // Change the upload host (Enterprise use)
verbose?: string // Run with verbose logging
xcode?: string // Run with xcode support
xcodeArchivePath?: string // Specify the xcode archive path. Likely specified as the -resultBundlePath and should end in .xcresult
}

export type UploaderEnvs = NodeJS.Dict<string>
Expand Down Expand Up @@ -82,3 +84,6 @@ export interface PutResults {
status: string
resultURL: URL
}

export type XcodeCoverageFileReport = Record<string, string | null>
export type XcodeCoverageReport = Record<string, XcodeCoverageFileReport>
Empty file.
Empty file.
Empty file.
35 changes: 35 additions & 0 deletions test/helpers/xcode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import td from 'testdouble'
import childProcess from 'child_process'
import { SPAWNPROCESSBUFFERSIZE } from '../../src/helpers/util'
import { generateXcodeCoverageFiles } from '../../src/helpers/xcode'

describe('generateXcodeCoverageFiles()', () => {
afterEach(() => {
td.reset()
})

it('should find all files in fixtures', async () => {
const spawnSync = td.replace(childProcess, 'spawnSync')
td.when(spawnSync('xcrun')).thenReturn({
stdout: 'xcrun installed',
error: null
})
td.when(spawnSync('xcrun', td.matchers.contains('--file-list'), { maxBuffer: SPAWNPROCESSBUFFERSIZE })).thenReturn({
stdout: '../fixtures/xcode/UI/View1.swift',
error: null
})
td.when(spawnSync('xcrun', td.matchers.contains('--file'), { maxBuffer: SPAWNPROCESSBUFFERSIZE })).thenReturn({
stdout: ' 1: *\n 2: 0',
error: null
})

expect(await generateXcodeCoverageFiles('../fixtures/xcode/test.xcresult')).toBe('./coverage-report-test.json')
})

it('should return an error when xcode is not installed', async () => {
const spawnSync = td.replace(childProcess, 'spawnSync')
td.when(spawnSync('xcrun')).thenReturn({ error: "Command 'xcrun' not found" })

await expect(generateXcodeCoverageFiles('')).rejects.toThrowError(/xcrun is not installed/)
})
})
7 changes: 5 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Module from 'module'

import * as app from '../src'

import { version } from '../package.json'
import nock from 'nock'
import childProcess from 'child_process'
import fs from 'fs'
import nock from 'nock'
import td from 'testdouble'
import { UploadLogger } from '../src/helpers/logger'
import { version } from '../package.json'

// Backup the env
const realEnv = { ...process.env }
Expand All @@ -30,6 +32,7 @@ describe('Uploader Core', () => {
afterEach(() => {
process.env = env
jest.restoreAllMocks()
td.reset()
})

it('Can return version', () => {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true
"skipLibCheck": true,
"noUncheckedIndexedAccess": true
},
"exclude": ["dist/*", "test/*"]
}

0 comments on commit 12304d1

Please sign in to comment.