chutney 0.5.0
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 +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
|