restest 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/restest +3 -0
- data/lib/MySQLReport.rb +16 -0
- data/lib/Result.rb +93 -0
- data/lib/ServiceAPI.rb +178 -0
- data/lib/SimpleFileReport.rb +26 -0
- data/lib/State.rb +182 -0
- data/lib/restest.rb +384 -0
- data/test/helper.rb +18 -0
- data/test/test_restest.rb +7 -0
- metadata +131 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", ">= 1.0.0"
|
12
|
+
gem "jeweler", "~> 1.8.4"
|
13
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Peter Salas
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= restest
|
2
|
+
|
3
|
+
Description goes here.
|
4
|
+
|
5
|
+
== Contributing to restest
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
+
* Fork the project.
|
10
|
+
* Start a feature/bugfix branch.
|
11
|
+
* Commit and push until you are happy with your contribution.
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2012 Peter Salas. See LICENSE.txt for
|
18
|
+
further details.
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "restest"
|
18
|
+
gem.homepage = "http://github.com/gradeawarrior/restest"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = "RESTful API Testing Framework"
|
21
|
+
gem.description = "A Ruby framework for testing RESTful API's"
|
22
|
+
gem.email = "psalas@proofpoint.com"
|
23
|
+
gem.authors = ["Jyri Virki", "Peter Salas"]
|
24
|
+
gem.executables << 'restest'
|
25
|
+
# dependencies defined in Gemfile
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :test
|
37
|
+
|
38
|
+
require 'rdoc/task'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "restest #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/restest
ADDED
data/lib/MySQLReport.rb
ADDED
data/lib/Result.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# The Result class wraps a HTTP Response object and provides some test framework-specific additional result
|
4
|
+
# tracking.
|
5
|
+
#
|
6
|
+
# The ServiceAPI#do_request* methods returns this Result object for each test.
|
7
|
+
#
|
8
|
+
class Result
|
9
|
+
|
10
|
+
attr_reader :response
|
11
|
+
attr_reader :is_ok
|
12
|
+
attr_reader :message
|
13
|
+
attr_accessor :allow_retry # if false, do not retry test even if test file requests retries
|
14
|
+
attr_accessor :abort_suite_run # if true, framework will end test suite run immediately
|
15
|
+
|
16
|
+
#-------------------------------------------------------------------------------------------------------------------
|
17
|
+
# Constructor.
|
18
|
+
#
|
19
|
+
def initialize(response)
|
20
|
+
@response = response
|
21
|
+
@is_ok = true
|
22
|
+
@allow_retry = true
|
23
|
+
@message = ""
|
24
|
+
end
|
25
|
+
|
26
|
+
#-------------------------------------------------------------------------------------------------------------------
|
27
|
+
# Fatal error factory. A service module should return a fatal_error when it encounters a problem which cannot
|
28
|
+
# change by re-trying the same test again. The most common case is if a required parameter is missing.
|
29
|
+
#
|
30
|
+
def self.fatal_error(message)
|
31
|
+
res = Result.new(nil)
|
32
|
+
res.error(message)
|
33
|
+
res.allow_retry = false
|
34
|
+
return res
|
35
|
+
end
|
36
|
+
|
37
|
+
#-------------------------------------------------------------------------------------------------------------------
|
38
|
+
# Constructs a Result object which tells the framework to abort the current test suite.
|
39
|
+
#
|
40
|
+
# A service module should generally never return this. Avoid it! Even if a particular misconfiguration is fatal
|
41
|
+
# for a certain subset of tests, it is possible the suite may have many other tests which can still run.
|
42
|
+
#
|
43
|
+
# It is provided for cases where a service module detects a misconfiguration so fatal that there truly is no point
|
44
|
+
# in attempting to run any more tests because you know for sure they will all fail.
|
45
|
+
#
|
46
|
+
def self.abort_suite(message)
|
47
|
+
res = Result.new(nil)
|
48
|
+
res.error(message)
|
49
|
+
res.allow_retry = false
|
50
|
+
res.abort_suite_run = true
|
51
|
+
return res
|
52
|
+
end
|
53
|
+
|
54
|
+
#-------------------------------------------------------------------------------------------------------------------
|
55
|
+
# Flags this Result as erroneous.
|
56
|
+
# After one call to this method, is_ok will return false.
|
57
|
+
# A descriptive message string needs to be provided, explaining why the result is erroneous.
|
58
|
+
# This method can be invoked multiple times, if multiple errors are noticed on the response.
|
59
|
+
# The message(s) will be available via the message method.
|
60
|
+
#
|
61
|
+
def error(msg)
|
62
|
+
@is_ok = false
|
63
|
+
add_message(msg)
|
64
|
+
end
|
65
|
+
|
66
|
+
#-------------------------------------------------------------------------------------------------------------------
|
67
|
+
# Adds a message sentence to the error message line.
|
68
|
+
# Typically callers should invoke error() above.
|
69
|
+
#
|
70
|
+
def add_message(msg)
|
71
|
+
@message = @message + msg
|
72
|
+
if (@message[-1,1] != " ")
|
73
|
+
@message = @message + " "
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#-------------------------------------------------------------------------------------------------------------------
|
78
|
+
# Returns the HTTP response code sent by server (as a String)
|
79
|
+
#
|
80
|
+
def code
|
81
|
+
return (-1) if @response == nil
|
82
|
+
return @response.code
|
83
|
+
end
|
84
|
+
|
85
|
+
#-------------------------------------------------------------------------------------------------------------------
|
86
|
+
# Returns the HTTP response body sent by server.
|
87
|
+
#
|
88
|
+
def body
|
89
|
+
return "" if @response == nil
|
90
|
+
return @response.body
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
data/lib/ServiceAPI.rb
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# This is the base class for all service module classes.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'json'
|
8
|
+
require 'Result'
|
9
|
+
|
10
|
+
class ServiceAPI
|
11
|
+
|
12
|
+
#-------------------------------------------------------------------------------------------------------------------
|
13
|
+
# Returns a string which can be eval'd to set a variable or return an error.
|
14
|
+
#
|
15
|
+
# A very common pattern in service module methods is to attempt to get a value from state or return
|
16
|
+
# a fatal error Result if it is missing. This convenience method allows reducing the code segment:
|
17
|
+
#
|
18
|
+
# xyz = state.get('xyz')
|
19
|
+
# if (xyz == nil)
|
20
|
+
# return Result.fatal_error("xyz required")
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# to this:
|
24
|
+
#
|
25
|
+
# xyz = ""; eval(need :xyz, state)
|
26
|
+
#
|
27
|
+
# (If only ruby has LISP macros, this could be so much cleaner...)
|
28
|
+
#
|
29
|
+
def need(symbol, state)
|
30
|
+
x = state.get(symbol.to_s)
|
31
|
+
if (x == nil)
|
32
|
+
return "return Result.fatal_error(\"#{symbol.to_s} required\")"
|
33
|
+
end
|
34
|
+
return "#{symbol.to_s} = state.get('#{symbol.to_s}')"
|
35
|
+
end
|
36
|
+
|
37
|
+
#-------------------------------------------------------------------------------------------------------------------
|
38
|
+
# Generate a random UUID.
|
39
|
+
#
|
40
|
+
# Attempts a few different ways, hopefully one of them work. If all fails, die.
|
41
|
+
#
|
42
|
+
def uuid
|
43
|
+
begin
|
44
|
+
require 'securerandom'
|
45
|
+
uuid = SecureRandom.uuid()
|
46
|
+
|
47
|
+
rescue Exception => e
|
48
|
+
if (File.exist?("/usr/bin/uuidgen")) # Centos e2fsprogs package
|
49
|
+
uuid = `/usr/bin/uuidgen`
|
50
|
+
return uuid.chomp
|
51
|
+
|
52
|
+
elsif (File.exist?("/usr/bin/uuid")) # Debian uuid package
|
53
|
+
uuid = `/usr/bin/uuid`
|
54
|
+
return uuid.chomp
|
55
|
+
|
56
|
+
else
|
57
|
+
die("Unable to generate UUIDs")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#-------------------------------------------------------------------------------------------------------------------
|
63
|
+
# Convenience method to build a string of HTTP query parameters.
|
64
|
+
#
|
65
|
+
def add_param(params, param)
|
66
|
+
if (param == nil || param.length() == 0)
|
67
|
+
return params
|
68
|
+
end
|
69
|
+
if (params == nil || params.length() == 0)
|
70
|
+
return "?#{param}"
|
71
|
+
end
|
72
|
+
return "#{params}&#{param}"
|
73
|
+
end
|
74
|
+
|
75
|
+
#-------------------------------------------------------------------------------------------------------------------
|
76
|
+
# Show details about request being sent.
|
77
|
+
#
|
78
|
+
def show_req(request)
|
79
|
+
if ($LOG_LEVEL >= 2)
|
80
|
+
puts "---[ Request ]----------------------------------------------------"
|
81
|
+
puts "#{request.method} #{request.path}"
|
82
|
+
request.each_header { |name,value|
|
83
|
+
puts "#{name}: #{value}"
|
84
|
+
}
|
85
|
+
puts "\n#{request.body}\n"
|
86
|
+
puts "------------------------------------------------------------------"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
#-------------------------------------------------------------------------------------------------------------------
|
91
|
+
# Show details about response received.
|
92
|
+
#
|
93
|
+
def show_response(response)
|
94
|
+
if ($LOG_LEVEL >= 2)
|
95
|
+
puts "---[ Response ]---------------------------------------------------"
|
96
|
+
puts "#{response.code} #{response.message}"
|
97
|
+
response.each_header { |name,value|
|
98
|
+
puts "#{name}: #{value}"
|
99
|
+
}
|
100
|
+
puts "\n#{response.body}\n"
|
101
|
+
puts "------------------------------------------------------------------"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
#-------------------------------------------------------------------------------------------------------------------
|
106
|
+
# Utility function to perform raw HTTP request.
|
107
|
+
#
|
108
|
+
# If url is not provided, will attempt to use generic 'server' and 'server_port' values from state.
|
109
|
+
#
|
110
|
+
def get_http_response(req, state, url)
|
111
|
+
|
112
|
+
# TODO: SSL support
|
113
|
+
|
114
|
+
auth = state.get('auth')
|
115
|
+
if (auth == "basic")
|
116
|
+
user = state.get('auth_user')
|
117
|
+
password = state.get('auth_password')
|
118
|
+
req.basic_auth(user, password)
|
119
|
+
end
|
120
|
+
|
121
|
+
if (url == nil)
|
122
|
+
server = state.get('server')
|
123
|
+
port = state.get('server_port')
|
124
|
+
die("no server specified") if (server == nil)
|
125
|
+
die("no server port specified") if (port == nil)
|
126
|
+
url = "http://#{server}:#{port}"
|
127
|
+
end
|
128
|
+
|
129
|
+
begin
|
130
|
+
urlobj = URI.parse(url)
|
131
|
+
http = Net::HTTP.new(urlobj.host, urlobj.port)
|
132
|
+
out(1, "Connecting to: #{urlobj.to_s}")
|
133
|
+
response = http.request(req)
|
134
|
+
|
135
|
+
rescue Exception => e
|
136
|
+
return Result.fatal_error("Failure connecting to server #{server}: #{e.to_s}")
|
137
|
+
end
|
138
|
+
|
139
|
+
return response
|
140
|
+
end
|
141
|
+
|
142
|
+
#-------------------------------------------------------------------------------------------------------------------
|
143
|
+
# Utility function to perform HTTP request.
|
144
|
+
# This is the method API subclasses should usually invoke when Discovery is available.
|
145
|
+
# It will connect to a service of the given type.
|
146
|
+
# It will log output and return a Result object.
|
147
|
+
#
|
148
|
+
def do_request_by_service(req, state, service)
|
149
|
+
service_list = state.get('service_list')
|
150
|
+
if (service_list == nil)
|
151
|
+
return Result.fatal_error("service_list not available in state!")
|
152
|
+
end
|
153
|
+
|
154
|
+
if (service_list[service] == nil)
|
155
|
+
return Result.fatal_error("No service of type #{service} available!")
|
156
|
+
end
|
157
|
+
|
158
|
+
url = service_list[service][0]
|
159
|
+
return do_request_by_url(req, state, url)
|
160
|
+
end
|
161
|
+
|
162
|
+
#-------------------------------------------------------------------------------------------------------------------
|
163
|
+
# Utility function to perform HTTP request.
|
164
|
+
# API subclasses can invoke this to connect to a specific host/port.
|
165
|
+
# It will log output and return a Result object.
|
166
|
+
#
|
167
|
+
def do_request_by_url(req, state, url)
|
168
|
+
show_req(req)
|
169
|
+
response = get_http_response(req, state, url)
|
170
|
+
show_response(response)
|
171
|
+
|
172
|
+
state.set_in_test('http_code', response.code)
|
173
|
+
result = Result.new(response)
|
174
|
+
|
175
|
+
return result
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Appends colon-separed results to a report file.
|
4
|
+
#
|
5
|
+
|
6
|
+
class SimpleFileReport
|
7
|
+
|
8
|
+
def initialize(file)
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def log(status, duration, service, api, name, message)
|
13
|
+
line = "#{Time.now.to_i}:#{status}:#{duration}:#{service}:#{api}:#{name}:#{message}"
|
14
|
+
file = File.new(@file, "a")
|
15
|
+
file.puts(line)
|
16
|
+
file.close()
|
17
|
+
end
|
18
|
+
|
19
|
+
def done(tests_ok, tests_fail)
|
20
|
+
line = "#{Time.now.to_i}:DONE:#{tests_ok}:#{tests_fail}"
|
21
|
+
file = File.new(@file, "a")
|
22
|
+
file.puts(line)
|
23
|
+
file.close()
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/State.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
#
|
2
|
+
# The State class is a container for both global and per-test state.
|
3
|
+
# The test framework keeps one global State object (in $GLOBAL_STATE).
|
4
|
+
# A per-test State object is created for each test. The per-test object
|
5
|
+
# inherits all the global state but can override values if needed.
|
6
|
+
# Values set in the per-test class are not persisted beyond the lifetime
|
7
|
+
# of a single test.
|
8
|
+
#
|
9
|
+
class State
|
10
|
+
|
11
|
+
attr_accessor :vars
|
12
|
+
|
13
|
+
#-------------------------------------------------------------------------------------------------------------------
|
14
|
+
# Constructor. If baseobj is nil, a top-level State object is created. Otherwise, a child (per-test) State
|
15
|
+
# object is created.
|
16
|
+
#
|
17
|
+
def initialize(baseobj = nil)
|
18
|
+
@vars = Hash.new()
|
19
|
+
@validations = Hash.new()
|
20
|
+
|
21
|
+
@headers = Hash.new
|
22
|
+
@headers['Content-Type'] = 'application/json'
|
23
|
+
@headers['User-Agent'] = 'restest'
|
24
|
+
|
25
|
+
@baseobj = baseobj
|
26
|
+
end
|
27
|
+
|
28
|
+
#-------------------------------------------------------------------------------------------------------------------
|
29
|
+
# Get a named value. Value is returned from either self, if present, or baseobj, if applicable.
|
30
|
+
#
|
31
|
+
def get(name)
|
32
|
+
if (@vars[name] != nil)
|
33
|
+
return @vars[name]
|
34
|
+
end
|
35
|
+
|
36
|
+
if (@baseobj != nil)
|
37
|
+
return @baseobj.get(name)
|
38
|
+
end
|
39
|
+
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
|
43
|
+
#-------------------------------------------------------------------------------------------------------------------
|
44
|
+
# Set a persistent value. For per-test State objects, this means the value is set in the parent (global) State
|
45
|
+
# object so it will persist beyond one test.
|
46
|
+
#
|
47
|
+
def set(name, value)
|
48
|
+
if (@baseobj != nil)
|
49
|
+
@baseobj.set(name, value)
|
50
|
+
else
|
51
|
+
@vars[name] = value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
#-------------------------------------------------------------------------------------------------------------------
|
56
|
+
# Remove a persistent value.
|
57
|
+
#
|
58
|
+
def unset(name)
|
59
|
+
if (@baseobj != nil)
|
60
|
+
@baseobj.unset(name)
|
61
|
+
else
|
62
|
+
@vars.delete(name)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#-------------------------------------------------------------------------------------------------------------------
|
67
|
+
# Set a non-persistent value in a per-test State object. Calling this method on the global State is not allowed.
|
68
|
+
#
|
69
|
+
def set_in_test(name, value)
|
70
|
+
if (@baseobj != nil)
|
71
|
+
@vars[name] = value
|
72
|
+
else
|
73
|
+
die("cannot set_in_test for top-level State (name=#{name}, value=#{value})")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#-------------------------------------------------------------------------------------------------------------------
|
78
|
+
# Return HTTP headers for request. A minimal default set of headers is returned.
|
79
|
+
#
|
80
|
+
# If a mix Hash is provided, its elements are merged to the default headers (headers in 'mix' override the defaults
|
81
|
+
# if both are present). This allows per-test header customization.
|
82
|
+
#
|
83
|
+
def headers(mix = nil)
|
84
|
+
if (mix != nil)
|
85
|
+
return @headers.merge(mix)
|
86
|
+
else
|
87
|
+
return @headers
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
#-------------------------------------------------------------------------------------------------------------------
|
92
|
+
# Record a per-test validation (loaded from test file). Calling this method on the global State is not allowed.
|
93
|
+
#
|
94
|
+
def set_validate(name, value)
|
95
|
+
if (@baseobj != nil)
|
96
|
+
@validations[name] =value
|
97
|
+
else
|
98
|
+
die("cannot set_validate for top-level State (name=#{name}, value=#{value})")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
#-------------------------------------------------------------------------------------------------------------------
|
103
|
+
# Validates all the 'validate' conditions for the current test.
|
104
|
+
# Calling this method on the global State is not allowed.
|
105
|
+
# Validating verifies that all entries set with set_validate have a matching value in test state.
|
106
|
+
# If any one or more fail, the result is marked as erroneous.
|
107
|
+
#
|
108
|
+
def validate(result)
|
109
|
+
@validations.each_key { |key|
|
110
|
+
failed = false
|
111
|
+
val = get(key)
|
112
|
+
wanted = @validations[key]
|
113
|
+
wanted =~ /(\S+) (.*)/
|
114
|
+
op = $1
|
115
|
+
target = $2
|
116
|
+
out(1, "validating: [#{key}]: want: [#{op} #{target}], got [#{val}]")
|
117
|
+
if (op == "=")
|
118
|
+
failed = true if (target != val.to_s)
|
119
|
+
|
120
|
+
elsif (op == "<")
|
121
|
+
failed = true if (val.to_i >= target.to_i)
|
122
|
+
|
123
|
+
elsif (op == ">")
|
124
|
+
failed = true if (val.to_i <= target.to_i)
|
125
|
+
|
126
|
+
elsif (op == "is")
|
127
|
+
if (target == "set")
|
128
|
+
failed = true if (val == nil || val.length == 0)
|
129
|
+
elsif (target == "unset")
|
130
|
+
failed = true if (val != nil && val.length > 0)
|
131
|
+
else
|
132
|
+
die("unknown directive for operation 'is': #{target}")
|
133
|
+
end
|
134
|
+
|
135
|
+
else
|
136
|
+
die("unknown validate operation #{op}")
|
137
|
+
end
|
138
|
+
|
139
|
+
if (failed)
|
140
|
+
result.error("For [#{key}] expected [#{op} #{target}] but got [#{val}].")
|
141
|
+
$VALIDATIONS_FAIL += 1
|
142
|
+
else
|
143
|
+
$VALIDATIONS_OK += 1
|
144
|
+
end
|
145
|
+
}
|
146
|
+
|
147
|
+
# This is Proofpoint specific and should not be in this class. There's not a good place to put it right
|
148
|
+
# now that doesn't involve copying multiple times, so put it here for now.
|
149
|
+
if (result.code == "500")
|
150
|
+
if (result.body != nil)
|
151
|
+
begin
|
152
|
+
json = JSON.parse(result.body)
|
153
|
+
if (json != nil && json['host'] != nil && json['message'] != nil)
|
154
|
+
result.add_message("Server: [#{json['host']}] said: [#{json['message']}])")
|
155
|
+
end
|
156
|
+
rescue
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
#-------------------------------------------------------------------------------------------------------------------
|
164
|
+
# To string...
|
165
|
+
#
|
166
|
+
def to_s
|
167
|
+
if (@baseobj == nil)
|
168
|
+
s = "Base State: "
|
169
|
+
@vars.each_key { |key|
|
170
|
+
s += "#{key}='#{@vars[key]}' "
|
171
|
+
}
|
172
|
+
else
|
173
|
+
s = "Test State: "
|
174
|
+
@vars.each_key { |key|
|
175
|
+
s += "#{key}='#{@vars[key]}' "
|
176
|
+
}
|
177
|
+
s += "(" + @baseobj.to_s + ")"
|
178
|
+
end
|
179
|
+
return s
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
data/lib/restest.rb
ADDED
@@ -0,0 +1,384 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# A test suite file must require this file, it provides the main entry point to the test harness.
|
4
|
+
#
|
5
|
+
# - Commands available in the test DSL are defined here.
|
6
|
+
# - Initialization is done here.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'State'
|
10
|
+
require 'optparse'
|
11
|
+
|
12
|
+
$GLOBAL_STATE = State.new()
|
13
|
+
$LOG_LEVEL = 0
|
14
|
+
$TESTS_OK = 0
|
15
|
+
$TESTS_FAIL = 0
|
16
|
+
$VALIDATIONS_OK = 0
|
17
|
+
$VALIDATIONS_FAIL = 0
|
18
|
+
$CONFIG_FILE=nil
|
19
|
+
$REPORT = nil
|
20
|
+
$TEST_FILES = Hash.new()
|
21
|
+
$INTERACTIVE = false
|
22
|
+
$SAVED_STATES = Hash.new()
|
23
|
+
|
24
|
+
#---------------------------------------------------------------------------------------------------------------------
|
25
|
+
# Add a to_bool() method to String
|
26
|
+
#
|
27
|
+
class String
|
28
|
+
def to_bool
|
29
|
+
return true if self == "true"
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#---------------------------------------------------------------------------------------------------------------------
|
35
|
+
# Utility method to show a line of output and exit. Use when a fatal misconfiguration or error is seen.
|
36
|
+
#
|
37
|
+
def die(line)
|
38
|
+
puts "[ERROR] #{line}"
|
39
|
+
exit(1)
|
40
|
+
end
|
41
|
+
|
42
|
+
#---------------------------------------------------------------------------------------------------------------------
|
43
|
+
# General purpose log output. Log level is zero by default, increased by one for each -v argument.
|
44
|
+
#
|
45
|
+
def out(level, line)
|
46
|
+
if ($LOG_LEVEL >= level)
|
47
|
+
puts line
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#---------------------------------------------------------------------------------------------------------------------
|
52
|
+
# Allow test scripts to get values from the global state.
|
53
|
+
#
|
54
|
+
def get(name)
|
55
|
+
return $GLOBAL_STATE.get(name)
|
56
|
+
end
|
57
|
+
|
58
|
+
#---------------------------------------------------------------------------------------------------------------------
|
59
|
+
# Allow test scripts to set values in the global state.
|
60
|
+
#
|
61
|
+
def set(name, value)
|
62
|
+
$GLOBAL_STATE.set(name, value)
|
63
|
+
end
|
64
|
+
|
65
|
+
#---------------------------------------------------------------------------------------------------------------------
|
66
|
+
# Allow test scripts to remove values from the global state.
|
67
|
+
#
|
68
|
+
def unset(name)
|
69
|
+
$GLOBAL_STATE.unset(name)
|
70
|
+
end
|
71
|
+
|
72
|
+
#---------------------------------------------------------------------------------------------------------------------
|
73
|
+
# Saves the current state.
|
74
|
+
#
|
75
|
+
def save_state(tag)
|
76
|
+
$SAVED_STATES[tag] = $GLOBAL_STATE.vars.clone()
|
77
|
+
end
|
78
|
+
|
79
|
+
#---------------------------------------------------------------------------------------------------------------------
|
80
|
+
# Restore global state to a previously saved state.
|
81
|
+
#
|
82
|
+
def restore_state(tag)
|
83
|
+
$GLOBAL_STATE.vars = $SAVED_STATES[tag]
|
84
|
+
if ($GLOBAL_STATE.vars == nil)
|
85
|
+
die("Attempted to restore to a saved stage [#{tag}] which does not exist!")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#---------------------------------------------------------------------------------------------------------------------
|
90
|
+
# Print out the difference from the state named by tag_a to the state named by tag_b (these are states previously
|
91
|
+
# saved with save_state(). If tag_b is not provided, diff is shown to the current global state.
|
92
|
+
#
|
93
|
+
def show_state_diff(tag_a, tag_b = nil)
|
94
|
+
|
95
|
+
a = $SAVED_STATES[tag_a]
|
96
|
+
if (tag_b == nil)
|
97
|
+
b = $GLOBAL_STATE.vars
|
98
|
+
else
|
99
|
+
b = $SAVED_STATES[tag_b]
|
100
|
+
end
|
101
|
+
|
102
|
+
if (a == nil || b == nil)
|
103
|
+
puts "[ERROR] Unable to show diff from state #{tag_a} to state #{tag_b}"
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
b.each { |k,v|
|
108
|
+
if (a[k] == nil)
|
109
|
+
puts " ADDED: '#{k}' => '#{v}'"
|
110
|
+
elsif (a[k] != v)
|
111
|
+
puts " CHANGED: '#{k}' from '#{a[k]}' to '#{v}'"
|
112
|
+
end
|
113
|
+
}
|
114
|
+
|
115
|
+
a.each { |k,v|
|
116
|
+
if (b[k] == nil)
|
117
|
+
puts " REMOVED: '#{k}' was '#{a[k]}'"
|
118
|
+
end
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
#---------------------------------------------------------------------------------------------------------------------
|
123
|
+
# Run a separate test suite file as part of current test suite.
|
124
|
+
# The global state is saved and then restored to isolate state changes made by the included test suite.
|
125
|
+
#
|
126
|
+
def run_suite(name)
|
127
|
+
out(1, "\n\n--Running included test suite #{name}")
|
128
|
+
save_state("_pre_run_suite")
|
129
|
+
load(name)
|
130
|
+
restore_state("_pre_run_suite")
|
131
|
+
out(1, "\n--Completed included test suite #{name}")
|
132
|
+
end
|
133
|
+
|
134
|
+
#---------------------------------------------------------------------------------------------------------------------
|
135
|
+
# Loads test file
|
136
|
+
#
|
137
|
+
def load_test(name, state)
|
138
|
+
if (!File.exists?("tests/#{name}"))
|
139
|
+
die("No test file #{name}")
|
140
|
+
end
|
141
|
+
|
142
|
+
state.set_in_test('filename', name)
|
143
|
+
file = File.new("tests/#{name}", "r")
|
144
|
+
doc = false
|
145
|
+
out(3, "Reading test file tests/#{name}")
|
146
|
+
|
147
|
+
# line 1: service-class-name api-name
|
148
|
+
line = file.gets
|
149
|
+
line =~ /(\S*)\s*(\S*)/
|
150
|
+
if (!$1 || !$2)
|
151
|
+
die("Test #{name} missing service and/or API")
|
152
|
+
end
|
153
|
+
state.set_in_test('service', $1)
|
154
|
+
state.set_in_test('api', $2)
|
155
|
+
|
156
|
+
# then process 'set' or 'validate' directives
|
157
|
+
while ((line = file.gets) != nil)
|
158
|
+
next if line =~ /^\s*$/
|
159
|
+
next if line =~ /^#/
|
160
|
+
|
161
|
+
if (line =~ /^set (\S*)\s+=\s+(.*)/)
|
162
|
+
state.set_in_test($1, $2)
|
163
|
+
|
164
|
+
elsif (line =~ /^validate (\S+)\s+(\S+)\s+(.*)/)
|
165
|
+
state.set_validate($1, "#{$2} #{$3}")
|
166
|
+
|
167
|
+
elsif (line =~ /^doc (.*)/)
|
168
|
+
state.set_in_test('doc', $1)
|
169
|
+
doc = true
|
170
|
+
|
171
|
+
else
|
172
|
+
die("Unknown directive [#{line}] in #{name}")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
if (!doc)
|
177
|
+
die("Test #{name} has no documentation (doc line)")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
#---------------------------------------------------------------------------------------------------------------------
|
182
|
+
# Entry point for running one test. Called from test script by invoking the name of the test file (see method_missing)
|
183
|
+
#
|
184
|
+
def run_test(name, ignore=false)
|
185
|
+
out(1, "\n\n\nRunning test #{name}")
|
186
|
+
|
187
|
+
if ($LOG_LEVEL > 0)
|
188
|
+
save_state('_internal_trace')
|
189
|
+
end
|
190
|
+
|
191
|
+
test_state = State.new($GLOBAL_STATE)
|
192
|
+
load_test(name, test_state)
|
193
|
+
|
194
|
+
service = test_state.get('service')
|
195
|
+
api = test_state.get('api')
|
196
|
+
out(1, "Service class: [#{service}] Calling API: [#{api}]")
|
197
|
+
|
198
|
+
if ($INTERACTIVE)
|
199
|
+
print "Interactive mode... <enter> to run this test now, <c>ontinue: "
|
200
|
+
STDOUT.flush
|
201
|
+
input = gets.chomp!
|
202
|
+
$INTERACTIVE = false if (input == "c")
|
203
|
+
end
|
204
|
+
|
205
|
+
before = Time.now()
|
206
|
+
|
207
|
+
allow_retries = test_state.get('allow_retries')
|
208
|
+
if (allow_retries != nil)
|
209
|
+
times = allow_retries.to_i
|
210
|
+
sleep_sec = test_state.get('retry_sleep').to_i
|
211
|
+
if (sleep_sec < 1)
|
212
|
+
die("allow_retries is set but retry_sleep is not")
|
213
|
+
end
|
214
|
+
out(1, "Test allow retries: allow_retries: #{times} retry_sleep: #{sleep_sec}")
|
215
|
+
begin
|
216
|
+
out(1, "Tries left: #{times}")
|
217
|
+
test_state = State.new($GLOBAL_STATE)
|
218
|
+
load_test(name, test_state)
|
219
|
+
testobj = Object::const_get(service).new
|
220
|
+
result = testobj.send(api, test_state)
|
221
|
+
times -= 1
|
222
|
+
sleep(sleep_sec) if !result.is_ok
|
223
|
+
end while (times > 0 && !result.is_ok && result.allow_retry)
|
224
|
+
|
225
|
+
else
|
226
|
+
testobj = Object::const_get(service).new
|
227
|
+
result = testobj.send(api, test_state)
|
228
|
+
end
|
229
|
+
|
230
|
+
if ($LOG_LEVEL > 0)
|
231
|
+
show_state_diff('_internal_trace')
|
232
|
+
end
|
233
|
+
|
234
|
+
duration = Integer((Time.now() - before) * 1000)
|
235
|
+
|
236
|
+
out(1, "Duration of test: #{duration} ms")
|
237
|
+
|
238
|
+
prefix = ""
|
239
|
+
if (ignore)
|
240
|
+
prefix="IGNORE-"
|
241
|
+
end
|
242
|
+
|
243
|
+
if (result.is_ok)
|
244
|
+
out(0, "[#{prefix}OK] (#{duration}ms) #{service}.#{api}: #{test_state.get('doc')}")
|
245
|
+
$TESTS_OK += 1 if !ignore
|
246
|
+
else
|
247
|
+
out(0, "[#{prefix}FAIL] (#{duration}ms) #{service}.#{api}: #{test_state.get('doc')} #{result.message}")
|
248
|
+
$TESTS_FAIL += 1 if !ignore
|
249
|
+
end
|
250
|
+
|
251
|
+
if ($REPORT != nil)
|
252
|
+
if (result.is_ok)
|
253
|
+
status = "#{prefix}OK"
|
254
|
+
else
|
255
|
+
status = "#{prefix}FAIL"
|
256
|
+
end
|
257
|
+
$REPORT.log(status, duration, service, api, name, result.message)
|
258
|
+
end
|
259
|
+
|
260
|
+
if (result.abort_suite_run)
|
261
|
+
die("Test suite run has been aborted by previous test!")
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
#---------------------------------------------------------------------------------------------------------------------
|
266
|
+
# Show Usage message
|
267
|
+
#
|
268
|
+
def show_usage(opts)
|
269
|
+
puts opts
|
270
|
+
puts <<EOF
|
271
|
+
|
272
|
+
Running a test suite
|
273
|
+
====================
|
274
|
+
|
275
|
+
To run a test suite, simply run the test suite file as it is an
|
276
|
+
executable ruby script. A configuration file argument must be
|
277
|
+
provided.
|
278
|
+
|
279
|
+
The configuration file is responsible for setting state values which
|
280
|
+
are specific to a test environment, and thus not suitable to set
|
281
|
+
elsewhere. These include values such as hostnames, user names, test
|
282
|
+
domains, etc.
|
283
|
+
|
284
|
+
(TODO: Until the framework packaging is organized, you need to run it
|
285
|
+
from the test suite directory. So for the share tests for example, "cd
|
286
|
+
share-tests" first. This is a temporary limitation.)
|
287
|
+
|
288
|
+
The initial share test script is called "suite1" so run it by:
|
289
|
+
|
290
|
+
% ./suite1 -c config.ENV
|
291
|
+
|
292
|
+
(Where ENV is the share environment to run it against. There are
|
293
|
+
separate config files for each share env.)
|
294
|
+
|
295
|
+
EOF
|
296
|
+
end
|
297
|
+
|
298
|
+
#---------------------------------------------------------------------------------------------------------------------
|
299
|
+
# Initialization sections below. This file must be included by a test script so this is run before any test entries.
|
300
|
+
#
|
301
|
+
|
302
|
+
# Process command line arguments if any.
|
303
|
+
optparse = OptionParser.new do|opts|
|
304
|
+
# Set a banner, displayed at the top
|
305
|
+
# of the help screen.
|
306
|
+
opts.banner = "Usage: YOUR_SCRIPT [options] -c|--config CONFIG_FILE"
|
307
|
+
|
308
|
+
# Define the options, and what they do
|
309
|
+
opts.on( '-c', '--config CONFIG', 'Test config file' ) do|config|
|
310
|
+
$CONFIG_FILE = config
|
311
|
+
if (File.exists?($CONFIG_FILE))
|
312
|
+
load $CONFIG_FILE
|
313
|
+
else
|
314
|
+
show_usage(opts)
|
315
|
+
die("Config file not found")
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
opts.on( '-l', '--log LOG_FILE', 'Log results into "LOG_FILE" in colon-separated text' ) do|file|
|
320
|
+
require 'SimpleFileReport'
|
321
|
+
$REPORT = SimpleFileReport.new(file)
|
322
|
+
end
|
323
|
+
|
324
|
+
opts.on( '-v', 'Increase verbosity (each -v increases verbosity further)' ) do
|
325
|
+
$LOG_LEVEL += 1
|
326
|
+
end
|
327
|
+
|
328
|
+
opts.on( '-q', 'Quiet, no standard output (useful for running from cron or such)' ) do
|
329
|
+
$LOG_LEVEL = -100
|
330
|
+
end
|
331
|
+
|
332
|
+
opts.on( '-i', 'Interactive. Runs tests one by one.' ) do
|
333
|
+
$INTERACTIVE = true
|
334
|
+
end
|
335
|
+
|
336
|
+
# This displays the help screen, all programs are
|
337
|
+
# assumed to have this option.
|
338
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
339
|
+
show_usage(opts)
|
340
|
+
exit
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
optparse.parse(ARGV)
|
345
|
+
|
346
|
+
# Add shutdown hook to report totals.
|
347
|
+
at_exit {
|
348
|
+
out(0, "Done!")
|
349
|
+
out(0, "#{$TESTS_OK} tests passed and #{$TESTS_FAIL} tests failed.")
|
350
|
+
out(0, "#{$VALIDATIONS_OK} validations passed and #{$VALIDATIONS_FAIL} validations failed.")
|
351
|
+
if ($REPORT != nil)
|
352
|
+
$REPORT.done($TESTS_OK, $TESTS_FAIL)
|
353
|
+
end
|
354
|
+
}
|
355
|
+
|
356
|
+
# Load names of existing test files
|
357
|
+
Dir.foreach("tests") { |file| $TEST_FILES[file] = 1 }
|
358
|
+
|
359
|
+
|
360
|
+
#---------------------------------------------------------------------------------------------------------------------
|
361
|
+
# Finally, add missing_method hook to enable test suite DSL to call test cases by their names.
|
362
|
+
#
|
363
|
+
# Note: In some ruby versions (at least 1.9.0) we can use File.exists? here instead of having to keep the list
|
364
|
+
# of test files in $TEST_FILES and checking that. However, in some other ruby versions (at least 1.9.2p290)
|
365
|
+
# File.exists? ends up invoking some non-existent methods which trigger a method_missing call, which leads
|
366
|
+
# to an infinite loop (until stack space runs out) if method_missing relies on File.exists?.
|
367
|
+
#
|
368
|
+
def method_missing(method_name, *args)
|
369
|
+
if ($TEST_FILES[method_name.to_s])
|
370
|
+
run_test(method_name)
|
371
|
+
elsif (method_name == :ignore) && $TEST_FILES[args[0].to_s]
|
372
|
+
run_test(args[0], true)
|
373
|
+
else
|
374
|
+
super
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
def respond_to?(method_name, include_private = false)
|
379
|
+
if ($TEST_FILES[method_name.to_s])
|
380
|
+
return true
|
381
|
+
else
|
382
|
+
super
|
383
|
+
end
|
384
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'restest'
|
16
|
+
|
17
|
+
class Test::Unit::TestCase
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: restest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jyri Virki
|
9
|
+
- Peter Salas
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-10-03 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: shoulda
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rdoc
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '3.12'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.12'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: bundler
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.0.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.0.0
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: jeweler
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ~>
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.8.4
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.8.4
|
79
|
+
description: A Ruby framework for testing RESTful API's
|
80
|
+
email: psalas@proofpoint.com
|
81
|
+
executables:
|
82
|
+
- restest
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files:
|
85
|
+
- LICENSE.txt
|
86
|
+
- README.rdoc
|
87
|
+
files:
|
88
|
+
- .document
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE.txt
|
91
|
+
- README.rdoc
|
92
|
+
- Rakefile
|
93
|
+
- VERSION
|
94
|
+
- bin/restest
|
95
|
+
- lib/MySQLReport.rb
|
96
|
+
- lib/Result.rb
|
97
|
+
- lib/ServiceAPI.rb
|
98
|
+
- lib/SimpleFileReport.rb
|
99
|
+
- lib/State.rb
|
100
|
+
- lib/restest.rb
|
101
|
+
- test/helper.rb
|
102
|
+
- test/test_restest.rb
|
103
|
+
homepage: http://github.com/gradeawarrior/restest
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
segments:
|
117
|
+
- 0
|
118
|
+
hash: 4020511203719455767
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.24
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: RESTful API Testing Framework
|
131
|
+
test_files: []
|