bpo-36876: Add a tool that identifies unsupported global C variables. (#15877)

diff --git a/Lib/test/test_check_c_globals.py b/Lib/test/test_check_c_globals.py
new file mode 100644
index 0000000..009560e
--- /dev/null
+++ b/Lib/test/test_check_c_globals.py
@@ -0,0 +1,22 @@
+import unittest
+import test.test_tools
+
+test.test_tools.skip_if_missing('c-analyzer')
+with test.test_tools.imports_under_tool('c-analyzer'):
+    from c_globals.__main__ import main
+
+
+class ActualChecks(unittest.TestCase):
+
+    # XXX Also run the check in "make check".
+    @unittest.expectedFailure
+    def test_check_c_globals(self):
+        try:
+            main('check', {})
+        except NotImplementedError:
+            raise unittest.SkipTest('not supported on this host')
+
+
+if __name__ == '__main__':
+    # Test needs to be a package, so we can do relative imports.
+    unittest.main()
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index 244c5fe..3d5dc47 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -2,7 +2,7 @@
 # Copyright 2012-2013 by Larry Hastings.
 # Licensed to the PSF under a contributor agreement.
 
-from test import support
+from test import support, test_tools
 from unittest import TestCase
 import collections
 import inspect
@@ -10,17 +10,10 @@
 import sys
 import unittest
 
-
-clinic_path = os.path.join(os.path.dirname(__file__), '..', '..', 'Tools', 'clinic')
-clinic_path = os.path.normpath(clinic_path)
-if not os.path.exists(clinic_path):
-    raise unittest.SkipTest(f'{clinic_path!r} path does not exist')
-sys.path.append(clinic_path)
-try:
+test_tools.skip_if_missing('clinic')
+with test_tools.imports_under_tool('clinic'):
     import clinic
     from clinic import DSLParser
-finally:
-    del sys.path[-1]
 
 
 class FakeConverter:
diff --git a/Lib/test/test_tools/__init__.py b/Lib/test/test_tools/__init__.py
index 4d0fca3..eb9acad 100644
--- a/Lib/test/test_tools/__init__.py
+++ b/Lib/test/test_tools/__init__.py
@@ -1,20 +1,33 @@
 """Support functions for testing scripts in the Tools directory."""
-import os
-import unittest
+import contextlib
 import importlib
+import os.path
+import unittest
 from test import support
 
-basepath = os.path.dirname(                 # <src/install dir>
-                os.path.dirname(                # Lib
-                    os.path.dirname(                # test
-                        os.path.dirname(__file__))))    # test_tools
+basepath = os.path.normpath(
+        os.path.dirname(                 # <src/install dir>
+            os.path.dirname(                # Lib
+                os.path.dirname(                # test
+                    os.path.dirname(__file__)))))    # test_tools
 
 toolsdir = os.path.join(basepath, 'Tools')
 scriptsdir = os.path.join(toolsdir, 'scripts')
 
-def skip_if_missing():
-    if not os.path.isdir(scriptsdir):
-        raise unittest.SkipTest('scripts directory could not be found')
+def skip_if_missing(tool=None):
+    if tool:
+        tooldir = os.path.join(toolsdir, tool)
+    else:
+        tool = 'scripts'
+        tooldir = scriptsdir
+    if not os.path.isdir(tooldir):
+        raise unittest.SkipTest(f'{tool} directory could not be found')
+
+@contextlib.contextmanager
+def imports_under_tool(name, *subdirs):
+    tooldir = os.path.join(toolsdir, name, *subdirs)
+    with support.DirsOnSysPath(tooldir) as cm:
+        yield cm
 
 def import_tool(toolname):
     with support.DirsOnSysPath(scriptsdir):
diff --git a/Lib/test/test_tools/test_c_analyzer/__init__.py b/Lib/test/test_tools/test_c_analyzer/__init__.py
new file mode 100644
index 0000000..d0b4c04
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/__init__.py
@@ -0,0 +1,15 @@
+import contextlib
+import os.path
+import test.test_tools
+from test.support import load_package_tests
+
+
+@contextlib.contextmanager
+def tool_imports_for_tests():
+    test.test_tools.skip_if_missing('c-analyzer')
+    with test.test_tools.imports_under_tool('c-analyzer'):
+        yield
+
+
+def load_tests(*args):
+    return load_package_tests(os.path.dirname(__file__), *args)
diff --git a/Lib/test/test_tools/test_c_analyzer/__main__.py b/Lib/test/test_tools/test_c_analyzer/__main__.py
new file mode 100644
index 0000000..b5b017d
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/__main__.py
@@ -0,0 +1,5 @@
+from . import load_tests
+import unittest
+
+
+unittest.main()
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/__init__.py b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/__init__.py
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_files.py b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_files.py
new file mode 100644
index 0000000..6d14aea
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_files.py
@@ -0,0 +1,470 @@
+import os.path
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common.files import (
+            iter_files, _walk_tree, glob_tree,
+            )
+
+
+def fixpath(filename):
+    return filename.replace('/', os.path.sep)
+
+
+class IterFilesTests(unittest.TestCase):
+
+    maxDiff = None
+
+    _return_walk = None
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+    def set_files(self, *filesperroot):
+        roots = []
+        result = []
+        for root, files in filesperroot:
+            root = fixpath(root)
+            roots.append(root)
+            result.append([os.path.join(root, fixpath(f))
+                           for f in files])
+        self._return_walk = result
+        return roots
+
+    def _walk(self, root, *, suffix=None, walk=None):
+        self.calls.append(('_walk', (root, suffix, walk)))
+        return iter(self._return_walk.pop(0))
+
+    def _glob(self, root, *, suffix=None):
+        self.calls.append(('_glob', (root, suffix)))
+        return iter(self._return_walk.pop(0))
+
+    def test_typical(self):
+        dirnames = self.set_files(
+            ('spam', ['file1.c', 'file2.c']),
+            ('eggs', ['ham/file3.h']),
+            )
+        suffixes = ('.c', '.h')
+
+        files = list(iter_files(dirnames, suffixes,
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            fixpath('eggs/ham/file3.h'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', None, _walk_tree)),
+            ('_walk', ('eggs', None, _walk_tree)),
+            ])
+
+    def test_single_root(self):
+        self._return_walk = [
+                [fixpath('spam/file1.c'), fixpath('spam/file2.c')],
+                ]
+
+        files = list(iter_files('spam', '.c',
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', '.c', _walk_tree)),
+            ])
+
+    def test_one_root(self):
+        self._return_walk = [
+                [fixpath('spam/file1.c'), fixpath('spam/file2.c')],
+                ]
+
+        files = list(iter_files(['spam'], '.c',
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', '.c', _walk_tree)),
+            ])
+
+    def test_multiple_roots(self):
+        dirnames = self.set_files(
+            ('spam', ['file1.c', 'file2.c']),
+            ('eggs', ['ham/file3.c']),
+            )
+
+        files = list(iter_files(dirnames, '.c',
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            fixpath('eggs/ham/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', '.c', _walk_tree)),
+            ('_walk', ('eggs', '.c', _walk_tree)),
+            ])
+
+    def test_no_roots(self):
+        files = list(iter_files([], '.c',
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [])
+        self.assertEqual(self.calls, [])
+
+    def test_single_suffix(self):
+        self._return_walk = [
+                [fixpath('spam/file1.c'),
+                 fixpath('spam/eggs/file3.c'),
+                 ],
+                ]
+
+        files = list(iter_files('spam', '.c',
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/eggs/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', '.c', _walk_tree)),
+            ])
+
+    def test_one_suffix(self):
+        self._return_walk = [
+                [fixpath('spam/file1.c'),
+                 fixpath('spam/file1.h'),
+                 fixpath('spam/file1.o'),
+                 fixpath('spam/eggs/file3.c'),
+                 ],
+                ]
+
+        files = list(iter_files('spam', ['.c'],
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/eggs/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', None, _walk_tree)),
+            ])
+
+    def test_multiple_suffixes(self):
+        self._return_walk = [
+                [fixpath('spam/file1.c'),
+                 fixpath('spam/file1.h'),
+                 fixpath('spam/file1.o'),
+                 fixpath('spam/eggs/file3.c'),
+                 ],
+                ]
+
+        files = list(iter_files('spam', ('.c', '.h'),
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file1.h'),
+            fixpath('spam/eggs/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', None, _walk_tree)),
+            ])
+
+    def test_no_suffix(self):
+        expected = [fixpath('spam/file1.c'),
+                    fixpath('spam/file1.h'),
+                    fixpath('spam/file1.o'),
+                    fixpath('spam/eggs/file3.c'),
+                    ]
+        for suffix in (None, '', ()):
+            with self.subTest(suffix):
+                self.calls.clear()
+                self._return_walk = [list(expected)]
+
+                files = list(iter_files('spam', suffix,
+                                        _glob=self._glob,
+                                        _walk=self._walk))
+
+                self.assertEqual(files, expected)
+                self.assertEqual(self.calls, [
+                    ('_walk', ('spam', suffix, _walk_tree)),
+                    ])
+
+    def test_relparent(self):
+        dirnames = self.set_files(
+            ('/x/y/z/spam', ['file1.c', 'file2.c']),
+            ('/x/y/z/eggs', ['ham/file3.c']),
+            )
+
+        files = list(iter_files(dirnames, '.c', fixpath('/x/y'),
+                                _glob=self._glob,
+                                _walk=self._walk))
+
+        self.assertEqual(files, [
+            fixpath('z/spam/file1.c'),
+            fixpath('z/spam/file2.c'),
+            fixpath('z/eggs/ham/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', (fixpath('/x/y/z/spam'), '.c', _walk_tree)),
+            ('_walk', (fixpath('/x/y/z/eggs'), '.c', _walk_tree)),
+            ])
+
+    def test_glob(self):
+        dirnames = self.set_files(
+            ('spam', ['file1.c', 'file2.c']),
+            ('eggs', ['ham/file3.c']),
+            )
+
+        files = list(iter_files(dirnames, '.c',
+                                get_files=glob_tree,
+                                _walk=self._walk,
+                                _glob=self._glob))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            fixpath('eggs/ham/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_glob', ('spam', '.c')),
+            ('_glob', ('eggs', '.c')),
+            ])
+
+
+    def test_alt_walk_func(self):
+        dirnames = self.set_files(
+            ('spam', ['file1.c', 'file2.c']),
+            ('eggs', ['ham/file3.c']),
+            )
+        def get_files(root):
+            return None
+
+        files = list(iter_files(dirnames, '.c',
+                                get_files=get_files,
+                                _walk=self._walk,
+                                _glob=self._glob))
+
+        self.assertEqual(files, [
+            fixpath('spam/file1.c'),
+            fixpath('spam/file2.c'),
+            fixpath('eggs/ham/file3.c'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_walk', ('spam', '.c', get_files)),
+            ('_walk', ('eggs', '.c', get_files)),
+            ])
+
+
+
+
+
+
+#    def test_no_dirnames(self):
+#        dirnames = []
+#        filter_by_name = None
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [])
+#        self.assertEqual(self.calls, [])
+#
+#    def test_no_filter(self):
+#        self._return_walk = [
+#                [('spam', (), ('file1', 'file2.c', 'file3.h', 'file4.o')),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                ]
+#        filter_by_name = None
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [
+#            fixpath('spam/file1'),
+#            fixpath('spam/file2.c'),
+#            fixpath('spam/file3.h'),
+#            fixpath('spam/file4.o'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ])
+#
+#    def test_no_files(self):
+#        self._return_walk = [
+#                [('spam', (), ()),
+#                 ],
+#                [(fixpath('eggs/ham'), (), ()),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                fixpath('eggs/ham'),
+#                ]
+#        filter_by_name = None
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ('_walk', (fixpath('eggs/ham'),)),
+#            ])
+#
+#    def test_tree(self):
+#        self._return_walk = [
+#                [('spam', ('sub1', 'sub2', 'sub3'), ('file1',)),
+#                 (fixpath('spam/sub1'), ('sub1sub1',), ('file2', 'file3')),
+#                 (fixpath('spam/sub1/sub1sub1'), (), ('file4',)),
+#                 (fixpath('spam/sub2'), (), ()),
+#                 (fixpath('spam/sub3'), (), ('file5',)),
+#                 ],
+#                [(fixpath('eggs/ham'), (), ('file6',)),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                fixpath('eggs/ham'),
+#                ]
+#        filter_by_name = None
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [
+#            fixpath('spam/file1'),
+#            fixpath('spam/sub1/file2'),
+#            fixpath('spam/sub1/file3'),
+#            fixpath('spam/sub1/sub1sub1/file4'),
+#            fixpath('spam/sub3/file5'),
+#            fixpath('eggs/ham/file6'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ('_walk', (fixpath('eggs/ham'),)),
+#            ])
+#
+#    def test_filter_suffixes(self):
+#        self._return_walk = [
+#                [('spam', (), ('file1', 'file2.c', 'file3.h', 'file4.o')),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                ]
+#        filter_by_name = ('.c', '.h')
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [
+#            fixpath('spam/file2.c'),
+#            fixpath('spam/file3.h'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ])
+#
+#    def test_some_filtered(self):
+#        self._return_walk = [
+#                [('spam', (), ('file1', 'file2', 'file3', 'file4')),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                ]
+#        def filter_by_name(filename, results=[False, True, False, True]):
+#            self.calls.append(('filter_by_name', (filename,)))
+#            return results.pop(0)
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [
+#            fixpath('spam/file2'),
+#            fixpath('spam/file4'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ('filter_by_name', ('file1',)),
+#            ('filter_by_name', ('file2',)),
+#            ('filter_by_name', ('file3',)),
+#            ('filter_by_name', ('file4',)),
+#            ])
+#
+#    def test_none_filtered(self):
+#        self._return_walk = [
+#                [('spam', (), ('file1', 'file2', 'file3', 'file4')),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                ]
+#        def filter_by_name(filename, results=[True, True, True, True]):
+#            self.calls.append(('filter_by_name', (filename,)))
+#            return results.pop(0)
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [
+#            fixpath('spam/file1'),
+#            fixpath('spam/file2'),
+#            fixpath('spam/file3'),
+#            fixpath('spam/file4'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ('filter_by_name', ('file1',)),
+#            ('filter_by_name', ('file2',)),
+#            ('filter_by_name', ('file3',)),
+#            ('filter_by_name', ('file4',)),
+#            ])
+#
+#    def test_all_filtered(self):
+#        self._return_walk = [
+#                [('spam', (), ('file1', 'file2', 'file3', 'file4')),
+#                 ],
+#                ]
+#        dirnames = [
+#                'spam',
+#                ]
+#        def filter_by_name(filename, results=[False, False, False, False]):
+#            self.calls.append(('filter_by_name', (filename,)))
+#            return results.pop(0)
+#
+#        files = list(iter_files(dirnames, filter_by_name,
+#                                _walk=self._walk))
+#
+#        self.assertEqual(files, [])
+#        self.assertEqual(self.calls, [
+#            ('_walk', ('spam',)),
+#            ('filter_by_name', ('file1',)),
+#            ('filter_by_name', ('file2',)),
+#            ('filter_by_name', ('file3',)),
+#            ('filter_by_name', ('file4',)),
+#            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_info.py b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_info.py
new file mode 100644
index 0000000..2d38671
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_info.py
@@ -0,0 +1,194 @@
+import string
+import unittest
+
+from ..util import PseudoStr, StrProxy, Object
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common.info import ID
+
+
+class IDTests(unittest.TestCase):
+
+    VALID_ARGS = (
+            'x/y/z/spam.c',
+            'func',
+            'eggs',
+            )
+    VALID_KWARGS = dict(zip(ID._fields, VALID_ARGS))
+    VALID_EXPECTED = VALID_ARGS
+
+    def test_from_raw(self):
+        tests = [
+            ('', None),
+            (None, None),
+            ('spam', (None, None, 'spam')),
+            (('spam',), (None, None, 'spam')),
+            (('x/y/z/spam.c', 'spam'), ('x/y/z/spam.c', None, 'spam')),
+            (self.VALID_ARGS, self.VALID_EXPECTED),
+            (self.VALID_KWARGS, self.VALID_EXPECTED),
+            ]
+        for raw, expected in tests:
+            with self.subTest(raw):
+                id = ID.from_raw(raw)
+
+                self.assertEqual(id, expected)
+
+    def test_minimal(self):
+        id = ID(
+                filename=None,
+                funcname=None,
+                name='eggs',
+                )
+
+        self.assertEqual(id, (
+                None,
+                None,
+                'eggs',
+                ))
+
+    def test_init_typical_global(self):
+        id = ID(
+                filename='x/y/z/spam.c',
+                funcname=None,
+                name='eggs',
+                )
+
+        self.assertEqual(id, (
+                'x/y/z/spam.c',
+                None,
+                'eggs',
+                ))
+
+    def test_init_typical_local(self):
+        id = ID(
+                filename='x/y/z/spam.c',
+                funcname='func',
+                name='eggs',
+                )
+
+        self.assertEqual(id, (
+                'x/y/z/spam.c',
+                'func',
+                'eggs',
+                ))
+
+    def test_init_all_missing(self):
+        for value in ('', None):
+            with self.subTest(repr(value)):
+                id = ID(
+                        filename=value,
+                        funcname=value,
+                        name=value,
+                        )
+
+                self.assertEqual(id, (
+                        None,
+                        None,
+                        None,
+                        ))
+
+    def test_init_all_coerced(self):
+        tests = [
+            ('str subclass',
+             dict(
+                 filename=PseudoStr('x/y/z/spam.c'),
+                 funcname=PseudoStr('func'),
+                 name=PseudoStr('eggs'),
+                 ),
+             ('x/y/z/spam.c',
+              'func',
+              'eggs',
+              )),
+            ('non-str',
+             dict(
+                 filename=StrProxy('x/y/z/spam.c'),
+                 funcname=Object(),
+                 name=('a', 'b', 'c'),
+                 ),
+             ('x/y/z/spam.c',
+              '<object>',
+              "('a', 'b', 'c')",
+              )),
+            ]
+        for summary, kwargs, expected in tests:
+            with self.subTest(summary):
+                id = ID(**kwargs)
+
+                for field in ID._fields:
+                    value = getattr(id, field)
+                    self.assertIs(type(value), str)
+                self.assertEqual(tuple(id), expected)
+
+    def test_iterable(self):
+        id = ID(**self.VALID_KWARGS)
+
+        filename, funcname, name = id
+
+        values = (filename, funcname, name)
+        for value, expected in zip(values, self.VALID_EXPECTED):
+            self.assertEqual(value, expected)
+
+    def test_fields(self):
+        id = ID('a', 'b', 'z')
+
+        self.assertEqual(id.filename, 'a')
+        self.assertEqual(id.funcname, 'b')
+        self.assertEqual(id.name, 'z')
+
+    def test_validate_typical(self):
+        id = ID(
+                filename='x/y/z/spam.c',
+                funcname='func',
+                name='eggs',
+                )
+
+        id.validate()  # This does not fail.
+
+    def test_validate_missing_field(self):
+        for field in ID._fields:
+            with self.subTest(field):
+                id = ID(**self.VALID_KWARGS)
+                id = id._replace(**{field: None})
+
+                if field == 'funcname':
+                    id.validate()  # The field can be missing (not set).
+                    id = id._replace(filename=None)
+                    id.validate()  # Both fields can be missing (not set).
+                    continue
+
+                with self.assertRaises(TypeError):
+                    id.validate()
+
+    def test_validate_bad_field(self):
+        badch = tuple(c for c in string.punctuation + string.digits)
+        notnames = (
+                '1a',
+                'a.b',
+                'a-b',
+                '&a',
+                'a++',
+                ) + badch
+        tests = [
+            ('filename', ()),  # Any non-empty str is okay.
+            ('funcname', notnames),
+            ('name', notnames),
+            ]
+        seen = set()
+        for field, invalid in tests:
+            for value in invalid:
+                seen.add(value)
+                with self.subTest(f'{field}={value!r}'):
+                    id = ID(**self.VALID_KWARGS)
+                    id = id._replace(**{field: value})
+
+                    with self.assertRaises(ValueError):
+                        id.validate()
+
+        for field, invalid in tests:
+            valid = seen - set(invalid)
+            for value in valid:
+                with self.subTest(f'{field}={value!r}'):
+                    id = ID(**self.VALID_KWARGS)
+                    id = id._replace(**{field: value})
+
+                    id.validate()  # This does not fail.
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_known.py b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_known.py
new file mode 100644
index 0000000..215023d
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_analyzer_common/test_known.py
@@ -0,0 +1,68 @@
+import re
+import textwrap
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_parser.info import Variable
+    from c_analyzer_common.info import ID
+    from c_analyzer_common.known import from_file
+
+
+class FromFileTests(unittest.TestCase):
+
+    maxDiff = None
+
+    _return_read_tsv = ()
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+    def _read_tsv(self, *args):
+        self.calls.append(('_read_tsv', args))
+        return self._return_read_tsv
+
+    def test_typical(self):
+        lines = textwrap.dedent('''
+            filename    funcname        name    kind    declaration
+            file1.c     -       var1    variable        static int
+            file1.c     func1   local1  variable        static int
+            file1.c     -       var2    variable        int
+            file1.c     func2   local2  variable        char *
+            file2.c     -       var1    variable        char *
+            ''').strip().splitlines()
+        lines = [re.sub(r'\s+', '\t', line, 4) for line in lines]
+        self._return_read_tsv = [tuple(v.strip() for v in line.split('\t'))
+                                 for line in lines[1:]]
+
+        known = from_file('spam.c', _read_tsv=self._read_tsv)
+
+        self.assertEqual(known, {
+            'variables': {v.id: v for v in [
+                Variable.from_parts('file1.c', '', 'var1', 'static int'),
+                Variable.from_parts('file1.c', 'func1', 'local1', 'static int'),
+                Variable.from_parts('file1.c', '', 'var2', 'int'),
+                Variable.from_parts('file1.c', 'func2', 'local2', 'char *'),
+                Variable.from_parts('file2.c', '', 'var1', 'char *'),
+                ]},
+            })
+        self.assertEqual(self.calls, [
+            ('_read_tsv', ('spam.c', 'filename\tfuncname\tname\tkind\tdeclaration')),
+            ])
+
+    def test_empty(self):
+        self._return_read_tsv = []
+
+        known = from_file('spam.c', _read_tsv=self._read_tsv)
+
+        self.assertEqual(known, {
+            'variables': {},
+            })
+        self.assertEqual(self.calls, [
+            ('_read_tsv', ('spam.c', 'filename\tfuncname\tname\tkind\tdeclaration')),
+            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/__init__.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/__init__.py
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/test___main__.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test___main__.py
new file mode 100644
index 0000000..5f52c58
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test___main__.py
@@ -0,0 +1,296 @@
+import sys
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common import SOURCE_DIRS
+    from c_analyzer_common.known import DATA_FILE as KNOWN_FILE
+    from c_parser import info
+    import c_globals as cg
+    from c_globals.supported import IGNORED_FILE
+    from c_globals.__main__ import cmd_check, cmd_show, parse_args, main
+
+
+TYPICAL = [
+        (info.Variable.from_parts('src1/spam.c', None, 'var1', 'const char *'),
+         True,
+         ),
+        (info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'int'),
+         True,
+         ),
+        (info.Variable.from_parts('src1/spam.c', None, 'var2', 'PyObject *'),
+         False,
+         ),
+        (info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'int'),
+         True,
+         ),
+        (info.Variable.from_parts('src1/spam.c', None, 'freelist', '(PyTupleObject *)[10]'),
+         False,
+         ),
+        (info.Variable.from_parts('src1/sub/ham.c', None, 'var1', 'const char const *'),
+         True,
+         ),
+        (info.Variable.from_parts('src2/jam.c', None, 'var1', 'int'),
+         True,
+         ),
+        (info.Variable.from_parts('src2/jam.c', None, 'var2', 'MyObject *'),
+         False,
+         ),
+        (info.Variable.from_parts('Include/spam.h', None, 'data', 'const int'),
+         True,
+         ),
+        ]
+
+
+class CMDBase(unittest.TestCase):
+
+    maxDiff = None
+
+    _return_find = ()
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+    def _find(self, *args):
+        self.calls.append(('_find', args))
+        return self._return_find
+
+    def _show(self, *args):
+        self.calls.append(('_show', args))
+
+    def _print(self, *args):
+        self.calls.append(('_print', args))
+
+
+class CheckTests(CMDBase):
+
+    def test_defaults(self):
+        self._return_find = []
+
+        cmd_check('check',
+                  _find=self._find,
+                  _show=self._show,
+                  _print=self._print,
+                  )
+
+        self.assertEqual(self.calls[0], (
+            '_find', (
+                SOURCE_DIRS,
+                KNOWN_FILE,
+                IGNORED_FILE,
+                ),
+            ))
+
+    def test_all_supported(self):
+        self._return_find = [(v, s) for v, s in TYPICAL if s]
+        dirs = ['src1', 'src2', 'Include']
+
+        cmd_check('check',
+                 dirs,
+                 ignored='ignored.tsv',
+                 known='known.tsv',
+                 _find=self._find,
+                 _show=self._show,
+                 _print=self._print,
+                 )
+
+        self.assertEqual(self.calls, [
+            ('_find', (dirs, 'known.tsv', 'ignored.tsv')),
+            #('_print', ('okay',)),
+            ])
+
+    def test_some_unsupported(self):
+        self._return_find = TYPICAL
+        dirs = ['src1', 'src2', 'Include']
+
+        with self.assertRaises(SystemExit) as cm:
+            cmd_check('check',
+                      dirs,
+                      ignored='ignored.tsv',
+                      known='known.tsv',
+                      _find=self._find,
+                      _show=self._show,
+                      _print=self._print,
+                      )
+
+        unsupported = [v for v, s in TYPICAL if not s]
+        self.assertEqual(self.calls, [
+            ('_find', (dirs, 'known.tsv', 'ignored.tsv')),
+            ('_print', ('ERROR: found unsupported global variables',)),
+            ('_print', ()),
+            ('_show', (sorted(unsupported),)),
+            ('_print', (' (3 total)',)),
+            ])
+        self.assertEqual(cm.exception.code, 1)
+
+
+class ShowTests(CMDBase):
+
+    def test_defaults(self):
+        self._return_find = []
+
+        cmd_show('show',
+                 _find=self._find,
+                 _show=self._show,
+                 _print=self._print,
+                 )
+
+        self.assertEqual(self.calls[0], (
+            '_find', (
+                SOURCE_DIRS,
+                KNOWN_FILE,
+                IGNORED_FILE,
+                ),
+            ))
+
+    def test_typical(self):
+        self._return_find = TYPICAL
+        dirs = ['src1', 'src2', 'Include']
+
+        cmd_show('show',
+                 dirs,
+                 known='known.tsv',
+                 ignored='ignored.tsv',
+                 _find=self._find,
+                 _show=self._show,
+                 _print=self._print,
+                 )
+
+        supported = [v for v, s in TYPICAL if s]
+        unsupported = [v for v, s in TYPICAL if not s]
+        self.assertEqual(self.calls, [
+            ('_find', (dirs, 'known.tsv', 'ignored.tsv')),
+            ('_print', ('supported:',)),
+            ('_print', ('----------',)),
+            ('_show', (sorted(supported),)),
+            ('_print', (' (6 total)',)),
+            ('_print', ()),
+            ('_print', ('unsupported:',)),
+            ('_print', ('------------',)),
+            ('_show', (sorted(unsupported),)),
+            ('_print', (' (3 total)',)),
+            ])
+
+
+class ParseArgsTests(unittest.TestCase):
+
+    maxDiff = None
+
+    def test_no_args(self):
+        self.errmsg = None
+        def fail(msg):
+            self.errmsg = msg
+            sys.exit(msg)
+
+        with self.assertRaises(SystemExit):
+            parse_args('cg', [], _fail=fail)
+
+        self.assertEqual(self.errmsg, 'missing command')
+
+    def test_check_no_args(self):
+        cmd, cmdkwargs = parse_args('cg', [
+            'check',
+            ])
+
+        self.assertEqual(cmd, 'check')
+        self.assertEqual(cmdkwargs, {
+            'ignored': IGNORED_FILE,
+            'known': KNOWN_FILE,
+            'dirs': SOURCE_DIRS,
+            })
+
+    def test_check_full_args(self):
+        cmd, cmdkwargs = parse_args('cg', [
+            'check',
+            '--ignored', 'spam.tsv',
+            '--known', 'eggs.tsv',
+            'dir1',
+            'dir2',
+            'dir3',
+            ])
+
+        self.assertEqual(cmd, 'check')
+        self.assertEqual(cmdkwargs, {
+            'ignored': 'spam.tsv',
+            'known': 'eggs.tsv',
+            'dirs': ['dir1', 'dir2', 'dir3']
+            })
+
+    def test_show_no_args(self):
+        cmd, cmdkwargs = parse_args('cg', [
+            'show',
+            ])
+
+        self.assertEqual(cmd, 'show')
+        self.assertEqual(cmdkwargs, {
+            'ignored': IGNORED_FILE,
+            'known': KNOWN_FILE,
+            'dirs': SOURCE_DIRS,
+            'skip_objects': False,
+            })
+
+    def test_show_full_args(self):
+        cmd, cmdkwargs = parse_args('cg', [
+            'show',
+            '--ignored', 'spam.tsv',
+            '--known', 'eggs.tsv',
+            'dir1',
+            'dir2',
+            'dir3',
+            ])
+
+        self.assertEqual(cmd, 'show')
+        self.assertEqual(cmdkwargs, {
+            'ignored': 'spam.tsv',
+            'known': 'eggs.tsv',
+            'dirs': ['dir1', 'dir2', 'dir3'],
+            'skip_objects': False,
+            })
+
+
+def new_stub_commands(*names):
+    calls = []
+    def cmdfunc(cmd, **kwargs):
+        calls.append((cmd, kwargs))
+    commands = {name: cmdfunc for name in names}
+    return commands, calls
+
+
+class MainTests(unittest.TestCase):
+
+    def test_no_command(self):
+        with self.assertRaises(ValueError):
+            main(None, {})
+
+    def test_check(self):
+        commands, calls = new_stub_commands('check', 'show')
+
+        cmdkwargs = {
+            'ignored': 'spam.tsv',
+            'known': 'eggs.tsv',
+            'dirs': ['dir1', 'dir2', 'dir3'],
+            }
+        main('check', cmdkwargs, _COMMANDS=commands)
+
+        self.assertEqual(calls, [
+            ('check', cmdkwargs),
+            ])
+
+    def test_show(self):
+        commands, calls = new_stub_commands('check', 'show')
+
+        cmdkwargs = {
+            'ignored': 'spam.tsv',
+            'known': 'eggs.tsv',
+            'dirs': ['dir1', 'dir2', 'dir3'],
+            }
+        main('show', cmdkwargs, _COMMANDS=commands)
+
+        self.assertEqual(calls, [
+            ('show', cmdkwargs),
+            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_find.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_find.py
new file mode 100644
index 0000000..b29f966
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_find.py
@@ -0,0 +1,332 @@
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_parser import info
+    from c_globals.find import globals_from_binary, globals
+
+
+class _Base(unittest.TestCase):
+
+    maxDiff = None
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+
+class StaticsFromBinaryTests(_Base):
+
+    _return_iter_symbols = ()
+    _return_resolve_symbols = ()
+    _return_get_symbol_resolver = None
+
+    def setUp(self):
+        super().setUp()
+
+        self.kwargs = dict(
+                _iter_symbols=self._iter_symbols,
+                _resolve=self._resolve_symbols,
+                _get_symbol_resolver=self._get_symbol_resolver,
+                )
+
+    def _iter_symbols(self, binfile, find_local_symbol):
+        self.calls.append(('_iter_symbols', (binfile, find_local_symbol)))
+        return self._return_iter_symbols
+
+    def _resolve_symbols(self, symbols, resolve):
+        self.calls.append(('_resolve_symbols', (symbols, resolve,)))
+        return self._return_resolve_symbols
+
+    def _get_symbol_resolver(self, knownvars, dirnames=None):
+        self.calls.append(('_get_symbol_resolver', (knownvars, dirnames)))
+        return self._return_get_symbol_resolver
+
+    def test_typical(self):
+        symbols = self._return_iter_symbols = ()
+        resolver = self._return_get_symbol_resolver = object()
+        variables = self._return_resolve_symbols = [
+            info.Variable.from_parts('dir1/spam.c', None, 'var1', 'int'),
+            info.Variable.from_parts('dir1/spam.c', None, 'var2', 'static int'),
+            info.Variable.from_parts('dir1/spam.c', None, 'var3', 'char *'),
+            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', 'const char *'),
+            info.Variable.from_parts('dir1/eggs.c', None, 'var1', 'static int'),
+            info.Variable.from_parts('dir1/eggs.c', 'func1', 'var2', 'static char *'),
+            ]
+        knownvars = object()
+
+        found = list(globals_from_binary('python',
+                                         knownvars=knownvars,
+                                         **self.kwargs))
+
+        self.assertEqual(found, [
+            info.Variable.from_parts('dir1/spam.c', None, 'var2', 'static int'),
+            info.Variable.from_parts('dir1/eggs.c', None, 'var1', 'static int'),
+            info.Variable.from_parts('dir1/eggs.c', 'func1', 'var2', 'static char *'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_iter_symbols', ('python', None)),
+            ('_get_symbol_resolver', (knownvars, None)),
+            ('_resolve_symbols', (symbols, resolver)),
+            ])
+
+#        self._return_iter_symbols = [
+#                s_info.Symbol(('dir1/spam.c', None, 'var1'), 'variable', False),
+#                s_info.Symbol(('dir1/spam.c', None, 'var2'), 'variable', False),
+#                s_info.Symbol(('dir1/spam.c', None, 'func1'), 'function', False),
+#                s_info.Symbol(('dir1/spam.c', None, 'func2'), 'function', True),
+#                s_info.Symbol(('dir1/spam.c', None, 'var3'), 'variable', False),
+#                s_info.Symbol(('dir1/spam.c', 'func2', 'var4'), 'variable', False),
+#                s_info.Symbol(('dir1/ham.c', None, 'var1'), 'variable', True),
+#                s_info.Symbol(('dir1/eggs.c', None, 'var1'), 'variable', False),
+#                s_info.Symbol(('dir1/eggs.c', None, 'xyz'), 'other', False),
+#                s_info.Symbol(('dir1/eggs.c', '???', 'var2'), 'variable', False),
+#                s_info.Symbol(('???', None, 'var_x'), 'variable', False),
+#                s_info.Symbol(('???', '???', 'var_y'), 'variable', False),
+#                s_info.Symbol((None, None, '???'), 'other', False),
+#                ]
+#        known = object()
+#
+#        globals_from_binary('python', knownvars=known, **this.kwargs)
+#        found = list(globals_from_symbols(['dir1'], self.iter_symbols))
+#
+#        self.assertEqual(found, [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('iter_symbols', (['dir1'],)),
+#            ])
+#
+#    def test_no_symbols(self):
+#        self._return_iter_symbols = []
+#
+#        found = list(globals_from_symbols(['dir1'], self.iter_symbols))
+#
+#        self.assertEqual(found, [])
+#        self.assertEqual(self.calls, [
+#            ('iter_symbols', (['dir1'],)),
+#            ])
+
+    # XXX need functional test
+
+
+#class StaticFromDeclarationsTests(_Base):
+#
+#    _return_iter_declarations = ()
+#
+#    def iter_declarations(self, dirnames):
+#        self.calls.append(('iter_declarations', (dirnames,)))
+#        return iter(self._return_iter_declarations)
+#
+#    def test_typical(self):
+#        self._return_iter_declarations = [
+#            None,
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            object(),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            object(),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            object(),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            object(),
+#            ]
+#
+#        found = list(globals_from_declarations(['dir1'], self.iter_declarations))
+#
+#        self.assertEqual(found, [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ])
+#        self.assertEqual(self.calls, [
+#            ('iter_declarations', (['dir1'],)),
+#            ])
+#
+#    def test_no_declarations(self):
+#        self._return_iter_declarations = []
+#
+#        found = list(globals_from_declarations(['dir1'], self.iter_declarations))
+#
+#        self.assertEqual(found, [])
+#        self.assertEqual(self.calls, [
+#            ('iter_declarations', (['dir1'],)),
+#            ])
+
+
+#class IterVariablesTests(_Base):
+#
+#    _return_from_symbols = ()
+#    _return_from_declarations = ()
+#
+#    def _from_symbols(self, dirnames, iter_symbols):
+#        self.calls.append(('_from_symbols', (dirnames, iter_symbols)))
+#        return iter(self._return_from_symbols)
+#
+#    def _from_declarations(self, dirnames, iter_declarations):
+#        self.calls.append(('_from_declarations', (dirnames, iter_declarations)))
+#        return iter(self._return_from_declarations)
+#
+#    def test_typical(self):
+#        expected = [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ]
+#        self._return_from_symbols = expected
+#
+#        found = list(iter_variables(['dir1'],
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, expected)
+#        self.assertEqual(self.calls, [
+#            ('_from_symbols', (['dir1'], b_symbols.iter_symbols)),
+#            ])
+#
+#    def test_no_symbols(self):
+#        self._return_from_symbols = []
+#
+#        found = list(iter_variables(['dir1'],
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, [])
+#        self.assertEqual(self.calls, [
+#            ('_from_symbols', (['dir1'], b_symbols.iter_symbols)),
+#            ])
+#
+#    def test_from_binary(self):
+#        expected = [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ]
+#        self._return_from_symbols = expected
+#
+#        found = list(iter_variables(['dir1'], 'platform',
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, expected)
+#        self.assertEqual(self.calls, [
+#            ('_from_symbols', (['dir1'], b_symbols.iter_symbols)),
+#            ])
+#
+#    def test_from_symbols(self):
+#        expected = [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ]
+#        self._return_from_symbols = expected
+#
+#        found = list(iter_variables(['dir1'], 'symbols',
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, expected)
+#        self.assertEqual(self.calls, [
+#            ('_from_symbols', (['dir1'], s_symbols.iter_symbols)),
+#            ])
+#
+#    def test_from_declarations(self):
+#        expected = [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ]
+#        self._return_from_declarations = expected
+#
+#        found = list(iter_variables(['dir1'], 'declarations',
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, expected)
+#        self.assertEqual(self.calls, [
+#            ('_from_declarations', (['dir1'], declarations.iter_all)),
+#            ])
+#
+#    def test_from_preprocessed(self):
+#        expected = [
+#            info.Variable.from_parts('dir1/spam.c', None, 'var1', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var2', '???'),
+#            info.Variable.from_parts('dir1/spam.c', None, 'var3', '???'),
+#            info.Variable.from_parts('dir1/spam.c', 'func2', 'var4', '???'),
+#            info.Variable.from_parts('dir1/eggs.c', None, 'var1', '???'),
+#            ]
+#        self._return_from_declarations = expected
+#
+#        found = list(iter_variables(['dir1'], 'preprocessed',
+#                                  _from_symbols=self._from_symbols,
+#                                  _from_declarations=self._from_declarations))
+#
+#        self.assertEqual(found, expected)
+#        self.assertEqual(self.calls, [
+#            ('_from_declarations', (['dir1'], declarations.iter_preprocessed)),
+#            ])
+
+
+class StaticsTest(_Base):
+
+    _return_iter_variables = None
+
+    def _iter_variables(self, kind, *, known, dirnames):
+        self.calls.append(
+                ('_iter_variables', (kind, known, dirnames)))
+        return iter(self._return_iter_variables or ())
+
+    def test_typical(self):
+        self._return_iter_variables = [
+            info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'),
+            info.Variable.from_parts('src1/spam.c', None, 'var1b', 'const char *'),
+            info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'),
+            info.Variable.from_parts('src1/spam.c', 'ham', 'result', 'int'),
+            info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'),
+            info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'),
+            info.Variable.from_parts('src1/spam.c', None, 'freelist', 'static (PyTupleObject *)[10]'),
+            info.Variable.from_parts('src1/sub/ham.c', None, 'var1', 'static const char const *'),
+            info.Variable.from_parts('src2/jam.c', None, 'var1', 'static int'),
+            info.Variable.from_parts('src2/jam.c', None, 'var2', 'static MyObject *'),
+            info.Variable.from_parts('Include/spam.h', None, 'data', 'static const int'),
+            ]
+        dirnames = object()
+        known = object()
+
+        found = list(globals(dirnames, known,
+                             kind='platform',
+                             _iter_variables=self._iter_variables,
+                             ))
+
+        self.assertEqual(found, [
+            info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'),
+            info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'),
+            info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'),
+            info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'),
+            info.Variable.from_parts('src1/spam.c', None, 'freelist', 'static (PyTupleObject *)[10]'),
+            info.Variable.from_parts('src1/sub/ham.c', None, 'var1', 'static const char const *'),
+            info.Variable.from_parts('src2/jam.c', None, 'var1', 'static int'),
+            info.Variable.from_parts('src2/jam.c', None, 'var2', 'static MyObject *'),
+            info.Variable.from_parts('Include/spam.h', None, 'data', 'static const int'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_iter_variables', ('platform', known, dirnames)),
+            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_functional.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_functional.py
new file mode 100644
index 0000000..9279790
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_functional.py
@@ -0,0 +1,34 @@
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    pass
+
+
+class SelfCheckTests(unittest.TestCase):
+
+    @unittest.expectedFailure
+    def test_known(self):
+        # Make sure known macros & vartypes aren't hiding unknown local types.
+        # XXX finish!
+        raise NotImplementedError
+
+    @unittest.expectedFailure
+    def test_compare_nm_results(self):
+        # Make sure the "show" results match the statics found by "nm" command.
+        # XXX Skip if "nm" is not available.
+        # XXX finish!
+        raise NotImplementedError
+
+
+class DummySourceTests(unittest.TestCase):
+
+    @unittest.expectedFailure
+    def test_check(self):
+        # XXX finish!
+        raise NotImplementedError
+
+    @unittest.expectedFailure
+    def test_show(self):
+        # XXX finish!
+        raise NotImplementedError
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_show.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_show.py
new file mode 100644
index 0000000..ce1dad8
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_show.py
@@ -0,0 +1,52 @@
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_parser import info
+    from c_globals.show import basic
+
+
+TYPICAL = [
+        info.Variable.from_parts('src1/spam.c', None, 'var1', 'static const char *'),
+        info.Variable.from_parts('src1/spam.c', 'ham', 'initialized', 'static int'),
+        info.Variable.from_parts('src1/spam.c', None, 'var2', 'static PyObject *'),
+        info.Variable.from_parts('src1/eggs.c', 'tofu', 'ready', 'static int'),
+        info.Variable.from_parts('src1/spam.c', None, 'freelist', 'static (PyTupleObject *)[10]'),
+        info.Variable.from_parts('src1/sub/ham.c', None, 'var1', 'static const char const *'),
+        info.Variable.from_parts('src2/jam.c', None, 'var1', 'static int'),
+        info.Variable.from_parts('src2/jam.c', None, 'var2', 'static MyObject *'),
+        info.Variable.from_parts('Include/spam.h', None, 'data', 'static const int'),
+        ]
+
+
+class BasicTests(unittest.TestCase):
+
+    maxDiff = None
+
+    def setUp(self):
+        self.lines = []
+
+    def print(self, line):
+        self.lines.append(line)
+
+    def test_typical(self):
+        basic(TYPICAL,
+              _print=self.print)
+
+        self.assertEqual(self.lines, [
+            'src1/spam.c:var1                                                 static const char *',
+            'src1/spam.c:ham():initialized                                    static int',
+            'src1/spam.c:var2                                                 static PyObject *',
+            'src1/eggs.c:tofu():ready                                         static int',
+            'src1/spam.c:freelist                                             static (PyTupleObject *)[10]',
+            'src1/sub/ham.c:var1                                              static const char const *',
+            'src2/jam.c:var1                                                  static int',
+            'src2/jam.c:var2                                                  static MyObject *',
+            'Include/spam.h:data                                              static const int',
+            ])
+
+    def test_no_rows(self):
+        basic([],
+              _print=self.print)
+
+        self.assertEqual(self.lines, [])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_supported.py b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_supported.py
new file mode 100644
index 0000000..1e7d40e
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_globals/test_supported.py
@@ -0,0 +1,96 @@
+import re
+import textwrap
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common.info import ID
+    from c_parser import info
+    from c_globals.supported import is_supported, ignored_from_file
+
+
+class IsSupportedTests(unittest.TestCase):
+
+    @unittest.expectedFailure
+    def test_supported(self):
+        statics = [
+                info.StaticVar('src1/spam.c', None, 'var1', 'const char *'),
+                info.StaticVar('src1/spam.c', None, 'var1', 'int'),
+                ]
+        for static in statics:
+            with self.subTest(static):
+                result = is_supported(static)
+
+                self.assertTrue(result)
+
+    @unittest.expectedFailure
+    def test_not_supported(self):
+        statics = [
+                info.StaticVar('src1/spam.c', None, 'var1', 'PyObject *'),
+                info.StaticVar('src1/spam.c', None, 'var1', 'PyObject[10]'),
+                ]
+        for static in statics:
+            with self.subTest(static):
+                result = is_supported(static)
+
+                self.assertFalse(result)
+
+
+class IgnoredFromFileTests(unittest.TestCase):
+
+    maxDiff = None
+
+    _return_read_tsv = ()
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+    def _read_tsv(self, *args):
+        self.calls.append(('_read_tsv', args))
+        return self._return_read_tsv
+
+    def test_typical(self):
+        lines = textwrap.dedent('''
+            filename    funcname        name    kind    reason
+            file1.c     -       var1    variable        ...
+            file1.c     func1   local1  variable        |
+            file1.c     -       var2    variable        ???
+            file1.c     func2   local2  variable           |
+            file2.c     -       var1    variable        reasons
+            ''').strip().splitlines()
+        lines = [re.sub(r'\s{1,8}', '\t', line, 4).replace('|', '')
+                 for line in lines]
+        self._return_read_tsv = [tuple(v.strip() for v in line.split('\t'))
+                                 for line in lines[1:]]
+
+        ignored = ignored_from_file('spam.c', _read_tsv=self._read_tsv)
+
+        self.assertEqual(ignored, {
+            'variables': {
+                ID('file1.c', '', 'var1'): '...',
+                ID('file1.c', 'func1', 'local1'): '',
+                ID('file1.c', '', 'var2'): '???',
+                ID('file1.c', 'func2', 'local2'): '',
+                ID('file2.c', '', 'var1'): 'reasons',
+                },
+            })
+        self.assertEqual(self.calls, [
+            ('_read_tsv', ('spam.c', 'filename\tfuncname\tname\tkind\treason')),
+            ])
+
+    def test_empty(self):
+        self._return_read_tsv = []
+
+        ignored = ignored_from_file('spam.c', _read_tsv=self._read_tsv)
+
+        self.assertEqual(ignored, {
+            'variables': {},
+            })
+        self.assertEqual(self.calls, [
+            ('_read_tsv', ('spam.c', 'filename\tfuncname\tname\tkind\treason')),
+            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_parser/__init__.py b/Lib/test/test_tools/test_c_analyzer/test_c_parser/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_parser/__init__.py
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_declarations.py b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_declarations.py
new file mode 100644
index 0000000..b68744e
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_declarations.py
@@ -0,0 +1,795 @@
+import textwrap
+import unittest
+
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_parser.declarations import (
+        iter_global_declarations, iter_local_statements,
+        parse_func, parse_var, parse_compound,
+        iter_variables,
+        )
+
+
+class TestCaseBase(unittest.TestCase):
+
+    maxDiff = None
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+
+class IterGlobalDeclarationsTests(TestCaseBase):
+
+    def test_functions(self):
+        tests = [
+            (textwrap.dedent('''
+                void func1() {
+                    return;
+                }
+                '''),
+             textwrap.dedent('''
+                void func1() {
+                return;
+                }
+                ''').strip(),
+             ),
+            (textwrap.dedent('''
+                static unsigned int * _func1(
+                    const char *arg1,
+                    int *arg2
+                    long long arg3
+                    )
+                {
+                    return _do_something(arg1, arg2, arg3);
+                }
+                '''),
+             textwrap.dedent('''
+                static unsigned int * _func1( const char *arg1, int *arg2 long long arg3 ) {
+                return _do_something(arg1, arg2, arg3);
+                }
+                ''').strip(),
+             ),
+            (textwrap.dedent('''
+                static PyObject *
+                _func1(const char *arg1, PyObject *arg2)
+                {
+                    static int initialized = 0;
+                    if (!initialized) {
+                        initialized = 1;
+                        _init(arg1);
+                    }
+
+                    PyObject *result = _do_something(arg1, arg2);
+                    Py_INCREF(result);
+                    return result;
+                }
+                '''),
+             textwrap.dedent('''
+                static PyObject * _func1(const char *arg1, PyObject *arg2) {
+                static int initialized = 0;
+                if (!initialized) {
+                initialized = 1;
+                _init(arg1);
+                }
+                PyObject *result = _do_something(arg1, arg2);
+                Py_INCREF(result);
+                return result;
+                }
+                ''').strip(),
+             ),
+            ]
+        for lines, expected in tests:
+            body = textwrap.dedent(
+                    expected.partition('{')[2].rpartition('}')[0]
+                    ).strip()
+            expected = (expected, body)
+            with self.subTest(lines):
+                lines = lines.splitlines()
+
+                stmts = list(iter_global_declarations(lines))
+
+                self.assertEqual(stmts, [expected])
+
+    @unittest.expectedFailure
+    def test_declarations(self):
+        tests = [
+            'int spam;',
+            'long long spam;',
+            'static const int const *spam;',
+            'int spam;',
+            'typedef int myint;',
+            'typedef PyObject * (*unaryfunc)(PyObject *);',
+            # typedef struct
+            # inline struct
+            # enum
+            # inline enum
+            ]
+        for text in tests:
+            expected = (text,
+                        ' '.join(l.strip() for l in text.splitlines()))
+            with self.subTest(lines):
+                lines = lines.splitlines()
+
+                stmts = list(iter_global_declarations(lines))
+
+                self.assertEqual(stmts, [expected])
+
+    @unittest.expectedFailure
+    def test_declaration_multiple_vars(self):
+        lines = ['static const int const *spam, *ham=NULL, eggs = 3;']
+
+        stmts = list(iter_global_declarations(lines))
+
+        self.assertEqual(stmts, [
+            ('static const int const *spam;', None),
+            ('static const int *ham=NULL;', None),
+            ('static const int eggs = 3;', None),
+            ])
+
+    def test_mixed(self):
+        lines = textwrap.dedent('''
+           int spam;
+           static const char const *eggs;
+
+           PyObject * start(void) {
+               static int initialized = 0;
+               if (initialized) {
+                   initialized = 1;
+                   init();
+               }
+               return _start();
+           }
+
+           char* ham;
+
+           static int stop(char *reason) {
+               ham = reason;
+               return _stop();
+           }
+           ''').splitlines()
+        expected = [
+            (textwrap.dedent('''
+                PyObject * start(void) {
+                static int initialized = 0;
+                if (initialized) {
+                initialized = 1;
+                init();
+                }
+                return _start();
+                }
+                ''').strip(),
+             textwrap.dedent('''
+                static int initialized = 0;
+                if (initialized) {
+                initialized = 1;
+                init();
+                }
+                return _start();
+                ''').strip(),
+             ),
+            (textwrap.dedent('''
+                static int stop(char *reason) {
+                ham = reason;
+                return _stop();
+                }
+                ''').strip(),
+             textwrap.dedent('''
+                ham = reason;
+                return _stop();
+                ''').strip(),
+             ),
+            ]
+
+        stmts = list(iter_global_declarations(lines))
+
+        self.assertEqual(stmts, expected)
+        #self.assertEqual([stmt for stmt, _ in stmts],
+        #                 [stmt for stmt, _ in expected])
+        #self.assertEqual([body for _, body in stmts],
+        #                 [body for _, body in expected])
+
+    def test_no_statements(self):
+        lines = []
+
+        stmts = list(iter_global_declarations(lines))
+
+        self.assertEqual(stmts, [])
+
+    def test_bogus(self):
+        tests = [
+                (textwrap.dedent('''
+                    int spam;
+                    static const char const *eggs;
+
+                    PyObject * start(void) {
+                        static int initialized = 0;
+                        if (initialized) {
+                            initialized = 1;
+                            init();
+                        }
+                        return _start();
+                    }
+
+                    char* ham;
+
+                    static int _stop(void) {
+                    // missing closing bracket
+
+                    static int stop(char *reason) {
+                        ham = reason;
+                        return _stop();
+                    }
+                    '''),
+                 [(textwrap.dedent('''
+                    PyObject * start(void) {
+                    static int initialized = 0;
+                    if (initialized) {
+                    initialized = 1;
+                    init();
+                    }
+                    return _start();
+                    }
+                    ''').strip(),
+                   textwrap.dedent('''
+                    static int initialized = 0;
+                    if (initialized) {
+                    initialized = 1;
+                    init();
+                    }
+                    return _start();
+                    ''').strip(),
+                   ),
+                   # Neither "stop()" nor "_stop()" are here.
+                  ],
+                 ),
+                ]
+        for lines, expected in tests:
+            with self.subTest(lines):
+                lines = lines.splitlines()
+
+                stmts = list(iter_global_declarations(lines))
+
+                self.assertEqual(stmts, expected)
+                #self.assertEqual([stmt for stmt, _ in stmts],
+                #                 [stmt for stmt, _ in expected])
+                #self.assertEqual([body for _, body in stmts],
+                #                 [body for _, body in expected])
+
+    def test_ignore_comments(self):
+        tests = [
+            ('// msg', None),
+            ('// int stmt;', None),
+            ('    // ...    ', None),
+            ('// /*', None),
+            ('/* int stmt; */', None),
+            ("""
+             /**
+              * ...
+              * int stmt;
+              */
+             """, None),
+            ]
+        for lines, expected in tests:
+            with self.subTest(lines):
+                lines = lines.splitlines()
+
+                stmts = list(iter_global_declarations(lines))
+
+                self.assertEqual(stmts, [expected] if expected else [])
+
+
+class IterLocalStatementsTests(TestCaseBase):
+
+    def test_vars(self):
+        tests = [
+            # POTS
+            'int spam;',
+            'unsigned int spam;',
+            'char spam;',
+            'float spam;',
+
+            # typedefs
+            'uint spam;',
+            'MyType spam;',
+
+            # complex
+            'struct myspam spam;',
+            'union choice spam;',
+            # inline struct
+            # inline union
+            # enum?
+            ]
+        # pointers
+        tests.extend([
+            # POTS
+            'int * spam;',
+            'unsigned int * spam;',
+            'char *spam;',
+            'char const *spam = "spamspamspam...";',
+            # typedefs
+            'MyType *spam;',
+            # complex
+            'struct myspam *spam;',
+            'union choice *spam;',
+            # packed with details
+            'const char const *spam;',
+            # void pointer
+            'void *data = NULL;',
+            # function pointers
+            'int (* func)(char *arg1);',
+            'char * (* func)(void);',
+            ])
+        # storage class
+        tests.extend([
+            'static int spam;',
+            'extern int spam;',
+            'static unsigned int spam;',
+            'static struct myspam spam;',
+            ])
+        # type qualifier
+        tests.extend([
+            'const int spam;',
+            'const unsigned int spam;',
+            'const struct myspam spam;',
+            ])
+        # combined
+        tests.extend([
+            'const char *spam = eggs;',
+            'static const char const *spam = "spamspamspam...";',
+            'extern const char const *spam;',
+            'static void *data = NULL;',
+            'static int (const * func)(char *arg1) = func1;',
+            'static char * (* func)(void);',
+            ])
+        for line in tests:
+            expected = line
+            with self.subTest(line):
+                stmts = list(iter_local_statements([line]))
+
+                self.assertEqual(stmts, [(expected, None)])
+
+    @unittest.expectedFailure
+    def test_vars_multiline_var(self):
+        lines = textwrap.dedent('''
+            PyObject *
+            spam
+            = NULL;
+            ''').splitlines()
+        expected = 'PyObject * spam = NULL;'
+
+        stmts = list(iter_local_statements(lines))
+
+        self.assertEqual(stmts, [(expected, None)])
+
+    @unittest.expectedFailure
+    def test_declaration_multiple_vars(self):
+        lines = ['static const int const *spam, *ham=NULL, ham2[]={1, 2, 3}, ham3[2]={1, 2}, eggs = 3;']
+
+        stmts = list(iter_global_declarations(lines))
+
+        self.assertEqual(stmts, [
+            ('static const int const *spam;', None),
+            ('static const int *ham=NULL;', None),
+            ('static const int ham[]={1, 2, 3};', None),
+            ('static const int ham[2]={1, 2};', None),
+            ('static const int eggs = 3;', None),
+            ])
+
+    @unittest.expectedFailure
+    def test_other_simple(self):
+        raise NotImplementedError
+
+    @unittest.expectedFailure
+    def test_compound(self):
+        raise NotImplementedError
+
+    @unittest.expectedFailure
+    def test_mixed(self):
+        raise NotImplementedError
+
+    def test_no_statements(self):
+        lines = []
+
+        stmts = list(iter_local_statements(lines))
+
+        self.assertEqual(stmts, [])
+
+    @unittest.expectedFailure
+    def test_bogus(self):
+        raise NotImplementedError
+
+    def test_ignore_comments(self):
+        tests = [
+            ('// msg', None),
+            ('// int stmt;', None),
+            ('    // ...    ', None),
+            ('// /*', None),
+            ('/* int stmt; */', None),
+            ("""
+             /**
+              * ...
+              * int stmt;
+              */
+             """, None),
+            # mixed with statements
+            ('int stmt; // ...', ('int stmt;', None)),
+            ( 'int stmt; /* ...  */', ('int stmt;', None)),
+            ( '/* ...  */ int stmt;', ('int stmt;', None)),
+            ]
+        for lines, expected in tests:
+            with self.subTest(lines):
+                lines = lines.splitlines()
+
+                stmts = list(iter_local_statements(lines))
+
+                self.assertEqual(stmts, [expected] if expected else [])
+
+
+class ParseFuncTests(TestCaseBase):
+
+    def test_typical(self):
+        tests = [
+            ('PyObject *\nspam(char *a)\n{\nreturn _spam(a);\n}',
+             'return _spam(a);',
+             ('spam', 'PyObject * spam(char *a)'),
+             ),
+            ]
+        for stmt, body, expected in tests:
+            with self.subTest(stmt):
+                name, signature = parse_func(stmt, body)
+
+                self.assertEqual((name, signature), expected)
+
+
+class ParseVarTests(TestCaseBase):
+
+    def test_typical(self):
+        tests = [
+            # POTS
+            ('int spam;', ('spam', 'int')),
+            ('unsigned int spam;', ('spam', 'unsigned int')),
+            ('char spam;', ('spam', 'char')),
+            ('float spam;', ('spam', 'float')),
+
+            # typedefs
+            ('uint spam;', ('spam', 'uint')),
+            ('MyType spam;', ('spam', 'MyType')),
+
+            # complex
+            ('struct myspam spam;', ('spam', 'struct myspam')),
+            ('union choice spam;', ('spam', 'union choice')),
+            # inline struct
+            # inline union
+            # enum?
+            ]
+        # pointers
+        tests.extend([
+            # POTS
+            ('int * spam;', ('spam', 'int *')),
+            ('unsigned int * spam;', ('spam', 'unsigned int *')),
+            ('char *spam;', ('spam', 'char *')),
+            ('char const *spam = "spamspamspam...";', ('spam', 'char const *')),
+            # typedefs
+            ('MyType *spam;', ('spam', 'MyType *')),
+            # complex
+            ('struct myspam *spam;', ('spam', 'struct myspam *')),
+            ('union choice *spam;', ('spam', 'union choice *')),
+            # packed with details
+            ('const char const *spam;', ('spam', 'const char const *')),
+            # void pointer
+            ('void *data = NULL;', ('data', 'void *')),
+            # function pointers
+            ('int (* func)(char *);', ('func', 'int (*)(char *)')),
+            ('char * (* func)(void);', ('func', 'char * (*)(void)')),
+            ])
+        # storage class
+        tests.extend([
+            ('static int spam;', ('spam', 'static int')),
+            ('extern int spam;', ('spam', 'extern int')),
+            ('static unsigned int spam;', ('spam', 'static unsigned int')),
+            ('static struct myspam spam;', ('spam', 'static struct myspam')),
+            ])
+        # type qualifier
+        tests.extend([
+            ('const int spam;', ('spam', 'const int')),
+            ('const unsigned int spam;', ('spam', 'const unsigned int')),
+            ('const struct myspam spam;', ('spam', 'const struct myspam')),
+            ])
+        # combined
+        tests.extend([
+            ('const char *spam = eggs;', ('spam', 'const char *')),
+            ('static const char const *spam = "spamspamspam...";',
+             ('spam', 'static const char const *')),
+            ('extern const char const *spam;',
+             ('spam', 'extern const char const *')),
+            ('static void *data = NULL;', ('data', 'static void *')),
+            ('static int (const * func)(char *) = func1;',
+             ('func', 'static int (const *)(char *)')),
+            ('static char * (* func)(void);',
+             ('func', 'static char * (*)(void)')),
+            ])
+        for stmt, expected in tests:
+            with self.subTest(stmt):
+                name, vartype = parse_var(stmt)
+
+                self.assertEqual((name, vartype), expected)
+
+
+@unittest.skip('not finished')
+class ParseCompoundTests(TestCaseBase):
+
+    def test_typical(self):
+        headers, bodies = parse_compound(stmt, blocks)
+        ...
+
+
+class IterVariablesTests(TestCaseBase):
+
+    _return_iter_source_lines = None
+    _return_iter_global = None
+    _return_iter_local = None
+    _return_parse_func = None
+    _return_parse_var = None
+    _return_parse_compound = None
+
+    def _iter_source_lines(self, filename):
+        self.calls.append(
+                ('_iter_source_lines', (filename,)))
+        return self._return_iter_source_lines.splitlines()
+
+    def _iter_global(self, lines):
+        self.calls.append(
+                ('_iter_global', (lines,)))
+        try:
+            return self._return_iter_global.pop(0)
+        except IndexError:
+            return ('???', None)
+
+    def _iter_local(self, lines):
+        self.calls.append(
+                ('_iter_local', (lines,)))
+        try:
+            return self._return_iter_local.pop(0)
+        except IndexError:
+            return ('???', None)
+
+    def _parse_func(self, stmt, body):
+        self.calls.append(
+                ('_parse_func', (stmt, body)))
+        try:
+            return self._return_parse_func.pop(0)
+        except IndexError:
+            return ('???', '???')
+
+    def _parse_var(self, lines):
+        self.calls.append(
+                ('_parse_var', (lines,)))
+        try:
+            return self._return_parse_var.pop(0)
+        except IndexError:
+            return ('???', '???')
+
+    def _parse_compound(self, stmt, blocks):
+        self.calls.append(
+                ('_parse_compound', (stmt, blocks)))
+        try:
+            return self._return_parse_compound.pop(0)
+        except IndexError:
+            return (['???'], ['???'])
+
+    def test_empty_file(self):
+        self._return_iter_source_lines = ''
+        self._return_iter_global = [
+            [],
+            ]
+        self._return_parse_func = None
+        self._return_parse_var = None
+        self._return_parse_compound = None
+
+        srcvars = list(iter_variables('spam.c',
+                                      _iter_source_lines=self._iter_source_lines,
+                                      _iter_global=self._iter_global,
+                                      _iter_local=self._iter_local,
+                                      _parse_func=self._parse_func,
+                                      _parse_var=self._parse_var,
+                                      _parse_compound=self._parse_compound,
+                                      ))
+
+        self.assertEqual(srcvars, [])
+        self.assertEqual(self.calls, [
+            ('_iter_source_lines', ('spam.c',)),
+            ('_iter_global', ([],)),
+            ])
+
+    def test_no_statements(self):
+        content = textwrap.dedent('''
+        ...
+        ''')
+        self._return_iter_source_lines = content
+        self._return_iter_global = [
+            [],
+            ]
+        self._return_parse_func = None
+        self._return_parse_var = None
+        self._return_parse_compound = None
+
+        srcvars = list(iter_variables('spam.c',
+                                      _iter_source_lines=self._iter_source_lines,
+                                      _iter_global=self._iter_global,
+                                      _iter_local=self._iter_local,
+                                      _parse_func=self._parse_func,
+                                      _parse_var=self._parse_var,
+                                      _parse_compound=self._parse_compound,
+                                      ))
+
+        self.assertEqual(srcvars, [])
+        self.assertEqual(self.calls, [
+            ('_iter_source_lines', ('spam.c',)),
+            ('_iter_global', (content.splitlines(),)),
+            ])
+
+    def test_typical(self):
+        content = textwrap.dedent('''
+        ...
+        ''')
+        self._return_iter_source_lines = content
+        self._return_iter_global = [
+            [('<lines 1>', None),  # var1
+             ('<lines 2>', None),  # non-var
+             ('<lines 3>', None),  # var2
+             ('<lines 4>', '<body 1>'),  # func1
+             ('<lines 9>', None),  # var4
+             ],
+            ]
+        self._return_iter_local = [
+            # func1
+            [('<lines 5>', None),  # var3
+             ('<lines 6>', [('<header 1>', '<block 1>')]),  # if
+             ('<lines 8>', None),  # non-var
+             ],
+            # if
+            [('<lines 7>', None),  # var2 ("collision" with global var)
+             ],
+            ]
+        self._return_parse_func = [
+            ('func1', '<sig 1>'),
+            ]
+        self._return_parse_var = [
+            ('var1', '<vartype 1>'),
+            (None, None),
+            ('var2', '<vartype 2>'),
+            ('var3', '<vartype 3>'),
+            ('var2', '<vartype 2b>'),
+            ('var4', '<vartype 4>'),
+            (None, None),
+            (None, None),
+            (None, None),
+            ('var5', '<vartype 5>'),
+            ]
+        self._return_parse_compound = [
+            ([[
+                'if (',
+                '<simple>',
+                ')',
+                ],
+              ],
+             ['<block 1>']),
+            ]
+
+        srcvars = list(iter_variables('spam.c',
+                                      _iter_source_lines=self._iter_source_lines,
+                                      _iter_global=self._iter_global,
+                                      _iter_local=self._iter_local,
+                                      _parse_func=self._parse_func,
+                                      _parse_var=self._parse_var,
+                                      _parse_compound=self._parse_compound,
+                                      ))
+
+        self.assertEqual(srcvars, [
+            (None, 'var1', '<vartype 1>'),
+            (None, 'var2', '<vartype 2>'),
+            ('func1', 'var3', '<vartype 3>'),
+            ('func1', 'var2', '<vartype 2b>'),
+            ('func1', 'var4', '<vartype 4>'),
+            (None, 'var5', '<vartype 5>'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_iter_source_lines', ('spam.c',)),
+            ('_iter_global', (content.splitlines(),)),
+            ('_parse_var', ('<lines 1>',)),
+            ('_parse_var', ('<lines 2>',)),
+            ('_parse_var', ('<lines 3>',)),
+            ('_parse_func', ('<lines 4>', '<body 1>')),
+            ('_iter_local', (['<body 1>'],)),
+            ('_parse_var', ('<lines 5>',)),
+            ('_parse_compound', ('<lines 6>', [('<header 1>', '<block 1>')])),
+            ('_parse_var', ('if (',)),
+            ('_parse_var', ('<simple>',)),
+            ('_parse_var', (')',)),
+            ('_parse_var', ('<lines 8>',)),
+            ('_iter_local', (['<block 1>'],)),
+            ('_parse_var', ('<lines 7>',)),
+            ('_parse_var', ('<lines 9>',)),
+            ])
+
+    def test_no_locals(self):
+        content = textwrap.dedent('''
+        ...
+        ''')
+        self._return_iter_source_lines = content
+        self._return_iter_global = [
+            [('<lines 1>', None),  # var1
+             ('<lines 2>', None),  # non-var
+             ('<lines 3>', None),  # var2
+             ('<lines 4>', '<body 1>'),  # func1
+             ],
+            ]
+        self._return_iter_local = [
+            # func1
+            [('<lines 5>', None),  # non-var
+             ('<lines 6>', [('<header 1>', '<block 1>')]),  # if
+             ('<lines 8>', None),  # non-var
+             ],
+            # if
+            [('<lines 7>', None),  # non-var
+             ],
+            ]
+        self._return_parse_func = [
+            ('func1', '<sig 1>'),
+            ]
+        self._return_parse_var = [
+            ('var1', '<vartype 1>'),
+            (None, None),
+            ('var2', '<vartype 2>'),
+            (None, None),
+            (None, None),
+            (None, None),
+            (None, None),
+            (None, None),
+            (None, None),
+            ]
+        self._return_parse_compound = [
+            ([[
+                'if (',
+                '<simple>',
+                ')',
+                ],
+              ],
+             ['<block 1>']),
+            ]
+
+        srcvars = list(iter_variables('spam.c',
+                                      _iter_source_lines=self._iter_source_lines,
+                                      _iter_global=self._iter_global,
+                                      _iter_local=self._iter_local,
+                                      _parse_func=self._parse_func,
+                                      _parse_var=self._parse_var,
+                                      _parse_compound=self._parse_compound,
+                                      ))
+
+        self.assertEqual(srcvars, [
+            (None, 'var1', '<vartype 1>'),
+            (None, 'var2', '<vartype 2>'),
+            ])
+        self.assertEqual(self.calls, [
+            ('_iter_source_lines', ('spam.c',)),
+            ('_iter_global', (content.splitlines(),)),
+            ('_parse_var', ('<lines 1>',)),
+            ('_parse_var', ('<lines 2>',)),
+            ('_parse_var', ('<lines 3>',)),
+            ('_parse_func', ('<lines 4>', '<body 1>')),
+            ('_iter_local', (['<body 1>'],)),
+            ('_parse_var', ('<lines 5>',)),
+            ('_parse_compound', ('<lines 6>', [('<header 1>', '<block 1>')])),
+            ('_parse_var', ('if (',)),
+            ('_parse_var', ('<simple>',)),
+            ('_parse_var', (')',)),
+            ('_parse_var', ('<lines 8>',)),
+            ('_iter_local', (['<block 1>'],)),
+            ('_parse_var', ('<lines 7>',)),
+            ])
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_info.py b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_info.py
new file mode 100644
index 0000000..1dfe5d0
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_info.py
@@ -0,0 +1,208 @@
+import string
+import unittest
+
+from ..util import PseudoStr, StrProxy, Object
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common.info import ID
+    from c_parser.info import (
+        normalize_vartype, Variable,
+        )
+
+
+class NormalizeVartypeTests(unittest.TestCase):
+
+    def test_basic(self):
+        tests = [
+                (None, None),
+                ('', ''),
+                ('int', 'int'),
+                (PseudoStr('int'), 'int'),
+                (StrProxy('int'), 'int'),
+                ]
+        for vartype, expected in tests:
+            with self.subTest(vartype):
+                normalized = normalize_vartype(vartype)
+
+                self.assertEqual(normalized, expected)
+
+
+class VariableTests(unittest.TestCase):
+
+    VALID_ARGS = (
+            ('x/y/z/spam.c', 'func', 'eggs'),
+            'int',
+            )
+    VALID_KWARGS = dict(zip(Variable._fields, VALID_ARGS))
+    VALID_EXPECTED = VALID_ARGS
+
+    def test_init_typical_global(self):
+        static = Variable(
+                id=ID(
+                    filename='x/y/z/spam.c',
+                    funcname=None,
+                    name='eggs',
+                    ),
+                vartype='int',
+                )
+
+        self.assertEqual(static, (
+                ('x/y/z/spam.c', None, 'eggs'),
+                'int',
+                ))
+
+    def test_init_typical_local(self):
+        static = Variable(
+                id=ID(
+                    filename='x/y/z/spam.c',
+                    funcname='func',
+                    name='eggs',
+                    ),
+                vartype='int',
+                )
+
+        self.assertEqual(static, (
+                ('x/y/z/spam.c', 'func', 'eggs'),
+                'int',
+                ))
+
+    def test_init_all_missing(self):
+        for value in ('', None):
+            with self.subTest(repr(value)):
+                static = Variable(
+                        id=value,
+                        vartype=value,
+                        )
+
+                self.assertEqual(static, (
+                        None,
+                        None,
+                        ))
+
+    def test_init_all_coerced(self):
+        id = ID('x/y/z/spam.c', 'func', 'spam')
+        tests = [
+            ('str subclass',
+             dict(
+                 id=(
+                    PseudoStr('x/y/z/spam.c'),
+                    PseudoStr('func'),
+                    PseudoStr('spam'),
+                    ),
+                 vartype=PseudoStr('int'),
+                 ),
+             (id,
+              'int',
+              )),
+            ('non-str 1',
+             dict(
+                 id=id,
+                 vartype=Object(),
+                 ),
+             (id,
+              '<object>',
+              )),
+            ('non-str 2',
+             dict(
+                 id=id,
+                 vartype=StrProxy('variable'),
+                 ),
+             (id,
+              'variable',
+              )),
+            ('non-str',
+             dict(
+                 id=id,
+                 vartype=('a', 'b', 'c'),
+                 ),
+             (id,
+              "('a', 'b', 'c')",
+              )),
+            ]
+        for summary, kwargs, expected in tests:
+            with self.subTest(summary):
+                static = Variable(**kwargs)
+
+                for field in Variable._fields:
+                    value = getattr(static, field)
+                    if field == 'id':
+                        self.assertIs(type(value), ID)
+                    else:
+                        self.assertIs(type(value), str)
+                self.assertEqual(tuple(static), expected)
+
+    def test_iterable(self):
+        static = Variable(**self.VALID_KWARGS)
+
+        id, vartype = static
+
+        values = (id, vartype)
+        for value, expected in zip(values, self.VALID_EXPECTED):
+            self.assertEqual(value, expected)
+
+    def test_fields(self):
+        static = Variable(('a', 'b', 'z'), 'x')
+
+        self.assertEqual(static.id, ('a', 'b', 'z'))
+        self.assertEqual(static.vartype, 'x')
+
+    def test___getattr__(self):
+        static = Variable(('a', 'b', 'z'), 'x')
+
+        self.assertEqual(static.filename, 'a')
+        self.assertEqual(static.funcname, 'b')
+        self.assertEqual(static.name, 'z')
+
+    def test_validate_typical(self):
+        static = Variable(
+                id=ID(
+                    filename='x/y/z/spam.c',
+                    funcname='func',
+                    name='eggs',
+                    ),
+                vartype='int',
+                )
+
+        static.validate()  # This does not fail.
+
+    def test_validate_missing_field(self):
+        for field in Variable._fields:
+            with self.subTest(field):
+                static = Variable(**self.VALID_KWARGS)
+                static = static._replace(**{field: None})
+
+                with self.assertRaises(TypeError):
+                    static.validate()
+
+    def test_validate_bad_field(self):
+        badch = tuple(c for c in string.punctuation + string.digits)
+        notnames = (
+                '1a',
+                'a.b',
+                'a-b',
+                '&a',
+                'a++',
+                ) + badch
+        tests = [
+            ('id', ()),  # Any non-empty str is okay.
+            ('vartype', ()),  # Any non-empty str is okay.
+            ]
+        seen = set()
+        for field, invalid in tests:
+            for value in invalid:
+                seen.add(value)
+                with self.subTest(f'{field}={value!r}'):
+                    static = Variable(**self.VALID_KWARGS)
+                    static = static._replace(**{field: value})
+
+                    with self.assertRaises(ValueError):
+                        static.validate()
+
+        for field, invalid in tests:
+            valid = seen - set(invalid)
+            for value in valid:
+                with self.subTest(f'{field}={value!r}'):
+                    static = Variable(**self.VALID_KWARGS)
+                    static = static._replace(**{field: value})
+
+                    static.validate()  # This does not fail.
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_preprocessor.py b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_preprocessor.py
new file mode 100644
index 0000000..89e1557
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_parser/test_preprocessor.py
@@ -0,0 +1,1562 @@
+import itertools
+import textwrap
+import unittest
+import sys
+
+from ..util import wrapped_arg_combos, StrProxy
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_parser.preprocessor import (
+        iter_lines,
+        # directives
+        parse_directive, PreprocessorDirective,
+        Constant, Macro, IfDirective, Include, OtherDirective,
+        )
+
+
+class TestCaseBase(unittest.TestCase):
+
+    maxDiff = None
+
+    def reset(self):
+        self._calls = []
+        self.errors = None
+
+    @property
+    def calls(self):
+        try:
+            return self._calls
+        except AttributeError:
+            self._calls = []
+            return self._calls
+
+    errors = None
+
+    def try_next_exc(self):
+        if not self.errors:
+            return
+        if exc := self.errors.pop(0):
+            raise exc
+
+    def check_calls(self, *expected):
+        self.assertEqual(self.calls, list(expected))
+        self.assertEqual(self.errors or [], [])
+
+
+class IterLinesTests(TestCaseBase):
+
+    parsed = None
+
+    def check_calls(self, *expected):
+        super().check_calls(*expected)
+        self.assertEqual(self.parsed or [], [])
+
+    def _parse_directive(self, line):
+        self.calls.append(
+                ('_parse_directive', line))
+        self.try_next_exc()
+        return self.parsed.pop(0)
+
+    def test_no_lines(self):
+        lines = []
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [])
+        self.check_calls()
+
+    def test_no_directives(self):
+        lines = textwrap.dedent('''
+
+            // xyz
+            typedef enum {
+                SPAM
+                EGGS
+            } kind;
+
+            struct info {
+                kind kind;
+                int status;
+            };
+
+            typedef struct spam {
+                struct info info;
+            } myspam;
+
+            static int spam = 0;
+
+            /**
+             * ...
+             */
+            static char *
+            get_name(int arg,
+                     char *default,
+                     )
+            {
+                return default
+            }
+
+            int check(void) {
+                return 0;
+            }
+
+            ''')[1:-1].splitlines()
+        expected = [(lno, line, None, ())
+                    for lno, line in enumerate(lines, 1)]
+        expected[1] = (2, ' ', None, ())
+        expected[20] = (21, ' ', None, ())
+        del expected[19]
+        del expected[18]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, expected)
+        self.check_calls()
+
+    def test_single_directives(self):
+        tests = [
+            ('#include <stdio>', Include('<stdio>')),
+            ('#define SPAM 1', Constant('SPAM', '1')),
+            ('#define SPAM() 1', Macro('SPAM', (), '1')),
+            ('#define SPAM(a, b) a = b;', Macro('SPAM', ('a', 'b'), 'a = b;')),
+            ('#if defined(SPAM)', IfDirective('if', 'defined(SPAM)')),
+            ('#ifdef SPAM', IfDirective('ifdef', 'SPAM')),
+            ('#ifndef SPAM', IfDirective('ifndef', 'SPAM')),
+            ('#elseif defined(SPAM)', IfDirective('elseif', 'defined(SPAM)')),
+            ('#else', OtherDirective('else', None)),
+            ('#endif', OtherDirective('endif', None)),
+            ('#error ...', OtherDirective('error', '...')),
+            ('#warning ...', OtherDirective('warning', '...')),
+            ('#__FILE__ ...', OtherDirective('__FILE__', '...')),
+            ('#__LINE__ ...', OtherDirective('__LINE__', '...')),
+            ('#__DATE__ ...', OtherDirective('__DATE__', '...')),
+            ('#__TIME__ ...', OtherDirective('__TIME__', '...')),
+            ('#__TIMESTAMP__ ...', OtherDirective('__TIMESTAMP__', '...')),
+            ]
+        for line, directive in tests:
+            with self.subTest(line):
+                self.reset()
+                self.parsed = [
+                    directive,
+                    ]
+                text = textwrap.dedent('''
+                    static int spam = 0;
+                    {}
+                    static char buffer[256];
+                    ''').strip().format(line)
+                lines = text.strip().splitlines()
+
+                results = list(
+                        iter_lines(lines, _parse_directive=self._parse_directive))
+
+                self.assertEqual(results, [
+                    (1, 'static int spam = 0;', None, ()),
+                    (2, line, directive, ()),
+                    ((3, 'static char buffer[256];', None, ('defined(SPAM)',))
+                     if directive.kind in ('if', 'ifdef', 'elseif')
+                     else (3, 'static char buffer[256];', None, ('! defined(SPAM)',))
+                     if directive.kind == 'ifndef'
+                     else (3, 'static char buffer[256];', None, ())),
+                    ])
+                self.check_calls(
+                        ('_parse_directive', line),
+                        )
+
+    def test_directive_whitespace(self):
+        line = ' # define  eggs  (  a  ,  b  )  {  a  =  b  ;  }  '
+        directive = Macro('eggs', ('a', 'b'), '{ a = b; }')
+        self.parsed = [
+            directive,
+            ]
+        lines = [line]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, line, directive, ()),
+            ])
+        self.check_calls(
+                ('_parse_directive', '#define eggs ( a , b ) { a = b ; }'),
+                )
+
+    @unittest.skipIf(sys.platform == 'win32', 'needs fix under Windows')
+    def test_split_lines(self):
+        directive = Macro('eggs', ('a', 'b'), '{ a = b; }')
+        self.parsed = [
+            directive,
+            ]
+        text = textwrap.dedent(r'''
+            static int spam = 0;
+            #define eggs(a, b) \
+                { \
+                    a = b; \
+                }
+            static char buffer[256];
+            ''').strip()
+        lines = [line + '\n' for line in text.splitlines()]
+        lines[-1] = lines[-1][:-1]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, 'static int spam = 0;\n', None, ()),
+            (5, '#define eggs(a, b)      {          a = b;      }\n', directive, ()),
+            (6, 'static char buffer[256];', None, ()),
+            ])
+        self.check_calls(
+                ('_parse_directive', '#define eggs(a, b) { a = b; }'),
+                )
+
+    def test_nested_conditions(self):
+        directives = [
+            IfDirective('ifdef', 'SPAM'),
+            IfDirective('if', 'SPAM == 1'),
+            IfDirective('elseif', 'SPAM == 2'),
+            OtherDirective('else', None),
+            OtherDirective('endif', None),
+            OtherDirective('endif', None),
+            ]
+        self.parsed = list(directives)
+        text = textwrap.dedent(r'''
+            static int spam = 0;
+
+            #ifdef SPAM
+            static int start = 0;
+            #  if SPAM == 1
+            static char buffer[10];
+            #  elif SPAM == 2
+            static char buffer[100];
+            #  else
+            static char buffer[256];
+            #  endif
+            static int end = 0;
+            #endif
+
+            static int eggs = 0;
+            ''').strip()
+        lines = [line for line in text.splitlines() if line.strip()]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, 'static int spam = 0;', None, ()),
+            (2, '#ifdef SPAM', directives[0], ()),
+            (3, 'static int start = 0;', None, ('defined(SPAM)',)),
+            (4, '#  if SPAM == 1', directives[1], ('defined(SPAM)',)),
+            (5, 'static char buffer[10];', None, ('defined(SPAM)', 'SPAM == 1')),
+            (6, '#  elif SPAM == 2', directives[2], ('defined(SPAM)', 'SPAM == 1')),
+            (7, 'static char buffer[100];', None, ('defined(SPAM)', '! (SPAM == 1)', 'SPAM == 2')),
+            (8, '#  else', directives[3], ('defined(SPAM)', '! (SPAM == 1)', 'SPAM == 2')),
+            (9, 'static char buffer[256];', None, ('defined(SPAM)', '! (SPAM == 1)', '! (SPAM == 2)')),
+            (10, '#  endif', directives[4], ('defined(SPAM)', '! (SPAM == 1)', '! (SPAM == 2)')),
+            (11, 'static int end = 0;', None, ('defined(SPAM)',)),
+            (12, '#endif', directives[5], ('defined(SPAM)',)),
+            (13, 'static int eggs = 0;', None, ()),
+            ])
+        self.check_calls(
+                ('_parse_directive', '#ifdef SPAM'),
+                ('_parse_directive', '#if SPAM == 1'),
+                ('_parse_directive', '#elif SPAM == 2'),
+                ('_parse_directive', '#else'),
+                ('_parse_directive', '#endif'),
+                ('_parse_directive', '#endif'),
+                )
+
+    def test_split_blocks(self):
+        directives = [
+            IfDirective('ifdef', 'SPAM'),
+            OtherDirective('else', None),
+            OtherDirective('endif', None),
+            ]
+        self.parsed = list(directives)
+        text = textwrap.dedent(r'''
+            void str_copy(char *buffer, *orig);
+
+            int init(char *name) {
+                static int initialized = 0;
+                if (initialized) {
+                    return 0;
+                }
+            #ifdef SPAM
+                static char buffer[10];
+                str_copy(buffer, char);
+            }
+
+            void copy(char *buffer, *orig) {
+                strncpy(buffer, orig, 9);
+                buffer[9] = 0;
+            }
+
+            #else
+                static char buffer[256];
+                str_copy(buffer, char);
+            }
+
+            void copy(char *buffer, *orig) {
+                strcpy(buffer, orig);
+            }
+
+            #endif
+            ''').strip()
+        lines = [line for line in text.splitlines() if line.strip()]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, 'void str_copy(char *buffer, *orig);', None, ()),
+            (2, 'int init(char *name) {', None, ()),
+            (3, '    static int initialized = 0;', None, ()),
+            (4, '    if (initialized) {', None, ()),
+            (5, '        return 0;', None, ()),
+            (6, '    }', None, ()),
+
+            (7, '#ifdef SPAM', directives[0], ()),
+
+            (8, '    static char buffer[10];', None, ('defined(SPAM)',)),
+            (9, '    str_copy(buffer, char);', None, ('defined(SPAM)',)),
+            (10, '}', None, ('defined(SPAM)',)),
+            (11, 'void copy(char *buffer, *orig) {', None, ('defined(SPAM)',)),
+            (12, '    strncpy(buffer, orig, 9);', None, ('defined(SPAM)',)),
+            (13, '    buffer[9] = 0;', None, ('defined(SPAM)',)),
+            (14, '}', None, ('defined(SPAM)',)),
+
+            (15, '#else', directives[1], ('defined(SPAM)',)),
+
+            (16, '    static char buffer[256];', None, ('! (defined(SPAM))',)),
+            (17, '    str_copy(buffer, char);', None, ('! (defined(SPAM))',)),
+            (18, '}', None, ('! (defined(SPAM))',)),
+            (19, 'void copy(char *buffer, *orig) {', None, ('! (defined(SPAM))',)),
+            (20, '    strcpy(buffer, orig);', None, ('! (defined(SPAM))',)),
+            (21, '}', None, ('! (defined(SPAM))',)),
+
+            (22, '#endif', directives[2], ('! (defined(SPAM))',)),
+            ])
+        self.check_calls(
+                ('_parse_directive', '#ifdef SPAM'),
+                ('_parse_directive', '#else'),
+                ('_parse_directive', '#endif'),
+                )
+
+    @unittest.skipIf(sys.platform == 'win32', 'needs fix under Windows')
+    def test_basic(self):
+        directives = [
+            Include('<stdio.h>'),
+            IfDirective('ifdef', 'SPAM'),
+            IfDirective('if', '! defined(HAM) || !HAM'),
+            Constant('HAM', '0'),
+            IfDirective('elseif', 'HAM < 0'),
+            Constant('HAM', '-1'),
+            OtherDirective('else', None),
+            OtherDirective('endif', None),
+            OtherDirective('endif', None),
+            IfDirective('if', 'defined(HAM) && (HAM < 0 || ! HAM)'),
+            OtherDirective('undef', 'HAM'),
+            OtherDirective('endif', None),
+            IfDirective('ifndef', 'HAM'),
+            OtherDirective('endif', None),
+            ]
+        self.parsed = list(directives)
+        text = textwrap.dedent(r'''
+            #include <stdio.h>
+            print("begin");
+            #ifdef SPAM
+               print("spam");
+               #if ! defined(HAM) || !HAM
+            #      DEFINE HAM 0
+               #elseif HAM < 0
+            #      DEFINE HAM -1
+               #else
+                   print("ham HAM");
+               #endif
+            #endif
+
+            #if defined(HAM) && \
+                (HAM < 0 || ! HAM)
+              print("ham?");
+              #undef HAM
+            # endif
+
+            #ifndef HAM
+               print("no ham");
+            #endif
+            print("end");
+            ''')[1:-1]
+        lines = [line + '\n' for line in text.splitlines()]
+        lines[-1] = lines[-1][:-1]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, '#include <stdio.h>\n', Include('<stdio.h>'), ()),
+            (2, 'print("begin");\n', None, ()),
+            #
+            (3, '#ifdef SPAM\n',
+                IfDirective('ifdef', 'SPAM'),
+                ()),
+            (4, '   print("spam");\n',
+                None,
+                ('defined(SPAM)',)),
+            (5, '   #if ! defined(HAM) || !HAM\n',
+                IfDirective('if', '! defined(HAM) || !HAM'),
+                ('defined(SPAM)',)),
+            (6, '#      DEFINE HAM 0\n',
+                Constant('HAM', '0'),
+                ('defined(SPAM)', '! defined(HAM) || !HAM')),
+            (7, '   #elseif HAM < 0\n',
+                IfDirective('elseif', 'HAM < 0'),
+                ('defined(SPAM)', '! defined(HAM) || !HAM')),
+            (8, '#      DEFINE HAM -1\n',
+                Constant('HAM', '-1'),
+                ('defined(SPAM)', '! (! defined(HAM) || !HAM)', 'HAM < 0')),
+            (9, '   #else\n',
+                OtherDirective('else', None),
+                ('defined(SPAM)', '! (! defined(HAM) || !HAM)', 'HAM < 0')),
+            (10, '       print("ham HAM");\n',
+                None,
+                ('defined(SPAM)', '! (! defined(HAM) || !HAM)', '! (HAM < 0)')),
+            (11, '   #endif\n',
+                OtherDirective('endif', None),
+                ('defined(SPAM)', '! (! defined(HAM) || !HAM)', '! (HAM < 0)')),
+            (12, '#endif\n',
+                OtherDirective('endif', None),
+                ('defined(SPAM)',)),
+            #
+            (13, '\n', None, ()),
+            #
+            (15, '#if defined(HAM) &&      (HAM < 0 || ! HAM)\n',
+                IfDirective('if', 'defined(HAM) && (HAM < 0 || ! HAM)'),
+                ()),
+            (16, '  print("ham?");\n',
+                None,
+                ('defined(HAM) && (HAM < 0 || ! HAM)',)),
+            (17, '  #undef HAM\n',
+                OtherDirective('undef', 'HAM'),
+                ('defined(HAM) && (HAM < 0 || ! HAM)',)),
+            (18, '# endif\n',
+                OtherDirective('endif', None),
+                ('defined(HAM) && (HAM < 0 || ! HAM)',)),
+            #
+            (19, '\n', None, ()),
+            #
+            (20, '#ifndef HAM\n',
+                IfDirective('ifndef', 'HAM'),
+                ()),
+            (21, '   print("no ham");\n',
+                None,
+                ('! defined(HAM)',)),
+            (22, '#endif\n',
+                OtherDirective('endif', None),
+                ('! defined(HAM)',)),
+            #
+            (23, 'print("end");', None, ()),
+            ])
+
+    @unittest.skipIf(sys.platform == 'win32', 'needs fix under Windows')
+    def test_typical(self):
+        # We use Include/compile.h from commit 66c4f3f38b86.  It has
+        # a good enough mix of code without being too large.
+        directives = [
+            IfDirective('ifndef', 'Py_COMPILE_H'),
+            Constant('Py_COMPILE_H', None),
+
+            IfDirective('ifndef', 'Py_LIMITED_API'),
+
+            Include('"code.h"'),
+
+            IfDirective('ifdef', '__cplusplus'),
+            OtherDirective('endif', None),
+
+            Constant('PyCF_MASK', '(CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT | CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION | CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL | CO_FUTURE_GENERATOR_STOP | CO_FUTURE_ANNOTATIONS)'),
+            Constant('PyCF_MASK_OBSOLETE', '(CO_NESTED)'),
+            Constant('PyCF_SOURCE_IS_UTF8', ' 0x0100'),
+            Constant('PyCF_DONT_IMPLY_DEDENT', '0x0200'),
+            Constant('PyCF_ONLY_AST', '0x0400'),
+            Constant('PyCF_IGNORE_COOKIE', '0x0800'),
+            Constant('PyCF_TYPE_COMMENTS', '0x1000'),
+            Constant('PyCF_ALLOW_TOP_LEVEL_AWAIT', '0x2000'),
+
+            IfDirective('ifndef', 'Py_LIMITED_API'),
+            OtherDirective('endif', None),
+
+            Constant('FUTURE_NESTED_SCOPES', '"nested_scopes"'),
+            Constant('FUTURE_GENERATORS', '"generators"'),
+            Constant('FUTURE_DIVISION', '"division"'),
+            Constant('FUTURE_ABSOLUTE_IMPORT', '"absolute_import"'),
+            Constant('FUTURE_WITH_STATEMENT', '"with_statement"'),
+            Constant('FUTURE_PRINT_FUNCTION', '"print_function"'),
+            Constant('FUTURE_UNICODE_LITERALS', '"unicode_literals"'),
+            Constant('FUTURE_BARRY_AS_BDFL', '"barry_as_FLUFL"'),
+            Constant('FUTURE_GENERATOR_STOP', '"generator_stop"'),
+            Constant('FUTURE_ANNOTATIONS', '"annotations"'),
+
+            Macro('PyAST_Compile', ('mod', 's', 'f', 'ar'), 'PyAST_CompileEx(mod, s, f, -1, ar)'),
+
+            Constant('PY_INVALID_STACK_EFFECT', 'INT_MAX'),
+
+            IfDirective('ifdef', '__cplusplus'),
+            OtherDirective('endif', None),
+
+            OtherDirective('endif', None),  # ifndef Py_LIMITED_API
+
+            Constant('Py_single_input', '256'),
+            Constant('Py_file_input', '257'),
+            Constant('Py_eval_input', '258'),
+            Constant('Py_func_type_input', '345'),
+
+            OtherDirective('endif', None),  # ifndef Py_COMPILE_H
+            ]
+        self.parsed = list(directives)
+        text = textwrap.dedent(r'''
+            #ifndef Py_COMPILE_H
+            #define Py_COMPILE_H
+
+            #ifndef Py_LIMITED_API
+            #include "code.h"
+
+            #ifdef __cplusplus
+            extern "C" {
+            #endif
+
+            /* Public interface */
+            struct _node; /* Declare the existence of this type */
+            PyAPI_FUNC(PyCodeObject *) PyNode_Compile(struct _node *, const char *);
+            /* XXX (ncoghlan): Unprefixed type name in a public API! */
+
+            #define PyCF_MASK (CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT | \
+                               CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION | \
+                               CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL | \
+                               CO_FUTURE_GENERATOR_STOP | CO_FUTURE_ANNOTATIONS)
+            #define PyCF_MASK_OBSOLETE (CO_NESTED)
+            #define PyCF_SOURCE_IS_UTF8  0x0100
+            #define PyCF_DONT_IMPLY_DEDENT 0x0200
+            #define PyCF_ONLY_AST 0x0400
+            #define PyCF_IGNORE_COOKIE 0x0800
+            #define PyCF_TYPE_COMMENTS 0x1000
+            #define PyCF_ALLOW_TOP_LEVEL_AWAIT 0x2000
+
+            #ifndef Py_LIMITED_API
+            typedef struct {
+                int cf_flags;  /* bitmask of CO_xxx flags relevant to future */
+                int cf_feature_version;  /* minor Python version (PyCF_ONLY_AST) */
+            } PyCompilerFlags;
+            #endif
+
+            /* Future feature support */
+
+            typedef struct {
+                int ff_features;      /* flags set by future statements */
+                int ff_lineno;        /* line number of last future statement */
+            } PyFutureFeatures;
+
+            #define FUTURE_NESTED_SCOPES "nested_scopes"
+            #define FUTURE_GENERATORS "generators"
+            #define FUTURE_DIVISION "division"
+            #define FUTURE_ABSOLUTE_IMPORT "absolute_import"
+            #define FUTURE_WITH_STATEMENT "with_statement"
+            #define FUTURE_PRINT_FUNCTION "print_function"
+            #define FUTURE_UNICODE_LITERALS "unicode_literals"
+            #define FUTURE_BARRY_AS_BDFL "barry_as_FLUFL"
+            #define FUTURE_GENERATOR_STOP "generator_stop"
+            #define FUTURE_ANNOTATIONS "annotations"
+
+            struct _mod; /* Declare the existence of this type */
+            #define PyAST_Compile(mod, s, f, ar) PyAST_CompileEx(mod, s, f, -1, ar)
+            PyAPI_FUNC(PyCodeObject *) PyAST_CompileEx(
+                struct _mod *mod,
+                const char *filename,       /* decoded from the filesystem encoding */
+                PyCompilerFlags *flags,
+                int optimize,
+                PyArena *arena);
+            PyAPI_FUNC(PyCodeObject *) PyAST_CompileObject(
+                struct _mod *mod,
+                PyObject *filename,
+                PyCompilerFlags *flags,
+                int optimize,
+                PyArena *arena);
+            PyAPI_FUNC(PyFutureFeatures *) PyFuture_FromAST(
+                struct _mod * mod,
+                const char *filename        /* decoded from the filesystem encoding */
+                );
+            PyAPI_FUNC(PyFutureFeatures *) PyFuture_FromASTObject(
+                struct _mod * mod,
+                PyObject *filename
+                );
+
+            /* _Py_Mangle is defined in compile.c */
+            PyAPI_FUNC(PyObject*) _Py_Mangle(PyObject *p, PyObject *name);
+
+            #define PY_INVALID_STACK_EFFECT INT_MAX
+            PyAPI_FUNC(int) PyCompile_OpcodeStackEffect(int opcode, int oparg);
+            PyAPI_FUNC(int) PyCompile_OpcodeStackEffectWithJump(int opcode, int oparg, int jump);
+
+            PyAPI_FUNC(int) _PyAST_Optimize(struct _mod *, PyArena *arena, int optimize);
+
+            #ifdef __cplusplus
+            }
+            #endif
+
+            #endif /* !Py_LIMITED_API */
+
+            /* These definitions must match corresponding definitions in graminit.h. */
+            #define Py_single_input 256
+            #define Py_file_input 257
+            #define Py_eval_input 258
+            #define Py_func_type_input 345
+
+            #endif /* !Py_COMPILE_H */
+            ''').strip()
+        lines = [line + '\n' for line in text.splitlines()]
+        lines[-1] = lines[-1][:-1]
+
+        results = list(
+                iter_lines(lines, _parse_directive=self._parse_directive))
+
+        self.assertEqual(results, [
+            (1, '#ifndef Py_COMPILE_H\n',
+                IfDirective('ifndef', 'Py_COMPILE_H'),
+                ()),
+            (2, '#define Py_COMPILE_H\n',
+                Constant('Py_COMPILE_H', None),
+                ('! defined(Py_COMPILE_H)',)),
+            (3, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)',)),
+            (4, '#ifndef Py_LIMITED_API\n',
+                IfDirective('ifndef', 'Py_LIMITED_API'),
+                ('! defined(Py_COMPILE_H)',)),
+            (5, '#include "code.h"\n',
+                Include('"code.h"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (6, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (7, '#ifdef __cplusplus\n',
+                IfDirective('ifdef', '__cplusplus'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (8, 'extern "C" {\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', 'defined(__cplusplus)')),
+            (9, '#endif\n',
+                OtherDirective('endif', None),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', 'defined(__cplusplus)')),
+            (10, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (11, ' \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (12, 'struct _node;  \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (13, 'PyAPI_FUNC(PyCodeObject *) PyNode_Compile(struct _node *, const char *);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (14, ' \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (15, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (19, '#define PyCF_MASK (CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT |                     CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION |                     CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL |                     CO_FUTURE_GENERATOR_STOP | CO_FUTURE_ANNOTATIONS)\n',
+                Constant('PyCF_MASK', '(CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT | CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION | CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL | CO_FUTURE_GENERATOR_STOP | CO_FUTURE_ANNOTATIONS)'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (20, '#define PyCF_MASK_OBSOLETE (CO_NESTED)\n',
+                Constant('PyCF_MASK_OBSOLETE', '(CO_NESTED)'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (21, '#define PyCF_SOURCE_IS_UTF8  0x0100\n',
+                Constant('PyCF_SOURCE_IS_UTF8', ' 0x0100'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (22, '#define PyCF_DONT_IMPLY_DEDENT 0x0200\n',
+                Constant('PyCF_DONT_IMPLY_DEDENT', '0x0200'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (23, '#define PyCF_ONLY_AST 0x0400\n',
+                Constant('PyCF_ONLY_AST', '0x0400'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (24, '#define PyCF_IGNORE_COOKIE 0x0800\n',
+                Constant('PyCF_IGNORE_COOKIE', '0x0800'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (25, '#define PyCF_TYPE_COMMENTS 0x1000\n',
+                Constant('PyCF_TYPE_COMMENTS', '0x1000'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (26, '#define PyCF_ALLOW_TOP_LEVEL_AWAIT 0x2000\n',
+                Constant('PyCF_ALLOW_TOP_LEVEL_AWAIT', '0x2000'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (27, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (28, '#ifndef Py_LIMITED_API\n',
+                IfDirective('ifndef', 'Py_LIMITED_API'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (29, 'typedef struct {\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', '! defined(Py_LIMITED_API)')),
+            (30, '    int cf_flags;   \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', '! defined(Py_LIMITED_API)')),
+            (31, '    int cf_feature_version;   \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', '! defined(Py_LIMITED_API)')),
+            (32, '} PyCompilerFlags;\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', '! defined(Py_LIMITED_API)')),
+            (33, '#endif\n',
+                OtherDirective('endif', None),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', '! defined(Py_LIMITED_API)')),
+            (34, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (35, ' \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (36, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (37, 'typedef struct {\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (38, '    int ff_features;       \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (39, '    int ff_lineno;         \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (40, '} PyFutureFeatures;\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (41, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (42, '#define FUTURE_NESTED_SCOPES "nested_scopes"\n',
+                Constant('FUTURE_NESTED_SCOPES', '"nested_scopes"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (43, '#define FUTURE_GENERATORS "generators"\n',
+                Constant('FUTURE_GENERATORS', '"generators"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (44, '#define FUTURE_DIVISION "division"\n',
+                Constant('FUTURE_DIVISION', '"division"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (45, '#define FUTURE_ABSOLUTE_IMPORT "absolute_import"\n',
+                Constant('FUTURE_ABSOLUTE_IMPORT', '"absolute_import"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (46, '#define FUTURE_WITH_STATEMENT "with_statement"\n',
+                Constant('FUTURE_WITH_STATEMENT', '"with_statement"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (47, '#define FUTURE_PRINT_FUNCTION "print_function"\n',
+                Constant('FUTURE_PRINT_FUNCTION', '"print_function"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (48, '#define FUTURE_UNICODE_LITERALS "unicode_literals"\n',
+                Constant('FUTURE_UNICODE_LITERALS', '"unicode_literals"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (49, '#define FUTURE_BARRY_AS_BDFL "barry_as_FLUFL"\n',
+                Constant('FUTURE_BARRY_AS_BDFL', '"barry_as_FLUFL"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (50, '#define FUTURE_GENERATOR_STOP "generator_stop"\n',
+                Constant('FUTURE_GENERATOR_STOP', '"generator_stop"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (51, '#define FUTURE_ANNOTATIONS "annotations"\n',
+                Constant('FUTURE_ANNOTATIONS', '"annotations"'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (52, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (53, 'struct _mod;  \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (54, '#define PyAST_Compile(mod, s, f, ar) PyAST_CompileEx(mod, s, f, -1, ar)\n',
+                Macro('PyAST_Compile', ('mod', 's', 'f', 'ar'), 'PyAST_CompileEx(mod, s, f, -1, ar)'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (55, 'PyAPI_FUNC(PyCodeObject *) PyAST_CompileEx(\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (56, '    struct _mod *mod,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (57, '    const char *filename,        \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (58, '    PyCompilerFlags *flags,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (59, '    int optimize,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (60, '    PyArena *arena);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (61, 'PyAPI_FUNC(PyCodeObject *) PyAST_CompileObject(\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (62, '    struct _mod *mod,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (63, '    PyObject *filename,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (64, '    PyCompilerFlags *flags,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (65, '    int optimize,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (66, '    PyArena *arena);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (67, 'PyAPI_FUNC(PyFutureFeatures *) PyFuture_FromAST(\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (68, '    struct _mod * mod,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (69, '    const char *filename         \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (70, '    );\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (71, 'PyAPI_FUNC(PyFutureFeatures *) PyFuture_FromASTObject(\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (72, '    struct _mod * mod,\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (73, '    PyObject *filename\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (74, '    );\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (75, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (76, ' \n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (77, 'PyAPI_FUNC(PyObject*) _Py_Mangle(PyObject *p, PyObject *name);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (78, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (79, '#define PY_INVALID_STACK_EFFECT INT_MAX\n',
+                Constant('PY_INVALID_STACK_EFFECT', 'INT_MAX'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (80, 'PyAPI_FUNC(int) PyCompile_OpcodeStackEffect(int opcode, int oparg);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (81, 'PyAPI_FUNC(int) PyCompile_OpcodeStackEffectWithJump(int opcode, int oparg, int jump);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (82, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (83, 'PyAPI_FUNC(int) _PyAST_Optimize(struct _mod *, PyArena *arena, int optimize);\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (84, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (85, '#ifdef __cplusplus\n',
+                IfDirective('ifdef', '__cplusplus'),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (86, '}\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', 'defined(__cplusplus)')),
+            (87, '#endif\n',
+                OtherDirective('endif', None),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)', 'defined(__cplusplus)')),
+            (88, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (89, '#endif  \n',
+                OtherDirective('endif', None),
+                ('! defined(Py_COMPILE_H)', '! defined(Py_LIMITED_API)')),
+            (90, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)',)),
+            (91, ' \n',
+                None,
+                ('! defined(Py_COMPILE_H)',)),
+            (92, '#define Py_single_input 256\n',
+                Constant('Py_single_input', '256'),
+                ('! defined(Py_COMPILE_H)',)),
+            (93, '#define Py_file_input 257\n',
+                Constant('Py_file_input', '257'),
+                ('! defined(Py_COMPILE_H)',)),
+            (94, '#define Py_eval_input 258\n',
+                Constant('Py_eval_input', '258'),
+                ('! defined(Py_COMPILE_H)',)),
+            (95, '#define Py_func_type_input 345\n',
+                Constant('Py_func_type_input', '345'),
+                ('! defined(Py_COMPILE_H)',)),
+            (96, '\n',
+                None,
+                ('! defined(Py_COMPILE_H)',)),
+            (97, '#endif  ',
+                OtherDirective('endif', None),
+                ('! defined(Py_COMPILE_H)',)),
+            ])
+        self.check_calls(
+                ('_parse_directive', '#ifndef Py_COMPILE_H'),
+                ('_parse_directive', '#define Py_COMPILE_H'),
+                ('_parse_directive', '#ifndef Py_LIMITED_API'),
+                ('_parse_directive', '#include "code.h"'),
+                ('_parse_directive', '#ifdef __cplusplus'),
+                ('_parse_directive', '#endif'),
+                ('_parse_directive', '#define PyCF_MASK (CO_FUTURE_DIVISION | CO_FUTURE_ABSOLUTE_IMPORT | CO_FUTURE_WITH_STATEMENT | CO_FUTURE_PRINT_FUNCTION | CO_FUTURE_UNICODE_LITERALS | CO_FUTURE_BARRY_AS_BDFL | CO_FUTURE_GENERATOR_STOP | CO_FUTURE_ANNOTATIONS)'),
+                ('_parse_directive', '#define PyCF_MASK_OBSOLETE (CO_NESTED)'),
+                ('_parse_directive', '#define PyCF_SOURCE_IS_UTF8 0x0100'),
+                ('_parse_directive', '#define PyCF_DONT_IMPLY_DEDENT 0x0200'),
+                ('_parse_directive', '#define PyCF_ONLY_AST 0x0400'),
+                ('_parse_directive', '#define PyCF_IGNORE_COOKIE 0x0800'),
+                ('_parse_directive', '#define PyCF_TYPE_COMMENTS 0x1000'),
+                ('_parse_directive', '#define PyCF_ALLOW_TOP_LEVEL_AWAIT 0x2000'),
+                ('_parse_directive', '#ifndef Py_LIMITED_API'),
+                ('_parse_directive', '#endif'),
+                ('_parse_directive', '#define FUTURE_NESTED_SCOPES "nested_scopes"'),
+                ('_parse_directive', '#define FUTURE_GENERATORS "generators"'),
+                ('_parse_directive', '#define FUTURE_DIVISION "division"'),
+                ('_parse_directive', '#define FUTURE_ABSOLUTE_IMPORT "absolute_import"'),
+                ('_parse_directive', '#define FUTURE_WITH_STATEMENT "with_statement"'),
+                ('_parse_directive', '#define FUTURE_PRINT_FUNCTION "print_function"'),
+                ('_parse_directive', '#define FUTURE_UNICODE_LITERALS "unicode_literals"'),
+                ('_parse_directive', '#define FUTURE_BARRY_AS_BDFL "barry_as_FLUFL"'),
+                ('_parse_directive', '#define FUTURE_GENERATOR_STOP "generator_stop"'),
+                ('_parse_directive', '#define FUTURE_ANNOTATIONS "annotations"'),
+                ('_parse_directive', '#define PyAST_Compile(mod, s, f, ar) PyAST_CompileEx(mod, s, f, -1, ar)'),
+                ('_parse_directive', '#define PY_INVALID_STACK_EFFECT INT_MAX'),
+                ('_parse_directive', '#ifdef __cplusplus'),
+                ('_parse_directive', '#endif'),
+                ('_parse_directive', '#endif'),
+                ('_parse_directive', '#define Py_single_input 256'),
+                ('_parse_directive', '#define Py_file_input 257'),
+                ('_parse_directive', '#define Py_eval_input 258'),
+                ('_parse_directive', '#define Py_func_type_input 345'),
+                ('_parse_directive', '#endif'),
+                )
+
+
+class ParseDirectiveTests(unittest.TestCase):
+
+    def test_directives(self):
+        tests = [
+            # includes
+            ('#include "internal/pycore_pystate.h"', Include('"internal/pycore_pystate.h"')),
+            ('#include <stdio>', Include('<stdio>')),
+
+            # defines
+            ('#define SPAM int', Constant('SPAM', 'int')),
+            ('#define SPAM', Constant('SPAM', '')),
+            ('#define SPAM(x, y) run(x, y)', Macro('SPAM', ('x', 'y'), 'run(x, y)')),
+            ('#undef SPAM', None),
+
+            # conditionals
+            ('#if SPAM', IfDirective('if', 'SPAM')),
+            # XXX complex conditionls
+            ('#ifdef SPAM', IfDirective('ifdef', 'SPAM')),
+            ('#ifndef SPAM', IfDirective('ifndef', 'SPAM')),
+            ('#elseif SPAM', IfDirective('elseif', 'SPAM')),
+            # XXX complex conditionls
+            ('#else', OtherDirective('else', '')),
+            ('#endif', OtherDirective('endif', '')),
+
+            # other
+            ('#error oops!', None),
+            ('#warning oops!', None),
+            ('#pragma ...', None),
+            ('#__FILE__ ...', None),
+            ('#__LINE__ ...', None),
+            ('#__DATE__ ...', None),
+            ('#__TIME__ ...', None),
+            ('#__TIMESTAMP__ ...', None),
+
+            # extra whitespace
+            (' # include  <stdio> ', Include('<stdio>')),
+            ('#else  ', OtherDirective('else', '')),
+            ('#endif  ', OtherDirective('endif', '')),
+            ('#define SPAM int  ', Constant('SPAM', 'int')),
+            ('#define SPAM  ', Constant('SPAM', '')),
+            ]
+        for line, expected in tests:
+            if expected is None:
+                kind, _, text = line[1:].partition(' ')
+                expected = OtherDirective(kind, text)
+            with self.subTest(line):
+                directive = parse_directive(line)
+
+                self.assertEqual(directive, expected)
+
+    def test_bad_directives(self):
+        tests = [
+            # valid directives with bad text
+            '#define 123',
+            '#else spam',
+            '#endif spam',
+            ]
+        for kind in PreprocessorDirective.KINDS:
+            # missing leading "#"
+            tests.append(kind)
+            if kind in ('else', 'endif'):
+                continue
+            # valid directives with missing text
+            tests.append('#' + kind)
+            tests.append('#' + kind + ' ')
+        for line in tests:
+            with self.subTest(line):
+                with self.assertRaises(ValueError):
+                    parse_directive(line)
+
+    def test_not_directives(self):
+        tests = [
+            '',
+            ' ',
+            'directive',
+            'directive?',
+            '???',
+            ]
+        for line in tests:
+            with self.subTest(line):
+                with self.assertRaises(ValueError):
+                    parse_directive(line)
+
+
+class ConstantTests(unittest.TestCase):
+
+    def test_type(self):
+        directive = Constant('SPAM', '123')
+
+        self.assertIs(type(directive), Constant)
+        self.assertIsInstance(directive, PreprocessorDirective)
+
+    def test_attrs(self):
+        d = Constant('SPAM', '123')
+        kind, name, value = d.kind, d.name, d.value
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertEqual(value, '123')
+
+    def test_text(self):
+        tests = [
+            (('SPAM', '123'), 'SPAM 123'),
+            (('SPAM',), 'SPAM'),
+            ]
+        for args, expected in tests:
+            with self.subTest(args):
+                d = Constant(*args)
+                text = d.text
+
+                self.assertEqual(text, expected)
+
+    def test_iter(self):
+        kind, name, value = Constant('SPAM', '123')
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertEqual(value, '123')
+
+    def test_defaults(self):
+        kind, name, value = Constant('SPAM')
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertIs(value, None)
+
+    def test_coerce(self):
+        tests = []
+        # coerced name, value
+        for args in wrapped_arg_combos('SPAM', '123'):
+            tests.append((args, ('SPAM', '123')))
+        # missing name, value
+        for name in ('', ' ', None, StrProxy(' '), ()):
+            for value in ('', ' ', None, StrProxy(' '), ()):
+                tests.append(
+                        ((name, value), (None, None)))
+        # whitespace
+        tests.extend([
+            ((' SPAM ', ' 123 '), ('SPAM', '123')),
+            ])
+
+        for args, expected in tests:
+            with self.subTest(args):
+                d = Constant(*args)
+
+                self.assertEqual(d[1:], expected)
+                for i, exp in enumerate(expected, start=1):
+                    if exp is not None:
+                        self.assertIs(type(d[i]), str)
+
+    def test_valid(self):
+        tests = [
+            ('SPAM', '123'),
+            # unusual name
+            ('_SPAM_', '123'),
+            ('X_1', '123'),
+            # unusual value
+            ('SPAM', None),
+            ]
+        for args in tests:
+            with self.subTest(args):
+                directive = Constant(*args)
+
+                directive.validate()
+
+    def test_invalid(self):
+        tests = [
+            # invalid name
+            ((None, '123'), TypeError),
+            (('_', '123'), ValueError),
+            (('1', '123'), ValueError),
+            (('_1_', '123'), ValueError),
+            # There is no invalid value (including None).
+            ]
+        for args, exctype in tests:
+            with self.subTest(args):
+                directive = Constant(*args)
+
+                with self.assertRaises(exctype):
+                    directive.validate()
+
+
+class MacroTests(unittest.TestCase):
+
+    def test_type(self):
+        directive = Macro('SPAM', ('x', 'y'), '123')
+
+        self.assertIs(type(directive), Macro)
+        self.assertIsInstance(directive, PreprocessorDirective)
+
+    def test_attrs(self):
+        d = Macro('SPAM', ('x', 'y'), '123')
+        kind, name, args, body = d.kind, d.name, d.args, d.body
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertEqual(args, ('x', 'y'))
+        self.assertEqual(body, '123')
+
+    def test_text(self):
+        tests = [
+            (('SPAM', ('x', 'y'), '123'), 'SPAM(x, y) 123'),
+            (('SPAM', ('x', 'y'),), 'SPAM(x, y)'),
+            ]
+        for args, expected in tests:
+            with self.subTest(args):
+                d = Macro(*args)
+                text = d.text
+
+                self.assertEqual(text, expected)
+
+    def test_iter(self):
+        kind, name, args, body = Macro('SPAM', ('x', 'y'), '123')
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertEqual(args, ('x', 'y'))
+        self.assertEqual(body, '123')
+
+    def test_defaults(self):
+        kind, name, args, body = Macro('SPAM', ('x', 'y'))
+
+        self.assertEqual(kind, 'define')
+        self.assertEqual(name, 'SPAM')
+        self.assertEqual(args, ('x', 'y'))
+        self.assertIs(body, None)
+
+    def test_coerce(self):
+        tests = []
+        # coerce name and body
+        for args in wrapped_arg_combos('SPAM', ('x', 'y'), '123'):
+            tests.append(
+                    (args, ('SPAM', ('x', 'y'), '123')))
+        # coerce args
+        tests.extend([
+            (('SPAM', 'x', '123'),
+             ('SPAM', ('x',), '123')),
+            (('SPAM', 'x,y', '123'),
+             ('SPAM', ('x', 'y'), '123')),
+            ])
+        # coerce arg names
+        for argnames in wrapped_arg_combos('x', 'y'):
+            tests.append(
+                    (('SPAM', argnames, '123'),
+                     ('SPAM', ('x', 'y'), '123')))
+        # missing name, body
+        for name in ('', ' ', None, StrProxy(' '), ()):
+            for argnames in (None, ()):
+                for body in ('', ' ', None, StrProxy(' '), ()):
+                    tests.append(
+                            ((name, argnames, body),
+                             (None, (), None)))
+        # missing args
+        tests.extend([
+            (('SPAM', None, '123'),
+             ('SPAM', (), '123')),
+            (('SPAM', (), '123'),
+             ('SPAM', (), '123')),
+            ])
+        # missing arg names
+        for arg in ('', ' ', None, StrProxy(' '), ()):
+            tests.append(
+                    (('SPAM', (arg,), '123'),
+                     ('SPAM', (None,), '123')))
+        tests.extend([
+            (('SPAM', ('x', '', 'z'), '123'),
+             ('SPAM', ('x', None, 'z'), '123')),
+            ])
+        # whitespace
+        tests.extend([
+            ((' SPAM ', (' x ', ' y '), ' 123 '),
+             ('SPAM', ('x', 'y'), '123')),
+            (('SPAM', 'x, y', '123'),
+             ('SPAM', ('x', 'y'), '123')),
+            ])
+
+        for args, expected in tests:
+            with self.subTest(args):
+                d = Macro(*args)
+
+                self.assertEqual(d[1:], expected)
+                for i, exp in enumerate(expected, start=1):
+                    if i == 2:
+                        self.assertIs(type(d[i]), tuple)
+                    elif exp is not None:
+                        self.assertIs(type(d[i]), str)
+
+    def test_init_bad_args(self):
+        tests = [
+            ('SPAM', StrProxy('x'), '123'),
+            ('SPAM', object(), '123'),
+            ]
+        for args in tests:
+            with self.subTest(args):
+                with self.assertRaises(TypeError):
+                    Macro(*args)
+
+    def test_valid(self):
+        tests = [
+            # unusual name
+            ('SPAM', ('x', 'y'), 'run(x, y)'),
+            ('_SPAM_', ('x', 'y'), 'run(x, y)'),
+            ('X_1', ('x', 'y'), 'run(x, y)'),
+            # unusual args
+            ('SPAM', (), 'run(x, y)'),
+            ('SPAM', ('_x_', 'y_1'), 'run(x, y)'),
+            ('SPAM', 'x', 'run(x, y)'),
+            ('SPAM', 'x, y', 'run(x, y)'),
+            # unusual body
+            ('SPAM', ('x', 'y'), None),
+            ]
+        for args in tests:
+            with self.subTest(args):
+                directive = Macro(*args)
+
+                directive.validate()
+
+    def test_invalid(self):
+        tests = [
+            # invalid name
+            ((None, ('x', 'y'), '123'), TypeError),
+            (('_', ('x', 'y'), '123'), ValueError),
+            (('1', ('x', 'y'), '123'), ValueError),
+            (('_1', ('x', 'y'), '123'), ValueError),
+            # invalid args
+            (('SPAM', (None, 'y'), '123'), ValueError),
+            (('SPAM', ('x', '_'), '123'), ValueError),
+            (('SPAM', ('x', '1'), '123'), ValueError),
+            (('SPAM', ('x', '_1_'), '123'), ValueError),
+            # There is no invalid body (including None).
+            ]
+        for args, exctype in tests:
+            with self.subTest(args):
+                directive = Macro(*args)
+
+                with self.assertRaises(exctype):
+                    directive.validate()
+
+
+class IfDirectiveTests(unittest.TestCase):
+
+    def test_type(self):
+        directive = IfDirective('if', '1')
+
+        self.assertIs(type(directive), IfDirective)
+        self.assertIsInstance(directive, PreprocessorDirective)
+
+    def test_attrs(self):
+        d = IfDirective('if', '1')
+        kind, condition = d.kind, d.condition
+
+        self.assertEqual(kind, 'if')
+        self.assertEqual(condition, '1')
+        #self.assertEqual(condition, (ArithmeticCondition('1'),))
+
+    def test_text(self):
+        tests = [
+            (('if', 'defined(SPAM) && 1 || (EGGS > 3 && defined(HAM))'),
+             'defined(SPAM) && 1 || (EGGS > 3 && defined(HAM))'),
+            ]
+        for kind in IfDirective.KINDS:
+            tests.append(
+                    ((kind, 'SPAM'), 'SPAM'))
+        for args, expected in tests:
+            with self.subTest(args):
+                d = IfDirective(*args)
+                text = d.text
+
+                self.assertEqual(text, expected)
+
+    def test_iter(self):
+        kind, condition = IfDirective('if', '1')
+
+        self.assertEqual(kind, 'if')
+        self.assertEqual(condition, '1')
+        #self.assertEqual(condition, (ArithmeticCondition('1'),))
+
+    #def test_complex_conditions(self):
+    #    ...
+
+    def test_coerce(self):
+        tests = []
+        for kind in IfDirective.KINDS:
+            if kind == 'ifdef':
+                cond = 'defined(SPAM)'
+            elif kind == 'ifndef':
+                cond = '! defined(SPAM)'
+            else:
+                cond = 'SPAM'
+            for args in wrapped_arg_combos(kind, 'SPAM'):
+                tests.append((args, (kind, cond)))
+            tests.extend([
+                ((' ' + kind + ' ', ' SPAM '), (kind, cond)),
+                ])
+            for raw in ('', ' ', None, StrProxy(' '), ()):
+                tests.append(((kind, raw), (kind, None)))
+        for kind in ('', ' ', None, StrProxy(' '), ()):
+            tests.append(((kind, 'SPAM'), (None, 'SPAM')))
+        for args, expected in tests:
+            with self.subTest(args):
+                d = IfDirective(*args)
+
+                self.assertEqual(tuple(d), expected)
+                for i, exp in enumerate(expected):
+                    if exp is not None:
+                        self.assertIs(type(d[i]), str)
+
+    def test_valid(self):
+        tests = []
+        for kind in IfDirective.KINDS:
+            tests.extend([
+                (kind, 'SPAM'),
+                (kind, '_SPAM_'),
+                (kind, 'X_1'),
+                (kind, '()'),
+                (kind, '--'),
+                (kind, '???'),
+                ])
+        for args in tests:
+            with self.subTest(args):
+                directive = IfDirective(*args)
+
+                directive.validate()
+
+    def test_invalid(self):
+        tests = []
+        # kind
+        tests.extend([
+            ((None, 'SPAM'), TypeError),
+            (('_', 'SPAM'), ValueError),
+            (('-', 'SPAM'), ValueError),
+            (('spam', 'SPAM'), ValueError),
+            ])
+        for kind in PreprocessorDirective.KINDS:
+            if kind in IfDirective.KINDS:
+                continue
+            tests.append(
+                ((kind, 'SPAM'), ValueError))
+        # condition
+        for kind in IfDirective.KINDS:
+            tests.extend([
+                ((kind, None), TypeError),
+                # Any other condition is valid.
+                ])
+        for args, exctype in tests:
+            with self.subTest(args):
+                directive = IfDirective(*args)
+
+                with self.assertRaises(exctype):
+                    directive.validate()
+
+
+class IncludeTests(unittest.TestCase):
+
+    def test_type(self):
+        directive = Include('<stdio>')
+
+        self.assertIs(type(directive), Include)
+        self.assertIsInstance(directive, PreprocessorDirective)
+
+    def test_attrs(self):
+        d = Include('<stdio>')
+        kind, file, text = d.kind, d.file, d.text
+
+        self.assertEqual(kind, 'include')
+        self.assertEqual(file, '<stdio>')
+        self.assertEqual(text, '<stdio>')
+
+    def test_iter(self):
+        kind, file = Include('<stdio>')
+
+        self.assertEqual(kind, 'include')
+        self.assertEqual(file, '<stdio>')
+
+    def test_coerce(self):
+        tests = []
+        for arg, in wrapped_arg_combos('<stdio>'):
+            tests.append((arg, '<stdio>'))
+        tests.extend([
+            (' <stdio> ', '<stdio>'),
+            ])
+        for arg in ('', ' ', None, StrProxy(' '), ()):
+            tests.append((arg, None ))
+        for arg, expected in tests:
+            with self.subTest(arg):
+                _, file = Include(arg)
+
+                self.assertEqual(file, expected)
+                if expected is not None:
+                    self.assertIs(type(file), str)
+
+    def test_valid(self):
+        tests = [
+            '<stdio>',
+            '"spam.h"',
+            '"internal/pycore_pystate.h"',
+            ]
+        for arg in tests:
+            with self.subTest(arg):
+                directive = Include(arg)
+
+                directive.validate()
+
+    def test_invalid(self):
+        tests = [
+            (None, TypeError),
+            # We currently don't check the file.
+            ]
+        for arg, exctype in tests:
+            with self.subTest(arg):
+                directive = Include(arg)
+
+                with self.assertRaises(exctype):
+                    directive.validate()
+
+
+class OtherDirectiveTests(unittest.TestCase):
+
+    def test_type(self):
+        directive = OtherDirective('undef', 'SPAM')
+
+        self.assertIs(type(directive), OtherDirective)
+        self.assertIsInstance(directive, PreprocessorDirective)
+
+    def test_attrs(self):
+        d = OtherDirective('undef', 'SPAM')
+        kind, text = d.kind, d.text
+
+        self.assertEqual(kind, 'undef')
+        self.assertEqual(text, 'SPAM')
+
+    def test_iter(self):
+        kind, text = OtherDirective('undef', 'SPAM')
+
+        self.assertEqual(kind, 'undef')
+        self.assertEqual(text, 'SPAM')
+
+    def test_coerce(self):
+        tests = []
+        for kind in OtherDirective.KINDS:
+            if kind in ('else', 'endif'):
+                continue
+            for args in wrapped_arg_combos(kind, '...'):
+                tests.append((args, (kind, '...')))
+            tests.extend([
+                ((' ' + kind + ' ', ' ... '), (kind, '...')),
+                ])
+            for raw in ('', ' ', None, StrProxy(' '), ()):
+                tests.append(((kind, raw), (kind, None)))
+        for kind in ('else', 'endif'):
+            for args in wrapped_arg_combos(kind, None):
+                tests.append((args, (kind, None)))
+            tests.extend([
+                ((' ' + kind + ' ', None), (kind, None)),
+                ])
+        for kind in ('', ' ', None, StrProxy(' '), ()):
+            tests.append(((kind, '...'), (None, '...')))
+        for args, expected in tests:
+            with self.subTest(args):
+                d = OtherDirective(*args)
+
+                self.assertEqual(tuple(d), expected)
+                for i, exp in enumerate(expected):
+                    if exp is not None:
+                        self.assertIs(type(d[i]), str)
+
+    def test_valid(self):
+        tests = []
+        for kind in OtherDirective.KINDS:
+            if kind in ('else', 'endif'):
+                continue
+            tests.extend([
+                (kind, '...'),
+                (kind, '???'),
+                (kind, 'SPAM'),
+                (kind, '1 + 1'),
+                ])
+        for kind in ('else', 'endif'):
+            tests.append((kind, None))
+        for args in tests:
+            with self.subTest(args):
+                directive = OtherDirective(*args)
+
+                directive.validate()
+
+    def test_invalid(self):
+        tests = []
+        # kind
+        tests.extend([
+            ((None, '...'), TypeError),
+            (('_', '...'), ValueError),
+            (('-', '...'), ValueError),
+            (('spam', '...'), ValueError),
+            ])
+        for kind in PreprocessorDirective.KINDS:
+            if kind in OtherDirective.KINDS:
+                continue
+            tests.append(
+                ((kind, None), ValueError))
+        # text
+        for kind in OtherDirective.KINDS:
+            if kind in ('else', 'endif'):
+                tests.extend([
+                    # Any text is invalid.
+                    ((kind, 'SPAM'), ValueError),
+                    ((kind, '...'), ValueError),
+                    ])
+            else:
+                tests.extend([
+                    ((kind, None), TypeError),
+                    # Any other text is valid.
+                    ])
+        for args, exctype in tests:
+            with self.subTest(args):
+                directive = OtherDirective(*args)
+
+                with self.assertRaises(exctype):
+                    directive.validate()
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_symbols/__init__.py b/Lib/test/test_tools/test_c_analyzer/test_c_symbols/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_symbols/__init__.py
diff --git a/Lib/test/test_tools/test_c_analyzer/test_c_symbols/test_info.py b/Lib/test/test_tools/test_c_analyzer/test_c_symbols/test_info.py
new file mode 100644
index 0000000..e029dcf
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/test_c_symbols/test_info.py
@@ -0,0 +1,192 @@
+import string
+import unittest
+
+from ..util import PseudoStr, StrProxy, Object
+from .. import tool_imports_for_tests
+with tool_imports_for_tests():
+    from c_analyzer_common.info import ID
+    from c_symbols.info import Symbol
+
+
+class SymbolTests(unittest.TestCase):
+
+    VALID_ARGS = (
+            ID('x/y/z/spam.c', 'func', 'eggs'),
+            Symbol.KIND.VARIABLE,
+            False,
+            )
+    VALID_KWARGS = dict(zip(Symbol._fields, VALID_ARGS))
+    VALID_EXPECTED = VALID_ARGS
+
+    def test_init_typical_binary_local(self):
+        id = ID(None, None, 'spam')
+        symbol = Symbol(
+                id=id,
+                kind=Symbol.KIND.VARIABLE,
+                external=False,
+                )
+
+        self.assertEqual(symbol, (
+            id,
+            Symbol.KIND.VARIABLE,
+            False,
+            ))
+
+    def test_init_typical_binary_global(self):
+        id = ID('Python/ceval.c', None, 'spam')
+        symbol = Symbol(
+                id=id,
+                kind=Symbol.KIND.VARIABLE,
+                external=False,
+                )
+
+        self.assertEqual(symbol, (
+            id,
+            Symbol.KIND.VARIABLE,
+            False,
+            ))
+
+    def test_init_coercion(self):
+        tests = [
+            ('str subclass',
+             dict(
+                 id=PseudoStr('eggs'),
+                 kind=PseudoStr('variable'),
+                 external=0,
+                 ),
+             (ID(None, None, 'eggs'),
+              Symbol.KIND.VARIABLE,
+              False,
+              )),
+            ('with filename',
+             dict(
+                 id=('x/y/z/spam.c', 'eggs'),
+                 kind=PseudoStr('variable'),
+                 external=0,
+                 ),
+             (ID('x/y/z/spam.c', None, 'eggs'),
+              Symbol.KIND.VARIABLE,
+              False,
+              )),
+            ('non-str 1',
+             dict(
+                 id=('a', 'b', 'c'),
+                 kind=StrProxy('variable'),
+                 external=0,
+                 ),
+             (ID('a', 'b', 'c'),
+              Symbol.KIND.VARIABLE,
+              False,
+              )),
+            ('non-str 2',
+             dict(
+                 id=('a', 'b', 'c'),
+                 kind=Object(),
+                 external=0,
+                 ),
+             (ID('a', 'b', 'c'),
+              '<object>',
+              False,
+              )),
+            ]
+        for summary, kwargs, expected in tests:
+            with self.subTest(summary):
+                symbol = Symbol(**kwargs)
+
+                for field in Symbol._fields:
+                    value = getattr(symbol, field)
+                    if field == 'external':
+                        self.assertIs(type(value), bool)
+                    elif field == 'id':
+                        self.assertIs(type(value), ID)
+                    else:
+                        self.assertIs(type(value), str)
+                self.assertEqual(tuple(symbol), expected)
+
+    def test_init_all_missing(self):
+        id = ID(None, None, 'spam')
+
+        symbol = Symbol(id)
+
+        self.assertEqual(symbol, (
+            id,
+            Symbol.KIND.VARIABLE,
+            None,
+            ))
+
+    def test_fields(self):
+        id = ID('z', 'x', 'a')
+
+        symbol = Symbol(id, 'b', False)
+
+        self.assertEqual(symbol.id, id)
+        self.assertEqual(symbol.kind, 'b')
+        self.assertIs(symbol.external, False)
+
+    def test___getattr__(self):
+        id = ID('z', 'x', 'a')
+        symbol = Symbol(id, 'b', False)
+
+        filename = symbol.filename
+        funcname = symbol.funcname
+        name = symbol.name
+
+        self.assertEqual(filename, 'z')
+        self.assertEqual(funcname, 'x')
+        self.assertEqual(name, 'a')
+
+    def test_validate_typical(self):
+        id = ID('z', 'x', 'a')
+
+        symbol = Symbol(
+                id=id,
+                kind=Symbol.KIND.VARIABLE,
+                external=False,
+                )
+
+        symbol.validate()  # This does not fail.
+
+    def test_validate_missing_field(self):
+        for field in Symbol._fields:
+            with self.subTest(field):
+                symbol = Symbol(**self.VALID_KWARGS)
+                symbol = symbol._replace(**{field: None})
+
+                with self.assertRaises(TypeError):
+                    symbol.validate()
+
+    def test_validate_bad_field(self):
+        badch = tuple(c for c in string.punctuation + string.digits)
+        notnames = (
+                '1a',
+                'a.b',
+                'a-b',
+                '&a',
+                'a++',
+                ) + badch
+        tests = [
+            ('id', notnames),
+            ('kind', ('bogus',)),
+            ]
+        seen = set()
+        for field, invalid in tests:
+            for value in invalid:
+                if field != 'kind':
+                    seen.add(value)
+                with self.subTest(f'{field}={value!r}'):
+                    symbol = Symbol(**self.VALID_KWARGS)
+                    symbol = symbol._replace(**{field: value})
+
+                    with self.assertRaises(ValueError):
+                        symbol.validate()
+
+        for field, invalid in tests:
+            if field == 'kind':
+                continue
+            valid = seen - set(invalid)
+            for value in valid:
+                with self.subTest(f'{field}={value!r}'):
+                    symbol = Symbol(**self.VALID_KWARGS)
+                    symbol = symbol._replace(**{field: value})
+
+                    symbol.validate()  # This does not fail.
diff --git a/Lib/test/test_tools/test_c_analyzer/util.py b/Lib/test/test_tools/test_c_analyzer/util.py
new file mode 100644
index 0000000..ba73b0a
--- /dev/null
+++ b/Lib/test/test_tools/test_c_analyzer/util.py
@@ -0,0 +1,60 @@
+import itertools
+
+
+class PseudoStr(str):
+    pass
+
+
+class StrProxy:
+    def __init__(self, value):
+        self.value = value
+    def __str__(self):
+        return self.value
+    def __bool__(self):
+        return bool(self.value)
+
+
+class Object:
+    def __repr__(self):
+        return '<object>'
+
+
+def wrapped_arg_combos(*args,
+                       wrappers=(PseudoStr, StrProxy),
+                       skip=(lambda w, i, v: not isinstance(v, str)),
+                       ):
+    """Yield every possible combination of wrapped items for the given args.
+
+    Effectively, the wrappers are applied to the args according to the
+    powerset of the args indicies.  So the result includes the args
+    completely unwrapped.
+
+    If "skip" is supplied (default is to skip all non-str values) and
+    it returns True for a given arg index/value then that arg will
+    remain unwrapped,
+
+    Only unique results are returned.  If an arg was skipped for one
+    of the combinations then it could end up matching one of the other
+    combinations.  In that case only one of them will be yielded.
+    """
+    if not args:
+        return
+    indices = list(range(len(args)))
+    # The powerset (from recipe in the itertools docs).
+    combos = itertools.chain.from_iterable(itertools.combinations(indices, r)
+                                           for r in range(len(indices)+1))
+    seen = set()
+    for combo in combos:
+        for wrap in wrappers:
+            indexes = []
+            applied = list(args)
+            for i in combo:
+                arg = args[i]
+                if skip and skip(wrap, i, arg):
+                    continue
+                indexes.append(i)
+                applied[i] = wrap(arg)
+            key = (wrap, tuple(indexes))
+            if key not in seen:
+                yield tuple(applied)
+                seen.add(key)