chutney 2.1.1 → 3.0.1
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/.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
|