mirror of
https://github.com/fog/fog.git
synced 2022-11-09 13:51:43 -05:00
[tests] Changes to format testing helper
* Adds and documents Shindo::Tests#data_matches_schema * Deprecates Shindo::Tests#format method * Strict mode is replace by expandable options * Uses Fog::Logger instead of p for debug output * Shindo::Tests#formats_kernel removed (was private) * Shindo::Tests#confirm_data_matches_schema added * #confirm_data_matches_schema uses yield Related to issue #1477 Shindo::Tests#formats incorrectly treated missing keys as valid if their value is allowed to be "nullable" or NilClass. These keys are not optional. They are required but can be the value nil or the Nullable class. Unfortunately due to lack of documentation and a bug in the code the current Nullable classes can be interpreted as optional and work as such. This replaces the old format checker with a new version that supports options to control the matching behaviour related to extra keys in either the data or the schema. This corrects the behaviour so extra keys in either the schema or the supplied data fail the check unless non strict has been used. A legacy version of the #formats test is in place that passes the options so it behaves as the old version does. Premptive patches have been added to fix those tests that were already broken but passed the original checks.
This commit is contained in:
parent
3863cd38c8
commit
0793a42a64
2 changed files with 162 additions and 67 deletions
|
@ -25,62 +25,144 @@ end
|
|||
module Shindo
|
||||
class Tests
|
||||
|
||||
def formats(format, strict=true)
|
||||
raise ArgumentError, 'format is nil' unless format
|
||||
# Generates a Shindo test that compares a hash schema to the result
|
||||
# of the passed in block returning true if they match.
|
||||
#
|
||||
# The schema that is passed in is a Hash or Array of hashes that
|
||||
# have Classes in place of values. When checking the schema the
|
||||
# value should match the Class.
|
||||
#
|
||||
# Strict mode will fail if the data has additional keys. Setting
|
||||
# +strict+ to +false+ will allow additional keys to appear.
|
||||
#
|
||||
# @param [Hash] schmea A Hash schema
|
||||
# @param [Hash] options Options to change validation rules
|
||||
# @option options [Boolean] :allow_extra_keys
|
||||
# If +true+ deoes not fail when keys are in the data that are
|
||||
# not specified in the schema. This allows new values to
|
||||
# appear in API output without breaking the check.
|
||||
# @option options [Boolean] :allow_optional_rules
|
||||
# If +true+ does not fail if extra keys are in the schema
|
||||
# that do not match the data. Not recommended!
|
||||
# @yield Data to check with schema
|
||||
#
|
||||
# @example Using in a test
|
||||
# Shindo.tests("data matches schema") do
|
||||
# data = {:string => "Hello" }
|
||||
# data_matches_schema(:string => String) { data }
|
||||
# end
|
||||
#
|
||||
# data matches schema
|
||||
# + has proper format
|
||||
#
|
||||
# @example Example schema
|
||||
# {
|
||||
# "id" => String,
|
||||
# "ram" => Integer,
|
||||
# "disks" => [
|
||||
# "size" => Float
|
||||
# ],
|
||||
# "dns_name" => Fog::Nullable::String,
|
||||
# "active" => Fog::Boolean,
|
||||
# "created" => DateTime
|
||||
# }
|
||||
#
|
||||
# @return [Boolean]
|
||||
def data_matches_schema(schema, options = {})
|
||||
test('data matches schema') do
|
||||
confirm_data_matches_schema(yield, schema, options)
|
||||
end
|
||||
end
|
||||
|
||||
# @deprecation #formats is deprecated. Use #data_matches_schema instead
|
||||
def formats(format, strict = true)
|
||||
test('has proper format') do
|
||||
formats_kernel(instance_eval(&Proc.new), format, true, strict)
|
||||
if strict
|
||||
options = {:allow_extra_keys => false, :allow_optional_rules => true}
|
||||
else
|
||||
options = {:allow_extra_keys => true, :allow_optional_rules => true}
|
||||
end
|
||||
confirm_data_matches_schema(yield, format, options)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def formats_kernel(original_data, original_format, original = true, strict = true)
|
||||
valid = true
|
||||
data = original_data.dup
|
||||
format = original_format.dup
|
||||
if format.is_a?(Array)
|
||||
data = {:element => data}
|
||||
format = {:element => format}
|
||||
end
|
||||
for key, value in format
|
||||
datum = data.delete(key)
|
||||
format.delete(key)
|
||||
case value
|
||||
when Array
|
||||
p("#{key.inspect} not Array: #{datum.inspect}") unless datum.is_a?(Array)
|
||||
valid &&= datum.is_a?(Array)
|
||||
if datum.is_a?(Array) && !value.empty?
|
||||
for element in datum
|
||||
type = value.first
|
||||
if type.is_a?(Hash)
|
||||
valid &&= formats_kernel({:element => element}, {:element => type}, false, strict)
|
||||
else
|
||||
valid &&= element.is_a?(type)
|
||||
end
|
||||
end
|
||||
end
|
||||
when Hash
|
||||
p("#{key.inspect} not Hash: #{datum.inspect}") unless datum.is_a?(Hash)
|
||||
valid &&= datum.is_a?(Hash)
|
||||
valid &&= formats_kernel(datum, value, false, strict)
|
||||
else
|
||||
p "#{key.inspect} not #{value.inspect}: #{datum.inspect}" unless datum.is_a?(value)
|
||||
valid &&= datum.is_a?(value)
|
||||
end
|
||||
end
|
||||
p data unless data.empty?
|
||||
p format unless format.empty?
|
||||
if strict
|
||||
valid &&= data.empty? && format.empty?
|
||||
else
|
||||
valid &&= format.empty?
|
||||
end
|
||||
if !valid && original
|
||||
@message = "#{original_data.inspect} does not match #{original_format.inspect}"
|
||||
# Checks if the data structure matches the schema passed in and
|
||||
# returns true if it fits.
|
||||
#
|
||||
# @param [Object] data Hash or Array to check
|
||||
# @param [Object] schema Schema pattern to check against
|
||||
# @param [Boolean] options
|
||||
# @option options [Boolean] :allow_extra_keys
|
||||
# If +true+ does not fail if extra keys are in the data
|
||||
# that are not in the schema. Allows
|
||||
# @option options [Boolean] :allow_optional_rules
|
||||
# If +true+ does not fail if extra keys are in the schema
|
||||
# that do not match the data. Not recommended!
|
||||
#
|
||||
# @return [Boolean] Did the data fit the schema?
|
||||
def confirm_data_matches_schema(data, schema, options = {})
|
||||
# Clear message passed to the Shindo tests
|
||||
@message = nil
|
||||
|
||||
valid = validate_value(schema, data, options)
|
||||
|
||||
unless valid
|
||||
@message = "#{data.inspect} does not match #{schema.inspect}"
|
||||
end
|
||||
valid
|
||||
end
|
||||
|
||||
# This contains a slightly modified version of the Hashidator gem
|
||||
# but unfortunately the gem does not cope with Array schemas.
|
||||
#
|
||||
# @see https://github.com/vangberg/hashidator/blob/master/lib/hashidator.rb
|
||||
#
|
||||
def validate_value(validator, value, options)
|
||||
Fog::Logger.write :debug, "[yellow][DEBUG] #{value.inspect} against #{validator.inspect}[/]\n"
|
||||
|
||||
# When being strict values not specified in the schema are fails
|
||||
unless options[:allow_extra_keys]
|
||||
if validator.respond_to?(:empty?) && value.respond_to?(:empty?)
|
||||
# Validator is empty but values are not
|
||||
return false if !value.empty? && validator.empty?
|
||||
end
|
||||
end
|
||||
|
||||
unless options[:allow_optional_rules]
|
||||
if validator.respond_to?(:empty?) && value.respond_to?(:empty?)
|
||||
# Validator has rules left but no more values
|
||||
return false if value.empty? && !validator.empty?
|
||||
end
|
||||
end
|
||||
|
||||
case validator
|
||||
when Array
|
||||
return false if value.is_a?(Hash)
|
||||
value.respond_to?(:all?) && value.all? {|x| validate_value(validator[0], x, options)}
|
||||
when Symbol
|
||||
value.respond_to? validator
|
||||
when Hash
|
||||
return false if value.is_a?(Array)
|
||||
validator.all? do |key, sub_validator|
|
||||
Fog::Logger.write :debug, "[blue][DEBUG] #{key.inspect} against #{sub_validator.inspect}[/]\n"
|
||||
validate_value(sub_validator, value[key], options)
|
||||
end
|
||||
else
|
||||
result = validator == value
|
||||
result = validator === value unless result
|
||||
# Repeat unless we have a Boolean already
|
||||
unless (result.is_a?(TrueClass) || result.is_a?(FalseClass))
|
||||
result = validate_value(result, value, options)
|
||||
end
|
||||
if result
|
||||
Fog::Logger.write :debug, "[green][DEBUG] Validation passed: #{value.inspect} against #{validator.inspect}[/]\n"
|
||||
else
|
||||
Fog::Logger.write :debug, "[red][DEBUG] Validation failed: #{value.inspect} against #{validator.inspect}[/]\n"
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,56 +52,61 @@ Shindo.tests('test_helper', 'meta') do
|
|||
|
||||
end
|
||||
|
||||
tests('#formats_kernel') do
|
||||
tests('data matches schema') do
|
||||
data = {:welcome => "Hello" }
|
||||
data_matches_schema(:welcome => String) { data }
|
||||
end
|
||||
|
||||
tests('#confirm_data_matches_schema') do
|
||||
|
||||
tests('returns true') do
|
||||
|
||||
returns(true, 'when value matches schema expectation') do
|
||||
formats_kernel({"key" => "Value"}, {"key" => String})
|
||||
confirm_data_matches_schema({"key" => "Value"}, {"key" => String})
|
||||
end
|
||||
|
||||
returns(true, 'when values within an array all match schema expectation') do
|
||||
formats_kernel({"key" => [1, 2]}, {"key" => [Integer]})
|
||||
confirm_data_matches_schema({"key" => [1, 2]}, {"key" => [Integer]})
|
||||
end
|
||||
|
||||
returns(true, 'when nested values match schema expectation') do
|
||||
formats_kernel({"key" => {:nested_key => "Value"}}, {"key" => {:nested_key => String}})
|
||||
confirm_data_matches_schema({"key" => {:nested_key => "Value"}}, {"key" => {:nested_key => String}})
|
||||
end
|
||||
|
||||
returns(true, 'when collection of values all match schema expectation') do
|
||||
formats_kernel([{"key" => "Value"}, {"key" => "Value"}], [{"key" => String}])
|
||||
confirm_data_matches_schema([{"key" => "Value"}, {"key" => "Value"}], [{"key" => String}])
|
||||
end
|
||||
|
||||
returns(true, 'when collection is empty although schema covers optional members') do
|
||||
formats_kernel([], [{"key" => String}])
|
||||
confirm_data_matches_schema([], [{"key" => String}], {:allow_optional_rules => true})
|
||||
end
|
||||
|
||||
returns(true, 'when additional keys are passed and not strict') do
|
||||
formats_kernel({"key" => "Value", :extra => "Bonus"}, {"key" => String}, true, false)
|
||||
confirm_data_matches_schema({"key" => "Value", :extra => "Bonus"}, {"key" => String}, {:allow_extra_keys => true})
|
||||
end
|
||||
|
||||
returns(true, 'when value is nil and schema expects NilClass') do
|
||||
formats_kernel({"key" => nil}, {"key" => NilClass})
|
||||
confirm_data_matches_schema({"key" => nil}, {"key" => NilClass})
|
||||
end
|
||||
|
||||
returns(true, 'when value and schema match as hashes') do
|
||||
formats_kernel({}, {})
|
||||
confirm_data_matches_schema({}, {})
|
||||
end
|
||||
|
||||
returns(true, 'when value and schema match as arrays') do
|
||||
formats_kernel([], [])
|
||||
confirm_data_matches_schema([], [])
|
||||
end
|
||||
|
||||
returns(true, 'when value is a Time') do
|
||||
formats_kernel({"time" => Time.now}, {"time" => Time})
|
||||
confirm_data_matches_schema({"time" => Time.now}, {"time" => Time})
|
||||
end
|
||||
|
||||
returns(true, 'when key is missing but value should be NilClass (#1477)') do
|
||||
formats_kernel({}, {"key" => NilClass})
|
||||
confirm_data_matches_schema({}, {"key" => NilClass}, {:allow_optional_rules => true})
|
||||
end
|
||||
|
||||
returns(true, 'when key is missing but value is nullable (#1477)') do
|
||||
formats_kernel({}, {"key" => Fog::Nullable::String})
|
||||
confirm_data_matches_schema({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => true})
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -109,35 +114,43 @@ Shindo.tests('test_helper', 'meta') do
|
|||
tests('returns false') do
|
||||
|
||||
returns(false, 'when value does not match schema expectation') do
|
||||
formats_kernel({"key" => nil}, {"key" => String})
|
||||
confirm_data_matches_schema({"key" => nil}, {"key" => String})
|
||||
end
|
||||
|
||||
returns(false, 'when key formats do not match') do
|
||||
formats_kernel({"key" => "Value"}, {:key => String})
|
||||
confirm_data_matches_schema({"key" => "Value"}, {:key => String})
|
||||
end
|
||||
|
||||
returns(false, 'when additional keys are passed and strict') do
|
||||
formats_kernel({"key" => "Missing"}, {})
|
||||
confirm_data_matches_schema({"key" => "Missing"}, {})
|
||||
end
|
||||
|
||||
returns(false, 'when some keys do not appear') do
|
||||
formats_kernel({}, {"key" => String})
|
||||
confirm_data_matches_schema({}, {"key" => String})
|
||||
end
|
||||
|
||||
returns(false, 'when collection contains a member that does not match schema') do
|
||||
formats_kernel([{"key" => "Value"}, {"key" => 5}], [{"key" => String}])
|
||||
confirm_data_matches_schema([{"key" => "Value"}, {"key" => 5}], [{"key" => String}])
|
||||
end
|
||||
|
||||
returns(false, 'when hash and array are compared') do
|
||||
formats_kernel({}, [])
|
||||
confirm_data_matches_schema({}, [])
|
||||
end
|
||||
|
||||
returns(false, 'when array and hash are compared') do
|
||||
formats_kernel([], {})
|
||||
confirm_data_matches_schema([], {})
|
||||
end
|
||||
|
||||
returns(false, 'when a hash is expected but another data type is found') do
|
||||
formats_kernel({"key" => {:nested_key => []}}, {"key" => {:nested_key => {}}})
|
||||
confirm_data_matches_schema({"key" => {:nested_key => []}}, {"key" => {:nested_key => {}}})
|
||||
end
|
||||
|
||||
returns(false, 'when key is missing but value should be NilClass (#1477)') do
|
||||
confirm_data_matches_schema({}, {"key" => NilClass}, {:allow_optional_rules => false})
|
||||
end
|
||||
|
||||
returns(false, 'when key is missing but value is nullable (#1477)') do
|
||||
confirm_data_matches_schema({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => false})
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue