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.
- data/.gitignore +28 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +22 -0
- data/README.md +238 -0
- data/Rakefile +33 -0
- data/bin/pact +4 -0
- data/lib/pact/app.rb +32 -0
- data/lib/pact/configuration.rb +54 -0
- data/lib/pact/consumer/app_manager.rb +177 -0
- data/lib/pact/consumer/configuration_dsl.rb +71 -0
- data/lib/pact/consumer/consumer_contract_builder.rb +79 -0
- data/lib/pact/consumer/consumer_contract_builders.rb +10 -0
- data/lib/pact/consumer/dsl.rb +98 -0
- data/lib/pact/consumer/interaction.rb +70 -0
- data/lib/pact/consumer/mock_service.rb +340 -0
- data/lib/pact/consumer/rspec.rb +43 -0
- data/lib/pact/consumer/run_condor.rb +4 -0
- data/lib/pact/consumer/run_mock_contract_service.rb +13 -0
- data/lib/pact/consumer/service_consumer.rb +22 -0
- data/lib/pact/consumer/service_producer.rb +23 -0
- data/lib/pact/consumer.rb +7 -0
- data/lib/pact/consumer_contract.rb +110 -0
- data/lib/pact/json_warning.rb +23 -0
- data/lib/pact/logging.rb +14 -0
- data/lib/pact/matchers/matchers.rb +85 -0
- data/lib/pact/matchers.rb +1 -0
- data/lib/pact/producer/configuration_dsl.rb +62 -0
- data/lib/pact/producer/matchers.rb +22 -0
- data/lib/pact/producer/pact_spec_runner.rb +57 -0
- data/lib/pact/producer/producer_state.rb +81 -0
- data/lib/pact/producer/rspec.rb +127 -0
- data/lib/pact/producer/test_methods.rb +89 -0
- data/lib/pact/producer.rb +1 -0
- data/lib/pact/rake_task.rb +64 -0
- data/lib/pact/reification.rb +26 -0
- data/lib/pact/request.rb +109 -0
- data/lib/pact/term.rb +40 -0
- data/lib/pact/verification_task.rb +57 -0
- data/lib/pact/version.rb +3 -0
- data/lib/pact.rb +5 -0
- data/lib/tasks/pact.rake +6 -0
- data/pact.gemspec +36 -0
- data/scratchpad.txt +36 -0
- data/spec/features/consumption_spec.rb +146 -0
- data/spec/features/producer_states/zebras.rb +28 -0
- data/spec/features/production_spec.rb +160 -0
- data/spec/integration/pact/configuration_spec.rb +65 -0
- data/spec/lib/pact/configuration_spec.rb +35 -0
- data/spec/lib/pact/consumer/app_manager_spec.rb +41 -0
- data/spec/lib/pact/consumer/consumer_contract_builder_spec.rb +87 -0
- data/spec/lib/pact/consumer/dsl_spec.rb +52 -0
- data/spec/lib/pact/consumer/interaction_spec.rb +108 -0
- data/spec/lib/pact/consumer/mock_service_spec.rb +147 -0
- data/spec/lib/pact/consumer/service_consumer_spec.rb +11 -0
- data/spec/lib/pact/consumer_contract_spec.rb +125 -0
- data/spec/lib/pact/matchers/matchers_spec.rb +354 -0
- data/spec/lib/pact/producer/configuration_dsl_spec.rb +101 -0
- data/spec/lib/pact/producer/producer_state_spec.rb +103 -0
- data/spec/lib/pact/producer/rspec_spec.rb +48 -0
- data/spec/lib/pact/reification_spec.rb +43 -0
- data/spec/lib/pact/request_spec.rb +316 -0
- data/spec/lib/pact/term_spec.rb +36 -0
- data/spec/lib/pact/verification_task_spec.rb +64 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/a_consumer-a_producer.json +34 -0
- data/spec/support/pact_rake_support.rb +41 -0
- data/spec/support/test_app_fail.json +22 -0
- data/spec/support/test_app_pass.json +21 -0
- data/tasks/pact-test.rake +19 -0
- 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
data/Gemfile
ADDED
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
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
|