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 FlowExists, 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_renaming_a_flow_carries_its_nodes(store: FlowStore): store.write_flow(a_flow()) store.write_node_source("heating", "sensor", "def process():\n return {}\n") renamed = store.rename_flow("heating", "warmth") assert renamed.name == "warmth" assert store.list_flows() == ["warmth"] assert "def process" in store.read_node_source("warmth", "sensor") def test_renaming_a_flow_repoints_the_flows_reading_it(store: FlowStore): store.write_flow(a_flow()) store.write_flow( FlowDef( name="display", nodes=[ NodeDef( id="gauge", # Reads across the flow boundary, so the name must follow. requires=[MessageSpec(name="heating.temp")], ) ], ) ) store.rename_flow("heating", "warmth") display = store.read_flow("display") assert display.nodes[0].requires[0].name == "warmth.temp" def test_renaming_onto_an_existing_name_is_refused(store: FlowStore): store.write_flow(a_flow()) store.write_flow(FlowDef(name="warmth")) with pytest.raises(FlowExists): store.rename_flow("heating", "warmth") assert store.list_flows() == ["heating", "warmth"] 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