Merge pull request #3003 from lonvia/rework-bdd-api-tests

Reorganize code around BDD API tests and extend reverse API tests
This commit is contained in:
Sarah Hoffmann
2023-03-10 10:01:24 +01:00
committed by GitHub
23 changed files with 1020 additions and 593 deletions

View File

@@ -524,12 +524,7 @@ class PlaceLookup
// Get the bounding box and outline polygon // Get the bounding box and outline polygon
$sSQL = 'select place_id,0 as numfeatures,st_area(geometry) as area,'; $sSQL = 'select place_id,0 as numfeatures,st_area(geometry) as area,';
if ($fLonReverse != null && $fLatReverse != null) {
$sSQL .= ' ST_Y(closest_point) as centrelat,';
$sSQL .= ' ST_X(closest_point) as centrelon,';
} else {
$sSQL .= ' ST_Y(centroid) as centrelat, ST_X(centroid) as centrelon,'; $sSQL .= ' ST_Y(centroid) as centrelat, ST_X(centroid) as centrelon,';
}
$sSQL .= ' ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,'; $sSQL .= ' ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,';
$sSQL .= ' ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon'; $sSQL .= ' ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon';
if ($this->bIncludePolygonAsGeoJSON) { if ($this->bIncludePolygonAsGeoJSON) {
@@ -544,19 +539,21 @@ class PlaceLookup
if ($this->bIncludePolygonAsText) { if ($this->bIncludePolygonAsText) {
$sSQL .= ',ST_AsText(geometry) as astext'; $sSQL .= ',ST_AsText(geometry) as astext';
} }
$sSQL .= ' FROM (SELECT place_id';
if ($fLonReverse != null && $fLatReverse != null) { if ($fLonReverse != null && $fLatReverse != null) {
$sFrom = ' from (SELECT * , CASE WHEN (class = \'highway\') AND (ST_GeometryType(geometry) = \'ST_LineString\') THEN '; $sSQL .= ',CASE WHEN (class = \'highway\') AND (ST_GeometryType(geometry) = \'ST_LineString\') THEN ';
$sFrom .=' ST_ClosestPoint(geometry, ST_SetSRID(ST_Point('.$fLatReverse.','.$fLonReverse.'),4326))'; $sSQL .=' ST_ClosestPoint(geometry, ST_SetSRID(ST_Point('.$fLatReverse.','.$fLonReverse.'),4326))';
$sFrom .=' ELSE centroid END AS closest_point'; $sSQL .=' ELSE centroid END AS centroid';
$sFrom .= ' from placex where place_id = '.$iPlaceID.') as plx';
} else { } else {
$sFrom = ' from placex where place_id = '.$iPlaceID; $sSQL .= ',centroid';
} }
if ($this->fPolygonSimplificationThreshold > 0) { if ($this->fPolygonSimplificationThreshold > 0) {
$sSQL .= ' from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,'.$this->fPolygonSimplificationThreshold.') as geometry'.$sFrom.') as plx'; $sSQL .= ',ST_SimplifyPreserveTopology(geometry,'.$this->fPolygonSimplificationThreshold.') as geometry';
} else { } else {
$sSQL .= $sFrom; $sSQL .= ',geometry';
} }
$sSQL .= ' FROM placex where place_id = '.$iPlaceID.') as plx';
$aPointPolygon = $this->oDB->getRow($sSQL, null, 'Could not get outline'); $aPointPolygon = $this->oDB->getRow($sSQL, null, 'Could not get outline');

View File

@@ -96,7 +96,7 @@ class HTMLLogger(BaseLogger):
.compile(conn.sync_engine, compile_kwargs={"literal_binds": True})) .compile(conn.sync_engine, compile_kwargs={"literal_binds": True}))
if CODE_HIGHLIGHT: if CODE_HIGHLIGHT:
sqlstr = highlight(sqlstr, PostgresLexer(), sqlstr = highlight(sqlstr, PostgresLexer(),
HtmlFormatter(nowrap=True, lineseparator='<br>')) HtmlFormatter(nowrap=True, lineseparator='<br />'))
self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>') self._write(f'<div class="highlight"><code class="lang-sql">{sqlstr}</code></div>')
else: else:
self._write(f'<code class="lang-sql">{sqlstr}</code>') self._write(f'<code class="lang-sql">{sqlstr}</code>')

View File

@@ -7,8 +7,8 @@ Feature: Object details
Then the result is valid json Then the result is valid json
And result has attributes geometry And result has attributes geometry
And result has not attributes keywords,address,linked_places,parentof And result has not attributes keywords,address,linked_places,parentof
And results contain And results contain in field geometry
| geometry+type | | type |
| Point | | Point |
Scenario: JSON Details with pretty printing Scenario: JSON Details with pretty printing
@@ -75,8 +75,8 @@ Feature: Object details
| 1 | | 1 |
Then the result is valid json Then the result is valid json
And result has attributes geometry And result has attributes geometry
And results contain And results contain in field geometry
| geometry+type | | type |
| <geometry> | | <geometry> |
Examples: Examples:

View File

@@ -103,3 +103,16 @@ Feature: Object details
| category | type | admin_level | | category | type | admin_level |
| place | postcode | 15 | | place | postcode | 15 |
And result has not attributes osm_type,osm_id And result has not attributes osm_type,osm_id
@v1-api-python-only
Scenario Outline: Details debug output returns no errors
When sending debug details query for <feature>
Then the result is valid html
Examples:
| feature |
| N5484325405 |
| W1 |
| 112820 |
| 112871 |

View File

@@ -1,29 +0,0 @@
@APIDB
Feature: Parameters for Reverse API
Testing correctness of geocodejson output.
Scenario: City housenumber-level address with street
When sending geocodejson reverse coordinates 47.1068011,9.52810091
Then results contain
| housenumber | street | postcode | city | country |
| 8 | Im Winkel | 9495 | Triesen | Liechtenstein |
Scenario: Town street-level address with street
When sending geocodejson reverse coordinates 47.066,9.504
| zoom |
| 16 |
Then results contain
| name | city | postcode | country |
| Gnetsch | Balzers | 9496 | Liechtenstein |
Scenario: Poi street-level address with footway
When sending geocodejson reverse coordinates 47.0653,9.5007
Then results contain
| street | city | postcode | country |
| Burgweg | Balzers | 9496 | Liechtenstein |
Scenario: City address with suburb
When sending geocodejson reverse coordinates 47.146861,9.511771
Then results contain
| housenumber | street | district | city | postcode | country |
| 5 | Lochgass | Ebenholz | Vaduz | 9490 | Liechtenstein |

View File

@@ -0,0 +1,44 @@
@APIDB
Feature: Geometries for reverse geocoding
Tests for returning geometries with reverse
Scenario: Polygons are returned fully by default
When sending v1/reverse at 47.13803,9.52264
| polygon_text |
| 1 |
Then results contain
| geotext |
| POLYGON((9.5225302 47.138066,9.5225348 47.1379282,9.5226142 47.1379294,9.5226143 47.1379257,9.522615 47.137917,9.5226225 47.1379098,9.5226334 47.1379052,9.5226461 47.1379037,9.5226588 47.1379056,9.5226693 47.1379107,9.5226762 47.1379181,9.5226762 47.1379268,9.5226761 47.1379308,9.5227366 47.1379317,9.5227352 47.1379753,9.5227608 47.1379757,9.5227595 47.1380148,9.5227355 47.1380145,9.5227337 47.1380692,9.5225302 47.138066)) |
Scenario: Polygons can be slightly simplified
When sending v1/reverse at 47.13803,9.52264
| polygon_text | polygon_threshold |
| 1 | 0.00001 |
Then results contain
| geotext |
| POLYGON((9.5225302 47.138066,9.5225348 47.1379282,9.5226142 47.1379294,9.5226225 47.1379098,9.5226588 47.1379056,9.5226761 47.1379308,9.5227366 47.1379317,9.5227352 47.1379753,9.5227608 47.1379757,9.5227595 47.1380148,9.5227355 47.1380145,9.5227337 47.1380692,9.5225302 47.138066)) |
Scenario: Polygons can be much simplified
When sending v1/reverse at 47.13803,9.52264
| polygon_text | polygon_threshold |
| 1 | 0.9 |
Then results contain
| geotext |
| POLYGON((9.5225302 47.138066,9.5225348 47.1379282,9.5227608 47.1379757,9.5227337 47.1380692,9.5225302 47.138066)) |
Scenario: For polygons return the centroid as center point
When sending v1/reverse at 47.13836,9.52304
Then results contain
| centroid |
| 9.52271080 47.13818045 |
Scenario: For streets return the closest point as center point
When sending v1/reverse at 47.13368,9.52942
Then results contain
| centroid |
| 9.529431527 47.13368172 |

View File

@@ -2,13 +2,13 @@
Feature: Localization of reverse search results Feature: Localization of reverse search results
Scenario: default language Scenario: default language
When sending json reverse coordinates 47.14,9.55 When sending v1/reverse at 47.14,9.55
Then result addresses contain Then result addresses contain
| ID | country | | ID | country |
| 0 | Liechtenstein | | 0 | Liechtenstein |
Scenario: accept-language parameter Scenario: accept-language parameter
When sending json reverse coordinates 47.14,9.55 When sending v1/reverse at 47.14,9.55
| accept-language | | accept-language |
| ja,en | | ja,en |
Then result addresses contain Then result addresses contain
@@ -19,7 +19,7 @@ Feature: Localization of reverse search results
Given the HTTP header Given the HTTP header
| accept-language | | accept-language |
| fo-ca,fo;q=0.8,en-ca;q=0.5,en;q=0.3 | | fo-ca,fo;q=0.8,en-ca;q=0.5,en;q=0.3 |
When sending json reverse coordinates 47.14,9.55 When sending v1/reverse at 47.14,9.55
Then result addresses contain Then result addresses contain
| ID | country | | ID | country |
| 0 | Liktinstein | | 0 | Liktinstein |
@@ -28,7 +28,7 @@ Feature: Localization of reverse search results
Given the HTTP header Given the HTTP header
| accept-language | | accept-language |
| fo-ca,fo;q=0.8,en-ca;q=0.5,en;q=0.3 | | fo-ca,fo;q=0.8,en-ca;q=0.5,en;q=0.3 |
When sending json reverse coordinates 47.14,9.55 When sending v1/reverse at 47.14,9.55
| accept-language | | accept-language |
| en | | en |
Then result addresses contain Then result addresses contain

View File

@@ -1,147 +0,0 @@
@APIDB
Feature: Parameters for Reverse API
Testing different parameter options for reverse API.
Scenario Outline: Reverse-geocoding without address
When sending <format> reverse coordinates 47.13,9.56
| addressdetails |
| 0 |
Then exactly 1 result is returned
And result has not attributes address
Examples:
| format |
| json |
| jsonv2 |
| geojson |
| xml |
Scenario Outline: Coordinates must be floating-point numbers
When sending reverse coordinates <coords>
Then a HTTP 400 is returned
Examples:
| coords |
| -45.3,; |
| gkjd,50 |
Scenario Outline: Zoom levels between 4 and 18 are allowed
When sending reverse coordinates 47.14122383,9.52169581334
| zoom |
| <zoom> |
Then exactly 1 result is returned
And result addresses contain
| country_code |
| li |
Examples:
| zoom |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
| 11 |
| 12 |
| 13 |
| 14 |
| 15 |
| 16 |
| 17 |
| 18 |
Scenario: Non-numerical zoom levels return an error
When sending reverse coordinates 47.14122383,9.52169581334
| zoom |
| adfe |
Then a HTTP 400 is returned
Scenario Outline: Reverse Geocoding with extratags
When sending <format> reverse coordinates 47.1395013150811,9.522098077031046
| extratags |
| 1 |
Then result 0 has attributes extratags
Examples:
| format |
| xml |
| json |
| jsonv2 |
| geojson |
Scenario Outline: Reverse Geocoding with namedetails
When sending <format> reverse coordinates 47.1395013150811,9.522098077031046
| namedetails |
| 1 |
Then result 0 has attributes namedetails
Examples:
| format |
| xml |
| json |
| jsonv2 |
| geojson |
Scenario Outline: Reverse Geocoding contains TEXT geometry
When sending <format> reverse coordinates 47.165989816710066,9.515774846076965
| polygon_text |
| 1 |
Then result 0 has attributes <response_attribute>
Examples:
| format | response_attribute |
| xml | geotext |
| json | geotext |
| jsonv2 | geotext |
Scenario Outline: Reverse Geocoding contains SVG geometry
When sending <format> reverse coordinates 47.165989816710066,9.515774846076965
| polygon_svg |
| 1 |
Then result 0 has attributes <response_attribute>
Examples:
| format | response_attribute |
| xml | geosvg |
| json | svg |
| jsonv2 | svg |
Scenario Outline: Reverse Geocoding contains KML geometry
When sending <format> reverse coordinates 47.165989816710066,9.515774846076965
| polygon_kml |
| 1 |
Then result 0 has attributes <response_attribute>
Examples:
| format | response_attribute |
| xml | geokml |
| json | geokml |
| jsonv2 | geokml |
Scenario Outline: Reverse Geocoding contains GEOJSON geometry
When sending <format> reverse coordinates 47.165989816710066,9.515774846076965
| polygon_geojson |
| 1 |
Then result 0 has attributes <response_attribute>
Examples:
| format | response_attribute |
| xml | geojson |
| json | geojson |
| jsonv2 | geojson |
| geojson | geojson |
Scenario Outline: Reverse Geocoding in geojson format contains no non-geojson geometry
When sending geojson reverse coordinates 47.165989816710066,9.515774846076965
| polygon_text | polygon_svg | polygon_geokml |
| 1 | 1 | 1 |
Then result 0 has not attributes <response_attribute>
Examples:
| response_attribute |
| geotext |
| polygonpoints |
| svg |
| geokml |

View File

@@ -2,19 +2,35 @@
Feature: Reverse geocoding Feature: Reverse geocoding
Testing the reverse function Testing the reverse function
Scenario Outline: Simple reverse-geocoding with no results
When sending v1/reverse at <lat>,<lon>
Then exactly 0 results are returned
Examples:
| lat | lon |
| 0.0 | 0.0 |
| -34.830 | -56.105 |
| 45.174 | -103.072 |
| 21.156 | -12.2744 |
| 91.3 | 0.4 |
| -700 | 0.4 |
| 0.2 | 324.44 |
| 0.2 | -180.4 |
@Tiger @Tiger
Scenario: TIGER house number Scenario: TIGER house number
When sending jsonv2 reverse coordinates 32.4752389363,-86.4810198619 When sending v1/reverse at 32.4752389363,-86.4810198619
Then results contain Then results contain
| osm_type | category | type | | category | type |
| way | place | house | | place | house |
And result addresses contain And result addresses contain
| house_number | road | postcode | country_code | | house_number | road | postcode | country_code |
| 707 | Upper Kingston Road | 36067 | us | | 707 | Upper Kingston Road | 36067 | us |
@Tiger @Tiger
Scenario: No TIGER house number for zoom < 18 Scenario: No TIGER house number for zoom < 18
When sending jsonv2 reverse coordinates 32.4752389363,-86.4810198619 When sending v1/reverse at 32.4752389363,-86.4810198619
| zoom | | zoom |
| 17 | | 17 |
Then results contain Then results contain
@@ -25,7 +41,7 @@ Feature: Reverse geocoding
| Upper Kingston Road | 30607 | us | | Upper Kingston Road | 30607 | us |
Scenario: Interpolated house number Scenario: Interpolated house number
When sending jsonv2 reverse coordinates 47.118533,9.57056562 When sending v1/reverse at 47.118533,9.57056562
Then results contain Then results contain
| osm_type | category | type | | osm_type | category | type |
| way | place | house | | way | place | house |
@@ -34,20 +50,20 @@ Feature: Reverse geocoding
| 1019 | Grosssteg | | 1019 | Grosssteg |
Scenario: Address with non-numerical house number Scenario: Address with non-numerical house number
When sending jsonv2 reverse coordinates 47.107465,9.52838521614 When sending v1/reverse at 47.107465,9.52838521614
Then result addresses contain Then result addresses contain
| house_number | road | | house_number | road |
| 39A/B | Dorfstrasse | | 39A/B | Dorfstrasse |
Scenario: Address with numerical house number Scenario: Address with numerical house number
When sending jsonv2 reverse coordinates 47.168440329479594,9.511551699184338 When sending v1/reverse at 47.168440329479594,9.511551699184338
Then result addresses contain Then result addresses contain
| house_number | road | | house_number | road |
| 6 | Schmedgässle | | 6 | Schmedgässle |
Scenario Outline: Zoom levels below 5 result in country Scenario Outline: Zoom levels below 5 result in country
When sending jsonv2 reverse coordinates 47.16,9.51 When sending v1/reverse at 47.16,9.51
| zoom | | zoom |
| <zoom> | | <zoom> |
Then results contain Then results contain
@@ -63,7 +79,7 @@ Feature: Reverse geocoding
| 4 | | 4 |
Scenario: When on a street, the closest interpolation is shown Scenario: When on a street, the closest interpolation is shown
When sending jsonv2 reverse coordinates 47.118457166193245,9.570678289621355 When sending v1/reverse at 47.118457166193245,9.570678289621355
| zoom | | zoom |
| 18 | | 18 |
Then results contain Then results contain
@@ -72,7 +88,7 @@ Feature: Reverse geocoding
# github 2214 # github 2214
Scenario: Interpolations do not override house numbers when they are closer Scenario: Interpolations do not override house numbers when they are closer
When sending jsonv2 reverse coordinates 47.11778,9.57255 When sending v1/reverse at 47.11778,9.57255
| zoom | | zoom |
| 18 | | 18 |
Then results contain Then results contain
@@ -80,7 +96,7 @@ Feature: Reverse geocoding
| 5, Grosssteg, Steg, Triesenberg, Oberland, 9497, Liechtenstein | | 5, Grosssteg, Steg, Triesenberg, Oberland, 9497, Liechtenstein |
Scenario: Interpolations do not override house numbers when they are closer (2) Scenario: Interpolations do not override house numbers when they are closer (2)
When sending jsonv2 reverse coordinates 47.11834,9.57167 When sending v1/reverse at 47.11834,9.57167
| zoom | | zoom |
| 18 | | 18 |
Then results contain Then results contain
@@ -88,7 +104,7 @@ Feature: Reverse geocoding
| 3, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein | | 3, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
Scenario: When on a street with zoom 18, the closest housenumber is returned Scenario: When on a street with zoom 18, the closest housenumber is returned
When sending jsonv2 reverse coordinates 47.11755503977281,9.572722250405036 When sending v1/reverse at 47.11755503977281,9.572722250405036
| zoom | | zoom |
| 18 | | 18 |
Then result addresses contain Then result addresses contain

View File

@@ -1,137 +0,0 @@
@APIDB
Feature: Simple Reverse Tests
Simple tests for internal server errors and response format.
Scenario Outline: Simple reverse-geocoding
When sending reverse coordinates <lat>,<lon>
Then the result is valid xml
When sending xml reverse coordinates <lat>,<lon>
Then the result is valid xml
When sending json reverse coordinates <lat>,<lon>
Then the result is valid json
When sending jsonv2 reverse coordinates <lat>,<lon>
Then the result is valid json
When sending geojson reverse coordinates <lat>,<lon>
Then the result is valid geojson
Examples:
| lat | lon |
| 0.0 | 0.0 |
| -34.830 | -56.105 |
| 45.174 | -103.072 |
| 21.156 | -12.2744 |
Scenario Outline: Testing different parameters
When sending reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid xml
When sending xml reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid xml
When sending json reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid json
When sending jsonv2 reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid json
When sending geojson reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid geojson
When sending geocodejson reverse coordinates 53.603,10.041
| param | value |
| <parameter> | <value> |
Then the result is valid geocodejson
Examples:
| parameter | value |
| polygon_text | 1 |
| polygon_text | 0 |
| polygon_kml | 1 |
| polygon_kml | 0 |
| polygon_geojson | 1 |
| polygon_geojson | 0 |
| polygon_svg | 1 |
| polygon_svg | 0 |
Scenario Outline: Wrapping of legal jsonp requests
When sending <format> reverse coordinates 67.3245,0.456
| json_callback |
| foo |
Then the result is valid <outformat>
Examples:
| format | outformat |
| json | json |
| jsonv2 | json |
| geojson | geojson |
Scenario Outline: Boundingbox is returned
When sending <format> reverse coordinates 47.11,9.57
| zoom |
| 8 |
Then result has bounding box in 47,48,9,10
Examples:
| format |
| json |
| jsonv2 |
| geojson |
| xml |
Scenario Outline: Reverse-geocoding with zoom
When sending <format> reverse coordinates 47.11,9.57
| zoom |
| 10 |
Then exactly 1 result is returned
Examples:
| format |
| json |
| jsonv2 |
| geojson |
| xml |
Scenario: Missing lon parameter
When sending reverse coordinates 52.52,
Then a HTTP 400 is returned
Scenario: Missing lat parameter
When sending reverse coordinates ,52.52
Then a HTTP 400 is returned
Scenario: Missing osm_id parameter
When sending reverse coordinates ,
| osm_type |
| N |
Then a HTTP 400 is returned
Scenario: Missing osm_type parameter
When sending reverse coordinates ,
| osm_id |
| 3498564 |
Then a HTTP 400 is returned
Scenario Outline: Bad format for lat or lon
When sending reverse coordinates ,
| lat | lon |
| <lat> | <lon> |
Then a HTTP 400 is returned
Examples:
| lat | lon |
| 48.9660 | 8,4482 |
| 48,9660 | 8.4482 |
| 48,9660 | 8,4482 |
| 48.966.0 | 8.4482 |
| 48.966 | 8.448.2 |
| Nan | 8.448 |
| 48.966 | Nan |
Scenario: Reverse Debug output returns no errors
When sending debug reverse coordinates 47.11,9.57
Then a HTTP 200 is returned

View File

@@ -0,0 +1,106 @@
@APIDB
Feature: Geocodejson for Reverse API
Testing correctness of geocodejson output (API version v1).
Scenario Outline: Simple OSM result
When sending v1/reverse at 47.066,9.504 with format geocodejson
| addressdetails |
| <has_address> |
Then result has attributes place_id, accuracy
And result has <attributes> country,postcode,county,city,district,street,housenumber, admin
Then results contain
| osm_type | osm_id | osm_key | osm_value | type |
| node | 6522627624 | shop | bakery | house |
And results contain
| name | label |
| Dorfbäckerei Herrmann | Dorfbäckerei Herrmann, 29, Gnetsch, Mäls, Balzers, Oberland, 9496, Liechtenstein |
And results contain in field geojson
| type | coordinates |
| Point | [9.5036065, 47.0660892] |
And results contain in field __geocoding
| version | licence | attribution |
| 0.1.0 | ODbL | Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright |
Examples:
| has_address | attributes |
| 1 | attributes |
| 0 | not attributes |
Scenario: City housenumber-level address with street
When sending v1/reverse at 47.1068011,9.52810091 with format geocodejson
Then results contain
| housenumber | street | postcode | city | country |
| 8 | Im Winkel | 9495 | Triesen | Liechtenstein |
And results contain in field admin
| level6 | level8 |
| Oberland | Triesen |
Scenario: Town street-level address with street
When sending v1/reverse at 47.066,9.504 with format geocodejson
| zoom |
| 16 |
Then results contain
| name | city | postcode | country |
| Gnetsch | Balzers | 9496 | Liechtenstein |
Scenario: Poi street-level address with footway
When sending v1/reverse at 47.06515,9.50083 with format geocodejson
Then results contain
| street | city | postcode | country |
| Burgweg | Balzers | 9496 | Liechtenstein |
Scenario: City address with suburb
When sending v1/reverse at 47.146861,9.511771 with format geocodejson
Then results contain
| housenumber | street | district | city | postcode | country |
| 5 | Lochgass | Ebenholz | Vaduz | 9490 | Liechtenstein |
@Tiger
Scenario: Tiger address
When sending v1/reverse at 32.4752389363,-86.4810198619 with format geocodejson
Then results contain
| osm_type | osm_id | osm_key | osm_value | type |
| way | 396009653 | place | house | house |
And results contain
| housenumber | street | city | county | postcode | country |
| 707 | Upper Kingston Road | Prattville | Autauga County | 36067 | United States |
Scenario: Interpolation address
When sending v1/reverse at 47.118533,9.57056562 with format geocodejson
Then results contain
| osm_type | osm_id | osm_key | osm_value | type |
| way | 1 | place | house | house |
And results contain
| label |
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
And result has not attributes name
Scenario: Line geometry output is supported
When sending v1/reverse at 47.06597,9.50467 with format geocodejson
| param | value |
| polygon_geojson | 1 |
Then results contain in field geojson
| type |
| LineString |
Scenario Outline: Only geojson polygons are supported
When sending v1/reverse at 47.06597,9.50467 with format geocodejson
| param | value |
| <param> | 1 |
Then results contain in field geojson
| type |
| Point |
Examples:
| param |
| polygon_text |
| polygon_svg |
| polygon_kml |

View File

@@ -0,0 +1,72 @@
@APIDB
Feature: Geojson for Reverse API
Testing correctness of geojson output (API version v1).
Scenario Outline: Simple OSM result
When sending v1/reverse at 47.066,9.504 with format geojson
| addressdetails |
| <has_address> |
Then result has attributes place_id, importance, __licence
And result has <attributes> address
And results contain
| osm_type | osm_id | place_rank | category | type | addresstype |
| node | 6522627624 | 30 | shop | bakery | shop |
And results contain
| name | display_name |
| Dorfbäckerei Herrmann | Dorfbäckerei Herrmann, 29, Gnetsch, Mäls, Balzers, Oberland, 9496, Liechtenstein |
And results contain
| boundingbox |
| [47.0660392, 47.0661392, 9.5035565, 9.5036565] |
And results contain in field geojson
| type | coordinates |
| Point | [9.5036065, 47.0660892] |
Examples:
| has_address | attributes |
| 1 | attributes |
| 0 | not attributes |
@Tiger
Scenario: Tiger address
When sending v1/reverse at 32.4752389363,-86.4810198619 with format geojson
Then results contain
| osm_type | osm_id | category | type | addresstype | place_rank |
| way | 396009653 | place | house | place | 30 |
Scenario: Interpolation address
When sending v1/reverse at 47.118533,9.57056562 with format geojson
Then results contain
| osm_type | osm_id | place_rank | category | type | addresstype |
| way | 1 | 30 | place | house | place |
And results contain
| boundingbox |
| [47.118495392, 47.118595392, 9.57049676, 9.57059676] |
And results contain
| display_name |
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
Scenario: Line geometry output is supported
When sending v1/reverse at 47.06597,9.50467 with format geojson
| param | value |
| polygon_geojson | 1 |
Then results contain in field geojson
| type |
| LineString |
Scenario Outline: Only geojson polygons are supported
When sending v1/reverse at 47.06597,9.50467 with format geojson
| param | value |
| <param> | 1 |
Then results contain in field geojson
| type |
| Point |
Examples:
| param |
| polygon_text |
| polygon_svg |
| polygon_kml |

View File

@@ -0,0 +1,129 @@
@APIDB
Feature: Json output for Reverse API
Testing correctness of json and jsonv2 output (API version v1).
Scenario Outline: OSM result with and without addresses
When sending v1/reverse at 47.066,9.504 with format json
| addressdetails |
| <has_address> |
Then result has <attributes> address
When sending v1/reverse at 47.066,9.504 with format jsonv2
| addressdetails |
| <has_address> |
Then result has <attributes> address
Examples:
| has_address | attributes |
| 1 | attributes |
| 0 | not attributes |
Scenario Outline: Siple OSM result
When sending v1/reverse at 47.066,9.504 with format <format>
Then result has attributes place_id
And results contain
| licence |
| Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright |
And results contain
| osm_type | osm_id |
| node | 6522627624 |
And results contain
| centroid | boundingbox |
| 9.5036065 47.0660892 | ['47.0660392', '47.0661392', '9.5035565', '9.5036565'] |
And results contain
| display_name |
| Dorfbäckerei Herrmann, 29, Gnetsch, Mäls, Balzers, Oberland, 9496, Liechtenstein |
And result has not attributes namedetails,extratags
Examples:
| format |
| json |
| jsonv2 |
Scenario: Extra attributes of jsonv2 result
When sending v1/reverse at 47.066,9.504 with format jsonv2
Then result has attributes importance
Then results contain
| category | type | name | place_rank | addresstype |
| shop | bakery | Dorfbäckerei Herrmann | 30 | shop |
@Tiger
Scenario: Tiger address
When sending v1/reverse at 32.4752389363,-86.4810198619 with format jsonv2
Then results contain
| osm_type | osm_id | category | type | addresstype |
| way | 396009653 | place | house | place |
Scenario Outline: Interpolation address
When sending v1/reverse at 47.118533,9.57056562 with format <format>
Then results contain
| osm_type | osm_id |
| way | 1 |
And results contain
| centroid | boundingbox |
| 9.57054676 47.118545392 | ['47.118495392', '47.118595392', '9.57049676', '9.57059676'] |
And results contain
| display_name |
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
Examples:
| format |
| json |
| jsonv2 |
Scenario Outline: Output of geojson
When sending v1/reverse at 47.06597,9.50467 with format <format>
| param | value |
| polygon_geojson | 1 |
Then results contain in field geojson
| type | coordinates |
| LineString | [[9.5039353, 47.0657546], [9.5040437, 47.0657781], [9.5040808, 47.065787], [9.5054298, 47.0661407]] |
Examples:
| format |
| json |
| jsonv2 |
Scenario Outline: Output of WKT
When sending v1/reverse at 47.06597,9.50467 with format <format>
| param | value |
| polygon_text | 1 |
Then results contain
| geotext |
| LINESTRING(9.5039353 47.0657546,9.5040437 47.0657781,9.5040808 47.065787,9.5054298 47.0661407) |
Examples:
| format |
| json |
| jsonv2 |
Scenario Outline: Output of SVG
When sending v1/reverse at 47.06597,9.50467 with format <format>
| param | value |
| polygon_svg | 1 |
Then results contain
| svg |
| M 9.5039353 -47.0657546 L 9.5040437 -47.0657781 9.5040808 -47.065787 9.5054298 -47.0661407 |
Examples:
| format |
| json |
| jsonv2 |
Scenario Outline: Output of KML
When sending v1/reverse at 47.06597,9.50467 with format <format>
| param | value |
| polygon_kml | 1 |
Then results contain
| geokml |
| ^<LineString><coordinates>9.5039\d*,47.0657\d* 9.5040\d*,47.0657\d* 9.5040\d*,47.065\d* 9.5054\d*,47.0661\d*</coordinates></LineString> |
Examples:
| format |
| json |
| jsonv2 |

View File

@@ -0,0 +1,220 @@
@APIDB
Feature: v1/reverse Parameter Tests
Tests for parameter inputs for the v1 reverse endpoint.
This file contains mostly bad parameter input. Valid parameters
are tested in the format tests.
Scenario: Bad format
When sending v1/reverse at 47.14122383,9.52169581334 with format sdf
Then a HTTP 400 is returned
Scenario: Missing lon parameter
When sending v1/reverse at 52.52,
Then a HTTP 400 is returned
Scenario: Missing lat parameter
When sending v1/reverse at ,52.52
Then a HTTP 400 is returned
@v1-api-php-only
Scenario: Missing osm_id parameter
When sending v1/reverse at ,
| osm_type |
| N |
Then a HTTP 400 is returned
@v1-api-php-only
Scenario: Missing osm_type parameter
When sending v1/reverse at ,
| osm_id |
| 3498564 |
Then a HTTP 400 is returned
Scenario Outline: Bad format for lat or lon
When sending v1/reverse at ,
| lat | lon |
| <lat> | <lon> |
Then a HTTP 400 is returned
Examples:
| lat | lon |
| 48.9660 | 8,4482 |
| 48,9660 | 8.4482 |
| 48,9660 | 8,4482 |
| 48.966.0 | 8.4482 |
| 48.966 | 8.448.2 |
| Nan | 8.448 |
| 48.966 | Nan |
| Inf | 5.6 |
| 5.6 | -Inf |
| <script></script> | 3.4 |
| 3.4 | <script></script> |
| -45.3 | ; |
| gkjd | 50 |
Scenario: Non-numerical zoom levels return an error
When sending v1/reverse at 47.14122383,9.52169581334
| zoom |
| adfe |
Then a HTTP 400 is returned
Scenario Outline: Truthy values for boolean parameters
When sending v1/reverse at 47.14122383,9.52169581334
| addressdetails |
| <value> |
Then exactly 1 result is returned
And result has attributes address
When sending v1/reverse at 47.14122383,9.52169581334
| extratags |
| <value> |
Then exactly 1 result is returned
And result has attributes extratags
When sending v1/reverse at 47.14122383,9.52169581334
| namedetails |
| <value> |
Then exactly 1 result is returned
And result has attributes namedetails
When sending v1/reverse at 47.14122383,9.52169581334
| polygon_geojson |
| <value> |
Then exactly 1 result is returned
And result has attributes geojson
When sending v1/reverse at 47.14122383,9.52169581334
| polygon_kml |
| <value> |
Then exactly 1 result is returned
And result has attributes geokml
When sending v1/reverse at 47.14122383,9.52169581334
| polygon_svg |
| <value> |
Then exactly 1 result is returned
And result has attributes svg
When sending v1/reverse at 47.14122383,9.52169581334
| polygon_text |
| <value> |
Then exactly 1 result is returned
And result has attributes geotext
Examples:
| value |
| yes |
| no |
| -1 |
| 100 |
| false |
| 00 |
Scenario: Only one geometry can be requested
When sending v1/reverse at 47.165989816710066,9.515774846076965
| polygon_text | polygon_svg |
| 1 | 1 |
Then a HTTP 400 is returned
Scenario Outline: Wrapping of legal jsonp requests
When sending v1/reverse at 67.3245,0.456 with format <format>
| json_callback |
| foo |
Then the result is valid <outformat>
Examples:
| format | outformat |
| json | json |
| jsonv2 | json |
| geojson | geojson |
| geocodejson | geocodejson |
Scenario Outline: Illegal jsonp are not allowed
When sending v1/reverse at 47.165989816710066,9.515774846076965
| param | value |
|json_callback | <data> |
Then a HTTP 400 is returned
Examples:
| data |
| 1asd |
| bar(foo) |
| XXX['bad'] |
| foo; evil |
@v1-api-python-only
Scenario Outline: Reverse debug mode produces valid HTML
When sending v1/reverse at , with format debug
| lat | lon |
| <lat> | <lon> |
Then the result is valid html
Examples:
| lat | lon |
| 0.0 | 0.0 |
| 47.06645 | 9.56601 |
| 47.14081 | 9.52267 |
Scenario Outline: Full address display for city housenumber-level address with street
When sending v1/reverse at 47.1068011,9.52810091 with format <format>
Then address of result 0 is
| type | value |
| house_number | 8 |
| road | Im Winkel |
| neighbourhood | Oberdorf |
| village | Triesen |
| ISO3166-2-lvl8 | LI-09 |
| county | Oberland |
| postcode | 9495 |
| country | Liechtenstein |
| country_code | li |
Examples:
| format |
| json |
| jsonv2 |
| geojson |
| xml |
Scenario Outline: Results with name details
When sending v1/reverse at 47.14052,9.52202 with format <format>
| zoom | namedetails |
| 14 | 1 |
Then results contain in field namedetails
| name |
| Ebenholz |
Examples:
| format |
| json |
| jsonv2 |
| xml |
| geojson |
Scenario Outline: Results with extratags
When sending v1/reverse at 47.14052,9.52202 with format <format>
| zoom | extratags |
| 14 | 1 |
Then results contain in field extratags
| wikidata |
| Q4529531 |
Examples:
| format |
| json |
| jsonv2 |
| xml |
| geojson |

View File

@@ -0,0 +1,87 @@
@APIDB
Feature: XML output for Reverse API
Testing correctness of xml output (API version v1).
Scenario Outline: OSM result with and without addresses
When sending v1/reverse at 47.066,9.504 with format xml
| addressdetails |
| <has_address> |
Then result has attributes place_id
Then result has <attributes> address
And results contain
| osm_type | osm_id | place_rank | address_rank |
| node | 6522627624 | 30 | 30 |
And results contain
| centroid | boundingbox |
| 9.5036065 47.0660892 | 47.0660392,47.0661392,9.5035565,9.5036565 |
And results contain
| ref | display_name |
| Dorfbäckerei Herrmann | Dorfbäckerei Herrmann, 29, Gnetsch, Mäls, Balzers, Oberland, 9496, Liechtenstein |
Examples:
| has_address | attributes |
| 1 | attributes |
| 0 | not attributes |
@Tiger
Scenario: Tiger address
When sending v1/reverse at 32.4752389363,-86.4810198619 with format xml
Then results contain
| osm_type | osm_id | place_rank | address_rank |
| way | 396009653 | 30 | 30 |
And results contain
| centroid | boundingbox |
| -86.4808553258 32.4753580256 | ^32.475308025\d*,32.475408025\d*,-86.480905325\d*,-86.480805325\d* |
And results contain
| display_name |
| 707, Upper Kingston Road, Upper Kingston, Prattville, Autauga County, 36067, United States |
Scenario: Interpolation address
When sending v1/reverse at 47.118533,9.57056562 with format xml
Then results contain
| osm_type | osm_id | place_rank | address_rank |
| way | 1 | 30 | 30 |
And results contain
| centroid | boundingbox |
| 9.57054676 47.118545392 | 47.118495392,47.118595392,9.57049676,9.57059676 |
And results contain
| display_name |
| 1019, Grosssteg, Sücka, Triesenberg, Oberland, 9497, Liechtenstein |
Scenario: Output of geojson
When sending v1/reverse at 47.06597,9.50467 with format xml
| param | value |
| polygon_geojson | 1 |
Then results contain
| geojson |
| {"type":"LineString","coordinates":[[9.5039353,47.0657546],[9.5040437,47.0657781],[9.5040808,47.065787],[9.5054298,47.0661407]]} |
Scenario: Output of WKT
When sending v1/reverse at 47.06597,9.50467 with format xml
| param | value |
| polygon_text | 1 |
Then results contain
| geotext |
| LINESTRING(9.5039353 47.0657546,9.5040437 47.0657781,9.5040808 47.065787,9.5054298 47.0661407) |
Scenario: Output of SVG
When sending v1/reverse at 47.06597,9.50467 with format xml
| param | value |
| polygon_svg | 1 |
Then results contain
| geosvg |
| M 9.5039353 -47.0657546 L 9.5040437 -47.0657781 9.5040808 -47.065787 9.5054298 -47.0661407 |
Scenario: Output of KML
When sending v1/reverse at 47.06597,9.50467 with format xml
| param | value |
| polygon_kml | 1 |
Then results contain
| geokml |
| ^<geokml><LineString><coordinates>9.5039\d*,47.0657\d* 9.5040\d*,47.0657\d* 9.5040\d*,47.065\d* 9.5054\d*,47.0661\d*</coordinates></LineString></geokml> |

View File

@@ -273,12 +273,12 @@ Feature: Import of address interpolations
| W3 | 14 | 14 | | W3 | 14 | 14 |
When sending search query "16 Cloud Street" When sending search query "16 Cloud Street"
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | N | 4 | | 0 | N4 |
When sending search query "14 Cloud Street" When sending search query "14 Cloud Street"
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | W | 11 | | 0 | W11 |
Scenario: addr:street on housenumber way Scenario: addr:street on housenumber way
Given the grid Given the grid
@@ -318,12 +318,12 @@ Feature: Import of address interpolations
| W3 | 14 | 14 | | W3 | 14 | 14 |
When sending search query "16 Cloud Street" When sending search query "16 Cloud Street"
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | N | 4 | | 0 | N4 |
When sending search query "14 Cloud Street" When sending search query "14 Cloud Street"
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | W | 11 | | 0 | W11 |
Scenario: Geometry of points and way don't match (github #253) Scenario: Geometry of points and way don't match (github #253)
Given the places Given the places
@@ -399,10 +399,10 @@ Feature: Import of address interpolations
Then W1 expands to interpolation Then W1 expands to interpolation
| start | end | geometry | | start | end | geometry |
| 2 | 8 | 10,11 | | 2 | 8 | 10,11 |
When sending jsonv2 reverse coordinates 1,1 When sending v1/reverse at 1,1
Then results contain Then results contain
| ID | osm_type | osm_id | type | display_name | | ID | osm | type | display_name |
| 0 | node | 1 | house | 0 | | 0 | N1 | house | 0 |
Scenario: Parenting of interpolation with additional tags Scenario: Parenting of interpolation with additional tags
Given the grid Given the grid

View File

@@ -55,8 +55,8 @@ Feature: Linking of places
| R23 | - | | R23 | - |
When sending search query "rhein" When sending search query "rhein"
Then results contain Then results contain
| osm_type | | osm |
| R | | R13 |
Scenario: Relations are not linked when in waterway relations Scenario: Relations are not linked when in waterway relations
Given the grid Given the grid
@@ -81,9 +81,9 @@ Feature: Linking of places
| R2 | - | | R2 | - |
When sending search query "rhein" When sending search query "rhein"
Then results contain Then results contain
| ID | osm_type | | ID | osm |
| 0 | R | | 0 | R1 |
| 1 | W | | 1 | W2 |
Scenario: Empty waterway relations are handled correctly Scenario: Empty waterway relations are handled correctly
@@ -138,8 +138,8 @@ Feature: Linking of places
| W2 | R1 | | W2 | R1 |
When sending search query "rhein2" When sending search query "rhein2"
Then results contain Then results contain
| osm_type | | osm |
| W | | W1 |
# github #573 # github #573
Scenario: Boundaries should only be linked to places Scenario: Boundaries should only be linked to places
@@ -205,14 +205,14 @@ Feature: Linking of places
| city | | city |
| Berlin | | Berlin |
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | R | 13 | | 0 | R13 |
When sending search query "" When sending search query ""
| state | | state |
| Berlin | | Berlin |
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | R | 13 | | 0 | R13 |
Scenario: Boundaries without place tags only link against same admin level Scenario: Boundaries without place tags only link against same admin level
@@ -237,14 +237,14 @@ Feature: Linking of places
| state | | state |
| Berlin | | Berlin |
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | R | 13 | | 0 | R13 |
When sending search query "" When sending search query ""
| city | | city |
| Berlin | | Berlin |
Then results contain Then results contain
| ID | osm_type | osm_id | | ID | osm |
| 0 | N | 2 | | 0 | N2 |
# github #1352 # github #1352
Scenario: Do not use linked centroid when it is outside the area Scenario: Do not use linked centroid when it is outside the area

View File

@@ -23,12 +23,12 @@ Feature: Parenting of objects
| N2 | W1 | | N2 | W1 |
When sending search query "4 galoo" When sending search query "4 galoo"
Then results contain Then results contain
| ID | osm_type | osm_id | display_name | | ID | osm | display_name |
| 0 | N | 1 | 4, galoo, 12345, Deutschland | | 0 | N1 | 4, galoo, 12345, Deutschland |
When sending search query "5 galoo" When sending search query "5 galoo"
Then results contain Then results contain
| ID | osm_type | osm_id | display_name | | ID | osm | display_name |
| 0 | N | 2 | 5, galoo, 99999, Deutschland | | 0 | N2 | 5, galoo, 99999, Deutschland |
Scenario: Address without tags, closest street Scenario: Address without tags, closest street
Given the grid Given the grid

View File

@@ -23,7 +23,7 @@ Feature: Query of address interpolations
| id | nodes | | id | nodes |
| 1 | 1,3 | | 1 | 1,3 |
When importing When importing
When sending jsonv2 reverse point 2 When sending v1/reverse N2
Then results contain Then results contain
| ID | display_name | | ID | display_name |
| 0 | 3, Nickway | | 0 | 3, Nickway |
@@ -43,12 +43,12 @@ Feature: Query of address interpolations
And the places And the places
| osm | class | type | housenr | geometry | | osm | class | type | housenr | geometry |
| N1 | place | house | 2 | 1 | | N1 | place | house | 2 | 1 |
| N3 | place | house | 16 | 3 | | N3 | place | house | 18 | 3 |
And the ways And the ways
| id | nodes | | id | nodes |
| 1 | 1,3 | | 1 | 1,3 |
When importing When importing
When sending jsonv2 reverse point 2 When sending v1/reverse N2
Then results contain Then results contain
| ID | display_name | centroid | | ID | display_name | centroid |
| 0 | 10, Nickway | 2 | | 0 | 10, Nickway | 2 |

View File

@@ -44,8 +44,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| R | | R1 |
When updating places When updating places
| osm | class | type | name | admin | geometry | | osm | class | type | name | admin | geometry |
| R1 | boundary | administrative | foobar | 8 | (10,11,12,13,10) | | R1 | boundary | administrative | foobar | 8 | (10,11,12,13,10) |
@@ -56,8 +56,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| N | | N1 |
Scenario: Add linked place when linking relation is removed Scenario: Add linked place when linking relation is removed
Given the 0.1 grid Given the 0.1 grid
@@ -75,8 +75,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| R | | R1 |
When marking for delete R1 When marking for delete R1
Then placex contains Then placex contains
| object | linked_place_id | | object | linked_place_id |
@@ -85,8 +85,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| N | | N1 |
Scenario: Remove linked place when linking relation is added Scenario: Remove linked place when linking relation is added
Given the 0.1 grid Given the 0.1 grid
@@ -101,8 +101,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| N | | N1 |
When updating places When updating places
| osm | class | type | name | admin | geometry | | osm | class | type | name | admin | geometry |
| R1 | boundary | administrative | foo | 8 | (10,11,12,13,10) | | R1 | boundary | administrative | foo | 8 | (10,11,12,13,10) |
@@ -113,8 +113,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| R | | R1 |
Scenario: Remove linked place when linking relation is renamed Scenario: Remove linked place when linking relation is renamed
Given the 0.1 grid Given the 0.1 grid
@@ -132,8 +132,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| N | | N1 |
When updating places When updating places
| osm | class | type | name | admin | geometry | | osm | class | type | name | admin | geometry |
| R1 | boundary | administrative | foo | 8 | (10,11,12,13,10) | | R1 | boundary | administrative | foo | 8 | (10,11,12,13,10) |
@@ -144,8 +144,8 @@ Feature: Updates of linked places
| dups | | dups |
| 1 | | 1 |
Then results contain Then results contain
| osm_type | | osm |
| R | | R1 |
Scenario: Update linking relation when linkee name is updated Scenario: Update linking relation when linkee name is updated
Given the 0.1 grid Given the 0.1 grid

View File

@@ -2,11 +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 math
import re
class Almost: class Almost:
""" Compares a float value with a certain jitter. """ Compares a float value with a certain jitter.
@@ -18,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.
""" """
@@ -41,3 +89,24 @@ class Bbox:
def __str__(self): def __str__(self):
return str(self.coord) return str(self.coord)
def check_for_attributes(obj, attrs, presence='present'):
""" Check that the object has the given attributes. 'attrs' is a
string with a comma-separated list of attributes. If 'presence'
is set to 'absent' then the function checks that the attributes do
not exist for the object
"""
def _dump_json():
return json.dumps(obj, sort_keys=True, indent=2, ensure_ascii=False)
for attr in attrs.split(','):
attr = attr.strip()
if presence == 'absent':
assert attr not in obj, \
f"Unexpected attribute {attr}. Full response:\n{_dump_json()}"
else:
assert attr in obj, \
f"No attribute '{attr}'. Full response:\n{_dump_json()}"

View File

@@ -7,43 +7,11 @@
""" """
Classes wrapping HTTP responses from the Nominatim API. Classes wrapping HTTP responses from the Nominatim API.
""" """
from collections import OrderedDict
import re import re
import json import json
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from check_functions import Almost 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:
@@ -70,63 +38,54 @@ class GenericResponse:
else: else:
code = m.group(2) code = m.group(2)
self.header['json_func'] = m.group(1) self.header['json_func'] = m.group(1)
self.result = json.JSONDecoder(object_pairs_hook=OrderedDict).decode(code) self.result = json.JSONDecoder().decode(code)
if isinstance(self.result, OrderedDict): if isinstance(self.result, dict):
if 'error' in self.result: if 'error' in self.result:
self.result = [] self.result = []
else: else:
self.result = [self.result] self.result = [self.result]
def _parse_geojson(self): def _parse_geojson(self):
self._parse_json() self._parse_json()
if self.result: if self.result:
self.result = list(map(_geojson_result_to_json_result, self.result[0]['features'])) geojson = self.result[0]
# check for valid geojson
check_for_attributes(geojson, 'type,features')
assert geojson['type'] == 'FeatureCollection'
assert isinstance(geojson['features'], list)
self.result = []
for result in geojson['features']:
check_for_attributes(result, 'type,properties,geometry')
assert result['type'] == 'Feature'
new = result['properties']
check_for_attributes(new, 'geojson', 'absent')
new['geojson'] = result['geometry']
if 'bbox' in result:
check_for_attributes(new, 'boundingbox', 'absent')
# bbox is minlon, minlat, maxlon, maxlat
# boundingbox is minlat, maxlat, minlon, maxlon
new['boundingbox'] = [result['bbox'][1],
result['bbox'][3],
result['bbox'][0],
result['bbox'][2]]
for k, v in geojson.items():
if k not in ('type', 'features'):
check_for_attributes(new, '__' + k, 'absent')
new['__' + k] = v
self.result.append(new)
def _parse_geocodejson(self): def _parse_geocodejson(self):
self._parse_geojson() self._parse_geojson()
if self.result is not None: if self.result:
self.result = [r['geocoding'] for r in self.result] for r in self.result:
assert set(r.keys()) == {'geocoding', 'geojson', '__geocoding'}, \
def assert_field(self, idx, field, value): f"Unexpected keys in result: {r.keys()}"
""" Check that result row `idx` has a field `field` with value `value`. check_for_attributes(r['geocoding'], 'geojson', 'absent')
Float numbers are matched approximately. When the expected value inner = r.pop('geocoding')
starts with a carat, regular expression matching is used. r.update(inner)
"""
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], OrderedDict):
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):
assert path
field = self.result[idx]
for p in path:
assert isinstance(field, OrderedDict)
assert p in field
field = field[p]
if isinstance(value, float):
assert Almost(value) == float(field)
elif value.startswith("^"):
assert re.fullmatch(value, field)
elif isinstance(field, OrderedDict):
assert field, eval('{' + value + '}')
else:
assert str(field) == str(value)
def assert_address_field(self, idx, field, value): def assert_address_field(self, idx, field, value):
@@ -139,20 +98,13 @@ 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, field=None):
""" Match the result fields against the given behave table row. """ Match the result fields against the given behave table row.
""" """
if 'ID' in row.headings: if 'ID' in row.headings:
@@ -161,19 +113,20 @@ class GenericResponse:
todo = range(len(self.result)) todo = range(len(self.result))
for i in todo: for i in todo:
subdict = self.result[i]
if field is not None:
for key in field.split('.'):
self.check_row(i, key in subdict, f"Missing subfield {key}")
subdict = subdict[key]
self.check_row(i, isinstance(subdict, dict),
f"Subfield {key} not a dict")
for name, value in zip(row.headings, row.cells): for name, value in zip(row.headings, row.cells):
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]), base=subdict)
"Result row {} has no field 'osm_type'.\nFull row: {}"\ self.check_row_field(i, 'osm_id', Field(value[1:]), base=subdict)
.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':
assert self.result[i]['osm_type'] in (OSM_TYPE[value[0]], 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(' ')
@@ -181,15 +134,43 @@ 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:
self.assert_subfield(i, name.split('+'), value)
else: else:
self.assert_field(i, name, value) self.check_row_field(i, name, Field(value), base=subdict)
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}")
def property_list(self, prop):
return [x[prop] for x in self.result]
class SearchResponse(GenericResponse): class SearchResponse(GenericResponse):
@@ -240,24 +221,33 @@ class ReverseResponse(GenericResponse):
if child.tag == 'result': if child.tag == 'result':
assert not self.result, "More than one result in reverse result" assert not self.result, "More than one result in reverse result"
self.result.append(dict(child.attrib)) self.result.append(dict(child.attrib))
check_for_attributes(self.result[0], 'display_name', 'absent')
self.result[0]['display_name'] = child.text
elif child.tag == 'addressparts': elif child.tag == 'addressparts':
assert 'address' not in self.result[0], "More than one address in result"
address = {} address = {}
for sub in child: for sub in child:
assert len(sub) == 0, f"Address element '{sub.tag}' has subelements"
address[sub.tag] = sub.text address[sub.tag] = sub.text
self.result[0]['address'] = address self.result[0]['address'] = address
elif child.tag == 'extratags': elif child.tag == 'extratags':
assert 'extratags' not in self.result[0], "More than one extratags in result"
self.result[0]['extratags'] = {} self.result[0]['extratags'] = {}
for tag in child: for tag in child:
assert len(tag) == 0, f"Extratags element '{tag.attrib['key']}' has subelements"
self.result[0]['extratags'][tag.attrib['key']] = tag.attrib['value'] self.result[0]['extratags'][tag.attrib['key']] = tag.attrib['value']
elif child.tag == 'namedetails': elif child.tag == 'namedetails':
assert 'namedetails' not in self.result[0], "More than one namedetails in result"
self.result[0]['namedetails'] = {} self.result[0]['namedetails'] = {}
for tag in child: for tag in child:
assert len(tag) == 0, f"Namedetails element '{tag.attrib['desc']}' has subelements"
self.result[0]['namedetails'][tag.attrib['desc']] = tag.text self.result[0]['namedetails'][tag.attrib['desc']] = tag.text
elif child.tag == 'geokml': elif child.tag == 'geokml':
self.result[0][child.tag] = True assert 'geokml' not in self.result[0], "More than one geokml in result"
self.result[0]['geokml'] = ET.tostring(child, encoding='unicode')
else: else:
assert child.tag == 'error', \ assert child.tag == 'error', \
"Unknown XML tag {} on page: {}".format(child.tag, self.page) f"Unknown XML tag {child.tag} on page: {self.page}"
class StatusResponse(GenericResponse): class StatusResponse(GenericResponse):

View File

@@ -15,11 +15,12 @@ import os
import re import re
import logging import logging
import asyncio import asyncio
import xml.etree.ElementTree as ET
from urllib.parse import urlencode from urllib.parse import urlencode
from utils import run_script from utils import run_script
from http_responses import GenericResponse, SearchResponse, ReverseResponse, StatusResponse from http_responses import GenericResponse, SearchResponse, ReverseResponse, StatusResponse
from check_functions import Bbox from check_functions import Bbox, check_for_attributes
from table_compare import NominatimID from table_compare import NominatimID
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -48,6 +49,15 @@ BASE_SERVER_ENV = {
} }
def make_todo_list(context, result_id):
if result_id is None:
context.execute_steps("then at least 1 result is returned")
return range(len(context.response.result))
context.execute_steps(f"then more than {result_id}results are returned")
return (int(result_id.strip()), )
def compare(operator, op1, op2): def compare(operator, op1, op2):
if operator == 'less than': if operator == 'less than':
return op1 < op2 return op1 < op2
@@ -60,12 +70,16 @@ def compare(operator, op1, op2):
elif operator == 'at most': elif operator == 'at most':
return op1 <= op2 return op1 <= op2
else: else:
raise Exception("unknown operator '%s'" % operator) raise ValueError(f"Unknown operator '{operator}'")
def send_api_query(endpoint, params, fmt, context): def send_api_query(endpoint, params, fmt, context):
if fmt is not None and fmt.strip() != 'debug': if fmt is not None:
if fmt.strip() == 'debug':
params['debug'] = '1'
else:
params['format'] = fmt.strip() params['format'] = fmt.strip()
if context.table: if context.table:
if context.table.headings[0] == 'param': if context.table.headings[0] == 'param':
for line in context.table: for line in context.table:
@@ -88,11 +102,11 @@ def send_api_query_php(endpoint, params, context):
env = dict(BASE_SERVER_ENV) env = dict(BASE_SERVER_ENV)
env['QUERY_STRING'] = urlencode(params) env['QUERY_STRING'] = urlencode(params)
env['SCRIPT_NAME'] = '/%s.php' % endpoint env['SCRIPT_NAME'] = f'/{endpoint}.php'
env['REQUEST_URI'] = '%s?%s' % (env['SCRIPT_NAME'], env['QUERY_STRING']) env['REQUEST_URI'] = f"{env['SCRIPT_NAME']}?{env['QUERY_STRING']}"
env['CONTEXT_DOCUMENT_ROOT'] = os.path.join(context.nominatim.website_dir.name, 'website') env['CONTEXT_DOCUMENT_ROOT'] = os.path.join(context.nominatim.website_dir.name, 'website')
env['SCRIPT_FILENAME'] = os.path.join(env['CONTEXT_DOCUMENT_ROOT'], env['SCRIPT_FILENAME'] = os.path.join(env['CONTEXT_DOCUMENT_ROOT'],
'%s.php' % endpoint) f'{endpoint}.php')
LOG.debug("Environment:" + json.dumps(env, sort_keys=True, indent=2)) LOG.debug("Environment:" + json.dumps(env, sort_keys=True, indent=2))
@@ -104,7 +118,7 @@ def send_api_query_php(endpoint, params, context):
env['XDEBUG_MODE'] = 'coverage' env['XDEBUG_MODE'] = 'coverage'
env['COV_SCRIPT_FILENAME'] = env['SCRIPT_FILENAME'] env['COV_SCRIPT_FILENAME'] = env['SCRIPT_FILENAME']
env['COV_PHP_DIR'] = context.nominatim.src_dir env['COV_PHP_DIR'] = context.nominatim.src_dir
env['COV_TEST_NAME'] = '%s:%s' % (context.scenario.filename, context.scenario.line) env['COV_TEST_NAME'] = f"{context.scenario.filename}:{context.scenario.line}"
env['SCRIPT_FILENAME'] = \ env['SCRIPT_FILENAME'] = \
os.path.join(os.path.split(__file__)[0], 'cgi-with-coverage.php') os.path.join(os.path.split(__file__)[0], 'cgi-with-coverage.php')
cmd.append(env['SCRIPT_FILENAME']) cmd.append(env['SCRIPT_FILENAME'])
@@ -113,11 +127,11 @@ def send_api_query_php(endpoint, params, context):
cmd.append(env['SCRIPT_FILENAME']) cmd.append(env['SCRIPT_FILENAME'])
for k,v in params.items(): for k,v in params.items():
cmd.append("%s=%s" % (k, v)) cmd.append(f"{k}={v}")
outp, err = run_script(cmd, cwd=context.nominatim.website_dir.name, env=env) outp, err = run_script(cmd, cwd=context.nominatim.website_dir.name, env=env)
assert len(err) == 0, "Unexpected PHP error: %s" % (err) assert len(err) == 0, f"Unexpected PHP error: {err}"
if outp.startswith('Status: '): if outp.startswith('Status: '):
status = int(outp[8:11]) status = int(outp[8:11])
@@ -145,41 +159,37 @@ def website_search_request(context, fmt, query, addr):
params['q'] = query params['q'] = query
if addr is not None: if addr is not None:
params['addressdetails'] = '1' params['addressdetails'] = '1'
if fmt and fmt.strip() == 'debug':
params['debug'] = '1'
outp, status = send_api_query('search', params, fmt, context) outp, status = send_api_query('search', params, fmt, context)
context.response = SearchResponse(outp, fmt or 'json', status) context.response = SearchResponse(outp, fmt or 'json', status)
@when(u'sending (?P<fmt>\S+ )?reverse coordinates (?P<lat>.+)?,(?P<lon>.+)?')
def website_reverse_request(context, fmt, lat, lon): @when('sending v1/reverse at (?P<lat>[\d.-]*),(?P<lon>[\d.-]*)(?: with format (?P<fmt>.+))?')
def api_endpoint_v1_reverse(context, lat, lon, fmt):
params = {} params = {}
if lat is not None: if lat is not None:
params['lat'] = lat params['lat'] = lat
if lon is not None: if lon is not None:
params['lon'] = lon params['lon'] = lon
if fmt and fmt.strip() == 'debug': if fmt is None:
params['debug'] = '1' fmt = 'jsonv2'
elif fmt == "''":
fmt = None
outp, status = send_api_query('reverse', params, fmt, context) outp, status = send_api_query('reverse', params, fmt, context)
context.response = ReverseResponse(outp, fmt or 'xml', status) context.response = ReverseResponse(outp, fmt or 'xml', status)
@when(u'sending (?P<fmt>\S+ )?reverse point (?P<nodeid>.+)')
def website_reverse_request(context, fmt, nodeid): @when('sending v1/reverse N(?P<nodeid>\d+)(?: with format (?P<fmt>.+))?')
def api_endpoint_v1_reverse_from_node(context, nodeid, fmt):
params = {} params = {}
if fmt and fmt.strip() == 'debug':
params['debug'] = '1'
params['lon'], params['lat'] = (f'{c:f}' for c in context.osm.grid_node(int(nodeid))) params['lon'], params['lat'] = (f'{c:f}' for c in context.osm.grid_node(int(nodeid)))
outp, status = send_api_query('reverse', params, fmt, context) outp, status = send_api_query('reverse', params, fmt, context)
context.response = ReverseResponse(outp, fmt or 'xml', status) context.response = ReverseResponse(outp, fmt or 'xml', status)
@when(u'sending (?P<fmt>\S+ )?details query for (?P<query>.*)') @when(u'sending (?P<fmt>\S+ )?details query for (?P<query>.*)')
def website_details_request(context, fmt, query): def website_details_request(context, fmt, query):
params = {} params = {}
@@ -211,15 +221,15 @@ def website_status_request(context, fmt):
@step(u'(?P<operator>less than|more than|exactly|at least|at most) (?P<number>\d+) results? (?:is|are) returned') @step(u'(?P<operator>less than|more than|exactly|at least|at most) (?P<number>\d+) results? (?:is|are) returned')
def validate_result_number(context, operator, number): def validate_result_number(context, operator, number):
assert context.response.errorcode == 200 context.execute_steps("Then a HTTP 200 is returned")
numres = len(context.response.result) numres = len(context.response.result)
assert compare(operator, numres, int(number)), \ assert compare(operator, numres, int(number)), \
"Bad number of results: expected {} {}, got {}.".format(operator, number, numres) f"Bad number of results: expected {operator} {number}, got {numres}."
@then(u'a HTTP (?P<status>\d+) is returned') @then(u'a HTTP (?P<status>\d+) is returned')
def check_http_return_status(context, status): def check_http_return_status(context, status):
assert context.response.errorcode == int(status), \ assert context.response.errorcode == int(status), \
"Return HTTP status is {}.".format(context.response.errorcode) f"Return HTTP status is {context.response.errorcode}."
@then(u'the page contents equals "(?P<text>.+)"') @then(u'the page contents equals "(?P<text>.+)"')
def check_page_content_equals(context, text): def check_page_content_equals(context, text):
@@ -228,8 +238,20 @@ def check_page_content_equals(context, text):
@then(u'the result is valid (?P<fmt>\w+)') @then(u'the result is valid (?P<fmt>\w+)')
def step_impl(context, fmt): def step_impl(context, fmt):
context.execute_steps("Then a HTTP 200 is returned") context.execute_steps("Then a HTTP 200 is returned")
if fmt.strip() == 'html':
try:
tree = ET.fromstring(context.response.page)
except Exception as ex:
assert False, f"Could not parse page:\n{context.response.page}"
assert tree.tag == 'html'
body = tree.find('./body')
assert body is not None
assert body.find('.//script') is None
else:
assert context.response.format == fmt assert context.response.format == fmt
@then(u'a (?P<fmt>\w+) user error is returned') @then(u'a (?P<fmt>\w+) user error is returned')
def check_page_error(context, fmt): def check_page_error(context, fmt):
context.execute_steps("Then a HTTP 400 is returned") context.execute_steps("Then a HTTP 400 is returned")
@@ -243,49 +265,31 @@ def check_page_error(context, fmt):
@then(u'result header contains') @then(u'result header contains')
def check_header_attr(context): def check_header_attr(context):
for line in context.table: for line in context.table:
assert re.fullmatch(line['value'], context.response.header[line['attr']]) is not None, \ value = context.response.header[line['attr']]
"attribute '%s': expected: '%s', got '%s'" % ( assert re.fullmatch(line['value'], value) is not None, \
line['attr'], line['value'], f"Attribute '{line['attr']}': expected: '{line['value']}', got '{value}'"
context.response.header[line['attr']])
@then(u'result header has (?P<neg>not )?attributes (?P<attrs>.*)') @then(u'result header has (?P<neg>not )?attributes (?P<attrs>.*)')
def check_header_no_attr(context, neg, attrs): def check_header_no_attr(context, neg, attrs):
for attr in attrs.split(','): check_for_attributes(context.response.header, attrs,
if neg: 'absent' if neg else 'present')
assert attr not in context.response.header, \
"Unexpected attribute {}. Full response:\n{}".format(
attr, json.dumps(context.response.header, sort_keys=True, indent=2))
else:
assert attr in context.response.header, \
"No attribute {}. Full response:\n{}".format(
attr, json.dumps(context.response.header, sort_keys=True, indent=2))
@then(u'results contain')
def step_impl(context): @then(u'results contain(?: in field (?P<field>.*))?')
def step_impl(context, field):
context.execute_steps("then at least 1 result is returned") context.execute_steps("then at least 1 result is returned")
for line in context.table: for line in context.table:
context.response.match_row(line, context=context) context.response.match_row(line, context=context, field=field)
@then(u'result (?P<lid>\d+ )?has (?P<neg>not )?attributes (?P<attrs>.*)') @then(u'result (?P<lid>\d+ )?has (?P<neg>not )?attributes (?P<attrs>.*)')
def validate_attributes(context, lid, neg, attrs): def validate_attributes(context, lid, neg, attrs):
if lid is None: for i in make_todo_list(context, lid):
idx = range(len(context.response.result)) check_for_attributes(context.response.result[i], attrs,
context.execute_steps("then at least 1 result is returned") 'absent' if neg else 'present')
else:
idx = [int(lid.strip())]
context.execute_steps("then more than %sresults are returned" % lid)
for i in idx:
for attr in attrs.split(','):
if neg:
assert attr not in context.response.result[i],\
"Unexpected attribute {}. Full response:\n{}".format(
attr, json.dumps(context.response.result[i], sort_keys=True, indent=2))
else:
assert attr in context.response.result[i], \
"No attribute {}. Full response:\n{}".format(
attr, json.dumps(context.response.result[i], sort_keys=True, indent=2))
@then(u'result addresses contain') @then(u'result addresses contain')
def step_impl(context): def step_impl(context):
@@ -300,7 +304,7 @@ def step_impl(context):
@then(u'address of result (?P<lid>\d+) has(?P<neg> no)? types (?P<attrs>.*)') @then(u'address of result (?P<lid>\d+) has(?P<neg> no)? types (?P<attrs>.*)')
def check_address(context, lid, neg, attrs): def check_address(context, lid, neg, attrs):
context.execute_steps("then more than %s results are returned" % lid) context.execute_steps(f"then more than {lid} results are returned")
addr_parts = context.response.result[int(lid)]['address'] addr_parts = context.response.result[int(lid)]['address']
@@ -312,7 +316,7 @@ def check_address(context, lid, neg, attrs):
@then(u'address of result (?P<lid>\d+) (?P<complete>is|contains)') @then(u'address of result (?P<lid>\d+) (?P<complete>is|contains)')
def check_address(context, lid, complete): def check_address(context, lid, complete):
context.execute_steps("then more than %s results are returned" % lid) context.execute_steps(f"then more than {lid} results are returned")
lid = int(lid) lid = int(lid)
addr_parts = dict(context.response.result[lid]['address']) addr_parts = dict(context.response.result[lid]['address'])
@@ -322,38 +326,30 @@ def check_address(context, lid, complete):
del addr_parts[line['type']] del addr_parts[line['type']]
if complete == 'is': if complete == 'is':
assert len(addr_parts) == 0, "Additional address parts found: %s" % str(addr_parts) assert len(addr_parts) == 0, f"Additional address parts found: {addr_parts!s}"
@then(u'result (?P<lid>\d+ )?has bounding box in (?P<coords>[\d,.-]+)') @then(u'result (?P<lid>\d+ )?has bounding box in (?P<coords>[\d,.-]+)')
def step_impl(context, lid, coords): def check_bounding_box_in_area(context, lid, coords):
if lid is None:
context.execute_steps("then at least 1 result is returned")
bboxes = context.response.property_list('boundingbox')
else:
context.execute_steps("then more than {}results are returned".format(lid))
bboxes = [context.response.result[int(lid)]['boundingbox']]
expected = Bbox(coords) expected = Bbox(coords)
for bbox in bboxes: for idx in make_todo_list(context, lid):
assert bbox in expected, "Bbox {} is not contained in {}.".format(bbox, expected) res = context.response.result[idx]
check_for_attributes(res, 'boundingbox')
context.response.check_row(idx, res['boundingbox'] in expected,
f"Bbox is not contained in {expected}")
@then(u'result (?P<lid>\d+ )?has centroid in (?P<coords>[\d,.-]+)') @then(u'result (?P<lid>\d+ )?has centroid in (?P<coords>[\d,.-]+)')
def step_impl(context, lid, coords): def check_centroid_in_area(context, lid, coords):
if lid is None:
context.execute_steps("then at least 1 result is returned")
centroids = zip(context.response.property_list('lon'),
context.response.property_list('lat'))
else:
context.execute_steps("then more than %sresults are returned".format(lid))
res = context.response.result[int(lid)]
centroids = [(res['lon'], res['lat'])]
expected = Bbox(coords) expected = Bbox(coords)
for centroid in centroids: for idx in make_todo_list(context, lid):
assert centroid in expected,\ res = context.response.result[idx]
"Centroid {} is not inside {}.".format(centroid, expected) check_for_attributes(res, 'lat,lon')
context.response.check_row(idx, (res['lon'], res['lat']) in expected,
f"Centroid is not inside {expected}")
@then(u'there are(?P<neg> no)? duplicates') @then(u'there are(?P<neg> no)? duplicates')
def check_for_duplicates(context, neg): def check_for_duplicates(context, neg):
@@ -370,6 +366,7 @@ def check_for_duplicates(context, neg):
resarr.add(dup) resarr.add(dup)
if neg: if neg:
assert not has_dupe, "Found duplicate for %s" % (dup, ) assert not has_dupe, f"Found duplicate for {dup}"
else: else:
assert has_dupe, "No duplicates found" assert has_dupe, "No duplicates found"