ephemeral_response 0.1.0 → 0.2.0

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.
data/History.markdown ADDED
@@ -0,0 +1,18 @@
1
+ History
2
+ =======
3
+
4
+ 0.2.0 / master
5
+ --------------
6
+
7
+ #### Enhancements
8
+
9
+ * Fixtures now have use .yml extension instead of .fixture.
10
+ * Varying POST data and query strings create new fixtures. Previously, GET /
11
+ and GET /?foo=bar resulted in the same fixture.
12
+ * Ability to reset configuration with EphemeralResponse::Configuration.reset
13
+ * Ability to white list certain hosts. Responses will not be saved for Requests
14
+ made to hosts in the white list.
15
+ Use EphemeralResponse::Configuration.white_list = "localhost"
16
+ * Ephemeral response prints to the Net/HTTP debugger when establishing a
17
+ connection. Just set http.set_debug_output = $stdout to see when Ephemeral
18
+ Response connects to a host.
data/README.markdown CHANGED
@@ -3,50 +3,87 @@ Ephemeral Response
3
3
 
4
4
  _Save HTTP responses to give your tests a hint of reality._
5
5
 
6
- This is pretty much NetRecorder without the fakeweb dependency.
7
-
8
6
  ## Premise
9
7
 
10
- 1. run tests
8
+ Web responses are volatile. Servers go down, API's change, responses change and
9
+ everytime something changes, your tests should fail. Mocking out web responses
10
+ may speed up your test suite but the tests essentially become lies. Ephemeral
11
+ Response encourages you to run your tests against real web services while
12
+ keeping your test suite snappy by caching the responses and reusing them until
13
+ they expire.
14
+
15
+ 1. run test suite
11
16
  2. all responses are saved to fixtures
12
- 3. run tests
13
- 4. Return the cached response if it exists and isn't out of date.
17
+ 3. disconnect from the network
18
+ 4. run test suite
19
+
20
+ ## Example
21
+
22
+ require 'benchmark'
23
+ require 'ephemeral_response'
14
24
 
15
- If a cached response exists but is out of date, update it with the real response
25
+ EphemeralResponse.activate
16
26
 
17
- Cache the response if it doesn't exist
27
+ 5.times do
28
+ puts Benchmark.realtime { Net::HTTP.get "example.com", "/" }
29
+ end
18
30
 
19
- ## Usage
31
+ 1.44242906570435 # First request caches the response as a fixture
32
+ 0.000689029693603516
33
+ 0.000646829605102539
34
+ 0.00064396858215332
35
+ 0.000645875930786133
20
36
 
21
- `$ vi spec/spec_helper.rb`
37
+ ## With Rspec
22
38
 
23
39
  require 'ephemeral_response'
24
40
 
25
41
  Spec::Runner.configure do |config|
42
+
26
43
  config.before(:suite) do
27
44
  EphemeralResponse.activate
28
45
  end
46
+
29
47
  config.after(:suite) do
30
48
  EphemeralResponse.deactivate
31
49
  end
50
+
32
51
  end
33
52
 
53
+ All responses are cached in yaml files within spec/fixtures/ephemeral\_response.
54
+
55
+ I'd recommend git ignoring this directory to ensure your tests always hit the
56
+ remote service at least once and to prevent credentials (like API keys) from
57
+ being stored in your repo.
58
+
34
59
  ### Configuration
35
- You can change the fixture directory which defaults to "spec/fixtures/ephemeral_response"
60
+
61
+ Change the fixture directory; defaults to "spec/fixtures/ephemeral\_response"
36
62
 
37
63
  EphemeralResponse::Configuration.fixture_directory = "test/fixtures/ephemeral_response"
38
64
 
39
- You can change the elapsed time for when a fixture will expire; defaults to 24 hours
65
+ Change the elapsed time for when a fixture will expire; defaults to 24 hours
40
66
 
41
67
  EphemeralResponse::Configuration.expiration = 86400 # 24 hours in seconds
42
68
 
43
- You can also pass a block when setting expiration which gets instance_eval'd
44
- giving you access to the awesome helper method `one_day`
69
+ Pass a block when setting expiration to gain access to the awesome helper
70
+ method `one_day`
45
71
 
46
- EphemeralResponse::Configuration.expiration do
47
- one_day * 30 # 60 * 60 * 24 * 30
72
+ EphemeralResponse::Configuration.expiration = lambda do
73
+ one_day * 30 # Expire in thirty days: 60 * 60 * 24 * 30
48
74
  end
49
75
 
76
+ Always allow requests to be made to a host by adding it to the white list.
77
+ Helpful when running ephemeral response with selenium which makes requests to
78
+ the local server.
79
+
80
+ EphemeralResponse::Configuration.white_list = "localhost", "smackaho.st"
81
+
82
+ ## Similar Projects
83
+ * [Net Recorder](http://github.com/chrisyoung/netrecorder)
84
+ * [Stalefish](http://github.com/jsmestad/stale_fish)
85
+ * [VCR](http://github.com/myronmarston/vcr)
86
+
50
87
  ## Note on Patches/Pull Requests
51
88
 
52
89
  * Fork the project.
@@ -59,4 +96,4 @@ giving you access to the awesome helper method `one_day`
59
96
 
60
97
  ## Copyright
61
98
 
62
- Copyright (c) 2010 Sandro Turriate. See LICENSE for details.
99
+ Copyright (c) 2010 Sandro Turriate. See MIT_LICENSE for details.
data/Rakefile CHANGED
@@ -14,8 +14,9 @@ begin
14
14
  gem.homepage = "http://github.com/sandro/ephemeral_response"
15
15
  gem.authors = ["Sandro Turriate"]
16
16
  gem.add_development_dependency "rspec", ">= 1.2.9"
17
- gem.add_development_dependency "yard", ">= 0"
17
+ gem.add_development_dependency "yard", ">= 0.5.0"
18
18
  gem.add_development_dependency "fakefs", ">= 0.2.1"
19
+ gem.add_development_dependency "unicorn", ">= 1.0.0"
19
20
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
21
  end
21
22
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift("lib")
2
+ require 'rubygems'
3
+ require 'lib/ephemeral_response'
4
+ require 'benchmark'
5
+
6
+ EphemeralResponse::Configuration.expiration = 5
7
+ EphemeralResponse.activate
8
+
9
+ # Run benchmarks against thefuckingweather.com
10
+ # The first request takes much longer than the rest
11
+ def benchmark_request(number=1)
12
+ uri = URI.parse('http://thefuckingweather.com/?RANDLOC=')
13
+ time = Benchmark.realtime do
14
+ Net::HTTP.get(uri)
15
+ end
16
+ puts "Request #{number} took #{time} secs"
17
+ end
18
+
19
+ 5.times {|n| benchmark_request n + 1 }
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift("lib")
2
+ require 'rubygems'
3
+ require 'lib/ephemeral_response'
4
+
5
+ # Don't create fixtures for the localhost domain
6
+ EphemeralResponse::Configuration.white_list = 'localhost'
7
+
8
+ EphemeralResponse::Configuration.expiration = 5
9
+ EphemeralResponse.activate
10
+
11
+ # Start an HTTP server on port 19876 using netcat
12
+ process = IO.popen %(echo "HTTP/1.1 200 OK\n\n" | nc -l 19876)
13
+ at_exit { Process.kill :KILL, process.pid }
14
+ sleep 1
15
+
16
+ # Make a request to the server started above
17
+ # No new fixtures are created in spec/fixtures/ephemeral_response/
18
+ uri = URI.parse('http://localhost:19876/')
19
+ Net::HTTP.get(uri)
20
+
21
+ # Fixtures are still created for Google
22
+ uri = URI.parse('http://www.google.com/')
23
+ Net::HTTP.get(uri)
24
+
25
+ puts "The following directory should only contain a fixture for google"
26
+ puts
27
+ listing_cmd = %(ls #{File.expand_path(EphemeralResponse::Configuration.fixture_directory)})
28
+ puts listing_cmd
29
+ puts %x(#{listing_cmd})
@@ -19,6 +19,20 @@ module EphemeralResponse
19
19
  @expiration || one_day
20
20
  end
21
21
 
22
+ def reset
23
+ @expiration = nil
24
+ @fixture_directory = nil
25
+ @white_list = nil
26
+ end
27
+
28
+ def white_list
29
+ @white_list ||= []
30
+ end
31
+
32
+ def white_list=(*hosts)
33
+ @white_list = hosts.flatten
34
+ end
35
+
22
36
  protected
23
37
 
24
38
  def one_day
@@ -1,7 +1,7 @@
1
1
  module EphemeralResponse
2
2
  class Fixture
3
3
  attr_accessor :response
4
- attr_reader :method, :uri, :created_at
4
+ attr_reader :request, :uri, :created_at
5
5
 
6
6
  def self.fixtures
7
7
  @fixtures ||= {}
@@ -11,20 +11,27 @@ module EphemeralResponse
11
11
  @fixtures = {}
12
12
  end
13
13
 
14
- def self.find(uri, method)
15
- fixtures[Fixture.new(uri, method).identifier]
14
+ def self.find(uri, request)
15
+ fixtures[Fixture.new(uri, request).identifier]
16
16
  end
17
17
 
18
18
  def self.load_all
19
19
  clear
20
20
  if File.directory?(Configuration.fixture_directory)
21
- Dir.glob("#{Configuration.fixture_directory}/*.fixture", &method(:load_fixture))
21
+ Dir.glob("#{Configuration.fixture_directory}/*.yml", &method(:load_fixture))
22
22
  end
23
23
  fixtures
24
24
  end
25
25
 
26
26
  def self.load_fixture(file_name)
27
- fixture = YAML.load_file file_name
27
+ register YAML.load_file(file_name)
28
+ end
29
+
30
+ def self.find_or_initialize(uri, request, &block)
31
+ find(uri, request) || new(uri, request, &block)
32
+ end
33
+
34
+ def self.register(fixture)
28
35
  if fixture.expired?
29
36
  FileUtils.rm fixture.path
30
37
  else
@@ -32,26 +39,22 @@ module EphemeralResponse
32
39
  end
33
40
  end
34
41
 
35
- def self.respond_to(uri, method)
36
- fixture = Fixture.new(uri, method)
37
- unless fixtures[fixture.identifier]
42
+ def self.respond_to(uri, request)
43
+ find_or_initialize(uri, request) do |fixture|
38
44
  fixture.response = yield
39
- fixture.save
40
- fixtures[fixture.identifier] = fixture
41
- end
42
- fixtures[fixture.identifier].response
45
+ fixture.register
46
+ end.response
43
47
  end
44
48
 
45
- def initialize(uri, method)
46
- @method = method
47
- @uri = uri
48
- @uri.normalize!
49
+ def initialize(uri, request)
50
+ @uri = uri.normalize
51
+ @request = deep_dup request
49
52
  @created_at = Time.now
50
53
  yield self if block_given?
51
54
  end
52
55
 
53
56
  def ==(other)
54
- %w(method uri created_at response).all? do |attribute|
57
+ %w(request_identifier uri_identifier created_at response).all? do |attribute|
55
58
  send(attribute) == other.send(attribute)
56
59
  end
57
60
  end
@@ -65,12 +68,19 @@ module EphemeralResponse
65
68
  end
66
69
 
67
70
  def identifier
68
- Digest::SHA1.hexdigest(normalized_name)[0..6]
71
+ Digest::SHA1.hexdigest("#{uri_identifier}#{request_identifier}")
72
+ end
73
+
74
+ def method
75
+ request.method
69
76
  end
70
77
 
71
78
  def normalized_name
72
- normalized_path = uri.path.gsub(/\/$/, '').gsub('/', '-')
73
- [uri.host, method, normalized_path].join("_")
79
+ [uri.host, method, fs_path].compact.join("_").gsub(/[\/.]/, '-')
80
+ end
81
+
82
+ def fs_path
83
+ uri.path.dup.sub!(/^\/(.+)$/, '\1')
74
84
  end
75
85
 
76
86
  def path
@@ -78,7 +88,14 @@ module EphemeralResponse
78
88
  end
79
89
 
80
90
  def register
81
- FakeWeb.register_uri(method, uri, :response => response)
91
+ unless Configuration.white_list.include? uri.host
92
+ save
93
+ self.class.register self
94
+ end
95
+ end
96
+
97
+ def request_identifier
98
+ request.to_yaml.split(//).sort
82
99
  end
83
100
 
84
101
  def save
@@ -88,10 +105,24 @@ module EphemeralResponse
88
105
  end
89
106
  end
90
107
 
108
+ def uri_identifier
109
+ if uri.query
110
+ parts = uri.to_s.split("?", 2)
111
+ parts[1] = parts[1].split('&').sort
112
+ parts
113
+ else
114
+ uri.to_s
115
+ end
116
+ end
117
+
91
118
  protected
92
119
 
120
+ def deep_dup(object)
121
+ Marshal.load(Marshal.dump(object))
122
+ end
123
+
93
124
  def generate_file_name
94
- "#{normalized_name}_#{identifier}.fixture"
125
+ "#{normalized_name}_#{identifier[0..6]}.yml"
95
126
  end
96
127
  end
97
128
  end
@@ -3,18 +3,28 @@ module Net
3
3
  alias request_without_ephemeral_response request
4
4
  alias connect_without_ephemeral_response connect
5
5
 
6
+ attr_reader :uri
7
+
6
8
  def connect
7
9
  end
8
10
  private :connect
9
11
 
12
+ def do_start_without_ephemeral_response
13
+ D "EphemeralResponse: establishing connection to #{uri}"
14
+ connect_without_ephemeral_response
15
+ @started = true
16
+ end
17
+ private :do_start_without_ephemeral_response
18
+
10
19
  def generate_uri(request)
11
20
  scheme = use_ssl? ? "https" : "http"
12
- URI.parse("#{scheme}://#{conn_address}:#{conn_port}#{request.path}")
21
+ @uri = URI.parse("#{scheme}://#{conn_address}:#{conn_port}#{request.path}")
13
22
  end
14
23
 
15
24
  def request(request, body = nil, &block)
16
- EphemeralResponse::Fixture.respond_to(generate_uri(request), request.method) do
17
- connect_without_ephemeral_response
25
+ generate_uri(request)
26
+ EphemeralResponse::Fixture.respond_to(uri, request) do
27
+ do_start_without_ephemeral_response
18
28
  request_without_ephemeral_response(request, body, &block)
19
29
  end
20
30
  end
@@ -7,7 +7,7 @@ require 'ephemeral_response/configuration'
7
7
  require 'ephemeral_response/fixture'
8
8
 
9
9
  module EphemeralResponse
10
- VERSION = "0.1.0".freeze
10
+ VERSION = "0.2.0".freeze
11
11
 
12
12
  def self.activate
13
13
  deactivate
@@ -18,6 +18,7 @@ module EphemeralResponse
18
18
  def self.deactivate
19
19
  Net::HTTP.class_eval do
20
20
  remove_method(:generate_uri) if method_defined?(:generate_uri)
21
+ remove_method(:uri) if method_defined?(:uri)
21
22
  alias_method(:connect, :connect_without_ephemeral_response) if private_method_defined?(:connect_without_ephemeral_response)
22
23
  alias_method(:request, :request_without_ephemeral_response) if method_defined?(:request_without_ephemeral_response)
23
24
  end
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe EphemeralResponse::Configuration do
4
4
  subject { EphemeralResponse::Configuration }
5
+
5
6
  describe "#fixture_directory" do
6
7
  it "has a default" do
7
8
  subject.fixture_directory.should == "spec/fixtures/ephemeral_response"
@@ -36,4 +37,51 @@ describe EphemeralResponse::Configuration do
36
37
  end
37
38
  end
38
39
  end
40
+
41
+ describe "#reset" do
42
+ it "resets expiration, fixture directory, and white list to the defaults" do
43
+ subject.fixture_directory = "test/fixtures/ephemeral_response"
44
+ subject.expiration = 1
45
+ subject.white_list = 'localhost'
46
+ subject.fixture_directory.should == "test/fixtures/ephemeral_response"
47
+ subject.expiration.should == 1
48
+ subject.white_list.should == ['localhost']
49
+
50
+ subject.reset
51
+
52
+ subject.fixture_directory.should == "spec/fixtures/ephemeral_response"
53
+ subject.expiration.should == 86400
54
+ subject.white_list.should == []
55
+ end
56
+
57
+ it "resets white list after the default has been modified" do
58
+ subject.white_list << "localhost"
59
+ subject.reset
60
+ subject.white_list.should be_empty
61
+ end
62
+ end
63
+
64
+ describe "#white_list" do
65
+ it "defaults to an empty array" do
66
+ subject.white_list.should == []
67
+ end
68
+
69
+ it "allows hosts to be pushed onto the white list" do
70
+ subject.white_list << 'localhost'
71
+ subject.white_list << 'smackaho.st'
72
+ subject.white_list.should == %w(localhost smackaho.st)
73
+ end
74
+ end
75
+
76
+ describe "#white_list=" do
77
+ it "sets a single host" do
78
+ subject.white_list = 'localhost'
79
+ subject.white_list.should == ['localhost']
80
+ end
81
+
82
+ it "sets multiple hosts" do
83
+ subject.white_list = 'localhost', 'smackaho.st'
84
+ subject.white_list.should == ['localhost', 'smackaho.st']
85
+ end
86
+ end
39
87
  end