├── Gemfile ├── .yardopts ├── lib └── rspec │ ├── tap_j.rb │ ├── tap_y.rb │ └── ontap.rb ├── .gitignore ├── MANIFEST ├── .travis.yml ├── try └── spec_example.rb ├── Assembly ├── HISTORY.md ├── INDEX.yml ├── .index ├── LICENSE.txt ├── README.md └── .gemspec /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title VCLog 2 | --protected 3 | --private 4 | lib/ 5 | - 6 | *.md 7 | *.txt 8 | *.yml 9 | 10 | -------------------------------------------------------------------------------- /lib/rspec/tap_j.rb: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION < '1.9' 2 | require File.dirname(__FILE__) + '/ontap' 3 | else 4 | require_relative 'ontap' 5 | end 6 | -------------------------------------------------------------------------------- /lib/rspec/tap_y.rb: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION < '1.9' 2 | require File.dirname(__FILE__) + '/ontap' 3 | else 4 | require_relative 'ontap' 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | doc 4 | log 5 | pkg 6 | tmp 7 | man/man1/index.txt 8 | man/man1/*.html 9 | man/man1/*.1 10 | web 11 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .index .yardopts bin features lib man spec test *.md *.txt 2 | .index 3 | .yardopts 4 | lib/rspec/ontap.rb 5 | lib/rspec/tap_j.rb 6 | lib/rspec/tap_y.rb 7 | README.md 8 | HISTORY.md 9 | LICENSE.txt 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | script: "bundle exec qed" 4 | rvm: 5 | - 2.1.0 6 | - 2.0.0 7 | - 1.9.3 8 | - rbx 9 | - jruby 10 | matrix: 11 | allow_failures: 12 | - rvm: rbx 13 | cache: bundler 14 | -------------------------------------------------------------------------------- /try/spec_example.rb: -------------------------------------------------------------------------------- 1 | describe "demonstration" do 2 | 3 | it "should show this passing" do 4 | expect(1).to == 1 5 | end 6 | 7 | it "should show this failing" do 8 | expect(1).to == 2 9 | end 10 | 11 | it "should show this raising an error" do 12 | raise NameError 13 | end 14 | 15 | it "should capture stdout" do 16 | puts "HELLO!" 17 | end 18 | 19 | end 20 | 21 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | email: 3 | mailto: 4 | - ruby-talk@ruby-lang.org 5 | - rubyworks-mailinglist@googlegroups.com 6 | 7 | gem: 8 | active: true 9 | 10 | github: 11 | folder: web 12 | 13 | dnote: 14 | title: Source Notes 15 | output: log/notes.html 16 | files: lib/**/*.rb 17 | 18 | locat: 19 | output: log/locat.html 20 | 21 | vclog: 22 | output: 23 | - log/history.html 24 | - log/changes.html 25 | 26 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | ## 0.3.0 / 2014-01-17 4 | 5 | This is a major overhaul of the formatter to work with RSpec 3+. 6 | It will no longer work with older versions of RSpec! Please use 7 | version 0.2.0 of this plugin if you need that. 8 | 9 | Changes: 10 | 11 | * Register formmater per new RSpec 3 API. 12 | * Change callback methods to use v3 Notification classes. 13 | 14 | 15 | ## 0.2.0 / 2012-02-01 16 | 17 | This release adds support for $stdout and $stderr capturing. 18 | 19 | Changes: 20 | 21 | * Handle captured stdout and stderr. 22 | 23 | 24 | ## 0.1.0 / 2011-12-13 25 | 26 | This is the first release of `RSpec On Tap`, a formatter for RSpec 27 | providing TAP-Y and TAP-J output, which can then be used with 28 | TapOut to produce a variety of formats. 29 | 30 | Changes: 31 | 32 | * The code was written. 33 | 34 | -------------------------------------------------------------------------------- /INDEX.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 3 | rspec-ontap 4 | 5 | version: 6 | 0.3.0 7 | 8 | title: 9 | RSpec-On-Tap 10 | 11 | summary: 12 | RSpec On Tap-Y/J 13 | 14 | description: 15 | RSpec-On-Tap is a TAP-Y/J formatter for RSpec. 16 | 17 | requirements: 18 | - rspec 19 | - tapout 20 | - mast (build) 21 | - indexer (build) 22 | #- detroit (build) 23 | 24 | resources: 25 | home: http://rubyworks.github.com/rspec-ontap 26 | code: http://github.com/rubyworks/rspec-ontap 27 | bugs: http://github.com/rubyworks/rspec-ontap/issues 28 | chat: irc://chat.us.freenode.net#rubyworks 29 | 30 | repositories: 31 | upstream: git://github.com/rubyworks/rspec-ontap.git 32 | 33 | authors: 34 | - Trans 35 | 36 | organizations: 37 | - Rubyworks 38 | 39 | copyrights: 40 | - (c) 2011 Rubyworks (BSD-2-Clause) 41 | 42 | created: 43 | 2011-12-13 44 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | revision: 2013 3 | type: ruby 4 | sources: 5 | - INDEX.yml 6 | authors: 7 | - name: Trans 8 | email: transfire@gmail.com 9 | organizations: 10 | - name: Rubyworks 11 | requirements: 12 | - name: rspec 13 | - name: tapout 14 | - groups: 15 | - build 16 | development: true 17 | name: mast 18 | - groups: 19 | - build 20 | development: true 21 | name: indexer 22 | conflicts: [] 23 | alternatives: [] 24 | resources: 25 | - type: home 26 | uri: http://rubyworks.github.com/rspec-ontap 27 | label: Homepage 28 | - type: code 29 | uri: http://github.com/rubyworks/rspec-ontap 30 | label: Source Code 31 | - type: bugs 32 | uri: http://github.com/rubyworks/rspec-ontap/issues 33 | label: Issue Tracker 34 | - type: chat 35 | uri: irc://chat.us.freenode.net#rubyworks 36 | label: IRC Channel 37 | repositories: 38 | - name: upstream 39 | scm: git 40 | uri: git://github.com/rubyworks/rspec-ontap.git 41 | categories: [] 42 | copyrights: 43 | - holder: Rubyworks 44 | year: '2011' 45 | license: BSD-2-Clause 46 | customs: [] 47 | paths: 48 | lib: 49 | - lib 50 | name: rspec-ontap 51 | title: RSpec-On-Tap 52 | version: 0.3.0 53 | summary: RSpec On Tap-Y/J 54 | description: RSpec-On-Tap is a TAP-Y/J formatter for RSpec. 55 | created: '2011-12-13' 56 | date: '2015-01-17' 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are met: 3 | 4 | 1. Redistributions of source code must retain the above copyright notice, 5 | this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | 11 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 12 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 13 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 14 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 15 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 16 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 17 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 18 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 19 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSpec On Tap 2 | 3 | [Homepage](http://rubyworks.github.com/rspec-ontap) | 4 | [Source Code](http://github.com/rubyworks/rspec-ontap) | 5 | [Report Issue](http://github.com/rubyworks/rspec-ontap/issues) 6 | 7 | 8 | ## Description 9 | 10 | **RSpec On Tap** is a TAP-Y/J formatter for RSpec. 11 | 12 | The latest version works with **RSpec 3+ only**. 13 | 14 | You can learn more about TAP-Y/J [here](https://github.com/rubyworks/tapout). 15 | 16 | 17 | ## Usage 18 | 19 | Usage is simply a matter of requiring the plugin and passing the name of the 20 | format class to the `rspec` command via the `-f` option. 21 | 22 | $ rspec -r rspec/ontap -f RSpec::TapY spec/*.rb 23 | 24 | This can be shortened to just: 25 | 26 | $ rspec -f RSpec::TapY spec/*.rb 27 | 28 | This works because RSpec will automatically require a related path -- in this 29 | case `rspec/tap_y` -- if the class if initially undefined. The library file 30 | `rspec/tap_y` itself simply requires `rspec/ontap` (the same is true for 31 | `rspec/tap_j`). 32 | 33 | With TAP-Y output in hand, the `tapout` tool can then be used to produce a 34 | variety of other output formats. First, make sure Tapout is installed: 35 | 36 | $ gem install tapout 37 | 38 | Then, for example: 39 | 40 | $ rspec -f RSpec::TapY spec/*.rb | tapout progress 41 | 42 | See the [TapOut project](http://rubyworks.github.com/tapout) for more information. 43 | 44 | 45 | 46 | ## Installation 47 | 48 | Installation follows the usual pattern: 49 | 50 | $ gem install rspec-ontap 51 | 52 | Or using your Gemfile, add something like: 53 | 54 | group :test do 55 | gem "rspec-ontap", :require => false 56 | end 57 | 58 | 59 | ## Copyrights 60 | 61 | Copyright (c) 2011 Rubyworks. All Rights Reserved. 62 | 63 | RSpecOnTap is distributable in accordance with the *FreeBSD* license. 64 | 65 | See LICENSE.txt for details. 66 | 67 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | require 'pathname' 5 | 6 | module Indexer 7 | 8 | # Convert index data into a gemspec. 9 | # 10 | # Notes: 11 | # * Assumes all executables are in bin/. 12 | # * Does not yet handle default_executable setting. 13 | # * Does not yet handle platform setting. 14 | # * Does not yet handle required_ruby_version. 15 | # * Support for rdoc entries is weak. 16 | # 17 | class GemspecExporter 18 | 19 | # File globs to include in package --unless a manifest file exists. 20 | FILES = ".index .yardopts alt bin data demo ext features lib man spec test try* [A-Z]*.*" unless defined?(FILES) 21 | 22 | # File globs to omit from FILES. 23 | OMIT = "Config.rb" unless defined?(OMIT) 24 | 25 | # Standard file patterns. 26 | PATTERNS = { 27 | :root => '{.index,Gemfile}', 28 | :bin => 'bin/*', 29 | :lib => 'lib/{**/}*', #.rb', 30 | :ext => 'ext/{**/}extconf.rb', 31 | :doc => '*.{txt,rdoc,md,markdown,tt,textile}', 32 | :test => '{test,spec}/{**/}*.rb' 33 | } unless defined?(PATTERNS) 34 | 35 | # For which revision of indexer spec is this converter intended? 36 | REVISION = 2013 unless defined?(REVISION) 37 | 38 | # 39 | def self.gemspec 40 | new.to_gemspec 41 | end 42 | 43 | # 44 | attr :metadata 45 | 46 | # 47 | def initialize(metadata=nil) 48 | @root_check = false 49 | 50 | if metadata 51 | root_dir = metadata.delete(:root) 52 | if root_dir 53 | @root = root_dir 54 | @root_check = true 55 | end 56 | metadata = nil if metadata.empty? 57 | end 58 | 59 | @metadata = metadata || YAML.load_file(root + '.index') 60 | 61 | if @metadata['revision'].to_i != REVISION 62 | warn "This gemspec exporter was not designed for this revision of index metadata." 63 | end 64 | end 65 | 66 | # 67 | def has_root? 68 | root ? true : false 69 | end 70 | 71 | # 72 | def root 73 | return @root if @root || @root_check 74 | @root_check = true 75 | @root = find_root 76 | end 77 | 78 | # 79 | def manifest 80 | return nil unless root 81 | @manifest ||= Dir.glob(root + 'manifest{,.txt}', File::FNM_CASEFOLD).first 82 | end 83 | 84 | # 85 | def scm 86 | return nil unless root 87 | @scm ||= %w{git hg}.find{ |m| (root + ".#{m}").directory? }.to_sym 88 | end 89 | 90 | # 91 | def files 92 | return [] unless root 93 | @files ||= \ 94 | if manifest 95 | File.readlines(manifest). 96 | map{ |line| line.strip }. 97 | reject{ |line| line.empty? || line[0,1] == '#' } 98 | else 99 | list = [] 100 | Dir.chdir(root) do 101 | FILES.split(/\s+/).each do |pattern| 102 | list.concat(glob(pattern)) 103 | end 104 | OMIT.split(/\s+/).each do |pattern| 105 | list = list - glob(pattern) 106 | end 107 | end 108 | list 109 | end.select{ |path| File.file?(path) }.uniq 110 | end 111 | 112 | # 113 | def glob_files(pattern) 114 | return [] unless root 115 | Dir.chdir(root) do 116 | Dir.glob(pattern).select do |path| 117 | File.file?(path) && files.include?(path) 118 | end 119 | end 120 | end 121 | 122 | def patterns 123 | PATTERNS 124 | end 125 | 126 | def executables 127 | @executables ||= \ 128 | glob_files(patterns[:bin]).map do |path| 129 | File.basename(path) 130 | end 131 | end 132 | 133 | def extensions 134 | @extensions ||= \ 135 | glob_files(patterns[:ext]).map do |path| 136 | File.basename(path) 137 | end 138 | end 139 | 140 | def name 141 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 142 | end 143 | 144 | def homepage 145 | page = ( 146 | metadata['resources'].find{ |r| r['type'] =~ /^home/i } || 147 | metadata['resources'].find{ |r| r['name'] =~ /^home/i } || 148 | metadata['resources'].find{ |r| r['name'] =~ /^web/i } 149 | ) 150 | page ? page['uri'] : false 151 | end 152 | 153 | def licenses 154 | metadata['copyrights'].map{ |c| c['license'] }.compact 155 | end 156 | 157 | def require_paths 158 | paths = metadata['paths'] || {} 159 | paths['load'] || ['lib'] 160 | end 161 | 162 | # 163 | # Convert to gemnspec. 164 | # 165 | def to_gemspec 166 | if has_root? 167 | Gem::Specification.new do |gemspec| 168 | to_gemspec_data(gemspec) 169 | to_gemspec_paths(gemspec) 170 | end 171 | else 172 | Gem::Specification.new do |gemspec| 173 | to_gemspec_data(gemspec) 174 | to_gemspec_paths(gemspec) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # Convert pure data settings. 181 | # 182 | def to_gemspec_data(gemspec) 183 | gemspec.name = name 184 | gemspec.version = metadata['version'] 185 | gemspec.summary = metadata['summary'] 186 | gemspec.description = metadata['description'] 187 | 188 | metadata['authors'].each do |author| 189 | gemspec.authors << author['name'] 190 | 191 | if author.has_key?('email') 192 | if gemspec.email 193 | gemspec.email << author['email'] 194 | else 195 | gemspec.email = [author['email']] 196 | end 197 | end 198 | end 199 | 200 | gemspec.licenses = licenses 201 | 202 | requirements = metadata['requirements'] || [] 203 | requirements.each do |req| 204 | next if req['optional'] 205 | next if req['external'] 206 | 207 | name = req['name'] 208 | groups = req['groups'] || [] 209 | 210 | version = gemify_version(req['version']) 211 | 212 | if groups.empty? or groups.include?('runtime') 213 | # populate runtime dependencies 214 | if gemspec.respond_to?(:add_runtime_dependency) 215 | gemspec.add_runtime_dependency(name,*version) 216 | else 217 | gemspec.add_dependency(name,*version) 218 | end 219 | else 220 | # populate development dependencies 221 | if gemspec.respond_to?(:add_development_dependency) 222 | gemspec.add_development_dependency(name,*version) 223 | else 224 | gemspec.add_dependency(name,*version) 225 | end 226 | end 227 | end 228 | 229 | # convert external dependencies into gemspec requirements 230 | requirements.each do |req| 231 | next unless req['external'] 232 | gemspec.requirements << ("%s-%s" % req.values_at('name', 'version')) 233 | end 234 | 235 | gemspec.homepage = homepage 236 | gemspec.require_paths = require_paths 237 | gemspec.post_install_message = metadata['install_message'] 238 | end 239 | 240 | # 241 | # Set gemspec settings that require a root directory path. 242 | # 243 | def to_gemspec_paths(gemspec) 244 | gemspec.files = files 245 | gemspec.extensions = extensions 246 | gemspec.executables = executables 247 | 248 | if Gem::VERSION < '1.7.' 249 | gemspec.default_executable = gemspec.executables.first 250 | end 251 | 252 | gemspec.test_files = glob_files(patterns[:test]) 253 | 254 | unless gemspec.files.include?('.document') 255 | gemspec.extra_rdoc_files = glob_files(patterns[:doc]) 256 | end 257 | end 258 | 259 | # 260 | # Return a copy of this file. This is used to generate a local 261 | # .gemspec file that can automatically read the index file. 262 | # 263 | def self.source_code 264 | File.read(__FILE__) 265 | end 266 | 267 | private 268 | 269 | def find_root 270 | root_files = patterns[:root] 271 | if Dir.glob(root_files).first 272 | Pathname.new(Dir.pwd) 273 | elsif Dir.glob("../#{root_files}").first 274 | Pathname.new(Dir.pwd).parent 275 | else 276 | #raise "Can't find root of project containing `#{root_files}'." 277 | warn "Can't find root of project containing `#{root_files}'." 278 | nil 279 | end 280 | end 281 | 282 | def glob(pattern) 283 | if File.directory?(pattern) 284 | Dir.glob(File.join(pattern, '**', '*')) 285 | else 286 | Dir.glob(pattern) 287 | end 288 | end 289 | 290 | def gemify_version(version) 291 | case version 292 | when /^(.*?)\+$/ 293 | ">= #{$1}" 294 | when /^(.*?)\-$/ 295 | "< #{$1}" 296 | when /^(.*?)\~$/ 297 | "~> #{$1}" 298 | else 299 | version 300 | end 301 | end 302 | 303 | end 304 | 305 | end 306 | 307 | Indexer::GemspecExporter.gemspec -------------------------------------------------------------------------------- /lib/rspec/ontap.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/core/formatters/base_formatter' 2 | 3 | module RSpec 4 | #module Core 5 | #module Formatters 6 | 7 | # 8 | class TapBaseFormatter < Core::Formatters::BaseFormatter 9 | 10 | # TAP-Y/J Revision 11 | REVISION = 4 12 | 13 | # 14 | attr_accessor :example_group_stack 15 | 16 | # 17 | def initialize(output) 18 | super(output) 19 | @example_group_stack = [] 20 | end 21 | 22 | # 23 | # This method is invoked before any examples are run, right after 24 | # they have all been collected. This can be useful for special 25 | # formatters that need to provide progress on feedback (graphical ones) 26 | # 27 | # This will only be invoked once, and the next one to be invoked 28 | # is #example_group_started 29 | # 30 | def start(notification) 31 | # there is a super method for this 32 | super(notification) 33 | 34 | @start_time = Time.now 35 | 36 | doc = { 37 | 'type' => 'suite', 38 | 'start' => @start_time.strftime('%Y-%m-%d %H:%M:%S'), 39 | 'count' => notification.count, 40 | 'seed' => @seed, 41 | 'rev' => REVISION 42 | } 43 | return doc 44 | end 45 | 46 | # 47 | # This method is invoked at the beginning of the execution of each example group. 48 | # +example_group+ is the example_group. 49 | # 50 | # The next method to be invoked after this is +example_passed+, 51 | # +example_pending+, or +example_finished+ 52 | # 53 | def example_group_started(notification) 54 | # there is a super method for this 55 | super(notification) 56 | doc = { 57 | 'type' => 'case', 58 | 'subtype' => 'describe', 59 | 'label' => "#{notification.group.description}", 60 | 'level' => @example_group_stack.size 61 | } 62 | @example_group_stack << example_group 63 | return doc 64 | end 65 | 66 | # This method is invoked at the end of the execution of each example group. 67 | # +example_group+ is the example_group. 68 | def example_group_finished(notification) 69 | #super(notification) 70 | @example_group_stack.pop 71 | end 72 | 73 | # Set up stdout and stderr to be captured. 74 | # 75 | # IMPORTANT: Comment out the `reset_output` line to debug!!!!!!!!! 76 | # 77 | def example_started(notification) 78 | reset_output 79 | end 80 | 81 | # 82 | def example_passed(notification) 83 | #super(notification) 84 | 85 | example = notification.example 86 | 87 | file, line = example.location.split(':') 88 | #file = self.class.relative_path(file) 89 | file = relative_path(file) 90 | line = line.to_i 91 | 92 | doc = { 93 | 'type' => 'test', 94 | 'subtype' => 'it', 95 | 'status' => 'pass', 96 | #'setup': foo instance 97 | 'label' => "#{example.description}", 98 | #'expected' => 2 99 | #'returned' => 2 100 | 'file' => file, 101 | 'line' => line, 102 | 'source' => source(file)[line-1].strip, 103 | 'snippet' => code_snippet(file, line), 104 | #'coverage' => { 105 | # file: lib/foo.rb 106 | # line: 11..13 107 | # code: Foo#* 108 | #} 109 | 'time' => Time.now - @start_time 110 | } 111 | 112 | doc.update(captured_output) 113 | 114 | return doc 115 | end 116 | 117 | # 118 | def example_pending(notification) 119 | #super(notification) 120 | 121 | example = notification.example 122 | 123 | file, line = example.location.split(':') 124 | #file = self.class.relative_path(file) 125 | file = relative_path(file) 126 | line = line.to_i 127 | 128 | doc = { 129 | 'type' => 'test', 130 | 'subtype' => 'it', 131 | 'status' => 'todo', 132 | #'setup': foo instance 133 | 'label' => "#{example.description}", 134 | 'file' => file, 135 | 'line' => line, 136 | 'source' => source(file)[line-1].strip, 137 | 'snippet' => code_snippet(file, line), 138 | #'coverage' => { 139 | # file: lib/foo.rb 140 | # line: 11..13 141 | # code: Foo#* 142 | #} 143 | 'time' => Time.now - @start_time 144 | } 145 | 146 | doc.update(captured_output) 147 | 148 | return doc 149 | end 150 | 151 | # 152 | def example_failed(notification) 153 | #super(notification) 154 | 155 | example = notification.example 156 | 157 | file, line = example.location.split(':') 158 | 159 | file = relative_path(file) 160 | line = line.to_i 161 | 162 | if RSpec::Expectations::ExpectationNotMetError === example.exception 163 | status = 'fail' 164 | if md = /expected:\s*(.*?)\n\s*got:\s*(.*?)\s+/.match(example.exception.to_s) 165 | expected, returned = md[1], md[2] 166 | else 167 | expected, returned = nil, nil 168 | end 169 | else 170 | status = 'error' 171 | end 172 | 173 | backtrace = format_backtrace(example.exception.backtrace, example.metadata) 174 | 175 | efile, eline = parse_source_location(backtrace) 176 | 177 | doc = { 178 | 'type' => 'test', 179 | 'subtype' => 'it', 180 | 'status' => status, 181 | 'label' => "#{example.description}", 182 | #'setup' => "foo instance", 183 | 'file' => file, 184 | 'line' => line, 185 | 'source' => source(file)[line-1].strip, 186 | 'snippet' => code_snippet(file, line), 187 | #'coverage' => 188 | #{ 189 | # 'file' => lib/foo.rb 190 | # 'line' => 11..13 191 | # 'code' => Foo#* 192 | #} 193 | } 194 | 195 | if expected or returned 196 | doc.update( 197 | 'expected' => expected, 198 | 'returned' => returned, 199 | ) 200 | end 201 | 202 | doc.update( 203 | 'exception' => { 204 | 'message' => example.exception.to_s.strip, 205 | 'class' => example.exception.class.name, 206 | 'file' => efile, 207 | 'line' => eline, 208 | 'source' => source(efile)[eline-1].strip, 209 | 'snippet' => code_snippet(efile, eline), 210 | 'backtrace' => backtrace 211 | }, 212 | 'time' => Time.now - @start_time 213 | ) 214 | 215 | doc.update(captured_output) 216 | 217 | return doc 218 | end 219 | 220 | # This method is invoked after the dumping of examples and failures. 221 | def dump_summary(summary_notification) 222 | #super(summary_notification) 223 | 224 | duration = summary_notification.duration 225 | example_count = summary_notification.examples.size 226 | failure_count = summary_notification.failed_examples.size 227 | pending_count = summary_notification.pending_examples.size 228 | 229 | failed_examples = summary_notification.failed_examples 230 | 231 | error_count = 0 232 | 233 | failed_examples.each do |e| 234 | if RSpec::Expectations::ExpectationNotMetError === e.exception 235 | #failure_count += 1 236 | else 237 | failure_count -= 1 238 | error_count += 1 239 | end 240 | end 241 | 242 | passing_count = example_count - failure_count - error_count - pending_count 243 | 244 | doc = { 245 | 'type' => 'final', 246 | 'time' => duration, 247 | 'counts' => { 248 | 'total' => example_count, 249 | 'pass' => passing_count, 250 | 'fail' => failure_count, 251 | 'error' => error_count, 252 | 'omit' => 0, 253 | 'todo' => pending_count 254 | } 255 | } 256 | return doc 257 | end 258 | 259 | # This gets invoked after the summary if option is set to do so. 260 | #def dump_pending 261 | #end 262 | 263 | def seed(notification) 264 | @seed = notification.seed 265 | end 266 | 267 | # Add any messages as notes. 268 | def message(message_notification) 269 | doc = { 270 | 'type' => 'note', 271 | 'text' => message_notification.message 272 | } 273 | return doc 274 | end 275 | 276 | # 277 | # NOTE: None of the following are being used. If ever added, be sure 278 | # to activate in register calls below. 279 | # 280 | 281 | # (not used) 282 | def stop(examples_notification) 283 | super(examples_notification) 284 | end 285 | 286 | # (not used) 287 | def start_dump(null_notification) 288 | end 289 | 290 | # (not used) 291 | def dump_pending(examples_notification) 292 | end 293 | 294 | # (not used) 295 | def dump_failures(examples_notification) 296 | end 297 | 298 | # (not used) 299 | def close(null_notification) 300 | # there is a super method for this 301 | super(null_notification) 302 | end 303 | 304 | private 305 | 306 | # Returns a String of source code. 307 | def code_snippet(file, line) 308 | s = [] 309 | if File.file?(file) 310 | source = source(file) 311 | radius = 2 # TODO: make customizable (number of surrounding lines to show) 312 | region = [line - radius, 1].max .. 313 | [line + radius, source.length].min 314 | 315 | s = region.map do |n| 316 | {n => source[n-1].chomp} 317 | end 318 | end 319 | return s 320 | end 321 | 322 | # Cache source file text. This is only used if the TAP-Y stream 323 | # doesn not provide a snippet and the test file is locatable. 324 | def source(file) 325 | @_source_cache ||= {} 326 | @_source_cache[file] ||= ( 327 | File.readlines(file) 328 | ) 329 | end 330 | 331 | # Parse source location from caller, caller[0] or an Exception object. 332 | def parse_source_location(caller) 333 | case caller 334 | when Exception 335 | trace = caller.backtrace #.reject{ |bt| bt =~ INTERNALS } 336 | caller = trace.first 337 | when Array 338 | caller = caller.first 339 | end 340 | caller =~ /(.+?):(\d+(?=:|\z))/ or return "" 341 | source_file, source_line = $1, $2.to_i 342 | return source_file, source_line 343 | end 344 | 345 | # 346 | def reset_output 347 | @_oldout = $stdout 348 | @_olderr = $stderr 349 | 350 | @_newout = StringIO.new 351 | @_newerr = StringIO.new 352 | 353 | $stdout = @_newout 354 | $stderr = @_newerr 355 | end 356 | 357 | # 358 | def captured_output 359 | return unless (@_newout && @_newerr) 360 | 361 | stdout = @_newout.string.chomp("\n") 362 | stderr = @_newerr.string.chomp("\n") 363 | 364 | doc = {} 365 | doc['stdout'] = stdout unless stdout.empty? 366 | doc['stderr'] = stderr unless stderr.empty? 367 | 368 | $stdout = @_oldout 369 | $stderr = @_olderr 370 | 371 | return doc 372 | end 373 | 374 | # 375 | def capture_io 376 | ostdout, ostderr = $stdout, $stderr 377 | cstdout, cstderr = StringIO.new, StringIO.new 378 | $stdout, $stderr = cstdout, cstderr 379 | 380 | yield 381 | 382 | return cstdout.string.chomp("\n"), cstderr.string.chomp("\n") 383 | ensure 384 | $stdout = ostdout 385 | $stderr = ostderr 386 | end 387 | 388 | # 389 | def relative_path_regex 390 | @relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/ 391 | end 392 | 393 | # Get relative path of file. 394 | # 395 | # line - current code line [String] 396 | # 397 | # Returns relative path to line. [String] 398 | def relative_path(line) 399 | line = line.sub(relative_path_regex, "\\1.\\2".freeze) 400 | line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze) 401 | return nil if line == '-e:1'.freeze 402 | line 403 | rescue SecurityError 404 | nil 405 | end 406 | 407 | # 408 | def format_backtrace(*args) 409 | backtrace_formatter.format_backtrace(*args) 410 | end 411 | 412 | # 413 | def backtrace_formatter 414 | RSpec.configuration.backtrace_formatter 415 | end 416 | 417 | end 418 | 419 | # 420 | class TapY < TapBaseFormatter 421 | ::RSpec::Core::Formatters.register self, 422 | :start, 423 | :example_group_started, 424 | :example_group_finished, 425 | :example_started, 426 | :example_passed, 427 | :example_failed, 428 | :example_pending, 429 | :dump_summary, 430 | :seed, 431 | :message, 432 | #:stop, 433 | #:start_dump, 434 | #:dump_pending, 435 | #:dump_failures, 436 | :close 437 | 438 | def initialize(*args) 439 | require 'yaml' 440 | super(*args) 441 | end 442 | def start(*args) 443 | output.puts super(*args).to_yaml 444 | end 445 | def example_group_started(*args) 446 | output.puts super(*args).to_yaml 447 | end 448 | def example_passed(*args) 449 | output.puts super(*args).to_yaml 450 | end 451 | def example_pending(*args) 452 | output.puts super(*args).to_yaml 453 | end 454 | def example_failed(*args) 455 | output.puts super(*args).to_yaml 456 | end 457 | #def dump_summary(duration, example_count, failure_count, pending_count) 458 | # output.puts super(duration, example_count, failure_count, pending_count).to_yaml 459 | # output.puts "..." 460 | #end 461 | def dump_summary(*args) 462 | output.puts super(*args).to_yaml 463 | output.puts "..." 464 | end 465 | end 466 | 467 | #rspec -f RSpec::TapY spec/*.rb | tapout progress 468 | class TapJ < TapBaseFormatter 469 | ::RSpec::Core::Formatters.register self, 470 | :start, 471 | :example_group_started, 472 | :example_group_finished, 473 | :example_started, 474 | :example_passed, 475 | :example_failed, 476 | :example_pending, 477 | :dump_summary, 478 | :seed, 479 | :message, 480 | #:stop, 481 | #:start_dump, 482 | #:dump_pending, 483 | #:dump_failures, 484 | :close 485 | 486 | def initialize(*args) 487 | require 'json' 488 | super(*args) 489 | end 490 | def start(*args) 491 | output.puts super(*args).to_json 492 | end 493 | def example_group_started(*args) 494 | output.puts super(*args).to_json 495 | end 496 | def example_passed(*args) 497 | output.puts super(*args).to_json 498 | end 499 | def example_pending(*args) 500 | output.puts super(*args).to_json 501 | end 502 | def example_failed(*args) 503 | output.puts super(*args).to_json 504 | end 505 | def dump_summary(*args) 506 | output.puts super(*args).to_json 507 | end 508 | end 509 | 510 | #end 511 | #end 512 | end 513 | --------------------------------------------------------------------------------