DataDesigner/tests/cli/forms/test_field.py
Johnny Greco f8c201e085
chore: update header script to check for diffs (#195)
* update script

* update headers

* refactor a bit and add test script

* update headers

* update for edge case

* update headers

* add step to get file creation date

* use git history to get copyright year

* generation type is printed with inference parameters

* fix unit test
2026-01-09 17:10:58 -05:00

450 lines
16 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from unittest.mock import Mock, patch
import pytest
from data_designer.cli.forms.field import Field, NumericField, SelectField, TextField, ValidationError
# ValidationError tests
def test_validation_error_is_exception() -> None:
"""Test that ValidationError is an exception."""
error = ValidationError("Test error")
assert isinstance(error, Exception)
assert str(error) == "Test error"
# TextField tests - focus on validation behavior
def test_text_field_value_setter_without_validator() -> None:
"""Test setting TextField value without validator succeeds."""
field = TextField(name="name", prompt="Enter name")
field.value = "John Doe"
assert field.value == "John Doe"
def test_text_field_value_setter_with_valid_value() -> None:
"""Test setting TextField value with validator that passes."""
validator = Mock(return_value=(True, None))
field = TextField(name="email", prompt="Enter email", validator=validator)
field.value = "test@example.com"
assert field.value == "test@example.com"
validator.assert_called_once_with("test@example.com")
def test_text_field_value_setter_with_invalid_value() -> None:
"""Test setting TextField value with validator that fails raises ValidationError."""
validator = Mock(return_value=(False, "Invalid format"))
field = TextField(name="email", prompt="Enter email", validator=validator)
with pytest.raises(ValidationError, match="Invalid format"):
field.value = "not-an-email"
assert field.value is None
def test_text_field_validator_receives_string() -> None:
"""Test that validator always receives string values."""
validator = Mock(return_value=(True, None))
field = TextField(name="field", prompt="Enter", validator=validator)
field.value = "text"
validator.assert_called_with("text")
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_prompt_user_returns_input(mock_prompt: Mock) -> None:
"""Test TextField prompt_user returns user input."""
mock_prompt.return_value = "user input"
field = TextField(name="name", prompt="Enter name")
assert field.prompt_user() == "user input"
@patch("data_designer.cli.forms.field.BACK", "BACK_SENTINEL")
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_prompt_user_handles_back_navigation(mock_prompt: Mock) -> None:
"""Test TextField prompt_user properly returns BACK sentinel."""
mock_prompt.return_value = "BACK_SENTINEL"
field = TextField(name="name", prompt="Enter name")
result = field.prompt_user(allow_back=True)
assert result == "BACK_SENTINEL"
# SelectField tests
def test_select_field_value_setter() -> None:
"""Test setting SelectField value."""
options = {"1": "One", "2": "Two"}
field = SelectField(name="number", prompt="Select number", options=options)
field.value = "1"
assert field.value == "1"
@patch("data_designer.cli.forms.field.select_with_arrows")
def test_select_field_prompt_user_returns_selection(mock_select: Mock) -> None:
"""Test SelectField prompt_user returns user selection."""
mock_select.return_value = "opt1"
options = {"opt1": "Option 1", "opt2": "Option 2"}
field = SelectField(name="choice", prompt="Select", options=options)
assert field.prompt_user() == "opt1"
@patch("data_designer.cli.forms.field.BACK", "BACK_SENTINEL")
@patch("data_designer.cli.forms.field.select_with_arrows")
def test_select_field_prompt_user_handles_back_navigation(mock_select: Mock) -> None:
"""Test SelectField prompt_user properly returns BACK sentinel."""
mock_select.return_value = "BACK_SENTINEL"
options = {"1": "One", "2": "Two"}
field = SelectField(name="num", prompt="Select", options=options)
result = field.prompt_user(allow_back=True)
assert result == "BACK_SENTINEL"
# NumericField validator tests - core business logic
def test_numeric_field_validator_valid_value() -> None:
"""Test NumericField validator accepts valid value within range."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
is_valid, error = field.validator("25")
assert is_valid is True
assert error is None
def test_numeric_field_validator_rejects_below_min() -> None:
"""Test NumericField validator rejects value below minimum."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
is_valid, error = field.validator("-5")
assert is_valid is False
assert error == "Value must be between 0.0 and 150.0"
def test_numeric_field_validator_rejects_above_max() -> None:
"""Test NumericField validator rejects value above maximum."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
is_valid, error = field.validator("200")
assert is_valid is False
assert error == "Value must be between 0.0 and 150.0"
def test_numeric_field_validator_only_min_accepts_valid() -> None:
"""Test NumericField validator with only min value accepts valid input."""
field = NumericField(name="count", prompt="Enter count", min_value=10.0)
is_valid, error = field.validator("15")
assert is_valid is True
assert error is None
def test_numeric_field_validator_only_min_rejects_invalid() -> None:
"""Test NumericField validator with only min value rejects invalid input."""
field = NumericField(name="count", prompt="Enter count", min_value=10.0)
is_valid, error = field.validator("5")
assert is_valid is False
assert error == "Value must be >= 10.0"
def test_numeric_field_validator_only_max_accepts_valid() -> None:
"""Test NumericField validator with only max value accepts valid input."""
field = NumericField(name="score", prompt="Enter score", max_value=100.0)
is_valid, error = field.validator("85")
assert is_valid is True
assert error is None
def test_numeric_field_validator_only_max_rejects_invalid() -> None:
"""Test NumericField validator with only max value rejects invalid input."""
field = NumericField(name="score", prompt="Enter score", max_value=100.0)
is_valid, error = field.validator("150")
assert is_valid is False
assert error == "Value must be <= 100.0"
def test_numeric_field_validator_rejects_non_numeric_with_range() -> None:
"""Test NumericField validator rejects non-numeric input when range is set."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
is_valid, error = field.validator("not-a-number")
assert is_valid is False
assert error == "Value must be between 0.0 and 150.0"
def test_numeric_field_validator_rejects_non_numeric_without_range() -> None:
"""Test NumericField validator rejects non-numeric input when no range is set."""
field = NumericField(name="count", prompt="Enter count")
is_valid, error = field.validator("not-a-number")
assert is_valid is False
assert error == "Must be a valid number"
def test_numeric_field_validator_accepts_empty_when_not_required() -> None:
"""Test NumericField validator accepts empty value when field is not required."""
field = NumericField(
name="optional_value",
prompt="Enter value",
required=False,
min_value=0.0,
max_value=100.0,
)
is_valid, error = field.validator("")
assert is_valid is True
assert error is None
def test_numeric_field_validator_handles_boundary_values() -> None:
"""Test NumericField validator accepts boundary values."""
field = NumericField(name="score", prompt="Enter score", min_value=0.0, max_value=100.0)
# Test min boundary
is_valid_min, error_min = field.validator("0.0")
assert is_valid_min is True
assert error_min is None
# Test max boundary
is_valid_max, error_max = field.validator("100.0")
assert is_valid_max is True
assert error_max is None
# NumericField value setter tests with validation
def test_numeric_field_value_setter_accepts_valid() -> None:
"""Test setting NumericField value with valid number succeeds."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
field.value = 25.5
assert field.value == 25.5
def test_numeric_field_value_setter_rejects_invalid() -> None:
"""Test setting NumericField value with invalid number raises ValidationError."""
field = NumericField(name="age", prompt="Enter age", min_value=0.0, max_value=150.0)
with pytest.raises(ValidationError):
field.value = 200.0
assert field.value is None
# NumericField prompt_user tests
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_prompt_user_returns_float(mock_prompt: Mock) -> None:
"""Test NumericField prompt_user converts string to float."""
mock_prompt.return_value = "42"
field = NumericField(name="age", prompt="Enter age")
result = field.prompt_user()
assert result == 42.0
assert isinstance(result, float)
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_prompt_user_returns_none_for_empty(mock_prompt: Mock) -> None:
"""Test NumericField prompt_user returns empty string for empty input on optional field."""
mock_prompt.return_value = ""
field = NumericField(name="optional", prompt="Enter value", required=False)
result = field.prompt_user()
assert result == ""
@patch("data_designer.cli.forms.field.BACK", "BACK_SENTINEL")
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_prompt_user_handles_back_navigation(mock_prompt: Mock) -> None:
"""Test NumericField prompt_user properly returns BACK sentinel."""
mock_prompt.return_value = "BACK_SENTINEL"
field = NumericField(name="value", prompt="Enter value")
result = field.prompt_user(allow_back=True)
assert result == "BACK_SENTINEL"
# Field base class tests - design constraints
def test_field_is_abstract() -> None:
"""Test that Field cannot be instantiated directly."""
with pytest.raises(TypeError):
Field(name="test", prompt="Test prompt") # type: ignore
def test_field_generic_type_preserved() -> None:
"""Test that Field generic type is preserved in subclasses."""
text_field = TextField(name="text", prompt="Enter text")
numeric_field = NumericField(name="num", prompt="Enter number")
text_field.value = "string value"
numeric_field.value = 42.0
assert isinstance(text_field.value, str)
assert isinstance(numeric_field.value, float)
def test_validator_converts_non_string_values() -> None:
"""Test that validator converts non-string values to strings before validation."""
validator = Mock(return_value=(True, None))
field = NumericField(name="num", prompt="Enter number", min_value=0.0, max_value=100.0)
# Override validator to test conversion
field.validator = validator
field.value = 42.5
# Validator should be called with string representation
validator.assert_called_once_with("42.5")
# Tests for clearing values with 'clear' keyword
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_accepts_clear_keyword(mock_prompt: Mock) -> None:
"""Test NumericField accepts 'clear' keyword to remove value."""
mock_prompt.return_value = "clear"
field = NumericField(name="optional", prompt="Enter value", default=42.0, required=False)
result = field.prompt_user()
assert result == ""
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_accepts_none_keyword(mock_prompt: Mock) -> None:
"""Test NumericField accepts 'none' keyword to remove value."""
mock_prompt.return_value = "none"
field = NumericField(name="optional", prompt="Enter value", default=42.0, required=False)
result = field.prompt_user()
assert result == ""
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_accepts_default_keyword(mock_prompt: Mock) -> None:
"""Test NumericField accepts 'default' keyword to remove value."""
mock_prompt.return_value = "default"
field = NumericField(name="optional", prompt="Enter value", default=42.0, required=False)
result = field.prompt_user()
assert result == ""
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_returns_default_for_empty_when_has_default(mock_prompt: Mock) -> None:
"""Test NumericField returns default value when user enters nothing and default exists."""
mock_prompt.return_value = ""
field = NumericField(name="optional", prompt="Enter value", default=42.0, required=False)
result = field.prompt_user()
assert result == 42.0
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_numeric_field_shows_current_label_with_default(mock_prompt: Mock) -> None:
"""Test NumericField shows '(current: X)' instead of '(default: X)' when default exists."""
mock_prompt.return_value = ""
field = NumericField(name="optional", prompt="Enter value", default=42.0, required=False)
field.prompt_user()
# Check that prompt_text_input was called with current value info in the prompt
call_args = mock_prompt.call_args
prompt_arg = call_args[0][0]
assert "current" in prompt_arg.lower()
assert "42.0" in prompt_arg
assert "clear" in prompt_arg.lower()
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_shows_current_label_with_default(mock_prompt: Mock) -> None:
"""Test TextField shows '(current: X)' instead of '(default: X)' when default exists."""
mock_prompt.return_value = ""
field = TextField(name="name", prompt="Enter name", default="test", required=False)
field.prompt_user()
# Check that prompt_text_input was called with current value info in the prompt
call_args = mock_prompt.call_args
prompt_arg = call_args[0][0]
assert "current" in prompt_arg.lower()
assert "test" in prompt_arg
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_returns_default_for_empty_when_has_default(mock_prompt: Mock) -> None:
"""Test TextField returns default value when user enters nothing and default exists."""
mock_prompt.return_value = ""
field = TextField(name="name", prompt="Enter name", default="test", required=False)
result = field.prompt_user()
assert result == "test"
def test_numeric_field_value_setter_converts_empty_string_to_none() -> None:
"""Test NumericField value setter converts empty string to None for optional fields."""
field = NumericField(name="optional", prompt="Enter value", required=False)
field.value = ""
assert field.value is None
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_accepts_clear_keyword(mock_prompt: Mock) -> None:
"""Test TextField accepts 'clear' keyword to remove value."""
mock_prompt.return_value = "clear"
field = TextField(name="optional", prompt="Enter value", default="test", required=False)
result = field.prompt_user()
assert result == ""
@patch("data_designer.cli.forms.field.prompt_text_input")
def test_text_field_shows_clear_instruction_for_optional_with_default(mock_prompt: Mock) -> None:
"""Test TextField shows clear instruction for optional fields with default values."""
mock_prompt.return_value = ""
field = TextField(name="optional", prompt="Enter value", default="test", required=False)
field.prompt_user()
# Check that prompt includes 'clear' instruction
call_args = mock_prompt.call_args
prompt_arg = call_args[0][0]
assert "clear" in prompt_arg.lower()
def test_text_field_value_setter_converts_empty_string_to_none() -> None:
"""Test TextField value setter converts empty string to None for optional fields."""
field = TextField(name="optional", prompt="Enter value", required=False)
field.value = ""
assert field.value is None