Replace fixed DSL fields with parameterized metric expressions
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.analysis_migration import migrate, rewrite
|
||||
from app.extensions import db
|
||||
from app.models import Activity, SavedAnalysis, User
|
||||
from app.stats import METRICS, explore, parse_query, query_text
|
||||
from app.stats_dsl import QueryError
|
||||
|
||||
pytest_plugins = ('test_app',)
|
||||
|
||||
|
||||
def parsed(text):
|
||||
return parse_query({'query': text})
|
||||
|
||||
|
||||
def test_removed_fields_and_clause_are_rejected():
|
||||
removed = ['best_5km', 'best_10km', 'days_since_long_run', 'days_since_run',
|
||||
'rolling_7d_distance', 'rolling_28d_count']
|
||||
for field in removed:
|
||||
assert field not in METRICS
|
||||
with pytest.raises(QueryError):
|
||||
parsed(f'runs\nsummarize {field}')
|
||||
with pytest.raises(QueryError):
|
||||
parsed('days\nlong run distance >= 10 km\nsummarize distance')
|
||||
|
||||
|
||||
def test_expressions_in_filters_sort_calculations_and_named_summaries(app):
|
||||
text = ('runs\nduring all time\nlet doubled = best_time(5000 m) * 2\n'
|
||||
'where best_time(5000 m) < 31 min\n'
|
||||
'summarize min(best_time(5000 m)) as "best_time(5 km)", doubled\n'
|
||||
'sort by best_time(5000 m) asc\nby run\ncompare none')
|
||||
q = parsed(text)
|
||||
canonical = query_text(q)
|
||||
assert 'analysis_expr_' not in canonical
|
||||
assert 'as "best_time(5 km)"' in canonical
|
||||
assert parsed(canonical) == q
|
||||
with app.app_context():
|
||||
row = db.session.get(Activity, 1)
|
||||
row.best_efforts = [{'distance': 5000, 'seconds': 1800}]
|
||||
db.session.commit()
|
||||
result = explore(1, 'UTC', q)
|
||||
assert result['count'] == 1
|
||||
assert [c['value'] for c in result['cards']] == ['30:00', '60:00']
|
||||
|
||||
|
||||
def test_multiple_since_conditions_use_individual_runs_and_prior_history(app):
|
||||
with app.app_context():
|
||||
db.session.query(Activity).delete()
|
||||
db.session.add_all([
|
||||
Activity(owner_id=owner, source_key=f'{owner}-{index}', title='Race ( A)',
|
||||
sport='running', started_at=datetime(2026, 9, day, tzinfo=UTC),
|
||||
distance_m=km * 1000, duration_s=1800, avg_hr=140)
|
||||
for index, (owner, day, km) in enumerate([(1, 1, 20), (1, 3, 10), (1, 4, 6), (1, 4, 6), (2, 5, 30)])
|
||||
])
|
||||
db.session.commit()
|
||||
q = parsed('days\nduring 2026-09-04 to 2026-09-05\nwhere distance >= 0 km\n'
|
||||
'summarize latest(days_since_run(distance >= 10 km)), '
|
||||
'latest(days_since_run(distance >= 20 km)), latest(days_since_run())\n'
|
||||
'by day\ncompare none')
|
||||
result = explore(1, 'UTC', q)
|
||||
assert [g['raw_values'] for g in result['groups']] == [[1, 3, 0], [2, 4, 1]]
|
||||
q = parsed('days\nduring 2026-09-05 to 2026-09-05\n'
|
||||
'summarize latest(days_since_run(title contains "Race ( A)"))\n'
|
||||
'by day\ncompare none')
|
||||
assert parsed(query_text(q)) == q
|
||||
assert explore(1, 'UTC', q)['groups'][0]['raw_values'] == [1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('expression', ['best_time(3 km)', 'rolling_sum(distance, 0 days)',
|
||||
'rolling_mean(unknown, 14 days)', 'days_since_run(distance >= 10 bpm)'])
|
||||
def test_invalid_expression_reports_original_line(expression):
|
||||
with pytest.raises(QueryError) as error:
|
||||
parsed(f'days\nduring last 16 weeks\nsummarize latest({expression})')
|
||||
assert error.value.line == 3
|
||||
|
||||
|
||||
def test_migration_preserves_labels_settings_other_owners_and_is_idempotent(app):
|
||||
old = ('days\nduring last 16 weeks\nlong run distance >= 20 km\n'
|
||||
'summarize latest(days_since_long_run) as "days_since_long_run", '
|
||||
'latest(rolling_7d_count)\nby day\ncompare none')
|
||||
with app.app_context():
|
||||
own = SavedAnalysis(owner_id=1, name='Continuity', query=old, pinned=True,
|
||||
position=7, width='full', notes='Keep these notes')
|
||||
other = SavedAnalysis(owner_id=2, name='Other owner', query=old)
|
||||
db.session.add_all([own, other])
|
||||
db.session.commit()
|
||||
assert migrate(db.session.get(User, 1)) == 1
|
||||
assert 'days_since_run(distance >= 20 km)' in own.query
|
||||
assert 'rolling_sum(sessions, 7 days)' in own.query
|
||||
assert 'as "days_since_long_run"' in own.query
|
||||
assert own.pinned and own.position == 7 and own.width == 'full'
|
||||
assert own.notes == 'Keep these notes' and other.query == old
|
||||
assert migrate(db.session.get(User, 1)) == 0
|
||||
assert rewrite(own.query) == own.query
|
||||
|
||||
|
||||
def test_daily_rolling_session_count_includes_multiple_runs_and_rest_days(app):
|
||||
with app.app_context():
|
||||
db.session.query(Activity).delete()
|
||||
db.session.add_all([
|
||||
Activity(owner_id=1, source_key=str(index), title='Run', sport='running',
|
||||
started_at=datetime(2026, 9, 1, index, tzinfo=UTC),
|
||||
distance_m=5000, duration_s=1800)
|
||||
for index in range(2)
|
||||
])
|
||||
db.session.commit()
|
||||
q = parsed('days\nduring 2026-09-02 to 2026-09-02\n'
|
||||
'summarize latest(rolling_sum(sessions, 28 days))\nby day\ncompare none')
|
||||
result = explore(1, 'UTC', q)
|
||||
assert result['groups'][0]['raw_values'] == [2]
|
||||
assert result['cards'][0]['unit'] == 'runs'
|
||||
Reference in New Issue
Block a user