added some data to facilitate debugging... to be decided wether to publish this data or not in github
This commit is contained in:
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Manual two-user dev test.
|
||||
|
||||
Sets up a fresh multi-user instance with dave + brut, extracts their
|
||||
activities, then hands off to `bincio dev` so you can browse the site.
|
||||
|
||||
Run from the project root:
|
||||
|
||||
uv run python scripts/dev_test.py
|
||||
|
||||
Options:
|
||||
--fresh Wipe DATA_DIR before starting (default: reuse if it exists)
|
||||
--no-dev Stop after extract (skip `bincio dev`)
|
||||
|
||||
Credentials: dave / testpass and brut / testpass
|
||||
URL: http://localhost:4321
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_DIR = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = Path("/tmp/bincio_dev_test")
|
||||
DAVE_INPUT = PROJECT_DIR / "tests" / "data" / "dave"
|
||||
BRUT_INPUT = PROJECT_DIR / "tests" / "data" / "brut"
|
||||
PASSWORD = "testpass"
|
||||
|
||||
|
||||
def section(msg: str) -> None:
|
||||
print(f"\n\033[1;36m▸ {msg}\033[0m")
|
||||
|
||||
|
||||
def ok(msg: str) -> None:
|
||||
print(f" \033[32m✓\033[0m {msg}")
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
print(f" \033[33m·\033[0m {msg}")
|
||||
|
||||
|
||||
# ── 1. Init instance (dave = admin) ──────────────────────────────────────────
|
||||
|
||||
def init_instance() -> None:
|
||||
section("Initialising instance")
|
||||
from bincio.serve.db import create_user, get_user, open_db
|
||||
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
db = open_db(DATA_DIR)
|
||||
ok("instance.db ready")
|
||||
|
||||
if get_user(db, "dave"):
|
||||
warn("user 'dave' already exists — skipping")
|
||||
else:
|
||||
create_user(db, "dave", "Dave", PASSWORD, is_admin=True)
|
||||
ok("admin user 'dave' created")
|
||||
|
||||
if get_user(db, "brut"):
|
||||
warn("user 'brut' already exists — skipping")
|
||||
else:
|
||||
create_user(db, "brut", "Brut", PASSWORD, is_admin=False)
|
||||
ok("user 'brut' created")
|
||||
|
||||
for handle in ("dave", "brut"):
|
||||
user_dir = DATA_DIR / handle
|
||||
(user_dir / "activities").mkdir(parents=True, exist_ok=True)
|
||||
(user_dir / "edits").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
root_index = DATA_DIR / "index.json"
|
||||
if not root_index.exists():
|
||||
root_index.write_text(json.dumps({
|
||||
"bas_version": "1.0",
|
||||
"instance": {"name": "Dev Test", "private": True},
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"shards": [
|
||||
{"handle": "dave", "url": "dave/index.json"},
|
||||
{"handle": "brut", "url": "brut/index.json"},
|
||||
],
|
||||
"activities": [],
|
||||
}, indent=2))
|
||||
ok("root index.json written")
|
||||
else:
|
||||
warn("root index.json already exists — skipping")
|
||||
|
||||
|
||||
# ── 2. Extract activities ─────────────────────────────────────────────────────
|
||||
|
||||
def extract_user(handle: str, input_dir: Path) -> None:
|
||||
section(f"Extracting activities for {handle}")
|
||||
if not input_dir.exists():
|
||||
print(f" \033[31m✗\033[0m Input dir not found: {input_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
cfg_path = DATA_DIR / f"_cfg_{handle}.yaml"
|
||||
cfg_path.write_text(
|
||||
f"owner:\n handle: {handle}\n"
|
||||
f"input:\n dirs:\n - {input_dir}\n"
|
||||
f"output:\n dir: {DATA_DIR}\n"
|
||||
)
|
||||
|
||||
from click.testing import CliRunner
|
||||
from bincio.extract.cli import extract as extract_cmd
|
||||
result = CliRunner().invoke(extract_cmd, ["--config", str(cfg_path)])
|
||||
|
||||
if result.exit_code != 0:
|
||||
print(f" \033[31m✗\033[0m Extract failed:\n{result.output}", file=sys.stderr)
|
||||
if result.exception:
|
||||
import traceback
|
||||
traceback.print_exception(type(result.exception), result.exception,
|
||||
result.exception.__traceback__, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
acts = list((DATA_DIR / handle / "activities").glob("*.json"))
|
||||
ok(f"{len(acts)} activities extracted → {DATA_DIR / handle / 'activities'}")
|
||||
|
||||
|
||||
# ── 3. Merge + manifest ───────────────────────────────────────────────────────
|
||||
|
||||
def prepare_serve() -> None:
|
||||
section("Merging sidecars + writing root manifest")
|
||||
from bincio.render.merge import merge_all
|
||||
from bincio.render.cli import _write_root_manifest
|
||||
import bincio.render.cli as render_cli
|
||||
from rich.console import Console
|
||||
render_cli.console = Console() # normal output
|
||||
|
||||
for handle in ("dave", "brut"):
|
||||
n = merge_all(DATA_DIR / handle)
|
||||
ok(f"{handle}: {n} sidecar(s) merged")
|
||||
|
||||
_write_root_manifest(DATA_DIR)
|
||||
ok("root manifest updated")
|
||||
|
||||
|
||||
# ── 4. Hand off to bincio dev ─────────────────────────────────────────────────
|
||||
|
||||
def start_dev() -> None:
|
||||
section("Starting bincio dev")
|
||||
print()
|
||||
print(" \033[1mCredentials\033[0m")
|
||||
print(f" dave / {PASSWORD} (admin)")
|
||||
print(f" brut / {PASSWORD}")
|
||||
print()
|
||||
print(" \033[1mURL\033[0m http://localhost:4321")
|
||||
print()
|
||||
print(" Press Ctrl+C to stop.\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["uv", "run", "bincio", "dev", "--data-dir", str(DATA_DIR)],
|
||||
cwd=PROJECT_DIR,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
# ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--fresh", action="store_true", help="Wipe DATA_DIR before starting")
|
||||
parser.add_argument("--no-dev", action="store_true", help="Stop after extract, skip bincio dev")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"\033[1mbincio dev test\033[0m → {DATA_DIR}")
|
||||
|
||||
if args.fresh and DATA_DIR.exists():
|
||||
section("Wiping existing data dir")
|
||||
shutil.rmtree(DATA_DIR)
|
||||
ok(f"{DATA_DIR} removed")
|
||||
|
||||
init_instance()
|
||||
extract_user("dave", DAVE_INPUT)
|
||||
extract_user("brut", BRUT_INPUT)
|
||||
prepare_serve()
|
||||
|
||||
if not args.no_dev:
|
||||
start_dev()
|
||||
else:
|
||||
print(f"\n\033[32mDone.\033[0m Data ready at {DATA_DIR}")
|
||||
print(f"Run: uv run bincio dev --data-dir {DATA_DIR}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
"""End-to-end pipeline test: extract → merge → root manifest for two users.
|
||||
|
||||
Uses the 20 real FIT files checked into tests/data/dave/ and tests/data/brut/.
|
||||
Run with:
|
||||
|
||||
uv run pytest tests/test_pipeline.py -v
|
||||
|
||||
Skip during normal CI runs:
|
||||
|
||||
uv run pytest -m "not integration"
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from bincio.extract.cli import extract as extract_cmd
|
||||
|
||||
TESTS_DIR = Path(__file__).parent
|
||||
DATA_DIR = TESTS_DIR / "data"
|
||||
DAVE_INPUT = DATA_DIR / "dave"
|
||||
BRUT_INPUT = DATA_DIR / "brut"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def data_root():
|
||||
"""Run extract for dave and brut into a shared temp dir, yield the data root."""
|
||||
with tempfile.TemporaryDirectory(prefix="bincio_test_") as tmp:
|
||||
root = Path(tmp)
|
||||
runner = CliRunner()
|
||||
|
||||
for handle, input_dir in [("dave", DAVE_INPUT), ("brut", BRUT_INPUT)]:
|
||||
cfg_path = root / f"cfg_{handle}.yaml"
|
||||
cfg_path.write_text(
|
||||
f"owner:\n handle: {handle}\n"
|
||||
f"input:\n dirs:\n - {input_dir}\n"
|
||||
f"output:\n dir: {root}\n"
|
||||
)
|
||||
result = runner.invoke(extract_cmd, ["--config", str(cfg_path)])
|
||||
assert result.exit_code == 0, (
|
||||
f"bincio extract failed for {handle}:\n{result.output}"
|
||||
)
|
||||
|
||||
yield root
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.slow
|
||||
class TestPipeline:
|
||||
def test_activities_extracted_dave(self, data_root):
|
||||
acts = list((data_root / "dave" / "activities").glob("*.json"))
|
||||
assert len(acts) >= 8, f"Expected ≥8 activities for dave, got {len(acts)}"
|
||||
|
||||
def test_activities_extracted_brut(self, data_root):
|
||||
acts = list((data_root / "brut" / "activities").glob("*.json"))
|
||||
assert len(acts) >= 8, f"Expected ≥8 activities for brut, got {len(acts)}"
|
||||
|
||||
def test_index_json_dave(self, data_root):
|
||||
index = json.loads((data_root / "dave" / "index.json").read_text())
|
||||
assert len(index["activities"]) >= 8
|
||||
assert index["owner"]["handle"] == "dave"
|
||||
|
||||
def test_index_json_brut(self, data_root):
|
||||
index = json.loads((data_root / "brut" / "index.json").read_text())
|
||||
assert len(index["activities"]) >= 8
|
||||
assert index["owner"]["handle"] == "brut"
|
||||
|
||||
def test_merge_produces_merged_dir(self, data_root):
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(data_root / "dave")
|
||||
merge_all(data_root / "brut")
|
||||
|
||||
assert (data_root / "dave" / "_merged" / "index.json").exists()
|
||||
assert (data_root / "brut" / "_merged" / "index.json").exists()
|
||||
|
||||
def test_merged_index_has_activities(self, data_root):
|
||||
# Ensure merge ran (idempotent if already done by earlier test in class)
|
||||
from bincio.render.merge import merge_all
|
||||
merge_all(data_root / "dave")
|
||||
merge_all(data_root / "brut")
|
||||
|
||||
for handle in ("dave", "brut"):
|
||||
merged = json.loads((data_root / handle / "_merged" / "index.json").read_text())
|
||||
assert len(merged["activities"]) >= 8, (
|
||||
f"Expected ≥8 merged activities for {handle}"
|
||||
)
|
||||
|
||||
def test_root_manifest(self, data_root):
|
||||
from bincio.render.cli import _user_dirs, _write_root_manifest
|
||||
from rich.console import Console
|
||||
|
||||
# _write_root_manifest uses the module-level console; patch it to suppress output
|
||||
import bincio.render.cli as render_cli
|
||||
render_cli.console = Console(quiet=True)
|
||||
|
||||
_write_root_manifest(data_root)
|
||||
|
||||
manifest = json.loads((data_root / "index.json").read_text())
|
||||
handles = {s["handle"] for s in manifest["shards"]}
|
||||
assert "dave" in handles
|
||||
assert "brut" in handles
|
||||
assert manifest["bas_version"] == "1.0"
|
||||
# Single-user path: no instance.db → private must be False
|
||||
assert manifest["instance"].get("private") is False
|
||||
|
||||
def test_activity_json_structure(self, data_root):
|
||||
"""Spot-check that extracted JSON has the required BAS fields."""
|
||||
acts = sorted((data_root / "dave" / "activities").glob("*.json"))
|
||||
detail = json.loads(acts[0].read_text())
|
||||
for field in ("id", "title", "sport", "started_at", "duration_s"):
|
||||
assert field in detail, f"Missing field '{field}' in activity JSON"
|
||||
|
||||
def test_geojson_exists_for_gps_activities(self, data_root):
|
||||
"""Each activity with GPS data should have a companion .geojson file."""
|
||||
acts_dir = data_root / "dave" / "activities"
|
||||
json_ids = {p.stem for p in acts_dir.glob("*.json")}
|
||||
geojson_ids = {p.stem for p in acts_dir.glob("*.geojson")}
|
||||
# At least some activities should have tracks (Karoo FIT files always have GPS)
|
||||
assert len(geojson_ids) >= 5, "Expected ≥5 GeoJSON track files for dave"
|
||||
assert geojson_ids.issubset(json_ids), "GeoJSON without matching detail JSON"
|
||||
Reference in New Issue
Block a user