Unify tests

This commit is contained in:
Alexey Zakharenkov 2023-06-02 14:26:57 +03:00 committed by Alexey Zakharenkov
parent 97dc8901b4
commit ed95c94aac
15 changed files with 1085 additions and 170 deletions

View file

@ -673,7 +673,7 @@ class Route:
self.stops = [] # List of RouteStop
# Would be a list of (lon, lat) for the longest stretch. Can be empty.
self.tracks = None
# Index of the fist stop that is located on/near the self.tracks
# Index of the first stop that is located on/near the self.tracks
self.first_stop_on_rails_index = None
# Index of the last stop that is located on/near the self.tracks
self.last_stop_on_rails_index = None

13
tests/README.md Normal file
View file

@ -0,0 +1,13 @@
To perform tests manually, run this command from the top directory
of the repository:
```bash
python -m unittest discover tests
```
or simply
```bash
python -m unittest
```

View file

@ -1,28 +0,0 @@
{
"w38836456": {
"lat": 55.73064775,
"lon": 37.446065950000005
},
"w489951237": {
"lat": 55.730760724999996,
"lon": 37.44602055
},
"r7588527": {
"lat": 55.73066371666667,
"lon": 37.44604881666667
},
"r7588528": {
"lat": 55.73075192499999,
"lon": 37.44609837
},
"r7588561": {
"lat": 55.73070782083333,
"lon": 37.44607359333334
},
"r13426423": {
"lat": 55.730760724999996,
"lon": 37.44602055
},
"r100": null,
"r101": null
}

217
tests/assets/tiny_world.osm Normal file
View file

@ -0,0 +1,217 @@
<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' visible='true' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' visible='true' version='1' lat='0.00466930266' lon='0.00473815872'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='3' visible='true' version='1' lat='0.0097589171' lon='0.01012040581'>
<tag k='name' v='Station 3' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='4' visible='true' version='1' lat='0.01' lon='0.0'>
<tag k='name' v='Station 4' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='5' visible='true' version='1' lat='0.00514739839' lon='0.0047718624'>
<tag k='name' v='Station 5' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='6' visible='true' version='1' lat='0.0' lon='0.01'>
<tag k='name' v='Station 6' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='7' visible='true' version='1' lat='0.01028895507' lon='0.0097109364'>
<tag k='name' v='Station 7' />
<tag k='railway' v='station' />
<tag k='station' v='light_rail' />
</node>
<node id='8' visible='true' version='1' lat='0.01433266979' lon='0.01236209024'>
<tag k='name' v='Station 8' />
<tag k='railway' v='station' />
<tag k='station' v='light_rail' />
</node>
<node id='101' visible='true' version='1' lat='0.0047037307' lon='0.00470373068'>
<tag k='public_transport' v='stop_position' />
<tag k='subway' v='yes' />
</node>
<node id='102' visible='true' version='1' lat='0.01025306758' lon='0.00976752835'>
<tag k='railway' v='stop' />
</node>
<node id='103' visible='true' version='1' lat='0.01434446439' lon='0.01245616794'>
<tag k='public_transport' v='stop_position' />
</node>
<node id='104' visible='true' version='1' lat='0.01031966791' lon='0.00966618028'>
<tag k='railway' v='stop' />
</node>
<node id='105' visible='true' version='1' lat='0.01441106473' lon='0.01235481987'>
<tag k='public_transport' v='stop_position' />
</node>
<node id='1001' visible='true' version='1' lat='0.01' lon='0.01' />
<way id='1' visible='true' version='1'>
<nd ref='1' />
<nd ref='101' />
<nd ref='1001' />
<tag k='layer' v='-2' />
<tag k='railway' v='subway' />
<tag k='tunnel' v='yes' />
</way>
<way id='2' visible='true' version='1'>
<nd ref='4' />
<nd ref='6' />
<tag k='layer' v='-3' />
<tag k='railway' v='subway' />
<tag k='tunnel' v='yes' />
</way>
<way id='3' visible='true' version='1'>
<nd ref='102' />
<nd ref='103' />
<tag k='railway' v='light_rail' />
</way>
<way id='4' visible='true' version='1'>
<nd ref='104' />
<nd ref='105' />
<tag k='railway' v='light_rail' />
</way>
<relation id='1' visible='true' version='1'>
<member type='node' ref='101' role='' />
<member type='node' ref='2' role='' />
<tag k='public_transport' v='stop_area' />
<tag k='type' v='public_transport' />
</relation>
<relation id='2' visible='true' version='1'>
<member type='node' ref='5' role='' />
<tag k='public_transport' v='stop_area' />
<tag k='type' v='public_transport' />
</relation>
<relation id='3' visible='true' version='1'>
<member type='node' ref='3' role='' />
<tag k='public_transport' v='stop_area' />
<tag k='type' v='public_transport' />
</relation>
<relation id='4' visible='true' version='1'>
<member type='node' ref='7' role='' />
<member type='node' ref='102' role='' />
<member type='node' ref='104' role='' />
<tag k='public_transport' v='stop_area' />
<tag k='type' v='public_transport' />
</relation>
<relation id='5' visible='true' version='1'>
<member type='relation' ref='2' role='' />
<member type='relation' ref='1' role='' />
<tag k='public_transport' v='stop_area_group' />
<tag k='type' v='public_transport' />
</relation>
<relation id='6' visible='true' version='1'>
<member type='relation' ref='4' role='' />
<member type='relation' ref='3' role='' />
<tag k='public_transport' v='stop_area_group' />
<tag k='type' v='public_transport' />
</relation>
<relation id='7' visible='true' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='101' role='' />
<member type='node' ref='3' role='' />
<member type='way' ref='1' role='' />
<tag k='colour' v='#0000FF' />
<tag k='name' v='1 forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='8' visible='true' version='1'>
<member type='node' ref='3' role='' />
<member type='node' ref='101' role='' />
<member type='node' ref='1' role='' />
<member type='way' ref='1' role='' />
<tag k='colour' v='#0000FF' />
<tag k='name' v='1 backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='9' visible='true' version='1'>
<member type='node' ref='102' role='' />
<member type='node' ref='103' role='' />
<member type='way' ref='3' role='' />
<tag k='name' v='LR forward' />
<tag k='ref' v='LR' />
<tag k='route' v='light_rail' />
<tag k='type' v='route' />
</relation>
<relation id='10' visible='true' version='1'>
<member type='node' ref='8' role='' />
<member type='node' ref='104' role='' />
<member type='way' ref='4' role='' />
<tag k='name' v='LR backward' />
<tag k='ref' v='LR' />
<tag k='route' v='light_rail' />
<tag k='type' v='route' />
</relation>
<relation id='11' visible='true' version='1'>
<member type='relation' ref='10' role='' />
<member type='relation' ref='9' role='' />
<tag k='colour' v='brown' />
<tag k='colour:infill' v='white' />
<tag k='network' v='network-2' />
<tag k='ref' v='LR' />
<tag k='name' v='LR Line' />
<tag k='route_master' v='light_rail' />
<tag k='type' v='route_master' />
</relation>
<relation id='12' visible='true' version='1'>
<member type='node' ref='4' role='' />
<member type='node' ref='5' role='' />
<member type='node' ref='6' role='' />
<member type='way' ref='2' role='' />
<tag k='name' v='2 forward' />
<tag k='ref' v='2' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='13' visible='true' version='1'>
<member type='node' ref='6' role='' />
<member type='node' ref='5' role='' />
<member type='node' ref='4' role='' />
<member type='way' ref='2' role='' />
<tag k='name' v='2 backward' />
<tag k='ref' v='2' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='14' visible='true' version='1'>
<member type='relation' ref='13' role='' />
<member type='relation' ref='12' role='' />
<tag k='colour' v='red' />
<tag k='network' v='network-1' />
<tag k='ref' v='2' />
<tag k='name' v='Red Line' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
<relation id='15' visible='true' version='1'>
<member type='relation' ref='8' role='' />
<member type='relation' ref='7' role='' />
<tag k='network' v='network-1' />
<tag k='ref' v='1' />
<tag k='name' v='Blue Line' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
<relation id='16' visible='true' version='1'>
<member type='node' ref='8' role='' />
<member type='node' ref='103' role='stop' />
<member type='node' ref='105' role='' />
<tag k='public_transport' v='stop_area' />
<tag k='type' v='public_transport' />
</relation>
</osm>

Binary file not shown.

View file

@ -1,5 +1,6 @@
sample_networks = {
"Only 2 stations, no rails": {
metro_samples = [
{
"name": "Only 2 stations, no rails",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -37,7 +38,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 2,
"cities_info": [
{
"num_stations": 2,
},
],
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -55,7 +60,8 @@ sample_networks = {
"positions_on_rails": [],
},
},
"Only 2 stations connected with rails": {
{
"name": "Only 2 stations connected with rails",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -100,7 +106,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 2,
"cities_info": [
{
"num_stations": 2,
},
],
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -124,7 +134,8 @@ sample_networks = {
"positions_on_rails": [[0], [1]],
},
},
"Only 6 stations, no rails": {
{
"name": "Only 6 stations, no rails",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -190,7 +201,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -212,7 +227,8 @@ sample_networks = {
"positions_on_rails": [],
},
},
"One rail line connecting all stations": {
{
"name": "One rail line connecting all stations",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -289,7 +305,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -325,7 +345,8 @@ sample_networks = {
"positions_on_rails": [[0], [1], [2], [3], [4], [5]],
},
},
"One rail line connecting all stations except the last": {
{
"name": "One rail line connecting all stations except the last",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -401,7 +422,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -435,7 +460,8 @@ sample_networks = {
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
},
"One rail line connecting all stations except the fist": {
{
"name": "One rail line connecting all stations except the first",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -511,7 +537,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(1.0, 0.0),
(2.0, 0.0),
@ -545,7 +575,11 @@ sample_networks = {
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
},
"One rail line connecting all stations except the fist and the last": {
{
"name": (
"One rail line connecting all stations "
"except the first and the last",
),
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -620,7 +654,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(1.0, 0.0),
(2.0, 0.0),
@ -652,7 +690,8 @@ sample_networks = {
"positions_on_rails": [[0], [1], [2], [3]],
},
},
"One rail line connecting only 2 first stations": {
{
"name": "One rail line connecting only 2 first stations",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -725,7 +764,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -753,7 +796,8 @@ sample_networks = {
"positions_on_rails": [[0], [1]],
},
},
"One rail line connecting only 2 last stations": {
{
"name": "One rail line connecting only 2 last stations",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -826,7 +870,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(4.0, 0.0),
(5.0, 0.0),
@ -854,7 +902,8 @@ sample_networks = {
"positions_on_rails": [[0], [1]],
},
},
"One rail connecting all stations and protruding at both ends": {
{
"name": "One rail connecting all stations and protruding at both ends",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -937,7 +986,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(-1.0, 0.0),
(0.0, 0.0),
@ -977,10 +1030,11 @@ sample_networks = {
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
},
(
"Several rails with reversed order for backward route, "
"connecting all stations and protruding at both ends"
): {
{
"name": (
"Several rails with reversed order for backward route, "
"connecting all stations and protruding at both ends"
),
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -1069,7 +1123,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(-1.0, 0.0),
(0.0, 0.0),
@ -1109,10 +1167,11 @@ sample_networks = {
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
},
(
"One rail laying near all stations requiring station projecting, "
"protruding at both ends"
): {
{
"name": (
"One rail laying near all stations requiring station projecting, "
"protruding at both ends"
),
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0001' lon='0.0'>
@ -1189,7 +1248,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(-1.0, 0.0),
(6.0, 0.0),
@ -1227,7 +1290,8 @@ sample_networks = {
],
},
},
"One rail laying near all stations except the first and last": {
{
"name": "One rail laying near all stations except the first and last",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0001' lon='0.0'>
@ -1304,7 +1368,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 6,
"cities_info": [
{
"num_stations": 6,
},
],
"tracks": [
(1.0, 0.0),
(4.0, 0.0),
@ -1330,7 +1398,8 @@ sample_networks = {
"positions_on_rails": [[0], [1 / 3], [2 / 3], [1]],
},
},
"Circle route without rails": {
{
"name": "Circle route without rails",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -1377,7 +1446,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 4,
"cities_info": [
{
"num_stations": 4,
},
],
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -1398,7 +1471,8 @@ sample_networks = {
"positions_on_rails": [],
},
},
"Circle route with closed rail line connecting all stations": {
{
"name": "Circle route with closed rail line connecting all stations",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -1455,7 +1529,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 4,
"cities_info": [
{
"num_stations": 4,
},
],
"tracks": [
(0.0, 0.0),
(0.0, 1.0),
@ -1488,4 +1566,4 @@ sample_networks = {
"positions_on_rails": [[0, 4], [1], [2], [3], [0, 4]],
},
},
}
]

View file

@ -1,4 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
metro_samples = [
{
"name": "Transfer at Kuntsevskaya",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='244036218' visible='true' version='27' lat='55.7306986' lon='37.4460134'>
<tag k='public_transport' v='stop_position' />
@ -80,3 +83,16 @@
<tag k='description' v='only incomplete members' />
</relation>
</osm>
""", # noqa: E501
"expected_centers": {
"w38836456": {"lat": 55.73064775, "lon": 37.446065950000005},
"w489951237": {"lat": 55.730760724999996, "lon": 37.44602055},
"r7588527": {"lat": 55.73066371666667, "lon": 37.44604881666667},
"r7588528": {"lat": 55.73075192499999, "lon": 37.44609837},
"r7588561": {"lat": 55.73070782083333, "lon": 37.44607359333334},
"r13426423": {"lat": 55.730760724999996, "lon": 37.44602055},
"r100": None,
"r101": None,
},
},
]

View file

@ -1,5 +1,6 @@
sample_networks = {
"No errors": {
metro_samples = [
{
"name": "No errors",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -38,7 +39,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 2,
"cities_info": [
{
"num_stations": 2,
},
],
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
@ -46,7 +51,8 @@ sample_networks = {
"warnings": [],
"notices": [],
},
"Bad station order": {
{
"name": "Bad station order",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -99,7 +105,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 4,
"cities_info": [
{
"num_stations": 4,
},
],
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
@ -112,7 +122,8 @@ sample_networks = {
"warnings": [],
"notices": [],
},
"Angle < 20 degrees": {
{
"name": "Angle < 20 degrees",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -159,7 +170,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 3,
"cities_info": [
{
"num_stations": 3,
},
],
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
@ -172,7 +187,8 @@ sample_networks = {
"warnings": [],
"notices": [],
},
"Angle between 20 and 45 degrees": {
{
"name": "Angle between 20 and 45 degrees",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -219,7 +235,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 3,
"cities_info": [
{
"num_stations": 3,
},
],
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
@ -232,7 +252,8 @@ sample_networks = {
'is too narrow, 27 degrees (relation 2, "Backward")',
],
},
"Stops unordered along tracks provided each angle > 45 degrees": {
{
"name": "Unordered stops provided each angle > 45 degrees",
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
@ -300,7 +321,11 @@ sample_networks = {
</relation>
</osm>
""",
"num_stations": 4,
"cities_info": [
{
"num_stations": 4,
},
],
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
@ -313,4 +338,4 @@ sample_networks = {
"warnings": [],
"notices": [],
},
}
]

View file

@ -0,0 +1,345 @@
metro_samples = [
{
"name": "tiny_world",
"xml_file": """assets/tiny_world.osm""",
"cities_info": [
{
"id": 1,
"name": "Intersecting 2 metro lines",
"country": "World",
"continent": "Africa",
"num_stations": 6,
"num_lines": 2,
"num_light_lines": 0,
"num_interchanges": 1,
"bbox": "-179, -89, 179, 89",
"networks": "network-1",
},
{
"id": 2,
"name": "One light rail line",
"country": "World",
"continent": "Africa",
"num_stations": 2,
"num_lines": 0,
"num_light_lines": 1,
"num_interchanges": 0,
"bbox": "-179, -89, 179, 89",
"networks": "network-2",
},
],
"gtfs_file": "assets/tiny_world_gtfs.zip",
"json_dump": """
{
"stopareas": {
"n1": {
"id": "n1",
"center": [
0,
0
],
"name": "Station 1",
"entrances": []
},
"r1": {
"id": "r1",
"center": [
0.00470373068,
0.0047037307
],
"name": "Station 2",
"entrances": []
},
"r3": {
"id": "r3",
"center": [
0.01012040581,
0.0097589171
],
"name": "Station 3",
"entrances": []
},
"n4": {
"id": "n4",
"center": [
0,
0.01
],
"name": "Station 4",
"entrances": []
},
"r2": {
"id": "r2",
"center": [
0.0047718624,
0.00514739839
],
"name": "Station 5",
"entrances": []
},
"n6": {
"id": "n6",
"center": [
0.01,
0
],
"name": "Station 6",
"entrances": []
},
"r4": {
"id": "r4",
"center": [
0.009716854315,
0.010286367745
],
"name": "Station 7",
"entrances": []
},
"r16": {
"id": "r16",
"center": [
0.012405493905,
0.014377764559999999
],
"name": "Station 8",
"entrances": []
}
},
"networks": {
"Intersecting 2 metro lines": {
"id": 1,
"name": "Intersecting 2 metro lines",
"routes": [
{
"id": "r15",
"mode": "subway",
"ref": "1",
"name": "Blue Line",
"colour": "#0000ff",
"infill": null,
"itineraries": [
{
"id": "r7",
"tracks": [
[
0,
0
],
[
0.00470373068,
0.0047037307
],
[
0.009939661455227341,
0.009939661455455193
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "n1",
"distance": 0
},
{
"stoparea_id": "r1",
"distance": 741
},
{
"stoparea_id": "r3",
"distance": 1565
}
]
},
{
"id": "r8",
"tracks": [
[
0.009939661455227341,
0.009939661455455193
],
[
0.00470373068,
0.0047037307
],
[
0,
0
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "r3",
"distance": 0
},
{
"stoparea_id": "r1",
"distance": 824
},
{
"stoparea_id": "n1",
"distance": 1565
}
]
}
]
},
{
"id": "r14",
"mode": "subway",
"ref": "2",
"name": "Red Line",
"colour": "#ff0000",
"infill": null,
"itineraries": [
{
"id": "r12",
"tracks": [
[
0,
0.01
],
[
0.01,
0
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "n4",
"distance": 0
},
{
"stoparea_id": "r2",
"distance": 758
},
{
"stoparea_id": "n6",
"distance": 1575
}
]
},
{
"id": "r13",
"tracks": [
[
0.01,
0
],
[
0,
0.01
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "n6",
"distance": 0
},
{
"stoparea_id": "r2",
"distance": 817
},
{
"stoparea_id": "n4",
"distance": 1575
}
]
}
]
}
]
},
"One light rail line": {
"id": 2,
"name": "One light rail line",
"routes": [
{
"id": "r11",
"mode": "light_rail",
"ref": "LR",
"name": "LR Line",
"colour": "#a52a2a",
"infill": "#ffffff",
"itineraries": [
{
"id": "r9",
"tracks": [
[
0.00976752835,
0.01025306758
],
[
0.01245616794,
0.01434446439
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "r4",
"distance": 0
},
{
"stoparea_id": "r16",
"distance": 545
}
]
},
{
"id": "r10",
"tracks": [
[
0.012321033122529725,
0.014359650255679167
],
[
0.00966618028,
0.01031966791
]
],
"start_time": null,
"end_time": null,
"interval": null,
"stops": [
{
"stoparea_id": "r16",
"distance": 0
},
{
"stoparea_id": "r4",
"distance": 538
}
]
}
]
}
]
}
},
"transfers": [
[
"r1",
"r2"
],
[
"r3",
"r4"
]
]
}
""",
},
]

View file

@ -1,24 +1,13 @@
"""
To perform tests manually, run this command from the top directory
of the repository:
> python -m unittest discover tests
or simply
> python -m unittest
"""
from tests.sample_data_for_build_tracks import sample_networks
from tests.sample_data_for_build_tracks import metro_samples
from tests.util import TestCase
class TestOneRouteTracks(TestCase):
"""Test tracks extending and truncating on one-route networks"""
def prepare_city_routes(self, network) -> tuple:
city = self.validate_city(network)
def prepare_city_routes(self, metro_sample: dict) -> tuple:
cities, transfers = self.prepare_cities(metro_sample)
city = cities[0]
self.assertTrue(city.is_good)
@ -30,56 +19,56 @@ class TestOneRouteTracks(TestCase):
return fwd_route, bwd_route
def _test_tracks_extending_for_network(self, network_data):
fwd_route, bwd_route = self.prepare_city_routes(network_data)
def _test_tracks_extending_for_network(self, metro_sample: dict) -> None:
fwd_route, bwd_route = self.prepare_city_routes(metro_sample)
self.assertEqual(
fwd_route.tracks,
network_data["tracks"],
metro_sample["tracks"],
"Wrong tracks",
)
extended_tracks = fwd_route.get_extended_tracks()
self.assertEqual(
extended_tracks,
network_data["extended_tracks"],
metro_sample["extended_tracks"],
"Wrong tracks after extending",
)
self.assertEqual(
bwd_route.tracks,
network_data["tracks"][::-1],
metro_sample["tracks"][::-1],
"Wrong backward tracks",
)
extended_tracks = bwd_route.get_extended_tracks()
self.assertEqual(
extended_tracks,
network_data["extended_tracks"][::-1],
metro_sample["extended_tracks"][::-1],
"Wrong backward tracks after extending",
)
def _test_tracks_truncating_for_network(self, network_data):
fwd_route, bwd_route = self.prepare_city_routes(network_data)
def _test_tracks_truncating_for_network(self, metro_sample: dict) -> None:
fwd_route, bwd_route = self.prepare_city_routes(metro_sample)
truncated_tracks = fwd_route.get_truncated_tracks(fwd_route.tracks)
self.assertEqual(
truncated_tracks,
network_data["truncated_tracks"],
metro_sample["truncated_tracks"],
"Wrong tracks after truncating",
)
truncated_tracks = bwd_route.get_truncated_tracks(bwd_route.tracks)
self.assertEqual(
truncated_tracks,
network_data["truncated_tracks"][::-1],
metro_sample["truncated_tracks"][::-1],
"Wrong backward tracks after truncating",
)
def _test_stop_positions_on_rails_for_network(self, network_data):
fwd_route, bwd_route = self.prepare_city_routes(network_data)
def _test_stop_positions_on_rails_for_network(self, sample: dict) -> None:
fwd_route, bwd_route = self.prepare_city_routes(sample)
for route, route_label in zip(
(fwd_route, bwd_route), ("forward", "backward")
):
route_data = network_data[route_label]
route_data = sample[route_label]
for attr in (
"first_stop_on_rails_index",
@ -97,21 +86,27 @@ class TestOneRouteTracks(TestCase):
rs.positions_on_rails
for rs in route.stops[first_ind : last_ind + 1] # noqa E203
]
self.assertListAlmostEqual(
self.assertSequenceAlmostEqual(
positions_on_rails, route_data["positions_on_rails"]
)
def test_tracks_extending(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_tracks_extending_for_network(network_data)
for sample in metro_samples:
sample_name = sample["name"]
sample["cities_info"][0]["name"] = sample_name
with self.subTest(msg=sample_name):
self._test_tracks_extending_for_network(sample)
def test_tracks_truncating(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_tracks_truncating_for_network(network_data)
for sample in metro_samples:
sample_name = sample["name"]
sample["cities_info"][0]["name"] = sample_name
with self.subTest(msg=sample_name):
self._test_tracks_truncating_for_network(sample)
def test_stop_position_on_rails(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_stop_positions_on_rails_for_network(network_data)
for sample in metro_samples:
sample_name = sample["name"]
sample["cities_info"][0]["name"] = sample_name
with self.subTest(msg=sample_name):
self._test_stop_positions_on_rails_for_network(sample)

View file

@ -1,28 +1,28 @@
import json
from pathlib import Path
import io
from unittest import TestCase
from process_subways import calculate_centers
from subway_io import load_xml
from tests.sample_data_for_center_calculation import metro_samples
class TestCenterCalculation(TestCase):
"""Test center calculation. Test data [should] contain among others
the following edge cases:
- an empty relation. It's element should not obtain "center" key.
- relation as member of relation, the child relation following the parent
in the OSM XML file.
- an empty relation. Its element should not obtain "center" key.
- relation as member of another relation, the child relation following
the parent in the OSM XML.
- relation with incomplete members (broken references).
- relations with cyclic references.
"""
ASSETS_PATH = Path(__file__).resolve().parent / "assets"
OSM_DATA = str(ASSETS_PATH / "kuntsevskaya_transfer.osm")
CORRECT_CENTERS = str(ASSETS_PATH / "kuntsevskaya_centers.json")
def test__calculate_centers(self) -> None:
elements = load_xml(self.OSM_DATA)
def test_calculate_centers(self) -> None:
for sample in metro_samples:
with self.subTest(msg=sample["name"]):
self._test_calculate_centers_for_sample(sample)
def _test_calculate_centers_for_sample(self, metro_sample: dict) -> None:
elements = load_xml(io.BytesIO(metro_sample["xml"].encode()))
calculate_centers(elements)
elements_dict = {
@ -36,12 +36,11 @@ class TestCenterCalculation(TestCase):
if "center" in el
}
with open(self.CORRECT_CENTERS) as f:
correct_centers = json.load(f)
expected_centers = metro_sample["expected_centers"]
self.assertTrue(set(calculated_centers).issubset(correct_centers))
self.assertTrue(set(calculated_centers).issubset(expected_centers))
for k, correct_center in correct_centers.items():
for k, correct_center in expected_centers.items():
if correct_center is None:
self.assertNotIn("center", elements_dict[k])
else:

View file

@ -1,4 +1,4 @@
from tests.sample_data_for_error_messages import sample_networks
from tests.sample_data_for_error_messages import metro_samples
from tests.util import TestCase
@ -7,16 +7,19 @@ class TestValidationMessages(TestCase):
on different types of errors in input OSM data.
"""
def _test_validation_messages_for_network(self, network_data):
city = self.validate_city(network_data)
def _test_validation_messages_for_network(
self, metro_sample: dict
) -> None:
cities, transfers = self.prepare_cities(metro_sample)
city = cities[0]
for err_level in ("errors", "warnings", "notices"):
self.assertListEqual(
sorted(getattr(city, err_level)),
sorted(network_data[err_level]),
sorted(metro_sample[err_level]),
)
def test_validation_messages(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_validation_messages_for_network(network_data)
for sample in metro_samples:
with self.subTest(msg=sample["name"]):
self._test_validation_messages_for_network(sample)

View file

@ -1,9 +1,13 @@
from unittest import TestCase
import codecs
import csv
from functools import partial
from pathlib import Path
from zipfile import ZipFile
from processors.gtfs import (
dict_to_row,
GTFS_COLUMNS,
)
from processors._common import transit_to_dict
from processors.gtfs import dict_to_row, GTFS_COLUMNS, transit_data_to_gtfs
from tests.util import TestCase
from tests.sample_data_for_outputs import metro_samples
class TestGTFS(TestCase):
@ -94,3 +98,62 @@ class TestGTFS(TestCase):
self.assertListEqual(
dict_to_row(shape["shape_data"], "shapes"), shape["answer"]
)
def test__transit_data_to_gtfs(self) -> None:
for metro_sample in metro_samples:
cities, transfers = self.prepare_cities(metro_sample)
calculated_transit_data = transit_to_dict(cities, transfers)
calculated_gtfs_data = transit_data_to_gtfs(
calculated_transit_data
)
control_gtfs_data = self._readGtfs(
Path(__file__).resolve().parent / metro_sample["gtfs_file"]
)
self._compareGtfs(calculated_gtfs_data, control_gtfs_data)
@staticmethod
def _readGtfs(filepath: str) -> dict:
gtfs_data = dict()
with ZipFile(filepath) as zf:
for gtfs_feature in GTFS_COLUMNS:
with zf.open(f"{gtfs_feature}.txt") as f:
reader = csv.reader(codecs.iterdecode(f, "utf-8"))
next(reader) # read header
rows = list(reader)
gtfs_data[gtfs_feature] = rows
return gtfs_data
def _compareGtfs(
self, calculated_gtfs_data: dict, control_gtfs_data: dict
) -> None:
for gtfs_feature in GTFS_COLUMNS:
calculated_rows = sorted(
map(
partial(dict_to_row, record_type=gtfs_feature),
calculated_gtfs_data[gtfs_feature],
)
)
control_rows = sorted(control_gtfs_data[gtfs_feature])
self.assertEqual(len(calculated_rows), len(control_rows))
for i, (calculated_row, control_row) in enumerate(
zip(calculated_rows, control_rows)
):
self.assertEqual(
len(calculated_row),
len(control_row),
f"Different length of {i}-th row of {gtfs_feature}",
)
for calculated_value, control_value in zip(
calculated_row, control_row
):
if calculated_value is None:
self.assertEqual(control_value, "", f"in {i}-th row")
else: # convert str to float/int/str
self.assertAlmostEqual(
calculated_value,
type(calculated_value)(control_value),
places=10,
)

26
tests/test_storage.py Normal file
View file

@ -0,0 +1,26 @@
import json
from processors._common import transit_to_dict
from tests.sample_data_for_outputs import metro_samples
from tests.util import TestCase, TestTransitDataMixin
class TestStorage(TestCase, TestTransitDataMixin):
def test_storage(self) -> None:
for sample in metro_samples:
with self.subTest(msg=sample["name"]):
self._test_storage_for_sample(sample)
def _test_storage_for_sample(self, metro_sample: dict) -> None:
cities, transfers = self.prepare_cities(metro_sample)
calculated_transit_data = transit_to_dict(cities, transfers)
control_transit_data = json.loads(metro_sample["json_dump"])
control_transit_data["transfers"] = set(
map(tuple, control_transit_data["transfers"])
)
self.compare_transit_data(
calculated_transit_data, control_transit_data
)

View file

@ -1,15 +1,23 @@
import io
from collections.abc import Sequence, Mapping
from operator import itemgetter
from pathlib import Path
from typing import Any
from unittest import TestCase as unittestTestCase
from process_subways import (
add_osm_elements_to_cities,
validate_cities,
calculate_centers,
)
from subway_io import load_xml
from subway_structure import City
from subway_structure import City, find_transfers
class TestCase(unittestTestCase):
"""TestCase class for testing the Subway Validator"""
CITY_TEMPLATE = {
"id": 1,
"name": "Null Island",
"country": "World",
"continent": "Africa",
@ -21,29 +29,184 @@ class TestCase(unittestTestCase):
"num_interchanges": 0,
}
def validate_city(self, network) -> City:
city_data = self.CITY_TEMPLATE.copy()
for attr in self.CITY_TEMPLATE.keys():
if attr in network:
city_data[attr] = network[attr]
@classmethod
def setUpClass(cls) -> None:
cls.city_class = City
city = City(city_data)
elements = load_xml(io.BytesIO(network["xml"].encode("utf-8")))
for el in elements:
city.add(el)
city.extract_routes()
city.validate()
return city
def prepare_cities(self, metro_sample: dict) -> tuple:
"""Load cities from file/string, validate them and return cities
and transfers.
"""
def assertListAlmostEqual(self, list1, list2, places=10) -> None:
if not (isinstance(list1, list) and isinstance(list2, list)):
raise RuntimeError(
f"Not lists passed to the '{self.__class__.__name__}."
"assertListAlmostEqual' method"
)
self.assertEqual(len(list1), len(list2))
for a, b in zip(list1, list2):
if isinstance(a, list) and isinstance(b, list):
self.assertListAlmostEqual(a, b, places)
def assign_unique_id(city_info: dict, cities_info: list[dict]) -> None:
"""city_info - newly added city, cities_info - already added
cities. Check city id uniqueness / assign unique id to the city.
"""
occupied_ids = set(c["id"] for c in cities_info)
if "id" in city_info:
if city_info["id"] in occupied_ids:
raise RuntimeError("Not unique city ids in test data")
else:
self.assertAlmostEqual(a, b, places)
city_info["id"] = max(occupied_ids, default=1) + 1
cities_given_info = metro_sample["cities_info"]
cities_info = list()
for city_given_info in cities_given_info:
city_info = self.CITY_TEMPLATE.copy()
for attr in city_given_info.keys():
city_info[attr] = city_given_info[attr]
assign_unique_id(city_info, cities_info)
cities_info.append(city_info)
if len(set(ci["name"] for ci in cities_info)) < len(cities_info):
raise RuntimeError("Not unique city names in test data")
cities = list(map(self.city_class, cities_info))
if "xml" in metro_sample:
xml_file = io.BytesIO(metro_sample["xml"].encode())
else:
xml_file = (
Path(__file__).resolve().parent / metro_sample["xml_file"]
)
elements = load_xml(xml_file)
calculate_centers(elements)
add_osm_elements_to_cities(elements, cities)
validate_cities(cities)
transfers = find_transfers(elements, cities)
return cities, transfers
def _assertAnyAlmostEqual(
self,
first: Any,
second: Any,
places: int = 10,
ignore_keys: set = None,
) -> None:
"""Dispatcher method to other "...AlmostEqual" methods
depending on argument types.
"""
if isinstance(first, Mapping):
self.assertMappingAlmostEqual(first, second, places, ignore_keys)
elif isinstance(first, Sequence) and not isinstance(
first, (str, bytes)
):
self.assertSequenceAlmostEqual(first, second, places, ignore_keys)
else:
self.assertAlmostEqual(first, second, places)
def assertSequenceAlmostEqual(
self,
seq1: Sequence,
seq2: Sequence,
places: int = 10,
ignore_keys: set = None,
) -> None:
"""Compare two sequences, items of numeric types being compared
approximately, containers being approx-compared recursively.
:param: seq1 a sequence of values of any types, including collections
:param: seq2 a sequence of values of any types, including collections
:param: places number of fractional digits (passed to
assertAlmostEqual() method of parent class)
:param: ignore_keys a set of strs with keys in dictionaries
that should be ignored during recursive comparison
:return: None
"""
if not (isinstance(seq1, Sequence) and isinstance(seq2, Sequence)):
raise RuntimeError(
f"Not a sequence passed to the '{self.__class__.__name__}."
"assertSequenceAlmostEqual' method"
)
self.assertEqual(len(seq1), len(seq2))
for a, b in zip(seq1, seq2):
self._assertAnyAlmostEqual(a, b, places, ignore_keys)
def assertMappingAlmostEqual(
self,
d1: Mapping,
d2: Mapping,
places: int = 10,
ignore_keys: set = None,
) -> None:
"""Compare dictionaries recursively, numeric values being compared
approximately.
:param: d1 a mapping of arbitrary key/value types,
including collections
:param: d1 a mapping of arbitrary key/value types,
including collections
:param: places number of fractional digits (passed to
assertAlmostEqual() method of parent class)
:param: ignore_keys a set of strs with keys in dictionaries
that should be ignored during recursive comparison
:return: None
"""
if not (isinstance(d1, Mapping) and isinstance(d2, Mapping)):
raise RuntimeError(
f"Not a dictionary passed to the '{self.__class__.__name__}."
"assertMappingAlmostEqual' method"
)
d1_keys = set(d1.keys())
d2_keys = set(d2.keys())
if ignore_keys:
d1_keys -= ignore_keys
d2_keys -= ignore_keys
self.assertSetEqual(d1_keys, d2_keys)
for k in d1_keys:
v1 = d1[k]
v2 = d2[k]
self._assertAnyAlmostEqual(v1, v2, places, ignore_keys)
class TestTransitDataMixin:
def compare_transit_data(self, td1: dict, td2: dict) -> None:
"""Compare transit data td1 and td2 remembering that:
- arrays that represent sets ("routes", "itineraries", "entrances")
should be compared without order;
- all floating-point values (coordinates) should be compared
approximately.
"""
self.assertMappingAlmostEqual(
td1,
td2,
ignore_keys={"stopareas", "routes", "itineraries"},
)
networks1 = td1["networks"]
networks2 = td2["networks"]
id_cmp = itemgetter("id")
for network_name, network_data1 in networks1.items():
network_data2 = networks2[network_name]
routes1 = sorted(network_data1["routes"], key=id_cmp)
routes2 = sorted(network_data2["routes"], key=id_cmp)
self.assertEqual(len(routes1), len(routes2))
for r1, r2 in zip(routes1, routes2):
self.assertMappingAlmostEqual(
r1, r2, ignore_keys={"itineraries"}
)
its1 = sorted(r1["itineraries"], key=id_cmp)
its2 = sorted(r2["itineraries"], key=id_cmp)
self.assertEqual(len(its1), len(its2))
for it1, it2 in zip(its1, its2):
self.assertMappingAlmostEqual(it1, it2)
transfers1 = td1["transfers"]
transfers2 = td2["transfers"]
self.assertSetEqual(transfers1, transfers2)
stopareas1 = td1["stopareas"]
stopareas2 = td2["stopareas"]
self.assertMappingAlmostEqual(
stopareas1, stopareas2, ignore_keys={"entrances"}
)
for sa_id, sa1_data in stopareas1.items():
sa2_data = stopareas2[sa_id]
entrances1 = sorted(sa1_data["entrances"], key=id_cmp)
entrances2 = sorted(sa2_data["entrances"], key=id_cmp)
self.assertEqual(len(entrances1), len(entrances2))
for e1, e2 in zip(entrances1, entrances2):
self.assertMappingAlmostEqual(e1, e2)