#!/usr/bin/env python

#
# Test script for the ValueType class family (AtomicType, InstanceType,
# etc.).  Does not test parsing of type specifications -- that's
# in test_parsetype.py.
#

__revision__ = "$Id: test_valuetype.py,v 1.49 2001/08/21 22:20:28 gward Exp $"

import string
from types import IntType, LongType, FloatType, StringType, ListType
from mems.tools.unittest import TestScenario, parse_args, run_scenarios
from grouch.schema import ClassDefinition, ObjectSchema
from grouch.valuetype import \
     AtomicType, UnionType, \
     InstanceType, ContainerType, InstanceContainerType, \
     AnyType, BooleanType, AliasType
from grouch.context import TypecheckContext

tested_modules = [ "grouch.valuetype" ]

# Set up a fake module hierarchy:
#   module 'foo' provides class Foo (and sub-module bar)
#   module 'foo.bar' provides class FooBar
import fake_module


class ValueTypeTest (TestScenario):

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

    def shutdown (self):
        del self.schema


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

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

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

        t = AtomicType(self.schema)
        self.test_exc("t.set_type('foo')", ValueError)
        self.test_exc("t.set_type('list')", ValueError)
        self.test_exc("t.set_type(ListType)", ValueError)
        self.test_val("str(t)", "*no type*")

    def check_instance (self):
        "instance types: 8"
        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)
        self.test_stmt("t.set_class_name('foo.Foo')")
        self.test_val("str(t)", "foo.Foo")
        self.test_bool("t.is_instance_type()")
        self.test_bool("t.is_plain_instance_type()")
        self.test_val("t.get_metatype()", "instance")

        t = InstanceType(self.schema)
        self.test_stmt("t.set_class_name('foo.bar.FooBar')")
        self.test_val("str(t)", "foo.bar.FooBar")

        t = InstanceType(self.schema)
        self.test_exc("t.set_class_name(FooBar)", TypeError)

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

        t = ListType(self.schema)
        self.test_exc("t.set_element_type(53)", TypeError)
        self.test_stmt("t.set_element_type('int')")
        self.test_val("str(t)", "[int]")
        self.test_val("t.element_type.type", IntType)

        t = ListType(self.schema)
        inttype = self.schema.make_atomic_type("int")
        self.test_stmt("t.set_element_type(inttype)")
        self.test_val("t.element_type", inttype)
        self.test_val("str(t)", "[int]")
        
    def check_tuple (self):
        "simple tuple types: 21"
        from grouch.valuetype import TupleType
        t = TupleType(self.schema)
        self.test_stmt("t.set_element_types((IntType,))")
        self.test_val("str(t)", "(int,)")
        self.test_val("t.element_types[0].type", IntType)
        self.test_val("t.get_element_types()[0].type", IntType)
        self.test_val("t.get_metatype()", "container")

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

        t = TupleType(self.schema)
        self.test_exc("t.set_element_types(('list',))", ValueError)
        self.test_exc("t.set_element_types((42,))", TypeError)
        self.test_exc("t.set_element_types('int')", TypeError)
        self.test_exc("t.set_element_types(42)", TypeError)
        self.test_stmt("t.set_element_types(('int',))")

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

        t = DictionaryType(self.schema)
        self.test_exc("t.set_item_types('foo', 'string')", ValueError)
        self.test_exc("t.set_item_types(('int', 'string'))", TypeError)
        self.test_exc("t.set_item_types('list', 'string')", ValueError)
        self.test_exc("t.set_item_types('int', 42)", TypeError)
        self.test_stmt("t.set_item_types(StringType, 'int')")
        self.test_val("str(t)", "{ string : int }")

        t = DictionaryType(self.schema)
        self.test_exc("t.set_item_types(IntType, 'foo')",
                      ValueError)

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

        t = UnionType(self.schema)
        self.test_exc("t.set_union_types(IntType)", TypeError)
        self.test_exc("t.set_union_types((IntType,))", TypeError)
        self.test_exc("t.set_union_types(['foo'])", ValueError)

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

        self.test_stmt("t.set_class_name('foo.Foo')")
        ctype = self.schema.make_list_type(IntType)
        self.test_stmt("t.set_container_type(ctype)")
        self.test_bool("t.is_instance_type()")
        self.test_bool("t.is_container_type()")
        self.test_bool("t.is_instance_container_type()")
        self.test_bool("t.is_plain_instance_type()", want_true=0)
        self.test_bool("t.is_plain_container_type()", want_true=0)
        self.test_val("t.get_metatype()", "instance-container")

        self.test_val("str(t)", "foo.Foo [int]")
        self.test_val("t.get_element_type().type", IntType)
        self.test_exc("t.get_element_types()", RuntimeError)
        self.test_exc("t.get_item_types()", RuntimeError)

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

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

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

        t = BooleanType(self.schema)
        self.test_bool("t.is_boolean_type()")
        self.test_bool("t.is_any_type()", want_true=0)
        self.test_val("str(t)", "boolean")
        
    def check_alias (self):
        "type aliasing: 4"
        schema = self.schema
        schema.add_alias("real", "int|float|long")

        t = schema.parse_type("real")
        self.test_bool("t.is_alias_type()")
        alias = t.get_alias_type()
        self.test_bool("alias.is_union_type()")
        self.test_val("str(t)", "real")

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

    def check_pickle (self):
        "pickling/unpickling of ValueType objects: 20"
        from pickle import dumps, loads
        
        schema = self.schema
        parse_type = schema.parse_type
        t1 = parse_type("int")
        t2 = loads(dumps(t1)) ; t2._fixup_atomic_types({})

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

        self.test_val("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
        self.test_bool("sch1.atomic_type_names == sch2.atomic_type_names")
        self.test_bool("sch1.atomic_type_objects == sch2.atomic_type_objects")
        self.test_bool("sch1.atomic_type_values == sch2.atomic_type_values")

        self.test_bool("sch2.get_type(IntType).schema is sch2")
        self.test_bool("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
        del t1.schema, t2.schema
        self.test_bool("t1.__dict__ == t2.__dict__")
        t1.schema = t1s ; t2.schema = t2s

        t1 = parse_type("[string | float]")
        t2 = loads(dumps(t1)) ; t2._fixup_atomic_types({})
        self.test_val("str(t2)", "[string | float]")

        from mx.DateTime import now
        schema = ObjectSchema()
        schema.add_alias("real", "int|long|float")
        schema.add_atomic_type(now())
        t1 = schema.parse_type("real")
        t2 = schema.parse_type("(string, DateTime)")
        
        t1b = loads(dumps(t1)) ; t1b._fixup_atomic_types({})
        t2b = loads(dumps(t2)) ; t2b._fixup_atomic_types({})

        self.test_val("str(t1b)", "real")
        self.test_bool("t1b.is_alias_type()")
        self.test_bool("t1b.get_alias_type().is_union_type()")

        self.test_val("str(t2b)", "(string, DateTime)")
        self.test_bool("t2b.is_container_type()")
        self.test_bool("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]
        self.test_bool("schema1.alias_value['real'].schema is schema1")
        self.test_bool("schema2.alias_value['real'].schema is schema2")
        self.test_bool("innertype1.schema is schema1")
        self.test_bool("innertype2.schema is schema2")

# class ValueTypeTest


class TypecheckTest (TestScenario):
    
    def setup (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 shutdown (self):
        del self.context, self.schema
        del self.foo_type, self.foo


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

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

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

        self.test_bool("t1.check_value(3, ctx)")
        self.test_bool("t1.check_value(3.0, ctx)", want_true=0)
        self.test_val("ctx.num_errors()", 1)
        self.test_val("ctx.errors",
                      [("", "expected int, got float (3.0)")])
        self.test_bool("t1.check_value(None, ctx)")
        self.test_val("ctx.visit_counts", {'atomic': {'int': 3}})

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

        self.test_bool("t1.check_value(None, ctx)")

        #t1.allow_none = 0
        #self.test_bool("t1.check_value(None, ctx)", want_true=0)

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

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

        # No errors: default values are OK
        self.test_bool("foo_type.check_value(foo, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("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()
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors", [("num", msg)])
        self.test_val("ctx.format_errors()", ["num: " + msg])

        # Two bad attributes
        ctx.reset()
        foo.num = "boo"
        foo.msg = 0
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("num", "expected int, got string ('boo')"),
                       ("msg", "expected string, got int (0)")])
        self.test_val("ctx.format_errors()",
                      ["num: expected int, got string ('boo')",
                       "msg: expected string, got int (0)"])
        self.test_val("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
        self.test_bool("foo_type.check_value(foo, ctx)")
        self.test_val("ctx.num_errors()", 0)

        # The "instance" itself is None
        self.test_bool("foo_type.check_value(None, ctx)")
        self.test_val("ctx.num_errors()", 0)

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

        class Blah: pass
        ctx.reset()
        blah = Blah()
        self.test_bool("foo_type.check_value(blah, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("",
                        "expected instance of foo.Foo, got instance of Blah"),
                       ("",
                        "found instance of class not in schema: Blah")])


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

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

        # One attribute missing
        del foo.msg
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected attribute 'msg' not found")])
        self.test_val("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
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected attribute 'num' not found"),
                       ("msg", "expected string, got int (0)")])
        self.test_val("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`
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", unexp_msg)])
        self.test_val("ctx.visit_counts",
                      {'atomic': {'int': 1, 'string': 1},
                       'instance': {'foo.Foo': 1}})

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

        # Mixups between instance and class attributes.
        from foo import Foo
        ctx.reset()
        del foo.num
        Foo.num = 37
        self.test_val("foo.num", 37)    # normal Python semantics are OK
        self.test_bool("getattr(foo, 'num')", 37)
        self.test_bool("hasattr(foo, 'num')")
        self.test_bool("foo_type.check_value(foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected attribute 'num' not found")])

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

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

        self.test_bool("foo_type.check_value(foo, ctx)")
        self.test_bool("foo_type.check_value(subfoo, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("ctx.visit_counts",
                      {'atomic': {'int': 3, 'string': 2},
                       'instance': {'foo.Foo': 2}})

        subfoo.num = "boo"
        ctx.reset(start_name='subfoo')
        self.test_bool("foo_type.check_value(subfoo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("subfoo.num", "expected int, got string ('boo')")])

        ctx.reset(start_name='subfoo')
        subfoo.num = 0
        subfoo.k = "hi"
        subfoo.i = None
        self.test_bool("foo_type.check_value(subfoo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("subfoo", "found unexpected attribute 'i': None"),
                       ("subfoo.k", "expected int, got string ('hi')")])

        ctx.reset(start_name='subfoo')
        subfoo = SubFoo()
        del subfoo.k, subfoo.num
        self.test_bool("foo_type.check_value(subfoo, ctx)", want_true=0)
        self.test_val("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()
        self.test_bool("foo_type.check_value(alt_foo, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("alt_foo",
                        "expected instance of foo.Foo, "
                        "got instance of unknown subclass foo.AltFoo")])

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

        self.test_bool("list_type.check_value([], ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("list_type.check_value([0, -34], ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("ctx.visit_counts",
                      {'container': {'list': 2},
                       'atomic': {'int': 2}})

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

        ctx.reset("L")
        self.test_bool("list_type.check_value(['foo', 0L], ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("L[0]", "expected int, got string ('foo')"),
                       ("L[1]", "expected int, got long int (0L)")])

        ctx.reset("L")
        self.test_bool("list_type.check_value((3, 4), ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("L", "expected list, got tuple ((3, 4))")])

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

        self.test_bool("tuple_type.check_value(('foo', 3), ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("ctx.visit_counts",
                      {'container': {'tuple': 1},
                       'atomic': {'string': 1, 'int': 1}})

        ctx.reset()
        self.test_bool("tuple_type.check_value(['foo', 3], ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected tuple, got list (['foo', 3])")])
        self.test_val("ctx.visit_counts",
                      {'container': {'tuple': 1}})

        ctx.reset()
        self.test_bool("tuple_type.check_value((), ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("","expected tuple of length 2, got one of length 0")])

        ctx.reset()
        self.test_bool("tuple_type.check_value(('foo', 3L), ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("[1]", "expected int, got long int (3L)")])

        ctx.reset()
        self.test_bool("tuple_type.check_value((3, 'foo'), ctx)",want_true=0)
        self.test_val("ctx.errors",
                      [("[0]", "expected string, got int (3)"),
                       ("[1]", "expected int, got string ('foo')")])

        ctx.reset()
        tuple_type.set_extended(1)
        self.test_bool("tuple_type.check_value(('foo', 3), ctx)")
        self.test_bool("tuple_type.check_value(('foo',), ctx)")
        self.test_bool("tuple_type.check_value(('foo', 3, 4, 5), ctx)")
        self.test_bool("tuple_type.check_value(('foo',''), ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("[1]", "expected int, got string ('')")])

        ctx.reset()
        self.test_bool("tuple_type.check_value((), ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("",
                        "expected tuple of at least length 1, "
                        "got one of length 0")])

        ctx.reset()
        self.test_bool("tuple_type.check_value(('foo',3,666,''), ctx)",
                       want_true=0)
        self.test_val("ctx.errors",
                      [("[3]", "expected int, got string ('')")])

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

        self.test_bool("dict_type.check_value({'foo': 3L, 'bar': 666L}, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("dict_type.check_value({}, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("ctx.visit_counts",
                      {'container': {'dictionary': 2},
                       'atomic': {'string': 2, 'long': 2}})
                
        # Bad value types
        ctx.reset()
        self.test_bool("dict_type.check_value({'foo': 3}, ctx)",
                       want_true=0)
        self.test_val("ctx.errors",
                      [("['foo']", "expected long, got int (3)")])
        self.test_val("ctx.visit_counts",
                      {'container': {'dictionary': 1},
                       'atomic': {'string': 1, 'long': 1}})

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

        # Bad key types
        ctx.reset()
        self.test_bool("dict_type.check_value({1: 3L}, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("[1] (key)", "expected string, got int (1)")])

        # A bit of each
        ctx.reset()
        self.test_bool("dict_type.check_value({1: 'foo'}, ctx)", want_true=0)
        self.test_seq("ctx.errors",
                      [("[1] (key)", "expected string, got int (1)"),
                       ("[1]", "expected long, got string ('foo')")],
                      match_order=0)

        # Same, but in different elements
        ctx.reset()
        self.test_bool("dict_type.check_value({1: 1L, 'foo': 'foo'}, ctx)",
                       want_true=0)
        self.test_seq("ctx.errors",
                      [("[1] (key)", "expected string, got int (1)"),
                       ("['foo']", "expected long, got string ('foo')")
                      ], match_order=0)
        
    def check_constraints (self):
        "type-checking with constraints (not none, not empty): 19"
        ctx = self.context
        int_type = self.schema.parse_type("int")
        int_type.allow_none = 1
        self.test_bool("int_type.check_value(None, ctx)")
        self.test_val("ctx.num_errors()", 0)

        ctx.reset()
        int_type.allow_none = 0
        self.test_bool("int_type.check_value(None, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected int, got None")])

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

        ctx.reset()
        list_type.allow_none = 0
        self.test_bool("list_type.check_value(None, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected list, got None")])
        ctx.reset()
        self.test_bool("list_type.check_value([], ctx)")
        self.test_val("ctx.num_errors()", 0)

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

        ctx.reset()
        list_type.allow_none = 1
        self.test_bool("list_type.check_value([], ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "empty list not allowed")])
        ctx.reset()
        self.test_bool("list_type.check_value(None, ctx)")
        
    def check_instcont (self):
        "type-checking instance-container types: 11"
        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"])
        
        self.test_bool("ulist_type.check_value(good_list, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("ulist_type.check_value(empty_list, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("ulist_type.check_value(bad_list, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("[0]", "expected string, got int (3)")])
        self.test_val("ctx.visit_counts",
                      {'instance-container': {'UserList.UserList': 3},
                       'container': {'list': 3},
                       'atomic': {'string': 4},
                       'any': {None: 4}})

        ctx.reset()
        self.test_bool("ulist_type.check_value(['foo'], ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected instance of UserList.UserList, "
                            "got list (['foo'])")])
        
        ctx.reset()
        self.test_bool("ulist_type.check_value('foo', ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected instance of UserList.UserList, "
                            "got string ('foo')")])
        
    def check_union (self):
        "type-checking of union types: 28"
        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([])
        self.test_bool("union_type.check_value(0, ctx)", want_true=0)
        self.test_bool("union_type.check_value('hi', ctx)", want_true=0)
        self.test_val("ctx.num_errors()", 2)
        self.test_val("ctx.errors[0][1]",
                      "no valid values for empty union type; got int (0)")
        self.test_val("ctx.errors[1][1]",
                      "no valid values for empty union type; "
                      "got string ('hi')")
        self.test_val("ctx.visit_counts",
                      {'union': {'': 2}})

        ctx.reset()
        self.test_bool("union_type.check_value(ctx, ctx)", want_true=0)
        self.test_val("ctx.num_errors()", 2)
        self.test_val("ctx.errors[0][1]",
                      "no valid values for empty union type; "
                      "got instance of grouch.context.TypecheckContext")
        self.test_val("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')])
        self.test_bool("union_type.check_value(0, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("union_type.check_value('hi', ctx)", want_true=0)
        self.test_val("ctx.num_errors()", 1)
        self.test_val("ctx.errors[0][1]", "expected int; got string ('hi')")
        self.test_val("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')])
        self.test_bool("union_type.check_value(100, ctx)")
        self.test_bool("union_type.check_value(2L**40, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("union_type.check_value(1.0, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected int or long; got float (1.0)")])
        self.test_val("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')])
        self.test_bool("union_type.check_value(100, ctx)")
        self.test_bool("union_type.check_value(2L**40, ctx)")
        self.test_bool("union_type.check_value(1.0, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("union_type.check_value('foo', ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected int, long, or float; "
                        "got string ('foo')")])

    # check_union ()

    def check_any (self):
        "type-checking of 'any' type: 19"
        ctx = self.context
        any_type = AnyType(self.schema)
        self.test_bool("any_type.check_value(3, ctx)")
        self.test_bool("any_type.check_value(3.0, ctx)") 
        self.test_bool("any_type.check_value(3L, ctx)")
        self.test_bool("any_type.check_value('three', ctx)")
        self.test_bool("any_type.check_value(self, ctx)")
        self.test_bool("any_type.check_value(self.__class__, ctx)")
        self.test_bool("any_type.check_value((self, self.check_any), ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_val("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
        self.test_bool("any_type.check_value(3, ctx)")
        self.test_bool("any_type.check_value(None, ctx)", want_true=0)
        self.test_val("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"
        self.test_bool("any_type.check_value(f, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("num", "expected int, got string ('37')")])
        self.test_val("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()
        self.test_bool("any_type.check_value(ctx, ctx)")
        self.test_val("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)
        self.test_bool("any_type.check_value(ctx, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "found instance of class not in schema: "
                            "grouch.context.TypecheckContext")])

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

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

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

    def check_alias (self):
        "type-checking of alias types: 6"
        ctx = self.context
        schema = self.schema
        alias_type = AliasType(schema)
        alias_type.set_alias_type("real", schema.parse_type("int|long|float"))
        
        self.test_bool("alias_type.check_value(100, ctx)")
        self.test_bool("alias_type.check_value(2L**40, ctx)")
        self.test_bool("alias_type.check_value(3.14159, ctx)")
        self.test_val("ctx.num_errors()", 0)
        self.test_bool("alias_type.check_value('foo', ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("", "expected int, long, or float; "
                        "got string ('foo')")])

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

        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')
        self.test_bool("ntype.check_value(val1, ctx)")
        self.test_val("ctx.num_errors()", 0)

        self.test_bool("ntype.check_value(val2, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("val['foo']", "expected list, got int (1)")])

        ctx.reset('val')
        self.test_bool("ntype.check_value(val3, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("val['bar'][0]", "expected int, got float (1.0)")])

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

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

        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')
        self.test_bool("foobar_type.check_value(foobar, ctx)")
        self.test_val("ctx.num_errors()", 0)

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

        # Now make 'b' an instance of SubFoo
        foobar.b = subfoo
        self.test_bool("foobar_type.check_value(foobar, ctx)")
        self.test_val("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')
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("foobar.b",
                        "expected instance of foo.Foo, got int (666)")])

        ctx.reset(start_name='foobar')
        foobar.b = [1, 'hello']
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("foobar.b",
                        "expected instance of foo.Foo, "
                        "got list ([1, 'hello'])")])

        ctx.reset(start_name='foobar')
        foobar.b = Plain()
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("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
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("ctx.format_errors()",
                      [("b.num: expected int, got float (1.0)")])

        ctx.reset()
        foo.num = FooBar()
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("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
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("ctx.num_errors()", 2)
        self.test_val("ctx.errors[0]",
                      ("b.num",
                       "expected int, got instance of foo.bar.FooBar"))
        self.test_val("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"
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("foobar.b.num",
                        "expected int, got string ('3')")])
        foobar.b.k = "FOO"
        ctx.reset(start_name='foobar')
        self.test_bool("foobar_type.check_value(foobar, ctx)", want_true=0)
        self.test_val("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: 16"

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

        # Yawn, boring, trivial.
        parent = TreeNode()
        self.test_bool("tn_type.check_value(parent, ctx)")
        self.test_val("ctx.num_errors()", 0)

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

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

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

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

        ctx.reset(start_name='child')
        self.test_bool("tn_type.check_value(child, ctx)", want_true=0)
        self.test_val("ctx.errors",
                      [("child.name",
                        "expected string, got instance of foo.TreeNode")])

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

    # check_recursive


# class TypecheckTest


if __name__ == "__main__":
    (scenarios, options) = parse_args()
    run_scenarios (scenarios, options)
