Spaces:
Running on Zero
Running on Zero
File size: 14,730 Bytes
603e09c 143e083 043ecd7 7a83d46 8d0070f 603e09c 90fc0f4 3872c0b 90fc0f4 3872c0b 603e09c 90fc0f4 603e09c 1c01d92 57a1d31 1c01d92 57a1d31 1c01d92 90fc0f4 1c01d92 603e09c 1c01d92 603e09c 1c01d92 603e09c 1c01d92 05795de 1c01d92 05795de 90fc0f4 603e09c 05795de badac0a 05795de f8a5dc6 c15ff11 639e59b c15ff11 90fc0f4 603e09c 05795de c15ff11 90fc0f4 603e09c 1a73708 0164e89 1c01d92 83587df 1c01d92 83587df f8a5dc6 05795de de4ab0d 043ecd7 de4ab0d 043ecd7 de4ab0d 043ecd7 de4ab0d 7633dda 043ecd7 de4ab0d 043ecd7 de4ab0d 043ecd7 de4ab0d 043ecd7 83587df 043ecd7 603e09c 83587df 603e09c 83587df 603e09c 83587df 603e09c 83587df 603e09c 83587df 603e09c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | 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_autofocus(demo):
"""Input textbox should autofocus so the cursor lands there on page load."""
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].autofocus is True, f"Expected autofocus=True, got {interactive[0].autofocus!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_translate_endpoint_has_stable_api_name(demo):
"""The lean submit handler (text + language -> string) should expose a stable 'translate' API endpoint."""
api_fns = [fn for fn in demo.fns.values() if getattr(fn, "api_name", None) == "translate"]
assert len(api_fns) == 1, "Expected exactly one handler with api_name='translate'"
fn = api_fns[0]
assert [type(i).__name__ for i in fn.inputs] == ["Textbox", "Dropdown"]
assert [type(o).__name__ for o in fn.outputs] == ["Textbox"]
def test_only_translate_endpoint_is_public(demo):
"""UI-only handlers (swap, button loading) should be private; only /translate is a public API endpoint."""
named = list(demo.get_api_info()["named_endpoints"].keys())
assert named == ["/translate"], f"Expected only ['/translate'] exposed, found {named}"
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)
|