Spaces:
Running on Zero
Running on Zero
| 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 --- | |
| 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() | |
| def loaded_app(): | |
| import app | |
| # Force model/tokenizer loading | |
| app._load_tokenizer() | |
| app._load_model() | |
| return app | |
| 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) | |
| 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 | |
| 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}" | |
| def test_translate_unsupported_language(loaded_app): | |
| with pytest.raises(ValueError, match="Unsupported language"): | |
| loaded_app.translate("hello", "FakeLanguage") | |
| def test_translate_returns_string(loaded_app): | |
| result = loaded_app.translate("Hello", "French (fr)") | |
| assert isinstance(result, str) | |
| assert len(result) > 0 | |
| 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 | |
| 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 | |
| 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) | |
| def test_translate_empty_string(loaded_app): | |
| """Translating an empty string should not crash.""" | |
| result = loaded_app.translate("", "French (fr)") | |
| assert isinstance(result, str) | |
| 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) | |