Skip to content

Commit

Permalink
feat: Output test result in github actions
Browse files Browse the repository at this point in the history
  • Loading branch information
Kesin11 committed May 31, 2020
1 parent cccd178 commit 82481e7
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 23 deletions.
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"axios": "0.19.2",
"dayjs": "1.8.27",
"js-yaml": "3.13.1",
"junit2json": "1.0.1",
"lodash": "4.17.15",
"yargs": "15.3.1"
},
Expand All @@ -49,6 +50,7 @@
"@types/js-yaml": "3.12.4",
"@types/lodash": "4.14.151",
"@types/node": "14.0.1",
"@types/xml2js": "0.4.5",
"@types/yargs": "15.0.5",
"codecov": "3.6.5",
"jest": "26.0.1",
Expand Down
9 changes: 9 additions & 0 deletions src/analyzer/analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { round } from "lodash"
import { TestSuites } from 'junit2json'

export type Status = 'SUCCESS' | 'FAILURE' | 'ABORTED' | 'OTHER'

Expand Down Expand Up @@ -51,6 +52,14 @@ type JobParameter = {
value: string
}

export type TestReport = {
workflowId: string
workflowRunId: string
buildNumber: number
workflowName: string
testSuites: TestSuites
}

export interface Analyzer {
createWorkflowReport(...args: any[]): WorkflowReport
}
Expand Down
29 changes: 28 additions & 1 deletion src/analyzer/github_analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods'
import { sumBy, min, max } from 'lodash'
import { Analyzer, diffSec, Status } from './analyzer'
import { Analyzer, diffSec, Status, TestReport } from './analyzer'
import { RepositoryTagMap } from '../client/github_repository_client'
import { TestSuites, parse } from 'junit2json'
import AdmZip from 'adm-zip'
export type WorkflowRunsItem = RestEndpointMethodTypes['actions']['listRepoWorkflowRuns']['response']['data']['workflow_runs'][0]
export type JobsItem = RestEndpointMethodTypes['actions']['listJobsForWorkflowRun']['response']['data']['jobs']

Expand Down Expand Up @@ -132,4 +134,29 @@ export class GithubAnalyzer implements Analyzer {
return 'OTHER';
}
}

async createTestReports(workflowName: string, workflow: WorkflowRunsItem, tests: AdmZip.IZipEntry[]): Promise<TestReport[]> {
const buildNumber = workflow.run_number
const repository = workflow.repository.full_name
const workflowId = `${repository}-${workflowName}`
const workflowRunId = `${repository}-${workflowName}-${buildNumber}`

const testReports: TestReport[] = []
for (const test of tests) {
const xmlString = test.getData().toString('utf-8')
try {
const testSuites = await parse(xmlString)
testReports.push({
workflowId,
workflowRunId,
buildNumber,
workflowName,
testSuites,
})
} catch (error) {
console.error(`Error: Could not parse as JUnit XML. ${test.entryName}`)
}
}
return testReports
}
}
6 changes: 0 additions & 6 deletions src/artifact_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,5 @@ export class ArtifactExtractor {
return !entry.isDirectory &&
globs.some((glob) => minimatch(entry.entryName, glob))
})
// .forEach((zipEntry) => {
// console.log(zipEntry.toString()) // outputs zip entries information
// console.log(zipEntry.entryName) // "zip_test/nest/nest.txt"
// console.log(zipEntry.name) // next.txt
// if (!zipEntry.isDirectory) console.log(zipEntry.getData().toString('utf8'))
// })
}
}
43 changes: 43 additions & 0 deletions src/client/github_client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
import axios, { AxiosInstance } from 'axios'
import { axiosRequestLogger } from './client'
import { minBy } from "lodash";
import { ArtifactExtractor } from "../artifact_extractor";

// Oktokit document: https://octokit.github.io/rest.js/v17#actions

Expand All @@ -11,12 +14,23 @@ type RunStatus = 'queued' | 'in_progress' | 'completed'

export class GithubClient {
private octokit: Octokit
private axios: AxiosInstance
constructor(token: string, baseUrl?: string) {
this.octokit = new Octokit({
auth: token,
baseUrl: (baseUrl) ? baseUrl : 'https://api.github.com',
log: (process.env['CI_ANALYZER_DEBUG']) ? console : undefined,
})

this.axios = axios.create({
baseURL: (baseUrl) ? baseUrl : 'https://api.github.com',
timeout: 1000,
auth: { username: '', password: token },
});

if (process.env['CI_ANALYZER_DEBUG']) {
this.axios.interceptors.request.use(axiosRequestLogger)
}
}

// see: https://developer.github.com/v3/actions/workflow-runs/#list-repository-workflow-runs
Expand Down Expand Up @@ -80,4 +94,33 @@ export class GithubClient {
})
return jobs.data.jobs
}

async fetchArtifacts(owner: string, repo: string, runId: number) {
const res = await this.octokit.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: runId
})
return res.data.artifacts
}

async fetchTests(owner: string, repo: string, runId: number, globs: string[]) {
// Skip if test file globs not provided
if (globs.length < 1) return []

const artifacts = await this.fetchArtifacts(owner, repo, runId)

const artifactsExtractor = new ArtifactExtractor()
for (const artifact of artifacts) {
const res = await this.axios.get(
artifact.archive_download_url,
{ responseType: 'arraybuffer'}
)
await artifactsExtractor.put(artifact.name, res.data)
}

const tests = await artifactsExtractor.extract(globs)
await artifactsExtractor.rmTmpZip()
return tests
}
}
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type YamlConfig = {
}

export type CommonConfig = {
baseUrl?: string
exporter?: ExporterConfig
lastRunStore?: LastRunStoreConfig
vscBaseUrl?: {
Expand Down
24 changes: 20 additions & 4 deletions src/config/github_config.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { YamlConfig, CommonConfig } from './config'

export type GithubConfig = CommonConfig & {
baseUrl?: string
repos: {
owner: string
repo: string
fullname: string
testGlob: string[]
}[]
}

type RepoYaml = string | {
name: string
tests: string[]
}

export const parseConfig = (config: YamlConfig): GithubConfig | undefined => {
if (!config.github) return

const githubConfig = config.github
// overwrite repos
githubConfig.repos = githubConfig.repos.map((fullname: string) => {
const [owner, repo] = fullname.split('/')
return { owner, repo, fullname }
githubConfig.repos = githubConfig.repos.map((repoYaml: RepoYaml) => {
let owner, repo
if (typeof repoYaml === 'string') {
[owner, repo] = repoYaml.split('/')
return { owner, repo, fullname: repoYaml, testGlob: [] }
}

[owner, repo] = repoYaml.name.split('/')
return {
owner,
repo,
fullname: repoYaml.name,
testGlob: repoYaml.tests
}
})

return githubConfig as GithubConfig
Expand Down
4 changes: 3 additions & 1 deletion src/exporter/bigquery_exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from "path"
import fs from "fs"
import crypto from "crypto"
import { BigQuery } from '@google-cloud/bigquery'
import { WorkflowReport } from "../analyzer/analyzer"
import { WorkflowReport, TestReport } from "../analyzer/analyzer"
import { Exporter } from "./exporter"

export class BigqueryExporter implements Exporter {
Expand Down Expand Up @@ -66,4 +66,6 @@ export class BigqueryExporter implements Exporter {
formatJsonLines (reports: WorkflowReport[]): string {
return reports.map((report) => JSON.stringify(report)).join("\n")
}

async exportTestReports (reports: TestReport[]) { }
}
9 changes: 8 additions & 1 deletion src/exporter/exporter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { LocalExporter } from "./local_exporter";
import { WorkflowReport } from "../analyzer/analyzer";
import { WorkflowReport, TestReport } from "../analyzer/analyzer";
import { ExporterConfig } from "../config/config";
import { BigqueryExporter } from "./bigquery_exporter";

export interface Exporter {
exportReports(reports: WorkflowReport[]): Promise<void>
exportTestReports(reports: TestReport[]): Promise<void>
}

export class CompositExporter implements Exporter {
Expand Down Expand Up @@ -32,4 +33,10 @@ export class CompositExporter implements Exporter {
this.exporters.map((exporter) => exporter?.exportReports(reports))
)
}

async exportTestReports(reports: TestReport[]) {
await Promise.all(
this.exporters.map((exporter) => exporter?.exportTestReports(reports))
)
}
}
20 changes: 16 additions & 4 deletions src/exporter/local_exporter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "path"
import fs from "fs"
import dayjs from 'dayjs'
import { WorkflowReport } from "../analyzer/analyzer"
import { WorkflowReport, TestReport } from "../analyzer/analyzer"
import { Exporter } from "./exporter"

type Format = 'json' | 'json_lines'
Expand All @@ -10,7 +10,7 @@ const defaultOutDir = 'output'
export class LocalExporter implements Exporter {
service: string
outDir: string
formatter: (report: WorkflowReport[]) => string
formatter: (report: unknown[]) => string
constructor(
service: string,
configDir: string,
Expand All @@ -37,11 +37,23 @@ export class LocalExporter implements Exporter {
console.info(`(Local) Export reports to ${outputPath}`)
}

formatJson (reports: WorkflowReport[]): string {
formatJson (reports: unknown[]): string {
return JSON.stringify(reports, null, 2)
}

formatJsonLines (reports: WorkflowReport[]): string {
formatJsonLines (reports: unknown[]): string {
return reports.map((report) => JSON.stringify(report)).join("\n")
}

async exportTestReports (reports: TestReport[]) {
fs.mkdirSync(this.outDir, { recursive: true })

const now = dayjs()
const outputPath = path.join(this.outDir, `${now.format('YYYYMMDD-HHmm')}-test-${this.service}.json`)

const formated = this.formatter(reports)
fs.writeFileSync(outputPath, formated, { encoding: 'utf8' })

console.info(`(Local) Export test reports to ${outputPath}`)
}
}
Loading

0 comments on commit 82481e7

Please sign in to comment.