feat(ppclock): 命令层/传输层/CLI/本地模板渲染,90 测试全绿
This commit is contained in:
1 parent
6f5b0c60be
commit
2b01e5f589
60 files changed
+16869
No files matched your search
@@ -0,0 +1,167 @@
|
||||
"""CLI 测试 — monkeypatch 传输工厂,验证 JSON 输出与退出码。"""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ppclock import cli
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
instances = []
|
||||
|
||||
def __init__(self, address=None, timeout=10.0):
|
||||
self.epd_writes = []
|
||||
self.rxtx_writes = []
|
||||
self.address = address
|
||||
FakeTransport.instances.append(self)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
async def write_epd(self, data, response=True):
|
||||
self.epd_writes.append(data)
|
||||
|
||||
async def write_rxtx(self, data, response=True):
|
||||
self.rxtx_writes.append(data)
|
||||
|
||||
async def request_device_id(self, timeout=8.0):
|
||||
return "DEVICE12345678"
|
||||
|
||||
async def delay(self, seconds):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_transport(monkeypatch):
|
||||
FakeTransport.instances.clear()
|
||||
monkeypatch.setattr(cli, "_make_transport", lambda args: FakeTransport(args.mac, args.timeout))
|
||||
yield FakeTransport
|
||||
|
||||
|
||||
def run_cli(capsys, argv):
|
||||
code = cli.main(argv)
|
||||
out = capsys.readouterr().out.strip()
|
||||
return code, json.loads(out) if out.startswith("{") else out
|
||||
|
||||
|
||||
class TestJsonContract:
|
||||
def test_mode_json(self, capsys):
|
||||
code, out = run_cli(capsys, ["--json", "mode", "clock1"])
|
||||
assert code == 0
|
||||
assert out["ok"] is True and out["cmd"] == "mode"
|
||||
t = FakeTransport.instances[0]
|
||||
assert t.rxtx_writes == [bytes.fromhex("e10201"), bytes([0xE2])]
|
||||
|
||||
def test_time_json(self, capsys):
|
||||
code, out = run_cli(capsys, ["--json", "time", "2026-07-24T15:30:00"])
|
||||
assert code == 0 and out["ok"]
|
||||
t = FakeTransport.instances[0]
|
||||
assert t.rxtx_writes[0][0] == 0xDD
|
||||
assert t.rxtx_writes[-1] == bytes([0xE2])
|
||||
|
||||
def test_error_json_contract(self, capsys):
|
||||
"""错误 schema:ok=false + error 字段,退出码 1"""
|
||||
code, out = run_cli(capsys, ["--json", "countdown"])
|
||||
assert code == 1
|
||||
assert out["ok"] is False and "error" in out
|
||||
|
||||
def test_mac_passthrough(self, capsys):
|
||||
run_cli(capsys, ["--json", "--mac", "AA:BB:CC:DD:EE:FF", "clear"])
|
||||
assert FakeTransport.instances[0].address == "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
|
||||
class TestCommands:
|
||||
def test_clear(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "clear"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].epd_writes[-1] == bytes([0x01])
|
||||
|
||||
def test_image_upload(self, capsys, tmp_path):
|
||||
p = tmp_path / "white.png"
|
||||
Image.new("RGB", (400, 300), (255, 255, 255)).save(p)
|
||||
code, out = run_cli(capsys, ["--json", "image", str(p), "--mono"])
|
||||
assert code == 0
|
||||
t = FakeTransport.instances[0]
|
||||
assert len(t.epd_writes) == 63 + 1
|
||||
assert t.epd_writes[-1] == bytes([0x01])
|
||||
assert out["data"]["bytes_bw"] == 15000
|
||||
|
||||
def test_image_slot_and_color(self, capsys, tmp_path):
|
||||
p = tmp_path / "red.png"
|
||||
Image.new("RGB", (400, 300), (255, 0, 0)).save(p)
|
||||
code, out = run_cli(capsys, ["--json", "image", str(p), "--slot", "2"])
|
||||
assert code == 0
|
||||
t = FakeTransport.instances[0]
|
||||
assert t.rxtx_writes[0] == bytes([0xEA, 0x02])
|
||||
assert out["data"]["bytes_red"] == 15000
|
||||
|
||||
def test_countdown(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "countdown", "2026-12-31"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes(
|
||||
[0xFF, 0x00, 0x20, 0x26, 0x12, 0x31])
|
||||
|
||||
def test_countdown_off(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "countdown", "--off"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes([0xFA])
|
||||
|
||||
def test_parking(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "parking", "12"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes([0xEF, 0x11, 0x22])
|
||||
|
||||
def test_sleep(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "sleep", "--on", "--start", "23", "--end", "7"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes([0xFB, 0x01, 0x23, 0x07])
|
||||
|
||||
def test_rotation(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "rotation", "--count", "4"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes.fromhex("EA0003")
|
||||
|
||||
def test_lut(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "lut", "0x05"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0] == bytes([0xE6, 0x05])
|
||||
|
||||
def test_activate_show_id(self, capsys):
|
||||
code, out = run_cli(capsys, ["--json", "activate", "--show-id"])
|
||||
assert code == 0
|
||||
assert out["data"]["device_id"] == "DEVICE12345678"
|
||||
|
||||
def test_wifi(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "wifi", "MyAP", "pw", "--city", "101010100"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes[0].startswith(b"WIFI_SSID:MyAP")
|
||||
|
||||
def test_raw(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "raw", "e2"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].rxtx_writes == [bytes([0xE2])]
|
||||
|
||||
def test_raw_epd_channel(self, capsys):
|
||||
code, _ = run_cli(capsys, ["--json", "raw", "01", "--channel", "epd"])
|
||||
assert code == 0
|
||||
assert FakeTransport.instances[0].epd_writes == [bytes([0x01])]
|
||||
|
||||
def test_scan(self, capsys, monkeypatch):
|
||||
import ppclock.transport as tr
|
||||
async def fake_scan(timeout=5.0, name_prefix="NRF-"):
|
||||
return [{"name": "NRF-TEST", "address": "AA:BB", "rssi": -50}]
|
||||
monkeypatch.setattr(tr, "scan", fake_scan)
|
||||
code, out = run_cli(capsys, ["--json", "scan"])
|
||||
assert code == 0
|
||||
assert out["data"]["devices"][0]["name"] == "NRF-TEST"
|
||||
|
||||
def test_template_custom(self, capsys):
|
||||
code, out = run_cli(capsys, ["--json", "template", "custom",
|
||||
"--data", '{"text": "你好"}'])
|
||||
assert code == 0
|
||||
t = FakeTransport.instances[0]
|
||||
assert len(t.epd_writes) == 63 + 63 + 1 # 默认三色双平面
|
||||
@@ -0,0 +1,138 @@
|
||||
"""命令层测试 — 用 FakeTransport 验证高层操作的帧序列与顺序(证据 docs/protocol.md)。"""
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from ppclock import commands as C
|
||||
|
||||
|
||||
class FakeTransport:
|
||||
def __init__(self, id_response="DEVICE12345678"):
|
||||
self.epd_writes = []
|
||||
self.rxtx_writes = []
|
||||
self.id_response = id_response
|
||||
self.delays = []
|
||||
|
||||
async def write_epd(self, data, response=True):
|
||||
self.epd_writes.append(data)
|
||||
|
||||
async def write_rxtx(self, data, response=True):
|
||||
self.rxtx_writes.append(data)
|
||||
|
||||
async def request_device_id(self, timeout=8.0):
|
||||
self.rxtx_writes.append(bytes([0xEF, 0xEF]))
|
||||
return self.id_response
|
||||
|
||||
async def delay(self, seconds):
|
||||
self.delays.append(seconds)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def t():
|
||||
return FakeTransport()
|
||||
|
||||
|
||||
class TestTimeAndMode:
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_time_sends_dd_then_commit(self, t):
|
||||
dt = datetime.datetime(2026, 7, 24, 15, 30, 0)
|
||||
await C.set_time(t, dt)
|
||||
assert t.rxtx_writes[0][0] == 0xDD
|
||||
assert t.rxtx_writes[-1] == bytes([0xE2])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_appends_commit(self, t):
|
||||
await C.set_mode(t, "clock1")
|
||||
assert t.rxtx_writes == [bytes.fromhex("e10201"), bytes([0xE2])]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clear_uses_epd_channel(self, t):
|
||||
await C.clear(t)
|
||||
assert t.epd_writes == [bytes([0x00, 0x00]), bytes([0x01])]
|
||||
assert t.rxtx_writes == []
|
||||
|
||||
|
||||
class TestUploadImage:
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_image_sequence(self, t):
|
||||
"""整图:bw块×63 → 红块×63 → 延时 → 01"""
|
||||
bw = bytes(15000)
|
||||
red = bytes([0xFF]) * 15000
|
||||
await C.upload_image(t, bw, red, small=False)
|
||||
assert t.epd_writes[0][:4] == bytes([0x03, 0xFF, 0x00, 0x00])
|
||||
assert t.epd_writes[63][:2] == bytes([0x03, 0x00]) # 红平面首块
|
||||
assert t.epd_writes[-1] == bytes([0x01])
|
||||
assert len(t.epd_writes) == 63 + 63 + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mono_image_no_red_blocks(self, t):
|
||||
await C.upload_image(t, bytes(15000), None, small=False)
|
||||
assert len(t.epd_writes) == 63 + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_slot_select_before_upload(self, t):
|
||||
await C.upload_image(t, bytes(15000), None, small=False, slot=2)
|
||||
assert t.rxtx_writes[0] == bytes([0xEA, 0x02])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_small_image_ends_with_AA(self, t):
|
||||
await C.upload_image(t, bytes(3500), None, small=True)
|
||||
assert t.epd_writes[-1] == bytes([0xAA])
|
||||
assert t.epd_writes[0][0] == 0x04
|
||||
|
||||
|
||||
class TestOtherCommands:
|
||||
@pytest.mark.asyncio
|
||||
async def test_countdown_set(self, t):
|
||||
d = datetime.date(2026, 12, 31)
|
||||
await C.countdown_set(t, d, "clock")
|
||||
assert t.rxtx_writes[0] == bytes([0xFF, 0x00, 0x20, 0x26, 0x12, 0x31])
|
||||
assert t.rxtx_writes[-1] == bytes([0xE2])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_countdown_off(self, t):
|
||||
await C.countdown_off(t)
|
||||
assert t.rxtx_writes == [bytes([0xFA]), bytes([0xE2])]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parking(self, t):
|
||||
await C.parking(t, "13800138000")
|
||||
assert t.rxtx_writes[0] == bytes([0xEF]) + bytes(
|
||||
int(f"{d}{d}", 16) for d in "13800138000"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sleep_on_off(self, t):
|
||||
await C.sleep(t, True, 23, 7)
|
||||
assert t.rxtx_writes[0] == bytes([0xFB, 0x01, 0x23, 0x07])
|
||||
await C.sleep(t, False, 23, 7)
|
||||
assert t.rxtx_writes[1] == bytes([0xFB, 0x00, 0x23, 0x07])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rotation(self, t):
|
||||
await C.rotation(t, count=4)
|
||||
assert t.rxtx_writes[0] == bytes.fromhex("EA0003")
|
||||
await C.rotation(t, interval=5, unit="min")
|
||||
assert t.rxtx_writes[1] == bytes.fromhex("e9010501")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activate(self, t):
|
||||
await C.activate(t, "A1B2" * 8)
|
||||
assert t.rxtx_writes[0] == bytes([0xEF]) + bytes.fromhex("A1B2" * 8)
|
||||
assert t.rxtx_writes[-1] == bytes([0xE2])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wifi(self, t):
|
||||
await C.wifi(t, "MyAP", "pw", "101010100")
|
||||
assert t.rxtx_writes[0] == b"WIFI_SSID:MyAP;WIFI_PASS:pw;CITY:101010100"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lut(self, t):
|
||||
await C.lut(t, 0x05)
|
||||
assert t.rxtx_writes[0] == bytes([0xE6, 0x05])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_device_id(self, t):
|
||||
dev_id = await C.get_device_id(t)
|
||||
assert t.rxtx_writes[0] == bytes([0xEF, 0xEF])
|
||||
assert dev_id == "DEVICE12345678"
|
||||
@@ -0,0 +1,159 @@
|
||||
"""图像管线测试 — 算法参数证据:analysis/web/timedjs.js(docs/protocol.md §5)。"""
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ppclock import image_pipeline as IP
|
||||
|
||||
|
||||
def make_img(w, h, pixels):
|
||||
"""pixels: list[(r,g,b)] 长度 w*h,行主序。"""
|
||||
img = Image.new("RGB", (w, h))
|
||||
img.putdata(pixels)
|
||||
return img
|
||||
|
||||
|
||||
class TestPack:
|
||||
def test_all_white_bw_plane(self):
|
||||
px = [(255, 255, 255)] * 16
|
||||
assert IP.pack_plane(px, "bw") == bytes([0xFF, 0xFF])
|
||||
|
||||
def test_all_black_bw_plane(self):
|
||||
px = [(0, 0, 0)] * 16
|
||||
assert IP.pack_plane(px, "bw") == bytes(2)
|
||||
|
||||
def test_msb_first_order(self):
|
||||
"""首像素=bit7:仅(0,0)白→0x80;仅第8像素白→0x01;第9像素→第二字节0x80"""
|
||||
px = [(255, 255, 255)] + [(0, 0, 0)] * 15
|
||||
assert IP.pack_plane(px, "bw") == bytes([0x80, 0x00])
|
||||
px = [(0, 0, 0)] * 7 + [(255, 255, 255)] + [(0, 0, 0)] * 8
|
||||
assert IP.pack_plane(px, "bw") == bytes([0x01, 0x00])
|
||||
px = [(0, 0, 0)] * 8 + [(255, 255, 255)] + [(0, 0, 0)] * 7
|
||||
assert IP.pack_plane(px, "bw") == bytes([0x00, 0x80])
|
||||
|
||||
def test_red_pixel_planes(self):
|
||||
"""红(255,0,0):bw 平面=0,红平面=1(timedjs.js:573-576,599-601)"""
|
||||
px = [(255, 0, 0)] * 8
|
||||
assert IP.pack_plane(px, "bw") == bytes([0x00])
|
||||
assert IP.pack_plane(px, "red") == bytes([0xFF])
|
||||
|
||||
def test_partial_channels_count_as_white(self):
|
||||
"""bw 规则:R,G,B 全>0 → 1;(1,1,1) 也是白"""
|
||||
px = [(1, 1, 1)] * 8
|
||||
assert IP.pack_plane(px, "bw") == bytes([0xFF])
|
||||
|
||||
def test_length_not_multiple_of_8_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
IP.pack_plane([(0, 0, 0)] * 7, "bw")
|
||||
|
||||
|
||||
class TestDither:
|
||||
def test_none_threshold(self):
|
||||
"""none:gray<threshold→0 否则 255(timedjs.js:42-51)"""
|
||||
px = [(100, 100, 100), (200, 200, 200)] * 4
|
||||
out = IP.dither_pixels(px, 8, 1, "none", threshold=125)
|
||||
assert out == [(0, 0, 0), (255, 255, 255)] * 4
|
||||
|
||||
def test_none_grayscale_coefficients(self):
|
||||
"""灰度=0.299R+0.587G+0.114B:纯绿128 → gray=75.1 <125 → 黑"""
|
||||
out = IP.dither_pixels([(0, 128, 0)] * 8, 8, 1, "none", threshold=125)
|
||||
assert out == [(0, 0, 0)] * 8
|
||||
|
||||
def test_red_detected_before_quantize(self):
|
||||
"""r>g*1.5 且 r>b*1.5 且 r>threshold → 纯红且不扩散误差"""
|
||||
px = [(200, 50, 50)] * 8
|
||||
out = IP.dither_pixels(px, 8, 1, "none", threshold=125, tricolor=True)
|
||||
assert out == [(255, 0, 0)] * 8
|
||||
|
||||
def test_not_red_when_threshold_high(self):
|
||||
px = [(200, 50, 50)] * 8
|
||||
out = IP.dither_pixels(px, 8, 1, "none", threshold=250, tricolor=True)
|
||||
# 200>250 假 → 走红判定失败 → 灰度 0.299*200+0.587*50+0.114*50=94.9 <250 → 黑
|
||||
assert out == [(0, 0, 0)] * 8
|
||||
|
||||
def test_floyd_steinberg_error_propagation(self):
|
||||
"""2x1 灰100:px0→黑 err=100*7/16=43.75 → px1=143.75→白(阈值125,扩散1.0)"""
|
||||
px = [(100, 100, 100)] * 2
|
||||
out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125)
|
||||
assert out[0] == (0, 0, 0)
|
||||
assert out[1] == (255, 255, 255)
|
||||
|
||||
def test_diffusion_zero_behaves_like_none(self):
|
||||
px = [(100, 100, 100)] * 2
|
||||
out = IP.dither_pixels(px, 2, 1, "floydsteinberg", threshold=125, diffusion=0.0)
|
||||
assert out == [(0, 0, 0)] * 2
|
||||
|
||||
def test_atkinson_kernel_smaller_error(self):
|
||||
"""Atkinson 只扩散 6/8:px0 黑 err=100*1/8=12.5 → px1=112.5<125 仍黑"""
|
||||
px = [(100, 100, 100)] * 2
|
||||
out = IP.dither_pixels(px, 2, 1, "atkinson", threshold=125)
|
||||
assert out == [(0, 0, 0), (0, 0, 0)]
|
||||
|
||||
def test_bayer_matrix_origin(self):
|
||||
"""bayer (0,0) 阈值=15:灰10→黑,灰20→白(矩阵 timedjs.js:116-136)"""
|
||||
out = IP.dither_pixels([(10, 10, 10)] * 8, 8, 1, "bayer", threshold=125)
|
||||
assert out[0] == (0, 0, 0)
|
||||
out = IP.dither_pixels([(20, 20, 20)] * 8, 8, 1, "bayer", threshold=125)
|
||||
assert out[0] == (255, 255, 255)
|
||||
|
||||
def test_stucki_and_jarvis_run(self):
|
||||
"""大核算法冒烟:全灰输入输出只有黑白"""
|
||||
px = [(128, 128, 128)] * 64
|
||||
for algo in ("stucki", "jarvis"):
|
||||
out = IP.dither_pixels(px, 8, 8, algo, threshold=125)
|
||||
assert set(out) <= {(0, 0, 0), (255, 255, 255)}
|
||||
|
||||
def test_unknown_algo_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
IP.dither_pixels([(0, 0, 0)] * 8, 8, 1, "magic")
|
||||
|
||||
|
||||
class TestAdjust:
|
||||
def test_brightness(self):
|
||||
"""亮度+50 → v+=127.5(timedjs.js:9-17)"""
|
||||
px = [(100, 100, 100)]
|
||||
out = IP.adjust_pixels(px, brightness=50, contrast=0, saturation=100)
|
||||
assert out == [(228, 228, 228)] # 100+127.5=227.5 → round=228
|
||||
|
||||
def test_contrast(self):
|
||||
"""对比度+100 → factor=2, v=v*2-128(timedjs.js:18-27)"""
|
||||
out = IP.adjust_pixels([(200, 50, 128)], brightness=0, contrast=100, saturation=100)
|
||||
assert out == [(255, 0, 128)]
|
||||
|
||||
def test_saturation_desaturate(self):
|
||||
"""饱和度35 → v=gray+(v-gray)*0.35(timedjs.js:28-38)"""
|
||||
out = IP.adjust_pixels([(200, 50, 50)], brightness=0, contrast=0, saturation=35)
|
||||
gray = 0.299 * 200 + 0.587 * 50 + 0.114 * 50
|
||||
exp = round(gray + (200 - gray) * 0.35)
|
||||
assert out[0][0] == exp
|
||||
|
||||
|
||||
class TestProcess:
|
||||
def test_full_size_output_lengths(self):
|
||||
"""400x300 → bw 15000B;三色另有红平面 15000B"""
|
||||
img = Image.new("RGB", (400, 300), (255, 255, 255))
|
||||
bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=True)
|
||||
assert len(bw) == 15000
|
||||
assert len(red) == 15000
|
||||
assert bw == bytes([0xFF]) * 15000
|
||||
assert red == bytes(15000)
|
||||
|
||||
def test_mono_has_no_red_plane(self):
|
||||
img = Image.new("RGB", (400, 300), (0, 0, 0))
|
||||
bw, red = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False)
|
||||
assert len(bw) == 15000
|
||||
assert red is None
|
||||
|
||||
def test_resize_to_fit(self):
|
||||
img = Image.new("RGB", (800, 600), (255, 255, 255))
|
||||
bw, _ = IP.process_image(img, size=(400, 300), algorithm="none", tricolor=False)
|
||||
assert len(bw) == 15000
|
||||
|
||||
def test_rotate_180(self):
|
||||
"""rotate=180:左上角白块移到右下"""
|
||||
img = Image.new("RGB", (8, 8), (0, 0, 0))
|
||||
for x in range(8):
|
||||
img.putpixel((x, 0), (255, 255, 255))
|
||||
bw, _ = IP.process_image(img, size=(8, 8), algorithm="none",
|
||||
tricolor=False, rotate=180)
|
||||
assert bw[0] == 0x00
|
||||
assert bw[-1] == 0xFF
|
||||
@@ -0,0 +1,34 @@
|
||||
"""模板渲染测试。"""
|
||||
import pytest
|
||||
|
||||
from ppclock import templates as T
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", sorted(T.RENDERERS))
|
||||
def test_all_templates_render_400x300(name):
|
||||
payloads = {
|
||||
"schedule": {"items": [["09:00", "晨会【重要】"]]},
|
||||
"businesscard": {"name": "张三", "title": "工程师", "phone": "138"},
|
||||
"memo": {"title": "购物", "lines": ["牛奶", "鸡蛋"]},
|
||||
"course": {"grid": [["数学"]]},
|
||||
"qrcode": {"content": "https://example.com", "caption": "扫码"},
|
||||
"custom": {"text": "你好【世界】\n第二行"},
|
||||
}
|
||||
img = T.render_template(name, payloads[name])
|
||||
assert img.size == (400, 300)
|
||||
assert img.mode == "RGB"
|
||||
|
||||
|
||||
def test_custom_wraps_long_text():
|
||||
img = T.render_template("custom", {"text": "长" * 100})
|
||||
assert img.size == (400, 300)
|
||||
|
||||
|
||||
def test_red_segments_rendered_red():
|
||||
img = T.render_template("custom", {"text": "【红】"})
|
||||
assert (255, 0, 0) in set(img.getdata())
|
||||
|
||||
|
||||
def test_unknown_template_raises():
|
||||
with pytest.raises(ValueError):
|
||||
T.render_template("nope", {})
|
||||
Reference in new issue
Block a user