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
@@ -0,0 +1,12 @@
1
+ module Chutney
2
+ # service class to lint for avoiding periods
3
+ class AvoidFullStop < Linter
4
+ def lint
5
+ 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
+
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,18 +1,15 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for avoiding outline for single example
5
3
  class AvoidOutlineForSingleExample < Linter
6
4
  def lint
7
- scenarios do |file, feature, scenario|
5
+ scenarios do |feature, scenario|
8
6
  next unless scenario[:type] == :ScenarioOutline
9
7
 
10
8
  next unless scenario.key? :examples
11
9
  next if scenario[:examples].length > 1
12
10
  next if scenario[:examples].first[:tableBody].length > 1
13
-
14
- references = [reference(file, feature, scenario)]
15
- add_error(references, 'You have a Scenarion Outline with a single example - rewrite to use a Scenario')
11
+
12
+ add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
16
13
  end
17
14
  end
18
15
  end
@@ -1,24 +1,19 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for avoid scripting
5
- class AvoidScripting < Linter
6
- MESSAGE = "You have multiple (%d) 'When' actions in your steps - you should only have one".freeze
7
-
3
+ class AvoidScripting < Linter
8
4
  def lint
9
- filled_scenarios do |file, feature, scenario|
10
- steps = filter_when_steps scenario[:steps]
11
- next if steps.length <= 1
12
-
13
- references = [reference(file, feature, scenario)]
14
- add_error(references, MESSAGE % steps.length)
5
+ scenarios do |feature, scenario|
6
+ when_steps = filter_when_steps(scenario[:steps])
7
+ whens = when_steps.count
8
+ add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
15
9
  end
16
10
  end
17
-
11
+
18
12
  def filter_when_steps(steps)
19
- steps = steps.drop_while { |step| step[:keyword] != 'When ' }
20
- steps = steps.reverse.drop_while { |step| step[:keyword] != 'Then ' }.reverse
21
- steps.reject { |step| step[:keyword] == 'Then ' }
13
+ 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]) } }
22
17
  end
23
18
  end
24
19
  end
@@ -1,19 +1,16 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for background that does more than setup
5
- class BackgroundDoesMoreThanSetup < Linter
6
- MESSAGE = 'A Feature\'s Background should just contain \'Given\' steps'.freeze
7
-
3
+ class BackgroundDoesMoreThanSetup < Linter
8
4
  def lint
9
- backgrounds do |file, feature, background|
5
+ background do |feature, background|
10
6
  next unless background.key? :steps
11
7
 
12
- invalid_steps = background[:steps].select { |step| step[:keyword] == 'When ' || step[:keyword] == 'Then ' }
8
+ invalid_steps = background[:steps].select do |step|
9
+ when_word?(step[:keyword]) || then_word?(step[:keyword])
10
+ end
13
11
  next if invalid_steps.empty?
14
-
15
- references = [reference(file, feature, background, invalid_steps[0])]
16
- add_error(references, MESSAGE)
12
+
13
+ add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
17
14
  end
18
15
  end
19
16
  end
@@ -6,12 +6,11 @@ module Chutney
6
6
  MESSAGE = 'Avoid using Background steps for just one scenario'.freeze
7
7
 
8
8
  def lint
9
- backgrounds do |file, feature, background|
9
+ background do |feature, background|
10
10
  scenarios = feature[:children].reject { |element| element[:type] == :Background }
11
11
  next if scenarios.length >= 2
12
12
 
13
- references = [reference(file, feature, background)]
14
- add_error(references, MESSAGE)
13
+ add_issue(I18n.t('linters.background_requires_multiple_scenarios'), feature, background)
15
14
  end
16
15
  end
17
16
  end
@@ -1,20 +1,13 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for bad scenario names
5
3
  class BadScenarioName < Linter
6
- MESSAGE = 'You should avoid using words like \'test\', \'check\' or \'verify\' ' \
7
- 'when naming your scenarios to keep them understandable'.freeze
8
-
9
4
  def lint
10
- scenarios do |file, feature, scenario|
5
+ scenarios do |feature, scenario|
11
6
  next if scenario[:name].empty?
12
7
 
13
- references = [reference(file, feature, scenario)]
14
- bad_words = %w[test verif check]
15
- bad_words.each do |bad_word|
16
- add_error(references, MESSAGE) if scenario[:name].downcase.include? bad_word
17
- end
8
+ bad = /\w*(test|verif|check)\w*/i
9
+ match = scenario[:name].match(bad).to_a.first
10
+ add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
18
11
  end
19
12
  end
20
13
  end
@@ -1,22 +1,17 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for file name differs feature name
5
3
  class FileNameDiffersFeatureName < Linter
6
4
  def lint
7
- features do |file, feature|
8
- next unless feature.include? :name
5
+ return unless feature.include? :name
9
6
 
10
- expected_feature_name = title_case file
11
- next if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
7
+ expected_feature_name = title_case(filename)
8
+ return if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
12
9
 
13
- references = [reference(file, feature)]
14
- add_error(references, "Feature name should be '#{expected_feature_name}'")
15
- end
10
+ add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
16
11
  end
17
12
 
18
13
  def title_case(value)
19
- value = File.basename(value, '.feature')
14
+ value = File.basename(value, '.*')
20
15
  value.split('_').collect(&:capitalize).join(' ')
21
16
  end
22
17
 
@@ -0,0 +1,17 @@
1
+ module Chutney
2
+ # service class to lint for bad scenario names
3
+ class GivensAfterBackground < Linter
4
+ def lint
5
+ return if background.nil?
6
+ return if background.empty?
7
+
8
+ 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
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,13 +4,21 @@ module Chutney
4
4
  # service class to lint for invalid file names
5
5
  class InvalidFileName < Linter
6
6
  def lint
7
- files do |file|
8
- base = File.basename file
9
- next unless base != base.downcase || base =~ /[ -]/
10
-
11
- references = [reference(file)]
12
- add_error(references, 'Feature files should be snake_cased')
7
+ feature do |f|
8
+ base = File.basename(filename, '.*')
9
+ if base != base.downcase || base =~ /[ -]/
10
+ add_issue(I18n.t('linters.invalid_file_name', recommended_name: recommend(filename)), f)
11
+ end
13
12
  end
14
13
  end
14
+
15
+ def recommend(filename)
16
+ File.basename(filename, '.*').gsub(/::/, '/')
17
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
18
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
19
+ .tr('-', '_')
20
+ .tr(' ', '_')
21
+ .downcase << '.feature'
22
+ end
15
23
  end
16
24
  end
@@ -1,40 +1,40 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for invalid step flow
5
3
  class InvalidStepFlow < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- steps = scenario[:steps].select { |step| step[:keyword] != 'And ' && step[:keyword] != 'But ' }
5
+ filled_scenarios do |feature, scenario|
6
+ steps = scenario[:steps].select { |step| !and_word?(step[:keyword]) && !but_word?(step[:keyword]) }
9
7
  next if steps.empty?
10
8
 
11
- last_step_is_an_action(file, feature, scenario, steps)
12
- given_after_non_given(file, feature, scenario, steps)
13
- verification_before_action(file, feature, scenario, steps)
9
+ last_step_is_an_action(feature, scenario, steps)
10
+ given_after_non_given(feature, scenario, steps)
11
+ verification_before_action(feature, scenario, steps)
14
12
  end
15
13
  end
16
14
 
17
- def last_step_is_an_action(file, feature, scenario, steps)
18
- references = [reference(file, feature, scenario, steps.last)]
19
- add_error(references, 'Last step is an action') if steps.last[:keyword] == 'When '
15
+ def last_step_is_an_action(feature, scenario, steps)
16
+ return unless when_word?(steps.last[:keyword])
17
+
18
+ add_issue(I18n.t('linters.invalid_step_flow.action_last'), feature, scenario, steps.last)
20
19
  end
21
20
 
22
- def given_after_non_given(file, feature, scenario, steps)
21
+ def given_after_non_given(feature, scenario, steps)
23
22
  last_step = steps.first
24
23
  steps.each do |step|
25
- references = [reference(file, feature, scenario, step)]
26
- description = 'Given after Action or Verification'
27
- add_error(references, description) if step[:keyword] == 'Given ' && last_step[:keyword] != 'Given '
24
+ if given_word?(step[:keyword]) && !given_word?(last_step[:keyword])
25
+ add_issue(I18n.t('linters.invalid_step_flow.given_order'), feature, scenario, step)
26
+ end
28
27
  last_step = step
29
28
  end
30
29
  end
31
30
 
32
- def verification_before_action(file, feature, scenario, steps)
31
+ def verification_before_action(feature, scenario, steps)
33
32
  steps.each do |step|
34
- break if step[:keyword] == 'When '
33
+ break if when_word?(step[:keyword])
35
34
 
36
- references = [reference(file, feature, scenario, step)]
37
- add_error(references, 'Missing Action step') if step[:keyword] == 'Then '
35
+ if then_word?(step[:keyword])
36
+ add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
37
+ end
38
38
  end
39
39
  end
40
40
  end
@@ -1,23 +1,25 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing example names
5
3
  class MissingExampleName < Linter
6
- MESSAGE = 'You have an unnamed or ambiguously named example'.freeze
7
4
 
8
5
  def lint
9
- scenarios do |file, feature, scenario|
10
- next unless scenario.key? :examples
11
- next unless scenario[:examples].length > 1
12
-
6
+ scenarios do |_feature, scenario|
7
+ next unless scenario[:examples]
8
+
13
9
  scenario[:examples].each do |example|
14
- name = example.key?(:name) ? example[:name].strip : ''
15
- next unless name.empty?
10
+ example_count = scenario[:examples]&.length || 0
11
+ next unless example_count > 1
16
12
 
17
- references = [reference(file, feature, scenario)]
18
- add_error(references, MESSAGE)
13
+ check_example(scenario, example)
19
14
  end
20
15
  end
16
+ end
17
+
18
+ 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
+ add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
21
22
  end
23
+
22
24
  end
23
25
  end
@@ -1,17 +1,10 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing feature descriptions
5
3
  class MissingFeatureDescription < Linter
6
4
  MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
7
5
  def lint
8
- features do |file, feature|
9
- name = feature.key?(:description) ? feature[:description].strip : ''
10
- next unless name.empty?
11
-
12
- references = [reference(file, feature)]
13
- add_error(references, MESSAGE)
14
- end
6
+ name = feature.key?(:description) ? feature[:description].strip : ''
7
+ add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
15
8
  end
16
9
  end
17
10
  end
@@ -1,18 +1,9 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing feature names
5
- class MissingFeatureName < Linter
6
- MESSAGE = 'All features should have a name'.freeze
7
-
3
+ class MissingFeatureName < Linter
8
4
  def lint
9
- features do |file, feature|
10
- name = feature.key?(:name) ? feature[:name].strip : ''
11
- next unless name.empty?
12
-
13
- references = [reference(file, feature)]
14
- add_error(references, MESSAGE)
15
- end
5
+ name = feature.key?(:name) ? feature[:name].strip : ''
6
+ add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
16
7
  end
17
8
  end
18
9
  end
@@ -1,17 +1,13 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing scenario names
5
- class MissingScenarioName < Linter
6
- MESSAGE = 'All scenarios should have a name'.freeze
3
+ class MissingScenarioName < Linter
7
4
 
8
5
  def lint
9
- scenarios do |file, feature, scenario|
6
+ scenarios do |feature, scenario|
10
7
  name = scenario.key?(:name) ? scenario[:name].strip : ''
11
- references = [reference(file, feature, scenario)]
12
8
  next unless name.empty?
13
9
 
14
- add_error(references, MESSAGE)
10
+ add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if name.empty?
15
11
  end
16
12
  end
17
13
  end
@@ -1,15 +1,12 @@
1
- require 'chutney/linter'
2
-
3
1
  module Chutney
4
2
  # service class to lint for missing test actions
5
3
  class MissingTestAction < Linter
6
4
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- when_steps = scenario[:steps].select { |step| step[:keyword] == 'When ' }
5
+ filled_scenarios do |feature, scenario|
6
+ when_steps = scenario[:steps].select { |step| when_word?(step[:keyword]) }
9
7
  next unless when_steps.empty?
10
8
 
11
- references = [reference(file, feature, scenario)]
12
- add_error(references, 'No \'When\'-Step')
9
+ add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
13
10
  end
14
11
  end
15
12
  end
@@ -4,12 +4,11 @@ module Chutney
4
4
  # service class to lint for missing verifications
5
5
  class MissingVerification < Linter
6
6
  def lint
7
- filled_scenarios do |file, feature, scenario|
8
- then_steps = scenario[:steps].select { |step| step[:keyword] == 'Then ' }
7
+ filled_scenarios do |feature, scenario|
8
+ then_steps = scenario[:steps].select { |step| then_word?(step[:keyword]) }
9
9
  next unless then_steps.empty?
10
10
 
11
- references = [reference(file, feature, scenario)]
12
- add_error(references, 'No \'Then\' step')
11
+ add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
13
12
  end
14
13
  end
15
14
  end
@@ -1,14 +1,29 @@
1
- require 'chutney/linter/tag_constraint'
2
- require 'chutney/linter'
3
-
4
1
  module Chutney
5
2
  # service class to lint for tags used multiple times
6
3
  class RequiredTagsStartsWith < Linter
7
- include TagConstraint
4
+
5
+ def lint
6
+ return unless pattern
8
7
 
8
+ scenarios do |feature, scenario|
9
+ next if match_pattern? tags_for(feature)
10
+ next if match_pattern? tags_for(scenario)
11
+
12
+ add_issue(
13
+ I18n.t('linters.required_tags_starts_with',
14
+ allowed: pattern.join(', ')),
15
+ feature, scenario
16
+ )
17
+ end
18
+ end
19
+
20
+ def pattern
21
+ configuration['Matcher'] || nil
22
+ end
23
+
9
24
  def match_pattern?(target)
10
25
  target.each do |t|
11
- return true if t.delete!('@').start_with?(*@pattern)
26
+ return true if t.start_with?(*pattern)
12
27
  end
13
28
  false
14
29
  end