chutney 2.0.3 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -1
  3. data/.rufo +1 -0
  4. data/Gemfile +2 -0
  5. data/README.md +2 -1
  6. data/Rakefile +2 -0
  7. data/chutney.gemspec +10 -5
  8. data/config/chutney.yml +4 -0
  9. data/config/cucumber.yml +1 -0
  10. data/docs/usage/rules.md +7 -1
  11. data/exe/chutney +2 -0
  12. data/lib/chutney.rb +19 -19
  13. data/lib/chutney/configuration.rb +2 -0
  14. data/lib/chutney/formatter.rb +2 -0
  15. data/lib/chutney/formatter/json_formatter.rb +2 -0
  16. data/lib/chutney/formatter/pie_formatter.rb +2 -0
  17. data/lib/chutney/formatter/rainbow_formatter.rb +2 -0
  18. data/lib/chutney/issue.rb +2 -0
  19. data/lib/chutney/linter.rb +65 -61
  20. data/lib/chutney/linter/avoid_full_stop.rb +3 -1
  21. data/lib/chutney/linter/avoid_outline_for_single_example.rb +8 -6
  22. data/lib/chutney/linter/avoid_scripting.rb +6 -4
  23. data/lib/chutney/linter/avoid_typographers_quotes.rb +39 -0
  24. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -6
  25. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +6 -3
  26. data/lib/chutney/linter/bad_scenario_name.rb +4 -2
  27. data/lib/chutney/linter/empty_feature_file.rb +10 -0
  28. data/lib/chutney/linter/file_name_differs_feature_name.rb +4 -2
  29. data/lib/chutney/linter/givens_after_background.rb +5 -6
  30. data/lib/chutney/linter/invalid_file_name.rb +2 -0
  31. data/lib/chutney/linter/invalid_step_flow.rb +7 -7
  32. data/lib/chutney/linter/missing_example_name.rb +7 -5
  33. data/lib/chutney/linter/missing_feature_description.rb +6 -3
  34. data/lib/chutney/linter/missing_feature_name.rb +5 -2
  35. data/lib/chutney/linter/missing_scenario_name.rb +3 -4
  36. data/lib/chutney/linter/missing_test_action.rb +3 -1
  37. data/lib/chutney/linter/missing_verification.rb +3 -1
  38. data/lib/chutney/linter/required_tags_starts_with.rb +2 -0
  39. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +11 -10
  40. data/lib/chutney/linter/scenario_names_match.rb +4 -3
  41. data/lib/chutney/linter/tag_used_multiple_times.rb +2 -0
  42. data/lib/chutney/linter/too_clumsy.rb +3 -1
  43. data/lib/chutney/linter/too_long_step.rb +4 -2
  44. data/lib/chutney/linter/too_many_different_tags.rb +4 -2
  45. data/lib/chutney/linter/too_many_steps.rb +4 -2
  46. data/lib/chutney/linter/too_many_tags.rb +2 -0
  47. data/lib/chutney/linter/unique_scenario_names.rb +3 -3
  48. data/lib/chutney/linter/unknown_variable.rb +13 -13
  49. data/lib/chutney/linter/unused_variable.rb +13 -13
  50. data/lib/chutney/linter/use_background.rb +17 -16
  51. data/lib/chutney/linter/use_outline.rb +8 -7
  52. data/lib/chutney/version.rb +3 -1
  53. data/lib/config/locales/en.yml +6 -0
  54. data/spec/chutney_spec.rb +2 -0
  55. data/spec/spec_helper.rb +2 -0
  56. metadata +47 -15
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoiding periods
3
5
  class AvoidFullStop < Linter
4
6
  def lint
5
7
  steps do |feature, child, step|
6
8
 
7
- add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step[:text].strip.end_with? '.'
9
+ add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
8
10
 
9
11
  end
10
12
  end
@@ -1,13 +1,15 @@
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
- scenarios do |feature, scenario|
6
- next unless scenario[:type] == :ScenarioOutline
7
-
8
- next unless scenario.key? :examples
9
- next if scenario[:examples].length > 1
10
- next if scenario[:examples].first[:tableBody].length > 1
7
+ scenarios do |feature, scenario|
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
11
13
 
12
14
  add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
13
15
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for avoid scripting
3
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
@@ -11,9 +13,9 @@ module Chutney
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
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chutney
4
+ # service class to lint for avoid scripting
5
+ class AvoidTypographersQuotes < Linter
6
+ TYPOGRAPHER_QUOTES = ["\u201c", "\u201d", "\u2018", "\u2019"].map(&:encode)
7
+
8
+ def lint
9
+ scenarios do |feature, scenario|
10
+ lint_steps(feature, scenario)
11
+
12
+ example_count = scenario.is_a?(CukeModeler::Outline) ? scenario.examples.length : 0
13
+ next unless example_count.positive?
14
+
15
+ lint_examples(feature, scenario)
16
+ end
17
+ end
18
+
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 }
22
+ end
23
+ end
24
+
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 }
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def issue(feature, scenario, location)
36
+ add_issue(I18n.t('linters.avoid_typographers_quotes'), feature, scenario, location)
37
+ end
38
+ end
39
+ end
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for background that does more than setup
3
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
+
9
+ invalid_steps = background&.steps&.select do |step|
10
+ when_word?(step.keyword) || then_word?(step.keyword)
10
11
  end
11
- next if invalid_steps.empty?
12
+
13
+ next if invalid_steps.nil? || invalid_steps.empty?
12
14
 
13
- add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
15
+ add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
14
16
  end
15
17
  end
16
18
  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
8
+ MESSAGE = 'Avoid using Background steps for just one scenario'
7
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
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?
8
+ next if scenario.name.empty?
7
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,11 +1,13 @@
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
7
+ return unless feature
6
8
 
7
9
  expected_feature_name = title_case(filename)
8
- return if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
10
+ return if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
9
11
 
10
12
  add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
11
13
  end
@@ -1,15 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
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
+ return unless background
7
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
@@ -1,9 +1,11 @@
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)
@@ -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])
35
+ break if when_word?(step.keyword)
34
36
 
35
- if then_word?(step[:keyword])
36
- add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
37
- end
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,13 +1,15 @@
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
6
 
5
7
  def lint
6
8
  scenarios do |_feature, scenario|
7
- next unless scenario[:examples]
9
+ next unless scenario.is_a? CukeModeler::Outline
8
10
 
9
- scenario[:examples].each do |example|
10
- example_count = scenario[:examples]&.length || 0
11
+ scenario.examples.each do |example|
12
+ example_count = scenario.examples&.length || 0
11
13
  next unless example_count > 1
12
14
 
13
15
  check_example(scenario, example)
@@ -16,8 +18,8 @@ module Chutney
16
18
  end
17
19
 
18
20
  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
+ name = example.name.strip
22
+ duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
21
23
  add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
22
24
  end
23
25
 
@@ -1,10 +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
- name = feature.key?(:description) ? feature[:description].strip : ''
7
- add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
8
+ return unless feature
9
+
10
+ add_issue(I18n.t('linters.missing_feature_description'), feature) if feature.description.empty?
8
11
  end
9
12
  end
10
13
  end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing feature names
3
5
  class MissingFeatureName < Linter
4
6
  def lint
5
- name = feature.key?(:name) ? feature[:name].strip : ''
6
- add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
7
+ return unless feature
8
+
9
+ add_issue(I18n.t('linters.missing_feature_name'), feature) if feature.name.empty?
7
10
  end
8
11
  end
9
12
  end
@@ -1,13 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for missing scenario names
3
5
  class MissingScenarioName < Linter
4
6
 
5
7
  def lint
6
8
  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?
9
+ add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if scenario.name.empty?
11
10
  end
12
11
  end
13
12
  end
@@ -1,9 +1,11 @@
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)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chutney/linter'
2
4
 
3
5
  module Chutney
@@ -5,7 +7,7 @@ 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)
@@ -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 RequiredTagsStartsWith < Linter
@@ -1,17 +1,19 @@
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
16
+ return unless feature.tests.length > 1
15
17
 
16
18
  tags.each do |tag|
17
19
  next if tag == 'skip'
@@ -25,10 +27,11 @@ module Chutney
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
33
+ next unless scenario.is_a? CukeModeler::Outline
34
+ next unless scenario.examples.length > 1
32
35
 
33
36
  tags.each do |tag|
34
37
  next if tag == 'skip'
@@ -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,10 +54,10 @@ module Chutney
53
54
 
54
55
  def example_tags(scenario)
55
56
  result = nil
56
- return result unless scenario.include? :examples
57
+ return result unless scenario.is_a?(CukeModeler::Outline) && scenario.examples
57
58
 
58
- scenario[:examples].each do |example|
59
- return nil unless example.include? :tags
59
+ scenario.examples.each do |example|
60
+ return nil unless example.tags
60
61
 
61
62
  tags = tags_for(example)
62
63
  result = tags if result.nil?