]> git.openstreetmap.org Git - nominatim.git/blob - test/bdd/utils/checks.py
replace behave BDD API tests with pytest-bdd tests
[nominatim.git] / test / bdd / utils / checks.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2025 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions to compare expected values.
9 """
10 import json
11 import re
12
13 COMPARATOR_TERMS = {
14     'exactly': lambda exp, act: exp == act,
15     'more than': lambda exp, act: act > exp,
16     'less than': lambda exp, act: act < exp,
17 }
18
19
20 def _pretty(obj):
21     return json.dumps(obj, sort_keys=True, indent=2)
22
23
24 def within_box(value, expect):
25     coord = [float(x) for x in expect.split(',')]
26
27     if isinstance(value, str):
28         value = value.split(',')
29     value = list(map(float, value))
30
31     if len(value) == 2:
32         return coord[0] <= value[0] <= coord[2] \
33                and coord[1] <= value[1] <= coord[3]
34
35     if len(value) == 4:
36         return value[0] >= coord[0] and value[1] <= coord[1] \
37                and value[2] >= coord[2] and value[3] <= coord[3]
38
39     raise ValueError("Not a coordinate or bbox.")
40
41
42 COMPARISON_FUNCS = {
43     None: lambda val, exp: str(val) == exp,
44     'i': lambda val, exp: str(val).lower() == exp.lower(),
45     'fm': lambda val, exp: re.fullmatch(exp, val) is not None,
46     'in_box': within_box
47 }
48
49 OSM_TYPE = {'node': 'n', 'way': 'w', 'relation': 'r'}
50
51
52 class ResultAttr:
53     """ Returns the given attribute as a string.
54
55         The key parameter determines how the value is formatted before
56         returning. To refer to sub attributes, use '+' to add more keys
57         (e.g. 'name+ref' will access obj['name']['ref']). A '!' introduces
58         a formatting suffix. If no suffix is given, the value will be
59         converted using the str() function.
60
61         Available formatters:
62
63         !:... - use a formatting expression according to Python Mini Format Spec
64         !i    - make case-insensitive comparison
65         !fm   - consider comparison string a regular expression and match full value
66     """
67
68     def __init__(self, obj, key):
69         self.obj = obj
70         if '!' in key:
71             self.key, self.fmt = key.rsplit('!', 1)
72         else:
73             self.key = key
74             self.fmt = None
75
76         if self.key == 'object':
77             assert 'osm_id' in obj
78             assert 'osm_type' in obj
79             self.subobj = OSM_TYPE[obj['osm_type']] + str(obj['osm_id'])
80             self.fmt = 'i'
81         else:
82             done = ''
83             self.subobj = self.obj
84             for sub in self.key.split('+'):
85                 done += f"[{sub}]"
86                 assert sub in self.subobj, \
87                     f"Missing attribute {done}. Full object:\n{_pretty(self.obj)}"
88                 self.subobj = self.subobj[sub]
89
90     def __eq__(self, other):
91         if not isinstance(other, str):
92             raise NotImplementedError()
93
94         # work around bad quoting by pytest-bdd
95         other = other.replace(r'\\', '\\')
96
97         if self.fmt in COMPARISON_FUNCS:
98             return COMPARISON_FUNCS[self.fmt](self.subobj, other)
99
100         if self.fmt.startswith(':'):
101             return other == f"{{{self.fmt}}}".format(self.subobj)
102
103         raise RuntimeError(f"Unknown format string '{self.fmt}'.")
104
105     def __repr__(self):
106         k = self.key.replace('+', '][')
107         if self.fmt:
108             k += '!' + self.fmt
109         return f"result[{k}]({self.subobj})"