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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -2
- data/Gemfile +2 -0
- data/README.md +5 -1
- data/Rakefile +2 -0
- data/chutney.gemspec +13 -6
- data/config/{chutney.yml → chutney_defaults.yml} +2 -0
- data/config/cucumber.yml +1 -0
- data/docs/usage/rules.md +3 -0
- data/exe/chutney +23 -3
- data/lib/chutney.rb +26 -22
- data/lib/chutney/configuration.rb +9 -2
- data/lib/chutney/formatter.rb +6 -5
- data/lib/chutney/formatter/json_formatter.rb +2 -0
- data/lib/chutney/formatter/pie_formatter.rb +10 -13
- data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
- data/lib/chutney/issue.rb +2 -0
- data/lib/chutney/linter.rb +95 -84
- data/lib/chutney/linter/avoid_full_stop.rb +4 -4
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
- data/lib/chutney/linter/avoid_scripting.rb +8 -6
- data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
- data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
- data/lib/chutney/linter/bad_scenario_name.rb +6 -4
- data/lib/chutney/linter/empty_feature_file.rb +10 -0
- data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
- data/lib/chutney/linter/givens_after_background.rb +7 -8
- data/lib/chutney/linter/invalid_file_name.rb +3 -1
- data/lib/chutney/linter/invalid_step_flow.rb +9 -9
- data/lib/chutney/linter/missing_example_name.rb +9 -9
- data/lib/chutney/linter/missing_feature_description.rb +5 -4
- data/lib/chutney/linter/missing_feature_name.rb +5 -4
- data/lib/chutney/linter/missing_scenario_name.rb +4 -6
- data/lib/chutney/linter/missing_test_action.rb +4 -2
- data/lib/chutney/linter/missing_verification.rb +4 -2
- data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
- data/lib/chutney/linter/scenario_names_match.rb +6 -6
- data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
- data/lib/chutney/linter/too_clumsy.rb +4 -2
- data/lib/chutney/linter/too_long_step.rb +6 -4
- data/lib/chutney/linter/too_many_different_tags.rb +10 -8
- data/lib/chutney/linter/too_many_steps.rb +6 -4
- data/lib/chutney/linter/too_many_tags.rb +5 -3
- data/lib/chutney/linter/unique_scenario_names.rb +5 -5
- data/lib/chutney/linter/unknown_variable.rb +15 -15
- data/lib/chutney/linter/unused_variable.rb +15 -16
- data/lib/chutney/linter/use_background.rb +20 -19
- data/lib/chutney/linter/use_outline.rb +15 -14
- data/lib/chutney/version.rb +3 -1
- data/lib/config/locales/en.yml +2 -0
- data/spec/chutney_spec.rb +11 -9
- data/spec/spec_helper.rb +2 -0
- metadata +19 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da2d0aa4432eaa81b87b877c67dcada20758c521f193cfca6a91a83904ea209a
|
4
|
+
data.tar.gz: 1e2c22d44911f0816e0e28504c8a465105edf406848ea15305ef25af470c95d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9f1821d7d97ab7fc2f67cdc6e33d4f5ff94d4a1b7e20017bfe71b3b784697e54c3493d91812412dcfda8b18f54a756a8232e36c53ac653a99b34d97eaaefa76
|
7
|
+
data.tar.gz: eebac54200ed820084c183ab48eeb538aacec5a93ff4eef09552fa4917ec65d76e0c12e266689ecb149c9b0ab0fd93d178a405822fa4010c904bb21a4ff9c4d1
|
data/.rubocop.yml
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
# Offense count: 1
|
10
10
|
Metrics/AbcSize:
|
11
|
-
|
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:
|
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
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
data/chutney.gemspec
CHANGED
@@ -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
|
16
|
-
spec.description = 'A linter for your Cucumber features. '
|
17
|
-
|
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 '
|
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', '~>
|
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.
|
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
|
data/config/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --publish-quiet
|
data/docs/usage/rules.md
CHANGED
@@ -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
|
|
data/exe/chutney
CHANGED
@@ -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
|
data/lib/chutney.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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', '
|
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 =
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/chutney/formatter.rb
CHANGED
@@ -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,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 " #{
|
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
|