1
0
mirror of https://gitlab.com/libvirt/libvirt-python.git synced 2025-08-17 09:49:25 +03:00
Files
libvirt-python/tests/test_api_coverage.py
Daniel P. Berrangé 60044515a2 setup: switch to running API coverage test using pytest
The API coverage test is no longer a special case.

Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
2022-04-21 15:00:29 +00:00

433 lines
16 KiB
Python

#!/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)