#!/usr/bin/env python


# pylint: disable=deprecated-method,useless-suppression

import datetime
import textwrap
import unittest

from stone.frontend.ast import (
    AstNamespace,
    AstAlias,
    AstVoidField,
    AstTagRef,
)
from stone.frontend.exception import InvalidSpec
from stone.frontend.frontend import specs_to_ir
from stone.frontend.parser import ParserFactory
from stone.ir import (
    Alias,
    is_boolean_type,
    is_integer_type,
    is_void_type,
    Nullable,
    RedactedBlot,
    RedactedHash,
    String,
    Map
)


class TestStone(unittest.TestCase):
    """
    Tests the Stone format.
    """

    def setUp(self):
        self.parser_factory = ParserFactory(debug=False)

    def test_namespace_decl(self):
        text = textwrap.dedent("""\
            namespace files
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertIsInstance(out[0], AstNamespace)
        self.assertEqual(out[0].name, 'files')

        # test starting with newlines
        text = textwrap.dedent("""\


            namespace files
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertIsInstance(out[0], AstNamespace)
        self.assertEqual(out[0].name, 'files')

    def test_comments(self):
        text = textwrap.dedent("""\
            # comment at top
            namespace files

            # another full line comment
            alias Rev = String # partial line comment

            struct S # comment before INDENT
                "Doc"
                # inner comment
                f1 UInt64 # partial line comment
            #    f2 UInt64
                # trailing comment

            struct S2 # struct def following comment
                # start with comment
                f1 String # end with partial-line comment

            # footer comment
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertIsInstance(out[0], AstNamespace)
        self.assertIsInstance(out[1], AstAlias)
        self.assertEqual(out[2].name, 'S')
        self.assertEqual(out[3].name, 'S2')

    def test_line_continuations(self):
        line_continuation_err = 'Line continuation must increment indent by 1.'

        # Test continuation in various contexts
        text = textwrap.dedent("""\
            namespace test

            alias U64 = UInt64(
                min_value=0, max_value=10)

            struct S
                val UInt64(
                    min_value=0,
                    max_value=10)
                val2 UInt64(
            # this
                # is

                    # a
                    min_value=0
                        # stress
                            # test
                    )

            route r(
                S,
                S,
                S
                )
                "Test route."
            """)
        specs_to_ir([('test.stone', text)])

        # Try over indenting
        text = textwrap.dedent("""\
                    namespace test

                    struct S
                        val UInt64(
                        # comment to throw it off
                                min_value=0)
                    """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(line_continuation_err, cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Try under indenting
        text = textwrap.dedent("""\
                            namespace test

                            struct S
                                val UInt64(
                                # comment to throw it off
                                # x2
                                min_value=0)
                            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(line_continuation_err, cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

    def test_type_args(self):
        text = textwrap.dedent("""\
            namespace test

            alias T = String(min_length=3)
            alias F = Float64(max_value=3.2e1)
            alias Numbers = List(UInt64)
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertIsInstance(out[1], AstAlias)
        self.assertEqual(out[1].name, 'T')
        self.assertEqual(out[1].type_ref.name, 'String')
        self.assertEqual(out[1].type_ref.args[1]['min_length'], 3)

        self.assertIsInstance(out[2], AstAlias)
        self.assertEqual(out[2].name, 'F')
        self.assertEqual(out[2].type_ref.name, 'Float64')
        self.assertEqual(out[2].type_ref.args[1]['max_value'], 3.2e1)

        self.assertIsInstance(out[3], AstAlias)
        self.assertEqual(out[3].name, 'Numbers')
        self.assertEqual(out[3].type_ref.name, 'List')
        self.assertEqual(out[3].type_ref.args[0][0].name, 'UInt64')

    def test_struct_decl(self):

        # test struct decl with no docs
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                quota UInt64
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')

        # test struct with only a top-level doc
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].doc, 'The space quota info for a user.')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')

        # test struct with field doc
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].doc, 'The space quota info for a user.')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')
        self.assertEqual(out[1].fields[0].doc, "The user's total quota allocation (bytes).")

        # test without newline after field doc
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].doc, 'The space quota info for a user.')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')
        self.assertEqual(out[1].fields[0].doc, "The user's total quota allocation (bytes).")

        # test with example
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=64000
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertIn('default', out[1].examples)

        # test with multiple examples
        text = textwrap.dedent("""\
            namespace files

            struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=2000000000
                example pro
                    quota=100000000000
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertIn('default', out[1].examples)
        self.assertIn('pro', out[1].examples)

        # test with inheritance
        text = textwrap.dedent("""\
            namespace test

            struct S1
                f1 UInt64

            struct S2 extends S1
                f2 String
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'S1')
        self.assertEqual(out[2].name, 'S2')
        self.assertEqual(out[2].extends.name, 'S1')

        # test with defaults
        text = textwrap.dedent("""\
            namespace ns
            struct S
                n1 Int32 = -5
                n2 Int32 = 5
                f1 Float64 = -1.
                f2 Float64 = -4.2
                f3 Float64 = -5e-3
                f4 Float64 = -5.1e-3
                f5 Float64 = 1
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'S')
        self.assertEqual(out[1].fields[0].name, 'n1')
        self.assertTrue(out[1].fields[0].has_default)
        self.assertEqual(out[1].fields[0].default, -5)
        self.assertEqual(out[1].fields[1].default, 5)
        self.assertEqual(out[1].fields[2].default, -1)
        self.assertEqual(out[1].fields[3].default, -4.2)
        self.assertEqual(out[1].fields[4].default, -5e-3)
        self.assertEqual(out[1].fields[5].default, -5.1e-3)

        # float type should always have default value in float
        api = specs_to_ir([('test.stone', text)])
        self.assertIsInstance(api.namespaces['ns'].data_type_by_name['S'].fields[6].default, float)

        # Try extending nullable type
        text = textwrap.dedent("""\
            namespace test

            struct S
                f1 String

            struct S2 extends S?
                f2 String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Reference cannot be nullable.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

    def test_struct_patch_decl(self):

        # test struct patch decl with no docs
        text = textwrap.dedent("""\
            namespace files
            patch struct QuotaInfo
                quota UInt64
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')

        # test struct patch with a top-level doc
        text = textwrap.dedent("""\
            namespace files
            patch struct QuotaInfo
                "The space quota info for a user."
                quota UInt64
            """)
        out = self.parser_factory.get_parser().parse(text)
        msg, lineno, _ = self.parser_factory.get_parser().errors[0]
        # Can't parse patch with doc-string.
        self.assertEqual(msg, "Unexpected STRING with value 'The " +
            "space quota info for a user.'.")
        self.assertEqual(lineno, 3)

        # test struct patch with field doc
        text = textwrap.dedent("""\
            namespace files
            patch struct QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertEqual(out[1].fields[0].name, 'quota')
        self.assertEqual(out[1].fields[0].type_ref.name, 'UInt64')
        self.assertEqual(out[1].fields[0].doc, "The user's total quota allocation (bytes).")

        # test with example
        text = textwrap.dedent("""\
            namespace files
            patch struct QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=64000
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertIn('default', out[1].examples)

        # test with multiple examples
        text = textwrap.dedent("""\
            namespace files
            patch struct QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=2000000000
                example pro
                    quota=100000000000
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'QuotaInfo')
        self.assertIn('default', out[1].examples)
        self.assertIn('pro', out[1].examples)

        # test with defaults
        text = textwrap.dedent("""\
            namespace ns
            patch struct S
                n1 Int32 = -5
                n2 Int32 = 5
                f1 Float64 = -1.
                f2 Float64 = -4.2
                f3 Float64 = -5e-3
                f4 Float64 = -5.1e-3
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'S')
        self.assertEqual(out[1].fields[0].name, 'n1')
        self.assertTrue(out[1].fields[0].has_default)
        self.assertEqual(out[1].fields[0].default, -5)
        self.assertEqual(out[1].fields[1].default, 5)
        self.assertEqual(out[1].fields[2].default, -1)
        self.assertEqual(out[1].fields[3].default, -4.2)
        self.assertEqual(out[1].fields[4].default, -5e-3)
        self.assertEqual(out[1].fields[5].default, -5.1e-3)

        # test no patching enumerated subtype
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File
                    folder Folder

            struct File extends Resource
                size UInt64

            struct Folder extends Resource
                icon String

            struct Deleted extends Resource
                is_folder Boolean

            patch struct Resource
                union
                    deleted Deleted
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Unexpected UNION with value 'union'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 18)

    def test_union_decl(self):
        # test union with only symbols
        text = textwrap.dedent("""\
            namespace files

            union Role
                "The role a user may have in a shared folder."

                owner
                    "Owner of a file."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'Role')
        self.assertEqual(out[1].doc, 'The role a user may have in a shared folder.')
        self.assertIsInstance(out[1].fields[0], AstVoidField)
        self.assertEqual(out[1].fields[0].name, 'owner')
        self.assertIsInstance(out[1].fields[1], AstVoidField)
        self.assertEqual(out[1].fields[1].name, 'viewer')
        self.assertIsInstance(out[1].fields[2], AstVoidField)
        self.assertEqual(out[1].fields[2].name, 'editor')

        # TODO: Test a union that includes a struct.

        text = textwrap.dedent("""\
            namespace files

            union Error
                A
                    "Variant A"
                B
                    "Variant B"
            """)
        self.parser_factory.get_parser().parse(text)

        # test with inheritance
        text = textwrap.dedent("""\
            namespace test

            union U1
                t1 UInt64

            union U2 extends U1
                t2 String
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'U1')
        self.assertEqual(out[2].name, 'U2')
        self.assertEqual(out[2].extends.name, 'U1')

    def test_union_patch_decl(self):
        # test union with only symbols
        text = textwrap.dedent("""\
            namespace files
            patch union Role
                owner
                    "Owner of a file."
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'Role')
        self.assertIsInstance(out[1].fields[0], AstVoidField)
        self.assertEqual(out[1].fields[0].name, 'owner')

        # test struct patch with a top-level doc
        text = textwrap.dedent("""\
            namespace files
            patch union Role
                "The role a user may have in a shared folder."
                owner
                    "Owner of a file."
            """)
        out = self.parser_factory.get_parser().parse(text)
        msg, lineno, _ = self.parser_factory.get_parser().errors[0]
        # Can't parse patch with doc-string.
        self.assertEqual(msg, "Unexpected STRING with value 'The " +
            "role a user may have in a shared folder.'.")
        self.assertEqual(lineno, 3)

        text = textwrap.dedent("""\
            namespace files
            patch union Error
                A
                    "Variant A"
            """)
        self.parser_factory.get_parser().parse(text)

    def test_composition(self):
        text = textwrap.dedent("""\
            namespace files

            union UploadMode
                add
                overwrite

            struct Upload
                path String
                mode UploadMode = add
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[2].name, 'Upload')
        self.assertIsInstance(out[2].fields[1].default, AstTagRef)
        self.assertEqual(out[2].fields[1].default.tag, 'add')

    def test_route_decl(self):

        # Test route definition with no docstring
        text = textwrap.dedent("""\
            namespace users

            route GetAccountInfo(Void, Void, Void)
            """)
        self.parser_factory.get_parser().parse(text)

        text = textwrap.dedent("""\
            namespace users

            struct AccountInfo
                email String

            route GetAccountInfo(AccountInfo, Void, Void)
                "Gets the account info for a user"
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].name, 'AccountInfo')
        self.assertEqual(out[2].name, 'GetAccountInfo')
        self.assertEqual(out[2].arg_type_ref.name, 'AccountInfo')
        self.assertEqual(out[2].result_type_ref.name, 'Void')
        self.assertEqual(out[2].error_type_ref.name, 'Void')

        # Test raw documentation
        text = textwrap.dedent("""\
            namespace users

            route GetAccountInfo(Void, Void, Void)
                "0

                1

                2

                3
                "
            """)
        out = self.parser_factory.get_parser().parse(text)
        self.assertEqual(out[1].doc, '0\n\n1\n\n2\n\n3\n')

        # Test deprecation
        text = textwrap.dedent("""\
            namespace test

            route old_route (Void, Void, Void) deprecated
            """)
        api = specs_to_ir([('test.stone', text)])
        r = api.namespaces['test'].route_by_name['old_route']
        self.assertIsNotNone(r.deprecated)
        self.assertIsNone(r.deprecated.by)

        # Test deprecation with target route
        text = textwrap.dedent("""\
            namespace test

            route old_route (Void, Void, Void) deprecated by new_route
            route new_route (Void, Void, Void)
            """)
        api = specs_to_ir([('test.stone', text)])
        r_old = api.namespaces['test'].route_by_name['old_route']
        r_new = api.namespaces['test'].route_by_name['new_route']
        self.assertIsNotNone(r_old.deprecated)
        self.assertEqual(r_old.deprecated.by, r_new)

        # Test deprecation with target route (more complex route names)
        text = textwrap.dedent("""\
            namespace test

            route test/old_route (Void, Void, Void) deprecated by test/new_route
            route test/new_route (Void, Void, Void)
            """)
        api = specs_to_ir([('test.stone', text)])
        r_old = api.namespaces['test'].route_by_name['test/old_route']
        r_new = api.namespaces['test'].route_by_name['test/new_route']
        self.assertIsNotNone(r_old.deprecated)
        self.assertEqual(r_old.deprecated.by, r_new)

        # Try deprecation by undefined route
        text = textwrap.dedent("""\
            namespace test

            route old_route (Void, Void, Void) deprecated by unk_route
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Undefined route 'unk_route' at version 1.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Try deprecation by struct
        text = textwrap.dedent("""\
            namespace test

            route old_route (Void, Void, Void) deprecated by S

            struct S
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("'S' must be a route.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test route with version number and deprecation
        text = textwrap.dedent("""\
            namespace test

            route get_metadata(Void, Void, Void) deprecated by get_metadata:2

            route get_metadata:2(Void, Void, Void)
            """)
        api = specs_to_ir([('test.stone', text)])
        routes = api.namespaces['test'].routes_by_name['get_metadata']
        route_v1 = routes.at_version[1]
        route_v2 = routes.at_version[2]
        self.assertEqual(route_v1.name, 'get_metadata')
        self.assertEqual(route_v1.version, 1)
        self.assertEqual(route_v2.name, 'get_metadata')
        self.assertEqual(route_v2.version, 2)
        self.assertIsNotNone(route_v1.deprecated)
        self.assertEqual(route_v1.deprecated.by, route_v2)

        # Test using string as version number
        text = textwrap.dedent("""\
            namespace test

            route get_metadata:beta(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Unexpected ID with value 'beta'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test using fraction as version number
        text = textwrap.dedent("""\
            namespace test

            route get_metadata:1.2(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Unexpected FLOAT with value 1.2.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test using zero as version number
        text = textwrap.dedent("""\
            namespace test

            route get_metadata:0(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Version number should be a positive integer.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test using negative integer as version number
        text = textwrap.dedent("""\
            namespace test

            route get_metadata:-1(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Version number should be a positive integer.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test deprecating by a route at an undefined version
        text = textwrap.dedent("""\
            namespace test

            route get_metadata(Void, Void, Void) deprecated by get_metadata:3

            route get_metadata:2(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Undefined route 'get_metadata' at version 3.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test duplicate routes of same version
        text = textwrap.dedent("""\
            namespace test

            route get_metadata:2(Void, Void, Void)

            route get_metadata:2(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Route 'get_metadata' at version 2 already defined (test.stone:3).", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 5)

        # Test user-friendly representation
        text = textwrap.dedent("""\
            namespace test

            route alpha/get_metadata(Void, Void, Void)

            route alpha/get_metadata:2(Void, Void, Void)
            """)
        api = specs_to_ir([('test.stone', text)])
        routes = api.namespaces['test'].routes_by_name['alpha/get_metadata']
        route_v1 = routes.at_version[1]
        route_v2 = routes.at_version[2]
        self.assertEqual(route_v1.name_with_version(), 'alpha/get_metadata')
        self.assertEqual(route_v2.name_with_version(), 'alpha/get_metadata:2')

    def test_alphabetizing(self):
        text1 = textwrap.dedent("""\
            namespace ns_b

            struct z
                f UInt64

            union x
                a
                b

            struct y
                f UInt64

            route b(Void, Void, Void)

            route a(Void, Void, Void)

            route c(Void, Void, Void)
            """)
        text2 = textwrap.dedent("""\
            namespace ns_a

            route d (Void, Void, Void)
            """)
        api = specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])
        assert ['ns_a', 'ns_b'] == list(api.namespaces.keys())
        ns_b = api.namespaces['ns_b']
        assert [dt.name for dt in ns_b.data_types] == ['x', 'y', 'z']
        assert [dt.name for dt in ns_b.routes] == ['a', 'b', 'c']

    def test_lexing_errors(self):
        text = textwrap.dedent("""\

            namespace users

            %

            # testing line numbers

            %

            struct AccountInfo
                email String
            """)
        parser = self.parser_factory.get_parser()
        out = parser.parse(text)
        msg, lineno = parser.lexer.errors[0]
        self.assertEqual(msg, "Illegal character '%'.")
        self.assertEqual(lineno, 4)
        msg, lineno = parser.lexer.errors[1]
        self.assertEqual(msg, "Illegal character '%'.")
        self.assertEqual(lineno, 8)
        # Check that despite lexing errors, parser marched on successfully.
        self.assertEqual(out[1].name, 'AccountInfo')

        text = textwrap.dedent("""\
            namespace test

            struct S
                # Indent below is only 3 spaces
               f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Indent is not divisible by 4.", cm.exception.msg)

    def test_parsing_errors(self):
        text = textwrap.dedent("""\

            namespace users

            strct AccountInfo
                email String
            """)
        parser = self.parser_factory.get_parser()
        parser.parse(text)
        msg, lineno, _ = parser.errors[0]
        self.assertEqual(msg, "Unexpected ID with value 'strct'.")
        self.assertEqual(lineno, 4)

        text = textwrap.dedent("""\
            namespace users

            route test_route(Blah, Blah, Blah)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Symbol 'Blah' is undefined", cm.exception.msg)

    def test_name_clash(self):
        # namespace / type clash
        text = textwrap.dedent("""\
            namespace test_namespace_test

            struct TestNamespaceTest
                str String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Name of user-defined type 'TestNamespaceTest' conflicts "
            "with name of namespace 'test_namespace_test'", cm.exception.msg)

        # namespace / route clash
        text = textwrap.dedent("""\
            namespace test_namespace_test

            route test_namespace_test(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Name of route 'test_namespace_test' conflicts "
            "with name of namespace 'test_namespace_test'", cm.exception.msg)

        # namespace / alias clash
        text = textwrap.dedent("""\
            namespace test_namespace_test

            alias TestNamespaceTest = String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Name of alias 'TestNamespaceTest' conflicts "
            "with name of namespace 'test_namespace_test'", cm.exception.msg)

        # route / type clash
        text = textwrap.dedent("""\
            namespace test_namespace

            struct TestStructTest
                str String

            route test_struct_test(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Name of route 'test_struct_test' conflicts "
            "with name of user-defined type 'TestStructTest'", cm.exception.msg)

        # alias / route clash
        text = textwrap.dedent("""\
            namespace test_namespace

            alias TestAliasTest = String

            route test_alias_test(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Name of route 'test_alias_test' conflicts "
            "with name of alias 'TestAliasTest'", cm.exception.msg)

    def test_docstrings(self):
        text = textwrap.dedent("""\
            namespace test

            # No docstrings at all
            struct E
                f String

            struct S
                "Only type doc"
                f String

            struct T
                f String
                    "Only field doc"

            union U
                "Only type doc"
                f String

            union V
                f String
                    "Only field doc"

            # Check for inherited doc
            struct W extends T
                g String
            """)
        api = specs_to_ir([('test.stone', text)])

        E_dt = api.namespaces['test'].data_type_by_name['E']
        self.assertFalse(E_dt.has_documented_type_or_fields())
        self.assertFalse(E_dt.has_documented_fields())

        S_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertTrue(S_dt.has_documented_type_or_fields())
        self.assertFalse(S_dt.has_documented_fields())

        T_dt = api.namespaces['test'].data_type_by_name['T']
        self.assertTrue(T_dt.has_documented_type_or_fields())
        self.assertTrue(T_dt.has_documented_fields())

        U_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertTrue(U_dt.has_documented_type_or_fields())
        self.assertFalse(U_dt.has_documented_fields())

        V_dt = api.namespaces['test'].data_type_by_name['V']
        self.assertTrue(V_dt.has_documented_type_or_fields())
        self.assertTrue(V_dt.has_documented_fields())

        W_dt = api.namespaces['test'].data_type_by_name['W']
        self.assertFalse(W_dt.has_documented_type_or_fields())
        self.assertFalse(W_dt.has_documented_fields())
        self.assertFalse(W_dt.has_documented_type_or_fields(), True)
        self.assertFalse(W_dt.has_documented_fields(), True)

    def test_alias(self):
        # Test aliasing to primitive
        text = textwrap.dedent("""\
            namespace test

            alias R = String
                "This is a test
                of docstrings"
            """)
        api = specs_to_ir([('test.stone', text)])
        test_ns = api.namespaces['test']
        self.assertIsInstance(test_ns.aliases[0], Alias)
        self.assertEqual(test_ns.aliases[0].name, 'R')
        self.assertIsInstance(test_ns.aliases[0].data_type, String)
        self.assertEqual(
            test_ns.aliases[0].doc, 'This is a test of docstrings')

        # Test aliasing to primitive with additional attributes and nullable
        text = textwrap.dedent("""\
            namespace test

            alias R = String(min_length=1)?
            """)
        api = specs_to_ir([('test.stone', text)])
        test_ns = api.namespaces['test']
        self.assertIsInstance(test_ns.aliases[0], Alias)
        self.assertEqual(test_ns.aliases[0].name, 'R')
        self.assertIsInstance(test_ns.aliases[0].data_type, Nullable)
        self.assertIsInstance(test_ns.aliases[0].data_type.data_type, String)

        # Test aliasing to alias
        text = textwrap.dedent("""\
            namespace test

            alias T = String
            alias R = T
            """)
        api = specs_to_ir([('test.stone', text)])
        test_ns = api.namespaces['test']
        self.assertIsInstance(test_ns.alias_by_name['T'], Alias)
        self.assertIsInstance(test_ns.alias_by_name['R'], Alias)
        self.assertIsInstance(test_ns.alias_by_name['R'].data_type, Alias)
        self.assertEqual(test_ns.alias_by_name['R'].data_type.name, 'T')

        # Test order invariance
        text = textwrap.dedent("""\
            namespace test

            alias R = T
            alias T = String
            """)
        api = specs_to_ir([('test.stone', text)])

        # Try re-definition
        text = textwrap.dedent("""\
            namespace test

            alias A = String
            alias A = UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Symbol 'A' already defined (test.stone:3).",
                      cm.exception.msg)

        # Try cyclical reference
        text = textwrap.dedent("""\
            namespace test

            alias A = B
            alias B = C
            alias C = A
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("Alias 'C' is part of a cycle.",
                      cm.exception.msg)

        # Try aliasing to alias with attributes already set.
        text = textwrap.dedent("""\
            namespace test

            alias T = String(min_length=1)
            alias R = T(min_length=1)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('Attributes cannot be specified for instantiated type',
                      cm.exception.msg)

        # Test aliasing to composite and making it nullable
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String
            alias R = S?
            """)
        api = specs_to_ir([('test.stone', text)])
        test_ns = api.namespaces['test']
        S_dt = test_ns.data_type_by_name['S']
        self.assertIsInstance(test_ns.alias_by_name['R'].data_type, Nullable)
        self.assertEqual(test_ns.alias_by_name['R'].data_type.data_type, S_dt)

        # Test aliasing to composite with attributes
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            alias R = S(min_length=1)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('Attributes cannot be specified for instantiated type',
                      cm.exception.msg)

        # Test aliasing to route
        text = textwrap.dedent("""\
            namespace test

            route test_route(Void, Void, Void)

            alias S = test_route
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("A route cannot be referenced here.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 5)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test aliasing from another namespace
        text1 = textwrap.dedent("""\
            namespace test1

            struct S
                f String
            """)
        text2 = textwrap.dedent("""\
            namespace test2

            import test1

            alias S = test1.S
            """)
        api = specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])
        test1_ns = api.namespaces['test1']
        S_dt = test1_ns.data_type_by_name['S']
        test2_ns = api.namespaces['test2']
        self.assertEqual(test2_ns.alias_by_name['S'].data_type, S_dt)

        # Try extending an alias-ed struct
        text1 = textwrap.dedent("""\
            namespace test1

            alias Z = S

            struct S
                f1 String

            struct T extends Z
                f2 String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])
        self.assertIn('A struct cannot extend an alias. Use the canonical name instead.',
                      cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Try extending an alias-ed union
        text1 = textwrap.dedent("""\
            namespace test1

            alias Z = S

            union S
                f1 String

            union T extends Z
                f2 String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])
        self.assertIn(
            'A union cannot extend an alias. Use the canonical name instead.',
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

    def test_struct_semantics(self):
        # Test field with implicit void type
        text = textwrap.dedent("""\
            namespace test

            struct S
                option_a
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Struct field 'option_a' cannot have a Void type.",
                         cm.exception.msg)

        # Test duplicate fields
        text = textwrap.dedent("""\
            namespace test

            struct A
                a UInt64
                a String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('already defined', cm.exception.msg)

        # Test duplicate field name -- earlier being in a parent type
        text = textwrap.dedent("""\
            namespace test

            struct A
                a UInt64

            struct B extends A
                b String

            struct C extends B
                a String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('already defined in parent', cm.exception.msg)

        # Test extending from wrong type
        text = textwrap.dedent("""\
            namespace test

            union A
                a

            struct B extends A
                b UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('struct can only extend another struct', cm.exception.msg)

    def test_union_semantics(self):
        # Test duplicate fields
        text = textwrap.dedent("""\
            namespace test

            union_closed A
                a UInt64
                a String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('already defined', cm.exception.msg)

        # Test duplicate field name -- earlier being in a parent type
        text = textwrap.dedent("""\
            namespace test

            union_closed A
                a UInt64

            union_closed B extends A
                b String

            union_closed C extends B
                a String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('already defined in parent', cm.exception.msg)

        # Test catch-all
        text = textwrap.dedent("""\
            namespace test

            union A
                a
                b
            """)
        api = specs_to_ir([('test.stone', text)])
        A_dt = api.namespaces['test'].data_type_by_name['A']
        # Test both ways catch-all is exposed
        self.assertEqual(A_dt.catch_all_field, A_dt._fields_by_name['other'])
        self.assertTrue(A_dt._fields_by_name['other'].catch_all)

        # Try defining a child type as closed if its parent is open
        text = textwrap.dedent("""\
            namespace test

            union A
                a

            union_closed B extends A
                b
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Union cannot be closed since parent type 'A' is open.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Try explicitly naming field "other"
        text = textwrap.dedent("""\
            namespace test

            union A
                other
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Union cannot define an 'other' field because it is reserved as "
            "the catch-all field for open unions.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)

        # Test extending from wrong type
        text = textwrap.dedent("""\
            namespace test

            struct A
                a UInt64

            union B extends A
                b UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('union can only extend another union', cm.exception.msg)

    def test_map_semantics(self):
        text = textwrap.dedent("""\
            namespace test

            alias M = Map(String, Int32)
        """)

        api = specs_to_ir([('test.stone', text)])
        m_alias = api.namespaces['test'].alias_by_name['M']
        self.assertIsInstance(m_alias, Alias)
        self.assertIsInstance(m_alias.data_type, Map)

        # maps of maps
        text = textwrap.dedent("""\
            namespace test

            alias M = Map(String, Map(String, Int32))
        """)
        api = specs_to_ir([('test.stone', text)])
        m_alias = api.namespaces['test'].alias_by_name['M']
        self.assertIsInstance(m_alias.data_type.value_data_type, Map)

        # Map type errors with 0 args
        text = textwrap.dedent("""\
            namespace test

            alias M = Map()
        """)

        # map type errors with only 1 args
        with self.assertRaises(InvalidSpec):
            specs_to_ir([('test.stone', text)])

        text = textwrap.dedent("""\
            namespace test

            alias M = Map(String)
        """)
        # map type errors with more than two args
        with self.assertRaises(InvalidSpec):
            specs_to_ir([('test.stone', text)])

        text = textwrap.dedent("""\
            namespace test

            alias M = Map(String, String, String)
        """)
        with self.assertRaises(InvalidSpec):
            specs_to_ir([('test.stone', text)])

        # map type errors when key data type is not a String
        text = textwrap.dedent("""\
            namespace test

            alias M = Map(Int32, String)
        """)
        with self.assertRaises(InvalidSpec):
            specs_to_ir([('test.stone', text)])

    def test_enumerated_subtypes(self):

        # Test correct definition
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File
                    folder Folder

            struct File extends Resource
                size UInt64

            struct Folder extends Resource
                icon String
            """)
        specs_to_ir([('test.stone', text)])

        # Test reference to non-struct
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('must be a struct', cm.exception.msg)

        # Test reference to undefined type
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('Undefined', cm.exception.msg)

        # Test reference to non-subtype
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File

            struct File
                size UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('not a subtype of', cm.exception.msg)

        # Test subtype listed more than once
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File
                    file2 File

            struct File extends Resource
                size UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn('only be specified once', cm.exception.msg)

        # Test missing subtype
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File

            struct File extends Resource
                size UInt64

            struct Folder extends Resource
                icon String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("missing 'Folder'", cm.exception.msg)

        # Test name conflict with field
        text = textwrap.dedent("""\
            namespace test

            struct Resource
                union
                    file File
                file String

            struct File extends Resource
                size UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("already defined on", cm.exception.msg)

        # Test if a leaf and its parent do not enumerate subtypes, but its
        # grandparent does.
        text = textwrap.dedent("""\
            namespace test

            struct A
                union
                    b B
                c String

            struct B extends A
                "No enumerated subtypes."

            struct C extends B
                "No enumerated subtypes."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("cannot be extended", cm.exception.msg)

    def unused_enumerated_subtypes_tests(self):
        # Currently, Stone does not allow for a struct that enumerates subtypes
        # to inherit from another struct that does. These tests only apply if
        # this restriction is removed.

        # Test name conflict with field in parent
        text = textwrap.dedent("""\
            namespace test

            struct A
                union
                    b B
                c String

            struct B extends A
                union
                    c C

            struct C extends B
                d String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("already defined in parent", cm.exception.msg)

        # Test name conflict with union field in parent
        text = textwrap.dedent("""\
            namespace test

            struct A
                union
                    b B
                c String

            struct B extends A
                union
                    b C

            struct C extends B
                d String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("already defined in parent", cm.exception.msg)

        # Test non-leaf with no enumerated subtypes
        text = textwrap.dedent("""\
            namespace test

            struct A
                union
                    b B
                c String

            struct B extends A
                "No enumerated subtypes."

            struct C extends B
                union
                    d D

            struct D extends C
                e String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn("cannot enumerate subtypes if parent", cm.exception.msg)

    def test_nullable(self):
        # Test stacking nullable
        text = textwrap.dedent("""\
            namespace test

            alias A = String?
            alias B = A?
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Cannot mark reference to nullable type as nullable.',
            cm.exception.msg)

        # Test stacking nullable
        text = textwrap.dedent("""\
            namespace test

            alias A = String?

            struct S
                f A?
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Cannot mark reference to nullable type as nullable.',
            cm.exception.msg)

        # Test extending nullable
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            struct T extends S?
                g String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Reference cannot be nullable.',
            cm.exception.msg)

    def test_forward_reference(self):
        # Test route def before struct def
        text = textwrap.dedent("""\
            namespace test

            route test_route(Void, S, Void)

            struct S
                f String
            """)
        specs_to_ir([('test.stone', text)])

        # Test extending after...
        text = textwrap.dedent("""\
            namespace test

            struct T extends S
                g String

            struct S
                f String
            """)
        specs_to_ir([('test.stone', text)])

        # Test field ref to later-defined struct
        text = textwrap.dedent("""\
            namespace test

            route test_route(Void, T, Void)

            struct T
                s S

            struct S
                f String
            """)
        specs_to_ir([('test.stone', text)])

        # Test self-reference
        text = textwrap.dedent("""\
            namespace test

            struct S
                s S?
            """)
        specs_to_ir([('test.stone', text)])

        # Test forward union ref
        text = textwrap.dedent("""\
            namespace test

            struct S
                s U = a

            union U
                a
            """)
        api = specs_to_ir([('test.stone', text)])
        self.assertTrue(api.namespaces['test'].data_types[0].fields[0].has_default)
        self.assertEqual(
            api.namespaces['test'].data_types[0].fields[0].default.union_data_type,
            api.namespaces['test'].data_types[1])
        self.assertEqual(
            api.namespaces['test'].data_types[0].fields[0].default.tag_name, 'a')

    def test_struct_patch_semantics(self):
        # Test patching normal struct
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch struct QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=2000000000
                example pro
                    quota=100000000000
            """)
        api = specs_to_ir([('test.stone', text)])

        quota_info_dt = api.namespaces['files'].data_type_by_name['QuotaInfo']
        self.assertEqual(quota_info_dt.fields[1].name, 'quota')
        self.assertTrue(is_integer_type(quota_info_dt.fields[1].data_type))
        self.assertEqual(quota_info_dt.fields[1].doc, "The user's total quota allocation (bytes).")
        self.assertEqual(quota_info_dt.get_examples()['default'].value['quota'], 2000000000)
        self.assertEqual(quota_info_dt.get_examples()['pro'].value['quota'], 100000000000)

        # Test patching inherited struct
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            struct QuotaInfoPersonal extends QuotaInfo
                "The space quota info for a personal user."
                personal_quota UInt64
                    "The user's personal quota allocation (bytes)."
            patch struct QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
                example default
                    quota=2000000000
                example pro
                    quota=100000000000
            """)
        api = specs_to_ir([('test.stone', text)])

        quota_info_dt = api.namespaces['files'].data_type_by_name['QuotaInfoPersonal']
        self.assertEqual(quota_info_dt.all_fields[1].name, 'quota')
        self.assertTrue(is_integer_type(quota_info_dt.all_fields[1].data_type))
        self.assertEqual(
            quota_info_dt.all_fields[1].doc, "The user's total quota allocation (bytes).")

        # Testing patching the parent type of an enumerated subtype.
        text = textwrap.dedent("""\
            namespace test
            struct Resource
                union
                    file File
                    folder Folder
            struct File extends Resource
                size UInt64
                    "The size of the file."
                example default
                    size=5
            struct Folder extends Resource
                icon String
                    "The name of the icon."
                example default
                    icon="My Icon"
            patch struct Resource
                is_public Boolean
                    "Whether the resource is public."
            patch struct File
                example default
                    is_public=true
            patch struct Folder
                example default
                    is_public=false
            """)
        api = specs_to_ir([('test.stone', text)])

        resource_dt = api.namespaces['test'].data_type_by_name['Resource']
        self.assertEqual(resource_dt.all_fields[0].name, 'is_public')
        self.assertTrue(is_boolean_type(resource_dt.all_fields[0].data_type))
        self.assertEqual(resource_dt.all_fields[0].doc, "Whether the resource is public.")

        file_dt = api.namespaces['test'].data_type_by_name['File']
        self.assertEqual(file_dt.all_fields[0].name, 'is_public')
        self.assertTrue(is_boolean_type(file_dt.all_fields[0].data_type))
        self.assertEqual(file_dt.all_fields[0].doc, "Whether the resource is public.")
        self.assertEqual(file_dt.get_examples()['default'].value['is_public'], True)

        folder_dt = api.namespaces['test'].data_type_by_name['Folder']
        self.assertEqual(folder_dt.all_fields[0].name, 'is_public')
        self.assertTrue(is_boolean_type(file_dt.all_fields[0].data_type))
        self.assertEqual(folder_dt.all_fields[0].doc, "Whether the resource is public.")
        self.assertEqual(folder_dt.get_examples()['default'].value['is_public'], False)

        # Try patching enumerated supertype struct with
        # nonnull field with missing example
        text = textwrap.dedent("""\
            namespace test
            struct Resource
                union
                    file File
                    folder Folder
            struct File extends Resource
                size UInt64
                    "The size of the file."
                example default
                    size=5
            struct Folder extends Resource
                icon String
                    "The name of the icon."
                example default
                    icon="My Icon"
            patch struct Resource
                is_public Boolean
                    "Whether the resource is public."
            patch struct File
                example default
                    is_public=true
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Missing field 'is_public' in example.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 14)

        # Try patching type without pre-existing type
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch struct QuotaInfoDoesNotExist
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Patch 'QuotaInfoDoesNotExist' must correspond " +
            "to a pre-existing data_type.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Try patching type with pre-existing name of different type
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch union QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Type mismatch. Patch 'QuotaInfo' corresponds to " +
            "pre-existing data_type 'QuotaInfo' (test.stone:2) that has " +
            "type other than 'union'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Try patching union_closed type with pre-existing name of union type
        text = textwrap.dedent("""\
            namespace files
            union_closed QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch union QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Type mismatch. Patch 'QuotaInfo' corresponds to " +
            "pre-existing data_type 'QuotaInfo' (test.stone:2) that has " +
            "type other than 'union_closed'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Try patching union type with pre-existing name of union_closed type
        text = textwrap.dedent("""\
            namespace files
            union QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch union_closed QuotaInfo
                quota UInt64
                    "The user's total quota allocation (bytes)."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Type mismatch. Patch 'QuotaInfo' corresponds to " +
            "pre-existing data_type 'QuotaInfo' (test.stone:2) that has " +
            "type other than 'union'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Try patching field with pre-existing name
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch struct QuotaInfo
                user_id Int32
                    "The user associated with the quota."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Patched field 'user_id' overrides " +
            "pre-existing field in 'QuotaInfo' (test.stone:4).", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 11)

        # Try patching field with example tag that doesn't exist
        text = textwrap.dedent("""\
            namespace files
            struct QuotaInfo
                "The space quota info for a user."
                user_id String
                    "The user associated with the quota."
                example default
                    user_id="1234"
                example pro
                    user_id="1234P"
            patch struct QuotaInfo
                quota UInt64
                    "The user associated with the quota."
                example doesNotExist
                    user_id="1234"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Example defined in patch 'QuotaInfo' must " +
            "correspond to a pre-existing example.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 13)

    def test_union_patch_semantics(self):
        # Test patching normal struct
        text = textwrap.dedent("""\
            namespace files
            union Role
                "The role a user may have in a shared folder."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            patch union Role
                owner
                    "Owner of a file."
            """)
        api = specs_to_ir([('test.stone', text)])

        role_info_dt = api.namespaces['files'].data_type_by_name['Role']
        self.assertEqual(role_info_dt.fields[2].name, 'owner')
        self.assertTrue(is_void_type(role_info_dt.fields[2].data_type))
        self.assertEqual(role_info_dt.fields[2].doc, "Owner of a file.")

        # Test patching inherited union
        text = textwrap.dedent("""\
            namespace files
            union Role
                "The role a user may have in a shared folder."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            union TeamRole extends Role
                "The team role a user may have in a shared folder."
                admin
                    "Admin permission."
            patch union Role
                owner
                    "Owner of a file."
            """)
        api = specs_to_ir([('test.stone', text)])

        role_info_dt = api.namespaces['files'].data_type_by_name['TeamRole']
        self.assertEqual(role_info_dt.all_fields[2].name, 'owner')
        self.assertTrue(is_void_type(role_info_dt.all_fields[2].data_type))
        self.assertEqual(role_info_dt.all_fields[2].doc, "Owner of a file.")

        # Try patching type without pre-existing type
        text = textwrap.dedent("""\
            namespace files
            union Role
                "The role a user may have in a shared folder."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            patch union RoleDoesNotExist
                owner
                    "Owner of a file."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Patch 'RoleDoesNotExist' must correspond " +
            "to a pre-existing data_type.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Try patching type with pre-existing name of different type
        text = textwrap.dedent("""\
            namespace files
            union Role
                "The role a user may have in a shared folder."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            patch struct Role
                owner String
                    "Owner of a file."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Type mismatch. Patch 'Role' corresponds to " +
            "pre-existing data_type 'Role' (test.stone:2) that has " +
            "type other than 'struct'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Try patching field with pre-existing name
        text = textwrap.dedent("""\
            namespace files
            union Role
                "The role a user may have in a shared folder."
                viewer
                    "Read only permission."
                editor
                    "Read and write permission."
            patch union Role
                viewer
                    "Owner of a file."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            api = specs_to_ir([('test.stone', text)])

        self.assertEqual("Patched field 'viewer' overrides " +
            "pre-existing field in 'Role' (test.stone:4).", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)

    def test_import(self):
        # Test field reference to another namespace
        ns1_text = textwrap.dedent("""\
            namespace ns1

            import ns2

            struct S
                f ns2.S
            """)
        ns2_text = textwrap.dedent("""\
            namespace ns2

            struct S
                f String
            """)
        specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])

        # Test incorrectly importing the current namespace
        text = textwrap.dedent("""\
            namespace test
            import test
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Cannot import current namespace.',
            cm.exception.msg)

        # Test importing a non-existent namespace
        text = textwrap.dedent("""\
            namespace test
            import missingns
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Namespace 'missingns' is not defined in any spec.",
            cm.exception.msg)

        # Test extending struct from another namespace
        ns1_text = textwrap.dedent("""\
            namespace ns1

            import ns2

            struct S extends ns2.T
                f String
            """)
        ns2_text = textwrap.dedent("""\
            namespace ns2

            struct T
                g String
            """)
        specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])

        # Test extending aliased struct from another namespace
        ns1_text = textwrap.dedent("""\
            namespace ns1

            import ns2

            struct S extends ns2.X
                f String
            """)
        ns2_text = textwrap.dedent("""\
            namespace ns2

            alias X = T

            struct T
                g String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])
        self.assertEqual(
            'A struct cannot extend an alias. Use the canonical name instead.',
            cm.exception.msg)

        # Test extending union from another namespace
        ns1_text = textwrap.dedent("""\
            namespace ns1

            import ns2

            union V extends ns2.U
                b String
            """)
        ns2_text = textwrap.dedent("""\
            namespace ns2

            union U
                a
            """)
        specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])

        # Try circular import
        ns1_text = textwrap.dedent("""\
            namespace ns1

            import ns2

            struct S
                t ns2.T
            """)
        ns2_text = textwrap.dedent("""\
            namespace ns2

            import ns1

            struct T
                s ns1.S
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])
        self.assertIn(
            "Circular import of namespaces 'ns2' and 'ns1' detected.",
            cm.exception.msg)

    def test_doc_refs(self):
        # Test union doc referencing a field
        text = textwrap.dedent("""\
            namespace test

            union U
                ":field:`a`"
                a
                b
            """)
        specs_to_ir([('test.stone', text)])

        # Test union field doc referencing another field
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                    ":field:`b`"
                b
            """)
        specs_to_ir([('test.stone', text)])

        # Test union field doc referencing a field from an imported namespace
        text1 = textwrap.dedent("""\
            namespace test1

            union U
                a
        """)
        text2 = textwrap.dedent("""\
            namespace test2
            import test1
            union U
                ":field:`test1.U.a`"
                b
        """)
        specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])

        # Test docs referencing a route
        text = textwrap.dedent("""\
            namespace test

            route test_route(Void, Void, Void)

            struct T
                "type doc ref :route:`test_route`"
                f String
                    "field doc ref :route:`test_route`"

            union U
                "type doc ref :route:`test_route`"
                f String
                    "field doc ref :route:`test_route`"
            """)
        specs_to_ir([('test.stone', text)])

        # Test docs referencing a route with version number
        text = textwrap.dedent("""\
            namespace test

            route test_route:2(Void, Void, Void)

            struct T
                "type doc ref :route:`test_route:2`"
                f String
                    "field doc ref :route:`test_route:2`"

            union U
                "type doc ref :route:`test_route:2`"
                f String
                    "field doc ref :route:`test_route:2`"
            """)
        specs_to_ir([('test.stone', text)])

        # Test referencing an undefined route
        text = textwrap.dedent("""\
            namespace test

            struct T
                "type doc ref :route:`test_route`"
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Unknown doc reference to route 'test_route'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test referencing a field as a route
        text = textwrap.dedent("""\
            namespace test

            union U
                a

            struct T
                "type doc ref :route:`U`"
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Doc reference to type 'U' is not a route.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test referencing a route at an undefined version
        text = textwrap.dedent("""\
            namespace test

            route test_route:2(Void, Void, Void)

            struct T
                "type doc ref :route:`test_route:3`"
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Doc reference to route 'test_route' has undefined version 3.",
                         cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test referencing a field of a route
        text = textwrap.dedent("""\
            namespace test

            route test_route(Void, Void, Void)

            struct T
                "type doc ref :field:`test_route.g`"
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual("Bad doc reference to field 'g' of route 'test_route'.", cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test referencing a field of alias
        text = textwrap.dedent("""\
                    namespace test

                    struct T
                        f String

                    alias A = T

                    struct B
                        "ref to alias field :field:`A.f`."
                    """)

        specs_to_ir([('test.stone', text)])

    def test_namespace(self):
        # Test that namespace docstrings are combined
        ns1_text = textwrap.dedent("""\
        namespace ns1
            "
            This is a docstring for ns1.
            "

        struct S
            f String
        """)
        ns2_text = textwrap.dedent("""\
            namespace ns1
                "
                This is another docstring for ns1.
                "

            struct S2
                f String
            """)
        api = specs_to_ir([('ns1.stone', ns1_text), ('ns2.stone', ns2_text)])
        self.assertEqual(
            api.namespaces['ns1'].doc,
            'This is a docstring for ns1.\nThis is another docstring for ns1.\n')

    def test_examples(self):

        # Test simple struct example
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_types[0]
        self.assertTrue(s_dt.get_examples()['default'], {'f': 'A'})

        # Test example with bad type
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
                    f = 5
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'f': integer is not a valid string",
            cm.exception.msg)

        # Test example with label "true". "false" and "null" are also
        # disallowed because they conflict with the identifiers for primitives.
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example true
                    f = "A"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            # This raises an unexpected token error.
            specs_to_ir([('test.stone', text)])

        # Test error case where two examples share the same label
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
                    f = "ZZZZZZ3"
                example default
                    f = "ZZZZZZ4"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Example with label 'default' already defined on line 6.",
            cm.exception.msg)

        # Test error case where an example has the same field defined twice.
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
                    f = "ZZZZZZ3"
                    f = "ZZZZZZ4"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Example with label 'default' defines field 'f' more than once.",
            cm.exception.msg)

        # Test empty examples
        text = textwrap.dedent("""\
            namespace test

            struct S

                example default
                example other
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_types[0]
        self.assertIn('default', s_dt.get_examples())
        self.assertIn('other', s_dt.get_examples())
        self.assertNotIn('missing', s_dt.get_examples())

        # Test missing field in example
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Missing field 'f' in example.",
            cm.exception.msg)

        # Test missing default example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T

                example default

            struct T
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Missing field 't' in example.",
            cm.exception.msg)

        # Test primitive field with default will use the default in the
        # example if it's missing.
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String = "S"

                example default
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_types[0]
        # Example should have no keys
        self.assertEqual(s_dt.get_examples()['default'].value['f'], 'S')

        # Test nullable primitive field missing from example
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String?

                example default
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_types[0]
        # Example should have no keys
        self.assertEqual(len(s_dt.get_examples()['default'].value), 0)

        # Test nullable primitive field explicitly set to null in example
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String?

                example default
                    f = null
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_types[0]
        # Example should have no keys
        self.assertEqual(len(s_dt.get_examples()['default'].value), 0)

        # Test non-nullable primitive field explicitly set to null in example
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

                example default
                    f = null
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'f': null is not a valid string",
            cm.exception.msg)

        # Test example of composite type
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T

                example default
                    t = default

            struct T
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'t': {'f': 'A'}})

        # Test nullable composite missing from example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T?

                example default
                    t = default

            struct T
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'t': {'f': 'A'}})

        # Test nullable composite explicitly set to null
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T?

                example default
                    t = null

            struct T
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {})

        # Test custom label
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T?

                example default
                    t = special

            struct T
                f String
                r R

                example default
                    f = "A"
                    r = default

                example special
                    f = "B"
                    r = other

            struct R
                g String

                example default
                    g = "D"

                example other
                    g = "C"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'t': {'f': 'B', 'r': {'g': 'C'}}})

        # Test missing label for composite example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T?

                example default
                    t = missing

            struct T
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Reference to example for 'T' with label 'missing' does not exist.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

        # Test missing label for composite example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T

                example default

            struct T
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Missing field 't' in example.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test bad label for composite example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T?

                example default
                    t = 34

            struct T
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 't': example must reference label of 'T'",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

        # Test solution for recursive struct
        # TODO: Omitting `s=null` will result in infinite recursion.
        text = textwrap.dedent("""\
            namespace test

            struct S
                s S?
                f String

                example default
                    f = "A"
                    s = null
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value, {'f': 'A'})

        # Test examples with inheritance trees
        text = textwrap.dedent("""\
            namespace test

            struct A
                a String

            struct B extends A
                b String

            struct C extends B
                c String

                example default
                    a = "A"
                    b = "B"
                    c = "C"
            """)
        specs_to_ir([('test.stone', text)])

        text = textwrap.dedent("""\
            namespace test

            struct A
                a String

            struct B extends A
                b String

            struct C extends B
                c String

                example default
                    b = "B"
                    c = "C"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Missing field 'a' in example.",
            cm.exception.msg)

    def test_examples_union(self):
        # Test bad example with no fields specified
        text = textwrap.dedent("""\
            namespace test

            union U
                a

                example default
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Example for union must specify exactly one tag.',
            cm.exception.msg)

        # Test bad example with more than one field specified
        text = textwrap.dedent("""\
            namespace test

            union U
                a String
                b String

                example default
                    a = "A"
                    b = "B"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            'Example for union must specify exactly one tag.',
            cm.exception.msg)

        # Test bad example with unknown tag
        text = textwrap.dedent("""\
            namespace test

            union U
                a String

                example default
                    z = "Z"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Unknown tag 'z' in example.",
            cm.exception.msg)

        # Test bad example with reference
        text = textwrap.dedent("""\
            namespace test

            union U
                a String

                example default
                    a = default
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'a': reference is not a valid string",
            cm.exception.msg)

        # Test bad example with null value for non-nullable
        text = textwrap.dedent("""\
            namespace test

            union U
                a String

                example default
                    a = null
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'a': null is not a valid string",
            cm.exception.msg)

        # Test example with null value for void type member
        text = textwrap.dedent("""\
            namespace test

            union U
                a

                example default
                    a = null
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value, {'.tag': 'a'})
        self.assertEqual(u_dt.get_examples(compact=True)['default'].value, 'a')

        # Test simple union
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                b String
                c UInt64

                example default
                    b = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value,
                         {'.tag': 'b', 'b': 'A'})
        self.assertEqual(u_dt.get_examples()['a'].value, {'.tag': 'a'})
        self.assertEqual(u_dt.get_examples(compact=True)['a'].value, 'a')
        self.assertNotIn('b', u_dt.get_examples())

        # Test union with inheritance
        text = textwrap.dedent("""\
            namespace test

            union U
                a String

            union V extends U
                b String

                example default
                    a = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        v_dt = api.namespaces['test'].data_type_by_name['V']
        self.assertEqual(v_dt.get_examples()['default'].value,
                         {'.tag': 'a', 'a': 'A'})

        # Test union and struct
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                s S

                example default
                    s = default

                example opt
                    s = opt

            struct S
                f String

                example default
                    f = "F"

                example opt
                    f = "O"
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value,
                         {'.tag': 's', 'f': 'F'})
        self.assertEqual(u_dt.get_examples()['opt'].value,
                         {'.tag': 's', 'f': 'O'})
        self.assertEqual(list(u_dt.get_examples()['default'].value.keys())[0],
                         '.tag')

        # Test union referencing non-existent struct example
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                s S

                example default
                    s = missing

            struct S
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Reference to example for 'S' with label 'missing' does not exist.",
            cm.exception.msg)

        # Test fallback to union void member
        text = textwrap.dedent("""\
            namespace test

            struct S
                u U

                example default
                    u = a

            union U
                a
                b
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'u': {'.tag': 'a'}})
        self.assertEqual(s_dt.get_examples(compact=True)['default'].value,
                         {'u': 'a'})

        # Test fallback to union member of composite type
        text = textwrap.dedent("""\
            namespace test

            struct S
                u U

                example default
                    u = default

            union U
                a
                b S2?

                example default
                    b = default

            struct S2
                f String

                example default
                    f = "F"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'u': {'.tag': 'b', 'f': 'F'}})

        # Test TagRef
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                b

            struct S
                u U = a

                example default
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'u': {'.tag': 'a'}})
        self.assertEqual(s_dt.get_examples(compact=True)['default'].value,
                         {'u': 'a'})

        # Try TagRef to non-void option
        text = textwrap.dedent("""\
            namespace test

            union U
                a UInt64
                b

            struct S
                u U = a

                example default
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Field 'u' has an invalid default: invalid reference to non-void option 'a'",
            cm.exception.msg)

        # Try TagRef to non-existent option
        text = textwrap.dedent("""\
            namespace test

            union U
                a UInt64
                b

            struct S
                u U = c

                example default
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Field 'u' has an invalid default: invalid reference to unknown tag 'c'",
            cm.exception.msg)

        # Test bad void union member example value
        text = textwrap.dedent("""\
            namespace test

            union U
                a

                example default
                    a = false
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'a': example of void type must be null",
            cm.exception.msg)

    def test_examples_text(self):
        # Test multi-line example text (verify it gets unwrapp-ed)
        text = textwrap.dedent("""\
            namespace test

            struct S
                a String

                example default
                    "This is the text for the example.
                    And I guess it's kind of long."
                    a = "Hello, World."
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        example = s_dt.get_examples()['default']
        self.assertEqual(
            example.text,
            "This is the text for the example. And I guess it's kind of long.")

        # Test union example
        text = textwrap.dedent("""\
            namespace test

            union U
                a
                b String

                example default
                    "This is the text for the example.
                    And I guess it's kind of long."
                    b = "Hi, World."
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        example = u_dt.get_examples()['default']
        self.assertEqual(
            example.text,
            "This is the text for the example. And I guess it's kind of long.")

    def test_examples_enumerated_subtypes(self):
        # Test missing custom example
        text = textwrap.dedent("""\
            namespace test

            struct S
                t T

                example other

            struct T
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Missing field 't' in example.",
            cm.exception.msg)

        # Test with two subtypes referenced
        text = textwrap.dedent("""\
            namespace test

            struct R
                union
                    s S
                    t T
                a String

                example default
                    s = default
                    t = default

            struct S extends R
                b String

            struct T extends R
                c String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Example for struct with enumerated subtypes must only specify one subtype tag.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)

        # Test bad subtype reference
        text = textwrap.dedent("""\
            namespace test

            struct R
                union
                    s S
                    t T
                a String

                example default
                    s = 34

            struct S extends R
                b String

            struct T extends R
                c String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Example of struct with enumerated subtypes must be a reference "
            "to a subtype's example.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Test unknown subtype
        text = textwrap.dedent("""\
            namespace test

            struct R
                union
                    s S
                    t T
                a String

                example default
                    z = default

            struct S extends R
                b String

            struct T extends R
                c String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Unknown subtype tag 'z' in example.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Test correct example of enumerated subtypes
        text = textwrap.dedent("""\
            namespace test

            struct R
                union
                    s S
                    t T
                a String

                example default
                    s = default

            struct S extends R
                b String

                example default
                    a = "A"
                    b = "B"

            struct T extends R
                c String
            """)
        api = specs_to_ir([('test.stone', text)])
        r_dt = api.namespaces['test'].data_type_by_name['R']
        self.assertEqual(r_dt.get_examples()['default'].value,
                         {'.tag': 's', 'a': 'A', 'b': 'B'})
        self.assertEqual(list(r_dt.get_examples()['default'].value.keys())[0],
                         '.tag')

        # Test missing custom example
        text = textwrap.dedent("""\
            namespace test

            struct R
                union
                    s S
                    t T
                a String

                example default
                    s = default

            struct S extends R
                b String

            struct T extends R
                c String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Reference to example for 'S' with label 'default' does not exist.",
            cm.exception.msg)

    def test_examples_list(self):

        # Test field of list of primitives with bad example
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(String)

                example default
                    l = "a"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'l': string is not a valid list",
            cm.exception.msg)

        # Test field of list of primitives
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(String)

                example default
                    l = ["a", "b", "c"]
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': ['a', 'b', 'c']})

        # Test nullable field of list of primitives
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(String)?
                l2 List(String)?

                example default
                    l = ["a", "b", "c"]
                    l2 = null
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': ['a', 'b', 'c']})

        # Test field of list of nullable primitives
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(String?)

                example default
                    l = ["a", null, "c"]
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': ['a', None, 'c']})

        # Test example of list of composite types with bad example
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(T)

                example default
                    l = default

            struct T
                f String

                example default
                    f = "A"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'l': reference is not a valid list",
            cm.exception.msg)

        # Test example of list of composite types
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(T)
                l2 List(T)?
                l3 List(T)?

                example default
                    l = [default, default]
                    l2 = [default]
                    l3 = null

            struct T
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': [{'f': 'A'}, {'f': 'A'}],
                          'l2': [{'f': 'A'}]})

        # Test example of list of nullable composite types
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(T?)

                example default
                    l = [default, null]

            struct T
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': [{'f': 'A'}, None]})

        # Test example of list of list of primitives
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(List(String))

                example default
                    l = [["a", "b"], [], ["z"]]
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'l': [['a', 'b'], [], ["z"]]})

        # Test example of list of list of primitives with parameterization
        text = textwrap.dedent("""\
            namespace test

            struct S
                l List(List(String, max_items=1))

                example default
                    l = [["a", "b"], [], ["z"]]
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'l': list has more than 1 item(s)",
            cm.exception.msg)

        # Test union with list (bad example)
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(String)

                example default
                    a = "hi"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'a': string is not a valid list",
            cm.exception.msg)

        # Test union with list of primitives
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(String)

                example default
                    a = ["hello", "world"]
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {".tag": "a", 'a': ["hello", "world"]})

        # Test union with list of composites
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(S)
                b List(S)?

                example default
                    a = [default, default]

                example default_b
                    b = [default]

            struct S
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        s_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(s_dt.get_examples()['default'].value,
                         {'.tag': 'a', 'a': [{'f': 'A'}, {'f': 'A'}]})
        self.assertEqual(s_dt.get_examples()['default_b'].value,
                         {'.tag': 'b', 'b': [{'f': 'A'}]})

        # Test union with list of lists of composites
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(List(S))

                example default
                    a = [[default]]

            struct S
                f String

                example default
                    f = "A"
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value,
                         {'.tag': 'a', 'a': [[{'f': 'A'}]]})

        # Test union with list of list of primitives
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(List(String))

                example default
                    a = [["hello", "world"]]
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value,
                         {'.tag': 'a', 'a': [['hello', 'world']]})

        # Test union with list of primitives
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(List(String))

                example default
                    a = 42
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Bad example for field 'a': integer is not a valid list",
            cm.exception.msg)

        # Test union with list of list of structs
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(List(S))

                example default
                    a = [[default, special]]

            struct S
                a UInt64

                example default
                    a = 42

                example special
                    a = 100
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(u_dt.get_examples()['default'].value,
                         {'.tag': 'a', 'a': [[{'a': 42}, {'a': 100}]]})

        # Test union with list of list of unions
        text = textwrap.dedent("""\
            namespace test

            union U
                a List(List(V))

                example default
                    a = [[default, special, x]]

            union V
                x
                y UInt64

                example default
                    x = null

                example special
                    y = 100
            """)
        api = specs_to_ir([('test.stone', text)])
        u_dt = api.namespaces['test'].data_type_by_name['U']
        self.assertEqual(
            u_dt.get_examples()['default'].value,
            {'.tag': 'a', 'a': [[{'.tag': 'x'}, {'.tag': 'y', 'y': 100}, {'.tag': 'x'}]]})

    def test_examples_map(self):
        # valid simple example
        text = textwrap.dedent("""\
        namespace test

        struct S
            m Map(String, Int32)

            example default
                m = {"one": 1, "two": 2}
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        # complex stone example
        text = textwrap.dedent("""\
            namespace test

            alias m = Map(String, Int32)
            alias mm = Map(String, m)

            struct S
                arg mm
                    "hash of hashes"

                example default
                    arg = {"key": {"one": 1}, "another_key" : {"two" : 2, "three": 3}}
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        # map of structs
        text = textwrap.dedent("""\
            namespace test

            struct Substruct
                m2 Map(String, Int32)

                example example_ref
                    m2 = {"one": 1, "two": 2}

            struct S
                m Map(String, Substruct)

                example default
                    m = {"key": example_ref, "another_key": example_ref}
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        # error when example doesn't match definition
        text = textwrap.dedent("""\
            namespace test

            struct S
                m Map(String, String)

                example default
                    m = {"one": 1}
        """)

        with self.assertRaises(InvalidSpec):
            specs_to_ir([('test.stone', text)])

        # test multiline docstrings
        text = textwrap.dedent("""\
            namespace test

            struct S
                m Map(String, Int32)

                example default
                    m = {
                        "one": 1, "two": 2
                    }
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        text = textwrap.dedent("""\
            namespace test

            struct S
                m Map(String, Int32)

                example default
                    m =
                        {
                            "one": 1, "two": 2
                        }
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        text = textwrap.dedent("""\
            namespace test

            struct S
                m Map(String, Int32)

                example default
                    m =
                        {
                            "one": 1,
                            "two": 2
                        }
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

        text = textwrap.dedent("""\
            namespace test

            struct S
                m Map(String, Map(String, Int32))

                example default
                    m = {
                        "one": {
                            "one": 11,
                            "two": 12
                        },
                        "two": {
                            "one": 21,
                            "two": 22
                        }
                    }
        """)

        api = specs_to_ir([('test.stone', text)])
        s = api.namespaces['test'].data_type_by_name['S']
        self.assertIsInstance(s.get_examples()['default'].value, dict)

    def test_name_conflicts(self):
        # Test name conflict in same file
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            struct S
                g String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Symbol 'S' already defined (test.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test name conflict by route
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            route S (Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Symbol 'S' already defined (test.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test name conflict by union
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            union S
                g String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Symbol 'S' already defined (test.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test name conflict by alias
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            alias S = UInt64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Symbol 'S' already defined (test.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test name from two specs that are part of the same namespace
        text1 = textwrap.dedent("""\
            namespace test

            struct S
                f String
            """)
        text2 = textwrap.dedent("""\
            namespace test


            struct S
                f String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test1.stone', text1), ('test2.stone', text2)])
        self.assertEqual(
            "Symbol 'S' already defined (test1.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)

    def test_imported_namespaces(self):
        text1 = textwrap.dedent("""\
            namespace ns1
            struct S1
                f1 String
            struct S2
                f2 String
            alias Iso8601 = Timestamp("%Y-%m-%dT%H:%M:%SZ")
            """)
        text2 = textwrap.dedent("""\
            namespace ns2
            import ns1
            struct S3
                f3 String
                f4 ns1.Iso8601?
                f5 ns1.S1?
                example default
                    f3 = "hello"
                    f4 = "2015-05-12T15:50:38Z"
            route r1(ns1.S1, ns1.S2, S3)
            """)
        api = specs_to_ir([('ns1.stone', text1), ('ns2.stone', text2)])
        self.assertEqual(api.namespaces['ns2'].get_imported_namespaces(),
                         [api.namespaces['ns1']])
        xs = api.namespaces['ns2'].get_route_io_data_types()
        xs = sorted(xs, key=lambda x: x.name.lower())
        self.assertEqual(len(xs), 3)

        ns1 = api.namespaces['ns1']
        ns2 = api.namespaces['ns2']

        self.assertEqual(xs[0].namespace, ns1)
        self.assertEqual(xs[1].namespace, ns1)

        s3_dt = ns2.data_type_by_name['S3']
        self.assertEqual(s3_dt.fields[2].data_type.data_type.namespace, ns1)
        self.assertEqual(xs[2].name, 'S3')

    def test_namespace_obj(self):
        text = textwrap.dedent("""\
            namespace ns1
            struct S1
                f1 String
            struct S2
                f2 String
                s3 S3
            struct S3
                f3 String
            struct S4
                f4 String
            alias A = S2
            route r(S1, List(S4?)?, A)
            """)
        api = specs_to_ir([('ns1.stone', text)])
        ns1 = api.namespaces['ns1']

        # Check that all data types are defined
        self.assertIn('S1', ns1.data_type_by_name)
        self.assertIn('S2', ns1.data_type_by_name)
        self.assertIn('S3', ns1.data_type_by_name)
        self.assertIn('S4', ns1.data_type_by_name)
        self.assertEqual(len(ns1.data_types), 4)

        # Check that route is defined
        self.assertIn('r', ns1.route_by_name)
        self.assertEqual(len(ns1.routes), 1)

        s1 = ns1.data_type_by_name['S1']
        a = ns1.alias_by_name['A']
        s3 = ns1.data_type_by_name['S3']
        s4 = ns1.data_type_by_name['S4']
        route_data_types = ns1.get_route_io_data_types()

        self.assertIn(s1, route_data_types)
        # Test that aliased reference is included
        self.assertIn(a, route_data_types)
        # Test that field type is not present
        self.assertNotIn(s3, route_data_types)
        # Check that type that is wrapped by a list and/or nullable is present
        self.assertIn(s4, route_data_types)

    def test_whitespace(self):
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String
            ++++
                g Int64
            ++++
                example default
                    f = "hi"
            ++++++++
                    g = 3

            route r(Void, S, Void)
            """).replace('+', ' ')
        specs_to_ir([('ns1.stone', text)])

        text = textwrap.dedent("""\
            namespace test

            struct S
                f String
            ++++
                g Int64
            ++++
                example default
                    f = "hi"
            ++++
            ++++++
                    g = 3

            route r(Void, S, Void)
            """).replace('+', ' ')
        specs_to_ir([('ns1.stone', text)])

        text = textwrap.dedent("""\
            namespace test

                # weirdly indented comment
            struct S
               # weirdly indented comment
                f String
                g Int64

                example default
                    f = "hi"
                       # weirdly indented comment
                    g = 3

            route r(Void, S, Void)
            """)
        specs_to_ir([('ns1.stone', text)])

    def test_route_attrs_schema(self):

        # Try to define route in stone_cfg
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String

            route r(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('stone_cfg.stone', stone_cfg_text)])
        self.assertEqual(
            'No routes can be defined in the stone_cfg namespace.',
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)
        self.assertEqual(cm.exception.path, 'stone_cfg.stone')

        # Try to set bad type for schema
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
                attrs
                    f1 = 3
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            'integer is not a valid string',
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Try missing attribute for route
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            "Route does not define attr key 'f1'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 2)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test missing attribute for route attribute with default
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String = "yay"
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
            """)
        api = specs_to_ir([
            ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        ns1 = api.namespaces['test']
        self.assertEqual(ns1.route_by_name['r1'].attrs['f1'], 'yay')

        # Test missing attribute for route attribute with optional
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String?
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
            """)
        api = specs_to_ir([
            ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        test = api.namespaces['test']
        self.assertEqual(test.route_by_name['r1'].attrs['f1'], None)

        # Test unknown route attributes
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 String?
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
                attrs
                    f2 = 3
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            "Route attribute 'f2' is not defined in 'stone_cfg.Route'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test no route attributes defined at all
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
                attrs
                    f1 = 3
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', test_text)])
        self.assertEqual(
            "Route attribute 'f1' is not defined in 'stone_cfg.Route'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 4)
        self.assertEqual(cm.exception.path, 'test.stone')

        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            struct Route
                f1 Boolean
                f2 Bytes
                f3 Float64
                f4 Int64
                f5 String
                f6 Timestamp("%Y-%m-%dT%H:%M:%SZ")
                f7 S
                f8 T
                f9 S?
                f10 T
                f11 S?

            alias S = String
            alias T = String?
            """)
        test_text = textwrap.dedent("""\
            namespace test
            route r1(Void, Void, Void)
                attrs
                    f1 = true
                    f2 = "asdf"
                    f3 = 3.2
                    f4 = 10
                    f5 = "Hello"
                    f6 = "2015-05-12T15:50:38Z"
                    f7 = "World"
                    f8 = "World"
                    f9 = "World"
            """)
        api = specs_to_ir([
            ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        test = api.namespaces['test']
        attrs = test.route_by_name['r1'].attrs
        self.assertEqual(attrs['f1'], True)
        self.assertEqual(attrs['f2'], b'asdf')
        self.assertEqual(attrs['f3'], 3.2)
        self.assertEqual(attrs['f4'], 10)
        self.assertEqual(attrs['f5'], 'Hello')
        self.assertEqual(
            attrs['f6'], datetime.datetime(2015, 5, 12, 15, 50, 38))
        self.assertEqual(attrs['f7'], 'World')
        self.assertEqual(attrs['f8'], 'World')
        self.assertEqual(attrs['f9'], 'World')
        self.assertEqual(attrs['f10'], None)
        self.assertEqual(attrs['f11'], None)

        # Try defining an attribute twice.
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            import test

            struct Route
                f1 String
            """)
        test_text = textwrap.dedent("""\
            namespace test

            route r1(Void, Void, Void)
                attrs
                    f1 = "1"
                    f1 = "2"
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            "Attribute 'f1' defined more than once.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Test union type
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            import test

            struct Route
                f1 test.U
            """)
        test_text = textwrap.dedent("""\
            namespace test

            union U
                a
                b

            route r1(Void, Void, Void)
                attrs
                    f1 = a
            """)
        specs_to_ir([
            ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])

        # Try union type with bad attribute
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            import test

            struct Route
                f1 test.U
            """)
        test_text = textwrap.dedent("""\
            namespace test

            union U
                a
                b

            route r1(Void, Void, Void)
                attrs
                    f1 = 3
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            "Expected union tag as value.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)
        self.assertEqual(cm.exception.path, 'test.stone')

        # Try union type attribute with non-void tag set
        stone_cfg_text = textwrap.dedent("""\
            namespace stone_cfg

            import test

            struct Route
                f1 test.U
            """)
        test_text = textwrap.dedent("""\
            namespace test

            union U
                a
                b String

            route r1(Void, Void, Void)
                attrs
                    f1 = b
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([
                ('stone_cfg.stone', stone_cfg_text), ('test.stone', test_text)])
        self.assertEqual(
            "invalid reference to non-void option 'b'",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)
        self.assertEqual(cm.exception.path, 'test.stone')

    def test_inline_type_def(self):
        text = textwrap.dedent("""\
            namespace test

            struct Photo

                dimensions Dimensions
                    "Dimensions for a photo."
                    struct
                        height UInt64
                            "Height of the photo."
                        width UInt64
                            "Width of the photo."

                        example default
                            height = 5
                            width = 10

                location GpsCoordinates?
                    struct
                        latitude Float64
                        longitude Float64

                        example default
                            latitude = 37.23
                            longitude = 122.2

                time_taken Int64
                    "The timestamp when the photo was taken."

                example default
                    "A typical photo"
                    dimensions = default
                    location = default
                    time_taken = 100


            union E
                e1
                e2 E2
                    "Test E2."
                    union
                        a
                        b

            route r(Void, Photo, E)
            """)
        specs_to_ir([('ns1.stone', text)])

        text = textwrap.dedent("""\
            namespace test

            struct T
                g Int64

            struct S
                f T
                    "Dimensions for a photo or video."
                    struct
                        a String
                        b Int64
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('ns1.stone', text)])
        self.assertEqual(
            "Symbol 'T' already defined (ns1.stone:3).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)
        self.assertEqual(cm.exception.path, 'ns1.stone')

    def test_annotations(self):
        # Test non-existant annotation type
        text = textwrap.dedent("""\
            namespace test

            annotation Broken = NonExistantType()
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Annotation type 'NonExistantType' does not exist",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test annotation that refers to something of the wrong type
        text = textwrap.dedent("""\
            namespace test

            struct ItsAStruct
                f String

            annotation NonExistant = ItsAStruct()
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "'ItsAStruct' is not an annotation type",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test non-existant annotation
        text = textwrap.dedent("""\
            namespace test

            struct S

                f String
                    @NonExistant
                    "Test field with a non-existant tag."
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Annotation 'NonExistant' does not exist.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test omission tag
        text = textwrap.dedent("""\
            namespace test

            annotation InternalOnly = Omitted("internal")

            struct S

                f String
                    @InternalOnly
                    "Test field with one omitted tag."

            """)
        api = specs_to_ir([('test.stone', text)])

        s = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s.all_fields[0].name, 'f')
        self.assertEqual(s.all_fields[0].omitted_caller, 'internal')

        # Test applying two omission tags to one field
        text = textwrap.dedent("""\
            namespace test

            annotation InternalOnly = Omitted("internal")
            annotation AlphaOnly = Omitted("alpha_only")

            struct S

                f String
                    @AlphaOnly
                    @InternalOnly
                    "Test field with two omitted tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Omitted caller already set as 'alpha_only'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test deprecated tag
        text = textwrap.dedent("""\
            namespace test

            annotation Deprecated = Deprecated()

            struct S

                f String
                    @Deprecated
                    "Test field with one deprecated tag."

            """)
        api = specs_to_ir([('test.stone', text)])

        s = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s.all_fields[0].name, 'f')
        self.assertEqual(s.all_fields[0].deprecated, True)
        self.assertIn('Field is deprecated.', s.all_fields[0].doc)

        # Test applying two deprecated tags to one field
        text = textwrap.dedent("""\
            namespace test

            annotation Deprecated = Deprecated()

            struct S

                f String
                    @Deprecated
                    @Deprecated
                    "Test field with two deprecated tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Deprecated value already set as 'True'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

        # Test preview tag
        text = textwrap.dedent("""\
            namespace test

            annotation Preview = Preview()

            struct S

                f String
                    @Preview
                    "Test field with one preview tag."

            """)
        api = specs_to_ir([('test.stone', text)])

        s = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s.all_fields[0].name, 'f')
        self.assertEqual(s.all_fields[0].preview, True)
        self.assertIn('Field is in preview mode - do not rely on in production.',
                      s.all_fields[0].doc)

        # Test applying two preview tags to one field
        text = textwrap.dedent("""\
            namespace test

            annotation Preview = Preview()

            struct S

                f String
                    @Preview
                    @Preview
                    "Test field with two preview tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Preview value already set as 'True'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

        # Test applying both preview and deprecated (preview then deprecated)
        text = textwrap.dedent("""\
            namespace test

            annotation Preview = Preview()
            annotation Deprecated = Deprecated()

            struct S

                f String
                    @Preview
                    @Deprecated
                    "Test field with deprecated and preview tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn(
            "'Deprecated' and 'Preview' can't both be set.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test applying both preview and deprecated (deprecated then preview)
        text = textwrap.dedent("""\
            namespace test

            annotation Preview = Preview()
            annotation Deprecated = Deprecated()

            struct S

                f String
                    @Deprecated
                    @Preview
                    "Test field with deprecated and preview tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn(
            "'Deprecated' and 'Preview' can't both be set.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test redacted blot tag
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedBlot("test_regex")

            struct S

                f String
                    @FieldRedactor
                    "Test field with one redacted blot tag."

            """)
        api = specs_to_ir([('test.stone', text)])

        s = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s.all_fields[0].name, 'f')
        self.assertTrue(isinstance(s.all_fields[0].redactor, RedactedBlot))
        self.assertEqual(s.all_fields[0].redactor.regex, "test_regex")

        # Test applying two redacted blot tags to one field
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedBlot("test_regex")
            annotation AnotherFieldRedactor = RedactedBlot("another_test_regex")

            struct S

                f String
                    @FieldRedactor
                    @AnotherFieldRedactor
                    "Test field with two redacted blot tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn(
            "Redactor already set as \"RedactedBlot",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test redacted hash tag
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            struct S

                f UInt32
                    @FieldRedactor
                    "Test field with one redacted hash tag."

            """)
        api = specs_to_ir([('test.stone', text)])

        s = api.namespaces['test'].data_type_by_name['S']
        self.assertEqual(s.all_fields[0].name, 'f')
        self.assertTrue(isinstance(s.all_fields[0].redactor, RedactedHash))
        self.assertEqual(s.all_fields[0].redactor.regex, "test_regex")

        # Test applying two redacted blot tags to one field
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")
            annotation AnotherFieldRedactor = RedactedHash("another_test_regex")

            struct S

                f String
                    @FieldRedactor
                    @AnotherFieldRedactor
                    "Test field with two redacted hash tags."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn(
            "Redactor already set as \"RedactedHash",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test redacted blot tag on alias
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedBlot("test_regex")

            alias TestAlias = UInt32
                @FieldRedactor
            """)
        api = specs_to_ir([('test.stone', text)])

        alias = api.namespaces['test'].alias_by_name['TestAlias']
        self.assertTrue(isinstance(alias.redactor, RedactedBlot))
        self.assertEqual(alias.redactor.regex, "test_regex")

        # Test redacted hash tag on alias
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            alias TestAlias = String
                @FieldRedactor
            """)
        api = specs_to_ir([('test.stone', text)])

        alias = api.namespaces['test'].alias_by_name['TestAlias']
        self.assertTrue(isinstance(alias.redactor, RedactedHash))
        self.assertEqual(alias.redactor.regex, "test_regex")

        # Test deprecated tag (non-redact) tag on alias
        text = textwrap.dedent("""\
            namespace test

            annotation Deprecated = Deprecated()

            alias TestAlias = String
                @Deprecated
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertIn(
            "Aliases only support 'Redacted' and custom annotations, not",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 5)

        # Test applying redactor tag to non-String/numeric field
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            struct T
                f String

            struct S

                f T?
                    @FieldRedactor
                    "Test field with non-String type."

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Redactors can't be applied to user-defined or void types.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 10)

        # Test applying redactor tag to non-String/numeric alias
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            struct T
                f String

            alias B = T
            alias A = B
                @FieldRedactor

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Redactors can't be applied to user-defined or void types.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)

        # Test applying redactor tag to alias to alias which already has redactor tag
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            alias B = String
                @FieldRedactor

            alias A = B
                @FieldRedactor

            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "A redactor has already been defined for 'A' by 'B'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test applying redactor tag to alias field
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            alias A = String

            struct T
                f A
                    @FieldRedactor
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Redactors can only be applied to alias definitions, not to alias references.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 8)

        # Test applying redactor tag list with user-defined object
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            struct S
                f String

            struct T
                f List(S)
                    @FieldRedactor
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Redactors can't be applied to user-defined or void types.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)

        # Test applying redactor tag to map with user-defined object
        text = textwrap.dedent("""\
            namespace test

            annotation FieldRedactor = RedactedHash("test_regex")

            struct S
                f String

            struct T
                f Map(String, S)
                    @FieldRedactor
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Redactors can't be applied to user-defined or void types.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 9)

    def test_custom_annotations(self):
        # Test annotation type with non-primitive parameter
        text = textwrap.dedent("""\
            namespace test

            struct S
                f String

            annotation_type AT
                s S
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Parameter 's' must have a primitive type (possibly nullable).",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 7)

        # Test annotation type with an annotation applied to a parameter
        text = textwrap.dedent("""\
            namespace test

            annotation Deprecated = Deprecated()

            annotation_type AT
                s String
                    @Deprecated
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Annotations cannot be applied to parameters of annotation types",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 6)

        # Test redefining built-in annotation types
        text = textwrap.dedent("""\
            namespace test

            annotation_type Deprecated
                s String
            """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Cannot redefine built-in annotation type 'Deprecated'.",
            cm.exception.msg)
        self.assertEqual(cm.exception.lineno, 3)

        # Test not mixing keyword and positional arguments
        text = textwrap.dedent("""\
            namespace test
            annotation_type Important
                owner String
                importance String = "very"

            annotation VeryImportant = Important("test-team", importance="very")
        """)
        with self.assertRaises(InvalidSpec) as cm:
            specs_to_ir([('test.stone', text)])
        self.assertEqual(
            "Annotations accept either positional or keyword arguments, not both",
            cm.exception.msg,
        )
        self.assertEqual(cm.exception.lineno, 6)

        # Test custom annotation type, instance, and application
        text = textwrap.dedent("""\
            namespace test

            annotation_type Important
                owner String
                importance String = "very"

            annotation VeryImportant = Important("test-team")
            annotation SortaImportant = Important(owner="test-team", importance="sorta")

            alias TestAlias = String
                @VeryImportant

            struct TestStruct
                f String
                    @SortaImportant
                g List(TestAlias)
            """)
        api = specs_to_ir([('test.stone', text)])

        annotation_type = api.namespaces['test'].annotation_type_by_name['Important']
        self.assertEqual(len(annotation_type.params), 2)
        self.assertEqual(annotation_type.params[0].name, 'owner')
        self.assertFalse(annotation_type.params[0].has_default)
        self.assertTrue(isinstance(annotation_type.params[0].data_type, String))
        self.assertEqual(annotation_type.params[1].name, 'importance')
        self.assertTrue(annotation_type.params[1].has_default)
        self.assertEqual(annotation_type.params[1].default, 'very')
        self.assertTrue(isinstance(annotation_type.params[1].data_type, String))

        # test both args and kwargs are set consistently in IR
        annotation = api.namespaces['test'].annotation_by_name['VeryImportant']
        self.assertTrue(annotation.annotation_type is annotation_type)
        self.assertEqual(annotation.kwargs['owner'], 'test-team')
        self.assertEqual(annotation.kwargs['importance'], 'very')
        self.assertEqual(annotation.args[0], 'test-team')
        self.assertEqual(annotation.args[1], 'very')

        annotation = api.namespaces['test'].annotation_by_name['SortaImportant']
        self.assertTrue(annotation.annotation_type is annotation_type)
        self.assertEqual(annotation.kwargs['owner'], 'test-team')
        self.assertEqual(annotation.kwargs['importance'], 'sorta')
        self.assertEqual(annotation.args[0], 'test-team')
        self.assertEqual(annotation.args[1], 'sorta')

        alias = api.namespaces['test'].alias_by_name['TestAlias']
        self.assertTrue(alias.custom_annotations[0].annotation_type is annotation_type)

        struct = api.namespaces['test'].data_type_by_name['TestStruct']
        self.assertEqual(struct.fields[0].custom_annotations[0], annotation)
        self.assertEqual(struct.recursive_custom_annotations, {
            (alias, api.namespaces['test'].annotation_by_name['VeryImportant']),
            (struct.fields[0], api.namespaces['test'].annotation_by_name['SortaImportant']),
        })

        # Test recursive references are captured
        ns2 = textwrap.dedent("""\
            namespace testchain

            import test

            alias TestAliasChain = String
                @test.SortaImportant

            struct TestStructChain
                f test.TestStruct
                g List(TestAliasChain)
            """)
        ns3 = textwrap.dedent("""\
            namespace teststruct

            import testchain

            struct TestStructToStruct
                f testchain.TestStructChain
            """)
        ns4 = textwrap.dedent("""\
            namespace testalias

            import testchain

            struct TestStructToAlias
                f testchain.TestAliasChain
            """)

        api = specs_to_ir([('test.stone', text), ('testchain.stone', ns2),
                           ('teststruct.stone', ns3), ('testalias.stone', ns4)])

        struct_namespaces = [ns.name for ns in
                             api.namespaces['teststruct'].get_imported_namespaces(
                                 consider_annotation_types=True)]
        self.assertTrue('test' in struct_namespaces)
        alias_namespaces = [ns.name for ns in
                            api.namespaces['testalias'].get_imported_namespaces(
                                consider_annotation_types=True)]
        self.assertTrue('test' in alias_namespaces)


if __name__ == '__main__':
    unittest.main()
