123 lines
5.5 KiB
Python
123 lines
5.5 KiB
Python
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from production_analytics.calculations.config import (
|
|
CalculationConfigError,
|
|
load_material_calculation,
|
|
)
|
|
from production_analytics.calculations.material_consumption import (
|
|
MaterialConsumptionIntegrator,
|
|
MaterialIntegratorConfig,
|
|
rotational_discharge_rate_kg_per_hour,
|
|
)
|
|
from production_analytics.enlyze.gateway import EnlyzeApiGateway
|
|
from production_analytics.service.material_runtime import build_material_runner
|
|
|
|
|
|
@pytest.mark.parametrize('speeds,width,factor,expected', [
|
|
([3.739, 3.956], 5, 3.12, 7202.52),
|
|
([3.739, 3.956], 4.85, 3.12, 6986.4444),
|
|
([2], 4, 1.5, 720),
|
|
([2, 3, 4], 4, 1.5, 3240),
|
|
])
|
|
def test_conversion(speeds, width, factor, expected):
|
|
assert rotational_discharge_rate_kg_per_hour(speeds, width, factor) == pytest.approx(expected)
|
|
|
|
|
|
@pytest.mark.parametrize('speeds,width,factor', [
|
|
([], 5, 3.12), ([float('nan')], 5, 3.12), ([1], 0, 3.12),
|
|
([1], float('inf'), 3.12), ([1], 5, 0), ([1], 5, float('nan')),
|
|
([1e308], 5, 3.12),
|
|
])
|
|
def test_invalid_conversion(speeds, width, factor):
|
|
with pytest.raises(ValueError):
|
|
rotational_discharge_rate_kg_per_hour(speeds, width, factor)
|
|
|
|
|
|
@pytest.mark.parametrize('gate_id', ['speed', None])
|
|
def test_gate_and_gap(gate_id):
|
|
start = datetime(2026, 8, 25, tzinfo=UTC)
|
|
times = [0, 10, 20, 40, 61, 71, 81]
|
|
gates = [2, 0.3, 0, 8, 1, 1, 1]
|
|
client = Mock()
|
|
client.post_json.return_value.body = {'data': {
|
|
'columns': ['time', 'right', 'left'] + ([gate_id] if gate_id else []),
|
|
'records': [[(start + timedelta(seconds=t)).isoformat(), 3.739, 3.956]
|
|
+ ([gate] if gate_id else []) for t, gate in zip(times, gates, strict=True)],
|
|
}}
|
|
samples = EnlyzeApiGateway(client).get_material_samples(
|
|
machine_id='m', rate_variable_id='unused', gate_variable_id=gate_id,
|
|
rotational_speed_variable_ids=('right', 'left'), nominal_width_m=5,
|
|
specific_discharge_kg_per_rev_m=3.12, start=start, end=start + timedelta(seconds=81),
|
|
)
|
|
assert all(s.material_rate_kg_per_hour == pytest.approx(7202.52) for s in samples)
|
|
state = MaterialConsumptionIntegrator(MaterialIntegratorConfig(0.3, 20)).process_many(samples)
|
|
seconds = 30 if gate_id else 60
|
|
assert state.integrated_running_seconds == seconds
|
|
assert state.cumulative_consumption_kg == pytest.approx(120.042 * seconds / 60)
|
|
assert client.post_json.call_args.args[1]['variables'] == [
|
|
{'uuid': ref} for ref in ['right', 'left'] + ([gate_id] if gate_id else [])]
|
|
|
|
|
|
@pytest.mark.parametrize('field,value', [
|
|
('specific_discharge_kg_per_rev_m', None), ('specific_discharge_kg_per_rev_m', 0),
|
|
('specific_discharge_kg_per_rev_m', True), ('specific_discharge_kg_per_rev_m', float('inf')),
|
|
('application_signal_refs', ['set']), ('rotational_speed_signal_refs', ['']),
|
|
])
|
|
def test_invalid_config(tmp_path, field, value):
|
|
document = yaml.safe_load(open('config/bento1-material-consumption.yaml'))
|
|
document['calculations'][0][field] = value
|
|
path = tmp_path / 'config.yaml'
|
|
path.write_text(yaml.safe_dump(document))
|
|
with pytest.raises(CalculationConfigError):
|
|
load_material_calculation(path)
|
|
|
|
|
|
def test_optional_gate_config(tmp_path):
|
|
document = yaml.safe_load(open('config/bento1-material-consumption.yaml'))
|
|
del document['calculations'][0]['process_application_calculation_id']
|
|
del document['calculations'][0]['gate_signal_ref']
|
|
del document['calculations'][0]['gate_threshold']
|
|
path = tmp_path / 'config.yaml'
|
|
path.write_text(yaml.safe_dump(document))
|
|
(tmp_path / 'material-calibrations.yaml').write_text(
|
|
Path('config/material-calibrations.yaml').read_text())
|
|
config = load_material_calculation(path)
|
|
assert config.gate_signal_ref is None
|
|
assert config.gate_threshold == 0
|
|
|
|
|
|
def test_bento_runtime_wiring(tmp_path, monkeypatch):
|
|
monkeypatch.setenv('ENLYZE_BASE_URL', 'https://example.invalid/api/')
|
|
for key, value in dict(HOST='localhost', PORT='5432', DB='analytics',
|
|
USER='user', PASSWORD='test').items():
|
|
monkeypatch.setenv(f'POSTGRES_{key}', value)
|
|
secrets = tmp_path / 'secret.env'
|
|
secrets.write_text('ENLYZE_API_KEY=test\n')
|
|
config = load_material_calculation('config/bento1-material-consumption.yaml')
|
|
with patch('production_analytics.service.material_runtime.ErpSettings') as erp_settings:
|
|
runner = build_material_runner(
|
|
config, poll_interval_seconds=10, state_directory=tmp_path / 'state',
|
|
secrets_file=secrets, erp_secrets_file=tmp_path / 'erp.env',
|
|
)
|
|
service = runner.service
|
|
assert config.source_mode == 'rotational_discharge'
|
|
assert service._process_application_calculation_id == 'bento1-fresh-bentonite-application'
|
|
from production_analytics.service.postgres_material_application import (
|
|
PostgresMaterialApplicationWriter,
|
|
)
|
|
assert isinstance(service._application_writer, PostgresMaterialApplicationWriter)
|
|
assert service._rotational_speed_variable_ids == (
|
|
'6e5d2d94-98f9-4cc1-8a88-7987c6282525', 'cd7385c4-337b-4759-ab32-45d65beaf190',
|
|
)
|
|
assert service._application_variable_ids == ()
|
|
assert service._specific_discharge == 2.75
|
|
assert service._gate_variable_id == 'fef41976-1103-4090-b780-eaeecc02fdfa'
|
|
assert service._config == MaterialIntegratorConfig(0.3, 20)
|
|
assert service._nominal_width_provider.workplace == 'Bento 1'
|
|
erp_settings.from_secret_file.assert_called_once_with(tmp_path / 'erp.env')
|