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
@@ -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