bit-backtest-lab / tests /test_ui.py
Bit-Trading-Company's picture
CI deploy b5b8ab30
c425891 verified
Raw
History Blame Contribute Delete
28.1 kB
"""Phase 3 acceptance: the UI renders, and every displayed number is traceable.
These run against the real cached store when it is present, and skip cleanly
when it is not (a fresh clone with no `.cache/store` yet).
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import pandas as pd
import plotly.graph_objects as go
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import app as bitapp
from src import charts, comparisons, config, runtime, strategies
from src.ui import components as C
from src.ui.format import money, num, pct
from src.runtime import RunRequest
def _store_ready() -> bool:
try:
m = runtime.get_store().load_manifest()
return len(m.prices) > 0
except Exception:
return False
pytestmark = pytest.mark.skipif(not _store_ready(),
reason="no cached signal store available")
@pytest.fixture(scope="module")
def rec():
return runtime.execute(RunRequest(
strategy="SMA Crossover", asset="BTC-USD", timeframe="1d", date_range="3Y",
))
@pytest.fixture(scope="module")
def three_runs():
reqs = [
RunRequest(strategy="SMA Crossover", asset="BTC-USD", timeframe="1d", date_range="3Y"),
RunRequest(strategy="RSI Mean Reversion", asset="ETH-USD", timeframe="1d", date_range="3Y"),
RunRequest(strategy="Buy & Hold (benchmark)", asset="SPY", timeframe="1d", date_range="3Y"),
]
return [runtime.execute(r) for r in reqs]
# --------------------------------------------------------------------------
# App construction
# --------------------------------------------------------------------------
def test_app_object_exists():
assert bitapp.demo is not None
def test_theme_css_carries_the_design_tokens():
from src.ui import theme
css = theme.full_css()
for token in ("--bg-canvas", "--accent-amber", "--fin-up", "--font-styrene"):
assert token in css, f"{token} missing from theme CSS"
assert "#161512" in css # stone-950 canvas
assert "#af9209" in css # accent amber
def test_disclaimer_is_present_in_the_footer():
assert "not indicative of future results" in C.footer()
assert "Not financial advice" in C.footer() or \
"not a licensed investment adviser" in C.footer()
def test_empty_state_offers_the_worked_example():
assert "No run loaded" in C.empty_state()
assert "worked example" in C.empty_state()
def test_glossary_covers_every_design_term():
terms = {t for t, _ in bitapp.GLOSSARY}
assert {"SHARPE", "SORTINO", "MAX DRAWDOWN", "PROFIT FACTOR",
"R-MULTIPLE", "MAE / MFE", "WALK-FORWARD", "OOS"} <= terms
# --------------------------------------------------------------------------
# The stat band must match engine output exactly
# --------------------------------------------------------------------------
def test_stat_band_values_match_engine_metrics(rec):
html = C.stat_band(rec)
m = rec.result.metrics_all
assert pct(m.total_return) in html
assert pct(m.cagr) in html
assert num(m.sharpe) in html
assert num(m.sortino) in html
assert pct(m.max_drawdown) in html
assert num(m.profit_factor) in html
assert f">{m.trade_count}<" in html
def test_stat_band_shows_is_and_oos_for_every_stat(rec):
html = C.stat_band(rec)
assert html.count("IS ") >= 9
assert html.count("· OOS") >= 9
assert num(rec.result.metrics_oos.sharpe) in html
assert num(rec.result.metrics_is.sharpe) in html
def test_stat_band_reports_costs_actually_paid(rec):
html = C.stat_band(rec)
assert money(rec.result.costs_paid) in html
assert rec.result.costs_paid > 0, "costs default to ON, so this must be positive"
def test_empty_segment_renders_an_em_dash_not_a_zero():
"""A segment with no bars must not read as 0.00."""
short = runtime.execute(RunRequest(
strategy="SMA Crossover", asset="BTC-USD", timeframe="1h", date_range="1Y"))
if short.result.metrics_oos.bars:
pytest.skip("this range did produce OOS windows")
html = C.stat_band(short)
assert "· OOS —" in html
assert "no out-of-sample windows" in html
def test_trade_table_rows_match_the_engine_trade_list(rec):
df = bitapp.trades_frame(rec)
assert len(df) == len(rec.result.trades)
if len(df):
assert df["Net"].iloc[0] == pytest.approx(
round(float(rec.result.trades["net_pnl"].iloc[0]), 2))
assert set(df["Segment"]) <= {"IS", "OOS", "holdout"}
assert (df["Costs"] >= 0).all()
def test_report_quotes_the_same_numbers_as_the_stat_band(rec):
md = bitapp.report_markdown(rec)
assert pct(rec.result.metrics_all.total_return) in md
assert money(rec.result.costs_paid) in md
assert rec.run_id in md
def test_costs_off_is_called_out_as_not_real():
off = runtime.execute(RunRequest(
strategy="SMA Crossover", asset="BTC-USD", timeframe="1d",
date_range="3Y", costs_on=False))
_, _, _, _, _, _, _, _, note = bitapp.build_overview(off, log_scale=False, cvd=False)
assert "COSTS ARE OFF" in note
assert off.result.costs_paid == 0.0
# --------------------------------------------------------------------------
# Charts
# --------------------------------------------------------------------------
def test_overview_builds_every_figure(rec):
figs = bitapp.build_overview(rec, log_scale=False, cvd=False)
assert len(figs) == 9
for f in figs[:8]:
assert isinstance(f, go.Figure)
assert isinstance(figs[8], str)
def test_log_scale_and_colorblind_variants_render(rec):
for log_s in (False, True):
for cb in (False, True):
figs = bitapp.build_overview(rec, log_scale=log_s, cvd=cb)
assert isinstance(figs[0], go.Figure)
def test_equity_chart_marks_the_holdout_band(rec):
fig = charts.equity_curve(rec.result.equity, rec.result.benchmark_equity,
plan=rec.result.plan)
if rec.result.plan.holdout_start is not None:
texts = [str(a.text) for a in fig.layout.annotations]
assert any("HOLDOUT" in t for t in texts)
def test_charts_survive_empty_inputs():
empty = pd.Series(dtype="float64")
assert isinstance(charts.equity_curve(empty), go.Figure)
assert isinstance(charts.underwater_chart(empty), go.Figure)
assert isinstance(charts.pnl_histogram(pd.DataFrame()), go.Figure)
assert isinstance(charts.mae_mfe_scatter(pd.DataFrame()), go.Figure)
assert isinstance(charts.walk_forward_bars([]), go.Figure)
assert isinstance(charts.monte_carlo_cone(None), go.Figure)
def test_colorblind_palette_differs_from_default():
assert charts.up_color(cvd=True) != charts.up_color(cvd=False)
assert charts.down_color(cvd=True) != charts.down_color(cvd=False)
# --------------------------------------------------------------------------
# Comparison tab with three runs
# --------------------------------------------------------------------------
def test_comparison_renders_three_runs(three_runs):
curves = {r.label[:28]: r.result.equity for r in three_runs}
rets = {r.label[:28]: r.result.equity.pct_change().dropna() for r in three_runs}
assert len(curves) == 3
assert isinstance(charts.overlaid_returns(curves), go.Figure)
assert isinstance(charts.small_multiples(curves), go.Figure)
corr = charts.correlation_matrix(rets)
assert isinstance(corr, go.Figure)
assert len(corr.data[0].z) == 3
def test_return_overlay_caps_the_series_it_will_draw():
"""The old six-run session picker was replaced by the catalog view; the
readability cap now lives in the chart itself."""
import numpy as np
idx = pd.date_range("2024-01-01", periods=30, freq="D", tz="UTC")
curves = {f"s{i}": pd.Series(np.linspace(0, 1, 30), index=idx) for i in range(50)}
assert len(charts.multi_return_overlay(curves, max_series=6).data) == 6
def test_precomputed_heatmap_loads_from_the_store():
heat = comparisons.load_table(runtime.get_store(), comparisons.HEATMAP)
if heat.empty:
pytest.skip("comparison tables not generated yet")
assert {"asset", "strategy", "timeframe", "oos_sharpe"} <= set(heat.columns)
assert isinstance(charts.strategy_timeframe_heatmap(heat), go.Figure)
def test_regime_breakdown_covers_the_named_regimes(rec):
df = runtime.regime_breakdown(rec)
if df.empty:
pytest.skip("no regime variation in this window")
assert set(df["regime"]) <= {"BULL", "BEAR", "CHOP"}
# --------------------------------------------------------------------------
# Coverage map & share links
# --------------------------------------------------------------------------
def test_coverage_map_renders_from_the_live_manifest():
df = runtime.coverage_frame()
assert not df.empty
assert {"Model", "Asset", "TF", "Coverage", "Rows", "Real?"} <= set(df.columns)
def test_placeholder_slices_are_labelled_in_the_coverage_map():
df = runtime.coverage_frame()
assert set(df["Real?"]) <= {"real", "PLACEHOLDER"}
def test_share_link_round_trips():
req = RunRequest(strategy="RSI Mean Reversion", asset="ETH-USD",
timeframe="1h", date_range="1Y", params={"rsi_period": 21})
again = RunRequest.decode(req.encode())
assert again.strategy == req.strategy
assert again.asset == req.asset
assert again.params["rsi_period"] == 21
@pytest.mark.parametrize("payload", [
'{"strategy":"__import__(\'os\').system","asset":"BTC-USD"}',
'{"strategy":"SMA Crossover","asset":"../../etc/passwd"}',
'{"strategy":"SMA Crossover","asset":"BTC-USD","timeframe":"99y"}',
'{"strategy":"SMA Crossover","asset":"BTC-USD","validation_mode":"eval"}',
])
def test_hostile_share_links_are_rejected(payload):
import base64
token = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
with pytest.raises(ValueError):
RunRequest.decode(token)
def test_unavailable_presets_are_listed_but_refuse_to_run():
assert "Custom (code)" in strategies.PRESETS
assert not strategies.PRESETS["Custom (code)"].available
with pytest.raises(runtime.RunError, match="never executes untrusted code"):
runtime.execute(RunRequest(strategy="Custom (code)", asset="BTC-USD"))
# --------------------------------------------------------------------------
# Performance budget
# --------------------------------------------------------------------------
def test_default_three_year_daily_run_is_under_two_seconds():
runtime.cache_clear()
req = RunRequest(strategy="SMA Crossover", asset="BTC-USD",
timeframe="1d", date_range="3Y")
runtime.execute(req) # warm the slice cache
import time
t0 = time.perf_counter()
runtime.execute(req)
assert time.perf_counter() - t0 < 2.0
# --------------------------------------------------------------------------
# Design-system fidelity
#
# The original bug this guards: base.css was vendored into assets/ but never
# loaded, so the app silently inherited Gradio's rounded, roomy defaults and
# only approximated the design.
# --------------------------------------------------------------------------
def test_every_global_stylesheet_the_design_system_declares_is_loaded():
"""The DS manifest lists six globalCssPaths; all of them must reach the page."""
from src.ui import theme
css = theme.full_css()
# colors / typography / spacing / base, each identified by a rule only it has
markers = {
"colors.css": "--accent-amber:#af9209",
"typography.css": "--text-2xs:8px",
"spacing.css": "--space-4:16px",
"base.css": "border-radius:0 !important",
}
for name, marker in markers.items():
assert marker in css, f"{name} is not loaded into the page"
def test_base_css_square_corner_reset_is_present():
from src.ui import theme
assert "border-radius:0 !important" in theme.full_css()
# The app's own shell must not reintroduce a rounded default on top of the
# reset. base.css legitimately defines opt-in `.bit-rounded*` utilities, so
# only our shell layer is checked here.
assert "border-radius" not in theme.SHELL_CSS
assert "border-radius" not in theme.GRADIO_RESET
def test_base_css_scrollbar_and_selection_rules_survive():
from src.ui import theme
css = theme.full_css()
assert "::-webkit-scrollbar" in css
assert "::selection" in css
def test_data_surfaces_stay_selectable_despite_base_css():
"""base.css sets user-select:none globally; tables must opt back in."""
from src.ui import theme
css = theme.full_css()
assert "user-select: text" in css
assert ".bit-table" in css.split("user-select: text")[0][-400:] or \
"table, table *" in css
def test_gradio_layout_spacing_is_configured_not_left_default():
"""Gradio defaults to layout_gap *spacing_xxl and block_padding *spacing_xl."""
from src.ui import theme
t = theme.bit_theme()
assert t.layout_gap == "8px"
assert t.block_padding == "0px"
assert t.form_gap_width == "0px"
def test_gradio_radius_is_squared_off():
from src.ui import theme
t = theme.bit_theme()
for attr in ("block_radius", "input_radius", "button_large_radius",
"button_small_radius", "container_radius"):
assert getattr(t, attr) == "0px", f"{attr} is not square"
def test_type_scale_matches_the_designs_dominant_sizes():
"""The design's workhorse is --text-xs (10px) with --text-2xs (8px) micro."""
from src.ui import theme
t = theme.bit_theme()
assert t.body_text_size == "10px"
assert t.block_label_text_size == "8px"
def test_no_hardcoded_pixel_font_sizes_outside_the_theme():
"""Component markup must size through tokens, never literal px."""
import re
from pathlib import Path
root = Path(__file__).resolve().parent.parent
for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"):
text = (root / rel).read_text()
assert not re.search(r"font-size:\s*\d", text), \
f"{rel} hardcodes a pixel font-size instead of using a token"
def test_every_bit_class_used_in_markup_has_a_css_rule():
"""A class with no rule renders unstyled and silently breaks the design."""
import re
from pathlib import Path
from src.ui import theme
root = Path(__file__).resolve().parent.parent
used = set()
for rel in ("src/ui/components.py", "src/ui/compare_tab.py", "app.py"):
text = (root / rel).read_text()
# Only real class attributes -- a bare `bit-…` token can appear in a
# comment or an asset filename and is not a class the page uses.
for attr in (re.findall(r'class="([^"]*)"', text)
+ re.findall(r"elem_classes=[\"']([^\"']+)", text)):
for cls in attr.split():
if not cls.startswith("bit-"):
continue
# Class names are built in f-strings, e.g. `bit-chip{cls}` or
# `bit-podium-{rank}`. Keep the static prefix; a name that is
# only a prefix (ends in `-`) has variant rules instead.
static = cls.split("{", 1)[0]
if static and not static.endswith("-"):
used.add(static)
defined = set(re.findall(r"\.(bit-[a-z0-9-]+)", theme.full_css()))
missing = sorted(c for c in used if c not in defined and not c.endswith("-"))
assert not missing, f"classes used in markup but never styled: {missing}"
def test_fonts_are_served_for_every_weight_the_design_uses():
from src.ui import theme
css = theme.full_css()
assert css.count("@font-face") >= 5
assert "Styrene A" in css and "Mac Minecraft" in css
def test_app_constructs_without_any_hugging_face_credentials(monkeypatch):
"""Reading and backtesting are open to anyone, so the app must build with
no token at all. Gradio's off-Space OAuth mock calls whoami and raises
without one, which previously made the whole app unconstructable."""
import importlib
for var in ("HF_TOKEN", "HF_WRITE_TOKEN", "HUGGING_FACE_HUB_TOKEN", "SPACE_ID"):
monkeypatch.delenv(var, raising=False)
import app as fresh
importlib.reload(fresh)
assert fresh.demo is not None
# --------------------------------------------------------------------------
# Design-markup rendering (shell) and the click bridge
# --------------------------------------------------------------------------
def test_left_panel_is_design_markup_not_gradio_components():
"""Every control in the left panel must be a real button or input."""
from src.ui import shell
st = bitapp.default_state()
html = bitapp.render_left(st)
assert 'width:286px' in html, "zone width is not the design's 286px"
assert html.count("<button") >= 12, "controls are not real buttons"
assert "data-bit=" in html, "buttons are not wired to the bridge"
# Gradio's own control DOM must not appear here.
assert "<fieldset" not in html
assert 'type="radio"' not in html
def test_left_panel_uses_the_designs_exact_spacing():
html = bitapp.render_left(bitapp.default_state())
for value in ("padding:9px 12px", # panel header
"padding:0 12px 12px", # section body
"gap:10px", # section body gap
"padding:3px 7px", # chips
"padding:8px 12px"): # accordion header
assert value in html, f"design spacing {value!r} missing"
def test_top_bar_uses_the_real_mark_and_design_header_treatment():
from src.ui import shell
html = shell.top_bar(context="BTC-USD", status="RUN COMPLETE", tone="ok")
assert "M50 10H90V90H50Z" in html, "not the design's mark"
assert "padding:8px 12px" in html
assert "margin-top:3px" in html, "optical alignment on the title is missing"
assert "mono-data" in html, "design-system utility class not used"
def test_selected_chip_is_the_only_active_one():
st = bitapp.default_state()
st["timeframe"] = "1h"
html = bitapp.render_left(st)
actives = re.findall(
r'<button data-bit="(tf:[^"]+)"[^>]*background:var\(--accent-amber\)', html)
assert actives == ["tf:1h"]
def test_html_entities_are_escaped_exactly_once():
html = bitapp.render_left(bitapp.default_state())
assert "&amp;amp;" not in html, "double-escaped entity"
assert "Universe &amp; Data" in html
@pytest.mark.parametrize("action,key,expected", [
("tf:15m", "timeframe", "15m"),
("asset:ETH-USD", "asset", "ETH-USD"),
("range:1Y", "range", "1Y"),
("validation:holdout", "validation_mode", "holdout"),
("sizing:vol_target", "sizing_mode", "vol_target"),
("slippage:volume_scaled", "slippage_model", "volume_scaled"),
])
def test_actions_fold_into_state(action, key, expected):
st, ran = bitapp.apply_action(bitapp.default_state(), f"{action}|nonce")
assert st[key] == expected
assert ran is False
def test_run_and_example_actions_request_a_run():
_, ran = bitapp.apply_action(bitapp.default_state(), "run:|n")
assert ran is True
st, ran = bitapp.apply_action(bitapp.default_state(), "example:|n")
assert ran is True and st["strategy"] == "Chronos Forecast Follower"
def test_accordion_action_toggles():
st = bitapp.default_state()
before = st["acc"]["costs"]
st, _ = bitapp.apply_action(st, "acc:costs|n")
assert st["acc"]["costs"] is not before
def test_numeric_param_commits_through_the_bridge():
st, _ = bitapp.apply_action(bitapp.default_state(), "param:commission_bps=25|n")
assert st["commission_bps"] == 25.0
def test_strategy_param_commits_and_survives_preset_defaults():
st = bitapp.default_state()
st, _ = bitapp.apply_action(st, "param:fast_ma=33|n")
assert st["params"]["fast_ma"] == 33.0
@pytest.mark.parametrize("bad", [
"param:commission_bps=nonsense", "param:commission_bps=", "topn:abc",
"tf:99y", "asset:../etc/passwd", "strategy:__import__",
])
def test_malformed_actions_leave_state_untouched(bad):
before = bitapp.default_state()
after, ran = bitapp.apply_action(bitapp.default_state(), f"{bad}|n")
assert ran is False
assert after["timeframe"] == before["timeframe"]
assert after["asset"] == before["asset"]
assert after["strategy"] == before["strategy"]
assert after["commission_bps"] == before["commission_bps"]
def test_unknown_action_keys_are_dropped():
from src.ui.bridge import parse_action
assert parse_action("evil:rm -rf|n") is None
assert parse_action("os.system:x|n") is None
def test_bridge_survives_values_containing_spaces():
"""Preset names contain spaces; the nonce separator must not collide."""
from src.ui.bridge import emit, parse_action
raw = emit("strategy", "Buy & Hold (benchmark)")
assert parse_action(raw + "|nonce").value == "Buy & Hold (benchmark)"
def test_tables_render_as_markup_not_dataframes():
df = pd.DataFrame({"A": [1, 2], "B": ["x", "y"]})
html = bitapp.render_table(df)
assert "<table" in html and "pixel-text" in html
assert "bit-selectable" in html, "table text must stay selectable"
def test_empty_table_says_so():
assert "no rows" in bitapp.render_table(pd.DataFrame(), empty="no rows")
def test_zone_widths_are_pinned_in_css():
from src.ui import theme
css = theme.full_css()
assert "flex:0 0 286px" in css, "left aside is not pinned to the design width"
# The right tray was removed; its rules should not linger.
assert "bit-zone-right" not in css
def test_bridge_target_is_hidden_but_present():
from src.ui import theme
css = theme.full_css()
assert "#bit-action" in css
assert "opacity:0" in css.split("#bit-action")[1][:220]
def test_gradio_content_wrapper_is_stretched_full_bleed():
"""Gradio centres the app in a padded `.contain`, which cost 208px of a
1680px viewport. The design is a full-bleed dashboard."""
from src.ui import theme
import re
css = theme.full_css()
# Match the rule whose selector list starts with `.contain`, not the
# `.container` rule -- one is a prefix of the other.
m = re.search(r"\.gradio-container \.contain,(.*?)\{(.*?)\}", css, re.S)
assert m, "no full-bleed rule for Gradio's .contain wrapper"
body = m.group(2)
assert "max-width:100% !important" in body
assert "padding:0 !important" in body
# --------------------------------------------------------------------------
# Regressions from the header/nav restructure
# --------------------------------------------------------------------------
def test_header_carries_nav_login_and_glossary():
from src.ui import shell
html = shell.top_bar(tab="backtest", glossary=bitapp.GLOSSARY, on_space=True)
assert 'data-bit="tab:compare"' in html
assert 'data-bit="tab:backtest"' in html
assert "bit-help" in html, "glossary tooltip missing from the header"
def test_signed_in_header_shows_the_handle():
from src.ui import shell
assert "@alice" in shell.top_bar(user="alice", on_space=True)
def test_sign_in_uses_a_real_login_button_not_a_handrolled_link():
"""Gradio only mounts /login/huggingface when it sees a LoginButton in the
app. A hand-rolled anchor points at a route that returns 404."""
import inspect
from src.ui import shell
src = inspect.getsource(bitapp.build_app)
assert "gr.LoginButton" in src, "no LoginButton, so OAuth routes never mount"
# and the markup must not fake one
assert "/login/huggingface" not in shell.top_bar(on_space=True)
def test_off_space_header_explains_sign_in_is_unavailable():
from src.ui import shell
assert "SIGN IN" in shell.top_bar(on_space=False)
def test_context_chip_only_appears_on_the_backtest_tab():
"""On Compare, a run context chip would describe something not on screen."""
st = bitapp.default_state()
st["tab"] = "compare"
class FakeRec:
run_id = "abcd1234"
elapsed_s = 1.0
# The chip is the only place the asset and timeframe appear together.
# "WALK-FORWARD" alone is ambiguous -- it is also a glossary term.
compare_header = bitapp.render_top(st, FakeRec())
assert "BTC-USD · 1D" not in compare_header
st["tab"] = "backtest"
assert "BTC-USD · 1D" in bitapp.render_top(st, FakeRec())
def test_tab_action_switches_views():
st, _ = bitapp.apply_action(bitapp.default_state(), "tab:backtest|n1")
assert st["tab"] == "backtest"
st, _ = bitapp.apply_action(st, "tab:compare|n2")
assert st["tab"] == "compare"
def test_unknown_tab_value_is_ignored():
st, _ = bitapp.apply_action(bitapp.default_state(), "tab:../admin|n")
assert st["tab"] == "compare"
def test_the_same_click_cannot_run_twice():
"""Two event bindings deliver one click; the nonce must de-duplicate it."""
st = bitapp.default_state()
st, first = bitapp.apply_action(st, "run:|nonce-a")
_, replay = bitapp.apply_action(st, "run:|nonce-a")
assert first is True and replay is False
def test_a_fresh_click_still_runs_after_a_deduped_one():
st = bitapp.default_state()
st, _ = bitapp.apply_action(st, "run:|nonce-a")
_, again = bitapp.apply_action(st, "run:|nonce-b")
assert again is True
def test_running_switches_to_the_backtest_tab():
st = bitapp.default_state()
assert st["tab"] == "compare"
st, should_run = bitapp.apply_action(st, "example:|n")
assert should_run is True
def test_right_sidebar_is_gone():
import inspect
src = inspect.getsource(bitapp.build_app)
assert "bit-zone-right" not in src
assert "right_panel" not in src
def test_bridge_elements_are_rendered_not_visible_false():
"""`visible=False` removes an element from the DOM, which left the click
bridge with nothing to write into."""
import inspect
src = inspect.getsource(bitapp.build_app)
box = src[src.index("action_box = gr.Textbox"):]
assert "visible=True" in box[:260]
trig = src[src.index("action_trigger = gr.Button"):]
assert "visible=True" in trig[:200]
def test_bridge_js_is_loaded_without_outputs():
"""Gradio treats a `js=` return value as the output values, so the bridge
installer must not share a load call that has outputs."""
import inspect
src = inspect.getsource(bitapp.build_app)
assert "demo.load(fn=None, inputs=None, outputs=None, js=BRIDGE_LOAD_JS)" in src
def test_no_oauth_annotated_load_handler():
"""A `demo.load` handler that takes `gr.OAuthProfile` asks for an
authenticated session on every page render. An unauthenticated visitor is
sent to sign in, returns, fires load again, and is sent back -- an infinite
redirect. The profile belongs on user-initiated handlers only."""
import inspect
import re
src = inspect.getsource(bitapp.build_app)
# Strip comments so the explanation of this rule does not trip it.
code = "\n".join(ln for ln in src.splitlines()
if not ln.lstrip().startswith("#"))
assert "OAuthProfile" not in code, \
"an OAuth-annotated handler is registered in build_app"
for call in re.findall(r"demo\.load\((.*?)\)\n", code, re.S):
assert "profile" not in call, f"load handler takes a profile: {call[:80]}"
def test_profile_is_read_only_on_user_initiated_handlers():
"""Where the profile *is* needed, it must hang off a click, not a load."""
import inspect
from src import extension
for fn in (extension.extend_ui, extension.add_model_ui):
params = inspect.signature(fn).parameters
assert "profile" in params, f"{fn.__name__} should receive the profile"
def test_login_button_is_the_only_sign_in_mechanism():
import inspect
from src.ui import shell
src = inspect.getsource(bitapp.build_app)
assert "gr.LoginButton" in src
# The header must not also hand-roll a link to the OAuth route.
assert "/login/huggingface" not in shell.top_bar(on_space=True)