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
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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user