`ruff format .` across the repo: 153 files, no behaviour change. This is the second gate CONTRIBUTING.md promises, and until now it reported 154 files needing reformat — so a first contributor ran the command the guide told them to run and could not tell their own diff from the background. Landed on its own, with nothing else in it, because a whole-repo reformat is only safe to skip past in blame if it is provably nothing but layout. It was checked that way rather than assumed: every changed file was formatted in memory and compared by full `ast.dump()`, and 152 of 153 come out structurally identical (the one exception was a docstring opening on a quote character, fixed in the commit before this one). The string and comment token streams were diffed separately, so a quote flip or an edited comment could not hide inside a re-layout — five quote-style normalisations and sixteen implicit-concatenation joins, all value-identical, and one commented-out line gaining the space after its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite is 1888 green, gallery gate and `test_theme16` included. What actually changed, in order of how much of the diff it is: wrapped calls and signatures rejoin onto one line where they fit inside the hundred columns; the magic trailing comma expands what it is there to expand; complex slices gain their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are laid out to be read were fenced in the previous commit and are untouched here. The aligned trailing-comment column is gone from about ninety lanes that were not fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced where it sits. Every comment stays on the entry it describes, so nothing there reads wrong; it just no longer lines up. Add this commit to `.git-blame-ignore-revs`, which follows immediately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
361 lines
16 KiB
Python
361 lines
16 KiB
Python
"""Braille chart widget tests: orientation, the zero baseline, scaling, and styles.
|
||
|
||
The module is pure Text-in/Text-out, so every behaviour is asserted straight off
|
||
the rendered braille bitmasks.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from meshterm.ui.braillechart import (
|
||
GAP,
|
||
activity_peak,
|
||
activity_sparkline,
|
||
axis_chart,
|
||
chart_span,
|
||
timeline_rows,
|
||
y_axis_labels,
|
||
)
|
||
|
||
#: Dot bits for a column filled bottom-up to height 0–4 (left / right column).
|
||
_L = (0x00, 0x40, 0x44, 0x46, 0x47)
|
||
_R = (0x00, 0x80, 0xA0, 0xB0, 0xB8)
|
||
_BASE = chr(0x2800 | 0x40 | 0x80) # the two-bottom-dot zero flatline
|
||
_BLANK = chr(0x2800)
|
||
|
||
|
||
def _ch(*bits: int) -> str:
|
||
mask = 0
|
||
for b in bits:
|
||
mask |= b
|
||
return chr(0x2800 | mask)
|
||
|
||
|
||
# --- chart_span ---------------------------------------------------------------------
|
||
|
||
|
||
def test_chart_span_always_folds_zero_in() -> None:
|
||
"""Positive, negative, and mixed series all keep zero inside the span."""
|
||
assert chart_span([3, 7]) == (0.0, 7.0)
|
||
assert chart_span([-3, -7]) == (-7.0, 0.0)
|
||
assert chart_span([-3, 7]) == (-3.0, 7.0)
|
||
assert chart_span([None, None]) == (0.0, 0.0)
|
||
assert chart_span([1], span=(-5, 10)) == (-5.0, 10.0)
|
||
|
||
|
||
# --- timeline_rows: all-positive series ------------------------------------------------
|
||
|
||
|
||
def test_timeline_peak_fills_the_height() -> None:
|
||
"""The series' peak fills every dot row; readings pair up two per cell."""
|
||
rows = timeline_rows([12, 12, 0, 0], rows=3)
|
||
assert len(rows) == 3
|
||
assert all(len(r.plain) == 2 for r in rows)
|
||
assert rows[0].plain[0] == _ch(_L[4], _R[4]) # the peak pair tops out
|
||
assert rows[0].plain[1] == _BLANK
|
||
assert rows[2].plain[1] == _BASE # the silent pair keeps the zero line
|
||
|
||
|
||
def test_timeline_each_dot_column_has_its_own_height() -> None:
|
||
"""Two readings sharing a cell rise independently — braille's full resolution."""
|
||
rows = timeline_rows([12, 6], rows=3)
|
||
assert rows[0].plain[0] == _ch(_L[4])
|
||
assert rows[1].plain[0] == _ch(_L[4], _R[2])
|
||
assert rows[2].plain[0] == _ch(_L[4], _R[4])
|
||
|
||
|
||
def test_timeline_never_hides_a_lone_packet() -> None:
|
||
"""A tiny non-zero value still lights a dot, in the lit style not the floor's."""
|
||
rows = timeline_rows([1, 0, 1000, 1000], rows=3)
|
||
bottom = rows[2]
|
||
assert bottom.plain[0] == _ch(_L[1], _R[1]) # bar dot + the baseline continuing
|
||
assert bottom.spans[0].style == "ok"
|
||
|
||
|
||
def test_timeline_all_silent_is_a_flatline() -> None:
|
||
"""With nothing to draw the chart is a faint floor line, not a hole."""
|
||
rows = timeline_rows([0, 0, 0, 0, 0, 0], rows=2)
|
||
assert rows[1].plain == _BASE * 3
|
||
assert rows[0].plain == _BLANK * 3
|
||
assert all(span.style == "faint" for span in rows[1].spans)
|
||
|
||
|
||
def test_timeline_pads_an_odd_tail_column() -> None:
|
||
"""An odd reading count still renders whole cells, the floor continuing under it."""
|
||
rows = timeline_rows([4], rows=1)
|
||
assert len(rows[0].plain) == 1
|
||
assert rows[0].plain[0] == _ch(_L[4], _R[1]) # full bar + the padded column's floor
|
||
|
||
|
||
# --- timeline_rows: GAP (a hard break in the baseline) --------------------------------
|
||
|
||
|
||
def test_timeline_gap_breaks_the_baseline_but_zero_keeps_it() -> None:
|
||
"""A GAP column draws nothing — not even the zero line — while a 0 keeps its dot.
|
||
|
||
The cell pairs a GAP (left) with a plain 0 (right): the left column is fully
|
||
blank down to the floor, the right column still shows its faint baseline dot —
|
||
the distinction that lets a day-boundary notch reach the axis while an empty
|
||
day stays a grey flatline.
|
||
"""
|
||
rows = timeline_rows([GAP, 0], rows=1)
|
||
assert rows[0].plain[0] == _ch(_R[1]) # only the right (0) column's baseline dot
|
||
assert rows[0].spans and rows[0].spans[0].style == "faint"
|
||
|
||
|
||
def test_timeline_gap_column_stays_blank_beside_a_full_bar() -> None:
|
||
"""A GAP paired with a tall bar keeps its own column blank to the axis."""
|
||
rows = timeline_rows([GAP, 4], rows=1) # left GAP, right full-height bar
|
||
# Right column carries the whole bar; the left (GAP) column lights no dot at all,
|
||
# not even the bottom baseline the bar's cell would otherwise share.
|
||
assert rows[0].plain[0] == _ch(_R[4])
|
||
assert not (ord(rows[0].plain[0]) & 0x40) # no bottom-left dot
|
||
|
||
|
||
def test_chart_span_ignores_gap_columns() -> None:
|
||
"""GAP carries no magnitude, so it never widens the span."""
|
||
assert chart_span([GAP, 3, 7]) == (0.0, 7.0)
|
||
assert chart_span([GAP, GAP]) == (0.0, 0.0)
|
||
|
||
|
||
# --- timeline_rows: negative and mixed series ------------------------------------------
|
||
|
||
|
||
def test_timeline_negative_series_hangs_from_a_top_baseline() -> None:
|
||
"""All-negative data pins zero to the ceiling and bars grow downward."""
|
||
rows = timeline_rows([-4, -2], rows=1)
|
||
# Left column full hang (0..3); right hangs two dots (2..3); the baseline row
|
||
# (the cell's top dots) is inside both bars.
|
||
assert rows[0].plain[0] == _ch(0x40, 0x04, 0x02, 0x01, 0x10, 0x08)
|
||
|
||
|
||
def test_timeline_mixed_series_puts_zero_mid_chart() -> None:
|
||
"""With readings both sides of zero, positives rise and negatives hang."""
|
||
rows = timeline_rows([-10, 5], rows=3, span=(-10, 10))
|
||
# Baseline lands on dot row 6: the full -10 hangs 0..6, the +5 rises 6..8.
|
||
assert rows[0].plain[0] == _ch(0x80) # the rise's head, above the baseline
|
||
assert rows[1].plain[0] == _ch(0x46, 0x18) # hang and rise straddle the zero row
|
||
assert rows[2].plain[0] == _ch(0x47) # the hang's tail, below everything
|
||
|
||
|
||
def test_timeline_baseline_shows_mid_chart_in_silent_cells() -> None:
|
||
"""A silent cell of a mixed-span chart draws the zero line where zero *is*."""
|
||
rows = timeline_rows([-10, 5, None, None], rows=3, span=(-10, 10))
|
||
assert rows[1].plain[1] == _ch(0x02, 0x10) # both columns' dot on row 6, faint
|
||
assert rows[1].spans[-1].style == "faint"
|
||
assert rows[0].plain[1] == _BLANK
|
||
assert rows[2].plain[1] == _BLANK
|
||
|
||
|
||
# --- styles -----------------------------------------------------------------------------
|
||
|
||
|
||
def test_timeline_per_cell_style_callable_sees_the_cells_readings() -> None:
|
||
"""A callable style is fed each lit cell's present readings."""
|
||
seen: list[list[float]] = []
|
||
|
||
def style(values: list[float]) -> str:
|
||
seen.append(values)
|
||
return "warn"
|
||
|
||
rows = timeline_rows([2, 4, None, 6], rows=1, style=style)
|
||
assert seen == [[2, 4], [6]]
|
||
assert all(span.style == "warn" for span in rows[0].spans)
|
||
|
||
|
||
# --- activity_sparkline -------------------------------------------------------------------
|
||
|
||
|
||
def test_sparkline_puts_now_on_the_right() -> None:
|
||
"""A newest-first histogram renders with the current bucket at the right edge."""
|
||
# Newest bucket the sole traffic, so it is the self-scaled peak and fills the
|
||
# column: only the final cell's *right* dot column lights (with the floor dot
|
||
# keeping the left column's baseline).
|
||
text = activity_sparkline((21, 0, 0, 0), 4)
|
||
assert text.plain == _BASE + _ch(_R[4], 0x40)
|
||
|
||
|
||
def test_sparkline_scales_to_the_window_peak() -> None:
|
||
"""With no shared peak, the busiest bucket fills the column and the rest scale to it."""
|
||
# Newest-first (16, 12, 8, 4) is a full/¾/½/¼ ramp against its own peak of 16;
|
||
# flipped for display it climbs 1→4 dots left→right toward now.
|
||
text = activity_sparkline((16, 12, 8, 4), 4)
|
||
assert text.plain == _ch(_L[1], _R[2]) + _ch(_L[3], _R[4])
|
||
|
||
|
||
def test_sparkline_shares_a_peak_across_instances() -> None:
|
||
"""A passed peak scales the bars to one ceiling, so quiet windows read short.
|
||
|
||
The same bucket draws a taller bar when it *is* the window's peak than when a
|
||
louder sibling's peak is scaled in — the channel manager's shared-scale column.
|
||
"""
|
||
solo = activity_sparkline((8, 0), 2) # self-scaled: 8 is its own peak → full height
|
||
shared = activity_sparkline((8, 0), 2, peak=32) # 8 against 32 → a quarter, one dot
|
||
assert solo.plain == _ch(0x40, _R[4]) # right column full (4 dots), left on baseline
|
||
assert shared.plain == _ch(0x40, _R[1]) # right column a single dot
|
||
|
||
|
||
def test_sparkline_pads_and_crops_to_the_window() -> None:
|
||
"""A short histogram pads with silence; a long one crops to the newest buckets."""
|
||
padded = activity_sparkline((21,), 6)
|
||
assert padded.plain == _BASE * 2 + _ch(_R[4], 0x40)
|
||
cropped = activity_sparkline((0, 0, 21, 21, 9, 9), 2)
|
||
assert cropped.plain == _BASE # only the two newest (silent) buckets survive
|
||
|
||
|
||
# --- activity_peak --------------------------------------------------------------------
|
||
|
||
|
||
def test_activity_peak_reads_a_percentile_not_the_max() -> None:
|
||
"""One freak-busy bucket sits above the ceiling instead of defining it."""
|
||
# Ten steady buckets of 10 and a lone spike of 1000: the ceiling is the 90th
|
||
# percentile of the counts — the steady level, not the spike.
|
||
assert activity_peak((1000,) + (10,) * 10, percentile=90) == 10.0
|
||
|
||
|
||
def test_activity_peak_ignores_bucket_age() -> None:
|
||
"""A burst weighs the same however old it is: recency no longer enters the ceiling."""
|
||
# The magnitude-decay recency term was removed (it starved the ceiling to the floor
|
||
# over a deep pool), so a 20-burst sets the same ceiling now or three buckets back.
|
||
assert activity_peak((20, 0, 0)) == activity_peak((0, 0, 20)) == 20.0
|
||
|
||
|
||
def test_activity_peak_floors_a_quiet_window() -> None:
|
||
"""The floor holds the ceiling up so a lull's stray packet stays a nub, not a column."""
|
||
assert activity_peak((0, 0, 0), floor=3.0) == 3.0 # silent → floor
|
||
lone = activity_peak((1,), floor=3.0)
|
||
assert lone == 3.0 # a single packet scales against the floor, not against itself
|
||
|
||
|
||
def test_activity_peak_pools_several_histograms_into_one_ceiling() -> None:
|
||
"""Pooled channels share a scale: a quiet channel reads short beside a busy one."""
|
||
pooled = activity_peak((10,) * 10, (1,), percentile=90)
|
||
assert pooled == 10.0 # the busy channel's level sets the shared ceiling…
|
||
solo = activity_peak((1,), percentile=90)
|
||
assert solo == 1.0 # …where the quiet one alone would have set just 1
|
||
|
||
|
||
# --- y_axis_labels / axis_chart -------------------------------------------------------
|
||
|
||
|
||
def test_y_axis_labels_blanks_a_silent_chart() -> None:
|
||
"""A zero peak draws no marks at all."""
|
||
assert y_axis_labels(0, 3) == ["", "", ""]
|
||
|
||
|
||
def test_y_axis_labels_marks_quote_the_ticked_dot() -> None:
|
||
"""Each mark reads the value at its row's third dot — where the ┤ tick points.
|
||
|
||
peak 10 over 2 rows (8 dots): the top row's tick crosses dot 6 (a 7-dot bar,
|
||
10·7/8 → 9), the bottom row's dot 2 (a 3-dot bar, 10·3/8 → 4) — not the rows'
|
||
top-edge values (10 and 5), which sit a dot above where the glyph points.
|
||
"""
|
||
assert y_axis_labels(10, 2) == ["9", "4"]
|
||
|
||
|
||
def test_y_axis_labels_blanks_a_repeated_mark() -> None:
|
||
"""A low peak rounds a lower row to the same value as the one above: left blank."""
|
||
assert y_axis_labels(1, 3) == ["1", "", ""]
|
||
|
||
|
||
def test_axis_chart_mirrors_marks_where_the_platform_affords_them() -> None:
|
||
"""An axis chart mirrors its marks only where the platform can afford them.
|
||
|
||
The desktop frames a chart with marks on both gutters; the console keeps the right
|
||
``├`` tick but spends the mirrored label's cells on the chart instead.
|
||
"""
|
||
from meshterm.platforms import PICOCALC, set_platform
|
||
from meshterm.ui.braillechart import axis_chrome
|
||
|
||
rows = timeline_rows([9] * 8, rows=2)
|
||
out = axis_chart(rows, 9, 4, lambda f: "now" if f >= 1.0 else "old")
|
||
assert len(out) == 4 # 2 chart rows + bottom border + caption
|
||
assert out[0].plain == "8 ┤⣿⣿⣿⣿├ 8" # ticked-dot values, mirrored on the desktop
|
||
assert out[1].plain == "3 ┤⣿⣿⣿⣿├ 3"
|
||
assert out[2].plain.startswith(" └") and out[2].plain.endswith("┘")
|
||
assert axis_chrome(1) == 2 * (1 + 2) # what a caller must budget beside the cells
|
||
|
||
set_platform(PICOCALC)
|
||
tight = axis_chart(timeline_rows([9] * 8, rows=2), 9, 4, lambda f: "x")
|
||
assert tight[0].plain == "8 ┤⣿⣿⣿⣿├" # the tick survives; the label's cells don't
|
||
assert tight[1].plain == "3 ┤⣿⣿⣿⣿├"
|
||
assert axis_chrome(1) == 1 + 3
|
||
|
||
|
||
def test_axis_chart_compacts_deep_tallies_into_the_gutter() -> None:
|
||
"""Counts above 999 read as k/M marks, and the gutter sizes to the widest printed mark.
|
||
|
||
A 1.5k peak prints ``1k`` while a lower tick still lands three digits wide (``562``),
|
||
so the gutter takes three cells — sized from the marks, never from the bare peak.
|
||
"""
|
||
from meshterm.ui.braillechart import axis_label_w, compact_label
|
||
|
||
assert compact_label(999) == "999" and compact_label(1500) == "2k"
|
||
assert compact_label(12_345) == "12k" and compact_label(2_000_000) == "2M"
|
||
assert compact_label(-1200) == "-1k"
|
||
|
||
rows = timeline_rows([1500] * 8, rows=2)
|
||
out = axis_chart(rows, 1500, 4, lambda f: "x")
|
||
assert axis_label_w(1500, 2) == 3
|
||
assert out[0].plain == " 1k ┤⣿⣿⣿⣿├ 1k" # round(1500 · 7/8) → 1312 → 1k
|
||
assert out[1].plain == "562 ┤⣿⣿⣿⣿├ 562" # round(1500 · 3/8) → the three-digit tick
|
||
|
||
|
||
def test_y_axis_labels_signed_span_quotes_both_extremes() -> None:
|
||
"""A signed chart's marks quote rise heights above zero and hang depths below."""
|
||
# rows=3 over +8..-4 (baseline on dot 4): the top tick reads a 7-dot rise (7),
|
||
# the middle a 3-dot rise (3), the bottom a 3-deep hang below zero (−2).
|
||
assert y_axis_labels(8, 3, lo=-4) == ["7", "3", "-2"]
|
||
# A symmetric span pins the middle tick onto the baseline itself: blanked zero.
|
||
assert y_axis_labels(6, 3, lo=-6) == ["5", "", "-4"]
|
||
|
||
|
||
def test_axis_chart_floor_frames_a_signed_chart_and_widens_the_gutter() -> None:
|
||
"""A signed floor draws negative marks and sizes the gutter for the widest one."""
|
||
rows = timeline_rows([5, -5], rows=3, span=(-5, 5))
|
||
out = axis_chart(rows, 5, 1, lambda f: "x", floor=-5)
|
||
assert out[0].plain.startswith(" 4 ┤") # gutter widened to two cells by the floor
|
||
assert out[2].plain.startswith("-4 ┤") # a negative mark near the floor
|
||
|
||
|
||
def test_axis_chart_honours_a_shared_label_width() -> None:
|
||
"""An explicit label_w widens the gutter past the peak's own digit count.
|
||
|
||
Lets several stacked charts (the Time Machine's per-day pair) share one gutter
|
||
width sized from the wider chart, so their marks line up column for column.
|
||
"""
|
||
rows = timeline_rows([9] * 8, rows=1)
|
||
out = axis_chart(rows, 9, 4, lambda f: "x", label_w=3)
|
||
assert out[0].plain.startswith(" 7 ┤")
|
||
|
||
|
||
# --- axis_chart column ticks ----------------------------------------------------------
|
||
|
||
|
||
def test_axis_chart_ticks_notch_the_border_and_centre_labels() -> None:
|
||
"""Explicit column ticks draw a ``┬`` on the border with the label centred beneath."""
|
||
rows = timeline_rows([9] * 8, rows=1)
|
||
out = axis_chart(rows, 9, 4, ticks=[(0, "A"), (3, "D")])
|
||
assert out[1].plain == " └┬──┬┘" # ticks at chart cells 0 and 3
|
||
assert out[2].plain == " A D" # labels centred under their ticks
|
||
|
||
|
||
def test_axis_chart_continuous_axis_notches_ticks_under_its_labels() -> None:
|
||
"""A continuous (label_at) chart now notches ┬ ticks under its ends-and-quarters labels."""
|
||
rows = timeline_rows([9] * 8, rows=1)
|
||
out = axis_chart(rows, 9, 20, lambda f: "now" if f >= 1.0 else str(round(f * 100)))
|
||
border, caption = out[1].plain, out[2].plain
|
||
assert "┬" in border # the continuous axis is ticked, not a plain rule
|
||
assert border.startswith(" └") and border.endswith("┘")
|
||
assert border[-2] == "┬" # the rightmost tick sits at the 'now' edge
|
||
assert "now" in caption and "0" in caption
|
||
|
||
|
||
def test_axis_chart_ticks_thin_a_label_that_would_collide() -> None:
|
||
"""A tick whose label would overprint the one before it is dropped, tick and all."""
|
||
rows = timeline_rows([9] * 16, rows=1)
|
||
out = axis_chart(rows, 9, 8, ticks=[(0, "aaaa"), (2, "aaaa"), (7, "b")])
|
||
border, caption = out[1].plain, out[2].plain
|
||
assert caption.count("aaaa") == 1 # the crowded twin was skipped
|
||
assert "b" in caption
|
||
assert border.count("┬") == 2 # …and its tick with it (only two survive)
|