spec_selector 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/README.md +48 -0
- data/lib/spec_selector.rb +92 -0
- data/lib/spec_selector/data_map.rb +38 -0
- data/lib/spec_selector/data_presentation.rb +179 -0
- data/lib/spec_selector/format.rb +99 -0
- data/lib/spec_selector/helpers.rb +63 -0
- data/lib/spec_selector/initialize.rb +83 -0
- data/lib/spec_selector/instructions.rb +139 -0
- data/lib/spec_selector/scripts/rerun.sh +40 -0
- data/lib/spec_selector/state.rb +142 -0
- data/lib/spec_selector/terminal.rb +45 -0
- data/lib/spec_selector/ui.rb +169 -0
- data/license.md +8 -0
- data/spec/factories.rb +89 -0
- data/spec/shared.rb +145 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/spec_selector_spec.rb +165 -0
- data/spec/spec_selector_util/data_map_spec.rb +98 -0
- data/spec/spec_selector_util/data_presentation_spec.rb +314 -0
- data/spec/spec_selector_util/format_spec.rb +213 -0
- data/spec/spec_selector_util/helpers_spec.rb +222 -0
- data/spec/spec_selector_util/initialize_spec.rb +93 -0
- data/spec/spec_selector_util/ui_spec.rb +459 -0
- metadata +96 -0
- metadata.gz.sig +1 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ee389a398dc0b3c27c22993575b8062e8d01e98f34984c9fb8ca7ac5d4fbfb79
|
4
|
+
data.tar.gz: e3bbbfd79b14f98abf321dc72b246dae1a505ea000c86e48d2aacb3aeaee9776
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6fa192690d6a205f5cf726be2a222cf4f8fab78c028af23c4a23fd408d38642f78f6f86f361108ccb2510c4d3a78cab0bf104ef068c5ae3ba701146e1d3f07c1
|
7
|
+
data.tar.gz: 2e1a87593d1ce2a8235bc62c5af1f4a3a9c8e5f38a98a95b9667d8fac98fc788e54f3b28acf3676073f1b7b2860185c2d0112c9e02ff6d1ab1f8585502c7efcb
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data.tar.gz.sig
ADDED
Binary file
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# spec_selector
|
2
|
+
|
3
|
+
SpecSelector is an RSpec formatter than opens a utility menu in your terminal window when you run tests (rather than just printing static text). The utility allows you to select, view, filter, and rerun specific test results with simple key controls.
|
4
|
+
|
5
|
+
**view test results**
|
6
|
+
|
7
|
+
Upon finishing the test run, the test result tree appears as a formatted list of top-level example groups. Select an example group to view its subgroups, select a subgroup to view its examples, and so on. You can view your test results with the selection tool, or just press T to immediately view the top failed test.
|
8
|
+
|
9
|
+
**filter and rerun test results**
|
10
|
+
|
11
|
+
Using the selection tool, press M to add the selected group or example to the inclusion filter. Press R to rerun RSpec with only selected tests.
|
12
|
+
|
13
|
+
Without using the selection tool, press F to rerun only failed tests. Press SHIFT + T to rerun only the top failed test.
|
14
|
+
|
15
|
+
Press C to clear the inclusion filter. Press A to clear the inclusion filter and rerun RSpec with all tests.
|
16
|
+
|
17
|
+
Press V to view the inclusion filter as a selection list.
|
18
|
+
|
19
|
+
_Filter Modes_
|
20
|
+
|
21
|
+
Whenever the inclusion filter is not empty, the filter mode will display at the top center of the terminal window.
|
22
|
+
|
23
|
+
There are two filter modes: _description_ and _location_.
|
24
|
+
|
25
|
+
The filter always uses description matching by default, but will use location (line number) matching if examples without descriptions (i.e. "one-liners") are selected for inclusion.
|
26
|
+
|
27
|
+
**Installation**
|
28
|
+
|
29
|
+
````
|
30
|
+
gem install spec_selector
|
31
|
+
````
|
32
|
+
|
33
|
+
Once installed, add the following line to your .rspec file:
|
34
|
+
|
35
|
+
````
|
36
|
+
--format SpecSelector
|
37
|
+
````
|
38
|
+
|
39
|
+
Or, use the -f option on the command line
|
40
|
+
|
41
|
+
````
|
42
|
+
rspec -f SpecSelector
|
43
|
+
````
|
44
|
+
|
45
|
+
**Author**
|
46
|
+
|
47
|
+
Trevor Almon
|
48
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console'
|
4
|
+
require 'json'
|
5
|
+
require 'byebug'
|
6
|
+
require 'pry'
|
7
|
+
require_relative 'spec_selector/terminal'
|
8
|
+
require_relative 'spec_selector/UI'
|
9
|
+
require_relative 'spec_selector/format'
|
10
|
+
require_relative 'spec_selector/data_presentation'
|
11
|
+
require_relative 'spec_selector/helpers'
|
12
|
+
require_relative 'spec_selector/data_map'
|
13
|
+
require_relative 'spec_selector/initialize'
|
14
|
+
require_relative 'spec_selector/instructions'
|
15
|
+
require_relative 'spec_selector/state'
|
16
|
+
|
17
|
+
# The SpecSelector instance receives example execution data from the reporter
|
18
|
+
# and arranges it into a formatted, navigable map.
|
19
|
+
class SpecSelector
|
20
|
+
include SpecSelectorUtil::UI
|
21
|
+
include SpecSelectorUtil::Terminal
|
22
|
+
include SpecSelectorUtil::Format
|
23
|
+
include SpecSelectorUtil::DataPresentation
|
24
|
+
include SpecSelectorUtil::Helpers
|
25
|
+
include SpecSelectorUtil::DataMap
|
26
|
+
include SpecSelectorUtil::Initialize
|
27
|
+
include SpecSelectorUtil::Instructions
|
28
|
+
include SpecSelectorUtil::State
|
29
|
+
|
30
|
+
RSpec::Core::Formatters.register self,
|
31
|
+
:message,
|
32
|
+
:example_group_started,
|
33
|
+
:example_passed,
|
34
|
+
:example_pending,
|
35
|
+
:example_failed,
|
36
|
+
:dump_summary
|
37
|
+
|
38
|
+
def initialize(output)
|
39
|
+
@output = output
|
40
|
+
hide_cursor
|
41
|
+
initialize_all
|
42
|
+
end
|
43
|
+
|
44
|
+
def message(notification)
|
45
|
+
@messages << notification.message
|
46
|
+
end
|
47
|
+
|
48
|
+
def example_group_started(notification)
|
49
|
+
group = notification.group
|
50
|
+
map_group(group)
|
51
|
+
@groups[group.metadata[:block]] = group
|
52
|
+
check_inclusion_status(group)
|
53
|
+
end
|
54
|
+
|
55
|
+
def example_passed(notification)
|
56
|
+
clear_frame
|
57
|
+
@passed << notification.example
|
58
|
+
map_example(notification.example)
|
59
|
+
check_inclusion_status(notification.example)
|
60
|
+
@pass_count += 1
|
61
|
+
status_count
|
62
|
+
end
|
63
|
+
|
64
|
+
def example_pending(notification)
|
65
|
+
clear_frame
|
66
|
+
@pending_summaries[notification.example] = notification
|
67
|
+
@pending << notification.example
|
68
|
+
map_example(notification.example)
|
69
|
+
check_inclusion_status(notification.example)
|
70
|
+
@pending_count += 1
|
71
|
+
status_count
|
72
|
+
end
|
73
|
+
|
74
|
+
def example_failed(notification)
|
75
|
+
clear_frame
|
76
|
+
@failure_summaries[notification.example] = notification
|
77
|
+
@failed << notification.example
|
78
|
+
map_example(notification.example)
|
79
|
+
check_inclusion_status(notification.example)
|
80
|
+
@fail_count += 1
|
81
|
+
status_count
|
82
|
+
end
|
83
|
+
|
84
|
+
def dump_summary(notification)
|
85
|
+
@example_count = notification.example_count
|
86
|
+
@outside_errors_count = notification.errors_outside_of_examples_count
|
87
|
+
errors_before_formatter_initialization
|
88
|
+
print_errors(notification) if @outside_errors_count.positive?
|
89
|
+
messages_only if @map.empty?
|
90
|
+
examples_summary(notification)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The DataMap module contains methods used to build a hash map of nested
|
5
|
+
# lists, which can be rendered in their interactive form through the
|
6
|
+
# DataPresentation methods.
|
7
|
+
module DataMap
|
8
|
+
def top_level_push(group)
|
9
|
+
@map[:top_level] ||= []
|
10
|
+
@map[:top_level] << group
|
11
|
+
@map[group.metadata[:block]] ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def parent_data(data)
|
15
|
+
keys = data.keys
|
16
|
+
return data[:example_group] if keys.include?(:example_group)
|
17
|
+
return data[:parent_example_group] if keys.include?(:parent_example_group)
|
18
|
+
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def map_group(group)
|
23
|
+
if !group.metadata[:parent_example_group]
|
24
|
+
top_level_push(group)
|
25
|
+
else
|
26
|
+
parent = group.metadata[:parent_example_group][:block]
|
27
|
+
@map[parent] ||= []
|
28
|
+
@map[parent] << group
|
29
|
+
@map[group.metadata[:block]] ||= []
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_example(example)
|
34
|
+
group = example.example_group
|
35
|
+
@map[group.metadata[:block]] << example
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The DataPresentation module contains methods used to render mapped data.
|
5
|
+
module DataPresentation
|
6
|
+
def test_data_summary
|
7
|
+
status_count
|
8
|
+
print_summary
|
9
|
+
end
|
10
|
+
|
11
|
+
# If an exception is raised before an instance of SpecSelector is
|
12
|
+
# initialized (for instance, a TypeError raised due to a configuration
|
13
|
+
# problem), the MessageNotification will be sent to the registered
|
14
|
+
# default formatter instead and will not be accessable to SpecSelector.
|
15
|
+
# In such a case, the formatted error information is printed immediately
|
16
|
+
# in the manner determined by the default formatter. This method simply
|
17
|
+
# checks for a condition caused by that situation and leaves the error
|
18
|
+
# information displayed until the user exits.
|
19
|
+
def errors_before_formatter_initialization
|
20
|
+
if @outside_errors_count.positive? && @messages == ['No examples found.']
|
21
|
+
empty_line
|
22
|
+
exit_only
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def print_errors(notification)
|
27
|
+
clear_frame
|
28
|
+
print_messages
|
29
|
+
errors_summary(notification)
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_messages
|
33
|
+
printed = 0
|
34
|
+
@messages.each do |message|
|
35
|
+
next if message.include?('Run options: include {:full_description=>')
|
36
|
+
next if message.include?('Run options: include {:locations=>')
|
37
|
+
|
38
|
+
italicize(message)
|
39
|
+
printed += 1
|
40
|
+
end
|
41
|
+
empty_line if printed.positive?
|
42
|
+
end
|
43
|
+
|
44
|
+
def examples_summary(notification)
|
45
|
+
@summary_notification = notification
|
46
|
+
status_summary(notification)
|
47
|
+
|
48
|
+
@list = if @inclusion_filter.empty? || @inclusion_filter.count > 10
|
49
|
+
@map[:top_level]
|
50
|
+
else
|
51
|
+
@inclusion_filter
|
52
|
+
end
|
53
|
+
|
54
|
+
selector
|
55
|
+
end
|
56
|
+
|
57
|
+
def errors_summary(notification)
|
58
|
+
err_count = notification.errors_outside_of_examples_count
|
59
|
+
word_form = err_count > 1 ? 'errors' : 'error'
|
60
|
+
italicize "Finished in #{notification.duration} seconds"
|
61
|
+
italicize "Files loaded in #{notification.load_time}"
|
62
|
+
empty_line
|
63
|
+
italicize "#{err_count} #{word_form} occurred outside of examples"
|
64
|
+
italicize 'Examples were not successfully executed'
|
65
|
+
exit_only
|
66
|
+
end
|
67
|
+
|
68
|
+
def status_count
|
69
|
+
pass_count
|
70
|
+
pending_count if @pending_count.positive?
|
71
|
+
fail_count
|
72
|
+
empty_line
|
73
|
+
end
|
74
|
+
|
75
|
+
def print_summary
|
76
|
+
@summary.each { |sum| italicize(sum) }
|
77
|
+
empty_line
|
78
|
+
end
|
79
|
+
|
80
|
+
def exclude_passing!
|
81
|
+
alt_map = @map.reject { |_, v| v.all? { |g| all_passed?(fetch_examples(g)) } }
|
82
|
+
alt_map.transform_values! { |v| v.reject { |g| all_passed?(fetch_examples(g)) } }
|
83
|
+
@active_map = alt_map
|
84
|
+
@exclude_passing = true
|
85
|
+
end
|
86
|
+
|
87
|
+
def include_passing!
|
88
|
+
@active_map = @map
|
89
|
+
@exclude_passing = false
|
90
|
+
end
|
91
|
+
|
92
|
+
def toggle_passing
|
93
|
+
return if all_passing?
|
94
|
+
|
95
|
+
@exclude_passing ? include_passing! : exclude_passing!
|
96
|
+
return if @example_display && @list != @passed && !@instructions
|
97
|
+
|
98
|
+
exit_instruction_page if @instructions
|
99
|
+
p_data = parent_data(@selected.metadata)
|
100
|
+
key = p_data ? p_data[:block] : :top_level
|
101
|
+
new_list = @active_map[key]
|
102
|
+
@list = new_list
|
103
|
+
@selected = nil
|
104
|
+
@example_display = false
|
105
|
+
set_selected
|
106
|
+
display_list
|
107
|
+
end
|
108
|
+
|
109
|
+
def status_summary(notification)
|
110
|
+
@summary = []
|
111
|
+
@summary << "Total Examples: #{@example_count}"
|
112
|
+
@summary << "Finished in #{notification.duration} seconds"
|
113
|
+
@summary << "Files loaded in #{notification.load_time} seconds"
|
114
|
+
end
|
115
|
+
|
116
|
+
def display_list
|
117
|
+
clear_frame
|
118
|
+
display_filter_mode
|
119
|
+
test_data_summary
|
120
|
+
print_messages unless @messages.empty?
|
121
|
+
all_passed_message if all_passing?
|
122
|
+
basic_instructions
|
123
|
+
empty_line
|
124
|
+
|
125
|
+
@list.each { |item| format_list_item(item) }
|
126
|
+
end
|
127
|
+
|
128
|
+
def view_inclusion_filter
|
129
|
+
if @inclusion_filter.empty?
|
130
|
+
empty_filter_notice
|
131
|
+
return
|
132
|
+
end
|
133
|
+
|
134
|
+
@example_display = false
|
135
|
+
exit_instruction_page if @instructions
|
136
|
+
@list = @inclusion_filter
|
137
|
+
@selected = @list.first unless @selected.metadata[:include]
|
138
|
+
set_selected
|
139
|
+
display_list
|
140
|
+
end
|
141
|
+
|
142
|
+
def refresh_display
|
143
|
+
set_selected
|
144
|
+
@example_display ? display_example : display_list
|
145
|
+
end
|
146
|
+
|
147
|
+
def display_example
|
148
|
+
@example_display = true
|
149
|
+
clear_frame
|
150
|
+
display_filter_mode
|
151
|
+
test_data_summary
|
152
|
+
status = @selected.execution_result.status
|
153
|
+
@list, data = example_list
|
154
|
+
example_summary_instructions
|
155
|
+
@output.puts 'Added to filter √' if @selected.metadata[:include]
|
156
|
+
@selector_index = @list.index(@selected)
|
157
|
+
view_other_examples(status) if @list.count > 1 && @instructions
|
158
|
+
format_example(status, data)
|
159
|
+
end
|
160
|
+
|
161
|
+
def example_list
|
162
|
+
status = @selected.execution_result.status
|
163
|
+
result_list = @failed if status == :failed
|
164
|
+
result_list = @pending if status == :pending
|
165
|
+
result_list = @passed if status == :passed
|
166
|
+
|
167
|
+
data = @failure_summaries[@selected] if status == :failed
|
168
|
+
data = @pending_summaries[@selected] if status == :pending
|
169
|
+
|
170
|
+
[result_list, data]
|
171
|
+
end
|
172
|
+
|
173
|
+
def messages_only
|
174
|
+
clear_frame
|
175
|
+
print_messages
|
176
|
+
exit_only
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The Format module contains methods used for simple text formatting, as well
|
5
|
+
# as methods that determine how specific list items will be formatted.
|
6
|
+
module Format
|
7
|
+
ESCAPE_CODES = {
|
8
|
+
green: '1;32', # the numeric codes for green, yellow, and red are
|
9
|
+
red: '1;31', # 32, 31, and 33 respectively. The '1;' is prepended
|
10
|
+
yellow: '1;33', # for bold lettering.
|
11
|
+
italicize: 3,
|
12
|
+
bold: 1
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
ESCAPE_CODES.each do |sym, num|
|
16
|
+
define_method(sym) do |text, included = false|
|
17
|
+
formatted = "\e[#{num}m#{text}\e[0m"
|
18
|
+
formatted = included ? formatted + ' √' : formatted
|
19
|
+
@output.puts formatted
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_examples(item)
|
24
|
+
return [item] if example?(item)
|
25
|
+
|
26
|
+
examples = item.examples
|
27
|
+
return examples if @map[item.metadata[:block]] == examples
|
28
|
+
|
29
|
+
examples.reject! { |ex| ex.execution_result.status.nil? }
|
30
|
+
|
31
|
+
@map[item.metadata[:block]].each do |d|
|
32
|
+
examples += fetch_examples(d)
|
33
|
+
end
|
34
|
+
|
35
|
+
examples
|
36
|
+
end
|
37
|
+
|
38
|
+
def format_list_item(item)
|
39
|
+
description = lineage(item.metadata)
|
40
|
+
data = example?(item) ? [item] : fetch_examples(item)
|
41
|
+
included = item.metadata[:include]
|
42
|
+
|
43
|
+
if @selected == item
|
44
|
+
highlight(description, included)
|
45
|
+
else
|
46
|
+
green(description, included) if all_passed?(data)
|
47
|
+
yellow(description, included) if any_pending?(data) && !any_failed?(data)
|
48
|
+
red(description, included) if any_failed?(data)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def pass_count
|
53
|
+
green("PASS: #{@pass_count}")
|
54
|
+
end
|
55
|
+
|
56
|
+
def pending_count
|
57
|
+
yellow("PENDING: #{@pending_count}")
|
58
|
+
end
|
59
|
+
|
60
|
+
def fail_count
|
61
|
+
red("FAIL: #{@fail_count}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def highlight(text, included = false)
|
65
|
+
text += ' √' if included
|
66
|
+
@output.puts "\e[1;7m#{text}\e[0m"
|
67
|
+
end
|
68
|
+
|
69
|
+
def lineage(data)
|
70
|
+
parent = parent_data(data)
|
71
|
+
return data[:description] unless parent
|
72
|
+
|
73
|
+
lineage(parent) + ' -> ' + data[:description]
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_example(status, data)
|
77
|
+
if %i[failed pending].include?(status)
|
78
|
+
print_nonpassing_example(data)
|
79
|
+
else
|
80
|
+
print_passing_example
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def print_nonpassing_example(data)
|
85
|
+
data = data.fully_formatted(@selector_index + 1).split("\n")
|
86
|
+
data[0] = ''
|
87
|
+
data.insert(1, '-' * term_width)
|
88
|
+
data.insert(3, '-' * term_width)
|
89
|
+
@output.puts data
|
90
|
+
end
|
91
|
+
|
92
|
+
def print_passing_example
|
93
|
+
@output.puts '-' * term_width
|
94
|
+
@output.puts "#{@selector_index + 1}) " + @selected.description
|
95
|
+
@output.puts '-' * term_width
|
96
|
+
green('PASSED')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|