diff --git a/README.md b/README.md
index f4be6e05..e98cc2d5 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ jobs:
# java-junit
# jest-junit
# mocha-json
+ # rspec-json
reporter: ''
# Allows you to generate only the summary.
diff --git a/__tests__/__outputs__/rspec-json.md b/__tests__/__outputs__/rspec-json.md
new file mode 100644
index 00000000..04bae8a5
--- /dev/null
+++ b/__tests__/__outputs__/rspec-json.md
@@ -0,0 +1,16 @@
+![Tests failed](https://img.shields.io/badge/tests-1%20passed%2C%201%20failed%2C%201%20skipped-critical)
+## ❌ fixtures/rspec-json.json
+**3** tests were completed in **0ms** with **1** passed, **1** failed and **1** skipped.
+|Test suite|Passed|Failed|Skipped|Time|
+|:---|---:|---:|---:|---:|
+|[./spec/config/check_env_vars_spec.rb](#r0s0)|1✅|1❌|1⚪|0ms|
+### ❌ ./spec/config/check_env_vars_spec.rb
+```
+CheckEnvVars#call when all env vars are defined behaves like success load
+ ❌ CheckEnvVars#call when all env vars are defined behaves like success load fails in assertion
+ (#ActiveSupport::BroadcastLogger:0x00007f1007fedf58).debug("All config env vars exist")
+ expected: 0 times with arguments: ("All config env vars exist")
+ received: 1 time with arguments: ("All config env vars exist")
+ ✅ CheckEnvVars#call when all env vars are defined behaves like success load logs success message
+ ⚪ CheckEnvVars#call when all env vars are defined behaves like success load skips the test
+```
\ No newline at end of file
diff --git a/__tests__/__snapshots__/rspec-json.test.ts.snap b/__tests__/__snapshots__/rspec-json.test.ts.snap
new file mode 100644
index 00000000..cc14bfbb
--- /dev/null
+++ b/__tests__/__snapshots__/rspec-json.test.ts.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rspec-json tests report from ./reports/rspec-json test results matches snapshot 1`] = `
+TestRunResult {
+ "path": "fixtures/rspec-json.json",
+ "suites": [
+ TestSuiteResult {
+ "groups": [
+ TestGroupResult {
+ "name": "CheckEnvVars#call when all env vars are defined behaves like success load",
+ "tests": [
+ TestCaseResult {
+ "error": {
+ "details": "/usr/local/bundle/ruby/3.3.0/gems/net-http-0.4.1/lib/net/http.rb:1603:in \`initialize'
+./config/check_env_vars.rb:11:in \`call'
+./spec/config/check_env_vars_spec.rb:7:in \`block (3 levels) in '
+./spec/config/check_env_vars_spec.rb:19:in \`block (4 levels) in '",
+ "line": 11,
+ "message": "(#ActiveSupport::BroadcastLogger:0x00007f1007fedf58).debug("All config env vars exist")
+ expected: 0 times with arguments: ("All config env vars exist")
+ received: 1 time with arguments: ("All config env vars exist")",
+ "path": "./config/check_env_vars.rb",
+ },
+ "name": "CheckEnvVars#call when all env vars are defined behaves like success load fails in assertion",
+ "result": "failed",
+ "time": 0.004411051,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "CheckEnvVars#call when all env vars are defined behaves like success load logs success message",
+ "result": "success",
+ "time": 0.079159625,
+ },
+ TestCaseResult {
+ "error": undefined,
+ "name": "CheckEnvVars#call when all env vars are defined behaves like success load skips the test",
+ "result": "skipped",
+ "time": 0.000023007,
+ },
+ ],
+ },
+ ],
+ "name": "./spec/config/check_env_vars_spec.rb",
+ "totalTime": undefined,
+ },
+ ],
+ "totalTime": 0.19118387,
+}
+`;
diff --git a/__tests__/fixtures/empty/rspec-json.json b/__tests__/fixtures/empty/rspec-json.json
new file mode 100644
index 00000000..3629097c
--- /dev/null
+++ b/__tests__/fixtures/empty/rspec-json.json
@@ -0,0 +1,17 @@
+{
+ "version": "3.13.0",
+ "messages": [
+ "No examples found."
+ ],
+ "examples": [
+
+ ],
+ "summary": {
+ "duration": 0.002514266,
+ "example_count": 0,
+ "failure_count": 0,
+ "pending_count": 0,
+ "errors_outside_of_examples_count": 0
+ },
+ "summary_line": "0 examples, 0 failures"
+}
diff --git a/__tests__/fixtures/rspec-json.json b/__tests__/fixtures/rspec-json.json
new file mode 100644
index 00000000..34d4e49e
--- /dev/null
+++ b/__tests__/fixtures/rspec-json.json
@@ -0,0 +1,53 @@
+{
+ "version": "3.13.0",
+ "examples": [
+ {
+ "id": "./spec/config/check_env_vars_spec.rb[1:1:1:1:1]",
+ "description": "logs success message",
+ "full_description": "CheckEnvVars#call when all env vars are defined behaves like success load logs success message",
+ "status": "passed",
+ "file_path": "./spec/config/check_env_vars_spec.rb",
+ "line_number": 12,
+ "run_time": 0.079159625,
+ "pending_message": null
+ },
+ {
+ "id": "./spec/config/check_env_vars_spec.rb[1:1:1:1:2]",
+ "description": "fails in assertion",
+ "full_description": "CheckEnvVars#call when all env vars are defined behaves like success load fails in assertion",
+ "status": "failed",
+ "file_path": "./spec/config/check_env_vars_spec.rb",
+ "line_number": 17,
+ "run_time": 0.004411051,
+ "pending_message": null,
+ "exception": {
+ "class": "RSpec::Mocks::MockExpectationError",
+ "message": "(#ActiveSupport::BroadcastLogger:0x00007f1007fedf58).debug(\"All config env vars exist\")\n expected: 0 times with arguments: (\"All config env vars exist\")\n received: 1 time with arguments: (\"All config env vars exist\")",
+ "backtrace": [
+ "/usr/local/bundle/ruby/3.3.0/gems/net-http-0.4.1/lib/net/http.rb:1603:in `initialize'",
+ "./config/check_env_vars.rb:11:in `call'",
+ "./spec/config/check_env_vars_spec.rb:7:in `block (3 levels) in \u003ctop (required)\u003e'",
+ "./spec/config/check_env_vars_spec.rb:19:in `block (4 levels) in \u003ctop (required)\u003e'"
+ ]
+ }
+ },
+ {
+ "id": "./spec/config/check_env_vars_spec.rb[1:1:1:1:4]",
+ "description": "skips the test",
+ "full_description": "CheckEnvVars#call when all env vars are defined behaves like success load skips the test",
+ "status": "pending",
+ "file_path": "./spec/config/check_env_vars_spec.rb",
+ "line_number": 27,
+ "run_time": 2.3007e-05,
+ "pending_message": "Temporarily skipped with xit"
+ }
+ ],
+ "summary": {
+ "duration": 0.19118387,
+ "example_count": 3,
+ "failure_count": 1,
+ "pending_count": 1,
+ "errors_outside_of_examples_count": 0
+ },
+ "summary_line": "3 examples, 1 failures, 1 pending"
+}
diff --git a/__tests__/rspec-json.test.ts b/__tests__/rspec-json.test.ts
new file mode 100644
index 00000000..f77475aa
--- /dev/null
+++ b/__tests__/rspec-json.test.ts
@@ -0,0 +1,45 @@
+import * as fs from 'fs'
+import * as path from 'path'
+
+import {RspecJsonParser} from '../src/parsers/rspec-json/rspec-json-parser'
+import {ParseOptions} from '../src/test-parser'
+import {getReport} from '../src/report/get-report'
+import {normalizeFilePath} from '../src/utils/path-utils'
+
+describe('rspec-json tests', () => {
+ it('produces empty test run result when there are no test cases', async () => {
+ const fixturePath = path.join(__dirname, 'fixtures', 'empty', 'rspec-json.json')
+ const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
+ const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
+
+ const opts: ParseOptions = {
+ parseErrors: true,
+ trackedFiles: []
+ }
+
+ const parser = new RspecJsonParser(opts)
+ const result = await parser.parse(filePath, fileContent)
+ expect(result.tests).toBe(0)
+ expect(result.result).toBe('success')
+ })
+
+ it('report from ./reports/rspec-json test results matches snapshot', async () => {
+ const fixturePath = path.join(__dirname, 'fixtures', 'rspec-json.json')
+ const outputPath = path.join(__dirname, '__outputs__', 'rspec-json.md')
+ const filePath = normalizeFilePath(path.relative(__dirname, fixturePath))
+ const fileContent = fs.readFileSync(fixturePath, {encoding: 'utf8'})
+
+ const opts: ParseOptions = {
+ parseErrors: true,
+ trackedFiles: ['test/main.test.js', 'test/second.test.js', 'lib/main.js']
+ }
+
+ const parser = new RspecJsonParser(opts)
+ const result = await parser.parse(filePath, fileContent)
+ expect(result).toMatchSnapshot()
+
+ const report = getReport([result])
+ fs.mkdirSync(path.dirname(outputPath), {recursive: true})
+ fs.writeFileSync(outputPath, report)
+ })
+})
diff --git a/action.yml b/action.yml
index 6f35ebfe..8d9d7282 100644
--- a/action.yml
+++ b/action.yml
@@ -31,6 +31,7 @@ inputs:
- java-junit
- jest-junit
- mocha-json
+ - rspec-json
- swift-xunit
required: true
list-suites:
diff --git a/dist/index.js b/dist/index.js
index 47a1b8d2..bbc5e787 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -265,6 +265,7 @@ const dotnet_trx_parser_1 = __nccwpck_require__(2664);
const java_junit_parser_1 = __nccwpck_require__(676);
const jest_junit_parser_1 = __nccwpck_require__(1113);
const mocha_json_parser_1 = __nccwpck_require__(6043);
+const rspec_json_parser_1 = __nccwpck_require__(406);
const swift_xunit_parser_1 = __nccwpck_require__(5366);
const path_utils_1 = __nccwpck_require__(4070);
const github_utils_1 = __nccwpck_require__(3522);
@@ -434,6 +435,8 @@ class TestReporter {
return new jest_junit_parser_1.JestJunitParser(options);
case 'mocha-json':
return new mocha_json_parser_1.MochaJsonParser(options);
+ case 'rspec-json':
+ return new rspec_json_parser_1.RspecJsonParser(options);
case 'swift-xunit':
return new swift_xunit_parser_1.SwiftXunitParser(options);
default:
@@ -1403,6 +1406,121 @@ class MochaJsonParser {
exports.MochaJsonParser = MochaJsonParser;
+/***/ }),
+
+/***/ 406:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.RspecJsonParser = void 0;
+const test_results_1 = __nccwpck_require__(2768);
+class RspecJsonParser {
+ constructor(options) {
+ this.options = options;
+ }
+ parse(path, content) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const mocha = this.getRspecJson(path, content);
+ const result = this.getTestRunResult(path, mocha);
+ result.sort(true);
+ return Promise.resolve(result);
+ });
+ }
+ getRspecJson(path, content) {
+ try {
+ return JSON.parse(content);
+ }
+ catch (e) {
+ throw new Error(`Invalid JSON at ${path}\n\n${e}`);
+ }
+ }
+ getTestRunResult(resultsPath, rspec) {
+ const suitesMap = {};
+ const getSuite = (test) => {
+ var _a;
+ const path = test.file_path;
+ return (_a = suitesMap[path]) !== null && _a !== void 0 ? _a : (suitesMap[path] = new test_results_1.TestSuiteResult(path, []));
+ };
+ for (const test of rspec.examples) {
+ const suite = getSuite(test);
+ if (test.status === 'failed') {
+ this.processTest(suite, test, 'failed');
+ }
+ else if (test.status === 'passed') {
+ this.processTest(suite, test, 'success');
+ }
+ else if (test.status === 'pending') {
+ this.processTest(suite, test, 'skipped');
+ }
+ }
+ const suites = Object.values(suitesMap);
+ return new test_results_1.TestRunResult(resultsPath, suites, rspec.summary.duration);
+ }
+ processTest(suite, test, result) {
+ var _a;
+ const groupName = test.full_description !== test.description
+ ? test.full_description.substr(0, test.full_description.length - test.description.length).trimEnd()
+ : null;
+ let group = suite.groups.find(grp => grp.name === groupName);
+ if (group === undefined) {
+ group = new test_results_1.TestGroupResult(groupName, []);
+ suite.groups.push(group);
+ }
+ const error = this.getTestCaseError(test);
+ const testCase = new test_results_1.TestCaseResult(test.full_description, result, (_a = test.run_time) !== null && _a !== void 0 ? _a : 0, error);
+ group.tests.push(testCase);
+ }
+ getTestCaseError(test) {
+ var _a, _b;
+ const backtrace = (_a = test.exception) === null || _a === void 0 ? void 0 : _a.backtrace;
+ const message = (_b = test.exception) === null || _b === void 0 ? void 0 : _b.message;
+ if (backtrace === undefined) {
+ return undefined;
+ }
+ let path;
+ let line;
+ const details = backtrace.join('\n');
+ const src = this.getExceptionSource(backtrace);
+ if (src) {
+ path = src.path;
+ line = src.line;
+ }
+ return {
+ path,
+ line,
+ message,
+ details
+ };
+ }
+ getExceptionSource(backtrace) {
+ const re = /^(.*?):(\d+):/;
+ for (const str of backtrace) {
+ const match = str.match(re);
+ if (match !== null) {
+ const [_, path, lineStr] = match;
+ if (path.startsWith('./')) {
+ const line = parseInt(lineStr);
+ return { path, line };
+ }
+ }
+ }
+ return undefined;
+ }
+}
+exports.RspecJsonParser = RspecJsonParser;
+
+
/***/ }),
/***/ 5366:
diff --git a/src/main.ts b/src/main.ts
index c3c89efd..8f481afe 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -15,6 +15,7 @@ import {DotnetTrxParser} from './parsers/dotnet-trx/dotnet-trx-parser'
import {JavaJunitParser} from './parsers/java-junit/java-junit-parser'
import {JestJunitParser} from './parsers/jest-junit/jest-junit-parser'
import {MochaJsonParser} from './parsers/mocha-json/mocha-json-parser'
+import {RspecJsonParser} from './parsers/rspec-json/rspec-json-parser'
import {SwiftXunitParser} from './parsers/swift-xunit/swift-xunit-parser'
import {normalizeDirPath, normalizeFilePath} from './utils/path-utils'
@@ -223,6 +224,8 @@ class TestReporter {
return new JestJunitParser(options)
case 'mocha-json':
return new MochaJsonParser(options)
+ case 'rspec-json':
+ return new RspecJsonParser(options)
case 'swift-xunit':
return new SwiftXunitParser(options)
default:
diff --git a/src/parsers/rspec-json/rspec-json-parser.ts b/src/parsers/rspec-json/rspec-json-parser.ts
new file mode 100644
index 00000000..bf404a12
--- /dev/null
+++ b/src/parsers/rspec-json/rspec-json-parser.ts
@@ -0,0 +1,113 @@
+import { Console } from 'console'
+import {ParseOptions, TestParser} from '../../test-parser'
+import {
+ TestCaseError,
+ TestCaseResult,
+ TestExecutionResult,
+ TestGroupResult,
+ TestRunResult,
+ TestSuiteResult
+} from '../../test-results'
+import {RspecJson, RspecExample} from './rspec-json-types'
+
+export class RspecJsonParser implements TestParser {
+ assumedWorkDir: string | undefined
+
+ constructor(readonly options: ParseOptions) {}
+
+ async parse(path: string, content: string): Promise {
+ const mocha = this.getRspecJson(path, content)
+ const result = this.getTestRunResult(path, mocha)
+ result.sort(true)
+ return Promise.resolve(result)
+ }
+
+ private getRspecJson(path: string, content: string): RspecJson {
+ try {
+ return JSON.parse(content)
+ } catch (e) {
+ throw new Error(`Invalid JSON at ${path}\n\n${e}`)
+ }
+ }
+
+ private getTestRunResult(resultsPath: string, rspec: RspecJson): TestRunResult {
+ const suitesMap: {[path: string]: TestSuiteResult} = {}
+
+ const getSuite = (test: RspecExample): TestSuiteResult => {
+ const path = test.file_path
+ return suitesMap[path] ?? (suitesMap[path] = new TestSuiteResult(path, []))
+ }
+
+ for (const test of rspec.examples) {
+ const suite = getSuite(test)
+ if (test.status === 'failed') {
+ this.processTest(suite, test, 'failed')
+ } else if (test.status === 'passed') {
+ this.processTest(suite, test, 'success')
+ } else if (test.status === 'pending') {
+ this.processTest(suite, test, 'skipped')
+ }
+ }
+
+ const suites = Object.values(suitesMap)
+ return new TestRunResult(resultsPath, suites, rspec.summary.duration)
+ }
+
+ private processTest(suite: TestSuiteResult, test: RspecExample, result: TestExecutionResult): void {
+ const groupName =
+ test.full_description !== test.description
+ ? test.full_description.substr(0, test.full_description.length - test.description.length).trimEnd()
+ : null
+
+ let group = suite.groups.find(grp => grp.name === groupName)
+ if (group === undefined) {
+ group = new TestGroupResult(groupName, [])
+ suite.groups.push(group)
+ }
+
+ const error = this.getTestCaseError(test)
+ const testCase = new TestCaseResult(test.full_description, result, test.run_time ?? 0, error)
+ group.tests.push(testCase)
+ }
+
+ private getTestCaseError(test: RspecExample): TestCaseError | undefined {
+ const backtrace = test.exception?.backtrace
+ const message = test.exception?.message
+ if (backtrace === undefined) {
+ return undefined
+ }
+
+ let path
+ let line
+ const details = backtrace.join('\n')
+
+ const src = this.getExceptionSource(backtrace)
+ if (src) {
+ path = src.path
+ line = src.line
+ }
+
+ return {
+ path,
+ line,
+ message,
+ details
+ }
+ }
+
+ private getExceptionSource(backtrace: string[]): {path: string; line: number} | undefined {
+ const re = /^(.*?):(\d+):/
+
+ for (const str of backtrace) {
+ const match = str.match(re)
+ if (match !== null) {
+ const [_, path, lineStr] = match
+ if (path.startsWith('./')) {
+ const line = parseInt(lineStr)
+ return {path, line}
+ }
+ }
+ }
+ return undefined
+ }
+}
diff --git a/src/parsers/rspec-json/rspec-json-types.ts b/src/parsers/rspec-json/rspec-json-types.ts
new file mode 100644
index 00000000..495af897
--- /dev/null
+++ b/src/parsers/rspec-json/rspec-json-types.ts
@@ -0,0 +1,34 @@
+export interface RspecJson {
+ version: number
+ examples: RspecExample[]
+ summary: RspecSummary
+ summary_line: string
+}
+
+export interface RspecExample {
+ id: string
+ description: string
+ full_description: string
+ status: TestStatus
+ file_path: string
+ line_number: number
+ run_time: number
+ pending_message: string | null
+ exception?: RspecException
+}
+
+type TestStatus = 'passed' | 'failed' | 'pending';
+
+export interface RspecException {
+ class: string
+ message: string
+ backtrace: string[]
+}
+
+export interface RspecSummary {
+ duration: number
+ example_count: number
+ failure_count: number
+ pending_count: number
+ errors_outside_of_examples_count: number
+}