Add channel material and downtime analytics
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user