cucumber 7.1.0 → 9.2.1

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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -27
  3. data/VERSION +1 -0
  4. data/lib/cucumber/cli/configuration.rb +4 -1
  5. data/lib/cucumber/cli/main.rb +5 -4
  6. data/lib/cucumber/cli/options.rb +73 -67
  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 +12 -6
  10. data/lib/cucumber/constantize.rb +1 -1
  11. data/lib/cucumber/deprecate.rb +6 -46
  12. data/lib/cucumber/errors.rb +3 -2
  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 +1 -1
  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 -78
  34. data/lib/cucumber/formatter/ast_lookup.rb +16 -8
  35. data/lib/cucumber/formatter/backtrace_filter.rb +2 -1
  36. data/lib/cucumber/formatter/console.rb +26 -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 +2 -0
  45. data/lib/cucumber/formatter/http_io.rb +10 -137
  46. data/lib/cucumber/formatter/ignore_missing_messages.rb +1 -1
  47. data/lib/cucumber/formatter/io.rb +5 -3
  48. data/lib/cucumber/formatter/io_http_buffer.rb +88 -0
  49. data/lib/cucumber/formatter/json.rb +10 -12
  50. data/lib/cucumber/formatter/junit.rb +10 -7
  51. data/lib/cucumber/formatter/message_builder.rb +24 -8
  52. data/lib/cucumber/formatter/pretty.rb +24 -10
  53. data/lib/cucumber/formatter/progress.rb +1 -0
  54. data/lib/cucumber/formatter/publish_banner_printer.rb +0 -2
  55. data/lib/cucumber/formatter/query/hook_by_test_step.rb +3 -0
  56. data/lib/cucumber/formatter/query/pickle_by_test.rb +2 -0
  57. data/lib/cucumber/formatter/query/pickle_step_by_test_step.rb +2 -0
  58. data/lib/cucumber/formatter/query/step_definitions_by_test_step.rb +2 -0
  59. data/lib/cucumber/formatter/query/test_case_started_by_test_case.rb +4 -0
  60. data/lib/cucumber/formatter/rerun.rb +5 -3
  61. data/lib/cucumber/formatter/summary.rb +1 -0
  62. data/lib/cucumber/formatter/unicode.rb +7 -7
  63. data/lib/cucumber/formatter/url_reporter.rb +3 -1
  64. data/lib/cucumber/formatter/usage.rb +3 -3
  65. data/lib/cucumber/gherkin/data_table_parser.rb +1 -0
  66. data/lib/cucumber/gherkin/formatter/ansi_escapes.rb +25 -27
  67. data/lib/cucumber/glue/dsl.rb +20 -25
  68. data/lib/cucumber/glue/hook.rb +6 -3
  69. data/lib/cucumber/glue/invoke_in_world.rb +5 -5
  70. data/lib/cucumber/glue/proto_world.rb +30 -34
  71. data/lib/cucumber/glue/registry_and_more.rb +15 -25
  72. data/lib/cucumber/glue/snippet.rb +4 -2
  73. data/lib/cucumber/glue/step_definition.rb +6 -3
  74. data/lib/cucumber/glue/world_factory.rb +2 -0
  75. data/lib/cucumber/hooks.rb +1 -0
  76. data/lib/cucumber/multiline_argument/data_table/diff_matrices.rb +4 -1
  77. data/lib/cucumber/multiline_argument/data_table.rb +68 -80
  78. data/lib/cucumber/platform.rb +11 -16
  79. data/lib/cucumber/rake/task.rb +22 -15
  80. data/lib/cucumber/rspec/disable_option_parser.rb +6 -3
  81. data/lib/cucumber/running_test_case.rb +2 -1
  82. data/lib/cucumber/runtime/for_programming_languages.rb +1 -2
  83. data/lib/cucumber/runtime/meta_message_builder.rb +108 -0
  84. data/lib/cucumber/runtime/support_code.rb +3 -0
  85. data/lib/cucumber/runtime/user_interface.rb +7 -6
  86. data/lib/cucumber/runtime.rb +22 -38
  87. data/lib/cucumber/step_match.rb +6 -4
  88. data/lib/cucumber/step_match_search.rb +3 -2
  89. data/lib/cucumber/term/ansicolor.rb +74 -50
  90. data/lib/cucumber/term/banner.rb +3 -0
  91. data/lib/cucumber.rb +2 -1
  92. data/lib/simplecov_setup.rb +1 -1
  93. metadata +95 -244
  94. data/CHANGELOG.md +0 -3131
  95. data/CONTRIBUTING.md +0 -250
  96. data/lib/autotest/cucumber.rb +0 -8
  97. data/lib/autotest/cucumber_mixin.rb +0 -130
  98. data/lib/autotest/cucumber_rails.rb +0 -8
  99. data/lib/autotest/cucumber_rails_rspec.rb +0 -8
  100. data/lib/autotest/cucumber_rails_rspec2.rb +0 -8
  101. data/lib/autotest/cucumber_rspec.rb +0 -8
  102. data/lib/autotest/cucumber_rspec2.rb +0 -8
  103. data/lib/autotest/discover.rb +0 -13
  104. data/lib/cucumber/version +0 -1
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ModuleLength
4
-
5
3
  require 'cucumber/formatter/ansicolor'
6
4
  require 'cucumber/formatter/duration'
7
5
  require 'cucumber/gherkin/i18n'
@@ -34,7 +32,7 @@ module Cucumber
34
32
 
35
33
  def format_step(keyword, step_match, status, source_indent)
36
34
  comment = if source_indent
37
- c = indent(('# ' + step_match.location.to_s), source_indent)
35
+ c = indent("# #{step_match.location}", source_indent)
38
36
  format_string(c, :comment)
39
37
  else
40
38
  ''
@@ -45,10 +43,10 @@ module Cucumber
45
43
  format_string(line, status)
46
44
  end
47
45
 
48
- def format_string(o, status)
46
+ def format_string(input, status)
49
47
  fmt = format_for(status)
50
- o.to_s.split("\n").map do |line|
51
- if Proc == fmt.class
48
+ input.to_s.split("\n").map do |line|
49
+ if fmt.instance_of?(Proc)
52
50
  fmt.call(line)
53
51
  else
54
52
  fmt % line
@@ -94,22 +92,25 @@ module Cucumber
94
92
  @io.flush
95
93
  end
96
94
 
97
- def print_exception(e, status, indent)
98
- string = exception_message_string(e, indent)
95
+ def print_exception(exception, status, indent)
96
+ string = exception_message_string(exception, indent)
99
97
  @io.puts(format_string(string, status))
100
98
  end
101
99
 
102
- def exception_message_string(e, indent_amount)
103
- message = "#{e.message} (#{e.class})".dup.force_encoding('UTF-8')
100
+ def exception_message_string(exception, indent_amount)
101
+ message = "#{exception.message} (#{exception.class})".dup.force_encoding('UTF-8')
104
102
  message = linebreaks(message, ENV['CUCUMBER_TRUNCATE_OUTPUT'].to_i)
105
103
 
106
- indent("#{message}\n#{e.backtrace.join("\n")}", indent_amount)
104
+ indent("#{message}\n#{exception.backtrace.join("\n")}", indent_amount)
107
105
  end
108
106
 
109
107
  # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/10655
110
108
  def linebreaks(msg, max)
111
- return msg unless max && max > 0
112
- msg.gsub(/.{1,#{max}}(?:\s|\Z)/) { ($& + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n") }.rstrip
109
+ return msg unless max.positive?
110
+
111
+ msg.gsub(/.{1,#{max}}(?:\s|\Z)/) do
112
+ (Regexp.last_match(0) + 5.chr).gsub(/\n\005/, "\n").gsub(/\005/, "\n")
113
+ end.rstrip
113
114
  end
114
115
 
115
116
  def collect_snippet_data(test_step, ast_lookup)
@@ -150,6 +151,7 @@ module Cucumber
150
151
 
151
152
  def print_passing_wip(config, passed_test_cases, ast_lookup)
152
153
  return unless config.wip?
154
+
153
155
  messages = passed_test_cases.map do |test_case|
154
156
  scenario_source = ast_lookup.scenario_source(test_case)
155
157
  keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
@@ -167,16 +169,23 @@ module Cucumber
167
169
  end
168
170
  end
169
171
 
170
- def attach(src, media_type)
172
+ def attach(src, media_type, filename)
171
173
  return unless media_type == 'text/x.cucumber.log+plain'
172
174
  return unless @io
175
+
173
176
  @io.puts
174
- @io.puts(format_string(src, :tag))
177
+ if filename
178
+ @io.puts("#{filename}: #{format_string(src, :tag)}")
179
+ else
180
+ @io.puts(format_string(src, :tag))
181
+ end
182
+
175
183
  @io.flush
176
184
  end
177
185
 
178
186
  def print_profile_information
179
187
  return if @options[:skip_profile_information] || @options[:profiles].nil? || @options[:profiles].empty?
188
+
180
189
  do_print_profile_information(@options[:profiles])
181
190
  end
182
191
 
@@ -224,6 +233,7 @@ module Cucumber
224
233
  key = keys.join('_').to_sym
225
234
  fmt = FORMATS[key]
226
235
  raise "No format for #{key.inspect}: #{FORMATS.inspect}" if fmt.nil?
236
+
227
237
  fmt
228
238
  end
229
239
 
@@ -246,6 +256,7 @@ module Cucumber
246
256
 
247
257
  class SnippetData
248
258
  attr_reader :actual_keyword, :step
259
+
249
260
  def initialize(actual_keyword, step)
250
261
  @actual_keyword = actual_keyword
251
262
  @step = step
@@ -254,4 +265,3 @@ module Cucumber
254
265
  end
255
266
  end
256
267
  end
257
- # rubocop:enable Metrics/ModuleLength
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cucumber/formatter/console'
2
4
 
3
5
  module Cucumber
@@ -30,7 +32,7 @@ module Cucumber
30
32
 
31
33
  def status_counts(summary)
32
34
  counts = Core::Test::Result::TYPES.map { |status| [status, summary.total(status)] }
33
- counts = counts.select { |_status, count| count > 0 }
35
+ counts = counts.select { |_status, count| count.positive? }
34
36
  counts = counts.map { |status, count| format_string("#{count} #{status}", status) }
35
37
  "(#{counts.join(', ')})" if counts.any?
36
38
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cucumber/formatter/console'
2
4
 
3
5
  module Cucumber
@@ -12,9 +14,9 @@ module Cucumber
12
14
  @config.on_event(:test_case_finished) do |event|
13
15
  if event.test_case != @previous_test_case
14
16
  @previous_test_case = event.test_case
15
- @issues[event.result.to_sym] << event.test_case unless event.result.ok?(@config.strict)
17
+ @issues[event.result.to_sym] << event.test_case unless event.result.ok?(strict: @config.strict)
16
18
  elsif event.result.passed?
17
- @issues[:flaky] << event.test_case unless Core::Test::Result::Flaky.ok?(@config.strict.strict?(:flaky))
19
+ @issues[:flaky] << event.test_case unless Core::Test::Result::Flaky.ok?(strict: @config.strict.strict?(:flaky))
18
20
  @issues[:failed].delete(event.test_case)
19
21
  end
20
22
  end
@@ -23,6 +25,7 @@ module Cucumber
23
25
 
24
26
  def to_s
25
27
  return if @issues.empty?
28
+
26
29
  result = Core::Test::Result::TYPES.map { |type| scenario_listing(type, @issues[type]) }
27
30
  result.flatten.join("\n")
28
31
  end
@@ -35,6 +38,7 @@ module Cucumber
35
38
 
36
39
  def scenario_listing(type, test_cases)
37
40
  return [] if test_cases.empty?
41
+
38
42
  [format_string("#{type_heading(type)} Scenarios:", type)] + test_cases.map do |test_case|
39
43
  scenario_source = @ast_lookup.scenario_source(test_case)
40
44
  keyword = scenario_source.type == :Scenario ? scenario_source.scenario.keyword : scenario_source.scenario_outline.keyword
@@ -54,7 +58,10 @@ module Cucumber
54
58
 
55
59
  def profiles_string
56
60
  return if @config.custom_profiles.empty?
57
- @config.custom_profiles.map { |profile| "-p #{profile}" }.join(' ') + ' '
61
+
62
+ profiles = @config.custom_profiles.map { |profile| "-p #{profile}" }.join(' ')
63
+
64
+ "#{profiles} "
58
65
  end
59
66
  end
60
67
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+
5
+ module Cucumber
6
+ module Formatter
7
+ class CurlOptionParser
8
+ def self.parse(options)
9
+ args = Shellwords.split(options)
10
+
11
+ url = nil
12
+ http_method = 'PUT'
13
+ headers = {}
14
+
15
+ until args.empty?
16
+ arg = args.shift
17
+ case arg
18
+ when '-X', '--request'
19
+ http_method = remove_arg_for(args, arg)
20
+ when '-H'
21
+ header_arg = remove_arg_for(args, arg)
22
+ headers = headers.merge(parse_header(header_arg))
23
+ else
24
+ raise StandardError, "#{options} was not a valid curl command. Can't set url to #{arg} it is already set to #{url}" if url
25
+
26
+ url = arg
27
+ end
28
+ end
29
+ raise StandardError, "#{options} was not a valid curl command" unless url
30
+
31
+ [url, http_method, headers]
32
+ end
33
+
34
+ # TODO: [LH] -> Switch below methods to private
35
+ def self.remove_arg_for(args, arg)
36
+ return args.shift unless args.empty?
37
+
38
+ raise StandardError, "Missing argument for #{arg}"
39
+ end
40
+
41
+ def self.parse_header(header_arg)
42
+ parts = header_arg.split(':', 2)
43
+ raise StandardError, "#{header_arg} was not a valid header" unless parts.length == 2
44
+
45
+ { parts[0].strip => parts[1].strip }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -4,6 +4,7 @@ module Cucumber
4
4
  module Formatter
5
5
  class DurationExtractor
6
6
  attr_reader :result_duration
7
+
7
8
  def initialize(result)
8
9
  @result_duration = 0
9
10
  result.describe_to(self)
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Cucumber
2
4
  module Formatter
3
5
  class TestCaseUnknownError < StandardError; end
6
+
4
7
  class TestStepUnknownError < StandardError; end
5
8
  end
6
9
  end
@@ -12,7 +12,7 @@ module Cucumber
12
12
  test_case, result = *event.attributes
13
13
  if test_case != @previous_test_case
14
14
  @previous_test_case = event.test_case
15
- Cucumber.wants_to_quit = true unless result.ok?(configuration.strict)
15
+ Cucumber.wants_to_quit = true unless result.ok?(strict: configuration.strict)
16
16
  elsif result.passed?
17
17
  Cucumber.wants_to_quit = false
18
18
  end
@@ -21,7 +21,7 @@ module Cucumber
21
21
  end
22
22
 
23
23
  def respond_to_missing?(name, include_private = false)
24
- recipients.any? { |recipient| recipient.respond_to?(name, include_private) }
24
+ recipients.any? { |recipient| recipient.respond_to?(name, include_private) } || super(name, include_private)
25
25
  end
26
26
  end
27
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'cucumber/formatter/io'
2
4
  require 'cucumber/html_formatter'
3
5
  require 'cucumber/formatter/message_builder'
@@ -1,146 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/http'
2
4
  require 'tempfile'
3
- require 'shellwords'
5
+ require_relative 'curl_option_parser'
6
+ require_relative 'io_http_buffer'
4
7
 
5
8
  module Cucumber
6
9
  module Formatter
7
10
  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
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)
144
17
  end
145
18
  end
146
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
@@ -57,19 +57,21 @@ module Cucumber
57
57
  end
58
58
 
59
59
  def url?(path_or_url_or_io)
60
- path_or_url_or_io.match(%r{^https?://})
60
+ path_or_url_or_io.match(/^https?:\/\//)
61
61
  end
62
62
 
63
63
  def ensure_file(path, name)
64
- 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
65
65
  raise "I can't write #{name} to a directory - it has to be a file" if File.directory?(path)
66
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
+
67
68
  ensure_io(path, nil)
68
69
  end
69
70
 
70
71
  def ensure_dir(path, name)
71
- 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
72
73
  raise "I can't write #{name} reports to a file - it has to be a directory" if File.file?(path)
74
+
73
75
  FileUtils.mkdir_p(path) unless File.directory?(path)
74
76
  File.absolute_path path
75
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
@@ -68,6 +68,7 @@ module Cucumber
68
68
  test_step, result = *event.attributes
69
69
  result = result.with_filtered_backtrace(Cucumber::Formatter::BacktraceFilter)
70
70
  return if internal_hook?(test_step)
71
+
71
72
  add_match_and_result(test_step, result)
72
73
  @any_step_failed = true if result.failed?
73
74
  end
@@ -81,10 +82,10 @@ module Cucumber
81
82
  end
82
83
 
83
84
  def on_test_run_finished(_event)
84
- @io.write(JSON.generate(@feature_hashes, pretty: true))
85
+ @io.write(JSON.pretty_generate(@feature_hashes))
85
86
  end
86
87
 
87
- def attach(src, mime_type)
88
+ def attach(src, mime_type, _filename)
88
89
  if mime_type == 'text/x.cucumber.log+plain'
89
90
  test_step_output << src
90
91
  return
@@ -101,21 +102,17 @@ module Cucumber
101
102
  private
102
103
 
103
104
  def same_feature_as_previous_test_case?(test_case)
104
- current_feature[:uri] == test_case.location.file
105
+ @feature_hash&.fetch(:uri, nil) == test_case.location.file
105
106
  end
106
107
 
107
108
  def first_step_after_background?(test_step)
108
- @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]
109
110
  end
110
111
 
111
112
  def internal_hook?(test_step)
112
113
  test_step.location.file.include?('lib/cucumber/')
113
114
  end
114
115
 
115
- def current_feature
116
- @feature_hash ||= {} # rubocop:disable Naming/MemoizedInstanceVariableName
117
- end
118
-
119
116
  def feature_elements
120
117
  @feature_hash[:elements] ||= []
121
118
  end
@@ -133,7 +130,7 @@ module Cucumber
133
130
  when 'AfterStep hook'
134
131
  after_step_hooks
135
132
  else
136
- raise 'Unknown hook type ' + hook_step.to_s
133
+ raise "Unknown hook type #{hook_step}"
137
134
  end
138
135
  end
139
136
 
@@ -175,15 +172,15 @@ module Cucumber
175
172
  name: test_step.text,
176
173
  line: test_step.location.lines.min
177
174
  }
178
- 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?
179
176
  step_hash[:rows] = create_data_table_value(step_source.data_table) unless step_source.data_table.nil?
180
177
  step_hash
181
178
  end
182
179
 
183
- def create_doc_string_hash(doc_string)
180
+ def create_doc_string_hash(doc_string, doc_string_content)
184
181
  content_type = doc_string.media_type || ''
185
182
  {
186
- value: doc_string.content,
183
+ value: doc_string_content,
187
184
  content_type: content_type,
188
185
  line: doc_string.location.line
189
186
  }
@@ -259,6 +256,7 @@ module Cucumber
259
256
  line: feature.location.line
260
257
  }
261
258
  return if feature.tags.empty?
259
+
262
260
  @feature_hash[:tags] = create_tags_array_from_hash_array(feature.tags)
263
261
  end
264
262