mirror of
https://github.com/dorny/test-reporter.git
synced 2025-12-16 06:17:10 +01:00
Add support for loading test results from artifacts
This commit is contained in:
parent
71f2f95ef0
commit
3510d9ac27
19 changed files with 11665 additions and 338 deletions
78
src/input-providers/artifact-provider.ts
Normal file
78
src/input-providers/artifact-provider.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import * as github from '@actions/github'
|
||||
import {GitHub} from '@actions/github/lib/utils'
|
||||
|
||||
import Zip from 'adm-zip'
|
||||
import picomatch from 'picomatch'
|
||||
|
||||
import {FileContent, InputProvider, ReportInput} from './input-provider'
|
||||
import {downloadArtifact, listFiles} from '../utils/github-utils'
|
||||
|
||||
export class ArtifactProvider implements InputProvider {
|
||||
private readonly artifactNameMatch: (name: string) => boolean
|
||||
private readonly fileNameMatch: (name: string) => boolean
|
||||
private readonly getReportName: (name: string) => string
|
||||
|
||||
constructor(
|
||||
readonly octokit: InstanceType<typeof GitHub>,
|
||||
readonly artifact: string,
|
||||
readonly name: string,
|
||||
readonly pattern: string[],
|
||||
readonly sha: string,
|
||||
readonly runId: number
|
||||
) {
|
||||
if (this.artifact.startsWith('/')) {
|
||||
const re = new RegExp(this.artifact)
|
||||
this.artifactNameMatch = (str: string) => re.test(str)
|
||||
this.getReportName = (str: string) => {
|
||||
const match = str.match(re)
|
||||
if (match === null) {
|
||||
throw new Error(`Artifact name '${str}' does not match regex ${this.artifact}`)
|
||||
}
|
||||
let reportName = this.name
|
||||
for (let i = 1; i < match.length; i++) {
|
||||
reportName = reportName.replace(new RegExp(`$${i}`, 'g'), match[i])
|
||||
}
|
||||
return reportName
|
||||
}
|
||||
} else {
|
||||
this.artifactNameMatch = (str: string) => str === this.artifact
|
||||
this.getReportName = () => this.name
|
||||
}
|
||||
|
||||
this.fileNameMatch = picomatch(pattern)
|
||||
}
|
||||
|
||||
async load(): Promise<ReportInput> {
|
||||
const result: ReportInput = {}
|
||||
|
||||
const resp = await this.octokit.actions.listWorkflowRunArtifacts({
|
||||
...github.context.repo,
|
||||
run_id: this.runId
|
||||
})
|
||||
|
||||
const artifacts = resp.data.artifacts.filter(a => this.artifactNameMatch(a.name))
|
||||
for (const art of artifacts) {
|
||||
await downloadArtifact(this.octokit, art.id, art.name)
|
||||
const reportName = this.getReportName(art.name)
|
||||
const files: FileContent[] = []
|
||||
const zip = new Zip(art.name)
|
||||
for (const entry of zip.getEntries()) {
|
||||
const file = entry.name
|
||||
if (entry.isDirectory || !this.fileNameMatch(file)) continue
|
||||
const content = zip.readAsText(entry)
|
||||
files.push({file, content})
|
||||
}
|
||||
if (result[reportName]) {
|
||||
result[reportName].push(...files)
|
||||
} else {
|
||||
result[reportName] = files
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async listTrackedFiles(): Promise<string[]> {
|
||||
return listFiles(this.octokit, this.sha)
|
||||
}
|
||||
}
|
||||
13
src/input-providers/input-provider.ts
Normal file
13
src/input-providers/input-provider.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export interface ReportInput {
|
||||
[reportName: string]: FileContent[]
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
file: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface InputProvider {
|
||||
load(): Promise<ReportInput>
|
||||
listTrackedFiles(): Promise<string[]>
|
||||
}
|
||||
25
src/input-providers/local-file-provider.ts
Normal file
25
src/input-providers/local-file-provider.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import * as fs from 'fs'
|
||||
import glob from 'fast-glob'
|
||||
import {FileContent, InputProvider, ReportInput} from './input-provider'
|
||||
import {listFiles} from '../utils/git'
|
||||
|
||||
export class LocalFileProvider implements InputProvider {
|
||||
constructor(readonly name: string, readonly pattern: string[]) {}
|
||||
|
||||
async load(): Promise<ReportInput> {
|
||||
const result: FileContent[] = []
|
||||
for (const pat of this.pattern) {
|
||||
const paths = await glob(pat, {dot: true})
|
||||
for (const file of paths) {
|
||||
const content = await fs.promises.readFile(file, {encoding: 'utf8'})
|
||||
result.push({file, content})
|
||||
}
|
||||
}
|
||||
|
||||
return {[this.name]: result}
|
||||
}
|
||||
|
||||
async listTrackedFiles(): Promise<string[]> {
|
||||
return listFiles()
|
||||
}
|
||||
}
|
||||
262
src/main.ts
262
src/main.ts
|
|
@ -1,8 +1,10 @@
|
|||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import * as fs from 'fs'
|
||||
import glob from 'fast-glob'
|
||||
import {GitHub} from '@actions/github/lib/utils'
|
||||
|
||||
import {ArtifactProvider} from './input-providers/artifact-provider'
|
||||
import {LocalFileProvider} from './input-providers/local-file-provider'
|
||||
import {FileContent} from './input-providers/input-provider'
|
||||
import {ParseOptions, TestParser} from './test-parser'
|
||||
import {TestRunResult} from './test-results'
|
||||
import {getAnnotations} from './report/get-annotations'
|
||||
|
|
@ -12,141 +14,157 @@ import {DartJsonParser} from './parsers/dart-json/dart-json-parser'
|
|||
import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser'
|
||||
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
|
||||
|
||||
import {normalizeDirPath} from './utils/file-utils'
|
||||
import {listFiles} from './utils/git'
|
||||
import {getCheckRunSha} from './utils/github-utils'
|
||||
import {normalizeDirPath} from './utils/path-utils'
|
||||
import {getCheckRunContext} from './utils/github-utils'
|
||||
import {Icon} from './utils/markdown-utils'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await main()
|
||||
const testReporter = new TestReporter()
|
||||
await testReporter.run()
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const name = core.getInput('name', {required: true})
|
||||
const path = core.getInput('path', {required: true})
|
||||
const reporter = core.getInput('reporter', {required: true})
|
||||
const listSuites = core.getInput('list-suites', {required: true})
|
||||
const listTests = core.getInput('list-tests', {required: true})
|
||||
const maxAnnotations = parseInt(core.getInput('max-annotations', {required: true}))
|
||||
const failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
|
||||
const workDirInput = core.getInput('working-directory', {required: false})
|
||||
const token = core.getInput('token', {required: true})
|
||||
class TestReporter {
|
||||
readonly artifact = core.getInput('artifact', {required: false})
|
||||
readonly name = core.getInput('name', {required: true})
|
||||
readonly path = core.getInput('path', {required: true})
|
||||
readonly reporter = core.getInput('reporter', {required: true})
|
||||
readonly listSuites = core.getInput('list-suites', {required: true}) as 'all' | 'failed'
|
||||
readonly listTests = core.getInput('list-tests', {required: true}) as 'all' | 'failed' | 'none'
|
||||
readonly maxAnnotations = parseInt(core.getInput('max-annotations', {required: true}))
|
||||
readonly failOnError = core.getInput('fail-on-error', {required: true}) === 'true'
|
||||
readonly workDirInput = core.getInput('working-directory', {required: false})
|
||||
readonly token = core.getInput('token', {required: true})
|
||||
readonly octokit: InstanceType<typeof GitHub>
|
||||
readonly context = getCheckRunContext()
|
||||
|
||||
if (listSuites !== 'all' && listSuites !== 'failed') {
|
||||
core.setFailed(`Input parameter 'list-suites' has invalid value`)
|
||||
return
|
||||
constructor() {
|
||||
this.octokit = github.getOctokit(this.token)
|
||||
|
||||
if (this.listSuites !== 'all' && this.listSuites !== 'failed') {
|
||||
core.setFailed(`Input parameter 'list-suites' has invalid value`)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.listTests !== 'all' && this.listTests !== 'failed' && this.listTests !== 'none') {
|
||||
core.setFailed(`Input parameter 'list-tests' has invalid value`)
|
||||
return
|
||||
}
|
||||
|
||||
if (isNaN(this.maxAnnotations) || this.maxAnnotations < 0 || this.maxAnnotations > 50) {
|
||||
core.setFailed(`Input parameter 'max-annotations' has invalid value`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (listTests !== 'all' && listTests !== 'failed' && listTests !== 'none') {
|
||||
core.setFailed(`Input parameter 'list-tests' has invalid value`)
|
||||
return
|
||||
async run(): Promise<void> {
|
||||
if (this.workDirInput) {
|
||||
core.info(`Changing directory to '${this.workDirInput}'`)
|
||||
process.chdir(this.workDirInput)
|
||||
}
|
||||
|
||||
const pattern = this.path.split(',')
|
||||
|
||||
const inputProvider = this.artifact
|
||||
? new ArtifactProvider(this.octokit, this.artifact, this.name, pattern, this.context.sha, this.context.runId)
|
||||
: new LocalFileProvider(this.name, pattern)
|
||||
|
||||
const parseErrors = this.maxAnnotations > 0
|
||||
const trackedFiles = await inputProvider.listTrackedFiles()
|
||||
const workDir = this.artifact ? undefined : normalizeDirPath(process.cwd(), true)
|
||||
|
||||
const options: ParseOptions = {
|
||||
workDir,
|
||||
trackedFiles,
|
||||
parseErrors
|
||||
}
|
||||
|
||||
core.info(`Using test report parser '${this.reporter}'`)
|
||||
const parser = this.getParser(this.reporter, options)
|
||||
|
||||
const results: TestRunResult[] = []
|
||||
const input = await inputProvider.load()
|
||||
for (const [reportName, files] of Object.entries(input)) {
|
||||
const tr = await this.createReport(parser, reportName, files)
|
||||
results.push(...tr)
|
||||
}
|
||||
|
||||
const isFailed = results.some(tr => tr.result === 'failed')
|
||||
const conclusion = isFailed ? 'failure' : 'success'
|
||||
const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
|
||||
const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
|
||||
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
|
||||
const time = results.reduce((sum, tr) => sum + tr.time, 0)
|
||||
|
||||
core.setOutput('conclusion', conclusion)
|
||||
core.setOutput('passed', passed)
|
||||
core.setOutput('failed', failed)
|
||||
core.setOutput('skipped', skipped)
|
||||
core.setOutput('time', time)
|
||||
|
||||
if (this.failOnError && isFailed) {
|
||||
core.setFailed(`Failed test has been found and 'fail-on-error' option is set to ${this.failOnError}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(maxAnnotations) || maxAnnotations < 0 || maxAnnotations > 50) {
|
||||
core.setFailed(`Input parameter 'max-annotations' has invalid value`)
|
||||
return
|
||||
async createReport(parser: TestParser, name: string, files: FileContent[]): Promise<TestRunResult[]> {
|
||||
if (files.length === 0) {
|
||||
core.error(`${name}: No file matches path ${this.path}`)
|
||||
return []
|
||||
}
|
||||
|
||||
const results: TestRunResult[] = []
|
||||
for (const {file, content} of files) {
|
||||
core.info(`Processing test report '${file}'`)
|
||||
const tr = await parser.parse(file, content)
|
||||
results.push(tr)
|
||||
}
|
||||
|
||||
core.info('Creating report summary')
|
||||
const {listSuites, listTests} = this
|
||||
const summary = getReport(results, {listSuites, listTests})
|
||||
|
||||
core.info('Creating annotations')
|
||||
const annotations = getAnnotations(results, this.maxAnnotations)
|
||||
|
||||
const isFailed = results.some(tr => tr.result === 'failed')
|
||||
const conclusion = isFailed ? 'failure' : 'success'
|
||||
const icon = isFailed ? Icon.fail : Icon.success
|
||||
|
||||
core.info(`Creating check run '${name}' with conclusion '${conclusion}'`)
|
||||
await this.octokit.checks.create({
|
||||
head_sha: this.context.sha,
|
||||
name,
|
||||
conclusion,
|
||||
status: 'completed',
|
||||
output: {
|
||||
title: `${name} ${icon}`,
|
||||
summary,
|
||||
annotations
|
||||
},
|
||||
...github.context.repo
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
if (workDirInput) {
|
||||
core.info(`Changing directory to '${workDirInput}'`)
|
||||
process.chdir(workDirInput)
|
||||
}
|
||||
|
||||
const workDir = normalizeDirPath(process.cwd(), true)
|
||||
core.info(`Using working-directory '${workDir}'`)
|
||||
const octokit = github.getOctokit(token)
|
||||
const sha = getCheckRunSha()
|
||||
|
||||
// We won't need tracked files if we are not going to create annotations
|
||||
const parseErrors = maxAnnotations > 0
|
||||
const trackedFiles = parseErrors ? await listFiles() : []
|
||||
|
||||
const options: ParseOptions = {
|
||||
trackedFiles,
|
||||
workDir,
|
||||
parseErrors
|
||||
}
|
||||
|
||||
core.info(`Using test report parser '${reporter}'`)
|
||||
const parser = getParser(reporter, options)
|
||||
|
||||
const files = await getFiles(path)
|
||||
if (files.length === 0) {
|
||||
core.setFailed(`No file matches path '${path}'`)
|
||||
return
|
||||
}
|
||||
|
||||
const results: TestRunResult[] = []
|
||||
for (const file of files) {
|
||||
core.info(`Processing test report '${file}'`)
|
||||
const content = await fs.promises.readFile(file, {encoding: 'utf8'})
|
||||
const tr = await parser.parse(file, content)
|
||||
results.push(tr)
|
||||
}
|
||||
|
||||
core.info('Creating report summary')
|
||||
const summary = getReport(results, {listSuites, listTests})
|
||||
|
||||
core.info('Creating annotations')
|
||||
const annotations = getAnnotations(results, maxAnnotations)
|
||||
|
||||
const isFailed = results.some(tr => tr.result === 'failed')
|
||||
const conclusion = isFailed ? 'failure' : 'success'
|
||||
const icon = isFailed ? Icon.fail : Icon.success
|
||||
|
||||
core.info(`Creating check run '${name}' with conclusion '${conclusion}'`)
|
||||
await octokit.checks.create({
|
||||
head_sha: sha,
|
||||
name,
|
||||
conclusion,
|
||||
status: 'completed',
|
||||
output: {
|
||||
title: `${name} ${icon}`,
|
||||
summary,
|
||||
annotations
|
||||
},
|
||||
...github.context.repo
|
||||
})
|
||||
|
||||
const passed = results.reduce((sum, tr) => sum + tr.passed, 0)
|
||||
const failed = results.reduce((sum, tr) => sum + tr.failed, 0)
|
||||
const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0)
|
||||
const time = results.reduce((sum, tr) => sum + tr.time, 0)
|
||||
|
||||
core.setOutput('conclusion', conclusion)
|
||||
core.setOutput('passed', passed)
|
||||
core.setOutput('failed', failed)
|
||||
core.setOutput('skipped', skipped)
|
||||
core.setOutput('time', time)
|
||||
|
||||
if (failOnError && isFailed) {
|
||||
core.setFailed(`Failed test has been found and 'fail-on-error' option is set to ${failOnError}`)
|
||||
getParser(reporter: string, options: ParseOptions): TestParser {
|
||||
switch (reporter) {
|
||||
case 'dart-json':
|
||||
return new DartJsonParser(options, 'dart')
|
||||
case 'dotnet-trx':
|
||||
return new DotnetTrxParser(options)
|
||||
case 'flutter-json':
|
||||
return new DartJsonParser(options, 'flutter')
|
||||
case 'jest-junit':
|
||||
return new JestJunitParser(options)
|
||||
default:
|
||||
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getParser(reporter: string, options: ParseOptions): TestParser {
|
||||
switch (reporter) {
|
||||
case 'dart-json':
|
||||
return new DartJsonParser(options, 'dart')
|
||||
case 'dotnet-trx':
|
||||
return new DotnetTrxParser(options)
|
||||
case 'flutter-json':
|
||||
return new DartJsonParser(options, 'flutter')
|
||||
case 'jest-junit':
|
||||
return new JestJunitParser(options)
|
||||
default:
|
||||
throw new Error(`Input variable 'reporter' is set to invalid value '${reporter}'`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFiles(pattern: string): Promise<string[]> {
|
||||
const tasks = pattern.split(',').map(async pat => glob(pat, {dot: true}))
|
||||
const paths = await Promise.all(tasks)
|
||||
return paths.flat()
|
||||
}
|
||||
|
||||
run()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
|
||||
import {normalizeFilePath} from '../../utils/file-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
ReportEvent,
|
||||
|
|
@ -72,6 +72,8 @@ class TestCase {
|
|||
}
|
||||
|
||||
export class DartJsonParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions, readonly sdk: 'dart' | 'flutter') {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
|
|
@ -207,14 +209,24 @@ export class DartJsonParser implements TestParser {
|
|||
}
|
||||
|
||||
private getRelativePath(path: string): string {
|
||||
const {workDir} = this.options
|
||||
const prefix = 'file://'
|
||||
if (path.startsWith(prefix)) {
|
||||
path = path.substr(prefix.length)
|
||||
}
|
||||
if (path.startsWith(workDir)) {
|
||||
|
||||
path = normalizeFilePath(path)
|
||||
const workDir = this.getWorkDir(path)
|
||||
if (workDir !== undefined && path.startsWith(workDir)) {
|
||||
path = path.substr(workDir.length)
|
||||
}
|
||||
return normalizeFilePath(path)
|
||||
return path
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {parseStringPromise} from 'xml2js'
|
|||
import {ErrorInfo, Outcome, TestMethod, TrxReport} from './dotnet-trx-types'
|
||||
import {ParseOptions, TestParser} from '../../test-parser'
|
||||
|
||||
import {normalizeFilePath} from '../../utils/file-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
import {parseIsoDate, parseNetDuration} from '../../utils/parse-utils'
|
||||
|
||||
import {
|
||||
|
|
@ -41,6 +41,8 @@ class Test {
|
|||
}
|
||||
|
||||
export class DotnetTrxParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
|
|
@ -137,19 +139,30 @@ export class DotnetTrxParser implements TestParser {
|
|||
private exceptionThrowSource(stackTrace: string): {path: string; line: number} | undefined {
|
||||
const lines = stackTrace.split(/\r*\n/)
|
||||
const re = / in (.+):line (\d+)$/
|
||||
const {workDir, trackedFiles} = this.options
|
||||
const {trackedFiles} = this.options
|
||||
|
||||
for (const str of lines) {
|
||||
const match = str.match(re)
|
||||
if (match !== null) {
|
||||
const [_, fileStr, lineStr] = match
|
||||
const filePath = normalizeFilePath(fileStr)
|
||||
const file = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath
|
||||
if (trackedFiles.includes(file)) {
|
||||
const line = parseInt(lineStr)
|
||||
return {path: file, line}
|
||||
const workDir = this.getWorkDir(filePath)
|
||||
if (workDir) {
|
||||
const file = filePath.substr(workDir.length)
|
||||
if (trackedFiles.includes(file)) {
|
||||
const line = parseInt(lineStr)
|
||||
return {path: file, line}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {ParseOptions, TestParser} from '../../test-parser'
|
|||
import {parseStringPromise} from 'xml2js'
|
||||
|
||||
import {JunitReport, TestCase, TestSuite} from './jest-junit-types'
|
||||
import {normalizeFilePath} from '../../utils/file-utils'
|
||||
import {getBasePath, normalizeFilePath} from '../../utils/path-utils'
|
||||
|
||||
import {
|
||||
TestExecutionResult,
|
||||
|
|
@ -14,6 +14,8 @@ import {
|
|||
} from '../../test-results'
|
||||
|
||||
export class JestJunitParser implements TestParser {
|
||||
assumedWorkDir: string | undefined
|
||||
|
||||
constructor(readonly options: ParseOptions) {}
|
||||
|
||||
async parse(path: string, content: string): Promise<TestRunResult> {
|
||||
|
|
@ -96,13 +98,20 @@ export class JestJunitParser implements TestParser {
|
|||
const lines = stackTrace.split(/\r?\n/)
|
||||
const re = /\((.*):(\d+):\d+\)$/
|
||||
|
||||
const {workDir, trackedFiles} = this.options
|
||||
const {trackedFiles} = this.options
|
||||
for (const str of lines) {
|
||||
const match = str.match(re)
|
||||
if (match !== null) {
|
||||
const [_, fileStr, lineStr] = match
|
||||
const filePath = normalizeFilePath(fileStr)
|
||||
const path = filePath.startsWith(workDir) ? filePath.substr(workDir.length) : filePath
|
||||
if (filePath.startsWith('internal/') || filePath.includes('/node_modules/')) {
|
||||
continue
|
||||
}
|
||||
const workDir = this.getWorkDir(filePath)
|
||||
if (!workDir) {
|
||||
continue
|
||||
}
|
||||
const path = filePath.substr(workDir.length)
|
||||
if (trackedFiles.includes(path)) {
|
||||
const line = parseInt(lineStr)
|
||||
|
||||
|
|
@ -111,4 +120,12 @@ export class JestJunitParser implements TestParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getWorkDir(path: string): string | undefined {
|
||||
return (
|
||||
this.options.workDir ??
|
||||
this.assumedWorkDir ??
|
||||
(this.assumedWorkDir = getBasePath(path, this.options.trackedFiles))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {TestRunResult} from './test-results'
|
|||
|
||||
export interface ParseOptions {
|
||||
parseErrors: boolean
|
||||
workDir: string
|
||||
workDir?: string
|
||||
trackedFiles: string[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
export function normalizeDirPath(path: string, addTrailingSlash: boolean): string {
|
||||
if (!path) {
|
||||
return path
|
||||
}
|
||||
|
||||
path = normalizeFilePath(path)
|
||||
if (addTrailingSlash && !path.endsWith('/')) {
|
||||
path += '/'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function normalizeFilePath(path: string): string {
|
||||
if (!path) {
|
||||
return path
|
||||
}
|
||||
|
||||
return path.trim().replace(/\\/g, '/')
|
||||
}
|
||||
|
|
@ -1,11 +1,92 @@
|
|||
import {createWriteStream} from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import {GitHub} from '@actions/github/lib/utils'
|
||||
import {EventPayloads} from '@octokit/webhooks'
|
||||
import * as stream from 'stream'
|
||||
import {promisify} from 'util'
|
||||
import got from 'got'
|
||||
const asyncStream = promisify(stream.pipeline)
|
||||
|
||||
export function getCheckRunSha(): string {
|
||||
if (github.context.payload.pull_request) {
|
||||
const pr = github.context.payload.pull_request as EventPayloads.WebhookPayloadPullRequestPullRequest
|
||||
return pr.head.sha
|
||||
export function getCheckRunContext(): {sha: string; runId: number} {
|
||||
if (github.context.eventName === 'workflow_run') {
|
||||
const event = github.context.payload as EventPayloads.WebhookPayloadWorkflowRun
|
||||
if (!event.workflow_run) {
|
||||
throw new Error("Event of type 'workflow_run' is missing 'workflow_run' field")
|
||||
}
|
||||
return {
|
||||
sha: event.workflow_run.head_commit.id,
|
||||
runId: event.workflow_run.id
|
||||
}
|
||||
}
|
||||
|
||||
return github.context.sha
|
||||
const runId = github.context.runId
|
||||
if (github.context.eventName === 'pullrequest' && github.context.payload.pull_request) {
|
||||
const pr = github.context.payload.pull_request as EventPayloads.WebhookPayloadPullRequestPullRequest
|
||||
return {sha: pr.head.sha, runId}
|
||||
}
|
||||
|
||||
return {sha: github.context.sha, runId}
|
||||
}
|
||||
|
||||
export async function downloadArtifact(
|
||||
octokit: InstanceType<typeof GitHub>,
|
||||
artifactId: number,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
const resp = await octokit.actions.downloadArtifact({
|
||||
...github.context.repo,
|
||||
artifact_id: artifactId,
|
||||
archive_format: 'zip'
|
||||
})
|
||||
|
||||
const url = resp.headers.location
|
||||
|
||||
if (url === undefined) {
|
||||
throw new Error('Location header was not found in API response')
|
||||
}
|
||||
|
||||
const downloadStream = got.stream(url)
|
||||
const fileWriterStream = createWriteStream(fileName)
|
||||
|
||||
downloadStream.on('downloadProgress', ({transferred, total, percent}) => {
|
||||
const percentage = Math.round(percent * 100)
|
||||
core.info(`progress: ${transferred}/${total} (${percentage}%)`)
|
||||
})
|
||||
|
||||
core.startGroup(`Downloading ${fileName} from ${url}`)
|
||||
try {
|
||||
await asyncStream(downloadStream, fileWriterStream)
|
||||
} finally {
|
||||
core.endGroup()
|
||||
}
|
||||
}
|
||||
|
||||
export async function listFiles(octokit: InstanceType<typeof GitHub>, sha: string): Promise<string[]> {
|
||||
core.info('Fetching list of tracked files from GitHub')
|
||||
const commit = await octokit.git.getCommit({
|
||||
commit_sha: sha,
|
||||
...github.context.repo
|
||||
})
|
||||
return await listGitTree(octokit, commit.data.tree.sha, '')
|
||||
}
|
||||
|
||||
async function listGitTree(octokit: InstanceType<typeof GitHub>, sha: string, path: string): Promise<string[]> {
|
||||
const tree = await octokit.git.getTree({
|
||||
tree_sha: sha,
|
||||
...github.context.repo
|
||||
})
|
||||
|
||||
const result: string[] = []
|
||||
for (const tr of tree.data.tree) {
|
||||
const file = `${path}${tr.path}`
|
||||
if (tr.type === 'tree') {
|
||||
const files = await listGitTree(octokit, tr.sha, `${file}/`)
|
||||
result.push(...files)
|
||||
} else {
|
||||
result.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
39
src/utils/path-utils.ts
Normal file
39
src/utils/path-utils.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
export function normalizeDirPath(path: string, addTrailingSlash: boolean): string {
|
||||
if (!path) {
|
||||
return path
|
||||
}
|
||||
|
||||
path = normalizeFilePath(path)
|
||||
if (addTrailingSlash && !path.endsWith('/')) {
|
||||
path += '/'
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
export function normalizeFilePath(path: string): string {
|
||||
if (!path) {
|
||||
return path
|
||||
}
|
||||
|
||||
return path.trim().replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
export function getBasePath(path: string, trackedFiles: string[]): string | undefined {
|
||||
if (trackedFiles.includes(path)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let max = ''
|
||||
for (const file of trackedFiles) {
|
||||
if (path.endsWith(file) && file.length > max.length) {
|
||||
max = file
|
||||
}
|
||||
}
|
||||
|
||||
if (max === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const base = path.substr(0, path.length - max.length)
|
||||
return base
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue