Precompute run analysis and serve compact route previews
This commit is contained in:
+112
-2
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from app import create_app
|
||||
from app.display import persist_analysis
|
||||
from app.extensions import db
|
||||
from app.models import Activity, Friendship, User
|
||||
from app.records import EFFORTS_VERSION, persist_efforts
|
||||
@@ -55,6 +56,8 @@ def app(tmp_path):
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
persist_analysis(db.session.get(Activity, 1))
|
||||
db.session.commit()
|
||||
yield app
|
||||
|
||||
|
||||
@@ -88,11 +91,106 @@ def test_owner_dashboard_activity_json_and_settings_render(app):
|
||||
assert "point" not in data["chart"][0]
|
||||
|
||||
|
||||
def test_route_preview_access_is_revoked_with_friendship(app):
|
||||
owner, friend, anonymous = app.test_client(), app.test_client(), app.test_client()
|
||||
login(owner)
|
||||
login(friend, "other")
|
||||
assert anonymous.get("/activities/1/route").status_code == 404
|
||||
assert friend.get("/activities/1/route").status_code == 404
|
||||
assert owner.get("/activities/1/route").json == {"segments": [[[-33, 151]]]}
|
||||
owner.post("/activities/1/edit", data={"title": "Shared", "visibility": "friends"})
|
||||
owner.post("/friends", data={"email": "other@example.test"})
|
||||
friend.post("/friends/1/accept")
|
||||
assert friend.get("/activities/1/route").status_code == 200
|
||||
friend.post("/friends/1/remove")
|
||||
assert friend.get("/activities/1/route").status_code == 404
|
||||
|
||||
|
||||
def test_stored_analysis_requests_do_not_read_raw_samples_or_recalculate(app, monkeypatch):
|
||||
from sqlalchemy import event
|
||||
|
||||
from app import display
|
||||
|
||||
def fail(_row):
|
||||
raise AssertionError("Analysis must be precomputed")
|
||||
|
||||
monkeypatch.setattr(display, "analyse", fail)
|
||||
with app.app_context():
|
||||
db.session.get(Activity, 1).share_token = "compact-test"
|
||||
db.session.commit()
|
||||
statements = []
|
||||
|
||||
def capture(_conn, _cursor, statement, _params, _context, _many):
|
||||
statements.append(statement)
|
||||
|
||||
engine = db.engine
|
||||
event.listen(engine, "before_cursor_execute", capture)
|
||||
client = app.test_client()
|
||||
login(client)
|
||||
try:
|
||||
for path in (
|
||||
"/dashboard",
|
||||
"/activities/1",
|
||||
"/activities/1/data",
|
||||
"/activities/1/route",
|
||||
"/shared/runs/compact-test",
|
||||
"/shared/runs/compact-test/data",
|
||||
):
|
||||
assert client.get(path).status_code == 200
|
||||
assert all("activity.samples" not in s and "activity.laps" not in s for s in statements)
|
||||
finally:
|
||||
event.remove(engine, "before_cursor_execute", capture)
|
||||
|
||||
|
||||
def test_analysis_backfill_resumes_after_failed_batch(app, monkeypatch):
|
||||
from app import display
|
||||
|
||||
with app.app_context():
|
||||
db.session.get(Activity, 1).analysis_version = 0
|
||||
db.session.add(
|
||||
Activity(
|
||||
owner_id=2,
|
||||
source_key="analysis:2",
|
||||
title="Older activity",
|
||||
started_at=datetime(2026, 1, 1, tzinfo=UTC),
|
||||
sport="cycling",
|
||||
samples=[],
|
||||
laps=[],
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
original = display.persist_analysis
|
||||
|
||||
def interrupt(row):
|
||||
if row.id == 2:
|
||||
raise ValueError("Interrupted")
|
||||
original(row)
|
||||
|
||||
monkeypatch.setattr(display, "persist_analysis", interrupt)
|
||||
runner = app.test_cli_runner()
|
||||
result = runner.invoke(args=["backfill-analysis", "--batch-size", "1"])
|
||||
assert result.exit_code != 0
|
||||
with app.app_context():
|
||||
assert db.session.get(Activity, 1).analysis_version == display.ANALYSIS_VERSION
|
||||
assert db.session.get(Activity, 2).analysis_version == 0
|
||||
monkeypatch.setattr(display, "persist_analysis", original)
|
||||
result = runner.invoke(args=["backfill-analysis", "--owner-id", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "0 activities backfilled" in result.output
|
||||
result = runner.invoke(args=["backfill-analysis", "--batch-size", "1"])
|
||||
assert result.exit_code == 0
|
||||
assert "1 activities backfilled" in result.output
|
||||
result = runner.invoke(args=["backfill-analysis"])
|
||||
assert result.exit_code == 0
|
||||
assert "0 activities backfilled" in result.output
|
||||
|
||||
|
||||
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)
|
||||
persist_analysis(row)
|
||||
row.share_token = "record-test"
|
||||
db.session.commit()
|
||||
client = app.test_client()
|
||||
@@ -127,6 +225,8 @@ def test_import_and_refresh_persist_efforts_in_the_run_transaction(app):
|
||||
row = db.session.get(Activity, row_id)
|
||||
assert row.efforts_version == EFFORTS_VERSION
|
||||
assert row.best_efforts[0]["seconds"] == 300
|
||||
assert row.analysis_version == 1
|
||||
assert row.analysis_summary["summary"]["samples"] == 2
|
||||
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)
|
||||
@@ -136,6 +236,8 @@ def test_import_and_refresh_persist_efforts_in_the_run_transaction(app):
|
||||
activity_from_fit(1, "test:new", [doc], refresh=True)
|
||||
db.session.commit()
|
||||
assert row.best_efforts == []
|
||||
assert row.display_data == {"route_segments": [], "chart": []}
|
||||
assert row.route_geometry == []
|
||||
assert row.efforts_version == EFFORTS_VERSION
|
||||
|
||||
|
||||
@@ -270,6 +372,10 @@ def test_efforts_migration_upgrades_existing_schema(tmp_path):
|
||||
row = db.session.get(Activity, 1)
|
||||
assert row.best_efforts == []
|
||||
assert row.efforts_version == 0
|
||||
assert row.analysis_version == 0
|
||||
assert row.analysis_summary == {}
|
||||
assert row.display_data == {}
|
||||
assert row.route_geometry == []
|
||||
assert row.has_route is False
|
||||
assert row.title == "Old run"
|
||||
result = runner.invoke(args=["backfill-efforts"])
|
||||
@@ -282,7 +388,7 @@ 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="dashboard-route-map" data-url="/activities/1/route"' in page
|
||||
assert b'id="toggle-route-replay"' not in page
|
||||
assert b"vendor/leaflet.js" in page
|
||||
assert b'class="dashboard-map"' not in page
|
||||
@@ -295,6 +401,7 @@ def test_dashboard_replays_latest_accessible_gps_run(app):
|
||||
row = db.session.get(Activity, 1)
|
||||
row.samples = []
|
||||
persist_efforts(row)
|
||||
persist_analysis(row)
|
||||
db.session.commit()
|
||||
client.post("/logout")
|
||||
login(client)
|
||||
@@ -343,7 +450,10 @@ def test_private_activity_is_hidden_even_from_accepted_friend(app):
|
||||
|
||||
def test_full_route_includes_start_and_finish():
|
||||
points = [[0, 0], [0, 0.001], [0, 0.01], [0, 0.02]]
|
||||
row = Activity(samples=[{"point": point} for point in points])
|
||||
row = Activity(
|
||||
sport="running", distance_m=0, laps=[], samples=[{"point": point} for point in points]
|
||||
)
|
||||
persist_analysis(row)
|
||||
assert activity_data(row)["segments"] == [points]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user