From 21423505173d11595f9f3e25ae71221425065e4b Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Mon, 9 Jan 2017 14:42:18 +0000 Subject: [PATCH 1/9] Add features for --audit --- features/step_auditing.feature | 19 ++++++++ features/steps/step_auditing.rb | 78 +++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 features/step_auditing.feature create mode 100644 features/steps/step_auditing.rb diff --git a/features/step_auditing.feature b/features/step_auditing.feature new file mode 100644 index 00000000..af337076 --- /dev/null +++ b/features/step_auditing.feature @@ -0,0 +1,19 @@ +Feature: Step auditing + In order to be able to update my features + As a developer + I want spinach to automatically audit which steps are missing and obsolete + + Scenario: Step file out of date + Given I have defined a "Cheezburger can I has" feature + And I have an associated step file with missing steps and obsolete steps + When I run spinach with "--audit" + Then I should see a list of obsolete steps + And I should see the code to paste for missing steps + + Scenario: With common steps + Given I have defined a "Cheezburger can I has" feature + And I have an associated step file with some steps in a common module + When I run spinach with "--audit" + Then I should not see the common steps marked as missing + + diff --git a/features/steps/step_auditing.rb b/features/steps/step_auditing.rb new file mode 100644 index 00000000..b5eeaa3f --- /dev/null +++ b/features/steps/step_auditing.rb @@ -0,0 +1,78 @@ +class Spinach::Features::StepAuditing < Spinach::FeatureSteps + + include Integration::SpinachRunner + step 'I have defined a "Cheezburger can I has" feature' do + write_file('features/cheezburger_can_i_has.feature', """ +Feature: Cheezburger can I has + Scenario: Some Lulz + Given I haz a sad + When I get some lulz + Then I haz a happy + Scenario: Cannot haz + Given I wantz cheezburger + When I ask can haz + Then I cannot haz +""") + end + + step 'I have an associated step file with missing steps and obsolete steps' do + write_file('features/steps/cheezburger_can_i_has.rb', """ +class Spinach::Features::CheezburgerCanIHaz < Spinach::FeatureSteps + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I get some roxxorz' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end +end +""") + end + + step 'I run spinach with "--audit"' do + run_feature 'features/cheezburger_can_i_has.feature', append: '--audit' + end + + step 'I should see a list of obsolete steps' do + @stdout.must_match("Obsolete step: step_auditing.rb:24 'I get some roxxorz'") + end + + step 'I should see the code to paste for missing steps' do + @stdout.must_match("step 'I get some lulz' do") + @stdout.must_match("step 'I cannot haz' do") + end + + step 'I have an associated step file with some steps in a common module' do + write_file('features/steps/cheezburger_can_i_has.rb', """ +class Spinach::Features::CheezburgerCanIHaz < Spinach::FeatureSteps + include HappySad + step 'I get some roxxorz' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end +end +""") + write_file('features/steps/happy_sad.rb', """ +module HappySad + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end +end +""") + end + + step 'I should not see the common steps marked as missing' do + @stdout.wont_match("step 'I haz a sad' do") + @stdout.wont_match("step 'I haz a happy' do") + end +end From 5367e55e66f32dbedcb9e83cd7472102e4d25612 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Mon, 9 Jan 2017 16:39:02 +0000 Subject: [PATCH 2/9] Get the most basic auditor working, prior to tidyup/refactor --- features/step_auditing.feature | 5 +++- features/steps/step_auditing.rb | 11 +++++-- lib/spinach.rb | 1 + lib/spinach/auditor.rb | 52 +++++++++++++++++++++++++++++++++ lib/spinach/cli.rb | 7 +++++ lib/spinach/config.rb | 13 ++++++++- lib/spinach/dsl.rb | 6 ++++ 7 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 lib/spinach/auditor.rb diff --git a/features/step_auditing.feature b/features/step_auditing.feature index af337076..b3baec63 100644 --- a/features/step_auditing.feature +++ b/features/step_auditing.feature @@ -7,7 +7,7 @@ Feature: Step auditing Given I have defined a "Cheezburger can I has" feature And I have an associated step file with missing steps and obsolete steps When I run spinach with "--audit" - Then I should see a list of obsolete steps + Then I should see a list of unused steps And I should see the code to paste for missing steps Scenario: With common steps @@ -16,4 +16,7 @@ Feature: Step auditing When I run spinach with "--audit" Then I should not see the common steps marked as missing + #Scenario: Steps not marked unused if they're used in at least one feature + #Scenario: Tells the user to generate if step file missing + \ No newline at end of file diff --git a/features/steps/step_auditing.rb b/features/steps/step_auditing.rb index b5eeaa3f..e2f6f16b 100644 --- a/features/steps/step_auditing.rb +++ b/features/steps/step_auditing.rb @@ -17,7 +17,7 @@ class Spinach::Features::StepAuditing < Spinach::FeatureSteps step 'I have an associated step file with missing steps and obsolete steps' do write_file('features/steps/cheezburger_can_i_has.rb', """ -class Spinach::Features::CheezburgerCanIHaz < Spinach::FeatureSteps +class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps step 'I haz a sad' do pending 'step not implemented' end @@ -38,8 +38,8 @@ class Spinach::Features::CheezburgerCanIHaz < Spinach::FeatureSteps run_feature 'features/cheezburger_can_i_has.feature', append: '--audit' end - step 'I should see a list of obsolete steps' do - @stdout.must_match("Obsolete step: step_auditing.rb:24 'I get some roxxorz'") + step 'I should see a list of unused steps' do + @stdout.must_match(/Unused step: .*cheezburger_can_i_has.rb:6 'I get some roxxorz'/) end step 'I should see the code to paste for missing steps' do @@ -75,4 +75,9 @@ module HappySad @stdout.wont_match("step 'I haz a sad' do") @stdout.wont_match("step 'I haz a happy' do") end + + step 'Deary me' do + puts "Oh dear" + end + end diff --git a/lib/spinach.rb b/lib/spinach.rb index a1877523..2ef16198 100644 --- a/lib/spinach.rb +++ b/lib/spinach.rb @@ -11,6 +11,7 @@ require_relative 'spinach/reporter' require_relative 'spinach/cli' require_relative 'spinach/generators' +require_relative 'spinach/auditor' require_relative 'spinach/background' require_relative 'spinach/feature' diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb new file mode 100644 index 00000000..69b47836 --- /dev/null +++ b/lib/spinach/auditor.rb @@ -0,0 +1,52 @@ +require 'pry' + +module Spinach + # The auditor audits steps and determines if any are missing or obsolete. + # + # It is a subclass of Runner because it uses many of the Runner's features + # when auditing. + # + class Auditor < Runner + # audits features + # @files [Array] + # filenames to audit + def run + require_dependencies + + filenames.each do |file| + feature = Parser.open_file(file).parse + step_defs_class = Spinach.find_step_definitions(feature.name) + if !step_defs_class + puts "No definitions!" + next + end + step_defs = step_defs_class.new + step_names = step_defs_class.steps + + feature.scenarios.each do |scenario| + scenario.steps.each do |step| + method_name = Spinach::Support.underscore step.name + if step_defs.respond_to?(method_name) + puts "Found step '#{step.name}'" + else + puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ') + end + step_names.delete step.name # we've audited this step + end + end + + # If there are any steps left at the end, let's mark them as obsolete + step_names.each do |obsolete_name| + location = [] + begin + location = step_defs.method(Spinach::Support.underscore obsolete_name).source_location + rescue NameError + # if the method doesn't exist, just leave location as empty + end + puts "Unused step: #{location.join ':'} '#{obsolete_name}'" + end + end + true + end + end +end \ No newline at end of file diff --git a/lib/spinach/cli.rb b/lib/spinach/cli.rb index f91f9f86..68397511 100644 --- a/lib/spinach/cli.rb +++ b/lib/spinach/cli.rb @@ -24,6 +24,8 @@ def run if Spinach.config.generate Spinach::Generators.run(feature_files) + elsif Spinach.config.audit + Spinach::Auditor.new(feature_files).run else Spinach::Runner.new(feature_files).run end @@ -132,6 +134,11 @@ def parse_options 'Terminate the suite run on the first failure') do |class_name| config[:fail_fast] = true end + + opts.on('-a', '--audit', + 'Audit steps instead of running them, outputting missing and obsolete steps') do + config[:audit] = true + end end.parse!(@args) Spinach.config.parse_from_file diff --git a/lib/spinach/config.rb b/lib/spinach/config.rb index ac227b69..51868e3d 100644 --- a/lib/spinach/config.rb +++ b/lib/spinach/config.rb @@ -33,7 +33,8 @@ class Config :save_and_open_page_on_failure, :reporter_class, :reporter_options, - :fail_fast + :fail_fast, + :audit # The "features path" holds the place where your features will be @@ -142,6 +143,16 @@ def failure_exceptions def fail_fast @fail_fast end + + # "audit" enables step auditing mode + # + # @return [true/false] + # The audit flag. + # + # @api public + def audit + @audit || false + end # It allows you to set a config file to parse for all the other options to be set # diff --git a/lib/spinach/dsl.rb b/lib/spinach/dsl.rb index e21c611f..1a3b1b67 100644 --- a/lib/spinach/dsl.rb +++ b/lib/spinach/dsl.rb @@ -56,6 +56,7 @@ def step(step, &block) end define_method(Spinach::Support.underscore(step), &method_body) + steps << step end alias_method :Given, :step @@ -141,6 +142,11 @@ def after(&block) def feature(name) @feature_name = name end + + # Get the list of step names in this class + def steps + @steps ||= [] + end private From d8ca0db8d6d43b6ae01fd40ac6c5c435ffab2b65 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Mon, 9 Jan 2017 17:21:30 +0000 Subject: [PATCH 3/9] Make it nicer to look at and do a little refactoring --- features/step_auditing.feature | 2 + lib/spinach/auditor.rb | 105 +++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/features/step_auditing.feature b/features/step_auditing.feature index b3baec63..fa61c9b7 100644 --- a/features/step_auditing.feature +++ b/features/step_auditing.feature @@ -19,4 +19,6 @@ Feature: Step auditing #Scenario: Steps not marked unused if they're used in at least one feature #Scenario: Tells the user to generate if step file missing + + #Scenario: Steps still marked unused if they appear in the wrong file \ No newline at end of file diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb index 69b47836..d98b3382 100644 --- a/lib/spinach/auditor.rb +++ b/lib/spinach/auditor.rb @@ -7,46 +7,91 @@ module Spinach # when auditing. # class Auditor < Runner + + attr_accessor :unused_steps + + def initialize(filenames) + super(filenames) + @unused_steps = {} + end + # audits features # @files [Array] # filenames to audit def run require_dependencies - filenames.each do |file| - feature = Parser.open_file(file).parse - step_defs_class = Spinach.find_step_definitions(feature.name) - if !step_defs_class - puts "No definitions!" - next - end - step_defs = step_defs_class.new - step_names = step_defs_class.steps - - feature.scenarios.each do |scenario| - scenario.steps.each do |step| - method_name = Spinach::Support.underscore step.name - if step_defs.respond_to?(method_name) - puts "Found step '#{step.name}'" - else - puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ') - end - step_names.delete step.name # we've audited this step + # Find any missing steps in each file, and keep track of unused steps + clean = true + filenames.each do |file| + clean &&= audit_file(file) + end + + # At the end, report any unused steps + report_unused_steps + + # If the audit was clean, make sure the user knows + if clean + puts "\nAudit clean - no missing steps.".colorize(:light_green) + end + + true + end + + private + + def audit_file(file) + puts "\nAuditing: ".colorize(:magenta) + file.colorize(:light_magenta) + + # Find the feature definition and its associated step defs class + feature = Parser.open_file(file).parse + step_defs_class = Spinach.find_step_definitions(feature.name) + if !step_defs_class + puts " Step file missing: please run --generate first!".colorize(:light_red) + return + end + step_defs = step_defs_class.new + unused_step_names = step_defs_class.steps + missing_steps = [] + + feature.scenarios.each do |scenario| + scenario.steps.each do |step| + method_name = Spinach::Support.underscore step.name + if step_defs.respond_to?(method_name) + # Do nothing for now + # FIXME: Remove unused steps + else + # OK - we can't find a step definition for this step + missing_steps << step end + # Having audited the step, remove it from the list of unused steps + unused_step_names.delete step.name end - - # If there are any steps left at the end, let's mark them as obsolete - step_names.each do |obsolete_name| - location = [] - begin - location = step_defs.method(Spinach::Support.underscore obsolete_name).source_location - rescue NameError - # if the method doesn't exist, just leave location as empty - end - puts "Unused step: #{location.join ':'} '#{obsolete_name}'" + end + + # If there are any steps left at the end, let's mark them as obsolete + unused_step_names.each do |name| + location = step_defs.step_location_for(name) + unused_steps[location.join ':'] = name + end + + # And then generate a report of missing steps + if missing_steps.length > 0 + puts "\nMissing steps:".colorize(:light_cyan) + missing_steps.each do |step| + puts "\n"+Generators::StepGenerator.new(step).generate.gsub(/^/, ' ').colorize(:cyan) end + return false + end + return true + end + + def report_unused_steps + unused_steps.each do |location, name| + puts "\n" + "Unused step: #{location} ".colorize(:yellow) + "'#{name}'".colorize(:light_yellow) end - true end + end + end \ No newline at end of file From 76ec146d3ecb98b593ba9db16531fc100f026898 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Mon, 9 Jan 2017 18:08:50 +0000 Subject: [PATCH 4/9] Much progress - just a few kinks to iron out --- features/step_auditing.feature | 38 ++++++- features/steps/step_auditing.rb | 173 ++++++++++++++++++++++++++++++-- features/support/env.rb | 2 +- lib/spinach/auditor.rb | 11 +- 4 files changed, 206 insertions(+), 18 deletions(-) diff --git a/features/step_auditing.feature b/features/step_auditing.feature index fa61c9b7..9f23e602 100644 --- a/features/step_auditing.feature +++ b/features/step_auditing.feature @@ -14,11 +14,39 @@ Feature: Step auditing Given I have defined a "Cheezburger can I has" feature And I have an associated step file with some steps in a common module When I run spinach with "--audit" - Then I should not see the common steps marked as missing + Then I should not see any steps marked as missing - #Scenario: Steps not marked unused if they're used in at least one feature + Scenario: Steps not marked unused if they're in common modules + Given I have defined a "Cheezburger can I has" feature + And I have associated step files with common steps that are all used somewhere + When I run spinach with "--audit" + Then I should not see any steps marked as unused + + #Scenario: Common steps are reported as missing if not used by any feature - #Scenario: Tells the user to generate if step file missing + Scenario: Tells the user to generate if step file missing + Given I have defined a "Cheezburger can I has" feature + And I have not created an associated step file + When I run spinach with "--audit" + Then I should be told to run "--generate" + + Scenario: Steps still marked unused if they appear in the wrong file + Given I have defined a "Cheezburger can I has" feature + And I have defined an "Awesome new feature" feature + And I have created a step file for each with a step from one feature pasted into the other's file + When I run spinach with "--audit" + Then I should be told that step is unused - #Scenario: Steps still marked unused if they appear in the wrong file - \ No newline at end of file + Scenario: Reports a clean audit if no steps are missing + Given I have defined a "Cheezburger can I has" feature + And I have defined an "Awesome new feature" feature + And I have complete step files for both + When I run spinach with "--audit" + Then I should be told this was a clean audit + + Scenario: Should not report a step as missing more than once + Given I have defined an "Exciting feature" feature with reused steps + And I have created a step file without those reused steps + When I run spinach with "--audit" + Then I should see the missing steps reported only once + diff --git a/features/steps/step_auditing.rb b/features/steps/step_auditing.rb index e2f6f16b..b36067b0 100644 --- a/features/steps/step_auditing.rb +++ b/features/steps/step_auditing.rb @@ -43,13 +43,14 @@ class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps end step 'I should see the code to paste for missing steps' do + @stdout.must_match("Missing steps") @stdout.must_match("step 'I get some lulz' do") @stdout.must_match("step 'I cannot haz' do") end step 'I have an associated step file with some steps in a common module' do write_file('features/steps/cheezburger_can_i_has.rb', """ -class Spinach::Features::CheezburgerCanIHaz < Spinach::FeatureSteps +class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps include HappySad step 'I get some roxxorz' do pending 'step not implemented' @@ -71,13 +72,173 @@ module HappySad """) end - step 'I should not see the common steps marked as missing' do - @stdout.wont_match("step 'I haz a sad' do") - @stdout.wont_match("step 'I haz a happy' do") + step 'I should not see any steps marked as missing' do + @stdout.wont_match("Missing steps") + end + + step 'I should not see any steps marked as unused' do + @stdout.wont_match("Unused step") end - step 'Deary me' do - puts "Oh dear" + step 'I have defined an "Awesome new feature" feature' do + write_file('features/awesome_new_feature.feature', """ + Feature: Awesome new feature + Scenario: Awesomeness + Given I am awesome + When I do anything + Then people will cheer + """) + end + + step 'I have associated step files with common steps that are all used somewhere' do + write_file('features/steps/cheezburger_can_i_has.rb', """ + class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps + include Awesome + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I get some lulz' do + pending 'step not implemented' + end + step 'I wantz cheezburger' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end + step 'I cannot haz' do + pending 'step not implemented' + end + end + """) + write_file('features/steps/awesome_new_feature.rb', """ + class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps + include Awesome + step 'I do anything' do + pending 'step not implemented' + end + end + """) + write_file('features/steps/awesome.rb', """ + module Awesome + step 'I am awesome' do + pending 'step not implemented' + end + step 'people will cheer' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end + end + """) + end + + step 'I have not created an associated step file' do + # Do nothing + end + + step 'I should be told to run "--generate"' do + @stdout.must_match("Step file missing: please run --generate first!") + end + + step 'I have created a step file for each with a step from one feature pasted into the other\'s file' do + write_file('features/steps/cheezburger_can_i_has.rb', """ +class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I get some lulz' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end + step 'I wantz cheezburger' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end + step 'I cannot haz' do + pending 'step not implemented' + end +end +""") + write_file('features/steps/awesome_new_feature.rb', """ +class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps + step 'I am awesome' do + pending 'step not implemented' + end + step 'I do anything' do + pending 'step not implemented' + end + step 'people will cheer' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end +end +""") + end + + step 'I should be told that step is unused' do + @stdout.must_match(/Unused step: .*awesome_new_feature.rb:12 'I haz a happy'/) + end + + step 'I have complete step files for both' do + write_file('features/steps/cheezburger_can_i_has.rb', """ +class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I get some lulz' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end + step 'I wantz cheezburger' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end + step 'I cannot haz' do + pending 'step not implemented' + end +end +""") + write_file('features/steps/awesome_new_feature.rb', """ +class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps + step 'I am awesome' do + pending 'step not implemented' + end + step 'I do anything' do + pending 'step not implemented' + end + step 'people will cheer' do + pending 'step not implemented' + end +end +""") + end + + step 'I should be told this was a clean audit' do + @stdout.must_match('Audit clean - no missing steps.') + end + + step 'I have defined an "Exciting feature" feature with reused steps' do + pending 'step not implemented' + end + + step 'I have created a step file without those reused steps' do + pending 'step not implemented' + end + + step 'I should see the missing steps reported only once' do + pending 'step not implemented' end end diff --git a/features/support/env.rb b/features/support/env.rb index eeec26de..65fd60ff 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -16,6 +16,6 @@ end -Spinach.hooks.after_run do |scenario| +Spinach.hooks.after_scenario do |scenario| FileUtils.rm_rf(Filesystem.dirs) end diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb index d98b3382..56de55c8 100644 --- a/lib/spinach/auditor.rb +++ b/lib/spinach/auditor.rb @@ -1,5 +1,3 @@ -require 'pry' - module Spinach # The auditor audits steps and determines if any are missing or obsolete. # @@ -47,7 +45,7 @@ def audit_file(file) feature = Parser.open_file(file).parse step_defs_class = Spinach.find_step_definitions(feature.name) if !step_defs_class - puts " Step file missing: please run --generate first!".colorize(:light_red) + puts "Step file missing: please run --generate first!".colorize(:light_red) return end step_defs = step_defs_class.new @@ -61,8 +59,9 @@ def audit_file(file) # Do nothing for now # FIXME: Remove unused steps else - # OK - we can't find a step definition for this step - missing_steps << step + # OK - we can't find a step definition for this step - add it to the missing steps + # unless there is already a missing step of the same name + missing_steps << step unless missing_steps.map(&:name).include?(step.name) end # Having audited the step, remove it from the list of unused steps unused_step_names.delete step.name @@ -79,7 +78,7 @@ def audit_file(file) if missing_steps.length > 0 puts "\nMissing steps:".colorize(:light_cyan) missing_steps.each do |step| - puts "\n"+Generators::StepGenerator.new(step).generate.gsub(/^/, ' ').colorize(:cyan) + puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ').colorize(:cyan) end return false end From 2b0814c8149ecfd4095fb3260fa522c953e3605f Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Tue, 10 Jan 2017 11:54:39 +0000 Subject: [PATCH 5/9] Last few bugs and scenarios to defend them --- features/step_auditing.feature | 9 +++- features/steps/step_auditing.rb | 80 ++++++++++++++++++++++++++++++++- lib/spinach/auditor.rb | 23 ++++++---- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/features/step_auditing.feature b/features/step_auditing.feature index 9f23e602..c5ebf8f1 100644 --- a/features/step_auditing.feature +++ b/features/step_auditing.feature @@ -18,11 +18,18 @@ Feature: Step auditing Scenario: Steps not marked unused if they're in common modules Given I have defined a "Cheezburger can I has" feature + And I have defined an "Awesome new feature" feature And I have associated step files with common steps that are all used somewhere When I run spinach with "--audit" Then I should not see any steps marked as unused - #Scenario: Common steps are reported as missing if not used by any feature + Scenario: Common steps are reported as missing if not used by any feature + Given I have defined a "Cheezburger can I has" feature + And I have defined an "Awesome new feature" feature + And I have step files for both with common steps, but one common step is not used by either + When I run spinach with "--audit" + Then I should be told the extra step is unused + But I should not be told the other common steps are unused Scenario: Tells the user to generate if step file missing Given I have defined a "Cheezburger can I has" feature diff --git a/features/steps/step_auditing.rb b/features/steps/step_auditing.rb index b36067b0..ff339b9c 100644 --- a/features/steps/step_auditing.rb +++ b/features/steps/step_auditing.rb @@ -35,7 +35,7 @@ class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps end step 'I run spinach with "--audit"' do - run_feature 'features/cheezburger_can_i_has.feature', append: '--audit' + run_feature 'features', append: '--audit' end step 'I should see a list of unused steps' do @@ -62,6 +62,7 @@ class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps """) write_file('features/steps/happy_sad.rb', """ module HappySad + include Spinach::DSL step 'I haz a sad' do pending 'step not implemented' end @@ -121,6 +122,7 @@ class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps """) write_file('features/steps/awesome.rb', """ module Awesome + include Spinach::DSL step 'I am awesome' do pending 'step not implemented' end @@ -230,15 +232,89 @@ class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps end step 'I have defined an "Exciting feature" feature with reused steps' do - pending 'step not implemented' + write_file('features/exciting_feature.feature', """ +Feature: Exciting feature + Scenario: One + Given I exist + When I do nothing + Then I should still exist + Scenario: Two + Given I exist + When I jump up and down + Then I should still exist +""") end step 'I have created a step file without those reused steps' do + write_file('features/steps/exciting_feature.rb', """ +class Spinach::Features::ExcitingFeature < Spinach::FeatureSteps + step 'I do nothing' do pending 'step not implemented' end +end +""") + end step 'I should see the missing steps reported only once' do + @stdout.scan('I exist').count.must_equal 1 + @stdout.scan('I should still exist').count.must_equal 1 + end + + step 'I have step files for both with common steps, but one common step is not used by either' do + write_file('features/steps/cheezburger_can_i_has.rb', """ +class Spinach::Features::CheezburgerCanIHas < Spinach::FeatureSteps + include Awesome + step 'I haz a sad' do + pending 'step not implemented' + end + step 'I get some lulz' do pending 'step not implemented' end + step 'I wantz cheezburger' do + pending 'step not implemented' + end + step 'I ask can haz' do + pending 'step not implemented' + end + step 'I cannot haz' do + pending 'step not implemented' + end +end +""") + write_file('features/steps/awesome_new_feature.rb', """ +class Spinach::Features::AwesomeNewFeature < Spinach::FeatureSteps + include Awesome + step 'I am awesome' do + pending 'step not implemented' + end + step 'people will cheer' do + pending 'step not implemented'; + end +end +""") + write_file('features/steps/awesome.rb', """ +module Awesome + include Spinach::DSL + step 'I do anything' do + pending 'step not implemented' + end + step 'I haz a happy' do + pending 'step not implemented' + end + step 'I enter the void' do + pending 'step not implemented' + end +end +""") + end + + step 'I should be told the extra step is unused' do + @stdout.must_match(/Unused step: .*awesome.rb:10 'I enter the void'/) + end + + step 'I should not be told the other common steps are unused' do + @stdout.wont_match('I do anything') + @stdout.wont_match('I haz a happy') + end end diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb index 56de55c8..a8918d59 100644 --- a/lib/spinach/auditor.rb +++ b/lib/spinach/auditor.rb @@ -1,3 +1,5 @@ +require 'set' + module Spinach # The auditor audits steps and determines if any are missing or obsolete. # @@ -6,11 +8,12 @@ module Spinach # class Auditor < Runner - attr_accessor :unused_steps + attr_accessor :unused_steps, :used_steps def initialize(filenames) super(filenames) @unused_steps = {} + @used_steps = Set.new end # audits features @@ -22,7 +25,8 @@ def run # Find any missing steps in each file, and keep track of unused steps clean = true filenames.each do |file| - clean &&= audit_file(file) + result = audit_file(file) + clean &&= result # set to false if any result is false end # At the end, report any unused steps @@ -49,15 +53,16 @@ def audit_file(file) return end step_defs = step_defs_class.new - unused_step_names = step_defs_class.steps + unused_step_names = step_defs_class.ancestors.map {|a| a.respond_to?(:steps) ? a.steps : [] }.flatten missing_steps = [] feature.scenarios.each do |scenario| scenario.steps.each do |step| method_name = Spinach::Support.underscore step.name if step_defs.respond_to?(method_name) - # Do nothing for now - # FIXME: Remove unused steps + # Remember the location so we can subtract from unused_steps at the end + location = step_defs.step_location_for(step.name).join(':') + used_steps << location else # OK - we can't find a step definition for this step - add it to the missing steps # unless there is already a missing step of the same name @@ -68,10 +73,10 @@ def audit_file(file) end end - # If there are any steps left at the end, let's mark them as obsolete + # If there are any steps left at the end, let's mark them as unused unused_step_names.each do |name| - location = step_defs.step_location_for(name) - unused_steps[location.join ':'] = name + location = step_defs.step_location_for(name).join(':') + unused_steps[location] = name end # And then generate a report of missing steps @@ -86,6 +91,8 @@ def audit_file(file) end def report_unused_steps + # Remove any unused_steps that were in common modules and used in another feature + used_steps.each {|location| unused_steps.delete location} unused_steps.each do |location, name| puts "\n" + "Unused step: #{location} ".colorize(:yellow) + "'#{name}'".colorize(:light_yellow) end From ad658c3ac3c2d4a59c017f62283aebf9ba71f152 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Tue, 10 Jan 2017 13:52:21 +0000 Subject: [PATCH 6/9] Fix hound issues --- lib/spinach/auditor.rb | 72 ++++++++++++++++++++++++------------------ lib/spinach/cli.rb | 5 +-- lib/spinach/config.rb | 2 +- lib/spinach/dsl.rb | 2 +- 4 files changed, 46 insertions(+), 35 deletions(-) diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb index a8918d59..24cae4ab 100644 --- a/lib/spinach/auditor.rb +++ b/lib/spinach/auditor.rb @@ -7,21 +7,21 @@ module Spinach # when auditing. # class Auditor < Runner - + attr_accessor :unused_steps, :used_steps - + def initialize(filenames) super(filenames) @unused_steps = {} @used_steps = Set.new - end - + end + # audits features # @files [Array] # filenames to audit def run require_dependencies - + # Find any missing steps in each file, and keep track of unused steps clean = true filenames.each do |file| @@ -29,75 +29,85 @@ def run clean &&= result # set to false if any result is false end - # At the end, report any unused steps + # At the end, report any unused steps report_unused_steps - + # If the audit was clean, make sure the user knows if clean puts "\nAudit clean - no missing steps.".colorize(:light_green) end - + true end - + private - + def audit_file(file) puts "\nAuditing: ".colorize(:magenta) + file.colorize(:light_magenta) - + # Find the feature definition and its associated step defs class feature = Parser.open_file(file).parse step_defs_class = Spinach.find_step_definitions(feature.name) if !step_defs_class - puts "Step file missing: please run --generate first!".colorize(:light_red) + puts "Step file missing: please run --generate first!". + colorize(:light_red) return end step_defs = step_defs_class.new - unused_step_names = step_defs_class.ancestors.map {|a| a.respond_to?(:steps) ? a.steps : [] }.flatten + unused_step_names = step_defs_class.ancestors. + map { |a| a.respond_to?(:steps) ? a.steps : [] }.flatten missing_steps = [] - + feature.scenarios.each do |scenario| scenario.steps.each do |step| method_name = Spinach::Support.underscore step.name if step_defs.respond_to?(method_name) - # Remember the location so we can subtract from unused_steps at the end + # Remember the location so we can subtract from unused_steps + # at the end location = step_defs.step_location_for(step.name).join(':') used_steps << location else - # OK - we can't find a step definition for this step - add it to the missing steps - # unless there is already a missing step of the same name - missing_steps << step unless missing_steps.map(&:name).include?(step.name) + # OK - we can't find a step definition for this step - add it to + # the missing steps unless there is already a missing step of the + # same name + unless missing_steps.map(&:name).include?(step.name) + missing_steps << step + end end # Having audited the step, remove it from the list of unused steps unused_step_names.delete step.name end end - + # If there are any steps left at the end, let's mark them as unused unused_step_names.each do |name| location = step_defs.step_location_for(name).join(':') unused_steps[location] = name end - + # And then generate a report of missing steps - if missing_steps.length > 0 + if missing_steps.empty? + return true + else puts "\nMissing steps:".colorize(:light_cyan) - missing_steps.each do |step| - puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ').colorize(:cyan) + missing_steps.each do |step| + puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' '). + colorize(:cyan) end return false end - return true end - + def report_unused_steps - # Remove any unused_steps that were in common modules and used in another feature - used_steps.each {|location| unused_steps.delete location} + # Remove any unused_steps that were in common modules and used + # in another feature + used_steps.each { |location| unused_steps.delete location } unused_steps.each do |location, name| - puts "\n" + "Unused step: #{location} ".colorize(:yellow) + "'#{name}'".colorize(:light_yellow) + puts "\n" + "Unused step: #{location} ".colorize(:yellow) + + "'#{name}'".colorize(:light_yellow) end end - + end - -end \ No newline at end of file + +end diff --git a/lib/spinach/cli.rb b/lib/spinach/cli.rb index 68397511..a95f7bd9 100644 --- a/lib/spinach/cli.rb +++ b/lib/spinach/cli.rb @@ -134,9 +134,10 @@ def parse_options 'Terminate the suite run on the first failure') do |class_name| config[:fail_fast] = true end - + opts.on('-a', '--audit', - 'Audit steps instead of running them, outputting missing and obsolete steps') do + 'Audit steps instead of running them, outputting missing '+ + 'and obsolete steps') do config[:audit] = true end end.parse!(@args) diff --git a/lib/spinach/config.rb b/lib/spinach/config.rb index 51868e3d..8df9badf 100644 --- a/lib/spinach/config.rb +++ b/lib/spinach/config.rb @@ -143,7 +143,7 @@ def failure_exceptions def fail_fast @fail_fast end - + # "audit" enables step auditing mode # # @return [true/false] diff --git a/lib/spinach/dsl.rb b/lib/spinach/dsl.rb index 1a3b1b67..d76906eb 100644 --- a/lib/spinach/dsl.rb +++ b/lib/spinach/dsl.rb @@ -142,7 +142,7 @@ def after(&block) def feature(name) @feature_name = name end - + # Get the list of step names in this class def steps @steps ||= [] From 52c3992af97c492b242f01a827ef78ffac507625 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Tue, 10 Jan 2017 14:45:16 +0000 Subject: [PATCH 7/9] Fix more rubocop issues --- features/steps/step_auditing.rb | 4 ++ lib/spinach/auditor.rb | 115 ++++++++++++++++++-------------- lib/spinach/cli.rb | 4 +- lib/spinach/feature.rb | 6 ++ 4 files changed, 77 insertions(+), 52 deletions(-) diff --git a/features/steps/step_auditing.rb b/features/steps/step_auditing.rb index ff339b9c..d7562a89 100644 --- a/features/steps/step_auditing.rb +++ b/features/steps/step_auditing.rb @@ -317,4 +317,8 @@ module Awesome @stdout.wont_match('I haz a happy') end + step 'bad' do + pending 'hello' + end + end diff --git a/lib/spinach/auditor.rb b/lib/spinach/auditor.rb index 24cae4ab..49bc8dc9 100644 --- a/lib/spinach/auditor.rb +++ b/lib/spinach/auditor.rb @@ -7,7 +7,6 @@ module Spinach # when auditing. # class Auditor < Runner - attr_accessor :unused_steps, :used_steps def initialize(filenames) @@ -33,9 +32,7 @@ def run report_unused_steps # If the audit was clean, make sure the user knows - if clean - puts "\nAudit clean - no missing steps.".colorize(:light_green) - end + puts "\nAudit clean - no missing steps.".colorize(:light_green) if clean true end @@ -46,68 +43,86 @@ def audit_file(file) puts "\nAuditing: ".colorize(:magenta) + file.colorize(:light_magenta) # Find the feature definition and its associated step defs class - feature = Parser.open_file(file).parse - step_defs_class = Spinach.find_step_definitions(feature.name) - if !step_defs_class - puts "Step file missing: please run --generate first!". - colorize(:light_red) - return - end - step_defs = step_defs_class.new - unused_step_names = step_defs_class.ancestors. - map { |a| a.respond_to?(:steps) ? a.steps : [] }.flatten - missing_steps = [] - - feature.scenarios.each do |scenario| - scenario.steps.each do |step| - method_name = Spinach::Support.underscore step.name - if step_defs.respond_to?(method_name) - # Remember the location so we can subtract from unused_steps - # at the end - location = step_defs.step_location_for(step.name).join(':') - used_steps << location - else - # OK - we can't find a step definition for this step - add it to - # the missing steps unless there is already a missing step of the - # same name - unless missing_steps.map(&:name).include?(step.name) - missing_steps << step - end - end - # Having audited the step, remove it from the list of unused steps - unused_step_names.delete step.name - end + feature, step_defs_class = get_feature_and_defs(file) + return step_file_missing if step_defs_class.nil? + step_defs = step_defs_class.new + unused_step_names = step_names_for_class(step_defs_class) + + missing_steps = {} + + feature.each_step do |step| + # Audit the step + missing_steps[step.name] = step if step_missing?(step, step_defs) + # Having audited the step, remove it from the list of unused steps + unused_step_names.delete step.name end # If there are any steps left at the end, let's mark them as unused - unused_step_names.each do |name| + store_unused_steps(unused_step_names, step_defs) + + # And then generate a report of missing steps + return true if missing_steps.empty? + report_missing_steps(missing_steps.values) + false + end + + # Get the feature and its definitions from the appropriate files + def get_feature_and_defs(file) + feature = Parser.open_file(file).parse + [feature, Spinach.find_step_definitions(feature.name)] + end + + # Process a step from the feature file using the given step_defs. + # If it is missing, return true. Otherwise, add it to the used_steps for + # the report at the end and return false. + def step_missing?(step, step_defs) + method_name = Spinach::Support.underscore step.name + return true unless step_defs.respond_to?(method_name) + # Remember that we have used this step + used_steps << step_defs.step_location_for(step.name).join(':') + false + end + + # Store any unused step names for the report at the end of the audit + def store_unused_steps(names, step_defs) + names.each do |name| location = step_defs.step_location_for(name).join(':') unused_steps[location] = name end + end - # And then generate a report of missing steps - if missing_steps.empty? - return true - else - puts "\nMissing steps:".colorize(:light_cyan) - missing_steps.each do |step| - puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' '). - colorize(:cyan) - end - return false - end + # Print a message alerting the user that there is no step file for this + # feature + def step_file_missing + puts 'Step file missing: please run --generate first!' + .colorize(:light_red) + false end + # Get the step names for all steps in the given class, including those in + # common modules + def step_names_for_class(klass) + klass.ancestors.map { |a| a.respond_to?(:steps) ? a.steps : [] }.flatten + end + + # Produce a report of unused steps that were not found anywhere in the audit def report_unused_steps # Remove any unused_steps that were in common modules and used # in another feature used_steps.each { |location| unused_steps.delete location } unused_steps.each do |location, name| - puts "\n" + "Unused step: #{location} ".colorize(:yellow) + - "'#{name}'".colorize(:light_yellow) + puts "\n" + "Unused step: #{location} ".colorize(:yellow) + + "'#{name}'".colorize(:light_yellow) end end + # Print a report of the missing step objects provided + def report_missing_steps(steps) + puts "\nMissing steps:".colorize(:light_cyan) + steps.each do |step| + puts Generators::StepGenerator.new(step).generate.gsub(/^/, ' ') + .colorize(:cyan) + end + end end - end diff --git a/lib/spinach/cli.rb b/lib/spinach/cli.rb index a95f7bd9..aa261c89 100644 --- a/lib/spinach/cli.rb +++ b/lib/spinach/cli.rb @@ -136,8 +136,8 @@ def parse_options end opts.on('-a', '--audit', - 'Audit steps instead of running them, outputting missing '+ - 'and obsolete steps') do + "Audit steps instead of running them, outputting missing \ +and obsolete steps") do config[:audit] = true end end.parse!(@args) diff --git a/lib/spinach/feature.rb b/lib/spinach/feature.rb index b7659ab2..7466d676 100644 --- a/lib/spinach/feature.rb +++ b/lib/spinach/feature.rb @@ -17,5 +17,11 @@ def background_steps def lines=(value) @lines = value.map(&:to_i) if value end + + # Run the provided code for every step + def each_step + scenarios.each { |scenario| scenario.steps.each { |step| yield step } } + end + end end From 9ee1c03929eb8d420b380e2dbed0109c231106b4 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Tue, 10 Jan 2017 14:46:23 +0000 Subject: [PATCH 8/9] Oops - fix issues in this file too --- lib/spinach/feature.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/spinach/feature.rb b/lib/spinach/feature.rb index 7466d676..83f1c3f1 100644 --- a/lib/spinach/feature.rb +++ b/lib/spinach/feature.rb @@ -17,11 +17,10 @@ def background_steps def lines=(value) @lines = value.map(&:to_i) if value end - + # Run the provided code for every step def each_step scenarios.each { |scenario| scenario.steps.each { |step| yield step } } end - end end From 5546f7c9230d3abd754f134fd98c042f0c2b4296 Mon Sep 17 00:00:00 2001 From: Rich Daley Date: Mon, 16 Jan 2017 09:48:06 +0000 Subject: [PATCH 9/9] Add --audit instructions to the README --- README.markdown | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.markdown b/README.markdown index 5fdf72de..215e5344 100644 --- a/README.markdown +++ b/README.markdown @@ -181,6 +181,29 @@ class Spinach::Features::BuyAWidget < Spinach::FeatureSteps end ``` +## Audit + +Over time, the definitions of your features will change. When you add, remove +or change steps in the feature files, you can easily audit your existing step +files with: + +```shell +$ spinach --audit +``` + +This will find any new steps and print out boilerplate for them, and alert you +to the filename and line number of any unused steps in your step files. + +This does not modify the step files, so you will need to paste the boilerplate +into the appropriate places. If a new feature file is detected, you will be +asked to run `spinach --generate` beforehand. + +**Important**: If auditing individual files, common steps (as above) may be +reported as unused when they are actually used in a feature file that is not +currently being audited. To avoid this, run the audit with no arguments to +audit all step files simultaneously. + + ## Tags Feature and Scenarios can be marked with tags in the form: `@tag`. Tags can be