165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
import json
|
|
import os
|
|
import subprocess
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fluksio.flow.messages import MessageSpec
|
|
from fluksio.flow.schemas import FlowDef, NodeDef
|
|
from fluksio.flow.store import FlowNotFound, FlowStore
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> FlowStore:
|
|
return FlowStore(tmp_path / "flows")
|
|
|
|
|
|
def commit_count(store: FlowStore) -> int:
|
|
result = subprocess.run(
|
|
["git", "-C", str(store.root), "rev-list", "--count", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return int(result.stdout.strip())
|
|
|
|
|
|
def a_flow() -> FlowDef:
|
|
return FlowDef(
|
|
name="heating",
|
|
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
|
|
)
|
|
|
|
|
|
def test_flow_round_trips(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
|
|
assert store.list_flows() == ["heating"]
|
|
assert store.read_flow("heating").nodes[0].provides[0].name == "temp"
|
|
|
|
|
|
def test_every_change_is_committed(store: FlowStore):
|
|
before = commit_count(store)
|
|
|
|
store.write_flow(a_flow())
|
|
assert commit_count(store) == before + 1
|
|
|
|
store.write_node_source("heating", "sensor", "def process():\n return {}\n")
|
|
assert commit_count(store) == before + 2
|
|
|
|
|
|
def test_saving_unchanged_content_does_nothing(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
commits = commit_count(store)
|
|
|
|
# Autosave repeats the same document; history should not grow.
|
|
assert store.write_flow(a_flow()) is False
|
|
assert commit_count(store) == commits
|
|
|
|
|
|
def test_a_flow_is_modified_when_only_its_node_code_is(store: FlowStore):
|
|
"""Node code is saved without touching the flow document."""
|
|
store.write_flow(a_flow())
|
|
# Backdated so the answer can only have come from the node file.
|
|
os.utime(store.root / "heating" / "flow.json", (0, 0))
|
|
|
|
store.write_node_source(
|
|
"heating", "sensor", "def process():\n return {'a': 1}\n"
|
|
)
|
|
|
|
updated = store.updated_at("heating")
|
|
assert updated is not None and updated > datetime.fromtimestamp(0, UTC)
|
|
|
|
|
|
def test_a_flow_that_is_not_there_has_no_modified_time(store: FlowStore):
|
|
assert store.updated_at("nope") is None
|
|
|
|
|
|
def test_missing_flow_is_reported(store: FlowStore):
|
|
with pytest.raises(FlowNotFound):
|
|
store.read_flow("nope")
|
|
|
|
|
|
def test_deleting_removes_flow_and_its_nodes(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", "def process():\n return {}\n")
|
|
|
|
store.delete_flow("heating")
|
|
|
|
assert store.list_flows() == []
|
|
assert not (store.root / "heating").exists()
|
|
|
|
|
|
def test_opening_the_store_renames_trigger_nodes_to_debounce(store: FlowStore):
|
|
"""The old name has no alias, so what is stored under it is rewritten once."""
|
|
store.write_flow(
|
|
FlowDef(
|
|
name="hall",
|
|
nodes=[NodeDef(id="hold", type="trigger"), NodeDef(id="lamp")],
|
|
)
|
|
)
|
|
store.write_draft(
|
|
FlowDef(name="hall", nodes=[NodeDef(id="hold", type="trigger")]),
|
|
base_version=1,
|
|
)
|
|
commits = commit_count(store)
|
|
|
|
reopened = FlowStore(store.root)
|
|
|
|
published = reopened.read_flow("hall")
|
|
draft = reopened.read_flow("hall", draft=True)
|
|
assert [node.type for node in published.nodes] == ["debounce", "python"]
|
|
assert [node.type for node in draft.nodes] == ["debounce"]
|
|
# Bumped, so an editor holding the old draft cannot save `trigger` back.
|
|
assert (published.version, draft.version) == (2, 3)
|
|
assert commit_count(store) == commits + 1
|
|
|
|
FlowStore(store.root)
|
|
assert commit_count(store) == commits + 1
|
|
|
|
|
|
def test_opening_the_store_renames_the_mqtt_publish_queue_size(store: FlowStore):
|
|
"""The mqtt setting is now `send_queue_size`, with no alias for the old name."""
|
|
relay = NodeDef(id="relay", type="mqtt", params={"publish_queue_size": 8})
|
|
store.write_flow(FlowDef(name="hall", nodes=[relay]))
|
|
commits = commit_count(store)
|
|
|
|
reopened = FlowStore(store.root)
|
|
|
|
published = reopened.read_flow("hall")
|
|
assert published.nodes[0].params == {"send_queue_size": 8}
|
|
assert published.version == 2
|
|
assert commit_count(store) == commits + 1
|
|
|
|
|
|
def test_opening_the_store_renames_a_nodes_device_to_worker(store: FlowStore):
|
|
"""`device` names a paired device now, so a node's worker label moves over."""
|
|
store.write_flow(FlowDef(name="lab", nodes=[NodeDef(id="fit")]))
|
|
path = store.root / "lab" / "flow.json"
|
|
data = json.loads(path.read_text())
|
|
node = data["nodes"][0]
|
|
del node["worker"], node["worker_policy"]
|
|
node.update(device="gpu", device_policy="prefer")
|
|
path.write_text(json.dumps(data))
|
|
|
|
published = FlowStore(store.root).read_flow("lab")
|
|
|
|
assert (published.nodes[0].worker, published.nodes[0].worker_policy) == (
|
|
"gpu",
|
|
"prefer",
|
|
)
|
|
assert published.version == 2
|
|
|
|
|
|
def test_a_client_from_before_the_rename_still_labels_its_workers():
|
|
"""An older `fluksio sync` or SDK sends `device`, read as `worker` for a release."""
|
|
node = NodeDef.model_validate(
|
|
{"id": "fit", "device": "gpu", "device_policy": "prefer"}
|
|
)
|
|
assert (node.worker, node.worker_policy) == ("gpu", "prefer")
|
|
# Beside the new key, the new key wins.
|
|
both = NodeDef.model_validate({"id": "fit", "device": "cpu", "worker": "gpu"})
|
|
assert both.worker == "gpu"
|