bdd: reorganise field comparisons

Move comparision on Field values from assert_field() into a
comparator class. Replace BadRowValueAssert with a simpler
check_row() function.
This commit is contained in:
Sarah Hoffmann
2023-03-09 17:05:05 +01:00
parent 9769a0dcdb
commit da0a7a765e
2 changed files with 91 additions and 76 deletions

View File

@@ -2,12 +2,14 @@
# #
# This file is part of Nominatim. (https://nominatim.org) # This file is part of Nominatim. (https://nominatim.org)
# #
# Copyright (C) 2022 by the Nominatim developer community. # Copyright (C) 2023 by the Nominatim developer community.
# For a full list of authors see the git log. # For a full list of authors see the git log.
""" """
Collection of assertion functions used for the steps. Collection of assertion functions used for the steps.
""" """
import json import json
import math
import re
class Almost: class Almost:
""" Compares a float value with a certain jitter. """ Compares a float value with a certain jitter.
@@ -19,6 +21,51 @@ class Almost:
def __eq__(self, other): def __eq__(self, other):
return abs(other - self.value) < self.offset return abs(other - self.value) < self.offset
OSM_TYPE = {'N' : 'node', 'W' : 'way', 'R' : 'relation',
'n' : 'node', 'w' : 'way', 'r' : 'relation',
'node' : 'n', 'way' : 'w', 'relation' : 'r'}
class OsmType:
""" Compares an OSM type, accepting both N/R/W and node/way/relation.
"""
def __init__(self, value):
self.value = value
def __eq__(self, other):
return other == self.value or other == OSM_TYPE[self.value]
def __str__(self):
return f"{self.value} or {OSM_TYPE[self.value]}"
class Field:
""" Generic comparator for fields, which looks at the type of the
value compared.
"""
def __init__(self, value):
self.value = value
def __eq__(self, other):
if isinstance(self.value, float):
return math.isclose(self.value, float(other))
if self.value.startswith('^'):
return re.fullmatch(self.value, other)
if isinstance(other, dict):
return other == eval('{' + self.value + '}')
return str(self.value) == str(other)
def __str__(self):
return str(self.value)
class Bbox: class Bbox:
""" Comparator for bounding boxes. """ Comparator for bounding boxes.
""" """

View File

@@ -11,38 +11,7 @@ import re
import json import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from check_functions import Almost, check_for_attributes from check_functions import Almost, OsmType, Field, check_for_attributes
OSM_TYPE = {'N' : 'node', 'W' : 'way', 'R' : 'relation',
'n' : 'node', 'w' : 'way', 'r' : 'relation',
'node' : 'n', 'way' : 'w', 'relation' : 'r'}
def _geojson_result_to_json_result(geojson_result):
result = geojson_result['properties']
result['geojson'] = geojson_result['geometry']
if 'bbox' in geojson_result:
# bbox is minlon, minlat, maxlon, maxlat
# boundingbox is minlat, maxlat, minlon, maxlon
result['boundingbox'] = [geojson_result['bbox'][1],
geojson_result['bbox'][3],
geojson_result['bbox'][0],
geojson_result['bbox'][2]]
return result
class BadRowValueAssert:
""" Lazily formatted message for failures to find a field content.
"""
def __init__(self, response, idx, field, value):
self.idx = idx
self.field = field
self.value = value
self.row = response.result[idx]
def __str__(self):
return "\nBad value for row {} field '{}'. Expected: {}, got: {}.\nFull row: {}"""\
.format(self.idx, self.field, self.value,
self.row[self.field], json.dumps(self.row, indent=4))
class GenericResponse: class GenericResponse:
@@ -118,29 +87,6 @@ class GenericResponse:
r |= r.pop('geocoding') r |= r.pop('geocoding')
def assert_field(self, idx, field, value):
""" Check that result row `idx` has a field `field` with value `value`.
Float numbers are matched approximately. When the expected value
starts with a carat, regular expression matching is used.
"""
assert field in self.result[idx], \
"Result row {} has no field '{}'.\nFull row: {}"\
.format(idx, field, json.dumps(self.result[idx], indent=4))
if isinstance(value, float):
assert Almost(value) == float(self.result[idx][field]), \
BadRowValueAssert(self, idx, field, value)
elif value.startswith("^"):
assert re.fullmatch(value, self.result[idx][field]), \
BadRowValueAssert(self, idx, field, value)
elif isinstance(self.result[idx][field], dict):
assert self.result[idx][field] == eval('{' + value + '}'), \
BadRowValueAssert(self, idx, field, value)
else:
assert str(self.result[idx][field]) == str(value), \
BadRowValueAssert(self, idx, field, value)
def assert_subfield(self, idx, path, value): def assert_subfield(self, idx, path, value):
assert path assert path
@@ -170,18 +116,11 @@ class GenericResponse:
todo = [int(idx)] todo = [int(idx)]
for idx in todo: for idx in todo:
assert 'address' in self.result[idx], \ self.check_row(idx, 'address' in self.result[idx], "No field 'address'")
"Result row {} has no field 'address'.\nFull row: {}"\
.format(idx, json.dumps(self.result[idx], indent=4))
address = self.result[idx]['address'] address = self.result[idx]['address']
assert field in address, \ self.check_row_field(idx, field, value, base=address)
"Result row {} has no field '{}' in address.\nFull address: {}"\
.format(idx, field, json.dumps(address, indent=4))
assert address[field] == value, \
"\nBad value for row {} field '{}' in address. Expected: {}, got: {}.\nFull address: {}"""\
.format(idx, field, value, address[field], json.dumps(address, indent=4))
def match_row(self, row, context=None): def match_row(self, row, context=None):
""" Match the result fields against the given behave table row. """ Match the result fields against the given behave table row.
@@ -196,15 +135,10 @@ class GenericResponse:
if name == 'ID': if name == 'ID':
pass pass
elif name == 'osm': elif name == 'osm':
assert 'osm_type' in self.result[i], \ self.check_row_field(i, 'osm_type', OsmType(value[0]))
"Result row {} has no field 'osm_type'.\nFull row: {}"\ self.check_row_field(i, 'osm_id', Field(value[1:]))
.format(i, json.dumps(self.result[i], indent=4))
assert self.result[i]['osm_type'] in (OSM_TYPE[value[0]], value[0]), \
BadRowValueAssert(self, i, 'osm_type', value)
self.assert_field(i, 'osm_id', value[1:])
elif name == 'osm_type': elif name == 'osm_type':
assert self.result[i]['osm_type'] in (OSM_TYPE[value[0]], value[0]), \ self.check_row_field(i, 'osm_type', OsmType(value[0]))
BadRowValueAssert(self, i, 'osm_type', value)
elif name == 'centroid': elif name == 'centroid':
if ' ' in value: if ' ' in value:
lon, lat = value.split(' ') lon, lat = value.split(' ')
@@ -212,17 +146,51 @@ class GenericResponse:
lon, lat = context.osm.grid_node(int(value)) lon, lat = context.osm.grid_node(int(value))
else: else:
raise RuntimeError("Context needed when using grid coordinates") raise RuntimeError("Context needed when using grid coordinates")
self.assert_field(i, 'lat', float(lat)) self.check_row_field(i, 'lat', Field(float(lat)), base=subdict)
self.assert_field(i, 'lon', float(lon)) self.check_row_field(i, 'lon', Field(float(lon)), base=subdict)
elif '+' in name: elif '+' in name:
self.assert_subfield(i, name.split('+'), value) self.assert_subfield(i, name.split('+'), value)
else: else:
self.assert_field(i, name, value) self.check_row_field(i, name, Field(value))
def property_list(self, prop): def property_list(self, prop):
return [x[prop] for x in self.result] return [x[prop] for x in self.result]
def check_row(self, idx, check, msg):
""" Assert for the condition 'check' and print 'msg' on fail together
with the contents of the failing result.
"""
class _RowError:
def __init__(self, row):
self.row = row
def __str__(self):
return f"{msg}. Full row {idx}:\n" \
+ json.dumps(self.row, indent=4, ensure_ascii=False)
assert check, _RowError(self.result[idx])
def check_row_field(self, idx, field, expected, base=None):
""" Check field 'field' of result 'idx' for the expected value
and print a meaningful error if the condition fails.
When 'base' is set to a dictionary, then the field is checked
in that base. The error message will still report the contents
of the full result.
"""
if base is None:
base = self.result[idx]
self.check_row(idx, field in base, f"No field '{field}'")
value = base[field]
self.check_row(idx, expected == value,
f"\nBad value for field '{field}'. Expected: {expected}, got: {value}")
class SearchResponse(GenericResponse): class SearchResponse(GenericResponse):
""" Specialised class for search and lookup responses. """ Specialised class for search and lookup responses.
Transforms the xml response in a format similar to json. Transforms the xml response in a format similar to json.