├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── example ├── .rspec ├── spec │ ├── example_spec.rb │ ├── shared_examples.rb │ └── spec_helper.rb └── tmp │ └── .keep ├── gemfiles ├── .bundle │ └── config ├── rspec_2_x.gemfile ├── rspec_2_x.gemfile.lock ├── rspec_3_0.gemfile ├── rspec_3_0.gemfile.lock ├── rspec_3_1.gemfile ├── rspec_3_1.gemfile.lock ├── rspec_3_10.gemfile ├── rspec_3_10.gemfile.lock ├── rspec_3_2.gemfile ├── rspec_3_2.gemfile.lock ├── rspec_3_3.gemfile ├── rspec_3_3.gemfile.lock ├── rspec_3_4.gemfile ├── rspec_3_4.gemfile.lock ├── rspec_3_5.gemfile ├── rspec_3_5.gemfile.lock ├── rspec_3_6.gemfile ├── rspec_3_6.gemfile.lock ├── rspec_3_7.gemfile ├── rspec_3_7.gemfile.lock ├── rspec_3_8.gemfile ├── rspec_3_8.gemfile.lock ├── rspec_3_9.gemfile └── rspec_3_9.gemfile.lock ├── lib ├── rspec_junit_formatter.rb └── rspec_junit_formatter │ ├── rspec2.rb │ └── rspec3.rb ├── reference └── JUnit.xsd ├── rspec_junit_formatter.gemspec └── spec └── rspec_junit_formatter_spec.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | ruby-version: ["2.6", "2.7", "3.0", "3.1"] 14 | rspec-version: ["2_x", "3_0", "3_1", "3_2", "3_3", "3_4", "3_5", "3_6", "3_7", "3_8", "3_9", "3_10"] 15 | 16 | runs-on: ubuntu-latest 17 | 18 | env: 19 | BUNDLE_GEMFILE: gemfiles/rspec_${{ matrix.rspec-version }}.gemfile 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true 28 | 29 | - name: Run tests 30 | run: bundle exec rake 31 | 32 | - name: Upload test artifacts 33 | uses: actions/upload-artifact@v2 34 | if: always() 35 | with: 36 | name: test-artifacts 37 | path: tmp 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /example/tmp/ 4 | /pkg/ 5 | /tmp/ 6 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rspec-2-x" do 2 | gem "rspec", "~> 2.14", "< 2.99" 3 | gem "rake", "~> 10.0" # Rake.last_comment 4 | end 5 | 6 | appraise "rspec-3-0" do 7 | gem "rspec", "~> 3.0.0" 8 | gem "rake", "~> 10.0" # Rake.last_comment 9 | end 10 | 11 | appraise "rspec-3-1" do 12 | gem "rspec", "~> 3.1.0" 13 | gem "rake", "~> 10.0" # Rake.last_comment 14 | end 15 | 16 | appraise "rspec-3-2" do 17 | gem "rspec", "~> 3.2.0" 18 | gem "rake", "~> 10.0" # Rake.last_comment 19 | end 20 | 21 | appraise "rspec-3-3" do 22 | gem "rspec", "~> 3.3.0" 23 | gem "rake", "~> 10.0" # Rake.last_comment 24 | end 25 | 26 | appraise "rspec-3-4" do 27 | gem "rspec", "~> 3.4.0" 28 | end 29 | 30 | appraise "rspec-3-5" do 31 | gem "rspec", "~> 3.5.0" 32 | end 33 | 34 | appraise "rspec-3-6" do 35 | gem "rspec", "~> 3.6.0" 36 | end 37 | 38 | appraise "rspec-3-7" do 39 | gem "rspec", "~> 3.7.0" 40 | end 41 | 42 | appraise "rspec-3-8" do 43 | gem "rspec", "~> 3.8.0" 44 | end 45 | 46 | appraise "rspec-3-9" do 47 | gem "rspec", "~> 3.9.0" 48 | end 49 | 50 | appraise "rspec-3-10" do 51 | gem "rspec", "~> 3.10.0" 52 | end 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning]. 6 | 7 | [Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ 8 | [Semantic Versioning]: http://semver.org/spec/v2.0.0.html 9 | 10 | ## [Unreleased] 11 | 12 | [Unreleased]: https://github.com/sj26/rspec_junit_formatter/compare/v0.6.0...main 13 | 14 | ## [v0.6.0] 15 | ### Changed 16 | - Restrict to Ruby 2.3+ 17 | ### Fixed 18 | - Fix handling of nil durations 19 | 20 | [v0.6.0]: https://github.com/sj26/rspec_junit_formatter/compare/v0.5.1...v0.6.0 21 | 22 | ## [v0.5.1] - 2022-01-06 23 | ### Fixed 24 | - Fixed compatibility with older rubies 25 | 26 | [v0.5.1]: https://github.com/sj26/rspec_junit_formatter/compare/v0.5.0...v0.5.1 27 | 28 | ## [v0.5.0] - 2022-01-04 29 | ### Added 30 | - Added support to read outside error count returned from XML formatter (#86) 31 | ### Changed 32 | - Moved to GitHub Actions for CI 33 | - Test on current Ruby and RSpec versions 34 | 35 | [v0.5.0]: https://github.com/sj26/rspec_junit_formatter/compare/v0.4.1...v0.5.0 36 | 37 | ## [v0.4.1] - 2018-05-26 38 | ### Fixed 39 | - Diff ANSI stripping now works for codes with multiple tags, too 40 | 41 | [v0.4.1]: https://github.com/sj26/rspec_junit_formatter/compare/v0.4.0...v0.4.1 42 | 43 | ## [v0.4.0] - 2018-05-26 44 | ### Added 45 | - Add support for including STDOUT and STDERR from tests in the JUnit output (see ["Capturing output"] in the readme for details) 46 | ### Fixed 47 | - When RSpec includes a diff in its output, strip out ANSI escape codes used to color it for shell display 48 | 49 | [v0.4.0]: https://github.com/sj26/rspec_junit_formatter/compare/v0.3.0...v0.4.0 50 | ["Capturing output"]: https://github.com/sj26/rspec_junit_formatter#capturing-output 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rspec" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.8.0) 17 | nokogiri (1.13.4) 18 | mini_portile2 (~> 2.8.0) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.10.0) 23 | rspec-core (~> 3.10.0) 24 | rspec-expectations (~> 3.10.0) 25 | rspec-mocks (~> 3.10.0) 26 | rspec-core (3.10.1) 27 | rspec-support (~> 3.10.0) 28 | rspec-expectations (3.10.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.10.0) 31 | rspec-mocks (3.10.2) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.10.0) 34 | rspec-support (3.10.3) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2022 Samuel Cochran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSpec JUnit Formatter 2 | 3 | [![Build results](https://github.com/sj26/rspec_junit_formatter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sj26/rspec_junit_formatter/actions/workflows/ci.yml?branch=main) 4 | [![Gem version](http://img.shields.io/gem/v/rspec_junit_formatter.svg)](https://rubygems.org/gems/rspec_junit_formatter) 5 | 6 | [RSpec][rspec] 2 & 3 results that your CI can read. [Jenkins][jenkins-junit], [Buildkite][buildkite-junit], [CircleCI][circleci-junit], [Gitlab][gitlab-junit], and probably more, too. 7 | 8 | [rspec]: http://rspec.info/ 9 | [jenkins-junit]: https://jenkins.io/doc/pipeline/steps/junit/ 10 | [buildkite-junit]: https://github.com/buildkite/rspec-junit-example 11 | [circleci-junit]: https://circleci.com/docs/2.0/collect-test-data/ 12 | [gitlab-junit]: https://docs.gitlab.com/ee/ci/unit_test_reports.html#ruby-example 13 | 14 | ## Usage 15 | 16 | Install the gem: 17 | 18 | ```sh 19 | gem install rspec_junit_formatter 20 | ``` 21 | 22 | Use it: 23 | 24 | ```sh 25 | rspec --format RspecJunitFormatter --out rspec.xml 26 | ``` 27 | 28 | You'll get an XML file `rspec.xml` with your results in it. 29 | 30 | You can use it in combination with other [formatters][rspec-formatters], too: 31 | 32 | ```sh 33 | rspec --format progress --format RspecJunitFormatter --out rspec.xml 34 | ``` 35 | 36 | [rspec-formatters]: https://rspec.info/features/3-12/rspec-core/formatters/ 37 | 38 | ### Using in your project with Bundler 39 | 40 | Add it to your Gemfile if you're using [Bundler][bundler]. Put it in the same groups as rspec. 41 | 42 | ```ruby 43 | group :test do 44 | gem "rspec" 45 | gem "rspec_junit_formatter", require: false 46 | end 47 | ``` 48 | 49 | Put the same arguments as the commands above in [your `.rspec`][rspec-file]: 50 | 51 | ```sh 52 | --format RspecJunitFormatter 53 | --out rspec.xml 54 | ``` 55 | [bundler]: https://bundler.io 56 | [rspec-file]: https://rspec.info/features/3-12/rspec-core/configuration/read-options-from-file/ 57 | 58 | ### Parallel tests 59 | 60 | For use with `parallel_tests`, add `$TEST_ENV_NUMBER` in the output file option (in `.rspec` or `.rspec_parallel`) to avoid concurrent process write conflicts. 61 | 62 | ```sh 63 | --format RspecJunitFormatter 64 | --out tmp/rspec<%= ENV["TEST_ENV_NUMBER"] %>.xml 65 | ``` 66 | 67 | The formatter includes `$TEST_ENV_NUMBER` in the test suite name within the XML, too. 68 | 69 | ### Capturing output 70 | 71 | If you like, you can capture the standard output and error streams of each test into the `:stdout` and `:stderr` example metadata which will be added to the junit report, e.g.: 72 | 73 | ```ruby 74 | # spec_helper.rb 75 | 76 | RSpec.configure do |config| 77 | # register around filter that captures stdout and stderr 78 | config.around(:each) do |example| 79 | $stdout = StringIO.new 80 | $stderr = StringIO.new 81 | 82 | example.run 83 | 84 | example.metadata[:stdout] = $stdout.string 85 | example.metadata[:stderr] = $stderr.string 86 | 87 | $stdout = STDOUT 88 | $stderr = STDERR 89 | end 90 | end 91 | ``` 92 | 93 | Note that this example captures all output from every example all the time, potentially interfering with local debugging. You might like to restrict this to only on CI, or by using [rspec filters](https://rspec.info/features/3-12/rspec-core/hooks/filtering/). 94 | 95 | ## Caveats 96 | 97 | * XML can only represent a [limited subset of characters][xml-charsets] which excludes null bytes and most control characters. This gem will use character entities where possible and fall back to replacing invalid characters with Ruby-like escape codes otherwise. For example, the null byte becomes `\0`. 98 | 99 | [xml-charsets]: https://www.w3.org/TR/xml/#charsets 100 | 101 | ## Development 102 | 103 | Run the specs with `bundle exec rake`, which uses [Appraisal][appraisal] to run the specs against all supported versions of rspec. 104 | 105 | [appraisal]: https://github.com/thoughtbot/appraisal 106 | 107 | ## Releasing 108 | 109 | Bump the gem version in the gemspec, and commit. Then `bundle exec rake build` to build a gem package, `bundle exec rake install` to install and test it locally, then `bundle exec rake release` to tag and push the commits and gem. 110 | 111 | ## License 112 | 113 | The MIT License, see [LICENSE](./LICENSE). 114 | 115 | ## Thanks 116 | 117 | Inspired by the work of [Diego Souza][dgvncsz0f] on [RSpec Formatters][dgvncsz0f/rspec_formatters] after frustration with [CI Reporter][ci_reporter]. 118 | 119 | [dgvncsz0f]: https://github.com/dgvncsz0f 120 | [dgvncsz0f/rspec_formatters]: https://github.com/dgvncsz0f/rspec_formatters 121 | [ci_reporter]: https://github.com/nicksieger/ci_reporter 122 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "appraisal" 3 | require "rspec/core/rake_task" 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | 9 | if !ENV["APPRAISAL_INITIALIZED"] && !ENV["CI"] 10 | task :default => :appraisal 11 | end 12 | -------------------------------------------------------------------------------- /example/.rspec: -------------------------------------------------------------------------------- 1 | --format RspecJunitFormatter 2 | --out tmp/rspec.xml 3 | -------------------------------------------------------------------------------- /example/spec/example_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require_relative "shared_examples" 3 | 4 | describe "some example specs" do 5 | it "should succeed" do 6 | expect(true).to be(true) 7 | end 8 | 9 | it "should fail" do 10 | expect(false).to be(true) 11 | end 12 | 13 | it "should raise" do 14 | raise ArgumentError 15 | end 16 | 17 | it "should be pending" do 18 | if defined? skip 19 | skip 20 | else 21 | pending 22 | end 23 | end 24 | 25 | it "shows diffs cleanly" do 26 | expect({a: "b", c: "d"}).to eql({a: 2, c: 4}) 27 | end 28 | 29 | it "replaces naughty \0 and \e characters, \x01 and \uFFFF too" do 30 | expect("\0\0\0").to eql("emergency services") 31 | end 32 | 33 | it "escapes controlling \u{7f} characters" do 34 | expect("\u{7f}").to eql("pacman om nom nom") 35 | end 36 | 37 | it "can include unicodes 😁" do 38 | expect("🚀").to eql("🔥") 39 | end 40 | 41 | it %{escapes } do 42 | expect("

This is important

").to eql("

This is very important

") 43 | end 44 | 45 | it_should_behave_like "shared examples" 46 | 47 | it "can capture stdout and stderr" do 48 | $stdout.puts "Test" 49 | $stderr.puts "Bar" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /example/spec/shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples "shared examples" do 2 | context "in a shared example" do 3 | it "succeeds" do 4 | expect(true).to be(true) 5 | end 6 | 7 | it "also fails" do 8 | expect(false).to be(true) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /example/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | # register around filter that captures stderr and stdout 3 | config.around(:each) do |example| 4 | $stdout = StringIO.new 5 | $stderr = StringIO.new 6 | 7 | example.run 8 | 9 | example.metadata[:stdout] = $stdout.string 10 | example.metadata[:stderr] = $stderr.string 11 | 12 | $stdout = STDOUT 13 | $stderr = STDERR 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sj26/rspec_junit_formatter/3f79074a9987d722976542d7d6e2ebb470ad47d1/example/tmp/.keep -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | BUNDLE_WITHOUT: "development,test" 4 | -------------------------------------------------------------------------------- /gemfiles/rspec_2_x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 2.14", "< 2.99" 6 | gem "rake", "~> 10.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rspec_2_x.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (10.5.0) 22 | rspec (2.14.1) 23 | rspec-core (~> 2.14.0) 24 | rspec-expectations (~> 2.14.0) 25 | rspec-mocks (~> 2.14.0) 26 | rspec-core (2.14.8) 27 | rspec-expectations (2.14.5) 28 | diff-lcs (>= 1.1.3, < 2.0) 29 | rspec-mocks (2.14.6) 30 | thor (1.1.0) 31 | 32 | PLATFORMS 33 | ruby 34 | 35 | DEPENDENCIES 36 | appraisal 37 | bundler 38 | coderay 39 | nokogiri (~> 1.8, >= 1.8.2) 40 | rake (~> 10.0) 41 | rspec (~> 2.14, < 2.99) 42 | rspec_junit_formatter! 43 | 44 | BUNDLED WITH 45 | 2.2.26 46 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.0.0" 6 | gem "rake", "~> 10.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (10.5.0) 22 | rspec (3.0.0) 23 | rspec-core (~> 3.0.0) 24 | rspec-expectations (~> 3.0.0) 25 | rspec-mocks (~> 3.0.0) 26 | rspec-core (3.0.4) 27 | rspec-support (~> 3.0.0) 28 | rspec-expectations (3.0.4) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.0.0) 31 | rspec-mocks (3.0.4) 32 | rspec-support (~> 3.0.0) 33 | rspec-support (3.0.4) 34 | thor (1.1.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | appraisal 41 | bundler 42 | coderay 43 | nokogiri (~> 1.8, >= 1.8.2) 44 | rake (~> 10.0) 45 | rspec (~> 3.0.0) 46 | rspec_junit_formatter! 47 | 48 | BUNDLED WITH 49 | 2.2.26 50 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.1.0" 6 | gem "rake", "~> 10.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (10.5.0) 22 | rspec (3.1.0) 23 | rspec-core (~> 3.1.0) 24 | rspec-expectations (~> 3.1.0) 25 | rspec-mocks (~> 3.1.0) 26 | rspec-core (3.1.7) 27 | rspec-support (~> 3.1.0) 28 | rspec-expectations (3.1.2) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.1.0) 31 | rspec-mocks (3.1.3) 32 | rspec-support (~> 3.1.0) 33 | rspec-support (3.1.2) 34 | thor (1.1.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | appraisal 41 | bundler 42 | coderay 43 | nokogiri (~> 1.8, >= 1.8.2) 44 | rake (~> 10.0) 45 | rspec (~> 3.1.0) 46 | rspec_junit_formatter! 47 | 48 | BUNDLED WITH 49 | 2.2.26 50 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_10.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.10.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_10.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.10.0) 23 | rspec-core (~> 3.10.0) 24 | rspec-expectations (~> 3.10.0) 25 | rspec-mocks (~> 3.10.0) 26 | rspec-core (3.10.1) 27 | rspec-support (~> 3.10.0) 28 | rspec-expectations (3.10.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.10.0) 31 | rspec-mocks (3.10.2) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.10.0) 34 | rspec-support (3.10.3) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.10.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.2.0" 6 | gem "rake", "~> 10.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (10.5.0) 22 | rspec (3.2.0) 23 | rspec-core (~> 3.2.0) 24 | rspec-expectations (~> 3.2.0) 25 | rspec-mocks (~> 3.2.0) 26 | rspec-core (3.2.3) 27 | rspec-support (~> 3.2.0) 28 | rspec-expectations (3.2.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.2.0) 31 | rspec-mocks (3.2.1) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.2.0) 34 | rspec-support (3.2.2) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake (~> 10.0) 46 | rspec (~> 3.2.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.3.0" 6 | gem "rake", "~> 10.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_3.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (10.5.0) 22 | rspec (3.3.0) 23 | rspec-core (~> 3.3.0) 24 | rspec-expectations (~> 3.3.0) 25 | rspec-mocks (~> 3.3.0) 26 | rspec-core (3.3.2) 27 | rspec-support (~> 3.3.0) 28 | rspec-expectations (3.3.1) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.3.0) 31 | rspec-mocks (3.3.2) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.3.0) 34 | rspec-support (3.3.0) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake (~> 10.0) 46 | rspec (~> 3.3.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.4.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_4.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.4.0) 23 | rspec-core (~> 3.4.0) 24 | rspec-expectations (~> 3.4.0) 25 | rspec-mocks (~> 3.4.0) 26 | rspec-core (3.4.4) 27 | rspec-support (~> 3.4.0) 28 | rspec-expectations (3.4.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.4.0) 31 | rspec-mocks (3.4.1) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.4.0) 34 | rspec-support (3.4.1) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.4.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.5.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_5.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.5.0) 23 | rspec-core (~> 3.5.0) 24 | rspec-expectations (~> 3.5.0) 25 | rspec-mocks (~> 3.5.0) 26 | rspec-core (3.5.4) 27 | rspec-support (~> 3.5.0) 28 | rspec-expectations (3.5.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.5.0) 31 | rspec-mocks (3.5.0) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.5.0) 34 | rspec-support (3.5.0) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.5.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.6.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_6.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.6.0) 23 | rspec-core (~> 3.6.0) 24 | rspec-expectations (~> 3.6.0) 25 | rspec-mocks (~> 3.6.0) 26 | rspec-core (3.6.0) 27 | rspec-support (~> 3.6.0) 28 | rspec-expectations (3.6.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.6.0) 31 | rspec-mocks (3.6.0) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.6.0) 34 | rspec-support (3.6.0) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.6.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_7.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.7.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_7.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.7.0) 23 | rspec-core (~> 3.7.0) 24 | rspec-expectations (~> 3.7.0) 25 | rspec-mocks (~> 3.7.0) 26 | rspec-core (3.7.1) 27 | rspec-support (~> 3.7.0) 28 | rspec-expectations (3.7.0) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.7.0) 31 | rspec-mocks (3.7.0) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.7.0) 34 | rspec-support (3.7.1) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.7.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_8.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.8.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_8.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.8.0) 23 | rspec-core (~> 3.8.0) 24 | rspec-expectations (~> 3.8.0) 25 | rspec-mocks (~> 3.8.0) 26 | rspec-core (3.8.2) 27 | rspec-support (~> 3.8.0) 28 | rspec-expectations (3.8.6) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.8.0) 31 | rspec-mocks (3.8.2) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.8.0) 34 | rspec-support (3.8.3) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.8.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_9.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rspec", "~> 3.9.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rspec_3_9.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | rspec_junit_formatter (0.6.0) 5 | rspec-core (>= 2, < 4, != 2.12.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | appraisal (2.4.1) 11 | bundler 12 | rake 13 | thor (>= 0.14.0) 14 | coderay (1.1.3) 15 | diff-lcs (1.5.0) 16 | mini_portile2 (2.6.1) 17 | nokogiri (1.12.5) 18 | mini_portile2 (~> 2.6.1) 19 | racc (~> 1.4) 20 | racc (1.6.0) 21 | rake (13.0.6) 22 | rspec (3.9.0) 23 | rspec-core (~> 3.9.0) 24 | rspec-expectations (~> 3.9.0) 25 | rspec-mocks (~> 3.9.0) 26 | rspec-core (3.9.3) 27 | rspec-support (~> 3.9.3) 28 | rspec-expectations (3.9.4) 29 | diff-lcs (>= 1.2.0, < 2.0) 30 | rspec-support (~> 3.9.0) 31 | rspec-mocks (3.9.1) 32 | diff-lcs (>= 1.2.0, < 2.0) 33 | rspec-support (~> 3.9.0) 34 | rspec-support (3.9.4) 35 | thor (1.1.0) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | appraisal 42 | bundler 43 | coderay 44 | nokogiri (~> 1.8, >= 1.8.2) 45 | rake 46 | rspec (~> 3.9.0) 47 | rspec_junit_formatter! 48 | 49 | BUNDLED WITH 50 | 2.2.26 51 | -------------------------------------------------------------------------------- /lib/rspec_junit_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "socket" 4 | require "time" 5 | 6 | require "rspec/core" 7 | require "rspec/core/formatters/base_formatter" 8 | 9 | # Dumps rspec results as a JUnit XML file. 10 | # Based on XML schema: http://windyroad.org/dl/Open%20Source/JUnit.xsd 11 | class RSpecJUnitFormatter < RSpec::Core::Formatters::BaseFormatter 12 | # rspec 2 and 3 implements are in separate files. 13 | 14 | private 15 | 16 | def xml_dump 17 | output << %{\n} 18 | output << %{\n} 28 | output << %{\n} 29 | output << %{\n} 33 | output << %{\n} 34 | xml_dump_examples 35 | output << %{\n} 36 | end 37 | 38 | def xml_dump_examples 39 | examples.each do |example| 40 | case result_of(example) 41 | when :pending 42 | xml_dump_pending(example) 43 | when :failed 44 | xml_dump_failed(example) 45 | else 46 | xml_dump_example(example) 47 | end 48 | end 49 | end 50 | 51 | def xml_dump_pending(example) 52 | xml_dump_example(example) do 53 | output << %{} 54 | end 55 | end 56 | 57 | def xml_dump_failed(example) 58 | xml_dump_example(example) do 59 | output << %{} 63 | output << escape(failure_for(example)) 64 | output << %{} 65 | end 66 | end 67 | 68 | def xml_dump_example(example) 69 | output << %{} 77 | yield if block_given? 78 | xml_dump_output(example) 79 | output << %{\n} 80 | end 81 | 82 | def xml_dump_output(example) 83 | if (stdout = stdout_for(example)) && !stdout.empty? 84 | output << %{} 85 | output << escape(stdout) 86 | output << %{} 87 | end 88 | 89 | if (stderr = stderr_for(example)) && !stderr.empty? 90 | output << %{} 91 | output << escape(stderr) 92 | output << %{} 93 | end 94 | end 95 | 96 | # Inversion of character range from https://www.w3.org/TR/xml/#charsets 97 | ILLEGAL_REGEXP = Regexp.new( 98 | "[^".dup << 99 | "\u{9}" << # => \t 100 | "\u{a}" << # => \n 101 | "\u{d}" << # => \r 102 | "\u{20}-\u{d7ff}" << 103 | "\u{e000}-\u{fffd}" << 104 | "\u{10000}-\u{10ffff}" << 105 | "]" 106 | ) 107 | 108 | # Replace illegals with a Ruby-like escape 109 | ILLEGAL_REPLACEMENT = Hash.new { |_, c| 110 | x = c.ord 111 | if x <= 0xff 112 | "\\x%02X".freeze % x 113 | elsif x <= 0xffff 114 | "\\u%04X".freeze % x 115 | else 116 | "\\u{%X}".freeze % x 117 | end.freeze 118 | }.update( 119 | "\0".freeze => "\\0".freeze, 120 | "\a".freeze => "\\a".freeze, 121 | "\b".freeze => "\\b".freeze, 122 | "\f".freeze => "\\f".freeze, 123 | "\v".freeze => "\\v".freeze, 124 | "\e".freeze => "\\e".freeze, 125 | ).freeze 126 | 127 | # Discouraged characters from https://www.w3.org/TR/xml/#charsets 128 | # Plus special characters with well-known entity replacements 129 | DISCOURAGED_REGEXP = Regexp.new( 130 | "[".dup << 131 | "\u{22}" << # => " 132 | "\u{26}" << # => & 133 | "\u{27}" << # => ' 134 | "\u{3c}" << # => < 135 | "\u{3e}" << # => > 136 | "\u{7f}-\u{84}" << 137 | "\u{86}-\u{9f}" << 138 | "\u{fdd0}-\u{fdef}" << 139 | "\u{1fffe}-\u{1ffff}" << 140 | "\u{2fffe}-\u{2ffff}" << 141 | "\u{3fffe}-\u{3ffff}" << 142 | "\u{4fffe}-\u{4ffff}" << 143 | "\u{5fffe}-\u{5ffff}" << 144 | "\u{6fffe}-\u{6ffff}" << 145 | "\u{7fffe}-\u{7ffff}" << 146 | "\u{8fffe}-\u{8ffff}" << 147 | "\u{9fffe}-\u{9ffff}" << 148 | "\u{afffe}-\u{affff}" << 149 | "\u{bfffe}-\u{bffff}" << 150 | "\u{cfffe}-\u{cffff}" << 151 | "\u{dfffe}-\u{dffff}" << 152 | "\u{efffe}-\u{effff}" << 153 | "\u{ffffe}-\u{fffff}" << 154 | "\u{10fffe}-\u{10ffff}" << 155 | "]" 156 | ) 157 | 158 | # Translate well-known entities, or use generic unicode hex entity 159 | DISCOURAGED_REPLACEMENTS = Hash.new { |_, c| "&#x#{c.ord.to_s(16)};".freeze }.update( 160 | ?".freeze => """.freeze, 161 | ?&.freeze => "&".freeze, 162 | ?'.freeze => "'".freeze, 163 | ?<.freeze => "<".freeze, 164 | ?>.freeze => ">".freeze, 165 | ).freeze 166 | 167 | def escape(text) 168 | # Make sure it's utf-8, replace illegal characters with ruby-like escapes, and replace special and discouraged characters with entities 169 | text.to_s.encode(Encoding::UTF_8).gsub(ILLEGAL_REGEXP, ILLEGAL_REPLACEMENT).gsub(DISCOURAGED_REGEXP, DISCOURAGED_REPLACEMENTS) 170 | end 171 | 172 | STRIP_DIFF_COLORS_BLOCK_REGEXP = /^ ( [ ]* ) Diff: (?: \e\[ 0 m )? (?: \n \1 \e\[ \d+ (?: ; \d+ )* m .* )* /x 173 | STRIP_DIFF_COLORS_CODES_REGEXP = /\e\[ \d+ (?: ; \d+ )* m/x 174 | 175 | def strip_diff_colors(string) 176 | # XXX: RSpec diffs are appended to the message lines fairly early and will 177 | # contain ANSI escape codes for colorizing terminal output if the global 178 | # rspec configuration is turned on, regardless of which notification lines 179 | # we ask for. We need to strip the codes from the diff part of the message 180 | # for XML output here. 181 | # 182 | # We also only want to target the diff hunks because the failure message 183 | # itself might legitimately contain ansi escape codes. 184 | # 185 | string.sub(STRIP_DIFF_COLORS_BLOCK_REGEXP) { |match| match.gsub(STRIP_DIFF_COLORS_CODES_REGEXP, "".freeze) } 186 | end 187 | end 188 | 189 | RspecJunitFormatter = RSpecJUnitFormatter 190 | 191 | if Gem::Version.new(RSpec::Core::Version::STRING) >= Gem::Version.new("3") 192 | require "rspec_junit_formatter/rspec3" 193 | else 194 | require "rspec_junit_formatter/rspec2" 195 | end 196 | -------------------------------------------------------------------------------- /lib/rspec_junit_formatter/rspec2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RSpecJUnitFormatter < RSpec::Core::Formatters::BaseFormatter 4 | attr_reader :started 5 | 6 | def start(example_count) 7 | @started = Time.now 8 | super 9 | end 10 | 11 | def dump_summary(duration, example_count, failure_count, pending_count) 12 | super 13 | xml_dump 14 | end 15 | 16 | private 17 | 18 | def result_of(example) 19 | example.execution_result[:status].to_sym 20 | end 21 | 22 | def example_group_file_path_for(example) 23 | meta = example.metadata 24 | while meta[:example_group] 25 | meta = meta[:example_group] 26 | end 27 | meta[:file_path] 28 | end 29 | 30 | def classname_for(example) 31 | fp = example_group_file_path_for(example) 32 | fp.sub(%r{\.[^/.]+\Z}, "").gsub("/", ".").gsub(/\A\.+|\.+\Z/, "") 33 | end 34 | 35 | def duration_for(example) 36 | example.execution_result[:run_time] 37 | end 38 | 39 | def description_for(example) 40 | example.full_description 41 | end 42 | 43 | def exception_for(example) 44 | example.execution_result[:exception] 45 | end 46 | 47 | def failure_type_for(example) 48 | exception_for(example).class.name 49 | end 50 | 51 | def failure_message_for(example) 52 | strip_diff_colors(exception_for(example).to_s) 53 | end 54 | 55 | def failure_for(example) 56 | exception = exception_for(example) 57 | message = strip_diff_colors(exception.message) 58 | backtrace = format_backtrace(exception.backtrace, example) 59 | 60 | if shared_group = find_shared_group(example) 61 | backtrace << "Shared Example Group: \"#{shared_group.metadata[:shared_group_name]}\" called from #{shared_group.metadata[:example_group][:location]}" 62 | end 63 | 64 | "#{message}\n#{backtrace.join("\n")}" 65 | end 66 | 67 | def error_count 68 | 0 69 | end 70 | 71 | def find_shared_group(example) 72 | group_and_parent_groups(example).find { |group| group.metadata[:shared_group_name] } 73 | end 74 | 75 | def group_and_parent_groups(example) 76 | example.example_group.parent_groups + [example.example_group] 77 | end 78 | 79 | def stdout_for(example) 80 | example.metadata[:stdout] 81 | end 82 | 83 | def stderr_for(example) 84 | example.metadata[:stderr] 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/rspec_junit_formatter/rspec3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RSpecJUnitFormatter < RSpec::Core::Formatters::BaseFormatter 4 | RSpec::Core::Formatters.register self, 5 | :start, 6 | :stop, 7 | :dump_summary 8 | 9 | def start(notification) 10 | @start_notification = notification 11 | @started = Time.now 12 | super 13 | end 14 | 15 | def stop(notification) 16 | @examples_notification = notification 17 | end 18 | 19 | def dump_summary(notification) 20 | @summary_notification = notification 21 | without_color { xml_dump } 22 | end 23 | 24 | private 25 | 26 | attr_reader :started 27 | 28 | def example_count 29 | @summary_notification.example_count 30 | end 31 | 32 | def pending_count 33 | @summary_notification.pending_count 34 | end 35 | 36 | def failure_count 37 | @summary_notification.failure_count 38 | end 39 | 40 | def duration 41 | @summary_notification.duration 42 | end 43 | 44 | def examples 45 | @examples_notification.notifications 46 | end 47 | 48 | def error_count 49 | # Introduced in rspec 3.6 50 | if @summary_notification.respond_to?(:errors_outside_of_examples_count) 51 | @summary_notification.errors_outside_of_examples_count 52 | else 53 | 0 54 | end 55 | end 56 | 57 | def result_of(notification) 58 | notification.example.execution_result.status 59 | end 60 | 61 | def example_group_file_path_for(notification) 62 | metadata = notification.example.metadata[:example_group] 63 | while parent_metadata = metadata[:parent_example_group] 64 | metadata = parent_metadata 65 | end 66 | metadata[:file_path] 67 | end 68 | 69 | def classname_for(notification) 70 | fp = example_group_file_path_for(notification) 71 | fp.sub(%r{\.[^/]*\Z}, "").gsub("/", ".").gsub(%r{\A\.+|\.+\Z}, "") 72 | end 73 | 74 | def duration_for(notification) 75 | notification.example.execution_result.run_time 76 | end 77 | 78 | def description_for(notification) 79 | notification.example.full_description 80 | end 81 | 82 | def failure_type_for(example) 83 | exception_for(example).class.name 84 | end 85 | 86 | def failure_message_for(example) 87 | strip_diff_colors(exception_for(example).to_s) 88 | end 89 | 90 | def failure_for(notification) 91 | strip_diff_colors(notification.message_lines.join("\n")) << "\n" << notification.formatted_backtrace.join("\n") 92 | end 93 | 94 | def exception_for(notification) 95 | notification.example.execution_result.exception 96 | end 97 | 98 | # rspec makes it really difficult to swap in configuration temporarily due to 99 | # the way it cascades defaults, command line arguments, and user 100 | # configuration. This method makes sure configuration gets swapped in 101 | # correctly, but also that the original state is definitely restored. 102 | def swap_rspec_configuration(key, value) 103 | unset = Object.new 104 | force = RSpec.configuration.send(:value_for, key) { unset } 105 | if unset.equal?(force) 106 | previous = RSpec.configuration.send(key) 107 | RSpec.configuration.send(:"#{key}=", value) 108 | else 109 | RSpec.configuration.force({key => value}) 110 | end 111 | yield 112 | ensure 113 | if unset.equal?(force) 114 | RSpec.configuration.send(:"#{key}=", previous) 115 | else 116 | RSpec.configuration.force({key => force}) 117 | end 118 | end 119 | 120 | # Completely gross hack for absolutely forcing off colorising for the 121 | # duration of a block. 122 | if RSpec.configuration.respond_to?(:color_mode=) 123 | def without_color(&block) 124 | swap_rspec_configuration(:color_mode, :off, &block) 125 | end 126 | elsif RSpec.configuration.respond_to?(:color=) 127 | def without_color(&block) 128 | swap_rspec_configuration(:color, false, &block) 129 | end 130 | else 131 | warn 'rspec_junit_formatter cannot prevent colorising due to an unexpected RSpec.configuration format' 132 | def without_color 133 | yield 134 | end 135 | end 136 | 137 | def stdout_for(example_notification) 138 | example_notification.example.metadata[:stdout] 139 | end 140 | 141 | def stderr_for(example_notification) 142 | example_notification.example.metadata[:stderr] 143 | end 144 | end 145 | 146 | # rspec-core 3.0.x forgot to mark this as a module function which causes: 147 | # 148 | # NoMethodError: undefined method `wrap' for RSpec::Core::Notifications::NullColorizer:Class 149 | # .../rspec-core-3.0.4/lib/rspec/core/notifications.rb:229:in `add_shared_group_line' 150 | # .../rspec-core-3.0.4/lib/rspec/core/notifications.rb:157:in `message_lines' 151 | # 152 | if defined?(RSpec::Core::Notifications::NullColorizer) && RSpec::Core::Notifications::NullColorizer.is_a?(Class) && !RSpec::Core::Notifications::NullColorizer.respond_to?(:wrap) 153 | RSpec::Core::Notifications::NullColorizer.class_eval do 154 | def self.wrap(*args) 155 | new.wrap(*args) 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /reference/JUnit.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | JUnit test result schema for the Apache Ant JUnit and JUnitReport tasks 8 | Copyright © 2011, Windy Road Technology Pty. Limited 9 | The Apache Ant JUnit XML Schema is distributed under the terms of the GNU Lesser General Public License (LGPL) http://www.gnu.org/licenses/lgpl.html 10 | Permission to waive conditions of this license may be requested from Windy Road Support (http://windyroad.org/support). 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Contains an aggregation of testsuite results 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Derived from testsuite/@name in the non-aggregated documents 31 | 32 | 33 | 34 | 35 | Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Contains the results of exexuting a testsuite 48 | 49 | 50 | 51 | 52 | Properties (e.g., environment settings) set during test execution 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace 77 | 78 | 79 | 80 | 81 | 82 | 83 | The error message. e.g., if a java exception is thrown, the return value of getMessage() 84 | 85 | 86 | 87 | 88 | The type of error that occured. e.g., if a java execption is thrown the full class name of the exception. 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace 98 | 99 | 100 | 101 | 102 | 103 | 104 | The message specified in the assert 105 | 106 | 107 | 108 | 109 | The type of the assert. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Name of the test method 120 | 121 | 122 | 123 | 124 | Full class name for the class the test method is in. 125 | 126 | 127 | 128 | 129 | Time taken (in seconds) to execute the test 130 | 131 | 132 | 133 | 134 | 135 | 136 | Data that was written to standard out while the test was executed 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | Data that was written to standard error while the test was executed 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | when the test was executed. Timezone may not be specified. 168 | 169 | 170 | 171 | 172 | Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined. 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | The total number of tests in the suite 183 | 184 | 185 | 186 | 187 | The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals 188 | 189 | 190 | 191 | 192 | The total number of tests in the suite that errorrd. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. 193 | 194 | 195 | 196 | 197 | Time taken (in seconds) to execute the tests in the suite 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /rspec_junit_formatter.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "rspec_junit_formatter" 3 | s.version = "0.6.0" 4 | s.platform = Gem::Platform::RUBY 5 | s.author = "Samuel Cochran" 6 | s.email = "sj26@sj26.com" 7 | s.homepage = "https://github.com/sj26/rspec_junit_formatter" 8 | s.summary = "RSpec JUnit XML formatter" 9 | s.description = "RSpec results that your continuous integration service can read." 10 | s.license = "MIT" 11 | 12 | s.required_ruby_version = ">= 2.3.0" 13 | s.required_rubygems_version = ">= 2.0.0" 14 | 15 | s.metadata = { 16 | 'changelog_uri' => 'https://github.com/sj26/rspec_junit_formatter/blob/HEAD/CHANGELOG.md', 17 | } 18 | 19 | # https://github.com/rspec/rspec-core/commit/f06254c00770387e3a8a2efbdbc973035c217f6a 20 | s.add_dependency "rspec-core", ">= 2", "< 4", "!= 2.12.0" 21 | 22 | s.add_development_dependency "bundler" 23 | s.add_development_dependency "appraisal" 24 | s.add_development_dependency "nokogiri", "~> 1.8", ">= 1.8.2" 25 | s.add_development_dependency "rake" 26 | s.add_development_dependency "coderay" 27 | 28 | s.files = Dir["lib/**/*", "README.md", "LICENSE"] 29 | s.require_path = "lib" 30 | end 31 | -------------------------------------------------------------------------------- /spec/rspec_junit_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require "pty" 2 | require "stringio" 3 | require "nokogiri" 4 | require "rspec_junit_formatter" 5 | 6 | describe RspecJunitFormatter do 7 | TMP_DIR = File.expand_path("../../tmp", __FILE__) 8 | EXAMPLE_DIR = File.expand_path("../../example", __FILE__) 9 | 10 | before(:all) { ENV.delete("TEST_ENV_NUMBER") } # Make sure this doesn't exist by default 11 | 12 | let(:formatter_output_path) { File.join(TMP_DIR, "junit.xml") } 13 | let(:formatter_output) { output; File.read(formatter_output_path) } 14 | 15 | let(:formatter_arguments) { ["--format", "RspecJunitFormatter", "--out", formatter_output_path] } 16 | let(:extra_arguments) { [] } 17 | 18 | let(:color_opt) do 19 | RSpec.configuration.respond_to?(:color_mode=) ? "--force-color" : "--color" 20 | end 21 | 22 | def safe_pty(command, **pty_options) 23 | output = StringIO.new 24 | 25 | PTY.spawn(*command, **pty_options) do |r, w, pid| 26 | begin 27 | r.each_line { |line| output.puts(line) } 28 | rescue Errno::EIO 29 | # Command closed output, or exited 30 | ensure 31 | Process.wait pid 32 | end 33 | end 34 | 35 | output.string 36 | end 37 | 38 | def execute_example_spec 39 | command = ["bundle", "exec", "rspec", *formatter_arguments, color_opt, *extra_arguments] 40 | 41 | safe_pty(command, chdir: EXAMPLE_DIR) 42 | end 43 | 44 | let(:output) { execute_example_spec } 45 | 46 | let(:doc) { Nokogiri::XML::Document.parse(formatter_output) } 47 | 48 | let(:testsuite) { doc.xpath("/testsuite").first } 49 | let(:testcases) { doc.xpath("/testsuite/testcase") } 50 | let(:successful_testcases) { doc.xpath("/testsuite/testcase[not(failure) and not(skipped)]") } 51 | let(:pending_testcases) { doc.xpath("/testsuite/testcase[skipped]") } 52 | let(:failed_testcases) { doc.xpath("/testsuite/testcase[failure]") } 53 | let(:shared_testcases) { doc.xpath("/testsuite/testcase[contains(@name, 'shared example')]") } 54 | let(:failed_shared_testcases) { doc.xpath("/testsuite/testcase[contains(@name, 'shared example')][failure]") } 55 | 56 | # Combined into a single example so we don't have to re-run the example rspec 57 | # process over and over. (We need to change the parameters in later specs so 58 | # we can't use before(:all).) 59 | # 60 | it "correctly describes the test results", aggregate_failures: true do 61 | # it has a testsuite 62 | 63 | expect(testsuite).not_to be(nil) 64 | 65 | expect(testsuite["name"]).to eql("rspec") 66 | expect(testsuite["tests"]).to eql("12") 67 | expect(testsuite["skipped"]).to eql("1") 68 | expect(testsuite["failures"]).to eql("8") 69 | expect(testsuite["errors"]).to eql("0") 70 | expect(Time.parse(testsuite["timestamp"])).to be_within(60).of(Time.now) 71 | expect(testsuite["time"].to_f).to be > 0 72 | expect(testsuite["hostname"]).not_to be_empty 73 | 74 | # it has some test cases 75 | 76 | expect(testcases.size).to eql(12) 77 | 78 | testcases.each do |testcase| 79 | expect(testcase["classname"]).to eql("spec.example_spec") 80 | expect(testcase["name"]).not_to be_empty 81 | expect(testcase["time"].to_f).to be > 0 82 | end 83 | 84 | # it has successful test cases 85 | 86 | expect(successful_testcases.size).to eql(3) 87 | 88 | successful_testcases.each do |testcase| 89 | expect(testcase).not_to be(nil) 90 | # test results that capture stdout / stderr are not 'empty' 91 | unless (testcase["name"]) =~ /capture stdout and stderr/ 92 | expect(testcase.children).to be_empty 93 | end 94 | end 95 | 96 | # it has pending test cases 97 | 98 | expect(pending_testcases.size).to eql(1) 99 | 100 | pending_testcases.each do |testcase| 101 | expect(testcase.element_children.size).to eql(1) 102 | child = testcase.element_children.first 103 | expect(child.name).to eql("skipped") 104 | expect(child.attributes).to be_empty 105 | expect(child.text).to be_empty 106 | end 107 | 108 | # it has failed test cases 109 | 110 | expect(failed_testcases.size).to eql(8) 111 | 112 | failed_testcases.each do |testcase| 113 | expect(testcase).not_to be(nil) 114 | expect(testcase.element_children.size).to eql(1) 115 | 116 | child = testcase.element_children.first 117 | expect(child.name).to eql("failure") 118 | expect(child["message"]).not_to be_empty 119 | expect(child.text.strip).not_to be_empty 120 | expect(child.text.strip).not_to match(/\\e\[(?:\d+;?)+m/) 121 | end 122 | 123 | # it has shared test cases which list both the inclusion and included files 124 | 125 | expect(shared_testcases.size).to eql(2) 126 | shared_testcases.each do |testcase| 127 | # shared examples should be groups with their including files 128 | expect(testcase["classname"]).to eql("spec.example_spec") 129 | end 130 | 131 | expect(failed_shared_testcases.size).to eql(1) 132 | failed_shared_testcases.each do |testcase| 133 | expect(testcase.text).to include("example_spec.rb") 134 | expect(testcase.text).to include("shared_examples.rb") 135 | end 136 | 137 | # it cleans up diffs 138 | 139 | diff_testcase_failure = doc.xpath("//testcase[contains(@name, 'diffs')]/failure").first 140 | expect(diff_testcase_failure[:message]).not_to match(/\e | \\e/x) 141 | expect(diff_testcase_failure.text).not_to match(/\e | \\e/x) 142 | 143 | # it correctly replaces illegal characters 144 | 145 | expect(doc.xpath("//testcase[contains(@name, 'naughty')]").first[:name]).to eql("some example specs replaces naughty \\0 and \\e characters, \\x01 and \\uFFFF too") 146 | 147 | # it correctly escapes discouraged characters 148 | 149 | expect(doc.xpath("//testcase[contains(@name, 'controlling')]").first[:name]).to eql("some example specs escapes controlling \u{7f} characters") 150 | 151 | # it correctly escapes emoji characters 152 | 153 | expect(doc.xpath("//testcase[contains(@name, 'unicodes')]").first[:name]).to eql("some example specs can include unicodes \u{1f601}") 154 | 155 | # it correctly escapes reserved xml characters 156 | 157 | expect(doc.xpath("//testcase[contains(@name, 'html')]").first[:name]).to eql(%{some example specs escapes }) 158 | 159 | # it correctly captures stdout / stderr output 160 | expect(doc.xpath("//testcase/system-out").text).to eql("Test\n") 161 | expect(doc.xpath("//testcase/system-err").text).to eql("Bar\n") 162 | end 163 | 164 | context "when $TEST_ENV_NUMBER is set" do 165 | around do |example| 166 | begin 167 | ENV["TEST_ENV_NUMBER"] = "42" 168 | example.call 169 | ensure 170 | ENV.delete("TEST_ENV_NUMBER") 171 | end 172 | end 173 | 174 | it "includes $TEST_ENV_NUMBER in the testsuite name" do 175 | expect(testsuite["name"]).to eql("rspec42") 176 | end 177 | end 178 | 179 | context "with a known rspec seed" do 180 | let(:extra_arguments) { ["--seed", "12345"] } 181 | 182 | let(:seed_property) { doc.xpath("/testsuite/properties/property[@name='seed']").first } 183 | 184 | it "has a property with seed info" do 185 | expect(seed_property["name"]).to eql("seed") 186 | expect(seed_property["value"]).to eql("12345") 187 | end 188 | end 189 | end 190 | --------------------------------------------------------------------------------