diff options
author | Daniel P. Berrangé <berrange@redhat.com> | 2022-03-22 18:27:15 +0000 |
---|---|---|
committer | Daniel P. Berrangé <berrange@redhat.com> | 2022-04-21 15:00:29 +0000 |
commit | 60044515a256c36afad0fc5efcb7ea946eb93783 (patch) | |
tree | 345f3e5a8b84612450eff55ee6d12d460706e9cf /tests | |
parent | 6feb31a7a10267e14183495915b191ecdf4ea3e8 (diff) | |
download | libvirt-python-60044515a256c36afad0fc5efcb7ea946eb93783.tar.gz |
setup: switch to running API coverage test using pytestv8.3.0
The API coverage test is no longer a special case.
Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
Diffstat (limited to 'tests')
-rw-r--r-- | tests/test_api_coverage.py | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/tests/test_api_coverage.py b/tests/test_api_coverage.py new file mode 100644 index 0000000..c922df2 --- /dev/null +++ b/tests/test_api_coverage.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python3 + +import sys +from typing import Dict, List, Set, Tuple # noqa F401 +import libvirt +import unittest +import os + + +def get_libvirt_api_xml_path(): + import subprocess + args = ["pkg-config", "--variable", "libvirt_api", "libvirt"] + proc = subprocess.Popen(args, stdout=subprocess.PIPE) + stdout, _ = proc.communicate() + if proc.returncode: + sys.exit(proc.returncode) + return stdout.splitlines()[0] + +# Identify all functions and enums in public API +def identify_functions_enums(tree): + enumvals = {} # type: Dict[str, Dict[str, int]] + second_pass = [] # type: List[str] + wantenums = [] # type: List[str] + wantfunctions = [] # type: List[str] + + wantfunctions = tree.xpath('/api/files/file/exports[@type="function"]/@symbol') + + for n in tree.xpath('/api/symbols/enum'): + typ = n.attrib['type'] + name = n.attrib['name'] + val = n.attrib['value'] + + if typ not in enumvals: + enumvals[typ] = {} + + # If the value cannot be converted to int, it is reference to + # another enum and needs to be sorted out later on + try: + val = int(val) + except ValueError: + second_pass.append(n) + continue + + enumvals[typ][name] = int(val) + + for n in second_pass: + typ = n.attrib['type'] + name = n.attrib['name'] + val = n.attrib['value'] + + for v in enumvals.values(): + if val in v: + val = int(v[val]) + break + + # Version 4.0.0 was broken as missing VIR_TYPED_PARAM enums + # constants. This is harmless from POV of validating API + # coverage so ignore this error. + if (not isinstance(val, int) and + not val.startswith("VIR_TYPED_PARAM_")): + fail = True + print("Cannot get a value of enum %s (originally %s)" % (val, name)) + enumvals[typ][name] = val + + for n in tree.xpath('/api/files/file/exports[@type="enum"]/@symbol'): + for enumval in enumvals.values(): + if n in enumval: + enumv = enumval + break + # Eliminate sentinels + if n.endswith('_LAST') and enumv[n] == max(enumv.values()): + continue + wantenums.append(n) + + return wantfunctions, wantenums, enumvals + + +# Identify all classes and methods in the 'libvirt' python module +def identify_class_methods(wantenums, enumvals): + gotenums = [] # type: List[str] + gottypes = [] # type: List[str] + gotfunctions = {"libvirt": []} # type: Dict[str, List[str]] + + for name in dir(libvirt): + if name.startswith('_'): + continue + thing = getattr(libvirt, name) + # Special-case libvirtError to deal with python 2.4 difference + # in Exception class type reporting. + if isinstance(thing, int): + gotenums.append(name) + elif getattr(thing, "__module__", "") == "typing": + continue + elif type(thing) == type or name == "libvirtError": + gottypes.append(name) + gotfunctions[name] = [] + elif callable(thing): + gotfunctions["libvirt"].append(name) + + for enum in wantenums: + if enum not in gotenums: + fail = True + for typ, enumval in enumvals.items(): + if enum in enumval: + raise Exception("Missing exported enum %s of type %s" % (enum, typ)) + + for klassname in gottypes: + klassobj = getattr(libvirt, klassname) + for name in dir(klassobj): + if name.startswith('_'): + continue + if name == 'c_pointer': + continue + thing = getattr(klassobj, name) + if callable(thing): + gotfunctions[klassname].append(name) + + return gotfunctions, gottypes + + +# First cut at mapping of C APIs to python classes + methods +def basic_class_method_mapping(wantfunctions, gottypes): + basicklassmap = {} # type: Dict[str, Tuple[str, str, str]] + + for cname in wantfunctions: + name = cname + # Some virConnect APIs have stupid names + if name[0:7] == "virNode" and name[0:13] != "virNodeDevice": + name = "virConnect" + name[7:] + if name[0:7] == "virConn" and name[0:10] != "virConnect": + name = "virConnect" + name[7:] + + # The typed param APIs are only for internal use + if name[0:14] == "virTypedParams": + continue + + if name[0:23] == "virNetworkDHCPLeaseFree": + continue + + if name[0:28] == "virDomainStatsRecordListFree": + continue + + if name[0:19] == "virDomainFSInfoFree": + continue + + if name[0:25] == "virDomainIOThreadInfoFree": + continue + + if name[0:22] == "virDomainInterfaceFree": + continue + + if name[0:21] == "virDomainListGetStats": + name = "virConnectDomainListGetStats" + + # These aren't functions, they're callback signatures + if name in ["virConnectAuthCallbackPtr", "virConnectCloseFunc", + "virStreamSinkFunc", "virStreamSourceFunc", "virStreamEventCallback", + "virEventHandleCallback", "virEventTimeoutCallback", "virFreeCallback", + "virStreamSinkHoleFunc", "virStreamSourceHoleFunc", "virStreamSourceSkipFunc"]: + continue + if name[0:21] == "virConnectDomainEvent" and name[-8:] == "Callback": + continue + if name[0:22] == "virConnectNetworkEvent" and name[-8:] == "Callback": + continue + if (name.startswith("virConnectStoragePoolEvent") and + name.endswith("Callback")): + continue + if (name.startswith("virConnectNodeDeviceEvent") and + name.endswith("Callback")): + continue + if (name.startswith("virConnectSecretEvent") and + name.endswith("Callback")): + continue + + # virEvent APIs go into main 'libvirt' namespace not any class + if name[0:8] == "virEvent": + if name[-4:] == "Func": + continue + basicklassmap[name] = ("libvirt", name, cname) + else: + found = False + # To start with map APIs to classes based on the + # naming prefix. Mistakes will be fixed in next + # loop + for klassname in gottypes: + klen = len(klassname) + if name[0:klen] == klassname: + found = True + if name not in basicklassmap: + basicklassmap[name] = (klassname, name[klen:], cname) + elif len(basicklassmap[name]) < klen: + basicklassmap[name] = (klassname, name[klen:], cname) + + # Anything which can't map to a class goes into the + # global namespaces + if not found: + basicklassmap[name] = ("libvirt", name[3:], cname) + + return basicklassmap + + +# Deal with oh so many special cases in C -> python mapping +def fixup_class_method_mapping(basicklassmap): + finalklassmap = {} # type: Dict[str, Tuple[str, str, str]] + + for name, (klass, func, cname) in sorted(basicklassmap.items()): + # The object lifecycle APIs are irrelevant since they're + # used inside the object constructors/destructors. + if func in ["Ref", "Free", "New", "GetConnect", "GetDomain", "GetNetwork"]: + if klass == "virStream" and func == "New": + klass = "virConnect" + func = "NewStream" + else: + continue + + # All the error handling methods need special handling + if klass == "libvirt": + if func in ["CopyLastError", "DefaultErrorFunc", + "ErrorFunc", "FreeError", + "SaveLastError", "ResetError"]: + continue + elif func in ["GetLastError", "GetLastErrorMessage", + "GetLastErrorCode", "GetLastErrorDomain", + "ResetLastError", "Initialize"]: + func = "vir" + func + elif func == "SetErrorFunc": + func = "RegisterErrorHandler" + elif klass == "virConnect": + if func in ["CopyLastError", "SetErrorFunc"]: + continue + elif func in ["GetLastError", "ResetLastError"]: + func = "virConn" + func + + # Remove 'Get' prefix from most APIs, except those in virConnect + # and virDomainSnapshot namespaces which stupidly used a different + # convention which we now can't fix without breaking API + if func[0:3] == "Get" and klass not in ["virConnect", "virDomainCheckpoint", "virDomainSnapshot", "libvirt"]: + if func not in ["GetCPUStats", "GetTime"]: + func = func[3:] + + # The object creation and lookup APIs all have to get re-mapped + # into the parent class + if func in ["CreateXML", "CreateLinux", "CreateXMLWithFiles", + "DefineXML", "CreateXMLFrom", "LookupByUUID", + "LookupByUUIDString", "LookupByVolume" "LookupByName", + "LookupByID", "LookupByName", "LookupByKey", "LookupByPath", + "LookupByMACString", "LookupByUsage", "LookupByVolume", + "LookupByTargetPath", "LookupSCSIHostByWWN", "LookupByPortDev", + "Restore", "RestoreFlags", + "SaveImageDefineXML", "SaveImageGetXMLDesc", "DefineXMLFlags", + "CreateXMLFlags"]: + if klass != "virDomain": + func = klass[3:] + func + + if klass in ["virDomainCheckpoint", "virDomainSnapshot"]: + klass = "virDomain" + func = func[6:] + elif klass == "virStorageVol" and func in ["StorageVolCreateXMLFrom", "StorageVolCreateXML"]: + klass = "virStoragePool" + func = func[10:] + elif klass == "virNetworkPort": + klass = "virNetwork" + func = func[7:] + elif func == "StoragePoolLookupByVolume": + klass = "virStorageVol" + elif func == "StorageVolLookupByName": + klass = "virStoragePool" + else: + klass = "virConnect" + + # The open methods get remapped to primary namespace + if klass == "virConnect" and func in ["Open", "OpenAuth", "OpenReadOnly"]: + klass = "libvirt" + + # These are inexplicably renamed in the python API + if func == "ListDomains": + func = "ListDomainsID" + elif func == "ListAllNodeDevices": + func = "ListAllDevices" + elif func == "ListNodeDevices": + func = "ListDevices" + + # The virInterfaceChangeXXXX APIs go into virConnect. Stupidly + # they have lost their 'interface' prefix in names, but we can't + # fix this name + if func[0:6] == "Change": + klass = "virConnect" + + # Need to special case the checkpoint and snapshot APIs + if klass == "virDomainSnapshot" and func in ["Current", "ListNames", "Num"]: + klass = "virDomain" + func = "snapshot" + func + + # Names should start with lowercase letter... + func = func[0:1].lower() + func[1:] + if func[0:8] == "nWFilter": + func = "nwfilter" + func[8:] + if func[0:8] == "fSFreeze" or func[0:6] == "fSThaw" or func[0:6] == "fSInfo": + func = "fs" + func[2:] + if func[0:12] == "iOThreadInfo": + func = "ioThreadInfo" + + if klass == "virNetwork": + func = func.replace("dHCP", "DHCP") + + # ...except when they don't. More stupid naming + # decisions we can't fix + if func == "iD": + func = "ID" + if func == "uUID": + func = "UUID" + if func == "uUIDString": + func = "UUIDString" + if func == "oSType": + func = "OSType" + if func == "xMLDesc": + func = "XMLDesc" + if func == "mACString": + func = "MACString" + + finalklassmap[name] = (klass, func, cname) + + return finalklassmap + + +# Validate that every C API is mapped to a python API +def validate_c_to_python_api_mappings(finalklassmap, gotfunctions): + usedfunctions = set() # type: Set[str] + for name, (klass, func, cname) in sorted(finalklassmap.items()): + if func in gotfunctions[klass]: + usedfunctions.add("%s.%s" % (klass, func)) + else: + raise Exception("%s -> %s.%s (C API not mapped to python)" % (name, klass, func)) + return usedfunctions + + +# Validate that every python API has a corresponding C API +def validate_python_to_c_api_mappings(gotfunctions, usedfunctions): + for klass in gotfunctions: + if klass == "libvirtError": + continue + for func in sorted(gotfunctions[klass]): + # These are pure python methods with no C APi + if func in ["connect", "getConnect", "domain", "getDomain", + "virEventInvokeFreeCallback", "network", + "sparseRecvAll", "sparseSendAll"]: + continue + + key = "%s.%s" % (klass, func) + if key not in usedfunctions: + raise Exception("%s.%s (Python API not mapped to C)" % (klass, func)) + + +# Validate that all the low level C APIs have binding +def validate_c_api_bindings_present(finalklassmap): + for name, (klass, func, cname) in sorted(finalklassmap.items()): + pyname = cname + if pyname == "virSetErrorFunc": + pyname = "virRegisterErrorHandler" + elif pyname == "virConnectListDomains": + pyname = "virConnectListDomainsID" + + # These exist in C and exist in python, but we've got + # a pure-python impl so don't check them + if name in ["virStreamRecvAll", "virStreamSendAll", + "virStreamSparseRecvAll", "virStreamSparseSendAll"]: + continue + + try: + thing = getattr(libvirt.libvirtmod, pyname) + except AttributeError: + raise Exception("libvirt.libvirtmod.%s (C binding does not exist)" % pyname) + +# Historically python lxml is incompatible with any other +# use of libxml2 in the same process. Merely importing +# 'lxml.etree' will result in libvirt's use of libxml2 +# triggering a SEGV: +# +# https://bugs.launchpad.net/lxml/+bug/1748019 +# +# per the last comment though, it was somewhat improved by +# +# https://github.com/lxml/lxml/commit/fa1d856cad369d0ac64323ddec14b02281491706 +# +# so if we have version >= 4.5.2, we are safe to import +# lxml.etree for the purposes of unit tests at least +def broken_lxml(): + import lxml + + if not hasattr(lxml, "__version__"): + return True + + digits = [int(d) for d in lxml.__version__.split(".")] + + # We have 3 digits in versions today, but be paranoid + # for possible future changes. + if len(digits) != 3: + return False + + version = (digits[0] * 1000 * 1000) + (digits[1] * 1000) + digits[2] + if version < 4005002: + return True + + return False + +api_test_flag = unittest.skipUnless( + os.environ.get('LIBVIRT_API_COVERAGE', False), + "API coverage test is only for upstream maintainers", +) + +lxml_broken_flag = unittest.skipIf( + broken_lxml(), + "lxml version clashes with libxml usage from libvirt" +) + +@api_test_flag +@lxml_broken_flag +class LibvirtAPICoverage(unittest.TestCase): + def test_libvirt_api(self): + xml = get_libvirt_api_xml_path() + + import lxml.etree + with open(xml, "r") as fp: + tree = lxml.etree.parse(fp) + + wantfunctions, wantenums, enumvals = identify_functions_enums(tree) + gotfunctions, gottypes = identify_class_methods(wantenums, enumvals) + basicklassmap = basic_class_method_mapping(wantfunctions, gottypes) + finalklassmap = fixup_class_method_mapping(basicklassmap) + usedfunctions = validate_c_to_python_api_mappings(finalklassmap, gotfunctions) + validate_python_to_c_api_mappings(gotfunctions, usedfunctions) + validate_c_api_bindings_present(finalklassmap) |