1
0
Fork 0
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:
Paul Thornthwaite 2013-01-16 15:44:56 +00:00
parent 3863cd38c8
commit 0793a42a64
2 changed files with 162 additions and 67 deletions

View file

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

View file

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