#!/usr/bin/env python3 """Tests fuer den Protokoll-Layer, gepruefte Referenz ist das Handbuch. python3 -m unittest -v test_migra """ import unittest import config import coords import migra from display import Display from transport import FakeTransport class TestTelegramm(unittest.TestCase): def test_handbuch_beispiel_mit_pruefsumme(self): """Handbuch 3.10, Beispiel 2: Zeichen 'A' an Anzeige mit Adresse 1. Master: 02 81 80 83 F0 F1 41 FA F6 03 """ frame = migra.build_telegram(b"A", dest=1, src=0, checksum=True, response=True) self.assertEqual(frame, bytes.fromhex("02818083F0F141FAF603")) def test_handbuch_beispiel_ohne_pruefsumme(self): """Handbuch 3.10, Beispiel 1: ohne Pruefsumme, mit Antwort. Ohne Pruefsumme duerfen LEN-H/L gerade *nicht* mitgesendet werden. """ frame = migra.build_telegram(b"Hallo Welt", dest=1, src=0, checksum=False, response=True) self.assertEqual(frame[:4], bytes([0x02, 0x81, 0x80, 0x81])) self.assertEqual(frame[4:-1], b"Hallo Welt") self.assertEqual(frame[-1], 0x03) def test_frame_control_bits(self): def fc(checksum, response): return migra.build_telegram(b"x", checksum=checksum, response=response)[3] self.assertEqual(fc(False, False), 0x80) self.assertEqual(fc(False, True), 0x81) # so faehrt das alte C-Programm self.assertEqual(fc(True, False), 0x82) self.assertEqual(fc(True, True), 0x83) def test_data_unit_zu_lang(self): with self.assertRaises(migra.MigraError): migra.build_telegram(b"x" * 231) class TestDataUnit(unittest.TestCase): def test_trennzeichen_nach_esc_sequenz(self): """Handbuch 3.9: Data-Unit = 1B 'Z01' 1B 'C002003' 1B 'A301' 1F Text.""" du = (migra.DataUnit() .charset(1, spaced=False) .cursor(2, 3) .attributes(fg=migra.YELLOW, bg=migra.BLACK, blink=migra.BLINKING) .text("Online-Text")) self.assertEqual( du.bytes(), b"\x1bZ01\x1bC002003\x1bA301\x1fOnline-Text", ) def test_kein_trennzeichen_zwischen_esc_sequenzen(self): du = migra.DataUnit().fill(migra.BLACK).charset(0) self.assertEqual(du.bytes(), b"\x1bF0\x1bz00") def test_trennzeichen_nur_einmal_pro_textblock(self): du = migra.DataUnit().cursor(0, 0).text("AB").text("CD") self.assertEqual(du.bytes(), b"\x1bC000000\x1fABCD") def test_cursor_ist_ascii_codiert(self): """Handbuch 3.7: Position 123 wird als '1' '2' '3' uebertragen.""" self.assertEqual(migra.DataUnit().cursor(123, 45).bytes(), b"\x1bC123045") def test_loeschen_und_helligkeit(self): self.assertEqual(migra.DataUnit().fill(migra.BLACK).bytes(), b"\x1bF0") self.assertEqual(migra.DataUnit().brightness(100, migra.RED).bytes(), b"\x1bH2100") self.assertEqual(migra.DataUnit().brightness(80, migra.RED).bytes(), b"\x1bH2080") def test_helligkeit_ausserhalb_bereich(self): with self.assertRaises(migra.MigraError): migra.DataUnit().brightness(101) def test_text_encoding_latin1(self): self.assertEqual(migra.encode_text("+45°12'"), b"+45\xb012'") self.assertEqual(migra.encode_text("✓"), b"?") # nicht darstellbar class TestAntwort(unittest.TestCase): def test_ok(self): frame = bytes([0x02, 0x80, 0x81, 0x80, ord("0"), 0x03]) self.assertEqual(migra.parse_response(frame), "0") def test_fehlercode_wirft(self): frame = bytes([0x02, 0x80, 0x81, 0x80, ord("3"), 0x03]) with self.assertRaises(migra.MigraError) as ctx: migra.parse_response(frame) self.assertIn("ESC-Sequenz", str(ctx.exception)) def test_timeout_wirft(self): with self.assertRaises(migra.MigraError): migra.parse_response(b"") class TestKoordinaten(unittest.TestCase): def test_ra_formate_werden_erkannt(self): for text in ("12h34m56s", "12:34:56", "12 34 56"): self.assertEqual(coords.parse_ra(text), (12, 34, 56)) def test_ra_dezimal(self): self.assertEqual(coords.parse_ra("12.5"), (12, 30, 0)) def test_dec_mit_vorzeichen(self): self.assertEqual(coords.parse_dec("+45d12m30s"), ("+", 45, 12, 30)) self.assertEqual(coords.parse_dec("-07:15:00"), ("-", 7, 15, 0)) def test_dec_ohne_vorzeichen_ist_positiv(self): self.assertEqual(coords.parse_dec("45:12:30"), ("+", 45, 12, 30)) def test_bereichsgrenzen(self): with self.assertRaises(ValueError): coords.parse_ra("24:00:01") with self.assertRaises(ValueError): coords.parse_dec("+91:00:00") def test_formatierung_hochpraezision(self): hp = dict(high_precision=True) self.assertEqual(coords.format_ra("12h34m56s", width=9, **hp), "12h34m56s") self.assertEqual(coords.format_ra("12h34m56s", width=8, **hp), "12:34:56") self.assertEqual(coords.format_dec("+45d12m30s", width=10, **hp), "+45°12'30\"") self.assertEqual(coords.format_dec("+45d12m30s", width=9, **hp), "+45°12'30") self.assertEqual(coords.format_dec("+45d12m30s", width=7, **hp), "+45°12'") def test_formatierung_niedrigpraezision(self): lp = dict(high_precision=False) # RA: Zehntel-Minute (0.9 min) kommt als Sekunden (54 s) heraus self.assertEqual(coords.format_ra("11:36.9", width=9, **lp), "11h36m54s") self.assertEqual(coords.format_ra("11:36.9", width=6, **lp), "11h36m") # DEC: ohne Sekunden self.assertEqual(coords.format_dec("-00°55", width=9, **lp), "-00°55'") self.assertEqual(coords.format_dec("-00°55", width=6, **lp), "-00:55") class TestDisplay(unittest.TestCase): def test_je_zeile_ein_telegramm(self): """Eine Zeile je Telegramm, jeweils sauber gerahmt. Frueher war beides ein Telegramm. Seit jedes Zeichen einzeln positioniert wird (10 statt 9 Zeichen), waeren beide Zeilen zusammen zu dicht an migra.MAX_DATA_UNIT -- siehe display.show_lines. """ t = FakeTransport(echo=False) Display(t, response=True).show_lines("12:34:56", "+45:12:30", clear=True) self.assertEqual(len(t.frames), 2) for frame in t.frames: self.assertEqual(frame[:4], bytes([0x02, 0x81, 0x80, 0x81])) self.assertEqual(frame[-1], 0x03) self.assertLessEqual(len(frame), migra.MAX_DATA_UNIT) unten, oben = t.frames # untere Zeile zuerst self.assertIn(b"\x1bF0", unten) # mit clear=True: einmal loeschen self.assertNotIn(b"\x1bF0", oben) # Jedes Zeichen sitzt auf seiner eigenen Zelle: x = LINE_X + i*CELL_WIDTH. def zelle(i, y): return b"\x1bC%03d%03d" % (config.LINE_X + i * config.CELL_WIDTH, y) # "12:34:56" hat 8 Zeichen und wird in 10 Zellen mittig gesetzt (ab Zelle 1), # "+45:12:30" hat 9 und beginnt daher schon bei Zelle 0. self.assertIn(zelle(1, 0) + b"\x1f1", oben) self.assertIn(zelle(2, 0) + b"\x1f2", oben) self.assertIn(zelle(0, 9) + b"\x1f+", unten) def test_ohne_clear_kein_fill_aber_aufgefuellt(self): """Standard (clear=False): kein Vollbild-Loeschen, Zeilen auf volle Breite.""" from test_display import zeile, zentriert t = FakeTransport(echo=False) Display(t, response=True).show_lines("12:34:56", clear=False) self.assertNotIn(b"\x1bF0", t.frames[0]) # auf CHARS_PER_LINE mit Leerzeichen aufgefuellt (mittig, siehe CENTER_LINES) self.assertEqual(zeile(t.frames, 0), zentriert("12:34:56")) def test_gradzeichen_via_zeichensatz(self): """Gradzeichen wird durch kurzen Wechsel auf Zeichensatz 1 erzeugt.""" t = FakeTransport(echo=False) Display(t, response=True).show_lines("12h34m56s", "+45°12'30\"") data = t.frames[0][4:-1] # Umschalten auf Zeichensatz 1, "/", zurueck auf Zeichensatz 0 self.assertIn(b"\x1bz01\x1f/\x1bz00", data) def test_oberste_zeile_wird_zuletzt_gezeichnet(self): """Wegen der Home-Zellen-Stoerung muss y=0 nach y=9 gesendet werden. Seit es ein Telegramm je Zeile gibt, entscheidet die Reihenfolge der Telegramme statt der Reihenfolge innerhalb eines Telegramms. """ t = FakeTransport(echo=False) Display(t, response=True).show_lines("12h34m56s", "+45°12'30\"") from test_display import zellen self.assertEqual(len(t.frames), 2) # Auf die y-Koordinaten schauen, nicht auf feste Cursor-Bytes. y_zuerst = set(y for _, y in zellen(t.frames[:1])) y_zuletzt = set(y for _, y in zellen(t.frames[1:])) self.assertEqual(y_zuerst, {config.LINE_Y[1]}) # untere Zeile zuerst self.assertEqual(y_zuletzt, {config.LINE_Y[0]}) # oberste Zeile zuletzt def test_gradzeichen_fallback_ohne_zeichensatz(self): """Ist DEGREE_CHARSET None, wird der Fallbacktext statt der Umschaltung genutzt.""" import config t = FakeTransport(echo=False) alt = config.DEGREE_CHARSET try: config.DEGREE_CHARSET = None Display(t, response=True).show_lines("+45°12'30\"") finally: config.DEGREE_CHARSET = alt data = t.frames[0][4:-1] self.assertNotIn(b"\x1bz01", data) self.assertIn(config.DEGREE_FALLBACK.encode(), data) def test_fehlercode_der_anzeige_wirft(self): t = FakeTransport(echo=False) t._response = bytes([0x02, 0x80, 0x81, 0x80, ord("4"), 0x03]) with self.assertRaises(migra.MigraError): Display(t, response=True).clear() def test_show_line_nur_eine_zeile(self): """show_line schreibt nur die angegebene Zeile, ohne Vollbild-Loeschen.""" from test_display import zellen t = FakeTransport(echo=False) Display(t, response=True).show_line(0, "18h36m56s") self.assertNotIn(b"\x1bF0", t.frames[0]) # kein Loeschen y_werte = set(y for _, y in zellen(t.frames)) self.assertEqual(y_werte, {config.LINE_Y[0]}) # nur Zeile 1, Zeile 2 unberuehrt def test_show_line_ungueltiger_index(self): t = FakeTransport(echo=False) with self.assertRaises(ValueError): Display(t, response=True).show_line(5, "x") def test_ohne_antwort_fc_bit0_ist_null(self): """Standardbetrieb ohne Antwort: FC = 0x80, kein Fehlercode wird gelesen.""" import config alt = config.QUIET_TIME try: config.QUIET_TIME = 0.0 t = FakeTransport(echo=False) # selbst wenn die (ignorierte) Antwort einen Fehler meldet, darf nichts fliegen t._response = bytes([0x02, 0x80, 0x81, 0x80, ord("4"), 0x03]) Display(t, response=False).clear() finally: config.QUIET_TIME = alt self.assertEqual(t.frames[0][3], 0x80) # FC ohne Antwort-Bit if __name__ == "__main__": unittest.main(verbosity=2)