cuke_sniffer 0.0.1

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.
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cuke_sniffer'
3
+
4
+ if ARGV.include? "-h" or ARGV.include? "--help"
5
+ puts HELP_CMD_TEXT
6
+ exit
7
+ end
8
+
9
+ @cuke_sniffer = nil
10
+ if (ARGV[0] != nil and File.directory?(ARGV[0])) and (ARGV[1] != nil and File.directory?(ARGV[1]))
11
+ @cuke_sniffer = CukeSniffer::CLI.new(ARGV[0], ARGV[1])
12
+ else
13
+ @cuke_sniffer = CukeSniffer::CLI.new
14
+ end
15
+
16
+ def print_results
17
+ puts @cuke_sniffer.output_results
18
+ end
19
+
20
+ if ARGV.include? "--out" or ARGV.include? "-o"
21
+ index = ARGV.index("--out")
22
+ index ||= ARGV.index("-o")
23
+ out_type = ARGV[index + 1]
24
+ case out_type
25
+ when "html"
26
+ @cuke_sniffer.output_html
27
+ else
28
+ print_results
29
+ end
30
+ else
31
+ print_results
32
+ end
33
+
34
+ puts "Completed Sniffing."
@@ -0,0 +1,13 @@
1
+
2
+ require 'cuke_sniffer/constants'
3
+ require 'cuke_sniffer/rule_config'
4
+ require 'cuke_sniffer/rules_evaluator'
5
+ require 'cuke_sniffer/feature_rules_evaluator'
6
+ require 'cuke_sniffer/feature'
7
+ require 'cuke_sniffer/scenario'
8
+ require 'cuke_sniffer/step_definition'
9
+ require 'cuke_sniffer/cli'
10
+
11
+ module CukeSniffer
12
+
13
+ end
@@ -0,0 +1,226 @@
1
+ require 'erb'
2
+ require 'cuke_sniffer/constants'
3
+
4
+ module CukeSniffer
5
+ class CLI
6
+ include CukeSniffer::Constants
7
+
8
+ attr_accessor :features, :step_definitions, :summary
9
+
10
+ def initialize(features_location = Dir.getwd, step_definitions_location = Dir.getwd)
11
+ @features_location = features_location
12
+ @step_definitions_location = step_definitions_location
13
+ @features = []
14
+ @step_definitions = []
15
+
16
+ puts "\nFeatures:"
17
+ unless features_location.nil?
18
+ if File.file?(features_location)
19
+ @features = [Feature.new(features_location)]
20
+ else
21
+ build_file_list_from_folder(features_location, ".feature").each { |location|
22
+ @features << Feature.new(location)
23
+ print '.'
24
+ }
25
+ end
26
+ end
27
+
28
+ puts("\nStep Definitions:")
29
+ unless step_definitions_location.nil?
30
+ if File.file?(step_definitions_location)
31
+ @step_definitions = [build_step_definitions(step_definitions_location)]
32
+ else
33
+ build_file_list_from_folder(step_definitions_location, "steps.rb").each { |location|
34
+ @step_definitions << build_step_definitions(location)
35
+ print '.'
36
+ }
37
+ end
38
+ end
39
+
40
+ @step_definitions.flatten!
41
+ @summary = {
42
+ :total_score => 0,
43
+ :features => {},
44
+ :step_definitions => {},
45
+ :improvement_list => {}
46
+ }
47
+ puts "\nCataloging Step Calls: "
48
+ catalog_step_calls
49
+ puts "\nAssessing Score: "
50
+ assess_score
51
+ end
52
+
53
+ def build_file_list_from_folder(folder_name, extension)
54
+ list = []
55
+ Dir.entries(folder_name).each_entry do |file_name|
56
+ unless FILE_IGNORE_LIST.include?(file_name)
57
+ file_name = "#{folder_name}/#{file_name}"
58
+ if File.directory?(file_name)
59
+ list << build_file_list_from_folder(file_name, extension)
60
+ elsif file_name.include?(extension)
61
+ list << file_name
62
+ end
63
+ end
64
+ end
65
+ list.flatten
66
+ end
67
+
68
+ def build_step_definitions(file_name)
69
+ step_file_lines = []
70
+ step_file = File.open(file_name)
71
+ step_file.each_line { |line| step_file_lines << line }
72
+ step_file.close
73
+
74
+ counter = 0
75
+ step_code = []
76
+ step_definitions = []
77
+ found_first_step = false
78
+ until counter >= step_file_lines.length
79
+ if step_file_lines[counter] =~ STEP_DEFINITION_REGEX and !step_code.empty? and found_first_step
80
+ step_definitions << StepDefinition.new("#{file_name}:#{counter+1 - step_code.count}", step_code)
81
+ step_code = []
82
+ end
83
+ found_first_step = true if step_file_lines[counter] =~ STEP_DEFINITION_REGEX
84
+ step_code << step_file_lines[counter].strip
85
+ counter+=1
86
+ end
87
+ step_definitions << StepDefinition.new("#{file_name}:#{counter+1}", step_code) unless step_code.empty?
88
+ step_definitions
89
+ end
90
+
91
+ def assess_array(array)
92
+ min, max, min_file, max_file = nil
93
+ total = 0
94
+ array.each do |node|
95
+ score = node.score
96
+ @summary[:total_score] += score
97
+ node.rules_hash.each_key do |key|
98
+ @summary[:improvement_list][key] ||= 0
99
+ @summary[:improvement_list][key] += node.rules_hash[key]
100
+ end
101
+ min, min_file = score, node.location if (min.nil? or score < min)
102
+ max, max_file = score, node.location if (max.nil? or score > max)
103
+ total += score
104
+ end
105
+ {
106
+ :total => array.count,
107
+ :min => min,
108
+ :min_file => min_file,
109
+ :max => max,
110
+ :max_file => max_file,
111
+ :average => (total.to_f/array.count.to_f).round(2)
112
+ }
113
+ end
114
+
115
+ def assess_score
116
+ @summary[:features] = assess_array(@features)
117
+ @summary[:step_definitions] = assess_array(@step_definitions) unless @step_definitions.empty?
118
+ sort_improvement_list
119
+ end
120
+
121
+ def sort_improvement_list
122
+ sorted_array = @summary[:improvement_list].sort_by { |improvement, occurrence| occurrence }
123
+ @summary[:improvement_list] = {}
124
+ sorted_array.reverse.each { |node|
125
+ @summary[:improvement_list][node[0]] = node[1]
126
+ }
127
+ end
128
+
129
+ def output_results
130
+ feature_results = @summary[:features]
131
+ step_definition_results = @summary[:step_definitions]
132
+ #todo this string is completely dependent on the tabbing in the string
133
+ output = "Suite Summary
134
+ Total Score: #{@summary[:total_score]}
135
+ Features (#@features_location)
136
+ Min: #{feature_results[:min]} (#{feature_results[:min_file]})
137
+ Max: #{feature_results[:max]} (#{feature_results[:max_file]})
138
+ Average: #{feature_results[:average]}
139
+ Step Definitions (#@step_definitions_location)
140
+ Min: #{step_definition_results[:min]} (#{step_definition_results[:min_file]})
141
+ Max: #{step_definition_results[:max]} (#{step_definition_results[:max_file]})
142
+ Average: #{step_definition_results[:average]}
143
+ Improvements to make:"
144
+ create_improvement_list.each { |item| output << "\n #{item}" }
145
+ output
146
+ end
147
+
148
+ def create_improvement_list
149
+ output = []
150
+ @summary[:improvement_list].each_key { |improvement| output << "(#{summary[:improvement_list][improvement]})#{improvement}" }
151
+ output
152
+ end
153
+
154
+ def get_all_steps
155
+ steps = {}
156
+ @features.each do |feature|
157
+ feature.scenarios.each do |scenario|
158
+ counter = 1
159
+ scenario.steps.each do |step|
160
+ location = scenario.location.gsub(/:\d*/, ":#{scenario.start_line + counter}")
161
+ steps[location] = step
162
+ counter += 1
163
+ end
164
+ end
165
+ end
166
+ @step_definitions.each do |definition|
167
+ definition.nested_steps.each_key do |key|
168
+ steps[key] = definition.nested_steps[key]
169
+ end
170
+ end
171
+ steps
172
+ end
173
+
174
+ def catalog_step_calls
175
+ steps = get_all_steps
176
+ @step_definitions.each do |step_definition|
177
+ print '.'
178
+ calls = steps.find_all { |location, step| step.gsub(STEP_STYLES, "") =~ step_definition.regex }
179
+ calls.each { |call|
180
+ step_definition.add_call(call[0], call[1])
181
+ }
182
+ end
183
+ end
184
+
185
+ def update_step_definition(location, step)
186
+ @step_definitions.each do |step_definition|
187
+ if step.gsub(STEP_STYLES, "") =~ step_definition.regex
188
+ step_definition.add_call(location, step)
189
+ break
190
+ end
191
+ end
192
+ end
193
+
194
+ def get_dead_steps
195
+ dead_steps = []
196
+ @step_definitions.each do |step_definition|
197
+ dead_steps << step_definition if step_definition.calls.empty?
198
+ end
199
+ dead_steps
200
+ end
201
+
202
+ def sort_by_score(array)
203
+ array.sort_by { |item| item.score }.reverse
204
+ end
205
+
206
+ def extract_markup
207
+ markup_location = File.join(File.dirname(__FILE__), 'report', 'markup.rhtml')
208
+ markup = ""
209
+ File.open(markup_location).lines.each do |line|
210
+ markup << line
211
+ end
212
+ markup
213
+ end
214
+
215
+ def output_html(file_name = "cuke_sniffer_results.html", cuke_sniffer = self)
216
+ @features = sort_by_score(@features)
217
+ @step_definitions = sort_by_score(@step_definitions)
218
+
219
+ markup_erb = ERB.new extract_markup
220
+ output = markup_erb.result(binding)
221
+ File.open(file_name, 'w') do |f|
222
+ f.write(output)
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,35 @@
1
+ module CukeSniffer
2
+ module Constants
3
+ FILE_IGNORE_LIST = %w(. .. .svn)
4
+ DATE_REGEX = /(?<date>\d{2}\/\d{2}\/\d{4})/
5
+ COMMENT_REGEX = /#?.*/
6
+
7
+ FEATURE_NAME_REGEX = /Feature:\s*(?<name>.*)/
8
+ TAG_REGEX = /(?<tag>@\S*)/
9
+ SCENARIO_TITLE_STYLES = /(?<type>Background|Scenario|Scenario Outline|Scenario Template):\s*/
10
+ SCENARIO_TITLE_REGEX = /#{COMMENT_REGEX}#{SCENARIO_TITLE_STYLES}(?<name>.*)/
11
+
12
+ STEP_STYLES = /(?<style>Given|When|Then|And|Or|But|Transform|\*)\s/
13
+ STEP_REGEX = /^#{COMMENT_REGEX}#{STEP_STYLES}(?<step_string>.*)/
14
+ STEP_DEFINITION_REGEX = /^#{STEP_STYLES}\/(?<step>.+)\/\sdo\s?(\|(?<parameters>.*)\|)?$/
15
+
16
+ SIMPLE_NESTED_STEP_REGEX = /steps\s"#{STEP_STYLES}(?<step_string>.*)"/
17
+ SAME_LINE_COMPLEX_STEP_REGEX = /^steps\s%Q?{#{STEP_STYLES}(?<step_string>.*)}/
18
+ START_COMPLEX_STEP_REGEX = /steps\s%Q?\{\s*/
19
+ END_COMPLEX_STEP_REGEX = /}/
20
+ START_COMPLEX_WITH_STEP_REGEX = /steps\s%Q?\{#{STEP_STYLES}(?<step_string>.*)/
21
+ END_COMPLEX_WITH_STEP_REGEX = /#{STEP_STYLES}(?<step_string>.*)}/
22
+
23
+ HELP_CMD_TEXT = "Welcome to CukeSniffer!
24
+ Calling CukeSniffer with no arguments will run it against the current directory.
25
+ Other Options for Running include:
26
+ <feature_file_path>, <step_def_file_path> : Runs CukeSniffer against the
27
+ specified paths.
28
+ -o, --out html (name) : Runs CukeSniffer then outputs an
29
+ html file in the current
30
+ directory (with optional name).
31
+ -h, --help : You get this lovely document."
32
+
33
+
34
+ end
35
+ end
@@ -0,0 +1,116 @@
1
+ require 'cuke_sniffer/constants'
2
+ require 'cuke_sniffer/rule_config'
3
+ require 'cuke_sniffer/feature_rules_evaluator'
4
+ require 'cuke_sniffer/scenario'
5
+
6
+ module CukeSniffer
7
+ class Feature < FeatureRulesEvaluator
8
+ include CukeSniffer::Constants
9
+ include CukeSniffer::RuleConfig
10
+
11
+ attr_accessor :background, :scenarios, :feature_rules_hash
12
+
13
+ def initialize(file_name)
14
+ super(file_name)
15
+ @scenarios = []
16
+ @feature_rules_hash = {}
17
+ split_feature(file_name)
18
+ evaluate_score
19
+ end
20
+
21
+ def split_feature(file_name)
22
+ feature_lines = []
23
+
24
+ feature_file = File.open(file_name)
25
+ feature_file.each_line { |line| feature_lines << line }
26
+ feature_file.close
27
+
28
+ index = 0
29
+ until feature_lines[index].match FEATURE_NAME_REGEX
30
+ update_tag_list(feature_lines[index])
31
+ index += 1
32
+ end
33
+
34
+ until index >= feature_lines.length or feature_lines[index].match TAG_REGEX or feature_lines[index].match SCENARIO_TITLE_REGEX
35
+ create_name(feature_lines[index], "Feature:")
36
+ index += 1
37
+ end
38
+
39
+ scenario_title_found = false
40
+ index_of_title = nil
41
+ code_block = []
42
+ until index >= feature_lines.length
43
+ if scenario_title_found and (feature_lines[index].match TAG_REGEX or feature_lines[index].match SCENARIO_TITLE_REGEX)
44
+ add_scenario_to_feature(code_block, index_of_title)
45
+ scenario_title_found = false
46
+ code_block = []
47
+ end
48
+ code_block << feature_lines[index].strip
49
+ if feature_lines[index].match SCENARIO_TITLE_REGEX
50
+ scenario_title_found = true
51
+ index_of_title = "#{file_name}:#{index + 1}"
52
+ end
53
+ index += 1
54
+ end
55
+ #TODO - Last scenario falling through above logic, needs a fix (code_block related)
56
+ add_scenario_to_feature(code_block, index_of_title) unless code_block==[]
57
+ end
58
+
59
+ def add_scenario_to_feature(code_block, index_of_title)
60
+ scenario = CukeSniffer::Scenario.new(index_of_title, code_block)
61
+ if scenario.type == "Background"
62
+ @background = scenario
63
+ else
64
+ @scenarios << scenario
65
+ end
66
+ end
67
+
68
+ def ==(comparison_object)
69
+ super(comparison_object)
70
+ comparison_object.scenarios == scenarios
71
+ end
72
+
73
+ def evaluate_score
74
+ super
75
+ rule_no_scenarios
76
+ rule_too_many_scenarios
77
+ rule_background_with_no_scenarios
78
+ rule_background_with_one_scenario
79
+ @feature_rules_hash = @rules_hash.clone
80
+ include_sub_scores(@background) unless @background.nil?
81
+ include_scenario_scores
82
+ end
83
+
84
+ def include_sub_scores(sub_class)
85
+ @score += sub_class.score
86
+ sub_class.rules_hash.each_key do |rule_descriptor|
87
+ rules_hash[rule_descriptor] ||= 0
88
+ rules_hash[rule_descriptor] += sub_class.rules_hash[rule_descriptor]
89
+ end
90
+ end
91
+
92
+ def include_scenario_scores
93
+ scenarios.each do |scenario|
94
+ include_sub_scores(scenario)
95
+ end
96
+ end
97
+
98
+ def rule_no_scenarios
99
+ store_rule(FEATURE_RULES[:no_scenarios]) if @scenarios.empty?
100
+ end
101
+
102
+ def rule_too_many_scenarios
103
+ rule = FEATURE_RULES[:too_many_scenarios]
104
+ store_rule(rule) if @scenarios.size >= rule[:max]
105
+ end
106
+
107
+ def rule_background_with_no_scenarios
108
+ store_rule( FEATURE_RULES[:background_with_no_scenarios]) if @scenarios.empty? and !@background.nil?
109
+ end
110
+
111
+ def rule_background_with_one_scenario
112
+ store_rule(FEATURE_RULES[:background_with_one_scenario]) if @scenarios.size == 1 and !@background.nil?
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,69 @@
1
+ require 'cuke_sniffer/constants'
2
+ require 'cuke_sniffer/rule_config'
3
+ require 'cuke_sniffer/rules_evaluator'
4
+
5
+ module CukeSniffer
6
+ class FeatureRulesEvaluator < RulesEvaluator
7
+ include CukeSniffer::Constants
8
+ include CukeSniffer::RuleConfig
9
+
10
+ attr_accessor :tags, :name
11
+
12
+ def initialize(location)
13
+ @name = ""
14
+ @tags = []
15
+ super(location)
16
+ end
17
+
18
+ def create_name(line, filter)
19
+ line.gsub!(/#{COMMENT_REGEX}#{filter}/, "")
20
+ line.strip!
21
+ @name += " " unless @name.empty? or line.empty?
22
+ @name += line
23
+ end
24
+
25
+ def update_tag_list(line)
26
+ if TAG_REGEX.match(line) && !is_comment?(line)
27
+ line.scan(TAG_REGEX).each { |tag| @tags << tag[0] }
28
+ else
29
+ @tags << line.strip unless line.empty?
30
+ end
31
+ end
32
+
33
+ def evaluate_score
34
+ super
35
+ cls_name = self.class.to_s.gsub('CukeSniffer::', '')
36
+ rule_too_many_tags(cls_name)
37
+ rule_no_description(cls_name)
38
+ rule_numbers_in_name(cls_name)
39
+ rule_long_name(cls_name)
40
+ end
41
+
42
+ def rule_too_many_tags(type)
43
+ rule = SHARED_RULES[:too_many_tags]
44
+ store_updated_rule(rule, rule[:phrase].gsub(/{.*}/, type)) if tags.size >= rule[:max]
45
+ end
46
+
47
+ def rule_no_description(type)
48
+ rule = SHARED_RULES[:no_description]
49
+ store_updated_rule(rule, rule[:phrase].gsub(/{.*}/, type)) if name.empty?
50
+ end
51
+
52
+ def rule_numbers_in_name(type)
53
+ rule = SHARED_RULES[:numbers_in_description]
54
+ store_updated_rule(rule, rule[:phrase].gsub(/{.*}/, type)) if name =~ /\d/
55
+ end
56
+
57
+ def rule_long_name(type)
58
+ rule = SHARED_RULES[:long_name]
59
+ store_updated_rule(rule, rule[:phrase].gsub(/{.*}/, type)) if name.size >= rule[:max]
60
+ end
61
+
62
+ def == (comparison_object)
63
+ super(comparison_object)
64
+ comparison_object.name == name
65
+ comparison_object.tags == tags
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,311 @@
1
+ <script>
2
+ function toggle(item, off) {
3
+ updateDisplayStatus(item.getElementsByTagName("div")[off]);
4
+ }
5
+ function toggleById(item, link) {
6
+ updateDisplayStatus(document.getElementById(item));
7
+ toggleText(link)
8
+ }
9
+ function updateDisplayStatus(object) {
10
+ object.style.display = (object.style.display == "block") ? 'none' : "block";
11
+ }
12
+ function toggleText(link) {
13
+ var char_result = link.innerHTML.indexOf("+") > -1 ? "-" : "+";
14
+ link.innerHTML = link.innerHTML.replace(/(\+|\-)/, char_result)
15
+ }
16
+
17
+ </script>
18
+ <style>
19
+ body {
20
+ font-size: 12pt;
21
+ font-family: arial;
22
+ }
23
+
24
+ .padded {
25
+ padding-left: 3%;
26
+ }
27
+
28
+ .sections {
29
+ border: 2px solid #90ee90;
30
+ }
31
+
32
+ .shrink_section {
33
+ display: none;
34
+ }
35
+
36
+ .sub_title {
37
+ font-weight: bold;
38
+ font-size: 13pt;
39
+ padding: 1%;
40
+ }
41
+
42
+ .scenarios, .steps, {
43
+ border-top: 1px solid #bbb;
44
+ padding: 4px;
45
+ padding-left: 90px;
46
+ }
47
+
48
+ .scenarios, .steps {
49
+ border-top: 1px solid #bbb;
50
+ padding: 4px;
51
+ padding-left: 90px;
52
+ }
53
+
54
+ .title {
55
+ padding-left: 2%;
56
+ width: 25%;
57
+ background-color: green;
58
+ color: white;
59
+ border-top: 0;
60
+ border-bottom: 0;
61
+ padding-top: 4px;
62
+ font-weight: bold;
63
+ font-size: 15pt;
64
+ padding-bottom: 1%;
65
+ }
66
+
67
+ .rule {
68
+ background-color: #ffaaaa;
69
+ padding: 3px;
70
+ font-size: 13px;
71
+ }
72
+
73
+ span {
74
+ font-size: 20pt;
75
+ font-weight: bold;
76
+ width: 100%;
77
+ border-left: 2px solid #fff;
78
+ padding-bottom: 3px;
79
+ padding-left: 30px;
80
+ padding-top: 4px
81
+ }
82
+
83
+ .even {
84
+ background-color: #eeeeff;
85
+ border-bottom: 1px solid #ccc;
86
+ }
87
+
88
+ .odd {
89
+ border-bottom: 1px solid #ddd;
90
+ }
91
+ </style>
92
+ <div style="float:left; color: green; font-weight: bold; padding-left:10px;">
93
+ Cuke Sniffer
94
+ </div>
95
+ <br style="clear:both">
96
+ <div style="border-top: 2px solid lightgreen;">
97
+ <div style="float:right;border-right: 2px solid lightgreen;">
98
+ <div style="background-color:green;border-right:2px solid #fff; color:white;padding:4px; padding-left:40px;font-size:11pt;font-weight:bold;padding-right:10px;">
99
+ <%= summary[:total_score] %> Total Score
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <br style="clear:both">
104
+ <div style="float:left;" class="title">
105
+ <a href=#summary style="color: white;font-style: normal;">
106
+ <span style="border-left-color:green;cursor: hand;" onclick="toggleById('summary',this)">- Summary</span>
107
+ </a>
108
+ </div>
109
+ <br style="clear:both">
110
+
111
+ <div class="sections">
112
+ <div name="summary" class="shrink_section" id="summary" style="display:block">
113
+ <%
114
+ feature_results = cuke_sniffer.summary[:features]
115
+ step_definition_results = cuke_sniffer.summary[:step_definitions]
116
+ %>
117
+ <br>
118
+
119
+ <div><span class="sub_title">Features</span><br></div>
120
+ <div class="padded">Min: <%= feature_results[:min] %> (<%= feature_results[:min_file] %>)<br>
121
+ Max: <%= feature_results[:max] %> (<%= feature_results[:max_file] %>)<br>
122
+ Average: <%= feature_results[:average] %>
123
+ </div>
124
+ <br>
125
+
126
+ <div><span class="sub_title">Step Definitions</span><br></div>
127
+ <div class="padded">Min: <%= step_definition_results[:min] %> (<%= step_definition_results[:min_file] %>)<br>
128
+ Max: <%= step_definition_results[:max] %> (<%= step_definition_results[:max_file] %>)<br>
129
+ Average: <%= step_definition_results[:average] %> </div>
130
+ <br>
131
+
132
+ <% list = summary[:improvement_list] %>
133
+ <div><span class="sub_title">Improvements to make: <%= list.size %></span><br></div>
134
+ <div class="padded">
135
+ <% list.each_key do |improvement| %>
136
+ (<%= list[improvement] %>) <%= improvement %>
137
+ <br>
138
+ <% end %>
139
+ <br>
140
+ </div>
141
+ <% dead_steps_list = cuke_sniffer.get_dead_steps %>
142
+ <div><span class="sub_title">Dead Steps: <%= dead_steps_list.size %></span><br></div>
143
+ <div class="padded">
144
+ <% dead_steps_list.each do |dead_step| %>
145
+ <table>
146
+ <tr>
147
+ <td style="width:300px">
148
+ /<%= dead_step.regex.to_s.match(/\(\?\-mix\:(?<regex>.*)\)/)[:regex] %>/
149
+ </td>
150
+ <td style="width:600px">
151
+ <%= dead_step.location %>
152
+ </td>
153
+ </tr>
154
+ </table>
155
+ <% end %>
156
+ <br>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ <br><br>
161
+ <div style="float:left;" class="title">
162
+ <a href=#feature_details style="color: white;font-style: normal;">
163
+ <span style="border-left-color:green;cursor: hand;" onclick="toggleById('feature_details',this)">+ Features</span>
164
+ </a>
165
+ </div>
166
+ <div class="links" style="float:right"></div>
167
+ <br style="clear:both">
168
+ <div class="sections">
169
+ <div name="feature_details" class="shrink_section" id="feature_details">
170
+ <% i=0 %>
171
+ <% cuke_sniffer.features.each do |feature| %>
172
+ <% next if feature.score == 0 %>
173
+ <div style="cursor: hand;" onclick="toggle(this,3)" class="feature <%= i%2==0 ? "even" : "odd" %>">
174
+ <div style="float:left;padding:4px;font-weight:bold;color:red; width:3%"><%= feature.score %></div>
175
+
176
+ <div style=" float:left;">
177
+ <font class="featurename"><%= feature.location[feature.location.rindex("/")..-1] %></font></div>
178
+ <div style="width:60%;padding:4px; float:right">
179
+ <font class="featurepath"><%= feature.location[0..feature.location.rindex("/")] %></font></div>
180
+ <br style="clear:both;">
181
+
182
+ <div class="scenarios" style="display:none;">
183
+
184
+ <table>
185
+ <tr>
186
+ <td style="width: 600px;vertical-align:top;">
187
+ <b style="text-indent:5px">Feature: <%= feature.name %></b>
188
+ <br>
189
+ <b style="text-indent:5px">Scenarios: <%= feature.scenarios.size %></b>
190
+ </td>
191
+ <td style="width: 600px;vertical-align:top;">
192
+ <% unless feature.feature_rules_hash.empty? %>
193
+ <b>Feature Improvements:</b>
194
+
195
+ <div style="padding: 1%">
196
+ <% feature.feature_rules_hash.each_key do |rule| %>
197
+ <%= rule %>
198
+ <br>
199
+ <% end %>
200
+ </div>
201
+ <% end %>
202
+ </td>
203
+ </tr>
204
+ </table>
205
+ <br>
206
+ <% feature.scenarios = [feature.background, feature.scenarios].flatten unless feature.background.nil? %>
207
+ <table style="border-bottom:2px solid lightgreen">
208
+ <% feature.scenarios.each do |scenario| %>
209
+ <% next if scenario.rules_hash.empty? %>
210
+ <tr>
211
+ <td style="border-top:2px solid lightblue;width: 600px;vertical-align:top;">
212
+ <b style="padding:1%;"><%= scenario.type %>: <%= scenario.name %></b>
213
+ <br>
214
+ <% scenario.steps.each do |step| %>
215
+ <div style="text-indent:15px">
216
+ <%= step %>
217
+ </div>
218
+ <% end %>
219
+ <br>
220
+ </td>
221
+ <td style="border-top:2px solid lightblue;width: 600px;vertical-align:top;">
222
+ <b>Score: <%= scenario.score %></b>
223
+ <br>
224
+ <%= scenario.location %>
225
+ <br>
226
+ <b>Scenario Improvements:</b>
227
+ <br>
228
+ <% scenario.rules_hash.each_key do |rule| %>
229
+ <div style="text-indent:15px">
230
+ <%= rule %>
231
+ </div>
232
+ <% end %>
233
+ </td>
234
+ </tr>
235
+ <% end %>
236
+ </table>
237
+ </div>
238
+ </div>
239
+ <% i+=1 %>
240
+ <% end %>
241
+ </div>
242
+ </div>
243
+ <br><br>
244
+ <div style="float:left;" class="title">
245
+ <a href=#step_definitions style="color: white;font-style: normal;">
246
+ <span style="border-left-color:green;cursor: hand;" onclick="toggleById('step_definitions',this)">+ Step Definitions</span>
247
+ </a>
248
+ </div>
249
+ <div class="links" style="float:right"></div>
250
+ <br style="clear:both">
251
+ <div class="sections">
252
+ <div name="step_definitions" class="shrink_section" id="step_definitions">
253
+ <% i=0 %>
254
+ <% cuke_sniffer.step_definitions.each do |step_definition| %>
255
+ <div style="cursor: hand;" onclick="toggle(this,4)" class="feature <%= i%2==0 ? "even" : "odd" %>">
256
+ <% next if step_definition.score <= 0 and !step_definition.calls.empty? %>
257
+ <div style="float:left;padding:4px;font-weight:bold;color:red;"><%= step_definition.score %></div>
258
+ <div style="width:55%;padding:4px; float:left;">
259
+
260
+ <% parameters = "" %>
261
+ <% step_definition.parameters.each do |param| %>
262
+ <% if param == step_definition.parameters.first %>
263
+ <% parameters << "| " %>
264
+ <% end %>
265
+ <% parameters << param %>
266
+
267
+ <% if param == step_definition.parameters.last %>
268
+ <% parameters << " |" %>
269
+ <% else %>
270
+ <% parameters << ", " %>
271
+ <% end %>
272
+ <% end %>
273
+ <font class="featurename"><%= "/" + step_definition.regex.to_s.match(/\(\?\-mix\:(?<regex>.*)\)/)[:regex] + "/ do " + parameters %></font>
274
+ </div>
275
+ <% if step_definition.calls.empty? %>
276
+ <div style="float:left; width:10%;padding:4px;"><span class="rule"><b>Dead step!</b></span></div>
277
+ <% else %>
278
+ <div style="float:left; width:10%;padding:4px;">&nbsp;</div>
279
+ <% end %>
280
+ <div style="width:30%;padding:4px; float:left;">
281
+ <font class="featurepath"><%= step_definition.location[step_definition.location.rindex("/")..-1] %></font>
282
+ </div>
283
+ <br style="clear:both;">
284
+
285
+ <div class="steps" style="display:none;">
286
+ <table>
287
+ <tr>
288
+ <td style="width: 600px;vertical-align:top;">
289
+ <b>Calls: <%= step_definition.calls.size %></b>
290
+ <% step_definition.calls.each_key do |call| %>
291
+ <div style="text-indent: 15px;"><%= step_definition.calls[call] %></div>
292
+ <div style="text-indent: 30px;"><%= call %></div>
293
+ <% end %>
294
+ </td>
295
+ <td style="width: 600px;vertical-align:top;">
296
+ <b>Step Definition improvements:</b>
297
+ <br>
298
+ <% step_definition.rules_hash.each_key do |improvement| %>
299
+ <div style="text-indent: 15px;">
300
+ (<%= step_definition.rules_hash[improvement] %>) <%= improvement %>
301
+ </div>
302
+ <% end %>
303
+ </td>
304
+ </tr>
305
+ </table>
306
+ </div>
307
+ </div>
308
+ <% i+=1 %>
309
+ <% end %>
310
+ </div>
311
+ </div>
@@ -0,0 +1,161 @@
1
+
2
+
3
+ module CukeSniffer
4
+ module RuleConfig
5
+
6
+ FATAL = 100 #will prevent suite from executing properly
7
+ ERROR = 25 #will cause problem with debugging
8
+ WARNING = 10 #readibility/misuse of cucumber
9
+ INFO = 1 #Small improvements that can be made
10
+
11
+ SHARED_RULES = {
12
+ :too_many_tags => {
13
+ :enabled => true,
14
+ :phrase => "{class} has too many tags.",
15
+ :score => INFO,
16
+ :max => 8
17
+ },
18
+ :no_description => {
19
+ :enabled => true,
20
+ :phrase => "{class} has no description.",
21
+ :score => ERROR,
22
+ },
23
+ :numbers_in_description => {
24
+ :enabled => true,
25
+ :phrase => "{class} has numbers in the description.",
26
+ :score => WARNING,
27
+ },
28
+ :long_name => {
29
+ :enabled => true,
30
+ :phrase => "{class} has a long description.",
31
+ :score => INFO,
32
+ :max => 180
33
+ },
34
+ :implementation_word => {
35
+ :enabled => true,
36
+ :phrase => "Implementation word used: {word}.",
37
+ :score => INFO,
38
+ :words => ["page", "site", "url", "button", "drop down", "dropdown", "select list", "click", "text box", "radio button", "check box", "xml", "window", "pop up", "pop-up", "screen"]
39
+ }
40
+ }
41
+
42
+ FEATURE_RULES = {
43
+ :background_with_no_scenarios => {
44
+ :enabled => true,
45
+ :phrase => "Feature has a background with no scenarios.",
46
+ :score => WARNING,
47
+ },
48
+ :background_with_one_scenario => {
49
+ :enabled => true,
50
+ :phrase => "Feature has a background with one scenario.",
51
+ :score => WARNING,
52
+ },
53
+ :no_scenarios => {
54
+ :enabled => true,
55
+ :phrase => "Feature with no scenarios.",
56
+ :score => ERROR,
57
+ },
58
+ :too_many_scenarios => {
59
+ :enabled => true,
60
+ :phrase => "Feature with too many scenarios.",
61
+ :score => INFO,
62
+ :max => 10,
63
+ },
64
+ }
65
+
66
+ SCENARIO_RULES = {
67
+ :too_many_steps => {
68
+ :enabled => true,
69
+ :phrase => "Scenario with too many steps.",
70
+ :score => WARNING,
71
+ :max => 7,
72
+ },
73
+ :out_of_order_steps => {
74
+ :enabled => true,
75
+ :phrase => "Scenario steps out of Given/When/Then order.",
76
+ :score => WARNING,
77
+ },
78
+ :invalid_first_step => {
79
+ :enabled => true,
80
+ :phrase => "Invalid first step. Began with And/But.",
81
+ :score => WARNING,
82
+ },
83
+ :asterisk_step => {
84
+ :enabled => true,
85
+ :phrase => "Step includes a * instead of Given/When/Then/And/But.",
86
+ :score => WARNING,
87
+ },
88
+ :commented_step => {
89
+ :enabled => true,
90
+ :phrase => "Commented step.",
91
+ :score => ERROR,
92
+ },
93
+ :commented_example => {
94
+ :enabled => true,
95
+ :phrase => "Commented example.",
96
+ :score => ERROR,
97
+ },
98
+ :no_examples => {
99
+ :enabled => true,
100
+ :phrase => "Scenario Outline with no examples.",
101
+ :score => FATAL,
102
+ },
103
+ :one_example => {
104
+ :enabled => true,
105
+ :phrase => "Scenario Outline with only one example.",
106
+ :score => WARNING,
107
+ },
108
+ :no_examples_table => {
109
+ :enabled => true,
110
+ :phrase => "Scenario Outline with no examples table.",
111
+ :score => FATAL,
112
+ },
113
+ :too_many_examples => {
114
+ :enabled => true,
115
+ :phrase => "Scenario Outline with too many examples.",
116
+ :score => WARNING,
117
+ :max => 10
118
+ },
119
+ :date_used => {
120
+ :enabled => true,
121
+ :phrase => "Date used.",
122
+ :score => INFO,
123
+ },
124
+ :no_steps => {
125
+ :enabled => true,
126
+ :phrase => "No steps in Scenario.",
127
+ :score => ERROR,
128
+ },
129
+ }
130
+
131
+ STEP_DEFINITION_RULES = {
132
+ :no_code => {
133
+ :enabled => true,
134
+ :phrase => "No code in Step Definition.",
135
+ :score => ERROR,
136
+ },
137
+ :too_many_parameters => {
138
+ :enabled => true,
139
+ :phrase => "Too many parameters in Step Definition.",
140
+ :score => WARNING,
141
+ :max => 4
142
+ },
143
+ :nested_step => {
144
+ :enabled => true,
145
+ :phrase => "Nested step call.",
146
+ :score => INFO,
147
+ },
148
+ :recursive_nested_step => {
149
+ :enabled => true,
150
+ :phrase => "Recursive nested step call.",
151
+ :score => FATAL,
152
+ },
153
+ :commented_code => {
154
+ :enabled => true,
155
+ :phrase => "Commented code in Step Definition.",
156
+ :score => INFO,
157
+ },
158
+ }
159
+
160
+ end
161
+ end
@@ -0,0 +1,41 @@
1
+ module CukeSniffer
2
+ class RulesEvaluator
3
+ attr_accessor :location, :score, :rules_hash
4
+
5
+ def initialize(location)
6
+ @location = location
7
+ @score = 0
8
+ @rules_hash = {}
9
+ end
10
+
11
+ def evaluate_score
12
+ end
13
+
14
+ def store_rule(rule)
15
+ if rule[:enabled]
16
+ @score += rule[:score]
17
+ @rules_hash[rule[:phrase]] ||= 0
18
+ @rules_hash[rule[:phrase]] += 1
19
+ end
20
+ end
21
+
22
+ def store_updated_rule(rule, phrase)
23
+ store_rule({:enabled => rule[:enabled], :score => rule[:score], :phrase => phrase})
24
+ end
25
+
26
+
27
+ def is_comment?(line)
28
+ if line =~ /^\#.*$/
29
+ true
30
+ else
31
+ false
32
+ end
33
+ end
34
+
35
+ def == (comparison_object)
36
+ comparison_object.location == location
37
+ comparison_object.score == score
38
+ comparison_object.rules_hash == rules_hash
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,163 @@
1
+ require 'cuke_sniffer/constants'
2
+ require 'cuke_sniffer/rule_config'
3
+
4
+ module CukeSniffer
5
+ class Scenario < FeatureRulesEvaluator
6
+ include CukeSniffer::Constants
7
+ include CukeSniffer::RuleConfig
8
+
9
+ attr_accessor :start_line, :type, :steps, :inline_tables, :examples_table
10
+
11
+ def initialize(location, scenario)
12
+ super(location)
13
+ @start_line = location.match(/:(?<line>\d*)$/)[:line].to_i
14
+ @steps = []
15
+ @inline_tables = {}
16
+ @examples_table = []
17
+ split_scenario(scenario)
18
+ evaluate_score
19
+ end
20
+
21
+ def split_scenario(scenario)
22
+ index = 0
23
+ until index >= scenario.length or scenario[index] =~ SCENARIO_TITLE_STYLES
24
+ update_tag_list(scenario[index])
25
+ index += 1
26
+ end
27
+
28
+ until index >= scenario.length or scenario[index].match STEP_REGEX or scenario[index].include?("Examples:")
29
+ match = scenario[index].match(SCENARIO_TITLE_STYLES)
30
+ @type = match[:type] unless match.nil?
31
+ create_name(scenario[index], SCENARIO_TITLE_STYLES)
32
+ index += 1
33
+ end
34
+
35
+ until index >= scenario.length or scenario[index].include?("Examples:")
36
+ if scenario[index] =~ /\|.*\|/
37
+ step = scenario[index - 1]
38
+ @inline_tables[step] = []
39
+ until index >= scenario.length or scenario[index] =~ STEP_REGEX
40
+ @inline_tables[step] << scenario[index]
41
+ index += 1
42
+ end
43
+ index += 1
44
+ else
45
+ @steps << scenario[index] if scenario[index] =~ STEP_REGEX
46
+ index += 1
47
+ end
48
+ end
49
+
50
+ if index < scenario.length and scenario[index].include?("Examples:")
51
+ index += 1
52
+ until index >= scenario.length
53
+ @examples_table << scenario[index] unless scenario[index].empty?
54
+ index += 1
55
+ end
56
+ end
57
+ end
58
+
59
+ def ==(comparison_object)
60
+ super(comparison_object)
61
+ comparison_object.steps == steps
62
+ comparison_object.examples_table == examples_table
63
+ end
64
+
65
+ def get_step_order
66
+ order = []
67
+ @steps.each { |line|
68
+ match = line.match(STEP_REGEX)
69
+ order << match[:style] unless match.nil?
70
+ }
71
+ order
72
+ end
73
+
74
+ def evaluate_score
75
+ super
76
+ rule_empty_scenario
77
+ rule_too_many_steps
78
+ rule_step_order
79
+ rule_invalid_first_step
80
+ rule_asterisk_step
81
+ rule_commented_step
82
+ rule_implementation_words
83
+ rule_date_used_in_step
84
+ evaluate_outline_scores if type == "Scenario Outline"
85
+ end
86
+
87
+ def evaluate_outline_scores
88
+ rule_no_examples_table
89
+ rule_no_examples
90
+ rule_one_example
91
+ rule_too_many_examples
92
+ rule_commented_example
93
+ end
94
+
95
+ def rule_empty_scenario
96
+ store_rule(SCENARIO_RULES[:no_steps]) if @steps.empty?
97
+ end
98
+
99
+ def rule_too_many_steps
100
+ rule = SCENARIO_RULES[:too_many_steps]
101
+ store_rule(rule) if @steps.size >= rule[:max]
102
+ end
103
+
104
+ def rule_step_order
105
+ step_order = get_step_order.uniq
106
+ %w(But * And).each { |type| step_order.delete(type) }
107
+ store_rule(SCENARIO_RULES[:out_of_order_steps]) unless step_order == %w(Given When Then) or step_order == %w(When Then)
108
+ end
109
+
110
+ def rule_invalid_first_step
111
+ first_step = get_step_order.first
112
+ store_rule(SCENARIO_RULES[:invalid_first_step]) if %w(And But).include?(first_step)
113
+ end
114
+
115
+ def rule_asterisk_step
116
+ get_step_order.count('*').times { store_rule(SCENARIO_RULES[:asterisk_step]) }
117
+ end
118
+
119
+ def rule_commented_step
120
+ @steps.each do |step|
121
+ store_rule(SCENARIO_RULES[:commented_step]) if is_comment?(step)
122
+ end
123
+ end
124
+
125
+ def rule_implementation_words
126
+ rule = SHARED_RULES[:implementation_word]
127
+ @steps.each do |step|
128
+ rule[:words].each do |word|
129
+ store_updated_rule(rule, rule[:phrase].gsub(/{.*}/, word)) if step.include?(word)
130
+ end
131
+ end
132
+ end
133
+
134
+ def rule_date_used_in_step
135
+ @steps.each do |step|
136
+ store_rule(SCENARIO_RULES[:date_used]) if step =~ DATE_REGEX
137
+ end
138
+ end
139
+
140
+ def rule_no_examples_table
141
+ store_rule(SCENARIO_RULES[:no_examples_table]) if @examples_table.empty?
142
+ end
143
+
144
+ def rule_no_examples
145
+ store_rule(SCENARIO_RULES[:no_examples]) if @examples_table.size == 1
146
+ end
147
+
148
+ def rule_one_example
149
+ store_rule(SCENARIO_RULES[:one_example]) if @examples_table.size == 2 and !is_comment?(@examples_table[1])
150
+ end
151
+
152
+ def rule_too_many_examples
153
+ store_rule(SCENARIO_RULES[:too_many_examples]) if (@examples_table.size - 1) >= 8
154
+ end
155
+
156
+ def rule_commented_example
157
+ @examples_table.each do |example|
158
+ store_rule(SCENARIO_RULES[:commented_example]) if is_comment?(example)
159
+ end
160
+ end
161
+
162
+ end
163
+ end
@@ -0,0 +1,116 @@
1
+ require 'cuke_sniffer/constants'
2
+ require 'cuke_sniffer/rule_config'
3
+
4
+ module CukeSniffer
5
+ class StepDefinition < RulesEvaluator
6
+ include CukeSniffer::Constants
7
+ include CukeSniffer::RuleConfig
8
+
9
+ attr_accessor :start_line, :regex, :code, :parameters, :calls, :nested_steps
10
+
11
+ def initialize(location, raw_code)
12
+ super(location)
13
+
14
+ @parameters = []
15
+ @calls = {}
16
+ @nested_steps = {}
17
+ @start_line = location.match(/:(?<line>\d*)/)[:line].to_i
18
+
19
+ end_match_index = (raw_code.size - 1) - raw_code.reverse.index("end")
20
+ @code = raw_code[1...end_match_index]
21
+
22
+ raw_code.each do |line|
23
+ if line =~ STEP_DEFINITION_REGEX
24
+ matches = STEP_DEFINITION_REGEX.match(line)
25
+ @regex = Regexp.new(matches[:step])
26
+ @parameters = matches[:parameters].split(",") unless matches[:parameters].nil?
27
+ end
28
+ end
29
+
30
+ detect_nested_steps
31
+ evaluate_score
32
+ end
33
+
34
+ def add_call(location, step_string)
35
+ @calls[location] = step_string
36
+ end
37
+
38
+ def detect_nested_steps
39
+ multi_line_step_flag = false
40
+ counter = 1
41
+ @code.each do |line|
42
+ regex = nil
43
+ case line
44
+ when SIMPLE_NESTED_STEP_REGEX
45
+ regex = SIMPLE_NESTED_STEP_REGEX
46
+ when SAME_LINE_COMPLEX_STEP_REGEX
47
+ regex = SAME_LINE_COMPLEX_STEP_REGEX
48
+ when START_COMPLEX_WITH_STEP_REGEX
49
+ multi_line_step_flag = true
50
+ regex = START_COMPLEX_WITH_STEP_REGEX
51
+ when START_COMPLEX_STEP_REGEX
52
+ multi_line_step_flag = true
53
+ when END_COMPLEX_WITH_STEP_REGEX
54
+ regex = END_COMPLEX_WITH_STEP_REGEX
55
+ multi_line_step_flag = false
56
+ when STEP_REGEX
57
+ regex = STEP_REGEX if multi_line_step_flag
58
+ when END_COMPLEX_STEP_REGEX
59
+ multi_line_step_flag = false
60
+ else
61
+ end
62
+
63
+ if regex
64
+ match = regex.match(line)
65
+ nested_step_line = (@start_line + counter)
66
+ @nested_steps[location.gsub(/:\d*/, ":" + nested_step_line.to_s)] = match[:step_string]
67
+ end
68
+ counter += 1
69
+ end
70
+ end
71
+
72
+ def ==(comparison_object)
73
+ super(comparison_object)
74
+ comparison_object.regex == regex
75
+ comparison_object.code == code
76
+ comparison_object.parameters == parameters
77
+ comparison_object.calls == calls
78
+ comparison_object.nested_steps == nested_steps
79
+ end
80
+
81
+ def evaluate_score
82
+ super
83
+ rule_no_code
84
+ rule_too_many_parameters
85
+ rule_nested_steps
86
+ rule_recursive_nested_step
87
+ rule_commented_code
88
+ end
89
+
90
+ def rule_no_code
91
+ store_rule(STEP_DEFINITION_RULES[:no_code]) if code.empty?
92
+ end
93
+
94
+ def rule_too_many_parameters
95
+ rule = STEP_DEFINITION_RULES[:too_many_parameters]
96
+ store_rule(rule) if parameters.size >= rule[:max]
97
+ end
98
+
99
+ def rule_nested_steps
100
+ store_rule(STEP_DEFINITION_RULES[:nested_step]) unless nested_steps.empty?
101
+ end
102
+
103
+ def rule_recursive_nested_step
104
+ nested_steps.each_value do |nested_step|
105
+ store_rule(STEP_DEFINITION_RULES[:recursive_nested_step]) if nested_step =~ regex
106
+ end
107
+ end
108
+
109
+ def rule_commented_code
110
+ code.each do |line|
111
+ store_rule(STEP_DEFINITION_RULES[:commented_code]) if is_comment?(line)
112
+ end
113
+ end
114
+
115
+ end
116
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cuke_sniffer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Robert Cochran
9
+ - Chris Vaughn
10
+ - Robert Anderson
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-01-15 00:00:00.000000000 Z
15
+ dependencies: []
16
+ description: A ruby library used to root out smells in your cukes.
17
+ email:
18
+ executables:
19
+ - cuke_sniffer.rb
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/cuke_sniffer.rb
24
+ - lib/cuke_sniffer/constants.rb
25
+ - lib/cuke_sniffer/feature.rb
26
+ - lib/cuke_sniffer/feature_rules_evaluator.rb
27
+ - lib/cuke_sniffer/rules_evaluator.rb
28
+ - lib/cuke_sniffer/rule_config.rb
29
+ - lib/cuke_sniffer/scenario.rb
30
+ - lib/cuke_sniffer/step_definition.rb
31
+ - lib/cuke_sniffer/report/markup.rhtml
32
+ - lib/cuke_sniffer/cli.rb
33
+ - bin/cuke_sniffer.rb
34
+ homepage: https://github.com/r-cochran/cuke_sniffer
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.23
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: CukeSniffer
58
+ test_files: []