From 747e315bbf53b947f43a91104346328a1209a03b Mon Sep 17 00:00:00 2001 From: Alex Watt Date: Fri, 23 Aug 2024 10:31:56 -0400 Subject: [PATCH 1/6] Cache header normalization to reduce object allocation --- lib/http/header_normalizer.rb | 87 +++++++++++++++++++++++++ lib/http/headers.rb | 27 +++----- spec/lib/http/header_normalizer_spec.rb | 24 +++++++ 3 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 lib/http/header_normalizer.rb create mode 100644 spec/lib/http/header_normalizer_spec.rb diff --git a/lib/http/header_normalizer.rb b/lib/http/header_normalizer.rb new file mode 100644 index 00000000..0dc0ed7e --- /dev/null +++ b/lib/http/header_normalizer.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module HTTP + class HeaderNormalizer + # Matches HTTP header names when in "Canonical-Http-Format" + CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/ + + # Matches valid header field name according to RFC. + # @see http://tools.ietf.org/html/rfc7230#section-3.2 + COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/ + + MAX_CACHE_SIZE = 200 + + def initialize + @cache = LRUCache.new(MAX_CACHE_SIZE) + end + + # Transforms `name` to canonical HTTP header capitalization + def normalize(name) + @cache[name] ||= normalize_header(name) + end + + private + + # Transforms `name` to canonical HTTP header capitalization + # + # @param [String] name + # @raise [HeaderError] if normalized name does not + # match {COMPLIANT_NAME_RE} + # @return [String] canonical HTTP header name + def normalize_header(name) + return name if CANONICAL_NAME_RE.match?(name) + + normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") + + return normalized if COMPLIANT_NAME_RE.match?(normalized) + + raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" + end + + class LRUCache + def initialize(max_size) + @max_size = max_size + @cache = {} + @order = [] + end + + def get(key) + return unless @cache.key?(key) + + # Move the accessed item to the end of the order array + @order.delete(key) + @order.push(key) + @cache[key] + end + + def set(key, value) + @cache[key] = value + @order.push(key) + + # Maintain cache size + return unless @order.size > @max_size + + oldest = @order.shift + @cache.delete(oldest) + end + + def size + @cache.size + end + + def key?(key) + @cache.key?(key) + end + + def [](key) + get(key) + end + + def []=(key, value) + set(key, value) + end + end + + private_constant :LRUCache + end +end diff --git a/lib/http/headers.rb b/lib/http/headers.rb index 7d48b46f..db5fdf93 100644 --- a/lib/http/headers.rb +++ b/lib/http/headers.rb @@ -5,6 +5,7 @@ require "http/errors" require "http/headers/mixin" require "http/headers/known" +require "http/header_normalizer" module HTTP # HTTP Headers container. @@ -12,13 +13,6 @@ class Headers extend Forwardable include Enumerable - # Matches HTTP header names when in "Canonical-Http-Format" - CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/ - - # Matches valid header field name according to RFC. - # @see http://tools.ietf.org/html/rfc7230#section-3.2 - COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/ - # Class constructor. def initialize # The @pile stores each header value using a three element array: @@ -219,20 +213,15 @@ def coerce(object) private + class << self + def header_normalizer + @header_normalizer ||= HeaderNormalizer.new + end + end + # Transforms `name` to canonical HTTP header capitalization - # - # @param [String] name - # @raise [HeaderError] if normalized name does not - # match {HEADER_NAME_RE} - # @return [String] canonical HTTP header name def normalize_header(name) - return name if CANONICAL_NAME_RE.match?(name) - - normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") - - return normalized if COMPLIANT_NAME_RE.match?(normalized) - - raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" + self.class.header_normalizer.normalize(name) end # Ensures there is no new line character in the header value diff --git a/spec/lib/http/header_normalizer_spec.rb b/spec/lib/http/header_normalizer_spec.rb new file mode 100644 index 00000000..b975ba61 --- /dev/null +++ b/spec/lib/http/header_normalizer_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.describe HTTP::HeaderNormalizer do + subject(:normalizer) { described_class.new } + + describe "#normalize" do + it "normalizes the header" do + expect(normalizer.normalize("content_type")).to eq "Content-Type" + end + + it "caches normalized headers" do + object_id = normalizer.normalize("content_type").object_id + expect(object_id).to eq normalizer.normalize("content_type").object_id + end + + it "only caches up to MAX_CACHE_SIZE headers" do + (1..described_class::MAX_CACHE_SIZE + 1).each do |i| + normalizer.normalize("header#{i}") + end + + expect(normalizer.instance_variable_get(:@cache).size).to eq described_class::MAX_CACHE_SIZE + end + end +end From 411d7e022715c10352539989273ff2485934d519 Mon Sep 17 00:00:00 2001 From: Alex Watt Date: Sat, 24 Aug 2024 20:52:58 -0400 Subject: [PATCH 2/6] Move to Headers::Normalizer --- lib/http/header_normalizer.rb | 87 ------------------ lib/http/headers.rb | 4 +- lib/http/headers/normalizer.rb | 89 +++++++++++++++++++ .../normalizer_spec.rb} | 2 +- 4 files changed, 92 insertions(+), 90 deletions(-) delete mode 100644 lib/http/header_normalizer.rb create mode 100644 lib/http/headers/normalizer.rb rename spec/lib/http/{header_normalizer_spec.rb => headers/normalizer_spec.rb} (94%) diff --git a/lib/http/header_normalizer.rb b/lib/http/header_normalizer.rb deleted file mode 100644 index 0dc0ed7e..00000000 --- a/lib/http/header_normalizer.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module HTTP - class HeaderNormalizer - # Matches HTTP header names when in "Canonical-Http-Format" - CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/ - - # Matches valid header field name according to RFC. - # @see http://tools.ietf.org/html/rfc7230#section-3.2 - COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/ - - MAX_CACHE_SIZE = 200 - - def initialize - @cache = LRUCache.new(MAX_CACHE_SIZE) - end - - # Transforms `name` to canonical HTTP header capitalization - def normalize(name) - @cache[name] ||= normalize_header(name) - end - - private - - # Transforms `name` to canonical HTTP header capitalization - # - # @param [String] name - # @raise [HeaderError] if normalized name does not - # match {COMPLIANT_NAME_RE} - # @return [String] canonical HTTP header name - def normalize_header(name) - return name if CANONICAL_NAME_RE.match?(name) - - normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") - - return normalized if COMPLIANT_NAME_RE.match?(normalized) - - raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" - end - - class LRUCache - def initialize(max_size) - @max_size = max_size - @cache = {} - @order = [] - end - - def get(key) - return unless @cache.key?(key) - - # Move the accessed item to the end of the order array - @order.delete(key) - @order.push(key) - @cache[key] - end - - def set(key, value) - @cache[key] = value - @order.push(key) - - # Maintain cache size - return unless @order.size > @max_size - - oldest = @order.shift - @cache.delete(oldest) - end - - def size - @cache.size - end - - def key?(key) - @cache.key?(key) - end - - def [](key) - get(key) - end - - def []=(key, value) - set(key, value) - end - end - - private_constant :LRUCache - end -end diff --git a/lib/http/headers.rb b/lib/http/headers.rb index db5fdf93..89ff78b2 100644 --- a/lib/http/headers.rb +++ b/lib/http/headers.rb @@ -4,8 +4,8 @@ require "http/errors" require "http/headers/mixin" +require "http/headers/normalizer" require "http/headers/known" -require "http/header_normalizer" module HTTP # HTTP Headers container. @@ -215,7 +215,7 @@ def coerce(object) class << self def header_normalizer - @header_normalizer ||= HeaderNormalizer.new + @header_normalizer ||= Headers::Normalizer.new end end diff --git a/lib/http/headers/normalizer.rb b/lib/http/headers/normalizer.rb new file mode 100644 index 00000000..d95b1d80 --- /dev/null +++ b/lib/http/headers/normalizer.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module HTTP + class Headers + class Normalizer + # Matches HTTP header names when in "Canonical-Http-Format" + CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/ + + # Matches valid header field name according to RFC. + # @see http://tools.ietf.org/html/rfc7230#section-3.2 + COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/ + + MAX_CACHE_SIZE = 200 + + def initialize + @cache = LRUCache.new(MAX_CACHE_SIZE) + end + + # Transforms `name` to canonical HTTP header capitalization + def normalize(name) + @cache[name] ||= normalize_header(name) + end + + private + + # Transforms `name` to canonical HTTP header capitalization + # + # @param [String] name + # @raise [HeaderError] if normalized name does not + # match {COMPLIANT_NAME_RE} + # @return [String] canonical HTTP header name + def normalize_header(name) + return name if CANONICAL_NAME_RE.match?(name) + + normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") + + return normalized if COMPLIANT_NAME_RE.match?(normalized) + + raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" + end + + class LRUCache + def initialize(max_size) + @max_size = max_size + @cache = {} + @order = [] + end + + def get(key) + return unless @cache.key?(key) + + # Move the accessed item to the end of the order array + @order.delete(key) + @order.push(key) + @cache[key] + end + + def set(key, value) + @cache[key] = value + @order.push(key) + + # Maintain cache size + return unless @order.size > @max_size + + oldest = @order.shift + @cache.delete(oldest) + end + + def size + @cache.size + end + + def key?(key) + @cache.key?(key) + end + + def [](key) + get(key) + end + + def []=(key, value) + set(key, value) + end + end + + private_constant :LRUCache + end + end +end diff --git a/spec/lib/http/header_normalizer_spec.rb b/spec/lib/http/headers/normalizer_spec.rb similarity index 94% rename from spec/lib/http/header_normalizer_spec.rb rename to spec/lib/http/headers/normalizer_spec.rb index b975ba61..d18cd95e 100644 --- a/spec/lib/http/header_normalizer_spec.rb +++ b/spec/lib/http/headers/normalizer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe HTTP::HeaderNormalizer do +RSpec.describe HTTP::Headers::Normalizer do subject(:normalizer) { described_class.new } describe "#normalize" do From d2438727df9711893234ecb1b8b9be809759d609 Mon Sep 17 00:00:00 2001 From: Alex Watt Date: Sat, 24 Aug 2024 21:00:53 -0400 Subject: [PATCH 3/6] LRUCache -> Cache for simplicity --- lib/http/headers/normalizer.rb | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/http/headers/normalizer.rb b/lib/http/headers/normalizer.rb index d95b1d80..7c260d0d 100644 --- a/lib/http/headers/normalizer.rb +++ b/lib/http/headers/normalizer.rb @@ -13,7 +13,7 @@ class Normalizer MAX_CACHE_SIZE = 200 def initialize - @cache = LRUCache.new(MAX_CACHE_SIZE) + @cache = Cache.new(MAX_CACHE_SIZE) end # Transforms `name` to canonical HTTP header capitalization @@ -39,31 +39,24 @@ def normalize_header(name) raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" end - class LRUCache + class Cache def initialize(max_size) @max_size = max_size @cache = {} - @order = [] end def get(key) - return unless @cache.key?(key) - - # Move the accessed item to the end of the order array - @order.delete(key) - @order.push(key) @cache[key] end def set(key, value) @cache[key] = value - @order.push(key) # Maintain cache size - return unless @order.size > @max_size + return unless @cache.size > @max_size - oldest = @order.shift - @cache.delete(oldest) + oldest_key = @cache.keys.first + @cache.delete(oldest_key) end def size @@ -83,7 +76,7 @@ def []=(key, value) end end - private_constant :LRUCache + private_constant :Cache end end end From 498a6d153b1a25443c76b8b05b46079fa83935fb Mon Sep 17 00:00:00 2001 From: Alex Watt Date: Mon, 26 Aug 2024 08:06:38 -0400 Subject: [PATCH 4/6] Take suggestions and work on updating tests --- .rubocop/rspec.yml | 1 + Gemfile | 1 + lib/http/headers.rb | 58 +++++++++---------- lib/http/headers/normalizer.rb | 74 ++++++++++-------------- spec/lib/http/headers/normalizer_spec.rb | 54 ++++++++++++++--- spec/spec_helper.rb | 1 + 6 files changed, 106 insertions(+), 83 deletions(-) diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml index 51711080..29ae542e 100644 --- a/.rubocop/rspec.yml +++ b/.rubocop/rspec.yml @@ -1,5 +1,6 @@ RSpec/ExampleLength: CountAsOne: - array + - hash - heredoc - method_call diff --git a/Gemfile b/Gemfile index dc6363ee..9db61f1b 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ group :test do gem "rspec", "~> 3.10" gem "rspec-its" + gem "rspec-memory" gem "yardstick" end diff --git a/lib/http/headers.rb b/lib/http/headers.rb index 89ff78b2..5f285616 100644 --- a/lib/http/headers.rb +++ b/lib/http/headers.rb @@ -13,6 +13,33 @@ class Headers extend Forwardable include Enumerable + class << self + # Coerces given `object` into Headers. + # + # @raise [Error] if object can't be coerced + # @param [#to_hash, #to_h, #to_a] object + # @return [Headers] + def coerce(object) + unless object.is_a? self + object = case + when object.respond_to?(:to_hash) then object.to_hash + when object.respond_to?(:to_h) then object.to_h + when object.respond_to?(:to_a) then object.to_a + else raise Error, "Can't coerce #{object.inspect} to Headers" + end + end + + headers = new + object.each { |k, v| headers.add k, v } + headers + end + alias [] coerce + + def normalizer + @normalizer ||= Headers::Normalizer.new + end + end + # Class constructor. def initialize # The @pile stores each header value using a three element array: @@ -188,40 +215,11 @@ def merge(other) dup.tap { |dupped| dupped.merge! other } end - class << self - # Coerces given `object` into Headers. - # - # @raise [Error] if object can't be coerced - # @param [#to_hash, #to_h, #to_a] object - # @return [Headers] - def coerce(object) - unless object.is_a? self - object = case - when object.respond_to?(:to_hash) then object.to_hash - when object.respond_to?(:to_h) then object.to_h - when object.respond_to?(:to_a) then object.to_a - else raise Error, "Can't coerce #{object.inspect} to Headers" - end - end - - headers = new - object.each { |k, v| headers.add k, v } - headers - end - alias [] coerce - end - private - class << self - def header_normalizer - @header_normalizer ||= Headers::Normalizer.new - end - end - # Transforms `name` to canonical HTTP header capitalization def normalize_header(name) - self.class.header_normalizer.normalize(name) + self.class.normalizer.call(name) end # Ensures there is no new line character in the header value diff --git a/lib/http/headers/normalizer.rb b/lib/http/headers/normalizer.rb index 7c260d0d..b4a53b42 100644 --- a/lib/http/headers/normalizer.rb +++ b/lib/http/headers/normalizer.rb @@ -10,15 +10,40 @@ class Normalizer # @see http://tools.ietf.org/html/rfc7230#section-3.2 COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/ - MAX_CACHE_SIZE = 200 + NAME_PARTS_SEPARATOR_RE = /[\-_]/ + + # @private + # Normalized header names cache + class Cache + MAX_SIZE = 200 + + def initialize + @store = {} + end + + def get(key) + @store[key] + end + alias [] get + + def set(key, value) + # Maintain cache size + @store.delete(@store.each_key.first) while MAX_SIZE <= @store.size + + @store[key] = value + end + alias []= set + end def initialize - @cache = Cache.new(MAX_CACHE_SIZE) + @cache = Cache.new end # Transforms `name` to canonical HTTP header capitalization - def normalize(name) - @cache[name] ||= normalize_header(name) + def call(name) + name = -name.to_s + value = (@cache[name] ||= -normalize_header(name)) + value.dup end private @@ -32,51 +57,12 @@ def normalize(name) def normalize_header(name) return name if CANONICAL_NAME_RE.match?(name) - normalized = name.split(/[\-_]/).each(&:capitalize!).join("-") + normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-") return normalized if COMPLIANT_NAME_RE.match?(normalized) raise HeaderError, "Invalid HTTP header field name: #{name.inspect}" end - - class Cache - def initialize(max_size) - @max_size = max_size - @cache = {} - end - - def get(key) - @cache[key] - end - - def set(key, value) - @cache[key] = value - - # Maintain cache size - return unless @cache.size > @max_size - - oldest_key = @cache.keys.first - @cache.delete(oldest_key) - end - - def size - @cache.size - end - - def key?(key) - @cache.key?(key) - end - - def [](key) - get(key) - end - - def []=(key, value) - set(key, value) - end - end - - private_constant :Cache end end end diff --git a/spec/lib/http/headers/normalizer_spec.rb b/spec/lib/http/headers/normalizer_spec.rb index d18cd95e..a512af08 100644 --- a/spec/lib/http/headers/normalizer_spec.rb +++ b/spec/lib/http/headers/normalizer_spec.rb @@ -3,22 +3,58 @@ RSpec.describe HTTP::Headers::Normalizer do subject(:normalizer) { described_class.new } - describe "#normalize" do + include_context RSpec::Memory + + describe "#call" do it "normalizes the header" do - expect(normalizer.normalize("content_type")).to eq "Content-Type" + expect(normalizer.call("content_type")).to eq "Content-Type" + end + + it "returns a non-frozen string" do + expect(normalizer.call("content_type")).not_to be_frozen end - it "caches normalized headers" do - object_id = normalizer.normalize("content_type").object_id - expect(object_id).to eq normalizer.normalize("content_type").object_id + it "evicts the oldest item when cache is full" do + max_headers = (1..described_class::Cache::MAX_SIZE).map { |i| "Header#{i}" } + max_headers.each { |header| normalizer.call(header) } + normalizer.call("New-Header") + cache_store = normalizer.instance_variable_get(:@cache).instance_variable_get(:@store) + expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"]) end - it "only caches up to MAX_CACHE_SIZE headers" do - (1..described_class::MAX_CACHE_SIZE + 1).each do |i| - normalizer.normalize("header#{i}") + describe "multiple invocations with the same input" do + let(:normalized_values) { Array.new(3) { normalizer.call("content_type") } } + + it "returns the same result each time" do + expect(normalized_values.uniq.size).to eq 1 + end + + it "returns different string objects each time" do + expect(normalized_values.map(&:object_id).uniq.size).to eq normalized_values.size end + end + + it "limits allocation counts for first normalization of a header" do + expected_allocations = { + Array => 1, + described_class => 1, + Hash => 1, + described_class::Cache => 1, + MatchData => 1, + String => 6 + } + + expect do + normalizer.call("content_type") + end.to limit_allocations(**expected_allocations) + end + + it "allocates minimal memory for subsequent normalization of the same header" do + normalizer.call("content_type") - expect(normalizer.instance_variable_get(:@cache).size).to eq described_class::MAX_CACHE_SIZE + expect do + normalizer.call("content_type") + end.to limit_allocations(String => 1) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1bb40781..1bdbf00b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ require "http" require "rspec/its" +require "rspec/memory" require "support/capture_warning" require "support/fakeio" From 151ee0bda78b751ab3628af0803048323e348a25 Mon Sep 17 00:00:00 2001 From: Alexey Zapparov Date: Mon, 26 Aug 2024 19:56:19 +0200 Subject: [PATCH 5/6] review: Specs wee cleanup :D --- lib/http/headers/normalizer.rb | 3 +- spec/lib/http/headers/normalizer_spec.rb | 52 ++++++++++-------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/http/headers/normalizer.rb b/lib/http/headers/normalizer.rb index b4a53b42..be623079 100644 --- a/lib/http/headers/normalizer.rb +++ b/lib/http/headers/normalizer.rb @@ -41,8 +41,9 @@ def initialize # Transforms `name` to canonical HTTP header capitalization def call(name) - name = -name.to_s + name = -name.to_s value = (@cache[name] ||= -normalize_header(name)) + value.dup end diff --git a/spec/lib/http/headers/normalizer_spec.rb b/spec/lib/http/headers/normalizer_spec.rb index a512af08..a304e3d3 100644 --- a/spec/lib/http/headers/normalizer_spec.rb +++ b/spec/lib/http/headers/normalizer_spec.rb @@ -22,39 +22,31 @@ expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"]) end - describe "multiple invocations with the same input" do - let(:normalized_values) { Array.new(3) { normalizer.call("content_type") } } + it "retuns mutable strings" do + normalized_headers = Array.new(3) { normalizer.call("content_type") } - it "returns the same result each time" do - expect(normalized_values.uniq.size).to eq 1 - end - - it "returns different string objects each time" do - expect(normalized_values.map(&:object_id).uniq.size).to eq normalized_values.size - end - end - - it "limits allocation counts for first normalization of a header" do - expected_allocations = { - Array => 1, - described_class => 1, - Hash => 1, - described_class::Cache => 1, - MatchData => 1, - String => 6 - } - - expect do - normalizer.call("content_type") - end.to limit_allocations(**expected_allocations) + expect(normalized_headers) + .to satisfy { |arr| arr.uniq.size == 1 } + .and satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size } + .and satisfy { |arr| arr.none?(&:frozen?) } end - it "allocates minimal memory for subsequent normalization of the same header" do - normalizer.call("content_type") - - expect do - normalizer.call("content_type") - end.to limit_allocations(String => 1) + it "allocates minimal memory for normalization of the same header" do + normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated + + # On first call it is expected to allocate during normalization + expect { normalizer.call("content_type") }.to limit_allocations( + Array => 1, + MatchData => 1, + String => 6 + ) + + # On subsequent call it is expected to only allocate copy of a cached string + expect { normalizer.call("content_type") }.to limit_allocations( + Array => 0, + MatchData => 0, + String => 1 + ) end end end From fa733a2de30b41b817805c5057e11567af93f70b Mon Sep 17 00:00:00 2001 From: Alexey Zapparov Date: Mon, 26 Aug 2024 20:06:43 +0200 Subject: [PATCH 6/6] review: Lint (a bit more than this PR is about) --- .rubocop/rspec.yml | 3 +++ .rubocop_todo.yml | 24 ----------------------- spec/lib/http/headers/normalizer_spec.rb | 4 ++-- spec/lib/http/redirector_spec.rb | 11 ++++++----- spec/lib/http/retriable/performer_spec.rb | 2 +- 5 files changed, 12 insertions(+), 32 deletions(-) diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml index 29ae542e..ebf556d3 100644 --- a/.rubocop/rspec.yml +++ b/.rubocop/rspec.yml @@ -4,3 +4,6 @@ RSpec/ExampleLength: - hash - heredoc - method_call + +RSpec/MultipleExpectations: + Max: 5 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 79be39a4..551b5075 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -267,30 +267,6 @@ RSpec/InstanceVariable: RSpec/MessageSpies: EnforcedStyle: receive -# Offense count: 74 -# Configuration parameters: Max. -RSpec/MultipleExpectations: - Exclude: - - 'spec/lib/http/client_spec.rb' - - 'spec/lib/http/connection_spec.rb' - - 'spec/lib/http/features/auto_deflate_spec.rb' - - 'spec/lib/http/headers_spec.rb' - - 'spec/lib/http/options/body_spec.rb' - - 'spec/lib/http/options/features_spec.rb' - - 'spec/lib/http/options/form_spec.rb' - - 'spec/lib/http/options/headers_spec.rb' - - 'spec/lib/http/options/json_spec.rb' - - 'spec/lib/http/options/merge_spec.rb' - - 'spec/lib/http/options/proxy_spec.rb' - - 'spec/lib/http/redirector_spec.rb' - - 'spec/lib/http/response/body_spec.rb' - - 'spec/lib/http/response/parser_spec.rb' - - 'spec/lib/http/retriable/delay_calculator_spec.rb' - - 'spec/lib/http/retriable/performer_spec.rb' - - 'spec/lib/http/uri_spec.rb' - - 'spec/lib/http_spec.rb' - - 'spec/support/http_handling_shared.rb' - # Offense count: 9 # Configuration parameters: AllowSubject, Max. RSpec/MultipleMemoizedHelpers: diff --git a/spec/lib/http/headers/normalizer_spec.rb b/spec/lib/http/headers/normalizer_spec.rb index a304e3d3..09d7889d 100644 --- a/spec/lib/http/headers/normalizer_spec.rb +++ b/spec/lib/http/headers/normalizer_spec.rb @@ -27,8 +27,8 @@ expect(normalized_headers) .to satisfy { |arr| arr.uniq.size == 1 } - .and satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size } - .and satisfy { |arr| arr.none?(&:frozen?) } + .and(satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size }) + .and(satisfy { |arr| arr.none?(&:frozen?) }) end it "allocates minimal memory for normalization of the same header" do diff --git a/spec/lib/http/redirector_spec.rb b/spec/lib/http/redirector_spec.rb index e182129a..e5c505cd 100644 --- a/spec/lib/http/redirector_spec.rb +++ b/spec/lib/http/redirector_spec.rb @@ -117,12 +117,13 @@ def redirect_response(status, location, set_cookie = {}) expect(req_cookie).to eq request_cookies.shift hops.shift end + expect(res.to_s).to eq "bar" - cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] } - expect(cookies["foo"]).to eq "42" - expect(cookies["bar"]).to eq "53" - expect(cookies["baz"]).to eq "65" - expect(cookies["deleted"]).to eq nil + expect(res.cookies.cookies.to_h { |c| [c.name, c.value] }).to eq({ + "foo" => "42", + "bar" => "53", + "baz" => "65" + }) end it "returns original cookies in response" do diff --git a/spec/lib/http/retriable/performer_spec.rb b/spec/lib/http/retriable/performer_spec.rb index 60c0e12c..38d693c2 100644 --- a/spec/lib/http/retriable/performer_spec.rb +++ b/spec/lib/http/retriable/performer_spec.rb @@ -199,7 +199,7 @@ def response(**options) end describe "should_retry option" do - it "decides if the request should be retried" do + it "decides if the request should be retried" do # rubocop:disable RSpec/MultipleExpectations retry_proc = proc do |req, err, res, attempt| expect(req).to eq request if res