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.
@@ -0,0 +1,94 @@
1
+ # Configuration information read from given configuration file
2
+ # ('config/resat.yaml' by default).
3
+ #
4
+ # Configuration example:
5
+ #
6
+ # # Hostname used for API calls
7
+ # host: my.rightscale.com
8
+ #
9
+ # # Common base URL to all API calls
10
+ # base_url: '/api/acct/71/'
11
+ #
12
+ # # Use HTTPS?
13
+ # use_ssl: yes
14
+ #
15
+ # # Basic auth username if any
16
+ # username: raphael@rightscale.com
17
+ #
18
+ # # Basic auth password if any
19
+ # password: Secret
20
+ #
21
+ # # Common request headers for all API calls
22
+ # headers:
23
+ # - name: X-API-VERSION
24
+ # value: '1.0'
25
+ #
26
+ # # Common parameters for all API calls
27
+ # params:
28
+ #
29
+ # See resat.rb for usage information.
30
+ #
31
+
32
+ module Resat
33
+
34
+ class Config
35
+
36
+ DEFAULT_FILE = 'config/resat.yaml'
37
+
38
+ DEFAULT_SCHEMA_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..', 'schemas'))
39
+
40
+ DEFAULTS = {
41
+ 'base_url' => '',
42
+ 'use_ssl' => false,
43
+ 'variables' => {}
44
+ }
45
+
46
+ def Config.init(filename, schemasdir)
47
+ (Config.methods - (Object.methods + [ 'init', 'valid?', 'method_missing' ])).each { |m| class << Config;self;end.send :remove_method, m.to_sym }
48
+ schemafile = File.join(schemasdir || DEFAULT_SCHEMA_DIR, 'config.yaml')
49
+ unless File.exists?(schemafile)
50
+ Log.error("Missing configuration file schema '#{schemafile}'")
51
+ @valid = false
52
+ return
53
+ end
54
+ schema = Kwalify::Yaml.load_file(schemafile)
55
+ validator = Kwalify::Validator.new(schema)
56
+ parser = Kwalify::Yaml::Parser.new(validator)
57
+ @valid = true
58
+ @config = { 'use_ssl' => false, 'username' => nil, 'password' => nil, 'port' => nil }
59
+ cfg_file = filename || DEFAULT_FILE
60
+ config = parser.parse_file(cfg_file)
61
+ if parser.errors.empty?
62
+ if config.nil?
63
+ Log.error("Configuration file '#{cfg_file}' is empty.")
64
+ @valid = false
65
+ else
66
+ @config.merge!(config)
67
+ # Dynamically define the methods to forward to the config hash
68
+ @config.each_key do |meth|
69
+ (class << self; self; end).class_eval do
70
+ define_method meth do |*args|
71
+ @config[meth] || DEFAULTS[meth]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ else
77
+ errors = parser.errors.inject("") do |msg, e|
78
+ msg << "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}\n\n"
79
+ end
80
+ Log.error("Configuration file '#{cfg_file}' is invalid:\n#{errors}")
81
+ @valid = false
82
+ end
83
+ end
84
+
85
+ def Config.valid?
86
+ @valid
87
+ end
88
+
89
+ def self.method_missing(*args)
90
+ nil
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,98 @@
1
+ # Resat test engine, reads test files and run them.
2
+ # See resat.rb for usage information.
3
+ #
4
+
5
+ ENG_DIR = File.dirname(__FILE__)
6
+ require File.join(ENG_DIR, 'file_set')
7
+ require File.join(ENG_DIR, 'log')
8
+ require File.join(ENG_DIR, 'scenario_runner')
9
+
10
+ module Resat
11
+
12
+ class Engine
13
+
14
+ attr_accessor :run_count # Total number of run scenarios
15
+ attr_accessor :requests_count # Total number of HTTP requests
16
+ attr_accessor :ignored_count # Total number of ignored scenarios
17
+ attr_accessor :skipped_count # Total number of skipped YAML files
18
+ attr_accessor :failures # Hash of error messages (string arrays)
19
+ # indexed by scenario filename
20
+
21
+ def initialize(options)
22
+ @options = options
23
+ @failures = Hash.new
24
+ @run_count = 0
25
+ @ignored_count = 0
26
+ @requests_count = 0
27
+ @skipped_count = 0
28
+ end
29
+
30
+ # Was test run successful?
31
+ def succeeded?
32
+ @failures.size == 0
33
+ end
34
+
35
+ # Run all scenarios and set attributes accordingly
36
+ def run(target=nil)
37
+ target ||= @options.target
38
+ begin
39
+ if File.directory?(target)
40
+ files = FileSet.new(target, %w{.yml .yaml})
41
+ elsif File.file?(target)
42
+ files = [target]
43
+ else
44
+ @failures[target] << "Invalid taget #{target}: Not a directory, nor a file"
45
+ return
46
+ end
47
+ @failures[target] ||= []
48
+ schemasdir = @options.schemasdir || Config::DEFAULT_SCHEMA_DIR
49
+ files.each do |file|
50
+ runner = ScenarioRunner.new(file, schemasdir, @options.config,
51
+ @options.variables, @options.failonerror, @options.dry_run)
52
+ @ignored_count += 1 if runner.ignored?
53
+ @skipped_count += 1 unless runner.valid?
54
+ if runner.valid? && !runner.ignored?
55
+ runner.run
56
+ @run_count += 1
57
+ @requests_count += runner.requests_count
58
+ @failures[file] = runner.failures unless runner.failures.empty?
59
+ else
60
+ unless runner.valid?
61
+ Log.info "Skipping '#{file}' (#{runner.parser_errors})"
62
+ end
63
+ Log.info "Ignoring '#{file}'" if runner.ignored?
64
+ end
65
+ end
66
+ rescue Exception => e
67
+ Log.error(e.message)
68
+ backtrace = " " + e.backtrace.inject("") { |msg, s| msg << "#{s}\n" }
69
+ Log.debug(backtrace)
70
+ end
71
+ end
72
+
73
+ def summary
74
+ if succeeded?
75
+ case run_count
76
+ when 0 then res = "\nNo scenario to run."
77
+ when 1 then res = "\nOne scenario SUCCEEDED"
78
+ else res = "\n#{run_count} scenarios SUCCEEDED"
79
+ end
80
+ else
81
+ i = 1
82
+ res = "\nErrors summary:\n"
83
+ failures.each do |file, errors|
84
+ res << "\n#{i.to_s}) Scenario '#{file}' failed with: "
85
+ errors.each do |error|
86
+ res << "\n "
87
+ res << error
88
+ end
89
+ i = i + 1
90
+ end
91
+ res << "\n\n#{i - 1} of #{run_count} scenario#{'s' if run_count > 1} FAILED"
92
+ end
93
+ res
94
+ end
95
+
96
+ end
97
+ end
98
+
@@ -0,0 +1,33 @@
1
+ # Set of files with given extension in given directory
2
+ # See resat.rb for usage information.
3
+ #
4
+
5
+ module Resat
6
+ class FileSet < Array
7
+
8
+ # Folders that won't be scanned for files
9
+ IGNORED_FOLDERS = %w{ . .. .svn .git }
10
+
11
+ # Initialize with all file names found in 'dir' and its sub-directories
12
+ # with given file extensions
13
+ def initialize(dir, extensions)
14
+ super(0)
15
+ concat(FileSet.gather_files(dir, extensions))
16
+ end
17
+
18
+ def self.gather_files(dir, extensions)
19
+ files = Array.new
20
+ Dir.foreach(dir) do |filename|
21
+ if File.directory?(filename)
22
+ unless IGNORED_FOLDERS.include?(filename)
23
+ files.concat(FileSet.gather_files(filename, extensions))
24
+ end
25
+ elsif extensions.include?(File.extname(filename))
26
+ files << File.join(dir, filename)
27
+ end
28
+ end
29
+ files
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,113 @@
1
+ # Resat response filter
2
+ # Use response filters to validate responses and/or store response elements in
3
+ # variables.
4
+ # Automatically hydrated with Kwalify from YAML definition.
5
+ # See resat.rb for usage information.
6
+ #
7
+
8
+ module Resat
9
+
10
+ class Filter
11
+ include Kwalify::Util::HashLike
12
+ attr_accessor :failures
13
+
14
+ def prepare
15
+ @is_empty ||= false
16
+ @failures = []
17
+ Log.info("Running filter '#{@name}'")
18
+ end
19
+
20
+ # Run filter on given response
21
+ def run(request)
22
+ @request = request
23
+ @response = request.response
24
+ validate
25
+ extract
26
+ end
27
+
28
+ # Validate response
29
+ def validate
30
+ unless @response
31
+ @failures << "No response to validate."
32
+ return
33
+ end
34
+
35
+ # 1. Check emptyness
36
+ if @target == 'header'
37
+ if @is_empty != (@response.size == 0)
38
+ @failures << "Response header #{'not ' if @is_empty}empty."
39
+ end
40
+ else
41
+ if @is_empty != (@response.body.nil? || @response.body.size <= 1)
42
+ @failures << "Response body #{'not ' if @is_empty}empty."
43
+ end
44
+ end
45
+
46
+ # 2. Check required fields
47
+ @required_fields.each do |field|
48
+ unless @request.has_response_field?(field, target)
49
+ @failures << "Missing #{target} field '#{field}'."
50
+ end
51
+ end if @required_fields
52
+
53
+ # 3. Evaluate validators
54
+ @validators.each do |v|
55
+ if @request.has_response_field?(v.field, @target)
56
+ field = @request.get_response_field(v.field, @target)
57
+ if v.pattern
58
+ Variables.substitute!(v.pattern)
59
+ unless Regexp.new(v.pattern).match(field)
60
+ @failures << "Validator /#{v.pattern} failed on '#{field}' from #{@target} field '#{v.field}'."
61
+ end
62
+ end
63
+ unless !!v.is_empty == field.empty?
64
+ @failures << "#{@target.capitalize} field '#{v.field}' #{'not ' if v.is_empty}empty."
65
+ end
66
+ else
67
+ @failures << "Missing #{@target} field '#{v.field}'."
68
+ end
69
+ end if @validators
70
+ end
71
+
72
+ # Extract elements from response
73
+ def extract
74
+ @extractors.each do |ex|
75
+ Variables.substitute!(ex.field)
76
+ if @request.has_response_field?(ex.field, @target)
77
+ field = @request.get_response_field(ex.field, @target)
78
+ if ex.pattern
79
+ Variables.substitute!(ex.pattern)
80
+ Regexp.new(ex.pattern).match(field)
81
+ if Regexp.last_match
82
+ Variables[ex.variable] = Regexp.last_match(1)
83
+ else
84
+ Log.warn("Extraction from response #{@target} field '#{ex.field}' ('#{field}') with pattern '#{ex.pattern}' failed.")
85
+ end
86
+ else
87
+ Variables[ex.variable] = field
88
+ end
89
+ Variables.mark_for_save(ex.variable) if ex.save
90
+ Variables.export(ex.variable) if ex.export
91
+ else
92
+ Log.warn("Extraction from response #{@target} field '#{ex.field}' failed: field not found.")
93
+ end
94
+ end if @extractors
95
+ end
96
+
97
+ end
98
+
99
+ # Classes for instances hydrated by Kwalify
100
+
101
+ class Validator
102
+ include Kwalify::Util::HashLike
103
+ attr_accessor :field, :is_empty, :pattern
104
+ end
105
+
106
+ class Extractor
107
+ include Kwalify::Util::HashLike
108
+ attr_accessor :field, :pattern, :variable
109
+ def save; @save || false; end
110
+ def export; @export || false; end
111
+ end
112
+
113
+ end
@@ -0,0 +1,36 @@
1
+ # Resat response filter
2
+ # Use response filters to validate responses and/or store response elements in
3
+ # variables.
4
+ # Automatically hydrated with Kwalify from YAML definition.
5
+ # See resat.rb for usage information.
6
+ #
7
+
8
+ module Resat
9
+
10
+ class Guard
11
+ include Kwalify::Util::HashLike
12
+ attr_accessor :failures
13
+
14
+ def prepare
15
+ @timeout ||= 120
16
+ @period ||= 5
17
+ @failures = []
18
+ Variables.substitute!(@pattern)
19
+ Log.info("Waiting for guard #{@name} with pattern /#{@pattern.to_s}/")
20
+ end
21
+
22
+ def wait(request)
23
+ r = Regexp.new(@pattern)
24
+ r.match(request.get_response_field(@field, @target))
25
+ expiration = DateTime.now + @timeout
26
+ while !Regexp.last_match && DateTime.now < expiration && request.failures.empty?
27
+ sleep @period
28
+ request.send
29
+ r.match(request.get_response_field(@field, @target))
30
+ end
31
+ @failures << "Guard '#{@name}' timed out waiting for field '#{@field}' with pattern '#{@pattern ? @pattern : '<NONE>'}' from response #{@target}." if !Regexp.last_match
32
+ end
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,50 @@
1
+ # Resat response handler
2
+ # Allows defining a handler that gets the request and response objects for custom processing
3
+ # Handler should be a module and expose the following methods:
4
+ # - :process takes two argumnents: first argument is an instance of Net::HTTPRequest while
5
+ # second argument is an instance of Net::HTTPResponse.
6
+ # :process should initialize the value returned by :failures (see below)
7
+ # - :failures which returns an array of error messages when processing
8
+ # the response results in errors or an empty array when the processing
9
+ # is successful.
10
+
11
+ module Resat
12
+
13
+ class Handler
14
+ include Kwalify::Util::HashLike
15
+ attr_accessor :failures
16
+
17
+ def prepare
18
+ @failures = []
19
+ Log.info("Running handler '#{@name}'")
20
+ end
21
+
22
+ def run(request)
23
+ klass = module_class(@module)
24
+ h = klass.new
25
+ h.process(request.request, request.response)
26
+ @failures += h.failures.values.to_a if h.failures
27
+ end
28
+
29
+ protected
30
+
31
+ # Create and cache instance of Class which includes
32
+ # given module.
33
+ def module_class(mod)
34
+ @@modules ||= {}
35
+ unless klass = @@modules[mod]
36
+ klass = Class.new(Object) { include Handler.get_module(mod) }
37
+ @@modules[mod] = klass
38
+ end
39
+ klass
40
+ end
41
+
42
+ def self.get_module(name)
43
+ parts = name.split('::')
44
+ index = 0
45
+ res = Kernel
46
+ index += 1 while (index < parts.size) && (res = res.const_get(parts[index]))
47
+ res
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ # Helper methods that wrap common Kwalify use case
2
+ #
3
+
4
+ require 'kwalify'
5
+
6
+ module Resat
7
+
8
+ class KwalifyHelper
9
+
10
+ # Create new parser from given schema file
11
+ def KwalifyHelper.new_parser(schema_file)
12
+ schema = Kwalify::Yaml.load_file(schema_file)
13
+ validator = Kwalify::Validator.new(schema)
14
+ res = Kwalify::Yaml::Parser.new(validator)
15
+ res.data_binding = true
16
+ res
17
+ end
18
+
19
+ # Format error message from parser errors
20
+ def KwalifyHelper.parser_error(parser)
21
+ first = true
22
+ parser.errors.inject("") do |msg, e|
23
+ msg << "\n" unless first
24
+ first = false if first
25
+ msg << "#{e.linenum}:#{e.column} [#{e.path}] #{e.message}"
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end