1
0
mirror of https://github.com/openSUSE/osc.git synced 2024-11-10 22:56:15 +01:00
github.com_openSUSE_osc/tests/common.py

277 lines
9.4 KiB
Python
Raw Normal View History

import io
import os
import shutil
import sys
import tempfile
import unittest
from unittest.mock import patch
from urllib.request import HTTPHandler, addinfourl, build_opener
from urllib.parse import urlparse, parse_qs
from xml.etree import ElementTree as ET
import urllib3.response
import osc.core
def urlcompare(url, *args):
"""compare all components of url except query string - it is converted to
dict, therefor different ordering does not makes url's different, as well
as quoting of a query string"""
components = urlparse(url)
query_args = parse_qs(components.query)
components = components._replace(query=None)
if not args:
return False
for url in args:
components2 = urlparse(url)
query_args2 = parse_qs(components2.query)
components2 = components2._replace(query=None)
if components != components2 or \
query_args != query_args2:
return False
return True
2013-04-09 14:22:45 +02:00
tests: Ignore the ordering of attributes in XML documents Old xml.etree.cElementTree versions (python2) reorder the attributes while recent xml.etree.cElementTree versions (python3) keep the document order. Example: python3: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) b'<foo y="foo" x="bar" />' >>> python2: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) '<foo x="bar" y="foo" />' >>> So far, the testsuite compared two serialized XML documents via a simple string comparison. For instance via, self.assertEqual(actual_serialized_xml, expected_serialized_xml) where the expected_serialized_xml is, for instance, a hardcoded str. Obviously, this would only work for python2 or python3. In order to support both python versions, we first parse both XML documents and then compare the corresponding trees (this is OK because we do not care about comments etc.). A related issue is the way how the testsuite compares data that is "send" to the API. So far, this was a plain bytes comparison. Again, this won't work in case of XML documents (see above). Moreover, we have currently no notion to "indicate" that the transmitted data is an XML document. As a workaround, we keep the plain bytes comparison and in case it fails, we try an xml comparison (see above) as a last resort. Strictly speaking, this is "wrong" (there might be cases (in the future) where we want to ensure that the transmitted XML data is bit identical to a fixture file) but a reasonable comprise for now. Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
def xml_equal(actual, exp):
try:
actual_xml = ET.fromstring(actual)
exp_xml = ET.fromstring(exp)
except ET.ParseError:
return False
todo = [(actual_xml, exp_xml)]
while todo:
actual_xml, exp_xml = todo.pop(0)
if actual_xml.tag != exp_xml.tag:
return False
if actual_xml.attrib != exp_xml.attrib:
return False
if actual_xml.text != exp_xml.text:
return False
if actual_xml.tail != exp_xml.tail:
return False
if len(actual_xml) != len(exp_xml):
return False
todo.extend(list(zip(actual_xml, exp_xml)))
return True
class RequestWrongOrder(Exception):
"""raised if an unexpected request is issued to urllib2"""
def __init__(self, url, exp_url, method, exp_method):
Exception.__init__(self)
self.url = url
self.exp_url = exp_url
self.method = method
self.exp_method = exp_method
def __str__(self):
return '%s, %s, %s, %s' % (self.url, self.exp_url, self.method, self.exp_method)
class RequestDataMismatch(Exception):
"""raised if POSTed or PUTed data doesn't match with the expected data"""
def __init__(self, url, got, exp):
self.url = url
self.got = got
self.exp = exp
def __str__(self):
return '%s, %s, %s' % (self.url, self.got, self.exp)
EXPECTED_REQUESTS = []
# HACK: Fix "ValueError: I/O operation on closed file." error in tests on openSUSE Leap 15.2.
# The problem seems to appear only in the tests, possibly some interaction with MockHTTPConnectionPool.
# Porting 753fbc03 to urllib3 in openSUSE Leap 15.2 would fix the problem.
urllib3.response.HTTPResponse.__iter__ = lambda self : iter(self._fp)
class MockHTTPConnectionPool:
def __init__(self, host, port=None, **conn_kw):
pass
def urlopen(self, method, url, body=None, headers=None, retries=None, **response_kw):
global EXPECTED_REQUESTS
request = EXPECTED_REQUESTS.pop(0)
url = f"http://localhost{url}"
if not urlcompare(request["url"], url) or request["method"] != method:
raise RequestWrongOrder(request["url"], url, request["method"], method)
if method in ("POST", "PUT"):
if 'exp' not in request and 'expfile' in request:
with open(request['expfile'], 'rb') as f:
exp = f.read()
elif 'exp' in request and 'expfile' not in request:
exp = request['exp'].encode('utf-8')
else:
raise RuntimeError('Specify either `exp` or `expfile`')
body = body or b""
if hasattr(body, "read"):
# if it is a file-like object, read it
body = body.read()
if hasattr(body, "encode"):
# if it can be encoded to bytes, do it
body = body.encode("utf-8")
if body != exp:
# We do not have a notion to explicitly mark xml content. In case
# of xml, we do not care about the exact xml representation (for
# now). Hence, if both, data and exp, are xml and are "equal",
# everything is fine (for now); otherwise, error out
# (of course, this is problematic if we want to ensure that XML
# documents are bit identical...)
if not xml_equal(body, exp):
raise RequestDataMismatch(url, repr(body), repr(exp))
if 'exception' in request:
raise request["exception"]
if 'text' not in request and 'file' in request:
with open(request['file'], 'rb') as f:
data = f.read()
elif 'text' in request and 'file' not in request:
data = request['text'].encode('utf-8')
else:
raise RuntimeError('Specify either `text` or `file`')
response = urllib3.response.HTTPResponse(body=data, status=request.get("code", 200))
response._fp = io.BytesIO(data)
return response
def urldecorator(method, url, **kwargs):
def decorate(test_method):
def wrapped_test_method(self):
# put all args into a single dictionary
kwargs["method"] = method
kwargs["url"] = url
# prepend fixtures dir to `file`
if "file" in kwargs:
kwargs["file"] = os.path.join(self._get_fixtures_dir(), kwargs["file"])
# prepend fixtures dir to `expfile`
if "expfile" in kwargs:
kwargs["expfile"] = os.path.join(self._get_fixtures_dir(), kwargs["expfile"])
EXPECTED_REQUESTS.append(kwargs)
test_method(self)
# mock connection pool, but only just once
if not hasattr(test_method, "_MockHTTPConnectionPool"):
wrapped_test_method = patch('urllib3.HTTPConnectionPool', MockHTTPConnectionPool)(wrapped_test_method)
wrapped_test_method._MockHTTPConnectionPool = True
wrapped_test_method.__name__ = test_method.__name__
return wrapped_test_method
return decorate
def GET(path, **kwargs):
return urldecorator('GET', path, **kwargs)
def PUT(path, **kwargs):
return urldecorator('PUT', path, **kwargs)
def POST(path, **kwargs):
return urldecorator('POST', path, **kwargs)
def DELETE(path, **kwargs):
return urldecorator('DELETE', path, **kwargs)
class OscTestCase(unittest.TestCase):
def setUp(self, copytree=True):
global EXPECTED_REQUESTS
EXPECTED_REQUESTS = []
os.chdir(os.path.dirname(__file__))
2013-01-18 22:58:53 +01:00
oscrc = os.path.join(self._get_fixtures_dir(), 'oscrc')
osc.core.conf.get_config(override_conffile=oscrc,
override_no_keyring=True, override_no_gnome_keyring=True)
2013-01-18 22:58:53 +01:00
os.environ['OSC_CONFIG'] = oscrc
self.tmpdir = tempfile.mkdtemp(prefix='osc_test')
if copytree:
shutil.copytree(os.path.join(self._get_fixtures_dir(), 'osctest'), os.path.join(self.tmpdir, 'osctest'))
self.stdout = sys.stdout
sys.stdout = io.StringIO()
def tearDown(self):
sys.stdout = self.stdout
try:
shutil.rmtree(self.tmpdir)
except:
pass
self.assertTrue(len(EXPECTED_REQUESTS) == 0)
def _get_fixtures_dir(self):
raise NotImplementedError('subclasses should implement this method')
def _change_to_pkg(self, name):
os.chdir(os.path.join(self.tmpdir, 'osctest', name))
def _check_list(self, fname, exp):
fname = os.path.join('.osc', fname)
self.assertTrue(os.path.exists(fname))
self.assertEqual(open(fname, 'r').read(), exp)
def _check_addlist(self, exp):
self._check_list('_to_be_added', exp)
def _check_deletelist(self, exp):
self._check_list('_to_be_deleted', exp)
def _check_conflictlist(self, exp):
self._check_list('_in_conflict', exp)
def _check_status(self, p, fname, exp):
self.assertEqual(p.status(fname), exp)
def _check_digests(self, fname, *skipfiles):
fname = os.path.join(self._get_fixtures_dir(), fname)
tests: Ignore the ordering of attributes in XML documents Old xml.etree.cElementTree versions (python2) reorder the attributes while recent xml.etree.cElementTree versions (python3) keep the document order. Example: python3: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) b'<foo y="foo" x="bar" />' >>> python2: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) '<foo x="bar" y="foo" />' >>> So far, the testsuite compared two serialized XML documents via a simple string comparison. For instance via, self.assertEqual(actual_serialized_xml, expected_serialized_xml) where the expected_serialized_xml is, for instance, a hardcoded str. Obviously, this would only work for python2 or python3. In order to support both python versions, we first parse both XML documents and then compare the corresponding trees (this is OK because we do not care about comments etc.). A related issue is the way how the testsuite compares data that is "send" to the API. So far, this was a plain bytes comparison. Again, this won't work in case of XML documents (see above). Moreover, we have currently no notion to "indicate" that the transmitted data is an XML document. As a workaround, we keep the plain bytes comparison and in case it fails, we try an xml comparison (see above) as a last resort. Strictly speaking, this is "wrong" (there might be cases (in the future) where we want to ensure that the transmitted XML data is bit identical to a fixture file) but a reasonable comprise for now. Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
with open(os.path.join('.osc', '_files'), 'r') as f:
files_act = f.read()
with open(fname, 'r') as f:
files_exp = f.read()
self.assertXMLEqual(files_act, files_exp)
root = ET.fromstring(files_act)
for i in root.findall('entry'):
if i.get('name') in skipfiles:
continue
self.assertTrue(os.path.exists(os.path.join('.osc', i.get('name'))))
self.assertEqual(osc.core.dgst(os.path.join('.osc', i.get('name'))), i.get('md5'))
tests: Ignore the ordering of attributes in XML documents Old xml.etree.cElementTree versions (python2) reorder the attributes while recent xml.etree.cElementTree versions (python3) keep the document order. Example: python3: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) b'<foo y="foo" x="bar" />' >>> python2: >>> ET.tostring(ET.fromstring('<foo y="foo" x="bar"/>')) '<foo x="bar" y="foo" />' >>> So far, the testsuite compared two serialized XML documents via a simple string comparison. For instance via, self.assertEqual(actual_serialized_xml, expected_serialized_xml) where the expected_serialized_xml is, for instance, a hardcoded str. Obviously, this would only work for python2 or python3. In order to support both python versions, we first parse both XML documents and then compare the corresponding trees (this is OK because we do not care about comments etc.). A related issue is the way how the testsuite compares data that is "send" to the API. So far, this was a plain bytes comparison. Again, this won't work in case of XML documents (see above). Moreover, we have currently no notion to "indicate" that the transmitted data is an XML document. As a workaround, we keep the plain bytes comparison and in case it fails, we try an xml comparison (see above) as a last resort. Strictly speaking, this is "wrong" (there might be cases (in the future) where we want to ensure that the transmitted XML data is bit identical to a fixture file) but a reasonable comprise for now. Fixes: #751 ("[python3.8] Testsuite fails")
2020-06-03 21:06:26 +02:00
def assertXMLEqual(self, act, exp):
if xml_equal(act, exp):
return
# ok, xmls are different, hence, assertEqual is expected to fail
# (we just use it in order to get a "nice" error message)
self.assertEqual(act, exp)
# not reached (unless assertEqual is overridden in an incompatible way)
raise RuntimeError('assertEqual assumptions violated')
def assertEqualMultiline(self, got, exp):
if (got + exp).find('\n') == -1:
self.assertEqual(got, exp)
else:
start_delim = "\n" + (" 8< ".join(["-----"] * 8)) + "\n"
end_delim = "\n" + (" >8 ".join(["-----"] * 8)) + "\n\n"
self.assertEqual(got, exp,
"got:" + start_delim + got + end_delim +
"expected:" + start_delim + exp + end_delim)