pact 0.1.28

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.
Files changed (72) hide show
  1. data/.gitignore +28 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +5 -0
  4. data/Gemfile.lock +83 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +238 -0
  7. data/Rakefile +33 -0
  8. data/bin/pact +4 -0
  9. data/lib/pact/app.rb +32 -0
  10. data/lib/pact/configuration.rb +54 -0
  11. data/lib/pact/consumer/app_manager.rb +177 -0
  12. data/lib/pact/consumer/configuration_dsl.rb +71 -0
  13. data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
  14. data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
  15. data/lib/pact/consumer/dsl.rb +98 -0
  16. data/lib/pact/consumer/interaction.rb +70 -0
  17. data/lib/pact/consumer/mock_service.rb +340 -0
  18. data/lib/pact/consumer/rspec.rb +43 -0
  19. data/lib/pact/consumer/run_condor.rb +4 -0
  20. data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
  21. data/lib/pact/consumer/service_consumer.rb +22 -0
  22. data/lib/pact/consumer/service_producer.rb +23 -0
  23. data/lib/pact/consumer.rb +7 -0
  24. data/lib/pact/consumer_contract.rb +110 -0
  25. data/lib/pact/json_warning.rb +23 -0
  26. data/lib/pact/logging.rb +14 -0
  27. data/lib/pact/matchers/matchers.rb +85 -0
  28. data/lib/pact/matchers.rb +1 -0
  29. data/lib/pact/producer/configuration_dsl.rb +62 -0
  30. data/lib/pact/producer/matchers.rb +22 -0
  31. data/lib/pact/producer/pact_spec_runner.rb +57 -0
  32. data/lib/pact/producer/producer_state.rb +81 -0
  33. data/lib/pact/producer/rspec.rb +127 -0
  34. data/lib/pact/producer/test_methods.rb +89 -0
  35. data/lib/pact/producer.rb +1 -0
  36. data/lib/pact/rake_task.rb +64 -0
  37. data/lib/pact/reification.rb +26 -0
  38. data/lib/pact/request.rb +109 -0
  39. data/lib/pact/term.rb +40 -0
  40. data/lib/pact/verification_task.rb +57 -0
  41. data/lib/pact/version.rb +3 -0
  42. data/lib/pact.rb +5 -0
  43. data/lib/tasks/pact.rake +6 -0
  44. data/pact.gemspec +36 -0
  45. data/scratchpad.txt +36 -0
  46. data/spec/features/consumption_spec.rb +146 -0
  47. data/spec/features/producer_states/zebras.rb +28 -0
  48. data/spec/features/production_spec.rb +160 -0
  49. data/spec/integration/pact/configuration_spec.rb +65 -0
  50. data/spec/lib/pact/configuration_spec.rb +35 -0
  51. data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
  52. data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
  53. data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
  54. data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
  55. data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
  56. data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
  57. data/spec/lib/pact/consumer_contract_spec.rb +125 -0
  58. data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
  59. data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
  60. data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
  61. data/spec/lib/pact/producer/rspec_spec.rb +48 -0
  62. data/spec/lib/pact/reification_spec.rb +43 -0
  63. data/spec/lib/pact/request_spec.rb +316 -0
  64. data/spec/lib/pact/term_spec.rb +36 -0
  65. data/spec/lib/pact/verification_task_spec.rb +64 -0
  66. data/spec/spec_helper.rb +5 -0
  67. data/spec/support/a_consumer-a_producer.json +34 -0
  68. data/spec/support/pact_rake_support.rb +41 -0
  69. data/spec/support/test_app_fail.json +22 -0
  70. data/spec/support/test_app_pass.json +21 -0
  71. data/tasks/pact-test.rake +19 -0
  72. metadata +381 -0
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ *.swp
18
+ .bin
19
+ tags
20
+ .rbenv-version
21
+ spec/pacts
22
+ .rvmrc
23
+ *.iml
24
+ .rakeTasks
25
+ *~
26
+ .ruby-gemset
27
+ .ruby-version
28
+ log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source 'http://rea-rubygems'
3
+
4
+ # Specify your gem's dependencies in pact.gemspec
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,83 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pact (0.1.28)
5
+ awesome_print (~> 1.1.0)
6
+ capybara (~> 2.1.0)
7
+ find_a_port (~> 1.0.1)
8
+ hashie (~> 2.0.5)
9
+ json
10
+ rack-test (~> 0.6.2)
11
+ randexp (~> 0.1.7)
12
+ rspec (~> 2.12)
13
+ thin
14
+ thor
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ remote: http://rea-rubygems/
19
+ specs:
20
+ addressable (2.3.5)
21
+ awesome_print (1.1.0)
22
+ capybara (2.1.0)
23
+ mime-types (>= 1.16)
24
+ nokogiri (>= 1.3.3)
25
+ rack (>= 1.0.0)
26
+ rack-test (>= 0.5.4)
27
+ xpath (~> 2.0)
28
+ coderay (1.0.9)
29
+ crack (0.4.0)
30
+ safe_yaml (~> 0.9.0)
31
+ daemons (1.1.9)
32
+ diff-lcs (1.2.4)
33
+ eventmachine (1.0.3)
34
+ find_a_port (1.0.1)
35
+ geminabox-client (0.0.2)
36
+ multipart-post
37
+ hashie (2.0.5)
38
+ json (1.8.0)
39
+ method_source (0.8.1)
40
+ mime-types (1.23)
41
+ mini_portile (0.5.1)
42
+ multipart-post (1.2.0)
43
+ nokogiri (1.6.0)
44
+ mini_portile (~> 0.5.0)
45
+ pry (0.9.12.2)
46
+ coderay (~> 1.0.5)
47
+ method_source (~> 0.8)
48
+ slop (~> 3.4)
49
+ rack (1.5.2)
50
+ rack-test (0.6.2)
51
+ rack (>= 1.0)
52
+ rake (10.0.4)
53
+ randexp (0.1.7)
54
+ rspec (2.14.1)
55
+ rspec-core (~> 2.14.0)
56
+ rspec-expectations (~> 2.14.0)
57
+ rspec-mocks (~> 2.14.0)
58
+ rspec-core (2.14.4)
59
+ rspec-expectations (2.14.0)
60
+ diff-lcs (>= 1.1.3, < 2.0)
61
+ rspec-mocks (2.14.1)
62
+ safe_yaml (0.9.4)
63
+ slop (3.4.5)
64
+ thin (1.5.1)
65
+ daemons (>= 1.0.9)
66
+ eventmachine (>= 0.12.6)
67
+ rack (>= 1.0.0)
68
+ thor (0.18.1)
69
+ webmock (1.9.3)
70
+ addressable (>= 2.2.7)
71
+ crack (>= 0.3.2)
72
+ xpath (2.0.0)
73
+ nokogiri (~> 1.3)
74
+
75
+ PLATFORMS
76
+ ruby
77
+
78
+ DEPENDENCIES
79
+ geminabox-client
80
+ pact!
81
+ pry
82
+ rake (~> 10.0.3)
83
+ webmock (~> 1.9.3)
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 James Fraser, Sergei Matheson, Brent Snook
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # Pact
2
+
3
+ Define a pact between service consumers and providers.
4
+
5
+
6
+ Pact provides an RSpec DSL for service consumers to define the request they will make to a service producer and the
7
+ response they expect back. This expectation is used in the consumers specs to provide a mock producer, and is also
8
+ played back in the producer specs to ensure the producer actually does provide the response the consumer expects.
9
+
10
+ This allows you to test both sides of an integration point using fast unit tests.
11
+
12
+ ## Installation
13
+
14
+ Put it in your Gemfile. You know how.
15
+
16
+ ## Usage
17
+
18
+ ### Consumer project
19
+
20
+ #### Configuration
21
+
22
+ Pact.configure do | config |
23
+ config.pact_dir = "???" # Optional, default is ./spec/pacts
24
+ config.log_dir = "???" # Optional, default is ./log
25
+ config.logger = "??"
26
+ config.logger.level = Logger::DEBUG #By default this is INFO, bump this up to debug for more detailed logs
27
+ end
28
+
29
+ #### Create a Consumer (Driven) Contract
30
+
31
+ ```ruby
32
+ require 'pact/consumer/rspec'
33
+
34
+ class SomeServiceClient
35
+ include HTTParty
36
+ # Load your base_uri from a stub-able source
37
+ base_uri App.configuration.some_service_base_uri
38
+
39
+ def get_something
40
+ JSON.parse(self.class.get("/something").body)
41
+ end
42
+ end
43
+
44
+ Pact.configure do | config |
45
+ config.consumer do
46
+ name 'My Consumer'
47
+ end
48
+ end
49
+
50
+ # The following block creates a service on localhost:1234 which will respond to your application's queries
51
+ # over HTTP as if it were the real "My Producer" app. It also creats a mock producer object
52
+ # which you will use to set up your expectations. The method name to access the mock producer
53
+ # will be what ever name you give as the service argument - in this case "my_producer"
54
+
55
+ Pact.with_producer "My Producer" do
56
+ mock_service :my_producer do
57
+ port 1234
58
+ end
59
+ end
60
+
61
+ # Use the :pact => true describe metadata to include all the pact generation functionality in your spec.
62
+
63
+ describe "a pact with My Producer", :pact => true do
64
+
65
+ before do
66
+ # Configure your client to point to the stub service on localhost using the port you have specified
67
+ Application.configuration.stub(:some_service_base_uri).and_return('localhost:1234')
68
+ end
69
+
70
+ it "returns something when requested" do
71
+ my_producer.
72
+ given("something exists").
73
+ upon_receiving("a request for something").
74
+ with({ method: :get, path: '/something' }).
75
+ will_respond_with({
76
+ status: 200,
77
+ headers: { 'Content-Type' => 'application/json' },
78
+ body: {something: 'A thing!', something_else: 'Woot!'}
79
+ })
80
+ # Use your service's client to make the request, rather than hand crafting a HTTP request,
81
+ # so that you can be sure that the request that you expect to
82
+ # be constructed is actually constructed by your client.
83
+ # Do a quick sanity test to ensure client passes back the response properly.
84
+ expect(SomeServiceClient.get_something).to eql({something: 'A thing!'})
85
+ end
86
+ end
87
+
88
+ ```
89
+
90
+ Running the consumer spec will generate a pact file in the configured pact dir (spec/pacts by default).
91
+ Logs will be output to the configured log dir that can be useful when diagnosing problems.
92
+
93
+ To run your consumer app as a process during your test (eg for a Capybara test):
94
+
95
+ ```ruby
96
+ Pact.configure do | config |
97
+ config.consumer do
98
+ name 'My Consumer'
99
+ app my_consumer_rack_app
100
+ port 4321
101
+ end
102
+ ```
103
+
104
+ ### Producer project
105
+
106
+ #### Configure your producer rack app
107
+
108
+ ```ruby
109
+
110
+ Pact.configure do | config |
111
+ config.producer do
112
+ name "My Producer"
113
+ app { MyApp.new }
114
+ end
115
+ end
116
+
117
+ ```
118
+
119
+ #### Set up the producer states
120
+
121
+ Having different producer states allows you to test the same request with different expected responses.
122
+
123
+ For example, some code that creates the pact in a consumer project might look like this:
124
+
125
+ ```ruby
126
+ my_service.
127
+ given("a thing exists").
128
+ upon_receiving("a request for a thing").
129
+ with({method: 'get', path: '/thing'}).
130
+ will_respond_with({status: 200, :body => {thing: "yay!"} })
131
+
132
+ my_service.
133
+ given("a thing does not exist").
134
+ upon_receiving("a request for a thing").
135
+ with({method: 'get', path: '/thing'}).
136
+ will_respond_with({status: 404, :body => {error: "There is no thing :("} })
137
+ ```
138
+
139
+ To define producer states that create the right data for "a thing exists" and "a thing does not exist", write the following in the producer project.
140
+ Note that these states have been defined only for the 'My Consumer' consumer by using the Pact.with_consumer block.
141
+
142
+
143
+ ```ruby
144
+ # The consumer name here must match the name of the consumer configured in your consumer project
145
+ # for it to use these states.
146
+
147
+ Pact.with_consumer 'My Consumer' do
148
+ producer_state "a thing exists" do
149
+ set_up do
150
+ # Create a thing here using your factory of choice
151
+ end
152
+
153
+ tear_down do
154
+ # Any tear down steps to clean up your code (or use RSpec.after(:each))
155
+ end
156
+ end
157
+
158
+ producer_state "a thing does not exist" do
159
+ set_up do
160
+ # Well, probably not much to do here, but you get the picture.
161
+ end
162
+ end
163
+ end
164
+
165
+ ```
166
+
167
+ If a state should be used for all consumers, the top level Pact.with_consumer can be skipped, and a global Pact.producer_state can be defined on its own.
168
+
169
+ #### Create a rake task to verify that the producer honours the pact
170
+
171
+ You'll need to create one or more pact:verify:xxx tasks, that allow the currently checked out producer to be tested against other versions of its consumers - most importantly, head and production.
172
+
173
+ Here is an example pact:verify:head task, pointing the the pact file for "some_consumer", found in the build artifacts of the latest successful build of "MY-CONSUMER" project.
174
+
175
+ ```ruby
176
+ Pact::VerificationTask.new(:head) do | pact |
177
+ pact.uri 'http://our_build_server/MY-CONSUMER-BUILD/latestSuccessful/artifact/Pacts/some_consumer-this_producer.json',
178
+ support_file: './spec/consumers/pact_helper'
179
+ end
180
+ ```
181
+
182
+ ```ruby
183
+ # Ideally we'd like to be able to create a production task like this, but firewalls are making this tricky right now.
184
+ Pact::VerificationTask.new(:production) do | pact |
185
+ pact.uri 'http://our_prod_server/pacts/some_consumer-this_producer.json',
186
+ support_file: './spec/consumers/pact_helper', consumer: 'some_consumer'
187
+ end
188
+ ```
189
+
190
+ The pact.uri may be a local file system path or a remote URL.
191
+
192
+ The consumer is optional, and specifies which consumer namespace to use when looking up the producer states, if consumer namespaces have been used.
193
+
194
+ The support_file should include the code that makes your rack app available for the rack testing framework, and should load all its dependencies (eg include spec_helper)
195
+
196
+ Multiple pact.uri may be defined in the same rake task if a producer has more than one consumer.
197
+
198
+ #### Verify that the producer honours the pact
199
+
200
+ rake pact:verify:head
201
+ rake pact:verify # will run all verify tasks
202
+
203
+
204
+ ### Running a standalone mock server
205
+ A pact service can be run locally and is really useful for debugging purposes.
206
+
207
+ $ bundle exec pact service -p <port-num>
208
+
209
+ The service prints messages it recieves to stdout which can be really useful
210
+ when diagnosing issues with pacts.
211
+
212
+ ## TODO
213
+
214
+ Short term:
215
+ - Rename ConsumerContract to ConsumerContract (Done)
216
+ - Simplify set up for consumer (Done)
217
+ - Move server spawning into to the "at" method (Done)
218
+ - automatically register before and after hooks in consumer (Done)
219
+ - Provide before and after hooks and a place to define the app for Pact configuration in producer (remove Rspc from interface of Pact setup) (Done)
220
+ - Set_up for state
221
+ - Tear_down for state
222
+ - Before hook for all
223
+ - After hook for all
224
+ - Make producer state lookup try consumer defined state first, then fall back to global one (Done)
225
+ - Put producer and consumer name into pact file (Done)
226
+ - Remove consumer name from the rake task, as it should now be able to be determined from the pact file. (Done)
227
+
228
+ Long term:
229
+ - Decouple Rspec from Pact and make rspec-pact gem for easy integration
230
+
231
+
232
+ ## Contributing
233
+
234
+ 1. Fork it
235
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
236
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
237
+ 4. Push to the branch (`git push origin my-new-feature`)
238
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'bundler/gem_helper'
2
+ module Bundler
3
+ class GemHelper
4
+ def install
5
+ desc "Build #{name}-#{version}.gem into the pkg directory"
6
+ task 'build' do
7
+ build_gem
8
+ end
9
+
10
+ desc "Build and install #{name}-#{version}.gem into system gems"
11
+ task 'install' do
12
+ install_gem
13
+ end
14
+
15
+ GemHelper.instance = self
16
+ end
17
+ end
18
+ end
19
+ Bundler::GemHelper.install_tasks
20
+ require 'rspec/core/rake_task'
21
+ require 'geminabox-client'
22
+
23
+ Dir.glob('lib/tasks/**/*.rake').each { |task| load task }
24
+ Dir.glob('tasks/**/*.rake').each { |task| load task }
25
+ RSpec::Core::RakeTask.new(:spec)
26
+
27
+ task :default => [:spec, 'pact:tests']
28
+
29
+ desc "Release to REA gems host"
30
+ task :publish => :build do
31
+ gem_file = "pkg/pact-#{Pact::VERSION}.gem"
32
+ Geminabox::Client.new('http://rea-rubygems').upload(gem_file)
33
+ end
data/bin/pact ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pact/app'
4
+ Pact::App.start
data/lib/pact/app.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'find_a_port'
2
+ require 'thor'
3
+ require 'thin'
4
+ require 'thwait'
5
+ require 'pact/consumer'
6
+
7
+ module Pact
8
+ class App < Thor
9
+
10
+ desc 'service', "starts a mock service"
11
+ method_option :port, aliases: "-p", desc: "Port on which to run the service"
12
+ method_option :log, aliases: "-l", desc: "File to which to log output"
13
+ method_option :quiet, aliases: "-q", desc: "If true, no admin messages will be shown"
14
+
15
+ def service
16
+ service_options = {}
17
+ if options[:log]
18
+ log = File.open(options[:log], 'w')
19
+ log.sync = true
20
+ service_options[:log_file] = log
21
+ end
22
+ port = options[:port] || FindAPort.available_port
23
+ mock_service = Consumer::MockService.new(service_options)
24
+ Thin::Server.start("0.0.0.0", port, mock_service)
25
+ end
26
+
27
+ private
28
+ def log message
29
+ puts message unless options[:quiet]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ require 'ostruct'
2
+ require 'logger'
3
+
4
+ module Pact
5
+
6
+ class Configuration
7
+ attr_accessor :pact_dir
8
+ attr_accessor :log_dir
9
+ attr_accessor :logger
10
+ attr_accessor :tmp_dir
11
+ attr_accessor :pactfile_write_mode
12
+
13
+ def log_path
14
+ log_dir + "/pact_gem.log"
15
+ end
16
+ end
17
+
18
+ def self.configuration
19
+ @configuration ||= default_configuration
20
+ end
21
+
22
+ def self.configure
23
+ yield configuration
24
+ FileUtils::mkdir_p configuration.tmp_dir
25
+ end
26
+
27
+ def self.clear_configuration
28
+ @configuration = default_configuration
29
+ end
30
+
31
+ private
32
+
33
+ def self.default_configuration
34
+ c = Configuration.new
35
+ c.pact_dir = File.expand_path('./spec/pacts')
36
+ c.tmp_dir = File.expand_path('./tmp/pacts')
37
+ c.log_dir = default_log_dir
38
+ c.logger = default_logger c.log_path
39
+ c.pactfile_write_mode = defined?(Rake) ? :overwrite : :update
40
+ c
41
+ end
42
+
43
+ def self.default_log_dir
44
+ File.expand_path("./log")
45
+ end
46
+
47
+ def self.default_logger path
48
+ FileUtils::mkdir_p File.dirname(path)
49
+ logger = Logger.new(path)
50
+ logger.level = Logger::INFO
51
+ logger
52
+ end
53
+
54
+ end
@@ -0,0 +1,177 @@
1
+ require 'thwait'
2
+ require 'capybara'
3
+
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'find_a_port'
7
+ require 'pact/logging'
8
+
9
+ module Pact
10
+ module Consumer
11
+ class AppManager
12
+
13
+ include Pact::Logging
14
+
15
+ include Singleton
16
+
17
+ def initialize
18
+ @apps_spawned = false
19
+ @app_registrations = []
20
+ end
21
+
22
+ def register_mock_service_for name, url
23
+ uri = URI(url)
24
+ raise "Currently only http is supported" unless uri.scheme == 'http'
25
+ raise "Currently only services on localhost are supported" unless uri.host == 'localhost'
26
+
27
+ register(MockService.new(log_file: create_log_file(name), name: name), uri.port)
28
+ end
29
+
30
+ def register(app, port = FindAPort.available_port)
31
+ existing = existing_app_on_port port
32
+ raise "Port #{port} is already being used by #{existing}" if existing and not existing == app
33
+ app_registration = register_app app, port
34
+ app_registration.spawn if @apps_spawned
35
+ port
36
+ end
37
+
38
+ def existing_app_on_port port
39
+ app_registration = @app_registrations.find { |app_registration| app_registration.port == port }
40
+ app_registration ? app_registration.app : nil
41
+ end
42
+
43
+ def app_registered_on?(port)
44
+ app_registrations.any? { |app_registration| app_registration.port == port }
45
+ end
46
+
47
+ def ports_of_mock_services
48
+ app_registrations.find_all(&:is_a_mock_service?).collect(&:port)
49
+ end
50
+
51
+ def kill_all
52
+ app_registrations.find_all(&:spawned?).collect(&:kill)
53
+ @apps_spawned = false
54
+ end
55
+
56
+ def clear_all
57
+ kill_all
58
+ @app_registrations = []
59
+ end
60
+
61
+ def spawn_all
62
+ app_registrations.find_all(&:not_spawned?).collect(&:spawn)
63
+ @apps_spawned = true
64
+ end
65
+
66
+ private
67
+
68
+ def create_log_file service_name
69
+ FileUtils::mkdir_p Pact.configuration.log_dir
70
+ log = File.open(log_file_path(service_name), 'w')
71
+ log.sync = true
72
+ log
73
+ end
74
+
75
+ def log_file_path service_name
76
+ File.join(Pact.configuration.log_dir, "#{log_file_name(service_name)}.log")
77
+ end
78
+
79
+ def log_file_name service_name
80
+ lower_case_name = service_name.downcase.gsub(/\s+/, '_')
81
+ if lower_case_name.include?('_service')
82
+ lower_case_name.gsub('_service', '_mock_service')
83
+ else
84
+ lower_case_name + '_mock_service'
85
+ end
86
+ end
87
+
88
+ def app_registrations
89
+ @app_registrations
90
+ end
91
+
92
+ def register_app app, port
93
+ app_registration = AppRegistration.new :app => app, :port => port
94
+ app_registrations << app_registration
95
+ app_registration
96
+ end
97
+ end
98
+
99
+ class AppRegistration
100
+ include Pact::Logging
101
+ attr_accessor :port
102
+ attr_accessor :app
103
+ attr_accessor :pid
104
+
105
+ def initialize opts
106
+ @max_wait = 10
107
+ @port = opts[:port]
108
+ @pid = opts[:pid]
109
+ @app = opts[:app]
110
+ end
111
+
112
+ def kill
113
+ logger.info "Killing #{self}"
114
+ Process.kill(9, pid)
115
+ Process.wait(pid)
116
+ self.pid = nil
117
+ end
118
+
119
+ def not_spawned?
120
+ !spawned?
121
+ end
122
+
123
+ def spawned?
124
+ self.pid != nil
125
+ end
126
+
127
+ def is_a_mock_service?
128
+ app.is_a? MockService
129
+ end
130
+
131
+ def to_s
132
+ "#{app} on port #{port}" + (@pid ? " with pid #{pid}" : "")
133
+ end
134
+
135
+ def spawn
136
+ # following stolen from https://github.com/jwilger/kookaburra
137
+ logger.info "Starting app #{self}..."
138
+ self.pid = fork do
139
+ begin
140
+ Capybara.server_port = port
141
+ Capybara::Server.new(app).boot
142
+
143
+ # This ensures that this forked process keeps running, because the
144
+ # actual server is started in a thread by Capybara.
145
+ ThreadsWait.all_waits(Thread.list)
146
+ rescue Exception => e
147
+ logger.error "Error starting up #{self}"
148
+ $stderr.puts "Error starting up #{self}"
149
+ raise e
150
+ end
151
+ end
152
+
153
+
154
+ wait_until do
155
+ begin
156
+ Net::HTTP.get_response(URI.parse("http://localhost:#{port}/index.html"))
157
+ rescue Errno::ECONNREFUSED
158
+ false
159
+ end
160
+ end
161
+ logger.info "Started with pid #{pid}"
162
+ end
163
+
164
+ def wait_until
165
+ waited = 0
166
+ wait_time = 0.1
167
+ while waited < @max_wait do
168
+ break if yield
169
+ sleep wait_time
170
+ waited += wait_time
171
+ raise "Waited longer than #{@max_wait} seconds" if waited >= @max_wait
172
+ end
173
+ end
174
+
175
+ end
176
+ end
177
+ end