Make Grid.gridify() use only pickup addresses
- ensure a `Restaurant` only has one unique `Order.pickup_address` - rework `Grid.gridify()` so that only pickup addresses are assigned into `Pixel`s - include database migrations to ensure the data adhere to these tighter constraints
This commit is contained in:
parent
0c1ff5338d
commit
1bfc7db916
11 changed files with 519 additions and 61 deletions
|
|
@ -8,24 +8,31 @@ from urban_meal_delivery.console import gridify
|
|||
|
||||
|
||||
@pytest.mark.db
|
||||
def test_four_pixels_with_two_addresses(
|
||||
cli, db_session, monkeypatch, city, make_address,
|
||||
def test_two_pixels_with_two_addresses( # noqa:WPS211
|
||||
cli, db_session, monkeypatch, city, make_address, make_restaurant, make_order,
|
||||
):
|
||||
"""Two `Address` objects in distinct `Pixel` objects.
|
||||
|
||||
This is roughly the same test case as
|
||||
`tests.db.test_grids.test_four_pixels_with_two_addresses`.
|
||||
`tests.db.test_grids.test_two_pixels_with_two_addresses`.
|
||||
The difference is that the result is written to the database.
|
||||
"""
|
||||
# Create two `Address` objects in distinct `Pixel`s.
|
||||
city.addresses = [
|
||||
# One `Address` in the lower-left `Pixel`, ...
|
||||
make_address(latitude=48.8357377, longitude=2.2517412),
|
||||
# ... and another one in the upper-right one.
|
||||
make_address(latitude=48.8898312, longitude=2.4357622),
|
||||
]
|
||||
# One `Address` in the lower-left `Pixel`, ...
|
||||
address1 = make_address(latitude=48.8357377, longitude=2.2517412)
|
||||
# ... and another one in the upper-right one.
|
||||
address2 = make_address(latitude=48.8898312, longitude=2.4357622)
|
||||
|
||||
db_session.add(city)
|
||||
# Locate a `Restaurant` at the two `Address` objects and
|
||||
# place one `Order` for each of them so that the `Address`
|
||||
# objects are used as `Order.pickup_address`s.
|
||||
restaurant1 = make_restaurant(address=address1)
|
||||
restaurant2 = make_restaurant(address=address2)
|
||||
order1 = make_order(restaurant=restaurant1)
|
||||
order2 = make_order(restaurant=restaurant2)
|
||||
|
||||
db_session.add(order1)
|
||||
db_session.add(order2)
|
||||
db_session.commit()
|
||||
|
||||
side_length = max(city.total_x // 2, city.total_y // 2) + 1
|
||||
|
|
|
|||
|
|
@ -74,18 +74,30 @@ class TestProperties:
|
|||
class TestGridification:
|
||||
"""Test the `Grid.gridify()` constructor."""
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_one_pixel_without_addresses(self, city):
|
||||
"""At the very least, there must be one `Pixel` ...
|
||||
@pytest.fixture
|
||||
def addresses_mock(self, mocker, monkeypatch):
|
||||
"""A `Mock` whose `.return_value` are to be set ...
|
||||
|
||||
... if the `side_length` is greater than both the
|
||||
horizontal and vertical distances of the viewport.
|
||||
... to the addresses that are gridified. The addresses are
|
||||
all considered `Order.pickup_address` attributes for some orders.
|
||||
"""
|
||||
mock = mocker.Mock()
|
||||
query = ( # noqa:ECE001
|
||||
mock.query.return_value.join.return_value.filter.return_value.all # noqa:E501,WPS219
|
||||
)
|
||||
monkeypatch.setattr(db, 'session', mock)
|
||||
|
||||
return query
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_no_pixel_without_addresses(self, city, addresses_mock):
|
||||
"""Without orders, there are no `Pixel` objects on the `grid`.
|
||||
|
||||
This test case skips the `for`-loop inside `Grid.gridify()`.
|
||||
Interestingly, coverage.py does not see this.
|
||||
"""
|
||||
city.addresses = []
|
||||
addresses_mock.return_value = []
|
||||
|
||||
# The chosen `side_length` would result in one `Pixel` if there were orders.
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
|
||||
|
|
@ -94,13 +106,13 @@ class TestGridification:
|
|||
assert isinstance(result, db.Grid)
|
||||
assert len(result.pixels) == 0 # noqa:WPS507
|
||||
|
||||
def test_one_pixel_with_one_address(self, city, address):
|
||||
def test_one_pixel_with_one_address(self, city, order, addresses_mock):
|
||||
"""At the very least, there must be one `Pixel` ...
|
||||
|
||||
... if the `side_length` is greater than both the
|
||||
horizontal and vertical distances of the viewport.
|
||||
"""
|
||||
city.addresses = [address]
|
||||
addresses_mock.return_value = [order.pickup_address]
|
||||
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
|
|
@ -110,7 +122,7 @@ class TestGridification:
|
|||
assert isinstance(result, db.Grid)
|
||||
assert len(result.pixels) == 1
|
||||
|
||||
def test_one_pixel_with_two_addresses(self, city, make_address):
|
||||
def test_one_pixel_with_two_addresses(self, city, make_order, addresses_mock):
|
||||
"""At the very least, there must be one `Pixel` ...
|
||||
|
||||
... if the `side_length` is greater than both the
|
||||
|
|
@ -119,7 +131,8 @@ class TestGridification:
|
|||
This test case is necessary as `test_one_pixel_with_one_address`
|
||||
does not have to re-use an already created `Pixel` object internally.
|
||||
"""
|
||||
city.addresses = [make_address(), make_address()]
|
||||
orders = [make_order(), make_order()]
|
||||
addresses_mock.return_value = [order.pickup_address for order in orders]
|
||||
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
|
|
@ -129,12 +142,11 @@ class TestGridification:
|
|||
assert isinstance(result, db.Grid)
|
||||
assert len(result.pixels) == 1
|
||||
|
||||
def test_one_pixel_with_address_too_far_south(self, city, address):
|
||||
def test_no_pixel_with_one_address_too_far_south(self, city, order, addresses_mock):
|
||||
"""An `address` outside the `city`'s viewport is discarded."""
|
||||
# Move the `address` just below `city.southwest`.
|
||||
address.latitude = city.southwest.latitude - 0.1
|
||||
|
||||
city.addresses = [address]
|
||||
order.pickup_address.latitude = city.southwest.latitude - 0.1
|
||||
addresses_mock.return_value = [order.pickup_address]
|
||||
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
|
|
@ -145,16 +157,15 @@ class TestGridification:
|
|||
assert len(result.pixels) == 0 # noqa:WPS507
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_one_pixel_with_address_too_far_west(self, city, address):
|
||||
def test_no_pixel_with_one_address_too_far_west(self, city, order, addresses_mock):
|
||||
"""An `address` outside the `city`'s viewport is discarded.
|
||||
|
||||
This test is a logical sibling to `test_one_pixel_with_address_too_far_south`
|
||||
and therefore redundant.
|
||||
This test is a logical sibling to
|
||||
`test_no_pixel_with_one_address_too_far_south` and therefore redundant.
|
||||
"""
|
||||
# Move the `address` just left to `city.southwest`.
|
||||
address.longitude = city.southwest.longitude - 0.1
|
||||
|
||||
city.addresses = [address]
|
||||
order.pickup_address.longitude = city.southwest.longitude - 0.1
|
||||
addresses_mock.return_value = [order.pickup_address]
|
||||
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
|
|
@ -165,13 +176,13 @@ class TestGridification:
|
|||
assert len(result.pixels) == 0 # noqa:WPS507
|
||||
|
||||
@pytest.mark.no_cover
|
||||
def test_four_pixels_with_two_addresses(self, city, make_address):
|
||||
def test_two_pixels_with_two_addresses(self, city, make_address, addresses_mock):
|
||||
"""Two `Address` objects in distinct `Pixel` objects.
|
||||
|
||||
This test is more of a sanity check.
|
||||
"""
|
||||
# Create two `Address` objects in distinct `Pixel`s.
|
||||
city.addresses = [
|
||||
addresses_mock.return_value = [
|
||||
# One `Address` in the lower-left `Pixel`, ...
|
||||
make_address(latitude=48.8357377, longitude=2.2517412),
|
||||
# ... and another one in the upper-right one.
|
||||
|
|
@ -194,7 +205,9 @@ class TestGridification:
|
|||
@pytest.mark.db
|
||||
@pytest.mark.no_cover
|
||||
@pytest.mark.parametrize('side_length', [250, 500, 1_000, 2_000, 4_000, 8_000])
|
||||
def test_make_random_grids(self, db_session, city, make_address, side_length):
|
||||
def test_make_random_grids( # noqa:WPS211
|
||||
self, db_session, city, make_address, make_restaurant, make_order, side_length,
|
||||
):
|
||||
"""With 100 random `Address` objects, a grid must have ...
|
||||
|
||||
... between 1 and a deterministic upper bound of `Pixel` objects.
|
||||
|
|
@ -202,7 +215,10 @@ class TestGridification:
|
|||
This test creates confidence that the created `Grid`
|
||||
objects adhere to the database constraints.
|
||||
"""
|
||||
city.addresses = [make_address() for _ in range(100)]
|
||||
addresses = [make_address() for _ in range(100)]
|
||||
restaurants = [make_restaurant(address=address) for address in addresses]
|
||||
orders = [make_order(restaurant=restaurant) for restaurant in restaurants]
|
||||
db_session.add_all(orders)
|
||||
|
||||
n_pixels_x = (city.total_x // side_length) + 1
|
||||
n_pixels_y = (city.total_y // side_length) + 1
|
||||
|
|
|
|||
|
|
@ -17,16 +17,34 @@ class TestAggregateOrders:
|
|||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def one_pixel_grid(self, db_session, city, restaurant):
|
||||
def addresses_mock(self, mocker, monkeypatch):
|
||||
"""A `Mock` whose `.return_value` are to be set ...
|
||||
|
||||
... to the addresses that are gridified. The addresses are
|
||||
all considered `Order.pickup_address` attributes for some orders.
|
||||
|
||||
Note: This fixture also exists in `tests.db.test_grids`.
|
||||
"""
|
||||
mock = mocker.Mock()
|
||||
query = ( # noqa:ECE001
|
||||
mock.query.return_value.join.return_value.filter.return_value.all # noqa:E501,WPS219
|
||||
)
|
||||
monkeypatch.setattr(db, 'session', mock)
|
||||
|
||||
return query
|
||||
|
||||
@pytest.fixture
|
||||
def one_pixel_grid(self, db_session, city, restaurant, addresses_mock):
|
||||
"""A persisted `Grid` with one `Pixel`.
|
||||
|
||||
`restaurant` must be a dependency as otherwise
|
||||
its `.address` is not put into the database.
|
||||
`restaurant` must be a dependency as otherwise the `restaurant.address`
|
||||
is not put into the database as an `Order.pickup_address`.
|
||||
"""
|
||||
addresses_mock.return_value = [restaurant.address]
|
||||
|
||||
# `+1` as otherwise there would be a second pixel in one direction.
|
||||
side_length = max(city.total_x, city.total_y) + 1
|
||||
grid = db.Grid.gridify(city=city, side_length=side_length)
|
||||
|
||||
db_session.add(grid)
|
||||
|
||||
assert len(grid.pixels) == 1 # sanity check
|
||||
|
|
@ -272,17 +290,17 @@ class TestAggregateOrders:
|
|||
assert result['total_orders'].sum() == 18
|
||||
|
||||
@pytest.fixture
|
||||
def two_pixel_grid(self, db_session, city, make_address, make_restaurant):
|
||||
"""A persisted `Grid` with two `Pixel` objects.
|
||||
|
||||
`restaurant` must be a dependency as otherwise
|
||||
its `.address` is not put into the database.
|
||||
"""
|
||||
def two_pixel_grid( # noqa:WPS211
|
||||
self, db_session, city, make_address, make_restaurant, addresses_mock,
|
||||
):
|
||||
"""A persisted `Grid` with two `Pixel` objects."""
|
||||
# One `Address` in the lower-left `Pixel`, ...
|
||||
address1 = make_address(latitude=48.8357377, longitude=2.2517412)
|
||||
# ... and another one in the upper-right one.
|
||||
address2 = make_address(latitude=48.8898312, longitude=2.4357622)
|
||||
|
||||
addresses_mock.return_value = [address1, address2]
|
||||
|
||||
# Create `Restaurant`s at the two addresses.
|
||||
make_restaurant(address=address1)
|
||||
make_restaurant(address=address2)
|
||||
|
|
@ -307,7 +325,8 @@ class TestAggregateOrders:
|
|||
In total, there are 30 orders.
|
||||
"""
|
||||
address1, address2 = two_pixel_grid.city.addresses
|
||||
restaurant1, restaurant2 = address1.restaurant, address2.restaurant
|
||||
# Rarely, an `Address` may have several `Restaurant`s in the real dataset.
|
||||
restaurant1, restaurant2 = address1.restaurants[0], address2.restaurants[0]
|
||||
|
||||
# Create one order every other hour for `restaurant1`.
|
||||
for hour in range(11, 23, 2):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue