testbeat 0.5.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.
- checksums.yaml +7 -0
- data/lib/spec_helper.rb +479 -0
- data/lib/testbeat.rb +14 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a07df1c8d2decd193451ed6563c37ecf5d77e4e6
|
4
|
+
data.tar.gz: efc84e08c072861996aca8d767fe7195da3e87d0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 90f7258973368c942e722f6b1be18804ca132da3e05d5da189ac339f49705b7cb191ceb2268cdb65567a131578e8f7204085ab73bc91727754724384abea11e7
|
7
|
+
data.tar.gz: fc8a054528bedcbc506d183138d5414fdcd1d30dd69ef73dd900b6000f305e5fde425a04838d8b36dae9b1c68d169026638f601bdbc13905648637410959befb
|
data/lib/spec_helper.rb
ADDED
@@ -0,0 +1,479 @@
|
|
1
|
+
|
2
|
+
require "awesome_print"
|
3
|
+
require "net/http"
|
4
|
+
require "openssl"
|
5
|
+
require "json"
|
6
|
+
require "hashie"
|
7
|
+
require "logger"
|
8
|
+
|
9
|
+
# TODO can we support http://www.relishapp.com/rspec/rspec-core/v/3-1/docs/example-groups/shared-context
|
10
|
+
|
11
|
+
# Node should have getters for repository and hosting specifics
|
12
|
+
# Configuration attributes for the node should are available as [:properties]
|
13
|
+
|
14
|
+
# global so we don't need to pass this around to utility classes
|
15
|
+
$logger = Logger.new('testbeat-debug.log')
|
16
|
+
|
17
|
+
class TestbeatNode
|
18
|
+
|
19
|
+
def initialize(nodename)
|
20
|
+
@name = nodename
|
21
|
+
if !@name || @name.length == 0
|
22
|
+
logger.error { "Node name not set, at #{Dir.pwd}" }
|
23
|
+
raise "Node name not set"
|
24
|
+
end
|
25
|
+
attributesFile = "#{folder}/chef.json"
|
26
|
+
if File.exists?(attributesFile)
|
27
|
+
@attributes = read_chef_attributes(attributesFile)
|
28
|
+
else
|
29
|
+
logger.warn { "Failed to locate node attributes, from #{attributesFile}, at #{Dir.pwd}" }
|
30
|
+
@attributes = nil
|
31
|
+
end
|
32
|
+
logger.info { "Node initialized #{nodename}, attributes from #{attributesFile}" }
|
33
|
+
end
|
34
|
+
|
35
|
+
def logger
|
36
|
+
$logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def folder
|
40
|
+
"nodes/#{@name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def vagrant?
|
44
|
+
vagrantfile = "#{folder}/Vagrantfile"
|
45
|
+
return File.exists?(vagrantfile)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Attributes defined specifically on the node, not aggregated like in chef runs
|
49
|
+
def attributes
|
50
|
+
@attributes
|
51
|
+
end
|
52
|
+
|
53
|
+
def attributes?
|
54
|
+
@attributes != nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# Provides access to attributes, if available, as @node[key] much like in chef cookbooks
|
58
|
+
# Use .keys to see if a key exists
|
59
|
+
def [](key)
|
60
|
+
return nil if @attributes == nil
|
61
|
+
# Raise exception so test issues are clearly displayed in rspec output (puts is displayed before test output, difficult to identify which test it comes from)
|
62
|
+
raise "Missing attribute key '#{key}', got #{@attributes.keys}" if not @attributes.key?(key)
|
63
|
+
@attributes[key]
|
64
|
+
end
|
65
|
+
|
66
|
+
# More methods to work like hash
|
67
|
+
def keys
|
68
|
+
@attributes.keys
|
69
|
+
end
|
70
|
+
|
71
|
+
def key?(key)
|
72
|
+
@attributes.key?(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
# host is assumed to be equivalent with node name now, but we could read it from attributes, see ticket:1017
|
76
|
+
def host
|
77
|
+
@name
|
78
|
+
end
|
79
|
+
|
80
|
+
# returns hash "username" and "password", or false if unsupported
|
81
|
+
def testauth
|
82
|
+
return false # we don't support authenticated nodes yet
|
83
|
+
end
|
84
|
+
|
85
|
+
# return command line access, instance of TestbeatNodeRsh, or false if unsupported
|
86
|
+
# This is probably easier to support than get_bats; on vagrant nodes we have 'vagrant ssh -c'
|
87
|
+
def shell
|
88
|
+
if not vagrant?
|
89
|
+
return TestbeatShellStub.new()
|
90
|
+
end
|
91
|
+
return TestbeatShellVagrant.new(folder)
|
92
|
+
end
|
93
|
+
|
94
|
+
def provision
|
95
|
+
if not vagrant?
|
96
|
+
raise "Provision support will probably require a vagrant box"
|
97
|
+
else
|
98
|
+
raise "Provision not implemented"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
def to_s
|
104
|
+
"Testbeat node #{@name}"
|
105
|
+
#ap @attributes
|
106
|
+
end
|
107
|
+
|
108
|
+
# The following methods are private
|
109
|
+
private
|
110
|
+
|
111
|
+
# Returns node attributes from node file compatible with "knife node from file"
|
112
|
+
# Returns as Mash because that's what chef uses
|
113
|
+
def read_chef_attributes(jsonPath)
|
114
|
+
#p "read attributes from #{jsonPath}"
|
115
|
+
data = nil
|
116
|
+
File::open(jsonPath) { |f|
|
117
|
+
data = f.read
|
118
|
+
}
|
119
|
+
raise "Failed to read file #{jsonPath}" if data == nil
|
120
|
+
json = JSON.parse(data)
|
121
|
+
mash = Hashie::Mash.new(json)
|
122
|
+
raise "Missing 'normal' attributes in node file #{jsonPath}" if not mash.key?("normal")
|
123
|
+
return mash["normal"]
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
# Support different types of command execution on node
|
129
|
+
class TestbeatShell
|
130
|
+
|
131
|
+
def exec(cmd)
|
132
|
+
raise "Command execution on this node is not supported"
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
class TestbeatShellVagrant
|
138
|
+
|
139
|
+
def initialize(vagrantFolder)
|
140
|
+
@vagrantFolder = vagrantFolder
|
141
|
+
end
|
142
|
+
|
143
|
+
def exec(cmd)
|
144
|
+
$logger.debug {"Exec: #{cmd}"}
|
145
|
+
stdout = nil
|
146
|
+
Dir.chdir(@vagrantFolder){
|
147
|
+
@out = %x[vagrant ssh -c '#{cmd}' 2>&1]
|
148
|
+
@status = $?
|
149
|
+
}
|
150
|
+
$logger.debug {"Exec result #{exitstatus}: #{@out[0,50].strip}"}
|
151
|
+
return self
|
152
|
+
end
|
153
|
+
|
154
|
+
# output of latest exec (stdout and stderr merged, as we haven't worked on separating them)
|
155
|
+
def out
|
156
|
+
@out
|
157
|
+
end
|
158
|
+
|
159
|
+
def exitstatus
|
160
|
+
@status.exitstatus
|
161
|
+
end
|
162
|
+
|
163
|
+
def ok?
|
164
|
+
exitstatus == 0
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
class TestbeatShellStub
|
170
|
+
|
171
|
+
def initialize()
|
172
|
+
@hasrun = Array.new
|
173
|
+
end
|
174
|
+
|
175
|
+
def hasrun
|
176
|
+
@hasrun
|
177
|
+
end
|
178
|
+
|
179
|
+
def exec(cmd)
|
180
|
+
@hasrun.push(cmd)
|
181
|
+
$logger.warn "No guest shell available to exec: '#{cmd}'"
|
182
|
+
end
|
183
|
+
|
184
|
+
def out
|
185
|
+
"(No guest shell for current testbeat context)"
|
186
|
+
end
|
187
|
+
|
188
|
+
def exitstatus
|
189
|
+
@hasrun.length
|
190
|
+
end
|
191
|
+
|
192
|
+
def ok?
|
193
|
+
false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Context has getters for test case parameters that can be digged out from Rspec examples,
|
198
|
+
# initialized for each "it"
|
199
|
+
class TestbeatContext
|
200
|
+
|
201
|
+
# reads context from RSpec example metadata
|
202
|
+
def initialize(example)
|
203
|
+
# enables de-duplication of requests within describe, using tostring to compare becase == wouldn't work
|
204
|
+
@context_block_id = "#{example.metadata[:example_group][:block]}"
|
205
|
+
|
206
|
+
# defaults
|
207
|
+
@user = { :username => 'testuser', :password => 'testpassword' }
|
208
|
+
@unencrypted = false
|
209
|
+
@unauthenticated = false
|
210
|
+
@rest = false
|
211
|
+
@reprovision = false
|
212
|
+
|
213
|
+
# actual context
|
214
|
+
parse_example_group(example.metadata[:example_group], 0)
|
215
|
+
|
216
|
+
@context_block_id_short = /([^\/]+)>$/.match(@context_block_id)[1]
|
217
|
+
logger.info{ "#{example.metadata[:example_group][:location]}: #{@rest ? @method : ''} #{@resource} #{@unencrypted ? '[unencrypted] ' : ' '}#{@unauthenticated ? '[unauthenticated] ' : ' '}" }
|
218
|
+
end
|
219
|
+
|
220
|
+
def logger
|
221
|
+
$logger
|
222
|
+
end
|
223
|
+
|
224
|
+
def context_block_id
|
225
|
+
@context_block_id
|
226
|
+
end
|
227
|
+
|
228
|
+
# Returns true if the current context has a REST request specified
|
229
|
+
def rest?
|
230
|
+
@rest
|
231
|
+
end
|
232
|
+
|
233
|
+
def nop?
|
234
|
+
!rest?
|
235
|
+
end
|
236
|
+
|
237
|
+
def user
|
238
|
+
@user
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns the REST resource if specified in context
|
242
|
+
def resource
|
243
|
+
if not rest?
|
244
|
+
return nil
|
245
|
+
end
|
246
|
+
@resource
|
247
|
+
end
|
248
|
+
|
249
|
+
def method
|
250
|
+
if not rest?
|
251
|
+
return nil
|
252
|
+
end
|
253
|
+
@method
|
254
|
+
end
|
255
|
+
|
256
|
+
def headers
|
257
|
+
@rest_headers
|
258
|
+
end
|
259
|
+
|
260
|
+
def headers?
|
261
|
+
!!headers
|
262
|
+
end
|
263
|
+
|
264
|
+
def body
|
265
|
+
@rest_body
|
266
|
+
end
|
267
|
+
|
268
|
+
def body?
|
269
|
+
!!body
|
270
|
+
end
|
271
|
+
|
272
|
+
def form
|
273
|
+
@rest_form
|
274
|
+
end
|
275
|
+
|
276
|
+
def form?
|
277
|
+
!!form
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns true if requests will be made without authentication even if the node expects authentication
|
281
|
+
def unauthenticated?
|
282
|
+
unencrypted? || @unauthenticated
|
283
|
+
end
|
284
|
+
|
285
|
+
def unencrypted?
|
286
|
+
@unencrypted
|
287
|
+
end
|
288
|
+
|
289
|
+
def reprovision?
|
290
|
+
@reprovision
|
291
|
+
end
|
292
|
+
|
293
|
+
def to_s
|
294
|
+
s = "Testbeat context"
|
295
|
+
if rest?
|
296
|
+
s += " #{method} #{resource}"
|
297
|
+
else
|
298
|
+
s += " non-REST"
|
299
|
+
end
|
300
|
+
if @unencrypted
|
301
|
+
s += " unencrypted"
|
302
|
+
elsif @unauthenticated
|
303
|
+
s += " unauthenticated"
|
304
|
+
end
|
305
|
+
s
|
306
|
+
end
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
def parse_example_group(example_group, uplevel)
|
311
|
+
if example_group[:parent_example_group]
|
312
|
+
parse_example_group(example_group[:parent_example_group], uplevel + 1)
|
313
|
+
end
|
314
|
+
logger.debug{ "Parsing context #{uplevel > 0 ? '-' : ' '}#{uplevel}: #{example_group[:description]}" }
|
315
|
+
parse_description_args(example_group[:description_args])
|
316
|
+
if rest?
|
317
|
+
if example_group[:body]
|
318
|
+
@rest_body = example_group[:body]
|
319
|
+
end
|
320
|
+
if example_group[:form]
|
321
|
+
@rest_form = example_group[:form]
|
322
|
+
end
|
323
|
+
if example_group[:headers]
|
324
|
+
@rest_headers = example_group[:headers]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
def parse_description_args(example_group_description_args)
|
331
|
+
a = example_group_description_args[0]
|
332
|
+
/unencrypted/i.match(a) {
|
333
|
+
@unencrypted = true
|
334
|
+
}
|
335
|
+
|
336
|
+
/unauthenticated/i.match(a) {
|
337
|
+
@unauthenticated = true
|
338
|
+
}
|
339
|
+
# We could iterate methods in Net::HTTP and support all
|
340
|
+
# Change method to rest_method for consistency
|
341
|
+
/(GET|HEAD|POST)\s+(\S*)(.*)/.match(a) { |rest|
|
342
|
+
@rest = true
|
343
|
+
@method = rest[1]
|
344
|
+
@resource = rest[2]
|
345
|
+
}
|
346
|
+
/reprovision/i.match(a) {
|
347
|
+
@reprovision = true
|
348
|
+
}
|
349
|
+
# idea: nodes that should not be modified (production etc), particularily not through shell
|
350
|
+
#/untouchable/
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
$_testbeat_rest_reuse = Hash.new
|
356
|
+
|
357
|
+
class TestbeatRestRequest
|
358
|
+
|
359
|
+
def initialize(node, testbeat)
|
360
|
+
#@headers = {}
|
361
|
+
@timeout = 10
|
362
|
+
@node = node
|
363
|
+
@testbeat = testbeat
|
364
|
+
end
|
365
|
+
|
366
|
+
# Initiate the request and return Net::HTTPResponse object,
|
367
|
+
# supporting response [:responseHeaderName], .body (string), .code (int), .msg (string)
|
368
|
+
def run
|
369
|
+
reuse_id = @testbeat.context_block_id
|
370
|
+
previous = $_testbeat_rest_reuse[reuse_id]
|
371
|
+
if previous
|
372
|
+
@response = previous
|
373
|
+
@testbeat.logger.info{ "Request reused within #{reuse_id} responded #{@response.code} #{@response.message}" }
|
374
|
+
return @response
|
375
|
+
end
|
376
|
+
# If there's no built in auth support in Net::HTTP we can check for 401 here and re-run the request with auth header
|
377
|
+
Net::HTTP.start(@node.host,
|
378
|
+
:use_ssl => !@testbeat.unencrypted?,
|
379
|
+
:verify_mode => OpenSSL::SSL::VERIFY_NONE, # Ideally verify should be enabled for non-labs hosts (anything with a FQDN including dots)
|
380
|
+
:open_timeout => @timeout,
|
381
|
+
:read_timeout => @timeout
|
382
|
+
) do |http|
|
383
|
+
|
384
|
+
req = Net::HTTP::Get.new(@testbeat.resource)
|
385
|
+
if @testbeat.method == 'POST'
|
386
|
+
req = Net::HTTP::Post.new(@testbeat.resource)
|
387
|
+
if @testbeat.form?
|
388
|
+
req.set_form_data(@testbeat.form)
|
389
|
+
end
|
390
|
+
if @testbeat.body?
|
391
|
+
req.body = @testbeat.body
|
392
|
+
end
|
393
|
+
end
|
394
|
+
if @testbeat.headers?
|
395
|
+
@testbeat.headers.each {|name, value| req[name] = value }
|
396
|
+
end
|
397
|
+
|
398
|
+
@response = http.request(req) # Net::HTTPResponse object
|
399
|
+
|
400
|
+
if @response.code == "401" and @testbeat.user and not @testbeat.unauthenticated?
|
401
|
+
u = @testbeat.user
|
402
|
+
@testbeat.logger.info{ "Authenticating to #{@testbeat.resource} with #{u[:username]}:#{u[:password]}" }
|
403
|
+
req.basic_auth u[:username], u[:password]
|
404
|
+
@response = http.request(req)
|
405
|
+
end
|
406
|
+
|
407
|
+
@testbeat.logger.info{ "Request #{@testbeat.resource} responded #{@response.code} #{@response.message}" }
|
408
|
+
$_testbeat_rest_reuse[reuse_id] = @response
|
409
|
+
return @response
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
end
|
414
|
+
|
415
|
+
RSpec.configure do |config|
|
416
|
+
|
417
|
+
activenodes = {}
|
418
|
+
|
419
|
+
# We can't use before(:suite) because it does not support instance variables
|
420
|
+
config.before(:context) do |context|
|
421
|
+
|
422
|
+
nodearg = ENV['NODE']
|
423
|
+
next if nodearg.nil?
|
424
|
+
|
425
|
+
if not activenodes.has_key?(nodearg)
|
426
|
+
activenodes[nodearg] = TestbeatNode.new(nodearg)
|
427
|
+
end
|
428
|
+
@node = activenodes[nodearg]
|
429
|
+
|
430
|
+
end
|
431
|
+
|
432
|
+
# https://www.relishapp.com/rspec/rspec-core/docs/hooks/before-and-after-hooks
|
433
|
+
config.before(:example) do |example|
|
434
|
+
#puts "------------- before"
|
435
|
+
#ap example.metadata
|
436
|
+
#ap example.metadata[:description_args]
|
437
|
+
#ap example[:description_args]
|
438
|
+
#ap example.full_description
|
439
|
+
|
440
|
+
# Testbeat can do nothing without a node, so the example will continue as a regular Rspec test
|
441
|
+
next if not @node
|
442
|
+
|
443
|
+
@testbeat = TestbeatContext.new(example)
|
444
|
+
|
445
|
+
# If there's no REST call in the example we're happy just to define @testbeat with access to command line etc
|
446
|
+
next if @testbeat.nop?
|
447
|
+
|
448
|
+
if @testbeat.unencrypted? and @testbeat.unauthenticated?
|
449
|
+
# Nodes that require non-test authentication are currently out of scope for the test framework
|
450
|
+
# This means we must skip specs in an "unauthenticated" context, or let them fail, because we won't get the 401 responses we'd expect
|
451
|
+
#p "--- Should be skipped; nodes that require authentication are currently unsupported"
|
452
|
+
elsif @testbeat.unencrypted?
|
453
|
+
# When we do add support for authentication to nodes, there should be no authentication on insecure channel
|
454
|
+
elsif @testbeat.reprovision?
|
455
|
+
# We have to run a new provisioning. For repos-backup amongst others.
|
456
|
+
@testbeat.logger{ "Reprovision triggered" }
|
457
|
+
@node.provision
|
458
|
+
else
|
459
|
+
# Authenticated is default, meaning that specs should be written with the assumption that all services are accessible
|
460
|
+
end
|
461
|
+
|
462
|
+
if @testbeat.rest?
|
463
|
+
@testbeat.logger{ "Request triggered" }
|
464
|
+
req = TestbeatRestRequest.new(@node, @testbeat)
|
465
|
+
begin
|
466
|
+
@response = req.run
|
467
|
+
rescue OpenSSL::SSL::SSLError => e
|
468
|
+
@response = { :error => e }
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
#p "Got reponse code #{@response.code}"
|
473
|
+
#ap @response.header.to_hash
|
474
|
+
end
|
475
|
+
|
476
|
+
config.after(:example) do
|
477
|
+
#p "------------- after"
|
478
|
+
end
|
479
|
+
end
|
data/lib/testbeat.rb
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: testbeat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Staffan Olsson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: hashie
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: awesome_print
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Rspec spec_helper and Vagrant integration for HTTP level testing, on
|
56
|
+
a box from the outside
|
57
|
+
email: solsson@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- lib/testbeat.rb
|
63
|
+
- lib/spec_helper.rb
|
64
|
+
homepage: https://github.com/Reposoft/testbeat
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
metadata: {}
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '2.0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 2.0.14
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: REST acceptance testing framework
|
88
|
+
test_files: []
|