diff options
author | Eric Snow <ericsnowcurrently@gmail.com> | 2019-09-11 18:49:45 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-09-11 18:49:45 (GMT) |
commit | ee536b2020b1f0baad1286dbd4345e13870324af (patch) | |
tree | 2486233603db05a76aaef863bd6639455e3dfef7 /Lib/test/test_tools | |
parent | 9936371af298d465095ae70bc9c2943b4b16eac4 (diff) | |
download | cpython-ee536b2020b1f0baad1286dbd4345e13870324af.zip cpython-ee536b2020b1f0baad1286dbd4345e13870324af.tar.gz cpython-ee536b2020b1f0baad1286dbd4345e13870324af.tar.bz2 |
bpo-36876: Add a tool that identifies unsupported global C variables. (#15877)
Diffstat (limited to 'Lib/test/test_tools')
20 files changed, 4401 insertions, 9 deletions
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) |