Add support for unordered comparison of features of layers

This is useful when an algorithm returns features in no particular order
and sorting features by attributes does not help because there may be
features with the same attributes, giving non-unique sorting orders.
This commit is contained in:
Martin Dobias 2018-05-09 19:55:17 -04:00
parent 73d10afe31
commit ef145afca6

View File

@ -109,6 +109,52 @@ class TestCase(_TestCase):
except KeyError:
topo_equal_check = False
try:
unordered = compare['unordered']
except KeyError:
unordered = False
if unordered:
features_expected = [f for f in layer_expected.getFeatures(request)]
for feat in layer_result.getFeatures(request):
feat_expected_equal = None
for feat_expected in features_expected:
if self.checkGeometriesEqual(feat.geometry(), feat_expected.geometry(),
feat.id(), feat_expected.id(),
False, precision, topo_equal_check) and \
self.checkAttributesEqual(feat, feat_expected, layer_expected.fields(), False, compare):
feat_expected_equal = feat_expected
break
if feat_expected_equal is not None:
features_expected.remove(feat_expected_equal)
else:
if use_asserts:
_TestCase.assertTrue(
self, False,
'Unexpected result feature: fid {}, geometry: {}, attributes: {}'.format(
feat.id(),
feat.geometry().constGet().asWkt(precision) if feat.geometry() else 'NULL',
feat.attributes())
)
else:
return False
if len(features_expected) != 0:
if use_asserts:
lst_missing = []
for feat in features_expected:
lst_missing.append('fid {}, geometry: {}, attributes: {}'.format(
feat.id(),
feat.geometry().constGet().asWkt(precision) if feat.geometry() else 'NULL',
feat.attributes())
)
_TestCase.assertTrue(self, False, 'Some expected features not found in results:\n' + '\n'.join(lst_missing))
else:
return False
return True
def sort_by_pk_or_fid(f):
if 'pk' in kwargs and kwargs['pk'] is not None:
key = kwargs['pk']
@ -129,68 +175,12 @@ class TestCase(_TestCase):
feats[0].id(),
feats[1].id(),
use_asserts, precision, topo_equal_check)
if not eq and use_asserts:
if not eq and not use_asserts:
return False
for attr_expected, field_expected in zip(feats[0].attributes(), layer_expected.fields().toList()):
try:
cmp = compare['fields'][field_expected.name()]
except KeyError:
try:
cmp = compare['fields']['__all__']
except KeyError:
cmp = {}
# Skip field
if 'skip' in cmp:
continue
if use_asserts:
_TestCase.assertIn(
self,
field_expected.name().lower(),
[name.lower() for name in feats[1].fields().names()])
attr_result = feats[1][field_expected.name()]
field_result = [fld for fld in layer_expected.fields().toList() if fld.name() == field_expected.name()][0]
# Cast field to a given type
if 'cast' in cmp:
if cmp['cast'] == 'int':
attr_expected = int(attr_expected) if attr_expected else None
attr_result = int(attr_result) if attr_result else None
if cmp['cast'] == 'float':
attr_expected = float(attr_expected) if attr_expected else None
attr_result = float(attr_result) if attr_result else None
if cmp['cast'] == 'str':
attr_expected = str(attr_expected) if attr_expected else None
attr_result = str(attr_result) if attr_result else None
# Round field (only numeric so it works with __all__)
if 'precision' in cmp and field_expected.type() in [QVariant.Int, QVariant.Double, QVariant.LongLong]:
if not attr_expected == NULL:
attr_expected = round(attr_expected, cmp['precision'])
if not attr_result == NULL:
attr_result = round(attr_result, cmp['precision'])
if use_asserts:
_TestCase.assertEqual(
self,
attr_expected,
attr_result,
'Features {}/{} differ in attributes\n\n * Field expected: {} ({})\n * result : {} ({})\n\n * Expected: {} != Result : {}'.format(
feats[0].id(),
feats[1].id(),
field_expected.name(),
field_expected.typeName(),
field_result.name(),
field_result.typeName(),
repr(attr_expected),
repr(attr_result)
)
)
elif attr_expected != attr_result:
return False
eq = self.checkAttributesEqual(feats[0], feats[1], layer_expected.fields(), use_asserts, compare)
if not eq and not use_asserts:
return False
return True
@ -227,13 +217,78 @@ class TestCase(_TestCase):
'Features (Expected fid: {}, Result fid: {}) differ in geometry: \n\n Expected geometry:\n {}\n\n Result geometry:\n {}'.format(
geom0_id,
geom1_id,
geom0.constGet().asWkt(precision) if geom0 is not None else 'NULL',
geom1.constGet().asWkt(precision) if geom1 is not None else 'NULL'
geom0.constGet().asWkt(precision) if not geom0.isNull() else 'NULL',
geom1.constGet().asWkt(precision) if not geom1.isNull() else 'NULL'
)
)
else:
return equal
def checkAttributesEqual(self, feat0, feat1, fields_expected, use_asserts, compare):
""" Checks whether attributes of two features are the same """
for attr_expected, field_expected in zip(feat0.attributes(), fields_expected.toList()):
try:
cmp = compare['fields'][field_expected.name()]
except KeyError:
try:
cmp = compare['fields']['__all__']
except KeyError:
cmp = {}
# Skip field
if 'skip' in cmp:
continue
if use_asserts:
_TestCase.assertIn(
self,
field_expected.name().lower(),
[name.lower() for name in feat1.fields().names()])
attr_result = feat1[field_expected.name()]
field_result = [fld for fld in fields_expected.toList() if fld.name() == field_expected.name()][0]
# Cast field to a given type
if 'cast' in cmp:
if cmp['cast'] == 'int':
attr_expected = int(attr_expected) if attr_expected else None
attr_result = int(attr_result) if attr_result else None
if cmp['cast'] == 'float':
attr_expected = float(attr_expected) if attr_expected else None
attr_result = float(attr_result) if attr_result else None
if cmp['cast'] == 'str':
attr_expected = str(attr_expected) if attr_expected else None
attr_result = str(attr_result) if attr_result else None
# Round field (only numeric so it works with __all__)
if 'precision' in cmp and field_expected.type() in [QVariant.Int, QVariant.Double, QVariant.LongLong]:
if not attr_expected == NULL:
attr_expected = round(attr_expected, cmp['precision'])
if not attr_result == NULL:
attr_result = round(attr_result, cmp['precision'])
if use_asserts:
_TestCase.assertEqual(
self,
attr_expected,
attr_result,
'Features {}/{} differ in attributes\n\n * Field expected: {} ({})\n * result : {} ({})\n\n * Expected: {} != Result : {}'.format(
feat0.id(),
feat1.id(),
field_expected.name(),
field_expected.typeName(),
field_result.name(),
field_result.typeName(),
repr(attr_expected),
repr(attr_result)
)
)
elif attr_expected != attr_result:
return False
return True
class _UnexpectedSuccess(Exception):