Skip to content

Commit

Permalink
Improve unhandled request error messages.
Browse files Browse the repository at this point in the history
Closes vcr#92.
  • Loading branch information
myronmarston committed Nov 14, 2011
1 parent 2dfd533 commit 19c1c47
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 10 deletions.
6 changes: 3 additions & 3 deletions features/cassettes/no_cassette.feature
Expand Up @@ -65,7 +65,7 @@ Feature: Error for HTTP request made when no cassette is in use
puts context
puts Net::HTTP.get_response('localhost', '/', 7777).body
rescue => e
puts "Error: #{e.message}"
puts "Error: #{e.class}"
end
VCR.turned_off do
Expand All @@ -89,7 +89,7 @@ Feature: Error for HTTP request made when no cassette is in use
And the output should contain:
"""
Outside of VCR.turned_off block
Error: An HTTP request has been made that VCR does not know how to handle
Error: VCR::Errors::UnhandledHTTPRequestError
"""
And the output should contain:
"""
Expand All @@ -99,7 +99,7 @@ Feature: Error for HTTP request made when no cassette is in use
And the output should contain:
"""
After calling VCR.turn_on!
Error: An HTTP request has been made that VCR does not know how to handle
Error: VCR::Errors::UnhandledHTTPRequestError
"""

Scenario: Turning VCR off prevents cassettes from being inserted
Expand Down
2 changes: 1 addition & 1 deletion features/configuration/ignore_request.feature
Expand Up @@ -65,7 +65,7 @@ Feature: Ignore Request
When I run `ruby ignore_request.rb`
Then it should fail with:
"""
An HTTP request has been made that VCR does not know how to handle: (VCR::Errors::UnhandledHTTPRequestError)
An HTTP request has been made that VCR does not know how to handle:
GET http://localhost:8888/
"""
And the output should contain:
Expand Down
8 changes: 4 additions & 4 deletions features/test_frameworks/cucumber.feature
Expand Up @@ -92,9 +92,9 @@ Feature: Usage with Cucumber
Then it should fail with "3 scenarios (2 failed, 1 passed)"
And the output should contain each of the following:
| An HTTP request has been made that VCR does not know how to handle: |
| GET http://localhost:7777/disallowed_1 (VCR::Errors::UnhandledHTTPRequestError) |
| GET http://localhost:7777/disallowed_1 |
| An HTTP request has been made that VCR does not know how to handle: |
| GET http://localhost:7777/disallowed_2 (VCR::Errors::UnhandledHTTPRequestError) |
| GET http://localhost:7777/disallowed_2 |
And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "body: Hello localhost_request_1"
And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "body: Hello localhost_request_2"
And the file "features/cassettes/nested_cassette.yml" should contain "body: Hello nested_cassette"
Expand All @@ -106,9 +106,9 @@ Feature: Usage with Cucumber
Then it should fail with "3 scenarios (2 failed, 1 passed)"
And the output should contain each of the following:
| An HTTP request has been made that VCR does not know how to handle: |
| GET http://localhost:7777/disallowed_1 (VCR::Errors::UnhandledHTTPRequestError) |
| GET http://localhost:7777/disallowed_1 |
| An HTTP request has been made that VCR does not know how to handle: |
| GET http://localhost:7777/disallowed_2 (VCR::Errors::UnhandledHTTPRequestError) |
| GET http://localhost:7777/disallowed_2 |
And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "body: Hello localhost_request_1"
And the file "features/cassettes/cucumber_tags/localhost_request.yml" should contain "body: Hello localhost_request_2"
And the file "features/cassettes/nested_cassette.yml" should contain "body: Hello nested_cassette"
Expand Down
10 changes: 10 additions & 0 deletions lib/vcr/cassette/http_interaction_list.rb
Expand Up @@ -5,6 +5,8 @@ module NullList
extend self
def response_for(*a); nil; end
def has_interaction_matching?(*a); false; end
def has_used_interaction_matching?(*a); false; end
def remaining_unused_interaction_count(*a); 0; end
end

attr_reader :interactions, :request_matchers, :allow_playback_repeats, :parent_list
Expand Down Expand Up @@ -35,6 +37,14 @@ def has_interaction_matching?(request)
@parent_list.has_interaction_matching?(request)
end

def has_used_interaction_matching?(request)
@used_interactions.any? { |i| interaction_matches_request?(request, i) }
end

def remaining_unused_interaction_count
@interactions.size
end

private

def matching_interaction_index_for(request)
Expand Down
157 changes: 155 additions & 2 deletions lib/vcr/errors.rb
Expand Up @@ -15,12 +15,165 @@ def initialize(request)
@request = request
super construct_message
end

private

def relish_version_slug
@relish_version_slug ||= VCR.version.gsub(/\W/, '-')
end

def construct_message
"An HTTP request has been made that VCR does not know how to handle:\n" +
" #{request.method.to_s.upcase} #{request.uri}"
["", "", "=" * 80,
"An HTTP request has been made that VCR does not know how to handle:",
" #{request_description}\n",
cassette_description,
formatted_suggestions,
"=" * 80, "", ""].join("\n")
end

def request_description
"#{request.method.to_s.upcase} #{request.uri}"
end

def cassette_description
if cassette = VCR.current_cassette
["VCR is currently using the following cassette:",
" - #{cassette.file}",
" - :record => #{cassette.record_mode.inspect}",
" - :match_requests_on => #{cassette.match_requests_on.inspect}\n",
"Under the current configuration VCR can not find a suitable HTTP interaction",
"to replay and is prevented from recording new requests. There are a few ways",
"you can deal with this:\n"].join("\n")
else
["There is currently no cassette in use. There are a few ways",
"you can configure VCR to handle this request:\n"].join("\n")
end
end

def formatted_suggestions
formatted_points, formatted_foot_notes = [], []

suggestions.each_with_index do |suggestion, index|
bullet_point, foot_note = suggestion.first, suggestion.last
formatted_points << format_bullet_point(bullet_point, index)
formatted_foot_notes << format_foot_note(foot_note, index)
end

[
formatted_points.join("\n"),
formatted_foot_notes.join("\n")
].join("\n\n")
end

def format_bullet_point(lines, index)
lines.first.insert(0, " * ")
lines.last << " [#{index + 1}]."
lines.join("\n ")
end

def format_foot_note(url, index)
"[#{index + 1}] #{url % relish_version_slug}"
end

ALL_SUGGESTIONS = {
:use_new_episodes => [
["You can use the :new_episodes record mode to allow VCR to",
"record this new request to the existing cassette"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/record-modes/new-episodes"
],

:delete_cassette_for_once => [
["The current record mode (:once) does not allow new requests to be recorded",
"to a previously recorded cassete. You can delete the cassette file and re-run",
"your tests to allow the cassette to be recorded with this request"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/record-modes/once"
],

:deal_with_none => [
["The current record mode (:none) does not allow requests to be recorded. You",
"can temporarily change the record mode to :once, delete the cassette file ",
"and re-run your tests to allow the cassette to be recorded with this request"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/record-modes/none"
],

:use_a_cassette => [
["If you want VCR to record this request and play it back during future test",
"runs, you should wrap your test (or this portion of your test) in a",
"`VCR.use_cassette` block"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/getting-started"
],

:allow_http_connections_when_no_cassette => [
["If you only want VCR to handle requests made while a cassette is in use,",
"configure `allow_http_connections_when_no_cassette = true`. VCR will",
"ignore this request since it is made when there is no cassette"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/configuration/allow-http-connections-when-no-cassette"
],

:ignore_request => [
["If you want VCR to ignore this request (and others like it), you can",
"set an `ignore_request` callback"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/configuration/ignore-request"
],

:allow_playback_repeats => [
["The cassette contains an HTTP interaction that matches this request,",
"but it has already been played back. If you wish to allow a single HTTP",
"interaction to be played back multiple times, set the `:allow_playback_repeats`",
"cassette option"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/request-matching/playback-repeats"
],

:match_requests_on => [
["The cassette contains %s not been",
"played back. If your request is non-deterministic, you may need to",
"change your :match_requests_on cassette option to be more lenient",
"or use a custom request matcher to allow it to match"],
"https://www.relishapp.com/myronmarston/vcr/v/%s/docs/request-matching"
]
}

def suggestions
return no_cassette_suggestions unless cassette = VCR.current_cassette

[:use_new_episodes, :ignore_request].tap do |suggestions|
suggestions.push(*record_mode_suggestion)
suggestions << :allow_playback_repeats if cassette.http_interactions.has_used_interaction_matching?(request)
suggestions.map! { |k| ALL_SUGGESTIONS[k] }
suggestions.push(*match_requests_on_suggestion)
end
end

def no_cassette_suggestions
[:use_a_cassette, :allow_http_connections_when_no_cassette, :ignore_request].map do |key|
ALL_SUGGESTIONS[key]
end
end

def record_mode_suggestion
case VCR.current_cassette.record_mode
when :none then [:deal_with_none]
when :once then [:delete_cassette_for_once]
else []
end
end

def match_requests_on_suggestion
num_remaining_interactions = VCR.current_cassette.http_interactions.remaining_unused_interaction_count
return [] if num_remaining_interactions.zero?

interaction_description = if num_remaining_interactions == 1
"1 HTTP interaction that has"
else
"#{num_remaining_interactions} HTTP interactions that have"
end

description_lines, link = ALL_SUGGESTIONS[:match_requests_on]
description_lines = description_lines.dup
description_lines[0] = description_lines[0] % interaction_description
[[description_lines, link]]
end

end
end
end
Expand Down
40 changes: 40 additions & 0 deletions spec/vcr/cassette/http_interaction_list_spec.rb
Expand Up @@ -126,6 +126,46 @@ def respond_with(value)
end
end

describe "#has_used_interaction_matching?" do
it 'returns false when no interactions have been used' do
list.should_not have_used_interaction_matching(request_with(:method => :put))
end

it 'returns true when there is a matching used interaction (even if there is also an unused one that matches)' do
list.response_for(request_with(:method => :post))
list.should have_used_interaction_matching(request_with(:method => :post))
end

it 'returns false when none of the used interactions match' do
list.response_for(request_with(:method => :put))
list.should_not have_used_interaction_matching(request_with(:method => :post))
end
end

describe "#remaining_unused_interaction_count" do
it 'returns the number of unused interactions' do
list.remaining_unused_interaction_count.should eq(3)

list.response_for(request_with(:method => :get))
list.remaining_unused_interaction_count.should eq(3)

list.response_for(request_with(:method => :put))
list.remaining_unused_interaction_count.should eq(2)

list.response_for(request_with(:method => :put))
list.remaining_unused_interaction_count.should eq(2)

list.response_for(request_with(:method => :post))
list.remaining_unused_interaction_count.should eq(1)

list.response_for(request_with(:method => :post))
list.remaining_unused_interaction_count.should eq(0)

list.response_for(request_with(:method => :post))
list.remaining_unused_interaction_count.should eq(0)
end
end

describe "#response_for" do
it_behaves_like "an HTTP interaction finding method", :response_for do
def respond_with(value)
Expand Down

0 comments on commit 19c1c47

Please sign in to comment.