resat 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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.
@@ -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
+
@@ -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