mirror of
https://github.com/brunopostle/ifcurl.git
synced 2026-07-12 02:08:13 +00:00
Implements SPECIFICATION.md §6 BCF→ifc:// direction: parse a BCF 3.0 REST viewpoint dict (camera, clips, selection/visibility) and apply it to a base IfcUrl to produce an ifc:// URL string. IfcUrl.to_string() serialises any IfcUrl back to a valid ifc:// URL, enabling round-trip parse→to_string for all URL forms. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
376 lines
11 KiB
Python
376 lines
11 KiB
Python
"""Tests for IfcUrl.parse()."""
|
|
|
|
import pytest
|
|
|
|
from ifcurl.url import IfcUrl
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Transport inference
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_https_transport():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main?path=model.ifc")
|
|
assert url.transport == "https"
|
|
assert url.host == "example.com"
|
|
assert url.user is None
|
|
|
|
|
|
def test_ssh_transport():
|
|
url = IfcUrl.parse("ifc://git@example.com/org/repo@abc123?path=model.ifc")
|
|
assert url.transport == "ssh"
|
|
assert url.host == "example.com"
|
|
assert url.user == "git"
|
|
|
|
|
|
def test_local_transport():
|
|
url = IfcUrl.parse("ifc:///home/alice/project@HEAD?path=model.ifc")
|
|
assert url.transport == "local"
|
|
assert url.host == ""
|
|
assert url.user is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Repo path and ref
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_repo_path_and_branch_ref():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main?path=model.ifc")
|
|
assert url.repo_path == "org/repo"
|
|
assert url.ref == "heads/main"
|
|
|
|
|
|
def test_tag_ref():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@tags/v1.2?path=model.ifc")
|
|
assert url.ref == "tags/v1.2"
|
|
|
|
|
|
def test_commit_hash_ref():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@abc123def?path=model.ifc")
|
|
assert url.ref == "abc123def"
|
|
|
|
|
|
def test_head_ref():
|
|
url = IfcUrl.parse("ifc:///home/alice/project@HEAD?path=model.ifc")
|
|
assert url.ref == "HEAD"
|
|
assert url.repo_path == "/home/alice/project"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# git_ref() conversion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_git_ref_branch():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main?path=model.ifc")
|
|
assert url.git_ref() == "main"
|
|
|
|
|
|
def test_git_ref_tag():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@tags/v2.0?path=model.ifc")
|
|
assert url.git_ref() == "refs/tags/v2.0"
|
|
|
|
|
|
def test_git_ref_commit():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@abc123def?path=model.ifc")
|
|
assert url.git_ref() == "abc123def"
|
|
|
|
|
|
def test_git_ref_head():
|
|
url = IfcUrl.parse("ifc:///home/alice/repo@HEAD?path=model.ifc")
|
|
assert url.git_ref() == "HEAD"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Query parameters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_path_parameter():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main?path=models/building.ifc")
|
|
assert url.path == "models/building.ifc"
|
|
|
|
|
|
def test_no_path():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main")
|
|
assert url.path is None
|
|
|
|
|
|
def test_selector():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&selector=IfcWall")
|
|
assert url.selector == "IfcWall"
|
|
|
|
|
|
def test_selector_percent_encoded():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/repo@HEAD?path=m.ifc&selector=IfcWall%2C%2BName%3D%22X%22"
|
|
)
|
|
assert url.selector == 'IfcWall,+Name="X"'
|
|
|
|
|
|
def test_camera_perspective():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/repo@HEAD?path=m.ifc"
|
|
"&camera=10,20,5,0,-1,0,0,0,1&fov=60"
|
|
)
|
|
assert url.camera == (10.0, 20.0, 5.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0)
|
|
assert url.fov == 60.0
|
|
assert url.scale is None
|
|
|
|
|
|
def test_camera_orthographic():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/repo@HEAD?path=m.ifc"
|
|
"&camera=0,0,8,0,0,-1,0,1,0&scale=50"
|
|
)
|
|
assert url.scale == 50.0
|
|
assert url.fov is None
|
|
|
|
|
|
def test_clip_single():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&clip=0,0,3,0,0,-1")
|
|
assert url.clips == [(0.0, 0.0, 3.0, 0.0, 0.0, -1.0)]
|
|
|
|
|
|
def test_clip_multiple():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/repo@HEAD?path=m.ifc"
|
|
"&clip=0,0,3,0,0,-1&clip=0,0,0,0,0,1"
|
|
)
|
|
assert len(url.clips) == 2
|
|
assert url.clips[0] == (0.0, 0.0, 3.0, 0.0, 0.0, -1.0)
|
|
assert url.clips[1] == (0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
|
|
|
|
|
|
def test_visibility_default():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc")
|
|
assert url.visibility == "highlight"
|
|
|
|
|
|
def test_visibility_ghost():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&visibility=ghost")
|
|
assert url.visibility == "ghost"
|
|
|
|
|
|
def test_visibility_isolate():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc&visibility=isolate")
|
|
assert url.visibility == "isolate"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# git_remote_url()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_git_remote_url_https():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@heads/main?path=m.ifc")
|
|
assert url.git_remote_url() == "https://example.com/org/repo"
|
|
|
|
|
|
def test_git_remote_url_ssh():
|
|
url = IfcUrl.parse("ifc://git@example.com/org/repo@heads/main?path=m.ifc")
|
|
assert url.git_remote_url() == "ssh://git@example.com/org/repo"
|
|
|
|
|
|
def test_git_remote_url_local():
|
|
url = IfcUrl.parse("ifc:///home/alice/project@HEAD?path=m.ifc")
|
|
assert url.git_remote_url() == "/home/alice/project"
|
|
|
|
|
|
def test_git_remote_url_https_nonstandard_port():
|
|
url = IfcUrl.parse("ifc://localhost:3000/org/repo@heads/main?path=m.ifc")
|
|
assert url.git_remote_url() == "https://localhost:3000/org/repo"
|
|
|
|
|
|
def test_git_remote_url_ssh_nonstandard_port():
|
|
url = IfcUrl.parse("ifc://git@localhost:2222/org/repo@heads/main?path=m.ifc")
|
|
assert url.git_remote_url() == "ssh://git@localhost:2222/org/repo"
|
|
|
|
|
|
def test_host_preserves_port():
|
|
url = IfcUrl.parse("ifc://example.com:8080/org/repo@HEAD?path=m.ifc")
|
|
assert url.host == "example.com:8080"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_mutable_ref()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_mutable_head():
|
|
assert IfcUrl.parse("ifc://h/o/r@HEAD?path=m.ifc").is_mutable_ref() is True
|
|
|
|
|
|
def test_mutable_branch():
|
|
assert IfcUrl.parse("ifc://h/o/r@heads/main?path=m.ifc").is_mutable_ref() is True
|
|
|
|
|
|
def test_immutable_commit():
|
|
assert IfcUrl.parse("ifc://h/o/r@abc123def?path=m.ifc").is_mutable_ref() is False
|
|
|
|
|
|
def test_immutable_tag():
|
|
assert IfcUrl.parse("ifc://h/o/r@tags/v1.0?path=m.ifc").is_mutable_ref() is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Spec examples
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_spec_example_default_view():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/project@heads/main?path=models/building.ifc"
|
|
)
|
|
assert url.transport == "https"
|
|
assert url.ref == "heads/main"
|
|
assert url.path == "models/building.ifc"
|
|
assert url.selector is None
|
|
assert url.camera is None
|
|
|
|
|
|
def test_spec_example_perspective():
|
|
url = IfcUrl.parse(
|
|
"ifc://git@example.com/org/project@abc123def"
|
|
"?path=models/building.ifc"
|
|
"&selector=IfcWall%2C%2BName%3D%22Core%2BWall%22"
|
|
"&camera=10,20,5,0,-1,0,0,0,1&fov=60&visibility=ghost"
|
|
)
|
|
assert url.transport == "ssh"
|
|
assert url.fov == 60.0
|
|
assert url.visibility == "ghost"
|
|
|
|
|
|
def test_spec_example_orthographic_with_clip():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/org/project@tags/v2.0"
|
|
"?path=models/building.ifc"
|
|
"&camera=0,0,8,0,0,-1,0,1,0&scale=50&clip=0,0,5,0,0,-1"
|
|
)
|
|
assert url.scale == 50.0
|
|
assert url.clips == [(0.0, 0.0, 5.0, 0.0, 0.0, -1.0)]
|
|
|
|
|
|
def test_spec_example_local():
|
|
url = IfcUrl.parse("ifc:///home/alice/projects/office@HEAD?path=model.ifc")
|
|
assert url.transport == "local"
|
|
assert url.repo_path == "/home/alice/projects/office"
|
|
assert url.ref == "HEAD"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Error cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_wrong_scheme():
|
|
with pytest.raises(ValueError, match="scheme"):
|
|
IfcUrl.parse("https://example.com/org/repo@HEAD")
|
|
|
|
|
|
def test_missing_ref():
|
|
with pytest.raises(ValueError, match="ref separator"):
|
|
IfcUrl.parse("ifc://example.com/org/repo?path=model.ifc")
|
|
|
|
|
|
def test_camera_wrong_count():
|
|
with pytest.raises(ValueError, match="9 values"):
|
|
IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&camera=1,2,3&fov=60")
|
|
|
|
|
|
def test_camera_without_projection():
|
|
with pytest.raises(ValueError, match="fov.*scale"):
|
|
IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&camera=0,0,0,0,0,1,0,1,0")
|
|
|
|
|
|
def test_fov_and_scale_exclusive():
|
|
with pytest.raises(ValueError, match="mutually exclusive"):
|
|
IfcUrl.parse(
|
|
"ifc://example.com/o/r@HEAD?path=m.ifc"
|
|
"&camera=0,0,0,0,0,1,0,1,0&fov=60&scale=50"
|
|
)
|
|
|
|
|
|
def test_clip_wrong_count():
|
|
with pytest.raises(ValueError, match="6 values"):
|
|
IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&clip=0,0,1")
|
|
|
|
|
|
def test_unknown_visibility():
|
|
with pytest.raises(ValueError, match="visibility"):
|
|
IfcUrl.parse("ifc://example.com/o/r@HEAD?path=m.ifc&visibility=wireframe")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# to_string()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_to_string_roundtrip_simple():
|
|
raw = "ifc://example.com/org/repo@heads/main?path=models/building.ifc"
|
|
assert IfcUrl.parse(raw).to_string() == raw
|
|
|
|
|
|
def test_to_string_roundtrip_ssh():
|
|
raw = "ifc://git@example.com/org/repo@abc123?path=model.ifc"
|
|
assert IfcUrl.parse(raw).to_string() == raw
|
|
|
|
|
|
def test_to_string_roundtrip_local():
|
|
raw = "ifc:///home/alice/project@HEAD?path=model.ifc"
|
|
assert IfcUrl.parse(raw).to_string() == raw
|
|
|
|
|
|
def test_to_string_roundtrip_perspective_camera():
|
|
raw = (
|
|
"ifc://example.com/o/r@heads/main"
|
|
"?path=model.ifc&camera=1.0,2.0,3.0,0.0,0.0,-1.0,0.0,1.0,0.0&fov=60.0"
|
|
)
|
|
url = IfcUrl.parse(raw)
|
|
parsed_back = IfcUrl.parse(url.to_string())
|
|
assert parsed_back.camera == url.camera
|
|
assert parsed_back.fov == url.fov
|
|
assert parsed_back.scale is None
|
|
|
|
|
|
def test_to_string_roundtrip_orthographic_with_clip():
|
|
raw = (
|
|
"ifc://example.com/o/r@tags/v2.0"
|
|
"?path=model.ifc&camera=0.0,0.0,8.0,0.0,0.0,-1.0,0.0,1.0,0.0"
|
|
"&scale=50.0&clip=0.0,0.0,5.0,0.0,0.0,-1.0"
|
|
)
|
|
url = IfcUrl.parse(raw)
|
|
parsed_back = IfcUrl.parse(url.to_string())
|
|
assert parsed_back.scale == 50.0
|
|
assert parsed_back.clips == [(0.0, 0.0, 5.0, 0.0, 0.0, -1.0)]
|
|
|
|
|
|
def test_to_string_selector_encoded():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/o/r@HEAD?path=m.ifc&selector=IfcWall%2BIfcSlab"
|
|
)
|
|
assert "selector=IfcWall%2BIfcSlab" in url.to_string()
|
|
|
|
|
|
def test_to_string_no_params():
|
|
url = IfcUrl.parse("ifc://example.com/org/repo@HEAD?path=m.ifc")
|
|
s = url.to_string()
|
|
assert s.startswith("ifc://example.com/org/repo@HEAD")
|
|
assert "path=m.ifc" in s
|
|
|
|
|
|
def test_to_string_visibility_ghost():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/o/r@HEAD?path=m.ifc"
|
|
"&camera=0.0,0.0,5.0,0.0,0.0,-1.0,0.0,1.0,0.0&fov=60.0&visibility=ghost"
|
|
)
|
|
assert "visibility=ghost" in url.to_string()
|
|
|
|
|
|
def test_to_string_omits_default_visibility():
|
|
url = IfcUrl.parse(
|
|
"ifc://example.com/o/r@HEAD?path=m.ifc"
|
|
"&camera=0.0,0.0,5.0,0.0,0.0,-1.0,0.0,1.0,0.0&fov=60.0"
|
|
)
|
|
assert "visibility" not in url.to_string()
|