madlad-400-translate / tests /test_app.py
Daryl Lim
test: clarify input-buttons test docstring
639e59b
Raw
History Blame Contribute Delete
13.5 kB
from unittest.mock import patch
import pytest
import torch
# --- Fast tests (no model loading required) ---
def test_app_imports_without_model_load():
"""Importing app should not trigger model download."""
import app
assert hasattr(app, "translate")
assert hasattr(app, "_load_tokenizer")
assert hasattr(app, "_load_model")
def test_app_has_main_function():
"""main() should be callable (UI construction)."""
import app
assert callable(app.main)
def test_translate_accepts_generation_params():
"""translate() signature must accept generation parameters."""
import inspect
import app
sig = inspect.signature(app.translate)
params = list(sig.parameters.keys())
assert "max_new_tokens" in params
assert "num_beams" in params
assert "temperature" in params
def test_translate_signature_defaults():
"""translate() should have correct default values for generation parameters."""
import inspect
import app
sig = inspect.signature(app.translate)
assert sig.parameters["max_new_tokens"].default == 512
assert sig.parameters["num_beams"].default == 1
assert sig.parameters["temperature"].default == 1.0
def test_get_device_returns_cpu_when_no_cuda():
"""_get_device() should return CPU when CUDA is not available."""
import app
with patch("app.torch.cuda.is_available", return_value=False):
with pytest.warns(UserWarning):
device = app._get_device()
assert device.type == "cpu"
def test_get_device_warns_on_cpu():
"""_get_device() should emit a UserWarning when falling back to CPU."""
import app
with patch("app.torch.cuda.is_available", return_value=False):
with pytest.warns(UserWarning, match="No GPU available"):
app._get_device()
# --- UI component tests ---
@pytest.fixture
def demo():
import app
return app._build_demo()
def test_demo_has_no_tabs(demo):
"""Redesigned UI should have no Tab components."""
tabs = [b for b in demo.blocks.values() if type(b).__name__ == "Tab"]
assert len(tabs) == 0, f"Expected no tabs, found {len(tabs)}"
def test_demo_has_no_sliders(demo):
"""Redesigned UI should have no Slider components."""
sliders = [b for b in demo.blocks.values() if type(b).__name__ == "Slider"]
assert len(sliders) == 0, f"Expected no sliders, found {len(sliders)}"
def test_demo_has_two_interactive_dropdowns(demo):
"""UI should have two interactive language dropdowns."""
dropdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Dropdown"]
assert len(dropdowns) == 2, f"Expected 2 dropdowns, found {len(dropdowns)}"
interactive = [d for d in dropdowns if d.interactive is not False]
assert len(interactive) == 2, f"Expected 2 interactive dropdowns, found {len(interactive)}"
def test_source_dropdown_default_is_english(demo):
"""Source dropdown should default to English (en)."""
dropdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Dropdown"]
english = [d for d in dropdowns if d.value == "English (en)"]
assert len(english) == 1, "Expected one dropdown defaulting to 'English (en)'"
def test_target_dropdown_default_is_french(demo):
"""Target dropdown should default to French (fr)."""
dropdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Dropdown"]
french = [d for d in dropdowns if d.value == "French (fr)"]
assert len(french) == 1, "Expected one dropdown defaulting to 'French (fr)'"
def test_both_dropdowns_filterable(demo):
"""Both language dropdowns should be filterable/searchable."""
dropdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Dropdown"]
interactive = [d for d in dropdowns if d.interactive is not False]
assert all(d.filterable is True for d in interactive), "Both dropdowns should be filterable"
def test_dropdown_choices_include_locale_codes(demo):
"""Dropdown choices should include locale codes like 'French (fr)'."""
dropdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Dropdown"]
interactive = [d for d in dropdowns if d.interactive is not False]
# Gradio stores choices as (label, value) tuples
labels = [c[0] if isinstance(c, tuple) else c for c in interactive[0].choices]
assert all("(" in label and ")" in label for label in labels), f"Expected locale codes in choices: {labels}"
def test_demo_has_two_textboxes(demo):
"""UI should have input and output textboxes."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
assert len(textboxes) == 2, f"Expected 2 textboxes, found {len(textboxes)}"
def test_input_textbox_height(demo):
"""Input textbox should use lines=6."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
interactive = [t for t in textboxes if t.interactive is not False]
assert interactive[0].lines == 6, f"Expected lines=6, got {interactive[0].lines}"
def test_input_textbox_max_length(demo):
"""Input textbox should enforce 2000 character limit."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
interactive = [t for t in textboxes if t.interactive is not False]
assert interactive[0].max_length == 2000, f"Expected max_length=2000, got {interactive[0].max_length}"
def test_input_textbox_has_no_placeholder(demo):
"""Input textbox should have no placeholder text."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
interactive = [t for t in textboxes if t.interactive is not False]
assert interactive[0].placeholder is None, f"Expected no placeholder, got {interactive[0].placeholder!r}"
def test_input_textbox_has_no_buttons(demo):
"""Input textbox should expose no toolbar buttons (gradio accepts invalid button values silently)."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
interactive = [t for t in textboxes if t.interactive is not False]
assert interactive[0].buttons == [], f"Expected no buttons, got {interactive[0].buttons!r}"
def test_output_textbox_is_non_interactive(demo):
"""Output textbox should be non-interactive."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
output = [t for t in textboxes if t.interactive is False]
assert len(output) == 1, "Expected exactly one non-interactive textbox"
def test_output_textbox_height(demo):
"""Output textbox should use lines=6."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
non_interactive = [t for t in textboxes if t.interactive is False]
assert non_interactive[0].lines == 6, f"Expected lines=6, got {non_interactive[0].lines}"
def test_output_placeholder(demo):
"""Output textbox should have 'Translation' as placeholder."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
output = [t for t in textboxes if t.interactive is False]
assert output[0].placeholder == "Translation"
def test_output_textbox_has_copy_button(demo):
"""Output textbox should expose a copy button."""
textboxes = [b for b in demo.blocks.values() if type(b).__name__ == "Textbox"]
output = [t for t in textboxes if t.interactive is False]
assert output[0].buttons == ["copy"], f"Expected ['copy'], got {output[0].buttons!r}"
def test_demo_has_translate_button(demo):
"""UI should have a Translate button."""
buttons = [b for b in demo.blocks.values() if type(b).__name__ == "Button"]
assert any(b.value == "Translate" for b in buttons), "Expected a 'Translate' button"
def test_translate_button_outside_columns(demo):
"""Translate button should not be inside either Column."""
buttons = [b for b in demo.blocks.values() if type(b).__name__ == "Button" and b.value == "Translate"]
assert len(buttons) == 1
node = getattr(buttons[0], "parent", None)
while node is not None:
assert type(node).__name__ != "Column", "Translate button should not be inside a Column"
node = getattr(node, "parent", None)
def test_demo_has_no_html_elements(demo):
"""UI should have no HTML elements (hint/char count removed)."""
html_blocks = [b for b in demo.blocks.values() if type(b).__name__ == "HTML"]
assert len(html_blocks) == 0, f"Expected no HTML blocks, found {len(html_blocks)}"
def test_demo_has_swap_button(demo):
"""UI should have a swap button."""
buttons = [b for b in demo.blocks.values() if type(b).__name__ == "Button"]
swap = [b for b in buttons if "⇄" in str(b.value)]
assert len(swap) == 1, "Expected one swap button"
def test_swap_handler_wired(demo):
"""Swap button should have a click handler with 4 inputs and 4 outputs."""
swap_fns = [fn for fn in demo.fns.values() if len(fn.inputs) == 4 and len(fn.outputs) == 4]
assert len(swap_fns) >= 1, "Expected a handler with 4 inputs and 4 outputs (swap handler)"
def test_translate_handlers_wire_text_and_language(demo):
"""Translate click and submit handlers should wire input text and target language."""
translate_fns = [fn for fn in demo.fns.values() if [type(i).__name__ for i in fn.inputs] == ["Textbox", "Dropdown"]]
assert len(translate_fns) == 2, f"Expected 2 translate handlers (click + submit), found {len(translate_fns)}"
def test_all_handlers_wired(demo):
"""UI should have exactly 3 click/submit handlers: translate click, translate submit, swap."""
assert len(demo.fns) == 3, f"Expected 3 handlers, found {len(demo.fns)}"
def test_no_title(demo):
"""UI should not have an H1 title."""
markdowns = [b for b in demo.blocks.values() if type(b).__name__ == "Markdown"]
for md in markdowns:
assert not md.value.startswith("# "), f"Found unexpected title: {md.value}"
# --- Slow tests (require CUDA + model download) ---
gpu_available = torch.cuda.is_available()
@pytest.fixture(scope="module")
def loaded_app():
import app
# Force model/tokenizer loading
app._load_tokenizer()
app._load_model()
return app
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_name_to_code_matches_language_names(loaded_app):
name_to_code, language_names = loaded_app._build_language_mappings()
assert set(name_to_code.keys()) == set(language_names)
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_language_names_sorted_by_region(loaded_app):
"""Language names should be sorted by region, then alphabetically within each region."""
from langmap.langid_mapping import langid_to_language
name_to_code, language_names = loaded_app._build_language_mappings()
expected = sorted(
language_names,
key=lambda n: (langid_to_language[name_to_code[n]]["region"], n),
)
assert language_names == expected
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_all_codes_are_bcp47_tokens(loaded_app):
name_to_code, _ = loaded_app._build_language_mappings()
for name, code in name_to_code.items():
assert code.startswith("<2") and code.endswith(">"), f"Invalid code {code} for {name}"
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_unsupported_language(loaded_app):
with pytest.raises(ValueError, match="Unsupported language"):
loaded_app.translate("hello", "FakeLanguage")
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_returns_string(loaded_app):
result = loaded_app.translate("Hello", "French (fr)")
assert isinstance(result, str)
assert len(result) > 0
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_with_beam_search(loaded_app):
"""Translation with beam search (num_beams=4) should return a string."""
result = loaded_app.translate("Hello", "French (fr)", num_beams=4)
assert isinstance(result, str)
assert len(result) > 0
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_with_custom_temperature(loaded_app):
"""Translation with custom temperature should return a string."""
result = loaded_app.translate("Hello", "French (fr)", temperature=0.5)
assert isinstance(result, str)
assert len(result) > 0
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_with_custom_max_tokens(loaded_app):
"""Translation with low max_new_tokens should return a short string."""
result = loaded_app.translate("Hello", "French (fr)", max_new_tokens=10)
assert isinstance(result, str)
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_empty_string(loaded_app):
"""Translating an empty string should not crash."""
result = loaded_app.translate("", "French (fr)")
assert isinstance(result, str)
@pytest.mark.slow
@pytest.mark.skipif(not gpu_available, reason="Requires CUDA")
def test_translate_beam_search_ignores_temperature(loaded_app):
"""When beam search is active with non-default temperature, gr.Info should be called."""
with patch("app.gr.Info") as mock_info:
result = loaded_app.translate("Hello", "French (fr)", num_beams=4, temperature=0.5)
mock_info.assert_called_once()
assert isinstance(result, str)