├── .rspec ├── .travis.yml ├── lib └── rspec │ ├── longrun │ ├── version.rb │ ├── dsl.rb │ └── formatter.rb │ └── longrun.rb ├── Gemfile ├── .gitignore ├── examples ├── nested_spec.rb ├── status_spec.rb ├── stepped_spec.rb └── example_spec.rb ├── Rakefile ├── rspec-longrun.gemspec ├── LICENSE ├── README.md └── spec └── rspec └── longrun └── formatter_spec.rb /.rspec: -------------------------------------------------------------------------------- 1 | -r rspec/longrun -f RSpec::Longrun::Formatter --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | env: 3 | - RSPEC_VERSION="~> 3.7.0" 4 | -------------------------------------------------------------------------------- /lib/rspec/longrun/version.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module Longrun 3 | VERSION = "3.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "rspec", ENV["RSPEC_VERSION"] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | coverage 5 | doc/ 6 | pkg 7 | rdoc 8 | spec/reports 9 | tmp 10 | -------------------------------------------------------------------------------- /lib/rspec/longrun.rb: -------------------------------------------------------------------------------- 1 | require "rspec/longrun/dsl" 2 | require "rspec/longrun/formatter" 3 | require "rspec/longrun/version" 4 | 5 | -------------------------------------------------------------------------------- /examples/nested_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/longrun' 3 | 4 | RSpec.describe("foo") do 5 | describe "bar" do 6 | describe "baz" do 7 | it "bleeds" 8 | end 9 | end 10 | describe "qux" do 11 | it "hurts" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) do |t| 7 | t.pattern = 'spec/**/*_spec.rb' 8 | t.rspec_opts = ["--colour", "--format", "documentation"] 9 | end 10 | 11 | task :default => :spec 12 | -------------------------------------------------------------------------------- /examples/status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/longrun' 3 | 4 | RSpec.describe("suite") do 5 | example "works" do; end 6 | example "is unimplemented" do 7 | pending "implement me" 8 | raise "not implemented" 9 | end 10 | example "fails" do 11 | fail "no worky" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /examples/stepped_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/longrun' 3 | 4 | RSpec.describe("stepped") do 5 | 6 | include RSpec::Longrun::DSL 7 | 8 | example "has steps" do 9 | step "Collect underpants" do 10 | end 11 | step "Er ..." do 12 | step "(thinking)" do 13 | end 14 | end 15 | step "Profit!" do 16 | pending "a real plan" 17 | fail 18 | end 19 | end 20 | 21 | example "deep fail" do 22 | step "Level 1" do 23 | step "Level 2" do 24 | step "Level 3" do 25 | expect(1+1).to eq(3) 26 | end 27 | end 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /rspec-longrun.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/rspec/longrun/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Mike Williams"] 6 | gem.email = ["mdub@dogbiscuit.org"] 7 | gem.summary = "An RSpec formatter for long-running specs." 8 | gem.homepage = "http://github.com/mdub/rspec-longrun" 9 | 10 | gem.files = `git ls-files`.split($\) 11 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 12 | gem.name = "rspec-longrun" 13 | gem.require_paths = ["lib"] 14 | gem.version = RSpec::Longrun::VERSION 15 | 16 | gem.add_runtime_dependency("rspec-core", ">= 3.5.0", "< 4") 17 | 18 | end 19 | -------------------------------------------------------------------------------- /examples/example_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'rspec/longrun' 3 | 4 | describe "Underpants gnomes" do 5 | 6 | include RSpec::Longrun::DSL 7 | 8 | it "The plan" do 9 | step "Collect underpants" do 10 | end 11 | step "Umm ..." do 12 | sleep 0.3 # thinking 13 | end 14 | step "Profit!" do 15 | skip "need a real business plan" 16 | end 17 | end 18 | 19 | end 20 | 21 | describe "Killer app" do 22 | 23 | include RSpec::Longrun::DSL 24 | 25 | describe "Some feature" do 26 | 27 | it "Scenario 1" do 28 | step "Step 1" do 29 | end 30 | step "Step 2" do 31 | end 32 | step "Step 3" do 33 | end 34 | end 35 | 36 | it "Scenario 2" do 37 | end 38 | 39 | end 40 | 41 | describe "Another feature" do 42 | 43 | it "Scenario" do 44 | step "Step 1" do 45 | end 46 | step "Step 2" do 47 | end 48 | end 49 | 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /lib/rspec/longrun/dsl.rb: -------------------------------------------------------------------------------- 1 | module RSpec 2 | module Longrun 3 | module DSL 4 | 5 | def step(description) 6 | rspec_longrun_formatter.step_started(description) 7 | ok = false 8 | begin 9 | yield if block_given? 10 | rspec_longrun_formatter.step_finished 11 | ok = true 12 | ensure 13 | rspec_longrun_formatter.step_failed unless ok 14 | end 15 | end 16 | 17 | def xstep(description) 18 | rspec_longrun_formatter.step_started(description) 19 | rspec_longrun_formatter.step_pending 20 | end 21 | 22 | private 23 | 24 | def rspec_longrun_formatter 25 | Thread.current["rspec.longrun.formatter"] || NullStepFormatter.new 26 | end 27 | 28 | class NullStepFormatter 29 | 30 | def step_started(_description) 31 | end 32 | 33 | def step_finished 34 | end 35 | 36 | def step_failed 37 | end 38 | 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mike Williams 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSpec::Longrun 2 | 3 | [![Gem Version](https://badge.fury.io/rb/rspec-longrun.png)](http://badge.fury.io/rb/rspec-longrun) 4 | [![Build Status](https://secure.travis-ci.org/mdub/rspec-longrun.png?branch=master)](http://travis-ci.org/mdub/rspec-longrun) 5 | 6 | RSpec is a fine unit-testing framework, but is also handy for acceptance and integration tests. But the default report formatters make it difficult to track progress of such long-running tests. 7 | 8 | The RSpec::Longrun::Formatter outputs the name of each test as it starts, rather than waiting until it passes or fails. It also provides a mechanism for reporting on progress of a test while it is still executing. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | gem 'rspec-longrun' 15 | 16 | In a Rails project, you can safely limit it to the "test" group. 17 | 18 | ## Usage 19 | 20 | ### Running tests 21 | 22 | Specify the custom output format when invoking RSpec, as follows: 23 | 24 | rspec -r rspec/longrun -f RSpec::Longrun::Formatter spec ... 25 | 26 | The resulting test output looks something like: 27 | 28 | Example group { 29 | First example OK (1.2s) 30 | Second example OK (3.4s) 31 | Third example PENDING (Not implemented yet) (0.2s) 32 | } (5.2s) 33 | 34 | (though a little more colourful). 35 | 36 | ### Tracking progress 37 | 38 | Include RSpec::Longrun::DSL to define the 'step' method, which can be used to group blocks of code within the context of a large test. For example: 39 | 40 | ```ruby 41 | describe "Account management" do 42 | 43 | include RSpec::Longrun::DSL # <-- important 44 | 45 | example "Log in and alter preferences" do 46 | 47 | step "Log in" do 48 | ui.go_home 49 | ui.authenticate_as "joe", "fnord" 50 | end 51 | 52 | step "Navigate to preferences page" do 53 | ui.nav.prefs_link.click 54 | end 55 | 56 | step "Change preferences" do 57 | ui.prefs_pane.enter_defaults 58 | ui.prefs_pane.save 59 | end 60 | 61 | end 62 | 63 | end 64 | ``` 65 | 66 | The resulting test output looks something like: 67 | 68 | Account management { 69 | Log in and alter preferences { 70 | Log in (0.5s) 71 | Navigate to preferences page (0.2s) 72 | Change preferences (5.2s) 73 | } OK (7.1s) 74 | } OK (7.2s) 75 | 76 | which gives you some extra context in the event that something fails, or hangs, during the test run. 77 | 78 | ## Contributing 79 | 80 | rspec-longrun is on Github. You know what to do. 81 | -------------------------------------------------------------------------------- /lib/rspec/longrun/formatter.rb: -------------------------------------------------------------------------------- 1 | require "rspec/core/formatters/base_text_formatter" 2 | 3 | module RSpec 4 | module Longrun 5 | 6 | class Formatter < RSpec::Core::Formatters::BaseTextFormatter 7 | 8 | RSpec::Core::Formatters.register self, 9 | :start, 10 | :example_group_started, :example_group_finished, 11 | :example_started, :example_passed, 12 | :example_pending, :example_failed 13 | 14 | def initialize(output) 15 | super(output) 16 | @blocks = [Block.new(true)] 17 | end 18 | 19 | def start(notification) 20 | Thread.current["rspec.longrun.formatter"] = self 21 | end 22 | 23 | def example_group_started(notification) 24 | begin_block(notification.group.description) 25 | end 26 | 27 | def example_group_finished(notification) 28 | end_block 29 | end 30 | 31 | def example_started(notification) 32 | begin_block(wrap(notification.example.description, :detail)) 33 | end 34 | 35 | def example_passed(notification) 36 | end_block(wrap("OK", :success)) 37 | end 38 | 39 | def example_pending(notification) 40 | end_block(wrap("PENDING: " + notification.example.execution_result.pending_message, :pending)) 41 | end 42 | 43 | def example_failed(notification) 44 | end_block(wrap("FAILED", :failure)) 45 | end 46 | 47 | def step_started(description) 48 | begin_block(description) 49 | end 50 | 51 | def step_finished 52 | end_block(wrap("✓", :success)) 53 | end 54 | 55 | def step_failed 56 | end_block(wrap("✗", :failure)) 57 | end 58 | 59 | def step_pending 60 | end_block(wrap("PENDING", :pending)) 61 | end 62 | 63 | private 64 | 65 | def wrap(*args) 66 | RSpec::Core::Formatters::ConsoleCodes.wrap(*args) 67 | end 68 | 69 | def current_block 70 | @blocks.last 71 | end 72 | 73 | def begin_block(message) 74 | unless current_block.nested? 75 | output << faint("{\n") 76 | current_block.nested! 77 | end 78 | output << current_indentation 79 | output << message.strip 80 | output << ' ' 81 | @blocks.push(Block.new) 82 | end 83 | 84 | def end_block(message = nil) 85 | block = @blocks.pop 86 | block.finished! 87 | if block.nested? 88 | output << current_indentation 89 | output << faint('} ') 90 | end 91 | if message 92 | output << message.strip 93 | output << ' ' 94 | end 95 | output << faint('(' + block.timing + ')') 96 | output << "\n" 97 | end 98 | 99 | def current_indentation 100 | ' ' * (@blocks.size - 1) 101 | end 102 | 103 | def faint(text) 104 | return text unless RSpec.configuration.color_enabled? 105 | "\e[2m#{text}\e[0m" 106 | end 107 | 108 | class Block 109 | 110 | def initialize(nested = false) 111 | @began_at = Time.now 112 | @nested = nested 113 | end 114 | 115 | def finished! 116 | @finished_at = Time.now 117 | end 118 | 119 | def timing 120 | delta = @finished_at - @began_at 121 | '%.2fs' % delta 122 | end 123 | 124 | def nested! 125 | @nested = true 126 | end 127 | 128 | def nested? 129 | @nested 130 | end 131 | 132 | end 133 | 134 | end 135 | 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/rspec/longrun/formatter_spec.rb: -------------------------------------------------------------------------------- 1 | require "rspec/longrun" 2 | require "stringio" 3 | 4 | describe RSpec::Longrun::Formatter do 5 | 6 | let(:output_buffer) { StringIO.new } 7 | 8 | def output 9 | output_buffer.string 10 | end 11 | 12 | def normalized_output 13 | output.gsub(/0\.\d\ds/, "N.NNs") 14 | end 15 | 16 | let(:formatter) { described_class.new(output_buffer) } 17 | 18 | before do 19 | allow(RSpec.configuration).to receive(:color_enabled?).and_return(false) 20 | end 21 | 22 | def example_group(desc) 23 | notification = double(group: double(description: desc)) 24 | formatter.example_group_started(notification) 25 | yield if block_given? 26 | formatter.example_group_finished(notification) 27 | end 28 | 29 | def example(desc, result, pending_message = nil) 30 | notification = double( 31 | example: double( 32 | description: desc, 33 | execution_result: double(pending_message: pending_message) 34 | ) 35 | ) 36 | formatter.example_started(notification) 37 | yield if block_given? 38 | formatter.public_send("example_#{result}", notification) 39 | end 40 | 41 | def step(desc) 42 | formatter.step_started(desc) 43 | yield if block_given? 44 | formatter.step_finished 45 | end 46 | 47 | def xstep(desc) 48 | formatter.step_started(desc) 49 | formatter.step_pending 50 | end 51 | 52 | context "given an empty example group" do 53 | 54 | before do 55 | example_group "suite" do 56 | end 57 | end 58 | 59 | it "outputs suite entry" do 60 | expect(normalized_output).to eql(<<~EOF) 61 | suite (N.NNs) 62 | EOF 63 | end 64 | 65 | end 66 | 67 | context "with examples" do 68 | 69 | before do 70 | example_group "suite" do 71 | example "works", :passed 72 | example "fails", :failed 73 | example "is unimplemented", :pending, "implement me" 74 | end 75 | end 76 | 77 | it "outputs example names and status" do 78 | expect(normalized_output).to eql(<<~EOF) 79 | suite { 80 | works OK (N.NNs) 81 | fails FAILED (N.NNs) 82 | is unimplemented PENDING: implement me (N.NNs) 83 | } (N.NNs) 84 | EOF 85 | end 86 | 87 | end 88 | 89 | context "with nested example groups" do 90 | 91 | before do 92 | example_group "top" do 93 | example_group "A" do 94 | end 95 | example_group "B" do 96 | example_group "1" do 97 | end 98 | end 99 | end 100 | end 101 | 102 | it "outputs nested group names" do 103 | expect(normalized_output).to eql(<<~EOF) 104 | top { 105 | A (N.NNs) 106 | B { 107 | 1 (N.NNs) 108 | } (N.NNs) 109 | } (N.NNs) 110 | EOF 111 | end 112 | 113 | end 114 | 115 | context "with steps" do 116 | 117 | before do 118 | example_group "suite" do 119 | example "has steps", :passed do 120 | step "Collect underpants" do 121 | end 122 | step "Er ..." do 123 | step "(thinking)" do 124 | end 125 | end 126 | step "Profit!" 127 | end 128 | end 129 | end 130 | 131 | it "outputs steps" do 132 | expect(normalized_output).to eql(<<~EOF) 133 | suite { 134 | has steps { 135 | Collect underpants ✓ (N.NNs) 136 | Er ... { 137 | (thinking) ✓ (N.NNs) 138 | } ✓ (N.NNs) 139 | Profit! ✓ (N.NNs) 140 | } OK (N.NNs) 141 | } (N.NNs) 142 | EOF 143 | end 144 | 145 | end 146 | 147 | context 'with xsteps' do 148 | 149 | before do 150 | example_group "suite" do 151 | example "has xsteps", :passed do 152 | xstep "Collect underpants" do 153 | end 154 | end 155 | end 156 | end 157 | 158 | it "outputs steps" do 159 | expect(normalized_output).to eql(<<~EOF) 160 | suite { 161 | has xsteps { 162 | Collect underpants PENDING (N.NNs) 163 | } OK (N.NNs) 164 | } (N.NNs) 165 | EOF 166 | end 167 | 168 | end 169 | 170 | end 171 | --------------------------------------------------------------------------------