chutney 2.2.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|