231 lines
7.3 KiB
Python
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
|