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