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