resat 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +321 -0
- data/Rakefile +33 -0
- data/bin/resat +223 -0
- data/examples/rightscale/README.rdoc +39 -0
- data/examples/rightscale/additional/run_server.yml +75 -0
- data/examples/rightscale/config/resat.yaml +34 -0
- data/examples/rightscale/scenarios/create_server.yml +48 -0
- data/examples/rightscale/scenarios/delete_server.yml +13 -0
- data/examples/rightscale/scenarios/list_servers.yml +9 -0
- data/examples/twitter/README.rdoc +50 -0
- data/examples/twitter/additional/follow.yml +15 -0
- data/examples/twitter/additional/send_message.yml +19 -0
- data/examples/twitter/config/resat.yaml +40 -0
- data/examples/twitter/output.yml +5 -0
- data/examples/twitter/scenarios/timelines.yml +31 -0
- data/examples/twitter/scenarios/tweet.yml +14 -0
- data/lib/api_request.rb +145 -0
- data/lib/config.rb +94 -0
- data/lib/engine.rb +98 -0
- data/lib/file_set.rb +33 -0
- data/lib/filter.rb +113 -0
- data/lib/guard.rb +36 -0
- data/lib/handler.rb +50 -0
- data/lib/kwalify_helper.rb +31 -0
- data/lib/log.rb +114 -0
- data/lib/net_patch.rb +15 -0
- data/lib/rdoc_patch.rb +37 -0
- data/lib/resat.rb +5 -0
- data/lib/scenario_runner.rb +203 -0
- data/lib/variables.rb +116 -0
- data/schemas/config.yaml +48 -0
- data/schemas/scenarios.yaml +169 -0
- data/schemas/variables.yaml +18 -0
- metadata +98 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 RightScale, Inc.
|
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 NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
= Resat
|
2
|
+
|
3
|
+
= DESCRIPTION
|
4
|
+
|
5
|
+
== Synopsis
|
6
|
+
|
7
|
+
Resat is a script engine which allows grouping web requests into <b>scenarios</b>.
|
8
|
+
|
9
|
+
A scenario consists of serie of HTTP requests called <b>steps</b>.
|
10
|
+
|
11
|
+
Each step may be associated with <b>guards</b> and/or <b>filters</b> and/or <b>handlers</b>.
|
12
|
+
|
13
|
+
The syntax used to defined scenarios is simple and can be used by programmers and
|
14
|
+
non-programmers alike. See the WRITING SCENARIOS section below for examples.
|
15
|
+
|
16
|
+
* Guards keep making the same request until the response header and/or body
|
17
|
+
satisfy(ies) certain conditions.
|
18
|
+
|
19
|
+
* Filters validate the response and may save some of its elements in variables.
|
20
|
+
Variables can be used to define requests, guards and filters.
|
21
|
+
|
22
|
+
* Handlers allow writing custom code to handle a request and its response.
|
23
|
+
|
24
|
+
Scenarios are defined as YAML documents that must adhere to the Kwalify
|
25
|
+
schemas defined in <tt>schemas/scenarios.yaml</tt>. See the comments in this
|
26
|
+
file for additional information.
|
27
|
+
|
28
|
+
Resat is configured through a YAML configuration file which defines
|
29
|
+
default values that applies to all requests including the host name,
|
30
|
+
base url, whether to use SSL, common headers and body parameters and
|
31
|
+
optionally a username and password to be used with basic authentication.
|
32
|
+
This configuration file is located in <tt>config/resat.yaml</tt> by default.
|
33
|
+
|
34
|
+
== Why resat?
|
35
|
+
|
36
|
+
There are two main use cases for resat:
|
37
|
+
|
38
|
+
1. Scripting: Resat can be used to chaing together a serie of REST API calls
|
39
|
+
that can be used to perform repetitive tasks.
|
40
|
+
|
41
|
+
2. API testing: For REST API implementors, resat is the ideal automated
|
42
|
+
regression tool. This is the tool we use at RightScale to test our APIs.
|
43
|
+
|
44
|
+
== How to use
|
45
|
+
|
46
|
+
resat can be used as a ruby library or as an application. Using it as library
|
47
|
+
involves instantiating the engine and calling the 'run' method:
|
48
|
+
|
49
|
+
require 'resat'
|
50
|
+
|
51
|
+
options = OpenStruct.new
|
52
|
+
options.verbose = false
|
53
|
+
options.quiet = false
|
54
|
+
options.norecursion = false
|
55
|
+
options.loglevel = 'info'
|
56
|
+
options.logfile = 'resat.log'
|
57
|
+
options.configfile = 'config/resat.yaml'
|
58
|
+
options.schemasdir = 'schemas'
|
59
|
+
|
60
|
+
Resat::Log.init(options)
|
61
|
+
engine = Resat::Engine.new(options)
|
62
|
+
engine.run('my_scenario.yaml')
|
63
|
+
|
64
|
+
if engine.succeeded?
|
65
|
+
puts engine.summary.dark_blue
|
66
|
+
else
|
67
|
+
puts engine.summary.dark_red
|
68
|
+
end
|
69
|
+
puts "#{engine.requests_count} request(s)."
|
70
|
+
puts "#{engine.ignored_count} scenario(s) ignored."
|
71
|
+
puts "#{engine.skipped_count} YAML file(s) skipped."
|
72
|
+
|
73
|
+
See the examples and usage sections below for using resat as an application.
|
74
|
+
|
75
|
+
== Examples
|
76
|
+
|
77
|
+
Run the scenario defined in scenario.yaml:
|
78
|
+
|
79
|
+
$ resat scenario.yaml
|
80
|
+
|
81
|
+
Execute scenarios defined in the 'scenarios' directory and its
|
82
|
+
sub-directories:
|
83
|
+
|
84
|
+
$ resat scenarios
|
85
|
+
|
86
|
+
Only execute the scenarios defined in the current directory, do not execute
|
87
|
+
scenarios found in sub-directories:
|
88
|
+
|
89
|
+
$ resat -n .
|
90
|
+
|
91
|
+
== Usage
|
92
|
+
|
93
|
+
resat [options] target
|
94
|
+
|
95
|
+
For help use: resat -h
|
96
|
+
|
97
|
+
== Options
|
98
|
+
|
99
|
+
-h, --help Display help message
|
100
|
+
-v, --version Display version, then exit
|
101
|
+
-q, --quiet Output as little as possible, override verbose
|
102
|
+
-V, --verbose Verbose output
|
103
|
+
-n, --norecursion Don't run scenarios defined in sub-directories
|
104
|
+
-d, --define NAME:VAL Define global variable (can appear multiple times,
|
105
|
+
escape ':' with '::')
|
106
|
+
-f, --failonerror Stop resat from continuing to run if an error occurs
|
107
|
+
-c, --config PATH Config file path (config/resat.yaml by default)
|
108
|
+
-s, --schemasdir DIR Path to schemas directory (schemas/ by default)
|
109
|
+
-l, --loglevel LVL Log level: debug, info, warn, error (info by default)
|
110
|
+
-F, --logfile PATH Log file path (resat.log by default)
|
111
|
+
|
112
|
+
= INSTALLATION
|
113
|
+
|
114
|
+
* <b>From source</b>: run the following command from the root folder to be able to run resat from anywhere:
|
115
|
+
|
116
|
+
$ sudo ln -s `pwd`/bin/resat /usr/local/bin/resat
|
117
|
+
|
118
|
+
* <b>Using the gem</b>:
|
119
|
+
|
120
|
+
$ sudo gem install resat
|
121
|
+
|
122
|
+
= DEVELOPMENT
|
123
|
+
|
124
|
+
== Source
|
125
|
+
|
126
|
+
The source code of Resat is available via Git: http://github.com/raphael/resat.git
|
127
|
+
Fork the project and send pull requests to contribute!
|
128
|
+
|
129
|
+
== Dependencies
|
130
|
+
|
131
|
+
resat relies on Kwalify for validating YAML files:
|
132
|
+
|
133
|
+
$ sudo gem install kwalify
|
134
|
+
|
135
|
+
* http://www.kuwata-lab.com/kwalify/
|
136
|
+
|
137
|
+
= WRITING SCENARIOS
|
138
|
+
|
139
|
+
At the heart of your resat scripts are the scenarios. A scenario consists of
|
140
|
+
one or more steps. A scenario may include other scenarios. A single execution
|
141
|
+
of Resat can apply to multiple scenarios (all scenarios in a given folder).
|
142
|
+
|
143
|
+
A simple scenario containing a single step is defined below:
|
144
|
+
|
145
|
+
name: List all servers
|
146
|
+
steps:
|
147
|
+
- request:
|
148
|
+
operation: index
|
149
|
+
resource: servers
|
150
|
+
|
151
|
+
The first element of the scenario is its name. The name is used by the command
|
152
|
+
line tool for update and error outputs.
|
153
|
+
|
154
|
+
The second element is the list of steps. A step must contain a request. A
|
155
|
+
request corresponds to one of the REST CRUD operations and applies to a
|
156
|
+
resource. CRUD operations are <i>create</i>, <i>show</i>, <i>index</i>, <i>update</i>,
|
157
|
+
and <i>destroy</i>.
|
158
|
+
|
159
|
+
Operations that apply to a single resource rather than to all resources require
|
160
|
+
the <i>id</i> element:
|
161
|
+
|
162
|
+
name: Show server 42
|
163
|
+
steps:
|
164
|
+
- request:
|
165
|
+
operation: show
|
166
|
+
resource: servers
|
167
|
+
id: 42
|
168
|
+
|
169
|
+
Resat also allows defining <i>custom</i> requests for making web requests that
|
170
|
+
don't map to a CRUD operation. A custom request is defined by a <i>type</i>
|
171
|
+
corresponding to the HTTP verb that the request should use (i.e. <tt>get</tt>, <tt>post</tt>,
|
172
|
+
<tt>put</tt> or <tt>delete</tt>) and its name.
|
173
|
+
|
174
|
+
name: Twitter Timelines
|
175
|
+
steps:
|
176
|
+
- request:
|
177
|
+
resource: statuses
|
178
|
+
custom: # Use a custom operation
|
179
|
+
name: public_timeline.xml # Operation name
|
180
|
+
type: get # GET request
|
181
|
+
|
182
|
+
Requests can then be followed by filters which can validate the response and/or
|
183
|
+
extract elements from it.
|
184
|
+
|
185
|
+
name: Get Mephisto ServerTemplate
|
186
|
+
steps:
|
187
|
+
- request:
|
188
|
+
operation: index
|
189
|
+
resource: server_templates
|
190
|
+
filters:
|
191
|
+
- name: get server template href
|
192
|
+
target: body
|
193
|
+
validators:
|
194
|
+
- field: server-templates/ec2-server-template[nickname='Mephisto all-in-one v8']/href
|
195
|
+
is_empty: false
|
196
|
+
extractors:
|
197
|
+
- field: server-templates/ec2-server-template[nickname='Mephisto all-in-one v8']/href
|
198
|
+
variable: server_template_href
|
199
|
+
|
200
|
+
Variables that are extracted from a request response can then be used for
|
201
|
+
other requests, filters or guards. A variable is used using the <tt>$</tt> sign
|
202
|
+
followed by the variable name. A variable may be written to an output file if
|
203
|
+
it has the <i>save</i> element and the configuration file defines an output
|
204
|
+
file. A variable can also be exported to other scenarios that will get run in
|
205
|
+
the same Resat execution (so a scenario can create resources and save their ids
|
206
|
+
and a following scenario can reuse the ids to delete or update the resources).
|
207
|
+
|
208
|
+
The element to extract can be a response header or a response body field. If it
|
209
|
+
is a response body field then an XPATH query is used to identity which part of
|
210
|
+
the response body should be extracted.
|
211
|
+
|
212
|
+
The value to be extracted can be further defined using a regular expression
|
213
|
+
with a capture block. The regular expression is applied to the field matching
|
214
|
+
the XPATH associated with the extractor.
|
215
|
+
|
216
|
+
<b>Note</b>: Because XPATH is used to define fields in extractors and
|
217
|
+
validators, only requests that return XML can be followed by filters.
|
218
|
+
|
219
|
+
name: Create Mephisto Server
|
220
|
+
steps:
|
221
|
+
- request:
|
222
|
+
operation: create
|
223
|
+
resource: servers
|
224
|
+
valid_codes:
|
225
|
+
- 201
|
226
|
+
params:
|
227
|
+
- name: server[nickname]
|
228
|
+
value: 'resat created server'
|
229
|
+
- name: server[server_template_href]
|
230
|
+
value: $server_template_href
|
231
|
+
- name: server[deployment_href]
|
232
|
+
value: $deployment_href
|
233
|
+
filters:
|
234
|
+
- name: validate server response
|
235
|
+
target: body
|
236
|
+
is_empty: true
|
237
|
+
- name: extract server id
|
238
|
+
target: header
|
239
|
+
extractors:
|
240
|
+
- field: location
|
241
|
+
pattern: '.*\/(\d+)$'
|
242
|
+
variable: server_id
|
243
|
+
|
244
|
+
A scenario request can also use <i>guards</i>. A guard identifies a response
|
245
|
+
element similarly to an extractor (response header or body field identified by
|
246
|
+
an XPATH and optionally a regular expression). A guard specifies a value that
|
247
|
+
the element must match together with a period and a timeout that should be used
|
248
|
+
to retry the request until the value matches the guard or the timeout is
|
249
|
+
reached.
|
250
|
+
|
251
|
+
name: Wait until server 42 is operational
|
252
|
+
steps:
|
253
|
+
- request:
|
254
|
+
resource: servers
|
255
|
+
id: 42
|
256
|
+
operation: show
|
257
|
+
guards:
|
258
|
+
- target: body
|
259
|
+
field: server/state
|
260
|
+
pattern: 'operational'
|
261
|
+
period: 10
|
262
|
+
timeout: 300
|
263
|
+
name: server operational
|
264
|
+
|
265
|
+
Finally a scenario request can include <i>handlers</i>. Handlers can only be
|
266
|
+
included when resat is used as a library. The handler definition lists a unique
|
267
|
+
name followed the corresponding ruby module name.
|
268
|
+
|
269
|
+
name: Store servers definitions
|
270
|
+
steps:
|
271
|
+
- request:
|
272
|
+
resource: servers
|
273
|
+
operation: index
|
274
|
+
handlers:
|
275
|
+
- name: save results
|
276
|
+
module: ServersPersister
|
277
|
+
|
278
|
+
The ruby module must define a <tt>process</tt> method which accepts two arguments:
|
279
|
+
|
280
|
+
def process(request, response)
|
281
|
+
|
282
|
+
* <i>request</i>: an instance of Net::HTTPRequest corresponding to the request associated with this handler.
|
283
|
+
* <i>response</i>: an instance of Net::HTTPResponse which contains the associated response.
|
284
|
+
|
285
|
+
It should also define a <tt>failures</tt> method which can return a list of errors.
|
286
|
+
The errors will get logged and optionally stop the execution of resat if the
|
287
|
+
<tt>failonerror</tt> option is set to <tt>true</tt>.
|
288
|
+
|
289
|
+
= ADDITIONAL RESOURCES
|
290
|
+
|
291
|
+
* Refer to the examples (http://github.com/raphael/resat/tree/master/examples)
|
292
|
+
for fully functional and documented scenarios.
|
293
|
+
* See the file <tt>schemas/scenarios.yaml</tt>
|
294
|
+
(http://github.com/raphael/resat/blob/master/schemas/scenarios.yaml) for
|
295
|
+
the complete reference on scenarios syntax.
|
296
|
+
|
297
|
+
= LICENSE
|
298
|
+
|
299
|
+
Resat - Web scripting for the masses
|
300
|
+
|
301
|
+
Author:: Raphael Simon (<raphael@rightscale.com>)
|
302
|
+
Copyright:: Copyright (c) 2009 RightScale, Inc.
|
303
|
+
|
304
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
305
|
+
a copy of this software and associated documentation files (the
|
306
|
+
'Software'), to deal in the Software without restriction, including
|
307
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
308
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
309
|
+
permit persons to whom the Software is furnished to do so, subject to
|
310
|
+
the following conditions:
|
311
|
+
|
312
|
+
The above copyright notice and this permission notice shall be
|
313
|
+
included in all copies or substantial portions of the Software.
|
314
|
+
|
315
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
316
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
317
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
318
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
319
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
320
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
321
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
|
4
|
+
GEM = 'resat'
|
5
|
+
GEM_VER = '0.7.0'
|
6
|
+
AUTHOR = 'Raphael Simon'
|
7
|
+
EMAIL = 'raphael@rightscale.com'
|
8
|
+
HOMEPAGE = 'http://github.com/raphael/resat'
|
9
|
+
SUMMARY = 'Web scripting for the masses'
|
10
|
+
|
11
|
+
spec = Gem::Specification.new do |s|
|
12
|
+
s.name = GEM
|
13
|
+
s.version = GEM_VER
|
14
|
+
s.author = AUTHOR
|
15
|
+
s.email = EMAIL
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
s.summary = SUMMARY
|
18
|
+
s.description = SUMMARY
|
19
|
+
s.homepage = HOMEPAGE
|
20
|
+
s.files = %w(LICENSE README.rdoc Rakefile) + FileList["{bin,lib,schemas,examples}/**/*"].to_a
|
21
|
+
s.executables = ['resat']
|
22
|
+
s.extra_rdoc_files = ["README.rdoc", "LICENSE"]
|
23
|
+
s.add_dependency("kwalify", ">= 0.7.1")
|
24
|
+
end
|
25
|
+
|
26
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
27
|
+
pkg.gem_spec = spec
|
28
|
+
end
|
29
|
+
|
30
|
+
task :install => [:package] do
|
31
|
+
sh %{sudo gem install pkg/#{GEM}-#{GEM_VER}}
|
32
|
+
end
|
33
|
+
|
data/bin/resat
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# === Synopsis
|
4
|
+
# resat - RightScale API Tester
|
5
|
+
#
|
6
|
+
# This application allows making automated REST requests optionally followed
|
7
|
+
# by validation. It reads scenarios defined in YAML files and executes the
|
8
|
+
# corresponding steps. A step consist of a REST request followed by any
|
9
|
+
# number of filters.
|
10
|
+
#
|
11
|
+
# Scenarios are defined as YAML documents that must adhere to the Kwalify
|
12
|
+
# schemas defined in schemas/scenarios.yaml. See the comments in this
|
13
|
+
# file for additional information.
|
14
|
+
#
|
15
|
+
# resat is configured through a YAML configuration file which defines
|
16
|
+
# information that applies to all requests including the host name,
|
17
|
+
# base url, whether to use SSL, common headers and body parameters and
|
18
|
+
# optionally a username and password to be used with basic authentication.
|
19
|
+
# This configuration file should be located in config/resat.yaml by default.
|
20
|
+
#
|
21
|
+
# === Examples
|
22
|
+
# Run the scenario defined in scenario.yaml:
|
23
|
+
# resat scenario.yaml
|
24
|
+
#
|
25
|
+
# Execute scenarios defined in the 'scenarios' directory and its
|
26
|
+
# sub-directories:
|
27
|
+
# resat scenarios
|
28
|
+
#
|
29
|
+
# Only execute the scenarios defined in the current directory, do not execute
|
30
|
+
# scenarios found in sub-directories:
|
31
|
+
# resat -n .
|
32
|
+
#
|
33
|
+
# === Usage
|
34
|
+
# resat [options] target
|
35
|
+
#
|
36
|
+
# For help use: resat -h
|
37
|
+
#
|
38
|
+
# === Options
|
39
|
+
# -h, --help Display help message
|
40
|
+
# -v, --version Display version, then exit
|
41
|
+
# -q, --quiet Output as little as possible, override verbose
|
42
|
+
# -V, --verbose Verbose output
|
43
|
+
# -n, --norecursion Don't run scenarios defined in sub-directories
|
44
|
+
# -d, --define NAME:VAL Define global variable (can appear multiple times,
|
45
|
+
# escape ':' with '::')
|
46
|
+
# -f, --failonerror Stop resat from continuing to run if an error occurs
|
47
|
+
# -c, --config PATH Config file path (config/resat.yaml by default)
|
48
|
+
# -s, --schemasdir DIR Path to schemas directory (schemas/ by default)
|
49
|
+
# -l, --loglevel LVL Log level: debug, info, warn, error (info by default)
|
50
|
+
# -F, --logfile PATH Log file path (resat.log by default)
|
51
|
+
# -D, --dry-run Print requests, don't actually make them
|
52
|
+
#
|
53
|
+
|
54
|
+
require 'rubygems'
|
55
|
+
require 'optparse'
|
56
|
+
require 'ostruct'
|
57
|
+
require 'date'
|
58
|
+
require 'benchmark'
|
59
|
+
THIS_FILE = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
|
60
|
+
require File.expand_path(File.join(File.dirname(THIS_FILE), '..', 'lib', 'rdoc_patch'))
|
61
|
+
require File.expand_path(File.join(File.dirname(THIS_FILE), '..', 'lib', 'engine'))
|
62
|
+
|
63
|
+
module Resat
|
64
|
+
class App
|
65
|
+
VERSION = '0.7.0'
|
66
|
+
|
67
|
+
def initialize(arguments)
|
68
|
+
@arguments = arguments
|
69
|
+
|
70
|
+
# Set defaults
|
71
|
+
@options = OpenStruct.new
|
72
|
+
@options.verbose = false
|
73
|
+
@options.quiet = false
|
74
|
+
@options.norecursion = false
|
75
|
+
@options.failonerror = false
|
76
|
+
@options.variables = {}
|
77
|
+
@options.config = nil
|
78
|
+
@options.schemasdir = File.join(File.dirname(THIS_FILE), 'schemas')
|
79
|
+
@options.loglevel = "info"
|
80
|
+
@options.logfile = "/tmp/resat.log"
|
81
|
+
@options.dry_run = false
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parse options, check arguments, then run tests
|
85
|
+
def run
|
86
|
+
if parsed_options? && arguments_valid?
|
87
|
+
begin
|
88
|
+
tms = Benchmark.measure { run_tests }
|
89
|
+
Log.info tms.format("\t\tUser\t\tSystem\t\tReal\nDuration:\t%u\t%y\t%r")
|
90
|
+
rescue Exception => e
|
91
|
+
puts "Error: #{e.message}"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
output_usage
|
95
|
+
@return_value = 1
|
96
|
+
end
|
97
|
+
exit @return_value
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def parsed_options?
|
103
|
+
opts = OptionParser.new
|
104
|
+
opts.on('-h', '--help') { output_help }
|
105
|
+
opts.on('-v', '--version') { output_version; exit 0 }
|
106
|
+
opts.on('-q', '--quiet') { @options.quiet = true }
|
107
|
+
opts.on('-V', '--verbose') { @options.verbose = true }
|
108
|
+
opts.on('-n', '--norecursion') { @options.norecursion = true }
|
109
|
+
opts.on('-f', '--failonerror') { @options.failonerror = true }
|
110
|
+
opts.on('-d', '--define VAR:VAL') { |v| @options.variables.merge!(var_hash(v)) }
|
111
|
+
opts.on('-c', '--config PATH') { |cfg| @options.config = cfg }
|
112
|
+
opts.on('-s', '--schemasdir DIR') { |dir| @options.schemasdir = dir }
|
113
|
+
opts.on('-l', '--loglevel LEVEL') { |level| @options.loglevel = level }
|
114
|
+
opts.on('-F', '--logfile LOG') { |log| @options.logfile = log }
|
115
|
+
opts.on('-D', '--dry-run') { @options.dry_run = true }
|
116
|
+
|
117
|
+
opts.parse!(@arguments) rescue return false
|
118
|
+
|
119
|
+
process_options
|
120
|
+
true
|
121
|
+
end
|
122
|
+
|
123
|
+
# Build variable hash from command line option
|
124
|
+
def var_hash(var)
|
125
|
+
parts = var.split('::')
|
126
|
+
key = value = ''
|
127
|
+
key_built = false
|
128
|
+
parts.each_index do |idx|
|
129
|
+
part = parts[idx]
|
130
|
+
if key_built
|
131
|
+
value = value + ':' + (part || '')
|
132
|
+
else
|
133
|
+
if part.include?(':')
|
134
|
+
subparts = part.split(':')
|
135
|
+
part = subparts[0]
|
136
|
+
value = subparts[1] || ''
|
137
|
+
key_built = true
|
138
|
+
end
|
139
|
+
key = key + ':' if idx > 0
|
140
|
+
key = key + (part || '')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
{ key => value }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Post-parse processing of options
|
147
|
+
def process_options
|
148
|
+
@options.verbose = false if @options.quiet
|
149
|
+
@options.loglevel.downcase!
|
150
|
+
@options.target = ARGV[0] unless ARGV.empty? # We'll catch that later
|
151
|
+
end
|
152
|
+
|
153
|
+
# Check arguments
|
154
|
+
def arguments_valid?
|
155
|
+
valid = ARGV.size == 1
|
156
|
+
if valid
|
157
|
+
unless %w{ debug info warn error }.include? @options.loglevel
|
158
|
+
Log.error "Invalid log level '#{@options.loglevel}'"
|
159
|
+
valid = false
|
160
|
+
end
|
161
|
+
unless File.directory?(@options.schemasdir)
|
162
|
+
Log.error "Non-existent schemas directory '#{@options.schemasdir}'"
|
163
|
+
valid = false
|
164
|
+
end
|
165
|
+
unless File.exists?(ARGV[0])
|
166
|
+
Log.error "Non-existent target '#{ARGV[0]}'"
|
167
|
+
valid = false
|
168
|
+
end
|
169
|
+
end
|
170
|
+
valid
|
171
|
+
end
|
172
|
+
|
173
|
+
def output_help
|
174
|
+
output_version
|
175
|
+
RDoc::usage_from_file(__FILE__)
|
176
|
+
exit 0
|
177
|
+
end
|
178
|
+
|
179
|
+
def output_usage
|
180
|
+
RDoc::usage_from_file(__FILE__, 'usage')
|
181
|
+
exit 0
|
182
|
+
end
|
183
|
+
|
184
|
+
def output_version
|
185
|
+
puts "#{File.basename(__FILE__)} - RightScale Automated API Tester v#{VERSION}\n".blue
|
186
|
+
end
|
187
|
+
|
188
|
+
def run_tests
|
189
|
+
Log.init(@options)
|
190
|
+
opts = "-" * 80 + "\nOptions:"
|
191
|
+
@options.marshal_dump.each do |name, val|
|
192
|
+
opts += "\n #{name} = #{val.inspect}"
|
193
|
+
end
|
194
|
+
Log.info opts
|
195
|
+
engine = Engine.new(@options)
|
196
|
+
engine.run
|
197
|
+
if engine.succeeded?
|
198
|
+
puts engine.summary.dark_blue
|
199
|
+
@return_value = 0
|
200
|
+
else
|
201
|
+
puts engine.summary.dark_red
|
202
|
+
@return_value = 1
|
203
|
+
end
|
204
|
+
unless @options.quiet
|
205
|
+
msg = ""
|
206
|
+
msg << "#{engine.requests_count} API call#{'s' if engine.requests_count > 1}*"
|
207
|
+
if engine.ignored_count > 1
|
208
|
+
msg << "*#{engine.ignored_count} scenario#{'s' if engine.ignored_count > 1} ignored*"
|
209
|
+
end
|
210
|
+
if engine.skipped_count > 1
|
211
|
+
msg << "*#{engine.skipped_count} YAML file#{'s' if engine.skipped_count >1} skipped"
|
212
|
+
end
|
213
|
+
msg.gsub!('**', ', ')
|
214
|
+
msg.delete!('*')
|
215
|
+
puts msg.dark_blue
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Create and run the app
|
222
|
+
app = Resat::App.new(ARGV)
|
223
|
+
app.run
|