Merge remote-tracking branch 'origin/main' into HEAD
Docs / docs (push) Successful in 36s
Playwright Tests / test-playwright (1, 2) (push) Successful in 4m11s
Playwright Tests / test-playwright (2, 2) (push) Successful in 2m34s
pre-commit / pre-commit (push) Successful in 3m35s
Test Backend / test-backend (3.12) (push) Failing after 6m25s
Test Backend / test-backend (3.14) (push) Failing after 6m2s
Compose Smoke Test / test-compose (push) Successful in 38s
Playwright Tests / merge-reports (push) Successful in 1m52s

This commit is contained in:
2026-11-10 18:32:06 +01:00
29 changed files with 796 additions and 132 deletions
+21
View File
@@ -5,6 +5,7 @@ from datetime import UTC, datetime
import pytest
from fluksio.flow.dashboards import (
CHART_RANGES,
DashboardDef,
DashboardNotFound,
DashboardStore,
@@ -354,6 +355,26 @@ def test_a_querying_chart_asks_with_a_record_and_draws_a_series():
WidgetDef(id="c", type="chart", config=query_chart(request_dtype="json"))
def test_a_querying_chart_offers_its_own_ranges():
"""A few labelled windows, where ``range_s: 0`` is the whole history."""
daily = [
{"label": "1M", "range_s": 30 * 86400, "interval_s": 86400},
{"label": "All", "range_s": 0, "interval_s": 86400},
]
WidgetDef(id="c", type="chart", config=query_chart(ranges=daily))
for bad in (
[{"label": "", "range_s": 3600, "interval_s": 60}],
[{"label": "1h", "range_s": -1, "interval_s": 60}],
[{"label": "1h", "range_s": 3600, "interval_s": 0}],
[{"label": "1h", "range_s": 3600.5, "interval_s": 60}],
daily * CHART_RANGES,
"1M",
):
with pytest.raises(ValueError, match="range"):
WidgetDef(id="c", type="chart", config=query_chart(ranges=bad))
def test_a_colour_widget_publishes_the_shape_its_format_names():
"""The format picks the payload, so the binding is held to that one."""
wheel = WidgetDef(
+49 -2
View File
@@ -5,13 +5,14 @@ service stops claiming and its timers once claimed work has finished. Both
wait without a limit and can be aborted.
"""
import asyncio
import threading
import time
import pytest
from fluksio.flow import executor
from fluksio.flow.controller import FlowController, LoadedNode
from fluksio.flow.controller import FlowController, LoadedNode, RebuildBusy
from fluksio.flow.executor import ExecutionService
from fluksio.flow.messages import DType, MessageSpec
from fluksio.flow.namespace import Rename
@@ -125,8 +126,54 @@ class SlowClaim(MemoryWorkQueue):
return items
def test_pause_intake_waits_out_a_claim_already_under_way(monkeypatch):
seen: list = []
queue = SlowClaim()
_, service, source = _engine(seen.append, queue)
wake = service._capacity_changed
def closing() -> None:
wake()
# The claim lands only once the intake has closed.
queue.go.set()
monkeypatch.setattr(service, "_capacity_changed", closing)
service.start()
try:
source.inject({"reading": 1.0})
assert queue.claiming.wait(5)
service.pause_intake()
assert seen == [1.0]
assert service.inflight == 0
finally:
queue.go.set()
service.stop()
def test_a_reload_that_cannot_drain_leaves_the_pipeline_as_it_was(
tmp_path, monkeypatch
):
monkeypatch.setattr(executor, "DRAIN_TIMEOUT_S", 0.05)
seen: list = []
running, finish, consume = _blocking(seen)
pipeline, service, source = _engine(consume)
controller = FlowController(FlowStore(tmp_path / "flows"), execution=service)
controller.pipeline = pipeline
service.start()
try:
source.inject({"reading": 1.0})
assert running.wait(5)
with pytest.raises(RebuildBusy):
asyncio.run(controller.reload())
assert controller.pipeline is pipeline
assert service._pipeline is pipeline
assert service._intake.is_set()
finally:
finish.set()
service.stop()
def test_a_hold_waits_out_a_claim_already_under_way():
"""`pause_intake` returns here and the item runs after it."""
seen: list = []
queue = SlowClaim()
_, service, source = _engine(seen.append, queue)
+54
View File
@@ -0,0 +1,54 @@
"""RedisState against a real server, enabled by FLUKSIO_TEST_REDIS_HOST (and
optional PORT)."""
import os
import uuid
import pytest
from fluksio.flow.state import RedisState
@pytest.fixture
def state():
host = os.environ.get("FLUKSIO_TEST_REDIS_HOST")
if not host:
pytest.skip("Set FLUKSIO_TEST_REDIS_HOST to run Redis integration checks")
port = int(os.environ.get("FLUKSIO_TEST_REDIS_PORT", "6379"))
state = RedisState(host, port, namespace=f"test-state-{uuid.uuid4().hex}", ttl=600)
try:
yield state
finally:
state.clear()
state.close()
def test_a_ttl_expires_counters_and_the_names_set(state):
state.set("heat.temp", 21)
state.increment("__version__:heat.a")
state.increment_multi(["__version__:heat.b"])
state.record({"heat.v": 1}, {}, ["__version__:heat.c"], 0.0)
keys = state.keys()
assert "__names__" in keys and "__version__:heat.c" in keys
assert all(state._client.ttl(state._key(k)) > 0 for k in keys)
def test_a_compare_and_swap_names_what_it_wrote(state):
state.set("heat.a", 1)
assert state.compare_and_swap_multi({"heat.a": 1}, {"heat.b": 2})
assert "heat.b" in state.message_names()
def test_a_compare_and_swap_loses_to_a_write_after_its_read(state, monkeypatch):
state.set("heat.a", 1)
note = RedisState._note
def racing_note(self, pipe, keys):
# Another writer lands between the compare and EXEC.
self._client.set(self._key("heat.a"), b"2")
note(self, pipe, keys)
monkeypatch.setattr(RedisState, "_note", racing_note)
assert not state.compare_and_swap_multi({"heat.a": 1}, {"heat.a": 3})
assert state.get("heat.a") == 2
+10
View File
@@ -346,6 +346,16 @@ def test_a_rate_limited_port_cannot_be_run_as_a_batch():
assert not batch_issues(double_flow())
def test_a_delay_nodes_interval_cannot_be_run_as_a_batch():
flow = double_flow()
flow.nodes[0].type = "delay"
flow.nodes[0].params = {"interval": 5}
# The node puts it on every output, where it holds values back the same way.
assert batch_issues(flow)
flow.nodes[0].params = {"delay": 5}
assert not batch_issues(flow)
def test_a_node_with_no_stored_code_cannot_be_run():
"""The store answers a template for one, and a template publishes nothing."""