chutney 2.1.1 → 3.0.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -2
  3. data/Gemfile +2 -0
  4. data/README.md +5 -1
  5. data/Rakefile +2 -0
  6. data/chutney.gemspec +13 -6
  7. data/config/{chutney.yml → chutney_defaults.yml} +2 -0
  8. data/config/cucumber.yml +1 -0
  9. data/docs/usage/rules.md +3 -0
  10. data/exe/chutney +23 -3
  11. data/lib/chutney.rb +26 -22
  12. data/lib/chutney/configuration.rb +9 -2
  13. data/lib/chutney/formatter.rb +6 -5
  14. data/lib/chutney/formatter/json_formatter.rb +2 -0
  15. data/lib/chutney/formatter/pie_formatter.rb +10 -13
  16. data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
  17. data/lib/chutney/issue.rb +2 -0
  18. data/lib/chutney/linter.rb +95 -84
  19. data/lib/chutney/linter/avoid_full_stop.rb +4 -4
  20. data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
  21. data/lib/chutney/linter/avoid_scripting.rb +8 -6
  22. data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
  23. data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
  24. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
  25. data/lib/chutney/linter/bad_scenario_name.rb +6 -4
  26. data/lib/chutney/linter/empty_feature_file.rb +10 -0
  27. data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
  28. data/lib/chutney/linter/givens_after_background.rb +7 -8
  29. data/lib/chutney/linter/invalid_file_name.rb +3 -1
  30. data/lib/chutney/linter/invalid_step_flow.rb +9 -9
  31. data/lib/chutney/linter/missing_example_name.rb +9 -9
  32. data/lib/chutney/linter/missing_feature_description.rb +5 -4
  33. data/lib/chutney/linter/missing_feature_name.rb +5 -4
  34. data/lib/chutney/linter/missing_scenario_name.rb +4 -6
  35. data/lib/chutney/linter/missing_test_action.rb +4 -2
  36. data/lib/chutney/linter/missing_verification.rb +4 -2
  37. data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
  38. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
  39. data/lib/chutney/linter/scenario_names_match.rb +6 -6
  40. data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
  41. data/lib/chutney/linter/too_clumsy.rb +4 -2
  42. data/lib/chutney/linter/too_long_step.rb +6 -4
  43. data/lib/chutney/linter/too_many_different_tags.rb +10 -8
  44. data/lib/chutney/linter/too_many_steps.rb +6 -4
  45. data/lib/chutney/linter/too_many_tags.rb +5 -3
  46. data/lib/chutney/linter/unique_scenario_names.rb +5 -5
  47. data/lib/chutney/linter/unknown_variable.rb +15 -15
  48. data/lib/chutney/linter/unused_variable.rb +15 -16
  49. data/lib/chutney/linter/use_background.rb +20 -19
  50. data/lib/chutney/linter/use_outline.rb +15 -14
  51. data/lib/chutney/version.rb +3 -1
  52. data/lib/config/locales/en.yml +2 -0
  53. data/spec/chutney_spec.rb +11 -9
  54. data/spec/spec_helper.rb +2 -0
  55. metadata +19 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2da8d4d9f8b99fbedf8ed3b2407e5ec9024496258f86cd69fb76d35699b4ffa4
4
- data.tar.gz: 1821ce09b96bc45203e1a65abac9d5c042bca5036933feda9c066c0ff852ede6
3
+ metadata.gz: da2d0aa4432eaa81b87b877c67dcada20758c521f193cfca6a91a83904ea209a
4
+ data.tar.gz: 1e2c22d44911f0816e0e28504c8a465105edf406848ea15305ef25af470c95d3
5
5
  SHA512:
6
- metadata.gz: 3adad450052c2008c3a1db8f82e65893c39605592b72ce676e9b3973fd19f6f2812a949ac5fa375909277298c12115e94771b7dcee06b34dc09362f415af16da
7
- data.tar.gz: ebb9e4d1ea6842131863666ea4362dfc3e94c50525b1444c75e5f8b662c3bd340481d4a8bb00b265991dec0546aab0017e3f86d3a76a850738fbf4407cc68d32
6
+ metadata.gz: f9f1821d7d97ab7fc2f67cdc6e33d4f5ff94d4a1b7e20017bfe71b3b784697e54c3493d91812412dcfda8b18f54a756a8232e36c53ac653a99b34d97eaaefa76
7
+ data.tar.gz: eebac54200ed820084c183ab48eeb538aacec5a93ff4eef09552fa4917ec65d76e0c12e266689ecb149c9b0ab0fd93d178a405822fa4010c904bb21a4ff9c4d1
@@ -8,7 +8,7 @@
8
8
 
9
9
  # Offense count: 1
10
10
  Metrics/AbcSize:
11
- Max: 16
11
+ Enabled: false
12
12
 
13
13
  # Offense count: 1
14
14
  # Configuration parameters: CountComments.
@@ -52,7 +52,13 @@ Layout/HeredocIndentation:
52
52
  - "**/*_spec.rb"
53
53
 
54
54
  Layout/TrailingWhitespace:
55
- Enabled: false
55
+ Enabled: true
56
56
 
57
57
  Style/FrozenStringLiteralComment:
58
+ Enabled: true
59
+
60
+ Style/StringConcatenation:
58
61
  Enabled: false
62
+
63
+ AllCops:
64
+ NewCops: enable
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/README.md CHANGED
@@ -19,4 +19,8 @@
19
19
 
20
20
  </div>
21
21
 
22
- Read the documentation [here](https://billyruffian.github.io/chutney/).
22
+ Read the documentation [here](https://billyruffian.github.io/chutney/) or try [Chutney Online](https://chutney.billy-ruffian.co.uk).
23
+
24
+ ## Notes
25
+
26
+ Chutney 3+ (in beta) has replaced its direct dependency on Cucumber and instead uses the excellent [cuke_modeller](https://github.com/enkessler/cuke_modeler) to parse your feature files.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  task default: :build
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Disable rubocop checks for the .gemspec
2
4
  # I'll take the output from 'bundle gem new' to be authoritative
3
5
  # rubocop:disable all
@@ -12,9 +14,13 @@ Gem::Specification.new do |spec|
12
14
  spec.authors = ['Nigel Brookes-Thomas', 'Stefan Rohe', 'Nishtha Argawal', 'John Gluck']
13
15
  spec.email = ['nigel@brookes-thomas.co.uk']
14
16
 
15
- spec.summary = 'A linter for English language Gherkin'
16
- spec.description = 'A linter for your Cucumber features. ' \
17
- 'It supports any spoken language Cucumber v3 supports.'
17
+ spec.summary = 'A linter for multi-lingual Gherkin'
18
+ spec.description = 'A linter for your Cucumber features. ' \
19
+ 'Making sure you have nice, expressible Gherkin is ' \
20
+ 'essential is making sure you have a readable test-base. ' \
21
+ 'Chutney is designed to sniff out smells in your feature ' \
22
+ 'files. ' \
23
+ 'It supports any spoken language Cucumber supports.'
18
24
 
19
25
  spec.homepage = 'https://billyruffian.github.io/chutney/'
20
26
  spec.license = 'MIT'
@@ -43,19 +49,20 @@ Gem::Specification.new do |spec|
43
49
  spec.require_paths = ['lib']
44
50
 
45
51
  spec.add_runtime_dependency 'amatch', '~> 0.4.0'
46
- spec.add_runtime_dependency 'gherkin', '~> 5.1.0'
52
+ spec.add_runtime_dependency 'cuke_modeler', '~> 3.3'
47
53
  spec.add_runtime_dependency 'i18n', '~> 1.8.2'
48
54
  spec.add_runtime_dependency 'pastel', '~> 0.7'
49
55
  spec.add_runtime_dependency 'tty-pie', '~> 0.3'
50
56
 
51
57
 
52
58
  spec.add_development_dependency 'coveralls', '~> 0.8'
53
- spec.add_development_dependency 'cucumber', '~> 3.0'
59
+ spec.add_development_dependency 'cucumber', '~> 5.1'
54
60
  spec.add_development_dependency 'pry-byebug', '~> 3.0'
55
61
  spec.add_development_dependency 'rake', '~> 13.0'
56
62
  spec.add_development_dependency 'rerun', '~> 0.13'
57
63
  spec.add_development_dependency 'rspec-expectations', '~> 3.0'
58
- spec.add_development_dependency 'rubocop', '~> 0.89.0'
64
+ spec.add_development_dependency 'rubocop', '~> 0.90.0'
59
65
  spec.add_development_dependency 'rspec', '~> 3.8'
60
66
 
67
+ spec.required_ruby_version = '~> 2.6'
61
68
  end
@@ -12,6 +12,8 @@ BackgroundRequiresMultipleScenarios:
12
12
  Enabled: true
13
13
  BadScenarioName:
14
14
  Enabled: true
15
+ EmptyFeatureFile:
16
+ Enabled: true
15
17
  FileNameDiffersFeatureName:
16
18
  Enabled: true
17
19
  GivensAfterBackground:
@@ -0,0 +1 @@
1
+ default: --publish-quiet
@@ -30,6 +30,9 @@ Chutney enforces its rules with the linters. These are:
30
30
  [BadScenarioName](https://github.com/BillyRuffian/chutney/blob/master/features/bad_scenario_name.feature)
31
31
  : You should avoid using words like 'test' or 'check' in your scenario names, instead you should define the behaviour of your system.
32
32
 
33
+ [EmptyFeatureFile](https://github.com/BillyRuffian/chutney/blob/master/features/empty_feature_file.feature)
34
+ : The feature should have content and should avoid committing empty features to repositories.
35
+
33
36
  [FileNameDiffersFeatureName](https://github.com/BillyRuffian/chutney/blob/master/features/file_name_differs_feature_name.feature)
34
37
  : The feature should have a name that follows the file name.
35
38
 
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'chutney'
3
5
  require 'chutney/formatter'
4
6
  require 'chutney/formatter/json_formatter'
@@ -10,19 +12,37 @@ formatters = Set.new
10
12
 
11
13
  OptionParser.new do |opts|
12
14
  opts.banner = 'Usage: chutney [files]'
13
- opts.on('-f',
14
- '--format [formatter]',
15
+ opts.on('-f',
16
+ '--format [formatter]',
15
17
  'One of JSONFormatter, PieFormatter or RainbowFormatter (default).') do |formatter|
16
18
  raise 'No Such Formatter' unless %w[JSONFormatter PieFormatter RainbowFormatter].include? formatter
17
19
 
18
20
  formatters << formatter
19
21
  end
22
+
23
+ opts.on('-l',
24
+ '--linters',
25
+ 'List the linter status by this configuration and exit') do
26
+ pastel = Pastel.new
27
+ chutney_config = Chutney::ChutneyLint.new.configuration
28
+ max_name_length = chutney_config.keys.map(&:length).max + 1
29
+ chutney_config.each do |linter, value|
30
+ print pastel.cyan(linter.ljust(max_name_length))
31
+
32
+ if value['Enabled']
33
+ puts pastel.green('enabled')
34
+ else
35
+ puts pastel.red('disabled')
36
+ end
37
+ end
38
+ exit
39
+ end
20
40
  end.parse!
21
41
 
22
42
  formatters << 'RainbowFormatter' if formatters.empty?
23
43
 
24
44
  files = ARGV.map { |pattern| Dir.glob(pattern) }.flatten
25
- files = Dir.glob('features/**/*.feature') if ARGV.empty?
45
+ files = Dir.glob('features/**/*.feature') if ARGV.empty?
26
46
 
27
47
  linter = Chutney::ChutneyLint.new(*files)
28
48
  report = linter.analyse
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'amatch'
4
+
2
5
  require 'chutney/configuration'
3
6
  require 'chutney/linter'
4
7
  require 'chutney/linter/avoid_full_stop'
@@ -8,6 +11,7 @@ require 'chutney/linter/avoid_typographers_quotes'
8
11
  require 'chutney/linter/background_does_more_than_setup'
9
12
  require 'chutney/linter/background_requires_multiple_scenarios'
10
13
  require 'chutney/linter/bad_scenario_name'
14
+ require 'chutney/linter/empty_feature_file'
11
15
  require 'chutney/linter/file_name_differs_feature_name'
12
16
  require 'chutney/linter/givens_after_background'
13
17
  require 'chutney/linter/invalid_file_name'
@@ -32,9 +36,12 @@ require 'chutney/linter/unknown_variable'
32
36
  require 'chutney/linter/unused_variable'
33
37
  require 'chutney/linter/use_background'
34
38
  require 'chutney/linter/use_outline'
39
+ require 'chutney/version'
40
+
41
+ require 'cuke_modeler'
35
42
  require 'forwardable'
36
- require 'gherkin/dialect'
37
- require 'gherkin/parser'
43
+ # require 'gherkin/dialect'
44
+ # require 'gherkin/parser'
38
45
  require 'i18n'
39
46
  require 'set'
40
47
  require 'yaml'
@@ -44,33 +51,36 @@ module Chutney
44
51
  class ChutneyLint
45
52
  extend Forwardable
46
53
  attr_accessor :verbose
47
- attr_reader :files
48
- attr_reader :results
49
-
54
+ attr_reader :files, :results
55
+
50
56
  def_delegators :@files, :<<, :clear, :delete, :include?
51
57
 
52
58
  def initialize(*files)
53
59
  @files = files
54
60
  @results = Hash.new { |h, k| h[k] = [] }
55
61
  i18n_paths = Dir[File.expand_path(File.join(__dir__, 'config/locales')) + '/*.yml']
56
- return if I18n.load_path.include?(i18n_paths)
57
62
 
58
- I18n.load_path << i18n_paths
63
+ i18n_paths.each do |path|
64
+ next if I18n.load_path.include?(path)
65
+
66
+ I18n.load_path << path
67
+ I18n.backend.reload!
68
+ end
59
69
  end
60
-
70
+
61
71
  def configuration
62
72
  unless @config
63
- default_file = [File.expand_path('..', __dir__), '**/config', 'chutney.yml']
73
+ default_file = [File.expand_path('..', __dir__), '**/config', 'chutney_defaults.yml']
64
74
  config_file = Dir.glob(File.join(default_file)).first.freeze
65
75
  @config = Configuration.new(config_file)
66
76
  end
67
77
  @config
68
78
  end
69
-
79
+
70
80
  def configuration=(config)
71
81
  @config = config
72
82
  end
73
-
83
+
74
84
  def analyse
75
85
  files.each do |f|
76
86
  lint(f)
@@ -80,25 +90,19 @@ module Chutney
80
90
  # alias for non-british English
81
91
  # https://dictionary.cambridge.org/dictionary/english/analyse
82
92
  alias analyze analyse
83
-
93
+
84
94
  def linters
85
95
  @linters ||= Linter.descendants.filter { |l| configuration.dig(l.linter_name, 'Enabled') }
86
96
  end
87
-
97
+
88
98
  def linters=(*linters)
89
99
  @linters = linters
90
100
  end
91
-
101
+
92
102
  private
93
-
94
- def parse(text)
95
- @parser ||= Gherkin::Parser.new
96
- scanner = Gherkin::TokenScanner.new(text)
97
- @parser.parse(scanner)
98
- end
99
-
103
+
100
104
  def lint(file)
101
- parsed = parse(File.read(file))
105
+ parsed = CukeModeler::FeatureFile.new(file)
102
106
  linters.each do |linter_class|
103
107
  linter = linter_class.new(file, parsed, configuration[linter_class.linter_name])
104
108
  linter.lint
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
- module Chutney
4
+
5
+ module Chutney
3
6
  # gherkin_lint configuration object
4
7
  class Configuration < SimpleDelegator
5
8
  def initialize(path)
@@ -18,7 +21,11 @@ module Chutney
18
21
  end
19
22
 
20
23
  def load_user_configuration
21
- config_file = Dir.glob(File.join(Dir.pwd, '**', '.chutney.yml')).first
24
+ config_files = ['chutney.yml', '.chutney.yml'].map do |fname|
25
+ Dir.glob(File.join(Dir.pwd, '**', fname))
26
+ end.flatten
27
+
28
+ config_file = config_files.first
22
29
  merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
23
30
  end
24
31
 
@@ -1,19 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # base class for all formatters
3
5
  class Formatter
4
6
  attr_accessor :results
5
-
7
+
6
8
  def initialize
7
9
  @results = {}
8
- end
9
-
10
+ end
11
+
10
12
  def files
11
13
  results.map { |k, _v| k }
12
14
  end
13
-
15
+
14
16
  def files_with_issues
15
17
  results.filter { |_k, v| v.any? { |r| r[:issues].count.positive? } }
16
18
  end
17
-
18
19
  end
19
20
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Chutney
2
4
  # Plain old JSON formatter
3
5
  class JSONFormatter < Formatter
@@ -1,13 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
  require 'tty/pie'
3
5
 
4
6
  module Chutney
5
7
  # format results as pie charts
6
8
  class PieFormatter < Formatter
7
- def initialize
8
- super
9
- end
10
-
11
9
  def format
12
10
  data = top_offences.map do |offence|
13
11
  {
@@ -19,14 +17,14 @@ module Chutney
19
17
  end
20
18
  print_report(data)
21
19
  end
22
-
20
+
23
21
  def print_report(data)
24
22
  return if data.empty?
25
23
 
26
24
  print TTY::Pie.new(data: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
27
25
  puts
28
26
  end
29
-
27
+
30
28
  def top_offences
31
29
  offence = Hash.new(0)
32
30
  files_with_issues.each do |_file, linter|
@@ -36,9 +34,9 @@ module Chutney
36
34
  end
37
35
  offence.reject { |_k, v| v.zero? }.sort_by { |_linter, count| -count }
38
36
  end
39
-
37
+
40
38
  def char_loop
41
- @char_looper ||= Fiber.new do
39
+ @char_looper ||= Fiber.new do
42
40
  chars = %w[• x + @ * / -]
43
41
  current = 0
44
42
  loop do
@@ -49,10 +47,10 @@ module Chutney
49
47
  end
50
48
  @char_looper.resume
51
49
  end
52
-
50
+
53
51
  def colour_loop
54
- @colour_looper ||= Fiber.new do
55
- colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
52
+ @colour_looper ||= Fiber.new do
53
+ colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
56
54
  current = 0
57
55
  loop do
58
56
  current = 0 if current >= colours.count
@@ -62,7 +60,7 @@ module Chutney
62
60
  end
63
61
  @colour_looper.resume
64
62
  end
65
-
63
+
66
64
  def put_summary
67
65
  pastel = Pastel.new
68
66
  print "#{files.count} features inspected, "
@@ -72,6 +70,5 @@ module Chutney
72
70
  puts pastel.red("#{files_with_issues.count} taste nasty")
73
71
  end
74
72
  end
75
-
76
73
  end
77
74
  end
@@ -1,39 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pastel'
2
4
 
3
5
  module Chutney
4
6
  # pretty formatter
5
7
  class RainbowFormatter < Formatter
6
-
7
8
  def initialize
8
9
  super
9
-
10
+
10
11
  @pastel = Pastel.new
11
12
  end
12
-
13
- def format
13
+
14
+ def format
14
15
  files_with_issues.each do |file, linter|
15
16
  put_file(file)
16
17
  linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
17
-
18
18
  put_linter(linter_with_issues)
19
- linter_with_issues[:issues].each { |i| put_issue(i) }
19
+ linter_with_issues[:issues].each { |i| put_issue(file, i) }
20
20
  end
21
21
  end
22
22
  put_summary
23
23
  end
24
-
24
+
25
25
  def put_file(file)
26
26
  puts @pastel.cyan(file.to_s)
27
27
  end
28
-
28
+
29
29
  def put_linter(linter)
30
30
  puts @pastel.red(" #{linter[:linter]}")
31
31
  end
32
-
33
- def put_issue(issue)
34
- puts " #{@pastel.dim(issue.dig(:location, :line))} #{issue[:message]}"
32
+
33
+ def put_issue(file, issue)
34
+ puts " #{issue[:message]}"
35
+ puts " #{@pastel.dim file.to_s}:#{@pastel.dim(issue.dig(:location, :line))}"
35
36
  end
36
-
37
+
37
38
  def put_summary
38
39
  print "#{files.count} features inspected, "
39
40
  if files_with_issues.count.zero?
@@ -42,6 +43,5 @@ module Chutney
42
43
  puts @pastel.red("#{files_with_issues.count} taste nasty")
43
44
  end
44
45
  end
45
-
46
46
  end
47
47
  end