Rebrand sexp → sx across web platform (173 files)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 11m37s
Rename all sexp directories, files, identifiers, and references to sx. artdag/ excluded (separate media processing DSL). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
245
shared/sx/tests/test_relations.py
Normal file
245
shared/sx/tests/test_relations.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""Tests for the relation registry (Phase A)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from shared.sx.evaluator import evaluate, EvalError
|
||||
from shared.sx.parser import parse
|
||||
from shared.sx.relations import (
|
||||
_RELATION_REGISTRY,
|
||||
clear_registry,
|
||||
get_relation,
|
||||
load_relation_registry,
|
||||
relations_from,
|
||||
relations_to,
|
||||
all_relations,
|
||||
)
|
||||
from shared.sx.types import RelationDef
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_registry():
|
||||
"""Clear registry before each test."""
|
||||
clear_registry()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# defrelation parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDefrelation:
|
||||
def test_basic_defrelation(self):
|
||||
tree = parse('''
|
||||
(defrelation :page->market
|
||||
:from "page"
|
||||
:to "market"
|
||||
:cardinality :one-to-many
|
||||
:inverse :market->page
|
||||
:nav :submenu
|
||||
:nav-icon "fa fa-shopping-bag"
|
||||
:nav-label "markets")
|
||||
''')
|
||||
result = evaluate(tree)
|
||||
assert isinstance(result, RelationDef)
|
||||
assert result.name == "page->market"
|
||||
assert result.from_type == "page"
|
||||
assert result.to_type == "market"
|
||||
assert result.cardinality == "one-to-many"
|
||||
assert result.inverse == "market->page"
|
||||
assert result.nav == "submenu"
|
||||
assert result.nav_icon == "fa fa-shopping-bag"
|
||||
assert result.nav_label == "markets"
|
||||
|
||||
def test_defrelation_registered(self):
|
||||
tree = parse('''
|
||||
(defrelation :a->b
|
||||
:from "a" :to "b" :cardinality :one-to-one :nav :hidden)
|
||||
''')
|
||||
evaluate(tree)
|
||||
assert get_relation("a->b") is not None
|
||||
assert get_relation("a->b").cardinality == "one-to-one"
|
||||
|
||||
def test_defrelation_one_to_one(self):
|
||||
tree = parse('''
|
||||
(defrelation :page->menu_node
|
||||
:from "page" :to "menu_node"
|
||||
:cardinality :one-to-one :nav :hidden)
|
||||
''')
|
||||
result = evaluate(tree)
|
||||
assert result.cardinality == "one-to-one"
|
||||
assert result.inverse is None
|
||||
assert result.nav == "hidden"
|
||||
|
||||
def test_defrelation_many_to_many(self):
|
||||
tree = parse('''
|
||||
(defrelation :post->entry
|
||||
:from "post" :to "calendar_entry"
|
||||
:cardinality :many-to-many
|
||||
:inverse :entry->post
|
||||
:nav :inline
|
||||
:nav-icon "fa fa-file-alt"
|
||||
:nav-label "events")
|
||||
''')
|
||||
result = evaluate(tree)
|
||||
assert result.cardinality == "many-to-many"
|
||||
|
||||
def test_default_nav_is_hidden(self):
|
||||
tree = parse('''
|
||||
(defrelation :x->y
|
||||
:from "x" :to "y" :cardinality :one-to-many)
|
||||
''')
|
||||
result = evaluate(tree)
|
||||
assert result.nav == "hidden"
|
||||
|
||||
def test_invalid_cardinality_raises(self):
|
||||
tree = parse('''
|
||||
(defrelation :bad
|
||||
:from "a" :to "b" :cardinality :wrong)
|
||||
''')
|
||||
with pytest.raises(EvalError, match="invalid cardinality"):
|
||||
evaluate(tree)
|
||||
|
||||
def test_invalid_nav_raises(self):
|
||||
tree = parse('''
|
||||
(defrelation :bad
|
||||
:from "a" :to "b" :cardinality :one-to-one :nav :bogus)
|
||||
''')
|
||||
with pytest.raises(EvalError, match="invalid nav"):
|
||||
evaluate(tree)
|
||||
|
||||
def test_missing_from_raises(self):
|
||||
tree = parse('''
|
||||
(defrelation :bad :to "b" :cardinality :one-to-one)
|
||||
''')
|
||||
with pytest.raises(EvalError, match="missing required :from"):
|
||||
evaluate(tree)
|
||||
|
||||
def test_missing_to_raises(self):
|
||||
tree = parse('''
|
||||
(defrelation :bad :from "a" :cardinality :one-to-one)
|
||||
''')
|
||||
with pytest.raises(EvalError, match="missing required :to"):
|
||||
evaluate(tree)
|
||||
|
||||
def test_missing_cardinality_raises(self):
|
||||
tree = parse('''
|
||||
(defrelation :bad :from "a" :to "b")
|
||||
''')
|
||||
with pytest.raises(EvalError, match="missing required :cardinality"):
|
||||
evaluate(tree)
|
||||
|
||||
def test_name_must_be_keyword(self):
|
||||
tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)')
|
||||
with pytest.raises(EvalError, match="must be a keyword"):
|
||||
evaluate(tree)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry queries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRegistry:
|
||||
def _load_sample(self):
|
||||
tree = parse('''
|
||||
(begin
|
||||
(defrelation :page->market
|
||||
:from "page" :to "market" :cardinality :one-to-many
|
||||
:nav :submenu :nav-icon "fa fa-shopping-bag" :nav-label "markets")
|
||||
(defrelation :page->calendar
|
||||
:from "page" :to "calendar" :cardinality :one-to-many
|
||||
:nav :submenu :nav-icon "fa fa-calendar" :nav-label "calendars")
|
||||
(defrelation :post->entry
|
||||
:from "post" :to "calendar_entry" :cardinality :many-to-many
|
||||
:nav :inline)
|
||||
(defrelation :page->menu_node
|
||||
:from "page" :to "menu_node" :cardinality :one-to-one
|
||||
:nav :hidden))
|
||||
''')
|
||||
evaluate(tree)
|
||||
|
||||
def test_get_relation(self):
|
||||
self._load_sample()
|
||||
rel = get_relation("page->market")
|
||||
assert rel is not None
|
||||
assert rel.to_type == "market"
|
||||
|
||||
def test_get_relation_not_found(self):
|
||||
assert get_relation("nonexistent") is None
|
||||
|
||||
def test_relations_from_page(self):
|
||||
self._load_sample()
|
||||
rels = relations_from("page")
|
||||
names = {r.name for r in rels}
|
||||
assert names == {"page->market", "page->calendar", "page->menu_node"}
|
||||
|
||||
def test_relations_from_post(self):
|
||||
self._load_sample()
|
||||
rels = relations_from("post")
|
||||
assert len(rels) == 1
|
||||
assert rels[0].name == "post->entry"
|
||||
|
||||
def test_relations_from_empty(self):
|
||||
self._load_sample()
|
||||
assert relations_from("nonexistent") == []
|
||||
|
||||
def test_relations_to_market(self):
|
||||
self._load_sample()
|
||||
rels = relations_to("market")
|
||||
assert len(rels) == 1
|
||||
assert rels[0].name == "page->market"
|
||||
|
||||
def test_relations_to_calendar_entry(self):
|
||||
self._load_sample()
|
||||
rels = relations_to("calendar_entry")
|
||||
assert len(rels) == 1
|
||||
assert rels[0].name == "post->entry"
|
||||
|
||||
def test_all_relations(self):
|
||||
self._load_sample()
|
||||
assert len(all_relations()) == 4
|
||||
|
||||
def test_clear_registry(self):
|
||||
self._load_sample()
|
||||
assert len(all_relations()) == 4
|
||||
clear_registry()
|
||||
assert len(all_relations()) == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# load_relation_registry() — built-in definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestLoadBuiltins:
|
||||
def test_loads_builtin_relations(self):
|
||||
load_relation_registry()
|
||||
assert get_relation("page->market") is not None
|
||||
assert get_relation("page->calendar") is not None
|
||||
assert get_relation("post->calendar_entry") is not None
|
||||
assert get_relation("page->menu_node") is not None
|
||||
|
||||
def test_builtin_page_market(self):
|
||||
load_relation_registry()
|
||||
rel = get_relation("page->market")
|
||||
assert rel.from_type == "page"
|
||||
assert rel.to_type == "market"
|
||||
assert rel.cardinality == "one-to-many"
|
||||
assert rel.inverse == "market->page"
|
||||
assert rel.nav == "submenu"
|
||||
assert rel.nav_icon == "fa fa-shopping-bag"
|
||||
|
||||
def test_builtin_post_entry(self):
|
||||
load_relation_registry()
|
||||
rel = get_relation("post->calendar_entry")
|
||||
assert rel.cardinality == "many-to-many"
|
||||
assert rel.nav == "inline"
|
||||
|
||||
def test_builtin_page_menu_node(self):
|
||||
load_relation_registry()
|
||||
rel = get_relation("page->menu_node")
|
||||
assert rel.cardinality == "one-to-one"
|
||||
assert rel.nav == "hidden"
|
||||
|
||||
def test_frozen_dataclass(self):
|
||||
load_relation_registry()
|
||||
rel = get_relation("page->market")
|
||||
with pytest.raises(AttributeError):
|
||||
rel.name = "changed"
|
||||
Reference in New Issue
Block a user