chutney 2.2.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chutney/linter'
4
+
5
+ module Chutney
6
+ # service class to lint for missing verifications
7
+ class SameTagDifferentCase < Linter
8
+ def all_known_tags
9
+ # rubocop:disable Style/ClassVars
10
+ @@all_known_tags ||= []
11
+ # rubocop:enable Style/ClassVars
12
+ end
13
+
14
+ def lint
15
+ scenarios do |feature, scenario|
16
+ total_tags = tags_for(feature) + tags_for(scenario)
17
+
18
+ total_tags.each do |tag|
19
+ collision_with = case_collision(tag)
20
+ if collision_with
21
+ add_issue(I18n.t('linters.same_tag_different_case',
22
+ existing_tag: collision_with, tag: tag),
23
+ feature, scenario)
24
+ else
25
+ @@all_known_tags << tag
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def case_collision(tag)
32
+ return nil if all_known_tags.include?(tag)
33
+
34
+ all_known_tags.select { |t| t.casecmp(tag).zero? }.first
35
+ end
36
+ end
37
+ end
@@ -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
@@ -1,24 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for too many different tags
3
5
  class TooManyDifferentTags < Linter
4
6
  def lint
5
7
  tags = all_tags
6
8
  return if tags.length <= maxcount
7
-
9
+
8
10
  add_issue(
9
- I18n.t('linters.too_many_different_tags', count: tags.length, max: maxcount),
11
+ I18n.t('linters.too_many_different_tags', count: tags.length, max: maxcount),
10
12
  feature
11
13
  )
12
14
  end
13
-
14
- def maxcount
15
+
16
+ def maxcount
15
17
  configuration['MaxCount']&.to_i || 3
16
18
  end
17
-
19
+
18
20
  def all_tags
19
- return [] unless feature&.include?(:children)
20
-
21
- tags_for(feature) + feature[:children].map { |scenario| tags_for(scenario) }.flatten
21
+ return [] unless feature&.scenarios
22
+
23
+ tags_for(feature) + feature.scenarios.map { |scenario| tags_for(scenario) }.flatten
22
24
  end
23
25
  end
24
26
  end
@@ -1,17 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for too many steps
3
5
  class TooManySteps < Linter
4
6
  def lint
5
7
  filled_scenarios do |feature, scenario|
6
- next if scenario[:steps].length <= maxcount
7
-
8
+ next if scenario.steps.length <= maxcount
9
+
8
10
  add_issue(
9
- I18n.t('linters.too_many_steps', count: scenario[:steps].length, max: maxcount),
11
+ I18n.t('linters.too_many_steps', count: scenario.steps.length, max: maxcount),
10
12
  feature
11
13
  )
12
14
  end
13
15
  end
14
-
16
+
15
17
  def maxcount
16
18
  configuration['MaxCount']&.to_i || 10
17
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for too many tags
3
5
  class TooManyTags < Linter
@@ -5,14 +7,14 @@ module Chutney
5
7
  scenarios do |feature, scenario|
6
8
  tags = tags_for(feature) + tags_for(scenario)
7
9
  next unless tags.length > maxcount
8
-
10
+
9
11
  add_issue(
10
- I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
12
+ I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
11
13
  feature
12
14
  )
13
15
  end
14
16
  end
15
-
17
+
16
18
  def maxcount
17
19
  configuration['MaxCount']&.to_i || 3
18
20
  end
@@ -1,12 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for unique scenario names
3
5
  class UniqueScenarioNames < Linter
4
6
  def lint
5
7
  references_by_name = {}
6
8
  scenarios do |feature, scenario|
7
- next unless scenario.key? :name
8
-
9
- name = scenario[:name]
9
+ name = scenario.name
10
10
  if references_by_name[name]
11
11
  issue(name, references_by_name[name], scenario)
12
12
  else
@@ -14,13 +14,13 @@ module Chutney
14
14
  end
15
15
  end
16
16
  end
17
-
17
+
18
18
  def issue(name, first_location, scenario)
19
19
  add_issue(
20
20
  I18n.t('linters.unique_scenario_names',
21
21
  name: name,
22
22
  line: first_location[:line],
23
- column: first_location[:column]),
23
+ column: first_location[:column]),
24
24
  feature, scenario
25
25
  )
26
26
  end
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for unknown variables
3
5
  class UnknownVariable < Linter
4
6
  def lint
5
7
  filled_scenarios do |feature, scenario|
6
8
  known_vars = Set.new(known_variables(scenario))
7
- scenario[:steps].each do |step|
9
+ scenario.steps.each do |step|
8
10
  step_vars(step).each do |used_var|
9
11
  next if known_vars.include? used_var
10
-
12
+
11
13
  add_issue(
12
14
  I18n.t('linters.unknown_variable', variable: used_var), feature, scenario
13
15
  )
@@ -17,17 +19,17 @@ module Chutney
17
19
  end
18
20
 
19
21
  def step_vars(step)
20
- vars = gather_vars step[:text]
21
- return vars unless step.include? :argument
22
-
23
- vars + gather_vars_from_argument(step[:argument])
22
+ vars = gather_vars step.text
23
+ return vars unless step.block
24
+
25
+ vars + gather_vars_from_argument(step.block)
24
26
  end
25
27
 
26
28
  def gather_vars_from_argument(argument)
27
- return gather_vars argument[:content] if argument[:type] == :DocString
28
-
29
- (argument[:rows] || []).map do |row|
30
- row[:cells].map { |value| gather_vars value[:value] }.flatten
29
+ return gather_vars argument.content if argument.is_a? CukeModeler::DocString
30
+
31
+ argument.rows.map do |row|
32
+ row.cells.map { |cell| gather_vars cell.value }.flatten
31
33
  end.flatten
32
34
  end
33
35
 
@@ -36,11 +38,9 @@ module Chutney
36
38
  end
37
39
 
38
40
  def known_variables(scenario)
39
- (scenario[:examples] || []).map do |example|
40
- next unless example.key? :tableHeader
41
-
42
- example[:tableHeader][:cells].map { |cell| cell[:value].strip }
43
- end.flatten
41
+ return [] unless scenario.is_a? CukeModeler::Outline
42
+
43
+ scenario.examples.map { |ex| ex.rows.first.cells.map(&:value) }.flatten
44
44
  end
45
45
  end
46
46
  end
@@ -1,14 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for unused variables
3
5
  class UnusedVariable < Linter
4
6
  def lint
5
7
  scenarios do |feature, scenario|
6
- next unless scenario.key?(:examples)
7
-
8
- scenario[:examples].each do |example|
9
- next unless example.key?(:tableHeader)
10
-
11
- example[:tableHeader][:cells].map { |cell| cell[:value] }.each do |variable|
8
+ next unless scenario.is_a? CukeModeler::Outline
9
+
10
+ scenario.examples.each do |example|
11
+ example.rows.first.cells.map(&:value).each do |variable|
12
12
  next if used?(variable, scenario)
13
13
 
14
14
  add_issue(I18n.t('linters.unused_variable', variable: variable), feature, scenario, example)
@@ -19,11 +19,10 @@ module Chutney
19
19
 
20
20
  def used?(variable, scenario)
21
21
  variable = "<#{variable}>"
22
- return false unless scenario.key? :steps
23
-
24
- scenario[:steps].each do |step|
25
- return true if step[:text].include? variable
26
- next unless step.include? :argument
22
+
23
+ scenario.steps.each do |step|
24
+ return true if step.text.include? variable
25
+ next unless step.block
27
26
  return true if used_in_docstring?(variable, step)
28
27
  return true if used_in_table?(variable, step)
29
28
  end
@@ -31,14 +30,14 @@ module Chutney
31
30
  end
32
31
 
33
32
  def used_in_docstring?(variable, step)
34
- step[:argument][:type] == :DocString && step[:argument][:content].include?(variable)
33
+ step.block.is_a?(CukeModeler::DocString) && step.block.content.include?(variable)
35
34
  end
36
35
 
37
36
  def used_in_table?(variable, step)
38
- return false unless step[:argument][:type] == :DataTable
39
-
40
- step[:argument][:rows].each do |row|
41
- row[:cells].each { |value| return true if value[:value].include?(variable) }
37
+ return false unless step.block.is_a?(CukeModeler::Table)
38
+
39
+ step.block.rows.each do |row|
40
+ row.cells.each { |cell| return true if cell.value.include?(variable) }
42
41
  end
43
42
  false
44
43
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # service class to lint for using background
3
5
  class UseBackground < Linter
@@ -13,14 +15,13 @@ module Chutney
13
15
  end
14
16
 
15
17
  def gather_givens
16
- return unless feature.include? :children
17
-
18
+ return unless feature.children
19
+
18
20
  has_non_given_step = false
19
- feature[:children].each do |scenario|
20
- next unless scenario.include? :steps
21
- next if scenario[:steps].empty?
22
-
23
- has_non_given_step = true unless given_word?(scenario[:steps].first[:keyword])
21
+ scenarios do |_feature, scenario|
22
+ next unless scenario.steps
23
+
24
+ has_non_given_step = true unless given_word?(scenario.steps.first.keyword)
24
25
  end
25
26
  return if has_non_given_step
26
27
 
@@ -29,15 +30,13 @@ module Chutney
29
30
  result
30
31
  end
31
32
 
32
- def expanded_steps
33
- feature[:children].each do |scenario|
34
- next unless scenario[:type] != :Background
35
- next unless scenario.include? :steps
36
- next if scenario[:steps].empty?
37
-
38
- prototypes = [render_step(scenario[:steps].first)]
39
- prototypes = expand_examples(scenario[:examples], prototypes) if scenario.key? :examples
40
- prototypes.each { |prototype| yield prototype }
33
+ def expanded_steps(&block)
34
+ scenarios do |_feature, scenario|
35
+ next unless scenario.steps
36
+
37
+ prototypes = [render_step(scenario.steps.first)]
38
+ prototypes = expand_examples(scenario.examples, prototypes) if scenario.is_a? CukeModeler::Outline
39
+ prototypes.each(&block)
41
40
  end
42
41
  end
43
42
 
@@ -50,10 +49,12 @@ module Chutney
50
49
 
51
50
  def expand_outlines(sentence, example)
52
51
  result = []
53
- headers = example[:tableHeader][:cells].map { |cell| cell[:value] }
54
- example[:tableBody].each do |row| # .slice(1, example[:tableBody].length).each do |row|
52
+ headers = example.rows.first.cells.map(&:value)
53
+ example.rows.each_with_index do |row, idx|
54
+ next if idx.zero? # skip the header
55
+
55
56
  modified_sentence = sentence.dup
56
- headers.zip(row[:cells].map { |cell| cell[:value] }).map do |key, value|
57
+ headers.zip(row.cells.map(&:value)).map do |key, value|
57
58
  modified_sentence.gsub!("<#{key}>", value)
58
59
  end
59
60
  result.push modified_sentence