cucumber_lint 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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