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_analyzer.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()
