#!/usr/bin/env python
"""
$URL: svn+ssh://svn/repos/trunk/grouch/lib/test/utest_valuetype.py $
$Id: utest_valuetype.py 25068 2004-09-10 17:13:58Z dbinger $
"""

import string
from types import IntType, LongType, FloatType, StringType, ListType
from sets import Set
from sancho.utest import UTest
from grouch.schema import ClassDefinition, ObjectSchema
from grouch.valuetype import \
     AtomicType, UnionType, \
     InstanceType, InstanceContainerType, \
     AnyType, BooleanType, AliasType
from grouch.context import TypecheckContext


# Set up a fake module hierarchy:
#   module 'foo' provides class Foo (and sub-module bar)
#   module 'foo.bar' provides class FooBar
from os.path import dirname
import sys
sys.path.append(dirname(__file__))
import fake_module


class ValueTypeTest (UTest):

    def _pre (self):
        self.schema = fake_module.make_schema()

    def _post (self):
        del self.schema


    test_cases = string.split("atomic instance "
                              "list tuple dict "
                              "union instcont misc alias "
                              "pickle"
                             )

    def check_atomic (self):
        "atomic types"
        t = AtomicType(self.schema)
        t.set_type('int')
        assert t.metatype == "atomic"
        assert t.is_atomic_type()
        assert t.type == IntType
        assert t.get_type() == IntType
        assert str(t) == "int"
        t.set_type('float')
        assert str(t) == "float"
        assert t.get_metatype() == "atomic"

        t = AtomicType(self.schema)
        t.set_type(FloatType)
        assert t.metatype == "atomic"
        assert t.type == FloatType
        assert str(t) == "float"

        t = AtomicType(self.schema)
        try:
            t.set_type('foo')
            assert 0
        except ValueError: pass
        try:
            t.set_type('list')
            assert 0
        except ValueError: pass
        try:
            t.set_type(ListType)
            assert 0
        except ValueError: pass
        assert str(t) == "*no type*"

    def check_instance (self):
        "instance types"
        from foo import Foo
        from foo.bar import FooBar

        Foo_def = self.schema.get_class_definition(Foo)
        FooBar_def = self.schema.get_class_definition(FooBar)
        t = InstanceType(self.schema)
        t.set_class_name('foo.Foo')
        assert str(t) == "foo.Foo"
        assert t.is_instance_type()
        assert t.is_plain_instance_type()
        assert t.get_metatype() == "instance"

        t = InstanceType(self.schema)
        t.set_class_name('foo.bar.FooBar')
        assert str(t) == "foo.bar.FooBar"

        t = InstanceType(self.schema)
        try:
            t.set_class_name(FooBar)
            assert 0
        except TypeError: pass

    def check_list (self):
        "simple list types"
        from grouch.valuetype import ListType
        t = ListType(self.schema)
        t.set_element_type(IntType)
        assert str(t) == "[int]"
        assert isinstance(t.element_type, AtomicType)
        assert str(t.element_type) == "int"
        assert t.is_container_type()
        assert t.is_plain_container_type()
        assert t.get_element_type().type == IntType

        t = ListType(self.schema)
        try:
            t.set_element_type(53)
            assert 0
        except TypeError: pass
        t.set_element_type('int')
        assert str(t) == "[int]"
        assert t.element_type.type == IntType

        t = ListType(self.schema)
        inttype = self.schema.make_atomic_type("int")
        t.set_element_type(inttype)
        assert t.element_type == inttype
        assert str(t) == "[int]"

    def check_tuple (self):
        "simple tuple types"
        from grouch.valuetype import TupleType
        t = TupleType(self.schema)
        t.set_element_types((IntType,))
        assert str(t) == "(int,)"
        assert t.element_types[0].type == IntType
        assert t.get_element_types()[0].type == IntType
        assert t.get_metatype() == "container"

        t = TupleType(self.schema)
        t.set_element_types(('int','string'))
        assert str(t) == "(int, string)"
        assert t.element_types[0].type == IntType
        assert t.element_types[1].type == StringType

        t = TupleType(self.schema)
        t.set_element_types(('int',StringType))
        assert str(t) == "(int, string)"
        assert t.element_types[0].type == IntType
        assert t.element_types[1].type == StringType
        assert not t.is_extended()
        t.set_extended(1)
        assert t.is_extended()
        assert str(t) == "(int, string*)"

        t = TupleType(self.schema)
        try:
            t.set_element_types(('list',))
            assert 0
        except ValueError: pass
        try:
            t.set_element_types((42,))
            assert 0
        except TypeError: pass
        try:
            t.set_element_types('int')
            assert 0
        except TypeError: pass
        try:
            t.set_element_types(42)
            assert 0
        except TypeError: pass
        t.set_element_types(('int',))

    def check_dict (self):
        "simple dictionary types"
        from grouch.valuetype import DictionaryType
        t = DictionaryType(self.schema)
        t.set_item_types('int', FloatType)
        assert t.key_type.type == IntType
        assert t.value_type.type == FloatType
        assert str(t) == "{ int : float }"
        assert t.get_key_type().type == IntType
        assert t.get_value_type().type == FloatType
        (keytype, valuetype) = t.get_item_types()
        assert keytype.get_type() == IntType
        assert valuetype.get_type() == FloatType

        t = DictionaryType(self.schema)
        try:
            t.set_item_types('foo', 'string')
            assert 0
        except ValueError: pass
        try:
            t.set_item_types(('int', 'string'))
            assert 0
        except TypeError: pass
        try:
            t.set_item_types('list', 'string')
            assert 0
        except ValueError: pass
        try:
            t.set_item_types('int', 42)
            assert 0
        except TypeError: pass
        t.set_item_types(StringType, 'int')
        assert str(t) == "{ string : int }"

        t = DictionaryType(self.schema)
        try:
            t.set_item_types(IntType, 'foo')
            assert 0
        except ValueError: pass


    def check_union (self):
        "simple union types"
        t = UnionType(self.schema)
        t.set_union_types([IntType, FloatType, LongType])
        assert t.is_union_type()
        assert len(t.union_types) == 3
        assert (t.union_types[0].type,
                t.union_types[1].type,
                 t.union_types[2].type) == (
            IntType, FloatType, LongType)
        assert len(t.get_union_types()) == 3
        assert str(t) == "int | float | long"

        t = UnionType(self.schema)
        try:
            t.set_union_types(IntType)
            assert 0
        except TypeError: pass
        try:
            t.set_union_types((IntType,))
            assert 0
        except TypeError: pass
        try:
            t.set_union_types(['foo'])
            assert 0
        except ValueError: pass

    def check_instcont (self):
        "simple instance-container types"
        t = InstanceContainerType(self.schema)
        import foo
        Foo_def = self.schema.get_class_definition("foo.Foo")

        t.set_class_name('foo.Foo')
        ctype = self.schema.make_list_type(IntType)
        t.set_container_type(ctype)
        assert t.is_instance_type()
        assert t.is_container_type()
        assert t.is_instance_container_type()
        assert not t.is_plain_instance_type()
        assert not t.is_plain_container_type()
        assert t.get_metatype() == "instance-container"

        assert str(t) == "foo.Foo [int]"
        assert t.get_element_type().type == IntType
        try:
            t.get_element_types()
            assert 0
        except RuntimeError: pass
        try:
            t.get_item_types()
            assert 0
        except RuntimeError: pass

        t = InstanceContainerType(self.schema)
        t.set_class_name('foo.Foo')
        ctype = self.schema.make_tuple_type((IntType, FloatType))
        t.set_container_type(ctype)
        assert str(t) == "foo.Foo (int, float)"
        etypes = t.get_element_types()
        assert (etypes[0].type, etypes[1].type) == (IntType, FloatType)

        t = InstanceContainerType(self.schema)
        foo_type = InstanceType(self.schema)
        foo_type.set_class_name('foo.Foo')
        t.set_class_name('foo.Foo')
        ctype = self.schema.make_dictionary_type(StringType, foo_type)
        t.set_container_type(ctype)
        assert str(t) == "foo.Foo { string : foo.Foo }"
        (key_type, value_type) = t.get_item_types()
        assert key_type.get_type() == StringType

    def check_misc (self):
        "miscellaneous type (any, boolean)"
        t = AnyType(self.schema)
        assert t.is_any_type()
        assert str(t) == "any"

        t = BooleanType(self.schema)
        assert t.is_boolean_type()
        assert not t.is_any_type()
        assert str(t) == "boolean"

    def check_alias (self):
        "type aliasing"
        schema = self.schema
        schema.add_alias("real", "int|float|long")

        t = schema.parse_type("real")
        assert t.is_alias_type()
        alias = t.get_alias_type()
        assert alias.is_union_type()
        assert str(t) == "real"

        types = alias.get_union_types()
        assert map(str, types) == ["int", "float", "long"]

    def check_pickle (self):
        "pickling/unpickling of ValueType objects"
        from pickle import dumps, loads

        schema = self.schema
        parse_type = schema.parse_type
        t1 = parse_type("int")
        t2 = loads(dumps(t1))

        # Let's make sure we understand the pre-pickle situation.
        assert t1.schema is schema
        assert schema.get_type(IntType).schema is schema

        assert str(t2) == "int"

        # Make sure that the easily-compared innards of the two schemata
        # match.  (Some attributes are full of non-comparable objects, like
        # klass_definitions -- a dict of ClassDefinitions.  We just skip
        # them out of laziness.)
        sch1 = t1.schema ; sch2 = t2.schema
        assert sch1.atomic_type_names == sch2.atomic_type_names
        assert sch1.atomic_type_objects == sch2.atomic_type_objects
        assert sch1.atomic_type_values == sch2.atomic_type_values

        assert sch2.get_type(IntType).schema is sch2
        assert sch2.get_type(FloatType).schema is sch2

        # The schemata are not directly comparable, so remove them from
        # consideration (now that I've verified that most of their innards
        # match).
        t1s = t1.schema ; t2s = t2.schema
        t2.get_type()                   # ensure type attribute set
        del t1.schema, t2.schema
        assert vars(t1) == vars(t2)
        t1.schema = t1s ; t2.schema = t2s

        t1 = parse_type("[string | float]")
        t2 = loads(dumps(t1))
        assert str(t2) == "[string | float]"

        from datetime import datetime
        schema = ObjectSchema()
        schema.add_alias("real", "int|long|float")
        schema.add_atomic_type(datetime.now())
        t1 = schema.parse_type("real")
        t2 = schema.parse_type("(string, datetime)")

        # Pickling/unpickling various ValueType classes caused Grouch to
        # crash, pre-Jan 2002.  (The pickle support code was rather
        # delicate, and upgrading to Python 2.2 exposed some flakiness
        # that had remained hidden with 2.1.  Of these tests, the first
        # crashed under 2.1 and 2.2, but the second only under 2.2.)
        alias = t1                      # AliasType instance
        union = t1.alias_type           # UnionType instance
        loads(dumps(union))
        loads(dumps(alias))

        t1b = loads(dumps(t1))
        t2b = loads(dumps(t2))

        assert str(t1b) == "real"
        assert t1b.is_alias_type()
        assert t1b.get_alias_type().is_union_type()

        assert str(t2b) == "(string, datetime)"
        assert t2b.is_container_type()
        assert t2b.get_element_types()[1].is_atomic_type()

        schema1 = schema
        schema2 = loads(dumps(schema1))
        innertype1 = schema1.alias_value['real'].union_types[0]
        innertype2 = schema2.alias_value['real'].union_types[0]
        assert schema1.alias_value['real'].schema is schema1
        assert schema2.alias_value['real'].schema is schema2
        assert innertype1.schema is schema1
        assert innertype2.schema is schema2

# class ValueTypeTest


class TypecheckTest (UTest):

    def _pre (self):
        self.context = TypecheckContext()
        self.context.report_errors = 0

        from foo import Foo
        from foo.bar import FooBar
        from fake_module import make_schema

        self.schema = make_schema()
        self.foo_type = self.schema.make_instance_type(Foo)
        self.foobar_type = self.schema.make_instance_type(FooBar)

        # Create canonical instances with known good values.
        self.foo = Foo()
        self.foobar = FooBar()

    def _post (self):
        del self.context, self.schema
        del self.foo_type, self.foo


    test_cases = string.split("atomic "
                              "instance_1 instance_2 instance_3 instance_4 "
                              "list tuple dict "
                              "constraints instcont union union_2 "
                              "any boolean alias "
                              "nested_1 nested_2 recursive")

    def check_atomic (self):
        "type-checking atomic values"

        t1 = self.schema.parse_type("int")
        t2 = self.schema.parse_type("string")
        ctx = self.context

        assert t1.check_value(3, ctx)
        assert not t1.check_value(3.0, ctx)
        assert ctx.num_errors() == 1
        assert ctx.errors == (
            [("", "expected int, got float (3.0)")])
        assert t1.check_value(None, ctx)
        assert ctx.visit_counts == {'atomic': {'int': 3}}

        assert t2.check_value('', ctx)
        assert not t2.check_value(3, ctx)
        assert ctx.num_errors() == 2
        assert ctx.errors == (
            [("", "expected int, got float (3.0)"),
             ("", "expected string, got int (3)")])
        assert ctx.format_errors() == (
            ["expected int, got float (3.0)",
             "expected string, got int (3)"])
        assert ctx.visit_counts == {'atomic': {'int': 3, 'string': 2}}

        assert t1.check_value(None, ctx)

        #t1.allow_none = 0
        #assert not t1.check_value(None, ctx)"

    def check_instance_1 (self):
        "type-checking of simple instance values"

        ctx = self.context ; foo = self.foo ; foo_type = self.foo_type

        # No errors: default values are OK
        assert foo_type.check_value(foo, ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
            {'atomic': {'int': 1, 'string': 1},
             'instance': {'foo.Foo': 1}})

        # One bad attribute (add to empty error list)
        foo.num = 3.7
        msg = "expected int, got float (%s)" % repr(3.7)
        ctx.reset()
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == [("num", msg)]
        assert ctx.format_errors() == ["num: " + msg]

        # Two bad attributes
        ctx.reset()
        foo.num = "boo"
        foo.msg = 0
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == [
            ("num", "expected int, got string ('boo')"),
            ("msg", "expected string, got int (0)")]
        assert ctx.format_errors() == [
            "num: expected int, got string ('boo')",
            "msg: expected string, got int (0)"]
        assert ctx.visit_counts == {
            'atomic': {'int': 1, 'string': 1},
            'instance': {'foo.Foo': 1}}

        # One definitely good, one None (should give no errors)
        ctx.reset()
        foo.num = 666
        foo.msg = None
        assert foo_type.check_value(foo, ctx)
        assert ctx.num_errors() == 0

        # The "instance" itself is None
        assert foo_type.check_value(None, ctx)
        assert ctx.num_errors() == 0

        # And the "instance" is not an instance or None
        assert not foo_type.check_value(42, ctx)
        assert ctx.errors == [
            ("", "expected instance of foo.Foo, got int (42)")]

        class Blah: pass
        ctx.reset()
        blah = Blah()
        assert not foo_type.check_value(blah, ctx)
        if __name__ == '__main__':
            Blah_name = Blah.__name__ # 'Blah'
        else:
            Blah_name = str(Blah) #  'test_valuetype.Blah'
        assert ctx.errors == [
            ("",
             "expected instance of foo.Foo, got instance of %s" %
             Blah_name),
            ("",
             "found instance of class not in schema: %s" %
             Blah_name)]

    def check_instance_2 (self):
        "type-checking of instance values with missing/unexpected attrs"

        ctx = self.context ; foo = self.foo ; foo_type = self.foo_type

        # One attribute missing
        del foo.msg
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == [
            ("", "expected attribute 'msg' not found")]
        assert ctx.visit_counts == (
            {'atomic': {'int': 1},
             'instance': {'foo.Foo': 1}})

        # One attribute missing, one with bad type
        ctx.reset()
        del foo.num
        foo.msg = 0
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == (
            [("", "expected attribute 'num' not found"),
             ("msg", "expected string, got int (0)")])
        assert ctx.visit_counts == (
            {'atomic': {'string': 1},
             'instance': {'foo.Foo': 1}})

        # One unexpected attribute
        ctx.reset()
        foo.num = 1
        foo.msg = "Foo!"
        foo.c = 6.02e23
        unexp_msg = "found unexpected attribute 'c': float (%s)" % `foo.c`
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == [("", unexp_msg)]
        assert ctx.visit_counts == (
            {'atomic': {'int': 1, 'string': 1},
             'instance': {'foo.Foo': 1}})

        # One unexpected attribute, one bad value
        ctx.reset()
        foo.num = 1L
        assert not foo_type.check_value(foo, ctx)
        assert len(ctx.errors) == 2
        assert ctx.errors[0] == ("", unexp_msg)
        assert ctx.errors[1] == (
            ("num", "expected int, got long (1L)"))
        del foo.c

        # Mixups between instance and class attributes.
        from foo import Foo
        ctx.reset()
        del foo.num
        Foo.num = 37
        assert foo.num == 37
        assert getattr(foo, 'num') == 37
        assert hasattr(foo, 'num')
        assert not foo_type.check_value(foo, ctx)
        assert ctx.errors == (
            [("", "expected attribute 'num' not found")])

        ctx.reset()
        foo.num = 42
        assert foo.num == 42
        assert foo_type.check_value(foo, ctx)
        Foo.x = "boo"
        assert foo_type.check_value(foo, ctx)
        del Foo.num
        assert foo_type.check_value(foo, ctx)
        del Foo.x
        assert foo_type.check_value(foo, ctx)
        assert ctx.errors == []

    def check_instance_3 (self):
        "type-checking of instances of subclasses"
        from foo import Foo, SubFoo
        ctx = self.context
        foo_type = self.foo_type
        foo = Foo()
        subfoo = SubFoo()

        assert foo_type.check_value(foo, ctx)
        assert foo_type.check_value(subfoo, ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
                      {'atomic': {'int': 3, 'string': 2},
                       'instance': {'foo.Foo': 2}})

        subfoo.num = "boo"
        ctx.reset(start_name='subfoo')
        assert not foo_type.check_value(subfoo, ctx)
        assert ctx.errors == (
            [("subfoo.num", "expected int, got string ('boo')")])

        ctx.reset(start_name='subfoo')
        subfoo.num = 0
        subfoo.k = "hi"
        subfoo.i = None
        assert not foo_type.check_value(subfoo, ctx)
        assert len(ctx.errors) == 2
        assert ctx.errors[0] == (
            ("subfoo", "found unexpected attribute 'i': None"))
        assert ctx.errors[1] == (
            ("subfoo.k", "expected int, got string ('hi')"))

        ctx.reset(start_name='subfoo')
        subfoo = SubFoo()
        del subfoo.k, subfoo.num
        assert not foo_type.check_value(subfoo, ctx)
        assert ctx.errors == (
            [("subfoo", "expected attribute 'num' not found"),
             ("subfoo", "expected attribute 'k' not found")])

        # Cook up a sub-class that's not known to the schema (to test
        # one measly line of code!)
        from fake_module import move_class
        class AltFoo (Foo): pass
        move_class("foo", AltFoo)      # now it's foo.AltFoo
        AltFoo_def = ClassDefinition("foo.AltFoo",
                                      self.schema,
                                      bases=["foo.Foo"])
        AltFoo_def.finish_definition()

        ctx.reset(start_name='alt_foo')
        alt_foo = AltFoo()
        assert not foo_type.check_value(alt_foo, ctx)
        assert ctx.errors == (
            [("alt_foo",
              "expected instance of foo.Foo, "
              "got instance of unknown subclass foo.AltFoo")])

    def check_instance_4 (self):
        "type-checking of instances with no __dict__"
        from foo import DictLess
        schema = self.schema
        cdef = ClassDefinition("foo.DictLess", schema)
        cdef.finish_definition()        # has no attributes
        schema.add_class(cdef)

        cdef = schema.get_class_definition("foo.Foo")
        cdef.add_attribute("t", schema.parse_type("foo.DictLess"))
        cdef.finish_definition()

        from foo import Foo
        f = Foo()
        f.t = DictLess()
        ctx = self.context
        schema.check_value(f, ctx)
        assert ctx.errors == []

    def check_list (self):
        "type-checking of list values"
        ctx = self.context
        list_type = self.schema.make_list_type('int')

        assert list_type.check_value([], ctx)
        assert ctx.num_errors() == 0
        assert list_type.check_value([0, -34], ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
            {'container': {'list': 2},
             'atomic': {'int': 2}})

        ctx.reset("L")
        assert not list_type.check_value([0, 3.4], ctx)
        assert ctx.errors == (
            [("L[1]", "expected int, got float (%s)" % repr(3.4))])
        assert ctx.visit_counts == (
            {'container': {'list': 1},
             'atomic': {'int': 2}})

        ctx.reset("L")
        assert not list_type.check_value(['foo', 0L], ctx)
        assert len(ctx.errors) == 2
        assert ctx.errors[0] == (
            ("L[0]", "expected int, got string ('foo')"))
        assert ctx.errors[1] == (
            ("L[1]", "expected int, got long (0L)"))

        ctx.reset("L")
        assert not list_type.check_value((3, 4), ctx)
        assert ctx.errors == (
            [("L", "expected list, got tuple ((3, 4))")])

        ctx.reset("L")
        assert list_type.check_value(None, ctx)
        assert ctx.num_errors() == 0
        assert list_type.check_value([0, None, 2], ctx)
        assert ctx.num_errors() == 0

    def check_tuple (self):
        "type-checking of tuple values"
        ctx = self.context
        tuple_type = self.schema.make_tuple_type(('string', 'int'))

        assert tuple_type.check_value(('foo', 3), ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
            {'container': {'tuple': 1},
             'atomic': {'string': 1, 'int': 1}})

        ctx.reset()
        assert not tuple_type.check_value(['foo', 3], ctx)
        assert ctx.errors == (
            [("", "expected tuple, got list (['foo', 3])")])
        assert ctx.visit_counts == {'container': {'tuple': 1}}

        ctx.reset()
        assert not tuple_type.check_value((), ctx)
        assert ctx.errors == (
            [("","expected tuple of length 2, got one of length 0")])

        ctx.reset()
        assert not tuple_type.check_value(('foo', 3L), ctx)
        assert ctx.errors == [("[1]", "expected int, got long (3L)")]

        ctx.reset()
        assert not tuple_type.check_value((3, 'foo'), ctx)
        assert ctx.errors == (
            [("[0]", "expected string, got int (3)"),
             ("[1]", "expected int, got string ('foo')")])

        ctx.reset()
        tuple_type.set_extended(1)
        assert tuple_type.check_value(('foo', 3), ctx)
        assert tuple_type.check_value(('foo',), ctx)
        assert tuple_type.check_value(('foo', 3, 4, 5), ctx)
        assert not tuple_type.check_value(('foo',''), ctx)
        assert ctx.errors == (
            [("[1]", "expected int, got string ('')")])

        ctx.reset()
        assert not tuple_type.check_value((), ctx)
        assert ctx.errors == (
            [("",
              "expected tuple of at least length 1, "
              "got one of length 0")])

        ctx.reset()
        assert not tuple_type.check_value(('foo',3,666,''), ctx)

        assert ctx.errors == (
            [("[3]", "expected int, got string ('')")])

    def check_dict (self):
        "type-checking of dictionary values"
        ctx = self.context
        dict_type = self.schema.make_dictionary_type('string', 'long')

        assert dict_type.check_value({'foo': 3L, 'bar': 666L}, ctx)
        assert ctx.num_errors() == 0
        assert dict_type.check_value({}, ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
                      {'container': {'dictionary': 2},
                       'atomic': {'string': 2, 'long': 2}})

        # Bad value types
        ctx.reset()
        assert not dict_type.check_value({'foo': 3}, ctx)
        assert ctx.errors == (
            [("['foo']", "expected long, got int (3)")])
        assert ctx.visit_counts == (
            {'container': {'dictionary': 1},
             'atomic': {'string': 1, 'long': 1}})

        ctx.reset()
        assert not dict_type.check_value({'foo': 3L, 'bar': 3.14}, ctx)
        assert ctx.errors == (
            [("['bar']",
              "expected long, got float (%s)" % repr(3.14))])

        # Bad key types
        ctx.reset()
        assert not dict_type.check_value({1: 3L}, ctx)
        assert ctx.errors== (
            [("[1] (key)", "expected string, got int (1)")])

        # A bit of each
        ctx.reset()
        assert not dict_type.check_value({1: 'foo'}, ctx)
        assert Set(ctx.errors) == Set(
            [("[1] (key)", "expected string, got int (1)"),
             ("[1]", "expected long, got string ('foo')")])

        # Same, but in different elements
        ctx.reset()
        assert not dict_type.check_value({1: 1L, 'foo': 'foo'}, ctx)
        assert Set(ctx.errors) == Set(
            [("[1] (key)", "expected string, got int (1)"),
             ("['foo']", "expected long, got string ('foo')")
             ])

    def check_set (self):
        "type-checking of set values"
        ctx = self.context
        set_type = self.schema.make_set_type('int')

        assert set_type.check_value(Set(), ctx)
        assert ctx.num_errors() == 0
        assert set_type.check_value(Set([0, -34]), ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == (
            {'container': {'set': 2},
             'atomic': {'int': 2}})

        ctx.reset("L")
        assert not set_type.check_value(Set([3.4]), ctx)
        assert ctx.errors == (
            [("L[0]", "expected int, got float (%s)" % repr(3.4))])
        assert ctx.visit_counts == (
            {'container': {'set': 1},
             'atomic': {'int': 1}})

        ctx.reset("L")
        assert not set_type.check_value((3, 4), ctx)
        assert ctx.errors == (
            [("L", "expected set, got tuple ((3, 4))")])


    def check_constraints (self):
        "type-checking with constraints (not none, not empty)"
        ctx = self.context
        int_type = self.schema.parse_type("int")
        int_type.allow_none = 1
        assert int_type.check_value(None, ctx)
        assert ctx.num_errors() == 0

        ctx.reset()
        int_type.allow_none = 0
        assert not int_type.check_value(None, ctx)
        assert ctx.errors == (
            [("", "expected int, got None")])

        ctx.reset()
        list_type = self.schema.parse_type("[int]")
        assert list_type.check_value(None, ctx)
        assert ctx.num_errors() == 0
        assert list_type.check_value([], ctx)
        assert ctx.num_errors() == 0

        ctx.reset()
        list_type.allow_none = 0
        assert not list_type.check_value(None, ctx)
        assert ctx.errors == (
            [("", "expected list, got None")])
        ctx.reset()
        assert list_type.check_value([], ctx)
        assert ctx.num_errors() == 0

        ctx.reset()
        list_type.allow_empty = 0
        assert not list_type.check_value([], ctx)
        assert ctx.errors == (
            [("", "empty list not allowed")])
        ctx.reset()
        assert not list_type.check_value(None, ctx)
        assert ctx.errors == (
            [("", "expected list, got None")])

        ctx.reset()
        list_type.allow_none = 1
        assert not list_type.check_value([], ctx)
        assert ctx.errors == (
            [("", "empty list not allowed")])
        ctx.reset()
        assert list_type.check_value(None, ctx)

    def check_instcont (self):
        "type-checking instance-container types"
        import UserList
        ctx = self.context
        ulist_def = ClassDefinition("UserList.UserList", self.schema)
        ulist_def.add_attribute("data", self.schema.parse_type("[any]"))
        ulist_def.finish_definition()
        self.schema.add_class(ulist_def)
        ulist_type = self.schema.parse_type("UserList.UserList [string]")

        good_list = UserList.UserList(["foo", "bar"])
        empty_list = UserList.UserList()
        bad_list = UserList.UserList([3, "foo"])

        assert ulist_type.check_value(good_list, ctx)
        assert ctx.num_errors() == 0
        assert ulist_type.check_value(empty_list, ctx)
        assert ctx.num_errors() == 0
        assert not ulist_type.check_value(bad_list, ctx)
        assert ctx.errors == (
            [("[0]", "expected string, got int (3)")])
        assert ctx.visit_counts == (
            {'instance-container': {'UserList.UserList': 3},
             'container': {'list': 3},
             'atomic': {'string': 4},
             'any': {None: 4}})

        ctx.reset()
        assert not ulist_type.check_value(['foo'], ctx)
        assert ctx.errors == (
            [("", "expected instance of UserList.UserList, "
              "got list (['foo'])")])

        ctx.reset()
        assert not ulist_type.check_value('foo', ctx)
        assert ctx.errors== (
            [("", "expected instance of UserList.UserList, "
              "got string ('foo')")])

        ctx.reset()
        assert ulist_type.check_value(None, ctx)
        assert ctx.num_errors() == 0
        ul = UserList.UserList(["foo", None, "bar"])
        assert ulist_type.check_value(ul, ctx)
        assert ctx.num_errors() == 0

    def check_union (self):
        "type-checking of union types"
        ctx = self.context
        union_type = UnionType(self.schema)

        # Start with a degenerate union type: the opposite of "any"
        # (ie. nothing is valid).
        union_type.set_union_types([])
        assert not union_type.check_value(0, ctx)
        assert not union_type.check_value('hi', ctx)
        assert ctx.num_errors() == 2
        assert ctx.errors[0][1] == (
            "no valid values for empty union type; got int (0)")
        assert ctx.errors[1][1] == (
            "no valid values for empty union type; "
            "got string ('hi')")
        assert ctx.visit_counts == (
            {'union': {'': 2}})

        ctx.reset()
        assert not union_type.check_value(ctx, ctx)
        assert ctx.num_errors() == 2
        assert ctx.errors[0][1] == (
            "no valid values for empty union type; "
            "got instance of grouch.context.TypecheckContext")
        assert ctx.errors[1][1] == (
            "found instance of class not in schema: "
            "grouch.context.TypecheckContext")

        # Equally pointless: just one type in the union.
        ctx.reset()
        union_type.set_union_types([self.schema.get_type('int')])
        assert union_type.check_value(0, ctx)
        assert ctx.num_errors() == 0
        assert not union_type.check_value('hi', ctx)
        assert ctx.num_errors() == 1
        assert ctx.errors[0][1] == "expected int; got string ('hi')"
        assert ctx.visit_counts == {'union': {'int': 2}}

        # Finally, something that would occur in reality: int|long
        ctx.reset()
        union_type.set_union_types([self.schema.get_type('int'),
                                    self.schema.get_type('long')])
        assert union_type.check_value(100, ctx)
        assert union_type.check_value(2L**40, ctx)
        assert ctx.num_errors() == 0
        assert not union_type.check_value(1.0, ctx)
        assert ctx.errors == (
            [("", "expected int or long; got float (1.0)")])
        assert ctx.visit_counts == (
            {'union': {'int | long': 3}})

        # Now one with three types, just to make sure the error message
        # is formatted correctly.
        ctx.reset()
        union_type.set_union_types([self.schema.get_type('int'),
                                    self.schema.get_type('long'),
                                    self.schema.get_type('float')])
        assert union_type.check_value(100, ctx)
        assert union_type.check_value(2L**40, ctx)
        assert union_type.check_value(1.0, ctx)
        assert ctx.num_errors() == 0
        assert not union_type.check_value('foo', ctx)
        assert ctx.errors == (
            [("", "expected int, long, or float; "
              "got string ('foo')")])

    # check_union ()

    def check_union_2 (self):
        "type-checking union types with buried errors"

        # For the first year of its existence, Grouch had a subtle,
        # well-hidden bug: when an object with a union type had
        # a buried type error, that error would be hidden by UnionType.
        # Eg. given the declaration
        #   x : Foo | Bar
        # and an instance of Foo with some error (eg. a missing attribute),
        # then UnionType would simply register the fact that there was
        # an error with the Foo instance, and move on to testing the
        # value as a Bar instance.  That would fail, so UnionType would
        # assume the value didn't meet *its* requirements... when it
        # really did; the problem was with the Foo-ness of the object.

        # To test this, let's create a "Foo | FooBar" union type
        # and a couple of test instances.
        import foo, foo.bar
        schema = self.schema
        ctx = self.context
        union_type = schema.parse_type("foo.Foo | foo.bar.FooBar")
        foo_type = schema.make_instance_type("foo.Foo")
        foobar_type = schema.make_instance_type("foo.bar.FooBar")
        val1 = foo.Foo()
        val2 = foo.bar.FooBar()

        # First, make sure everything works without any errors.
        union_type.check_value(val1, ctx)
        union_type.check_value(val2, ctx)
        assert ctx.errors == []

        # Now introduce an error in val1 and ensure that we get the
        # same error message through both InstanceType and UnionType.
        val1.num = "x"
        ctx.reset()
        err = "expected int, got string ('x')"
        foo_type.check_value(val1, ctx)
        assert ctx.errors == [('num', err)]
        ctx.reset()
        union_type.check_value(val1, ctx)
        assert ctx.errors == [('num', err)]

        # Same thing with val2
        val2.b = 37
        ctx.reset()
        err = "expected instance of foo.Foo, got int (37)"
        foobar_type.check_value(val2, ctx)
        assert ctx.errors == [('b', err)]
        ctx.reset()
        union_type.check_value(val2, ctx)
        assert ctx.errors == [('b', err)]

        # There's still more subtlety lurking here: what do we do
        # about declarations like
        #   x : [int] | [string]
        #   x : [(int, string)] | [(string,int)]
        #
        # How far into a value do we have to go to determine which
        # branch of the UnionType it satisfies?  Can we just throw our
        # hands up in despair and say "This union is ambiguous", or is
        # that too lazy?  Hmmm.

    def check_any (self):
        "type-checking of 'any' type"
        ctx = self.context
        any_type = AnyType(self.schema)
        assert any_type.check_value(3, ctx)
        assert any_type.check_value(3.0, ctx)
        assert any_type.check_value(3L, ctx)
        assert any_type.check_value('three', ctx)
        assert any_type.check_value(self, ctx)
        assert any_type.check_value(self.__class__, ctx)
        assert any_type.check_value((self, self.check_any), ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == {'any': {None: 7}}

        # This won't work right now, 'cause AnyType accepts *anything*,
        # even if None isn't allowed.  Hmmm.
        ctx.reset()
        any_type.allow_none = 0
        assert any_type.check_value(3, ctx)
        assert not any_type.check_value(None, ctx)
        assert ctx.errors == [("", "expected anything except None")]

        # Ensure that we typecheck instances when seen in an "any" slot.
        from foo import Foo
        ctx.reset()
        f = Foo() ; f.num = "37"
        assert not any_type.check_value(f, ctx)
        assert ctx.errors == (
            [("num", "expected int, got string ('37')")])
        assert ctx.visit_counts == (
            {'any': {None: 1},
             'instance': {'foo.Foo': 1},
             'atomic': {'int': 1, 'string': 1}})

        # Ensure that instances of "foreign" classes (classes not in
        # the schema) don't cause any problems by default.
        ctx.reset()
        assert any_type.check_value(ctx, ctx)
        assert ctx.num_errors() == 0

        # But if we set the 'allow_any_instance' flag to false, foreign
        # instances are trouble.
        ctx.reset()
        any_type.set_allow_any_instance(0)
        assert not any_type.check_value(ctx, ctx)
        assert ctx.errors == (
            [("", "found instance of class not in schema: "
              "grouch.context.TypecheckContext")])

    def check_boolean (self):
        "type-checking of boolean type"
        ctx = self.context
        bool_type = BooleanType(self.schema)
        assert bool_type.check_value(0, ctx)
        assert bool_type.check_value(1, ctx)
        assert bool_type.check_value(None, ctx)
        assert ctx.num_errors() == 0
        assert ctx.visit_counts == {'boolean': {None: 3}}

        assert not bool_type.check_value('', ctx)
        assert not bool_type.check_value('foo', ctx)
        assert not bool_type.check_value(2, ctx)
        errs = ctx.errors
        assert errs[0] == ("", "expected a boolean, got string ('')")
        assert errs[1] == ("", "expected a boolean, got string ('foo')")
        assert errs[2] == ("", "expected a boolean, got int (2)")

        # Again, allow_none is currently ignored -- hmmm!
        #ctx.reset()
        #bool_type.allow_none = 0
        #assert not bool_type.check_value(None, ctx)
        #assertctx.errors",
        #              [("", "expected a boolean, got None")])

    def check_alias (self):
        "type-checking of alias types"
        ctx = self.context
        schema = self.schema
        alias_type = AliasType(schema)
        alias_type.set_alias_type("real", schema.parse_type("int|long|float"))

        assert alias_type.check_value(100, ctx)
        assert alias_type.check_value(2L**40, ctx)
        assert alias_type.check_value(3.14159, ctx)
        assert ctx.num_errors() == 0
        assert not alias_type.check_value('foo', ctx)
        assert ctx.errors == (
            [("", "expected int, long, or float; "
              "got string ('foo')")])

    def check_nested_1 (self):
        "type-checking of nested container structures"

        ctx = self.context
        ntype = self.schema.parse_type("{string : [int]}")

        # val1 - completely good value
        # val2 - bad at top level (not a list)
        # val3 - bad deeper down (a list, but not of ints)
        # val4 - bad in both ways
        val1 = {"foo": [1, 2], "bar": [0]}
        val2 = {"foo": 1, "bar": [0]}
        val3 = {"foo": [4, 5], "bar": [1.0]}
        val4 = {"foo": "bar", "bar": [4, 2L]}

        ctx.start_context('val')
        assert ntype.check_value(val1, ctx)
        assert ctx.num_errors() == 0

        assert not ntype.check_value(val2, ctx)
        assert ctx.errors == (
            [("val['foo']", "expected list, got int (1)")])

        ctx.reset('val')
        assert not ntype.check_value(val3, ctx)
        assert ctx.errors == (
            [("val['bar'][0]", "expected int, got float (1.0)")])

        ctx.reset('val')
        assert not ntype.check_value(val4, ctx)
        ctx.errors.sort()
        assert ctx.num_errors() == 2
        assert ctx.errors[0]== (
            ("val['bar'][1]", "expected int, got long (2L)"))
        assert ctx.errors[1] == (
            ("val['foo']", "expected list, got string ('bar')"))

    def check_nested_2 (self):
        "type-checking of nested instances"

        from __main__ import Plain
        from foo import SubFoo
        from foo.bar import FooBar
        ctx = self.context
        foobar_type = self.foobar_type


        # Schema for Foo and FooBar classes (see also fake_module.py):
        # class foo.Foo:
        #     num : int = 37
        #     msg : msg = "hello!"
        #
        # class foo.bar.FooBar:
        #     a : float = 3.14159
        #     b : Foo = None

        # Get initial objects with default values -- will only check
        # foobar because foobar.b is None.
        foo = self.foo ; foobar = self.foobar
        subfoo = SubFoo()
        ctx.start_context('foobar')
        assert foobar_type.check_value(foobar, ctx)
        assert ctx.num_errors() == 0

        # Now link the two: still should not have any errors!
        foobar.b = foo
        assert foobar_type.check_value(foobar, ctx)
        assert ctx.num_errors() == 0

        # Now make 'b' an instance of SubFoo
        foobar.b = subfoo
        assert foobar_type.check_value(foobar, ctx)
        assert ctx.num_errors() == 0

        # Try a couple of different bad values for 'b'; all of these should
        # generate exactly one error, since they're not instances whose
        # class the schema knows anything about.
        foobar.b = 666
        ctx.reset(start_name='foobar')
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.errors == (
            [("foobar.b",
              "expected instance of foo.Foo, got int (666)")])

        ctx.reset(start_name='foobar')
        foobar.b = [1, 'hello']
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.errors== (
            [("foobar.b",
              "expected instance of foo.Foo, "
              "got list ([1, 'hello'])")])

        ctx.reset(start_name='foobar')
        foobar.b = Plain()
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.errors == (
            [("foobar.b",
              "expected instance of foo.Foo, got instance of Plain")
             ])

        # Back to 'b' being (properly) a Foo instance, but now give it a
        # bad attribute value or two.
        ctx.reset()
        foobar.b = foo
        foo.num = 1.0
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.format_errors() == (
            [("b.num: expected int, got float (1.0)")])

        ctx.reset()
        foo.num = FooBar()
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.format_errors() == (
            [("b.num: expected int, "
              "got instance of foo.bar.FooBar")])

        # Make sure that we check instances of known classes, even if
        # we stumble across them in the wrong place.
        ctx.reset()
        foo.num.b = 1
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.num_errors() == 2
        assert ctx.errors[0] == (
            ("b.num",
             "expected int, got instance of foo.bar.FooBar"))
        assert ctx.errors[1] == (
            ("b.num.b",
             "expected instance of foo.Foo, got int (1)"))

        # Make sure that we detect errors when foobar.b is an instance
        # of SubFoo rather than Foo directly.
        ctx.reset(start_name='foobar')
        foobar.b = subfoo
        foobar.b.num = "3"
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.errors == (
            [("foobar.b.num",
              "expected int, got string ('3')")])
        foobar.b.k = "FOO"
        ctx.reset(start_name='foobar')
        assert not foobar_type.check_value(foobar, ctx)
        assert ctx.errors == (
            [("foobar.b.num",
              "expected int, got string ('3')"),
             ("foobar.b.k",
              "expected int, got string ('FOO')")])

    def check_recursive (self):
        "type-check recursive data structures"

        from foo import TreeNode
        ctx = self.context
        tn_type = self.schema.make_instance_type(TreeNode)

        # Yawn, boring, trivial.
        parent = TreeNode()
        assert tn_type.check_value(parent, ctx)
        assert ctx.num_errors() == 0

        # Let's get a bit more interesting.
        child = TreeNode()
        parent.children.append(child)
        child.parent = parent
        assert tn_type.check_value(parent, ctx)
        assert ctx.num_errors() == 0
        assert tn_type.check_value(child, ctx)
        assert ctx.num_errors() == 0

        # Add some errors to make things more fun.
        parent.name = 666
        ctx.reset(start_name='parent')
        assert not tn_type.check_value(parent, ctx)
        assert ctx.errors == (
            [("parent.name", "expected string, got int (666)")])

        ctx.reset(start_name='parent')
        parent.name = "foo"
        child.name = 1
        assert not tn_type.check_value(parent, ctx)
        assert ctx.errors == (
            [("parent.children[0].name",
              "expected string, got int (1)")])

        ctx.reset(start_name='parent')
        child.name = child
        assert not tn_type.check_value(parent, ctx)
        assert ctx.errors == (
            [("parent.children[0].name",
              "expected string, got instance of foo.TreeNode")])

        ctx.reset(start_name='child')
        assert not tn_type.check_value(child, ctx)
        assert ctx.errors == (
            [("child.name",
              "expected string, got instance of foo.TreeNode")])

        ctx.reset(start_name='child')
        child.name = "bar"
        parent.children.append(1)
        assert not tn_type.check_value(child, ctx)
        assert ctx.errors == (
            [("child.parent.children[1]",
              "expected instance of foo.TreeNode, got int (1)")])

    # check_recursive


# class TypecheckTest


if __name__ == "__main__":
    ValueTypeTest()
    TypecheckTest()
