Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring in Shopify modifications #2

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ee1bd80
convert from stubbed TCPRecordableSocket to module in a real TCPSocket
Mar 20, 2014
a61be26
refactor turned_off
Mar 20, 2014
2f45e81
use a class for the current recording
Mar 20, 2014
2c43870
move functionality from Recordable to Cassette
Mar 20, 2014
b372bc7
always write at the end of a block, even if the socket isn't closed
Mar 20, 2014
8fadb4f
yield the current cassette
Mar 20, 2014
efb6ca2
refactoring recordable; adding some more recorded methods
Mar 20, 2014
6cb8be0
build cassettes with a factory method
Mar 20, 2014
a112abc
split up into two types of cassettes
Mar 20, 2014
2362791
refactor cassette
Mar 20, 2014
3962d5e
refactor sessions
Mar 20, 2014
327c833
clean up return values a little
Mar 20, 2014
fd41bfe
store sessions in json attribute; paving the way for new cassette-lev…
Mar 20, 2014
4b61924
store recordings as objects, too
Mar 20, 2014
22c282c
add originally_recorded_at attribute to cassettes
Mar 20, 2014
3ee8526
remove to_io
Mar 20, 2014
9131fa0
override return value for connect
Mar 20, 2014
c21732b
add ability to check if sessions are empty
Mar 20, 2014
7c0cb49
remove unused method param
Mar 20, 2014
fb85c55
record args for read commands
Mar 20, 2014
cb7a4c0
apply changes to SSL sockets, too
Mar 20, 2014
e802608
Merge pull request #1 from Shopify/modifications
peterjm Mar 22, 2014
8b6e75e
Add `#read` to the recordable methods
Mar 25, 2014
a818403
Merge pull request #2 from Shopify/add-read-to-recordable-socket
nwjsmith Mar 25, 2014
c7c5e8d
Extract separate extension for SSL; don't prepend to OpenSSL::SSL::So…
girasquid Oct 29, 2014
7b3314e
Merge pull request #3 from Shopify/dont_prepend_ssl
girasquid Oct 30, 2014
b294f63
Cut shopify-tcr gem
girasquid Oct 30, 2014
463d18a
Merge pull request #4 from Shopify/shopify-tcr
girasquid Oct 30, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions lib/tcr.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
require "tcr/cassette"
require "tcr/configuration"
require "tcr/errors"
require "tcr/recordable_tcp_socket"
require "tcr/recordable"
require "tcr/socket_extension"
require "tcr/ssl_socket_extension"
require "tcr/version"
require "socket"
require "json"


module TCR
ALL_PORTS = '*'

extend self

def configure
Expand All @@ -34,37 +37,31 @@ def disabled=(v)
@disabled = v
end

def save_session
def record_port?(port)
!disabled && configuration.hook_tcp_ports == ALL_PORTS || configuration.hook_tcp_ports.include?(port)
end

def use_cassette(name, options = {}, &block)
raise ArgumentError, "`TCR.use_cassette` requires a block." unless block
TCR.cassette = Cassette.new(name)
yield
TCR.cassette = nil
begin
TCR.cassette = Cassette.build(name)
yield TCR.cassette
ensure
TCR.cassette.finish
TCR.cassette = nil
end
end

def turned_off(&block)
raise ArgumentError, "`TCR.turned_off` requires a block." unless block
current_hook_tcp_ports = configuration.hook_tcp_ports
configuration.hook_tcp_ports = []
yield
configuration.hook_tcp_ports = current_hook_tcp_ports
end
end


# The monkey patch shim
class TCPSocket
class << self
alias_method :real_open, :open

def open(address, port)
if TCR.configuration.hook_tcp_ports.include?(port)
TCR::RecordableTCPSocket.new(address, port, TCR.cassette)
else
real_open(address, port)
end
begin
disabled = true
yield
ensure
disabled = false
end
end
end

TCPSocket.prepend(TCR::SocketExtension)
OpenSSL::SSL::SSLSocket.send(:include, TCR::SSLSocketExtension)
178 changes: 157 additions & 21 deletions lib/tcr/cassette.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,175 @@ module TCR
class Cassette
attr_reader :name

def initialize(name)
@name = name

if File.exists?(filename)
@recording = false
@contents = File.open(filename) { |f| f.read }
@sessions = JSON.parse(@contents)
def self.build(name)
if cassette_exists?(name)
RecordedCassette.new(name)
else
@recording = true
@sessions = []
RecordingCassette.new(name)
end
end

def recording?
@recording
def self.filename(name)
"#{TCR.configuration.cassette_library_dir}/#{name}.json"
end

def next_session
session = @sessions.shift
raise NoMoreSessionsError unless session
session
def self.cassette_exists?(name)
File.exists?(filename(name))
end

def append(session)
raise "Can't append session unless recording" unless recording?
@sessions << session
File.open(filename, "w") { |f| f.write(JSON.pretty_generate(@sessions)) }
def initialize(name)
@name = name
end

protected

def filename
"#{TCR.configuration.cassette_library_dir}/#{name}.json"
self.class.filename(name)
end

class RecordingCassette < Cassette
attr_reader :originally_recorded_at

def initialize(*args)
super
@originally_recorded_at = Time.now
end

def sessions
@sessions ||= []
end

def next_session
Session.new.tap do |session|
sessions << session
end
end

def finish
FileUtils.mkdir_p(File.dirname(filename))
File.open(filename, "w") { |f| f.write(serialized_form) }
end

private

def serialized_form
raw = {
"originally_recorded_at" => originally_recorded_at,
'sessions' => sessions.map(&:as_json)
}
JSON.pretty_generate(raw)
end

def empty?
true
end

class Session
def initialize
@recording = []
end

def connect(&block)
next_command('connect', ret: nil, &block)
end

def close(&block)
next_command('close', &block)
end

def read(*args, &block)
next_command('read', data: args, &block)
end

def write(str, &block)
next_command('write', data: str, &block)
end

def as_json
{"recording" => @recording}
end

private

def next_command(command, options={}, &block)
yield.tap do |return_value|
return_value = options[:ret] if options.has_key?(:ret)
@recording << [command, return_value] + Array(options[:data])
end
end
end
end

class RecordedCassette < Cassette
def sessions
@sessions ||= serialized_form['sessions'].map{|raw| Session.new(raw)}
end

def originally_recorded_at
serialized_form['originally_recorded_at']
end

def next_session
session = sessions.shift
raise NoMoreSessionsError unless session
session
end

def finish
# no-op
end

def empty?
sessions.all?(&:empty?)
end

private

def serialized_form
@serialized_form ||= begin
raw = File.open(filename) { |f| f.read }
JSON.parse(raw)
end
end

class Session
def initialize(raw)
@recording = raw["recording"]
end

def connect
next_command('connect')
end

def close
next_command('close')
end

def read(*args)
next_command('read') do |str, *data|
raise TCR::DataMismatchError.new("Expected to read to be called with args '#{args.inspect}' but was called with '#{data.inspect}'") unless args == data
end
end

def write(str)
next_command('write') do |len, data|
raise TCR::DataMismatchError.new("Expected to write '#{str}' but next data in recording was '#{data}'") unless str == data
end
end

def empty?
@recording.empty?
end

private

def next_command(expected)
command, return_value, data = @recording.shift
raise TCR::CommandMismatchError.new("Expected to '#{expected}' but next in recording was '#{command}'") unless expected == command
yield return_value, *data if block_given?
return_value
end
end
end
end
end
end
4 changes: 2 additions & 2 deletions lib/tcr/errors.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module TCR
class TCRError < StandardError; end
class NoCassetteError < TCRError; end
class NoMoreSessionsError < TCRError; end
class DirectionMismatchError < TCRError; end
class CommandMismatchError < TCRError; end
class DataMismatchError < TCRError; end
end
45 changes: 45 additions & 0 deletions lib/tcr/recordable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module TCR
module Recordable
attr_accessor :cassette

def recording
@recording ||= cassette.next_session
end

def connect
recording.connect do
super
end
end

def read_nonblock(bytes)
recording.read do
super
end
end

def gets(*args)
recording.read do
super
end
end

def write(str)
recording.write(str) do
super
end
end

def read(*args)
recording.read do
super
end
end

def close
recording.close do
super
end
end
end
end
66 changes: 0 additions & 66 deletions lib/tcr/recordable_tcp_socket.rb

This file was deleted.

Loading