chutney 1.6.3 → 2.0.0.rc1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +3 -3
  4. data/README.md +44 -30
  5. data/Rakefile +10 -24
  6. data/chutney.gemspec +13 -10
  7. data/config/{default.yml → chutney.yml} +6 -3
  8. data/docs/.keep +0 -0
  9. data/exe/chutney +28 -22
  10. data/img/chutney.svg +852 -0
  11. data/img/formatters.png +0 -0
  12. data/lib/chutney.rb +61 -85
  13. data/lib/chutney/configuration.rb +6 -7
  14. data/lib/chutney/formatter.rb +21 -0
  15. data/lib/chutney/formatter/json_formatter.rb +8 -0
  16. data/lib/chutney/formatter/pie_formatter.rb +78 -0
  17. data/lib/chutney/formatter/rainbow_formatter.rb +47 -0
  18. data/lib/chutney/linter.rb +145 -113
  19. data/lib/chutney/linter/avoid_full_stop.rb +12 -0
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -6
  21. data/lib/chutney/linter/avoid_scripting.rb +10 -15
  22. data/lib/chutney/linter/background_does_more_than_setup.rb +7 -10
  23. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +2 -3
  24. data/lib/chutney/linter/bad_scenario_name.rb +4 -11
  25. data/lib/chutney/linter/file_name_differs_feature_name.rb +5 -10
  26. data/lib/chutney/linter/givens_after_background.rb +17 -0
  27. data/lib/chutney/linter/invalid_file_name.rb +14 -6
  28. data/lib/chutney/linter/invalid_step_flow.rb +18 -18
  29. data/lib/chutney/linter/missing_example_name.rb +13 -11
  30. data/lib/chutney/linter/missing_feature_description.rb +2 -9
  31. data/lib/chutney/linter/missing_feature_name.rb +3 -12
  32. data/lib/chutney/linter/missing_scenario_name.rb +3 -7
  33. data/lib/chutney/linter/missing_test_action.rb +3 -6
  34. data/lib/chutney/linter/missing_verification.rb +3 -4
  35. data/lib/chutney/linter/required_tags_starts_with.rb +20 -5
  36. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +24 -28
  37. data/lib/chutney/linter/scenario_names_match.rb +6 -8
  38. data/lib/chutney/linter/tag_used_multiple_times.rb +6 -13
  39. data/lib/chutney/linter/too_clumsy.rb +4 -4
  40. data/lib/chutney/linter/too_long_step.rb +8 -18
  41. data/lib/chutney/linter/too_many_different_tags.rb +13 -34
  42. data/lib/chutney/linter/too_many_steps.rb +10 -6
  43. data/lib/chutney/linter/too_many_tags.rb +11 -10
  44. data/lib/chutney/linter/unique_scenario_names.rb +19 -13
  45. data/lib/chutney/linter/unknown_variable.rb +5 -6
  46. data/lib/chutney/linter/unused_variable.rb +6 -7
  47. data/lib/chutney/linter/use_background.rb +11 -29
  48. data/lib/chutney/linter/use_outline.rb +21 -15
  49. data/lib/chutney/version.rb +1 -1
  50. data/lib/config/locales/en.yml +93 -0
  51. data/spec/chutney_spec.rb +54 -62
  52. data/spec/spec_helper.rb +103 -0
  53. metadata +75 -44
  54. data/Guardfile +0 -3
  55. data/lib/chutney/linter/avoid_period.rb +0 -19
  56. data/lib/chutney/linter/be_declarative.rb +0 -49
  57. data/lib/chutney/linter/tag_collector.rb +0 -10
  58. data/lib/chutney/linter/tag_constraint.rb +0 -35
  59. data/spec/configuration_spec.rb +0 -58
  60. data/spec/required_tags_starts_with_spec.rb +0 -74
  61. data/spec/shared_contexts/file_exists.rb +0 -12
  62. data/spec/shared_contexts/gherkin_linter.rb +0 -14
@@ -4,65 +4,61 @@ module Chutney
4
4
  # service class to lint for using same tag on all scenarios
5
5
  class SameTagForAllScenarios < Linter
6
6
  def lint
7
- features do |file, feature|
8
- next unless feature.include? :children
9
-
10
- lint_scenarios file, feature
11
- lint_examples file, feature
12
- end
7
+ lint_scenarios if feature.include? :children
8
+ lint_examples if feature.include? :children
13
9
  end
14
10
 
15
- def lint_scenarios(file, feature)
16
- tags = gather_same_tags feature
17
- return if tags.nil?
18
- return if tags.empty?
11
+ def lint_scenarios
12
+ tags = scenario_tags
13
+ return if tags.nil? || tags.empty?
19
14
  return unless feature[:children].length > 1
20
-
21
- references = [reference(file, feature)]
15
+
22
16
  tags.each do |tag|
23
- next if tag == '@skip'
17
+ next if tag == 'skip'
24
18
 
25
- add_error(references, "Tag '#{tag}' should be used at Feature level")
19
+ add_issue(
20
+ I18n.t('linters.same_tag_for_all_scenarios.feature_level',
21
+ tag: tag),
22
+ feature
23
+ )
26
24
  end
27
25
  end
28
26
 
29
- def lint_examples(file, feature)
27
+ def lint_examples
30
28
  feature[:children].each do |scenario|
31
- tags = gather_same_tags_for_outline scenario
29
+ tags = example_tags(scenario)
32
30
  next if tags.nil? || tags.empty?
33
31
  next unless scenario[:examples].length > 1
34
32
 
35
- references = [reference(file, feature, scenario)]
36
33
  tags.each do |tag|
37
- next if tag == '@skip'
34
+ next if tag == 'skip'
38
35
 
39
- add_error(references, "Tag '#{tag}' should be used at Scenario Outline level")
36
+ add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
37
+ tag: tag), feature, scenario)
40
38
  end
41
39
  end
42
40
  end
43
41
 
44
- def gather_same_tags(feature)
42
+ def scenario_tags
45
43
  result = nil
46
- feature[:children].each do |scenario|
44
+ scenarios do |_feature, scenario|
47
45
  next if scenario[:type] == :Background
48
- return nil unless scenario.include? :tags
49
-
50
- tags = scenario[:tags].map { |tag| tag[:name] }
51
- result = tags if result.nil?
52
-
46
+
47
+ tags = tags_for(scenario)
48
+ result ||= tags
53
49
  result &= tags
54
50
  end
55
51
  result
56
52
  end
57
53
 
58
- def gather_same_tags_for_outline(scenario)
54
+ def example_tags(scenario)
59
55
  result = nil
60
56
  return result unless scenario.include? :examples
61
57
 
62
58
  scenario[:examples].each do |example|
63
59
  return nil unless example.include? :tags
64
60
 
65
- tags = example[:tags].map { |tag| tag[:name] }
61
+ tags = tags_for(example)
66
62
  result = tags if result.nil?
67
63
 
68
64
  result &= tags
@@ -7,17 +7,15 @@ module Chutney
7
7
 
8
8
 
9
9
  def lint
10
- scenarios do |file, feature, scenario|
10
+ scenarios do |feature, scenario|
11
11
  name = scenario.key?(:name) ? scenario[:name].strip : ''
12
- references = [reference(file, feature, scenario)]
13
- next unless (name =~ /#{@pattern}/).nil?
12
+ next unless (name =~ /#{configuration['Matcher']}/).nil?
14
13
 
15
- add_warning(references, MESSAGE)
14
+ add_issue(
15
+ I18n.t('linters.scenario_names_match',
16
+ pattern: configuration['Matcher']), feature, scenario
17
+ )
16
18
  end
17
19
  end
18
-
19
- def matcher(pattern)
20
- @pattern = pattern
21
- end
22
20
  end
23
21
  end
@@ -1,23 +1,16 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for tags used multiple times
5
3
  class TagUsedMultipleTimes < Linter
6
4
  def lint
7
- scenarios do |file, feature, scenario|
8
- references = [reference(file, feature, scenario)]
9
- total_tags = tags(feature) + tags(scenario)
5
+ scenarios do |feature, scenario|
6
+ total_tags = tags_for(feature) + tags_for(scenario)
10
7
  double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
11
8
  next if double_used_tags.nil?
12
-
13
- add_error(references, "Tag #{double_used_tags.join(' and ')} used multiple times")
9
+
10
+ add_issue(
11
+ I18n.t('linters.tag_used_multiple_times', tags: double_used_tags.join(',')), feature
12
+ )
14
13
  end
15
14
  end
16
-
17
- def tags(element)
18
- return [] unless element.include? :tags
19
-
20
- element[:tags].map { |a| a[:name] }
21
- end
22
15
  end
23
16
  end
@@ -3,14 +3,14 @@ require 'chutney/linter'
3
3
  module Chutney
4
4
  # service class to lint for too clumsy scenarios
5
5
  class TooClumsy < Linter
6
- MESSAGE = 'This scenario is too long at %d characters'.freeze
7
6
  def lint
8
- filled_scenarios do |file, feature, scenario|
7
+ filled_scenarios do |feature, scenario|
9
8
  characters = scenario[:steps].map { |step| step[:text].length }.inject(0, :+)
10
9
  next if characters < 400
11
10
 
12
- references = [reference(file, feature, scenario)]
13
- add_error(references, MESSAGE % characters)
11
+ add_issue(
12
+ I18n.t('linters.too_clumsy', length: characters), feature, scenario
13
+ )
14
14
  end
15
15
  end
16
16
  end
@@ -1,29 +1,19 @@
1
- require 'chutney/linter'
2
-
3
- # rubocop:disable Lint/MissingCopEnableDirective
4
1
  module Chutney
5
2
  # service class to lint for too long steps
6
3
  class TooLongStep < Linter
7
-
8
- def initialize
9
- @maxlength = 80
10
- super
11
- end
12
-
13
- MESSAGE = 'This step is too long at %d characters'.freeze
14
-
15
4
  def lint
16
- steps do |file, feature, scenario, step|
17
- next if step[:text].length <= @maxlength
5
+ steps do |feature, scenario, step|
6
+ next if step[:text].length <= maxlength
18
7
 
19
- references = [reference(file, feature, scenario, step)]
20
- add_error(references, MESSAGE % step[:text].length)
8
+ add_issue(
9
+ I18n.t('linters.too_long_step', length: step[:text].length, max: maxlength),
10
+ feature, scenario
11
+ )
21
12
  end
22
13
  end
23
14
 
24
- # rubocop:disable Style/TrivialAccessors
25
- def maxlength(length)
26
- @maxlength = length
15
+ def maxlength
16
+ configuration['MaxLength'] || '120'
27
17
  end
28
18
  end
29
19
  end
@@ -1,45 +1,24 @@
1
- require 'chutney/linter'
2
- require 'chutney/linter/tag_collector'
3
-
4
1
  module Chutney
5
2
  # service class to lint for too many different tags
6
3
  class TooManyDifferentTags < Linter
7
- include TagCollector
8
-
9
4
  def lint
10
- overall_tags = []
11
- overall_references = []
12
- features do |file, feature|
13
- tags = tags_for_feature(feature)
14
- overall_tags += tags
15
- references = [reference(file, feature)]
16
- overall_references += references unless tags.empty?
17
-
18
- warn_single_feature(references, tags)
19
- end
20
- warn_across_all_features(overall_references, overall_tags)
21
- end
22
-
23
- def warn_single_feature(references, tags)
24
- tags.uniq!
25
- references.uniq!
26
- return false unless tags.length >= 3
5
+ tags = all_tags
6
+ return if tags.length <= maxcount
27
7
 
28
- add_error(references, "Used #{tags.length} Tags within single Feature")
8
+ add_issue(
9
+ I18n.t('linters.too_many_different_tags', count: tags.length, max: maxcount),
10
+ feature
11
+ )
29
12
  end
30
-
31
- def warn_across_all_features(references, tags)
32
- tags.uniq!
33
- references.uniq!
34
- return false unless tags.length >= 10
35
-
36
- add_error(references, "Used #{tags.length} Tags across all Features")
13
+
14
+ def maxcount
15
+ configuration['MaxCount']&.to_i || 3
37
16
  end
38
-
39
- def tags_for_feature(feature)
40
- return [] unless feature.include? :children
17
+
18
+ def all_tags
19
+ return [] unless feature.include?(:children)
41
20
 
42
- gather_tags(feature) + feature[:children].map { |scenario| gather_tags(scenario) }.flatten
21
+ tags_for(feature) + feature[:children].map { |scenario| tags_for(scenario) }.flatten
43
22
  end
44
23
  end
45
24
  end
@@ -1,15 +1,19 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for too many steps
5
3
  class TooManySteps < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- next if scenario[:steps].length < 10
5
+ filled_scenarios do |feature, scenario|
6
+ next if scenario[:steps].length <= maxcount
9
7
 
10
- references = [reference(file, feature, scenario)]
11
- add_error(references, "Scenario is too long at #{scenario[:steps].length} steps")
8
+ add_issue(
9
+ I18n.t('linters.too_many_steps', count: scenario[:steps].length, max: maxcount),
10
+ feature
11
+ )
12
12
  end
13
13
  end
14
+
15
+ def maxcount
16
+ configuration['MaxCount']&.to_i || 10
17
+ end
14
18
  end
15
19
  end
@@ -1,19 +1,20 @@
1
- require 'chutney/linter'
2
- require 'chutney/linter/tag_collector'
3
-
4
1
  module Chutney
5
2
  # service class to lint for too many tags
6
3
  class TooManyTags < Linter
7
- include TagCollector
8
-
9
4
  def lint
10
- scenarios do |file, feature, scenario|
11
- tags = gather_tags(feature) + gather_tags(scenario)
12
- next unless tags.length >= 3
5
+ scenarios do |feature, scenario|
6
+ tags = tags_for(feature) + tags_for(scenario)
7
+ next unless tags.length > maxcount
13
8
 
14
- references = [reference(file, feature, scenario)]
15
- add_error(references, "Used #{tags.length} Tags")
9
+ add_issue(
10
+ I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
11
+ feature
12
+ )
16
13
  end
17
14
  end
15
+
16
+ def maxcount
17
+ configuration['MaxCount']&.to_i || 3
18
+ end
18
19
  end
19
20
  end
@@ -1,22 +1,28 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for unique scenario names
5
3
  class UniqueScenarioNames < Linter
6
4
  def lint
7
- references_by_name = Hash.new []
8
- scenarios do |file, feature, scenario|
5
+ references_by_name = {}
6
+ scenarios do |feature, scenario|
9
7
  next unless scenario.key? :name
10
-
11
- scenario_name = "#{feature[:name]}.#{scenario[:name]}"
12
- references_by_name[scenario_name] = references_by_name[scenario_name] + [reference(file, feature, scenario)]
13
- end
14
-
15
- references_by_name.each do |name, references|
16
- next if references.length <= 1
17
-
18
- add_error(references, "Scenario name should be unique, '#{name}' used #{references.length} times")
8
+
9
+ name = scenario[:name]
10
+ if references_by_name[name]
11
+ issue(name, references_by_name[name], scenario)
12
+ else
13
+ references_by_name[name] = location(feature, scenario, nil)
14
+ end
19
15
  end
20
16
  end
17
+
18
+ def issue(name, first_location, scenario)
19
+ add_issue(
20
+ I18n.t('linters.unique_scenario_names',
21
+ name: name,
22
+ line: first_location[:line],
23
+ column: first_location[:column]),
24
+ feature, scenario
25
+ )
26
+ end
21
27
  end
22
28
  end
@@ -1,17 +1,16 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for unknown variables
5
3
  class UnknownVariable < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- known_vars = Set.new known_variables scenario
5
+ filled_scenarios do |feature, scenario|
6
+ known_vars = Set.new(known_variables(scenario))
9
7
  scenario[:steps].each do |step|
10
8
  step_vars(step).each do |used_var|
11
9
  next if known_vars.include? used_var
12
10
 
13
- references = [reference(file, feature, scenario)]
14
- add_error(references, "Variable '<#{used_var}>' is unknown")
11
+ add_issue(
12
+ I18n.t('linters.unknown_variable', variable: used_var), feature, scenario
13
+ )
15
14
  end
16
15
  end
17
16
  end
@@ -1,18 +1,17 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for unused variables
5
3
  class UnusedVariable < Linter
6
4
  def lint
7
- scenarios do |file, feature, scenario|
8
- next unless scenario.key? :examples
5
+ scenarios do |feature, scenario|
6
+ next unless scenario.key?(:examples)
9
7
 
10
8
  scenario[:examples].each do |example|
11
- next unless example.key? :tableHeader
9
+ next unless example.key?(:tableHeader)
12
10
 
13
11
  example[:tableHeader][:cells].map { |cell| cell[:value] }.each do |variable|
14
- references = [reference(file, feature, scenario)]
15
- add_error(references, "Variable '<#{variable}>' is unused") unless used?(variable, scenario)
12
+ next if used?(variable, scenario)
13
+
14
+ add_issue(I18n.t('linters.unused_variable', variable: variable), feature, scenario, example)
16
15
  end
17
16
  end
18
17
  end
@@ -1,36 +1,18 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for using background
5
3
  class UseBackground < Linter
6
4
  def lint
7
- features do |file, feature|
8
- next if scenarios_with_steps(feature) <= 1
9
-
10
- givens = gather_givens feature
11
- next if givens.nil?
12
- next if givens.length <= 1
13
- next if givens.uniq.length > 1
14
-
15
- references = [reference(file, feature)]
16
- add_error(references, "Step '#{givens.uniq.first}' should be part of background")
17
- end
18
- end
5
+ return unless filled_scenarios.count > 1
19
6
 
20
- def scenarios_with_steps(feature)
21
- scenarios = 0
22
- return 0 unless feature.key? :children
23
-
24
- feature[:children].each do |scenario|
25
- next unless scenario.include? :steps
26
- next if scenario[:steps].empty?
27
-
28
- scenarios += 1
29
- end
30
- scenarios
7
+ givens = gather_givens
8
+ return if givens.nil?
9
+ return if givens.length <= 1
10
+ return if givens.uniq.length > 1
11
+
12
+ add_issue(I18n.t('linters.use_background', step: givens.uniq.first), feature)
31
13
  end
32
14
 
33
- def gather_givens(feature)
15
+ def gather_givens
34
16
  return unless feature.include? :children
35
17
 
36
18
  has_non_given_step = false
@@ -38,16 +20,16 @@ module Chutney
38
20
  next unless scenario.include? :steps
39
21
  next if scenario[:steps].empty?
40
22
 
41
- has_non_given_step = true unless scenario[:steps].first[:keyword] == 'Given '
23
+ has_non_given_step = true unless given_word?(scenario[:steps].first[:keyword])
42
24
  end
43
25
  return if has_non_given_step
44
26
 
45
27
  result = []
46
- expanded_steps(feature) { |given| result.push given }
28
+ expanded_steps { |given| result.push given }
47
29
  result
48
30
  end
49
31
 
50
- def expanded_steps(feature)
32
+ def expanded_steps
51
33
  feature[:children].each do |scenario|
52
34
  next unless scenario[:type] != :Background
53
35
  next unless scenario.include? :steps