138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "tools"))
|
|
|
|
import session_timeline_exchange as timeline
|
|
|
|
SESSION = "se_11111111-1111-4111-8111-111111111111"
|
|
ENTRY = "sxe_22222222-2222-4222-8222-222222222222"
|
|
EXERCISE = "ex_33333333-3333-4333-8333-333333333333"
|
|
|
|
|
|
def schema(db: sqlite3.Connection) -> None:
|
|
db.executescript(
|
|
"""
|
|
CREATE TABLE sessions(
|
|
id INTEGER PRIMARY KEY,
|
|
session_id TEXT UNIQUE,
|
|
started_at TEXT,
|
|
ended_at TEXT
|
|
);
|
|
CREATE TABLE exercises(
|
|
id INTEGER PRIMARY KEY,
|
|
exercise_id TEXT UNIQUE
|
|
);
|
|
CREATE TABLE session_exercises(
|
|
id INTEGER PRIMARY KEY,
|
|
session_row_id INTEGER,
|
|
exercise_row_id INTEGER,
|
|
entry_id TEXT UNIQUE
|
|
);
|
|
CREATE TABLE session_exercise_timeline(
|
|
session_id TEXT,
|
|
entry_id TEXT,
|
|
exercise_id TEXT,
|
|
started_at TEXT,
|
|
ended_at TEXT,
|
|
imported_at TEXT,
|
|
PRIMARY KEY(session_id,entry_id)
|
|
);
|
|
PRAGMA user_version=31;
|
|
"""
|
|
)
|
|
db.execute(
|
|
"INSERT INTO sessions(id,session_id,started_at,ended_at) VALUES(1,?,?,?)",
|
|
(SESSION, "2026-09-23T08:00:00+02:00", "2026-09-23T09:00:00+02:00"),
|
|
)
|
|
db.execute("INSERT INTO exercises(id,exercise_id) VALUES(1,?)", (EXERCISE,))
|
|
db.execute(
|
|
"INSERT INTO session_exercises(id,session_row_id,exercise_row_id,entry_id) "
|
|
"VALUES(1,1,1,?)",
|
|
(ENTRY,),
|
|
)
|
|
|
|
|
|
def document() -> dict:
|
|
return {
|
|
"format": "trainlog-session-timeline",
|
|
"version": 1,
|
|
"generated_at": "2026-09-23T09:01:00+02:00",
|
|
"sessions": [
|
|
{
|
|
"session_id": SESSION,
|
|
"started_at": "2026-09-23T08:00:00+02:00",
|
|
"ended_at": "2026-09-23T09:00:00+02:00",
|
|
"exercises": [
|
|
{
|
|
"entry_id": ENTRY,
|
|
"exercise_id": EXERCISE,
|
|
"started_at": "2026-09-23T08:10:00+02:00",
|
|
"ended_at": "2026-09-23T08:20:00+02:00",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
class SessionTimelineExchangeTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.db = sqlite3.connect(Path(self.temp.name) / "timeline.sqlite")
|
|
schema(self.db)
|
|
|
|
def tearDown(self):
|
|
self.db.close()
|
|
self.temp.cleanup()
|
|
|
|
def test_import_and_exact_replay(self):
|
|
payload = timeline.validate(document())
|
|
self.assertEqual((1, 0), timeline.apply(self.db, payload))
|
|
self.assertEqual((0, 1), timeline.apply(self.db, payload))
|
|
self.assertEqual(
|
|
(
|
|
SESSION,
|
|
ENTRY,
|
|
EXERCISE,
|
|
"2026-09-23T08:10:00+02:00",
|
|
"2026-09-23T08:20:00+02:00",
|
|
),
|
|
self.db.execute(
|
|
"SELECT session_id,entry_id,exercise_id,started_at,ended_at "
|
|
"FROM session_exercise_timeline"
|
|
).fetchone(),
|
|
)
|
|
|
|
def test_rejects_identity_reuse_with_different_content(self):
|
|
payload = timeline.validate(document())
|
|
timeline.apply(self.db, payload)
|
|
changed = deepcopy(document())
|
|
changed["sessions"][0]["exercises"][0]["ended_at"] = "2026-09-23T08:21:00+02:00"
|
|
with self.assertRaisesRegex(ValueError, "different content"):
|
|
timeline.apply(self.db, timeline.validate(changed))
|
|
|
|
def test_rejects_occurrence_that_does_not_match_history(self):
|
|
payload = document()
|
|
payload["sessions"][0]["exercises"][0]["exercise_id"] = (
|
|
"ex_44444444-4444-4444-8444-444444444444"
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "does not match session history"):
|
|
timeline.apply(self.db, timeline.validate(payload))
|
|
|
|
def test_rejects_exercise_outside_session_bounds(self):
|
|
payload = document()
|
|
payload["sessions"][0]["exercises"][0]["started_at"] = "2026-09-23T07:59:59+02:00"
|
|
with self.assertRaisesRegex(ValueError, "outside session"):
|
|
timeline.validate(payload)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|