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
@@ -1,11 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for avoiding periods
|
3
|
-
class AvoidFullStop < Linter
|
5
|
+
class AvoidFullStop < Linter
|
4
6
|
def lint
|
5
7
|
steps do |feature, child, step|
|
6
|
-
|
7
|
-
add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step[:text].strip.end_with? '.'
|
8
|
-
|
8
|
+
add_issue(I18n.t('linters.avoid_full_stop'), feature, child, step) if step.text.strip.end_with? '.'
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for avoiding outline for single example
|
3
5
|
class AvoidOutlineForSingleExample < Linter
|
4
6
|
def lint
|
5
7
|
scenarios do |feature, scenario|
|
6
|
-
next unless scenario
|
8
|
+
next unless scenario.is_a? CukeModeler::Outline
|
9
|
+
next unless scenario.examples
|
10
|
+
|
11
|
+
next if scenario.examples.length > 1
|
12
|
+
next if scenario.examples.first.rows.length > 2 # first row is the header
|
7
13
|
|
8
|
-
next unless scenario.key? :examples
|
9
|
-
next if scenario[:examples].length > 1
|
10
|
-
next if scenario[:examples].first[:tableBody].length > 1
|
11
|
-
|
12
14
|
add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
|
13
15
|
end
|
14
16
|
end
|
@@ -1,19 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for avoid scripting
|
3
|
-
class AvoidScripting < Linter
|
5
|
+
class AvoidScripting < Linter
|
4
6
|
def lint
|
5
7
|
scenarios do |feature, scenario|
|
6
|
-
when_steps = filter_when_steps(scenario
|
8
|
+
when_steps = filter_when_steps(scenario.steps)
|
7
9
|
whens = when_steps.count
|
8
10
|
add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
|
9
11
|
end
|
10
12
|
end
|
11
|
-
|
13
|
+
|
12
14
|
def filter_when_steps(steps)
|
13
15
|
steps
|
14
|
-
.drop_while { |step| !when_word?(step
|
15
|
-
.then { |s| s.reverse.drop_while { |step| !then_word?(step
|
16
|
-
.then { |s| s.reject { |step| then_word?(step
|
16
|
+
.drop_while { |step| !when_word?(step.keyword) }
|
17
|
+
.then { |s| s.reverse.drop_while { |step| !then_word?(step.keyword) }.reverse }
|
18
|
+
.then { |s| s.reject { |step| then_word?(step.keyword) } }
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
@@ -1,37 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for avoid scripting
|
3
5
|
class AvoidTypographersQuotes < Linter
|
4
6
|
TYPOGRAPHER_QUOTES = ["\u201c", "\u201d", "\u2018", "\u2019"].map(&:encode)
|
5
7
|
|
6
8
|
def lint
|
7
|
-
scenarios do |
|
8
|
-
lint_steps(scenario
|
9
|
+
scenarios do |feature, scenario|
|
10
|
+
lint_steps(feature, scenario)
|
9
11
|
|
10
|
-
example_count = scenario
|
12
|
+
example_count = scenario.is_a?(CukeModeler::Outline) ? scenario.examples.length : 0
|
11
13
|
next unless example_count.positive?
|
12
14
|
|
13
|
-
lint_examples(scenario
|
15
|
+
lint_examples(feature, scenario)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
|
-
def lint_steps(
|
18
|
-
steps.each do |step|
|
19
|
-
issue(step) if TYPOGRAPHER_QUOTES.any? { |tq| step
|
19
|
+
def lint_steps(feature, scenario)
|
20
|
+
scenario.steps.each do |step|
|
21
|
+
issue(feature, scenario, step) if TYPOGRAPHER_QUOTES.any? { |tq| step.text.include? tq }
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
|
-
def lint_examples(
|
24
|
-
examples.each do |example|
|
25
|
-
example
|
26
|
-
|
27
|
-
issue(cell) if TYPOGRAPHER_QUOTES.any? { |tq| cell
|
25
|
+
def lint_examples(feature, scenario)
|
26
|
+
scenario.examples.each do |example|
|
27
|
+
example.rows.each do |row|
|
28
|
+
row.cells.each do |cell|
|
29
|
+
issue(feature, scenario, cell) if TYPOGRAPHER_QUOTES.any? { |tq| cell.value.include? tq }
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
33
|
-
def issue(location)
|
34
|
-
add_issue(I18n.t('linters.avoid_typographers_quotes'), location)
|
35
|
+
def issue(feature, scenario, location)
|
36
|
+
add_issue(I18n.t('linters.avoid_typographers_quotes'), feature, scenario, location)
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
@@ -1,16 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# service class to lint for background that does more than setup
|
3
|
-
class BackgroundDoesMoreThanSetup < Linter
|
5
|
+
class BackgroundDoesMoreThanSetup < Linter
|
4
6
|
def lint
|
5
7
|
background do |feature, background|
|
6
|
-
|
7
|
-
|
8
|
-
invalid_steps = background[:steps].select do |step|
|
9
|
-
when_word?(step[:keyword]) || then_word?(step[:keyword])
|
8
|
+
invalid_steps = background&.steps&.select do |step|
|
9
|
+
when_word?(step.keyword) || then_word?(step.keyword)
|
10
10
|
end
|
11
|
-
next if invalid_steps.empty?
|
12
11
|
|
13
|
-
|
12
|
+
next if invalid_steps.nil? || invalid_steps.empty?
|
13
|
+
|
14
|
+
add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background, invalid_steps.first)
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
@@ -1,14 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'chutney/linter'
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# service class for check that there are multiple scenarios once a background is used
|
5
7
|
class BackgroundRequiresMultipleScenarios < Linter
|
6
|
-
MESSAGE = 'Avoid using Background steps for just one scenario'
|
7
|
-
|
8
|
+
MESSAGE = 'Avoid using Background steps for just one scenario'
|
9
|
+
|
8
10
|
def lint
|
9
11
|
background do |feature, background|
|
10
|
-
|
11
|
-
next
|
12
|
+
next unless background
|
13
|
+
next unless feature&.tests
|
14
|
+
next if feature.tests.length >= 2
|
12
15
|
|
13
16
|
add_issue(I18n.t('linters.background_requires_multiple_scenarios'), feature, background)
|
14
17
|
end
|
@@ -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)
|