chutney 2.1.1 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/Gemfile +2 -0
  4. data/README.md +5 -1
  5. data/Rakefile +2 -0
  6. data/chutney.gemspec +13 -6
  7. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  8. data/config/cucumber.yml +1 -0
  9. data/docs/usage/rules.md +3 -0
  10. data/exe/chutney +23 -3
  11. data/lib/chutney.rb +26 -22
  12. data/lib/chutney/configuration.rb +9 -2
  13. data/lib/chutney/formatter.rb +6 -5
  14. data/lib/chutney/formatter/json_formatter.rb +2 -0
  15. data/lib/chutney/formatter/pie_formatter.rb +10 -13
  16. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  17. data/lib/chutney/issue.rb +2 -0
  18. data/lib/chutney/linter.rb +95 -84
  19. data/lib/chutney/linter/avoid_full_stop.rb +4 -4
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
  21. data/lib/chutney/linter/avoid_scripting.rb +8 -6
  22. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  23. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
  24. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
  25. data/lib/chutney/linter/bad_scenario_name.rb +6 -4
  26. data/lib/chutney/linter/empty_feature_file.rb +10 -0
  27. data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
  28. data/lib/chutney/linter/givens_after_background.rb +7 -8
  29. data/lib/chutney/linter/invalid_file_name.rb +3 -1
  30. data/lib/chutney/linter/invalid_step_flow.rb +9 -9
  31. data/lib/chutney/linter/missing_example_name.rb +9 -9
  32. data/lib/chutney/linter/missing_feature_description.rb +5 -4
  33. data/lib/chutney/linter/missing_feature_name.rb +5 -4
  34. data/lib/chutney/linter/missing_scenario_name.rb +4 -6
  35. data/lib/chutney/linter/missing_test_action.rb +4 -2
  36. data/lib/chutney/linter/missing_verification.rb +4 -2
  37. data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
  38. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
  39. data/lib/chutney/linter/scenario_names_match.rb +6 -6
  40. data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
  41. data/lib/chutney/linter/too_clumsy.rb +4 -2
  42. data/lib/chutney/linter/too_long_step.rb +6 -4
  43. data/lib/chutney/linter/too_many_different_tags.rb +10 -8
  44. data/lib/chutney/linter/too_many_steps.rb +6 -4
  45. data/lib/chutney/linter/too_many_tags.rb +5 -3
  46. data/lib/chutney/linter/unique_scenario_names.rb +5 -5
  47. data/lib/chutney/linter/unknown_variable.rb +15 -15
  48. data/lib/chutney/linter/unused_variable.rb +15 -16
  49. data/lib/chutney/linter/use_background.rb +20 -19
  50. data/lib/chutney/linter/use_outline.rb +15 -14
  51. data/lib/chutney/version.rb +3 -1
  52. data/lib/config/locales/en.yml +2 -0
  53. data/spec/chutney_spec.rb +11 -9
  54. data/spec/spec_helper.rb +2 -0
  55. metadata +19 -15
@@ -1,12 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
- # service class to lint for bad scenario names
4
+ # service class to lint for bad scenario names
3
5
  class BadScenarioName < Linter
4
6
  def lint
5
7
  scenarios do |feature, scenario|
6
- next if scenario[:name].empty?
7
-
8
+ next if scenario.name.empty?
9
+
8
10
  bad = /\w*(test|verif|check)\w*/i
9
- match = scenario[:name].match(bad).to_a.first
11
+ match = scenario.name.match(bad).to_a.first
10
12
  add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
11
13
  end
12
14
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chutney
4
+ # service class to lint for Features that have no content
5
+ class EmptyFeatureFile < Linter
6
+ def lint
7
+ add_issue(I18n.t('linters.empty_feature_file')) if feature.nil?
8
+ end
9
+ end
10
+ end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for file name differs feature name
3
5
  class FileNameDiffersFeatureName < Linter
4
6
  def lint
5
- return unless feature&.include?(:name)
6
-
7
+ return unless feature
8
+
7
9
  expected_feature_name = title_case(filename)
8
- return if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
9
-
10
- add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
10
+ return if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
11
+
12
+ add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
11
13
  end
12
14
 
13
15
  def title_case(value)
@@ -1,15 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
- # service class to lint for bad scenario names
4
+ # service class to lint for bad scenario names
3
5
  class GivensAfterBackground < Linter
4
6
  def lint
5
- return if background.nil?
6
- return if background.empty?
7
-
7
+ return unless background
8
+
8
9
  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
10
+ scenario.steps.each do |step|
11
+ add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step) if given_word?(step.keyword)
13
12
  end
14
13
  end
15
14
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
@@ -11,7 +13,7 @@ module Chutney
11
13
  end
12
14
  end
13
15
  end
14
-
16
+
15
17
  def recommend(filename)
16
18
  File.basename(filename, '.*').gsub(/::/, '/')
17
19
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for invalid step flow
3
5
  class InvalidStepFlow < Linter
4
6
  def lint
5
7
  filled_scenarios do |feature, scenario|
6
- steps = scenario[:steps].select { |step| !and_word?(step[:keyword]) && !but_word?(step[:keyword]) }
8
+ steps = scenario.steps.select { |step| !and_word?(step.keyword) && !but_word?(step.keyword) }
7
9
  next if steps.empty?
8
-
10
+
9
11
  last_step_is_an_action(feature, scenario, steps)
10
12
  given_after_non_given(feature, scenario, steps)
11
13
  verification_before_action(feature, scenario, steps)
@@ -13,7 +15,7 @@ module Chutney
13
15
  end
14
16
 
15
17
  def last_step_is_an_action(feature, scenario, steps)
16
- return unless when_word?(steps.last[:keyword])
18
+ return unless when_word?(steps.last.keyword)
17
19
 
18
20
  add_issue(I18n.t('linters.invalid_step_flow.action_last'), feature, scenario, steps.last)
19
21
  end
@@ -21,7 +23,7 @@ module Chutney
21
23
  def given_after_non_given(feature, scenario, steps)
22
24
  last_step = steps.first
23
25
  steps.each do |step|
24
- if given_word?(step[:keyword]) && !given_word?(last_step[:keyword])
26
+ if given_word?(step.keyword) && !given_word?(last_step.keyword)
25
27
  add_issue(I18n.t('linters.invalid_step_flow.given_order'), feature, scenario, step)
26
28
  end
27
29
  last_step = step
@@ -30,11 +32,9 @@ module Chutney
30
32
 
31
33
  def verification_before_action(feature, scenario, steps)
32
34
  steps.each do |step|
33
- break if when_word?(step[:keyword])
34
-
35
- if then_word?(step[:keyword])
36
- add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
37
- end
35
+ break if when_word?(step.keyword)
36
+
37
+ add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario) if then_word?(step.keyword)
38
38
  end
39
39
  end
40
40
  end
@@ -1,25 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing example names
3
5
  class MissingExampleName < Linter
4
-
5
6
  def lint
6
7
  scenarios do |_feature, scenario|
7
- next unless scenario[:examples]
8
+ next unless scenario.is_a? CukeModeler::Outline
8
9
 
9
- scenario[:examples].each do |example|
10
- example_count = scenario[:examples]&.length || 0
10
+ scenario.examples.each do |example|
11
+ example_count = scenario.examples&.length || 0
11
12
  next unless example_count > 1
12
-
13
+
13
14
  check_example(scenario, example)
14
15
  end
15
16
  end
16
- end
17
+ end
17
18
 
18
19
  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
20
+ name = example.name.strip
21
+ duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
21
22
  add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
22
23
  end
23
-
24
24
  end
25
25
  end
@@ -1,12 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing feature descriptions
3
5
  class MissingFeatureDescription < Linter
4
- MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
6
+ MESSAGE = 'Features should have a description so that its purpose is clear'
5
7
  def lint
6
8
  return unless feature
7
-
8
- name = feature.key?(:description) ? feature[:description].strip : ''
9
- add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
9
+
10
+ add_issue(I18n.t('linters.missing_feature_description'), feature) if feature.description.empty?
10
11
  end
11
12
  end
12
13
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing feature names
3
- class MissingFeatureName < Linter
5
+ class MissingFeatureName < Linter
4
6
  def lint
5
7
  return unless feature
6
-
7
- name = feature.key?(:name) ? feature[:name].strip : ''
8
- add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
8
+
9
+ add_issue(I18n.t('linters.missing_feature_name'), feature) if feature.name.empty?
9
10
  end
10
11
  end
11
12
  end
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing scenario names
3
- class MissingScenarioName < Linter
4
-
5
+ class MissingScenarioName < Linter
5
6
  def lint
6
7
  scenarios do |feature, scenario|
7
- name = scenario.key?(:name) ? scenario[:name].strip : ''
8
- next unless name.empty?
9
-
10
- add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if name.empty?
8
+ add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if scenario.name.empty?
11
9
  end
12
10
  end
13
11
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing test actions
3
5
  class MissingTestAction < Linter
4
6
  def lint
5
7
  filled_scenarios do |feature, scenario|
6
- when_steps = scenario[:steps].select { |step| when_word?(step[:keyword]) }
8
+ when_steps = scenario.steps.select { |step| when_word?(step.keyword) }
7
9
  next unless when_steps.empty?
8
-
10
+
9
11
  add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
10
12
  end
11
13
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
@@ -5,9 +7,9 @@ module Chutney
5
7
  class MissingVerification < Linter
6
8
  def lint
7
9
  filled_scenarios do |feature, scenario|
8
- then_steps = scenario[:steps].select { |step| then_word?(step[:keyword]) }
10
+ then_steps = scenario.steps.select { |step| then_word?(step.keyword) }
9
11
  next unless then_steps.empty?
10
-
12
+
11
13
  add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
12
14
  end
13
15
  end
@@ -1,26 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for tags used multiple times
3
5
  class RequiredTagsStartsWith < Linter
4
-
5
6
  def lint
6
7
  return unless pattern
7
8
 
8
9
  scenarios do |feature, scenario|
9
10
  next if match_pattern? tags_for(feature)
10
11
  next if match_pattern? tags_for(scenario)
11
-
12
+
12
13
  add_issue(
13
- I18n.t('linters.required_tags_starts_with',
14
- allowed: pattern.join(', ')),
14
+ I18n.t('linters.required_tags_starts_with',
15
+ allowed: pattern.join(', ')),
15
16
  feature, scenario
16
17
  )
17
18
  end
18
19
  end
19
-
20
+
20
21
  def pattern
21
22
  configuration['Matcher'] || nil
22
23
  end
23
-
24
+
24
25
  def match_pattern?(target)
25
26
  target.each do |t|
26
27
  return true if t.start_with?(*pattern)
@@ -1,40 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
4
6
  # service class to lint for using same tag on all scenarios
5
7
  class SameTagForAllScenarios < Linter
6
8
  def lint
7
- lint_scenarios if feature&.include?(:children)
8
- lint_examples if feature&.include?(:children)
9
+ lint_scenarios if feature&.scenarios
10
+ lint_examples if feature&.scenarios
9
11
  end
10
12
 
11
13
  def lint_scenarios
12
14
  tags = scenario_tags
13
15
  return if tags.nil? || tags.empty?
14
- return unless feature[:children].length > 1
15
-
16
+ return unless feature.tests.length > 1
17
+
16
18
  tags.each do |tag|
17
19
  next if tag == 'skip'
18
20
 
19
21
  add_issue(
20
- I18n.t('linters.same_tag_for_all_scenarios.feature_level',
21
- tag: tag),
22
+ I18n.t('linters.same_tag_for_all_scenarios.feature_level',
23
+ tag: tag),
22
24
  feature
23
25
  )
24
26
  end
25
27
  end
26
28
 
27
29
  def lint_examples
28
- feature[:children].each do |scenario|
30
+ scenarios do |_feature, scenario|
29
31
  tags = example_tags(scenario)
30
32
  next if tags.nil? || tags.empty?
31
- next unless scenario[:examples].length > 1
32
-
33
+ next unless scenario.is_a? CukeModeler::Outline
34
+ next unless scenario.examples.length > 1
35
+
33
36
  tags.each do |tag|
34
37
  next if tag == 'skip'
35
38
 
36
- add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
37
- tag: tag), feature, scenario)
39
+ add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
40
+ tag: tag), feature, scenario)
38
41
  end
39
42
  end
40
43
  end
@@ -42,8 +45,6 @@ module Chutney
42
45
  def scenario_tags
43
46
  result = nil
44
47
  scenarios do |_feature, scenario|
45
- next if scenario[:type] == :Background
46
-
47
48
  tags = tags_for(scenario)
48
49
  result ||= tags
49
50
  result &= tags
@@ -53,14 +54,14 @@ module Chutney
53
54
 
54
55
  def example_tags(scenario)
55
56
  result = nil
56
- return result unless scenario.include? :examples
57
-
58
- scenario[:examples].each do |example|
59
- return nil unless example.include? :tags
60
-
57
+ return result unless scenario.is_a?(CukeModeler::Outline) && scenario.examples
58
+
59
+ scenario.examples.each do |example|
60
+ return nil unless example.tags
61
+
61
62
  tags = tags_for(example)
62
63
  result = tags if result.nil?
63
-
64
+
64
65
  result &= tags
65
66
  end
66
67
  result
@@ -1,18 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
4
6
  # service class to lint for tags used multiple times
5
7
  class ScenarioNamesMatch < Linter
6
- MESSAGE = 'Scenario Name does not match pattern'.freeze
7
-
8
+ MESSAGE = 'Scenario Name does not match pattern'
8
9
 
9
10
  def lint
10
11
  scenarios do |feature, scenario|
11
- name = scenario.key?(:name) ? scenario[:name].strip : ''
12
- next unless (name =~ /#{configuration['Matcher']}/).nil?
13
-
12
+ next unless (scenario.name =~ /#{configuration['Matcher']}/).nil?
13
+
14
14
  add_issue(
15
- I18n.t('linters.scenario_names_match',
15
+ I18n.t('linters.scenario_names_match',
16
16
  pattern: configuration['Matcher']), feature, scenario
17
17
  )
18
18
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for tags used multiple times
3
5
  class TagUsedMultipleTimes < Linter
@@ -6,7 +8,7 @@ module Chutney
6
8
  total_tags = tags_for(feature) + tags_for(scenario)
7
9
  double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
8
10
  next if double_used_tags.nil?
9
-
11
+
10
12
  add_issue(
11
13
  I18n.t('linters.tag_used_multiple_times', tags: double_used_tags.join(',')), feature
12
14
  )
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
@@ -5,9 +7,9 @@ module Chutney
5
7
  class TooClumsy < Linter
6
8
  def lint
7
9
  filled_scenarios do |feature, scenario|
8
- characters = scenario[:steps].map { |step| step[:text].length }.inject(0, :+)
10
+ characters = scenario.steps.map { |step| step.text.length }.inject(0, :+)
9
11
  next if characters < 400
10
-
12
+
11
13
  add_issue(
12
14
  I18n.t('linters.too_clumsy', length: characters), feature, scenario
13
15
  )
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for too long steps
3
5
  class TooLongStep < Linter
4
6
  def lint
5
7
  steps do |feature, scenario, step|
6
- next if step[:text].length <= maxlength
7
-
8
+ next if step.text.length <= maxlength
9
+
8
10
  add_issue(
9
- I18n.t('linters.too_long_step', length: step[:text].length, max: maxlength),
11
+ I18n.t('linters.too_long_step', length: step.text.length, max: maxlength),
10
12
  feature, scenario
11
13
  )
12
14
  end
13
15
  end
14
-
16
+
15
17
  def maxlength
16
18
  configuration['MaxLength'] || '120'
17
19
  end