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/lib/log.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Log info, warnings and errors
|
2
|
+
# See resat.rb for usage information.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
# Add ability to output colored text to console
|
8
|
+
# e.g.: puts "Hello".red
|
9
|
+
class String
|
10
|
+
def bold; colorize("\e[1m\e[29m"); end
|
11
|
+
def grey; colorize("\e[30m"); end
|
12
|
+
def red; colorize("\e[1m\e[31m"); end
|
13
|
+
def dark_red; colorize("\e[31m"); end
|
14
|
+
def green; colorize("\e[1m\e[32m"); end
|
15
|
+
def dark_green; colorize("\e[32m"); end
|
16
|
+
def yellow; colorize("\e[1m\e[33m"); end
|
17
|
+
def blue; colorize("\e[1m\e[34m"); end
|
18
|
+
def dark_blue; colorize("\e[34m"); end
|
19
|
+
def pur; colorize("\e[1m\e[35m"); end
|
20
|
+
def colorize(color_code)
|
21
|
+
# Doesn't work with the Windows prompt...
|
22
|
+
RUBY_PLATFORM =~ /(win|w)32$/ ? to_s : "#{color_code}#{to_s}\e[0m"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Resat
|
27
|
+
|
28
|
+
class LogFormatter
|
29
|
+
|
30
|
+
def call(severity, time, progname, msg)
|
31
|
+
msg.gsub!("\n", "\n ")
|
32
|
+
res = ""
|
33
|
+
res << "*** " if severity == Logger::ERROR || severity == Logger::FATAL
|
34
|
+
res << "#{severity} [#{time.strftime('%H:%M:%S')}]: #{msg.to_s}\n"
|
35
|
+
res
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Log
|
40
|
+
|
41
|
+
LEVELS = %w{ debug info warn error fatal }
|
42
|
+
|
43
|
+
# Initialize singleton instance
|
44
|
+
def Log.init(options)
|
45
|
+
File.delete(options.logfile) rescue nil
|
46
|
+
if options.dry_run
|
47
|
+
options.logfile = STDOUT
|
48
|
+
else
|
49
|
+
options.logfile = 'resat.log' unless File.directory?(File.dirname(options.logfile))
|
50
|
+
end
|
51
|
+
@logger = Logger.new(options.logfile)
|
52
|
+
@logger.formatter = LogFormatter.new
|
53
|
+
@level = LEVELS.index(options.loglevel.downcase) if options.loglevel
|
54
|
+
@level = Logger::WARN unless @level # default to warning
|
55
|
+
@logger.level = @level
|
56
|
+
@verbose = options.verbose
|
57
|
+
@quiet = options.quiet
|
58
|
+
end
|
59
|
+
|
60
|
+
def Log.debug(debug)
|
61
|
+
@logger.debug { debug } if @logger
|
62
|
+
puts "\n#{debug}".grey if @level == Logger::DEBUG
|
63
|
+
end
|
64
|
+
|
65
|
+
def Log.info(info)
|
66
|
+
puts "\n#{info}".dark_green if @verbose
|
67
|
+
@logger.info { info } if @logger
|
68
|
+
end
|
69
|
+
|
70
|
+
def Log.warn(warning)
|
71
|
+
puts "\nWarning: #{warning}".yellow unless @quiet
|
72
|
+
@logger.warn { warning } if @logger
|
73
|
+
end
|
74
|
+
|
75
|
+
def Log.error(error)
|
76
|
+
puts "\nError: #{error}".dark_red
|
77
|
+
@logger.error { error } if @logger
|
78
|
+
end
|
79
|
+
|
80
|
+
def Log.fatal(fatal)
|
81
|
+
puts "\nCrash: #{fatal}".red
|
82
|
+
@logger.fatal { fatal } if @logger
|
83
|
+
end
|
84
|
+
|
85
|
+
def Log.request(request)
|
86
|
+
msg = "REQUEST #{request.method} #{request.path}"
|
87
|
+
if request.size > 0
|
88
|
+
msg << "\nheaders:"
|
89
|
+
request.each_header do |name, value|
|
90
|
+
msg << "\n #{name}: #{value}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
msg << "\nbody: #{request.body}" unless request.body.nil?
|
94
|
+
Log.info(msg)
|
95
|
+
end
|
96
|
+
|
97
|
+
def Log.response(response, succeeded = true)
|
98
|
+
msg = "RESPONSE #{response.code} #{response.message}"
|
99
|
+
if response.size > 0
|
100
|
+
msg << "\nheaders:"
|
101
|
+
response.each_header do |name, value|
|
102
|
+
msg << "\n #{name}: #{value}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
msg << "\nbody: #{response.body}" unless response.body.nil?
|
106
|
+
if succeeded
|
107
|
+
Log.info(msg)
|
108
|
+
else
|
109
|
+
Log.warn(msg)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
data/lib/net_patch.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Patch Net::HTTP so that SSL requests don't output:
|
2
|
+
# warning: peer certificate won't be verified in this SSL session
|
3
|
+
# See resat.rb for usage information.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'net/https'
|
8
|
+
|
9
|
+
module Net
|
10
|
+
class HTTP
|
11
|
+
def warn(*obj)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
data/lib/rdoc_patch.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Patch RDoc so that RDoc::usage works even when the application is started via
|
2
|
+
# a proxy such as a bash script instead of being run directly.
|
3
|
+
# See resat.rb for usage information.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'rdoc/usage'
|
7
|
+
|
8
|
+
module RDoc
|
9
|
+
# Force the use of comments in this file so RDoc::usage works even when
|
10
|
+
# invoked from a proxy (e.g. 'resat' bash script)
|
11
|
+
def usage_no_exit(*args)
|
12
|
+
main_program_file = caller[-1].sub(/:\d+$/, '')
|
13
|
+
usage_from_file(main_program_file)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Display usage from the given file
|
17
|
+
def RDoc.usage_from_file(input_file, *args)
|
18
|
+
comment = File.open(input_file) do |file|
|
19
|
+
find_comment(file)
|
20
|
+
end
|
21
|
+
comment = comment.gsub(/^\s*#/, '')
|
22
|
+
markup = SM::SimpleMarkup.new
|
23
|
+
flow_convertor = SM::ToFlow.new
|
24
|
+
flow = markup.convert(comment, flow_convertor)
|
25
|
+
format = "plain"
|
26
|
+
unless args.empty?
|
27
|
+
flow = extract_sections(flow, args)
|
28
|
+
end
|
29
|
+
options = RI::Options.instance
|
30
|
+
if args = ENV["RI"]
|
31
|
+
options.parse(args.split)
|
32
|
+
end
|
33
|
+
formatter = options.formatter.new(options, "")
|
34
|
+
formatter.display_flow(flow)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
data/lib/resat.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
# Resat test scenario, sequence of api calls and filters.
|
2
|
+
# See resat.rb for usage information.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'kwalify/util/hashlike'
|
6
|
+
require File.join(File.dirname(__FILE__), 'kwalify_helper')
|
7
|
+
require File.join(File.dirname(__FILE__), 'config')
|
8
|
+
require File.join(File.dirname(__FILE__), 'variables')
|
9
|
+
require File.join(File.dirname(__FILE__), 'api_request')
|
10
|
+
require File.join(File.dirname(__FILE__), 'guard')
|
11
|
+
require File.join(File.dirname(__FILE__), 'filter')
|
12
|
+
require File.join(File.dirname(__FILE__), 'handler')
|
13
|
+
|
14
|
+
module Resat
|
15
|
+
|
16
|
+
class ScenarioRunner
|
17
|
+
|
18
|
+
attr_accessor :requests_count, :parser_errors, :failures
|
19
|
+
|
20
|
+
# Instantiate new scenario runner with given YAML definition document and
|
21
|
+
# schemas directory.
|
22
|
+
# If parsing the scenario YAML definition fails then 'valid?' returns false
|
23
|
+
# and 'parser_errors' contains the error messages.
|
24
|
+
def initialize(doc, schemasdir, config, variables, failonerror, dry_run)
|
25
|
+
@schemasdir = schemasdir
|
26
|
+
@valid = true
|
27
|
+
@ignored = false
|
28
|
+
@name = ''
|
29
|
+
@failures = Array.new
|
30
|
+
@requests_count = 0
|
31
|
+
@failonerror = failonerror
|
32
|
+
@dry_run = dry_run
|
33
|
+
parse(doc)
|
34
|
+
if @valid
|
35
|
+
Config.init(config || @cfg_file, schemasdir)
|
36
|
+
@valid = Config.valid?
|
37
|
+
if @valid
|
38
|
+
Variables.reset
|
39
|
+
Variables.load(Config.input, schemasdir) if Config.input && File.readable?(Config.input)
|
40
|
+
Config.variables.each { |v| Variables[v['name']] = v['value'] } if Config.variables
|
41
|
+
variables.each { |k, v| Variables[k] = v } if variables
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def ignored?; @ignored; end
|
47
|
+
def valid?; @valid; end # parser_errors contains the details
|
48
|
+
def succeeded?; @failures.empty?; end
|
49
|
+
|
50
|
+
# Run the scenario.
|
51
|
+
# Once scenario has run check 'succeeded?'.
|
52
|
+
# If 'succeeded?' returns false, use 'failures' to retrieve error messages.
|
53
|
+
def run
|
54
|
+
return if @ignored || !@valid
|
55
|
+
Log.info("-" * 80 + "\nRunning scenario #{@name}")
|
56
|
+
unless Variables.empty?
|
57
|
+
info_msg = Variables.all.inject("Using variables:") do |msg, (k, v)|
|
58
|
+
msg << "\n - #{k}: #{v}"
|
59
|
+
end
|
60
|
+
Log.info(info_msg)
|
61
|
+
end
|
62
|
+
@steps.each_index do |index|
|
63
|
+
@current_step = index
|
64
|
+
@current_file = @steps[index][:origin]
|
65
|
+
step = @steps[index][:step]
|
66
|
+
case step
|
67
|
+
when ApiRequest
|
68
|
+
@requests_count += @request.send_count if @request # Last request
|
69
|
+
@request = step
|
70
|
+
@request.prepare
|
71
|
+
@request.send unless @dry_run
|
72
|
+
when Guard
|
73
|
+
step.prepare
|
74
|
+
step.wait(@request) unless @dry_run
|
75
|
+
when Filter, Handler
|
76
|
+
step.prepare
|
77
|
+
step.run(@request) unless @dry_run
|
78
|
+
end
|
79
|
+
puts step.inspect if step.failures.nil?
|
80
|
+
step.failures.each { |f| add_failure(f) }
|
81
|
+
break if @failonerror && !succeeded? # Abort on failure
|
82
|
+
end
|
83
|
+
|
84
|
+
@requests_count += @request.send_count
|
85
|
+
Variables.save(Config.output) if Config.output
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
# Parse YAML definition file and set 'valid?' and 'parser_errors'
|
91
|
+
# accordingly
|
92
|
+
def parse(doc)
|
93
|
+
parser = KwalifyHelper.new_parser(File.join(@schemasdir, 'scenarios.yaml'))
|
94
|
+
scenario = parser.parse_file(doc)
|
95
|
+
if parser.errors.empty?
|
96
|
+
@ignored = !scenario || scenario.ignore
|
97
|
+
@cfg_file = File.expand_path(File.join(File.dirname(doc), scenario.config)) if scenario.config
|
98
|
+
unless @ignored
|
99
|
+
@name = scenario.name
|
100
|
+
@steps = Array.new
|
101
|
+
scenario.includes.each do |inc|
|
102
|
+
process_include(inc, File.dirname(doc))
|
103
|
+
end if scenario.includes
|
104
|
+
scenario.steps.each do |step|
|
105
|
+
@steps << { :step => step.request, :origin => doc }
|
106
|
+
if step.filters
|
107
|
+
@steps.concat(step.filters.map { |f| { :step => f, :origin => doc } })
|
108
|
+
end
|
109
|
+
if step.handlers
|
110
|
+
@steps.concat(step.handlers.map { |h| { :step => h, :origin => doc } })
|
111
|
+
end
|
112
|
+
if step.guards
|
113
|
+
@steps.concat(step.guards.map { |g| { :step => g, :origin => doc } })
|
114
|
+
end
|
115
|
+
end if scenario.steps
|
116
|
+
end
|
117
|
+
else
|
118
|
+
@valid = false
|
119
|
+
@parser_errors = KwalifyHelper.parser_error(parser)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def process_include(inc, dir)
|
124
|
+
if File.directory?(File.join(dir, inc))
|
125
|
+
includes = FileSet.new(File.join(dir, inc), %{.yml .yaml})
|
126
|
+
else
|
127
|
+
path = find_include(inc, dir)
|
128
|
+
if path
|
129
|
+
includes = [path]
|
130
|
+
else
|
131
|
+
Log.warn("Cannot find include file or directory '#{inc}'")
|
132
|
+
includes = []
|
133
|
+
end
|
134
|
+
end
|
135
|
+
includes.each { |i| include_steps(i) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def include_steps(path)
|
139
|
+
parser = KwalifyHelper.new_parser(File.join(@schemasdir, 'scenarios.yaml'))
|
140
|
+
scenario = parser.parse_file(path)
|
141
|
+
if parser.errors.empty?
|
142
|
+
scenario.includes.each do |inc|
|
143
|
+
process_include(inc, File.dirname(path))
|
144
|
+
end if scenario.includes
|
145
|
+
scenario.steps.each do |step|
|
146
|
+
@steps << { :step => step.request, :origin => path }
|
147
|
+
if step.filters
|
148
|
+
@steps.concat(step.filters.map { |f| { :step => f, :origin => path } })
|
149
|
+
end
|
150
|
+
if step.handlers
|
151
|
+
@steps.concat(step.handlers.map { |h| { :step => h, :origin => path } })
|
152
|
+
end
|
153
|
+
if step.guards
|
154
|
+
@steps.concat(step.guards.map { |g| { :step => g, :origin => path } })
|
155
|
+
end
|
156
|
+
end
|
157
|
+
else
|
158
|
+
Log.error("Cannot include file '#{path}': #{parser.errors.join(", ")}")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Path to include file if it's found, nil otherwise
|
163
|
+
def find_include(inc, dir)
|
164
|
+
# File extension is optional in YAML definition
|
165
|
+
# We'll use the one in the current folder if we can't find it in the same
|
166
|
+
# folder as the including file
|
167
|
+
path = test if File.file?(test = File.join(dir, inc + '.yml'))
|
168
|
+
path = test if File.file?(test = File.join(dir, inc + '.yaml'))
|
169
|
+
path = test if File.file?(test = File.join(dir, inc))
|
170
|
+
return path if path
|
171
|
+
subs = Dir.entries(dir).select { |f| File.directory?(f) }
|
172
|
+
subs = subs - FileSet::IGNORED_FOLDERS
|
173
|
+
subs.detect { |sub| find_include(inc, File.join(dir, sub)) }
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
# Append error message to list of failures
|
178
|
+
def add_failure(failure)
|
179
|
+
@failures << "Step ##{@current_step} from '#{@current_file}': #{failure}"
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
183
|
+
|
184
|
+
# Classes automatically hydrated with Kwalify from YAML definition
|
185
|
+
|
186
|
+
class Scenario
|
187
|
+
include Kwalify::Util::HashLike # defines [], []= and keys?
|
188
|
+
attr_accessor :name, :config, :includes, :steps
|
189
|
+
def ignore; @ignore || false; end
|
190
|
+
end
|
191
|
+
|
192
|
+
class Step
|
193
|
+
include Kwalify::Util::HashLike
|
194
|
+
attr_accessor :request, :filters, :handlers, :guards
|
195
|
+
end
|
196
|
+
|
197
|
+
class CustomOperation
|
198
|
+
include Kwalify::Util::HashLike
|
199
|
+
attr_accessor :name, :type
|
200
|
+
def separator; @separator || "/"; end
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
data/lib/variables.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# Resat scenario variables
|
2
|
+
# Manages variables and provides substitution.
|
3
|
+
# See resat.rb for usage information.
|
4
|
+
#
|
5
|
+
|
6
|
+
require 'singleton'
|
7
|
+
|
8
|
+
module Resat
|
9
|
+
|
10
|
+
class Variables
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
attr_reader :vars, :marked_for_save, :exported
|
14
|
+
|
15
|
+
# Replace occurrences of environment variables in +raw+ with their value
|
16
|
+
def Variables.substitute!(raw)
|
17
|
+
instance().substitute!(raw)
|
18
|
+
end
|
19
|
+
def substitute!(raw)
|
20
|
+
if raw.kind_of?(String)
|
21
|
+
scans = Array.new
|
22
|
+
raw.scan(/[^\$]*\$(\w+)+/) { |scan| scans << scan }
|
23
|
+
scans.each do |scan|
|
24
|
+
scan.each do |var|
|
25
|
+
raw.gsub!('$' + var, @vars[var]) if @vars.include?(var)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
elsif raw.kind_of?(Array)
|
29
|
+
raw.each { |i| substitute!(i) }
|
30
|
+
elsif raw.kind_of?(Hash)
|
31
|
+
raw.each { |k, v| substitute!(v) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def Variables.[](key)
|
36
|
+
instance().vars[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def Variables.[]=(key, value)
|
40
|
+
instance().vars[key] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def Variables.include?(key)
|
44
|
+
instance().vars.include?(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def Variables.empty?
|
48
|
+
instance().vars.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def Variables.all
|
52
|
+
instance().vars.sort
|
53
|
+
end
|
54
|
+
|
55
|
+
def Variables.load(file, schemasdir)
|
56
|
+
schemafile = File.join(schemasdir, 'variables.yaml')
|
57
|
+
schema = Kwalify::Yaml.load_file(schemafile)
|
58
|
+
validator = Kwalify::Validator.new(schema)
|
59
|
+
parser = Kwalify::Yaml::Parser.new(validator)
|
60
|
+
serialized_vars = parser.parse_file(file)
|
61
|
+
parser.errors.push(Kwalify::ValidationError.new("No variables defined")) unless serialized_vars
|
62
|
+
if parser.errors.empty?
|
63
|
+
vars = instance().vars
|
64
|
+
serialized_vars.each { |v| vars[v['name']] = v['value'] }
|
65
|
+
else
|
66
|
+
Log.warn("Error loading variables from '#{file}': #{KwalifyHelper.parser_error(parser)}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def Variables.save(file)
|
71
|
+
serialized_vars = []
|
72
|
+
i = instance()
|
73
|
+
i.vars.each do |k, v|
|
74
|
+
if i.marked_for_save.include?(k)
|
75
|
+
serialized_vars << { 'name' => k, 'value' => v }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
File.open(file, 'w') do |out|
|
79
|
+
YAML.dump(serialized_vars, out)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def Variables.mark_for_save(key)
|
84
|
+
instance().mark_for_save(key)
|
85
|
+
end
|
86
|
+
def mark_for_save(key)
|
87
|
+
@marked_for_save << key
|
88
|
+
end
|
89
|
+
|
90
|
+
# Exported values will be kept even after a call to reset
|
91
|
+
def Variables.export(key)
|
92
|
+
instance().export(key)
|
93
|
+
end
|
94
|
+
def export(key)
|
95
|
+
@exported[key] = @vars[key]
|
96
|
+
end
|
97
|
+
|
98
|
+
def Variables.reset
|
99
|
+
instance().reset
|
100
|
+
end
|
101
|
+
def reset
|
102
|
+
@vars = @exported.clone
|
103
|
+
@marked_for_save = Array.new
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def initialize
|
109
|
+
@exported = Hash.new
|
110
|
+
reset
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|