cucumber 4.1.0 → 9.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +134 -21
  3. data/VERSION +1 -0
  4. data/lib/cucumber/cli/configuration.rb +27 -2
  5. data/lib/cucumber/cli/main.rb +5 -4
  6. data/lib/cucumber/cli/options.rb +102 -65
  7. data/lib/cucumber/cli/profile_loader.rb +6 -10
  8. data/lib/cucumber/cli/rerun_file.rb +1 -1
  9. data/lib/cucumber/configuration.rb +26 -13
  10. data/lib/cucumber/constantize.rb +1 -1
  11. data/lib/cucumber/deprecate.rb +6 -46
  12. data/lib/cucumber/errors.rb +4 -3
  13. data/lib/cucumber/events/envelope.rb +2 -0
  14. data/lib/cucumber/events/gherkin_source_parsed.rb +2 -0
  15. data/lib/cucumber/events/gherkin_source_read.rb +2 -0
  16. data/lib/cucumber/events/hook_test_step_created.rb +1 -2
  17. data/lib/cucumber/events/step_activated.rb +0 -6
  18. data/lib/cucumber/events/step_definition_registered.rb +0 -5
  19. data/lib/cucumber/events/test_case_created.rb +1 -2
  20. data/lib/cucumber/events/test_case_finished.rb +2 -0
  21. data/lib/cucumber/events/test_case_started.rb +2 -0
  22. data/lib/cucumber/events/test_run_finished.rb +2 -1
  23. data/lib/cucumber/events/test_step_created.rb +1 -2
  24. data/lib/cucumber/events/test_step_finished.rb +2 -0
  25. data/lib/cucumber/events/test_step_started.rb +2 -0
  26. data/lib/cucumber/events/undefined_parameter_type.rb +3 -2
  27. data/lib/cucumber/events.rb +2 -2
  28. data/lib/cucumber/file_specs.rb +2 -1
  29. data/lib/cucumber/filters/activate_steps.rb +1 -0
  30. data/lib/cucumber/filters/retry.rb +20 -1
  31. data/lib/cucumber/filters/tag_limits/verifier.rb +1 -3
  32. data/lib/cucumber/filters/tag_limits.rb +1 -3
  33. data/lib/cucumber/formatter/ansicolor.rb +70 -85
  34. data/lib/cucumber/formatter/ast_lookup.rb +16 -10
  35. data/lib/cucumber/formatter/backtrace_filter.rb +3 -1
  36. data/lib/cucumber/formatter/console.rb +34 -16
  37. data/lib/cucumber/formatter/console_counts.rb +3 -1
  38. data/lib/cucumber/formatter/console_issues.rb +10 -3
  39. data/lib/cucumber/formatter/curl_option_parser.rb +49 -0
  40. data/lib/cucumber/formatter/duration_extractor.rb +1 -0
  41. data/lib/cucumber/formatter/errors.rb +3 -0
  42. data/lib/cucumber/formatter/fail_fast.rb +1 -1
  43. data/lib/cucumber/formatter/fanout.rb +1 -1
  44. data/lib/cucumber/formatter/html.rb +3 -1
  45. data/lib/cucumber/formatter/http_io.rb +10 -136
  46. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  47. data/lib/cucumber/formatter/interceptor.rb +4 -3
  48. data/lib/cucumber/formatter/io.rb +50 -12
  49. data/lib/cucumber/formatter/io_http_buffer.rb +88 -0
  50. data/lib/cucumber/formatter/json.rb +36 -37
  51. data/lib/cucumber/formatter/junit.rb +29 -9
  52. data/lib/cucumber/formatter/message.rb +3 -2
  53. data/lib/cucumber/formatter/message_builder.rb +32 -16
  54. data/lib/cucumber/formatter/pretty.rb +44 -29
  55. data/lib/cucumber/formatter/progress.rb +2 -1
  56. data/lib/cucumber/formatter/publish_banner_printer.rb +75 -0
  57. data/lib/cucumber/formatter/query/hook_by_test_step.rb +3 -0
  58. data/lib/cucumber/formatter/query/pickle_by_test.rb +2 -0
  59. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +2 -0
  60. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +2 -0
  61. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +4 -0
  62. data/lib/cucumber/formatter/rerun.rb +7 -5
  63. data/lib/cucumber/formatter/steps.rb +6 -3
  64. data/lib/cucumber/formatter/summary.rb +2 -1
  65. data/lib/cucumber/formatter/unicode.rb +7 -7
  66. data/lib/cucumber/formatter/url_reporter.rb +19 -0
  67. data/lib/cucumber/formatter/usage.rb +9 -7
  68. data/lib/cucumber/gherkin/data_table_parser.rb +2 -1
  69. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +25 -27
  70. data/lib/cucumber/gherkin/steps_parser.rb +1 -1
  71. data/lib/cucumber/glue/dsl.rb +30 -16
  72. data/lib/cucumber/glue/hook.rb +6 -3
  73. data/lib/cucumber/glue/invoke_in_world.rb +5 -5
  74. data/lib/cucumber/glue/proto_world.rb +37 -57
  75. data/lib/cucumber/glue/registry_and_more.rb +31 -12
  76. data/lib/cucumber/glue/registry_wrapper.rb +31 -0
  77. data/lib/cucumber/glue/snippet.rb +4 -2
  78. data/lib/cucumber/glue/step_definition.rb +9 -7
  79. data/lib/cucumber/glue/world_factory.rb +2 -0
  80. data/lib/cucumber/hooks.rb +1 -0
  81. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +5 -2
  82. data/lib/cucumber/multiline_argument/data_table.rb +65 -79
  83. data/lib/cucumber/platform.rb +11 -16
  84. data/lib/cucumber/rake/task.rb +22 -17
  85. data/lib/cucumber/rspec/disable_option_parser.rb +6 -3
  86. data/lib/cucumber/rspec/doubles.rb +3 -5
  87. data/lib/cucumber/running_test_case.rb +2 -1
  88. data/lib/cucumber/runtime/for_programming_languages.rb +1 -2
  89. data/lib/cucumber/runtime/meta_message_builder.rb +108 -0
  90. data/lib/cucumber/runtime/support_code.rb +3 -0
  91. data/lib/cucumber/runtime/user_interface.rb +7 -6
  92. data/lib/cucumber/runtime.rb +50 -24
  93. data/lib/cucumber/step_match.rb +7 -11
  94. data/lib/cucumber/step_match_search.rb +3 -2
  95. data/lib/cucumber/term/ansicolor.rb +74 -50
  96. data/lib/cucumber/term/banner.rb +59 -0
  97. data/lib/cucumber.rb +2 -1
  98. data/lib/simplecov_setup.rb +1 -1
  99. metadata +103 -229
  100. data/CHANGELOG.md +0 -2682
  101. data/CONTRIBUTING.md +0 -71
  102. data/lib/autotest/cucumber.rb +0 -8
  103. data/lib/autotest/cucumber_mixin.rb +0 -132
  104. data/lib/autotest/cucumber_rails.rb +0 -8
  105. data/lib/autotest/cucumber_rails_rspec.rb +0 -8
  106. data/lib/autotest/cucumber_rails_rspec2.rb +0 -8
  107. data/lib/autotest/cucumber_rspec.rb +0 -8
  108. data/lib/autotest/cucumber_rspec2.rb +0 -8
  109. data/lib/autotest/discover.rb +0 -13
  110. data/lib/cucumber/core_ext/string.rb +0 -11
  111. data/lib/cucumber/version +0 -1
@@ -1,145 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/http'
2
4
  require 'tempfile'
5
+ require_relative 'curl_option_parser'
6
+ require_relative 'io_http_buffer'
3
7
 
4
8
  module Cucumber
5
9
  module Formatter
6
10
  class HTTPIO
7
- class << self
8
- # Returns an IO that will write to a HTTP request's body
9
- def open(url, https_verify_mode = nil)
10
- @https_verify_mode = https_verify_mode
11
- uri, method, headers = CurlOptionParser.parse(url)
12
- IOHTTPBuffer.new(uri, method, headers, https_verify_mode)
13
- end
14
- end
15
- end
16
-
17
- class CurlOptionParser
18
- def self.parse(options)
19
- chunks = options.split(/\s/).compact
20
- http_method = 'PUT'
21
- url = chunks[0]
22
- headers = ''
23
-
24
- last_flag = nil
25
- chunks.each do |chunk|
26
- if ['-X', '--request'].include?(chunk)
27
- last_flag = '-X'
28
- next
29
- end
30
-
31
- if chunk == '-H'
32
- last_flag = '-H'
33
- next
34
- end
35
-
36
- if last_flag == '-X'
37
- http_method = chunk
38
- last_flag = nil
39
- end
40
-
41
- headers += chunk if last_flag == '-H'
42
- end
43
-
44
- [
45
- url,
46
- http_method,
47
- make_headers(headers)
48
- ]
49
- end
50
-
51
- def self.make_headers(headers)
52
- hash_headers = {}
53
- str_scanner = /("(?<key>[^":]+)\s*:\s*(?<value>[^":]+)")|('(?<key1>[^':]+)\s*:\s*(?<value1>[^':]+)')/
54
-
55
- headers.scan(str_scanner) do |header|
56
- header = header.compact!
57
- hash_headers[header[0]] = header[1]&.strip
58
- end
59
-
60
- hash_headers
61
- end
62
- end
63
-
64
- class IOHTTPBuffer
65
- attr_reader :uri, :method, :headers
66
-
67
- def initialize(uri, method, headers = {}, https_verify_mode = nil)
68
- @uri = URI(uri)
69
- @method = method
70
- @headers = headers
71
- @write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
72
- @https_verify_mode = https_verify_mode
73
- end
74
-
75
- def close
76
- post_content(@uri, @method, @headers)
77
- @write_io.close
78
- end
79
-
80
- def write(data)
81
- @write_io.write(data)
82
- end
83
-
84
- def flush
85
- @write_io.flush
86
- end
87
-
88
- def closed?
89
- @write_io.closed?
90
- end
91
-
92
- private
93
-
94
- def post_content(uri, method, headers, attempt = 10)
95
- content = @write_io
96
- http = build_client(uri, @https_verify_mode)
97
-
98
- raise StandardError, "request to #{uri} failed (too many redirections)" if attempt <= 0
99
- req = build_request(
100
- uri,
101
- method,
102
- headers.merge(
103
- 'Content-Length' => content.size.to_s
104
- )
105
- )
106
-
107
- content.rewind
108
- req.body_stream = content
109
-
110
- begin
111
- response = http.request(req)
112
- rescue SystemCallError
113
- # We may get the redirect response before pushing the file.
114
- response = http.request(build_request(uri, method, headers))
115
- end
116
-
117
- case response
118
- when Net::HTTPSuccess
119
- response
120
- when Net::HTTPRedirection
121
- post_content(URI(response['Location']), method, headers, attempt - 1)
122
- else
123
- raise StandardError, "request to #{uri} failed with status #{response.code}"
124
- end
125
- end
126
-
127
- def build_request(uri, method, headers)
128
- method_class_name = "#{method[0].upcase}#{method[1..-1].downcase}"
129
- req = Net::HTTP.const_get(method_class_name).new(uri)
130
- headers.each do |header, value|
131
- req[header] = value
132
- end
133
- req
134
- end
135
-
136
- def build_client(uri, https_verify_mode)
137
- http = Net::HTTP.new(uri.hostname, uri.port)
138
- if uri.scheme == 'https'
139
- http.use_ssl = true
140
- http.verify_mode = https_verify_mode if https_verify_mode
141
- end
142
- http
11
+ # Returns an IO that will write to a HTTP request's body
12
+ # https_verify_mode can be set to OpenSSL::SSL::VERIFY_NONE
13
+ # to ignore unsigned certificate - setting to nil will verify the certificate
14
+ def self.open(url, https_verify_mode = nil, reporter = nil)
15
+ uri, method, headers = CurlOptionParser.parse(url)
16
+ IOHTTPBuffer.new(uri, method, headers, https_verify_mode, reporter)
143
17
  end
144
18
  end
145
19
  end
@@ -12,7 +12,7 @@ module Cucumber
12
12
  end
13
13
 
14
14
  def respond_to_missing?(name, include_private = false)
15
- @receiver.respond_to?(name, include_private)
15
+ @receiver.respond_to?(name, include_private) || super(name, include_private)
16
16
  end
17
17
  end
18
18
  end
@@ -5,6 +5,7 @@ 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
@@ -31,7 +32,7 @@ module Cucumber
31
32
  end
32
33
 
33
34
  def method_missing(method, *args, &blk)
34
- @pipe.send(method, *args, &blk) || super
35
+ @pipe.respond_to?(method) ? @pipe.send(method, *args, &blk) : super
35
36
  end
36
37
 
37
38
  def respond_to_missing?(method, include_private = false)
@@ -62,10 +63,10 @@ module Cucumber
62
63
  case pipe
63
64
  when :stderr
64
65
  $stderr = new($stderr)
65
- return $stderr
66
+ $stderr
66
67
  when :stdout
67
68
  $stdout = new($stdout)
68
- return $stdout
69
+ $stdout
69
70
  end
70
71
  end
71
72
  end
@@ -1,39 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cucumber/formatter/http_io'
4
+ require 'cucumber/formatter/url_reporter'
5
+ require 'cucumber/cli/options'
4
6
 
5
7
  module Cucumber
6
8
  module Formatter
7
9
  module Io
8
10
  module_function
9
11
 
10
- def ensure_io(path_or_url_or_io)
12
+ def ensure_io(path_or_url_or_io, error_stream)
11
13
  return nil if path_or_url_or_io.nil?
12
- return path_or_url_or_io if path_or_url_or_io.respond_to?(:write)
13
- io = if path_or_url_or_io.match(%r{^https?://})
14
- HTTPIO.open(path_or_url_or_io)
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)
15
20
  else
16
21
  File.open(path_or_url_or_io, Cucumber.file_mode('w'))
17
22
  end
18
- at_exit do
19
- unless io.closed?
20
- io.flush
21
- io.close
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
22
45
  end
46
+
47
+ instance
23
48
  end
24
- io
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(/^https?:\/\//)
25
61
  end
26
62
 
27
63
  def ensure_file(path, name)
28
- raise "You *must* specify --out FILE for the #{name} formatter" unless String == path.class
64
+ raise "You *must* specify --out FILE for the #{name} formatter" unless path.instance_of? String
29
65
  raise "I can't write #{name} to a directory - it has to be a file" if File.directory?(path)
30
66
  raise "I can't write #{name} to a file in the non-existing directory #{File.dirname(path)}" unless File.directory?(File.dirname(path))
31
- ensure_io(path)
67
+
68
+ ensure_io(path, nil)
32
69
  end
33
70
 
34
71
  def ensure_dir(path, name)
35
- raise "You *must* specify --out DIR for the #{name} formatter" unless String == path.class
72
+ raise "You *must* specify --out DIR for the #{name} formatter" unless path.instance_of? String
36
73
  raise "I can't write #{name} reports to a file - it has to be a directory" if File.file?(path)
74
+
37
75
  FileUtils.mkdir_p(path) unless File.directory?(path)
38
76
  File.absolute_path path
39
77
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cucumber
4
+ module Formatter
5
+ class IOHTTPBuffer
6
+ attr_reader :uri, :method, :headers
7
+
8
+ def initialize(uri, method, headers = {}, https_verify_mode = nil, reporter = nil)
9
+ @uri = URI(uri)
10
+ @method = method
11
+ @headers = headers
12
+ @write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
13
+ @https_verify_mode = https_verify_mode
14
+ @reporter = reporter || NoReporter.new
15
+ end
16
+
17
+ def close
18
+ @reporter.report(response.body)
19
+ @write_io.close
20
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
21
+
22
+ raise StandardError, "request to #{uri} failed with status #{response.code}"
23
+ end
24
+
25
+ def write(data)
26
+ @write_io.write(data)
27
+ end
28
+
29
+ def flush
30
+ @write_io.flush
31
+ end
32
+
33
+ def closed?
34
+ @write_io.closed?
35
+ end
36
+
37
+ private
38
+
39
+ def response
40
+ @response ||= send_content(uri, method, headers)
41
+ end
42
+
43
+ def send_content(uri, method, headers, attempts_remaining = 10)
44
+ content = (method == 'GET' ? StringIO.new : @write_io)
45
+ http = build_client(uri)
46
+
47
+ raise StandardError, "request to #{uri} failed (too many redirections)" if attempts_remaining <= 0
48
+
49
+ request = build_request(uri, method, headers.merge('Content-Length' => content.size.to_s))
50
+ content.rewind
51
+ request.body_stream = content
52
+
53
+ begin
54
+ response = http.request(request)
55
+ rescue SystemCallError
56
+ # We may get the redirect response before pushing the file.
57
+ response = http.request(build_request(uri, method, headers))
58
+ end
59
+
60
+ case response
61
+ when Net::HTTPAccepted
62
+ send_content(URI(response['Location']), 'PUT', {}, attempts_remaining - 1) if response['Location']
63
+ when Net::HTTPRedirection
64
+ send_content(URI(response['Location']), method, headers, attempts_remaining - 1)
65
+ end
66
+ response
67
+ end
68
+
69
+ def build_request(uri, method, headers)
70
+ method_class_name = "#{method[0].upcase}#{method[1..].downcase}"
71
+ Net::HTTP.const_get(method_class_name).new(uri).tap do |request|
72
+ headers.each do |header, value|
73
+ request[header] = value
74
+ end
75
+ end
76
+ end
77
+
78
+ def build_client(uri)
79
+ Net::HTTP.new(uri.hostname, uri.port).tap do |http|
80
+ if uri.scheme == 'https'
81
+ http.use_ssl = true
82
+ http.verify_mode = @https_verify_mode if @https_verify_mode
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -5,7 +5,6 @@ require 'base64'
5
5
  require 'cucumber/formatter/backtrace_filter'
6
6
  require 'cucumber/formatter/io'
7
7
  require 'cucumber/formatter/ast_lookup'
8
- require 'cucumber/deprecate'
9
8
 
10
9
  module Cucumber
11
10
  module Formatter
@@ -14,15 +13,7 @@ module Cucumber
14
13
  include Io
15
14
 
16
15
  def initialize(config)
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
- '5.0.0'
23
- )
24
-
25
- @io = ensure_io(config.out_stream)
16
+ @io = ensure_io(config.out_stream, config.error_stream)
26
17
  @ast_lookup = AstLookup.new(config)
27
18
  @feature_hashes = []
28
19
  @step_or_hook_hash = {}
@@ -41,21 +32,23 @@ module Cucumber
41
32
  @feature_hashes << @feature_hash
42
33
  end
43
34
  @test_case_hash = builder.test_case_hash
44
- if builder.background?
45
- @in_background = true
46
- feature_elements << builder.background_hash
47
- @element_hash = builder.background_hash
48
- else
49
- @in_background = false
50
- feature_elements << @test_case_hash
51
- @element_hash = @test_case_hash
52
- end
35
+
36
+ @element_hash = nil
37
+ @element_background_hash = builder.background_hash
38
+ @in_background = builder.background?
39
+
53
40
  @any_step_failed = false
54
41
  end
55
42
 
56
43
  def on_test_step_started(event)
57
44
  test_step = event.test_step
58
45
  return if internal_hook?(test_step)
46
+
47
+ if @element_hash.nil?
48
+ @element_hash = create_element_hash(test_step)
49
+ feature_elements << @element_hash
50
+ end
51
+
59
52
  if test_step.hook?
60
53
  @step_or_hook_hash = {}
61
54
  hooks_of_type(test_step) << @step_or_hook_hash
@@ -75,29 +68,29 @@ module Cucumber
75
68
  test_step, result = *event.attributes
76
69
  result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
77
70
  return if internal_hook?(test_step)
71
+
78
72
  add_match_and_result(test_step, result)
79
73
  @any_step_failed = true if result.failed?
80
74
  end
81
75
 
82
76
  def on_test_case_finished(event)
77
+ feature_elements << @test_case_hash if @in_background
78
+
83
79
  _test_case, result = *event.attributes
84
80
  result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
85
81
  add_failed_around_hook(result) if result.failed? && !@any_step_failed
86
82
  end
87
83
 
88
84
  def on_test_run_finished(_event)
89
- @io.write(JSON.generate(@feature_hashes, pretty: true))
85
+ @io.write(JSON.pretty_generate(@feature_hashes))
90
86
  end
91
87
 
92
- def attach(src, mime_type)
88
+ def attach(src, mime_type, _filename)
93
89
  if mime_type == 'text/x.cucumber.log+plain'
94
90
  test_step_output << src
95
91
  return
96
92
  end
97
- if File.file?(src)
98
- content = File.open(src, 'rb', &:read)
99
- data = encode64(content)
100
- elsif mime_type =~ /;base64$/
93
+ if mime_type =~ /;base64$/
101
94
  mime_type = mime_type[0..-8]
102
95
  data = src
103
96
  else
@@ -109,21 +102,17 @@ module Cucumber
109
102
  private
110
103
 
111
104
  def same_feature_as_previous_test_case?(test_case)
112
- current_feature[:uri] == test_case.location.file
105
+ @feature_hash&.fetch(:uri, nil) == test_case.location.file
113
106
  end
114
107
 
115
108
  def first_step_after_background?(test_step)
116
- @in_background && test_step.location.lines.max >= @test_case_hash[:line]
109
+ @in_background && test_step.location.file == @feature_hash[:uri] && test_step.location.lines.max >= @test_case_hash[:line]
117
110
  end
118
111
 
119
112
  def internal_hook?(test_step)
120
113
  test_step.location.file.include?('lib/cucumber/')
121
114
  end
122
115
 
123
- def current_feature
124
- @feature_hash ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
125
- end
126
-
127
116
  def feature_elements
128
117
  @feature_hash[:elements] ||= []
129
118
  end
@@ -141,7 +130,7 @@ module Cucumber
141
130
  when 'AfterStep hook'
142
131
  after_step_hooks
143
132
  else
144
- raise 'Unknown hook type ' + hook_step.to_s
133
+ raise "Unknown hook type #{hook_step}"
145
134
  end
146
135
  end
147
136
 
@@ -169,6 +158,13 @@ module Cucumber
169
158
  @step_or_hook_hash[:embeddings] ||= []
170
159
  end
171
160
 
161
+ def create_element_hash(test_step)
162
+ return @element_background_hash if @in_background && !first_step_after_background?(test_step)
163
+
164
+ @in_background = false
165
+ @test_case_hash
166
+ end
167
+
172
168
  def create_step_hash(test_step)
173
169
  step_source = @ast_lookup.step_source(test_step).step
174
170
  step_hash = {
@@ -176,15 +172,15 @@ module Cucumber
176
172
  name: test_step.text,
177
173
  line: test_step.location.lines.min
178
174
  }
179
- step_hash[:doc_string] = create_doc_string_hash(step_source.doc_string) unless step_source.doc_string.nil?
175
+ step_hash[:doc_string] = create_doc_string_hash(step_source.doc_string, test_step.multiline_arg.content) unless step_source.doc_string.nil?
180
176
  step_hash[:rows] = create_data_table_value(step_source.data_table) unless step_source.data_table.nil?
181
177
  step_hash
182
178
  end
183
179
 
184
- def create_doc_string_hash(doc_string)
180
+ def create_doc_string_hash(doc_string, doc_string_content)
185
181
  content_type = doc_string.media_type || ''
186
182
  {
187
- value: doc_string.content,
183
+ value: doc_string_content,
188
184
  content_type: content_type,
189
185
  line: doc_string.location.line
190
186
  }
@@ -260,6 +256,7 @@ module Cucumber
260
256
  line: feature.location.line
261
257
  }
262
258
  return if feature.tags.empty?
259
+
263
260
  @feature_hash[:tags] = create_tags_array_from_hash_array(feature.tags)
264
261
  end
265
262
 
@@ -269,7 +266,8 @@ module Cucumber
269
266
  name: background.name,
270
267
  description: value_or_empty_string(background.description),
271
268
  line: background.location.line,
272
- type: 'background'
269
+ type: 'background',
270
+ steps: []
273
271
  }
274
272
  end
275
273
 
@@ -281,7 +279,8 @@ module Cucumber
281
279
  name: test_case.name,
282
280
  description: value_or_empty_string(scenario.description),
283
281
  line: test_case.location.lines.max,
284
- type: 'scenario'
282
+ type: 'scenario',
283
+ steps: []
285
284
  }
286
285
  @test_case_hash[:tags] = create_tags_array_from_tags_array(test_case.tags) unless test_case.tags.empty?
287
286
  end
@@ -54,7 +54,7 @@ module Cucumber
54
54
  test_step, result = *event.attributes
55
55
  return if @failing_test_step
56
56
 
57
- @failing_test_step = test_step unless result.ok?(@config.strict)
57
+ @failing_test_step = test_step unless result.ok?(strict: @config.strict)
58
58
  end
59
59
 
60
60
  def on_test_case_finished(event)
@@ -71,7 +71,7 @@ module Cucumber
71
71
  end
72
72
 
73
73
  def on_test_run_finished(_event)
74
- @features_data.each { |_file, data| end_feature(data) }
74
+ @features_data.each_value { |data| end_feature(data) }
75
75
  end
76
76
 
77
77
  private
@@ -84,6 +84,7 @@ module Cucumber
84
84
  uri = test_case.location.file
85
85
  feature = @ast_lookup.gherkin_document(uri).feature
86
86
  raise UnNamedFeatureError, uri if feature.name.empty?
87
+
87
88
  @current_feature_data = @features_data[uri]
88
89
  @current_feature_data[:uri] = uri unless @current_feature_data[:uri]
89
90
  @current_feature_data[:feature] = feature unless @current_feature_data[:feature]
@@ -106,11 +107,12 @@ module Cucumber
106
107
  write_file(feature_result_filename(feature_data[:uri]), @testsuite.target!)
107
108
  end
108
109
 
109
- def create_output_string(test_case, scenario, result, row_name) # rubocop:disable Metrics/PerceivedComplexity
110
+ def create_output_string(test_case, scenario, result, row_name)
110
111
  scenario_source = @ast_lookup.scenario_source(test_case)
111
112
  keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
112
113
  output = "#{keyword}: #{scenario}\n\n"
113
- return output if result.ok?(@config.strict)
114
+ return output if result.ok?(strict: @config.strict)
115
+
114
116
  if scenario_source.type == :Scenario
115
117
  if @failing_test_step
116
118
  if @failing_test_step.hook?
@@ -125,17 +127,20 @@ module Cucumber
125
127
  else
126
128
  output += "Example row: #{row_name}\n"
127
129
  end
128
- output + "\nMessage:\n"
130
+ "#{output}\nMessage:\n"
129
131
  end
130
132
 
131
133
  def build_testcase(result, scenario_designation, output)
132
134
  duration = ResultBuilder.new(result).test_case_duration
133
135
  @current_feature_data[:time] += duration
134
136
  classname = @current_feature_data[:feature].name
137
+ filename = @current_feature_data[:uri]
135
138
  name = scenario_designation
136
139
 
137
- @current_feature_data[:builder].testcase(classname: classname, name: name, time: format('%<duration>.6f', duration: duration)) do
138
- if !result.passed? && result.ok?(@config.strict)
140
+ testcase_attributes = get_testcase_attributes(classname, name, duration, filename)
141
+
142
+ @current_feature_data[:builder].testcase(testcase_attributes) do
143
+ if !result.passed? && result.ok?(strict: @config.strict)
139
144
  @current_feature_data[:builder].skipped
140
145
  @current_feature_data[:skipped] += 1
141
146
  elsif !result.passed?
@@ -157,6 +162,20 @@ module Cucumber
157
162
  @current_feature_data[:tests] += 1
158
163
  end
159
164
 
165
+ def get_testcase_attributes(classname, name, duration, filename)
166
+ { classname: classname, name: name, time: format('%<duration>.6f', duration: duration) }.tap do |attributes|
167
+ attributes[:file] = filename if add_fileattribute?
168
+ end
169
+ end
170
+
171
+ def add_fileattribute?
172
+ return false if @config.formats.nil? || @config.formats.empty?
173
+
174
+ !!@config.formats.find do |format|
175
+ format.first == 'junit' && format.dig(1, 'fileattribute') == 'true'
176
+ end
177
+ end
178
+
160
179
  def get_backtrace_object(result)
161
180
  if result.failed?
162
181
  result.exception
@@ -174,7 +193,7 @@ module Cucumber
174
193
  end
175
194
 
176
195
  def basename(feature_file)
177
- File.basename(feature_file.gsub(/[\\\/]/, '-'), '.feature') # rubocop:disable Style/RegexpLiteral
196
+ File.basename(feature_file.gsub(/[\\\/]/, '-'), '.feature')
178
197
  end
179
198
 
180
199
  def write_file(feature_filename, data)
@@ -211,13 +230,14 @@ module Cucumber
211
230
  end
212
231
 
213
232
  def examples_table_row(row)
214
- @row_name = '| ' + row.cells.map(&:value).join(' | ') + ' |'
233
+ @row_name = "| #{row.cells.map(&:value).join(' | ')} |"
215
234
  @name_suffix = " (outline example : #{@row_name})"
216
235
  end
217
236
  end
218
237
 
219
238
  class ResultBuilder
220
239
  attr_reader :test_case_duration
240
+
221
241
  def initialize(result)
222
242
  @test_case_duration = 0
223
243
  result.describe_to(self)
@@ -10,12 +10,13 @@ module Cucumber
10
10
  include Io
11
11
 
12
12
  def initialize(config)
13
- @io = ensure_io(config.out_stream)
13
+ @io = ensure_io(config.out_stream, config.error_stream)
14
14
  super(config)
15
15
  end
16
16
 
17
17
  def output_envelope(envelope)
18
- envelope.write_ndjson_to(@io)
18
+ @io.write(envelope.to_json)
19
+ @io.write("\n")
19
20
  end
20
21
  end
21
22
  end