webspicy 0.1.0.pre.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/LICENSE.md +22 -0
- data/README.md +7 -0
- data/Rakefile +11 -0
- data/examples/restful/Gemfile +5 -0
- data/examples/restful/Gemfile.lock +69 -0
- data/examples/restful/Rakefile +21 -0
- data/examples/restful/app.rb +32 -0
- data/examples/restful/webspicy/schema.fio +4 -0
- data/examples/restful/webspicy/todo/getTodo.yml +52 -0
- data/examples/restful/webspicy/todo/getTodos.yml +39 -0
- data/lib/webspicy/checker.rb +26 -0
- data/lib/webspicy/client/http_client.rb +70 -0
- data/lib/webspicy/client.rb +20 -0
- data/lib/webspicy/configuration.rb +168 -0
- data/lib/webspicy/formaldoc.fio +47 -0
- data/lib/webspicy/resource/service/invocation.rb +158 -0
- data/lib/webspicy/resource/service/test_case.rb +74 -0
- data/lib/webspicy/resource/service.rb +49 -0
- data/lib/webspicy/resource.rb +43 -0
- data/lib/webspicy/scope.rb +113 -0
- data/lib/webspicy/tester/asserter.rb +94 -0
- data/lib/webspicy/tester/assertions.rb +103 -0
- data/lib/webspicy/tester.rb +96 -0
- data/lib/webspicy/version.rb +8 -0
- data/lib/webspicy.rb +112 -0
- data/spec/unit/resource/service/test_dress_params.rb +34 -0
- data/spec/unit/resource/test_instantiate_url.rb +20 -0
- data/spec/unit/resource/test_url_placeholders.rb +16 -0
- data/spec/unit/scope/test_each_resource.rb +59 -0
- data/spec/unit/scope/test_each_service.rb +51 -0
- data/spec/unit/scope/test_to_real_url.rb +75 -0
- data/spec/unit/spec_helper.rb +28 -0
- data/spec/unit/test_configuration.rb +84 -0
- data/spec/unit/tester/test_assertions.rb +108 -0
- data/tasks/test.rake +27 -0
- metadata +149 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Resource
|
3
|
+
class Service
|
4
|
+
class Invocation
|
5
|
+
|
6
|
+
def initialize(service, test_case, response)
|
7
|
+
@service = service
|
8
|
+
@test_case = test_case
|
9
|
+
@response = response
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :service, :test_case, :response
|
13
|
+
|
14
|
+
def done?
|
15
|
+
!response.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_expected_success?
|
19
|
+
test_case.expected_status >= 200 && test_case.expected_status < 300
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_success?
|
23
|
+
response.status.code >= 200 && response.status.code < 300
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_empty_response?
|
27
|
+
response.status.code == 204
|
28
|
+
end
|
29
|
+
|
30
|
+
def is_redirect?
|
31
|
+
response.status.code >= 300 && response.status.code < 400
|
32
|
+
end
|
33
|
+
|
34
|
+
### Check of HTTP status
|
35
|
+
|
36
|
+
def expected_status_unmet
|
37
|
+
expected = test_case.expected_status
|
38
|
+
got = response.status
|
39
|
+
expected == got ? nil : "#{expected} != #{got}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def meets_expected_status?
|
43
|
+
expected_status_unmet.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
### Check of the expected output type
|
47
|
+
|
48
|
+
def expected_content_type_unmet
|
49
|
+
ect = test_case.expected_content_type
|
50
|
+
got = response.content_type.mime_type.to_s
|
51
|
+
ect == got ? nil : "#{ect} != #{got}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def meets_expected_content_type?
|
55
|
+
expected_content_type_unmet.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
### Check of output schema
|
59
|
+
|
60
|
+
def expected_schema_unmet
|
61
|
+
if is_empty_response?
|
62
|
+
body = response.body.to_s.strip
|
63
|
+
body.empty? ? nil : "Expected empty body, got #{body}"
|
64
|
+
elsif is_redirect?
|
65
|
+
else
|
66
|
+
case dressed_body
|
67
|
+
when Finitio::TypeError then dressed_body.root_cause.message
|
68
|
+
when StandardError then dressed_body.message
|
69
|
+
else nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def meets_expected_schema?
|
75
|
+
expected_schema_unmet.nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
### Check of assertions
|
79
|
+
|
80
|
+
def assertions_unmet
|
81
|
+
unmet = []
|
82
|
+
asserter = Tester::Asserter.new(dressed_body)
|
83
|
+
test_case.assert.each do |assert|
|
84
|
+
begin
|
85
|
+
asserter.instance_eval(assert)
|
86
|
+
rescue => ex
|
87
|
+
unmet << ex.message
|
88
|
+
end
|
89
|
+
end
|
90
|
+
unmet.empty? ? nil : unmet.join("\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
def value_equal(exp, got)
|
94
|
+
case exp
|
95
|
+
when Hash
|
96
|
+
exp.all?{|(k,v)|
|
97
|
+
got[k] == v
|
98
|
+
}
|
99
|
+
else
|
100
|
+
exp == got
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
### Check of expected error message
|
105
|
+
|
106
|
+
def expected_error_unmet
|
107
|
+
expected = test_case.expected_error
|
108
|
+
case test_case.expected_content_type
|
109
|
+
when %r{json}
|
110
|
+
got = meets_expected_schema? ? dressed_body[:description] : response.body
|
111
|
+
expected == got ? nil : "`#{expected}` vs. `#{got}`"
|
112
|
+
else
|
113
|
+
dressed_body.include?(expected) ? nil : "#{expected} not found" unless expected.nil?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
### Check of expected headers
|
118
|
+
|
119
|
+
def expected_headers_unmet
|
120
|
+
unmet = []
|
121
|
+
expected = test_case.expected_headers
|
122
|
+
expected.each_pair do |k,v|
|
123
|
+
got = response.headers[k]
|
124
|
+
unmet << "#{v} expected for #{k}, got #{got}" unless (got == v)
|
125
|
+
end
|
126
|
+
unmet.empty? ? nil : unmet.join("\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def loaded_body
|
132
|
+
case test_case.expected_content_type
|
133
|
+
when %r{json}
|
134
|
+
raise "Body empty while expected" if response.body.to_s.empty?
|
135
|
+
@loaded_body ||= ::JSON.parse(response.body)
|
136
|
+
else
|
137
|
+
response.body.to_s
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def dressed_body
|
142
|
+
@dressed_body ||= case test_case.expected_content_type
|
143
|
+
when %r{json}
|
144
|
+
schema = is_expected_success? ? service.output_schema : service.error_schema
|
145
|
+
begin
|
146
|
+
schema.dress(loaded_body)
|
147
|
+
rescue Finitio::TypeError => ex
|
148
|
+
ex
|
149
|
+
end
|
150
|
+
else
|
151
|
+
loaded_body
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end # class Invocation
|
156
|
+
end # class Service
|
157
|
+
end # class Resource
|
158
|
+
end # module Webspicy
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Resource
|
3
|
+
class Service
|
4
|
+
class TestCase
|
5
|
+
|
6
|
+
def initialize(raw)
|
7
|
+
@raw = raw
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.info(raw)
|
11
|
+
new(raw)
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
@raw[:description]
|
16
|
+
end
|
17
|
+
|
18
|
+
def seeds
|
19
|
+
@raw[:seeds]
|
20
|
+
end
|
21
|
+
|
22
|
+
def headers
|
23
|
+
@raw[:headers] || {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def dress_params
|
27
|
+
@raw.fetch(:dress_params){ true }
|
28
|
+
end
|
29
|
+
alias :dress_params? :dress_params
|
30
|
+
|
31
|
+
def params
|
32
|
+
@raw[:params]
|
33
|
+
end
|
34
|
+
|
35
|
+
def expected_content_type
|
36
|
+
@raw[:expected][:content_type] || 'application/json'
|
37
|
+
end
|
38
|
+
|
39
|
+
def expected_status
|
40
|
+
@raw[:expected][:status]
|
41
|
+
end
|
42
|
+
|
43
|
+
def expected_error
|
44
|
+
@raw[:expected][:error]
|
45
|
+
end
|
46
|
+
|
47
|
+
def has_expected_error?
|
48
|
+
!expected_error.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
def expected_headers
|
52
|
+
@raw[:expected][:headers] || {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_expected_headers?
|
56
|
+
!expected_headers.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def assert
|
60
|
+
@raw[:assert] || []
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_assertions?
|
64
|
+
!assert.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_info
|
68
|
+
@raw
|
69
|
+
end
|
70
|
+
|
71
|
+
end # class TestCase
|
72
|
+
end # class Service
|
73
|
+
end # class Resource
|
74
|
+
end # module Webspicy
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Resource
|
3
|
+
class Service
|
4
|
+
|
5
|
+
def initialize(raw)
|
6
|
+
@raw = raw
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.info(raw)
|
10
|
+
new(raw)
|
11
|
+
end
|
12
|
+
|
13
|
+
def method
|
14
|
+
@raw[:method]
|
15
|
+
end
|
16
|
+
|
17
|
+
def examples
|
18
|
+
@raw[:examples]
|
19
|
+
end
|
20
|
+
|
21
|
+
def counterexamples
|
22
|
+
@raw[:counterexamples]
|
23
|
+
end
|
24
|
+
|
25
|
+
def input_schema
|
26
|
+
@raw[:input_schema]
|
27
|
+
end
|
28
|
+
|
29
|
+
def output_schema
|
30
|
+
@raw[:output_schema]
|
31
|
+
end
|
32
|
+
|
33
|
+
def error_schema
|
34
|
+
@raw[:error_schema]
|
35
|
+
end
|
36
|
+
|
37
|
+
def dress_params(params)
|
38
|
+
input_schema.dress(params)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_info
|
42
|
+
@raw
|
43
|
+
end
|
44
|
+
|
45
|
+
end # class Service
|
46
|
+
end # class Resource
|
47
|
+
end # module Webspicy
|
48
|
+
require_relative 'service/test_case'
|
49
|
+
require_relative 'service/invocation'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Resource
|
3
|
+
|
4
|
+
def initialize(raw)
|
5
|
+
@raw = raw
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.info(raw)
|
9
|
+
new(raw)
|
10
|
+
end
|
11
|
+
|
12
|
+
def url
|
13
|
+
@raw[:url]
|
14
|
+
end
|
15
|
+
|
16
|
+
def services
|
17
|
+
@raw[:services]
|
18
|
+
end
|
19
|
+
|
20
|
+
def url_placeholders
|
21
|
+
url.scan(/\{([a-zA-Z]+)\}/).map{|x| x.first.to_sym }
|
22
|
+
end
|
23
|
+
|
24
|
+
def instantiate_url(params)
|
25
|
+
url, rest = self.url, params.dup
|
26
|
+
url_placeholders.each do |placeholder|
|
27
|
+
if (params.has_key?(placeholder))
|
28
|
+
url = url.gsub("{#{placeholder}}", params[placeholder].to_s)
|
29
|
+
rest.delete(placeholder)
|
30
|
+
else
|
31
|
+
raise "Missing URL parameter `#{placeholder}`\n\t(#{params.inspect})"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
[ url, rest ]
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_info
|
38
|
+
@raw
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
require_relative 'resource/service'
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Scope
|
3
|
+
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
end
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
###
|
10
|
+
### Eachers -- Allow navigating the web service definitions
|
11
|
+
###
|
12
|
+
|
13
|
+
# Yields each resource file in the current scope
|
14
|
+
def each_resource_file(&bl)
|
15
|
+
return enum_for(:each_resource_file) unless block_given?
|
16
|
+
config.folders.each do |folder|
|
17
|
+
_each_resource_file(folder, &bl)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Recursive implementation of `each_resource_file` for each
|
22
|
+
# folder in the configuration.
|
23
|
+
def _each_resource_file(folder)
|
24
|
+
folder.glob("**/*.yml").select(&to_filter_proc(config.file_filter)).each do |file|
|
25
|
+
yield file, folder
|
26
|
+
end
|
27
|
+
end
|
28
|
+
private :_each_resource_file
|
29
|
+
|
30
|
+
# Yields each resource in the current scope in turn.
|
31
|
+
def each_resource(&bl)
|
32
|
+
return enum_for(:each_resource) unless block_given?
|
33
|
+
each_resource_file do |file, folder|
|
34
|
+
yield Webspicy.resource(file.load, file)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_service(resource, &bl)
|
39
|
+
resource.services.select(&to_filter_proc(config.service_filter)).each(&bl)
|
40
|
+
end
|
41
|
+
|
42
|
+
def each_example(service, &bl)
|
43
|
+
service.examples.each(&bl)
|
44
|
+
end
|
45
|
+
|
46
|
+
def each_counterexamples(service, &bl)
|
47
|
+
service.counterexamples.each(&bl) if config.run_counterexamples?
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
###
|
52
|
+
### Schemas -- For parsing input and output data schemas found in
|
53
|
+
### web service definitions
|
54
|
+
###
|
55
|
+
|
56
|
+
# Parses a Finitio schema based on the data system.
|
57
|
+
def parse_schema(fio)
|
58
|
+
data_system.parse(fio)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the Data system to use for parsing schemas
|
62
|
+
def data_system
|
63
|
+
@data_system ||= begin
|
64
|
+
root = config.folders.find{|f| (f/"schema.fio").file? }
|
65
|
+
root ? Finitio::DEFAULT_SYSTEM.parse((root/"schema.fio").read) : Finitio::DEFAULT_SYSTEM
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
###
|
71
|
+
### Service invocation: abstract the configuration about what client is
|
72
|
+
### used and how to instantiate it
|
73
|
+
###
|
74
|
+
|
75
|
+
# Returns an instance of the client to use to invoke web services
|
76
|
+
def get_client
|
77
|
+
config.client.new(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Convert an instantiated URL found in a webservice definition
|
81
|
+
# to a real URL, using the configuration host
|
82
|
+
def to_real_url(url)
|
83
|
+
case config.host
|
84
|
+
when Proc
|
85
|
+
config.host.call(url)
|
86
|
+
when String
|
87
|
+
url =~ /^http/ ? url : "#{config.host}#{url}"
|
88
|
+
else
|
89
|
+
return url if url =~ /^http/
|
90
|
+
raise "Unable to resolve `#{url}` : no host resolver provided\nSee `Configuration#host="
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
###
|
95
|
+
### Private methods
|
96
|
+
###
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Returns a proc that implements file_filter strategy according to the
|
101
|
+
# type of filter installed
|
102
|
+
def to_filter_proc(filter)
|
103
|
+
case ff = filter
|
104
|
+
when NilClass then ->(f){ true }
|
105
|
+
when Proc then ff
|
106
|
+
when Regexp then ->(f){ ff =~ f.to_s }
|
107
|
+
else
|
108
|
+
->(f){ ff === f }
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Tester
|
3
|
+
class Asserter
|
4
|
+
|
5
|
+
NO_ARG = Object.new
|
6
|
+
|
7
|
+
class AssertionsClass
|
8
|
+
include Assertions
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(target)
|
12
|
+
@target = target
|
13
|
+
@assertions = AssertionsClass.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists(path = '')
|
17
|
+
unless @assertions.exists(@target, path)
|
18
|
+
_! "Expected #{_s(@target)} to exists"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def notExists(path = '')
|
23
|
+
unless @assertions.notExists(@target, path)
|
24
|
+
_! "Expected #{_s(@target)} not to exists"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty(path = '')
|
29
|
+
unless @assertions.empty(@target, path)
|
30
|
+
_! "Expected #{_s(@target)} to be empty"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def notEmpty(path = '')
|
35
|
+
unless @assertions.notEmpty(@target, path)
|
36
|
+
_! "Expected #{_s(@target)} to be non empty"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def size(path, expected = NO_ARG)
|
41
|
+
path, expected = '', path if expected == NO_ARG
|
42
|
+
unless @assertions.size(@target, path, expected)
|
43
|
+
_! "Expected #{_s(@target)} to have a size of #{expected}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def idIn(path, *expected)
|
48
|
+
path, expected = '', [path]+expected unless path.is_a?(String)
|
49
|
+
unless @assertions.idIn(@target, path, expected)
|
50
|
+
_! "Expected #{_s(@target)} to have ids #{expected.join(',')}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def idNotIn(path, *expected)
|
55
|
+
path, expected = '', [path]+expected unless path.is_a?(String)
|
56
|
+
unless @assertions.idNotIn(@target, path, expected)
|
57
|
+
_! "Expected #{_s(@target)} to not have ids #{expected.join(',')}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def idFD(path, id, expected = NO_ARG)
|
62
|
+
if expected == NO_ARG
|
63
|
+
expected = id
|
64
|
+
id, path = path, ''
|
65
|
+
end
|
66
|
+
unless @assertions.idFD(@target, path, id, expected)
|
67
|
+
_! "Expected #{_s(@target)} to meet FD #{expected.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def pathFD(path, expected)
|
72
|
+
unless @assertions.pathFD(@target, path, expected)
|
73
|
+
_! "Expected #{_s(@target)} to meet FD #{expected.inspect}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def DateTime(str)
|
80
|
+
DateTime.parse(str)
|
81
|
+
end
|
82
|
+
|
83
|
+
def _s(target)
|
84
|
+
target.inspect[0..25]
|
85
|
+
end
|
86
|
+
|
87
|
+
def _!(msg)
|
88
|
+
raise msg
|
89
|
+
end
|
90
|
+
|
91
|
+
end # class Asserter
|
92
|
+
end # class Tester
|
93
|
+
end # module Webspicy
|
94
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Webspicy
|
2
|
+
class Tester
|
3
|
+
module Assertions
|
4
|
+
|
5
|
+
class InvalidArgError < StandardError; end
|
6
|
+
|
7
|
+
NO_ARG = Object.new
|
8
|
+
|
9
|
+
def exists(target, path = NO_ARG)
|
10
|
+
target = extract_path(target, path)
|
11
|
+
not target.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def notExists(target, path = NO_ARG)
|
15
|
+
target = extract_path(target, path)
|
16
|
+
target.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty(target, path = NO_ARG)
|
20
|
+
target = extract_path(target, path)
|
21
|
+
respond_to!(target, :empty?).empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def notEmpty(target, path = NO_ARG)
|
25
|
+
not empty(target, path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def size(target, path, expected = NO_ARG)
|
29
|
+
path, expected = '', path if expected == NO_ARG
|
30
|
+
target = extract_path(target, path)
|
31
|
+
respond_to!(target, :size).size == expected
|
32
|
+
end
|
33
|
+
|
34
|
+
def idIn(target, path, expected = NO_ARG)
|
35
|
+
path, expected = '', path if expected == NO_ARG
|
36
|
+
target = extract_path(target, path)
|
37
|
+
ids = an_array(target).map do |tuple|
|
38
|
+
respond_to!(tuple, :[])[:id]
|
39
|
+
end
|
40
|
+
ids.to_set == expected.to_set
|
41
|
+
end
|
42
|
+
|
43
|
+
def idNotIn(target, path, expected = NO_ARG)
|
44
|
+
path, expected = '', path if expected == NO_ARG
|
45
|
+
target = extract_path(target, path)
|
46
|
+
ids = an_array(target).map do |tuple|
|
47
|
+
respond_to!(tuple, :[])[:id]
|
48
|
+
end
|
49
|
+
(ids.to_set & expected.to_set).empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def idFD(target, path, id, expected = NO_ARG)
|
53
|
+
if expected == NO_ARG
|
54
|
+
expected = id
|
55
|
+
id, path = path, ''
|
56
|
+
end
|
57
|
+
target = extract_path(target, path)
|
58
|
+
found = an_array(target).find{|t| t[:id] == id }
|
59
|
+
expected.keys.all?{|k|
|
60
|
+
value_equal(expected[k], found[k])
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def pathFD(target, path, expected)
|
65
|
+
target = extract_path(target, path)
|
66
|
+
expected.keys.all?{|k|
|
67
|
+
value_equal(expected[k], target[k])
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def extract_path(target, path = NO_ARG)
|
74
|
+
return target if path.nil? or path==NO_ARG or path.empty?
|
75
|
+
return nil unless target.is_a?(Hash)
|
76
|
+
path.split('/').inject(target) do |memo,key|
|
77
|
+
memo && (memo.is_a?(Array) ? memo[key.to_i] : memo[key.to_sym])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def respond_to!(target, method)
|
82
|
+
unless target.respond_to?(method)
|
83
|
+
raise InvalidArgError, "Expecting instance responding to #{method}"
|
84
|
+
end
|
85
|
+
target
|
86
|
+
end
|
87
|
+
|
88
|
+
def an_array(target)
|
89
|
+
target.is_a?(Array) ? target : [target]
|
90
|
+
end
|
91
|
+
|
92
|
+
def value_equal(exp, got)
|
93
|
+
case exp
|
94
|
+
when Hash
|
95
|
+
exp.all?{|(k,v)| got[k] == v }
|
96
|
+
else
|
97
|
+
exp == got
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end # module Assertions
|
102
|
+
end # class Tester
|
103
|
+
end # module Webspicy
|