chutney 1.6.3 → 2.0.0.rc1
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/.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
|