def test_invalid_deviation_multiple_args_percent(self): with self.assertRaises(ValidationError) as cm: with AcceptedPercent(0.5): # <- Accepts +/- 50%. raise ValidationError([ Invalid(50, 100), # <- ACCEPTED: -50% deviation. Invalid(150, 100), # <- ACCEPTED: +50% deviation. Invalid(0.5, 0), # <- Rejected: Can not be accepted by percent. Invalid(4, 2), # <- Rejected: +100% is outside range. ]) remaining = cm.exception.differences self.assertEqual(remaining, [Invalid(0.5, 0), Invalid(4, 2)])
def test_accept_string(self): with self.assertRaises(ValidationError) as cm: with AcceptedKeys('aaa'): # <- Accept by string! raise ValidationError({ 'aaa': Missing(1), 'bbb': Missing(2), }) remaining_diffs = cm.exception.differences self.assertEqual(dict(remaining_diffs), {'bbb': Missing(2)})
def test_invalid_deviation_single_arg_percent(self): # Check error percent. with self.assertRaises(ValidationError) as cm: with AcceptedPercent(2.0): # <- Accepts +/- 200%. raise ValidationError([ Invalid(-1), # <- Rejected: Can not be accepted by percent. Invalid(0), # <- ACCEPTED! Invalid(2), # <- Rejected: Can not be accepted by percent. ]) remaining = cm.exception.differences self.assertEqual(remaining, [Invalid(-1), Invalid(2)])
def test_composite_key(self): with self.assertRaises(ValidationError) as cm: with AcceptedKeys(('a', 7)): # <- Accept using tuple! raise ValidationError({ ('a', 7): Missing(1), ('b', 7): Missing(2) }) remaining_diffs = cm.exception.differences self.assertEqual(dict(remaining_diffs), {('b', 7): Missing(2)})
def test_scope(self): with self.assertRaises(ValidationError) as cm: with AcceptedCount(2, scope='group'): # <- Accepts 2 per group. raise ValidationError({ 'foo': [Extra('xxx'), Extra('yyy')], 'bar': [Missing('xxx'), Missing('yyy')], 'baz': [Invalid('xxx'), Invalid('yyy'), Invalid('zzz')], }) remaining = cm.exception.differences self.assertEqual(remaining, {'baz': Invalid('zzz')})
def assertAcceptance(self, differences, acceptance, expected): """Helper method to test acceptances.""" with self.assertRaises(ValidationError) as cm: with acceptance: # <- Apply acceptance! raise ValidationError(differences) remaining_diffs = cm.exception.differences if isinstance(differences, Mapping): remaining_diffs = dict(remaining_diffs) elif nonstringiter(remaining_diffs): remaining_diffs = list(remaining_diffs) self.assertEqual(remaining_diffs, expected)
def test_IntersectedAcceptance(self): original_diffs = [Extra('a'), Missing('a'), Missing('b'), Extra('b')] with self.assertRaises(ValidationError) as cm: with IntersectedAcceptance(self.accepted_missing, self.accepted_letter_a): raise ValidationError(original_diffs) differences = cm.exception.differences self.assertEqual( list(differences), [Extra('a'), Missing('b'), Extra('b')]) # Test with acceptances in reverse-order (should give same result). with self.assertRaises(ValidationError) as cm: with IntersectedAcceptance(self.accepted_letter_a, self.accepted_missing): raise ValidationError(original_diffs) differences = cm.exception.differences self.assertEqual( list(differences), [Extra('a'), Missing('b'), Extra('b')])
def test_nonnumeric_but_compatible(self): with self.assertRaises(ValidationError) as cm: with AcceptedTolerance(datetime.timedelta(hours=2)): # <- Accepts +/- 2 hours. raise ValidationError([ Invalid(datetime.datetime(1989, 2, 24, hour=10, minute=30), datetime.datetime(1989, 2, 24, hour=11, minute=30)), Invalid(datetime.datetime(1989, 2, 24, hour=15, minute=10), datetime.datetime(1989, 2, 24, hour=11, minute=30)) ]) remaining = cm.exception.differences self.assertEqual(remaining, [Invalid(datetime.datetime(1989, 2, 24, 15, 10), expected=datetime.datetime(1989, 2, 24, 11, 30))])
def test_missing_deviation_percent(self): with self.assertRaises(ValidationError) as cm: with AcceptedPercent(1.0): # <- Accepts +/- 100%. raise ValidationError([ Missing(-1), # <- ACCEPTED! Missing(0), # <- ACCEPTED! Missing(2), # <- ACCEPTED! Missing((1, 2)), # <- Rejected: Wrong type. Missing('abc'), # <- Rejected: Wrong type. ]) remaining = cm.exception.differences self.assertEqual(remaining, [Missing((1, 2)), Missing('abc')])
def test_string_predicate(self): with self.assertRaises(ValidationError) as cm: with allowed_args('bbb'): # <- Allowance! raise ValidationError([ Missing('aaa'), Missing('bbb'), Extra('bbb'), ]) remaining_diffs = cm.exception.differences self.assertEqual(list(remaining_diffs), [Missing('aaa')])
def test_dict_and_list(self): """List of allowed differences applied to each group separately.""" differences = {'foo': Extra('xxx'), 'bar': [Extra('xxx'), Missing('yyy')]} allowed = [Extra('xxx')] with self.assertRaises(ValidationError) as cm: with allowed_specific(allowed): raise ValidationError(differences) actual = cm.exception.differences expected = {'bar': Missing('yyy')} self.assertEqual(actual, expected)
def test_nonmapping_container(self): """When differences container is not a mapping, the keys that allowed_key() sees are all None. """ with self.assertRaises(ValidationError) as cm: with allowed_keys('foo'): # <- Allow keys that equal 'foo'. differences = [Missing(1), Extra(2)] # <- List has no keys! raise ValidationError(differences) remaining_diffs = cm.exception.differences self.assertEqual(list(remaining_diffs), [Missing(1), Extra(2)])
def test_multiarg_predicate(self): with self.assertRaises(ValidationError) as cm: def func(diff): return diff < 2 with allowed_args((func, 5)): raise ValidationError([ Deviation(+1, 5), Deviation(+2, 5), ]) remaining_diffs = cm.exception.differences self.assertEqual(list(remaining_diffs), [Deviation(+2, 5)])
def test_allow_function(self): with self.assertRaises(ValidationError) as cm: def function(key): return key == 'aaa' with allowed_keys(function): # <- Allow by function! raise ValidationError({ 'aaa': Missing(1), 'bbb': Missing(2), }) remaining_diffs = cm.exception.differences self.assertEqual(dict(remaining_diffs), {'bbb': Missing(2)})
def test_function_predicate(self): with self.assertRaises(ValidationError) as cm: def function(args): diff, expected = args return diff < 2 and expected == 5 with allowed_args(function): # <- Allowance! raise ValidationError([ Deviation(+1, 5), Deviation(+2, 5), ]) remaining_diffs = cm.exception.differences self.assertEqual(list(remaining_diffs), [Deviation(+2, 5)])
def test_str_method(self): # Assert basic format and trailing comma. err = ValidationError([MinimalDifference('A')], 'invalid data') expected = """ invalid data (1 difference): [ MinimalDifference('A'), ] """ expected = textwrap.dedent(expected).strip() self.assertEqual(str(err), expected) # Assert without description. err = ValidationError([MinimalDifference('A')]) # <- No description! expected = """ 1 difference: [ MinimalDifference('A'), ] """ expected = textwrap.dedent(expected).strip() self.assertEqual(str(err), expected) # Assert "no cacheing"--objects that inhereit from some # Exceptions can cache their str--but ValidationError should # not do this. err._differences = [MinimalDifference('B')] err._description = 'changed' updated = textwrap.dedent(""" changed (1 difference): [ MinimalDifference('B'), ] """).strip() self.assertEqual(str(err), updated) # Assert dict format and trailing comma. err = ValidationError( { 'x': MinimalDifference('A'), 'y': MinimalDifference('B') }, 'invalid data') regex = textwrap.dedent(r""" invalid data \(2 differences\): \{ '[xy]': MinimalDifference\('[AB]'\), '[xy]': MinimalDifference\('[AB]'\), \} """).strip() self.assertRegex(str(err), regex) # <- Using regex because dict order
def test_combination_of_cases(self): """This is a bit of an integration test.""" differences = { 'foo': [Extra('xxx'), Missing('yyy')], 'bar': [Extra('xxx')], 'baz': [Extra('xxx'), Missing('yyy'), Extra('zzz')], } #allowed = {Ellipsis: [Extra('xxx'), Missing('yyy')]} allowed = [Extra('xxx'), Missing('yyy')] with self.assertRaises(ValidationError) as cm: with allowed_specific(allowed): raise ValidationError(differences) actual = cm.exception.differences self.assertEqual(actual, {'baz': Extra('zzz')})
def test_incompatible_diffs(self): """Test differences that cannot be fuzzy matched.""" incompatible_diffs = [ Missing('foo'), Extra('bar'), Invalid('baz'), # <- Cannot accept if there's no expected value. Deviation(1, 10), ] differences = incompatible_diffs + self.differences with self.assertRaises(ValidationError) as cm: with AcceptedFuzzy(cutoff=0.5): raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, incompatible_diffs)
def test_dict_global_wildcard_predicate(self): """Ellipsis wildcard key matches all, treats as a single group.""" differences = {'foo': Extra('xxx'), 'bar': [Extra('xxx'), Missing('yyy')]} allowed = {Ellipsis: Extra('xxx')} with self.assertRaises(ValidationError) as cm: with allowed_specific(allowed): raise ValidationError(differences) actual = cm.exception.differences # Actual result can vary with unordered dictionaries. if len(actual) == 1: expected = {'bar': [Extra('xxx'), Missing('yyy')]} else: expected = {'foo': Extra('xxx'), 'bar': Missing('yyy')} self.assertEqual(actual, expected)
def test_predicate_collision(self): """Ellipsis wildcard key matches all, treats as a single group.""" differences = { 'foo': Extra('xxx'), 'bar': [Extra('yyy'), Missing('yyy')], } def allow1(x): return x.startswith('ba') def allow2(x): return x == 'bar' allowed = { allow1: Extra('yyy'), allow2: Missing('yyy'), } regex = ("the key 'bar' matches multiple predicates: " "allow[12], allow[12]") with self.assertRaisesRegex(KeyError, regex): with allowed_specific(allowed): raise ValidationError(differences)
def test_repr(self): err = ValidationError([MinimalDifference('A')]) # <- No description. expected = "ValidationError([MinimalDifference('A')])" self.assertEqual(repr(err), expected) err = ValidationError([MinimalDifference('A')], 'description string') expected = "ValidationError([MinimalDifference('A')], 'description string')" self.assertEqual(repr(err), expected) # Objects that inhereit from some Exceptions can cache their # repr--but ValidationError should not do this. err._differences = [MinimalDifference('B')] err._description = 'changed' self.assertNotEqual(repr(err), expected, 'exception should not cache repr') updated = "ValidationError([MinimalDifference('B')], 'changed')" self.assertEqual(repr(err), updated)
def test_str_truncation(self): # Assert optional truncation behavior. err = ValidationError([ MinimalDifference('A'), MinimalDifference('B'), MinimalDifference('C'), ], 'invalid data') self.assertIsNone(err._should_truncate) self.assertIsNone(err._truncation_notice) no_truncation = """ invalid data (3 differences): [ MinimalDifference('A'), MinimalDifference('B'), MinimalDifference('C'), ] """ no_truncation = textwrap.dedent(no_truncation).strip() self.assertEqual(str(err), no_truncation) # Truncate without notice. err._should_truncate = lambda line_count, char_count: char_count > 35 err._truncation_notice = None truncation_witout_notice = """ invalid data (3 differences): [ MinimalDifference('A'), ... """ truncation_witout_notice = textwrap.dedent( truncation_witout_notice).strip() self.assertEqual(str(err), truncation_witout_notice) # Truncate and use truncation notice. err._should_truncate = lambda line_count, char_count: char_count > 35 err._truncation_notice = 'Message truncated.' truncation_plus_notice = """ invalid data (3 differences): [ MinimalDifference('A'), ... Message truncated. """ truncation_plus_notice = textwrap.dedent( truncation_plus_notice).strip() self.assertEqual(str(err), truncation_plus_notice)
def test_tolerance_syntax(self): with self.assertRaises(ValidationError) as cm: with allowed_percent(0.2): # <- Allows +/- 20%. raise ValidationError(self.differences) remaining = cm.exception.differences self.assertEqual(remaining, {'bbb': Deviation(+4, 16)})
def test_all_allowed(self): differences = {'foo': Extra('xxx'), 'bar': Missing('yyy')} allowed = {'foo': Extra('xxx'), 'bar': Missing('yyy')} with allowed_specific(allowed): # <- Allows all differences, no error! raise ValidationError(differences)
def test_NaN_values(self): with self.assertRaises(ValidationError): # <- NaN values should not be caught! with allowed_deviation(0): raise ValidationError(Deviation(float('nan'), 0))
def test_at_limit(self): with allowed_limit(2): # <- Allows 2 and there are 2. raise ValidationError([Extra('xxx'), Missing('yyy')]) with allowed_limit(3): # <- Allows 2 and there are 2. raise ValidationError({'foo': Extra('xxx'), 'bar': Missing('yyy')})
def test_excess_allowed(self): diffs = [Extra('xxx')] allowed = [Extra('xxx'), Missing('yyy')] # <- More allowed than with allowed_specific(allowed): # are actually found. raise ValidationError(diffs)
def test_same_value_case(self): with self.assertRaises(ValidationError) as cm: with allowed_percent(0.25, 0.25): # <- Allows +25% only. raise ValidationError(self.differences) result_diffs = cm.exception.differences self.assertEqual({'aaa': Deviation(-1, 16), 'ccc': Deviation(+2, 16)}, result_diffs)
def test_integration_examples(self): # Test allowance of +/- 2 OR +/- 6%. with self.assertRaises(ValidationError) as cm: differences = [ Deviation(+2, 1), # 200% Deviation(+4, 8), # 50% Deviation(+8, 32), # 25% ] with allowed_deviation(2) | allowed_percent(0.25): raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, [Deviation(+4, 8)]) # Test missing-type AND matching-value. with self.assertRaises(ValidationError) as cm: differences = [ Missing('A'), Missing('B'), Extra('C'), ] with allowed_missing() & allowed_args(lambda x: x == 'A'): raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, [Missing('B'), Extra('C')]) # Test missing-type OR allowed-limit. with self.assertRaises(ValidationError) as cm: differences = [ Extra('A'), Missing('B'), Extra('C'), Missing('D'), ] with allowed_limit(1) | allowed_missing(): raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, [Extra('C')]) # Test missing-type AND allowed-limit. with self.assertRaises(ValidationError) as cm: differences = [ Extra('A'), Missing('B'), Missing('C'), ] with allowed_limit(1) & allowed_missing(): # Allows only 1 missing. raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, [Extra('A'), Missing('C')]) # Test missing-type OR allowed-limit. with self.assertRaises(ValidationError) as cm: differences = [ Extra('A'), Missing('B'), Extra('C'), Missing('D'), ] with allowed_limit(1) | allowed_specific(Extra('A')): raise ValidationError(differences) remaining = cm.exception.differences self.assertEqual(remaining, [Extra('C'), Missing('D')])
def test_lower_upper_syntax(self): with self.assertRaises(ValidationError) as cm: with allowed_percent(0.0, 0.3): # <- Allows from 0 to 30%. raise ValidationError(self.differences) result_diffs = cm.exception.differences self.assertEqual({'aaa': Deviation(-1, 16)}, result_diffs)