rspec-live 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.md ADDED
@@ -0,0 +1,7 @@
1
+ ### Version 0.0.1
2
+ 2014-8-12
3
+
4
+ * Watch for file changes and rerun all specs
5
+ * Concise backtraces with configurable verbosity level
6
+ * Rotate list of failures to show different specs at the top
7
+ * Fullscreen Curses output with color and resizing detection
data/License.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Brian Auton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # rspec-live
2
+
3
+ rspec-live is a test runner and formatter for RSpec 3+ that shows the state of your test suite
4
+ clearly and concisely in a console window. As files in your project are updated, created, or
5
+ removed, rspec-live reruns your tests in the background and continually updates the displayed
6
+ status.
7
+
8
+ ### Requirements
9
+
10
+ * Ruby 1.8.7 or newer
11
+ * RSpec 3.0.0 or newer
12
+
13
+ ### Getting Started
14
+
15
+ Add the gem to your Gemfile,
16
+
17
+ gem 'rspec-live'
18
+
19
+ Update your bundle,
20
+
21
+ bundle
22
+
23
+ And start it up.
24
+
25
+ rspec-live
data/bin/rspec-live ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ require "rspec-live/suite"
3
+ require "rspec-live/runner"
4
+ require "rspec-live/watcher"
5
+ require "rspec-live/display"
6
+
7
+ display = RSpecLive::Display.new
8
+ suite = RSpecLive::Suite.new(RSpecLive::Runner.new, display.suite_display)
9
+ RSpecLive::Watcher.new(suite, display.watcher_display).start
@@ -0,0 +1,13 @@
1
+ require "json"
2
+
3
+ class InventoryFormatter
4
+ RSpec::Core::Formatters.register self, :example_started
5
+
6
+ def initialize(output)
7
+ @output = output
8
+ end
9
+
10
+ def example_started(notification)
11
+ @output << "#{JSON.unparse(name: notification.example.location)}\n"
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ class UpdateFormatter
4
+ RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
5
+
6
+ def initialize(output)
7
+ @output = output
8
+ end
9
+
10
+ def example_passed(notification)
11
+ report :name => notification.example.location, :status => "passed"
12
+ end
13
+
14
+ def example_failed(notification)
15
+ report({
16
+ :name => notification.example.location,
17
+ :status => "failed",
18
+ :backtrace => notification.exception.backtrace,
19
+ :message => notification.exception.message,
20
+ })
21
+ end
22
+
23
+ def example_pending(notification)
24
+ report :name => notification.example.location, :status => "pending"
25
+ end
26
+
27
+ def report(data)
28
+ @output << "#{JSON.unparse data}\n"
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ module RSpecLive
2
+ class Backtrace
3
+ def initialize(data, verbosity)
4
+ @components = data.map { |text| BacktraceComponent.new text, verbosity }
5
+ end
6
+
7
+ def components
8
+ strip_setup collapsed_components.map(&:to_s)
9
+ end
10
+
11
+ private
12
+
13
+ def strip_setup(text_components)
14
+ list = text_components.dup
15
+ while ["gem:ruby", "gem:rspec-core"].include? list.last do
16
+ list = list[0, list.length-1]
17
+ end
18
+ list
19
+ end
20
+
21
+ def collapsed_components
22
+ @components.inject([]) do |group, component|
23
+ group.dup.tap do |new_group|
24
+ new_group << component unless group.last && (group.last.to_s == component.to_s)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class BacktraceComponent
31
+ def initialize(text, verbosity)
32
+ @file, @line, @method = text.split(":")
33
+ @verbosity = verbosity
34
+ end
35
+
36
+ def to_s
37
+ local_file_reference || gem_reference || "other"
38
+ end
39
+
40
+ private
41
+
42
+ def local_file_reference
43
+ if @file.start_with? Dir.pwd
44
+ ref = "#{@file.gsub(/^#{Dir.pwd}\//, "")}:#{@line}"
45
+ ref += ":#{cleaned_method}" if @verbosity > 1
46
+ ref
47
+ end
48
+ end
49
+
50
+ def cleaned_method
51
+ @method.gsub(/^in `/, "").gsub(/'$/, "")
52
+ end
53
+
54
+ def gem_reference
55
+ if @file.include? "/gems/"
56
+ local_reference = @file.split("/gems/").last
57
+ path = local_reference.gsub(/^\/*/, "")
58
+ gem_name_parts = local_reference.split("/").first.split("-")
59
+ gem_name = gem_name_parts[0, gem_name_parts.length - 1].join("-")
60
+ ref = "gem:#{gem_name}"
61
+ ref += "/#{path}:#{@line}:#{cleaned_method}" if @verbosity > 2
62
+ ref
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,68 @@
1
+ require "rspec-live/terminal"
2
+
3
+ module RSpecLive
4
+ class Display
5
+ attr_reader :watcher_display, :suite_display
6
+
7
+ def initialize
8
+ @terminal = Terminal.new
9
+ @watcher_display = WatcherDisplay.new(@terminal.add_section :xalign => :center)
10
+ @terminal.add_section :display => :block, :content => key_command_info, :color => :blue
11
+ @suite_display = SuiteDisplay.new(@terminal.add_section :display => :block)
12
+ end
13
+
14
+ private
15
+
16
+ def key_command_info
17
+ "Keys: A:show/hide-all N:next V:verbosity R:rerun Q:quit"
18
+ end
19
+ end
20
+
21
+ class WatcherDisplay
22
+ def initialize(section)
23
+ @section = section
24
+ end
25
+
26
+ def status=(status)
27
+ @section.content = "RSpec summary for #{File.basename Dir.pwd} (#{status})"
28
+ @section.refresh
29
+ end
30
+ end
31
+
32
+ class SuiteDisplay
33
+ def initialize(section)
34
+ @section = section
35
+ end
36
+
37
+ def show_examples(examples, suite_status, detailed_examples, verbosity)
38
+ @section.clear
39
+ @section.add_section :display => :block
40
+ examples.map(&:status).each do |status|
41
+ @section.add_section :content => character[status], :color => color[status]
42
+ end
43
+ @section.add_section :display => :block
44
+ @section.add_section :content => "#{suite_status}", :display => :block
45
+ @section.add_section :display => :block
46
+ last_failed = true
47
+ bullet_width = (detailed_examples.length-1).to_s.length
48
+ detailed_examples.each_with_index do |example, index|
49
+ bullet = "#{index+1}.".rjust(bullet_width+1, " ")
50
+ @section.add_section :display => :block if (!last_failed && example.failed?)
51
+ @section.add_section :content => example.details(verbosity), :display => :block, :color => color[example.status], :wrap => true, :bullet => bullet
52
+ @section.add_section :display => :block if example.failed?
53
+ last_failed = example.failed?
54
+ end
55
+ @section.refresh
56
+ end
57
+
58
+ private
59
+
60
+ def character
61
+ {:unknown => ".", :passed => ".", :failed => "F", :pending => "S"}
62
+ end
63
+
64
+ def color
65
+ {:unknown => :blue, :passed => :green, :failed => :red, :pending => :yellow}
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ require "rspec-live/backtrace"
2
+
3
+ module RSpecLive
4
+ class Example
5
+ attr_reader :name, :status
6
+
7
+ def initialize
8
+ @name = ""
9
+ @status = :unknown
10
+ @backtrace = []
11
+ end
12
+
13
+ def update(data)
14
+ @name = data["name"] if data["name"]
15
+ @status = data["status"].to_sym if data["status"]
16
+ @backtrace = data["backtrace"] if data["backtrace"]
17
+ @message = data["message"] if data["message"]
18
+ end
19
+
20
+ def status=(value)
21
+ @status = value.to_sym
22
+ end
23
+
24
+ def passed?
25
+ @status == :passed
26
+ end
27
+
28
+ def failed?
29
+ @status == :failed
30
+ end
31
+
32
+ def details(verbosity)
33
+ failed? ? failure_message(verbosity) : name_component
34
+ end
35
+
36
+ def name_component
37
+ "(" + @name.gsub(/^.\//, "") + ")"
38
+ end
39
+
40
+ def failure_message(verbosity)
41
+ ([name_component] + backtrace_components(verbosity) + exception_components).compact.join " -> "
42
+ end
43
+
44
+ def backtrace_components(verbosity)
45
+ return [] if verbosity < 1
46
+ Backtrace.new(@backtrace, verbosity).components.reverse.map { |c| "(#{c})" }
47
+ end
48
+
49
+ def exception_components
50
+ [@message.gsub("\n", " ").strip.inspect]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ require "pty"
2
+ require "json"
3
+
4
+ module RSpecLive
5
+ class Runner
6
+ def inventory(&block)
7
+ run "inventory", "--dry-run", &block
8
+ end
9
+
10
+ def update(&block)
11
+ run "update", &block
12
+ end
13
+
14
+ private
15
+
16
+ def run(formatter, options="", &block)
17
+ PTY.spawn formatter_command(formatter, options) do |stdin, stdout, pid|
18
+ begin
19
+ stdin.each do |line|
20
+ block.call JSON.parse line
21
+ end
22
+ rescue Errno::EIO
23
+ end
24
+ end
25
+ rescue PTY::ChildExited
26
+ end
27
+
28
+ def formatter_command(formatter, options)
29
+ options << " --format #{formatter_class formatter}"
30
+ options << " --require #{formatter_source formatter}"
31
+ "rspec #{options}"
32
+ end
33
+
34
+ def formatter_source(formatter)
35
+ File.join File.dirname(__FILE__), "../formatters/#{formatter}_formatter.rb"
36
+ end
37
+
38
+ def formatter_class(formatter)
39
+ "#{formatter.capitalize}Formatter"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,94 @@
1
+ require "rspec-live/example"
2
+
3
+ module RSpecLive
4
+ class Suite
5
+ def initialize(runner, display)
6
+ @runner = runner
7
+ @display = display
8
+ @example_names = []
9
+ @examples = {}
10
+ @show_all = false
11
+ @verbosity = 1
12
+ end
13
+
14
+ def toggle_all
15
+ @show_all = !@show_all
16
+ update_display
17
+ end
18
+
19
+ def inventory
20
+ @example_names = []
21
+ @runner.inventory do |example_data|
22
+ update_or_create_example example_data
23
+ update_display
24
+ end
25
+ end
26
+
27
+ def update
28
+ @runner.update do |example_data|
29
+ update_or_create_example example_data
30
+ update_display
31
+ end
32
+ end
33
+
34
+ def focus_next
35
+ @focused = detailed_examples[1].name if detailed_examples[1]
36
+ update_display
37
+ end
38
+
39
+ def clear_status
40
+ @examples.each_value { |example| example.status = :unknown }
41
+ end
42
+
43
+ def cycle_verbosity
44
+ @verbosity = (@verbosity + 1) % 4
45
+ update_display
46
+ end
47
+
48
+ private
49
+
50
+ def next_visible(name)
51
+ end
52
+
53
+ def update_or_create_example(data)
54
+ name = data["name"]
55
+ @example_names << name unless @example_names.include? name
56
+ sort_example_names
57
+ @examples[name] ||= Example.new
58
+ @examples[name].update data
59
+ @examples[name]
60
+ end
61
+
62
+ def update_display
63
+ @display.show_examples ordered_examples, summary, detailed_examples, @verbosity
64
+ end
65
+
66
+ def detailed_examples
67
+ all = ordered_examples
68
+ if @focused
69
+ index = @example_names.index(@focused) || 0
70
+ all = all[index, all.length-index] + all[0, index]
71
+ end
72
+ @show_all ? all : all.select(&:failed?)
73
+ end
74
+
75
+ def ordered_examples
76
+ @example_names.map { |name| @examples[name] }
77
+ end
78
+
79
+ def sort_example_names
80
+ @example_names.sort_by! do |name|
81
+ file, line = name.split(":")
82
+ line = line.rjust(8, "0")
83
+ [file, line].join(":")
84
+ end
85
+ end
86
+
87
+ def summary
88
+ passed = ordered_examples.select(&:passed?).length
89
+ total = ordered_examples.length
90
+ percent = (100*passed/total.to_f).round
91
+ "#{passed} of #{total} examples passed (#{percent}%)"
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,121 @@
1
+ require "curses"
2
+
3
+ module RSpecLive
4
+ class Terminal
5
+ def initialize
6
+ Terminal.reset_curses
7
+ @root_section = TerminalSection.new
8
+ Signal.trap("SIGWINCH", proc { Terminal.reset_curses; @root_section.refresh })
9
+ end
10
+
11
+ def self.reset_curses
12
+ Curses.init_screen
13
+ Curses.curs_set 0
14
+ Curses.clear
15
+ Curses.refresh
16
+ Curses.start_color
17
+ Curses.use_default_colors
18
+ available_colors.each do |name|
19
+ Curses.init_pair color_constant(name), color_constant(name), -1
20
+ end
21
+ @width = `tput cols`.to_i
22
+ @height = `tput lines`.to_i
23
+ Curses.resizeterm @height, @width
24
+ end
25
+
26
+ def self.width
27
+ @width
28
+ end
29
+
30
+ def add_section(*args)
31
+ @root_section.add_section(*args)
32
+ end
33
+
34
+ def self.color_constant(name)
35
+ Curses.const_get "COLOR_#{name.to_s.upcase}"
36
+ end
37
+
38
+ def self.available_colors
39
+ [:blue, :green, :red, :yellow, :white]
40
+ end
41
+ end
42
+
43
+ class TerminalSection
44
+ def initialize(parent = nil, options = {})
45
+ @content = ""
46
+ @parent = parent
47
+ options.each_pair { |key, value| instance_variable_set "@#{key}", value }
48
+ @children = []
49
+ end
50
+
51
+ def add_section(options = {})
52
+ new_section = TerminalSection.new(self, options)
53
+ @children << new_section
54
+ new_section
55
+ end
56
+
57
+ def content=(value)
58
+ @content = value.to_s
59
+ end
60
+
61
+ def clear
62
+ @content = ""
63
+ @children = []
64
+ refresh
65
+ end
66
+
67
+ def refresh
68
+ if @parent
69
+ @parent.refresh
70
+ else
71
+ Curses.clear
72
+ Curses.setpos 0, 0
73
+ draw
74
+ Curses.refresh
75
+ end
76
+ end
77
+
78
+ def draw
79
+ Curses.addstr "\n" if @display == :block
80
+ draw_left_margin
81
+ if @color
82
+ Curses.attron(color_attribute @color) { draw_content }
83
+ else
84
+ draw_content
85
+ end
86
+ draw_right_margin
87
+ @children.each(&:draw)
88
+ end
89
+
90
+ def draw_content
91
+ text = @content
92
+ bullet = @bullet ? "#{@bullet} " : ""
93
+ if @display == :block && @wrap
94
+ text = bullet + wrap_with_margin(text, Terminal.width-1, bullet.length)
95
+ end
96
+ Curses.addstr text
97
+ end
98
+
99
+ def wrap_with_margin(text, width, margin_width)
100
+ wrap(text, width - margin_width).split("\n").join("\n" + (" " * margin_width))
101
+ end
102
+
103
+ def wrap(text, width)
104
+ text.scan(/\S.{0,#{width-2}}\S(?=\s|$)|\S+/).join("\n")
105
+ end
106
+
107
+ def draw_left_margin
108
+ Curses.addstr(("=" * [0, (((Terminal.width - @content.length) / 2) - 1)].max) + " ") if @align == :center
109
+ end
110
+
111
+ def draw_right_margin
112
+ Curses.addstr(" " + ("=" * [0, (((Terminal.width - @content.length) / 2) - 2)].max)) if @align == :center
113
+ end
114
+
115
+ private
116
+
117
+ def color_attribute(name)
118
+ Curses.color_pair(Terminal.color_constant name) | Curses::A_NORMAL
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,3 @@
1
+ module RSpecLive
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,42 @@
1
+ require "listen"
2
+
3
+ module RSpecLive
4
+ class Watcher
5
+ def initialize(suite, display)
6
+ @suite = suite
7
+ @display = display
8
+ end
9
+
10
+ def start
11
+ process_tests
12
+ Listen.to(Dir.pwd) { process_tests }.start
13
+ while perform_key_command; end
14
+ rescue Interrupt
15
+ end
16
+
17
+ private
18
+
19
+ def perform_key_command
20
+ key = STDIN.getc.chr.downcase
21
+ @suite.toggle_all if key == "a"
22
+ @suite.focus_next if key == "n"
23
+ return false if key == "q"
24
+ reset if key == "r"
25
+ @suite.cycle_verbosity if key == "v"
26
+ true
27
+ end
28
+
29
+ def reset
30
+ @suite.clear_status
31
+ process_tests
32
+ end
33
+
34
+ def process_tests
35
+ @display.status = "analyzing specs"
36
+ @suite.inventory
37
+ @display.status = "running specs"
38
+ @suite.update
39
+ @display.status = "watching for updates..."
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-live
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brian Auton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-08-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: listen
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - "~>"
36
+ - !ruby/object:Gem::Version
37
+ version: 2.7.9
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: 2.7.9
46
+ - !ruby/object:Gem::Dependency
47
+ name: curses
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.1
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.0.1
62
+ description:
63
+ email:
64
+ - brianauton@gmail.com
65
+ executables:
66
+ - rspec-live
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - lib/rspec-live/terminal.rb
71
+ - lib/rspec-live/example.rb
72
+ - lib/rspec-live/runner.rb
73
+ - lib/rspec-live/version.rb
74
+ - lib/rspec-live/suite.rb
75
+ - lib/rspec-live/watcher.rb
76
+ - lib/rspec-live/backtrace.rb
77
+ - lib/rspec-live/display.rb
78
+ - lib/formatters/inventory_formatter.rb
79
+ - lib/formatters/update_formatter.rb
80
+ - README.md
81
+ - History.md
82
+ - License.txt
83
+ - bin/rspec-live
84
+ homepage: http://github.com/brianauton/rspec-live
85
+ licenses:
86
+ - MIT
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.3.6
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 1.8.29
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Continually updating console output for RSpec 3+
109
+ test_files: []