Crude hack for testing stable ABI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import unittest | |
import subprocess | |
import tempfile | |
import collections | |
import sys | |
import re | |
# | |
# Collect symbols tested for below | |
# | |
_STABLE_SYMBOLS = collections.defaultdict(set) | |
def stable_symbol(symbolname, py_ver=None): | |
def decorator(function): | |
_STABLE_SYMBOLS[py_ver].add(symbolname) | |
return function | |
return decorator | |
# | |
# Actual test cases | |
# | |
class TestPubliCAPI (unittest.TestCase): | |
def validate_symbols(self, api_version, *symbolnames): | |
with tempfile.NamedTemporaryFile(mode='w+', suffix='.c') as fp: | |
fp.write('#include "Python.h"\n') | |
fp.write('#include <stdio.h>\n') | |
fp.write('\n') | |
fp.write('int main(void) {\n') | |
for symbol in symbolnames: | |
fp.write(f' printf("%p\\n", &{symbol});\n') | |
fp.write(' return 0;\n') | |
fp.write('}\n') | |
fp.flush() | |
if api_version is None: | |
api_flags = "-DPy_LIMITED_API" | |
else: | |
version_num = tuple(int(x) for x in api_version.split('.')) | |
api_flags = "-DPy_LIMITED_API=0x%02x%02x0000"%version_num | |
subprocess.check_call(["cc", api_flags, | |
"-Werror=implicit-function-declaration", | |
"-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/", | |
"-L", "/Library/Frameworks/Python.framework/Versions/3.7/lib/", | |
'-lpython3.7', | |
fp.name]) | |
@stable_symbol('PyTuple_GetItem') | |
@stable_symbol('PyIndex_Check') | |
def test_abi(self): | |
self.validate_symbols(None, 'PyTuple_GetItem') | |
self.validate_symbols(None, 'PyIndex_Check') | |
@stable_symbol('PySlice_Unpack', '3.7') | |
@stable_symbol('PySlice_AdjustIndices', '3.7') | |
@stable_symbol('PyInterpreterState_GetID', '3.7') | |
def test_abi_37(self): | |
self.validate_symbols('3.7', 'PySlice_Unpack', 'PySlice_AdjustIndices', 'PyInterpreterState_GetID') | |
# | |
# The hard part, find a way to reliably extract symbols from headers (and on windows the stable abi library stub) | |
# | |
class TestExportedSymbols (unittest.TestCase): | |
def get_exported_names(self, api_version=None): | |
with tempfile.NamedTemporaryFile(mode='w+', suffix='.c') as fp: | |
fp.write('#include "Python.h"\n') | |
fp.flush() | |
if api_version is None: | |
output = subprocess.check_output(["cc", "-DPy_LIMITED_API", "-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/", "-E", fp.name]) | |
else: | |
version_num = tuple(int(x) for x in api_version.split('.')) | |
output = subprocess.check_output(["cc", "-DPy_LIMITED_API=0x%02x%02x0000"%version_num, "-I", "/Library/Frameworks/Python.framework/Versions/3.7/include/python3.7m/", "-E", fp.name]) | |
python_file = False | |
symbols = set() | |
definitions = [] | |
for ln in output.decode('utf-8').splitlines(): | |
if not ln: | |
continue | |
elif ln.startswith('#') and ln.split(None, 2)[1].isdigit(): | |
python_file = sys.prefix in ln.split(None,2)[-1] | |
else: | |
if python_file: | |
definitions.append(ln) | |
exported_names = set() | |
for definition in re.findall('[^;]*;', ' '.join(definitions), re.MULTILINE): | |
#print("BEFORE", definition) | |
definition = re.sub('__attribute__.*', '', definition) | |
definition = re.sub('\([^)]*\)', '', definition) | |
definition = re.sub('{[^)]*}', '', definition) | |
definition = definition.replace(';', '') | |
definition = definition.strip() | |
exported_names.add(definition.split()[-1].lstrip('*')) | |
return exported_names | |
def test_exported_names(self): | |
seen = set() | |
default_exported = self.get_exported_names() | |
with self.subTest("default"): | |
seen.update(default_exported) | |
self.assertEqual(len(_STABLE_SYMBOLS[None]), len(default_exported)) | |
for version in ('3.%d'%(i,) for i in range(sys.version_info.minor+1)): | |
exported = self.get_exported_names(version) - seen | |
seen.update(exported) | |
with self.subTest(version): | |
self.assertEqual(_STABLE_SYMBOLS[version], exported) | |
if __name__ == "__main__": | |
unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment