debian-xmlschema/xmlschema/tests/test_schemas.py

711 lines
28 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2016-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
"""
This module runs tests concerning the building of XSD schemas with the 'xmlschema' package.
"""
from __future__ import print_function, unicode_literals
import unittest
import pdb
import os
import pickle
import time
import warnings
import xmlschema
from xmlschema import XMLSchemaBase, XMLSchemaParseError, XMLSchemaModelError, \
XMLSchemaIncludeWarning, XMLSchemaImportWarning
from xmlschema.compat import PY3, unicode_type
from xmlschema.etree import lxml_etree, etree_element, py_etree_element
from xmlschema.qnames import XSD_LIST, XSD_UNION, XSD_ELEMENT, XSI_TYPE
from xmlschema.tests import tests_factory, SchemaObserver, XMLSchemaTestCase
from xmlschema.validators import XsdValidator, XMLSchema11
from xmlschema.xpath import ElementPathContext
class TestXMLSchema10(XMLSchemaTestCase):
def check_schema(self, source, expected=None, **kwargs):
"""
Create a schema for a test case.
:param source: A relative path or a root Element or a portion of schema for a template.
:param expected: If it's an Exception class test the schema for raise an error. \
Otherwise build the schema and test a condition if expected is a callable, or make \
a substring test if it's not `None` (maybe a string). Then returns the schema instance.
"""
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected, self.schema_class, self.retrieve_schema_source(source), **kwargs)
else:
schema = self.schema_class(self.retrieve_schema_source(source), **kwargs)
if callable(expected):
self.assertTrue(expected(schema))
return schema
def check_complex_restriction(self, base, restriction, expected=None, **kwargs):
content = 'complex' if self.content_pattern.search(base) else 'simple'
source = """
<complexType name="targetType">
{0}
</complexType>
<complexType name="restrictedType">
<{1}Content>
<restriction base="ns:targetType">
{2}
</restriction>
</{1}Content>
</complexType>
""".format(base.strip(), content, restriction.strip())
self.check_schema(source, expected, **kwargs)
def test_schema_copy(self):
schema = self.vh_schema.copy()
self.assertNotEqual(id(self.vh_schema), id(schema))
self.assertNotEqual(id(self.vh_schema.namespaces), id(schema.namespaces))
self.assertNotEqual(id(self.vh_schema.maps), id(schema.maps))
def test_resolve_qname(self):
schema = self.schema_class("""<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xs:element name="root" />
</xs:schema>""")
self.assertEqual(schema.resolve_qname('xs:element'), XSD_ELEMENT)
self.assertEqual(schema.resolve_qname('xsi:type'), XSI_TYPE)
self.assertEqual(schema.resolve_qname(XSI_TYPE), XSI_TYPE)
self.assertEqual(schema.resolve_qname('element'), 'element')
self.assertRaises(ValueError, schema.resolve_qname, '')
self.assertRaises(ValueError, schema.resolve_qname, 'xsi:a type ')
self.assertRaises(ValueError, schema.resolve_qname, 'xml::lang')
def test_simple_types(self):
# Issue #54: set list or union schema element.
xs = self.check_schema("""
<simpleType name="test_list">
<annotation/>
<list itemType="string"/>
</simpleType>
<simpleType name="test_union">
<annotation/>
<union memberTypes="string integer boolean"/>
</simpleType>
""")
xs.types['test_list'].elem = xs.root[0] # elem.tag == 'simpleType'
self.assertEqual(xs.types['test_list'].elem.tag, XSD_LIST)
xs.types['test_union'].elem = xs.root[1] # elem.tag == 'simpleType'
self.assertEqual(xs.types['test_union'].elem.tag, XSD_UNION)
def test_wrong_includes_and_imports(self):
with warnings.catch_warnings(record=True) as context:
warnings.simplefilter("always")
self.check_schema("""
<include schemaLocation="example.xsd" />
<import schemaLocation="example.xsd" />
<redefine schemaLocation="example.xsd"/>
<import namespace="http://missing.example.test/" />
<import/>
""")
self.assertEqual(len(context), 3, "Wrong number of include/import warnings")
self.assertEqual(context[0].category, XMLSchemaIncludeWarning)
self.assertEqual(context[1].category, XMLSchemaIncludeWarning)
self.assertEqual(context[2].category, XMLSchemaImportWarning)
self.assertTrue(str(context[0].message).startswith("Include"))
self.assertTrue(str(context[1].message).startswith("Redefine"))
self.assertTrue(str(context[2].message).startswith("Namespace import"))
def test_wrong_references(self):
# Wrong namespace for element type's reference
self.check_schema("""
<element name="dimension" type="dimensionType"/>
<simpleType name="dimensionType">
<restriction base="short"/>
</simpleType>
""", XMLSchemaParseError)
def test_restriction_has_annotation(self):
# Wrong namespace for element type's reference
schema = self.check_schema("""
<simpleType name='Magic'>
<annotation>
<documentation> stuff </documentation>
</annotation>
<restriction base='string'>
<enumeration value='A'/>
</restriction>
</simpleType>""")
self.assertIsNotNone(schema.types["Magic"].annotation)
def test_facets(self):
# Issue #55 and a near error (derivation from xs:integer)
self.check_schema("""
<simpleType name="dtype">
<restriction base="decimal">
<fractionDigits value="3" />
<totalDigits value="20" />
</restriction>
</simpleType>
<simpleType name="ntype">
<restriction base="ns:dtype">
<totalDigits value="3" />
<fractionDigits value="1" />
</restriction>
</simpleType>
""")
self.check_schema("""
<simpleType name="dtype">
<restriction base="integer">
<fractionDigits value="3" /> <!-- <<< value must be 0 -->
<totalDigits value="20" />
</restriction>
</simpleType>
""", xmlschema.XMLSchemaParseError)
# Issue #56
self.check_schema("""
<simpleType name="mlengthparent">
<restriction base="string">
<maxLength value="200"/>
</restriction>
</simpleType>
<simpleType name="mlengthchild">
<restriction base="ns:mlengthparent">
<maxLength value="20"/>
</restriction>
</simpleType>
""")
def test_element_restrictions(self):
base = """
<sequence>
<element name="A" maxOccurs="7"/>
<element name="B" type="string"/>
<element name="C" fixed="5"/>
</sequence>
"""
self.check_complex_restriction(
base, restriction="""
<sequence>
<element name="A" maxOccurs="6"/>
<element name="B" type="NCName"/>
<element name="C" fixed="5"/>
</sequence>
""")
self.check_complex_restriction(
base, restriction="""
<sequence>
<element name="A" maxOccurs="8"/> <!-- <<< More occurrences -->
<element name="B" type="NCName"/>
<element name="C" fixed="5"/>
</sequence>
""", expected=XMLSchemaParseError)
self.check_complex_restriction(
base, restriction="""
<sequence>
<element name="A" maxOccurs="6"/>
<element name="B" type="float"/> <!-- <<< Not a derived type -->
<element name="C" fixed="5"/>
</sequence>
""", expected=XMLSchemaParseError)
self.check_complex_restriction(
base, restriction="""
<sequence>
<element name="A" maxOccurs="6"/>
<element name="B" type="NCName"/>
<element name="C" fixed="3"/> <!-- <<< Different fixed value -->
</sequence>
""", expected=XMLSchemaParseError)
self.check_complex_restriction(
base, restriction="""
<sequence>
<element name="A" maxOccurs="6" nillable="true"/> <!-- <<< nillable is True -->
<element name="B" type="NCName"/>
<element name="C" fixed="5"/>
</sequence>
""", expected=XMLSchemaParseError)
def test_sequence_group_restriction(self):
# Meaningless sequence group
base = """
<sequence>
<sequence>
<element name="A"/>
<element name="B"/>
</sequence>
</sequence>
"""
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="B"/></sequence>'
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="C"/></sequence>', XMLSchemaParseError
)
base = """
<sequence>
<element name="A"/>
<element name="B" minOccurs="0"/>
</sequence>
"""
self.check_complex_restriction(base, '<sequence><element name="A"/></sequence>')
self.check_complex_restriction(base, '<sequence><element name="B"/></sequence>', XMLSchemaParseError)
self.check_complex_restriction(base, '<sequence><element name="C"/></sequence>', XMLSchemaParseError)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="B"/></sequence>'
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="C"/></sequence>', XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="A" minOccurs="0"/><element name="B"/></sequence>',
XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="B" minOccurs="0"/><element name="A"/></sequence>',
XMLSchemaParseError
)
def test_all_group_restriction(self):
base = """
<all>
<element name="A"/>
<element name="B" minOccurs="0"/>
<element name="C" minOccurs="0"/>
</all>
"""
self.check_complex_restriction(base, '<all><element name="A"/><element name="C"/></all>')
self.check_complex_restriction(
base, '<all><element name="C" minOccurs="0"/><element name="A"/></all>', XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="C"/></sequence>'
)
self.check_complex_restriction(
base, '<sequence><element name="C" minOccurs="0"/><element name="A"/></sequence>',
)
self.check_complex_restriction(
base, '<sequence><element name="C" minOccurs="0"/><element name="A" minOccurs="0"/></sequence>',
XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence><element name="A"/><element name="X"/></sequence>', XMLSchemaParseError
)
base = """
<all>
<element name="A" minOccurs="0" maxOccurs="0"/>
</all>
"""
self.check_complex_restriction(base, '<all><element name="A"/></all>', XMLSchemaParseError)
def test_choice_group_restriction(self):
base = """
<choice maxOccurs="2">
<element name="A"/>
<element name="B"/>
<element name="C"/>
</choice>
"""
self.check_complex_restriction(base, '<choice><element name="A"/><element name="C"/></choice>')
self.check_complex_restriction(
base, '<choice maxOccurs="2"><element name="C"/><element name="A"/></choice>',
XMLSchemaParseError
)
self.check_complex_restriction(
base, '<choice maxOccurs="2"><element name="A"/><element name="C"/></choice>',
)
def test_occurs_restriction(self):
base = """
<sequence minOccurs="3" maxOccurs="10">
<element name="A"/>
</sequence>
"""
self.check_complex_restriction(
base, '<sequence minOccurs="3" maxOccurs="7"><element name="A"/></sequence>')
self.check_complex_restriction(
base, '<sequence minOccurs="4" maxOccurs="10"><element name="A"/></sequence>')
self.check_complex_restriction(
base, '<sequence minOccurs="3" maxOccurs="11"><element name="A"/></sequence>',
XMLSchemaParseError
)
self.check_complex_restriction(
base, '<sequence minOccurs="2" maxOccurs="10"><element name="A"/></sequence>',
XMLSchemaParseError
)
def test_union_restrictions(self):
# Wrong union restriction (not admitted facets, see issue #67)
self.check_schema(r"""
<simpleType name="Percentage">
<restriction base="ns:Integer">
<minInclusive value="0"/>
<maxInclusive value="100"/>
</restriction>
</simpleType>
<simpleType name="Integer">
<union memberTypes="int ns:IntegerString"/>
</simpleType>
<simpleType name="IntegerString">
<restriction base="string">
<pattern value="-?[0-9]+(\.[0-9]+)?%"/>
</restriction>
</simpleType>
""", XMLSchemaParseError)
def test_final_attribute(self):
self.check_schema("""
<simpleType name="aType" final="list restriction">
<restriction base="string"/>
</simpleType>
""")
def test_wrong_attribute(self):
self.check_schema("""
<attributeGroup name="alpha">
<attribute name="name" type="string"/>
<attribute ref="phone"/> <!-- Missing "phone" attribute -->
</attributeGroup>
""", XMLSchemaParseError)
def test_wrong_attribute_group(self):
self.check_schema("""
<attributeGroup name="alpha">
<attribute name="name" type="string"/>
<attributeGroup ref="beta"/> <!-- Missing "beta" attribute group -->
</attributeGroup>
""", XMLSchemaParseError)
schema = self.check_schema("""
<attributeGroup name="alpha">
<attribute name="name" type="string"/>
<attributeGroup name="beta"/> <!-- attribute "name" instead of "ref" -->
</attributeGroup>
""", validation='lax')
self.assertTrue(isinstance(schema.all_errors[1], XMLSchemaParseError))
def test_date_time_facets(self):
self.check_schema("""
<simpleType name="restricted_date">
<restriction base="date">
<minInclusive value="1900-01-01"/>
<maxInclusive value="2030-12-31"/>
</restriction>
</simpleType>""")
self.check_schema("""
<simpleType name="restricted_year">
<restriction base="gYear">
<minInclusive value="1900"/>
<maxInclusive value="2030"/>
</restriction>
</simpleType>""")
def test_base_schemas(self):
from xmlschema.validators.schema import XML_SCHEMA_FILE
self.schema_class(XML_SCHEMA_FILE)
def test_recursive_complex_type(self):
schema = self.schema_class("""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="elemA" type="typeA"/>
<xs:complexType name="typeA">
<xs:sequence>
<xs:element ref="elemA" minOccurs="0" maxOccurs="5"/>
</xs:sequence>
</xs:complexType>
</xs:schema>""")
self.assertEqual(schema.elements['elemA'].type, schema.types['typeA'])
def test_upa_violations(self):
self.check_schema("""
<complexType name="typeA">
<sequence>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="A"/>
<element name="B"/>
</sequence>
<element name="A" minOccurs="0"/>
</sequence>
</complexType>""", XMLSchemaModelError)
self.check_schema("""
<complexType name="typeA">
<sequence>
<sequence minOccurs="0" maxOccurs="unbounded">
<element name="B"/>
<element name="A"/>
</sequence>
<element name="A" minOccurs="0"/>
</sequence>
</complexType>""")
def test_root_elements(self):
# Test issue #107 fix
schema = self.schema_class("""<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="root1" type="root"/>
<xs:element name="root2" type="root"/>
<xs:complexType name="root">
<xs:sequence>
<xs:element name="elementWithNoType"/>
</xs:sequence>
</xs:complexType>
</xs:schema>""")
self.assertEqual(set(schema.root_elements), {schema.elements['root1'], schema.elements['root2']})
def test_is_restriction_method(self):
# Test issue #111 fix
schema = self.schema_class(source=os.path.join(self.test_cases_dir, 'issues/issue_111/issue_111.xsd'))
extended_header_def = schema.types['extendedHeaderDef']
self.assertTrue(extended_header_def.is_derived(schema.types['blockDef']))
class TestXMLSchema11(TestXMLSchema10):
schema_class = XMLSchema11
def test_explicit_timezone_facet(self):
schema = self.check_schema("""
<simpleType name='opt-tz-date'>
<restriction base='date'>
<explicitTimezone value='optional'/>
</restriction>
</simpleType>
<simpleType name='req-tz-date'>
<restriction base='date'>
<explicitTimezone value='required'/>
</restriction>
</simpleType>
<simpleType name='no-tz-date'>
<restriction base='date'>
<explicitTimezone value='prohibited'/>
</restriction>
</simpleType>
""")
self.assertTrue(schema.types['req-tz-date'].is_valid('2002-10-10-05:00'))
self.assertTrue(schema.types['req-tz-date'].is_valid('2002-10-10Z'))
self.assertFalse(schema.types['req-tz-date'].is_valid('2002-10-10'))
def test_assertion_facet(self):
self.check_schema("""
<simpleType name='DimensionType'>
<restriction base='integer'>
<assertion test='string-length($value) &lt; 2'/>
</restriction>
</simpleType>""", XMLSchemaParseError)
schema = self.check_schema("""
<simpleType name='MeasureType'>
<restriction base='integer'>
<assertion test='$value &gt; 0'/>
</restriction>
</simpleType>""")
self.assertTrue(schema.types['MeasureType'].is_valid('10'))
self.assertFalse(schema.types['MeasureType'].is_valid('-1.5'))
self.check_schema("""
<simpleType name='RestrictedDateTimeType'>
<restriction base='dateTime'>
<assertion test="$value > '1999-12-31T23:59:59'"/>
</restriction>
</simpleType>""", XMLSchemaParseError)
schema = self.check_schema("""
<simpleType name='RestrictedDateTimeType'>
<restriction base='dateTime'>
<assertion test="$value > xs:dateTime('1999-12-31T23:59:59')"/>
</restriction>
</simpleType>""")
self.assertTrue(schema.types['RestrictedDateTimeType'].is_valid('2000-01-01T12:00:00'))
schema = self.check_schema("""<simpleType name="Percentage">
<restriction base="integer">
<assertion test="$value >= 0"/>
<assertion test="$value &lt;= 100"/>
</restriction>
</simpleType>""")
self.assertTrue(schema.types['Percentage'].is_valid('10'))
self.assertTrue(schema.types['Percentage'].is_valid('100'))
self.assertTrue(schema.types['Percentage'].is_valid('0'))
self.assertFalse(schema.types['Percentage'].is_valid('-1'))
self.assertFalse(schema.types['Percentage'].is_valid('101'))
self.assertFalse(schema.types['Percentage'].is_valid('90.1'))
def test_complex_type_assertion(self):
schema = self.check_schema("""
<complexType name="intRange">
<attribute name="min" type="int"/>
<attribute name="max" type="int"/>
<assert test="@min le @max"/>
</complexType>""")
xsd_type = schema.types['intRange']
xsd_type.decode(etree_element('a', attrib={'min': '10', 'max': '19'}))
self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '10', 'max': '19'})))
self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '19', 'max': '19'})))
self.assertFalse(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '19'})))
self.assertTrue(xsd_type.is_valid(etree_element('a', attrib={'min': '25', 'max': '100'})))
def test_open_content(self):
self.check_schema("""
<element name="Book">
<complexType>
<openContent mode="interleave">
<any />
</openContent>
<sequence>
<element name="Title" type="string"/>
<element name="Author" type="string" />
<element name="Date" type="gYear"/>
<element name="ISBN" type="string"/>
<element name="Publisher" type="string"/>
</sequence>
</complexType>
</element>""")
def make_schema_test_class(test_file, test_args, test_num, schema_class, check_with_lxml):
"""
Creates a schema test class.
:param test_file: the schema test file path.
:param test_args: line arguments for test case.
:param test_num: a positive integer number associated with the test case.
:param schema_class: the schema class to use.
:param check_with_lxml: if `True` compare with lxml XMLSchema class, reporting anomalies. \
Works only for XSD 1.0 tests.
"""
xsd_file = os.path.relpath(test_file)
# Extract schema test arguments
expected_errors = test_args.errors
expected_warnings = test_args.warnings
inspect = test_args.inspect
locations = test_args.locations
defuse = test_args.defuse
debug_mode = test_args.debug
class TestSchema(XMLSchemaTestCase):
@classmethod
def setUpClass(cls):
cls.schema_class = schema_class
cls.errors = []
cls.longMessage = True
if debug_mode:
print("\n##\n## Testing %r schema in debug mode.\n##" % xsd_file)
pdb.set_trace()
def check_schema(self):
if expected_errors > 0:
xs = schema_class(xsd_file, validation='lax', locations=locations, defuse=defuse)
else:
xs = schema_class(xsd_file, locations=locations, defuse=defuse)
self.errors.extend(xs.maps.all_errors)
if inspect:
components_ids = set([id(c) for c in xs.maps.iter_components()])
missing = [c for c in SchemaObserver.components if id(c) not in components_ids]
if any([c for c in missing]):
raise ValueError("schema missing %d components: %r" % (len(missing), missing))
# Pickling test (only for Python 3, skip inspected schema classes test)
if not inspect and PY3:
try:
obj = pickle.dumps(xs)
deserialized_schema = pickle.loads(obj)
except pickle.PicklingError:
# Don't raise if some schema parts (eg. a schema loaded from remote)
# are built with the SafeXMLParser that uses pure Python elements.
for e in xs.maps.iter_components():
elem = getattr(e, 'elem', getattr(e, 'root', None))
if isinstance(elem, py_etree_element):
break
else:
raise
else:
self.assertTrue(isinstance(deserialized_schema, XMLSchemaBase))
self.assertEqual(xs.built, deserialized_schema.built)
# XPath API tests
if not inspect and not self.errors:
context = ElementPathContext(xs)
elements = [x for x in xs.iter()]
context_elements = [x for x in context.iter() if isinstance(x, XsdValidator)]
self.assertEqual(context_elements, [x for x in context.iter_descendants()])
self.assertEqual(context_elements, elements)
def check_lxml_schema(self, xmlschema_time):
start_time = time.time()
lxs = lxml_etree.parse(xsd_file)
try:
lxml_etree.XMLSchema(lxs.getroot())
except lxml_etree.XMLSchemaParseError as err:
if not self.errors:
print("\nSchema error with lxml.etree.XMLSchema for file {!r} ({}): {}".format(
xsd_file, self.__class__.__name__, unicode_type(err)
))
else:
if self.errors:
print("\nUnrecognized errors with lxml.etree.XMLSchema for file {!r} ({}): {}".format(
xsd_file, self.__class__.__name__,
'\n++++++\n'.join([unicode_type(e) for e in self.errors])
))
lxml_schema_time = time.time() - start_time
if lxml_schema_time >= xmlschema_time:
print(
"\nSlower lxml.etree.XMLSchema ({:.3f}s VS {:.3f}s) with file {!r} ({})".format(
lxml_schema_time, xmlschema_time, xsd_file, self.__class__.__name__
))
def test_xsd_schema(self):
if inspect:
SchemaObserver.clear()
del self.errors[:]
start_time = time.time()
if expected_warnings > 0:
with warnings.catch_warnings(record=True) as ctx:
warnings.simplefilter("always")
self.check_schema()
self.assertEqual(len(ctx), expected_warnings,
"%r: Wrong number of include/import warnings" % xsd_file)
else:
self.check_schema()
# Check with lxml.etree.XMLSchema class
if check_with_lxml and lxml_etree is not None:
self.check_lxml_schema(xmlschema_time=time.time() - start_time)
self.check_errors(xsd_file, expected_errors)
TestSchema.__name__ = TestSchema.__qualname__ = str('TestSchema{0:03}'.format(test_num))
return TestSchema
# Creates schema tests from XSD files
globals().update(tests_factory(make_schema_test_class, 'xsd'))
if __name__ == '__main__':
from xmlschema.tests import print_test_header
print_test_header()
unittest.main()