resat 0.7.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.
- 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
|