step 2: add gauge state sizing and animation model
This commit is contained in:
0
tests/.gitkeep
Normal file
0
tests/.gitkeep
Normal file
117
tests/test_step1_core.py
Normal file
117
tests/test_step1_core.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import pytest
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
from dpg_gauges.exceptions import GaugeConfigError, GaugeNotFoundError, GaugeValueError
|
||||
from dpg_gauges.gauges import AnalogGaugeConfig, DigitalGaugeConfig
|
||||
from dpg_gauges.scales import (
|
||||
clamp_value,
|
||||
clamped_threshold,
|
||||
fill_fraction,
|
||||
format_gauge_value,
|
||||
generate_ticks,
|
||||
normalize_value,
|
||||
value_to_angle,
|
||||
)
|
||||
|
||||
|
||||
def test_public_api_exports_required_names() -> None:
|
||||
required = {
|
||||
"configure",
|
||||
"GaugeTheme",
|
||||
"GaugeStyle",
|
||||
"AnimationConfig",
|
||||
"SmoothingConfig",
|
||||
"ThresholdBand",
|
||||
"GaugeMarker",
|
||||
"StatusState",
|
||||
"GaugeValue",
|
||||
"GaugeStats",
|
||||
"analog_gauge",
|
||||
"digital_gauge",
|
||||
"bar_gauge",
|
||||
"level_gauge",
|
||||
"status_light",
|
||||
"segmented_gauge",
|
||||
"compass_gauge",
|
||||
"thermometer_gauge",
|
||||
"battery_gauge",
|
||||
"multi_value_gauge",
|
||||
"gauge_panel",
|
||||
"gauge_grid",
|
||||
"set_value",
|
||||
"get_value",
|
||||
"update_gauge",
|
||||
"configure_gauge",
|
||||
"set_thresholds",
|
||||
"set_markers",
|
||||
"set_show",
|
||||
"delete_gauge",
|
||||
"get_gauge_debug_state",
|
||||
}
|
||||
|
||||
assert required.issubset(set(dpgg.__all__))
|
||||
for name in required:
|
||||
assert hasattr(dpgg, name)
|
||||
|
||||
|
||||
def test_dataclass_construction_and_context_stub() -> None:
|
||||
threshold = dpgg.ThresholdBand(10, 20, (255, 0, 0, 255), "hot")
|
||||
marker = dpgg.GaugeMarker(50, (0, 255, 0), thickness=1.5)
|
||||
config = AnalogGaugeConfig(
|
||||
label="RPM",
|
||||
min_value=0,
|
||||
max_value=8000,
|
||||
thresholds=[threshold],
|
||||
markers=[marker],
|
||||
animation=True,
|
||||
smoothing=False,
|
||||
)
|
||||
|
||||
assert config.thresholds == (threshold,)
|
||||
assert config.markers == (marker,)
|
||||
assert config.animation == dpgg.AnimationConfig(enabled=True)
|
||||
assert config.smoothing == dpgg.SmoothingConfig(enabled=False)
|
||||
|
||||
with dpgg.digital_gauge(label="Speed", value=12.3) as digital:
|
||||
assert isinstance(digital, DigitalGaugeConfig)
|
||||
assert digital.label == "Speed"
|
||||
|
||||
|
||||
def test_clamp_normalize_angle_and_fill_math() -> None:
|
||||
assert clamp_value(150, 0, 100) == 100
|
||||
assert normalize_value(25, 0, 100) == 0.25
|
||||
assert fill_fraction(-10, 0, 100) == 0
|
||||
assert value_to_angle(50, 0, 100, 225, -45) == 90
|
||||
|
||||
with pytest.raises(GaugeValueError):
|
||||
clamp_value(float("nan"), 0, 100)
|
||||
|
||||
|
||||
def test_tick_generation_and_formatting() -> None:
|
||||
assert generate_ticks(0, 100, 5) == [0, 25, 50, 75, 100]
|
||||
assert format_gauge_value(12.345, precision=1, unit="V") == "12.3 V"
|
||||
assert format_gauge_value(float("inf"), precision=1) == "--"
|
||||
|
||||
with pytest.raises(GaugeConfigError):
|
||||
generate_ticks(0, 100, 1)
|
||||
|
||||
|
||||
def test_threshold_validation_and_clamping() -> None:
|
||||
threshold = dpgg.ThresholdBand(-10, 120, (255, 128, 0, 255))
|
||||
assert clamped_threshold(threshold, 0, 100) == (0, 100)
|
||||
|
||||
with pytest.raises(GaugeConfigError):
|
||||
dpgg.ThresholdBand(20, 10, (255, 0, 0))
|
||||
|
||||
with pytest.raises(GaugeConfigError):
|
||||
AnalogGaugeConfig(min_value=100, max_value=0)
|
||||
|
||||
|
||||
def test_runtime_stubs_import_without_dpg_context() -> None:
|
||||
dpgg.configure(debug=True)
|
||||
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.set_value("missing", 1)
|
||||
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.get_gauge_debug_state("missing")
|
||||
162
tests/test_step2_state_model.py
Normal file
162
tests/test_step2_state_model.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import pytest
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
from dpg_gauges.animation import advance_animation, interpolate, start_animation
|
||||
from dpg_gauges.exceptions import GaugeConfigError, GaugeNotFoundError
|
||||
from dpg_gauges.gauges import DigitalGaugeConfig
|
||||
from dpg_gauges.sizing import (
|
||||
SizeState,
|
||||
effective_size,
|
||||
requested_size,
|
||||
square_gauge_rect,
|
||||
stretch_gauge_rect,
|
||||
update_measured_size,
|
||||
)
|
||||
from dpg_gauges.smoothing import SmoothingState, advance_smoothing, smooth_value
|
||||
from dpg_gauges.state import DirtyFlags, clear_registry, get_gauge_state, register_gauge
|
||||
from dpg_gauges.types import AnimationConfig, SmoothingConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_registry() -> None:
|
||||
clear_registry()
|
||||
dpgg.configure()
|
||||
|
||||
|
||||
def test_registry_register_get_and_delete() -> None:
|
||||
with dpgg.digital_gauge(tag="speed", value=42, min_value=0, max_value=100) as config:
|
||||
assert config.tag == "speed"
|
||||
|
||||
assert dpgg.get_value("speed") == 42
|
||||
state = get_gauge_state("speed")
|
||||
assert state.gauge_type == "digital"
|
||||
assert state.clamped_value == 42
|
||||
assert state.displayed_value == 42
|
||||
|
||||
with pytest.raises(GaugeConfigError):
|
||||
register_gauge("digital", DigitalGaugeConfig(tag="speed"))
|
||||
|
||||
dpgg.delete_gauge("speed")
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.get_value("speed")
|
||||
|
||||
|
||||
def test_value_clamped_displayed_and_dirty_flags_are_separate() -> None:
|
||||
dpgg.digital_gauge(tag="rpm", value=10, min_value=0, max_value=100, animation=True)
|
||||
state = get_gauge_state("rpm")
|
||||
state.dirty = DirtyFlags.NONE
|
||||
|
||||
dpgg.set_value("rpm", 150)
|
||||
|
||||
assert dpgg.get_value("rpm") == 150
|
||||
assert state.clamped_value == 100
|
||||
assert state.displayed_value == 10
|
||||
assert state.animation_state.active
|
||||
assert state.dirty & DirtyFlags.VALUE
|
||||
assert state.dirty & DirtyFlags.DYNAMIC
|
||||
assert state.dirty & DirtyFlags.TEXT
|
||||
|
||||
|
||||
def test_update_gauge_marks_static_text_size_and_style_dirty() -> None:
|
||||
dpgg.digital_gauge(tag="temp", value=20, min_value=0, max_value=100)
|
||||
state = get_gauge_state("temp")
|
||||
state.dirty = DirtyFlags.NONE
|
||||
|
||||
dpgg.update_gauge(
|
||||
"temp",
|
||||
label="Temperature",
|
||||
unit="C",
|
||||
min_value=-40,
|
||||
max_value=140,
|
||||
width=200,
|
||||
style=state.config.style,
|
||||
)
|
||||
|
||||
assert state.config.label == "Temperature"
|
||||
assert state.config.unit == "C"
|
||||
assert state.config.min_value == -40
|
||||
assert state.dirty & DirtyFlags.STATIC
|
||||
assert state.dirty & DirtyFlags.DYNAMIC
|
||||
assert state.dirty & DirtyFlags.TEXT
|
||||
assert state.dirty & DirtyFlags.SIZE
|
||||
assert state.dirty & DirtyFlags.STYLE
|
||||
|
||||
|
||||
def test_threshold_marker_and_show_runtime_updates() -> None:
|
||||
dpgg.digital_gauge(tag="fuel", min_value=0, max_value=100)
|
||||
threshold = dpgg.ThresholdBand(0, 15, (255, 0, 0))
|
||||
marker = dpgg.GaugeMarker(50, (0, 255, 0))
|
||||
|
||||
dpgg.set_thresholds("fuel", [threshold])
|
||||
dpgg.set_markers("fuel", [marker])
|
||||
dpgg.set_show("fuel", False)
|
||||
|
||||
state = get_gauge_state("fuel")
|
||||
assert state.config.thresholds == (threshold,)
|
||||
assert state.config.markers == (marker,)
|
||||
assert state.config.show is False
|
||||
assert state.dirty & DirtyFlags.STATIC
|
||||
assert state.dirty & DirtyFlags.SIZE
|
||||
|
||||
|
||||
def test_non_finite_values_do_not_crash_state_helpers() -> None:
|
||||
dpgg.digital_gauge(tag="boost", min_value=0, max_value=30)
|
||||
|
||||
dpgg.set_value("boost", float("nan"))
|
||||
state = get_gauge_state("boost")
|
||||
|
||||
assert state.target_value != state.target_value
|
||||
assert state.clamped_value is None
|
||||
assert state.displayed_value is None
|
||||
assert state.invalid_value is True
|
||||
|
||||
|
||||
def test_sizing_preserves_last_nonzero_size_and_rects() -> None:
|
||||
size = SizeState(requested=requested_size(100, 80))
|
||||
|
||||
assert update_measured_size(size, 320, 180) is True
|
||||
assert effective_size(size) == (320, 180)
|
||||
|
||||
update_measured_size(size, 0, 0)
|
||||
assert effective_size(size) == (320, 180)
|
||||
assert square_gauge_rect(320, 180).width == 180
|
||||
assert square_gauge_rect(320, 180).x == 70
|
||||
assert stretch_gauge_rect(320, 180).width == 320
|
||||
assert stretch_gauge_rect(320, 180).height == 180
|
||||
|
||||
|
||||
def test_animation_interpolation_and_completion() -> None:
|
||||
config = AnimationConfig(enabled=True, duration=1.0)
|
||||
state = start_animation(0, 100, config, now=10.0)
|
||||
|
||||
assert interpolate(0, 100, 0.25) == 25
|
||||
value, active = advance_animation(state, config, now=10.5)
|
||||
assert value == 50
|
||||
assert active is True
|
||||
|
||||
value, active = advance_animation(state, config, now=11.0)
|
||||
assert value == 100
|
||||
assert active is False
|
||||
|
||||
|
||||
def test_smoothing_progression() -> None:
|
||||
config = SmoothingConfig(enabled=True, alpha=0.5, max_step=10)
|
||||
|
||||
assert smooth_value(None, 100, config) == (100, False)
|
||||
|
||||
state = SmoothingState(value=0)
|
||||
value, active = advance_smoothing(state, 100, config)
|
||||
assert value == 10
|
||||
assert active is True
|
||||
|
||||
|
||||
def test_debug_state_reports_model_fields() -> None:
|
||||
dpgg.digital_gauge(tag="debug", value=5, width=120, height=30)
|
||||
|
||||
debug = dpgg.get_gauge_debug_state("debug")
|
||||
|
||||
assert debug["gauge_type"] == "digital"
|
||||
assert debug["target_value"] == 5
|
||||
assert debug["clamped_value"] == 5
|
||||
assert debug["measured_size"] == (120, 30)
|
||||
assert "VALUE" in debug["dirty_names"]
|
||||
Reference in New Issue
Block a user