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 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