spec_selector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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