Skip to content
This repository has been archived by the owner on Dec 5, 2019. It is now read-only.

Commit

Permalink
Merge pull request #132 from thoughtworks/stenographer
Browse files Browse the repository at this point in the history
Stenographer logging and playback
  • Loading branch information
maxlinc committed Jul 5, 2014
2 parents 394bf95 + fe8548b commit b061db5
Show file tree
Hide file tree
Showing 16 changed files with 163 additions and 12 deletions.
1 change: 1 addition & 0 deletions lib/pacto.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
require 'pacto/investigation'
require 'pacto/meta_schema'
require 'pacto/hooks/erb_hook'
require 'pacto/observers/stenographer'
require 'pacto/generator'
require 'pacto/generator/filters'
require 'pacto/contract_files'
Expand Down
29 changes: 29 additions & 0 deletions lib/pacto/consumer.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
module Pacto
def self.simulate_consumer(consumer_name = :consumer, &block)
Consumer.new(consumer_name).simulate(&block)
end

class Consumer
include Logger
include Resettable

def initialize(name = :consumer)
@name = name
end

def simulate(&block)
instance_eval(&block)
end

def playback(stenographer_script)
script = File.read(stenographer_script)
instance_eval script, stenographer_script
end

def self.reset!
@actor = nil
@driver = nil
Expand All @@ -16,6 +34,17 @@ def self.actor=(actor)
@actor = actor
end

def request(contract, data = {})
contract = Pacto.contract_registry.find_by_name(contract) if contract.is_a? String
values = data[:values]
# response = data[:response]
logger.info "Sending request to #{contract.name.inspect}"
logger.info " with #{values.inspect} values"
self.class.reenact(contract, values)
rescue ContractNotFound => e
logger.warn "Ignoring request: #{e.message}"
end

def self.build_request(contract, data = {})
actor.build_request contract, data
end
Expand Down
3 changes: 2 additions & 1 deletion lib/pacto/core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ module Pacto
class Configuration
attr_accessor :adapter, :strict_matchers,
:contracts_path, :logger, :generator_options,
:hide_deprecations, :default_consumer, :default_provider
:hide_deprecations, :default_consumer, :default_provider, :stenographer_log_file
attr_reader :hook

def initialize
@middleware = Pacto::Core::HTTPMiddleware.new
@middleware.add_observer Pacto::Cops, :investigate
@generator = Pacto::Generator.new
@middleware.add_observer @generator, :generate
@stenographer_log_file ||= File.expand_path('pacto_stenographer.log')
@default_consumer = Pacto::Consumer
@default_provider = Pacto::Provider
@adapter = Stubs::WebMockAdapter.new(@middleware)
Expand Down
8 changes: 8 additions & 0 deletions lib/pacto/core/contract_registry.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
module Pacto
class ContractNotFound < StandardError; end

class ContractRegistry < Set
include Logger

Expand All @@ -8,6 +10,12 @@ def register(contract)
add contract
end

def find_by_name(name)
contract = select { |c| c.name == name }.first
fail ContractNotFound, "No contract found for #{name}" unless contract
contract
end

def contracts_for(request_signature)
select { |c| c.matches? request_signature }
end
Expand Down
13 changes: 13 additions & 0 deletions lib/pacto/core/investigation_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def self.reset!

def reset!
@investigations.clear
@stenographer = nil
end

def validated?(request_pattern)
Expand All @@ -26,6 +27,7 @@ def validated?(request_pattern)

def register_investigation(investigation)
@investigations << investigation
stenographer.log_investigation investigation
logger.info "Detected #{investigation.summary}"
investigation
end
Expand All @@ -41,5 +43,16 @@ def failed_investigations
!investigation.successful?
end
end

protected

def stenographer
@stenographer ||= create_stenographer
end

def create_stenographer
stenographer_log = File.open(Pacto.configuration.stenographer_log_file, 'a+')
Pacto::Observers::Stenographer.new stenographer_log
end
end
end
6 changes: 4 additions & 2 deletions lib/pacto/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ class Generator
def initialize(schema_version = 'draft3',
schema_generator = JSON::SchemaGenerator,
validator = Pacto::MetaSchema.new,
filters = Pacto::Generator::Filters.new)
filters = Pacto::Generator::Filters.new,
consumer = Pacto::Consumer.new)
@schema_version = schema_version
@validator = validator
@schema_generator = schema_generator
@filters = filters
@consumer = consumer
end

def generate(pacto_request, pacto_response)
Expand All @@ -36,7 +38,7 @@ def generate(pacto_request, pacto_response)

def generate_from_partial_contract(request_file, host)
contract = Pacto.load_contract request_file, host
request, response = contract.execute
request, response = @consumer.request(contract)
save(request_file, request, response)
end

Expand Down
41 changes: 41 additions & 0 deletions lib/pacto/observers/stenographer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Pacto
module Observers
class Stenographer
def initialize(output)
@output = output
end

def log_investigation(investigation)
return if @output.nil?

contract = investigation.contract
request = investigation.request
response = investigation.response
name = name_for(contract, request)
values = values_for(contract, request)

msg = "request #{name.inspect}, values: #{values.inspect}, response: {status: #{response.status}} # #{number_of_citations(investigation)} contract violations"
@output.puts msg
@output.flush
end

protected

def name_for(contract, request)
return "Unknown (#{request.uri})" if contract.nil?
contract.name
end

def number_of_citations(investigation)
return 0 if investigation.nil?
return 0 if investigation.citations.nil?
investigation.citations.size.to_s
end

def values_for(_contract, request)
# FIXME: Extract vars w/ URI::Template
request.uri.query_values
end
end
end
end
2 changes: 2 additions & 0 deletions lib/pacto/server/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def options_parser(opts, options) # rubocop:disable MethodLength
options[:strict] ||= false
options[:directory] ||= File.expand_path('contracts', @original_pwd)
options[:config] ||= File.expand_path('../config.rb', __FILE__)
options[:stenographer_log_file] ||= File.expand_path('pacto_stenographer.log', @original_pwd)
options[:strip_port] ||= true

opts.on('-l', '--live', 'Send requests to live services (instead of stubs)') { |_val| options[:live] = true }
Expand All @@ -33,6 +34,7 @@ def options_parser(opts, options) # rubocop:disable MethodLength
opts.on('-r', '--recursive-loading', 'Load contracts from folders named after the host to be stubbed') { |_val| options[:recursive_loading] = true }
opts.on('--strip-port', 'Strip the port from the request URI to build the proxied URI') { |_val| options[:strip_port] = true }
opts.on('--strip-dev', 'Strip .dev from the request domain to build the proxied URI') { |_val| options[:strip_dev] = true }
opts.on('--stenographer-log-file', 'Location for the stenographer log file') { |val| options[:stenographer_log_file] = val }
end

def response(env)
Expand Down
1 change: 1 addition & 0 deletions lib/pacto/server/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def prepare_contracts(contracts)
schema_version: :draft3,
token_map: token_map
}
pacto_config.stenographer_log_file = options[:stenographer_log_file]
end

if options[:generate]
Expand Down
1 change: 1 addition & 0 deletions samples/contracts/localhost/api/echo.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "Echo",
"request": {
"headers": {
},
Expand Down
1 change: 1 addition & 0 deletions samples/contracts/localhost/api/ping.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "Ping",
"request": {
"headers": {
},
Expand Down
16 changes: 16 additions & 0 deletions samples/stenographer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require 'pacto'
Pacto.configure do |c|
c.contracts_path = 'contracts'
end
contracts = Pacto.load_contracts('contracts', 'http://localhost:5000')
contracts.stub_providers

Pacto.simulate_consumer do
request 'Echo', values: nil, response: { status: 200 } # 0 contract violations
request 'Ping', values: nil, response: { status: 200 } # 0 contract violations
request 'Unknown (http://localhost:8000/404)', values: nil, response: { status: 500 } # 0 contract violations
end

Pacto.simulate_consumer :my_consumer do
playback 'pacto_stenographer.log'
end
1 change: 1 addition & 0 deletions spec/fixtures/contracts/simple_contract.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "Simple Contract",
"request": {
"http_method": "GET",
"path": "/hello",
Expand Down
5 changes: 3 additions & 2 deletions spec/unit/pacto/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ module Pacto
let(:schema_generator) { double('schema_generator') }
let(:validator) { double('validator') }
let(:filters) { double :filters }
let(:consumer) { double 'consumer' }
let(:request_file) { 'request.json' }
let(:generator) { described_class.new version, schema_generator, validator, filters }
let(:generator) { described_class.new version, schema_generator, validator, filters, consumer }
let(:request_contract) do
Fabricate(:partial_contract, request: request_clause, file: request_file)
end
Expand All @@ -41,7 +42,7 @@ def pretty(obj)
let(:generated_contract) { Fabricate(:contract) }
before do
expect(Pacto).to receive(:load_contract).with(request_file, record_host).and_return request_contract
expect(request_contract).to receive(:execute).and_return([request, response_adapter])
expect(consumer).to receive(:request).with(request_contract).and_return([request, response_adapter])
end

it 'parses the request' do
Expand Down
15 changes: 8 additions & 7 deletions spec/unit/pacto/investigation_registry_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
subject(:registry) { described_class.instance }
let(:request_pattern) { Fabricate(:webmock_request_pattern) }
let(:request_signature) { Fabricate(:webmock_request_signature) }
let(:pacto_response) { Fabricate(:pacto_response) }
let(:different_request_signature) { Fabricate(:webmock_request_signature, uri: 'www.thoughtworks.com') }
let(:investigation) { Pacto::Investigation.new(request_signature, double, nil, []) }
let(:investigation_for_a_similar_request) { Pacto::Investigation.new(request_signature, double, nil, []) }
let(:investigation_for_a_different_request) { Pacto::Investigation.new(different_request_signature, double, nil, []) }
let(:investigation) { Pacto::Investigation.new(request_signature, pacto_response, nil, []) }
let(:investigation_for_a_similar_request) { Pacto::Investigation.new(request_signature, pacto_response, nil, []) }
let(:investigation_for_a_different_request) { Pacto::Investigation.new(different_request_signature, pacto_response, nil, []) }

before(:each) do
registry.reset!
Expand Down Expand Up @@ -42,7 +43,7 @@
let(:contract) { Fabricate(:contract) }

it 'returns investigations with no contract' do
investigation_with_citations = Pacto::Investigation.new(different_request_signature, double, contract, [])
investigation_with_citations = Pacto::Investigation.new(different_request_signature, pacto_response, contract, [])
registry.register_investigation(investigation)
registry.register_investigation(investigation_for_a_similar_request)
registry.register_investigation(investigation_for_a_different_request)
Expand All @@ -54,12 +55,12 @@

describe '.failed_investigations' do
let(:contract) { Fabricate(:contract) }
let(:citations2) { double('citations2', :empty? => false, :join => 'wtf') }
let(:citations2) { ['a sample citation'] }

it 'returns investigations with unsuccessful citations' do
allow(contract).to receive(:name).and_return 'test'
investigation_with_successful_citations = Pacto::Investigation.new(request_signature, double, nil, ['error'])
investigation_with_unsuccessful_citations = Pacto::Investigation.new(request_signature, double, nil, %w(error2 error3))
investigation_with_successful_citations = Pacto::Investigation.new(request_signature, pacto_response, nil, ['error'])
investigation_with_unsuccessful_citations = Pacto::Investigation.new(request_signature, pacto_response, nil, %w(error2 error3))

expect(investigation_with_successful_citations).to receive(:successful?).and_return true
expect(investigation_with_unsuccessful_citations).to receive(:successful?).and_return false
Expand Down
32 changes: 32 additions & 0 deletions spec/unit/pacto/stubs/observers/stenographer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'spec_helper'

module Pacto
module Observers
describe Stenographer do
let(:pacto_request) { Fabricate(:pacto_request) }
let(:pacto_response) { Fabricate(:pacto_response) }
let(:contract) { Fabricate(:contract) }
let(:citations) { %w(one two) }
let(:investigation) { Pacto::Investigation.new(pacto_request, pacto_response, contract, citations) }

subject(:stream) { StringIO.new }

subject { described_class.new stream }

it 'writes to the stenographer log stream' do
subject.log_investigation investigation
expected_log_line = "request #{contract.name.inspect}, values: {}, response: {status: #{pacto_response.status}} # #{citations.size} contract violations\n"
expect(stream.string).to eq expected_log_line
end

context 'when the stenographer log stream is nil' do
let(:stream) { nil }

it 'does nothing' do
# Would raise an error if it tried to write to stream
subject.log_investigation investigation
end
end
end
end
end

0 comments on commit b061db5

Please sign in to comment.