Files
impakt/tests/test_web/test_p2_features.py
2026-04-10 16:54:40 -04:00

231 lines
7.3 KiB
Python

"""Tests for Priority 2 web UI features.
Covers: templates, per-channel overrides, corridors, channel values, math expressions.
"""
from pathlib import Path
from typing import Any
import numpy as np
import pytest
from impakt.io.mme import MMEReader
from impakt.web.state import AppState
FIXTURE_DATA = Path(__file__).parent.parent / "fixtures" / "sample_mme"
MME_DATA = Path(__file__).parent.parent / "mme_data"
class TestTemplateManagement:
def test_save_and_apply_template(self, tmp_path):
from impakt.template.library import TemplateLibrary
state = AppState()
state._template_library = TemplateLibrary(tmp_path / "templates")
state.load_test(FIXTURE_DATA)
# Save current state as template
selected = [
"IMPAKT_SYNTH_001::11HEAD0000ACXA",
"IMPAKT_SYNTH_001::11HEAD0000ACYA",
"IMPAKT_SYNTH_001::11HEAD0000ACZA",
]
template = state.save_as_template(
name="Test Head Analysis",
description="Head acceleration channels",
selected_keys=selected,
cfc_value="1000",
x1=0.0,
x2=0.05,
)
assert template.name == "Test Head Analysis"
assert template.default_cfc == 1000
assert len(template.plots) == 1
assert len(template.plots[0].channel_patterns) == 3
# Apply the template
resolved_keys, transforms = state.apply_template("test_head_analysis")
assert len(resolved_keys) == 3
assert transforms["cfc"] == "1000"
assert state.active_template is not None
assert state.active_template.name == "Test Head Analysis"
def test_template_names(self, tmp_path):
from impakt.template.library import TemplateLibrary
state = AppState()
state._template_library = TemplateLibrary(tmp_path / "templates")
assert state.template_names == []
state.load_test(FIXTURE_DATA)
state.save_as_template(
name="Test",
description="",
selected_keys=["IMPAKT_SYNTH_001::11HEAD0000ACXA"],
cfc_value="none",
x1=None,
x2=None,
)
assert "test" in state.template_names
class TestPerChannelOverrides:
def test_set_and_read_override(self):
state = AppState()
state.load_test(FIXTURE_DATA)
key = "IMPAKT_SYNTH_001::11HEAD0000ACXA"
state.channel_overrides[key] = {"cfc": "600"}
assert state.channel_overrides[key]["cfc"] == "600"
def test_override_clears(self):
state = AppState()
state.channel_overrides["key"] = {"cfc": "180"}
state.channel_overrides.pop("key", None)
assert "key" not in state.channel_overrides
class TestCorridors:
def test_add_corridor(self):
state = AppState()
corridor = {
"name": "Test Corridor",
"time": [0.0, 0.01, 0.02, 0.03],
"lower": [-10, -20, -20, -10],
"upper": [10, 20, 20, 10],
"visible": True,
}
state.corridors.append(corridor)
assert len(state.corridors) == 1
assert state.corridors[0]["name"] == "Test Corridor"
class TestChannelValues:
def test_build_channel_values_data(self):
from impakt.web.components.channel_values import build_channel_values_data
state = AppState()
state.load_test(FIXTURE_DATA)
channels = []
for name in ["11HEAD0000ACXA", "11HEAD0000ACYA"]:
ch = state.get_channel("IMPAKT_SYNTH_001", name)
if ch:
channels.append((ch.code.short_label, ch))
rows = build_channel_values_data(channels, hover_x=0.05, x1=0.0, x2=0.1)
assert len(rows) == 2
for row in rows:
assert "ch_num" in row
assert "iso_code" in row
assert "description" in row
assert "unit" in row
assert "min" in row
assert "min_time" in row
assert "max" in row
assert "max_time" in row
assert "x1" in row
assert "x2" in row
assert "cursor" in row
# Channel numbers should be sequential starting at 1
assert rows[0]["ch_num"] == 1
assert rows[1]["ch_num"] == 2
# Values should be parseable as floats
for row in rows:
float(row["min"])
float(row["max"])
float(row["x1"])
float(row["x2"])
float(row["cursor"])
def test_channel_values_no_hover(self):
from impakt.web.components.channel_values import build_channel_values_data
state = AppState()
state.load_test(FIXTURE_DATA)
ch = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACXA")
rows = build_channel_values_data([("Head X", ch)], hover_x=None, x1=0.0, x2=0.1)
assert len(rows) == 1
assert rows[0]["cursor"] == "" # No hover = empty cursor column
@pytest.mark.skipif(not (MME_DATA / "3239").exists(), reason="Real data not available")
def test_channel_values_real_data(self):
from impakt.web.components.channel_values import build_channel_values_data
state = AppState()
state.load_test(MME_DATA / "3239")
ch = state.get_channel("3239", "11HEAD0000H3ACXP")
assert ch is not None
rows = build_channel_values_data([("Head Accel X", ch)], hover_x=0.05, x1=0.0, x2=0.05)
assert len(rows) == 1
# Min should be significant (frontal crash head X accel)
assert abs(float(rows[0]["min"])) > 100
class TestMathExpression:
def test_math_expr_in_state(self):
from impakt.transform.math_expr import math_expr
state = AppState()
state.load_test(FIXTURE_DATA)
ch_x = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACXA")
ch_z = state.get_channel("IMPAKT_SYNTH_001", "11HEAD0000ACZA")
assert ch_x is not None and ch_z is not None
result = math_expr(
expression="sqrt(a**2 + b**2)",
channels={"a": ch_x, "b": ch_z},
name="head_xz_resultant",
unit="g",
)
assert result.name == "head_xz_resultant"
assert result.unit == "g"
assert result.peak > 0
assert len(result.data) == len(ch_x.data)
# Store in primary test
primary = state.primary_test
primary.data._channels["head_xz_resultant"] = result
# Should be retrievable
retrieved = state.get_channel("IMPAKT_SYNTH_001", "head_xz_resultant")
assert retrieved is not None
assert retrieved.peak == result.peak
class TestSessionAutoSave:
def test_save_and_load_session(self, tmp_path):
# Create a minimal MME fixture in tmp_path
import shutil
test_dir = tmp_path / "test_session"
shutil.copytree(FIXTURE_DATA, test_dir)
state = AppState()
state.load_test(test_dir)
# Save session
selected = ["IMPAKT_SYNTH_001::11HEAD0000ACXA"]
state.save_session(selected, "600")
# Verify .impakt directory was created
impakt_dir = test_dir / ".impakt"
assert impakt_dir.exists()
assert (impakt_dir / "session.yaml").exists()
# Load session state
session_data = state.load_session_state()
assert session_data is not None
assert session_data["cfc"] == "600"
assert session_data["selected_channels"] == selected