chutney 3.0.0.beta.2 → 3.0.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/README.md +1 -1
  4. data/chutney.gemspec +1 -1
  5. data/config/{chutney.yml → chutney_defaults.yml} +0 -0
  6. data/exe/chutney +5 -5
  7. data/lib/chutney.rb +8 -3
  8. data/lib/chutney/configuration.rb +3 -2
  9. data/lib/chutney/formatter.rb +4 -5
  10. data/lib/chutney/formatter/pie_formatter.rb +9 -10
  11. data/lib/chutney/formatter/rainbow_formatter.rb +8 -11
  12. data/lib/chutney/linter.rb +34 -34
  13. data/lib/chutney/linter/avoid_full_stop.rb +1 -3
  14. data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -3
  15. data/lib/chutney/linter/avoid_scripting.rb +2 -2
  16. data/lib/chutney/linter/background_does_more_than_setup.rb +3 -4
  17. data/lib/chutney/linter/background_requires_multiple_scenarios.rb +1 -1
  18. data/lib/chutney/linter/bad_scenario_name.rb +2 -2
  19. data/lib/chutney/linter/file_name_differs_feature_name.rb +3 -3
  20. data/lib/chutney/linter/givens_after_background.rb +2 -2
  21. data/lib/chutney/linter/invalid_file_name.rb +1 -1
  22. data/lib/chutney/linter/invalid_step_flow.rb +2 -2
  23. data/lib/chutney/linter/missing_example_name.rb +2 -4
  24. data/lib/chutney/linter/missing_feature_description.rb +1 -1
  25. data/lib/chutney/linter/missing_feature_name.rb +2 -2
  26. data/lib/chutney/linter/missing_scenario_name.rb +1 -2
  27. data/lib/chutney/linter/missing_test_action.rb +1 -1
  28. data/lib/chutney/linter/missing_verification.rb +1 -1
  29. data/lib/chutney/linter/required_tags_starts_with.rb +5 -6
  30. data/lib/chutney/linter/same_tag_for_all_scenarios.rb +9 -9
  31. data/lib/chutney/linter/scenario_names_match.rb +2 -3
  32. data/lib/chutney/linter/tag_used_multiple_times.rb +1 -1
  33. data/lib/chutney/linter/too_clumsy.rb +1 -1
  34. data/lib/chutney/linter/too_long_step.rb +3 -3
  35. data/lib/chutney/linter/too_many_different_tags.rb +6 -6
  36. data/lib/chutney/linter/too_many_steps.rb +3 -3
  37. data/lib/chutney/linter/too_many_tags.rb +3 -3
  38. data/lib/chutney/linter/unique_scenario_names.rb +2 -2
  39. data/lib/chutney/linter/unknown_variable.rb +3 -3
  40. data/lib/chutney/linter/unused_variable.rb +3 -4
  41. data/lib/chutney/linter/use_background.rb +4 -4
  42. data/lib/chutney/linter/use_outline.rb +7 -7
  43. data/lib/chutney/version.rb +1 -1
  44. data/spec/chutney_spec.rb +9 -9
  45. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c99f32f7cba016b8d419d1867c5005b448e05939565afff74db238c68222f2d9
4
- data.tar.gz: 9f21a905656a69d4bbfc7288b7abcec37e36e3fb5950c852aa8c6e4183e52cd9
3
+ metadata.gz: a489eb526fae6a79ff386986463cf473aa4f5bc353448e99338781556f7fd194
4
+ data.tar.gz: 5cd7a3264aba055c8fe738147edfa661f57e2c0e258d5991b42adbfb2af45f05
5
5
  SHA512:
6
- metadata.gz: 3a4339e31214900e809bdbdaf968a2d21af9bc7526365cd2f00a17745b10272947114c018e6b182c6679769636ffc127d6880d69dbbbb8dc66f0d5f5762a410d
7
- data.tar.gz: b755781258f1781e4586a77f639682b7ebb0a584d50aa0c001a7eca5c5e8b0c16487b2d1dd1978e97744c5f7ec163ea169ae8d9c6114e1ed22cda716e3173a34
6
+ metadata.gz: 9fb8325c116764fadd900b0b98961eca7cf95f49ac171ef64e77e29f1806d6ff78bcca90ea9e2119320d33d81b13e67749fb3bf8e916df13e154871b54602b35
7
+ data.tar.gz: 8290dd430b9b55c900179717c3b3fcd83ea805e9142751dec8c69ae4c1e2233a3d5392d8951705a3fed0678b3a9fd768941889b5e9086465fa54ced8c43b1811
@@ -52,11 +52,11 @@ 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
58
  Enabled: true
59
-
59
+
60
60
  Style/StringConcatenation:
61
61
  Enabled: false
62
62
 
data/README.md CHANGED
@@ -19,7 +19,7 @@
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
23
 
24
24
  ## Notes
25
25
 
@@ -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.89.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'
@@ -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
@@ -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
- 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
64
69
  end
65
70
 
66
71
  def configuration
67
72
  unless @config
68
- default_file = [File.expand_path('..', __dir__), '**/config', 'chutney.yml']
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
@@ -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
@@ -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)
@@ -13,7 +13,7 @@ module Chutney
13
13
  end
14
14
  end
15
15
  end
16
-
16
+
17
17
  def recommend(filename)
18
18
  File.basename(filename, '.*').gsub(/::/, '/')
19
19
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -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
@@ -6,7 +6,7 @@ module Chutney
6
6
  MESSAGE = 'Features should have a description so that its purpose is clear'
7
7
  def lint
8
8
  return unless feature
9
-
9
+
10
10
  add_issue(I18n.t('linters.missing_feature_description'), feature) if feature.description.empty?
11
11
  end
12
12
  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?
@@ -7,7 +7,7 @@ module Chutney
7
7
  filled_scenarios do |feature, scenario|
8
8
  when_steps = scenario.steps.select { |step| when_word?(step.keyword) }
9
9
  next unless when_steps.empty?
10
-
10
+
11
11
  add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
12
12
  end
13
13
  end
@@ -9,7 +9,7 @@ module Chutney
9
9
  filled_scenarios do |feature, scenario|
10
10
  then_steps = scenario.steps.select { |step| then_word?(step.keyword) }
11
11
  next unless then_steps.empty?
12
-
12
+
13
13
  add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
14
14
  end
15
15
  end
@@ -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
  )
@@ -9,7 +9,7 @@ module Chutney
9
9
  filled_scenarios do |feature, scenario|
10
10
  characters = scenario.steps.map { |step| step.text.length }.inject(0, :+)
11
11
  next if characters < 400
12
-
12
+
13
13
  add_issue(
14
14
  I18n.t('linters.too_clumsy', length: characters), feature, scenario
15
15
  )
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Chutney
4
- VERSION = '3.0.0.beta.2'
4
+ VERSION = '3.0.0'
5
5
  end
@@ -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.beta.2
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-17 00:00:00.000000000 Z
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.89.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.89.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/chutney.yml
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: 1.3.1
302
+ version: '0'
303
303
  requirements: []
304
304
  rubygems_version: 3.1.2
305
305
  signing_key: