LCD emulation
This commit is contained in:
@@ -82,9 +82,9 @@ To start the current emulator harness:
|
|||||||
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
|
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
|
||||||
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
|
- Recognizes likely LCD E-clock access routines at `H'F200`/`H'F201`, including busy-flag polling and data/control writes.
|
||||||
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
|
- Generates a separate C-like pseudocode view from the JSON, preserving labels, calls, branches, register names, inferred symbols, metadata comments, optional cycle notes, and simple structured `if`/`do while` patterns.
|
||||||
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, and an opt-in P9 transfer fast path.
|
- Provides an early H8/536 emulator harness with ROM/RAM/register memory mapping, reset-vector boot, SCI1 transmit capture, MOV condition-code updates, `SCB/F`, stack/call/return support, indirect `JMP/JSR @Rn` dispatch, scaffolded SCI1 RXI/ERI/TXI and interval/FRT1-OCIA/FRT2-OCIA interrupt scheduling, a P9 bit-banged bus model, a 16x4 LCD bus/DDRAM model for `H'F200`/`H'F201`, and an opt-in P9 transfer fast path.
|
||||||
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
|
- Includes an emulator probe that reports hot PCs, recent P9/SCI accesses, serial report queue/gate traces, RAM lifecycle watches, final SCI1/TXI state, and captured P9 byte candidates while running the real ROM.
|
||||||
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, listens for device TX frames, and reports serial latch/table/LCD-buffer effects.
|
- Includes an RX command probe that boots until SCI1 RXI is serviceable, injects host six-byte frames through RDR/RDRF, listens for device TX frames, and reports serial latch/table/LCD-buffer and emulated-LCD effects.
|
||||||
|
|
||||||
Current serial observations:
|
Current serial observations:
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ Current serial observations:
|
|||||||
- Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms.
|
- Idle cadence from the reference file: 54 frames, average about 699.9 ms, min 601 ms, max 803 ms.
|
||||||
- Static/runtime finding: `F9C4` is a candidate idle heartbeat/report countdown. Init loads `H'14`, `loc_BA26` reloads `H'07` after a send, FRT2 OCIA decrements it, and `loc_4046` can enqueue report `H'0000` when it reaches zero and the queue is empty.
|
- Static/runtime finding: `F9C4` is a candidate idle heartbeat/report countdown. Init loads `H'14`, `loc_BA26` reloads `H'07` after a send, FRT2 OCIA decrements it, and `loc_4046` can enqueue report `H'0000` when it reaches zero and the queue is empty.
|
||||||
- Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`.
|
- Runtime-confirmed heartbeat path: `loc_4067` writes `H'0000` into the queue via a zero-extended word move, `loc_BAF2/loc_BB08` dequeue it, `loc_BB1C/loc_BB20/loc_BB2B` stage the TX bytes, and `loc_BA26` emits `00 00 00 00 80 DA`.
|
||||||
|
- Emulator LCD finding: the ROM writes the boot/no-active-session message to the LCD bus as ` CONNECT:NOT ACT` on line 0 by the time SCI1 RX is serviceable. Valid and invalid six-byte host frames leave that display active while normal serial replies/heartbeats continue.
|
||||||
- RX probe finding: the `--preset connect-lcd` sequence reaches the command-`0x04` handler; `04 00 00 80 00 DE` writes table index zero, fills the LCD line buffer with `CONNECT: OK`, and emits `02 00 02 00 00 5A` in the current emulator model.
|
- RX probe finding: the `--preset connect-lcd` sequence reaches the command-`0x04` handler; `04 00 00 80 00 DE` writes table index zero, fills the LCD line buffer with `CONNECT: OK`, and emits `02 00 02 00 00 5A` in the current emulator model.
|
||||||
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
- Observed capture labels such as `cam_power_button_candidate` and `call_button_candidate` are deliberately treated as capture overlays, not protocol facts hard-coded in ROM.
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ python h8536_emulator_rx_probe.py --help
|
|||||||
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
|
- `--target-frame "00 00 00 00 80 DA"`: compare staged/emitted TX bytes against an expected six-byte frame.
|
||||||
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
|
- `h8536_emulator_rx_probe.py "04 00 00 80 00"`: append the checksum, inject the host frame through SCI1 RX, and summarize responses.
|
||||||
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
|
- `h8536_emulator_rx_probe.py --preset connect-lcd`: replay the current CONNECT LCD activation candidates.
|
||||||
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, and schedules FRT1/FRT2 OCIA. With the P9 fast path and current timer cadence, the emulator reaches the SCI1 transmit path and emits the observed heartbeat frame `00 00 00 00 80 DA`.
|
- Current status: boots from `H'1000`, initializes SCI1, models the first P9 bit-banged handshakes, captures P9 byte candidates, can optionally fast-path known P9 routines, schedules FRT1/FRT2 OCIA, captures the ROM-driven LCD line ` CONNECT:NOT ACT`, and emits the observed heartbeat frame `00 00 00 00 80 DA`.
|
||||||
|
|
||||||
## Code Layout
|
## Code Layout
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ python h8536_emulator_rx_probe.py --help
|
|||||||
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
|
- `h8536/report_source_trace.py`: direct `loc_3E54` report enqueue source tracer.
|
||||||
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
|
- `h8536/table_xrefs.py`: table/index xrefs and LCD correlation report generation.
|
||||||
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
|
- `h8536/consistency.py`: decompiler/pseudocode semantic consistency checks.
|
||||||
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, runner, probe, CLI, and peripheral scaffolding.
|
- `h8536/emulator/`: early H8/536 emulator package split into CPU state, memory map, SCI1 TX capture, P9 bus model, LCD model, runner, probe, CLI, and peripheral scaffolding.
|
||||||
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
|
- `h8536/emulator/rx_probe.py`: host-frame injection and response/listener probe for SCI1 RX experiments.
|
||||||
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
|
- `h8536/board_profile.py`: Sony RCP-TX7 board-trace annotations, including the MAX202 RS232 path.
|
||||||
- `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis.
|
- `h8536/peripheral_access.py`: FRT/A-D TEMP-register access analysis.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from .cpu import CPUState
|
|||||||
from .errors import EmulatorError, UnsupportedInstruction
|
from .errors import EmulatorError, UnsupportedInstruction
|
||||||
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
|
from .fast_paths import P9FastPath, P9FastPathConfig, P9FastPathEvent
|
||||||
from .memory import MemoryAccess, MemoryMap, describe_regions
|
from .memory import MemoryAccess, MemoryMap, describe_regions
|
||||||
|
from .peripherals import LCD
|
||||||
from .runner import H8536Emulator, RunReport
|
from .runner import H8536Emulator, RunReport
|
||||||
from .sci import SCI1, SciTxEvent
|
from .sci import SCI1, SciTxEvent
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ __all__ = [
|
|||||||
"IPRA",
|
"IPRA",
|
||||||
"IPRC",
|
"IPRC",
|
||||||
"IPRE",
|
"IPRE",
|
||||||
|
"LCD",
|
||||||
"MemoryAccess",
|
"MemoryAccess",
|
||||||
"MemoryMap",
|
"MemoryMap",
|
||||||
"ON_CHIP_RAM_END",
|
"ON_CHIP_RAM_END",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from .constants import (
|
|||||||
SCI1_SSR,
|
SCI1_SSR,
|
||||||
SCI1_TDR,
|
SCI1_TDR,
|
||||||
)
|
)
|
||||||
from .peripherals.lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
|
from .peripherals.lcd import LCD, LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
|
||||||
from .peripherals.p9_bus import P9Bus
|
from .peripherals.p9_bus import P9Bus
|
||||||
from .sci import SCI1
|
from .sci import SCI1
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ class MemoryMap:
|
|||||||
def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None:
|
def __init__(self, rom_bytes: bytes, sci1: SCI1 | None = None) -> None:
|
||||||
self.rom = Rom(rom_bytes, base=0)
|
self.rom = Rom(rom_bytes, base=0)
|
||||||
self.sci1 = sci1 if sci1 is not None else SCI1()
|
self.sci1 = sci1 if sci1 is not None else SCI1()
|
||||||
|
self.lcd = LCD()
|
||||||
self.p9_bus = P9Bus()
|
self.p9_bus = P9Bus()
|
||||||
self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1)
|
self.ram = bytearray(ON_CHIP_RAM_END - ON_CHIP_RAM_START + 1)
|
||||||
self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1)
|
self.registers = bytearray(REGISTER_FIELD_END - REGISTER_FIELD_START + 1)
|
||||||
@@ -60,11 +61,9 @@ class MemoryMap:
|
|||||||
value = self.sci1.read(address)
|
value = self.sci1.read(address)
|
||||||
self._set_register(address, value)
|
self._set_register(address, value)
|
||||||
elif address == LCD_E_CLOCK_STATUS:
|
elif address == LCD_E_CLOCK_STATUS:
|
||||||
# LCD E-clock/status space. Default to ready/zero so boot can pass
|
value = self.lcd.read_status()
|
||||||
# busy-flag polling until a fuller external bus model exists.
|
|
||||||
value = 0x00
|
|
||||||
elif address == LCD_E_CLOCK_DATA:
|
elif address == LCD_E_CLOCK_DATA:
|
||||||
value = self.external.get(address, 0x00)
|
value = self.lcd.read_data()
|
||||||
elif address in self.external:
|
elif address in self.external:
|
||||||
value = self.external[address]
|
value = self.external[address]
|
||||||
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
|
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
|
||||||
@@ -94,6 +93,12 @@ class MemoryMap:
|
|||||||
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
|
if address in (SCI1_SMR, SCI1_BRR, SCI1_SCR, SCI1_TDR, SCI1_SSR, SCI1_RDR):
|
||||||
self.sci1.write(address, value)
|
self.sci1.write(address, value)
|
||||||
self._set_register(address, self.sci1.read(address))
|
self._set_register(address, self.sci1.read(address))
|
||||||
|
elif address == LCD_E_CLOCK_STATUS:
|
||||||
|
self.lcd.write_command(value)
|
||||||
|
self.external[address] = value
|
||||||
|
elif address == LCD_E_CLOCK_DATA:
|
||||||
|
self.lcd.write_data(value)
|
||||||
|
self.external[address] = value
|
||||||
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
|
elif ON_CHIP_RAM_START <= address <= ON_CHIP_RAM_END:
|
||||||
self.ram[address - ON_CHIP_RAM_START] = value
|
self.ram[address - ON_CHIP_RAM_START] = value
|
||||||
elif address == P9DDR:
|
elif address == P9DDR:
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .lcd import LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS
|
from .lcd import LCD, LCD_E_CLOCK_DATA, LCD_E_CLOCK_STATUS, LCD_LINE_WIDTH
|
||||||
from .p9_bus import P9_ACK_BIT, P9_STROBE_BIT, P9Bus, P9StrobeEvent
|
from .p9_bus import P9_ACK_BIT, P9_STROBE_BIT, P9Bus, P9StrobeEvent
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LCD_E_CLOCK_DATA",
|
"LCD_E_CLOCK_DATA",
|
||||||
"LCD_E_CLOCK_STATUS",
|
"LCD_E_CLOCK_STATUS",
|
||||||
|
"LCD",
|
||||||
|
"LCD_LINE_WIDTH",
|
||||||
"P9_ACK_BIT",
|
"P9_ACK_BIT",
|
||||||
"P9_STROBE_BIT",
|
"P9_STROBE_BIT",
|
||||||
"P9Bus",
|
"P9Bus",
|
||||||
|
|||||||
@@ -1,5 +1,69 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
LCD_E_CLOCK_STATUS = 0xF200
|
LCD_E_CLOCK_STATUS = 0xF200
|
||||||
LCD_E_CLOCK_DATA = 0xF201
|
LCD_E_CLOCK_DATA = 0xF201
|
||||||
|
|
||||||
|
LCD_LINE_WIDTH = 16
|
||||||
|
LCD_LINE_STARTS = (0x00, 0x40, 0x10, 0x50)
|
||||||
|
LCD_DDRAM_SIZE = 0x80
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LCD:
|
||||||
|
ddram: bytearray = field(default_factory=lambda: bytearray([0x20] * LCD_DDRAM_SIZE))
|
||||||
|
cursor: int = 0
|
||||||
|
data_latch: int = 0
|
||||||
|
command_latch: int = 0
|
||||||
|
|
||||||
|
def read_status(self) -> int:
|
||||||
|
return self.cursor & 0x7F
|
||||||
|
|
||||||
|
def read_data(self) -> int:
|
||||||
|
return self.data_latch & 0xFF
|
||||||
|
|
||||||
|
def write_command(self, value: int) -> None:
|
||||||
|
value &= 0xFF
|
||||||
|
self.command_latch = value
|
||||||
|
if value & 0x80:
|
||||||
|
self.cursor = value & 0x7F
|
||||||
|
elif value == 0x01:
|
||||||
|
self.ddram[:] = bytes([0x20]) * LCD_DDRAM_SIZE
|
||||||
|
self.cursor = 0
|
||||||
|
elif value == 0x02:
|
||||||
|
self.cursor = 0
|
||||||
|
|
||||||
|
def write_data(self, value: int) -> None:
|
||||||
|
value &= 0xFF
|
||||||
|
self.data_latch = value
|
||||||
|
if 0 <= self.cursor < LCD_DDRAM_SIZE:
|
||||||
|
self.ddram[self.cursor] = value
|
||||||
|
self.cursor = (self.cursor + 1) & 0x7F
|
||||||
|
|
||||||
|
def line_text(self, line: int, width: int = LCD_LINE_WIDTH) -> str:
|
||||||
|
if not 0 <= line < len(LCD_LINE_STARTS):
|
||||||
|
raise ValueError(f"LCD line out of range: {line}")
|
||||||
|
start = LCD_LINE_STARTS[line]
|
||||||
|
return "".join(_display_char(self.ddram[(start + offset) & 0x7F]) for offset in range(width))
|
||||||
|
|
||||||
|
def display_lines(self, lines: int = 4, width: int = LCD_LINE_WIDTH) -> list[str]:
|
||||||
|
return [self.line_text(line, width) for line in range(lines)]
|
||||||
|
|
||||||
|
def display_text(self, lines: int = 4, width: int = LCD_LINE_WIDTH) -> str:
|
||||||
|
return " | ".join(self.display_lines(lines, width))
|
||||||
|
|
||||||
|
|
||||||
|
def _display_char(value: int) -> str:
|
||||||
|
return chr(value) if 0x20 <= value <= 0x7E else "."
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LCD",
|
||||||
|
"LCD_DDRAM_SIZE",
|
||||||
|
"LCD_E_CLOCK_DATA",
|
||||||
|
"LCD_E_CLOCK_STATUS",
|
||||||
|
"LCD_LINE_STARTS",
|
||||||
|
"LCD_LINE_WIDTH",
|
||||||
|
]
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ class FrameResult:
|
|||||||
lines.append(" new_tx_frames=" + " | ".join(format_frame(frame) for frame in self.new_tx_frames))
|
lines.append(" new_tx_frames=" + " | ".join(format_frame(frame) for frame in self.new_tx_frames))
|
||||||
else:
|
else:
|
||||||
lines.append(" new_tx_frames=none")
|
lines.append(" new_tx_frames=none")
|
||||||
|
lcd_display = self.state_after.get("lcd_display_ascii")
|
||||||
|
if isinstance(lcd_display, str):
|
||||||
|
lines.append(f" lcd_display={lcd_display!r}")
|
||||||
state_changes = _state_change_lines(self.state_before, self.state_after)
|
state_changes = _state_change_lines(self.state_before, self.state_after)
|
||||||
if state_changes:
|
if state_changes:
|
||||||
lines.append(" state_changes:")
|
lines.append(" state_changes:")
|
||||||
@@ -220,7 +223,8 @@ def run_rx_probe(
|
|||||||
boot_summary = (
|
boot_summary = (
|
||||||
f"boot={boot_reason} steps={boot_steps_used} pc={h16(emulator.cpu.pc)} "
|
f"boot={boot_reason} steps={boot_steps_used} pc={h16(emulator.cpu.pc)} "
|
||||||
f"SCR={emulator.sci1.scr:02X} SSR={emulator.sci1.ssr:02X} "
|
f"SCR={emulator.sci1.scr:02X} SSR={emulator.sci1.ssr:02X} "
|
||||||
f"rx_serviceable={int(_rx_ready(emulator))}"
|
f"rx_serviceable={int(_rx_ready(emulator))} "
|
||||||
|
f"lcd_display={emulator.memory.lcd.display_text(lines=4)!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
results = [
|
results = [
|
||||||
@@ -380,6 +384,7 @@ def _state_snapshot(emulator: H8536Emulator) -> dict[str, int | str]:
|
|||||||
for address, name in STATE_WORDS.items():
|
for address, name in STATE_WORDS.items():
|
||||||
snapshot[name] = emulator.memory.read16(address)
|
snapshot[name] = emulator.memory.read16(address)
|
||||||
snapshot["lcd_line_buffer_ascii"] = _ascii_window(emulator, 0xFAF0, 16)
|
snapshot["lcd_line_buffer_ascii"] = _ascii_window(emulator, 0xFAF0, 16)
|
||||||
|
snapshot["lcd_display_ascii"] = emulator.memory.lcd.display_text(lines=4, width=16)
|
||||||
snapshot["tx_frame_staging"] = format_frame(bytes(emulator.memory.read8(0xF850 + offset) for offset in range(6)))
|
snapshot["tx_frame_staging"] = format_frame(bytes(emulator.memory.read8(0xF850 + offset) for offset in range(6)))
|
||||||
snapshot["rx_frame_validation"] = format_frame(bytes(emulator.memory.read8(0xF860 + offset) for offset in range(6)))
|
snapshot["rx_frame_validation"] = format_frame(bytes(emulator.memory.read8(0xF860 + offset) for offset in range(6)))
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|||||||
@@ -20,6 +20,29 @@ class EmulatorLcdBusTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x41)
|
self.assertEqual(memory.read8(LCD_E_CLOCK_DATA), 0x41)
|
||||||
|
|
||||||
|
def test_command_sets_ddram_cursor_and_data_updates_display_text(self):
|
||||||
|
memory = MemoryMap(b"\x00" * 4)
|
||||||
|
|
||||||
|
memory.write8(LCD_E_CLOCK_STATUS, 0x80)
|
||||||
|
for value in b"CONNECT: NOT ACT":
|
||||||
|
memory.write8(LCD_E_CLOCK_DATA, value)
|
||||||
|
|
||||||
|
self.assertEqual(memory.lcd.line_text(0), "CONNECT: NOT ACT")
|
||||||
|
|
||||||
|
def test_rom_line_mapping_matches_16x4_lcd_addresses(self):
|
||||||
|
memory = MemoryMap(b"\x00" * 4)
|
||||||
|
|
||||||
|
memory.write8(LCD_E_CLOCK_STATUS, 0xC0)
|
||||||
|
memory.write8(LCD_E_CLOCK_DATA, ord("1"))
|
||||||
|
memory.write8(LCD_E_CLOCK_STATUS, 0x90)
|
||||||
|
memory.write8(LCD_E_CLOCK_DATA, ord("2"))
|
||||||
|
memory.write8(LCD_E_CLOCK_STATUS, 0xD0)
|
||||||
|
memory.write8(LCD_E_CLOCK_DATA, ord("3"))
|
||||||
|
|
||||||
|
self.assertTrue(memory.lcd.line_text(1).startswith("1"))
|
||||||
|
self.assertTrue(memory.lcd.line_text(2).startswith("2"))
|
||||||
|
self.assertTrue(memory.lcd.line_text(3).startswith("3"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user