chutney 2.1.1 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -2
- data/Gemfile +2 -0
- data/README.md +5 -1
- data/Rakefile +2 -0
- data/chutney.gemspec +13 -6
- data/config/{chutney.yml → chutney_defaults.yml} +2 -0
- data/config/cucumber.yml +1 -0
- data/docs/usage/rules.md +3 -0
- data/exe/chutney +23 -3
- data/lib/chutney.rb +26 -22
- data/lib/chutney/configuration.rb +9 -2
- data/lib/chutney/formatter.rb +6 -5
- data/lib/chutney/formatter/json_formatter.rb +2 -0
- data/lib/chutney/formatter/pie_formatter.rb +10 -13
- data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
- data/lib/chutney/issue.rb +2 -0
- data/lib/chutney/linter.rb +95 -84
- data/lib/chutney/linter/avoid_full_stop.rb +4 -4
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
- data/lib/chutney/linter/avoid_scripting.rb +8 -6
- data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
- data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
- data/lib/chutney/linter/bad_scenario_name.rb +6 -4
- data/lib/chutney/linter/empty_feature_file.rb +10 -0
- data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
- data/lib/chutney/linter/givens_after_background.rb +7 -8
- data/lib/chutney/linter/invalid_file_name.rb +3 -1
- data/lib/chutney/linter/invalid_step_flow.rb +9 -9
- data/lib/chutney/linter/missing_example_name.rb +9 -9
- data/lib/chutney/linter/missing_feature_description.rb +5 -4
- data/lib/chutney/linter/missing_feature_name.rb +5 -4
- data/lib/chutney/linter/missing_scenario_name.rb +4 -6
- data/lib/chutney/linter/missing_test_action.rb +4 -2
- data/lib/chutney/linter/missing_verification.rb +4 -2
- data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
- data/lib/chutney/linter/scenario_names_match.rb +6 -6
- data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
- data/lib/chutney/linter/too_clumsy.rb +4 -2
- data/lib/chutney/linter/too_long_step.rb +6 -4
- data/lib/chutney/linter/too_many_different_tags.rb +10 -8
- data/lib/chutney/linter/too_many_steps.rb +6 -4
- data/lib/chutney/linter/too_many_tags.rb +5 -3
- data/lib/chutney/linter/unique_scenario_names.rb +5 -5
- data/lib/chutney/linter/unknown_variable.rb +15 -15
- data/lib/chutney/linter/unused_variable.rb +15 -16
- data/lib/chutney/linter/use_background.rb +20 -19
- data/lib/chutney/linter/use_outline.rb +15 -14
- data/lib/chutney/version.rb +3 -1
- data/lib/config/locales/en.yml +2 -0
- data/spec/chutney_spec.rb +11 -9
- data/spec/spec_helper.rb +2 -0
- metadata +19 -15
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
3
5
|
class BadScenarioName < Linter
|
4
6
|
def lint
|
5
7
|
scenarios do |feature, scenario|
|
6
|
-
next if scenario
|
7
|
-
|
8
|
+
next if scenario.name.empty?
|
9
|
+
|
8
10
|
bad = /\w*(test|verif|check)\w*/i
|
9
|
-
match = scenario
|
11
|
+
match = scenario.name.match(bad).to_a.first
|
10
12
|
add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
|
11
13
|
end
|
12
14
|
end
|
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for file name differs feature name
|
3
5
|
class FileNameDiffersFeatureName < Linter
|
4
6
|
def lint
|
5
|
-
return unless feature
|
6
|
-
|
7
|
+
return unless feature
|
8
|
+
|
7
9
|
expected_feature_name = title_case(filename)
|
8
|
-
return if ignore_whitespaces(feature
|
9
|
-
|
10
|
-
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
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
13
|
end
|
12
14
|
|
13
15
|
def title_case(value)
|
@@ -1,15 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
|
-
# service class to lint for bad scenario names
|
4
|
+
# service class to lint for bad scenario names
|
3
5
|
class GivensAfterBackground < Linter
|
4
6
|
def lint
|
5
|
-
return
|
6
|
-
|
7
|
-
|
7
|
+
return unless background
|
8
|
+
|
8
9
|
filled_scenarios do |feature, scenario|
|
9
|
-
scenario
|
10
|
-
if given_word?(step
|
11
|
-
add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step)
|
12
|
-
end
|
10
|
+
scenario.steps.each do |step|
|
11
|
+
add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step) if given_word?(step.keyword)
|
13
12
|
end
|
14
13
|
end
|
15
14
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'chutney/linter'
|
2
4
|
|
3
5
|
module Chutney
|
@@ -11,7 +13,7 @@ module Chutney
|
|
11
13
|
end
|
12
14
|
end
|
13
15
|
end
|
14
|
-
|
16
|
+
|
15
17
|
def recommend(filename)
|
16
18
|
File.basename(filename, '.*').gsub(/::/, '/')
|
17
19
|
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for invalid step flow
|
3
5
|
class InvalidStepFlow < Linter
|
4
6
|
def lint
|
5
7
|
filled_scenarios do |feature, scenario|
|
6
|
-
steps = scenario
|
8
|
+
steps = scenario.steps.select { |step| !and_word?(step.keyword) && !but_word?(step.keyword) }
|
7
9
|
next if steps.empty?
|
8
|
-
|
10
|
+
|
9
11
|
last_step_is_an_action(feature, scenario, steps)
|
10
12
|
given_after_non_given(feature, scenario, steps)
|
11
13
|
verification_before_action(feature, scenario, steps)
|
@@ -13,7 +15,7 @@ module Chutney
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def last_step_is_an_action(feature, scenario, steps)
|
16
|
-
return unless when_word?(steps.last
|
18
|
+
return unless when_word?(steps.last.keyword)
|
17
19
|
|
18
20
|
add_issue(I18n.t('linters.invalid_step_flow.action_last'), feature, scenario, steps.last)
|
19
21
|
end
|
@@ -21,7 +23,7 @@ module Chutney
|
|
21
23
|
def given_after_non_given(feature, scenario, steps)
|
22
24
|
last_step = steps.first
|
23
25
|
steps.each do |step|
|
24
|
-
if given_word?(step
|
26
|
+
if given_word?(step.keyword) && !given_word?(last_step.keyword)
|
25
27
|
add_issue(I18n.t('linters.invalid_step_flow.given_order'), feature, scenario, step)
|
26
28
|
end
|
27
29
|
last_step = step
|
@@ -30,11 +32,9 @@ module Chutney
|
|
30
32
|
|
31
33
|
def verification_before_action(feature, scenario, steps)
|
32
34
|
steps.each do |step|
|
33
|
-
break if when_word?(step
|
34
|
-
|
35
|
-
if then_word?(step
|
36
|
-
add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
|
37
|
-
end
|
35
|
+
break if when_word?(step.keyword)
|
36
|
+
|
37
|
+
add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario) if then_word?(step.keyword)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -1,25 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for missing example names
|
3
5
|
class MissingExampleName < Linter
|
4
|
-
|
5
6
|
def lint
|
6
7
|
scenarios do |_feature, scenario|
|
7
|
-
next unless scenario
|
8
|
+
next unless scenario.is_a? CukeModeler::Outline
|
8
9
|
|
9
|
-
scenario
|
10
|
-
example_count = scenario
|
10
|
+
scenario.examples.each do |example|
|
11
|
+
example_count = scenario.examples&.length || 0
|
11
12
|
next unless example_count > 1
|
12
|
-
|
13
|
+
|
13
14
|
check_example(scenario, example)
|
14
15
|
end
|
15
16
|
end
|
16
|
-
end
|
17
|
+
end
|
17
18
|
|
18
19
|
def check_example(scenario, example)
|
19
|
-
name = example.
|
20
|
-
duplicate_name_count = scenario
|
20
|
+
name = example.name.strip
|
21
|
+
duplicate_name_count = scenario.examples.filter { |e| e.name == name }.count
|
21
22
|
add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
|
22
23
|
end
|
23
|
-
|
24
24
|
end
|
25
25
|
end
|
@@ -1,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for missing feature descriptions
|
3
5
|
class MissingFeatureDescription < Linter
|
4
|
-
MESSAGE = 'Features should have a description so that its purpose is clear'
|
6
|
+
MESSAGE = 'Features should have a description so that its purpose is clear'
|
5
7
|
def lint
|
6
8
|
return unless feature
|
7
|
-
|
8
|
-
|
9
|
-
add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
|
9
|
+
|
10
|
+
add_issue(I18n.t('linters.missing_feature_description'), feature) if feature.description.empty?
|
10
11
|
end
|
11
12
|
end
|
12
13
|
end
|
@@ -1,11 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for missing feature names
|
3
|
-
class MissingFeatureName < Linter
|
5
|
+
class MissingFeatureName < Linter
|
4
6
|
def lint
|
5
7
|
return unless feature
|
6
|
-
|
7
|
-
|
8
|
-
add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
|
8
|
+
|
9
|
+
add_issue(I18n.t('linters.missing_feature_name'), feature) if feature.name.empty?
|
9
10
|
end
|
10
11
|
end
|
11
12
|
end
|
@@ -1,13 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for missing scenario names
|
3
|
-
class MissingScenarioName < Linter
|
4
|
-
|
5
|
+
class MissingScenarioName < Linter
|
5
6
|
def lint
|
6
7
|
scenarios do |feature, scenario|
|
7
|
-
|
8
|
-
next unless name.empty?
|
9
|
-
|
10
|
-
add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if name.empty?
|
8
|
+
add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if scenario.name.empty?
|
11
9
|
end
|
12
10
|
end
|
13
11
|
end
|
@@ -1,11 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for missing test actions
|
3
5
|
class MissingTestAction < Linter
|
4
6
|
def lint
|
5
7
|
filled_scenarios do |feature, scenario|
|
6
|
-
when_steps = scenario
|
8
|
+
when_steps = scenario.steps.select { |step| when_word?(step.keyword) }
|
7
9
|
next unless when_steps.empty?
|
8
|
-
|
10
|
+
|
9
11
|
add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
|
10
12
|
end
|
11
13
|
end
|
@@ -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 MissingVerification < Linter
|
6
8
|
def lint
|
7
9
|
filled_scenarios do |feature, scenario|
|
8
|
-
then_steps = scenario
|
10
|
+
then_steps = scenario.steps.select { |step| then_word?(step.keyword) }
|
9
11
|
next unless then_steps.empty?
|
10
|
-
|
12
|
+
|
11
13
|
add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
|
12
14
|
end
|
13
15
|
end
|
@@ -1,26 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for tags used multiple times
|
3
5
|
class RequiredTagsStartsWith < Linter
|
4
|
-
|
5
6
|
def lint
|
6
7
|
return unless pattern
|
7
8
|
|
8
9
|
scenarios do |feature, scenario|
|
9
10
|
next if match_pattern? tags_for(feature)
|
10
11
|
next if match_pattern? tags_for(scenario)
|
11
|
-
|
12
|
+
|
12
13
|
add_issue(
|
13
|
-
I18n.t('linters.required_tags_starts_with',
|
14
|
-
allowed: pattern.join(', ')),
|
14
|
+
I18n.t('linters.required_tags_starts_with',
|
15
|
+
allowed: pattern.join(', ')),
|
15
16
|
feature, scenario
|
16
17
|
)
|
17
18
|
end
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
def pattern
|
21
22
|
configuration['Matcher'] || nil
|
22
23
|
end
|
23
|
-
|
24
|
+
|
24
25
|
def match_pattern?(target)
|
25
26
|
target.each do |t|
|
26
27
|
return true if t.start_with?(*pattern)
|
@@ -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
|