cuke_sniffer 0.0.5 → 0.0.6

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.
Files changed (31) hide show
  1. data/bin/cuke_sniffer +85 -63
  2. data/lib/cuke_sniffer.rb +23 -16
  3. data/lib/cuke_sniffer/cli.rb +269 -523
  4. data/lib/cuke_sniffer/constants.rb +5 -2
  5. data/lib/cuke_sniffer/cuke_sniffer_helper.rb +165 -0
  6. data/lib/cuke_sniffer/dead_steps_helper.rb +54 -0
  7. data/lib/cuke_sniffer/feature.rb +13 -81
  8. data/lib/cuke_sniffer/feature_rules_evaluator.rb +56 -115
  9. data/lib/cuke_sniffer/formatter.rb +142 -0
  10. data/lib/cuke_sniffer/hook.rb +77 -160
  11. data/lib/cuke_sniffer/report/dead_steps.html.erb +36 -0
  12. data/lib/cuke_sniffer/report/features.html.erb +60 -0
  13. data/lib/cuke_sniffer/report/hooks.html.erb +49 -0
  14. data/lib/cuke_sniffer/report/improvement_list.html.erb +18 -0
  15. data/lib/cuke_sniffer/report/legend.html.erb +167 -0
  16. data/lib/cuke_sniffer/report/min_template.html.erb +125 -0
  17. data/lib/cuke_sniffer/report/rules.html.erb +9 -0
  18. data/lib/cuke_sniffer/report/standard_template.html.erb +137 -0
  19. data/lib/cuke_sniffer/report/step_definitions.html.erb +49 -0
  20. data/lib/cuke_sniffer/report/sub_rules.html.erb +24 -0
  21. data/lib/cuke_sniffer/report/summary.html.erb +71 -0
  22. data/lib/cuke_sniffer/rule.rb +28 -0
  23. data/lib/cuke_sniffer/rule_config.rb +241 -48
  24. data/lib/cuke_sniffer/rule_target.rb +62 -0
  25. data/lib/cuke_sniffer/rules_evaluator.rb +65 -70
  26. data/lib/cuke_sniffer/scenario.rb +102 -238
  27. data/lib/cuke_sniffer/step_definition.rb +163 -239
  28. data/lib/cuke_sniffer/summary_helper.rb +91 -0
  29. data/lib/cuke_sniffer/summary_node.rb +19 -0
  30. metadata +23 -5
  31. data/lib/cuke_sniffer/report/markup.rhtml +0 -353
data/bin/cuke_sniffer CHANGED
@@ -1,63 +1,85 @@
1
- #!/usr/bin/env ruby
2
- $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
3
-
4
- require 'cuke_sniffer'
5
-
6
- help_cmd_txt = "Welcome to CukeSniffer!
7
- Calling CukeSniffer with no arguments will run it against the current directory.
8
- Other Options for Running include:
9
- <feature_file_path>, <step_def_file_path> : Runs CukeSniffer against the
10
- specified paths.
11
- -o, --out <type> (name) : Where <type> is 'html' or 'xml'.
12
- Runs CukeSniffer then outputs an
13
- html/xml file in the current
14
- directory (with optional name).
15
- -h, --help : You get this lovely document."
16
-
17
-
18
- if ARGV.include? "-h" or ARGV.include? "--help"
19
- puts help_cmd_txt
20
- exit
21
- end
22
-
23
- cuke_sniffer = nil
24
- if (ARGV[0] != nil and File.directory?(ARGV[0])) and (ARGV[1] != nil and File.directory?
25
- (ARGV[1]))
26
- cuke_sniffer = CukeSniffer::CLI.new(ARGV[0], ARGV[1])
27
- else
28
- cuke_sniffer = CukeSniffer::CLI.new
29
- end
30
-
31
- def print_results(cuke_sniffer)
32
- puts cuke_sniffer.output_results
33
- end
34
-
35
- if ARGV.include? "--out" or ARGV.include? "-o"
36
- index = ARGV.index("--out")
37
- index ||= ARGV.index("-o")
38
- out_type = ARGV[index + 1]
39
- case out_type
40
- when "html"
41
- file_name = ARGV[index + 2]
42
- if file_name.nil?
43
- cuke_sniffer.output_html
44
- else
45
- file_name = file_name + ".html" unless file_name =~ /\.html$/
46
- cuke_sniffer.output_html(file_name)
47
- end
48
- when "xml"
49
- file_name = ARGV[index + 2]
50
- if file_name.nil?
51
- cuke_sniffer.output_xml
52
- else
53
- file_name = file_name + ".xml" unless file_name =~ /\.xml$/
54
- cuke_sniffer.output_xml(file_name)
55
- end
56
- else
57
- print_results(cuke_sniffer)
58
- end
59
- else
60
- print_results(cuke_sniffer)
61
- end
62
-
63
- puts "Completed Sniffing."
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
3
+
4
+ require 'cuke_sniffer'
5
+
6
+ help_cmd_txt = "Welcome to CukeSniffer!
7
+ Calling CukeSniffer with no arguments will run it against the current directory.
8
+ Other Options for Running include:
9
+ -o, --out <type> (name) : Where <type> is 'html', 'min_html' or 'xml'.
10
+ Runs CukeSniffer then outputs an
11
+ html/xml file in the current
12
+ directory (with optional name).
13
+ -h, --help : You get this lovely document.
14
+ -p, --project <project_root> : Root directory of project.
15
+ -f, --features <feature_path> : Path to features directory.
16
+ -s, --step_definitions <step_def_path> : Path to step definitions directory.
17
+ -hk, --hooks <hooks_path> : Path to support directory. "
18
+
19
+ def find_index_by_regex_match(list, regex)
20
+ list.each do |item|
21
+ return list.index(item) if item =~ regex
22
+ end
23
+ nil
24
+ end
25
+
26
+ def build_cli_hash(argv)
27
+ cucumber_hash ={
28
+ :hooks_location => /-hk|--hooks/,
29
+ :features_location => /-f|--features/,
30
+ :step_definitions_location => /-s|--step_definitions/,
31
+ :project_location => /-p|--project/}
32
+ cli_hash = {}
33
+ cucumber_hash.each do |symbol, regex|
34
+ index_of_key = find_index_by_regex_match(argv, regex)
35
+ if !index_of_key.nil?
36
+ cli_hash[symbol] = argv[index_of_key + 1]
37
+ end
38
+ end
39
+ cli_hash
40
+ end
41
+
42
+ def handle_output(argv)
43
+ if find_index_by_regex_match(argv, /-o|--out/)
44
+ output_type_hash = {
45
+ :html => "html",
46
+ :min_html => "min_html",
47
+ :xml => "xml",
48
+ }
49
+ output_type_hash.each_value do |value|
50
+ index_of_key = argv.index(value)
51
+ if !index_of_key.nil?
52
+ file_type = argv[index_of_key]
53
+ file_name = argv[index_of_key+1]
54
+ call_output(file_type, file_name)
55
+ end
56
+ end
57
+ else
58
+ puts @cuke_sniffer.output_results
59
+ end
60
+ end
61
+
62
+ def call_output(file_type, file_name)
63
+ case file_type
64
+ when "xml"
65
+ file_name.nil? ? @cuke_sniffer.output_xml : @cuke_sniffer.output_xml(file_name)
66
+ when "html"
67
+ file_name.nil? ? @cuke_sniffer.output_html : @cuke_sniffer.output_html(file_name)
68
+ when "min_html"
69
+ file_name.nil? ? @cuke_sniffer.output_min_html : @cuke_sniffer.output_min_html(file_name)
70
+ else
71
+ puts "#{file_type} is not a supported version of cuke_sniffer output."
72
+ end
73
+ end
74
+
75
+ if ARGV.include? "-h" or ARGV.include? "--help"
76
+ puts help_cmd_txt
77
+ exit
78
+ end
79
+
80
+ arguments_hash = build_cli_hash(ARGV)
81
+ @cuke_sniffer = CukeSniffer::CLI.new(arguments_hash)
82
+ handle_output(ARGV)
83
+
84
+
85
+ puts "Completed Sniffing."
data/lib/cuke_sniffer.rb CHANGED
@@ -1,16 +1,23 @@
1
- require 'nokogiri'
2
- require 'roxml'
3
-
4
- require 'cuke_sniffer/constants'
5
- require 'cuke_sniffer/rule_config'
6
- require 'cuke_sniffer/rules_evaluator'
7
- require 'cuke_sniffer/feature_rules_evaluator'
8
- require 'cuke_sniffer/feature'
9
- require 'cuke_sniffer/scenario'
10
- require 'cuke_sniffer/step_definition'
11
- require 'cuke_sniffer/hook'
12
- require 'cuke_sniffer/cli'
13
-
14
- module CukeSniffer
15
-
16
- end
1
+ require 'nokogiri'
2
+ require 'roxml'
3
+
4
+ require 'cuke_sniffer/constants'
5
+ require 'cuke_sniffer/rule_config'
6
+ require 'cuke_sniffer/rule_target'
7
+ require 'cuke_sniffer/feature_rules_evaluator'
8
+ require 'cuke_sniffer/feature'
9
+ require 'cuke_sniffer/scenario'
10
+ require 'cuke_sniffer/step_definition'
11
+ require 'cuke_sniffer/hook'
12
+ require 'cuke_sniffer/rule'
13
+ require 'cuke_sniffer/summary_node'
14
+ require 'cuke_sniffer/rules_evaluator'
15
+ require 'cuke_sniffer/cuke_sniffer_helper'
16
+ require 'cuke_sniffer/summary_helper'
17
+ require 'cuke_sniffer/dead_steps_helper'
18
+ require 'cuke_sniffer/cli'
19
+ require 'cuke_sniffer/formatter'
20
+
21
+ module CukeSniffer
22
+
23
+ end
@@ -1,523 +1,269 @@
1
- require 'erb'
2
- require 'roxml'
3
-
4
- module CukeSniffer
5
-
6
- # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
7
- # Copyright:: Copyright (C) 2013 Robert Cochran
8
- # License:: Distributes under the MIT License
9
- # Mixins: CukeSniffer::Constants, ROXML
10
- class CLI
11
- include CukeSniffer::Constants
12
- include ROXML
13
-
14
- class SummaryNode
15
- include ROXML
16
- xml_accessor :score
17
- xml_accessor :count
18
- xml_accessor :average
19
- xml_accessor :good
20
- xml_accessor :bad
21
- xml_accessor :threshold
22
- end # :nodoc:
23
-
24
- xml_name "cuke_sniffer"
25
- xml_accessor :features_summary, :as => SummaryNode
26
- xml_accessor :scenarios_summary, :as => SummaryNode
27
- xml_accessor :step_definitions_summary, :as => SummaryNode
28
- xml_accessor :hooks_summary, :as => SummaryNode
29
- xml_accessor :improvement_list, :as => {:key => "rule", :value => "total"}, :in => "improvement_list", :from => "improvement"
30
- xml_accessor :features, :as => [CukeSniffer::Feature], :in => "features"
31
- xml_accessor :step_definitions, :as => [CukeSniffer::StepDefinition], :in => "step_definitions"
32
- xml_accessor :hooks, :as => [CukeSniffer::Hook], :in => "hooks"
33
-
34
-
35
- # Feature array: All Features gathered from the specified folder
36
- attr_accessor :features
37
-
38
- # StepDefinition Array: All StepDefinitions objects gathered from the specified folder
39
- attr_accessor :step_definitions
40
-
41
- # Hash: Summary objects and improvement lists
42
- # * Key: symbol, :total_score, :features, :step_definitions, :improvement_list
43
- # * Value: hash or array
44
- attr_accessor :summary
45
-
46
- # string: Location of the feature file or root folder that was searched in
47
- attr_accessor :features_location
48
-
49
- # string: Location of the step definition file or root folder that was searched in
50
- attr_accessor :step_definitions_location
51
-
52
- # string: Location of the hook file or root folder that was searched in
53
- attr_accessor :hooks_location
54
-
55
- # Scenario array: All Scenarios found in the features from the specified folder
56
- attr_accessor :scenarios
57
-
58
- # Hook array: All Hooks found in the current directory
59
- attr_accessor :hooks
60
-
61
-
62
- # Does analysis against the passed features and step definition locations
63
- #
64
- # Can be called in several ways.
65
- #
66
- #
67
- # No argument(assumes current directory is the project)
68
- # cuke_sniffer = CukeSniffer::CLI.new
69
- #
70
- # Against single files
71
- # cuke_sniffer = CukeSniffer::CLI.new("my_feature.feature", nil)
72
- # Or
73
- # cuke_sniffer = CukeSniffer::CLI.new(nil, "my_steps.rb")
74
- #
75
- #
76
- # Against folders
77
- # cuke_sniffer = CukeSniffer::CLI.new("my_features_directory\", "my_steps_directory\")
78
- #
79
- # You can mix and match all of the above examples
80
- #
81
- # Displays the sequence and a . indicator for each new loop in that process.
82
- # Handles creation of all Feature and StepDefinition objects
83
- # Then catalogs all step definition calls to be used for rules and identification
84
- # of dead steps.
85
- def initialize(features_location = Dir.getwd, step_definitions_location = Dir.getwd, hooks_location = Dir.getwd)
86
- @features_location = features_location
87
- @step_definitions_location = step_definitions_location
88
- @hooks_location = hooks_location
89
- @features = []
90
- @scenarios = []
91
- @step_definitions = []
92
- @hooks = []
93
-
94
- puts "\nFeatures:"
95
- #extract this to a method that accepts a block and yields for the build pattern
96
- unless features_location.nil?
97
- if File.file?(features_location)
98
- @features = [CukeSniffer::Feature.new(features_location)]
99
- else
100
- build_file_list_from_folder(features_location, ".feature").each { |location|
101
- @features << CukeSniffer::Feature.new(location)
102
- print '.'
103
- }
104
- end
105
- end
106
-
107
- @scenarios = get_all_scenarios(@features)
108
-
109
- puts("\nStep Definitions:")
110
- unless step_definitions_location.nil?
111
- if File.file?(step_definitions_location)
112
- @step_definitions = [build_step_definitions(step_definitions_location)]
113
- else
114
- build_file_list_from_folder(step_definitions_location, ".rb").each { |location|
115
- @step_definitions << build_step_definitions(location)
116
- print '.'
117
- }
118
- end
119
- end
120
- @step_definitions.flatten!
121
-
122
- puts("\nHooks:")
123
- unless hooks_location.nil?
124
- if File.file?(hooks_location)
125
- @hooks = [build_hooks(hooks_location)]
126
- else
127
- build_file_list_from_folder(hooks_location, ".rb").each { |location|
128
- @hooks << build_hooks(location)
129
- print '.'
130
- }
131
- end
132
- end
133
- @hooks.flatten!
134
-
135
- @summary = {
136
- :total_score => 0,
137
- :features => {},
138
- :step_definitions => {},
139
- :hooks => {},
140
- :improvement_list => {}
141
- }
142
- puts "\nCataloging Step Calls: "
143
- catalog_step_calls
144
- puts "\nAssessing Score: "
145
- assess_score
146
- @improvement_list = @summary[:improvement_list]
147
- @features_summary = load_summary_data(@summary[:features])
148
- @scenarios_summary = load_summary_data(@summary[:scenarios])
149
- @step_definitions_summary = load_summary_data(@summary[:step_definitions])
150
- @hooks_summary = load_summary_data(@summary[:hooks])
151
- end
152
-
153
- # Returns the status of the overall project based on a comparison of the score to the threshold score
154
- def good?
155
- @summary[:total_score] <= Constants::THRESHOLDS["Project"]
156
- end
157
-
158
- # Calculates the score to threshold percentage of an object
159
- # Return: Float
160
- def problem_percentage
161
- @summary[:total_score].to_f / Constants::THRESHOLDS["Project"].to_f
162
- end
163
-
164
- # Prints out a summary of the results and the list of improvements to be made
165
- def output_results
166
- feature_results = @summary[:features]
167
- step_definition_results = @summary[:step_definitions]
168
- hooks_results = @summary[:hooks]
169
- output = "Suite Summary
170
- Total Score: #{@summary[:total_score]}
171
- Features (#@features_location)
172
- Min: #{feature_results[:min]} (#{feature_results[:min_file]})
173
- Max: #{feature_results[:max]} (#{feature_results[:max_file]})
174
- Average: #{feature_results[:average]}
175
- Step Definitions (#@step_definitions_location)
176
- Min: #{step_definition_results[:min]} (#{step_definition_results[:min_file]})
177
- Max: #{step_definition_results[:max]} (#{step_definition_results[:max_file]})
178
- Average: #{step_definition_results[:average]}
179
- Hooks (#@hooks_location)
180
- Min: #{hooks_results[:min]} (#{hooks_results[:min_file]})
181
- Max: #{hooks_results[:max]} (#{hooks_results[:max_file]})
182
- Average: #{hooks_results[:average]}
183
- Improvements to make:"
184
- create_improvement_list.each { |item| output << "\n #{item}" }
185
- output
186
- end
187
-
188
- # Creates a html file with the collected project details
189
- # file_name defaults to "cuke_sniffer_results.html" unless specified
190
- # Second parameter used for passing into the markup.
191
- # cuke_sniffer.output_html
192
- # Or
193
- # cuke_sniffer.output_html("results01-01-0001.html")
194
- def output_html(file_name = "cuke_sniffer_results.html", cuke_sniffer = self)
195
- @features = @features.sort_by { |feature| feature.total_score }.reverse
196
- @step_definitions = @step_definitions.sort_by { |step_definition| step_definition.score }.reverse
197
- @hooks = @hooks.sort_by { |hook| hook.score }.reverse
198
-
199
- markup_erb = ERB.new extract_markup
200
- output = markup_erb.result(binding)
201
- File.open(file_name, 'w') do |f|
202
- f.write(output)
203
- end
204
- end
205
-
206
- # Creates a xml file with the collected project details
207
- # file_name defaults to "cuke_sniffer.xml" unless specified
208
- # cuke_sniffer.output_xml
209
- # Or
210
- # cuke_sniffer.output_xml("cuke_sniffer01-01-0001.xml")
211
- def output_xml(file_name = "cuke_sniffer.xml")
212
- doc = Nokogiri::XML::Document.new
213
- doc.root = self.to_xml
214
- open(file_name, "w") do |file|
215
- file << doc.serialize
216
- end
217
- end
218
-
219
- # Gathers all StepDefinitions that have no calls
220
- # Returns a hash that has two different types of records
221
- # 1: String of the file with a dead step with an array of the line and regex of each dead step
222
- # 2: Symbol of :total with an integer that is the total number of dead steps
223
- def get_dead_steps
224
- dead_steps_hash = {}
225
- @step_definitions.each do |step_definition|
226
- location_match = step_definition.location.match(/(?<file>.*).rb:(?<line>\d+)/)
227
- file_name = location_match[:file]
228
- regex = step_definition.regex.to_s.match(/\(\?\-mix\:(?<regex>.*)\)/)[:regex]
229
- dead_steps_hash[file_name] ||= []
230
- dead_steps_hash[file_name] << "#{location_match[:line]}: /#{regex}/" if step_definition.calls.empty?
231
- end
232
- total = 0
233
- dead_steps_hash.each_key do |key|
234
- unless dead_steps_hash[key] == []
235
- total += dead_steps_hash[key].size
236
- dead_steps_hash[key].sort_by! { |row| row[/^\d+/].to_i }
237
- else
238
- dead_steps_hash.delete(key)
239
- end
240
- end
241
- dead_steps_hash[:total] = total
242
- dead_steps_hash
243
- end
244
-
245
- # Determines all normal and nested step calls and assigns them to the corresponding step definition.
246
- # Does direct and fuzzy matching
247
- def catalog_step_calls
248
- steps = get_all_steps
249
- @step_definitions.each do |step_definition|
250
- print '.'
251
- calls = steps.find_all { |location, step| step.gsub(STEP_STYLES, "") =~ step_definition.regex }
252
- calls.each { |call| step_definition.add_call(call[0], call[1].gsub(STEP_STYLES, "")) }
253
- end
254
-
255
- converted_steps = convert_steps_with_expressions(get_steps_with_expressions(steps))
256
- catalog_possible_dead_steps(converted_steps)
257
- end
258
-
259
- private
260
-
261
- def extract_variables_from_example(example)
262
- example = example[example.index('|')..example.length]
263
- example.split(/\s*\|\s*/) - [""]
264
- end
265
-
266
- def assess_score
267
- @summary[:features] = assess_array(@features, "Feature")
268
- @summary[:scenarios] = assess_array(@scenarios, "Scenario")
269
- @summary[:step_definitions] = assess_array(@step_definitions, "StepDefinition")
270
- @summary[:hooks] = assess_array(@hooks, "Hook")
271
- sort_improvement_list
272
- end
273
-
274
- def get_all_steps
275
- feature_steps = extract_steps_from_features
276
- step_definition_steps = extract_steps_from_step_definitions
277
- feature_steps.merge step_definition_steps
278
- end
279
-
280
- def get_steps_with_expressions(steps)
281
- steps_with_expressions = {}
282
- steps.each do |step_location, step_value|
283
- if step_value =~ /\#{.*}/
284
- steps_with_expressions[step_location] = step_value
285
- end
286
- end
287
- steps_with_expressions
288
- end
289
-
290
- def catalog_possible_dead_steps(steps_with_expressions)
291
- @step_definitions.each do |step_definition|
292
- next unless step_definition.calls.empty?
293
- regex_as_string = step_definition.regex.to_s.gsub(/\(\?-mix:\^?/, "").gsub(/\$\)$/, "")
294
- steps_with_expressions.each do |step_location, step_value|
295
- if regex_as_string =~ step_value
296
- step_definition.add_call(step_location, step_value)
297
- end
298
- end
299
- end
300
- end
301
-
302
- def convert_steps_with_expressions(steps_with_expressions)
303
- step_regexs = {}
304
- steps_with_expressions.each do |step_location, step_value|
305
- modified_step = step_value.gsub(/\#{[^}]*}/, '.*')
306
- step_regexs[step_location] = Regexp.new('^' + modified_step + '$')
307
- end
308
- step_regexs
309
- end
310
-
311
- def load_summary_data(summary_hash)
312
- summary_node = SummaryNode.new
313
- summary_node.count = summary_hash[:total]
314
- summary_node.score = summary_hash[:total_score]
315
- summary_node.average = summary_hash[:average]
316
- summary_node.threshold = summary_hash[:threshold]
317
- summary_node.good = summary_hash[:good]
318
- summary_node.bad = summary_hash[:bad]
319
- summary_node
320
- end
321
-
322
- def build_file_list_from_folder(folder_name, extension)
323
- list = []
324
- Dir.entries(folder_name).each_entry do |file_name|
325
- unless FILE_IGNORE_LIST.include?(file_name)
326
- file_name = "#{folder_name}/#{file_name}"
327
- if File.directory?(file_name)
328
- list << build_file_list_from_folder(file_name, extension)
329
- elsif file_name.downcase.include?(extension)
330
- list << file_name
331
- end
332
- end
333
- end
334
- list.flatten
335
- end
336
-
337
- def build_step_definitions(file_name)
338
- step_file_lines = []
339
- step_file = File.open(file_name)
340
- step_file.each_line { |line| step_file_lines << line }
341
- step_file.close
342
-
343
- counter = 0
344
- step_code = []
345
- step_definitions = []
346
- found_first_step = false
347
- until counter >= step_file_lines.length
348
- if step_file_lines[counter] =~ STEP_DEFINITION_REGEX and !step_code.empty? and found_first_step
349
- step_definitions << CukeSniffer::StepDefinition.new("#{file_name}:#{counter+1 - step_code.count}", step_code)
350
- step_code = []
351
- end
352
- found_first_step = true if step_file_lines[counter] =~ STEP_DEFINITION_REGEX
353
- step_code << step_file_lines[counter].strip
354
- counter+=1
355
- end
356
- step_definitions << CukeSniffer::StepDefinition.new("#{file_name}:#{counter+1 -step_code.count}", step_code) unless step_code.empty? or !found_first_step
357
- step_definitions
358
- end
359
-
360
- def assess_array(array, type)
361
- min, max, min_file, max_file = nil
362
- total = 0
363
- good = 0
364
- bad = 0
365
- total_score = 0
366
- unless array.empty?
367
- array.each do |node|
368
- score = node.score
369
- @summary[:total_score] += score
370
- total_score += score
371
- node.rules_hash.each_key do |key|
372
- @summary[:improvement_list][key] ||= 0
373
- @summary[:improvement_list][key] += node.rules_hash[key]
374
- end
375
- min, min_file = score, node.location if (min.nil? or score < min)
376
- max, max_file = score, node.location if (max.nil? or score > max)
377
- if node.good?
378
- good += 1
379
- else
380
- bad += 1
381
- end
382
- total += score
383
- end
384
- end
385
- {
386
- :total => array.count,
387
- :total_score => total_score,
388
- :min => min,
389
- :min_file => min_file,
390
- :max => max,
391
- :max_file => max_file,
392
- :average => (total.to_f/array.count.to_f).round(2),
393
- :threshold => THRESHOLDS[type],
394
- :good => good,
395
- :bad => bad,
396
- }
397
- end
398
-
399
- def get_all_scenarios(features)
400
- scenarios = []
401
- features.each do |feature|
402
- scenarios << feature.background unless feature.background.nil?
403
- scenarios << feature.scenarios
404
- end
405
- scenarios.flatten
406
- end
407
-
408
- def sort_improvement_list
409
- sorted_array = @summary[:improvement_list].sort_by { |improvement, occurrence| occurrence }
410
- @summary[:improvement_list] = {}
411
- sorted_array.reverse.each { |node|
412
- @summary[:improvement_list][node[0]] = node[1]
413
- }
414
- end
415
-
416
- def create_improvement_list
417
- output = []
418
- @summary[:improvement_list].each_key { |improvement| output << "(#{summary[:improvement_list][improvement]})#{improvement}" }
419
- output
420
- end
421
-
422
- def extract_steps_from_features
423
- steps = {}
424
- @features.each do |feature|
425
- steps.merge! extract_scenario_steps(feature.background) unless feature.background.nil?
426
- feature.scenarios.each do |scenario|
427
- if scenario.type == "Scenario Outline"
428
- steps.merge! extract_scenario_outline_steps(scenario)
429
- else
430
- steps.merge! extract_scenario_steps(scenario)
431
- end
432
- end
433
- end
434
- steps
435
- end
436
-
437
- def extract_scenario_steps(scenario)
438
- steps_hash = {}
439
- counter = 1
440
- scenario.steps.each do |step|
441
- location = scenario.location.gsub(/:\d*$/, ":#{scenario.start_line + counter}")
442
- steps_hash[location] = step
443
- counter += 1
444
- end
445
- steps_hash
446
- end
447
-
448
- def extract_scenario_outline_steps(scenario)
449
- steps = {}
450
- examples = scenario.examples_table
451
- return {} if examples.empty?
452
- variable_list = extract_variables_from_example(examples.first)
453
- (1...examples.size).each do |example_counter|
454
- #TODO Abstraction needed for this regex matcher (constants?)
455
- next if examples[example_counter] =~ /^\#.*$/
456
- row_variables = extract_variables_from_example(examples[example_counter])
457
- step_counter = 1
458
- scenario.steps.each do |step|
459
- step_line = scenario.start_line + step_counter
460
- location = "#{scenario.location.gsub(/\d+$/, step_line.to_s)}(Example #{example_counter})"
461
- steps[location] = build_updated_step_from_example(step, variable_list, row_variables)
462
- step_counter += 1
463
- end
464
- end
465
- steps
466
- end
467
-
468
- def build_updated_step_from_example(step, variable_list, row_variables)
469
- new_step = step.dup
470
- variable_list.each do |variable|
471
- if step.include? variable
472
- table_variable_to_insert = row_variables[variable_list.index(variable)]
473
- table_variable_to_insert ||= ""
474
- new_step.gsub!("<#{variable}>", table_variable_to_insert)
475
- end
476
- end
477
- new_step
478
- end
479
-
480
- def extract_steps_from_step_definitions
481
- steps = {}
482
- @step_definitions.each do |definition|
483
- definition.nested_steps.each_key do |key|
484
- steps[key] = definition.nested_steps[key]
485
- end
486
- end
487
- steps
488
- end
489
-
490
- def extract_markup
491
- markup_location = File.join(File.dirname(__FILE__), 'report', 'markup.rhtml')
492
- markup = ""
493
- File.open(markup_location).lines.each do |line|
494
- markup << line
495
- end
496
- markup
497
- end
498
-
499
- def build_hooks(file_name)
500
- hooks_file_lines = []
501
- hooks_file = File.open(file_name)
502
- hooks_file.each_line { |line| hooks_file_lines << line }
503
- hooks_file.close
504
-
505
- counter = 0
506
- hooks_code = []
507
- hooks = []
508
- found_first_hook = false
509
- until counter >= hooks_file_lines.length
510
- if hooks_file_lines[counter] =~ HOOK_REGEX and !hooks_code.empty? and found_first_hook
511
- hooks << CukeSniffer::Hook.new("#{file_name}:#{counter+1 - hooks_code.count}", hooks_code)
512
- hooks_code = []
513
- end
514
- found_first_hook = true if hooks_file_lines[counter] =~ HOOK_REGEX
515
- hooks_code << hooks_file_lines[counter].strip
516
- counter+=1
517
- end
518
- hooks << CukeSniffer::Hook.new("#{file_name}:#{counter+1 -hooks_code.count}", hooks_code) unless hooks_code.empty? or !found_first_hook
519
- hooks
520
- end
521
-
522
- end
523
- end
1
+ require 'erb'
2
+ require 'roxml'
3
+
4
+ module CukeSniffer
5
+
6
+ # Author:: Robert Cochran (mailto:cochrarj@miamioh.edu)
7
+ # Copyright:: Copyright (C) 2013 Robert Cochran
8
+ # License:: Distributes under the MIT License
9
+ # Mixins: CukeSniffer::Constants, ROXML
10
+ class CLI
11
+ include CukeSniffer::Constants
12
+ include CukeSniffer::RuleConfig
13
+ include ROXML
14
+
15
+ xml_name "cuke_sniffer"
16
+ xml_accessor :rules, :as => [Rule], :in => "rules"
17
+ xml_accessor :features_summary, :as => CukeSniffer::SummaryNode
18
+ xml_accessor :scenarios_summary, :as => CukeSniffer::SummaryNode
19
+ xml_accessor :step_definitions_summary, :as => CukeSniffer::SummaryNode
20
+ xml_accessor :hooks_summary, :as => CukeSniffer::SummaryNode
21
+ xml_accessor :improvement_list, :as => {:key => "rule", :value => "total"}, :in => "improvement_list", :from => "improvement"
22
+ xml_accessor :features, :as => [CukeSniffer::Feature], :in => "features"
23
+ xml_accessor :step_definitions, :as => [CukeSniffer::StepDefinition], :in => "step_definitions"
24
+ xml_accessor :hooks, :as => [CukeSniffer::Hook], :in => "hooks"
25
+
26
+
27
+ # Feature array: All Features gathered from the specified folder
28
+ attr_accessor :features
29
+
30
+ # StepDefinition Array: All StepDefinitions objects gathered from the specified folder
31
+ attr_accessor :step_definitions
32
+
33
+ # Hash: Summary objects and improvement lists
34
+ # * Key: symbol, :total_score, :features, :step_definitions, :improvement_list
35
+ # * Value: hash or array
36
+ attr_accessor :summary
37
+
38
+ # string: Location of the feature file or root folder that was searched in
39
+ attr_accessor :features_location
40
+
41
+ # string: Location of the step definition file or root folder that was searched in
42
+ attr_accessor :step_definitions_location
43
+
44
+ # string: Location of the hook file or root folder that was searched in
45
+ attr_accessor :hooks_location
46
+
47
+ # Scenario array: All Scenarios found in the features from the specified folder
48
+ attr_accessor :scenarios
49
+
50
+ # Hook array: All Hooks found in the current directory
51
+ attr_accessor :hooks
52
+
53
+ # Rules hash: All the rules that exist at runtime and their corresponding data
54
+ attr_accessor :rules
55
+
56
+
57
+ # Does analysis against the passed features and step definition locations
58
+ #
59
+ # Can be called in several ways.
60
+ #
61
+ #
62
+ # No argument(assumes current directory is the project)
63
+ # cuke_sniffer = CukeSniffer::CLI.new
64
+ #
65
+ # Against single files
66
+ # cuke_sniffer = CukeSniffer::CLI.new({:features_location =>"my_feature.feature"})
67
+ # Or
68
+ # cuke_sniffer = CukeSniffer::CLI.new({:step_definitions_location =>"my_steps.rb"})
69
+ # Or
70
+ # cuke_sniffer = CukeSniffer::CLI.new({:hooks_location =>"my_hooks.rb"})
71
+ #
72
+ #
73
+ # Against folders
74
+ # cuke_sniffer = CukeSniffer::CLI.new({:features_location =>"my_features_directory\", :step_definitions_location =>"my_steps_directory\"})
75
+ #
76
+ # You can mix and match all of the above examples
77
+ #
78
+ # Displays the sequence and a . indicator for each new loop in that process.
79
+ # Handles creation of all Feature and StepDefinition objects
80
+ # Then catalogs all step definition calls to be used for rules and identification
81
+ # of dead steps.
82
+ def initialize(parameters = {})
83
+ initialize_rule_targets(parameters)
84
+ evaluate_rules
85
+ catalog_step_calls
86
+ assess_score
87
+ end
88
+
89
+ # Returns the status of the overall project based on a comparison of the score to the threshold score
90
+ def good?
91
+ @summary[:total_score] <= Constants::THRESHOLDS["Project"]
92
+ end
93
+
94
+ # Calculates the score to threshold percentage of an object
95
+ # Return: Float
96
+ def problem_percentage
97
+ @summary[:total_score].to_f / Constants::THRESHOLDS["Project"].to_f
98
+ end
99
+
100
+ # Prints out a summary of the results and the list of improvements to be made
101
+ def output_results
102
+ CukeSniffer::Formatter.output_console(self)
103
+ end
104
+
105
+ # Creates a html file with the collected project details
106
+ # file_name defaults to "cuke_sniffer_results.html" unless specified
107
+ # Second parameter used for passing into the markup.
108
+ # cuke_sniffer.output_html
109
+ # Or
110
+ # cuke_sniffer.output_html("results01-01-0001.html")
111
+ def output_html(file_name = DEFAULT_OUTPUT_FILE_NAME + ".html")
112
+ CukeSniffer::Formatter.output_html(self, file_name)
113
+ end
114
+
115
+ # Creates a html file with minimum information: Summary, Rules, Improvement List.
116
+ # file_name defaults to "cuke_sniffer_results.html" unless specified
117
+ # Second parameter used for passing into the markup.
118
+ # cuke_sniffer.output_min_html
119
+ # Or
120
+ # cuke_sniffer.output_min_html("results01-01-0001.html")
121
+ def output_min_html(file_name = DEFAULT_OUTPUT_FILE_NAME + ".html")
122
+ CukeSniffer::Formatter.output_min_html(self, file_name)
123
+ end
124
+
125
+ # Creates a xml file with the collected project details
126
+ # file_name defaults to "cuke_sniffer.xml" unless specified
127
+ # cuke_sniffer.output_xml
128
+ # Or
129
+ # cuke_sniffer.output_xml("cuke_sniffer01-01-0001.xml")
130
+ def output_xml(file_name = DEFAULT_OUTPUT_FILE_NAME + ".xml")
131
+ CukeSniffer::Formatter.output_xml(self, file_name)
132
+ end
133
+
134
+ # Gathers all StepDefinitions that have no calls
135
+ # Returns a hash that has two different types of records
136
+ # 1: String of the file with a dead step with an array of the line and regex of each dead step
137
+ # 2: Symbol of :total with an integer that is the total number of dead steps
138
+ def get_dead_steps
139
+ CukeSniffer::DeadStepsHelper::build_dead_steps_hash(@step_definitions)
140
+ end
141
+
142
+ # Determines all normal and nested step calls and assigns them to the corresponding step definition.
143
+ # Does direct and fuzzy matching
144
+ def catalog_step_calls
145
+ puts "\nCataloging Step Calls: "
146
+ steps = CukeSniffer::CukeSnifferHelper.get_all_steps(@features, @step_definitions)
147
+ @step_definitions.each do |step_definition|
148
+ print '.'
149
+ calls = steps.find_all { |location, step| step.gsub(STEP_STYLES, "") =~ step_definition.regex }
150
+ calls.each { |call| step_definition.add_call(call[0], call[1].gsub(STEP_STYLES, "")) }
151
+ end
152
+
153
+ steps_with_expressions = CukeSniffer::CukeSnifferHelper.get_steps_with_expressions(steps)
154
+ converted_steps = CukeSniffer::CukeSnifferHelper.convert_steps_with_expressions(steps_with_expressions)
155
+ CukeSniffer::CukeSnifferHelper.catalog_possible_dead_steps(@step_definitions, converted_steps)
156
+ end
157
+
158
+ private
159
+
160
+ def initialize_rule_targets(parameters)
161
+ initialize_locations(parameters)
162
+ initialize_feature_objects
163
+
164
+ puts("\nStep Definitions: ")
165
+ @step_definitions = build_objects_for_extension_from_location(@step_definitions_location, "rb") { |location| build_step_definitions(location) }
166
+
167
+ puts("\nHooks:")
168
+ @hooks = build_objects_for_extension_from_location(@hooks_location, "rb") { |location| build_hooks(location) }
169
+ end
170
+
171
+ def initialize_locations(parameters)
172
+ @features_location = parameters[:features_location] ? parameters[:features_location] : Dir.getwd
173
+ @step_definitions_location = parameters[:step_definitions_location] ? parameters[:step_definitions_location] : Dir.getwd
174
+ @hooks_location = parameters[:hooks_location] ? parameters[:hooks_location] : Dir.getwd
175
+ end
176
+
177
+ def initialize_feature_objects
178
+ puts "\nFeatures:"
179
+ @features = build_objects_for_extension_from_location(features_location, "feature") { |location| CukeSniffer::Feature.new(location) }
180
+ @scenarios = CukeSniffer::CukeSnifferHelper.get_all_scenarios(@features)
181
+ end
182
+
183
+ def evaluate_rules
184
+ @rules = CukeSniffer::CukeSnifferHelper.build_rules(RULES)
185
+ CukeSniffer::RulesEvaluator.new(self, @rules)
186
+ end
187
+
188
+ def initialize_summary
189
+ @summary = {
190
+ :total_score => 0,
191
+ :improvement_list => {}
192
+ }
193
+ end
194
+
195
+ def assess_score
196
+ puts "\nAssessing Score: "
197
+ initialize_summary
198
+ summarize(:features, @features, "Feature", @features_summary)
199
+ summarize(:scenarios, @scenarios, "Scenario", @scenarios_summary)
200
+ summarize(:step_definitions, @step_definitions, "StepDefinition", @step_definitions_summary)
201
+ summarize(:hooks, @hooks, "Hook", @hooks_summary)
202
+ @summary[:improvement_list] = CukeSniffer::SummaryHelper.sort_improvement_list(@summary[:improvement_list])
203
+ @improvement_list = @summary[:improvement_list]
204
+ end
205
+
206
+ def summarize(symbol, list, name, summary_object)
207
+ @summary[symbol] = CukeSniffer::SummaryHelper.assess_rule_target_list(list, name)
208
+ @summary[:total_score] = @summary[symbol][:total_score]
209
+ @summary[symbol][:improvement_list].each do |phrase, count|
210
+ @summary[:improvement_list][phrase] ||= 0
211
+ @summary[:improvement_list][phrase] += @summary[symbol][:improvement_list][phrase]
212
+ end
213
+ summary_object = CukeSniffer::SummaryHelper::load_summary_data(@summary[symbol])
214
+ end
215
+
216
+ def build_step_definitions(file_name)
217
+ build_object_for_extension_from_file(file_name, STEP_DEFINITION_REGEX, CukeSniffer::StepDefinition)
218
+ end
219
+
220
+ def build_hooks(file_name)
221
+ build_object_for_extension_from_file(file_name, HOOK_REGEX, CukeSniffer::Hook)
222
+ end
223
+
224
+ def build_file_list_for_extension_from_location(pattern_location, extension)
225
+ list = []
226
+ unless pattern_location.nil?
227
+ if File.file?(pattern_location)
228
+ [pattern_location]
229
+ else
230
+ Dir["#{pattern_location}/**/*.#{extension}"]
231
+ end
232
+ end
233
+ end
234
+
235
+ def build_objects_for_extension_from_location(pattern_location, extension, &block)
236
+ file_list = build_file_list_for_extension_from_location(pattern_location, extension)
237
+ list = []
238
+ file_list.each {|file_name|
239
+ print '.'
240
+ list << block.call(file_name)
241
+ }
242
+ list.flatten
243
+ end
244
+
245
+
246
+ def build_object_for_extension_from_file(file_name, regex, cuke_sniffer_class)
247
+ file_lines = IO.readlines(file_name)
248
+
249
+ counter = 0
250
+ code = []
251
+ object_list = []
252
+ found_first_object = false
253
+ until counter >= file_lines.length
254
+ if file_lines[counter] =~ regex and !code.empty? and found_first_object
255
+ location = "#{file_name}:#{counter+1 - code.count}"
256
+ object_list << cuke_sniffer_class.new(location, code)
257
+ code = []
258
+ end
259
+ found_first_object = true if file_lines[counter] =~ regex
260
+ code << file_lines[counter].strip
261
+ counter+=1
262
+ end
263
+ location = "#{file_name}:#{counter+1 -code.count}"
264
+ object_list << cuke_sniffer_class.new(location, code) unless code.empty? or !found_first_object
265
+ object_list
266
+ end
267
+
268
+ end
269
+ end