Persist best efforts during import and add resumable backfill
This commit is contained in:
+179
-5
@@ -6,6 +6,7 @@ import pytest
|
||||
from app import create_app
|
||||
from app.extensions import db
|
||||
from app.models import Activity, Friendship, User
|
||||
from app.records import EFFORTS_VERSION, persist_efforts
|
||||
from app.services import activity_data, import_cache
|
||||
|
||||
|
||||
@@ -34,6 +35,7 @@ def app(tmp_path):
|
||||
owner_id=1,
|
||||
source_key="test:1",
|
||||
title="A quiet morning run",
|
||||
has_route=True,
|
||||
sport="running",
|
||||
started_at=datetime(2026, 9, 30, tzinfo=UTC),
|
||||
distance_m=5000,
|
||||
@@ -90,6 +92,7 @@ def test_personal_records_render_and_remain_owner_only(app):
|
||||
with app.app_context():
|
||||
row = db.session.get(Activity, 1)
|
||||
row.samples = [{"distance": 0, "time": 0}, {"distance": 5000, "time": 1500}]
|
||||
persist_efforts(row)
|
||||
row.share_token = "record-test"
|
||||
db.session.commit()
|
||||
client = app.test_client()
|
||||
@@ -106,25 +109,196 @@ def test_personal_records_render_and_remain_owner_only(app):
|
||||
assert b"5 km PR" not in client.get("/dashboard").data
|
||||
|
||||
|
||||
def test_import_and_refresh_persist_efforts_in_the_run_transaction(app):
|
||||
from app.services import activity_from_fit
|
||||
|
||||
doc = {
|
||||
"session": [{"sport": "running", "start_time": "2026-09-30T10:00:00Z"}],
|
||||
"record": [
|
||||
{"timestamp": "2026-09-30T10:00:00Z", "distance": 0},
|
||||
{"timestamp": "2026-09-30T10:05:00Z", "distance": 1000},
|
||||
],
|
||||
}
|
||||
with app.app_context():
|
||||
row = activity_from_fit(1, "test:new", [doc])
|
||||
db.session.commit()
|
||||
row_id = row.id
|
||||
db.session.remove()
|
||||
row = db.session.get(Activity, row_id)
|
||||
assert row.efforts_version == EFFORTS_VERSION
|
||||
assert row.best_efforts[0]["seconds"] == 300
|
||||
assert activity_from_fit(1, "test:new", [doc]) is None
|
||||
doc["record"][1]["timestamp"] = "2026-09-30T10:04:00Z"
|
||||
activity_from_fit(1, "test:new", [doc], refresh=True)
|
||||
db.session.commit()
|
||||
assert row.best_efforts[0]["seconds"] == 240
|
||||
doc["record"] = []
|
||||
activity_from_fit(1, "test:new", [doc], refresh=True)
|
||||
db.session.commit()
|
||||
assert row.best_efforts == []
|
||||
assert row.efforts_version == EFFORTS_VERSION
|
||||
|
||||
|
||||
def test_backfill_is_resumable_and_dashboard_uses_only_stored_efforts(app, monkeypatch):
|
||||
from sqlalchemy import event
|
||||
|
||||
from app import records
|
||||
|
||||
with app.app_context():
|
||||
row = db.session.get(Activity, 1)
|
||||
row.samples = [
|
||||
{"distance": 0, "time": 0, "point": [-33, 151]},
|
||||
{"distance": 5000, "time": 1500},
|
||||
]
|
||||
db.session.add(
|
||||
Activity(
|
||||
owner_id=1,
|
||||
source_key="old:2",
|
||||
title="Old ride",
|
||||
sport="cycling",
|
||||
started_at=row.started_at,
|
||||
samples=[],
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
runner = app.test_cli_runner()
|
||||
result = runner.invoke(args=["backfill-efforts", "--batch-size", "1"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "2 activities backfilled" in result.output
|
||||
result = runner.invoke(args=["backfill-efforts"])
|
||||
assert result.exit_code == 0
|
||||
assert "0 activities backfilled" in result.output
|
||||
with app.app_context():
|
||||
assert db.session.get(Activity, 2).best_efforts == []
|
||||
assert db.session.get(Activity, 2).efforts_version == EFFORTS_VERSION
|
||||
statements = []
|
||||
|
||||
def capture(_conn, _cursor, statement, _params, _context, _many):
|
||||
statements.append(statement)
|
||||
|
||||
event.listen(db.engine, "before_cursor_execute", capture)
|
||||
engine = db.engine
|
||||
|
||||
def fail_calculation(_row):
|
||||
raise AssertionError("Page requests must never calculate from raw samples")
|
||||
|
||||
monkeypatch.setattr(records, "best_efforts", fail_calculation)
|
||||
try:
|
||||
client = app.test_client()
|
||||
login(client)
|
||||
assert b"5 km PR" in client.get("/dashboard").data
|
||||
assert all("activity.samples" not in s and "activity.laps" not in s for s in statements)
|
||||
assert b"Best efforts on this run" in client.get("/activities/1").data
|
||||
finally:
|
||||
event.remove(engine, "before_cursor_execute", capture)
|
||||
|
||||
|
||||
def test_backfill_keeps_completed_batches_after_failure(app, monkeypatch):
|
||||
from app import records
|
||||
|
||||
with app.app_context():
|
||||
db.session.add(
|
||||
Activity(
|
||||
owner_id=2,
|
||||
source_key="old:2",
|
||||
title="Second athlete",
|
||||
sport="running",
|
||||
started_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
samples=[],
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
original = records.persist_efforts
|
||||
|
||||
def fail_second(row):
|
||||
if row.id == 2:
|
||||
raise ValueError("Interrupted batch")
|
||||
original(row)
|
||||
|
||||
monkeypatch.setattr(records, "persist_efforts", fail_second)
|
||||
runner = app.test_cli_runner()
|
||||
result = runner.invoke(args=["backfill-efforts", "--batch-size", "1"])
|
||||
assert result.exit_code != 0
|
||||
with app.app_context():
|
||||
assert db.session.get(Activity, 1).efforts_version == EFFORTS_VERSION
|
||||
assert db.session.get(Activity, 2).efforts_version == 0
|
||||
monkeypatch.setattr(records, "persist_efforts", original)
|
||||
result = runner.invoke(args=["backfill-efforts", "--owner-id", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "0 activities backfilled" in result.output
|
||||
result = runner.invoke(args=["backfill-efforts", "--batch-size", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "1 activities backfilled" in result.output
|
||||
result = runner.invoke(args=["backfill-efforts", "--force", "--owner-id", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "1 activities backfilled" in result.output
|
||||
|
||||
|
||||
def test_efforts_migration_upgrades_existing_schema(tmp_path):
|
||||
from sqlalchemy import text
|
||||
|
||||
application = create_app(
|
||||
{
|
||||
"TESTING": True,
|
||||
"SECRET_KEY": "test",
|
||||
"SQLALCHEMY_DATABASE_URI": f"sqlite:///{tmp_path / 'migration.db'}",
|
||||
"RATELIMIT_ENABLED": False,
|
||||
}
|
||||
)
|
||||
runner = application.test_cli_runner()
|
||||
result = runner.invoke(args=["db", "upgrade", "d81b97ae5401"])
|
||||
assert result.exit_code == 0, result.output
|
||||
with application.app_context():
|
||||
db.session.execute(
|
||||
text(
|
||||
"INSERT INTO user (id, username, email, display_name, password_hash, timezone) "
|
||||
"VALUES (1, 'test', 'test@example.test', 'Test', 'test', 'UTC')"
|
||||
)
|
||||
)
|
||||
db.session.execute(
|
||||
text(
|
||||
"INSERT INTO activity (owner_id, source_key, title, sport, started_at, "
|
||||
"distance_m, duration_s, elevation_m, visibility, samples, laps) "
|
||||
"VALUES (1, 'old', 'Old run', 'running', '2026-01-01', "
|
||||
"1000, 300, 0, 'private', '[]', '[]')"
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
result = runner.invoke(args=["db", "upgrade"])
|
||||
assert result.exit_code == 0, result.output
|
||||
with application.app_context():
|
||||
row = db.session.get(Activity, 1)
|
||||
assert row.best_efforts == []
|
||||
assert row.efforts_version == 0
|
||||
assert row.has_route is False
|
||||
assert row.title == "Old run"
|
||||
result = runner.invoke(args=["backfill-efforts"])
|
||||
assert result.exit_code == 0, result.output
|
||||
with application.app_context():
|
||||
assert db.session.get(Activity, 1).efforts_version == EFFORTS_VERSION
|
||||
|
||||
|
||||
def test_dashboard_replays_latest_accessible_gps_run(app):
|
||||
client = app.test_client()
|
||||
login(client)
|
||||
page = client.get("/dashboard").data
|
||||
assert b'id="dashboard-route-map" data-url="/activities/1/data"' in page
|
||||
assert b'id="toggle-route-replay"' not in page
|
||||
assert b'vendor/leaflet.js' in page
|
||||
assert b"vendor/leaflet.js" in page
|
||||
assert b'class="dashboard-map"' not in page
|
||||
client.post("/logout")
|
||||
login(client, "other")
|
||||
page = client.get("/dashboard").data
|
||||
assert b'No GPS running route available yet.' in page
|
||||
assert b'/activities/1/data' not in page
|
||||
assert b"No GPS running route available yet." in page
|
||||
assert b"/activities/1/data" not in page
|
||||
with app.app_context():
|
||||
db.session.get(Activity, 1).samples = []
|
||||
row = db.session.get(Activity, 1)
|
||||
row.samples = []
|
||||
persist_efforts(row)
|
||||
db.session.commit()
|
||||
client.post("/logout")
|
||||
login(client)
|
||||
assert b'No GPS running route available yet.' in client.get("/dashboard").data
|
||||
assert b"No GPS running route available yet." in client.get("/dashboard").data
|
||||
|
||||
|
||||
def test_friend_access_requires_acceptance_and_revokes_immediately(app):
|
||||
|
||||
Reference in New Issue
Block a user