Spaces:
Sleeping
Sleeping
Download tests/test_note_fidelity.py from Viney/readthrough: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/spaces/Viney/readthrough/resolve/main/tests/test_note_fidelity.py
- Command line
-
hf download hf://spaces/Viney/readthrough/tests/test_note_fidelity.py
-
curl -L -o test_note_fidelity.py https://huggingface.co/spaces/Viney/readthrough/resolve/main/tests/test_note_fidelity.py
15.1 kB
| # -*- coding: utf-8 -*- | |
| """The note must render as the published note, not merely in its spirit. | |
| The brief was asked to reproduce a specific published document exactly β same | |
| colours, same placements. "Looks close" is not the requirement, so these tests | |
| compare against the document itself rather than against a description of it. | |
| ``tests/fixtures/published_note.html`` holds the two things that can be | |
| compared mechanically: the stylesheet, and the three figures. Everything else | |
| about the page is data-dependent, so those assertions are structural β the ten | |
| sections in their published order, six tiles in a six-column grid, a section | |
| that keeps its slot when its data is missing. | |
| Two divergences from the published file are deliberate, and each is asserted | |
| here so it cannot drift into an accidental third: | |
| * every selector carries a ``.rt-note`` prefix, because Streamlit's own base | |
| styles would otherwise win on specificity; | |
| * every ``<svg>`` carries ``xmlns``, because Streamlit's sanitiser drops an | |
| SVG without it and the figure renders as an empty box. | |
| """ | |
| from __future__ import annotations | |
| import pathlib | |
| import re | |
| import pytest | |
| FIXTURE = pathlib.Path(__file__).parent / "fixtures" / "published_note.html" | |
| def published() -> str: | |
| return FIXTURE.read_text(encoding="utf-8") | |
| # ββ The stylesheet βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _rules(css: str) -> list[tuple[str, str]]: | |
| """Flatten CSS to ``(selector, sorted declarations)`` pairs, in order.""" | |
| css = re.sub(r"/\*.*?\*/", "", css, flags=re.S) | |
| out = [] | |
| for selector, block in re.findall(r"([^{}]+)\{([^{}]*)\}", css): | |
| selector = " ".join(selector.split()) | |
| if selector.startswith("@"): | |
| continue | |
| declarations = ";".join( | |
| sorted(d.strip() for d in block.split(";") if d.strip()) | |
| ) | |
| out.append((selector, declarations)) | |
| return out | |
| def _unscope(selector: str) -> str: | |
| """Strip the ``.rt-note`` prefix so selectors can be compared.""" | |
| parts = [] | |
| for part in selector.split(","): | |
| part = part.strip() | |
| if part == ".rt-note": | |
| part = "body" # the note is a div here, a document there | |
| elif part.startswith(".rt-note "): | |
| part = part[len(".rt-note "):] | |
| parts.append(part) | |
| return ",".join(parts) | |
| # Rules we add on top of the published stylesheet, each with its reason. | |
| # Anything not listed here is drift, and the test says so. Empty is the goal: | |
| # the stylesheet is currently the published one, rule for rule. | |
| ADDED_RULES: dict[str, str] = {} | |
| def test_stylesheet_declarations_are_the_published_ones(published): | |
| """Every published rule, in order, with the same declarations and values.""" | |
| from dashboard.note_css import NOTE_CSS, TOKENS | |
| original = _rules(published[published.index("<style>") + 7: published.index("</style>")]) | |
| # margin:0 belongs to the real <body> there; here the note is a div inside | |
| # a page that already sets it. | |
| original = [ | |
| (sel, ";".join(d for d in decl.split(";") if d != "margin:0") if sel == "body" else decl) | |
| for sel, decl in original | |
| ] | |
| mine = [(_unscope(sel), decl) for sel, decl in _rules(TOKENS + NOTE_CSS)] | |
| extra = [(sel, decl) for sel, decl in mine if sel in ADDED_RULES] | |
| assert len(extra) == len(ADDED_RULES), ( | |
| f"expected the additions {sorted(ADDED_RULES)}, found {[s for s, _ in extra]}" | |
| ) | |
| kept = [(sel, decl) for sel, decl in mine if sel not in ADDED_RULES] | |
| assert len(kept) == len(original), ( | |
| f"rule count drifted: published {len(original)}, ours {len(kept)}. " | |
| f"A new rule needs an entry in ADDED_RULES with its reason." | |
| ) | |
| for (sel_a, decl_a), (sel_b, decl_b) in zip(original, kept): | |
| assert sel_a.replace(" ", "") == sel_b.replace(" ", ""), ( | |
| f"selector drifted: published {sel_a!r}, ours {sel_b!r}" | |
| ) | |
| assert decl_a == decl_b, f"{sel_a}: published {decl_a!r}, ours {decl_b!r}" | |
| def test_every_selector_is_scoped_to_the_note(): | |
| """An unscoped selector would restyle the whole Streamlit page.""" | |
| from dashboard.note_css import NOTE_CSS | |
| for selector, _ in _rules(NOTE_CSS): | |
| for part in selector.split(","): | |
| part = part.strip() | |
| assert part == ".rt-note" or part.startswith(".rt-note "), ( | |
| f"{part!r} escapes the note's scope" | |
| ) | |
| def test_the_light_tokens_are_the_published_values(): | |
| """The palette the app is pinned to is the note's own light palette.""" | |
| from dashboard.note_css import TOKENS, TOKENS_LIGHT | |
| def values(css: str) -> dict[str, str]: | |
| block = css[css.index(":root{") + 6: css.index("}", css.index(":root{"))] | |
| out = {} | |
| for decl in block.split(";"): | |
| if "--" in decl: | |
| name, _, value = decl.partition(":") | |
| out[name.strip()] = value.strip() | |
| return out | |
| published_light = values(TOKENS) | |
| pinned = values(TOKENS_LIGHT) | |
| for name, value in published_light.items(): | |
| assert pinned.get(name) == value, f"{name}: pinned {pinned.get(name)!r} != {value!r}" | |
| # ββ The figures ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _normalise(svg: str) -> str: | |
| """Compare on rendered output, not on the two encodings we must differ in. | |
| Undone here: the ``xmlns`` the sanitiser requires, and ``$`` for the | |
| dollar sign, which st.markdown would otherwise read as LaTeX. Both render | |
| exactly as the published note does; everything else must match literally. | |
| """ | |
| svg = re.sub(r">\s+<", "><", svg.strip()) | |
| return svg.replace(' xmlns="http://www.w3.org/2000/svg"', "").replace("$", "$") | |
| def _fixture_figure(published: str, name: str) -> str: | |
| block = re.search(rf'<figure id="{name}">(.*?)</figure>', published, re.S) | |
| assert block, f"fixture has no figure {name!r}" | |
| return block.group(1) | |
| def _footnote(svg: str) -> str: | |
| from html import unescape | |
| match = re.search(r'<text x="44" y="16" class="ct">([^<]*)</text>', svg) | |
| return unescape(match.group(1)) if match else "" | |
| def test_bar_figure_geometry_is_identical(published): | |
| """Same bars, ticks, labels and coordinates as the published figure. | |
| The series is recovered from the published SVG's own geometry rather than | |
| from its printed labels: the note plots the unrounded value and prints the | |
| rounded one, so reading the labels back would move every bar by a tenth of | |
| a pixel and make the comparison meaningless. | |
| """ | |
| from dashboard.note_charts import bars_vs_guidance | |
| svg = _fixture_figure(published, "revenue_vs_guidance") | |
| scale = (274.0 - 50.4) / 100.0 | |
| bars = re.findall( | |
| r'<rect x="[\d.]+" y="([\d.]+)" width="[\d.]+" height="[\d.]+" ' | |
| r'fill="var\((--[a-z0-9-]+)\)"', | |
| svg, | |
| ) | |
| labels = re.findall(r'y="290" text-anchor="middle" class="ct">([^<]+)</text>', svg) | |
| printed = re.findall(r'class="cv">([\d,]+)</text>', svg) | |
| deltas = re.findall(r'y="306" text-anchor="middle" class="cb">([^<]+)</text>', svg) | |
| tick_y = re.findall(r'y1="([\d.]+)" y2="[\d.]+" stroke="var\(--warn\)"', svg) | |
| guides = [None] + [(274.0 - float(y)) / scale for y in tick_y] | |
| points = [ | |
| { | |
| "label": label, | |
| "value": (274.0 - float(bar[0])) / scale, | |
| "value_text": printed[n], | |
| "guide": guides[n], | |
| "delta": deltas[n - 1] if n else "", | |
| "derived": bar[1] == "--ink-3", | |
| } | |
| for n, (bar, label) in enumerate(zip(bars, labels)) | |
| ] | |
| assert len(points) == 14 | |
| mine = bars_vs_guidance( | |
| points, | |
| label="Chiffre dβaffaires trimestriel et point mΓ©dian de la guidance", | |
| footnote=_footnote(svg), | |
| ) | |
| assert _normalise(mine) == _normalise(svg) | |
| def test_paired_figure_geometry_is_identical(published): | |
| from dashboard.note_charts import paired_bars | |
| svg = _fixture_figure(published, "net_income_vs_fcf") | |
| scale = (250.0 - 59.7) / 60.0 | |
| bars = re.findall( | |
| r'<rect x="[\d.]+" y="([\d.]+)" width="[\d.]+" height="[\d.]+" fill="var\(--[a-z0-9-]+\)"', | |
| svg, | |
| ) | |
| printed = re.findall(r'class="cv">([\d,]+)</text>', svg) | |
| labels = re.findall(r'y="266" text-anchor="middle" class="ct">([^<]+)</text>', svg) | |
| ratios = re.findall(r'y="284" text-anchor="middle" class="(cb(?: warn)?)">([^<]+)</text>', svg) | |
| groups = [ | |
| { | |
| "label": label, | |
| "left": (250.0 - float(bars[2 * n])) / scale, | |
| "left_text": printed[2 * n], | |
| "right": (250.0 - float(bars[2 * n + 1])) / scale, | |
| "right_text": printed[2 * n + 1], | |
| "ratio": ratios[n][1], | |
| "ratio_warn": "warn" in ratios[n][0], | |
| } | |
| for n, label in enumerate(labels) | |
| ] | |
| mine = paired_bars( | |
| groups, | |
| label="RΓ©sultat net contre free cash-flow par trimestre", | |
| footnote=_footnote(svg), | |
| ) | |
| assert _normalise(mine) == _normalise(svg) | |
| def test_line_figure_geometry_is_identical(published): | |
| from dashboard.note_charts import line_series | |
| svg = _fixture_figure(published, "days_sales_outstanding") | |
| scale = (226.0 - 28.0) / 40.0 | |
| dots = re.findall(r'<circle cx="[\d.]+" cy="([\d.]+)" r="(\d)"', svg) | |
| printed = re.findall(r'class="cv">([\d,]+)</text>', svg) | |
| labels = re.findall(r'y="242" text-anchor="middle" class="ct">([^<]+)</text>', svg) | |
| points = [ | |
| { | |
| "label": label, | |
| "value": 30.0 + (226.0 - float(cy)) / scale, | |
| "value_text": text, | |
| "emphasis": radius == "6", | |
| } | |
| for (cy, radius), text, label in zip(dots, printed, labels) | |
| ] | |
| mine = line_series( | |
| points, | |
| label="DΓ©lai de recouvrement clients en jours", | |
| footnote=_footnote(svg), | |
| ) | |
| assert _normalise(mine) == _normalise(svg) | |
| def test_figures_declare_their_namespace(): | |
| """Without xmlns, Streamlit's sanitiser drops the SVG and the figure empties.""" | |
| from dashboard.note_charts import bars_vs_guidance, line_series, paired_bars | |
| bars = bars_vs_guidance([{"label": "Q1", "value": 1.0}], label="x") | |
| pair = paired_bars([{"label": "Q1", "left": 1.0, "right": 0.5}], label="x") | |
| line = line_series([{"label": "Q1", "value": 1.0}], label="x") | |
| for svg in (bars, pair, line): | |
| assert svg.startswith('<svg xmlns="http://www.w3.org/2000/svg"') | |
| def test_figures_never_emit_a_bare_dollar_sign(): | |
| """A pair of raw dollar signs is read as LaTeX by st.markdown.""" | |
| from dashboard.note_charts import bars_vs_guidance | |
| svg = bars_vs_guidance( | |
| [{"label": "Q1", "value": 1.0}], label="x", footnote="bn$ per share" | |
| ) | |
| assert "$" not in svg | |
| assert "$" in svg | |
| def test_the_stylesheet_contains_no_angle_bracket(): | |
| """A "<" anywhere in the CSS truncates the <style> element that carries it. | |
| This is not hypothetical: a comment in this stylesheet that mentioned the | |
| svg tag by name cut the element off at that point, dropping every rule | |
| after it β the note's grid, its sticky rail, and the app chrome CSS | |
| concatenated behind it. The page still rendered, just with no layout. | |
| """ | |
| from dashboard.note_css import FONT_IMPORT, NOTE_CSS, TOKENS, TOKENS_LIGHT | |
| for name, css in (("FONT_IMPORT", FONT_IMPORT), ("TOKENS", TOKENS), | |
| ("TOKENS_LIGHT", TOKENS_LIGHT), ("NOTE_CSS", NOTE_CSS)): | |
| assert "<" not in css, f"{name} contains '<', which truncates the style element" | |
| assert "</" not in css, f"{name} contains '</'" | |
| # ββ The document's structure βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PUBLISHED_SECTIONS = ( | |
| "resume", "print", "cash", "bilan", "segments", | |
| "direction", "capital", "bullbear", "watch", "annexes", | |
| ) | |
| def thin_brief() -> dict: | |
| """A brief with nothing in it β the case that must not lose a section.""" | |
| return { | |
| "ticker": "ZZZZ", | |
| "company_name": "Nothing Ingested Inc", | |
| "filing_date": "2026-01-31", | |
| "what_matters_most": "", | |
| "what_changed": [], | |
| "bull_points": [], | |
| "bear_points": [], | |
| "what_to_watch": [], | |
| } | |
| def _sections(html: str) -> list[str]: | |
| return re.findall(r'<section id="([a-z]+)"', html) | |
| def test_sections_are_the_published_ones_in_order(thin_brief): | |
| """The ten sections, in the published order, from any brief.""" | |
| from dashboard.note import build | |
| assert tuple(_sections(build(thin_brief, series=[], market=None, lang="fr"))) == PUBLISHED_SECTIONS | |
| def test_a_section_with_no_data_keeps_its_slot(thin_brief): | |
| """A section that vanished when empty would move every heading below it.""" | |
| from dashboard.note import build | |
| html = build(thin_brief, series=[], market=None, lang="en") | |
| assert len(_sections(html)) == len(PUBLISHED_SECTIONS) | |
| # and it says so rather than standing empty | |
| assert "ZZZZ" in html | |
| def test_the_tile_row_is_always_six_wide(thin_brief): | |
| """The grid is repeat(6,1fr): five tiles would leave a hole in the rule.""" | |
| from dashboard.note import build | |
| for brief, series in ((thin_brief, []),): | |
| html = build(brief, series=series, market=None, lang="fr") | |
| assert html.count('class="tile"') == 6 | |
| def test_the_document_never_emits_a_bare_dollar_sign(thin_brief): | |
| """st.markdown reads a pair of dollar signs as LaTeX and eats the middle.""" | |
| from dashboard.note import build | |
| assert "$" not in build(thin_brief, series=[], market=None, lang="en") | |
| def test_the_masthead_and_rail_are_always_present(thin_brief): | |
| from dashboard.note import build | |
| html = build(thin_brief, series=[], market=None, lang="fr") | |
| for marker in ('class="mast"', 'class="head"', 'nav class="toc"', | |
| 'class="grid"', "<aside>", 'class="foot"'): | |
| assert marker in html, f"{marker} missing" | |
| def test_currency_follows_the_language(thin_brief): | |
| """"96,2 Md$" in French, "$96.2bn" in English β same amount, same tile.""" | |
| from dashboard.note import _money, _per_share | |
| with_lang = lambda fn, lang: fn(96_200_000_000, lang=lang) if fn is _money else fn(2.46, lang=lang) | |
| assert _money(96_200_000_000, lang="fr") == "96,2 Md$" | |
| assert _money(96_200_000_000, lang="en") == "$96.2bn" | |
| assert _per_share(2.46, lang="fr") == "2,46 $" | |
| assert _per_share(2.46, lang="en") == "$2.46" | |
| def test_rounding_is_half_up_like_a_printed_table(): | |
| """13.25 prints as 13.3, the way the filing does β not 13.2.""" | |
| from dashboard.note import _num | |
| assert _num(13.25, 1, lang="en") == "13.3" | |
| assert _num(13.25, 1, lang="fr") == "13,3" | |