rspec-live 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/History.md +7 -0
- data/README.md +23 -1
- data/bin/rspec-live +10 -4
- data/lib/formatters/coverage_analyzer.rb +17 -0
- data/lib/formatters/inventory_formatter.rb +1 -1
- data/lib/formatters/update_formatter.rb +20 -8
- data/lib/rspec-live/concurrent_process.rb +48 -0
- data/lib/rspec-live/display.rb +38 -41
- data/lib/rspec-live/example.rb +19 -0
- data/lib/rspec-live/file_watcher.rb +30 -0
- data/lib/rspec-live/formatted_text.rb +24 -0
- data/lib/rspec-live/key_handler.rb +36 -0
- data/lib/rspec-live/log.rb +7 -0
- data/lib/rspec-live/monitor.rb +39 -0
- data/lib/rspec-live/result_detail.rb +32 -0
- data/lib/rspec-live/runner.rb +34 -14
- data/lib/rspec-live/screen.rb +64 -0
- data/lib/rspec-live/suite.rb +50 -61
- data/lib/rspec-live/version.rb +1 -1
- metadata +25 -26
- data/lib/rspec-live/terminal.rb +0 -121
- data/lib/rspec-live/watcher.rb +0 -42
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dfd5934b1feefe47f06e7a318dbf945b4ffdce28
|
4
|
+
data.tar.gz: 53366efac24205bc76f277a2fd60229850a3b252
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 75a233e27e16b972e8e822e8322e3c7b87a5f0bfb42af40e24221e34457492b65910bcd52b95d0861878d1686ca04d76d78204015b311fe204aa5eecc2528deb
|
7
|
+
data.tar.gz: 7650642092fb1d3a46004bfbf7c8061ecb1b370569120ddfcc66f4a4e6bc7e9b72449546bfb15b43daea8246b45cb45ae475d3907e99b7f640fca8769474de07
|
data/History.md
CHANGED
data/README.md
CHANGED
@@ -2,9 +2,17 @@
|
|
2
2
|
|
3
3
|
rspec-live is a test runner and formatter for RSpec 3+ that shows the state of your test suite
|
4
4
|
clearly and concisely in a console window. As files in your project are updated, created, or
|
5
|
-
removed, rspec-live reruns your
|
5
|
+
removed, rspec-live reruns your specs in the background and continually updates the displayed
|
6
6
|
status.
|
7
7
|
|
8
|
+
Basic code coverage analysis is used to determine which specs to re-run based on which files
|
9
|
+
have changed. Currently, specs that run code in a separate process may not get re-run as often
|
10
|
+
as necessary (this includes acceptance tests that launch a JavaScript-capable browser). To see
|
11
|
+
updated results for these specs, you may need to re-run all specs by pressing "R".
|
12
|
+
|
13
|
+
RSpec background processes are managed intelligently, so that only one RSpec process is
|
14
|
+
running at a time, and future RSpec runs won't get queued up based on outdated file statuses.
|
15
|
+
|
8
16
|
### Requirements
|
9
17
|
|
10
18
|
* Ruby 1.8.7 or newer
|
@@ -23,3 +31,17 @@ Update your bundle,
|
|
23
31
|
And start it up.
|
24
32
|
|
25
33
|
rspec-live
|
34
|
+
|
35
|
+
### Key Commands
|
36
|
+
|
37
|
+
- **A** (all): Toggle the display of all specs (as opposed to just failing specs) in the
|
38
|
+
detailed list at the bottom of the screen.
|
39
|
+
|
40
|
+
- **R** (refresh): Re-run all specs.
|
41
|
+
|
42
|
+
- **N** (next): Reorder the detailed information about failing specs shown at the bottom of
|
43
|
+
the screen, by rotating the next spec into the top position.
|
44
|
+
|
45
|
+
- **V** (verbosity): Change the amount of backtrace information shown about failing specs.
|
46
|
+
|
47
|
+
- **Q** (quit)
|
data/bin/rspec-live
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require "rspec-live/suite"
|
3
3
|
require "rspec-live/runner"
|
4
|
-
require "rspec-live/
|
4
|
+
require "rspec-live/monitor"
|
5
5
|
require "rspec-live/display"
|
6
|
+
require "rspec-live/result_detail"
|
7
|
+
require "rspec-live/file_watcher"
|
8
|
+
require "rspec-live/log"
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
RSpecLive::
|
10
|
+
runner = RSpecLive::Runner.new
|
11
|
+
file_watcher = RSpecLive::FileWatcher.new(Dir.pwd)
|
12
|
+
suite = RSpecLive::Suite.new(runner, file_watcher)
|
13
|
+
detail = RSpecLive::ResultDetail.new(suite)
|
14
|
+
display = RSpecLive::Display.new(suite, detail)
|
15
|
+
RSpecLive::Monitor.new(suite, display, detail).start
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CoverageAnalyzer
|
2
|
+
def self.install
|
3
|
+
RSpec.configure do |c|
|
4
|
+
c.around(:example) do |example|
|
5
|
+
CoverageAnalyzer.run_example_with_coverage example
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.run_example_with_coverage(example)
|
11
|
+
example.metadata[:files_touched] = {}
|
12
|
+
set_trace_func proc { |event, file| example.metadata[:files_touched][file] = true }
|
13
|
+
example.run
|
14
|
+
ensure
|
15
|
+
set_trace_func nil
|
16
|
+
end
|
17
|
+
end
|
@@ -1,30 +1,42 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "coverage_analyzer")
|
1
2
|
require "json"
|
2
3
|
|
3
4
|
class UpdateFormatter
|
4
5
|
RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
|
6
|
+
CoverageAnalyzer.install
|
5
7
|
|
6
8
|
def initialize(output)
|
7
9
|
@output = output
|
8
10
|
end
|
9
11
|
|
10
12
|
def example_passed(notification)
|
11
|
-
report
|
13
|
+
report notification.example, "passed"
|
12
14
|
end
|
13
15
|
|
14
16
|
def example_failed(notification)
|
15
|
-
report
|
16
|
-
:name => notification.example.location,
|
17
|
-
:status => "failed",
|
17
|
+
report notification.example, "failed", {
|
18
18
|
:backtrace => notification.exception.backtrace,
|
19
19
|
:message => notification.exception.message,
|
20
|
-
}
|
20
|
+
}
|
21
21
|
end
|
22
22
|
|
23
23
|
def example_pending(notification)
|
24
|
-
report
|
24
|
+
report notification.example, "pending"
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
27
|
+
private
|
28
|
+
|
29
|
+
def report(example, status, options = {})
|
30
|
+
data = {
|
31
|
+
:name => example.location,
|
32
|
+
:status => status,
|
33
|
+
:files_touched => filtered_files_touched(example),
|
34
|
+
}
|
35
|
+
@output << "\n#{JSON.unparse data.merge(options)}\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def filtered_files_touched(example)
|
39
|
+
raw_files = example.metadata[:files_touched].keys
|
40
|
+
raw_files.select { |file| file.start_with? Dir.pwd }
|
29
41
|
end
|
30
42
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "pty"
|
2
|
+
|
3
|
+
module RSpecLive
|
4
|
+
class ConcurrentProcess
|
5
|
+
def initialize(command)
|
6
|
+
@command = command
|
7
|
+
@output = ""
|
8
|
+
end
|
9
|
+
|
10
|
+
def running?
|
11
|
+
@running
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
@started_at = Time.now
|
16
|
+
@stdout, @stdin, @pid = PTY.spawn @command
|
17
|
+
@running = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_line
|
21
|
+
read_characters if running?
|
22
|
+
shift_completed_lines.each_line { |line| yield line }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def shift_completed_lines
|
28
|
+
lines = ""
|
29
|
+
while index = @output.index("\n")
|
30
|
+
lines << @output.slice(0, index + 1)
|
31
|
+
@output = @output.slice(index + 1, @output.length - index - 1)
|
32
|
+
end
|
33
|
+
lines
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_characters
|
37
|
+
while char = @stdout.read_nonblock(100000)
|
38
|
+
@output << char
|
39
|
+
end
|
40
|
+
rescue IO::WaitReadable
|
41
|
+
rescue Errno::EIO
|
42
|
+
@stdout.close
|
43
|
+
@stdin.close
|
44
|
+
@running = false
|
45
|
+
@duration = Time.now - @started_at
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/rspec-live/display.rb
CHANGED
@@ -1,64 +1,61 @@
|
|
1
|
-
require "rspec-live/
|
1
|
+
require "rspec-live/screen"
|
2
2
|
|
3
3
|
module RSpecLive
|
4
4
|
class Display
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@
|
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)
|
5
|
+
def initialize(suite, detail)
|
6
|
+
@suite = suite
|
7
|
+
@detail = detail
|
8
|
+
@screen = Screen.new
|
12
9
|
end
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
def update
|
12
|
+
@screen.start_frame
|
13
|
+
show_header
|
14
|
+
show_summary
|
15
|
+
show_details
|
16
|
+
@screen.end_frame
|
18
17
|
end
|
19
|
-
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
@section = section
|
19
|
+
def update_required?
|
20
|
+
@screen.update_required?
|
24
21
|
end
|
25
22
|
|
26
|
-
|
27
|
-
@section.content = "RSpec summary for #{File.basename Dir.pwd} (#{status})"
|
28
|
-
@section.refresh
|
29
|
-
end
|
30
|
-
end
|
23
|
+
private
|
31
24
|
|
32
|
-
|
33
|
-
|
34
|
-
@
|
25
|
+
def show_header
|
26
|
+
@screen.print "RSpec summary for #{File.basename Dir.pwd} (#{@suite.activity_status})\n"
|
27
|
+
@screen.print "Keys: A:show/hide-all N:next V:verbosity R:rerun Q:quit", :color => :blue
|
35
28
|
end
|
36
29
|
|
37
|
-
def
|
38
|
-
@
|
39
|
-
@
|
40
|
-
|
41
|
-
@section.add_section :content => character[status], :color => color[status]
|
30
|
+
def show_summary
|
31
|
+
@screen.print "\n\n"
|
32
|
+
@suite.ordered_examples.map(&:status).each do |status|
|
33
|
+
@screen.print character[status], :color => color[status]
|
42
34
|
end
|
43
|
-
@
|
44
|
-
@
|
45
|
-
|
35
|
+
@screen.print "\n\n"
|
36
|
+
@screen.print @suite.summary
|
37
|
+
end
|
38
|
+
|
39
|
+
def show_details
|
40
|
+
@screen.print "\n\n"
|
46
41
|
last_failed = true
|
47
|
-
bullet_width = (detailed_examples.length-1).to_s.length
|
48
|
-
detailed_examples.each_with_index do |example, index|
|
42
|
+
bullet_width = (@detail.detailed_examples.length-1).to_s.length
|
43
|
+
@detail.detailed_examples.each_with_index do |example, index|
|
49
44
|
bullet = "#{index+1}.".rjust(bullet_width+1, " ")
|
50
|
-
@
|
51
|
-
@
|
52
|
-
|
45
|
+
@screen.print "\n" if (!last_failed && example.failed?)
|
46
|
+
@screen.print "#{bullet} #{example.details @detail.verbosity}", {
|
47
|
+
:color => color[example.status],
|
48
|
+
:wrap => true,
|
49
|
+
:hanging_indent => (bullet_width + 1)
|
50
|
+
}
|
51
|
+
@screen.print "\n"
|
52
|
+
@screen.print "\n" if example.failed?
|
53
53
|
last_failed = example.failed?
|
54
54
|
end
|
55
|
-
@section.refresh
|
56
55
|
end
|
57
56
|
|
58
|
-
private
|
59
|
-
|
60
57
|
def character
|
61
|
-
{:unknown => "
|
58
|
+
{:unknown => "?", :passed => ".", :failed => "F", :pending => "*"}
|
62
59
|
end
|
63
60
|
|
64
61
|
def color
|
data/lib/rspec-live/example.rb
CHANGED
@@ -15,6 +15,7 @@ module RSpecLive
|
|
15
15
|
@status = data["status"].to_sym if data["status"]
|
16
16
|
@backtrace = data["backtrace"] if data["backtrace"]
|
17
17
|
@message = data["message"] if data["message"]
|
18
|
+
@files_touched = data["files_touched"] || []
|
18
19
|
end
|
19
20
|
|
20
21
|
def status=(value)
|
@@ -29,6 +30,10 @@ module RSpecLive
|
|
29
30
|
@status == :failed
|
30
31
|
end
|
31
32
|
|
33
|
+
def stale?
|
34
|
+
@status == :unknown
|
35
|
+
end
|
36
|
+
|
32
37
|
def details(verbosity)
|
33
38
|
failed? ? failure_message(verbosity) : name_component
|
34
39
|
end
|
@@ -49,5 +54,19 @@ module RSpecLive
|
|
49
54
|
def exception_components
|
50
55
|
[@message.gsub("\n", " ").strip.inspect]
|
51
56
|
end
|
57
|
+
|
58
|
+
def file_touched(name)
|
59
|
+
@status = :unknown if @files_touched.include? name
|
60
|
+
end
|
61
|
+
|
62
|
+
def in_file?(filename)
|
63
|
+
absolute_path == filename
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def absolute_path
|
69
|
+
@name.split(":").first.gsub(/^\./, Dir.pwd)
|
70
|
+
end
|
52
71
|
end
|
53
72
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RSpecLive
|
2
|
+
class FileWatcher
|
3
|
+
def initialize(path)
|
4
|
+
@updated, @added, @removed = [], [], []
|
5
|
+
Listen.to(path) do |updated, added, removed|
|
6
|
+
@updated += updated
|
7
|
+
@added += added
|
8
|
+
@removed += removed
|
9
|
+
end.start
|
10
|
+
end
|
11
|
+
|
12
|
+
def updated
|
13
|
+
queued_results @updated
|
14
|
+
end
|
15
|
+
|
16
|
+
def added
|
17
|
+
queued_results @added
|
18
|
+
end
|
19
|
+
|
20
|
+
def removed
|
21
|
+
queued_results @removed
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def queued_results(queue)
|
27
|
+
queue.pop queue.length
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module RSpecLive
|
2
|
+
class FormattedText
|
3
|
+
def initialize(text, options = {})
|
4
|
+
@text = text
|
5
|
+
@wrap = options[:wrap]
|
6
|
+
@width = options[:width]
|
7
|
+
@hanging_indent = options[:hanging_indent] || 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def text
|
11
|
+
if @wrap
|
12
|
+
wrap(@text, @width - @hanging_indent).split("\n").join("\n" + (" " * @hanging_indent))
|
13
|
+
else
|
14
|
+
@text
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def wrap(text, width)
|
21
|
+
text.scan(/\S.{0,#{width-2}}\S(?=\s|$)|\S+/).join("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module RSpecLive
|
2
|
+
class KeyHandler
|
3
|
+
def initialize
|
4
|
+
@event = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def on(*keys, &block)
|
8
|
+
keys.each { |key| @event[key] = block }
|
9
|
+
end
|
10
|
+
|
11
|
+
def process_updates
|
12
|
+
any_processed = false
|
13
|
+
while key = get_character_if_available
|
14
|
+
handle key
|
15
|
+
any_processed = true
|
16
|
+
end
|
17
|
+
any_processed
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def get_character_if_available
|
23
|
+
STDIN.read_nonblock 1
|
24
|
+
rescue Errno::EINTR
|
25
|
+
rescue Errno::EAGAIN
|
26
|
+
rescue EOFError
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle(key)
|
30
|
+
if @event[key]
|
31
|
+
@event[key].call
|
32
|
+
@update_listener.call if @update_listener
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "listen"
|
2
|
+
require "rspec-live/key_handler"
|
3
|
+
require "rspec-live/result_detail"
|
4
|
+
|
5
|
+
module RSpecLive
|
6
|
+
class Monitor
|
7
|
+
def initialize(suite, display, detail)
|
8
|
+
@suite = suite
|
9
|
+
@display = display
|
10
|
+
@detail = detail
|
11
|
+
@quit = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
@display.update
|
16
|
+
while !@quit do
|
17
|
+
@display.update if process_updates || @display.update_required?
|
18
|
+
sleep 0.05
|
19
|
+
end
|
20
|
+
rescue Interrupt
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def process_updates
|
26
|
+
key_handler.process_updates || @suite.process_updates
|
27
|
+
end
|
28
|
+
|
29
|
+
def key_handler
|
30
|
+
@key_handler ||= KeyHandler.new.tap do |handler|
|
31
|
+
handler.on("a") { @detail.toggle_all }
|
32
|
+
handler.on("n") { @detail.focus_next }
|
33
|
+
handler.on("q") { @quit = true }
|
34
|
+
handler.on("r") { @suite.reset }
|
35
|
+
handler.on("v") { @detail.cycle_verbosity }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RSpecLive
|
2
|
+
class ResultDetail
|
3
|
+
attr_reader :verbosity
|
4
|
+
|
5
|
+
def initialize(suite)
|
6
|
+
@suite = suite
|
7
|
+
@show_all = false
|
8
|
+
@verbosity = 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def toggle_all
|
12
|
+
@show_all = !@show_all
|
13
|
+
end
|
14
|
+
|
15
|
+
def focus_next
|
16
|
+
@focused = detailed_examples[1].name if detailed_examples[1]
|
17
|
+
end
|
18
|
+
|
19
|
+
def cycle_verbosity
|
20
|
+
@verbosity = (@verbosity + 1) % 4
|
21
|
+
end
|
22
|
+
|
23
|
+
def detailed_examples
|
24
|
+
all = @suite.ordered_examples
|
25
|
+
if @focused
|
26
|
+
index = @suite.ordered_example_names.index(@focused) || 0
|
27
|
+
all = all[index, all.length-index] + all[0, index]
|
28
|
+
end
|
29
|
+
@show_all ? all : all.select(&:failed?)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/rspec-live/runner.rb
CHANGED
@@ -1,28 +1,48 @@
|
|
1
|
-
require "
|
1
|
+
require "rspec-live/concurrent_process"
|
2
2
|
require "json"
|
3
3
|
|
4
4
|
module RSpecLive
|
5
5
|
class Runner
|
6
|
-
def
|
7
|
-
|
6
|
+
def initialize
|
7
|
+
@queued_examples = []
|
8
|
+
@results = []
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
|
11
|
+
def request_inventory(status)
|
12
|
+
@inventory_requested = status
|
13
|
+
end
|
14
|
+
|
15
|
+
def request_results(examples)
|
16
|
+
@queued_examples = examples
|
17
|
+
return if @queued_examples.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def results
|
21
|
+
start_process unless @process && @process.running?
|
22
|
+
@process.each_line { |line| record_result line } if @process
|
23
|
+
@results.pop @results.length
|
12
24
|
end
|
13
25
|
|
14
26
|
private
|
15
27
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
28
|
+
def record_result(text)
|
29
|
+
@results << JSON.parse(text)
|
30
|
+
rescue JSON::ParserError
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_process
|
34
|
+
if @inventory_requested
|
35
|
+
run "inventory", "--dry-run"
|
36
|
+
@inventory_requested = false
|
37
|
+
elsif @queued_examples.any?
|
38
|
+
run "update", @queued_examples.join(" ")
|
39
|
+
@queued_examples = []
|
24
40
|
end
|
25
|
-
|
41
|
+
end
|
42
|
+
|
43
|
+
def run(formatter, options="", &block)
|
44
|
+
@process = ConcurrentProcess.new formatter_command(formatter, options)
|
45
|
+
@process.start
|
26
46
|
end
|
27
47
|
|
28
48
|
def formatter_command(formatter, options)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "rspec-live/formatted_text"
|
2
|
+
require "curses"
|
3
|
+
|
4
|
+
module RSpecLive
|
5
|
+
class Screen
|
6
|
+
def initialize
|
7
|
+
reset_curses
|
8
|
+
Signal.trap("SIGWINCH", proc { reset_curses; @update_required = true })
|
9
|
+
end
|
10
|
+
|
11
|
+
def update_required?
|
12
|
+
@update_required
|
13
|
+
end
|
14
|
+
|
15
|
+
def start_frame
|
16
|
+
Curses.clear
|
17
|
+
Curses.setpos 0, 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def end_frame
|
21
|
+
Curses.refresh
|
22
|
+
end
|
23
|
+
|
24
|
+
def print(text, options = {})
|
25
|
+
color = options[:color]
|
26
|
+
options[:width] = @width if options[:wrap]
|
27
|
+
text = FormattedText.new(text, options).text if options.any?
|
28
|
+
if color
|
29
|
+
Curses.attron(color_attribute color) { Curses.addstr text }
|
30
|
+
else
|
31
|
+
Curses.addstr text
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def reset_curses
|
38
|
+
Curses.init_screen
|
39
|
+
Curses.curs_set 0
|
40
|
+
Curses.clear
|
41
|
+
Curses.refresh
|
42
|
+
Curses.start_color
|
43
|
+
Curses.use_default_colors
|
44
|
+
available_colors.each do |name|
|
45
|
+
Curses.init_pair color_constant(name), color_constant(name), -1
|
46
|
+
end
|
47
|
+
@width = `tput cols`.to_i
|
48
|
+
@height = `tput lines`.to_i
|
49
|
+
Curses.resizeterm @height, @width
|
50
|
+
end
|
51
|
+
|
52
|
+
def available_colors
|
53
|
+
[:blue, :green, :red, :yellow, :white]
|
54
|
+
end
|
55
|
+
|
56
|
+
def color_constant(name)
|
57
|
+
Curses.const_get "COLOR_#{name.to_s.upcase}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def color_attribute(name)
|
61
|
+
Curses.color_pair(color_constant name) | Curses::A_NORMAL
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/rspec-live/suite.rb
CHANGED
@@ -2,93 +2,82 @@ require "rspec-live/example"
|
|
2
2
|
|
3
3
|
module RSpecLive
|
4
4
|
class Suite
|
5
|
-
def initialize(runner,
|
5
|
+
def initialize(runner, file_watcher)
|
6
6
|
@runner = runner
|
7
|
-
@
|
8
|
-
@example_names = []
|
7
|
+
@file_watcher = file_watcher
|
9
8
|
@examples = {}
|
10
|
-
@show_all = false
|
11
|
-
@verbosity = 1
|
12
9
|
end
|
13
10
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@example_names = []
|
21
|
-
@runner.inventory do |example_data|
|
22
|
-
update_or_create_example example_data
|
23
|
-
update_display
|
11
|
+
def process_updates
|
12
|
+
any_processed = need_inventory = false
|
13
|
+
@file_watcher.updated.each do |path|
|
14
|
+
@examples.values.each { |example| example.file_touched path }
|
15
|
+
@examples.delete_if { |name, example| example.in_file? path }
|
16
|
+
any_processed = need_inventory = true
|
24
17
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
@runner.update do |example_data|
|
29
|
-
update_or_create_example example_data
|
30
|
-
update_display
|
18
|
+
@file_watcher.removed.each do |path|
|
19
|
+
@examples.delete_if { |name, example| example.in_file? path }
|
20
|
+
any_processed = true
|
31
21
|
end
|
22
|
+
if @examples.empty? || @file_watcher.added.any?
|
23
|
+
any_processed = need_inventory = true
|
24
|
+
end
|
25
|
+
@runner.request_inventory(need_inventory)
|
26
|
+
@runner.request_results stale_example_names
|
27
|
+
@runner.results.each do |result|
|
28
|
+
update_or_create_example result
|
29
|
+
any_processed = true
|
30
|
+
end
|
31
|
+
any_processed
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
@
|
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
|
34
|
+
def stale_example_names
|
35
|
+
@examples.values.select(&:stale?).map(&:name)
|
46
36
|
end
|
47
37
|
|
48
|
-
|
49
|
-
|
50
|
-
|
38
|
+
def activity_status
|
39
|
+
run_count = stale_example_names.count
|
40
|
+
run_count.zero? ? "watching for updates..." : "running #{example_count run_count}"
|
51
41
|
end
|
52
42
|
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
@examples[name].update data
|
59
|
-
@examples[name]
|
43
|
+
def summary
|
44
|
+
passed = ordered_examples.select(&:passed?).length
|
45
|
+
total = ordered_examples.length
|
46
|
+
percent = total.zero? ? 0 : (100*passed/total.to_f).round
|
47
|
+
"#{passed} of #{example_count total} passed (#{percent}%)"
|
60
48
|
end
|
61
49
|
|
62
|
-
def
|
63
|
-
|
50
|
+
def example_count(number)
|
51
|
+
"#{number} example" + (number == 1 ? "" : "s")
|
64
52
|
end
|
65
53
|
|
66
|
-
def
|
67
|
-
|
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?)
|
54
|
+
def ordered_examples
|
55
|
+
ordered_example_names.map { |name| @examples[name] }
|
73
56
|
end
|
74
57
|
|
75
|
-
def
|
76
|
-
@
|
58
|
+
def reset
|
59
|
+
@examples = {}
|
77
60
|
end
|
78
61
|
|
79
|
-
def
|
80
|
-
|
62
|
+
def ordered_example_names
|
63
|
+
example_names.sort_by do |name|
|
81
64
|
file, line = name.split(":")
|
82
65
|
line = line.rjust(8, "0")
|
83
66
|
[file, line].join(":")
|
84
67
|
end
|
85
68
|
end
|
86
69
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def update_or_create_example(data)
|
73
|
+
name = data["name"]
|
74
|
+
@examples[name] ||= Example.new
|
75
|
+
@examples[name].update data
|
76
|
+
@examples[name]
|
77
|
+
end
|
78
|
+
|
79
|
+
def example_names
|
80
|
+
@examples.keys
|
92
81
|
end
|
93
82
|
end
|
94
83
|
end
|
data/lib/rspec-live/version.rb
CHANGED
metadata
CHANGED
@@ -1,36 +1,32 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-live
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.2
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Brian Auton
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2014-
|
11
|
+
date: 2014-09-19 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rspec
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - "~>"
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 3.0
|
19
|
+
version: '3.0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - "~>"
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: 3.0
|
26
|
+
version: '3.0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: listen
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
31
|
- - "~>"
|
36
32
|
- !ruby/object:Gem::Version
|
@@ -38,7 +34,6 @@ dependencies:
|
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
38
|
- - "~>"
|
44
39
|
- !ruby/object:Gem::Version
|
@@ -46,7 +41,6 @@ dependencies:
|
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: curses
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - "~>"
|
52
46
|
- !ruby/object:Gem::Version
|
@@ -54,7 +48,6 @@ dependencies:
|
|
54
48
|
type: :runtime
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - "~>"
|
60
53
|
- !ruby/object:Gem::Version
|
@@ -67,43 +60,49 @@ executables:
|
|
67
60
|
extensions: []
|
68
61
|
extra_rdoc_files: []
|
69
62
|
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
63
|
- History.md
|
82
64
|
- License.txt
|
65
|
+
- README.md
|
83
66
|
- bin/rspec-live
|
67
|
+
- lib/formatters/coverage_analyzer.rb
|
68
|
+
- lib/formatters/inventory_formatter.rb
|
69
|
+
- lib/formatters/update_formatter.rb
|
70
|
+
- lib/rspec-live/backtrace.rb
|
71
|
+
- lib/rspec-live/concurrent_process.rb
|
72
|
+
- lib/rspec-live/display.rb
|
73
|
+
- lib/rspec-live/example.rb
|
74
|
+
- lib/rspec-live/file_watcher.rb
|
75
|
+
- lib/rspec-live/formatted_text.rb
|
76
|
+
- lib/rspec-live/key_handler.rb
|
77
|
+
- lib/rspec-live/log.rb
|
78
|
+
- lib/rspec-live/monitor.rb
|
79
|
+
- lib/rspec-live/result_detail.rb
|
80
|
+
- lib/rspec-live/runner.rb
|
81
|
+
- lib/rspec-live/screen.rb
|
82
|
+
- lib/rspec-live/suite.rb
|
83
|
+
- lib/rspec-live/version.rb
|
84
84
|
homepage: http://github.com/brianauton/rspec-live
|
85
85
|
licenses:
|
86
86
|
- MIT
|
87
|
+
metadata: {}
|
87
88
|
post_install_message:
|
88
89
|
rdoc_options: []
|
89
90
|
require_paths:
|
90
91
|
- lib
|
91
92
|
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
-
none: false
|
93
93
|
requirements:
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
-
none: false
|
99
98
|
requirements:
|
100
99
|
- - ">="
|
101
100
|
- !ruby/object:Gem::Version
|
102
101
|
version: 1.3.6
|
103
102
|
requirements: []
|
104
103
|
rubyforge_project:
|
105
|
-
rubygems_version:
|
104
|
+
rubygems_version: 2.2.2
|
106
105
|
signing_key:
|
107
|
-
specification_version:
|
106
|
+
specification_version: 4
|
108
107
|
summary: Continually updating console output for RSpec 3+
|
109
108
|
test_files: []
|
data/lib/rspec-live/terminal.rb
DELETED
@@ -1,121 +0,0 @@
|
|
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
|
data/lib/rspec-live/watcher.rb
DELETED
@@ -1,42 +0,0 @@
|
|
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
|