chutney 2.2.1 → 3.1.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +16 -0
- data/.rubocop.yml +10 -3
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +7 -1
- data/Rakefile +8 -8
- data/chutney.gemspec +13 -6
- data/config/{chutney.yml → chutney_defaults.yml} +2 -0
- data/config/cucumber.yml +1 -0
- data/docs/index.md +1 -1
- data/docs/usage/rules.md +34 -64
- data/exe/chutney +23 -3
- data/lib/chutney.rb +18 -14
- 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 +92 -86
- 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 +2 -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_different_case.rb +37 -0
- 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 +3 -0
- data/spec/chutney_spec.rb +11 -9
- data/spec/spec_helper.rb +2 -0
- metadata +21 -16
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chutney/linter'
|
4
|
+
|
5
|
+
module Chutney
|
6
|
+
# service class to lint for missing verifications
|
7
|
+
class SameTagDifferentCase < Linter
|
8
|
+
def all_known_tags
|
9
|
+
# rubocop:disable Style/ClassVars
|
10
|
+
@@all_known_tags ||= []
|
11
|
+
# rubocop:enable Style/ClassVars
|
12
|
+
end
|
13
|
+
|
14
|
+
def lint
|
15
|
+
scenarios do |feature, scenario|
|
16
|
+
total_tags = tags_for(feature) + tags_for(scenario)
|
17
|
+
|
18
|
+
total_tags.each do |tag|
|
19
|
+
collision_with = case_collision(tag)
|
20
|
+
if collision_with
|
21
|
+
add_issue(I18n.t('linters.same_tag_different_case',
|
22
|
+
existing_tag: collision_with, tag: tag),
|
23
|
+
feature, scenario)
|
24
|
+
else
|
25
|
+
@@all_known_tags << tag
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def case_collision(tag)
|
32
|
+
return nil if all_known_tags.include?(tag)
|
33
|
+
|
34
|
+
all_known_tags.select { |t| t.casecmp(tag).zero? }.first
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,40 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'chutney/linter'
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# service class to lint for using same tag on all scenarios
|
5
7
|
class SameTagForAllScenarios < Linter
|
6
8
|
def lint
|
7
|
-
lint_scenarios if feature&.
|
8
|
-
lint_examples if feature&.
|
9
|
+
lint_scenarios if feature&.scenarios
|
10
|
+
lint_examples if feature&.scenarios
|
9
11
|
end
|
10
12
|
|
11
13
|
def lint_scenarios
|
12
14
|
tags = scenario_tags
|
13
15
|
return if tags.nil? || tags.empty?
|
14
|
-
return unless feature
|
15
|
-
|
16
|
+
return unless feature.tests.length > 1
|
17
|
+
|
16
18
|
tags.each do |tag|
|
17
19
|
next if tag == 'skip'
|
18
20
|
|
19
21
|
add_issue(
|
20
|
-
I18n.t('linters.same_tag_for_all_scenarios.feature_level',
|
21
|
-
tag: tag),
|
22
|
+
I18n.t('linters.same_tag_for_all_scenarios.feature_level',
|
23
|
+
tag: tag),
|
22
24
|
feature
|
23
25
|
)
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
29
|
def lint_examples
|
28
|
-
|
30
|
+
scenarios do |_feature, scenario|
|
29
31
|
tags = example_tags(scenario)
|
30
32
|
next if tags.nil? || tags.empty?
|
31
|
-
next unless scenario
|
32
|
-
|
33
|
+
next unless scenario.is_a? CukeModeler::Outline
|
34
|
+
next unless scenario.examples.length > 1
|
35
|
+
|
33
36
|
tags.each do |tag|
|
34
37
|
next if tag == 'skip'
|
35
38
|
|
36
|
-
add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
|
37
|
-
tag: tag), feature, scenario)
|
39
|
+
add_issue(I18n.t('linters.same_tag_for_all_scenarios.example_level',
|
40
|
+
tag: tag), feature, scenario)
|
38
41
|
end
|
39
42
|
end
|
40
43
|
end
|
@@ -42,8 +45,6 @@ module Chutney
|
|
42
45
|
def scenario_tags
|
43
46
|
result = nil
|
44
47
|
scenarios do |_feature, scenario|
|
45
|
-
next if scenario[:type] == :Background
|
46
|
-
|
47
48
|
tags = tags_for(scenario)
|
48
49
|
result ||= tags
|
49
50
|
result &= tags
|
@@ -53,14 +54,14 @@ module Chutney
|
|
53
54
|
|
54
55
|
def example_tags(scenario)
|
55
56
|
result = nil
|
56
|
-
return result unless scenario.
|
57
|
-
|
58
|
-
scenario
|
59
|
-
return nil unless example.
|
60
|
-
|
57
|
+
return result unless scenario.is_a?(CukeModeler::Outline) && scenario.examples
|
58
|
+
|
59
|
+
scenario.examples.each do |example|
|
60
|
+
return nil unless example.tags
|
61
|
+
|
61
62
|
tags = tags_for(example)
|
62
63
|
result = tags if result.nil?
|
63
|
-
|
64
|
+
|
64
65
|
result &= tags
|
65
66
|
end
|
66
67
|
result
|
@@ -1,18 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'chutney/linter'
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# service class to lint for tags used multiple times
|
5
7
|
class ScenarioNamesMatch < Linter
|
6
|
-
MESSAGE = 'Scenario Name does not match pattern'
|
7
|
-
|
8
|
+
MESSAGE = 'Scenario Name does not match pattern'
|
8
9
|
|
9
10
|
def lint
|
10
11
|
scenarios do |feature, scenario|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
next unless (scenario.name =~ /#{configuration['Matcher']}/).nil?
|
13
|
+
|
14
14
|
add_issue(
|
15
|
-
I18n.t('linters.scenario_names_match',
|
15
|
+
I18n.t('linters.scenario_names_match',
|
16
16
|
pattern: configuration['Matcher']), feature, scenario
|
17
17
|
)
|
18
18
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for tags used multiple times
|
3
5
|
class TagUsedMultipleTimes < Linter
|
@@ -6,7 +8,7 @@ module Chutney
|
|
6
8
|
total_tags = tags_for(feature) + tags_for(scenario)
|
7
9
|
double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
|
8
10
|
next if double_used_tags.nil?
|
9
|
-
|
11
|
+
|
10
12
|
add_issue(
|
11
13
|
I18n.t('linters.tag_used_multiple_times', tags: double_used_tags.join(',')), feature
|
12
14
|
)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'chutney/linter'
|
2
4
|
|
3
5
|
module Chutney
|
@@ -5,9 +7,9 @@ module Chutney
|
|
5
7
|
class TooClumsy < Linter
|
6
8
|
def lint
|
7
9
|
filled_scenarios do |feature, scenario|
|
8
|
-
characters = scenario
|
10
|
+
characters = scenario.steps.map { |step| step.text.length }.inject(0, :+)
|
9
11
|
next if characters < 400
|
10
|
-
|
12
|
+
|
11
13
|
add_issue(
|
12
14
|
I18n.t('linters.too_clumsy', length: characters), feature, scenario
|
13
15
|
)
|
@@ -1,17 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for too long steps
|
3
5
|
class TooLongStep < Linter
|
4
6
|
def lint
|
5
7
|
steps do |feature, scenario, step|
|
6
|
-
next if step
|
7
|
-
|
8
|
+
next if step.text.length <= maxlength
|
9
|
+
|
8
10
|
add_issue(
|
9
|
-
I18n.t('linters.too_long_step', length: step
|
11
|
+
I18n.t('linters.too_long_step', length: step.text.length, max: maxlength),
|
10
12
|
feature, scenario
|
11
13
|
)
|
12
14
|
end
|
13
15
|
end
|
14
|
-
|
16
|
+
|
15
17
|
def maxlength
|
16
18
|
configuration['MaxLength'] || '120'
|
17
19
|
end
|
@@ -1,24 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for too many different tags
|
3
5
|
class TooManyDifferentTags < Linter
|
4
6
|
def lint
|
5
7
|
tags = all_tags
|
6
8
|
return if tags.length <= maxcount
|
7
|
-
|
9
|
+
|
8
10
|
add_issue(
|
9
|
-
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),
|
10
12
|
feature
|
11
13
|
)
|
12
14
|
end
|
13
|
-
|
14
|
-
def maxcount
|
15
|
+
|
16
|
+
def maxcount
|
15
17
|
configuration['MaxCount']&.to_i || 3
|
16
18
|
end
|
17
|
-
|
19
|
+
|
18
20
|
def all_tags
|
19
|
-
return [] unless feature&.
|
20
|
-
|
21
|
-
tags_for(feature) + feature
|
21
|
+
return [] unless feature&.scenarios
|
22
|
+
|
23
|
+
tags_for(feature) + feature.scenarios.map { |scenario| tags_for(scenario) }.flatten
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -1,17 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for too many steps
|
3
5
|
class TooManySteps < Linter
|
4
6
|
def lint
|
5
7
|
filled_scenarios do |feature, scenario|
|
6
|
-
next if scenario
|
7
|
-
|
8
|
+
next if scenario.steps.length <= maxcount
|
9
|
+
|
8
10
|
add_issue(
|
9
|
-
I18n.t('linters.too_many_steps', count: scenario
|
11
|
+
I18n.t('linters.too_many_steps', count: scenario.steps.length, max: maxcount),
|
10
12
|
feature
|
11
13
|
)
|
12
14
|
end
|
13
15
|
end
|
14
|
-
|
16
|
+
|
15
17
|
def maxcount
|
16
18
|
configuration['MaxCount']&.to_i || 10
|
17
19
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for too many tags
|
3
5
|
class TooManyTags < Linter
|
@@ -5,14 +7,14 @@ module Chutney
|
|
5
7
|
scenarios do |feature, scenario|
|
6
8
|
tags = tags_for(feature) + tags_for(scenario)
|
7
9
|
next unless tags.length > maxcount
|
8
|
-
|
10
|
+
|
9
11
|
add_issue(
|
10
|
-
I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
|
12
|
+
I18n.t('linters.too_many_tags', count: tags.length, max: maxcount),
|
11
13
|
feature
|
12
14
|
)
|
13
15
|
end
|
14
16
|
end
|
15
|
-
|
17
|
+
|
16
18
|
def maxcount
|
17
19
|
configuration['MaxCount']&.to_i || 3
|
18
20
|
end
|
@@ -1,12 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for unique scenario names
|
3
5
|
class UniqueScenarioNames < Linter
|
4
6
|
def lint
|
5
7
|
references_by_name = {}
|
6
8
|
scenarios do |feature, scenario|
|
7
|
-
|
8
|
-
|
9
|
-
name = scenario[:name]
|
9
|
+
name = scenario.name
|
10
10
|
if references_by_name[name]
|
11
11
|
issue(name, references_by_name[name], scenario)
|
12
12
|
else
|
@@ -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
|
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for unknown variables
|
3
5
|
class UnknownVariable < Linter
|
4
6
|
def lint
|
5
7
|
filled_scenarios do |feature, scenario|
|
6
8
|
known_vars = Set.new(known_variables(scenario))
|
7
|
-
scenario
|
9
|
+
scenario.steps.each do |step|
|
8
10
|
step_vars(step).each do |used_var|
|
9
11
|
next if known_vars.include? used_var
|
10
|
-
|
12
|
+
|
11
13
|
add_issue(
|
12
14
|
I18n.t('linters.unknown_variable', variable: used_var), feature, scenario
|
13
15
|
)
|
@@ -17,17 +19,17 @@ module Chutney
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def step_vars(step)
|
20
|
-
vars = gather_vars step
|
21
|
-
return vars unless step.
|
22
|
-
|
23
|
-
vars + gather_vars_from_argument(step
|
22
|
+
vars = gather_vars step.text
|
23
|
+
return vars unless step.block
|
24
|
+
|
25
|
+
vars + gather_vars_from_argument(step.block)
|
24
26
|
end
|
25
27
|
|
26
28
|
def gather_vars_from_argument(argument)
|
27
|
-
return gather_vars argument
|
28
|
-
|
29
|
-
|
30
|
-
row
|
29
|
+
return gather_vars argument.content if argument.is_a? CukeModeler::DocString
|
30
|
+
|
31
|
+
argument.rows.map do |row|
|
32
|
+
row.cells.map { |cell| gather_vars cell.value }.flatten
|
31
33
|
end.flatten
|
32
34
|
end
|
33
35
|
|
@@ -36,11 +38,9 @@ module Chutney
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def known_variables(scenario)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
example[:tableHeader][:cells].map { |cell| cell[:value].strip }
|
43
|
-
end.flatten
|
41
|
+
return [] unless scenario.is_a? CukeModeler::Outline
|
42
|
+
|
43
|
+
scenario.examples.map { |ex| ex.rows.first.cells.map(&:value) }.flatten
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
@@ -1,14 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for unused variables
|
3
5
|
class UnusedVariable < Linter
|
4
6
|
def lint
|
5
7
|
scenarios do |feature, scenario|
|
6
|
-
next unless scenario.
|
7
|
-
|
8
|
-
scenario
|
9
|
-
|
10
|
-
|
11
|
-
example[:tableHeader][:cells].map { |cell| cell[:value] }.each do |variable|
|
8
|
+
next unless scenario.is_a? CukeModeler::Outline
|
9
|
+
|
10
|
+
scenario.examples.each do |example|
|
11
|
+
example.rows.first.cells.map(&:value).each do |variable|
|
12
12
|
next if used?(variable, scenario)
|
13
13
|
|
14
14
|
add_issue(I18n.t('linters.unused_variable', variable: variable), feature, scenario, example)
|
@@ -19,11 +19,10 @@ module Chutney
|
|
19
19
|
|
20
20
|
def used?(variable, scenario)
|
21
21
|
variable = "<#{variable}>"
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
next unless step.include? :argument
|
22
|
+
|
23
|
+
scenario.steps.each do |step|
|
24
|
+
return true if step.text.include? variable
|
25
|
+
next unless step.block
|
27
26
|
return true if used_in_docstring?(variable, step)
|
28
27
|
return true if used_in_table?(variable, step)
|
29
28
|
end
|
@@ -31,14 +30,14 @@ module Chutney
|
|
31
30
|
end
|
32
31
|
|
33
32
|
def used_in_docstring?(variable, step)
|
34
|
-
step
|
33
|
+
step.block.is_a?(CukeModeler::DocString) && step.block.content.include?(variable)
|
35
34
|
end
|
36
35
|
|
37
36
|
def used_in_table?(variable, step)
|
38
|
-
return false unless step
|
39
|
-
|
40
|
-
step
|
41
|
-
row
|
37
|
+
return false unless step.block.is_a?(CukeModeler::Table)
|
38
|
+
|
39
|
+
step.block.rows.each do |row|
|
40
|
+
row.cells.each { |cell| return true if cell.value.include?(variable) }
|
42
41
|
end
|
43
42
|
false
|
44
43
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for using background
|
3
5
|
class UseBackground < Linter
|
@@ -13,14 +15,13 @@ module Chutney
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def gather_givens
|
16
|
-
return unless feature.
|
17
|
-
|
18
|
+
return unless feature.children
|
19
|
+
|
18
20
|
has_non_given_step = false
|
19
|
-
|
20
|
-
next unless scenario.
|
21
|
-
|
22
|
-
|
23
|
-
has_non_given_step = true unless given_word?(scenario[:steps].first[:keyword])
|
21
|
+
scenarios do |_feature, scenario|
|
22
|
+
next unless scenario.steps
|
23
|
+
|
24
|
+
has_non_given_step = true unless given_word?(scenario.steps.first.keyword)
|
24
25
|
end
|
25
26
|
return if has_non_given_step
|
26
27
|
|
@@ -29,15 +30,13 @@ module Chutney
|
|
29
30
|
result
|
30
31
|
end
|
31
32
|
|
32
|
-
def expanded_steps
|
33
|
-
|
34
|
-
next unless scenario
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
prototypes
|
39
|
-
prototypes = expand_examples(scenario[:examples], prototypes) if scenario.key? :examples
|
40
|
-
prototypes.each { |prototype| yield prototype }
|
33
|
+
def expanded_steps(&block)
|
34
|
+
scenarios do |_feature, scenario|
|
35
|
+
next unless scenario.steps
|
36
|
+
|
37
|
+
prototypes = [render_step(scenario.steps.first)]
|
38
|
+
prototypes = expand_examples(scenario.examples, prototypes) if scenario.is_a? CukeModeler::Outline
|
39
|
+
prototypes.each(&block)
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
@@ -50,10 +49,12 @@ module Chutney
|
|
50
49
|
|
51
50
|
def expand_outlines(sentence, example)
|
52
51
|
result = []
|
53
|
-
headers = example
|
54
|
-
example
|
52
|
+
headers = example.rows.first.cells.map(&:value)
|
53
|
+
example.rows.each_with_index do |row, idx|
|
54
|
+
next if idx.zero? # skip the header
|
55
|
+
|
55
56
|
modified_sentence = sentence.dup
|
56
|
-
headers.zip(row
|
57
|
+
headers.zip(row.cells.map(&:value)).map do |key, value|
|
57
58
|
modified_sentence.gsub!("<#{key}>", value)
|
58
59
|
end
|
59
60
|
result.push modified_sentence
|