DELETE /flows/library/{name} still answers 409 while nodes use the source.
With detach=true each of them keeps a copy of the code, in the published
flow and the draft, the source is deleted and the running users rebuild.
147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
"""Nodes shared across flows: one source, many instances."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fluksio.flow.controller import FlowController
|
|
from fluksio.flow.messages import MessageSpec
|
|
from fluksio.flow.schemas import FlowDef, NodeDef
|
|
from fluksio.flow.store import FlowStore, LibExists, LibNotFound
|
|
|
|
SOURCE = "def process():\n return {'temp': 1}\n"
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> FlowStore:
|
|
return FlowStore(tmp_path / "flows")
|
|
|
|
|
|
def a_flow(name: str = "heating") -> FlowDef:
|
|
return FlowDef(
|
|
name=name,
|
|
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
|
|
)
|
|
|
|
|
|
def test_sharing_moves_the_source_and_points_the_node_at_it(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
|
|
assert store.list_lib() == ["read_temp"]
|
|
assert store.read_lib_source("read_temp") == SOURCE
|
|
node = store.read_flow("heating", draft=True).nodes[0]
|
|
assert node.source_ref == "read_temp"
|
|
# The private copy is gone; the library one is what it runs.
|
|
assert not (store.root / "heating" / "nodes" / "sensor.py").exists()
|
|
|
|
|
|
def test_the_library_is_not_mistaken_for_a_flow(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
|
|
assert store.list_flows() == ["heating"]
|
|
assert [flow.name for flow in store.read_all()] == ["heating"]
|
|
|
|
|
|
def test_a_second_flow_can_use_the_same_source(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
|
|
store.write_flow(
|
|
FlowDef(
|
|
name="cooling",
|
|
nodes=[
|
|
NodeDef(
|
|
id="sensor",
|
|
source_ref="read_temp",
|
|
provides=[MessageSpec(name="temp")],
|
|
)
|
|
],
|
|
)
|
|
)
|
|
|
|
assert store.usages("read_temp") == ["cooling.sensor", "heating.sensor"]
|
|
|
|
|
|
def test_a_shared_source_change_rebuilds_only_what_is_published(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
# Published with the shared node, which the draft has since dropped.
|
|
store.publish_flow("heating", store.read_flow("heating", draft=True).version)
|
|
store.unshare_node("heating", "sensor")
|
|
# Shared in a draft that was never published.
|
|
cooling = a_flow("cooling")
|
|
cooling.nodes[0].source_ref = "read_temp"
|
|
store.write_draft(cooling, 0)
|
|
|
|
controller = FlowController(store)
|
|
rebuilt: list[str] = []
|
|
|
|
async def reload_flow(name: str) -> None:
|
|
rebuilt.append(name)
|
|
|
|
controller.reload_flow = reload_flow # type: ignore[method-assign]
|
|
asyncio.run(controller.reload_lib_users("read_temp"))
|
|
|
|
assert rebuilt == ["heating"]
|
|
|
|
|
|
def test_a_shared_name_is_not_taken_twice(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
|
|
store.write_flow(FlowDef(name="cooling", nodes=[NodeDef(id="sensor")]))
|
|
with pytest.raises(LibExists):
|
|
store.share_node("cooling", "sensor", "read_temp")
|
|
|
|
|
|
def test_unsharing_takes_a_private_copy_back(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
|
|
store.unshare_node("heating", "sensor")
|
|
|
|
node = store.read_flow("heating", draft=True).nodes[0]
|
|
assert node.source_ref is None
|
|
assert store.read_node_source("heating", "sensor", draft=True) == SOURCE
|
|
# The library keeps its copy for whoever else is using it.
|
|
assert store.list_lib() == ["read_temp"]
|
|
|
|
|
|
def test_a_missing_shared_source_is_reported(store: FlowStore):
|
|
with pytest.raises(LibNotFound):
|
|
store.read_lib_source("nothing_here")
|
|
|
|
|
|
def test_retiring_a_shared_source_leaves_each_user_a_copy(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", SOURCE)
|
|
store.share_node("heating", "sensor", "read_temp")
|
|
store.publish_flow("heating", store.read_flow("heating", draft=True).version)
|
|
version = store.read_flow("heating").version
|
|
# Shared in a draft that was never published.
|
|
cooling = a_flow("cooling")
|
|
cooling.nodes[0].source_ref = "read_temp"
|
|
store.write_draft(cooling, 0)
|
|
|
|
store.detach_lib("read_temp")
|
|
|
|
assert store.list_lib() == []
|
|
heating = store.read_flow("heating")
|
|
assert heating.nodes[0].source_ref is None
|
|
assert heating.version == version + 1
|
|
assert store.read_node_source("heating", "sensor") == SOURCE
|
|
# What runs is the same code, so there is nothing to publish.
|
|
assert not store.has_draft("heating")
|
|
assert store.read_flow("cooling", draft=True).nodes[0].source_ref is None
|
|
assert store.read_node_source("cooling", "sensor", draft=True) == SOURCE
|