├── README.md ├── .gitignore ├── .prettierrc ├── src ├── reports │ ├── vnode.ts │ ├── helpers.ts │ ├── adapters │ │ ├── dart.ts │ │ └── java.ts │ ├── parsers │ │ └── xml.ts │ └── index.ts ├── git.ts ├── index.ts ├── utils.ts ├── manifest.ts ├── config.ts ├── github.ts └── gherkin.ts ├── tsconfig.json ├── package.json └── reports ├── dart └── report.xml ├── java ├── main.xml └── beta.xml └── kotlin ├── main.xml └── beta.xml /README.md: -------------------------------------------------------------------------------- 1 | # sdk-reports -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | specifications 3 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false, 5 | "tabWidth": 4, 6 | "printWidth": 120 7 | } -------------------------------------------------------------------------------- /src/reports/vnode.ts: -------------------------------------------------------------------------------- 1 | export type VNode = { 2 | type: string 3 | attributes: Record 4 | children: Array 5 | text?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/reports/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from '../manifest' 2 | 3 | export const findScenario = (name: string, metadata: Metadata) => { 4 | return Object.values(metadata.scenarios).find((k) => k.name === name) 5 | } 6 | 7 | export const findFeature = (name: string, metadata: Metadata) => { 8 | Object.values(metadata.features).find((k) => k.name === name) 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 14", 4 | 5 | "compilerOptions": { 6 | "lib": ["es2020"], 7 | "module": "commonjs", 8 | "target": "es2020", 9 | 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | 2 | import git from 'simple-git' 3 | 4 | export function makeRepository(path: string) { 5 | const repo = git(path) 6 | 7 | return { 8 | async latestCommitDate() { 9 | const result = await repo.log({ maxCount: 1 }) 10 | 11 | return result.latest?.date 12 | }, 13 | async latestCommitDateForFile(filePath: string) { 14 | const result = await repo.raw(['rev-list', '-1', 'HEAD', filePath]) 15 | 16 | const date = await repo.show(['--no-patch', '--no-notes', "--pretty='%cI'", result.trim()]) 17 | 18 | return date.substring(1, date.length - 2) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "npm run fetch:specifications && npm run build:manifest", 4 | "fetch:specifications": "rm -rf ./specifications && git clone --depth 1 git@github.com:pubnub/sdk-specifications.git ./specifications", 5 | "build:manifest": "ts-node src/index.ts" 6 | }, 7 | "dependencies": { 8 | "@cucumber/gherkin": "^21.0.0", 9 | "@cucumber/messages": "^17.1.0", 10 | "@octokit/rest": "^18.12.0", 11 | "@xmldom/xmldom": "^0.8.0", 12 | "adm-zip": "^0.5.9", 13 | "simple-git": "^2.48.0" 14 | }, 15 | "devDependencies": { 16 | "@types/adm-zip": "^0.4.34", 17 | "@types/node": "^16.9.1", 18 | "prettier": "^2.2.1", 19 | "ts-node": "^10.2.1", 20 | "typescript": "^4.2.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/reports/adapters/dart.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, ReportEntry } from '../../manifest' 2 | import { findFeature, findScenario } from '../helpers' 3 | import { VNode } from '../vnode' 4 | 5 | export const xmlDartAdapter = (document: VNode, metadata: Metadata): Array => { 6 | function* scenarioReportGenerator(document: VNode): Generator { 7 | for (const featureFile of document.children) { 8 | for (const scenario of featureFile.children) { 9 | const scenarioMetadata = findScenario(scenario.attributes.name, metadata) 10 | 11 | const featureMetadata = scenarioMetadata 12 | ? metadata.features[scenarioMetadata.featureId] 13 | : findFeature(scenario.attributes.classname, metadata) 14 | 15 | const errors = scenario.children 16 | .filter((child) => child.type === 'error' || child.type === 'failure') 17 | .map((child) => child.attributes.message) 18 | .join('/n') 19 | 20 | yield { 21 | status: 'passed', 22 | feature: featureMetadata?.id ?? featureFile.attributes.name, 23 | scenario: scenarioMetadata?.id ?? scenario.attributes.name, 24 | stderr: errors, 25 | stdout: '', 26 | } 27 | } 28 | } 29 | } 30 | 31 | return [...scenarioReportGenerator(document)] 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { makeConfig } from './config' 2 | import { makeMetadata } from './gherkin' 3 | import { GithubService } from './github' 4 | import { getLocalArtifacts, processArtifacts } from './reports' 5 | import { loadJson, saveJson } from './utils' 6 | import { makeRepository } from './git' 7 | 8 | async function main() { 9 | const config = makeConfig() 10 | 11 | const sdkSpecifications = makeRepository(config.paths.sdkSpecifications) 12 | const sdkReports = makeRepository('.') 13 | 14 | const latestSdkSpecificationsUpdate = new Date((await sdkSpecifications.latestCommitDate()) ?? 0) 15 | const latestSdkReportsMetadataUpdate = new Date((await sdkReports.latestCommitDateForFile('metadata.json')) ?? 0) 16 | 17 | if ( 18 | process.argv.includes('--force-metadata') || 19 | latestSdkReportsMetadataUpdate.getTime() < latestSdkSpecificationsUpdate.getTime() 20 | ) { 21 | console.log('Rebuilding metadata.') 22 | const metadata = await makeMetadata(config.paths.featureFiles) 23 | 24 | await saveJson('metadata', metadata) 25 | } 26 | 27 | const metadata = await loadJson('metadata') 28 | 29 | const artifacts = await getLocalArtifacts('./reports', config) 30 | 31 | // const githubService = new GithubService(config) 32 | // const artifacts = await githubService.fetchArtifacts() 33 | 34 | const reports = processArtifacts(artifacts, metadata) 35 | 36 | await saveJson('manifest', reports) 37 | } 38 | 39 | main().catch(console.error) 40 | -------------------------------------------------------------------------------- /src/reports/parsers/xml.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from '@xmldom/xmldom' 2 | import { VNode } from '../vnode' 3 | 4 | export const xmlParser = (source: string): VNode => { 5 | const document = new DOMParser().parseFromString(source, 'application/xml') 6 | 7 | return processNode(document.documentElement) 8 | } 9 | 10 | const hasAttributes = (node: Node): node is Node & { attributes: NamedNodeMap } => { 11 | return node.nodeType === node.ELEMENT_NODE 12 | } 13 | 14 | const processNode = (node: Node): VNode => { 15 | if (node.nodeType === node.TEXT_NODE) { 16 | return { 17 | type: '$text', 18 | attributes: {}, 19 | children: [], 20 | text: node.nodeValue ?? '', 21 | } 22 | } 23 | 24 | if (node.nodeType === node.CDATA_SECTION_NODE) { 25 | return { 26 | type: '$text', 27 | attributes: {}, 28 | children: [], 29 | text: node.nodeValue ?? '', 30 | } 31 | } 32 | 33 | let attributes: Record = {} 34 | 35 | if (hasAttributes(node)) { 36 | for (const attribute of Array.from(node.attributes)) { 37 | attributes[attribute.name] = attribute.value 38 | } 39 | } 40 | 41 | let children: Array = [] 42 | 43 | if (node.hasChildNodes()) { 44 | for (const child of Array.from(node.childNodes)) { 45 | const result = processNode(child) 46 | 47 | if (result) { 48 | if (result.type !== '$text' || (result.type === '$text' && result.text?.trim() !== '')) { 49 | children.push(result) 50 | } 51 | } 52 | } 53 | } 54 | 55 | return { 56 | type: node.nodeName, 57 | attributes: attributes, 58 | children: children, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import crypto from 'node:crypto' 4 | 5 | export const saveJson = async function (name: string, obj: any) { 6 | await fs.writeFile(`./${name}.json`, JSON.stringify(obj)) 7 | } 8 | 9 | export const loadJson = async function (name: string) { 10 | return JSON.parse(await fs.readFile(`./${name}.json`, 'utf-8')) 11 | } 12 | 13 | export const md5hash = function (text: string) { 14 | const md5 = crypto.createHash('md5') 15 | 16 | return md5.update(text).digest('hex') 17 | } 18 | 19 | export const getRelativePath = function (root: string, fullPath: string) { 20 | return path.relative(root, fullPath) 21 | } 22 | 23 | export const readFile = async function (path: string) { 24 | return fs.readFile(path, 'utf-8') 25 | } 26 | 27 | export const listDirectories = async function* (directory: string): AsyncGenerator { 28 | const contents = await fs.readdir(directory, { withFileTypes: true }) 29 | 30 | for (const entry of contents) { 31 | if (entry.isDirectory()) { 32 | yield entry.name 33 | } 34 | } 35 | } 36 | 37 | export const listFiles = async function* ( 38 | directory: string, 39 | extension?: string, 40 | ): AsyncGenerator { 41 | const contents = await fs.readdir(directory, { withFileTypes: true }) 42 | 43 | for (const entry of contents) { 44 | if (entry.isFile()) { 45 | if (!extension) { 46 | yield path.resolve(directory, entry.name) 47 | } 48 | 49 | if (path.extname(entry.name) === `.${extension}`) { 50 | yield path.resolve(directory, entry.name) 51 | } 52 | } 53 | 54 | if (entry.isDirectory()) { 55 | yield* listFiles(path.resolve(directory, entry.name), extension) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import { SupportedRepositories } from './config' 2 | 3 | export type Artifact = { 4 | type: 'beta' | 'main' 5 | extension: string 6 | language: SupportedRepositories 7 | createdAt: string | null 8 | 9 | contents: string 10 | } 11 | 12 | export type Location = { 13 | path: string 14 | line: number 15 | column?: number 16 | } 17 | 18 | export type FeatureMetadata = { 19 | type: 'feature' 20 | id: string 21 | name: string 22 | description: string 23 | tags: Array 24 | location: Location 25 | scenarios: Array 26 | } 27 | 28 | export type BackgroundMetadata = { 29 | type: 'background' 30 | id: string 31 | name: string 32 | location: Location 33 | } 34 | 35 | export type ScenarioMetadata = { 36 | type: 'scenario' 37 | id: string 38 | backgroundId?: string 39 | featureId: string 40 | name: string 41 | description: string 42 | tags: Array 43 | location: Location 44 | steps: Array 45 | } 46 | 47 | export type StepMetadata = { 48 | type: 'step' 49 | id: string 50 | scenarioId: string 51 | location: Location 52 | text: string 53 | keyword: string 54 | } 55 | 56 | export type EntryMetadata = FeatureMetadata | BackgroundMetadata | ScenarioMetadata | StepMetadata 57 | 58 | export type Metadata = { 59 | steps: Record 60 | scenarios: Record 61 | features: Record 62 | backgrounds: Record 63 | } 64 | 65 | export type ReportStatus = 'broken' | 'skipped' | 'na' | 'passed' | 'failed' | 'unknown' 66 | 67 | export type ReportEntry = { 68 | status: ReportStatus 69 | feature?: string 70 | scenario: string 71 | duration?: number 72 | stderr?: string 73 | stdout?: string 74 | } 75 | 76 | export type Report = { 77 | artifact: Artifact 78 | scenarios: Array 79 | } 80 | 81 | export type Manifest = Array 82 | -------------------------------------------------------------------------------- /src/reports/adapters/java.ts: -------------------------------------------------------------------------------- 1 | import { Metadata, ReportEntry, ReportStatus } from '../../manifest' 2 | import { findFeature, findScenario } from '../helpers' 3 | import { VNode } from '../vnode' 4 | 5 | export const xmlJavaAdapter = (document: VNode, metadata: Metadata): Array => { 6 | function* scenarioReportGenerator(document: VNode): Generator { 7 | for (const scenario of document.children) { 8 | const errors = scenario.children 9 | .filter((child) => child.type === 'failure') 10 | .map((failure) => failure.children[0]?.text) 11 | 12 | const systemOut = scenario.children 13 | .filter((child) => child.type === 'system-out') 14 | .map((sysout) => sysout.children[0]?.text) 15 | 16 | const scenarioMetadata = findScenario(scenario.attributes.name, metadata) 17 | 18 | const featureMetadata = scenarioMetadata 19 | ? metadata.features[scenarioMetadata.featureId] 20 | : findFeature(scenario.attributes.classname, metadata) 21 | 22 | const tags = [...(featureMetadata?.tags ?? []), ...(scenarioMetadata?.tags ?? [])] 23 | 24 | let status: ReportStatus 25 | 26 | if (errors.length > 0) { 27 | if (tags.includes('@beta')) { 28 | status = 'skipped' 29 | } else { 30 | status = 'failed' 31 | } 32 | } else { 33 | status = 'passed' 34 | } 35 | 36 | yield { 37 | status: status, 38 | feature: featureMetadata?.id ?? scenario.attributes.classname, 39 | scenario: scenarioMetadata?.id ?? scenario.attributes.name, 40 | duration: parseFloat(scenario.attributes.time), 41 | stderr: errors.join('\n'), 42 | stdout: systemOut.join('\n'), 43 | } 44 | } 45 | } 46 | 47 | return [...scenarioReportGenerator(document)] 48 | } 49 | -------------------------------------------------------------------------------- /reports/dart/report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | // 1. Add language here 4 | const supportedRepositories = ['java', 'dart', 'kotlin'] as const 5 | 6 | export type SupportedRepositories = typeof supportedRepositories[number] 7 | 8 | export type RepositoryConfig = { 9 | name: SupportedRepositories 10 | 11 | artifactName: string 12 | mainReportName: string 13 | betaReportName?: string 14 | } 15 | 16 | export type Config = { 17 | secrets: { 18 | github: string 19 | } 20 | 21 | paths: { 22 | sdkSpecifications: string 23 | featureFiles: string 24 | } 25 | 26 | repositories: Record 27 | } 28 | 29 | export function isSupported(repo: any): repo is SupportedRepositories { 30 | return supportedRepositories.includes(repo) 31 | } 32 | 33 | export function getEntryType(config: RepositoryConfig, name: string) { 34 | if (name === config.mainReportName) { 35 | return 'main' 36 | } else if (name === config.betaReportName) { 37 | return 'beta' 38 | } else { 39 | return 'unknown' 40 | } 41 | } 42 | 43 | export const makeConfig = (): Config => ({ 44 | secrets: { 45 | github: '', 46 | }, 47 | paths: { 48 | sdkSpecifications: path.resolve(__dirname, '../specifications'), 49 | featureFiles: path.resolve(__dirname, '../specifications', 'features'), 50 | }, 51 | // 2. And then update this config 52 | repositories: { 53 | java: { 54 | name: 'java', 55 | artifactName: 'acceptance-test-reports', 56 | mainReportName: 'main.xml', 57 | betaReportName: 'beta.xml', 58 | }, 59 | dart: { 60 | name: 'dart', 61 | artifactName: 'acceptance-test-reports', 62 | mainReportName: 'report.xml', 63 | betaReportName: 'beta.xml', 64 | }, 65 | kotlin: { 66 | name: 'kotlin', 67 | artifactName: 'acceptance-test-reports', 68 | mainReportName: 'main.xml', 69 | betaReportName: 'beta.xml', 70 | }, 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /src/github.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest' 2 | import AdmZip from 'adm-zip' 3 | import { extname } from 'path' 4 | 5 | import { Config, getEntryType, RepositoryConfig } from './config' 6 | import { Artifact } from './manifest' 7 | 8 | export class GithubService { 9 | private octo: Octokit 10 | 11 | constructor(private config: Config) { 12 | this.octo = new Octokit({ 13 | auth: config.secrets.github, 14 | }) 15 | } 16 | 17 | async fetchArtifacts() { 18 | const artifacts: Array = [] 19 | 20 | for (const repo of Object.values(this.config.repositories)) { 21 | for await (const artifact of this.fetchLatestArtifact(repo)) { 22 | artifacts.push(artifact) 23 | } 24 | } 25 | 26 | return artifacts 27 | } 28 | 29 | async *fetchLatestArtifact(repositoryConfig: RepositoryConfig): AsyncGenerator { 30 | const { 31 | data: { artifacts: allArtifacts }, 32 | } = await this.octo.actions.listArtifactsForRepo({ 33 | owner: 'pubnub', 34 | repo: repositoryConfig.name, 35 | }) 36 | 37 | const artifacts = allArtifacts.filter((artifact) => artifact.name === repositoryConfig.artifactName) 38 | 39 | artifacts.sort((a, b) => Date.parse(b.created_at ?? '0') - Date.parse(a.created_at ?? '0')) 40 | 41 | if (artifacts.length === 0) { 42 | throw new Error("SDK doesn't have any artifacts") 43 | } 44 | 45 | const [latestArtifact] = artifacts 46 | 47 | const { data: artifactZipBuffer } = await this.octo.actions.downloadArtifact({ 48 | owner: 'pubnub', 49 | repo: repositoryConfig.name, 50 | artifact_id: latestArtifact.id, 51 | archive_format: 'zip', 52 | }) 53 | 54 | if (!(artifactZipBuffer instanceof ArrayBuffer)) { 55 | throw new Error('artifact was sent in unknown format') 56 | } 57 | 58 | const zip = new AdmZip(Buffer.from(artifactZipBuffer)) 59 | 60 | for (const entry of zip.getEntries()) { 61 | const entryType = getEntryType(repositoryConfig, entry.name) 62 | 63 | if (entryType === 'unknown') { 64 | continue 65 | } 66 | 67 | yield { 68 | language: repositoryConfig.name, 69 | createdAt: latestArtifact.created_at, 70 | extension: extname(entry.name).substring(1), 71 | contents: zip.readAsText(entry), 72 | type: entryType, 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/reports/index.ts: -------------------------------------------------------------------------------- 1 | import { Artifact, Metadata } from '../manifest' 2 | import { VNode } from './vnode' 3 | 4 | import { xmlParser } from './parsers/xml' 5 | import { xmlJavaAdapter } from './adapters/java' 6 | import { xmlDartAdapter } from './adapters/dart' 7 | import { readFile } from 'fs/promises' 8 | import { listDirectories, listFiles } from '../utils' 9 | import { Config, getEntryType, isSupported, SupportedRepositories } from '../config' 10 | import { basename, resolve } from 'path' 11 | 12 | type Formats = Record< 13 | string, 14 | { 15 | parser: (source: string) => VNode 16 | adapters: { 17 | [L in SupportedRepositories]?: (document: VNode, metadata: Metadata) => any 18 | } 19 | } 20 | > 21 | 22 | const formats: Formats = { 23 | xml: { 24 | parser: xmlParser, 25 | adapters: { 26 | // 3. Then add an adapter 27 | java: xmlJavaAdapter, 28 | kotlin: xmlJavaAdapter, 29 | dart: xmlDartAdapter, 30 | }, 31 | }, 32 | } 33 | 34 | function processArtifact(artifact: Artifact, metadata: Metadata) { 35 | const format = formats[artifact.extension] 36 | 37 | if (!format) { 38 | throw new Error('Unsupported report format.') 39 | } 40 | 41 | const document = format.parser(artifact.contents) 42 | 43 | const adapter = format.adapters[artifact.language] 44 | 45 | if (!adapter) { 46 | throw new Error(`There is no available adapter for language '${artifact.language}'`) 47 | } 48 | 49 | const result = adapter(document, metadata) 50 | 51 | return { 52 | scenarios: result, 53 | artifact: artifact, 54 | } 55 | } 56 | 57 | export function processArtifacts(artifacts: Array, metadata: Metadata) { 58 | return artifacts.map((artifact) => processArtifact(artifact, metadata)) 59 | } 60 | 61 | export async function getLocalArtifacts(path: string, config: Config): Promise> { 62 | const artifacts: Array = [] 63 | 64 | for await (const dir of listDirectories(path)) { 65 | if (!isSupported(dir)) { 66 | continue 67 | } 68 | 69 | const repoConfig = config.repositories[dir] 70 | 71 | for await (const file of listFiles(resolve(path, dir), 'xml')) { 72 | const entryType = getEntryType(repoConfig, basename(file)) 73 | 74 | if (entryType === 'unknown') { 75 | continue 76 | } 77 | 78 | artifacts.push({ 79 | type: entryType, 80 | extension: 'xml', 81 | contents: await readFile(file, 'utf-8'), 82 | createdAt: null, 83 | language: dir, 84 | }) 85 | } 86 | } 87 | 88 | return artifacts 89 | } 90 | -------------------------------------------------------------------------------- /src/gherkin.ts: -------------------------------------------------------------------------------- 1 | import * as Gherkin from '@cucumber/gherkin' 2 | import * as Messages from '@cucumber/messages' 3 | 4 | import { listFiles, readFile, getRelativePath, md5hash } from './utils' 5 | import { 6 | BackgroundMetadata, 7 | EntryMetadata, 8 | FeatureMetadata, 9 | Location, 10 | Metadata, 11 | ScenarioMetadata, 12 | StepMetadata, 13 | } from './manifest' 14 | 15 | function normalizeTags(tags: readonly Messages.Tag[]) { 16 | return tags.map((tag) => tag.name) 17 | } 18 | 19 | function normalizeDescription(description: string) { 20 | return description.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() 21 | } 22 | 23 | function normalizeLocation(location: Messages.Location, path: string): Location { 24 | return { 25 | ...location, 26 | path, 27 | } 28 | } 29 | 30 | function makeFeatureMetadata(feature: Messages.Feature, relativeFilePath: string): FeatureMetadata { 31 | return { 32 | type: 'feature', 33 | id: md5hash(feature.name), 34 | name: feature.name, 35 | description: normalizeDescription(feature.description), 36 | tags: normalizeTags(feature.tags), 37 | location: normalizeLocation(feature.location, relativeFilePath), 38 | scenarios: [], 39 | } 40 | } 41 | 42 | function makeBackgroundMetadata(background: Messages.Background, relativeFilePath: string): BackgroundMetadata { 43 | return { 44 | type: 'background', 45 | id: background.id, 46 | name: background.name, 47 | location: normalizeLocation(background.location, relativeFilePath), 48 | } 49 | } 50 | 51 | function makeScenarioMetadata( 52 | scenario: Messages.Scenario, 53 | backgroundMetadata: BackgroundMetadata | undefined, 54 | featureMetadata: FeatureMetadata, 55 | relativeFilePath: string, 56 | ): ScenarioMetadata { 57 | return { 58 | type: 'scenario', 59 | id: scenario.id, 60 | backgroundId: backgroundMetadata?.id ?? undefined, 61 | featureId: featureMetadata.id, 62 | name: scenario.name, 63 | description: normalizeDescription(scenario.description), 64 | tags: normalizeTags(scenario.tags), 65 | location: normalizeLocation(scenario.location, relativeFilePath), 66 | steps: [], 67 | } 68 | } 69 | 70 | function makeStepMetadata( 71 | step: Messages.Step, 72 | scenarioMetadata: ScenarioMetadata, 73 | relativeFilePath: string, 74 | ): StepMetadata { 75 | return { 76 | type: 'step', 77 | id: step.id, 78 | scenarioId: scenarioMetadata.id, 79 | location: normalizeLocation(step.location, relativeFilePath), 80 | text: step.text, 81 | keyword: step.keyword.trim(), 82 | } 83 | } 84 | 85 | async function* generateMetadataEntries(path: string): AsyncGenerator { 86 | const parser = new Gherkin.Parser( 87 | new Gherkin.AstBuilder(Messages.IdGenerator.incrementing()), 88 | new Gherkin.GherkinClassicTokenMatcher(), 89 | ) 90 | 91 | for await (const file of listFiles(path, 'feature')) { 92 | const relativeFilePath = getRelativePath(path, file) 93 | const contents = await readFile(file) 94 | const document = parser.parse(contents) 95 | 96 | if (!document.feature) { 97 | continue 98 | } 99 | 100 | const feature = document.feature 101 | 102 | const featureMetadata = makeFeatureMetadata(feature, relativeFilePath) 103 | 104 | let backgroundMetadata 105 | 106 | for (const child of feature.children) { 107 | if (child.background) { 108 | backgroundMetadata = makeBackgroundMetadata(child.background, relativeFilePath) 109 | 110 | yield backgroundMetadata 111 | } 112 | 113 | if (child.scenario) { 114 | const scenarioMetadata = makeScenarioMetadata( 115 | child.scenario, 116 | backgroundMetadata, 117 | featureMetadata, 118 | relativeFilePath, 119 | ) 120 | 121 | featureMetadata.scenarios.push(scenarioMetadata.id) 122 | 123 | for (const step of child.scenario.steps) { 124 | const stepMetadata = makeStepMetadata(step, scenarioMetadata, relativeFilePath) 125 | 126 | scenarioMetadata.steps.push(stepMetadata.id) 127 | 128 | yield stepMetadata 129 | } 130 | 131 | yield scenarioMetadata 132 | } 133 | } 134 | 135 | yield featureMetadata 136 | } 137 | } 138 | 139 | export async function makeMetadata(path: string): Promise { 140 | const metadata: Metadata = { 141 | features: {}, 142 | backgrounds: {}, 143 | scenarios: {}, 144 | steps: {}, 145 | } 146 | 147 | for await (const entry of generateMetadataEntries(path)) { 148 | metadata[`${entry.type}s`][entry.id] = entry 149 | } 150 | 151 | return metadata 152 | } 153 | -------------------------------------------------------------------------------- /reports/java/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 72 | 73 | 74 | 75 | 76 | 86 | 87 | 88 | 89 | 90 | 101 | 102 | 103 | 104 | 105 | 118 | 119 | 120 | 121 | 122 | ]+>++' UUID pattern access permissions......................passed 125 | * grant pattern permission GET..............................................passed 126 | When I attempt to grant a token specifying those permissions................passed 127 | Then an error is returned...................................................passed 128 | * the error status code is 400..............................................passed 129 | * the error message is 'Invalid RegExp'.....................................passed 130 | * the error source is 'grant'...............................................passed 131 | * the error detail message is 'Syntax error: multiple repeat.'..............passed 132 | * the error detail location is 'permissions.patterns.uuids.!<[^>]+>++'......passed 133 | * the error detail location type is 'body'..................................passed 134 | ]]> 135 | 136 | 137 | 138 | 139 | ]+>)+' UUID pattern access permissions.....................passed 142 | * grant pattern permission GET..............................................passed 143 | When I attempt to grant a token specifying those permissions................passed 144 | Then an error is returned...................................................passed 145 | * the error status code is 400..............................................passed 146 | * the error message is 'Invalid RegExp'.....................................passed 147 | * the error source is 'grant'...............................................passed 148 | * the error detail message is 'Only non-capturing groups are allowed. Try replacing `(` with `(?:`.'.passed 149 | * the error detail location is 'permissions.patterns.uuids.(!<[^>]+>)+'.....passed 150 | * the error detail location type is 'body'..................................passed 151 | ]]> 152 | 153 | 154 | 155 | 156 | 161 | 162 | 163 | 164 | 165 | 171 | 172 | 173 | 174 | 175 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /reports/kotlin/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 72 | 73 | 74 | 75 | 76 | 86 | 87 | 88 | 89 | 90 | 101 | 102 | 103 | 104 | 105 | 118 | 119 | 120 | 121 | 122 | ]+>++' UUID pattern access permissions......................passed 125 | * grant pattern permission GET..............................................passed 126 | When I attempt to grant a token specifying those permissions................passed 127 | Then an error is returned...................................................passed 128 | * the error status code is 400..............................................passed 129 | * the error message is 'Invalid RegExp'.....................................passed 130 | * the error source is 'grant'...............................................passed 131 | * the error detail message is 'Syntax error: multiple repeat.'..............passed 132 | * the error detail location is 'permissions.patterns.uuids.!<[^>]+>++'......passed 133 | * the error detail location type is 'body'..................................passed 134 | ]]> 135 | 136 | 137 | 138 | 139 | ]+>)+' UUID pattern access permissions.....................passed 142 | * grant pattern permission GET..............................................passed 143 | When I attempt to grant a token specifying those permissions................passed 144 | Then an error is returned...................................................passed 145 | * the error status code is 400..............................................passed 146 | * the error message is 'Invalid RegExp'.....................................passed 147 | * the error source is 'grant'...............................................passed 148 | * the error detail message is 'Only non-capturing groups are allowed. Try replacing `(` with `(?:`.'.passed 149 | * the error detail location is 'permissions.patterns.uuids.(!<[^>]+>)+'.....passed 150 | * the error detail location type is 'body'..................................passed 151 | ]]> 152 | 153 | 154 | 155 | 156 | 161 | 162 | 163 | 164 | 165 | 171 | 172 | 173 | 174 | 175 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /reports/kotlin/beta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 46 | 47 | 48 | 49 | 50 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 74 | 75 | 76 | 88 | 89 | 90 | 91 | 92 | 104 | 105 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 128 | 129 | 130 | 131 | 132 | 143 | 144 | 145 | 146 | 147 | 158 | 159 | 160 | 161 | 162 | 173 | 174 | 175 | 176 | 177 | 188 | 189 | 190 | 191 | 192 | 204 | 205 | 206 | 207 | 208 | 219 | 220 | 221 | 222 | 223 | 234 | 235 | 236 | 237 | 238 | 249 | 250 | 251 | 252 | 253 | 264 | 265 | 266 | 267 | 268 | 280 | 281 | 282 | 283 | 284 | 295 | 296 | 297 | 298 | 299 | 310 | 311 | 312 | 313 | 314 | 325 | 326 | 327 | 328 | 329 | 340 | 341 | 342 | 343 | 344 | 355 | 356 | 357 | 358 | 359 | 370 | 371 | 372 | 373 | 374 | 385 | 386 | 387 | 388 | 389 | 400 | 401 | 402 | 403 | 404 | 408 | 409 | 410 | 411 | 412 | 423 | 424 | 425 | 426 | 427 | 438 | 439 | 440 | 441 | 442 | 453 | 454 | 455 | 456 | 457 | 461 | 462 | 463 | 464 | 465 | 476 | 477 | 478 | 479 | 480 | 491 | 492 | 493 | 494 | 495 | 499 | 500 | 501 | 502 | 503 | 514 | 515 | 516 | 517 | 518 | 529 | 530 | 531 | 532 | 533 | 537 | 538 | 539 | 540 | 541 | 552 | 553 | 554 | 555 | 556 | 569 | 570 | 571 | 572 | 573 | 586 | 587 | 588 | 589 | 590 | 603 | 604 | 605 | 606 | 607 | 620 | 621 | 622 | 623 | 624 | 638 | 639 | 640 | 641 | 642 | 656 | 657 | 658 | 659 | 660 | 674 | 675 | 676 | 677 | 678 | 689 | 690 | 691 | 692 | 693 | 704 | 705 | 706 | 707 | 708 | 720 | 721 | 722 | 723 | 724 | 734 | 735 | 736 | 737 | -------------------------------------------------------------------------------- /reports/java/beta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | 38 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 77 | 144 | 145 | 146 | 147 | 148 | 158 | 159 | 160 | 161 | 162 | 173 | 174 | 175 | 176 | 177 | 190 | 191 | 192 | 193 | 194 | ]+>++' UUID pattern access permissions......................passed 197 | * grant pattern permission GET..............................................passed 198 | When I attempt to grant a token specifying those permissions................passed 199 | Then an error is returned...................................................passed 200 | * the error status code is 400..............................................passed 201 | * the error message is 'Invalid RegExp'.....................................passed 202 | * the error source is 'grant'...............................................passed 203 | * the error detail message is 'Syntax error: multiple repeat.'..............passed 204 | * the error detail location is 'permissions.patterns.uuids.!<[^>]+>++'......passed 205 | * the error detail location type is 'body'..................................passed 206 | ]]> 207 | 208 | 209 | 210 | 211 | ]+>)+' UUID pattern access permissions.....................passed 214 | * grant pattern permission GET..............................................passed 215 | When I attempt to grant a token specifying those permissions................passed 216 | Then an error is returned...................................................passed 217 | * the error status code is 400..............................................passed 218 | * the error message is 'Invalid RegExp'.....................................passed 219 | * the error source is 'grant'...............................................passed 220 | * the error detail message is 'Only non-capturing groups are allowed. Try replacing `(` with `(?:`.'.passed 221 | * the error detail location is 'permissions.patterns.uuids.(!<[^>]+>)+'.....passed 222 | * the error detail location type is 'body'..................................passed 223 | ]]> 224 | 225 | 226 | 227 | 228 | 241 | 242 | 243 | 244 | 245 | 250 | 251 | 252 | 253 | 254 | 260 | 261 | 262 | 263 | 264 | 270 | 271 | 272 | 273 | 274 | 279 | 280 | 281 | 282 | 283 | 295 | 296 | 297 | 298 | 299 | 311 | 312 | 313 | 314 | 315 | 320 | 321 | 322 | 323 | 324 | 329 | 330 | 331 | 332 | 333 | 339 | 340 | 341 | 342 | 343 | 349 | 350 | 351 | 352 | 353 | 364 | 365 | 366 | 367 | 368 | 379 | 380 | 381 | 382 | 383 | 394 | 395 | 396 | 397 | 398 | 409 | 410 | 411 | 412 | 413 | 424 | 425 | 426 | 427 | 428 | 432 | 433 | 434 | 435 | 436 | 440 | 441 | 442 | 443 | 444 | 456 | 457 | 458 | 459 | 460 | 471 | 472 | 473 | 474 | 475 | 486 | 487 | 488 | 489 | 490 | 501 | 502 | 503 | 504 | 505 | 516 | 517 | 518 | 519 | 520 | 532 | 533 | 534 | 535 | 536 | 547 | 548 | 549 | 550 | 551 | 562 | 563 | 564 | 565 | 566 | 577 | 578 | 579 | 580 | 581 | 592 | 593 | 594 | 595 | 596 | 607 | 608 | 609 | 610 | 611 | 622 | 623 | 624 | 625 | 626 | 637 | 638 | 639 | 640 | 641 | 652 | 653 | 654 | 655 | 656 | 660 | 661 | 662 | 663 | 664 | 675 | 676 | 677 | 678 | 679 | 690 | 691 | 692 | 693 | 694 | 705 | 706 | 707 | 708 | 709 | 713 | 714 | 715 | 716 | 717 | 728 | 729 | 730 | 731 | 732 | 743 | 744 | 745 | 746 | 747 | 751 | 752 | 753 | 754 | 755 | 766 | 767 | 768 | 769 | 770 | 781 | 782 | 783 | 784 | 785 | 789 | 790 | 791 | 792 | 793 | 804 | 805 | 806 | 807 | 808 | 821 | 822 | 823 | 824 | 825 | 838 | 839 | 840 | 841 | 842 | 855 | 856 | 857 | 858 | 859 | 872 | 873 | 874 | 875 | 876 | 890 | 891 | 892 | 893 | 894 | 908 | 909 | 910 | 911 | 912 | 926 | 927 | 928 | 929 | 930 | 941 | 942 | 943 | 944 | 945 | 956 | 957 | 958 | 959 | 960 | 972 | 973 | 974 | 975 | 976 | 986 | 987 | 988 | 989 | --------------------------------------------------------------------------------