chutney 3.0.0.beta.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/README.md +1 -1
- data/chutney.gemspec +1 -1
- data/config/{chutney.yml → chutney_defaults.yml} +0 -0
- data/exe/chutney +5 -5
- data/lib/chutney.rb +8 -3
- data/lib/chutney/configuration.rb +3 -2
- data/lib/chutney/formatter.rb +4 -5
- data/lib/chutney/formatter/pie_formatter.rb +9 -10
- data/lib/chutney/formatter/rainbow_formatter.rb +8 -11
- data/lib/chutney/linter.rb +34 -34
- data/lib/chutney/linter/avoid_full_stop.rb +1 -3
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -3
- data/lib/chutney/linter/avoid_scripting.rb +2 -2
- data/lib/chutney/linter/background_does_more_than_setup.rb +3 -4
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +1 -1
- data/lib/chutney/linter/bad_scenario_name.rb +2 -2
- data/lib/chutney/linter/file_name_differs_feature_name.rb +3 -3
- data/lib/chutney/linter/givens_after_background.rb +2 -2
- data/lib/chutney/linter/invalid_file_name.rb +1 -1
- data/lib/chutney/linter/invalid_step_flow.rb +2 -2
- data/lib/chutney/linter/missing_example_name.rb +2 -4
- data/lib/chutney/linter/missing_feature_description.rb +1 -1
- data/lib/chutney/linter/missing_feature_name.rb +2 -2
- data/lib/chutney/linter/missing_scenario_name.rb +1 -2
- data/lib/chutney/linter/missing_test_action.rb +1 -1
- data/lib/chutney/linter/missing_verification.rb +1 -1
- data/lib/chutney/linter/required_tags_starts_with.rb +5 -6
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +9 -9
- data/lib/chutney/linter/scenario_names_match.rb +2 -3
- data/lib/chutney/linter/tag_used_multiple_times.rb +1 -1
- data/lib/chutney/linter/too_clumsy.rb +1 -1
- data/lib/chutney/linter/too_long_step.rb +3 -3
- data/lib/chutney/linter/too_many_different_tags.rb +6 -6
- data/lib/chutney/linter/too_many_steps.rb +3 -3
- data/lib/chutney/linter/too_many_tags.rb +3 -3
- data/lib/chutney/linter/unique_scenario_names.rb +2 -2
- data/lib/chutney/linter/unknown_variable.rb +3 -3
- data/lib/chutney/linter/unused_variable.rb +3 -4
- data/lib/chutney/linter/use_background.rb +4 -4
- data/lib/chutney/linter/use_outline.rb +7 -7
- data/lib/chutney/version.rb +1 -1
- data/spec/chutney_spec.rb +9 -9
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a489eb526fae6a79ff386986463cf473aa4f5bc353448e99338781556f7fd194
|
4
|
+
data.tar.gz: 5cd7a3264aba055c8fe738147edfa661f57e2c0e258d5991b42adbfb2af45f05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fb8325c116764fadd900b0b98961eca7cf95f49ac171ef64e77e29f1806d6ff78bcca90ea9e2119320d33d81b13e67749fb3bf8e916df13e154871b54602b35
|
7
|
+
data.tar.gz: 8290dd430b9b55c900179717c3b3fcd83ea805e9142751dec8c69ae4c1e2233a3d5392d8951705a3fed0678b3a9fd768941889b5e9086465fa54ced8c43b1811
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
data/chutney.gemspec
CHANGED
@@ -61,7 +61,7 @@ Gem::Specification.new do |spec|
|
|
61
61
|
spec.add_development_dependency 'rake', '~> 13.0'
|
62
62
|
spec.add_development_dependency 'rerun', '~> 0.13'
|
63
63
|
spec.add_development_dependency 'rspec-expectations', '~> 3.0'
|
64
|
-
spec.add_development_dependency 'rubocop', '~> 0.
|
64
|
+
spec.add_development_dependency 'rubocop', '~> 0.90.0'
|
65
65
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
66
66
|
|
67
67
|
spec.required_ruby_version = '~> 2.6'
|
File without changes
|
data/exe/chutney
CHANGED
@@ -12,14 +12,14 @@ formatters = Set.new
|
|
12
12
|
|
13
13
|
OptionParser.new do |opts|
|
14
14
|
opts.banner = 'Usage: chutney [files]'
|
15
|
-
opts.on('-f',
|
16
|
-
'--format [formatter]',
|
15
|
+
opts.on('-f',
|
16
|
+
'--format [formatter]',
|
17
17
|
'One of JSONFormatter, PieFormatter or RainbowFormatter (default).') do |formatter|
|
18
18
|
raise 'No Such Formatter' unless %w[JSONFormatter PieFormatter RainbowFormatter].include? formatter
|
19
19
|
|
20
20
|
formatters << formatter
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
opts.on('-l',
|
24
24
|
'--linters',
|
25
25
|
'List the linter status by this configuration and exit') do
|
@@ -28,7 +28,7 @@ OptionParser.new do |opts|
|
|
28
28
|
max_name_length = chutney_config.keys.map(&:length).max + 1
|
29
29
|
chutney_config.each do |linter, value|
|
30
30
|
print pastel.cyan(linter.ljust(max_name_length))
|
31
|
-
|
31
|
+
|
32
32
|
if value['Enabled']
|
33
33
|
puts pastel.green('enabled')
|
34
34
|
else
|
@@ -42,7 +42,7 @@ end.parse!
|
|
42
42
|
formatters << 'RainbowFormatter' if formatters.empty?
|
43
43
|
|
44
44
|
files = ARGV.map { |pattern| Dir.glob(pattern) }.flatten
|
45
|
-
files = Dir.glob('features/**/*.feature') if ARGV.empty?
|
45
|
+
files = Dir.glob('features/**/*.feature') if ARGV.empty?
|
46
46
|
|
47
47
|
linter = Chutney::ChutneyLint.new(*files)
|
48
48
|
report = linter.analyse
|
data/lib/chutney.rb
CHANGED
@@ -36,6 +36,7 @@ require 'chutney/linter/unknown_variable'
|
|
36
36
|
require 'chutney/linter/unused_variable'
|
37
37
|
require 'chutney/linter/use_background'
|
38
38
|
require 'chutney/linter/use_outline'
|
39
|
+
require 'chutney/version'
|
39
40
|
|
40
41
|
require 'cuke_modeler'
|
41
42
|
require 'forwardable'
|
@@ -58,14 +59,18 @@ module Chutney
|
|
58
59
|
@files = files
|
59
60
|
@results = Hash.new { |h, k| h[k] = [] }
|
60
61
|
i18n_paths = Dir[File.expand_path(File.join(__dir__, 'config/locales')) + '/*.yml']
|
61
|
-
return if I18n.load_path.include?(i18n_paths)
|
62
62
|
|
63
|
-
|
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
|
64
69
|
end
|
65
70
|
|
66
71
|
def configuration
|
67
72
|
unless @config
|
68
|
-
default_file = [File.expand_path('..', __dir__), '**/config', '
|
73
|
+
default_file = [File.expand_path('..', __dir__), '**/config', 'chutney_defaults.yml']
|
69
74
|
config_file = Dir.glob(File.join(default_file)).first.freeze
|
70
75
|
@config = Configuration.new(config_file)
|
71
76
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'delegate'
|
3
4
|
|
4
|
-
module Chutney
|
5
|
+
module Chutney
|
5
6
|
# gherkin_lint configuration object
|
6
7
|
class Configuration < SimpleDelegator
|
7
8
|
def initialize(path)
|
@@ -23,7 +24,7 @@ module Chutney
|
|
23
24
|
config_files = ['chutney.yml', '.chutney.yml'].map do |fname|
|
24
25
|
Dir.glob(File.join(Dir.pwd, '**', fname))
|
25
26
|
end.flatten
|
26
|
-
|
27
|
+
|
27
28
|
config_file = config_files.first
|
28
29
|
merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
|
29
30
|
end
|
data/lib/chutney/formatter.rb
CHANGED
@@ -4,18 +4,17 @@ module Chutney
|
|
4
4
|
# base class for all formatters
|
5
5
|
class Formatter
|
6
6
|
attr_accessor :results
|
7
|
-
|
7
|
+
|
8
8
|
def initialize
|
9
9
|
@results = {}
|
10
|
-
end
|
11
|
-
|
10
|
+
end
|
11
|
+
|
12
12
|
def files
|
13
13
|
results.map { |k, _v| k }
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def files_with_issues
|
17
17
|
results.filter { |_k, v| v.any? { |r| r[:issues].count.positive? } }
|
18
18
|
end
|
19
|
-
|
20
19
|
end
|
21
20
|
end
|
@@ -9,7 +9,7 @@ module Chutney
|
|
9
9
|
def initialize
|
10
10
|
super
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def format
|
14
14
|
data = top_offences.map do |offence|
|
15
15
|
{
|
@@ -21,14 +21,14 @@ module Chutney
|
|
21
21
|
end
|
22
22
|
print_report(data)
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
def print_report(data)
|
26
26
|
return if data.empty?
|
27
27
|
|
28
28
|
print TTY::Pie.new(data: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
|
29
29
|
puts
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
def top_offences
|
33
33
|
offence = Hash.new(0)
|
34
34
|
files_with_issues.each do |_file, linter|
|
@@ -38,9 +38,9 @@ module Chutney
|
|
38
38
|
end
|
39
39
|
offence.reject { |_k, v| v.zero? }.sort_by { |_linter, count| -count }
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def char_loop
|
43
|
-
@char_looper ||= Fiber.new do
|
43
|
+
@char_looper ||= Fiber.new do
|
44
44
|
chars = %w[• x + @ * / -]
|
45
45
|
current = 0
|
46
46
|
loop do
|
@@ -51,10 +51,10 @@ module Chutney
|
|
51
51
|
end
|
52
52
|
@char_looper.resume
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def colour_loop
|
56
|
-
@colour_looper ||= Fiber.new do
|
57
|
-
colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
|
56
|
+
@colour_looper ||= Fiber.new do
|
57
|
+
colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
|
58
58
|
current = 0
|
59
59
|
loop do
|
60
60
|
current = 0 if current >= colours.count
|
@@ -64,7 +64,7 @@ module Chutney
|
|
64
64
|
end
|
65
65
|
@colour_looper.resume
|
66
66
|
end
|
67
|
-
|
67
|
+
|
68
68
|
def put_summary
|
69
69
|
pastel = Pastel.new
|
70
70
|
print "#{files.count} features inspected, "
|
@@ -74,6 +74,5 @@ module Chutney
|
|
74
74
|
puts pastel.red("#{files_with_issues.count} taste nasty")
|
75
75
|
end
|
76
76
|
end
|
77
|
-
|
78
77
|
end
|
79
78
|
end
|
@@ -5,38 +5,36 @@ require 'pastel'
|
|
5
5
|
module Chutney
|
6
6
|
# pretty formatter
|
7
7
|
class RainbowFormatter < Formatter
|
8
|
-
|
9
8
|
def initialize
|
10
9
|
super
|
11
|
-
|
10
|
+
|
12
11
|
@pastel = Pastel.new
|
13
12
|
end
|
14
|
-
|
15
|
-
def format
|
13
|
+
|
14
|
+
def format
|
16
15
|
files_with_issues.each do |file, linter|
|
17
16
|
put_file(file)
|
18
17
|
linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
|
19
|
-
|
20
18
|
put_linter(linter_with_issues)
|
21
19
|
linter_with_issues[:issues].each { |i| put_issue(file, i) }
|
22
20
|
end
|
23
21
|
end
|
24
22
|
put_summary
|
25
23
|
end
|
26
|
-
|
24
|
+
|
27
25
|
def put_file(file)
|
28
26
|
puts @pastel.cyan(file.to_s)
|
29
27
|
end
|
30
|
-
|
28
|
+
|
31
29
|
def put_linter(linter)
|
32
30
|
puts @pastel.red(" #{linter[:linter]}")
|
33
31
|
end
|
34
|
-
|
35
|
-
def put_issue(file, issue)
|
32
|
+
|
33
|
+
def put_issue(file, issue)
|
36
34
|
puts " #{issue[:message]}"
|
37
35
|
puts " #{@pastel.dim file.to_s}:#{@pastel.dim(issue.dig(:location, :line))}"
|
38
36
|
end
|
39
|
-
|
37
|
+
|
40
38
|
def put_summary
|
41
39
|
print "#{files.count} features inspected, "
|
42
40
|
if files_with_issues.count.zero?
|
@@ -45,6 +43,5 @@ module Chutney
|
|
45
43
|
puts @pastel.red("#{files_with_issues.count} taste nasty")
|
46
44
|
end
|
47
45
|
end
|
48
|
-
|
49
46
|
end
|
50
47
|
end
|
data/lib/chutney/linter.rb
CHANGED
@@ -7,7 +7,7 @@ module Chutney
|
|
7
7
|
class Linter
|
8
8
|
attr_accessor :issues
|
9
9
|
attr_reader :filename, :configuration
|
10
|
-
|
10
|
+
|
11
11
|
Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
|
12
12
|
|
13
13
|
def self.descendants
|
@@ -26,56 +26,56 @@ module Chutney
|
|
26
26
|
def lint
|
27
27
|
raise 'not implemented'
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
def and_word?(word)
|
31
31
|
dialect_word(:and).include?(word)
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
def background_word?(word)
|
35
35
|
dialect_word(:background).include?(word)
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def but_word?(word)
|
39
39
|
dialect_word(:but).include?(word)
|
40
40
|
end
|
41
|
-
|
41
|
+
|
42
42
|
def examples_word?(word)
|
43
43
|
dialect_word(:examples).include?(word)
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
def feature_word?(word)
|
47
47
|
dialect_word(:feature).include?(word)
|
48
48
|
end
|
49
|
-
|
49
|
+
|
50
50
|
def given_word?(word)
|
51
51
|
dialect_word(:given).include?(word)
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
def scenario_outline_word?(word)
|
55
55
|
dialect_word(:scenarioOutline).include?(word)
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
def then_word?(word)
|
59
59
|
dialect_word(:then).include?(word)
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
62
|
def when_word?(word)
|
63
63
|
dialect_word(:when).include?(word)
|
64
64
|
end
|
65
|
-
|
65
|
+
|
66
66
|
def dialect_word(word)
|
67
67
|
CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
70
|
def dialect
|
71
71
|
@content.feature&.parsing_data&.dig(:language) || 'en'
|
72
72
|
end
|
73
|
-
|
73
|
+
|
74
74
|
def tags_for(element)
|
75
75
|
element.tags.map { |tag| tag.name[1..-1] }
|
76
76
|
end
|
77
|
-
|
78
|
-
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
77
|
+
|
78
|
+
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
79
79
|
issues << Lint.new(
|
80
80
|
message: message,
|
81
81
|
gherkin_type: type(feature, scenario, item),
|
@@ -85,17 +85,17 @@ module Chutney
|
|
85
85
|
step: item&.parsing_data&.dig(:name)
|
86
86
|
).to_h
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
def location(feature, scenario, step)
|
90
90
|
if step
|
91
91
|
step.parsing_data[:location]
|
92
92
|
elsif scenario
|
93
93
|
scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
|
94
|
-
else
|
95
|
-
feature ? feature.parsing_data[:location] : 0
|
94
|
+
else
|
95
|
+
feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
|
96
96
|
end
|
97
97
|
end
|
98
|
-
|
98
|
+
|
99
99
|
def type(_feature, scenario, step)
|
100
100
|
if step
|
101
101
|
:step
|
@@ -111,14 +111,14 @@ module Chutney
|
|
111
111
|
@content.feature
|
112
112
|
end
|
113
113
|
end
|
114
|
-
|
115
|
-
def elements
|
114
|
+
|
115
|
+
def elements
|
116
116
|
return [] unless feature
|
117
|
-
|
118
|
-
if block_given?
|
117
|
+
|
118
|
+
if block_given?
|
119
119
|
feature.children.each do |child|
|
120
120
|
next if off_switch?(child)
|
121
|
-
|
121
|
+
|
122
122
|
yield(feature, child)
|
123
123
|
end
|
124
124
|
else
|
@@ -136,38 +136,38 @@ module Chutney
|
|
136
136
|
off_switch ||= off_switch?(feature) unless element == feature
|
137
137
|
off_switch
|
138
138
|
end
|
139
|
-
|
139
|
+
|
140
140
|
def background
|
141
|
-
if block_given?
|
141
|
+
if block_given?
|
142
142
|
yield(feature, feature&.background)
|
143
143
|
else
|
144
144
|
feature&.background
|
145
145
|
end
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
def scenarios
|
149
149
|
if block_given?
|
150
150
|
feature&.tests&.each do |test|
|
151
151
|
yield(feature, test)
|
152
152
|
end
|
153
|
-
|
153
|
+
|
154
154
|
else
|
155
155
|
feature&.tests
|
156
156
|
end
|
157
157
|
end
|
158
|
-
|
158
|
+
|
159
159
|
def filled_scenarios
|
160
160
|
if block_given?
|
161
161
|
scenarios do |feature, scenario|
|
162
162
|
next if scenario.steps.empty?
|
163
|
-
|
163
|
+
|
164
164
|
yield(feature, scenario)
|
165
165
|
end
|
166
166
|
else
|
167
167
|
scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
|
168
168
|
end
|
169
169
|
end
|
170
|
-
|
170
|
+
|
171
171
|
def steps
|
172
172
|
feature&.tests&.each do |t|
|
173
173
|
t.steps.each { |s| yield(feature, t, s) }
|
@@ -177,7 +177,7 @@ module Chutney
|
|
177
177
|
def self.linter_name
|
178
178
|
name.split('::').last
|
179
179
|
end
|
180
|
-
|
180
|
+
|
181
181
|
def linter_name
|
182
182
|
self.class.linter_name
|
183
183
|
end
|
@@ -187,10 +187,10 @@ module Chutney
|
|
187
187
|
value += render_step_argument(step.block) if step.block
|
188
188
|
value
|
189
189
|
end
|
190
|
-
|
190
|
+
|
191
191
|
def render_step_argument(argument)
|
192
192
|
return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
|
193
|
-
|
193
|
+
|
194
194
|
result = argument.rows.map do |row|
|
195
195
|
"|#{row.cells.map(&:value).join '|'}|"
|
196
196
|
end.join "\n"
|
@@ -2,12 +2,10 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for avoiding periods
|
5
|
-
class AvoidFullStop < Linter
|
5
|
+
class AvoidFullStop < Linter
|
6
6
|
def lint
|
7
7
|
steps do |feature, child, step|
|
8
|
-
|
9
8
|
add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
|
10
|
-
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
@@ -4,13 +4,13 @@ module Chutney
|
|
4
4
|
# service class to lint for avoiding outline for single example
|
5
5
|
class AvoidOutlineForSingleExample < Linter
|
6
6
|
def lint
|
7
|
-
scenarios do |feature, scenario|
|
7
|
+
scenarios do |feature, scenario|
|
8
8
|
next unless scenario.is_a? CukeModeler::Outline
|
9
9
|
next unless scenario.examples
|
10
|
-
|
10
|
+
|
11
11
|
next if scenario.examples.length > 1
|
12
12
|
next if scenario.examples.first.rows.length > 2 # first row is the header
|
13
|
-
|
13
|
+
|
14
14
|
add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
|
15
15
|
end
|
16
16
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for avoid scripting
|
5
|
-
class AvoidScripting < Linter
|
5
|
+
class AvoidScripting < Linter
|
6
6
|
def lint
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
when_steps = filter_when_steps(scenario.steps)
|
@@ -10,7 +10,7 @@ module Chutney
|
|
10
10
|
add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
|
11
11
|
end
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def filter_when_steps(steps)
|
15
15
|
steps
|
16
16
|
.drop_while { |step| !when_word?(step.keyword) }
|
@@ -2,14 +2,13 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for background that does more than setup
|
5
|
-
class BackgroundDoesMoreThanSetup < Linter
|
5
|
+
class BackgroundDoesMoreThanSetup < Linter
|
6
6
|
def lint
|
7
7
|
background do |feature, background|
|
8
|
-
|
9
|
-
invalid_steps = background&.steps&.select do |step|
|
8
|
+
invalid_steps = background&.steps&.select do |step|
|
10
9
|
when_word?(step.keyword) || then_word?(step.keyword)
|
11
10
|
end
|
12
|
-
|
11
|
+
|
13
12
|
next if invalid_steps.nil? || invalid_steps.empty?
|
14
13
|
|
15
14
|
add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
|
@@ -6,7 +6,7 @@ module Chutney
|
|
6
6
|
# service class for check that there are multiple scenarios once a background is used
|
7
7
|
class BackgroundRequiresMultipleScenarios < Linter
|
8
8
|
MESSAGE = 'Avoid using Background steps for just one scenario'
|
9
|
-
|
9
|
+
|
10
10
|
def lint
|
11
11
|
background do |feature, background|
|
12
12
|
next unless background
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Chutney
|
4
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
5
5
|
class BadScenarioName < Linter
|
6
6
|
def lint
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
next if scenario.name.empty?
|
9
|
-
|
9
|
+
|
10
10
|
bad = /\w*(test|verif|check)\w*/i
|
11
11
|
match = scenario.name.match(bad).to_a.first
|
12
12
|
add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
|
@@ -5,11 +5,11 @@ module Chutney
|
|
5
5
|
class FileNameDiffersFeatureName < Linter
|
6
6
|
def lint
|
7
7
|
return unless feature
|
8
|
-
|
8
|
+
|
9
9
|
expected_feature_name = title_case(filename)
|
10
10
|
return if ignore_whitespaces(feature.name).casecmp(ignore_whitespaces(expected_feature_name)) == 0
|
11
|
-
|
12
|
-
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
11
|
+
|
12
|
+
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
13
13
|
end
|
14
14
|
|
15
15
|
def title_case(value)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Chutney
|
4
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
5
5
|
class GivensAfterBackground < Linter
|
6
6
|
def lint
|
7
7
|
return unless background
|
8
|
-
|
8
|
+
|
9
9
|
filled_scenarios do |feature, scenario|
|
10
10
|
scenario.steps.each do |step|
|
11
11
|
add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step) if given_word?(step.keyword)
|
@@ -7,7 +7,7 @@ module Chutney
|
|
7
7
|
filled_scenarios do |feature, scenario|
|
8
8
|
steps = scenario.steps.select { |step| !and_word?(step.keyword) && !but_word?(step.keyword) }
|
9
9
|
next if steps.empty?
|
10
|
-
|
10
|
+
|
11
11
|
last_step_is_an_action(feature, scenario, steps)
|
12
12
|
given_after_non_given(feature, scenario, steps)
|
13
13
|
verification_before_action(feature, scenario, steps)
|
@@ -33,7 +33,7 @@ module Chutney
|
|
33
33
|
def verification_before_action(feature, scenario, steps)
|
34
34
|
steps.each do |step|
|
35
35
|
break if when_word?(step.keyword)
|
36
|
-
|
36
|
+
|
37
37
|
add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario) if then_word?(step.keyword)
|
38
38
|
end
|
39
39
|
end
|
@@ -3,7 +3,6 @@
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for missing example names
|
5
5
|
class MissingExampleName < Linter
|
6
|
-
|
7
6
|
def lint
|
8
7
|
scenarios do |_feature, scenario|
|
9
8
|
next unless scenario.is_a? CukeModeler::Outline
|
@@ -11,17 +10,16 @@ module Chutney
|
|
11
10
|
scenario.examples.each do |example|
|
12
11
|
example_count = scenario.examples&.length || 0
|
13
12
|
next unless example_count > 1
|
14
|
-
|
13
|
+
|
15
14
|
check_example(scenario, example)
|
16
15
|
end
|
17
16
|
end
|
18
|
-
end
|
17
|
+
end
|
19
18
|
|
20
19
|
def check_example(scenario, example)
|
21
20
|
name = example.name.strip
|
22
21
|
duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
|
23
22
|
add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
|
24
23
|
end
|
25
|
-
|
26
24
|
end
|
27
25
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for missing feature names
|
5
|
-
class MissingFeatureName < Linter
|
5
|
+
class MissingFeatureName < Linter
|
6
6
|
def lint
|
7
7
|
return unless feature
|
8
|
-
|
8
|
+
|
9
9
|
add_issue(I18n.t('linters.missing_feature_name'), feature) if feature.name.empty?
|
10
10
|
end
|
11
11
|
end
|
@@ -2,8 +2,7 @@
|
|
2
2
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for missing scenario names
|
5
|
-
class MissingScenarioName < Linter
|
6
|
-
|
5
|
+
class MissingScenarioName < Linter
|
7
6
|
def lint
|
8
7
|
scenarios do |feature, scenario|
|
9
8
|
add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if scenario.name.empty?
|
@@ -3,26 +3,25 @@
|
|
3
3
|
module Chutney
|
4
4
|
# service class to lint for tags used multiple times
|
5
5
|
class RequiredTagsStartsWith < Linter
|
6
|
-
|
7
6
|
def lint
|
8
7
|
return unless pattern
|
9
8
|
|
10
9
|
scenarios do |feature, scenario|
|
11
10
|
next if match_pattern? tags_for(feature)
|
12
11
|
next if match_pattern? tags_for(scenario)
|
13
|
-
|
12
|
+
|
14
13
|
add_issue(
|
15
|
-
I18n.t('linters.required_tags_starts_with',
|
16
|
-
allowed: pattern.join(', ')),
|
14
|
+
I18n.t('linters.required_tags_starts_with',
|
15
|
+
allowed: pattern.join(', ')),
|
17
16
|
feature, scenario
|
18
17
|
)
|
19
18
|
end
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
def pattern
|
23
22
|
configuration['Matcher'] || nil
|
24
23
|
end
|
25
|
-
|
24
|
+
|
26
25
|
def match_pattern?(target)
|
27
26
|
target.each do |t|
|
28
27
|
return true if t.start_with?(*pattern)
|
@@ -14,13 +14,13 @@ module Chutney
|
|
14
14
|
tags = scenario_tags
|
15
15
|
return if tags.nil? || tags.empty?
|
16
16
|
return unless feature.tests.length > 1
|
17
|
-
|
17
|
+
|
18
18
|
tags.each do |tag|
|
19
19
|
next if tag == 'skip'
|
20
20
|
|
21
21
|
add_issue(
|
22
|
-
I18n.t('linters.same_tag_for_all_scenarios.feature_level',
|
23
|
-
tag: tag),
|
22
|
+
I18n.t('linters.same_tag_for_all_scenarios.feature_level',
|
23
|
+
tag: tag),
|
24
24
|
feature
|
25
25
|
)
|
26
26
|
end
|
@@ -32,12 +32,12 @@ module Chutney
|
|
32
32
|
next if tags.nil? || tags.empty?
|
33
33
|
next unless scenario.is_a? CukeModeler::Outline
|
34
34
|
next unless scenario.examples.length > 1
|
35
|
-
|
35
|
+
|
36
36
|
tags.each do |tag|
|
37
37
|
next if tag == 'skip'
|
38
38
|
|
39
|
-
add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
|
40
|
-
tag: tag), feature, scenario)
|
39
|
+
add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
|
40
|
+
tag: tag), feature, scenario)
|
41
41
|
end
|
42
42
|
end
|
43
43
|
end
|
@@ -55,13 +55,13 @@ module Chutney
|
|
55
55
|
def example_tags(scenario)
|
56
56
|
result = nil
|
57
57
|
return result unless scenario.is_a?(CukeModeler::Outline) && scenario.examples
|
58
|
-
|
58
|
+
|
59
59
|
scenario.examples.each do |example|
|
60
60
|
return nil unless example.tags
|
61
|
-
|
61
|
+
|
62
62
|
tags = tags_for(example)
|
63
63
|
result = tags if result.nil?
|
64
|
-
|
64
|
+
|
65
65
|
result &= tags
|
66
66
|
end
|
67
67
|
result
|
@@ -6,14 +6,13 @@ module Chutney
|
|
6
6
|
# service class to lint for tags used multiple times
|
7
7
|
class ScenarioNamesMatch < Linter
|
8
8
|
MESSAGE = 'Scenario Name does not match pattern'
|
9
|
-
|
10
9
|
|
11
10
|
def lint
|
12
11
|
scenarios do |feature, scenario|
|
13
12
|
next unless (scenario.name =~ /#{configuration['Matcher']}/).nil?
|
14
|
-
|
13
|
+
|
15
14
|
add_issue(
|
16
|
-
I18n.t('linters.scenario_names_match',
|
15
|
+
I18n.t('linters.scenario_names_match',
|
17
16
|
pattern: configuration['Matcher']), feature, scenario
|
18
17
|
)
|
19
18
|
end
|
@@ -8,7 +8,7 @@ module Chutney
|
|
8
8
|
total_tags = tags_for(feature) + tags_for(scenario)
|
9
9
|
double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
|
10
10
|
next if double_used_tags.nil?
|
11
|
-
|
11
|
+
|
12
12
|
add_issue(
|
13
13
|
I18n.t('linters.tag_used_multiple_times', tags: double_used_tags.join(',')), feature
|
14
14
|
)
|
@@ -6,14 +6,14 @@ module Chutney
|
|
6
6
|
def lint
|
7
7
|
steps do |feature, scenario, step|
|
8
8
|
next if step.text.length <= maxlength
|
9
|
-
|
9
|
+
|
10
10
|
add_issue(
|
11
|
-
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),
|
12
12
|
feature, scenario
|
13
13
|
)
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def maxlength
|
18
18
|
configuration['MaxLength'] || '120'
|
19
19
|
end
|
@@ -6,20 +6,20 @@ module Chutney
|
|
6
6
|
def lint
|
7
7
|
tags = all_tags
|
8
8
|
return if tags.length <= maxcount
|
9
|
-
|
9
|
+
|
10
10
|
add_issue(
|
11
|
-
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),
|
12
12
|
feature
|
13
13
|
)
|
14
14
|
end
|
15
|
-
|
16
|
-
def maxcount
|
15
|
+
|
16
|
+
def maxcount
|
17
17
|
configuration['MaxCount']&.to_i || 3
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def all_tags
|
21
21
|
return [] unless feature&.scenarios
|
22
|
-
|
22
|
+
|
23
23
|
tags_for(feature) + feature.scenarios.map { |scenario| tags_for(scenario) }.flatten
|
24
24
|
end
|
25
25
|
end
|
@@ -6,14 +6,14 @@ module Chutney
|
|
6
6
|
def lint
|
7
7
|
filled_scenarios do |feature, scenario|
|
8
8
|
next if scenario.steps.length <= maxcount
|
9
|
-
|
9
|
+
|
10
10
|
add_issue(
|
11
|
-
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),
|
12
12
|
feature
|
13
13
|
)
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def maxcount
|
18
18
|
configuration['MaxCount']&.to_i || 10
|
19
19
|
end
|
@@ -7,14 +7,14 @@ module Chutney
|
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
tags = tags_for(feature) + tags_for(scenario)
|
9
9
|
next unless tags.length > maxcount
|
10
|
-
|
10
|
+
|
11
11
|
add_issue(
|
12
|
-
I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
|
12
|
+
I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
|
13
13
|
feature
|
14
14
|
)
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def maxcount
|
19
19
|
configuration['MaxCount']&.to_i || 3
|
20
20
|
end
|
@@ -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
|
@@ -9,7 +9,7 @@ module Chutney
|
|
9
9
|
scenario.steps.each do |step|
|
10
10
|
step_vars(step).each do |used_var|
|
11
11
|
next if known_vars.include? used_var
|
12
|
-
|
12
|
+
|
13
13
|
add_issue(
|
14
14
|
I18n.t('linters.unknown_variable', variable: used_var), feature, scenario
|
15
15
|
)
|
@@ -21,7 +21,7 @@ module Chutney
|
|
21
21
|
def step_vars(step)
|
22
22
|
vars = gather_vars step.text
|
23
23
|
return vars unless step.block
|
24
|
-
|
24
|
+
|
25
25
|
vars + gather_vars_from_argument(step.block)
|
26
26
|
end
|
27
27
|
|
@@ -39,7 +39,7 @@ module Chutney
|
|
39
39
|
|
40
40
|
def known_variables(scenario)
|
41
41
|
return [] unless scenario.is_a? CukeModeler::Outline
|
42
|
-
|
42
|
+
|
43
43
|
scenario.examples.map { |ex| ex.rows.first.cells.map(&:value) }.flatten
|
44
44
|
end
|
45
45
|
end
|
@@ -6,9 +6,8 @@ module Chutney
|
|
6
6
|
def lint
|
7
7
|
scenarios do |feature, scenario|
|
8
8
|
next unless scenario.is_a? CukeModeler::Outline
|
9
|
-
|
9
|
+
|
10
10
|
scenario.examples.each do |example|
|
11
|
-
|
12
11
|
example.rows.first.cells.map(&:value).each do |variable|
|
13
12
|
next if used?(variable, scenario)
|
14
13
|
|
@@ -20,7 +19,7 @@ module Chutney
|
|
20
19
|
|
21
20
|
def used?(variable, scenario)
|
22
21
|
variable = "<#{variable}>"
|
23
|
-
|
22
|
+
|
24
23
|
scenario.steps.each do |step|
|
25
24
|
return true if step.text.include? variable
|
26
25
|
next unless step.block
|
@@ -36,7 +35,7 @@ module Chutney
|
|
36
35
|
|
37
36
|
def used_in_table?(variable, step)
|
38
37
|
return false unless step.block.is_a?(CukeModeler::Table)
|
39
|
-
|
38
|
+
|
40
39
|
step.block.rows.each do |row|
|
41
40
|
row.cells.each { |cell| return true if cell.value.include?(variable) }
|
42
41
|
end
|
@@ -16,11 +16,11 @@ module Chutney
|
|
16
16
|
|
17
17
|
def gather_givens
|
18
18
|
return unless feature.children
|
19
|
-
|
19
|
+
|
20
20
|
has_non_given_step = false
|
21
21
|
scenarios do |_feature, scenario|
|
22
22
|
next unless scenario.steps
|
23
|
-
|
23
|
+
|
24
24
|
has_non_given_step = true unless given_word?(scenario.steps.first.keyword)
|
25
25
|
end
|
26
26
|
return if has_non_given_step
|
@@ -33,7 +33,7 @@ module Chutney
|
|
33
33
|
def expanded_steps(&block)
|
34
34
|
scenarios do |_feature, scenario|
|
35
35
|
next unless scenario.steps
|
36
|
-
|
36
|
+
|
37
37
|
prototypes = [render_step(scenario.steps.first)]
|
38
38
|
prototypes = expand_examples(scenario.examples, prototypes) if scenario.is_a? CukeModeler::Outline
|
39
39
|
prototypes.each(&block)
|
@@ -52,7 +52,7 @@ module Chutney
|
|
52
52
|
headers = example.rows.first.cells.map(&:value)
|
53
53
|
example.rows.each_with_index do |row, idx|
|
54
54
|
next if idx.zero? # skip the header
|
55
|
-
|
55
|
+
|
56
56
|
modified_sentence = sentence.dup
|
57
57
|
headers.zip(row.cells.map(&:value)).map do |key, value|
|
58
58
|
modified_sentence.gsub!("<#{key}>", value)
|
@@ -11,18 +11,18 @@ module Chutney
|
|
11
11
|
scenarios.product(scenarios) do |lhs, rhs|
|
12
12
|
next if lhs == rhs
|
13
13
|
next if lhs[:reference][:line] > rhs[:reference][:line]
|
14
|
-
|
14
|
+
|
15
15
|
similarity = determine_similarity(lhs[:text], rhs[:text])
|
16
16
|
next unless similarity >= 0.95
|
17
|
-
|
17
|
+
|
18
18
|
similarity_pct = similarity.round(3) * 100
|
19
|
-
|
19
|
+
|
20
20
|
add_issue(lhs, rhs, similarity_pct)
|
21
21
|
end
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def add_issue(lhs, rhs, pct)
|
25
|
-
super(I18n.t('linters.use_outline',
|
25
|
+
super(I18n.t('linters.use_outline',
|
26
26
|
pct: pct,
|
27
27
|
lhs_name: lhs[:name],
|
28
28
|
lhs_line: lhs[:reference][:line],
|
@@ -39,11 +39,11 @@ module Chutney
|
|
39
39
|
def gather_scenarios(feature)
|
40
40
|
scenarios = []
|
41
41
|
return scenarios if feature.nil? || !feature.tests
|
42
|
-
|
42
|
+
|
43
43
|
scenarios do |_feature, scenario|
|
44
44
|
next unless scenario.steps
|
45
45
|
next if scenario.steps.empty?
|
46
|
-
|
46
|
+
|
47
47
|
scenarios.push generate_reference(feature, scenario)
|
48
48
|
end
|
49
49
|
scenarios
|
data/lib/chutney/version.rb
CHANGED
data/spec/chutney_spec.rb
CHANGED
@@ -8,25 +8,25 @@ describe Chutney::ChutneyLint do
|
|
8
8
|
it 'has a version number' do
|
9
9
|
expect(Chutney::VERSION).not_to be nil
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
describe '#initialize' do
|
13
13
|
it 'creates an instance' do
|
14
14
|
expect(subject).not_to be_nil
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it 'has an empty list of files if none are given' do
|
18
18
|
expect(subject.files).to eq []
|
19
19
|
end
|
20
|
-
|
20
|
+
|
21
21
|
it 'has a list of files given on initialization' do
|
22
22
|
alt_subject = Chutney::ChutneyLint.new('a', 'b')
|
23
23
|
expect(alt_subject.files).to eq %w[a b]
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
it 'initializes a results hash' do
|
27
27
|
expect(subject.results).to eq({})
|
28
28
|
end
|
29
|
-
|
29
|
+
|
30
30
|
it 'sets the load path for I18n' do
|
31
31
|
expect(I18n.load_path).not_to eq []
|
32
32
|
end
|
@@ -36,25 +36,25 @@ describe Chutney::ChutneyLint do
|
|
36
36
|
expect(subject.configuration).not_to be_nil
|
37
37
|
expect(subject.configuration).to respond_to :[]
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
40
|
it 'allows the configuration to be set explicitly' do
|
41
41
|
config = { 'BackgroundDoesMoreThanSetup' => { 'Enabled' => true } }
|
42
42
|
subject.configuration = config
|
43
43
|
expect(subject.configuration).to be config
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
it 'controls the available linters' do
|
47
47
|
subject.configuration = {}
|
48
48
|
expect(subject.linters).to be_empty
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
it 'enables linters to be activated' do
|
52
52
|
config = { 'BackgroundDoesMoreThanSetup' => { 'Enabled' => true } }
|
53
53
|
subject.configuration = config
|
54
54
|
expect(subject.linters).to eq [Chutney::BackgroundDoesMoreThanSetup]
|
55
55
|
end
|
56
56
|
end
|
57
|
-
|
57
|
+
|
58
58
|
context 'linting' do
|
59
59
|
it 'aliases analyse and analyze' do
|
60
60
|
expect(subject).to respond_to :analyse
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chutney
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.0
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nigel Brookes-Thomas
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: exe
|
13
13
|
cert_chain: []
|
14
|
-
date: 2020-09-
|
14
|
+
date: 2020-09-23 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: amatch
|
@@ -173,14 +173,14 @@ dependencies:
|
|
173
173
|
requirements:
|
174
174
|
- - "~>"
|
175
175
|
- !ruby/object:Gem::Version
|
176
|
-
version: 0.
|
176
|
+
version: 0.90.0
|
177
177
|
type: :development
|
178
178
|
prerelease: false
|
179
179
|
version_requirements: !ruby/object:Gem::Requirement
|
180
180
|
requirements:
|
181
181
|
- - "~>"
|
182
182
|
- !ruby/object:Gem::Version
|
183
|
-
version: 0.
|
183
|
+
version: 0.90.0
|
184
184
|
- !ruby/object:Gem::Dependency
|
185
185
|
name: rspec
|
186
186
|
requirement: !ruby/object:Gem::Requirement
|
@@ -217,7 +217,7 @@ files:
|
|
217
217
|
- README.md
|
218
218
|
- Rakefile
|
219
219
|
- chutney.gemspec
|
220
|
-
- config/
|
220
|
+
- config/chutney_defaults.yml
|
221
221
|
- config/cucumber.yml
|
222
222
|
- docs/.keep
|
223
223
|
- docs/_config.yml
|
@@ -297,9 +297,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
297
297
|
version: '2.6'
|
298
298
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
299
299
|
requirements:
|
300
|
-
- - "
|
300
|
+
- - ">="
|
301
301
|
- !ruby/object:Gem::Version
|
302
|
-
version:
|
302
|
+
version: '0'
|
303
303
|
requirements: []
|
304
304
|
rubygems_version: 3.1.2
|
305
305
|
signing_key:
|