chutney 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +14 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +55 -0
- data/Dockerfile +9 -0
- data/Gemfile +3 -0
- data/Guardfile +3 -0
- data/LICENSE +22 -0
- data/README.md +84 -0
- data/Rakefile +51 -0
- data/chutney.gemspec +54 -0
- data/config/default.yml +58 -0
- data/exe/chutney +35 -0
- data/lib/chutney/.DS_Store +0 -0
- data/lib/chutney/configuration.rb +32 -0
- data/lib/chutney/issue.rb +35 -0
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +19 -0
- data/lib/chutney/linter/avoid_period.rb +19 -0
- data/lib/chutney/linter/avoid_scripting.rb +24 -0
- data/lib/chutney/linter/background_does_more_than_setup.rb +20 -0
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +18 -0
- data/lib/chutney/linter/bad_scenario_name.rb +21 -0
- data/lib/chutney/linter/be_declarative.rb +49 -0
- data/lib/chutney/linter/file_name_differs_feature_name.rb +27 -0
- data/lib/chutney/linter/invalid_file_name.rb +16 -0
- data/lib/chutney/linter/invalid_step_flow.rb +41 -0
- data/lib/chutney/linter/missing_example_name.rb +23 -0
- data/lib/chutney/linter/missing_feature_description.rb +17 -0
- data/lib/chutney/linter/missing_feature_name.rb +18 -0
- data/lib/chutney/linter/missing_scenario_name.rb +18 -0
- data/lib/chutney/linter/missing_test_action.rb +16 -0
- data/lib/chutney/linter/missing_verification.rb +16 -0
- data/lib/chutney/linter/required_tags_starts_with.rb +16 -0
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +73 -0
- data/lib/chutney/linter/tag_collector.rb +10 -0
- data/lib/chutney/linter/tag_constraint.rb +35 -0
- data/lib/chutney/linter/tag_used_multiple_times.rb +23 -0
- data/lib/chutney/linter/too_clumsy.rb +17 -0
- data/lib/chutney/linter/too_long_step.rb +17 -0
- data/lib/chutney/linter/too_many_different_tags.rb +45 -0
- data/lib/chutney/linter/too_many_steps.rb +15 -0
- data/lib/chutney/linter/too_many_tags.rb +19 -0
- data/lib/chutney/linter/unique_scenario_names.rb +22 -0
- data/lib/chutney/linter/unknown_variable.rb +47 -0
- data/lib/chutney/linter/unused_variable.rb +47 -0
- data/lib/chutney/linter/use_background.rb +82 -0
- data/lib/chutney/linter/use_outline.rb +53 -0
- data/lib/chutney/linter.rb +164 -0
- data/lib/chutney/version.rb +3 -0
- data/lib/chutney.rb +131 -0
- data/spec/chutney_spec.rb +68 -0
- data/spec/configuration_spec.rb +58 -0
- data/spec/required_tags_starts_with_spec.rb +74 -0
- data/spec/shared_contexts/file_exists.rb +12 -0
- data/spec/shared_contexts/gherkin_linter.rb +14 -0
- metadata +201 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for file name differs feature name
|
5
|
+
class FileNameDiffersFeatureName < Linter
|
6
|
+
def lint
|
7
|
+
features do |file, feature|
|
8
|
+
next unless feature.include? :name
|
9
|
+
|
10
|
+
expected_feature_name = title_case file
|
11
|
+
next if ignore_whitespaces(feature[:name]).casecmp(ignore_whitespaces(expected_feature_name)) == 0
|
12
|
+
|
13
|
+
references = [reference(file, feature)]
|
14
|
+
add_error(references, "Feature name should be '#{expected_feature_name}'")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def title_case(value)
|
19
|
+
value = File.basename(value, '.feature')
|
20
|
+
value.split('_').collect(&:capitalize).join(' ')
|
21
|
+
end
|
22
|
+
|
23
|
+
def ignore_whitespaces(value)
|
24
|
+
value.delete('-').delete('_').delete(' ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for invalid file names
|
5
|
+
class InvalidFileName < Linter
|
6
|
+
def lint
|
7
|
+
files do |file|
|
8
|
+
base = File.basename file
|
9
|
+
next unless base != base.downcase || base =~ /[ -]/
|
10
|
+
|
11
|
+
references = [reference(file)]
|
12
|
+
add_error(references, 'Feature files should be snake_cased')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for invalid step flow
|
5
|
+
class InvalidStepFlow < Linter
|
6
|
+
def lint
|
7
|
+
filled_scenarios do |file, feature, scenario|
|
8
|
+
steps = scenario[:steps].select { |step| step[:keyword] != 'And ' && step[:keyword] != 'But ' }
|
9
|
+
next if steps.empty?
|
10
|
+
|
11
|
+
last_step_is_an_action(file, feature, scenario, steps)
|
12
|
+
given_after_non_given(file, feature, scenario, steps)
|
13
|
+
verification_before_action(file, feature, scenario, steps)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def last_step_is_an_action(file, feature, scenario, steps)
|
18
|
+
references = [reference(file, feature, scenario, steps.last)]
|
19
|
+
add_error(references, 'Last step is an action') if steps.last[:keyword] == 'When '
|
20
|
+
end
|
21
|
+
|
22
|
+
def given_after_non_given(file, feature, scenario, steps)
|
23
|
+
last_step = steps.first
|
24
|
+
steps.each do |step|
|
25
|
+
references = [reference(file, feature, scenario, step)]
|
26
|
+
description = 'Given after Action or Verification'
|
27
|
+
add_error(references, description) if step[:keyword] == 'Given ' && last_step[:keyword] != 'Given '
|
28
|
+
last_step = step
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def verification_before_action(file, feature, scenario, steps)
|
33
|
+
steps.each do |step|
|
34
|
+
break if step[:keyword] == 'When '
|
35
|
+
|
36
|
+
references = [reference(file, feature, scenario, step)]
|
37
|
+
add_error(references, 'Missing Action step') if step[:keyword] == 'Then '
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing example names
|
5
|
+
class MissingExampleName < Linter
|
6
|
+
MESSAGE = 'You have an unnamed or ambiguously named example'.freeze
|
7
|
+
|
8
|
+
def lint
|
9
|
+
scenarios do |file, feature, scenario|
|
10
|
+
next unless scenario.key? :examples
|
11
|
+
next unless scenario[:examples].length > 1
|
12
|
+
|
13
|
+
scenario[:examples].each do |example|
|
14
|
+
name = example.key?(:name) ? example[:name].strip : ''
|
15
|
+
next unless name.empty?
|
16
|
+
|
17
|
+
references = [reference(file, feature, scenario)]
|
18
|
+
add_error(references, MESSAGE)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing feature descriptions
|
5
|
+
class MissingFeatureDescription < Linter
|
6
|
+
MESSAGE = 'Features should have a description so that its purpose is clear'.freeze
|
7
|
+
def lint
|
8
|
+
features do |file, feature|
|
9
|
+
name = feature.key?(:description) ? feature[:description].strip : ''
|
10
|
+
next unless name.empty?
|
11
|
+
|
12
|
+
references = [reference(file, feature)]
|
13
|
+
add_error(references, MESSAGE)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing feature names
|
5
|
+
class MissingFeatureName < Linter
|
6
|
+
MESSAGE = 'All features should have a name'.freeze
|
7
|
+
|
8
|
+
def lint
|
9
|
+
features do |file, feature|
|
10
|
+
name = feature.key?(:name) ? feature[:name].strip : ''
|
11
|
+
next unless name.empty?
|
12
|
+
|
13
|
+
references = [reference(file, feature)]
|
14
|
+
add_error(references, MESSAGE)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing scenario names
|
5
|
+
class MissingScenarioName < Linter
|
6
|
+
MESSAGE = 'All scenarios should have a name'.freeze
|
7
|
+
|
8
|
+
def lint
|
9
|
+
scenarios do |file, feature, scenario|
|
10
|
+
name = scenario.key?(:name) ? scenario[:name].strip : ''
|
11
|
+
references = [reference(file, feature, scenario)]
|
12
|
+
next unless name.empty?
|
13
|
+
|
14
|
+
add_error(references, MESSAGE)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing test actions
|
5
|
+
class MissingTestAction < Linter
|
6
|
+
def lint
|
7
|
+
filled_scenarios do |file, feature, scenario|
|
8
|
+
when_steps = scenario[:steps].select { |step| step[:keyword] == 'When ' }
|
9
|
+
next unless when_steps.empty?
|
10
|
+
|
11
|
+
references = [reference(file, feature, scenario)]
|
12
|
+
add_error(references, 'No \'When\'-Step')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for missing verifications
|
5
|
+
class MissingVerification < Linter
|
6
|
+
def lint
|
7
|
+
filled_scenarios do |file, feature, scenario|
|
8
|
+
then_steps = scenario[:steps].select { |step| step[:keyword] == 'Then ' }
|
9
|
+
next unless then_steps.empty?
|
10
|
+
|
11
|
+
references = [reference(file, feature, scenario)]
|
12
|
+
add_error(references, 'No \'Then\' step')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'chutney/linter/tag_constraint'
|
2
|
+
require 'chutney/linter'
|
3
|
+
|
4
|
+
module Chutney
|
5
|
+
# service class to lint for tags used multiple times
|
6
|
+
class RequiredTagsStartsWith < Linter
|
7
|
+
include TagConstraint
|
8
|
+
|
9
|
+
def match_pattern?(target)
|
10
|
+
target.each do |t|
|
11
|
+
return true if t.delete!('@').start_with?(*@pattern)
|
12
|
+
end
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for using same tag on all scenarios
|
5
|
+
class SameTagForAllScenarios < Linter
|
6
|
+
def lint
|
7
|
+
features do |file, feature|
|
8
|
+
next unless feature.include? :children
|
9
|
+
|
10
|
+
lint_scenarios file, feature
|
11
|
+
lint_examples file, feature
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def lint_scenarios(file, feature)
|
16
|
+
tags = gather_same_tags feature
|
17
|
+
return if tags.nil?
|
18
|
+
return if tags.empty?
|
19
|
+
return unless feature[:children].length > 1
|
20
|
+
|
21
|
+
references = [reference(file, feature)]
|
22
|
+
tags.each do |tag|
|
23
|
+
next if tag == '@skip'
|
24
|
+
|
25
|
+
add_error(references, "Tag '#{tag}' should be used at Feature level")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def lint_examples(file, feature)
|
30
|
+
feature[:children].each do |scenario|
|
31
|
+
tags = gather_same_tags_for_outline scenario
|
32
|
+
next if tags.nil? || tags.empty?
|
33
|
+
next unless scenario[:examples].length > 1
|
34
|
+
|
35
|
+
references = [reference(file, feature, scenario)]
|
36
|
+
tags.each do |tag|
|
37
|
+
next if tag == '@skip'
|
38
|
+
|
39
|
+
add_error(references, "Tag '#{tag}' should be used at Scenario Outline level")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def gather_same_tags(feature)
|
45
|
+
result = nil
|
46
|
+
feature[:children].each do |scenario|
|
47
|
+
next if scenario[:type] == :Background
|
48
|
+
return nil unless scenario.include? :tags
|
49
|
+
|
50
|
+
tags = scenario[:tags].map { |tag| tag[:name] }
|
51
|
+
result = tags if result.nil?
|
52
|
+
|
53
|
+
result &= tags
|
54
|
+
end
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
def gather_same_tags_for_outline(scenario)
|
59
|
+
result = nil
|
60
|
+
return result unless scenario.include? :examples
|
61
|
+
|
62
|
+
scenario[:examples].each do |example|
|
63
|
+
return nil unless example.include? :tags
|
64
|
+
|
65
|
+
tags = example[:tags].map { |tag| tag[:name] }
|
66
|
+
result = tags if result.nil?
|
67
|
+
|
68
|
+
result &= tags
|
69
|
+
end
|
70
|
+
result
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Chutney
|
2
|
+
# Mixin to lint for tags that have certain string contraints
|
3
|
+
module TagConstraint
|
4
|
+
def lint
|
5
|
+
scenarios do |file, feature, scenario|
|
6
|
+
next if match_pattern? tags(feature)
|
7
|
+
next if match_pattern? tags(scenario)
|
8
|
+
|
9
|
+
references = [reference(file, feature, scenario)]
|
10
|
+
add_error(references, 'Required Tag not found')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def tags(element)
|
15
|
+
return [] unless element.include? :tags
|
16
|
+
|
17
|
+
element[:tags].map { |a| a[:name] }
|
18
|
+
end
|
19
|
+
|
20
|
+
def matcher(pattern)
|
21
|
+
@pattern = pattern
|
22
|
+
validate_input
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_pattern?(_tags)
|
26
|
+
raise NoMethodError, 'This is an abstraction that must be implemented by the includer'
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_input
|
30
|
+
raise 'No Tags provided in the YAML' if @pattern.nil?
|
31
|
+
|
32
|
+
warn 'Required Tags matcher has no value' if @pattern.empty?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for tags used multiple times
|
5
|
+
class TagUsedMultipleTimes < Linter
|
6
|
+
def lint
|
7
|
+
scenarios do |file, feature, scenario|
|
8
|
+
references = [reference(file, feature, scenario)]
|
9
|
+
total_tags = tags(feature) + tags(scenario)
|
10
|
+
double_used_tags = total_tags.find_all { |a| total_tags.count(a) > 1 }.uniq!
|
11
|
+
next if double_used_tags.nil?
|
12
|
+
|
13
|
+
add_error(references, "Tag #{double_used_tags.join(' and ')} used multiple times")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def tags(element)
|
18
|
+
return [] unless element.include? :tags
|
19
|
+
|
20
|
+
element[:tags].map { |a| a[:name] }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for too clumsy scenarios
|
5
|
+
class TooClumsy < Linter
|
6
|
+
MESSAGE = 'This scenario is too long at %d characters'.freeze
|
7
|
+
def lint
|
8
|
+
filled_scenarios do |file, feature, scenario|
|
9
|
+
characters = scenario[:steps].map { |step| step[:text].length }.inject(0, :+)
|
10
|
+
next if characters < 400
|
11
|
+
|
12
|
+
references = [reference(file, feature, scenario)]
|
13
|
+
add_error(references, MESSAGE % characters)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for too long steps
|
5
|
+
class TooLongStep < Linter
|
6
|
+
MESSAGE = 'This step is too long at %d characters'.freeze
|
7
|
+
|
8
|
+
def lint
|
9
|
+
steps do |file, feature, scenario, step|
|
10
|
+
next if step[:text].length < 80
|
11
|
+
|
12
|
+
references = [reference(file, feature, scenario, step)]
|
13
|
+
add_error(references, MESSAGE % step[:text].length)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
require 'chutney/linter/tag_collector'
|
3
|
+
|
4
|
+
module Chutney
|
5
|
+
# service class to lint for too many different tags
|
6
|
+
class TooManyDifferentTags < Linter
|
7
|
+
include TagCollector
|
8
|
+
|
9
|
+
def lint
|
10
|
+
overall_tags = []
|
11
|
+
overall_references = []
|
12
|
+
features do |file, feature|
|
13
|
+
tags = tags_for_feature(feature)
|
14
|
+
overall_tags += tags
|
15
|
+
references = [reference(file, feature)]
|
16
|
+
overall_references += references unless tags.empty?
|
17
|
+
|
18
|
+
warn_single_feature(references, tags)
|
19
|
+
end
|
20
|
+
warn_across_all_features(overall_references, overall_tags)
|
21
|
+
end
|
22
|
+
|
23
|
+
def warn_single_feature(references, tags)
|
24
|
+
tags.uniq!
|
25
|
+
references.uniq!
|
26
|
+
return false unless tags.length >= 3
|
27
|
+
|
28
|
+
add_error(references, "Used #{tags.length} Tags within single Feature")
|
29
|
+
end
|
30
|
+
|
31
|
+
def warn_across_all_features(references, tags)
|
32
|
+
tags.uniq!
|
33
|
+
references.uniq!
|
34
|
+
return false unless tags.length >= 10
|
35
|
+
|
36
|
+
add_error(references, "Used #{tags.length} Tags across all Features")
|
37
|
+
end
|
38
|
+
|
39
|
+
def tags_for_feature(feature)
|
40
|
+
return [] unless feature.include? :children
|
41
|
+
|
42
|
+
gather_tags(feature) + feature[:children].map { |scenario| gather_tags(scenario) }.flatten
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for too many steps
|
5
|
+
class TooManySteps < Linter
|
6
|
+
def lint
|
7
|
+
filled_scenarios do |file, feature, scenario|
|
8
|
+
next if scenario[:steps].length < 10
|
9
|
+
|
10
|
+
references = [reference(file, feature, scenario)]
|
11
|
+
add_error(references, "Scenario is too long at #{scenario[:steps].length} steps")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
require 'chutney/linter/tag_collector'
|
3
|
+
|
4
|
+
module Chutney
|
5
|
+
# service class to lint for too many tags
|
6
|
+
class TooManyTags < Linter
|
7
|
+
include TagCollector
|
8
|
+
|
9
|
+
def lint
|
10
|
+
scenarios do |file, feature, scenario|
|
11
|
+
tags = gather_tags(feature) + gather_tags(scenario)
|
12
|
+
next unless tags.length >= 3
|
13
|
+
|
14
|
+
references = [reference(file, feature, scenario)]
|
15
|
+
add_error(references, "Used #{tags.length} Tags")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for unique scenario names
|
5
|
+
class UniqueScenarioNames < Linter
|
6
|
+
def lint
|
7
|
+
references_by_name = Hash.new []
|
8
|
+
scenarios do |file, feature, scenario|
|
9
|
+
next unless scenario.key? :name
|
10
|
+
|
11
|
+
scenario_name = "#{feature[:name]}.#{scenario[:name]}"
|
12
|
+
references_by_name[scenario_name] = references_by_name[scenario_name] + [reference(file, feature, scenario)]
|
13
|
+
end
|
14
|
+
|
15
|
+
references_by_name.each do |name, references|
|
16
|
+
next if references.length <= 1
|
17
|
+
|
18
|
+
add_error(references, "Scenario name should be unique, '#{name}' used #{references.length} times")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for unknown variables
|
5
|
+
class UnknownVariable < Linter
|
6
|
+
def lint
|
7
|
+
filled_scenarios do |file, feature, scenario|
|
8
|
+
known_vars = Set.new known_variables scenario
|
9
|
+
scenario[:steps].each do |step|
|
10
|
+
step_vars(step).each do |used_var|
|
11
|
+
next if known_vars.include? used_var
|
12
|
+
|
13
|
+
references = [reference(file, feature, scenario)]
|
14
|
+
add_error(references, "Variable '<#{used_var}>' is unknown")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def step_vars(step)
|
21
|
+
vars = gather_vars step[:text]
|
22
|
+
return vars unless step.include? :argument
|
23
|
+
|
24
|
+
vars + gather_vars_from_argument(step[:argument])
|
25
|
+
end
|
26
|
+
|
27
|
+
def gather_vars_from_argument(argument)
|
28
|
+
return gather_vars argument[:content] if argument[:type] == :DocString
|
29
|
+
|
30
|
+
(argument[:rows] || []).map do |row|
|
31
|
+
row[:cells].map { |value| gather_vars value[:value] }.flatten
|
32
|
+
end.flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
def gather_vars(string)
|
36
|
+
string.scan(/<.+?>/).map { |val| val[1..-2] }
|
37
|
+
end
|
38
|
+
|
39
|
+
def known_variables(scenario)
|
40
|
+
(scenario[:examples] || []).map do |example|
|
41
|
+
next unless example.key? :tableHeader
|
42
|
+
|
43
|
+
example[:tableHeader][:cells].map { |cell| cell[:value].strip }
|
44
|
+
end.flatten
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for unused variables
|
5
|
+
class UnusedVariable < Linter
|
6
|
+
def lint
|
7
|
+
scenarios do |file, feature, scenario|
|
8
|
+
next unless scenario.key? :examples
|
9
|
+
|
10
|
+
scenario[:examples].each do |example|
|
11
|
+
next unless example.key? :tableHeader
|
12
|
+
|
13
|
+
example[:tableHeader][:cells].map { |cell| cell[:value] }.each do |variable|
|
14
|
+
references = [reference(file, feature, scenario)]
|
15
|
+
add_error(references, "Variable '<#{variable}>' is unused") unless used?(variable, scenario)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def used?(variable, scenario)
|
22
|
+
variable = "<#{variable}>"
|
23
|
+
return false unless scenario.key? :steps
|
24
|
+
|
25
|
+
scenario[:steps].each do |step|
|
26
|
+
return true if step[:text].include? variable
|
27
|
+
next unless step.include? :argument
|
28
|
+
return true if used_in_docstring?(variable, step)
|
29
|
+
return true if used_in_table?(variable, step)
|
30
|
+
end
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def used_in_docstring?(variable, step)
|
35
|
+
step[:argument][:type] == :DocString && step[:argument][:content].include?(variable)
|
36
|
+
end
|
37
|
+
|
38
|
+
def used_in_table?(variable, step)
|
39
|
+
return false unless step[:argument][:type] == :DataTable
|
40
|
+
|
41
|
+
step[:argument][:rows].each do |row|
|
42
|
+
row[:cells].each { |value| return true if value[:value].include?(variable) }
|
43
|
+
end
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'chutney/linter'
|
2
|
+
|
3
|
+
module Chutney
|
4
|
+
# service class to lint for using background
|
5
|
+
class UseBackground < Linter
|
6
|
+
def lint
|
7
|
+
features do |file, feature|
|
8
|
+
next if scenarios_with_steps(feature) <= 1
|
9
|
+
|
10
|
+
givens = gather_givens feature
|
11
|
+
next if givens.nil?
|
12
|
+
next if givens.length <= 1
|
13
|
+
next if givens.uniq.length > 1
|
14
|
+
|
15
|
+
references = [reference(file, feature)]
|
16
|
+
add_error(references, "Step '#{givens.uniq.first}' should be part of background")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def scenarios_with_steps(feature)
|
21
|
+
scenarios = 0
|
22
|
+
return 0 unless feature.key? :children
|
23
|
+
|
24
|
+
feature[:children].each do |scenario|
|
25
|
+
next unless scenario.include? :steps
|
26
|
+
next if scenario[:steps].empty?
|
27
|
+
|
28
|
+
scenarios += 1
|
29
|
+
end
|
30
|
+
scenarios
|
31
|
+
end
|
32
|
+
|
33
|
+
def gather_givens(feature)
|
34
|
+
return unless feature.include? :children
|
35
|
+
|
36
|
+
has_non_given_step = false
|
37
|
+
feature[:children].each do |scenario|
|
38
|
+
next unless scenario.include? :steps
|
39
|
+
next if scenario[:steps].empty?
|
40
|
+
|
41
|
+
has_non_given_step = true unless scenario[:steps].first[:keyword] == 'Given '
|
42
|
+
end
|
43
|
+
return if has_non_given_step
|
44
|
+
|
45
|
+
result = []
|
46
|
+
expanded_steps(feature) { |given| result.push given }
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def expanded_steps(feature)
|
51
|
+
feature[:children].each do |scenario|
|
52
|
+
next unless scenario[:type] != :Background
|
53
|
+
next unless scenario.include? :steps
|
54
|
+
next if scenario[:steps].empty?
|
55
|
+
|
56
|
+
prototypes = [render_step(scenario[:steps].first)]
|
57
|
+
prototypes = expand_examples(scenario[:examples], prototypes) if scenario.key? :examples
|
58
|
+
prototypes.each { |prototype| yield prototype }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def expand_examples(examples, prototypes)
|
63
|
+
examples.each do |example|
|
64
|
+
prototypes = prototypes.map { |prototype| expand_outlines(prototype, example) }.flatten
|
65
|
+
end
|
66
|
+
prototypes
|
67
|
+
end
|
68
|
+
|
69
|
+
def expand_outlines(sentence, example)
|
70
|
+
result = []
|
71
|
+
headers = example[:tableHeader][:cells].map { |cell| cell[:value] }
|
72
|
+
example[:tableBody].each do |row| # .slice(1, example[:tableBody].length).each do |row|
|
73
|
+
modified_sentence = sentence.dup
|
74
|
+
headers.zip(row[:cells].map { |cell| cell[:value] }).map do |key, value|
|
75
|
+
modified_sentence.gsub!("<#{key}>", value)
|
76
|
+
end
|
77
|
+
result.push modified_sentence
|
78
|
+
end
|
79
|
+
result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|