cucumber_lint 0.0.3 → 0.0.4
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/.rubocop.yml +0 -4
- data/.travis.yml +9 -0
- data/Gemfile +7 -5
- data/Gemfile.lock +3 -1
- data/README.md +28 -40
- data/Rakefile +12 -6
- data/cucumber_lint.gemspec +2 -0
- data/features/cucumber_lint/fix/nothing.feature +12 -0
- data/features/cucumber_lint/fix/repeating_steps.feature +25 -0
- data/features/cucumber_lint/fix/table_whitespace.feature +25 -0
- data/features/cucumber_lint/fix/uppercase_table_headers.feature +25 -0
- data/features/cucumber_lint/lint/nothing.feature +12 -0
- data/features/cucumber_lint/lint/repeating_steps.feature +31 -0
- data/features/cucumber_lint/lint/table_whitespace.feature +27 -0
- data/features/cucumber_lint/lint/uppercase_table_headers.feature +30 -0
- data/features/step_definitions/cli_steps.rb +9 -6
- data/features/step_definitions/fixtures/repeating_steps/bad.feature.example +12 -0
- data/features/step_definitions/fixtures/repeating_steps/good.feature.example +12 -0
- data/features/step_definitions/fixtures/table_whitespace/bad.feature.example +20 -0
- data/features/step_definitions/fixtures/table_whitespace/good.feature.example +20 -0
- data/features/step_definitions/fixtures/uppercase_table_headers/bad.feature.example +20 -0
- data/features/step_definitions/fixtures/uppercase_table_headers/good.feature.example +20 -0
- data/lib/core_ext/hash.rb +29 -0
- data/lib/cucumber_lint/cli.rb +50 -38
- data/lib/cucumber_lint/fix_list.rb +37 -0
- data/lib/cucumber_lint/linter/feature_linter.rb +92 -0
- data/lib/cucumber_lint/linter/scenario_outline_linter.rb +34 -0
- data/lib/cucumber_lint/linter/steps_linter.rb +49 -0
- data/lib/cucumber_lint/linter/table_linter.rb +70 -0
- data/lib/cucumber_lint/linter.rb +21 -0
- data/lib/cucumber_lint/version.rb +2 -2
- data/lib/cucumber_lint.rb +7 -4
- metadata +64 -16
- data/features/cucumber_lint/cli_fix.feature +0 -51
- data/features/cucumber_lint/cli_lint.feature +0 -55
- data/features/cucumber_lint/feature_formatter.feature +0 -106
- data/features/cucumber_lint/steps_formatter.feature +0 -30
- data/features/cucumber_lint/table_formatter.feature +0 -45
- data/features/step_definitions/fixtures/formatted.feature.example +0 -9
- data/features/step_definitions/fixtures/unformatted.feature.example +0 -9
- data/features/step_definitions/steps_formatter_steps.rb +0 -15
- data/features/step_definitions/table_formatter_steps.rb +0 -15
- data/lib/core_ext/array.rb +0 -22
- data/lib/cucumber_lint/feature_formatter.rb +0 -48
- data/lib/cucumber_lint/steps_formatter.rb +0 -50
- data/lib/cucumber_lint/table_formatter.rb +0 -65
- data/spec/core_ext/array_spec.rb +0 -19
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Test Feature
|
2
|
+
|
3
|
+
Scenario: Test Scenario
|
4
|
+
Given a table
|
5
|
+
|VEGETABLE|CODENAME|
|
6
|
+
|Asparagus|Alpha|
|
7
|
+
|Broccoli|Bravo|
|
8
|
+
|Carrot|Charlie|
|
9
|
+
Then my tests pass
|
10
|
+
|
11
|
+
|
12
|
+
Scenario Outline: Test Scenario Outline
|
13
|
+
Given <VEGETABLE> and <FRUIT>
|
14
|
+
Then I expect <CODENAME>
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
|VEGETABLE| FRUIT | CODENAME |
|
18
|
+
|Asparagus | Apple | Alpha |
|
19
|
+
|Broccoli | Banana | Bravo |
|
20
|
+
| Carrot| Cherry | Charlie |
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Test Feature
|
2
|
+
|
3
|
+
Scenario: Test Scenario
|
4
|
+
Given a table
|
5
|
+
| VEGETABLE | CODENAME |
|
6
|
+
| Asparagus | Alpha |
|
7
|
+
| Broccoli | Bravo |
|
8
|
+
| Carrot | Charlie |
|
9
|
+
Then my tests pass
|
10
|
+
|
11
|
+
|
12
|
+
Scenario Outline: Test Scenario Outline
|
13
|
+
Given <VEGETABLE> and <FRUIT>
|
14
|
+
Then I expect <CODENAME>
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
| VEGETABLE | FRUIT | CODENAME |
|
18
|
+
| Asparagus | Apple | Alpha |
|
19
|
+
| Broccoli | Banana | Bravo |
|
20
|
+
| Carrot | Cherry | Charlie |
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Test Feature
|
2
|
+
|
3
|
+
Scenario: Test Scenario
|
4
|
+
Given a table
|
5
|
+
| vegetable | codename |
|
6
|
+
| Asparagus | Alpha |
|
7
|
+
| Broccoli | Bravo |
|
8
|
+
| Carrot | Charlie |
|
9
|
+
Then my tests pass
|
10
|
+
|
11
|
+
|
12
|
+
Scenario Outline: Test Scenario Outline
|
13
|
+
Given <vegetable> and <fruit>
|
14
|
+
Then I expect <codename>
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
| vegetable | fruit | codename |
|
18
|
+
| Asparagus | Apple | Alpha |
|
19
|
+
| Broccoli | Banana | Bravo |
|
20
|
+
| Carrot | Cherry | Charlie |
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Feature: Test Feature
|
2
|
+
|
3
|
+
Scenario: Test Scenario
|
4
|
+
Given a table
|
5
|
+
| VEGETABLE | CODENAME |
|
6
|
+
| Asparagus | Alpha |
|
7
|
+
| Broccoli | Bravo |
|
8
|
+
| Carrot | Charlie |
|
9
|
+
Then my tests pass
|
10
|
+
|
11
|
+
|
12
|
+
Scenario Outline: Test Scenario Outline
|
13
|
+
Given <VEGETABLE> and <FRUIT>
|
14
|
+
Then I expect <CODENAME>
|
15
|
+
|
16
|
+
Examples:
|
17
|
+
| VEGETABLE | FRUIT | CODENAME |
|
18
|
+
| Asparagus | Apple | Alpha |
|
19
|
+
| Broccoli | Banana | Bravo |
|
20
|
+
| Carrot | Cherry | Charlie |
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Monkey-patching Hash
|
2
|
+
class Hash
|
3
|
+
|
4
|
+
def to_open_struct
|
5
|
+
out = OpenStruct.new self
|
6
|
+
out.each_pair { |k, v| out[k] = object_to_open_struct v }
|
7
|
+
out
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
|
14
|
+
def object_to_open_struct object
|
15
|
+
if object.is_a? Hash
|
16
|
+
object.to_open_struct
|
17
|
+
elsif object.is_a? Array
|
18
|
+
array_to_open_struct object
|
19
|
+
else
|
20
|
+
object
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def array_to_open_struct array
|
26
|
+
array.map { |element| object_to_open_struct element }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/lib/cucumber_lint/cli.rb
CHANGED
@@ -13,72 +13,84 @@ module CucumberLint
|
|
13
13
|
|
14
14
|
def initialize args, out: STDOUT
|
15
15
|
@out = out
|
16
|
-
@
|
17
|
-
@results = OpenStruct.new(
|
16
|
+
@fix = args[0] == '--fix'
|
17
|
+
@results = OpenStruct.new(total: 0, passed: 0, failed: 0, written: 0, errors: [])
|
18
18
|
end
|
19
19
|
|
20
20
|
|
21
21
|
def execute!
|
22
|
-
Dir.glob('./features/**/*.feature').each do |filename|
|
23
|
-
|
24
|
-
|
25
|
-
if formatter.formatted?
|
26
|
-
file_formatted
|
27
|
-
else
|
28
|
-
file_unformatted filename, formatter.formatted_content
|
29
|
-
end
|
22
|
+
Dir.glob('./features/**/*.feature').sort.each do |filename|
|
23
|
+
lint_feature filename
|
30
24
|
end
|
31
25
|
|
32
26
|
output_results
|
33
|
-
exit 1 unless @results.
|
27
|
+
exit 1 unless @results.errors.empty?
|
34
28
|
end
|
35
29
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
30
|
+
private
|
31
|
+
|
32
|
+
def lint_feature filename
|
33
|
+
linter = FeatureLinter.new filename, fix: @fix
|
34
|
+
linter.lint
|
35
|
+
|
36
|
+
if linter.errors?
|
37
|
+
@results.errors += linter.errors
|
38
|
+
file_failed
|
39
|
+
elsif linter.can_fix?
|
40
|
+
file_written
|
42
41
|
else
|
43
|
-
|
42
|
+
file_passed
|
44
43
|
end
|
45
|
-
|
44
|
+
|
45
|
+
linter.write if linter.can_fix?
|
46
|
+
end
|
47
|
+
|
48
|
+
def file_counts
|
49
|
+
out = ["#{@results.passed} passed".green]
|
50
|
+
out << "#{@results.written} written".yellow if @results.written > 0
|
51
|
+
out << "#{@results.failed} failed".red if @results.failed > 0
|
52
|
+
"(#{out.join(', ')})"
|
46
53
|
end
|
47
54
|
|
48
55
|
|
49
|
-
def
|
50
|
-
@out.print "
|
51
|
-
@out.print
|
56
|
+
def output_errors
|
57
|
+
@out.print "\n\n"
|
58
|
+
@out.print @results.errors.join("\n").red
|
52
59
|
end
|
53
60
|
|
54
61
|
|
55
|
-
def
|
56
|
-
@out.print "
|
62
|
+
def output_counts
|
63
|
+
@out.print "\n\n"
|
64
|
+
@out.print "#{@results.total} file#{'s' if @results.total != 1} inspected"
|
65
|
+
@out.print " #{file_counts}" if @results.total > 0
|
66
|
+
@out.print "\n"
|
57
67
|
end
|
58
68
|
|
59
69
|
|
60
|
-
def
|
61
|
-
@
|
62
|
-
|
70
|
+
def output_results
|
71
|
+
output_errors unless @results.errors.empty?
|
72
|
+
output_counts
|
63
73
|
end
|
64
74
|
|
65
75
|
|
66
|
-
def
|
67
|
-
@results.
|
76
|
+
def file_passed
|
77
|
+
@results.total += 1
|
78
|
+
@results.passed += 1
|
68
79
|
@out.print '.'.green
|
69
80
|
end
|
70
81
|
|
71
82
|
|
72
|
-
def
|
73
|
-
@results.
|
83
|
+
def file_failed
|
84
|
+
@results.total += 1
|
85
|
+
@results.failed += 1
|
86
|
+
@out.print 'F'.red
|
87
|
+
end
|
74
88
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
@out.print 'W'.yellow
|
81
|
-
end
|
89
|
+
|
90
|
+
def file_written
|
91
|
+
@results.total += 1
|
92
|
+
@results.written += 1
|
93
|
+
@out.print 'W'.yellow
|
82
94
|
end
|
83
95
|
|
84
96
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CucumberLint
|
2
|
+
# A class that represents a list of fixes to apply to a feature
|
3
|
+
class FixList
|
4
|
+
|
5
|
+
attr_reader :list
|
6
|
+
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@list = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def add line_number, fix
|
14
|
+
@list[line_number] ||= []
|
15
|
+
@list[line_number] += Array(fix)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def apply lines
|
20
|
+
lines.each_with_index.map do |line, index|
|
21
|
+
line_number = index + 1
|
22
|
+
|
23
|
+
@list.fetch(line_number, []).each do |fix|
|
24
|
+
line = fix.call(line)
|
25
|
+
end
|
26
|
+
|
27
|
+
line
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def empty?
|
33
|
+
@list.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'core_ext/hash'
|
2
|
+
require 'gherkin/formatter/json_formatter'
|
3
|
+
require 'gherkin/parser/parser'
|
4
|
+
require 'multi_json'
|
5
|
+
|
6
|
+
module CucumberLint
|
7
|
+
# A linter for a given feature (represented by a filename)
|
8
|
+
class FeatureLinter < Linter
|
9
|
+
|
10
|
+
attr_reader :errors, :fix_list
|
11
|
+
|
12
|
+
def initialize path, fix:
|
13
|
+
super(fix: fix)
|
14
|
+
|
15
|
+
@path = path
|
16
|
+
@content = IO.read(path)
|
17
|
+
@file_lines = @content.lines
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def can_fix?
|
22
|
+
!fix_list.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def errors?
|
27
|
+
!errors.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def lint
|
32
|
+
feature = parse_content
|
33
|
+
|
34
|
+
feature.elements.each do |element|
|
35
|
+
lint_steps element.steps
|
36
|
+
|
37
|
+
if element.type == 'scenario_outline'
|
38
|
+
lint_scenario_outline element.steps
|
39
|
+
lint_examples element.examples
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
errors.map! { |error| "#{feature.uri}:#{error}" }
|
44
|
+
end
|
45
|
+
|
46
|
+
def write
|
47
|
+
fixed_content = fix_list.apply(@file_lines).join
|
48
|
+
IO.write(@path, fixed_content)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
|
54
|
+
def linter_options
|
55
|
+
{ file_lines: @file_lines, fix: @fix, parent: self }
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
def lint_examples examples
|
60
|
+
examples.each { |example| lint_table example.rows }
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def lint_scenario_outline steps
|
65
|
+
linter = ScenarioOutlineLinter.new linter_options.merge(steps: steps)
|
66
|
+
linter.lint
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def lint_steps steps
|
71
|
+
linter = StepsLinter.new linter_options.merge(steps: steps)
|
72
|
+
linter.lint
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def lint_table rows
|
77
|
+
linter = TableLinter.new linter_options.merge(rows: rows)
|
78
|
+
linter.lint
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def parse_content
|
83
|
+
io = StringIO.new
|
84
|
+
formatter = Gherkin::Formatter::JSONFormatter.new(io)
|
85
|
+
parser = Gherkin::Parser::Parser.new(formatter)
|
86
|
+
parser.parse(@content, @path, 0)
|
87
|
+
formatter.done
|
88
|
+
MultiJson.load(io.string)[0].to_open_struct
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CucumberLint
|
2
|
+
# A linter for a series of steps in a scenario outline (as parsed by Gherkin)
|
3
|
+
class ScenarioOutlineLinter < Linter
|
4
|
+
|
5
|
+
def initialize steps:, file_lines:, fix:, parent:
|
6
|
+
super(fix: fix, parent: parent)
|
7
|
+
|
8
|
+
@steps = steps
|
9
|
+
@file_lines = file_lines
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def lint
|
14
|
+
@steps.each do |step|
|
15
|
+
step.name.scan(/<.+?>/).each do |placeholder|
|
16
|
+
bad_placeholder step.line, placeholder unless placeholder == placeholder.upcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
|
25
|
+
def bad_placeholder line_number, placeholder
|
26
|
+
if @fix
|
27
|
+
fix_list.add line_number, -> (line) { line.sub(placeholder, placeholder.upcase) }
|
28
|
+
else
|
29
|
+
errors << "#{line_number}: Make \"#{placeholder}\" uppercase"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module CucumberLint
|
2
|
+
# A linter for a series of steps (as parsed by Gherkin)
|
3
|
+
class StepsLinter < Linter
|
4
|
+
|
5
|
+
def initialize steps:, file_lines:, fix:, parent:
|
6
|
+
super(fix: fix, parent: parent)
|
7
|
+
|
8
|
+
@steps = steps
|
9
|
+
@file_lines = file_lines
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def lint
|
14
|
+
previous_keyword = nil
|
15
|
+
|
16
|
+
@steps.each do |step|
|
17
|
+
current_keyword = step.keyword.strip
|
18
|
+
|
19
|
+
if STEP_TYPES.include?(current_keyword) && current_keyword == previous_keyword
|
20
|
+
repeated_keyword step.line, current_keyword
|
21
|
+
else
|
22
|
+
previous_keyword = current_keyword
|
23
|
+
end
|
24
|
+
|
25
|
+
lint_table(step.rows) if step.rows && step.rows.is_a?(Array)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
STEP_TYPES = %w(Given When Then)
|
33
|
+
|
34
|
+
def repeated_keyword line_number, keyword
|
35
|
+
if @fix
|
36
|
+
fix_list.add line_number, -> (line) { line.sub(keyword, 'And') }
|
37
|
+
else
|
38
|
+
errors << "#{line_number}: Use \"And\" instead of repeating \"#{keyword}\""
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def lint_table rows
|
44
|
+
table_linter = TableLinter.new rows: rows, file_lines: @file_lines, fix: @fix, parent: self
|
45
|
+
table_linter.lint
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|