Files
MyFastHtml/tests/core/data/test_dataservice.py

214 lines
8.1 KiB
Python

"""Unit tests for DataService."""
import pandas as pd
import pytest
from myfasthtml.core.constants import ColumnType
from myfasthtml.core.data.ColumnDefinition import ColumnDefinition
class TestDataInitialisation:
"""Tests for the Data initialisation section of DataService."""
@pytest.fixture
def service(self, dsm):
return dsm.create_service("ns.tbl", save_state=False)
def test_i_can_load_a_dataframe(self, service):
"""load_dataframe() populates the store and column definitions."""
df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]})
service.load_dataframe(df)
assert service.get_store().ne_df is not None
assert service.get_store().ns_total_rows == 2
assert len(service.columns) == 2
def test_i_can_load_an_empty_dataframe(self, service):
"""load_dataframe() with empty DataFrame sets total_rows to 0."""
service.load_dataframe(pd.DataFrame())
assert service.get_store().ns_total_rows == 0
assert service.columns == []
def test_i_can_load_dataframe_without_reinitializing_columns(self, service):
"""load_dataframe(init_columns=False) preserves existing column definitions."""
df = pd.DataFrame({"a": [1]})
service.load_dataframe(df)
original_columns = list(service.columns)
df2 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
service.load_dataframe(df2, init_columns=False)
assert service.columns == original_columns
def test_i_can_load_none_dataframe_without_error(self, service):
"""load_dataframe(None) is a no-op and does not raise.
Why this matters:
- Early return on None protects against uninitialized callers.
- ne_df must remain None (no side effects on the store).
"""
service.load_dataframe(None)
assert service.get_store().ne_df is None
def test_i_can_load_dataframe_with_column_name_normalization(self, service):
"""load_dataframe() normalizes column names to safe IDs via make_safe_id.
Why this matters:
- Columns with spaces or special characters must be accessible as safe IDs.
- make_safe_id lowercases and replaces non-safe characters with underscores.
"""
df = pd.DataFrame({"First Name": ["Alice"], "Last Name": ["Smith"]})
service.load_dataframe(df)
col_ids = [c.col_id for c in service.columns]
assert col_ids == ["first_name", "last_name"]
class TestMutations:
"""Tests for the Mutations section of DataService."""
@pytest.fixture
def service(self, dsm):
svc = dsm.create_service("ns.mutations", save_state=False)
svc.load_dataframe(pd.DataFrame({"value": [1, 2, 3]}))
return svc
def test_i_can_add_a_row(self, service):
"""add_row() appends a row with default values and updates the caches."""
service.add_row()
assert service.get_store().ns_total_rows == 4
assert len(service.get_store().ne_df) == 4
def test_i_can_add_a_row_with_custom_data(self, service):
"""add_row() with explicit data stores the provided values."""
service.add_row(row_data={"value": 99})
assert service.get_store().ne_df.iloc[-1]["value"] == 99
def test_i_can_set_data(self, service):
"""set_data() updates the cell in the DataFrame, fast-access cache, and row data."""
service.set_data("value", 1, 99)
assert service.get_store().ne_df.at[1, "value"] == 99
assert service.get_store().ns_fast_access["value"][1] == 99
assert service.get_store().ns_row_data[1]["value"] == 99
@pytest.mark.parametrize("col_type, expected_default", [
(ColumnType.Text, ""),
(ColumnType.Number, 0),
(ColumnType.Bool, False),
(ColumnType.Datetime, pd.NaT),
(ColumnType.Choice, ""),
(ColumnType.Enum, ""),
(ColumnType.RowSelection_, ""),
])
def test_i_can_add_column_with_correct_default_value(self, service, col_type, expected_default):
"""add_column() creates a DataFrame column with the type-appropriate default value.
Why these assertions matter:
- col_id in ne_df.columns: Confirms the column is materialized in the DataFrame.
- len(columns) == 2: Confirms the column is registered in the metadata.
- default value: Each type has a specific sentinel value; wrong defaults corrupt data.
- pd.isna() for Datetime: pd.NaT does not support equality comparison.
"""
col_def = ColumnDefinition(col_id="__new__", col_index=-1, title="New Col", type=col_type)
service.add_column(col_def)
assert col_def.col_id in service.get_store().ne_df.columns
assert len(service.columns) == 2
actual = service.get_store().ne_df[col_def.col_id].iloc[0]
if pd.isna(expected_default):
assert pd.isna(actual)
else:
assert actual == expected_default
@pytest.mark.parametrize("col_type", [ColumnType.Formula, ColumnType.RowIndex])
def test_i_can_add_virtual_column_without_dataframe_column(self, service, col_type):
"""add_column() with virtual types does not create a DataFrame column.
Why these assertions matter:
- col_id not in ne_df.columns: Virtual columns are computed, not stored in the DataFrame.
- col_index == -1: Sentinel value marking virtual columns.
- len(columns) == 2: Column is registered in the state metadata despite being virtual.
"""
col_def = ColumnDefinition(col_id="__new__", col_index=-1, title="Virtual", type=col_type)
service.add_column(col_def)
assert col_def.col_id not in service.get_store().ne_df.columns
assert col_def.col_index == -1
assert len(service.columns) == 2
def test_i_can_add_row_without_loaded_dataframe_without_error(self, dsm):
"""add_row() is a no-op and does not raise when no DataFrame is loaded."""
service = dsm.create_service("ns.nodf_row", save_state=False)
service.add_row()
assert service.get_store().ne_df is None
def test_i_can_set_data_without_loaded_dataframe_without_error(self, dsm):
"""set_data() is a no-op and does not raise when no DataFrame is loaded."""
service = dsm.create_service("ns.nodf_set", save_state=False)
service.set_data("x", 0, 42)
assert service.get_store().ne_df is None
class TestFormulaManagement:
"""Tests for the Formula management section of DataService."""
@pytest.fixture
def service(self, dsm):
svc = dsm.create_service("ns.formula", save_state=False)
svc.load_dataframe(pd.DataFrame({"a": [1, 2, 3]}))
return svc
def test_i_can_get_table_name(self, service):
"""table_name property returns the value set at creation."""
assert service.table_name == "ns.formula"
def test_i_can_update_table_name(self, service):
"""set_table_name() updates the table name."""
service.set_table_name("ns.new_name")
assert service.table_name == "ns.new_name"
def test_i_can_register_formula(self, service):
"""register_formula() registers a formula in the shared FormulaEngine.
Why these assertions matter:
- has_formula: Confirms the formula was registered in the engine's DAG.
- get_formula_text: Confirms the source expression is stored as-is.
"""
service.register_formula("computed", "{a} + 1")
engine = service.get_formula_engine()
assert engine.has_formula("ns.formula", "computed")
assert engine.get_formula_text("ns.formula", "computed") == "{a} + 1"
def test_i_can_remove_formula(self, service):
"""remove_formula() unregisters a formula from the FormulaEngine."""
service.register_formula("computed", "{a} + 1")
service.remove_formula("computed")
engine = service.get_formula_engine()
assert not engine.has_formula("ns.formula", "computed")
def test_i_cannot_register_invalid_formula(self, service):
"""register_formula() with invalid DSL syntax does not register the formula.
Why this matters:
- parse_formula() raises DSLSyntaxError when it cannot parse the expression.
- register_formula() catches the exception to protect the caller, but the
formula must remain absent from the engine — not silently removed.
"""
service.register_formula("computed", "invalid syntax without braces")
engine = service.get_formula_engine()
assert not engine.has_formula("ns.formula", "computed")