cucumber 3.1.2 → 5.2.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.
Files changed (115) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +287 -14
  3. data/CONTRIBUTING.md +11 -25
  4. data/README.md +4 -5
  5. data/bin/cucumber +1 -1
  6. data/lib/autotest/cucumber_mixin.rb +46 -53
  7. data/lib/cucumber.rb +1 -1
  8. data/lib/cucumber/cli/configuration.rb +5 -5
  9. data/lib/cucumber/cli/main.rb +12 -12
  10. data/lib/cucumber/cli/options.rb +97 -76
  11. data/lib/cucumber/cli/profile_loader.rb +49 -26
  12. data/lib/cucumber/configuration.rb +44 -29
  13. data/lib/cucumber/constantize.rb +2 -5
  14. data/lib/cucumber/deprecate.rb +31 -7
  15. data/lib/cucumber/errors.rb +5 -7
  16. data/lib/cucumber/events.rb +13 -6
  17. data/lib/cucumber/events/envelope.rb +9 -0
  18. data/lib/cucumber/events/gherkin_source_parsed.rb +11 -0
  19. data/lib/cucumber/events/hook_test_step_created.rb +13 -0
  20. data/lib/cucumber/events/step_activated.rb +2 -1
  21. data/lib/cucumber/events/test_case_created.rb +13 -0
  22. data/lib/cucumber/events/test_case_ready.rb +12 -0
  23. data/lib/cucumber/events/test_step_created.rb +13 -0
  24. data/lib/cucumber/events/undefined_parameter_type.rb +10 -0
  25. data/lib/cucumber/file_specs.rb +6 -6
  26. data/lib/cucumber/filters.rb +1 -0
  27. data/lib/cucumber/filters/activate_steps.rb +5 -3
  28. data/lib/cucumber/filters/broadcast_test_case_ready_event.rb +12 -0
  29. data/lib/cucumber/filters/prepare_world.rb +5 -9
  30. data/lib/cucumber/filters/quit.rb +1 -3
  31. data/lib/cucumber/filters/tag_limits/verifier.rb +2 -4
  32. data/lib/cucumber/formatter/ansicolor.rb +40 -45
  33. data/lib/cucumber/formatter/ast_lookup.rb +163 -0
  34. data/lib/cucumber/formatter/backtrace_filter.rb +9 -8
  35. data/lib/cucumber/formatter/console.rb +58 -66
  36. data/lib/cucumber/formatter/console_counts.rb +4 -9
  37. data/lib/cucumber/formatter/console_issues.rb +6 -3
  38. data/lib/cucumber/formatter/duration.rb +1 -1
  39. data/lib/cucumber/formatter/duration_extractor.rb +3 -1
  40. data/lib/cucumber/formatter/errors.rb +6 -0
  41. data/lib/cucumber/formatter/fanout.rb +2 -0
  42. data/lib/cucumber/formatter/html.rb +11 -598
  43. data/lib/cucumber/formatter/http_io.rb +147 -0
  44. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  45. data/lib/cucumber/formatter/interceptor.rb +11 -30
  46. data/lib/cucumber/formatter/io.rb +55 -13
  47. data/lib/cucumber/formatter/json.rb +102 -110
  48. data/lib/cucumber/formatter/junit.rb +55 -55
  49. data/lib/cucumber/formatter/message.rb +22 -0
  50. data/lib/cucumber/formatter/message_builder.rb +255 -0
  51. data/lib/cucumber/formatter/pretty.rb +359 -153
  52. data/lib/cucumber/formatter/progress.rb +30 -32
  53. data/lib/cucumber/formatter/publish_banner_printer.rb +77 -0
  54. data/lib/cucumber/formatter/query/hook_by_test_step.rb +31 -0
  55. data/lib/cucumber/formatter/query/pickle_by_test.rb +26 -0
  56. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +26 -0
  57. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +40 -0
  58. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +40 -0
  59. data/lib/cucumber/formatter/rerun.rb +22 -4
  60. data/lib/cucumber/formatter/stepdefs.rb +1 -2
  61. data/lib/cucumber/formatter/steps.rb +3 -4
  62. data/lib/cucumber/formatter/summary.rb +16 -8
  63. data/lib/cucumber/formatter/unicode.rb +15 -17
  64. data/lib/cucumber/formatter/url_reporter.rb +17 -0
  65. data/lib/cucumber/formatter/usage.rb +11 -10
  66. data/lib/cucumber/gherkin/data_table_parser.rb +17 -6
  67. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +13 -17
  68. data/lib/cucumber/gherkin/formatter/escaping.rb +2 -2
  69. data/lib/cucumber/gherkin/steps_parser.rb +17 -8
  70. data/lib/cucumber/glue/hook.rb +34 -11
  71. data/lib/cucumber/glue/invoke_in_world.rb +13 -18
  72. data/lib/cucumber/glue/proto_world.rb +42 -33
  73. data/lib/cucumber/glue/registry_and_more.rb +42 -12
  74. data/lib/cucumber/glue/snippet.rb +23 -22
  75. data/lib/cucumber/glue/step_definition.rb +42 -19
  76. data/lib/cucumber/glue/world_factory.rb +1 -1
  77. data/lib/cucumber/hooks.rb +11 -11
  78. data/lib/cucumber/multiline_argument.rb +4 -6
  79. data/lib/cucumber/multiline_argument/data_table.rb +97 -64
  80. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +2 -2
  81. data/lib/cucumber/multiline_argument/doc_string.rb +1 -1
  82. data/lib/cucumber/platform.rb +3 -3
  83. data/lib/cucumber/rake/task.rb +16 -18
  84. data/lib/cucumber/rspec/disable_option_parser.rb +9 -8
  85. data/lib/cucumber/rspec/doubles.rb +3 -5
  86. data/lib/cucumber/running_test_case.rb +2 -53
  87. data/lib/cucumber/runtime.rb +41 -58
  88. data/lib/cucumber/runtime/after_hooks.rb +8 -4
  89. data/lib/cucumber/runtime/before_hooks.rb +8 -4
  90. data/lib/cucumber/runtime/for_programming_languages.rb +4 -2
  91. data/lib/cucumber/runtime/step_hooks.rb +6 -2
  92. data/lib/cucumber/runtime/support_code.rb +13 -15
  93. data/lib/cucumber/runtime/user_interface.rb +6 -16
  94. data/lib/cucumber/step_definition_light.rb +4 -3
  95. data/lib/cucumber/step_definitions.rb +2 -2
  96. data/lib/cucumber/step_match.rb +12 -11
  97. data/lib/cucumber/step_match_search.rb +2 -1
  98. data/lib/cucumber/term/ansicolor.rb +9 -9
  99. data/lib/cucumber/term/banner.rb +56 -0
  100. data/lib/cucumber/version +1 -1
  101. metadata +254 -83
  102. data/lib/cucumber/events/gherkin_source_parsed.rb~ +0 -14
  103. data/lib/cucumber/formatter/ast_lookup.rb~ +0 -9
  104. data/lib/cucumber/formatter/cucumber.css +0 -286
  105. data/lib/cucumber/formatter/cucumber.sass +0 -247
  106. data/lib/cucumber/formatter/hook_query_visitor.rb +0 -42
  107. data/lib/cucumber/formatter/html_builder.rb +0 -121
  108. data/lib/cucumber/formatter/inline-js.js +0 -30
  109. data/lib/cucumber/formatter/jquery-min.js +0 -154
  110. data/lib/cucumber/formatter/json_pretty.rb +0 -11
  111. data/lib/cucumber/formatter/legacy_api/adapter.rb +0 -1028
  112. data/lib/cucumber/formatter/legacy_api/ast.rb +0 -394
  113. data/lib/cucumber/formatter/legacy_api/results.rb +0 -50
  114. data/lib/cucumber/formatter/legacy_api/runtime_facade.rb +0 -32
  115. data/lib/cucumber/step_argument.rb +0 -25
@@ -0,0 +1,147 @@
1
+ require 'net/http'
2
+ require 'tempfile'
3
+ require 'shellwords'
4
+
5
+ module Cucumber
6
+ module Formatter
7
+ class HTTPIO
8
+ class << self
9
+ # Returns an IO that will write to a HTTP request's body
10
+ # https_verify_mode can be set to OpenSSL::SSL::VERIFY_NONE
11
+ # to ignore unsigned certificate - setting to nil will verify the certificate
12
+ def open(url, https_verify_mode = nil, reporter = nil)
13
+ @https_verify_mode = https_verify_mode
14
+ uri, method, headers = CurlOptionParser.parse(url)
15
+ IOHTTPBuffer.new(uri, method, headers, https_verify_mode, reporter)
16
+ end
17
+ end
18
+ end
19
+
20
+ class CurlOptionParser
21
+ def self.parse(options)
22
+ args = Shellwords.split(options)
23
+
24
+ url = nil
25
+ http_method = 'PUT'
26
+ headers = {}
27
+
28
+ until args.empty?
29
+ arg = args.shift
30
+ case arg
31
+ when '-X', '--request'
32
+ http_method = remove_arg_for(args, arg)
33
+ when '-H'
34
+ header_arg = remove_arg_for(args, arg)
35
+ headers = headers.merge(parse_header(header_arg))
36
+ else
37
+ raise StandardError, "#{options} was not a valid curl command. Can't set url to #{arg} it is already set to #{url}" if url
38
+ url = arg
39
+ end
40
+ end
41
+ raise StandardError, "#{options} was not a valid curl command" unless url
42
+
43
+ [
44
+ url,
45
+ http_method,
46
+ headers
47
+ ]
48
+ end
49
+
50
+ def self.remove_arg_for(args, arg)
51
+ return args.shift unless args.empty?
52
+ raise StandardError, "Missing argument for #{arg}"
53
+ end
54
+
55
+ def self.parse_header(header_arg)
56
+ parts = header_arg.split(':', 2)
57
+ raise StandardError, "#{header_arg} was not a valid header" unless parts.length == 2
58
+ { parts[0].strip => parts[1].strip }
59
+ end
60
+ end
61
+
62
+ class IOHTTPBuffer
63
+ attr_reader :uri, :method, :headers
64
+
65
+ def initialize(uri, method, headers = {}, https_verify_mode = nil, reporter = nil)
66
+ @uri = URI(uri)
67
+ @method = method
68
+ @headers = headers
69
+ @write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
70
+ @https_verify_mode = https_verify_mode
71
+ @reporter = reporter || NoReporter.new
72
+ end
73
+
74
+ def close
75
+ response = send_content(@uri, @method, @headers)
76
+ @reporter.report(response.body)
77
+ @write_io.close
78
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
79
+ raise StandardError, "request to #{uri} failed with status #{response.code}"
80
+ end
81
+
82
+ def write(data)
83
+ @write_io.write(data)
84
+ end
85
+
86
+ def flush
87
+ @write_io.flush
88
+ end
89
+
90
+ def closed?
91
+ @write_io.closed?
92
+ end
93
+
94
+ private
95
+
96
+ def send_content(uri, method, headers, attempt = 10)
97
+ content = (method == 'GET' ? StringIO.new : @write_io)
98
+ http = build_client(uri, @https_verify_mode)
99
+
100
+ raise StandardError, "request to #{uri} failed (too many redirections)" if attempt <= 0
101
+ req = build_request(
102
+ uri,
103
+ method,
104
+ headers.merge(
105
+ 'Content-Length' => content.size.to_s
106
+ )
107
+ )
108
+
109
+ content.rewind
110
+ req.body_stream = content
111
+
112
+ begin
113
+ response = http.request(req)
114
+ rescue SystemCallError
115
+ # We may get the redirect response before pushing the file.
116
+ response = http.request(build_request(uri, method, headers))
117
+ end
118
+
119
+ case response
120
+ when Net::HTTPAccepted
121
+ send_content(URI(response['Location']), 'PUT', {}, attempt - 1) if response['Location']
122
+ when Net::HTTPRedirection
123
+ send_content(URI(response['Location']), method, headers, attempt - 1)
124
+ end
125
+ response
126
+ end
127
+
128
+ def build_request(uri, method, headers)
129
+ method_class_name = "#{method[0].upcase}#{method[1..-1].downcase}"
130
+ req = Net::HTTP.const_get(method_class_name).new(uri)
131
+ headers.each do |header, value|
132
+ req[header] = value
133
+ end
134
+ req
135
+ end
136
+
137
+ def build_client(uri, https_verify_mode)
138
+ http = Net::HTTP.new(uri.hostname, uri.port)
139
+ if uri.scheme == 'https'
140
+ http.use_ssl = true
141
+ http.verify_mode = https_verify_mode if https_verify_mode
142
+ end
143
+ http
144
+ end
145
+ end
146
+ end
147
+ end
@@ -8,7 +8,7 @@ module Cucumber
8
8
  end
9
9
 
10
10
  def method_missing(message, *args)
11
- @receiver.send(message, *args) if @receiver.respond_to?(message)
11
+ @receiver.respond_to?(message) ? @receiver.send(message, *args) : super
12
12
  end
13
13
 
14
14
  def respond_to_missing?(name, include_private = false)
@@ -5,34 +5,23 @@ module Cucumber
5
5
  module Interceptor
6
6
  class Pipe
7
7
  attr_reader :pipe
8
+
8
9
  def initialize(pipe)
9
10
  @pipe = pipe
10
11
  @buffer = StringIO.new
11
12
  @wrapped = true
13
+ @lock = Mutex.new
12
14
  end
13
15
 
14
16
  def write(str)
15
- lock.synchronize do
17
+ @lock.synchronize do
16
18
  @buffer << str if @wrapped
17
19
  return @pipe.write(str)
18
20
  end
19
21
  end
20
22
 
21
- # @deprecated use #buffer_string
22
- def buffer
23
- require 'cucumber/deprecate.rb'
24
- Cucumber.deprecate(
25
- 'Use Cucumber::Formatter::Interceptor::Pipe#buffer_string instead',
26
- 'Cucumber::Formatter::Interceptor::Pipe#buffer',
27
- '3.99'
28
- )
29
- lock.synchronize do
30
- return @buffer.string.lines
31
- end
32
- end
33
-
34
23
  def buffer_string
35
- lock.synchronize do
24
+ @lock.synchronize do
36
25
  return @buffer.string.dup
37
26
  end
38
27
  end
@@ -43,17 +32,15 @@ module Cucumber
43
32
  end
44
33
 
45
34
  def method_missing(method, *args, &blk)
46
- @pipe.send(method, *args, &blk)
35
+ @pipe.respond_to?(method) ? @pipe.send(method, *args, &blk) : super
47
36
  end
48
37
 
49
- def respond_to?(method, include_private = false)
38
+ def respond_to_missing?(method, include_private = false)
50
39
  super || @pipe.respond_to?(method, include_private)
51
40
  end
52
41
 
53
42
  def self.validate_pipe(pipe)
54
- unless [:stdout, :stderr].include? pipe
55
- raise ArgumentError, '#wrap only accepts :stderr or :stdout'
56
- end
43
+ raise ArgumentError, '#wrap only accepts :stderr or :stdout' unless %i[stdout stderr].include? pipe
57
44
  end
58
45
 
59
46
  def self.unwrap!(pipe)
@@ -75,19 +62,13 @@ module Cucumber
75
62
 
76
63
  case pipe
77
64
  when :stderr
78
- $stderr = self.new($stderr)
79
- return $stderr
65
+ $stderr = new($stderr)
66
+ $stderr
80
67
  when :stdout
81
- $stdout = self.new($stdout)
82
- return $stdout
68
+ $stdout = new($stdout)
69
+ $stdout
83
70
  end
84
71
  end
85
-
86
- private
87
-
88
- def lock
89
- @lock ||= Mutex.new
90
- end
91
72
  end
92
73
  end
93
74
  end
@@ -1,32 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cucumber/formatter/http_io'
4
+ require 'cucumber/formatter/url_reporter'
5
+ require 'cucumber/cli/options'
6
+
3
7
  module Cucumber
4
8
  module Formatter
5
9
  module Io
6
10
  module_function
7
11
 
8
- def ensure_io(path_or_io)
9
- return nil if path_or_io.nil?
10
- return path_or_io if path_or_io.respond_to?(:write)
11
- file = File.open(path_or_io, Cucumber.file_mode('w'))
12
- at_exit do
13
- unless file.closed?
14
- file.flush
15
- file.close
12
+ def ensure_io(path_or_url_or_io, error_stream)
13
+ return nil if path_or_url_or_io.nil?
14
+ return path_or_url_or_io if io?(path_or_url_or_io)
15
+
16
+ io = if url?(path_or_url_or_io)
17
+ url = path_or_url_or_io
18
+ reporter = url.start_with?(Cucumber::Cli::Options::CUCUMBER_PUBLISH_URL) ? URLReporter.new(error_stream) : NoReporter.new
19
+ HTTPIO.open(url, nil, reporter)
20
+ else
21
+ File.open(path_or_url_or_io, Cucumber.file_mode('w'))
22
+ end
23
+ @io_objects_to_close ||= []
24
+ @io_objects_to_close.push(io)
25
+ io
26
+ end
27
+
28
+ module ClassMethods
29
+ def new(*args, &block)
30
+ instance = super
31
+
32
+ config = args[0]
33
+ if config.respond_to? :on_event
34
+ config.on_event :test_run_finished do
35
+ ios = instance.instance_variable_get(:@io_objects_to_close) || []
36
+ ios.each do |io|
37
+ at_exit do
38
+ unless io.closed?
39
+ io.flush
40
+ io.close
41
+ end
42
+ end
43
+ end
44
+ end
16
45
  end
46
+
47
+ instance
17
48
  end
18
- file
49
+ end
50
+
51
+ def self.included(formatter_class)
52
+ formatter_class.extend(ClassMethods)
53
+ end
54
+
55
+ def io?(path_or_url_or_io)
56
+ path_or_url_or_io.respond_to?(:write)
57
+ end
58
+
59
+ def url?(path_or_url_or_io)
60
+ path_or_url_or_io.match(%r{^https?://})
19
61
  end
20
62
 
21
63
  def ensure_file(path, name)
22
- raise "You *must* specify --out FILE for the #{name} formatter" unless String === path
64
+ raise "You *must* specify --out FILE for the #{name} formatter" unless String == path.class
23
65
  raise "I can't write #{name} to a directory - it has to be a file" if File.directory?(path)
24
- raise "I can't write #{name} to a file in the non-existing directory #{File.dirname(path)}" if !File.directory?(File.dirname(path))
25
- ensure_io(path)
66
+ raise "I can't write #{name} to a file in the non-existing directory #{File.dirname(path)}" unless File.directory?(File.dirname(path))
67
+ ensure_io(path, nil)
26
68
  end
27
69
 
28
70
  def ensure_dir(path, name)
29
- raise "You *must* specify --out DIR for the #{name} formatter" unless String === path
71
+ raise "You *must* specify --out DIR for the #{name} formatter" unless String == path.class
30
72
  raise "I can't write #{name} reports to a file - it has to be a directory" if File.file?(path)
31
73
  FileUtils.mkdir_p(path) unless File.directory?(path)
32
74
  File.absolute_path path
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'multi_json'
3
+ require 'json'
4
4
  require 'base64'
5
5
  require 'cucumber/formatter/backtrace_filter'
6
6
  require 'cucumber/formatter/io'
7
- require 'cucumber/formatter/hook_query_visitor'
7
+ require 'cucumber/formatter/ast_lookup'
8
+ require 'cucumber/deprecate'
8
9
 
9
10
  module Cucumber
10
11
  module Formatter
@@ -13,7 +14,16 @@ module Cucumber
13
14
  include Io
14
15
 
15
16
  def initialize(config)
16
- @io = ensure_io(config.out_stream)
17
+ Cucumber::Deprecate::CliOption.deprecate(
18
+ config.error_stream,
19
+ '--format=json',
20
+ "Please use --format=message and stand-alone json-formatter.\n" \
21
+ 'json-formatter homepage: https://github.com/cucumber/cucumber/tree/master/json-formatter#cucumber-json-formatter',
22
+ '6.0.0'
23
+ )
24
+
25
+ @io = ensure_io(config.out_stream, config.error_stream)
26
+ @ast_lookup = AstLookup.new(config)
17
27
  @feature_hashes = []
18
28
  @step_or_hook_hash = {}
19
29
  config.on_event :test_case_started, &method(:on_test_case_started)
@@ -25,16 +35,18 @@ module Cucumber
25
35
 
26
36
  def on_test_case_started(event)
27
37
  test_case = event.test_case
28
- builder = Builder.new(test_case)
29
- unless same_feature_as_previous_test_case?(test_case.feature)
38
+ builder = Builder.new(test_case, @ast_lookup)
39
+ unless same_feature_as_previous_test_case?(test_case)
30
40
  @feature_hash = builder.feature_hash
31
41
  @feature_hashes << @feature_hash
32
42
  end
33
43
  @test_case_hash = builder.test_case_hash
34
44
  if builder.background?
45
+ @in_background = true
35
46
  feature_elements << builder.background_hash
36
47
  @element_hash = builder.background_hash
37
48
  else
49
+ @in_background = false
38
50
  feature_elements << @test_case_hash
39
51
  @element_hash = @test_case_hash
40
52
  end
@@ -44,17 +56,17 @@ module Cucumber
44
56
  def on_test_step_started(event)
45
57
  test_step = event.test_step
46
58
  return if internal_hook?(test_step)
47
- hook_query = HookQueryVisitor.new(test_step)
48
- if hook_query.hook?
59
+ if test_step.hook?
49
60
  @step_or_hook_hash = {}
50
- hooks_of_type(hook_query) << @step_or_hook_hash
61
+ hooks_of_type(test_step) << @step_or_hook_hash
51
62
  return
52
63
  end
53
64
  if first_step_after_background?(test_step)
65
+ @in_background = false
54
66
  feature_elements << @test_case_hash
55
67
  @element_hash = @test_case_hash
56
68
  end
57
- @step_or_hook_hash = create_step_hash(test_step.source.last)
69
+ @step_or_hook_hash = create_step_hash(test_step)
58
70
  steps << @step_or_hook_hash
59
71
  @step_hash = @step_or_hook_hash
60
72
  end
@@ -74,44 +86,42 @@ module Cucumber
74
86
  end
75
87
 
76
88
  def on_test_run_finished(_event)
77
- @io.write(MultiJson.dump(@feature_hashes, pretty: true))
89
+ @io.write(JSON.generate(@feature_hashes, pretty: true))
78
90
  end
79
91
 
80
- def puts(message)
81
- test_step_output << message
82
- end
83
-
84
- def embed(src, mime_type, _label)
92
+ def attach(src, mime_type)
93
+ if mime_type == 'text/x.cucumber.log+plain'
94
+ test_step_output << src
95
+ return
96
+ end
85
97
  if File.file?(src)
86
98
  content = File.open(src, 'rb', &:read)
87
99
  data = encode64(content)
100
+ elsif mime_type =~ /;base64$/
101
+ mime_type = mime_type[0..-8]
102
+ data = src
88
103
  else
89
- if mime_type =~ /;base64$/
90
- mime_type = mime_type[0..-8]
91
- data = src
92
- else
93
- data = encode64(src)
94
- end
104
+ data = encode64(src)
95
105
  end
96
106
  test_step_embeddings << { mime_type: mime_type, data: data }
97
107
  end
98
108
 
99
109
  private
100
110
 
101
- def same_feature_as_previous_test_case?(feature)
102
- current_feature[:uri] == feature.file && current_feature[:line] == feature.location.line
111
+ def same_feature_as_previous_test_case?(test_case)
112
+ current_feature[:uri] == test_case.location.file
103
113
  end
104
114
 
105
115
  def first_step_after_background?(test_step)
106
- test_step.source[1].to_s != @element_hash[:name]
116
+ @in_background && test_step.location.lines.max >= @test_case_hash[:line]
107
117
  end
108
118
 
109
119
  def internal_hook?(test_step)
110
- test_step.source.last.location.file.include?('lib/cucumber/')
120
+ test_step.location.file.include?('lib/cucumber/')
111
121
  end
112
122
 
113
123
  def current_feature
114
- @feature_hash ||= {}
124
+ @feature_hash ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
115
125
  end
116
126
 
117
127
  def feature_elements
@@ -122,16 +132,16 @@ module Cucumber
122
132
  @element_hash[:steps] ||= []
123
133
  end
124
134
 
125
- def hooks_of_type(hook_query)
126
- case hook_query.type
127
- when :before
128
- return before_hooks
129
- when :after
130
- return after_hooks
131
- when :after_step
132
- return after_step_hooks
135
+ def hooks_of_type(hook_step)
136
+ case hook_step.text
137
+ when 'Before hook'
138
+ before_hooks
139
+ when 'After hook'
140
+ after_hooks
141
+ when 'AfterStep hook'
142
+ after_step_hooks
133
143
  else
134
- fail 'Unknown hook type ' + hook_query.type.to_s
144
+ raise 'Unknown hook type ' + hook_step.to_s
135
145
  end
136
146
  end
137
147
 
@@ -159,20 +169,20 @@ module Cucumber
159
169
  @step_or_hook_hash[:embeddings] ||= []
160
170
  end
161
171
 
162
- def create_step_hash(step_source)
172
+ def create_step_hash(test_step)
173
+ step_source = @ast_lookup.step_source(test_step).step
163
174
  step_hash = {
164
175
  keyword: step_source.keyword,
165
- name: step_source.to_s,
166
- line: step_source.original_location.line
176
+ name: test_step.text,
177
+ line: test_step.location.lines.min
167
178
  }
168
- step_hash[:comments] = Formatter.create_comments_array(step_source.comments) unless step_source.comments.empty?
169
- step_hash[:doc_string] = create_doc_string_hash(step_source.multiline_arg) if step_source.multiline_arg.doc_string?
170
- step_hash[:rows] = create_data_table_value(step_source.multiline_arg) if step_source.multiline_arg.data_table?
179
+ step_hash[:doc_string] = create_doc_string_hash(step_source.doc_string) unless step_source.doc_string.nil?
180
+ step_hash[:rows] = create_data_table_value(step_source.data_table) unless step_source.data_table.nil?
171
181
  step_hash
172
182
  end
173
183
 
174
184
  def create_doc_string_hash(doc_string)
175
- content_type = doc_string.content_type ? doc_string.content_type : ''
185
+ content_type = doc_string.media_type || ''
176
186
  {
177
187
  value: doc_string.content,
178
188
  content_type: content_type,
@@ -181,14 +191,15 @@ module Cucumber
181
191
  end
182
192
 
183
193
  def create_data_table_value(data_table)
184
- data_table.raw.map do |row|
185
- { cells: row }
194
+ data_table.rows.map do |row|
195
+ { cells: row.cells.map(&:value) }
186
196
  end
187
197
  end
188
198
 
189
199
  def add_match_and_result(test_step, result)
190
200
  @step_or_hook_hash[:match] = create_match_hash(test_step, result)
191
201
  @step_or_hook_hash[:result] = create_result_hash(result)
202
+ result.embeddings.each { |e| embed(e['src'], e['mime_type'], e['label']) } if result.respond_to?(:embeddings)
192
203
  end
193
204
 
194
205
  def add_failed_around_hook(result)
@@ -226,113 +237,94 @@ module Cucumber
226
237
  class Builder
227
238
  attr_reader :feature_hash, :background_hash, :test_case_hash
228
239
 
229
- def initialize(test_case)
240
+ def initialize(test_case, ast_lookup)
230
241
  @background_hash = nil
231
- test_case.describe_source_to(self)
232
- test_case.feature.background.describe_to(self)
242
+ uri = test_case.location.file
243
+ feature = ast_lookup.gherkin_document(uri).feature
244
+ feature(feature, uri)
245
+ background(feature.children.first.background) unless feature.children.first.background.nil?
246
+ scenario(ast_lookup.scenario_source(test_case), test_case)
233
247
  end
234
248
 
235
249
  def background?
236
250
  @background_hash != nil
237
251
  end
238
252
 
239
- def feature(feature)
253
+ def feature(feature, uri)
240
254
  @feature_hash = {
241
- uri: feature.file,
242
- id: create_id(feature),
255
+ id: create_id(feature.name),
256
+ uri: uri,
243
257
  keyword: feature.keyword,
244
- name: feature.to_s,
245
- description: feature.description,
258
+ name: feature.name,
259
+ description: value_or_empty_string(feature.description),
246
260
  line: feature.location.line
247
261
  }
248
- unless feature.tags.empty?
249
- @feature_hash[:tags] = create_tags_array(feature.tags)
250
- @test_case_hash[:tags] = if @test_case_hash[:tags]
251
- @feature_hash[:tags] + @test_case_hash[:tags]
252
- else
253
- @feature_hash[:tags]
254
- end
255
- end
256
- @feature_hash[:comments] = Formatter.create_comments_array(feature.comments) unless feature.comments.empty?
257
- @test_case_hash[:id].insert(0, @feature_hash[:id] + ';')
262
+ return if feature.tags.empty?
263
+ @feature_hash[:tags] = create_tags_array_from_hash_array(feature.tags)
258
264
  end
259
265
 
260
266
  def background(background)
261
267
  @background_hash = {
262
268
  keyword: background.keyword,
263
- name: background.to_s,
264
- description: background.description,
269
+ name: background.name,
270
+ description: value_or_empty_string(background.description),
265
271
  line: background.location.line,
266
272
  type: 'background'
267
273
  }
268
- @background_hash[:comments] = Formatter.create_comments_array(background.comments) unless background.comments.empty?
269
274
  end
270
275
 
271
- def scenario(scenario)
276
+ def scenario(scenario_source, test_case)
277
+ scenario = scenario_source.type == :Scenario ? scenario_source.scenario : scenario_source.scenario_outline
272
278
  @test_case_hash = {
273
- id: create_id(scenario),
279
+ id: "#{@feature_hash[:id]};#{create_id_from_scenario_source(scenario_source)}",
274
280
  keyword: scenario.keyword,
275
- name: scenario.to_s,
276
- description: scenario.description,
277
- line: scenario.location.line,
281
+ name: test_case.name,
282
+ description: value_or_empty_string(scenario.description),
283
+ line: test_case.location.lines.max,
278
284
  type: 'scenario'
279
285
  }
280
- @test_case_hash[:tags] = create_tags_array(scenario.tags) unless scenario.tags.empty?
281
- @test_case_hash[:comments] = Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty?
286
+ @test_case_hash[:tags] = create_tags_array_from_tags_array(test_case.tags) unless test_case.tags.empty?
282
287
  end
283
288
 
284
- def scenario_outline(scenario)
285
- @test_case_hash = {
286
- id: create_id(scenario) + ';' + @example_id,
287
- keyword: scenario.keyword,
288
- name: scenario.to_s,
289
- description: scenario.description,
290
- line: @row.location.line,
291
- type: 'scenario'
292
- }
293
- tags = []
294
- tags += create_tags_array(scenario.tags) unless scenario.tags.empty?
295
- tags += @examples_table_tags if @examples_table_tags
296
- @test_case_hash[:tags] = tags unless tags.empty?
297
- comments = []
298
- comments += Formatter.create_comments_array(scenario.comments) unless scenario.comments.empty?
299
- comments += @examples_table_comments if @examples_table_comments
300
- comments += @row_comments if @row_comments
301
- @test_case_hash[:comments] = comments unless comments.empty?
302
- end
289
+ private
303
290
 
304
- def examples_table(examples_table)
305
- # the json file have traditionally used the header row as row 1,
306
- # wheras cucumber-ruby-core used the first example row as row 1.
307
- @example_id = create_id(examples_table) + ";#{@row.number + 1}"
291
+ def value_or_empty_string(value)
292
+ value.nil? ? '' : value
293
+ end
308
294
 
309
- @examples_table_tags = create_tags_array(examples_table.tags) unless examples_table.tags.empty?
310
- @examples_table_comments = Formatter.create_comments_array(examples_table.comments) unless examples_table.comments.empty?
295
+ def create_id(name)
296
+ name.downcase.tr(' ', '-')
311
297
  end
312
298
 
313
- def examples_table_row(row)
314
- @row = row
315
- @row_comments = Formatter.create_comments_array(row.comments) unless row.comments.empty?
299
+ def create_id_from_scenario_source(scenario_source)
300
+ if scenario_source.type == :Scenario
301
+ create_id(scenario_source.scenario.name)
302
+ else
303
+ scenario_outline_name = scenario_source.scenario_outline.name
304
+ examples_name = scenario_source.examples.name
305
+ row_number = calculate_row_number(scenario_source)
306
+ "#{create_id(scenario_outline_name)};#{create_id(examples_name)};#{row_number}"
307
+ end
316
308
  end
317
309
 
318
- private
310
+ def calculate_row_number(scenario_source)
311
+ scenario_source.examples.table_body.each_with_index do |row, index|
312
+ return index + 2 if row == scenario_source.row
313
+ end
314
+ end
319
315
 
320
- def create_id(element)
321
- element.to_s.downcase.tr(' ', '-')
316
+ def create_tags_array_from_hash_array(tags)
317
+ tags_array = []
318
+ tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
319
+ tags_array
322
320
  end
323
321
 
324
- def create_tags_array(tags)
322
+ def create_tags_array_from_tags_array(tags)
325
323
  tags_array = []
326
324
  tags.each { |tag| tags_array << { name: tag.name, line: tag.location.line } }
327
325
  tags_array
328
326
  end
329
327
  end
330
328
  end
331
-
332
- def self.create_comments_array(comments)
333
- comments_array = []
334
- comments.each { |comment| comments_array << { value: comment.to_s.strip, line: comment.location.line } }
335
- comments_array
336
- end
337
329
  end
338
330
  end