214 lines
8.1 KiB
Python
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")
|