chutney 2.2.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +16 -0
  3. data/.rubocop.yml +10 -3
  4. data/Gemfile +2 -0
  5. data/LICENSE.txt +1 -1
  6. data/README.md +7 -1
  7. data/Rakefile +8 -8
  8. data/chutney.gemspec +13 -6
  9. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  10. data/config/cucumber.yml +1 -0
  11. data/docs/index.md +1 -1
  12. data/docs/usage/rules.md +34 -64
  13. data/exe/chutney +23 -3
  14. data/lib/chutney.rb +18 -14
  15. data/lib/chutney/configuration.rb +9 -2
  16. data/lib/chutney/formatter.rb +6 -5
  17. data/lib/chutney/formatter/json_formatter.rb +2 -0
  18. data/lib/chutney/formatter/pie_formatter.rb +10 -13
  19. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  20. data/lib/chutney/issue.rb +2 -0
  21. data/lib/chutney/linter.rb +92 -86
  22. data/lib/chutney/linter/avoid_full_stop.rb +4 -4
  23. data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
  24. data/lib/chutney/linter/avoid_scripting.rb +8 -6
  25. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  26. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
  27. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
  28. data/lib/chutney/linter/bad_scenario_name.rb +6 -4
  29. data/lib/chutney/linter/empty_feature_file.rb +2 -0
  30. data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
  31. data/lib/chutney/linter/givens_after_background.rb +7 -8
  32. data/lib/chutney/linter/invalid_file_name.rb +3 -1
  33. data/lib/chutney/linter/invalid_step_flow.rb +9 -9
  34. data/lib/chutney/linter/missing_example_name.rb +9 -9
  35. data/lib/chutney/linter/missing_feature_description.rb +5 -4
  36. data/lib/chutney/linter/missing_feature_name.rb +5 -4
  37. data/lib/chutney/linter/missing_scenario_name.rb +4 -6
  38. data/lib/chutney/linter/missing_test_action.rb +4 -2
  39. data/lib/chutney/linter/missing_verification.rb +4 -2
  40. data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
  41. data/lib/chutney/linter/same_tag_different_case.rb +37 -0
  42. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
  43. data/lib/chutney/linter/scenario_names_match.rb +6 -6
  44. data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
  45. data/lib/chutney/linter/too_clumsy.rb +4 -2
  46. data/lib/chutney/linter/too_long_step.rb +6 -4
  47. data/lib/chutney/linter/too_many_different_tags.rb +10 -8
  48. data/lib/chutney/linter/too_many_steps.rb +6 -4
  49. data/lib/chutney/linter/too_many_tags.rb +5 -3
  50. data/lib/chutney/linter/unique_scenario_names.rb +5 -5
  51. data/lib/chutney/linter/unknown_variable.rb +15 -15
  52. data/lib/chutney/linter/unused_variable.rb +15 -16
  53. data/lib/chutney/linter/use_background.rb +20 -19
  54. data/lib/chutney/linter/use_outline.rb +15 -14
  55. data/lib/chutney/version.rb +3 -1
  56. data/lib/config/locales/en.yml +3 -0
  57. data/spec/chutney_spec.rb +11 -9
  58. data/spec/spec_helper.rb +2 -0
  59. metadata +21 -16
@@ -1,11 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoiding periods
3
- class AvoidFullStop < Linter
5
+ class AvoidFullStop < Linter
4
6
  def lint
5
7
  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
-
8
+ add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
9
9
  end
10
10
  end
11
11
  end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoiding outline for single example
3
5
  class AvoidOutlineForSingleExample < Linter
4
6
  def lint
5
7
  scenarios do |feature, scenario|
6
- next unless scenario[:type] == :ScenarioOutline
8
+ next unless scenario.is_a? CukeModeler::Outline
9
+ next unless scenario.examples
10
+
11
+ next if scenario.examples.length > 1
12
+ next if scenario.examples.first.rows.length > 2 # first row is the header
7
13
 
8
- next unless scenario.key? :examples
9
- next if scenario[:examples].length > 1
10
- next if scenario[:examples].first[:tableBody].length > 1
11
-
12
14
  add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
13
15
  end
14
16
  end
@@ -1,19 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoid scripting
3
- class AvoidScripting < Linter
5
+ class AvoidScripting < Linter
4
6
  def lint
5
7
  scenarios do |feature, scenario|
6
- when_steps = filter_when_steps(scenario[:steps])
8
+ when_steps = filter_when_steps(scenario.steps)
7
9
  whens = when_steps.count
8
10
  add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
9
11
  end
10
12
  end
11
-
13
+
12
14
  def filter_when_steps(steps)
13
15
  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]) } }
16
+ .drop_while { |step| !when_word?(step.keyword) }
17
+ .then { |s| s.reverse.drop_while { |step| !then_word?(step.keyword) }.reverse }
18
+ .then { |s| s.reject { |step| then_word?(step.keyword) } }
17
19
  end
18
20
  end
19
21
  end
@@ -1,37 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoid scripting
3
5
  class AvoidTypographersQuotes < Linter
4
6
  TYPOGRAPHER_QUOTES = ["\u201c", "\u201d", "\u2018", "\u2019"].map(&:encode)
5
7
 
6
8
  def lint
7
- scenarios do |_feature, scenario|
8
- lint_steps(scenario[:steps])
9
+ scenarios do |feature, scenario|
10
+ lint_steps(feature, scenario)
9
11
 
10
- example_count = scenario[:examples]&.length || 0
12
+ example_count = scenario.is_a?(CukeModeler::Outline) ? scenario.examples.length : 0
11
13
  next unless example_count.positive?
12
14
 
13
- lint_examples(scenario[:examples])
15
+ lint_examples(feature, scenario)
14
16
  end
15
17
  end
16
18
 
17
- def lint_steps(steps)
18
- steps.each do |step|
19
- issue(step) if TYPOGRAPHER_QUOTES.any? { |tq| step[:text].include? tq }
19
+ def lint_steps(feature, scenario)
20
+ scenario.steps.each do |step|
21
+ issue(feature, scenario, step) if TYPOGRAPHER_QUOTES.any? { |tq| step.text.include? tq }
20
22
  end
21
23
  end
22
24
 
23
- def lint_examples(examples)
24
- examples.each do |example|
25
- example[:tableBody].each do |body|
26
- body[:cells].each do |cell|
27
- issue(cell) if TYPOGRAPHER_QUOTES.any? { |tq| cell[:value].include? tq }
25
+ def lint_examples(feature, scenario)
26
+ scenario.examples.each do |example|
27
+ example.rows.each do |row|
28
+ row.cells.each do |cell|
29
+ issue(feature, scenario, cell) if TYPOGRAPHER_QUOTES.any? { |tq| cell.value.include? tq }
28
30
  end
29
31
  end
30
32
  end
31
33
  end
32
34
 
33
- def issue(location)
34
- add_issue(I18n.t('linters.avoid_typographers_quotes'), location)
35
+ def issue(feature, scenario, location)
36
+ add_issue(I18n.t('linters.avoid_typographers_quotes'), feature, scenario, location)
35
37
  end
36
38
  end
37
39
  end
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for background that does more than setup
3
- class BackgroundDoesMoreThanSetup < Linter
5
+ class BackgroundDoesMoreThanSetup < Linter
4
6
  def lint
5
7
  background do |feature, background|
6
- next unless background.key? :steps
7
-
8
- invalid_steps = background[:steps].select do |step|
9
- when_word?(step[:keyword]) || then_word?(step[:keyword])
8
+ invalid_steps = background&.steps&.select do |step|
9
+ when_word?(step.keyword) || then_word?(step.keyword)
10
10
  end
11
- next if invalid_steps.empty?
12
11
 
13
- add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
12
+ next if invalid_steps.nil? || invalid_steps.empty?
13
+
14
+ add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
14
15
  end
15
16
  end
16
17
  end
@@ -1,14 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
4
6
  # service class for check that there are multiple scenarios once a background is used
5
7
  class BackgroundRequiresMultipleScenarios < Linter
6
- MESSAGE = 'Avoid using Background steps for just one scenario'.freeze
7
-
8
+ MESSAGE = 'Avoid using Background steps for just one scenario'
9
+
8
10
  def lint
9
11
  background do |feature, background|
10
- scenarios = feature[:children].reject { |element| element[:type] == :Background }
11
- next if scenarios.length >= 2
12
+ next unless background
13
+ next unless feature&.tests
14
+ next if feature.tests.length >= 2
12
15
 
13
16
  add_issue(I18n.t('linters.background_requires_multiple_scenarios'), feature, background)
14
17
  end
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for Features that have no content
3
5
  class EmptyFeatureFile < Linter
@@ -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)