Skip to content

Commit 3573da4

Browse files
authored
Refactor/http io uncoupling (#1752)
* Simplify HTTPIO top level class * Reduce complexity of building class * Rename / organisation of Buffer methods2 * Reduce ABC Score of HTTP IO spec massively * Split out triple class into 3 single classes * Partially port spec code for io_http_buffer * DRY up rspec shared contexts correctly * Move webrick alias handler to isolated support file for rspec * Fix require path issues and after hook problem * Split up of final pieces of spec and partition correctly - remove excess crud * Clean up some fake objects * Place server archetype in subject and remove https scheme alteration * Moved received headers into the mock class * Remove redundant assign of a localhost entity * Move body IO into mock server class * Move starting of server into before hook of shared context * Re-generate TODO file now a lot of the porting is complete * Fix up a couple of RSpec named offenses that could slip through * Move shellwords requirement to correct location and refactor excess complexity of initial build methods * Add changelog
1 parent d9f0278 commit 3573da4

14 files changed

+572
-476
lines changed

.rubocop_todo.yml

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2024-01-17 12:23:33 UTC using RuboCop version 1.56.4.
3+
# on 2024-02-01 10:58:34 UTC using RuboCop version 1.56.4.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

99
# TODO - [LH] -> Oct '23 - 355 files inspected, 642 offenses detected, 205 offenses autocorrectable
1010
# TODO - [LH] -> Dec '23 - 350 files inspected, 595 offenses detected, 171 offenses autocorrectable
11-
# TODO - [LH] -> Jan '23 (cleaned up most of the config) - 364 files inspected, 713 offenses detected, 132 offenses autocorrectable
11+
# TODO - [LH] -> Feb '23 - 370 files inspected, 635 offenses detected, 166 offenses autocorrectable
12+
13+
# Offense count: 10
14+
# This cop supports safe autocorrection (--autocorrect).
15+
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
16+
# SupportedHashRocketStyles: key, separator, table
17+
# SupportedColonStyles: key, separator, table
18+
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
19+
Layout/HashAlignment:
20+
Exclude:
21+
- 'lib/cucumber/cli/options.rb'
22+
23+
# Offense count: 19
24+
# This cop supports safe autocorrection (--autocorrect).
25+
# Configuration parameters: AllowInHeredoc.
26+
Layout/TrailingWhitespace:
27+
Exclude:
28+
- 'lib/cucumber/multiline_argument/data_table.rb'
29+
30+
# Offense count: 3
31+
# This cop supports safe autocorrection (--autocorrect).
32+
Lint/AmbiguousOperator:
33+
Exclude:
34+
- 'lib/cucumber/multiline_argument/data_table.rb'
35+
- 'lib/cucumber/running_test_case.rb'
36+
- 'spec/cucumber/formatter/spec_helper.rb'
1237

1338
# Offense count: 4
1439
Lint/RescueException:
@@ -118,6 +143,7 @@ RSpec/ContextWording:
118143
- 'spec/cucumber/formatter/junit_spec.rb'
119144
- 'spec/cucumber/formatter/publish_banner_printer_spec.rb'
120145
- 'spec/cucumber/glue/step_definition_spec.rb'
146+
- 'spec/support/shared_context/http_server.rb'
121147

122148
# Offense count: 8
123149
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -144,7 +170,7 @@ RSpec/EmptyLineAfterFinalLet:
144170
- 'spec/cucumber/configuration_spec.rb'
145171
- 'spec/cucumber/hooks_spec.rb'
146172

147-
# Offense count: 83
173+
# Offense count: 82
148174
# Configuration parameters: CountAsOne.
149175
RSpec/ExampleLength:
150176
Max: 58
@@ -182,19 +208,19 @@ RSpec/ExpectOutput:
182208
RSpec/HookArgument:
183209
Enabled: false
184210

185-
# Offense count: 15
211+
# Offense count: 8
186212
# Configuration parameters: AssignmentOnly.
187213
RSpec/InstanceVariable:
188214
Exclude:
189-
- 'spec/cucumber/formatter/http_io_spec.rb'
215+
- 'spec/support/shared_context/http_server.rb'
190216

191217
# Offense count: 5
192218
# Configuration parameters: EnforcedStyle.
193219
# SupportedStyles: have_received, receive
194220
RSpec/MessageSpies:
195221
Exclude:
196222
- 'spec/cucumber/deprecate_spec.rb'
197-
- 'spec/cucumber/formatter/http_io_spec.rb'
223+
- 'spec/cucumber/formatter/io_http_buffer_spec.rb'
198224
- 'spec/cucumber/runtime/hooks_examples.rb'
199225

200226
# Offense count: 15
@@ -310,6 +336,15 @@ Style/ClassVars:
310336
Exclude:
311337
- 'spec/cucumber/glue/step_definition_spec.rb'
312338

339+
# Offense count: 2
340+
# This cop supports unsafe autocorrection (--autocorrect-all).
341+
# Configuration parameters: EnforcedStyle.
342+
# SupportedStyles: always, always_true, never
343+
Style/FrozenStringLiteralComment:
344+
Exclude:
345+
- 'spec/support/shared_context/http_server.rb'
346+
- 'spec/support/webrick_proc_handler_alias.rb'
347+
313348
# Offense count: 7
314349
# Configuration parameters: AllowedVariables.
315350
Style/GlobalVars:

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo
1212
### Changed
1313
- Updated cucumber dependencies (Specifically cucumber-core) ([luke-hill](https://github.com/luke-hill))
1414

15+
- Uncoupled a lot of dual-responsibility complexity in HTTP classes (Specifically the builders/parsers)
16+
([#1752](https://github.com/cucumber/cucumber-ruby/pull/1750) [luke-hill](https://github.com/luke-hill))
17+
1518
### Removed
1619
- Some legacy JRuby local testing profiles are now removed ([luke-hill](https://github.com/luke-hill))
1720

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/cucumber/formatter/http_io.rb

Lines changed: 8 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -2,152 +2,18 @@
22

33
require 'net/http'
44
require 'tempfile'
5-
require 'shellwords'
5+
require_relative 'curl_option_parser'
6+
require_relative 'io_http_buffer'
67

78
module Cucumber
89
module Formatter
910
class HTTPIO
10-
class << self
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 open(url, https_verify_mode = nil, reporter = nil)
15-
@https_verify_mode = https_verify_mode
16-
uri, method, headers = CurlOptionParser.parse(url)
17-
IOHTTPBuffer.new(uri, method, headers, https_verify_mode, reporter)
18-
end
19-
end
20-
end
21-
22-
class CurlOptionParser
23-
def self.parse(options)
24-
args = Shellwords.split(options)
25-
26-
url = nil
27-
http_method = 'PUT'
28-
headers = {}
29-
30-
until args.empty?
31-
arg = args.shift
32-
case arg
33-
when '-X', '--request'
34-
http_method = remove_arg_for(args, arg)
35-
when '-H'
36-
header_arg = remove_arg_for(args, arg)
37-
headers = headers.merge(parse_header(header_arg))
38-
else
39-
raise StandardError, "#{options} was not a valid curl command. Can't set url to #{arg} it is already set to #{url}" if url
40-
41-
url = arg
42-
end
43-
end
44-
raise StandardError, "#{options} was not a valid curl command" unless url
45-
46-
[
47-
url,
48-
http_method,
49-
headers
50-
]
51-
end
52-
53-
def self.remove_arg_for(args, arg)
54-
return args.shift unless args.empty?
55-
56-
raise StandardError, "Missing argument for #{arg}"
57-
end
58-
59-
def self.parse_header(header_arg)
60-
parts = header_arg.split(':', 2)
61-
raise StandardError, "#{header_arg} was not a valid header" unless parts.length == 2
62-
63-
{ parts[0].strip => parts[1].strip }
64-
end
65-
end
66-
67-
class IOHTTPBuffer
68-
attr_reader :uri, :method, :headers
69-
70-
def initialize(uri, method, headers = {}, https_verify_mode = nil, reporter = nil)
71-
@uri = URI(uri)
72-
@method = method
73-
@headers = headers
74-
@write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
75-
@https_verify_mode = https_verify_mode
76-
@reporter = reporter || NoReporter.new
77-
end
78-
79-
def close
80-
response = send_content(@uri, @method, @headers)
81-
@reporter.report(response.body)
82-
@write_io.close
83-
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
84-
85-
raise StandardError, "request to #{uri} failed with status #{response.code}"
86-
end
87-
88-
def write(data)
89-
@write_io.write(data)
90-
end
91-
92-
def flush
93-
@write_io.flush
94-
end
95-
96-
def closed?
97-
@write_io.closed?
98-
end
99-
100-
private
101-
102-
def send_content(uri, method, headers, attempt = 10)
103-
content = (method == 'GET' ? StringIO.new : @write_io)
104-
http = build_client(uri, @https_verify_mode)
105-
106-
raise StandardError, "request to #{uri} failed (too many redirections)" if attempt <= 0
107-
108-
req = build_request(
109-
uri,
110-
method,
111-
headers.merge(
112-
'Content-Length' => content.size.to_s
113-
)
114-
)
115-
116-
content.rewind
117-
req.body_stream = content
118-
119-
begin
120-
response = http.request(req)
121-
rescue SystemCallError
122-
# We may get the redirect response before pushing the file.
123-
response = http.request(build_request(uri, method, headers))
124-
end
125-
126-
case response
127-
when Net::HTTPAccepted
128-
send_content(URI(response['Location']), 'PUT', {}, attempt - 1) if response['Location']
129-
when Net::HTTPRedirection
130-
send_content(URI(response['Location']), method, headers, attempt - 1)
131-
end
132-
response
133-
end
134-
135-
def build_request(uri, method, headers)
136-
method_class_name = "#{method[0].upcase}#{method[1..].downcase}"
137-
req = Net::HTTP.const_get(method_class_name).new(uri)
138-
headers.each do |header, value|
139-
req[header] = value
140-
end
141-
req
142-
end
143-
144-
def build_client(uri, https_verify_mode)
145-
http = Net::HTTP.new(uri.hostname, uri.port)
146-
if uri.scheme == 'https'
147-
http.use_ssl = true
148-
http.verify_mode = https_verify_mode if https_verify_mode
149-
end
150-
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)
15117
end
15218
end
15319
end

0 commit comments

Comments
 (0)