restest 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "shoulda", ">= 0"
10
+ gem "rdoc", "~> 3.12"
11
+ gem "bundler", ">= 1.0.0"
12
+ gem "jeweler", "~> 1.8.4"
13
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Peter Salas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ = restest
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to restest
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2012 Peter Salas. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "restest"
18
+ gem.homepage = "http://github.com/gradeawarrior/restest"
19
+ gem.license = "MIT"
20
+ gem.summary = "RESTful API Testing Framework"
21
+ gem.description = "A Ruby framework for testing RESTful API's"
22
+ gem.email = "psalas@proofpoint.com"
23
+ gem.authors = ["Jyri Virki", "Peter Salas"]
24
+ gem.executables << 'restest'
25
+ # dependencies defined in Gemfile
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rake/testtask'
30
+ Rake::TestTask.new(:test) do |test|
31
+ test.libs << 'lib' << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+
36
+ task :default => :test
37
+
38
+ require 'rdoc/task'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "restest #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'restest'
@@ -0,0 +1,16 @@
1
+
2
+ class MySQLReport
3
+
4
+ def initialize(file)
5
+
6
+ end
7
+
8
+ def log(status, duration, service, api, name, message)
9
+
10
+ end
11
+
12
+ def done(tests_ok, tests_fail)
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,93 @@
1
+
2
+ #
3
+ # The Result class wraps a HTTP Response object and provides some test framework-specific additional result
4
+ # tracking.
5
+ #
6
+ # The ServiceAPI#do_request* methods returns this Result object for each test.
7
+ #
8
+ class Result
9
+
10
+ attr_reader :response
11
+ attr_reader :is_ok
12
+ attr_reader :message
13
+ attr_accessor :allow_retry # if false, do not retry test even if test file requests retries
14
+ attr_accessor :abort_suite_run # if true, framework will end test suite run immediately
15
+
16
+ #-------------------------------------------------------------------------------------------------------------------
17
+ # Constructor.
18
+ #
19
+ def initialize(response)
20
+ @response = response
21
+ @is_ok = true
22
+ @allow_retry = true
23
+ @message = ""
24
+ end
25
+
26
+ #-------------------------------------------------------------------------------------------------------------------
27
+ # Fatal error factory. A service module should return a fatal_error when it encounters a problem which cannot
28
+ # change by re-trying the same test again. The most common case is if a required parameter is missing.
29
+ #
30
+ def self.fatal_error(message)
31
+ res = Result.new(nil)
32
+ res.error(message)
33
+ res.allow_retry = false
34
+ return res
35
+ end
36
+
37
+ #-------------------------------------------------------------------------------------------------------------------
38
+ # Constructs a Result object which tells the framework to abort the current test suite.
39
+ #
40
+ # A service module should generally never return this. Avoid it! Even if a particular misconfiguration is fatal
41
+ # for a certain subset of tests, it is possible the suite may have many other tests which can still run.
42
+ #
43
+ # It is provided for cases where a service module detects a misconfiguration so fatal that there truly is no point
44
+ # in attempting to run any more tests because you know for sure they will all fail.
45
+ #
46
+ def self.abort_suite(message)
47
+ res = Result.new(nil)
48
+ res.error(message)
49
+ res.allow_retry = false
50
+ res.abort_suite_run = true
51
+ return res
52
+ end
53
+
54
+ #-------------------------------------------------------------------------------------------------------------------
55
+ # Flags this Result as erroneous.
56
+ # After one call to this method, is_ok will return false.
57
+ # A descriptive message string needs to be provided, explaining why the result is erroneous.
58
+ # This method can be invoked multiple times, if multiple errors are noticed on the response.
59
+ # The message(s) will be available via the message method.
60
+ #
61
+ def error(msg)
62
+ @is_ok = false
63
+ add_message(msg)
64
+ end
65
+
66
+ #-------------------------------------------------------------------------------------------------------------------
67
+ # Adds a message sentence to the error message line.
68
+ # Typically callers should invoke error() above.
69
+ #
70
+ def add_message(msg)
71
+ @message = @message + msg
72
+ if (@message[-1,1] != " ")
73
+ @message = @message + " "
74
+ end
75
+ end
76
+
77
+ #-------------------------------------------------------------------------------------------------------------------
78
+ # Returns the HTTP response code sent by server (as a String)
79
+ #
80
+ def code
81
+ return (-1) if @response == nil
82
+ return @response.code
83
+ end
84
+
85
+ #-------------------------------------------------------------------------------------------------------------------
86
+ # Returns the HTTP response body sent by server.
87
+ #
88
+ def body
89
+ return "" if @response == nil
90
+ return @response.body
91
+ end
92
+
93
+ end
@@ -0,0 +1,178 @@
1
+
2
+ #
3
+ # This is the base class for all service module classes.
4
+ #
5
+
6
+ require 'net/http'
7
+ require 'json'
8
+ require 'Result'
9
+
10
+ class ServiceAPI
11
+
12
+ #-------------------------------------------------------------------------------------------------------------------
13
+ # Returns a string which can be eval'd to set a variable or return an error.
14
+ #
15
+ # A very common pattern in service module methods is to attempt to get a value from state or return
16
+ # a fatal error Result if it is missing. This convenience method allows reducing the code segment:
17
+ #
18
+ # xyz = state.get('xyz')
19
+ # if (xyz == nil)
20
+ # return Result.fatal_error("xyz required")
21
+ # end
22
+ #
23
+ # to this:
24
+ #
25
+ # xyz = ""; eval(need :xyz, state)
26
+ #
27
+ # (If only ruby has LISP macros, this could be so much cleaner...)
28
+ #
29
+ def need(symbol, state)
30
+ x = state.get(symbol.to_s)
31
+ if (x == nil)
32
+ return "return Result.fatal_error(\"#{symbol.to_s} required\")"
33
+ end
34
+ return "#{symbol.to_s} = state.get('#{symbol.to_s}')"
35
+ end
36
+
37
+ #-------------------------------------------------------------------------------------------------------------------
38
+ # Generate a random UUID.
39
+ #
40
+ # Attempts a few different ways, hopefully one of them work. If all fails, die.
41
+ #
42
+ def uuid
43
+ begin
44
+ require 'securerandom'
45
+ uuid = SecureRandom.uuid()
46
+
47
+ rescue Exception => e
48
+ if (File.exist?("/usr/bin/uuidgen")) # Centos e2fsprogs package
49
+ uuid = `/usr/bin/uuidgen`
50
+ return uuid.chomp
51
+
52
+ elsif (File.exist?("/usr/bin/uuid")) # Debian uuid package
53
+ uuid = `/usr/bin/uuid`
54
+ return uuid.chomp
55
+
56
+ else
57
+ die("Unable to generate UUIDs")
58
+ end
59
+ end
60
+ end
61
+
62
+ #-------------------------------------------------------------------------------------------------------------------
63
+ # Convenience method to build a string of HTTP query parameters.
64
+ #
65
+ def add_param(params, param)
66
+ if (param == nil || param.length() == 0)
67
+ return params
68
+ end
69
+ if (params == nil || params.length() == 0)
70
+ return "?#{param}"
71
+ end
72
+ return "#{params}&#{param}"
73
+ end
74
+
75
+ #-------------------------------------------------------------------------------------------------------------------
76
+ # Show details about request being sent.
77
+ #
78
+ def show_req(request)
79
+ if ($LOG_LEVEL >= 2)
80
+ puts "---[ Request ]----------------------------------------------------"
81
+ puts "#{request.method} #{request.path}"
82
+ request.each_header { |name,value|
83
+ puts "#{name}: #{value}"
84
+ }
85
+ puts "\n#{request.body}\n"
86
+ puts "------------------------------------------------------------------"
87
+ end
88
+ end
89
+
90
+ #-------------------------------------------------------------------------------------------------------------------
91
+ # Show details about response received.
92
+ #
93
+ def show_response(response)
94
+ if ($LOG_LEVEL >= 2)
95
+ puts "---[ Response ]---------------------------------------------------"
96
+ puts "#{response.code} #{response.message}"
97
+ response.each_header { |name,value|
98
+ puts "#{name}: #{value}"
99
+ }
100
+ puts "\n#{response.body}\n"
101
+ puts "------------------------------------------------------------------"
102
+ end
103
+ end
104
+
105
+ #-------------------------------------------------------------------------------------------------------------------
106
+ # Utility function to perform raw HTTP request.
107
+ #
108
+ # If url is not provided, will attempt to use generic 'server' and 'server_port' values from state.
109
+ #
110
+ def get_http_response(req, state, url)
111
+
112
+ # TODO: SSL support
113
+
114
+ auth = state.get('auth')
115
+ if (auth == "basic")
116
+ user = state.get('auth_user')
117
+ password = state.get('auth_password')
118
+ req.basic_auth(user, password)
119
+ end
120
+
121
+ if (url == nil)
122
+ server = state.get('server')
123
+ port = state.get('server_port')
124
+ die("no server specified") if (server == nil)
125
+ die("no server port specified") if (port == nil)
126
+ url = "http://#{server}:#{port}"
127
+ end
128
+
129
+ begin
130
+ urlobj = URI.parse(url)
131
+ http = Net::HTTP.new(urlobj.host, urlobj.port)
132
+ out(1, "Connecting to: #{urlobj.to_s}")
133
+ response = http.request(req)
134
+
135
+ rescue Exception => e
136
+ return Result.fatal_error("Failure connecting to server #{server}: #{e.to_s}")
137
+ end
138
+
139
+ return response
140
+ end
141
+
142
+ #-------------------------------------------------------------------------------------------------------------------
143
+ # Utility function to perform HTTP request.
144
+ # This is the method API subclasses should usually invoke when Discovery is available.
145
+ # It will connect to a service of the given type.
146
+ # It will log output and return a Result object.
147
+ #
148
+ def do_request_by_service(req, state, service)
149
+ service_list = state.get('service_list')
150
+ if (service_list == nil)
151
+ return Result.fatal_error("service_list not available in state!")
152
+ end
153
+
154
+ if (service_list[service] == nil)
155
+ return Result.fatal_error("No service of type #{service} available!")
156
+ end
157
+
158
+ url = service_list[service][0]
159
+ return do_request_by_url(req, state, url)
160
+ end
161
+
162
+ #-------------------------------------------------------------------------------------------------------------------
163
+ # Utility function to perform HTTP request.
164
+ # API subclasses can invoke this to connect to a specific host/port.
165
+ # It will log output and return a Result object.
166
+ #
167
+ def do_request_by_url(req, state, url)
168
+ show_req(req)
169
+ response = get_http_response(req, state, url)
170
+ show_response(response)
171
+
172
+ state.set_in_test('http_code', response.code)
173
+ result = Result.new(response)
174
+
175
+ return result
176
+ end
177
+
178
+ end
@@ -0,0 +1,26 @@
1
+
2
+ #
3
+ # Appends colon-separed results to a report file.
4
+ #
5
+
6
+ class SimpleFileReport
7
+
8
+ def initialize(file)
9
+ @file = file
10
+ end
11
+
12
+ def log(status, duration, service, api, name, message)
13
+ line = "#{Time.now.to_i}:#{status}:#{duration}:#{service}:#{api}:#{name}:#{message}"
14
+ file = File.new(@file, "a")
15
+ file.puts(line)
16
+ file.close()
17
+ end
18
+
19
+ def done(tests_ok, tests_fail)
20
+ line = "#{Time.now.to_i}:DONE:#{tests_ok}:#{tests_fail}"
21
+ file = File.new(@file, "a")
22
+ file.puts(line)
23
+ file.close()
24
+ end
25
+
26
+ end
@@ -0,0 +1,182 @@
1
+ #
2
+ # The State class is a container for both global and per-test state.
3
+ # The test framework keeps one global State object (in $GLOBAL_STATE).
4
+ # A per-test State object is created for each test. The per-test object
5
+ # inherits all the global state but can override values if needed.
6
+ # Values set in the per-test class are not persisted beyond the lifetime
7
+ # of a single test.
8
+ #
9
+ class State
10
+
11
+ attr_accessor :vars
12
+
13
+ #-------------------------------------------------------------------------------------------------------------------
14
+ # Constructor. If baseobj is nil, a top-level State object is created. Otherwise, a child (per-test) State
15
+ # object is created.
16
+ #
17
+ def initialize(baseobj = nil)
18
+ @vars = Hash.new()
19
+ @validations = Hash.new()
20
+
21
+ @headers = Hash.new
22
+ @headers['Content-Type'] = 'application/json'
23
+ @headers['User-Agent'] = 'restest'
24
+
25
+ @baseobj = baseobj
26
+ end
27
+
28
+ #-------------------------------------------------------------------------------------------------------------------
29
+ # Get a named value. Value is returned from either self, if present, or baseobj, if applicable.
30
+ #
31
+ def get(name)
32
+ if (@vars[name] != nil)
33
+ return @vars[name]
34
+ end
35
+
36
+ if (@baseobj != nil)
37
+ return @baseobj.get(name)
38
+ end
39
+
40
+ return nil
41
+ end
42
+
43
+ #-------------------------------------------------------------------------------------------------------------------
44
+ # Set a persistent value. For per-test State objects, this means the value is set in the parent (global) State
45
+ # object so it will persist beyond one test.
46
+ #
47
+ def set(name, value)
48
+ if (@baseobj != nil)
49
+ @baseobj.set(name, value)
50
+ else
51
+ @vars[name] = value
52
+ end
53
+ end
54
+
55
+ #-------------------------------------------------------------------------------------------------------------------
56
+ # Remove a persistent value.
57
+ #
58
+ def unset(name)
59
+ if (@baseobj != nil)
60
+ @baseobj.unset(name)
61
+ else
62
+ @vars.delete(name)
63
+ end
64
+ end
65
+
66
+ #-------------------------------------------------------------------------------------------------------------------
67
+ # Set a non-persistent value in a per-test State object. Calling this method on the global State is not allowed.
68
+ #
69
+ def set_in_test(name, value)
70
+ if (@baseobj != nil)
71
+ @vars[name] = value
72
+ else
73
+ die("cannot set_in_test for top-level State (name=#{name}, value=#{value})")
74
+ end
75
+ end
76
+
77
+ #-------------------------------------------------------------------------------------------------------------------
78
+ # Return HTTP headers for request. A minimal default set of headers is returned.
79
+ #
80
+ # If a mix Hash is provided, its elements are merged to the default headers (headers in 'mix' override the defaults
81
+ # if both are present). This allows per-test header customization.
82
+ #
83
+ def headers(mix = nil)
84
+ if (mix != nil)
85
+ return @headers.merge(mix)
86
+ else
87
+ return @headers
88
+ end
89
+ end
90
+
91
+ #-------------------------------------------------------------------------------------------------------------------
92
+ # Record a per-test validation (loaded from test file). Calling this method on the global State is not allowed.
93
+ #
94
+ def set_validate(name, value)
95
+ if (@baseobj != nil)
96
+ @validations[name] =value
97
+ else
98
+ die("cannot set_validate for top-level State (name=#{name}, value=#{value})")
99
+ end
100
+ end
101
+
102
+ #-------------------------------------------------------------------------------------------------------------------
103
+ # Validates all the 'validate' conditions for the current test.
104
+ # Calling this method on the global State is not allowed.
105
+ # Validating verifies that all entries set with set_validate have a matching value in test state.
106
+ # If any one or more fail, the result is marked as erroneous.
107
+ #
108
+ def validate(result)
109
+ @validations.each_key { |key|
110
+ failed = false
111
+ val = get(key)
112
+ wanted = @validations[key]
113
+ wanted =~ /(\S+) (.*)/
114
+ op = $1
115
+ target = $2
116
+ out(1, "validating: [#{key}]: want: [#{op} #{target}], got [#{val}]")
117
+ if (op == "=")
118
+ failed = true if (target != val.to_s)
119
+
120
+ elsif (op == "<")
121
+ failed = true if (val.to_i >= target.to_i)
122
+
123
+ elsif (op == ">")
124
+ failed = true if (val.to_i <= target.to_i)
125
+
126
+ elsif (op == "is")
127
+ if (target == "set")
128
+ failed = true if (val == nil || val.length == 0)
129
+ elsif (target == "unset")
130
+ failed = true if (val != nil && val.length > 0)
131
+ else
132
+ die("unknown directive for operation 'is': #{target}")
133
+ end
134
+
135
+ else
136
+ die("unknown validate operation #{op}")
137
+ end
138
+
139
+ if (failed)
140
+ result.error("For [#{key}] expected [#{op} #{target}] but got [#{val}].")
141
+ $VALIDATIONS_FAIL += 1
142
+ else
143
+ $VALIDATIONS_OK += 1
144
+ end
145
+ }
146
+
147
+ # This is Proofpoint specific and should not be in this class. There's not a good place to put it right
148
+ # now that doesn't involve copying multiple times, so put it here for now.
149
+ if (result.code == "500")
150
+ if (result.body != nil)
151
+ begin
152
+ json = JSON.parse(result.body)
153
+ if (json != nil && json['host'] != nil && json['message'] != nil)
154
+ result.add_message("Server: [#{json['host']}] said: [#{json['message']}])")
155
+ end
156
+ rescue
157
+ end
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ #-------------------------------------------------------------------------------------------------------------------
164
+ # To string...
165
+ #
166
+ def to_s
167
+ if (@baseobj == nil)
168
+ s = "Base State: "
169
+ @vars.each_key { |key|
170
+ s += "#{key}='#{@vars[key]}' "
171
+ }
172
+ else
173
+ s = "Test State: "
174
+ @vars.each_key { |key|
175
+ s += "#{key}='#{@vars[key]}' "
176
+ }
177
+ s += "(" + @baseobj.to_s + ")"
178
+ end
179
+ return s
180
+ end
181
+
182
+ end
@@ -0,0 +1,384 @@
1
+
2
+ #
3
+ # A test suite file must require this file, it provides the main entry point to the test harness.
4
+ #
5
+ # - Commands available in the test DSL are defined here.
6
+ # - Initialization is done here.
7
+ #
8
+
9
+ require 'State'
10
+ require 'optparse'
11
+
12
+ $GLOBAL_STATE = State.new()
13
+ $LOG_LEVEL = 0
14
+ $TESTS_OK = 0
15
+ $TESTS_FAIL = 0
16
+ $VALIDATIONS_OK = 0
17
+ $VALIDATIONS_FAIL = 0
18
+ $CONFIG_FILE=nil
19
+ $REPORT = nil
20
+ $TEST_FILES = Hash.new()
21
+ $INTERACTIVE = false
22
+ $SAVED_STATES = Hash.new()
23
+
24
+ #---------------------------------------------------------------------------------------------------------------------
25
+ # Add a to_bool() method to String
26
+ #
27
+ class String
28
+ def to_bool
29
+ return true if self == "true"
30
+ false
31
+ end
32
+ end
33
+
34
+ #---------------------------------------------------------------------------------------------------------------------
35
+ # Utility method to show a line of output and exit. Use when a fatal misconfiguration or error is seen.
36
+ #
37
+ def die(line)
38
+ puts "[ERROR] #{line}"
39
+ exit(1)
40
+ end
41
+
42
+ #---------------------------------------------------------------------------------------------------------------------
43
+ # General purpose log output. Log level is zero by default, increased by one for each -v argument.
44
+ #
45
+ def out(level, line)
46
+ if ($LOG_LEVEL >= level)
47
+ puts line
48
+ end
49
+ end
50
+
51
+ #---------------------------------------------------------------------------------------------------------------------
52
+ # Allow test scripts to get values from the global state.
53
+ #
54
+ def get(name)
55
+ return $GLOBAL_STATE.get(name)
56
+ end
57
+
58
+ #---------------------------------------------------------------------------------------------------------------------
59
+ # Allow test scripts to set values in the global state.
60
+ #
61
+ def set(name, value)
62
+ $GLOBAL_STATE.set(name, value)
63
+ end
64
+
65
+ #---------------------------------------------------------------------------------------------------------------------
66
+ # Allow test scripts to remove values from the global state.
67
+ #
68
+ def unset(name)
69
+ $GLOBAL_STATE.unset(name)
70
+ end
71
+
72
+ #---------------------------------------------------------------------------------------------------------------------
73
+ # Saves the current state.
74
+ #
75
+ def save_state(tag)
76
+ $SAVED_STATES[tag] = $GLOBAL_STATE.vars.clone()
77
+ end
78
+
79
+ #---------------------------------------------------------------------------------------------------------------------
80
+ # Restore global state to a previously saved state.
81
+ #
82
+ def restore_state(tag)
83
+ $GLOBAL_STATE.vars = $SAVED_STATES[tag]
84
+ if ($GLOBAL_STATE.vars == nil)
85
+ die("Attempted to restore to a saved stage [#{tag}] which does not exist!")
86
+ end
87
+ end
88
+
89
+ #---------------------------------------------------------------------------------------------------------------------
90
+ # Print out the difference from the state named by tag_a to the state named by tag_b (these are states previously
91
+ # saved with save_state(). If tag_b is not provided, diff is shown to the current global state.
92
+ #
93
+ def show_state_diff(tag_a, tag_b = nil)
94
+
95
+ a = $SAVED_STATES[tag_a]
96
+ if (tag_b == nil)
97
+ b = $GLOBAL_STATE.vars
98
+ else
99
+ b = $SAVED_STATES[tag_b]
100
+ end
101
+
102
+ if (a == nil || b == nil)
103
+ puts "[ERROR] Unable to show diff from state #{tag_a} to state #{tag_b}"
104
+ return
105
+ end
106
+
107
+ b.each { |k,v|
108
+ if (a[k] == nil)
109
+ puts " ADDED: '#{k}' => '#{v}'"
110
+ elsif (a[k] != v)
111
+ puts " CHANGED: '#{k}' from '#{a[k]}' to '#{v}'"
112
+ end
113
+ }
114
+
115
+ a.each { |k,v|
116
+ if (b[k] == nil)
117
+ puts " REMOVED: '#{k}' was '#{a[k]}'"
118
+ end
119
+ }
120
+ end
121
+
122
+ #---------------------------------------------------------------------------------------------------------------------
123
+ # Run a separate test suite file as part of current test suite.
124
+ # The global state is saved and then restored to isolate state changes made by the included test suite.
125
+ #
126
+ def run_suite(name)
127
+ out(1, "\n\n--Running included test suite #{name}")
128
+ save_state("_pre_run_suite")
129
+ load(name)
130
+ restore_state("_pre_run_suite")
131
+ out(1, "\n--Completed included test suite #{name}")
132
+ end
133
+
134
+ #---------------------------------------------------------------------------------------------------------------------
135
+ # Loads test file
136
+ #
137
+ def load_test(name, state)
138
+ if (!File.exists?("tests/#{name}"))
139
+ die("No test file #{name}")
140
+ end
141
+
142
+ state.set_in_test('filename', name)
143
+ file = File.new("tests/#{name}", "r")
144
+ doc = false
145
+ out(3, "Reading test file tests/#{name}")
146
+
147
+ # line 1: service-class-name api-name
148
+ line = file.gets
149
+ line =~ /(\S*)\s*(\S*)/
150
+ if (!$1 || !$2)
151
+ die("Test #{name} missing service and/or API")
152
+ end
153
+ state.set_in_test('service', $1)
154
+ state.set_in_test('api', $2)
155
+
156
+ # then process 'set' or 'validate' directives
157
+ while ((line = file.gets) != nil)
158
+ next if line =~ /^\s*$/
159
+ next if line =~ /^#/
160
+
161
+ if (line =~ /^set (\S*)\s+=\s+(.*)/)
162
+ state.set_in_test($1, $2)
163
+
164
+ elsif (line =~ /^validate (\S+)\s+(\S+)\s+(.*)/)
165
+ state.set_validate($1, "#{$2} #{$3}")
166
+
167
+ elsif (line =~ /^doc (.*)/)
168
+ state.set_in_test('doc', $1)
169
+ doc = true
170
+
171
+ else
172
+ die("Unknown directive [#{line}] in #{name}")
173
+ end
174
+ end
175
+
176
+ if (!doc)
177
+ die("Test #{name} has no documentation (doc line)")
178
+ end
179
+ end
180
+
181
+ #---------------------------------------------------------------------------------------------------------------------
182
+ # Entry point for running one test. Called from test script by invoking the name of the test file (see method_missing)
183
+ #
184
+ def run_test(name, ignore=false)
185
+ out(1, "\n\n\nRunning test #{name}")
186
+
187
+ if ($LOG_LEVEL > 0)
188
+ save_state('_internal_trace')
189
+ end
190
+
191
+ test_state = State.new($GLOBAL_STATE)
192
+ load_test(name, test_state)
193
+
194
+ service = test_state.get('service')
195
+ api = test_state.get('api')
196
+ out(1, "Service class: [#{service}] Calling API: [#{api}]")
197
+
198
+ if ($INTERACTIVE)
199
+ print "Interactive mode... <enter> to run this test now, <c>ontinue: "
200
+ STDOUT.flush
201
+ input = gets.chomp!
202
+ $INTERACTIVE = false if (input == "c")
203
+ end
204
+
205
+ before = Time.now()
206
+
207
+ allow_retries = test_state.get('allow_retries')
208
+ if (allow_retries != nil)
209
+ times = allow_retries.to_i
210
+ sleep_sec = test_state.get('retry_sleep').to_i
211
+ if (sleep_sec < 1)
212
+ die("allow_retries is set but retry_sleep is not")
213
+ end
214
+ out(1, "Test allow retries: allow_retries: #{times} retry_sleep: #{sleep_sec}")
215
+ begin
216
+ out(1, "Tries left: #{times}")
217
+ test_state = State.new($GLOBAL_STATE)
218
+ load_test(name, test_state)
219
+ testobj = Object::const_get(service).new
220
+ result = testobj.send(api, test_state)
221
+ times -= 1
222
+ sleep(sleep_sec) if !result.is_ok
223
+ end while (times > 0 && !result.is_ok && result.allow_retry)
224
+
225
+ else
226
+ testobj = Object::const_get(service).new
227
+ result = testobj.send(api, test_state)
228
+ end
229
+
230
+ if ($LOG_LEVEL > 0)
231
+ show_state_diff('_internal_trace')
232
+ end
233
+
234
+ duration = Integer((Time.now() - before) * 1000)
235
+
236
+ out(1, "Duration of test: #{duration} ms")
237
+
238
+ prefix = ""
239
+ if (ignore)
240
+ prefix="IGNORE-"
241
+ end
242
+
243
+ if (result.is_ok)
244
+ out(0, "[#{prefix}OK] (#{duration}ms) #{service}.#{api}: #{test_state.get('doc')}")
245
+ $TESTS_OK += 1 if !ignore
246
+ else
247
+ out(0, "[#{prefix}FAIL] (#{duration}ms) #{service}.#{api}: #{test_state.get('doc')} #{result.message}")
248
+ $TESTS_FAIL += 1 if !ignore
249
+ end
250
+
251
+ if ($REPORT != nil)
252
+ if (result.is_ok)
253
+ status = "#{prefix}OK"
254
+ else
255
+ status = "#{prefix}FAIL"
256
+ end
257
+ $REPORT.log(status, duration, service, api, name, result.message)
258
+ end
259
+
260
+ if (result.abort_suite_run)
261
+ die("Test suite run has been aborted by previous test!")
262
+ end
263
+ end
264
+
265
+ #---------------------------------------------------------------------------------------------------------------------
266
+ # Show Usage message
267
+ #
268
+ def show_usage(opts)
269
+ puts opts
270
+ puts <<EOF
271
+
272
+ Running a test suite
273
+ ====================
274
+
275
+ To run a test suite, simply run the test suite file as it is an
276
+ executable ruby script. A configuration file argument must be
277
+ provided.
278
+
279
+ The configuration file is responsible for setting state values which
280
+ are specific to a test environment, and thus not suitable to set
281
+ elsewhere. These include values such as hostnames, user names, test
282
+ domains, etc.
283
+
284
+ (TODO: Until the framework packaging is organized, you need to run it
285
+ from the test suite directory. So for the share tests for example, "cd
286
+ share-tests" first. This is a temporary limitation.)
287
+
288
+ The initial share test script is called "suite1" so run it by:
289
+
290
+ % ./suite1 -c config.ENV
291
+
292
+ (Where ENV is the share environment to run it against. There are
293
+ separate config files for each share env.)
294
+
295
+ EOF
296
+ end
297
+
298
+ #---------------------------------------------------------------------------------------------------------------------
299
+ # Initialization sections below. This file must be included by a test script so this is run before any test entries.
300
+ #
301
+
302
+ # Process command line arguments if any.
303
+ optparse = OptionParser.new do|opts|
304
+ # Set a banner, displayed at the top
305
+ # of the help screen.
306
+ opts.banner = "Usage: YOUR_SCRIPT [options] -c|--config CONFIG_FILE"
307
+
308
+ # Define the options, and what they do
309
+ opts.on( '-c', '--config CONFIG', 'Test config file' ) do|config|
310
+ $CONFIG_FILE = config
311
+ if (File.exists?($CONFIG_FILE))
312
+ load $CONFIG_FILE
313
+ else
314
+ show_usage(opts)
315
+ die("Config file not found")
316
+ end
317
+ end
318
+
319
+ opts.on( '-l', '--log LOG_FILE', 'Log results into "LOG_FILE" in colon-separated text' ) do|file|
320
+ require 'SimpleFileReport'
321
+ $REPORT = SimpleFileReport.new(file)
322
+ end
323
+
324
+ opts.on( '-v', 'Increase verbosity (each -v increases verbosity further)' ) do
325
+ $LOG_LEVEL += 1
326
+ end
327
+
328
+ opts.on( '-q', 'Quiet, no standard output (useful for running from cron or such)' ) do
329
+ $LOG_LEVEL = -100
330
+ end
331
+
332
+ opts.on( '-i', 'Interactive. Runs tests one by one.' ) do
333
+ $INTERACTIVE = true
334
+ end
335
+
336
+ # This displays the help screen, all programs are
337
+ # assumed to have this option.
338
+ opts.on( '-h', '--help', 'Display this screen' ) do
339
+ show_usage(opts)
340
+ exit
341
+ end
342
+ end
343
+
344
+ optparse.parse(ARGV)
345
+
346
+ # Add shutdown hook to report totals.
347
+ at_exit {
348
+ out(0, "Done!")
349
+ out(0, "#{$TESTS_OK} tests passed and #{$TESTS_FAIL} tests failed.")
350
+ out(0, "#{$VALIDATIONS_OK} validations passed and #{$VALIDATIONS_FAIL} validations failed.")
351
+ if ($REPORT != nil)
352
+ $REPORT.done($TESTS_OK, $TESTS_FAIL)
353
+ end
354
+ }
355
+
356
+ # Load names of existing test files
357
+ Dir.foreach("tests") { |file| $TEST_FILES[file] = 1 }
358
+
359
+
360
+ #---------------------------------------------------------------------------------------------------------------------
361
+ # Finally, add missing_method hook to enable test suite DSL to call test cases by their names.
362
+ #
363
+ # Note: In some ruby versions (at least 1.9.0) we can use File.exists? here instead of having to keep the list
364
+ # of test files in $TEST_FILES and checking that. However, in some other ruby versions (at least 1.9.2p290)
365
+ # File.exists? ends up invoking some non-existent methods which trigger a method_missing call, which leads
366
+ # to an infinite loop (until stack space runs out) if method_missing relies on File.exists?.
367
+ #
368
+ def method_missing(method_name, *args)
369
+ if ($TEST_FILES[method_name.to_s])
370
+ run_test(method_name)
371
+ elsif (method_name == :ignore) && $TEST_FILES[args[0].to_s]
372
+ run_test(args[0], true)
373
+ else
374
+ super
375
+ end
376
+ end
377
+
378
+ def respond_to?(method_name, include_private = false)
379
+ if ($TEST_FILES[method_name.to_s])
380
+ return true
381
+ else
382
+ super
383
+ end
384
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'restest'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestRestest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jyri Virki
9
+ - Peter Salas
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-10-03 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoulda
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rdoc
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '3.12'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '3.12'
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 1.0.0
63
+ - !ruby/object:Gem::Dependency
64
+ name: jeweler
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ version: 1.8.4
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: 1.8.4
79
+ description: A Ruby framework for testing RESTful API's
80
+ email: psalas@proofpoint.com
81
+ executables:
82
+ - restest
83
+ extensions: []
84
+ extra_rdoc_files:
85
+ - LICENSE.txt
86
+ - README.rdoc
87
+ files:
88
+ - .document
89
+ - Gemfile
90
+ - LICENSE.txt
91
+ - README.rdoc
92
+ - Rakefile
93
+ - VERSION
94
+ - bin/restest
95
+ - lib/MySQLReport.rb
96
+ - lib/Result.rb
97
+ - lib/ServiceAPI.rb
98
+ - lib/SimpleFileReport.rb
99
+ - lib/State.rb
100
+ - lib/restest.rb
101
+ - test/helper.rb
102
+ - test/test_restest.rb
103
+ homepage: http://github.com/gradeawarrior/restest
104
+ licenses:
105
+ - MIT
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ segments:
117
+ - 0
118
+ hash: 4020511203719455767
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 1.8.24
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: RESTful API Testing Framework
131
+ test_files: []