fix(display): Freigabe-Ziel lesbar, Tests schriftunabhaengig
Sechs Review-Befunde an e2d836a/2aab1b0. C1: die Pixelschwellen in test_render.py waren gegen Pillow 12/Arial kalibriert und auf dem Pi wie auf Debian 13 (DejaVu) rot - "> 150 schwarze Pixel in Zeile 11" sind dort nur 143. Die Zusagen ueber den Kopfbalken sind jetzt Anteile und Symmetrien statt Pixelzahlen (mindestens 55 % des Balkens schwarz, weisse Schrift links UND rechts der Mitte, Balken ueber die volle Breite). Zwei weitere schriftabhaengige Schwellen ebenso: die grosse Zeile hebt sich ueber das VERHAELTNIS der Tintenhoehen vom Beiwerk ab, und die Randgrenze zaehlt Zeilenbaender statt Tinten-Cluster. I2: im Zustand "armed" stand "FREIGEGEBEN: <ziel>" in der Kopfzeile, wo 112 px Platz sind und der Text 182 px misst - gezeichnet wurde "FREIGEGEBE…", bei jedem Ziel gleich. Im Kopf steht jetzt nur noch das Label, der Zielname als eigene Beiwerkzeile ueber die volle Breite; die SSID-Zeile gibt dafuer in "armed" ihren Platz ab. Geprueft wird das gezeichnete Bild (Pixelvergleich gegen eine Referenzzeile), nicht lines(). I3: der Kommentar an display_handover() begruendete die Rueckgabewert-Durchreichung damit, dass der Handgriff bei stummem Panel rot werde. Das stimmt nicht - display.py beendet sich bei fehlendem Panel/SPI/Pillow mit 0. Der Kommentar sagt jetzt, dass man die Revision am Panel beurteilt und nicht am Exit-Code. I4: zwischen "systemctl stop" und "systemctl start" gab es keinen Signalhandler - ein Strg-C waehrend "display test" (der dokumentierte Weg, die Revision zu bestimmen, bei falscher Revision haengt das Panel) liess den Anzeigedienst gestoppt zurueck. trap auf INT/TERM/HUP, das Wiederanfahren in display_service_resume(), Handler beenden selbst mit 128 + Signalnummer. M2: HEADER_PHASE_MIN_WIDTH_PX bleibt als Sicherheitsnetz, ist jetzt aber getestet statt toter Code. M4: compose() baute je Aufruf fuenf Schriften neu. Prozess-Cache (mit ImageFont-Modul und FONT_SEARCH_PATHS im Schluessel, damit die Tests nicht verfaelscht werden) und faules Laden je Rolle: 25,0 ms -> 7,0 ms. Die Pixeltests laufen zusaetzlich unter Pillow 11.1 mit echtem DejaVu und unter fuenf weiteren Schriften. Sieben Mutationsproben angewandt, angesehen und zurueckgenommen. tools/test_host.sh gruen.
This commit is contained in:
@@ -148,25 +148,94 @@ def test_off_screen_is_drawn_big_and_centered():
|
||||
assert abs(top - bottom) <= 4, f"nicht senkrecht mittig (oben {top}, unten {bottom})"
|
||||
|
||||
|
||||
def test_header_is_a_black_bar_with_white_text():
|
||||
"""Die Kopfzeile ist invertiert: schwarzer Balken ueber die volle Breite, Modus in
|
||||
Weiss darin. Geprueft an einer Zeile mitten im Balken - die muss ueberwiegend schwarz
|
||||
sein UND weisse Pixel enthalten (sonst stuende dort kein Text, sondern nur ein
|
||||
Balken)."""
|
||||
image = render.compose(_migration_state())
|
||||
pixels = image.load()
|
||||
row = render.HEADER_BAR_HEIGHT_PX // 2
|
||||
HEADER_MIN_BLACK_SHARE = 0.55
|
||||
|
||||
black = sum(1 for x in range(render.PANEL_WIDTH_PX) if pixels[x, row] == 0)
|
||||
white = sum(1 for x in range(render.PANEL_WIDTH_PX) if pixels[x, row] != 0)
|
||||
assert black > 150, f"Kopfzeile ist kein schwarzer Balken (nur {black} schwarze Pixel)"
|
||||
assert white >= 5, "keine weisse Schrift im Balken - der Balken uebermalt den Modus"
|
||||
|
||||
def _assert_header_bar_with_text_on_both_sides(image, hint=""):
|
||||
"""Die Zusage ueber den Kopfbalken, SCHRIFTUNABHAENGIG formuliert (dritte
|
||||
Hardware-Abnahme, Befund C1).
|
||||
|
||||
Vorher stand hier eine absolute Pixelzahl ("> 150 schwarze Pixel in Zeile 11"), gegen
|
||||
Pillow 12 mit ARIAL auf der Entwicklungsmaschine kalibriert. Auf dem Pi und auf der
|
||||
Debian-13-Maschine zeichnet DejaVu, das merklich breiter laeuft - dort blieben 149
|
||||
schwarze Pixel uebrig und die Suite war rot, ohne dass sich am Code etwas geaendert
|
||||
hatte. Die Schwelle einfach auf 145 nachzuziehen haette nur gegen die naechste
|
||||
Schrift kalibriert.
|
||||
|
||||
Stattdessen drei Zusagen, die keine Schrift kennen:
|
||||
* ANTEIL statt Anzahl, und ueber den GANZEN Balken statt ueber eine einzelne Zeile:
|
||||
mindestens 55 % des Balkens sind schwarz. Ueber sechs sehr verschiedene Schriften
|
||||
gemessen (Arial, Arial Black, Verdana, Courier New, Impact, Georgia) liegt der
|
||||
Anteil zwischen 69 % und 93 %, mit load_default() bei 93 % - die Schwelle trennt
|
||||
also "Balken mit Schrift darin" von "kein Balken" (ohne Rechteck: rund 5 %), ohne
|
||||
an einer Schriftbreite zu haengen. Eine einzelne Zeile allein taugt dafuer nicht:
|
||||
dieselben sechs Schriften streuen in Zeile 11 zwischen 43 % und 69 %.
|
||||
* Weisse Schrift LINKS UND RECHTS der Panelmitte: der Modus steht links, die Phase
|
||||
rechts. Faellt eine von beiden aus (oder uebermalt der Balken den Text), ist die
|
||||
betreffende Haelfte leer.
|
||||
* Der Balken reicht ueber die volle Breite: die aeussersten Spalten sind
|
||||
durchgehend schwarz - sie liegen vor TEXT_MARGIN_PX, dort kann keine Schrift
|
||||
stehen."""
|
||||
pixels = image.load()
|
||||
height = render.HEADER_BAR_HEIGHT_PX
|
||||
width = render.PANEL_WIDTH_PX
|
||||
middle = width // 2
|
||||
|
||||
black = sum(1 for y in range(height) for x in range(width) if pixels[x, y] == 0)
|
||||
share = black / float(height * width)
|
||||
assert share >= HEADER_MIN_BLACK_SHARE, (
|
||||
f"Kopfzeile ist kein schwarzer Balken (nur {share:.0%} schwarz){hint}")
|
||||
|
||||
white_left = sum(1 for y in range(height) for x in range(middle) if pixels[x, y] != 0)
|
||||
white_right = sum(1 for y in range(height) for x in range(middle, width)
|
||||
if pixels[x, y] != 0)
|
||||
assert white_left >= 5, f"kein Modus-Text links im Balken{hint}"
|
||||
assert white_right >= 5, f"keine Phase rechts im Balken{hint}"
|
||||
|
||||
for x in (0, width - 1):
|
||||
assert all(pixels[x, y] == 0 for y in range(height)), (
|
||||
f"Spalte {x} des Kopfbalkens ist nicht durchgehend schwarz{hint}")
|
||||
|
||||
|
||||
def test_header_is_a_black_bar_with_white_text():
|
||||
"""Die Kopfzeile ist invertiert: schwarzer Balken ueber die volle Breite, Modus links
|
||||
und Phase rechts in Weiss darin."""
|
||||
_assert_header_bar_with_text_on_both_sides(render.compose(_migration_state()))
|
||||
|
||||
|
||||
def test_header_promise_survives_a_noticeably_wider_font(monkeypatch):
|
||||
"""Gegenprobe zu C1: dieselbe Zusage muss auch dann halten, wenn die Schrift deutlich
|
||||
mehr Platz braucht als die hier gefundene.
|
||||
|
||||
Statt eine zweite Schriftdatei vorauszusetzen (DejaVu gibt es auf der macOS-
|
||||
Entwicklungsmaschine nicht, Arial nicht auf dem Pi - ein Test, der die eine oder die
|
||||
andere braucht, waere auf je einer der beiden Maschinen nur ein Skip) wird dieselbe
|
||||
Schrift 25 % groesser gezogen: das ist genau der Effekt, den eine breitere Schrift auf
|
||||
diese Zusage hat, und es laeuft auf jeder Maschine gleich. Der Abstand DejaVu-zu-Arial
|
||||
liegt bei rund 13 % Breite ("MIGRATION": 128 px gegen 113 px), hier also mit Puffer
|
||||
ueberschritten. Dass die Phase dabei gekuerzt wird, ist erlaubt - sie darf nur nicht
|
||||
verschwinden oder den Modus ueberlaufen."""
|
||||
monkeypatch.setattr(render, "HEADER_FONT_PX", int(render.HEADER_FONT_PX * 1.25))
|
||||
monkeypatch.setattr(render, "HEADER_PHASE_FONT_PX",
|
||||
int(render.HEADER_PHASE_FONT_PX * 1.25))
|
||||
|
||||
_assert_header_bar_with_text_on_both_sides(
|
||||
render.compose(_migration_state()), hint=" (mit 25 % breiterer Schrift)")
|
||||
|
||||
|
||||
def test_hero_line_is_bigger_than_the_details():
|
||||
"""Die wichtigste Zeile (hier: der erkannte Produktname) muss sich vom Beiwerk
|
||||
abheben - sonst ist die Rollenvergabe in blocks() folgenlos. Gemessen: 15 px Tinte
|
||||
fuer die grosse Zeile gegen 10 px fuer die erste Beiwerkzeile."""
|
||||
abheben - sonst ist die Rollenvergabe in blocks() folgenlos.
|
||||
|
||||
Als VERHAELTNIS der Tintenhoehen, nicht als absoluter Pixelabstand (dritte
|
||||
Hardware-Abnahme, C1: auch die uebrigen Schwellen dieser Datei schriftunabhaengig
|
||||
machen). Die Zusage kommt aus dem Verhaeltnis der Schriftgroessen -
|
||||
HERO_FONT_PX/DETAIL_FONT_PX = 16/11 = 1,45 - und das ueberlebt einen
|
||||
Schriftwechsel, ein "mindestens 3 px mehr" nicht: ueber sieben Schriften gemessen
|
||||
liegt das Verhaeltnis zwischen 1,30 (Courier New) und 1,60 (Verdana), der absolute
|
||||
Abstand dagegen zwischen 3 px und 6 px - mit 3 px waere Courier New genau auf der
|
||||
Kante. Mit load_default() (beide Rollen rund 8 px) liegt das Verhaeltnis bei 1,0,
|
||||
der Test also weiterhin rot, wenn die Schriftsuche ausfaellt."""
|
||||
image = render.compose(_migration_state())
|
||||
hero_top = render.HEADER_BAR_HEIGHT_PX + render.TEXT_MARGIN_PX
|
||||
hero_bottom = hero_top + render.HERO_LINE_HEIGHT_PX
|
||||
@@ -177,9 +246,10 @@ def test_hero_line_is_bigger_than_the_details():
|
||||
|
||||
hero_height = hero[3] - hero[1] + 1
|
||||
detail_height = detail[3] - detail[1] + 1
|
||||
assert hero_height >= detail_height + 3, (
|
||||
assert hero_height >= detail_height * 1.2, (
|
||||
f"grosse Zeile ({hero_height} px) hebt sich nicht vom Beiwerk "
|
||||
f"({detail_height} px) ab")
|
||||
f"({detail_height} px) ab - Verhaeltnis "
|
||||
f"{hero_height / float(detail_height):.2f}, erwartet mindestens 1,20")
|
||||
|
||||
|
||||
def test_the_last_delivery_line_still_fits_on_the_panel():
|
||||
@@ -239,12 +309,22 @@ def test_compose_stops_drawing_details_before_the_bottom_margin(monkeypatch):
|
||||
image = render.compose({"mode": "migration"})
|
||||
pixels = image.load()
|
||||
|
||||
rows_with_ink = [
|
||||
any(pixels[x, y] == 0 for x in range(render.PANEL_WIDTH_PX))
|
||||
for y in range(render.HEADER_BAR_HEIGHT_PX, render.PANEL_HEIGHT_PX)]
|
||||
# Je Zeile IHR eigenes Band gezaehlt, nicht zusammenhaengende Tinten-Cluster
|
||||
# (dritte Hardware-Abnahme, C1: "auch anderswo schriftabhaengige Schwellen suchen"):
|
||||
# die Clusterzaehlung setzte stillschweigend voraus, dass zwischen zwei Zeilen eine
|
||||
# komplett tintenfreie Pixelzeile bleibt. Das ist eine Aussage ueber die
|
||||
# Schrifthoehe bei DETAIL_LINE_HEIGHT_PX Zeilenabstand - mit einer hoeher bauenden
|
||||
# Schrift waeren zwei Zeilen zu einem Cluster verschmolzen und der Test rot, ohne
|
||||
# dass am Code etwas falsch waere. Bandweise gezaehlt ist es egal, ob sich die
|
||||
# Zeilen beruehren.
|
||||
first_top = render.HEADER_BAR_HEIGHT_PX + render.TEXT_MARGIN_PX
|
||||
bands = range((render.PANEL_HEIGHT_PX - first_top) // render.DETAIL_LINE_HEIGHT_PX)
|
||||
drawn_lines = sum(
|
||||
1 for i, has_ink in enumerate(rows_with_ink)
|
||||
if has_ink and not (i > 0 and rows_with_ink[i - 1]))
|
||||
1 for i in bands
|
||||
if any(pixels[x, y] == 0
|
||||
for y in range(first_top + i * render.DETAIL_LINE_HEIGHT_PX,
|
||||
first_top + (i + 1) * render.DETAIL_LINE_HEIGHT_PX)
|
||||
for x in range(render.PANEL_WIDTH_PX)))
|
||||
|
||||
assert drawn_lines == 6, (
|
||||
f"{drawn_lines} Beiwerkzeilen gezeichnet, sechs erwartet - "
|
||||
@@ -350,6 +430,115 @@ def test_long_armed_target_does_not_overlap_the_mode_in_the_header():
|
||||
)
|
||||
|
||||
|
||||
def test_header_phase_is_dropped_when_too_little_room_is_left(monkeypatch):
|
||||
"""Deckt den "Weglassen"-Zweig ab (dritte Hardware-Abnahme, Befund M2): bleibt neben
|
||||
dem Modus weniger als HEADER_PHASE_MIN_WIDTH_PX uebrig, wird die Phase ganz
|
||||
weggelassen statt ein einsames, rechtsbuendiges "…" zu zeichnen, das sich mit dem
|
||||
Modus ueberlagert.
|
||||
|
||||
Mit den heutigen Werten kann der Zweig nicht von selbst feuern - der Modus-Text im
|
||||
Migrationsmodus ist die Konstante "MIGRATION", available_px also fest (112 px DejaVu,
|
||||
127 px Arial). Hier wird deshalb der Mindestabstand hochgedreht: das ist der
|
||||
direkteste, von jeder Schrift unabhaengige Weg, available_px unter die Schwelle zu
|
||||
druecken, und genau das ist die Bedingung, um die es geht."""
|
||||
monkeypatch.setattr(render, "HEADER_PHASE_GAP_PX", 200)
|
||||
|
||||
image = render.compose(_migration_state())
|
||||
pixels = image.load()
|
||||
header_rows = range(render.HEADER_BAR_HEIGHT_PX)
|
||||
white_cols = [x for x in range(render.PANEL_WIDTH_PX)
|
||||
if any(pixels[x, y] == 255 for y in header_rows)]
|
||||
|
||||
assert white_cols, "der Modus fehlt jetzt auch"
|
||||
# Vorbedingung: der Modus selbst reicht nicht in den geprueften rechten Streifen -
|
||||
# sonst wuerde der Test etwas anderes messen als "die Phase fehlt".
|
||||
reserved_from = render.PANEL_WIDTH_PX - render.HEADER_PHASE_MIN_WIDTH_PX
|
||||
assert max(white_cols) < reserved_from - 4, (
|
||||
f"der Modus-Text reicht bis Spalte {max(white_cols)} - der Test kann so nicht "
|
||||
"zwischen Modus und Phase unterscheiden")
|
||||
assert not [x for x in white_cols if x >= reserved_from], (
|
||||
"die Phase wurde trotz fehlenden Platzes gezeichnet - rechtsbuendig, also "
|
||||
"quer ueber den Modus")
|
||||
|
||||
|
||||
# --- Schriftladen: Prozess-Cache und faules Laden (Befund M4) ------------------------
|
||||
#
|
||||
# compose() baute je Aufruf fuenf Schriften neu: auf der Entwicklungsmaschine 21,2 ms von
|
||||
# 25,0 ms, auf einem ARMv6-Kern grob 0,5-1 s - bei JEDER ausgelieferten Lampe, auf
|
||||
# demselben Kern wie server.py.
|
||||
|
||||
def test_font_cache_reuses_the_same_font_object():
|
||||
"""Zweimal dieselbe Rolle -> die Schriftdatei wird nur einmal gelesen."""
|
||||
image_font = _FakeImageFontModule(available=(DEJAVU_REGULAR, DEJAVU_BOLD))
|
||||
|
||||
first = render._load_font(image_font, 11)
|
||||
second = render._load_font(image_font, 11)
|
||||
|
||||
assert first is second
|
||||
assert len(image_font.truetype_calls) == 1, (
|
||||
f"Schrift mehrfach geladen: {image_font.truetype_calls}")
|
||||
|
||||
|
||||
def test_font_cache_keeps_different_modules_and_search_paths_apart(monkeypatch):
|
||||
"""Der Cache-Schluessel muss das hereingereichte ImageFont-Modul UND
|
||||
FONT_SEARCH_PATHS enthalten - beides wird in den Tests ersetzt. Ein Cache, der nur
|
||||
Groesse/Fettung kennt, wuerde z.B. test_compose_still_works_without_any_font_file
|
||||
(leere Suchkette) eine vorher geladene echte Schrift unterschieben und die Zusage
|
||||
"eine fehlende Schrift haelt nichts auf" stillschweigend entwerten."""
|
||||
first_module = _FakeImageFontModule(available=(DEJAVU_REGULAR,))
|
||||
second_module = _FakeImageFontModule(available=(DEJAVU_REGULAR,))
|
||||
|
||||
render._load_font(first_module, 11)
|
||||
render._load_font(second_module, 11)
|
||||
assert len(second_module.truetype_calls) == 1, "Cache reicht Modulgrenzen hinueber"
|
||||
|
||||
monkeypatch.setattr(render, "FONT_SEARCH_PATHS", ())
|
||||
assert render._load_font(first_module, 11) == ("default",), (
|
||||
"leere Suchkette liefert eine zwischengespeicherte Schrift von vorher")
|
||||
|
||||
|
||||
def test_compose_loads_only_the_roles_it_actually_draws(monkeypatch):
|
||||
""""Station aus" zeichnet genau eine Rolle - die anderen vier Schriften duerfen gar
|
||||
nicht erst gebaut werden (faules Laden, Befund M4)."""
|
||||
real_load_font = render._load_font
|
||||
requested = []
|
||||
|
||||
def spy(image_font, size_px, bold=False):
|
||||
requested.append(size_px)
|
||||
return real_load_font(image_font, size_px, bold=bold)
|
||||
|
||||
monkeypatch.setattr(render, "_load_font", spy)
|
||||
render.compose({"mode": "off"})
|
||||
|
||||
assert requested == [render.BIG_FONT_PX], (
|
||||
f"compose() hat {len(requested)} Schriften angefordert, gezeichnet wird eine: "
|
||||
f"{requested}")
|
||||
|
||||
|
||||
def test_compose_reads_no_font_file_a_second_time(monkeypatch):
|
||||
"""Der Prozess-Cache am echten Pillow: der zweite compose()-Aufruf desselben
|
||||
Prozesses fasst keine Schriftdatei mehr an. Das ist die Zusage, an der auf dem Pi die
|
||||
Zeichenzeit haengt - der Daemon zeichnet bei jeder ausgelieferten Lampe neu."""
|
||||
from PIL import ImageFont
|
||||
|
||||
state = _migration_state()
|
||||
render.compose(state) # Cache fuellen
|
||||
|
||||
truetype_calls = []
|
||||
real_truetype = ImageFont.truetype
|
||||
|
||||
def counting_truetype(*args, **kwargs):
|
||||
truetype_calls.append(args)
|
||||
return real_truetype(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(ImageFont, "truetype", counting_truetype)
|
||||
render.compose(state)
|
||||
|
||||
assert truetype_calls == [], (
|
||||
f"Schriften werden bei jedem compose() neu von der Platte gelesen: "
|
||||
f"{truetype_calls}")
|
||||
|
||||
|
||||
def test_compose_still_works_without_any_font_file(monkeypatch):
|
||||
"""Die Zusage "eine fehlende Schrift haelt die Anzeige nicht auf" gilt unveraendert:
|
||||
ohne einen einzigen Treffer in der Suchkette faellt compose() auf load_default()
|
||||
@@ -451,10 +640,95 @@ def test_missing_fields_do_not_raise():
|
||||
assert render.lines(state)
|
||||
|
||||
|
||||
def test_armed_phase_is_visible():
|
||||
# --- "armed": WELCHE Lampe ist freigegeben? (dritte Hardware-Abnahme, Befund I2) -----
|
||||
#
|
||||
# Frueher stand "FREIGEGEBEN: <ziel>" als Phase im Kopfbalken. Neben dem Modus bleiben
|
||||
# dort 112 px (DejaVu) - der Text misst 182 px, gezeichnet wurde "FREIGEGEBE…", also ein
|
||||
# Wortfragment, das bei jedem Ziel gleich aussieht. Der alte Test dazu
|
||||
# (test_armed_phase_is_visible) prueft lines(), also den UNGEKUERZTEN Text, und war
|
||||
# deshalb gruen, waehrend auf dem Panel nichts davon zu lesen war. Die Tests hier pruefen
|
||||
# das GEZEICHNETE BILD.
|
||||
|
||||
def _armed_state():
|
||||
"""Wie _migration_state(), aber mit einem Produktnamen OHNE Unterlaengen: die
|
||||
Bildvergleiche unten schneiden die erste Beiwerkzeile zeilengenau aus, und ein 'y'
|
||||
oder 'q' der grossen Zeile darueber ragte in diesen Ausschnitt hinein."""
|
||||
state = _migration_state()
|
||||
state["last_seen"] = dict(state["last_seen"], product_name="KLYQA GU10 RGB")
|
||||
return state
|
||||
|
||||
|
||||
def _detail_band(index):
|
||||
"""(oben, unten) der `index`-ten Beiwerkzeile unter der grossen Zeile."""
|
||||
top = (render.HEADER_BAR_HEIGHT_PX + render.TEXT_MARGIN_PX
|
||||
+ render.HERO_LINE_HEIGHT_PX + index * render.DETAIL_LINE_HEIGHT_PX)
|
||||
return top, top + render.DETAIL_LINE_HEIGHT_PX
|
||||
|
||||
|
||||
def _band_pixels(image, top, bottom):
|
||||
pixels = image.load()
|
||||
return [tuple(pixels[x, y] for x in range(render.PANEL_WIDTH_PX))
|
||||
for y in range(top, bottom)]
|
||||
|
||||
|
||||
def _reference_detail_line(text, top):
|
||||
"""Zeichnet `text` allein, mit derselben Schrift und an derselben Stelle, die
|
||||
compose() fuer eine Beiwerkzeile benutzt. Der Vergleich mit dem echten Bild ist damit
|
||||
schriftunabhaengig: er sagt "genau dieser Text, ungekuerzt", ohne eine Pixelbreite zu
|
||||
behaupten."""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
image = Image.new("1", (render.PANEL_WIDTH_PX, render.PANEL_HEIGHT_PX), 255)
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.text((render.TEXT_MARGIN_PX, top), text,
|
||||
font=render._load_font(ImageFont, render.DETAIL_FONT_PX), fill=0)
|
||||
return image
|
||||
|
||||
|
||||
def test_armed_target_is_drawn_in_full_as_its_own_line():
|
||||
"""Der Zielname muss vollstaendig und lesbar auf dem Panel stehen - als eigene
|
||||
Beiwerkzeile ueber die volle Breite, nicht als Rest eines im Kopf abgeschnittenen
|
||||
Texts. Geprueft durch Pixelvergleich mit einer unabhaengig gezeichneten Referenzzeile:
|
||||
kuerzt compose() den Namen (oder laesst die Zeile ganz weg), stimmt der Ausschnitt
|
||||
nicht mehr ueberein."""
|
||||
state = _armed_state()
|
||||
expected = render.ARMED_DETAIL_PREFIX + state["armed_target"]
|
||||
assert expected in render.lines(state), render.lines(state)
|
||||
|
||||
top, bottom = _detail_band(0)
|
||||
drawn = _band_pixels(render.compose(state), top, bottom)
|
||||
reference = _band_pixels(_reference_detail_line(expected, top), top, bottom)
|
||||
|
||||
assert drawn == reference, (
|
||||
f"die erste Beiwerkzeile zeigt nicht ungekuerzt {expected!r} - der Bediener "
|
||||
"kann nicht sehen, welche Lampe freigegeben ist")
|
||||
|
||||
|
||||
def test_header_does_not_depend_on_the_armed_target_name():
|
||||
"""Die Gegenrichtung, und die eigentliche Mutationswache fuer I2: der Kopfbalken darf
|
||||
den Zielnamen gar nicht mehr enthalten. Das laesst sich ohne jede Schriftannahme
|
||||
pruefen - zwei sehr verschiedene Zielnamen muessen denselben Kopfbalken ergeben.
|
||||
Zeichnet ihn jemand wieder in den Kopf, unterscheiden sich die beiden Balken (oder,
|
||||
bei fruehem Abschneiden, spaetestens beim langen Namen)."""
|
||||
short = _armed_state()
|
||||
long_name = _armed_state()
|
||||
long_name["armed_target"] = "sehr_langer_zielname_" * 4
|
||||
|
||||
band = (0, render.HEADER_BAR_HEIGHT_PX)
|
||||
assert (_band_pixels(render.compose(short), *band)
|
||||
== _band_pixels(render.compose(long_name), *band)), (
|
||||
"der Kopfbalken haengt am Zielnamen - dort ist kein Platz dafuer, er wird "
|
||||
"abgeschnitten und ist bei jedem Ziel gleich unlesbar")
|
||||
|
||||
|
||||
def test_armed_phase_label_is_visible():
|
||||
"""Das kurze Label bleibt (es sagt, DASS etwas freigegeben ist) - nur der Zielname
|
||||
steht nicht mehr daneben."""
|
||||
lines = render.lines({"mode": "migration", "phase": "armed", "armed_target": "g95_cw",
|
||||
"served": 0, "recent": [], "ssid": "qconnex-firmware"})
|
||||
assert "FREIGEGEBEN" in "\n".join(lines)
|
||||
text = "\n".join(lines)
|
||||
assert render.ARMED_LABEL in text
|
||||
assert "g95_cw" in text
|
||||
|
||||
|
||||
# --- display.build_state() (Fixrunde 1, I3) ---------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user