chutney 1.6.3 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +3 -3
- data/README.md +44 -30
- data/Rakefile +10 -24
- data/chutney.gemspec +13 -10
- data/config/{default.yml → chutney.yml} +6 -3
- data/docs/.keep +0 -0
- data/exe/chutney +28 -22
- data/img/chutney.svg +852 -0
- data/img/formatters.png +0 -0
- data/lib/chutney.rb +61 -85
- data/lib/chutney/configuration.rb +6 -7
- data/lib/chutney/formatter.rb +21 -0
- data/lib/chutney/formatter/json_formatter.rb +8 -0
- data/lib/chutney/formatter/pie_formatter.rb +78 -0
- data/lib/chutney/formatter/rainbow_formatter.rb +47 -0
- data/lib/chutney/linter.rb +145 -113
- data/lib/chutney/linter/avoid_full_stop.rb +12 -0
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +3 -6
- data/lib/chutney/linter/avoid_scripting.rb +10 -15
- data/lib/chutney/linter/background_does_more_than_setup.rb +7 -10
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +2 -3
- data/lib/chutney/linter/bad_scenario_name.rb +4 -11
- data/lib/chutney/linter/file_name_differs_feature_name.rb +5 -10
- data/lib/chutney/linter/givens_after_background.rb +17 -0
- data/lib/chutney/linter/invalid_file_name.rb +14 -6
- data/lib/chutney/linter/invalid_step_flow.rb +18 -18
- data/lib/chutney/linter/missing_example_name.rb +13 -11
- data/lib/chutney/linter/missing_feature_description.rb +2 -9
- data/lib/chutney/linter/missing_feature_name.rb +3 -12
- data/lib/chutney/linter/missing_scenario_name.rb +3 -7
- data/lib/chutney/linter/missing_test_action.rb +3 -6
- data/lib/chutney/linter/missing_verification.rb +3 -4
- data/lib/chutney/linter/required_tags_starts_with.rb +20 -5
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +24 -28
- data/lib/chutney/linter/scenario_names_match.rb +6 -8
- data/lib/chutney/linter/tag_used_multiple_times.rb +6 -13
- data/lib/chutney/linter/too_clumsy.rb +4 -4
- data/lib/chutney/linter/too_long_step.rb +8 -18
- data/lib/chutney/linter/too_many_different_tags.rb +13 -34
- data/lib/chutney/linter/too_many_steps.rb +10 -6
- data/lib/chutney/linter/too_many_tags.rb +11 -10
- data/lib/chutney/linter/unique_scenario_names.rb +19 -13
- data/lib/chutney/linter/unknown_variable.rb +5 -6
- data/lib/chutney/linter/unused_variable.rb +6 -7
- data/lib/chutney/linter/use_background.rb +11 -29
- data/lib/chutney/linter/use_outline.rb +21 -15
- data/lib/chutney/version.rb +1 -1
- data/lib/config/locales/en.yml +93 -0
- data/spec/chutney_spec.rb +54 -62
- data/spec/spec_helper.rb +103 -0
- metadata +75 -44
- data/Guardfile +0 -3
- data/lib/chutney/linter/avoid_period.rb +0 -19
- data/lib/chutney/linter/be_declarative.rb +0 -49
- data/lib/chutney/linter/tag_collector.rb +0 -10
- data/lib/chutney/linter/tag_constraint.rb +0 -35
- data/spec/configuration_spec.rb +0 -58
- data/spec/required_tags_starts_with_spec.rb +0 -74
- data/spec/shared_contexts/file_exists.rb +0 -12
- data/spec/shared_contexts/gherkin_linter.rb +0 -14
@@ -0,0 +1,12 @@
|
|
1
|
+
module Chutney
|
2
|
+
# service class to lint for avoiding periods
|
3
|
+
class AvoidFullStop < Linter
|
4
|
+
def lint
|
5
|
+
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
|
+
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,18 +1,15 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for avoiding outline for single example
|
5
3
|
class AvoidOutlineForSingleExample < Linter
|
6
4
|
def lint
|
7
|
-
scenarios do |
|
5
|
+
scenarios do |feature, scenario|
|
8
6
|
next unless scenario[:type] == :ScenarioOutline
|
9
7
|
|
10
8
|
next unless scenario.key? :examples
|
11
9
|
next if scenario[:examples].length > 1
|
12
10
|
next if scenario[:examples].first[:tableBody].length > 1
|
13
|
-
|
14
|
-
|
15
|
-
add_error(references, 'You have a Scenarion Outline with a single example - rewrite to use a Scenario')
|
11
|
+
|
12
|
+
add_issue(I18n.t('linters.avoid_outline_for_single_example'), feature, scenario)
|
16
13
|
end
|
17
14
|
end
|
18
15
|
end
|
@@ -1,24 +1,19 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for avoid scripting
|
5
|
-
class AvoidScripting < Linter
|
6
|
-
MESSAGE = "You have multiple (%d) 'When' actions in your steps - you should only have one".freeze
|
7
|
-
|
3
|
+
class AvoidScripting < Linter
|
8
4
|
def lint
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
references = [reference(file, feature, scenario)]
|
14
|
-
add_error(references, MESSAGE % steps.length)
|
5
|
+
scenarios do |feature, scenario|
|
6
|
+
when_steps = filter_when_steps(scenario[:steps])
|
7
|
+
whens = when_steps.count
|
8
|
+
add_issue(I18n.t('linters.avoid_scripting', count: whens), feature, scenario, when_steps.last) if whens > 1
|
15
9
|
end
|
16
10
|
end
|
17
|
-
|
11
|
+
|
18
12
|
def filter_when_steps(steps)
|
19
|
-
steps
|
20
|
-
|
21
|
-
|
13
|
+
steps
|
14
|
+
.drop_while { |step| !when_word?(step[:keyword]) }
|
15
|
+
.then { |s| s.reverse.drop_while { |step| !then_word?(step[:keyword]) }.reverse }
|
16
|
+
.then { |s| s.reject { |step| then_word?(step[:keyword]) } }
|
22
17
|
end
|
23
18
|
end
|
24
19
|
end
|
@@ -1,19 +1,16 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for background that does more than setup
|
5
|
-
class BackgroundDoesMoreThanSetup < Linter
|
6
|
-
MESSAGE = 'A Feature\'s Background should just contain \'Given\' steps'.freeze
|
7
|
-
|
3
|
+
class BackgroundDoesMoreThanSetup < Linter
|
8
4
|
def lint
|
9
|
-
|
5
|
+
background do |feature, background|
|
10
6
|
next unless background.key? :steps
|
11
7
|
|
12
|
-
invalid_steps = background[:steps].select
|
8
|
+
invalid_steps = background[:steps].select do |step|
|
9
|
+
when_word?(step[:keyword]) || then_word?(step[:keyword])
|
10
|
+
end
|
13
11
|
next if invalid_steps.empty?
|
14
|
-
|
15
|
-
|
16
|
-
add_error(references, MESSAGE)
|
12
|
+
|
13
|
+
add_issue(I18n.t('linters.background_does_more_than_setup'), feature, background)
|
17
14
|
end
|
18
15
|
end
|
19
16
|
end
|
@@ -6,12 +6,11 @@ module Chutney
|
|
6
6
|
MESSAGE = 'Avoid using Background steps for just one scenario'.freeze
|
7
7
|
|
8
8
|
def lint
|
9
|
-
|
9
|
+
background do |feature, background|
|
10
10
|
scenarios = feature[:children].reject { |element| element[:type] == :Background }
|
11
11
|
next if scenarios.length >= 2
|
12
12
|
|
13
|
-
|
14
|
-
add_error(references, MESSAGE)
|
13
|
+
add_issue(I18n.t('linters.background_requires_multiple_scenarios'), feature, background)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
@@ -1,20 +1,13 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for bad scenario names
|
5
3
|
class BadScenarioName < Linter
|
6
|
-
MESSAGE = 'You should avoid using words like \'test\', \'check\' or \'verify\' ' \
|
7
|
-
'when naming your scenarios to keep them understandable'.freeze
|
8
|
-
|
9
4
|
def lint
|
10
|
-
scenarios do |
|
5
|
+
scenarios do |feature, scenario|
|
11
6
|
next if scenario[:name].empty?
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
add_error(references, MESSAGE) if scenario[:name].downcase.include? bad_word
|
17
|
-
end
|
8
|
+
bad = /\w*(test|verif|check)\w*/i
|
9
|
+
match = scenario[:name].match(bad).to_a.first
|
10
|
+
add_issue(I18n.t('linters.bad_scenario_name', word: match), feature, scenario) if match
|
18
11
|
end
|
19
12
|
end
|
20
13
|
end
|
@@ -1,22 +1,17 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for file name differs feature name
|
5
3
|
class FileNameDiffersFeatureName < Linter
|
6
4
|
def lint
|
7
|
-
|
8
|
-
next unless feature.include? :name
|
5
|
+
return unless feature.include? :name
|
9
6
|
|
10
|
-
|
11
|
-
|
7
|
+
expected_feature_name = title_case(filename)
|
8
|
+
return if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
|
12
9
|
|
13
|
-
|
14
|
-
add_error(references, "Feature name should be '#{expected_feature_name}'")
|
15
|
-
end
|
10
|
+
add_issue(I18n.t('linters.file_name_differs_feature_name', expected: expected_feature_name), feature)
|
16
11
|
end
|
17
12
|
|
18
13
|
def title_case(value)
|
19
|
-
value = File.basename(value, '
|
14
|
+
value = File.basename(value, '.*')
|
20
15
|
value.split('_').collect(&:capitalize).join(' ')
|
21
16
|
end
|
22
17
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Chutney
|
2
|
+
# service class to lint for bad scenario names
|
3
|
+
class GivensAfterBackground < Linter
|
4
|
+
def lint
|
5
|
+
return if background.nil?
|
6
|
+
return if background.empty?
|
7
|
+
|
8
|
+
filled_scenarios do |feature, scenario|
|
9
|
+
scenario[:steps].each do |step|
|
10
|
+
if given_word?(step[:keyword])
|
11
|
+
add_issue(I18n.t('linters.givens_after_background'), feature, scenario, step)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -4,13 +4,21 @@ module Chutney
|
|
4
4
|
# service class to lint for invalid file names
|
5
5
|
class InvalidFileName < Linter
|
6
6
|
def lint
|
7
|
-
|
8
|
-
base = File.basename
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
add_error(references, 'Feature files should be snake_cased')
|
7
|
+
feature do |f|
|
8
|
+
base = File.basename(filename, '.*')
|
9
|
+
if base != base.downcase || base =~ /[ -]/
|
10
|
+
add_issue(I18n.t('linters.invalid_file_name', recommended_name: recommend(filename)), f)
|
11
|
+
end
|
13
12
|
end
|
14
13
|
end
|
14
|
+
|
15
|
+
def recommend(filename)
|
16
|
+
File.basename(filename, '.*').gsub(/::/, '/')
|
17
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
18
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
19
|
+
.tr('-', '_')
|
20
|
+
.tr(' ', '_')
|
21
|
+
.downcase << '.feature'
|
22
|
+
end
|
15
23
|
end
|
16
24
|
end
|
@@ -1,40 +1,40 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for invalid step flow
|
5
3
|
class InvalidStepFlow < Linter
|
6
4
|
def lint
|
7
|
-
filled_scenarios do |
|
8
|
-
steps = scenario[:steps].select { |step| step[:keyword]
|
5
|
+
filled_scenarios do |feature, scenario|
|
6
|
+
steps = scenario[:steps].select { |step| !and_word?(step[:keyword]) && !but_word?(step[:keyword]) }
|
9
7
|
next if steps.empty?
|
10
8
|
|
11
|
-
last_step_is_an_action(
|
12
|
-
given_after_non_given(
|
13
|
-
verification_before_action(
|
9
|
+
last_step_is_an_action(feature, scenario, steps)
|
10
|
+
given_after_non_given(feature, scenario, steps)
|
11
|
+
verification_before_action(feature, scenario, steps)
|
14
12
|
end
|
15
13
|
end
|
16
14
|
|
17
|
-
def last_step_is_an_action(
|
18
|
-
|
19
|
-
|
15
|
+
def last_step_is_an_action(feature, scenario, steps)
|
16
|
+
return unless when_word?(steps.last[:keyword])
|
17
|
+
|
18
|
+
add_issue(I18n.t('linters.invalid_step_flow.action_last'), feature, scenario, steps.last)
|
20
19
|
end
|
21
20
|
|
22
|
-
def given_after_non_given(
|
21
|
+
def given_after_non_given(feature, scenario, steps)
|
23
22
|
last_step = steps.first
|
24
23
|
steps.each do |step|
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
if given_word?(step[:keyword]) && !given_word?(last_step[:keyword])
|
25
|
+
add_issue(I18n.t('linters.invalid_step_flow.given_order'), feature, scenario, step)
|
26
|
+
end
|
28
27
|
last_step = step
|
29
28
|
end
|
30
29
|
end
|
31
30
|
|
32
|
-
def verification_before_action(
|
31
|
+
def verification_before_action(feature, scenario, steps)
|
33
32
|
steps.each do |step|
|
34
|
-
break if step[:keyword]
|
33
|
+
break if when_word?(step[:keyword])
|
35
34
|
|
36
|
-
|
37
|
-
|
35
|
+
if then_word?(step[:keyword])
|
36
|
+
add_issue(I18n.t('linters.invalid_step_flow.missing_action'), feature, scenario)
|
37
|
+
end
|
38
38
|
end
|
39
39
|
end
|
40
40
|
end
|
@@ -1,23 +1,25 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for missing example names
|
5
3
|
class MissingExampleName < Linter
|
6
|
-
MESSAGE = 'You have an unnamed or ambiguously named example'.freeze
|
7
4
|
|
8
5
|
def lint
|
9
|
-
scenarios do |
|
10
|
-
next unless scenario
|
11
|
-
|
12
|
-
|
6
|
+
scenarios do |_feature, scenario|
|
7
|
+
next unless scenario[:examples]
|
8
|
+
|
13
9
|
scenario[:examples].each do |example|
|
14
|
-
|
15
|
-
next unless
|
10
|
+
example_count = scenario[:examples]&.length || 0
|
11
|
+
next unless example_count > 1
|
16
12
|
|
17
|
-
|
18
|
-
add_error(references, MESSAGE)
|
13
|
+
check_example(scenario, example)
|
19
14
|
end
|
20
15
|
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_example(scenario, example)
|
19
|
+
name = example.key?(:name) ? example[:name].strip : ''
|
20
|
+
duplicate_name_count = scenario[:examples].filter { |e| e[:name] == name }.count
|
21
|
+
add_issue(I18n.t('linters.missing_example_name'), feature, scenario, example) if duplicate_name_count >= 2
|
21
22
|
end
|
23
|
+
|
22
24
|
end
|
23
25
|
end
|
@@ -1,17 +1,10 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for missing feature descriptions
|
5
3
|
class MissingFeatureDescription < Linter
|
6
4
|
MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
|
7
5
|
def lint
|
8
|
-
|
9
|
-
|
10
|
-
next unless name.empty?
|
11
|
-
|
12
|
-
references = [reference(file, feature)]
|
13
|
-
add_error(references, MESSAGE)
|
14
|
-
end
|
6
|
+
name = feature.key?(:description) ? feature[:description].strip : ''
|
7
|
+
add_issue(I18n.t('linters.missing_feature_description'), feature) if name.empty?
|
15
8
|
end
|
16
9
|
end
|
17
10
|
end
|
@@ -1,18 +1,9 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for missing feature names
|
5
|
-
class MissingFeatureName < Linter
|
6
|
-
MESSAGE = 'All features should have a name'.freeze
|
7
|
-
|
3
|
+
class MissingFeatureName < Linter
|
8
4
|
def lint
|
9
|
-
|
10
|
-
|
11
|
-
next unless name.empty?
|
12
|
-
|
13
|
-
references = [reference(file, feature)]
|
14
|
-
add_error(references, MESSAGE)
|
15
|
-
end
|
5
|
+
name = feature.key?(:name) ? feature[:name].strip : ''
|
6
|
+
add_issue(I18n.t('linters.missing_feature_name'), feature) if name.empty?
|
16
7
|
end
|
17
8
|
end
|
18
9
|
end
|
@@ -1,17 +1,13 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for missing scenario names
|
5
|
-
class MissingScenarioName < Linter
|
6
|
-
MESSAGE = 'All scenarios should have a name'.freeze
|
3
|
+
class MissingScenarioName < Linter
|
7
4
|
|
8
5
|
def lint
|
9
|
-
scenarios do |
|
6
|
+
scenarios do |feature, scenario|
|
10
7
|
name = scenario.key?(:name) ? scenario[:name].strip : ''
|
11
|
-
references = [reference(file, feature, scenario)]
|
12
8
|
next unless name.empty?
|
13
9
|
|
14
|
-
|
10
|
+
add_issue(I18n.t('linters.missing_scenario_name'), feature, scenario) if name.empty?
|
15
11
|
end
|
16
12
|
end
|
17
13
|
end
|
@@ -1,15 +1,12 @@
|
|
1
|
-
require 'chutney/linter'
|
2
|
-
|
3
1
|
module Chutney
|
4
2
|
# service class to lint for missing test actions
|
5
3
|
class MissingTestAction < Linter
|
6
4
|
def lint
|
7
|
-
filled_scenarios do |
|
8
|
-
when_steps = scenario[:steps].select { |step| step[:keyword]
|
5
|
+
filled_scenarios do |feature, scenario|
|
6
|
+
when_steps = scenario[:steps].select { |step| when_word?(step[:keyword]) }
|
9
7
|
next unless when_steps.empty?
|
10
8
|
|
11
|
-
|
12
|
-
add_error(references, 'No \'When\'-Step')
|
9
|
+
add_issue(I18n.t('linters.missing_test_action'), feature, scenario)
|
13
10
|
end
|
14
11
|
end
|
15
12
|
end
|
@@ -4,12 +4,11 @@ module Chutney
|
|
4
4
|
# service class to lint for missing verifications
|
5
5
|
class MissingVerification < Linter
|
6
6
|
def lint
|
7
|
-
filled_scenarios do |
|
8
|
-
then_steps = scenario[:steps].select { |step| step[:keyword]
|
7
|
+
filled_scenarios do |feature, scenario|
|
8
|
+
then_steps = scenario[:steps].select { |step| then_word?(step[:keyword]) }
|
9
9
|
next unless then_steps.empty?
|
10
10
|
|
11
|
-
|
12
|
-
add_error(references, 'No \'Then\' step')
|
11
|
+
add_issue(I18n.t('linters.missing_test_verification'), feature, scenario)
|
13
12
|
end
|
14
13
|
end
|
15
14
|
end
|
@@ -1,14 +1,29 @@
|
|
1
|
-
require 'chutney/linter/tag_constraint'
|
2
|
-
require 'chutney/linter'
|
3
|
-
|
4
1
|
module Chutney
|
5
2
|
# service class to lint for tags used multiple times
|
6
3
|
class RequiredTagsStartsWith < Linter
|
7
|
-
|
4
|
+
|
5
|
+
def lint
|
6
|
+
return unless pattern
|
8
7
|
|
8
|
+
scenarios do |feature, scenario|
|
9
|
+
next if match_pattern? tags_for(feature)
|
10
|
+
next if match_pattern? tags_for(scenario)
|
11
|
+
|
12
|
+
add_issue(
|
13
|
+
I18n.t('linters.required_tags_starts_with',
|
14
|
+
allowed: pattern.join(', ')),
|
15
|
+
feature, scenario
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pattern
|
21
|
+
configuration['Matcher'] || nil
|
22
|
+
end
|
23
|
+
|
9
24
|
def match_pattern?(target)
|
10
25
|
target.each do |t|
|
11
|
-
return true if t.
|
26
|
+
return true if t.start_with?(*pattern)
|
12
27
|
end
|
13
28
|
false
|
14
29
|
end
|