cuke_sniffer 0.0.5 → 0.0.6

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