resat 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
+
@@ -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
+
@@ -0,0 +1,5 @@
1
+ # This file is here so Resat can be used as a Rails plugin.
2
+ # Use the 'resat.rb' in the root folder to run resat from the command line.
3
+ #
4
+
5
+ require File.join(File.dirname(__FILE__), 'engine')
@@ -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
@@ -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