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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +0 -4
  3. data/.travis.yml +9 -0
  4. data/Gemfile +7 -5
  5. data/Gemfile.lock +3 -1
  6. data/README.md +28 -40
  7. data/Rakefile +12 -6
  8. data/cucumber_lint.gemspec +2 -0
  9. data/features/cucumber_lint/fix/nothing.feature +12 -0
  10. data/features/cucumber_lint/fix/repeating_steps.feature +25 -0
  11. data/features/cucumber_lint/fix/table_whitespace.feature +25 -0
  12. data/features/cucumber_lint/fix/uppercase_table_headers.feature +25 -0
  13. data/features/cucumber_lint/lint/nothing.feature +12 -0
  14. data/features/cucumber_lint/lint/repeating_steps.feature +31 -0
  15. data/features/cucumber_lint/lint/table_whitespace.feature +27 -0
  16. data/features/cucumber_lint/lint/uppercase_table_headers.feature +30 -0
  17. data/features/step_definitions/cli_steps.rb +9 -6
  18. data/features/step_definitions/fixtures/repeating_steps/bad.feature.example +12 -0
  19. data/features/step_definitions/fixtures/repeating_steps/good.feature.example +12 -0
  20. data/features/step_definitions/fixtures/table_whitespace/bad.feature.example +20 -0
  21. data/features/step_definitions/fixtures/table_whitespace/good.feature.example +20 -0
  22. data/features/step_definitions/fixtures/uppercase_table_headers/bad.feature.example +20 -0
  23. data/features/step_definitions/fixtures/uppercase_table_headers/good.feature.example +20 -0
  24. data/lib/core_ext/hash.rb +29 -0
  25. data/lib/cucumber_lint/cli.rb +50 -38
  26. data/lib/cucumber_lint/fix_list.rb +37 -0
  27. data/lib/cucumber_lint/linter/feature_linter.rb +92 -0
  28. data/lib/cucumber_lint/linter/scenario_outline_linter.rb +34 -0
  29. data/lib/cucumber_lint/linter/steps_linter.rb +49 -0
  30. data/lib/cucumber_lint/linter/table_linter.rb +70 -0
  31. data/lib/cucumber_lint/linter.rb +21 -0
  32. data/lib/cucumber_lint/version.rb +2 -2
  33. data/lib/cucumber_lint.rb +7 -4
  34. metadata +64 -16
  35. data/features/cucumber_lint/cli_fix.feature +0 -51
  36. data/features/cucumber_lint/cli_lint.feature +0 -55
  37. data/features/cucumber_lint/feature_formatter.feature +0 -106
  38. data/features/cucumber_lint/steps_formatter.feature +0 -30
  39. data/features/cucumber_lint/table_formatter.feature +0 -45
  40. data/features/step_definitions/fixtures/formatted.feature.example +0 -9
  41. data/features/step_definitions/fixtures/unformatted.feature.example +0 -9
  42. data/features/step_definitions/steps_formatter_steps.rb +0 -15
  43. data/features/step_definitions/table_formatter_steps.rb +0 -15
  44. data/lib/core_ext/array.rb +0 -22
  45. data/lib/cucumber_lint/feature_formatter.rb +0 -48
  46. data/lib/cucumber_lint/steps_formatter.rb +0 -50
  47. data/lib/cucumber_lint/table_formatter.rb +0 -65
  48. 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
@@ -13,72 +13,84 @@ module CucumberLint
13
13
 
14
14
  def initialize args, out: STDOUT
15
15
  @out = out
16
- @lint = args[0] != '--fix'
17
- @results = OpenStruct.new(formatted: 0, unformatted: 0, unformatted_files: [])
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
- formatter = FeatureFormatter.new IO.read(filename)
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.unformatted_files.empty?
27
+ exit 1 unless @results.errors.empty?
34
28
  end
35
29
 
36
- def output_results
37
- output_failures if @lint && !@results.unformatted_files.empty?
38
- total = @results.formatted + @results.unformatted
39
- @out.print "\n\n#{total} file#{'s' if total != 1} inspected ("
40
- if @lint
41
- output_inspect_results
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
- output_write_results
42
+ file_passed
44
43
  end
45
- @out.puts ')'
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 output_inspect_results
50
- @out.print "#{@results.formatted} passed".green
51
- @out.print ', ' + "#{@results.unformatted} failed".red if @results.unformatted > 0
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 output_write_results
56
- @out.print "#{@results.unformatted} written".yellow
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 output_failures
61
- @out.puts "\n\nFiles with errors:".red
62
- @out.print @results.unformatted_files.join("\n").red
70
+ def output_results
71
+ output_errors unless @results.errors.empty?
72
+ output_counts
63
73
  end
64
74
 
65
75
 
66
- def file_formatted
67
- @results.formatted += 1
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 file_unformatted filename, content
73
- @results.unformatted += 1
83
+ def file_failed
84
+ @results.total += 1
85
+ @results.failed += 1
86
+ @out.print 'F'.red
87
+ end
74
88
 
75
- if @lint
76
- @results.unformatted_files << filename
77
- @out.print 'F'.red
78
- else
79
- IO.write filename, content
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