cuke_sniffer 0.0.1

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