spec_selector 0.1.0
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
- 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
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The Helpers module contains helper methods shared across multiple
|
5
|
+
# concerns.
|
6
|
+
module Helpers
|
7
|
+
def all_passing?
|
8
|
+
(@pending_count + @fail_count).zero? && @pass_count.positive?
|
9
|
+
end
|
10
|
+
|
11
|
+
def none_passing?
|
12
|
+
(@pending_count + @fail_count).positive? && @pass_count.zero?
|
13
|
+
end
|
14
|
+
|
15
|
+
def all_passed?(examples)
|
16
|
+
examples.all? { |example| example.execution_result.status == :passed }
|
17
|
+
end
|
18
|
+
|
19
|
+
def any_failed?(examples)
|
20
|
+
examples.any? { |example| example.execution_result.status == :failed }
|
21
|
+
end
|
22
|
+
|
23
|
+
def any_pending?(examples)
|
24
|
+
examples.any? { |example| example.execution_result.status == :pending }
|
25
|
+
end
|
26
|
+
|
27
|
+
def example?(item)
|
28
|
+
item.is_a?(RSpec::Core::Example)
|
29
|
+
end
|
30
|
+
|
31
|
+
def status(example)
|
32
|
+
example.execution_result.status
|
33
|
+
end
|
34
|
+
|
35
|
+
def description_mode?
|
36
|
+
@filter_mode == :description
|
37
|
+
end
|
38
|
+
|
39
|
+
def location_mode?
|
40
|
+
@filter_mode == :location
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty_line
|
44
|
+
@output.puts "\n"
|
45
|
+
end
|
46
|
+
|
47
|
+
def top_level?
|
48
|
+
@list == @active_map[:top_level]
|
49
|
+
end
|
50
|
+
|
51
|
+
def filter_view?
|
52
|
+
@list == @inclusion_filter
|
53
|
+
end
|
54
|
+
|
55
|
+
def current_path
|
56
|
+
File.dirname(__FILE__)
|
57
|
+
end
|
58
|
+
|
59
|
+
def one_liner?(example)
|
60
|
+
example.metadata[:description_args].empty?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The Initialize module contains methods that initialize specific sets of
|
5
|
+
# instance variables for the SpecSelector instance.
|
6
|
+
module Initialize
|
7
|
+
def init_example_store
|
8
|
+
@failed = []
|
9
|
+
@passed = []
|
10
|
+
@pending = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def init_summaries
|
14
|
+
@failure_summaries = {}
|
15
|
+
@pending_summaries = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def init_counters
|
19
|
+
@pass_count = 0
|
20
|
+
@fail_count = 0
|
21
|
+
@pending_count = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def init_pass_inclusion
|
25
|
+
@exclude_passing = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def init_map
|
29
|
+
@groups = {}
|
30
|
+
@map = {}
|
31
|
+
@active_map = @map
|
32
|
+
@list = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def init_selector
|
36
|
+
@selected = nil
|
37
|
+
@selector_index = 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_locations
|
41
|
+
if File.exist?(@locations_file)
|
42
|
+
locations = File.open(@locations_file)
|
43
|
+
@last_run_locations = JSON.load(locations)
|
44
|
+
@filter_mode = :location
|
45
|
+
else
|
46
|
+
@last_run_locations = []
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_descriptions
|
51
|
+
if File.exist?(@descriptions_file)
|
52
|
+
included = File.open(@descriptions_file)
|
53
|
+
@last_run_descriptions = JSON.load(included)
|
54
|
+
else
|
55
|
+
@last_run_descriptions = []
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def init_filter
|
60
|
+
inclusion_filter_path = "#{current_path}/inclusion_filter"
|
61
|
+
Dir.mkdir(inclusion_filter_path) unless Dir.exist?(inclusion_filter_path)
|
62
|
+
@descriptions_file = "#{current_path}/inclusion_filter/descriptions.json"
|
63
|
+
@locations_file = "#{current_path}/inclusion_filter/locations.json"
|
64
|
+
@inclusion_filter = []
|
65
|
+
@filter_mode = :description
|
66
|
+
get_descriptions
|
67
|
+
get_locations
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize_all
|
71
|
+
@messages = []
|
72
|
+
@notices = []
|
73
|
+
init_example_store
|
74
|
+
init_summaries
|
75
|
+
init_counters
|
76
|
+
init_pass_inclusion
|
77
|
+
init_map
|
78
|
+
init_selector
|
79
|
+
init_filter
|
80
|
+
@instructions = false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecSelectorUtil
|
4
|
+
# The Instructions module contains methods used to render the
|
5
|
+
# appropriate user instructions.
|
6
|
+
module Instructions
|
7
|
+
def basic_instructions
|
8
|
+
i_for_instructions
|
9
|
+
up_down_select_instructions
|
10
|
+
q_to_exit
|
11
|
+
end
|
12
|
+
|
13
|
+
def all_passed_message
|
14
|
+
bold("ALL EXAMPLES PASSED\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
def empty_filter_notice
|
18
|
+
notice = '**********FILTER EMPTY**********'
|
19
|
+
row = term_width / 2 - notice.length / 2
|
20
|
+
position_cursor(1, row)
|
21
|
+
@output.puts notice
|
22
|
+
reset_cursor
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def display_filter_mode
|
27
|
+
unless @inclusion_filter.empty?
|
28
|
+
notice = "FILTER MODE: #{@filter_mode.to_s.upcase}"
|
29
|
+
col = term_width / 2 - notice.length / 2
|
30
|
+
position_cursor(1, col)
|
31
|
+
italicize notice
|
32
|
+
reset_cursor
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def back_instructions
|
37
|
+
back_inst = 'Press [back] to view to parent group list'
|
38
|
+
escape_inst = 'Press [escape] to view to top-level group list'
|
39
|
+
|
40
|
+
[back_inst, escape_inst].each do |inst|
|
41
|
+
if @instructions
|
42
|
+
bold(inst)
|
43
|
+
empty_line
|
44
|
+
else
|
45
|
+
@output.puts inst
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def q_to_exit
|
51
|
+
@output.puts 'Press Q to exit'
|
52
|
+
end
|
53
|
+
|
54
|
+
def view_other_examples(status)
|
55
|
+
verb = (status == :passed ? 'passing' : status.to_s)
|
56
|
+
@output.puts "Press ↑ or ↓ to view other #{verb} examples"
|
57
|
+
end
|
58
|
+
|
59
|
+
def filter_pass_instructions
|
60
|
+
verb = @exclude_passing ? 'show' : 'hide'
|
61
|
+
bold "Press P to #{verb} passing examples in current set"
|
62
|
+
end
|
63
|
+
|
64
|
+
def i_for_instructions
|
65
|
+
@output.puts 'Press I to view full instructions'
|
66
|
+
end
|
67
|
+
|
68
|
+
def up_down_select_instructions
|
69
|
+
up_down_inst = 'Press ↑ or ↓ to navigate list' if @list.count > 1
|
70
|
+
select_inst = 'press [enter] to select'
|
71
|
+
|
72
|
+
[up_down_inst, select_inst].each do |inst|
|
73
|
+
if @instructions
|
74
|
+
bold(inst)
|
75
|
+
empty_line
|
76
|
+
else
|
77
|
+
@output.puts inst
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def view_instructions_page
|
83
|
+
@instructions = true
|
84
|
+
open_alt_buffer
|
85
|
+
|
86
|
+
unless @failed.empty? || @selected == @failed.first
|
87
|
+
top_fail_text
|
88
|
+
empty_line
|
89
|
+
end
|
90
|
+
|
91
|
+
unless all_passing? || none_passing?
|
92
|
+
filter_pass_instructions
|
93
|
+
empty_line
|
94
|
+
end
|
95
|
+
|
96
|
+
up_down_select_instructions
|
97
|
+
back_instructions unless top_level?
|
98
|
+
bold('Press R to rerun examples with filter selection')
|
99
|
+
empty_line
|
100
|
+
bold('Press F to rerun only failed examples')
|
101
|
+
empty_line
|
102
|
+
bold('Press SHIFT + T to rerun only the top failed example')
|
103
|
+
empty_line
|
104
|
+
bold('Press M to include or remove selected item from run filter')
|
105
|
+
empty_line
|
106
|
+
|
107
|
+
if @inclusion_filter.size.positive?
|
108
|
+
bold('Press C to clear filter')
|
109
|
+
empty_line
|
110
|
+
bold('Press A to clear filter and rerun all examples')
|
111
|
+
empty_line
|
112
|
+
end
|
113
|
+
|
114
|
+
bold('Press I to exit instructions')
|
115
|
+
empty_line
|
116
|
+
bold('Press Q to quit')
|
117
|
+
bind_input
|
118
|
+
end
|
119
|
+
|
120
|
+
def top_fail_text
|
121
|
+
bold 'Press T to view top failed example'
|
122
|
+
end
|
123
|
+
|
124
|
+
def exit_instruction_page
|
125
|
+
@instructions = false
|
126
|
+
close_alt_buffer
|
127
|
+
end
|
128
|
+
|
129
|
+
def example_summary_instructions
|
130
|
+
i_for_instructions
|
131
|
+
@output.puts 'Press M to remove from filter' if @selected.metadata[:include]
|
132
|
+
@output.puts 'Press C to clear filter' unless @inclusion_filter.empty?
|
133
|
+
top_fail_text unless @failed.empty? || @selected == @failed.first
|
134
|
+
back_instructions
|
135
|
+
q_to_exit
|
136
|
+
empty_line
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
cd $2
|
4
|
+
args=("$@")
|
5
|
+
len=${args[@]: -1}
|
6
|
+
let start=-$len-1
|
7
|
+
|
8
|
+
filter() {
|
9
|
+
descriptions=()
|
10
|
+
for arg in "${args[@]: $start: $len}"
|
11
|
+
do description=$(echo $arg | sed 's/^\[//;s/\]$//;s/,$//')
|
12
|
+
for (( i=0; i<${#description}; i++ )); do
|
13
|
+
if [ "${description:$i:1}" = "'" ]; then
|
14
|
+
description="\"$description\""
|
15
|
+
escaped=true
|
16
|
+
break
|
17
|
+
fi
|
18
|
+
done
|
19
|
+
|
20
|
+
if ! [ $escaped ]; then
|
21
|
+
description="'$description"
|
22
|
+
description="$description'"
|
23
|
+
fi
|
24
|
+
|
25
|
+
descriptions+=" -e "
|
26
|
+
descriptions+=$description
|
27
|
+
done
|
28
|
+
}
|
29
|
+
|
30
|
+
let length=$#+$start-2
|
31
|
+
|
32
|
+
if [ $len -eq 0 ]
|
33
|
+
then
|
34
|
+
eval "${args[@]:2:$length}"
|
35
|
+
else
|
36
|
+
filter
|
37
|
+
eval "${args[@]:2:$length} ${descriptions[@]}"
|
38
|
+
fi
|
39
|
+
|
40
|
+
kill $1
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module SpecSelectorUtil
|
2
|
+
module State
|
3
|
+
def rerun
|
4
|
+
prepare_rerun
|
5
|
+
descriptions, marker = appended_arguments
|
6
|
+
rerun_script = current_path + '/scripts/rerun.sh'
|
7
|
+
prepend = [rerun_script, Process.pid, Dir.pwd].join(' ')
|
8
|
+
Signal.trap('TERM') { clear_frame; exit }
|
9
|
+
system("#{prepend} #{$0} #{@rerun_arguments} #{descriptions} #{marker}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter_include(item = @selected)
|
13
|
+
@filter_mode = :location if one_liner?(item)
|
14
|
+
item.metadata[:include] = true
|
15
|
+
@inclusion_filter << item
|
16
|
+
end
|
17
|
+
|
18
|
+
def prepare_rerun
|
19
|
+
display_rerun
|
20
|
+
persist_filter
|
21
|
+
reset_arguments
|
22
|
+
prepare_location_arguments if location_mode?
|
23
|
+
delete_filter_data if @inclusion_filter.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def display_rerun
|
27
|
+
close_alt_buffer if @instructions
|
28
|
+
clear_frame
|
29
|
+
italicize('running examples...')
|
30
|
+
end
|
31
|
+
|
32
|
+
def appended_arguments
|
33
|
+
return [nil, 0] if location_mode?
|
34
|
+
|
35
|
+
[prepare_description_arguments, @filtered_descriptions.count]
|
36
|
+
end
|
37
|
+
|
38
|
+
def persist_filter
|
39
|
+
persist_descriptions
|
40
|
+
persist_locations if location_mode?
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_only_fails
|
44
|
+
return if @failed.empty?
|
45
|
+
|
46
|
+
@inclusion_filter = []
|
47
|
+
@failed.each { |example| filter_include(example) }
|
48
|
+
rerun
|
49
|
+
end
|
50
|
+
|
51
|
+
def rerun_all
|
52
|
+
@inclusion_filter = []
|
53
|
+
rerun
|
54
|
+
end
|
55
|
+
|
56
|
+
def filter_remove
|
57
|
+
@inclusion_filter -= [@selected]
|
58
|
+
@selected.metadata[:include] = nil
|
59
|
+
@filter_mode = :descripton unless @inclusion_filter.any? { |item| one_liner?(item) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def persist_descriptions
|
63
|
+
@filtered_descriptions = @inclusion_filter.map do |item|
|
64
|
+
item.metadata[:full_description]
|
65
|
+
end
|
66
|
+
|
67
|
+
filter = @filtered_descriptions.to_json
|
68
|
+
File.write(@descriptions_file, filter)
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete_filter_data
|
72
|
+
[@descriptions_file, @locations_file].each do |file|
|
73
|
+
File.delete(file) if File.exist?(file)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def reset_arguments
|
78
|
+
remove_old_descriptions
|
79
|
+
remove_old_locations
|
80
|
+
end
|
81
|
+
|
82
|
+
def remove_old_locations
|
83
|
+
return if @last_run_locations.empty?
|
84
|
+
|
85
|
+
@last_run_locations.each { |loc| @rerun_arguments.slice!(loc) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def remove_old_descriptions
|
89
|
+
old_descriptions = @last_run_descriptions.map { |desc| "-e #{desc}" }
|
90
|
+
@rerun_arguments = ARGV.join(' ')
|
91
|
+
old_descriptions.each { |desc| @rerun_arguments.slice!(desc) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def persist_locations
|
95
|
+
@filtered_locations = @inclusion_filter.map { |item| item.location }
|
96
|
+
locations = @filtered_locations.to_json
|
97
|
+
File.write(@locations_file, locations)
|
98
|
+
end
|
99
|
+
|
100
|
+
def prepare_location_arguments
|
101
|
+
@rerun_arguments += " #{@filtered_locations.join(' ')}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def prepare_description_arguments
|
105
|
+
return if @inclusion_filter.empty?
|
106
|
+
|
107
|
+
contains_singles = @filtered_descriptions.select { |desc| desc.include?("'") }
|
108
|
+
included = @filtered_descriptions - contains_singles
|
109
|
+
return contains_singles.to_s if included.empty?
|
110
|
+
|
111
|
+
included = included.to_s.gsub('"', "'")
|
112
|
+
return included if contains_singles.empty?
|
113
|
+
|
114
|
+
contains_singles.map!(&:dump)
|
115
|
+
included[-1] = ", #{contains_singles.join(', ')}]"
|
116
|
+
included
|
117
|
+
end
|
118
|
+
|
119
|
+
def clear_filter
|
120
|
+
return if @inclusion_filter.empty?
|
121
|
+
|
122
|
+
@inclusion_filter.each { |item| item.metadata[:include] = nil }
|
123
|
+
@inclusion_filter = []
|
124
|
+
return if @instructions
|
125
|
+
|
126
|
+
@example_display ? display_example : top_level_list
|
127
|
+
end
|
128
|
+
|
129
|
+
def top_fail!
|
130
|
+
@inclusion_filter = []
|
131
|
+
filter_include(@failed.first)
|
132
|
+
rerun
|
133
|
+
end
|
134
|
+
|
135
|
+
def check_inclusion_status(item)
|
136
|
+
if @last_run_descriptions.include?(item.metadata[:full_description])
|
137
|
+
@inclusion_filter << item
|
138
|
+
item.metadata[:include] = true
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|