chutney 1.6.3 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -3
  4. data/README.md +44 -30
  5. data/Rakefile +10 -24
  6. data/chutney.gemspec +13 -10
  7. data/config/{default.yml → chutney.yml} +6 -3
  8. data/docs/.keep +0 -0
  9. data/exe/chutney +28 -22
  10. data/img/chutney.svg +852 -0
  11. data/img/formatters.png +0 -0
  12. data/lib/chutney.rb +61 -85
  13. data/lib/chutney/configuration.rb +6 -7
  14. data/lib/chutney/formatter.rb +21 -0
  15. data/lib/chutney/formatter/json_formatter.rb +8 -0
  16. data/lib/chutney/formatter/pie_formatter.rb +78 -0
  17. data/lib/chutney/formatter/rainbow_formatter.rb +47 -0
  18. data/lib/chutney/linter.rb +145 -113
  19. data/lib/chutney/linter/avoid_full_stop.rb +12 -0
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -6
  21. data/lib/chutney/linter/avoid_scripting.rb +10 -15
  22. data/lib/chutney/linter/background_does_more_than_setup.rb +7 -10
  23. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +2 -3
  24. data/lib/chutney/linter/bad_scenario_name.rb +4 -11
  25. data/lib/chutney/linter/file_name_differs_feature_name.rb +5 -10
  26. data/lib/chutney/linter/givens_after_background.rb +17 -0
  27. data/lib/chutney/linter/invalid_file_name.rb +14 -6
  28. data/lib/chutney/linter/invalid_step_flow.rb +18 -18
  29. data/lib/chutney/linter/missing_example_name.rb +13 -11
  30. data/lib/chutney/linter/missing_feature_description.rb +2 -9
  31. data/lib/chutney/linter/missing_feature_name.rb +3 -12
  32. data/lib/chutney/linter/missing_scenario_name.rb +3 -7
  33. data/lib/chutney/linter/missing_test_action.rb +3 -6
  34. data/lib/chutney/linter/missing_verification.rb +3 -4
  35. data/lib/chutney/linter/required_tags_starts_with.rb +20 -5
  36. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +24 -28
  37. data/lib/chutney/linter/scenario_names_match.rb +6 -8
  38. data/lib/chutney/linter/tag_used_multiple_times.rb +6 -13
  39. data/lib/chutney/linter/too_clumsy.rb +4 -4
  40. data/lib/chutney/linter/too_long_step.rb +8 -18
  41. data/lib/chutney/linter/too_many_different_tags.rb +13 -34
  42. data/lib/chutney/linter/too_many_steps.rb +10 -6
  43. data/lib/chutney/linter/too_many_tags.rb +11 -10
  44. data/lib/chutney/linter/unique_scenario_names.rb +19 -13
  45. data/lib/chutney/linter/unknown_variable.rb +5 -6
  46. data/lib/chutney/linter/unused_variable.rb +6 -7
  47. data/lib/chutney/linter/use_background.rb +11 -29
  48. data/lib/chutney/linter/use_outline.rb +21 -15
  49. data/lib/chutney/version.rb +1 -1
  50. data/lib/config/locales/en.yml +93 -0
  51. data/spec/chutney_spec.rb +54 -62
  52. data/spec/spec_helper.rb +103 -0
  53. metadata +75 -44
  54. data/Guardfile +0 -3
  55. data/lib/chutney/linter/avoid_period.rb +0 -19
  56. data/lib/chutney/linter/be_declarative.rb +0 -49
  57. data/lib/chutney/linter/tag_collector.rb +0 -10
  58. data/lib/chutney/linter/tag_constraint.rb +0 -35
  59. data/spec/configuration_spec.rb +0 -58
  60. data/spec/required_tags_starts_with_spec.rb +0 -74
  61. data/spec/shared_contexts/file_exists.rb +0 -12
  62. data/spec/shared_contexts/gherkin_linter.rb +0 -14
@@ -0,0 +1,12 @@
1
+ module Chutney
2
+ # service class to lint for avoiding periods
3
+ class AvoidFullStop < Linter
4
+ def lint
5
+ steps do |feature, child, step|
6
+
7
+ add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step[:text].strip.end_with? '.'
8
+
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,18 +1,15 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for avoiding outline for single example
5
3
  class AvoidOutlineForSingleExample < Linter
6
4
  def lint
7
- scenarios do |file, feature, scenario|
5
+ scenarios do |feature, scenario|
8
6
  next unless scenario[:type] == :ScenarioOutline
9
7
 
10
8
  next unless scenario.key? :examples
11
9
  next if scenario[:examples].length > 1
12
10
  next if scenario[:examples].first[:tableBody].length > 1
13
-
14
- references = [reference(file, feature, scenario)]
15
- add_error(references, 'You have a Scenarion Outline with a single example - rewrite to use a Scenario')
11
+
12
+ add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
16
13
  end
17
14
  end
18
15
  end
@@ -1,24 +1,19 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for avoid scripting
5
- class AvoidScripting < Linter
6
- MESSAGE = "You have multiple (%d) 'When' actions in your steps - you should only have one".freeze
7
-
3
+ class AvoidScripting < Linter
8
4
  def lint
9
- filled_scenarios do |file, feature, scenario|
10
- steps = filter_when_steps scenario[:steps]
11
- next if steps.length <= 1
12
-
13
- references = [reference(file, feature, scenario)]
14
- add_error(references, MESSAGE % steps.length)
5
+ scenarios do |feature, scenario|
6
+ when_steps = filter_when_steps(scenario[:steps])
7
+ whens = when_steps.count
8
+ add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
15
9
  end
16
10
  end
17
-
11
+
18
12
  def filter_when_steps(steps)
19
- steps = steps.drop_while { |step| step[:keyword] != 'When ' }
20
- steps = steps.reverse.drop_while { |step| step[:keyword] != 'Then ' }.reverse
21
- steps.reject { |step| step[:keyword] == 'Then ' }
13
+ steps
14
+ .drop_while { |step| !when_word?(step[:keyword]) }
15
+ .then { |s| s.reverse.drop_while { |step| !then_word?(step[:keyword]) }.reverse }
16
+ .then { |s| s.reject { |step| then_word?(step[:keyword]) } }
22
17
  end
23
18
  end
24
19
  end
@@ -1,19 +1,16 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for background that does more than setup
5
- class BackgroundDoesMoreThanSetup < Linter
6
- MESSAGE = 'A Feature\'s Background should just contain \'Given\' steps'.freeze
7
-
3
+ class BackgroundDoesMoreThanSetup < Linter
8
4
  def lint
9
- backgrounds do |file, feature, background|
5
+ background do |feature, background|
10
6
  next unless background.key? :steps
11
7
 
12
- invalid_steps = background[:steps].select { |step| step[:keyword] == 'When ' || step[:keyword] == 'Then ' }
8
+ invalid_steps = background[:steps].select do |step|
9
+ when_word?(step[:keyword]) || then_word?(step[:keyword])
10
+ end
13
11
  next if invalid_steps.empty?
14
-
15
- references = [reference(file, feature, background, invalid_steps[0])]
16
- add_error(references, MESSAGE)
12
+
13
+ add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
17
14
  end
18
15
  end
19
16
  end
@@ -6,12 +6,11 @@ module Chutney
6
6
  MESSAGE = 'Avoid using Background steps for just one scenario'.freeze
7
7
 
8
8
  def lint
9
- backgrounds do |file, feature, background|
9
+ background do |feature, background|
10
10
  scenarios = feature[:children].reject { |element| element[:type] == :Background }
11
11
  next if scenarios.length >= 2
12
12
 
13
- references = [reference(file, feature, background)]
14
- add_error(references, MESSAGE)
13
+ add_issue(I18n.t('linters.background_requires_multiple_scenarios'), feature, background)
15
14
  end
16
15
  end
17
16
  end
@@ -1,20 +1,13 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for bad scenario names
5
3
  class BadScenarioName < Linter
6
- MESSAGE = 'You should avoid using words like \'test\', \'check\' or \'verify\' ' \
7
- 'when naming your scenarios to keep them understandable'.freeze
8
-
9
4
  def lint
10
- scenarios do |file, feature, scenario|
5
+ scenarios do |feature, scenario|
11
6
  next if scenario[:name].empty?
12
7
 
13
- references = [reference(file, feature, scenario)]
14
- bad_words = %w[test verif check]
15
- bad_words.each do |bad_word|
16
- add_error(references, MESSAGE) if scenario[:name].downcase.include? bad_word
17
- end
8
+ bad = /\w*(test|verif|check)\w*/i
9
+ match = scenario[:name].match(bad).to_a.first
10
+ add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
18
11
  end
19
12
  end
20
13
  end
@@ -1,22 +1,17 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for file name differs feature name
5
3
  class FileNameDiffersFeatureName < Linter
6
4
  def lint
7
- features do |file, feature|
8
- next unless feature.include? :name
5
+ return unless feature.include? :name
9
6
 
10
- expected_feature_name = title_case file
11
- next if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
7
+ expected_feature_name = title_case(filename)
8
+ return if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
12
9
 
13
- references = [reference(file, feature)]
14
- add_error(references, "Feature name should be '#{expected_feature_name}'")
15
- end
10
+ add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
16
11
  end
17
12
 
18
13
  def title_case(value)
19
- value = File.basename(value, '.feature')
14
+ value = File.basename(value, '.*')
20
15
  value.split('_').collect(&:capitalize).join(' ')
21
16
  end
22
17
 
@@ -0,0 +1,17 @@
1
+ module Chutney
2
+ # service class to lint for bad scenario names
3
+ class GivensAfterBackground < Linter
4
+ def lint
5
+ return if background.nil?
6
+ return if background.empty?
7
+
8
+ filled_scenarios do |feature, scenario|
9
+ scenario[:steps].each do |step|
10
+ if given_word?(step[:keyword])
11
+ add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,13 +4,21 @@ module Chutney
4
4
  # service class to lint for invalid file names
5
5
  class InvalidFileName < Linter
6
6
  def lint
7
- files do |file|
8
- base = File.basename file
9
- next unless base != base.downcase || base =~ /[ -]/
10
-
11
- references = [reference(file)]
12
- add_error(references, 'Feature files should be snake_cased')
7
+ feature do |f|
8
+ base = File.basename(filename, '.*')
9
+ if base != base.downcase || base =~ /[ -]/
10
+ add_issue(I18n.t('linters.invalid_file_name', recommended_name: recommend(filename)), f)
11
+ end
13
12
  end
14
13
  end
14
+
15
+ def recommend(filename)
16
+ File.basename(filename, '.*').gsub(/::/, '/')
17
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
18
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
19
+ .tr('-', '_')
20
+ .tr(' ', '_')
21
+ .downcase << '.feature'
22
+ end
15
23
  end
16
24
  end
@@ -1,40 +1,40 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for invalid step flow
5
3
  class InvalidStepFlow < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- steps = scenario[:steps].select { |step| step[:keyword] != 'And ' && step[:keyword] != 'But ' }
5
+ filled_scenarios do |feature, scenario|
6
+ steps = scenario[:steps].select { |step| !and_word?(step[:keyword]) && !but_word?(step[:keyword]) }
9
7
  next if steps.empty?
10
8
 
11
- last_step_is_an_action(file, feature, scenario, steps)
12
- given_after_non_given(file, feature, scenario, steps)
13
- verification_before_action(file, feature, scenario, steps)
9
+ last_step_is_an_action(feature, scenario, steps)
10
+ given_after_non_given(feature, scenario, steps)
11
+ verification_before_action(feature, scenario, steps)
14
12
  end
15
13
  end
16
14
 
17
- def last_step_is_an_action(file, feature, scenario, steps)
18
- references = [reference(file, feature, scenario, steps.last)]
19
- add_error(references, 'Last step is an action') if steps.last[:keyword] == 'When '
15
+ def last_step_is_an_action(feature, scenario, steps)
16
+ return unless when_word?(steps.last[:keyword])
17
+
18
+ add_issue(I18n.t('linters.invalid_step_flow.action_last'), feature, scenario, steps.last)
20
19
  end
21
20
 
22
- def given_after_non_given(file, feature, scenario, steps)
21
+ def given_after_non_given(feature, scenario, steps)
23
22
  last_step = steps.first
24
23
  steps.each do |step|
25
- references = [reference(file, feature, scenario, step)]
26
- description = 'Given after Action or Verification'
27
- add_error(references, description) if step[:keyword] == 'Given ' && last_step[:keyword] != 'Given '
24
+ if given_word?(step[:keyword]) && !given_word?(last_step[:keyword])
25
+ add_issue(I18n.t('linters.invalid_step_flow.given_order'), feature, scenario, step)
26
+ end
28
27
  last_step = step
29
28
  end
30
29
  end
31
30
 
32
- def verification_before_action(file, feature, scenario, steps)
31
+ def verification_before_action(feature, scenario, steps)
33
32
  steps.each do |step|
34
- break if step[:keyword] == 'When '
33
+ break if when_word?(step[:keyword])
35
34
 
36
- references = [reference(file, feature, scenario, step)]
37
- add_error(references, 'Missing Action step') if step[:keyword] == 'Then '
35
+ if then_word?(step[:keyword])
36
+ add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
37
+ end
38
38
  end
39
39
  end
40
40
  end
@@ -1,23 +1,25 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing example names
5
3
  class MissingExampleName < Linter
6
- MESSAGE = 'You have an unnamed or ambiguously named example'.freeze
7
4
 
8
5
  def lint
9
- scenarios do |file, feature, scenario|
10
- next unless scenario.key? :examples
11
- next unless scenario[:examples].length > 1
12
-
6
+ scenarios do |_feature, scenario|
7
+ next unless scenario[:examples]
8
+
13
9
  scenario[:examples].each do |example|
14
- name = example.key?(:name) ? example[:name].strip : ''
15
- next unless name.empty?
10
+ example_count = scenario[:examples]&.length || 0
11
+ next unless example_count > 1
16
12
 
17
- references = [reference(file, feature, scenario)]
18
- add_error(references, MESSAGE)
13
+ check_example(scenario, example)
19
14
  end
20
15
  end
16
+ end
17
+
18
+ def check_example(scenario, example)
19
+ name = example.key?(:name) ? example[:name].strip : ''
20
+ duplicate_name_count = scenario[:examples].filter { |e| e[:name] == name }.count
21
+ add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
21
22
  end
23
+
22
24
  end
23
25
  end
@@ -1,17 +1,10 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing feature descriptions
5
3
  class MissingFeatureDescription < Linter
6
4
  MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
7
5
  def lint
8
- features do |file, feature|
9
- name = feature.key?(:description) ? feature[:description].strip : ''
10
- next unless name.empty?
11
-
12
- references = [reference(file, feature)]
13
- add_error(references, MESSAGE)
14
- end
6
+ name = feature.key?(:description) ? feature[:description].strip : ''
7
+ add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
15
8
  end
16
9
  end
17
10
  end
@@ -1,18 +1,9 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing feature names
5
- class MissingFeatureName < Linter
6
- MESSAGE = 'All features should have a name'.freeze
7
-
3
+ class MissingFeatureName < Linter
8
4
  def lint
9
- features do |file, feature|
10
- name = feature.key?(:name) ? feature[:name].strip : ''
11
- next unless name.empty?
12
-
13
- references = [reference(file, feature)]
14
- add_error(references, MESSAGE)
15
- end
5
+ name = feature.key?(:name) ? feature[:name].strip : ''
6
+ add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
16
7
  end
17
8
  end
18
9
  end
@@ -1,17 +1,13 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing scenario names
5
- class MissingScenarioName < Linter
6
- MESSAGE = 'All scenarios should have a name'.freeze
3
+ class MissingScenarioName < Linter
7
4
 
8
5
  def lint
9
- scenarios do |file, feature, scenario|
6
+ scenarios do |feature, scenario|
10
7
  name = scenario.key?(:name) ? scenario[:name].strip : ''
11
- references = [reference(file, feature, scenario)]
12
8
  next unless name.empty?
13
9
 
14
- add_error(references, MESSAGE)
10
+ add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if name.empty?
15
11
  end
16
12
  end
17
13
  end
@@ -1,15 +1,12 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing test actions
5
3
  class MissingTestAction < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- when_steps = scenario[:steps].select { |step| step[:keyword] == 'When ' }
5
+ filled_scenarios do |feature, scenario|
6
+ when_steps = scenario[:steps].select { |step| when_word?(step[:keyword]) }
9
7
  next unless when_steps.empty?
10
8
 
11
- references = [reference(file, feature, scenario)]
12
- add_error(references, 'No \'When\'-Step')
9
+ add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
13
10
  end
14
11
  end
15
12
  end
@@ -4,12 +4,11 @@ module Chutney
4
4
  # service class to lint for missing verifications
5
5
  class MissingVerification < Linter
6
6
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- then_steps = scenario[:steps].select { |step| step[:keyword] == 'Then ' }
7
+ filled_scenarios do |feature, scenario|
8
+ then_steps = scenario[:steps].select { |step| then_word?(step[:keyword]) }
9
9
  next unless then_steps.empty?
10
10
 
11
- references = [reference(file, feature, scenario)]
12
- add_error(references, 'No \'Then\' step')
11
+ add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
13
12
  end
14
13
  end
15
14
  end
@@ -1,14 +1,29 @@
1
- require 'chutney/linter/tag_constraint'
2
- require 'chutney/linter'
3
-
4
1
  module Chutney
5
2
  # service class to lint for tags used multiple times
6
3
  class RequiredTagsStartsWith < Linter
7
- include TagConstraint
4
+
5
+ def lint
6
+ return unless pattern
8
7
 
8
+ scenarios do |feature, scenario|
9
+ next if match_pattern? tags_for(feature)
10
+ next if match_pattern? tags_for(scenario)
11
+
12
+ add_issue(
13
+ I18n.t('linters.required_tags_starts_with',
14
+ allowed: pattern.join(', ')),
15
+ feature, scenario
16
+ )
17
+ end
18
+ end
19
+
20
+ def pattern
21
+ configuration['Matcher'] || nil
22
+ end
23
+
9
24
  def match_pattern?(target)
10
25
  target.each do |t|
11
- return true if t.delete!('@').start_with?(*@pattern)
26
+ return true if t.start_with?(*pattern)
12
27
  end
13
28
  false
14
29
  end