Files
production-analytics/tests/test_channel_material_runner.py
T

142 lines
5.2 KiB
Python

import io
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import Mock, patch
from production_analytics.calculations.channel_config import load_channel_material_calculation
from production_analytics.calculations.material_consumption import MaterialIntegrationState
from production_analytics.cli.__main__ import _parser, main
from production_analytics.enlyze.gateway import EnlyzeProductionRun
from production_analytics.service.channel_material import ChannelPollResult
from production_analytics.service.channel_material_runner import ChannelMaterialPollingRunner
from production_analytics.service.channel_material_runtime import build_channel_material_runner
EXAMPLE = (
Path(__file__).resolve().parents[1] / "config/e1-channel-material-consumption.example.yaml"
)
NOW = datetime(2026, 9, 1, tzinfo=UTC)
def test_cli_channel_material_arguments():
args = _parser().parse_args(
[
"run",
"channel-material-poll",
"--config",
str(EXAMPLE),
"--poll-interval-seconds",
"15",
"--state-directory",
"state",
"--secrets-file",
"secrets.env",
"--once",
]
)
assert args.command == "channel-material-poll"
assert args.config == EXAMPLE
assert args.poll_interval_seconds == 15
assert args.state_directory == Path("state")
assert args.secrets_file == Path("secrets.env")
assert args.once
def test_runtime_constructs_channel_service(tmp_path, monkeypatch):
monkeypatch.setenv("ENLYZE_BASE_URL", "https://example.invalid/api/")
for key, value in {
"HOST": "localhost",
"PORT": "5432",
"DB": "analytics",
"USER": "user",
"PASSWORD": "environment-password",
}.items():
monkeypatch.setenv(f"POSTGRES_{key}", value)
secrets = tmp_path / "secrets.env"
secrets.write_text("ENLYZE_API_KEY=secret-from-file\nPOSTGRES_PASSWORD=file-password\n")
calculation = load_channel_material_calculation(EXAMPLE)
with patch(
"production_analytics.service.channel_material_runtime.ChannelMaterialPollingService"
) as service:
runner = build_channel_material_runner(
calculation,
poll_interval_seconds=7,
state_directory=tmp_path / "state",
secrets_file=secrets,
)
kwargs = service.call_args.kwargs
assert kwargs["machine_id"] == calculation.machine_ref
assert kwargs["channels"] == calculation.channels
assert kwargs["max_sample_gap_seconds"] == calculation.max_sample_gap_seconds
assert kwargs["percentage_tolerance"] == calculation.percentage_tolerance
assert kwargs["material_names"] == calculation.material_names
assert kwargs["gateway"]._client._settings.api_key == "secret-from-file"
assert kwargs["state_store"].directory == tmp_path / "state"
assert runner.service is service.return_value
assert runner.interval == 7
assert runner.snapshot_writer.settings.password == "file-password"
def test_once_executes_one_cycle_and_exits(capsys):
with patch(
"production_analytics.service.channel_material_runtime.build_channel_material_runner"
) as build:
assert main(["run", "channel-material-poll", "--config", str(EXAMPLE), "--once"]) == 0
build.return_value.run_once.assert_called_once_with()
build.return_value.run.assert_not_called()
assert capsys.readouterr().err == ""
def test_no_snapshots_are_written_without_eligible_production_run():
writer = Mock()
output = io.StringIO()
runner = ChannelMaterialPollingRunner(
Mock(poll_once=Mock(return_value=None)),
machine_id="machine",
calculation_id="calculation",
snapshot_writer=writer,
poll_interval_seconds=10,
clock=lambda: NOW,
stdout=output,
)
assert not runner.run_once()
writer.write.assert_not_called()
assert "snapshots=0" in output.getvalue()
def test_one_cycle_writes_each_channel_snapshot():
run = EnlyzeProductionRun("run", "machine", None, "order", NOW, None)
first = ChannelPollResult(
run, "Ex1", "C1", MaterialIntegrationState(1.5, 10), "42", "Material", "MAPPED", True
)
second = ChannelPollResult(
run, "Ex1", "C2", MaterialIntegrationState(2.5, 10), None, None, "AMBIGUOUS", False
)
writer = Mock()
output = io.StringIO()
runner = ChannelMaterialPollingRunner(
Mock(poll_once=Mock(return_value=(first, second))),
machine_id="machine",
calculation_id="calculation",
snapshot_writer=writer,
poll_interval_seconds=10,
clock=lambda: NOW,
stdout=output,
)
assert runner.run_once()
assert writer.write.call_count == 2
assert writer.write.call_args_list[0].kwargs == {
"timestamp": NOW,
"calculation_id": "calculation",
"machine_id": "machine",
"extruder": "Ex1",
"channel": "C1",
"production_order": "order",
"run_id": "run",
"cumulative_consumption_kg": 1.5,
"material_number": "42",
"material_name": "Material",
"material_mapping_status": "MAPPED",
"percentage_sum_valid": True,
}
assert "channel snapshots=2 written" in output.getvalue()