mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
extended msd api for future otg
This commit is contained in:
@@ -183,6 +183,7 @@ def _get_config_scheme() -> Dict:
|
|||||||
"unix_rm": Option(False, type=valid_bool),
|
"unix_rm": Option(False, type=valid_bool),
|
||||||
"unix_mode": Option(0, type=valid_unix_mode),
|
"unix_mode": Option(0, type=valid_unix_mode),
|
||||||
"heartbeat": Option(3.0, type=valid_float_f01),
|
"heartbeat": Option(3.0, type=valid_float_f01),
|
||||||
|
"sync_chunk_size": Option(65536, type=(lambda arg: valid_number(arg, min=1024))),
|
||||||
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
"access_log_format": Option("[%P / %{X-Real-IP}i] '%r' => %s; size=%b ---"
|
||||||
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
" referer='%{Referer}i'; user_agent='%{User-Agent}i'"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ from ...validators.kvm import valid_atx_button
|
|||||||
from ...validators.kvm import valid_log_seek
|
from ...validators.kvm import valid_log_seek
|
||||||
from ...validators.kvm import valid_stream_quality
|
from ...validators.kvm import valid_stream_quality
|
||||||
from ...validators.kvm import valid_stream_fps
|
from ...validators.kvm import valid_stream_fps
|
||||||
|
from ...validators.kvm import valid_msd_image_name
|
||||||
from ...validators.kvm import valid_hid_key
|
from ...validators.kvm import valid_hid_key
|
||||||
from ...validators.kvm import valid_hid_mouse_move
|
from ...validators.kvm import valid_hid_mouse_move
|
||||||
from ...validators.kvm import valid_hid_mouse_button
|
from ...validators.kvm import valid_hid_mouse_button
|
||||||
@@ -250,6 +251,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
self.__streamer = streamer
|
self.__streamer = streamer
|
||||||
|
|
||||||
self.__heartbeat: Optional[float] = None # Assigned in run() for consistance
|
self.__heartbeat: Optional[float] = None # Assigned in run() for consistance
|
||||||
|
self.__sync_chunk_size: Optional[int] = None # Ditto
|
||||||
self.__sockets: Set[aiohttp.web.WebSocketResponse] = set()
|
self.__sockets: Set[aiohttp.web.WebSocketResponse] = set()
|
||||||
self.__sockets_lock = asyncio.Lock()
|
self.__sockets_lock = asyncio.Lock()
|
||||||
|
|
||||||
@@ -266,6 +268,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
unix_rm: bool,
|
unix_rm: bool,
|
||||||
unix_mode: int,
|
unix_mode: int,
|
||||||
heartbeat: float,
|
heartbeat: float,
|
||||||
|
sync_chunk_size: int,
|
||||||
access_log_format: str,
|
access_log_format: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
@@ -274,6 +277,7 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
setproctitle.setproctitle("[main] " + setproctitle.getproctitle())
|
setproctitle.setproctitle("[main] " + setproctitle.getproctitle())
|
||||||
|
|
||||||
self.__heartbeat = heartbeat
|
self.__heartbeat = heartbeat
|
||||||
|
self.__sync_chunk_size = sync_chunk_size
|
||||||
|
|
||||||
assert port or unix_path
|
assert port or unix_path
|
||||||
if unix_path:
|
if unix_path:
|
||||||
@@ -474,31 +478,40 @@ class Server: # pylint: disable=too-many-instance-attributes
|
|||||||
async def __msd_disconnect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_disconnect_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
return _json(await self.__msd.disconnect())
|
return _json(await self.__msd.disconnect())
|
||||||
|
|
||||||
|
@_exposed("POST", "/msd/select")
|
||||||
|
async def __msd_select_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
return _json(await self.__msd.select(valid_msd_image_name(request.query.get("image_name"))))
|
||||||
|
|
||||||
|
@_exposed("POST", "/msd/remove")
|
||||||
|
async def __msd_remove_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
return _json(await self.__msd.remove(valid_msd_image_name(request.query.get("image_name"))))
|
||||||
|
|
||||||
@_exposed("POST", "/msd/write")
|
@_exposed("POST", "/msd/write")
|
||||||
async def __msd_write_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_write_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
assert self.__sync_chunk_size is not None
|
||||||
logger = get_logger(0)
|
logger = get_logger(0)
|
||||||
reader = await request.multipart()
|
reader = await request.multipart()
|
||||||
|
image_name = ""
|
||||||
written = 0
|
written = 0
|
||||||
try:
|
try:
|
||||||
async with self.__msd:
|
async with self.__msd:
|
||||||
name_field = await _get_multipart_field(reader, "image_name")
|
name_field = await _get_multipart_field(reader, "image_name")
|
||||||
image_name = (await name_field.read()).decode("utf-8")[:256]
|
image_name = valid_msd_image_name((await name_field.read()).decode("utf-8"))
|
||||||
|
|
||||||
data_field = await _get_multipart_field(reader, "image_data")
|
data_field = await _get_multipart_field(reader, "image_data")
|
||||||
|
|
||||||
logger.info("Writing image %r to MSD ...", image_name)
|
logger.info("Writing image %r to MSD ...", image_name)
|
||||||
await self.__msd.write_image_info(image_name, False)
|
await self.__msd.write_image_info(image_name, False)
|
||||||
chunk_size = self.__msd.get_chunk_size()
|
|
||||||
while True:
|
while True:
|
||||||
chunk = await data_field.read_chunk(chunk_size)
|
chunk = await data_field.read_chunk(self.__sync_chunk_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
written = await self.__msd.write_image_chunk(chunk)
|
written = await self.__msd.write_image_chunk(chunk)
|
||||||
await self.__msd.write_image_info(image_name, True)
|
await self.__msd.write_image_info(image_name, True)
|
||||||
finally:
|
finally:
|
||||||
if written != 0:
|
if written != 0:
|
||||||
logger.info("Written %d bytes to MSD", written)
|
logger.info("Written image %r with size=%d bytes to MSD", image_name, written)
|
||||||
return _json({"written": written})
|
return _json({"image": {"name": image_name, "size": written}})
|
||||||
|
|
||||||
@_exposed("POST", "/msd/reset")
|
@_exposed("POST", "/msd/reset")
|
||||||
async def __msd_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
async def __msd_reset_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
|||||||
@@ -64,6 +64,11 @@ class MsdIsBusyError(MsdOperationError):
|
|||||||
super().__init__("Performing another MSD operation, please try again later")
|
super().__init__("Performing another MSD operation, please try again later")
|
||||||
|
|
||||||
|
|
||||||
|
class MsdMultiNotSupported(MsdOperationError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("This MSD does not support storing multiple images")
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
class BaseMsd(BasePlugin):
|
class BaseMsd(BasePlugin):
|
||||||
def get_state(self) -> Dict:
|
def get_state(self) -> Dict:
|
||||||
@@ -73,24 +78,29 @@ class BaseMsd(BasePlugin):
|
|||||||
yield {}
|
yield {}
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def reset(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
async def connect(self) -> Dict:
|
async def connect(self) -> Dict:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def disconnect(self) -> Dict:
|
async def disconnect(self) -> Dict:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def select(self, name: str) -> Dict:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def remove(self, name: str) -> Dict:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def __aenter__(self) -> "BaseMsd":
|
async def __aenter__(self) -> "BaseMsd":
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_chunk_size(self) -> int:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
async def write_image_info(self, name: str, complete: bool) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class Plugin(BaseMsd):
|
|||||||
def get_state(self) -> Dict:
|
def get_state(self) -> Dict:
|
||||||
return {
|
return {
|
||||||
"enabled": False,
|
"enabled": False,
|
||||||
|
"multi": False,
|
||||||
"online": False,
|
"online": False,
|
||||||
"busy": False,
|
"busy": False,
|
||||||
"uploading": False,
|
"uploading": False,
|
||||||
@@ -56,21 +57,26 @@ class Plugin(BaseMsd):
|
|||||||
yield self.get_state()
|
yield self.get_state()
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def reset(self) -> None:
|
||||||
|
raise MsdDisabledError()
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
async def connect(self) -> Dict:
|
async def connect(self) -> Dict:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def disconnect(self) -> Dict:
|
async def disconnect(self) -> Dict:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def reset(self) -> None:
|
async def select(self, name: str) -> Dict:
|
||||||
|
raise MsdDisabledError()
|
||||||
|
|
||||||
|
async def remove(self, name: str) -> Dict:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
async def __aenter__(self) -> BaseMsd:
|
async def __aenter__(self) -> BaseMsd:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
def get_chunk_size(self) -> int:
|
|
||||||
raise MsdDisabledError()
|
|
||||||
|
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
async def write_image_info(self, name: str, complete: bool) -> None:
|
||||||
raise MsdDisabledError()
|
raise MsdDisabledError()
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ from ... import gpio
|
|||||||
|
|
||||||
from ...yamlconf import Option
|
from ...yamlconf import Option
|
||||||
|
|
||||||
from ...validators.basic import valid_number
|
|
||||||
from ...validators.basic import valid_int_f1
|
from ...validators.basic import valid_int_f1
|
||||||
from ...validators.basic import valid_float_f01
|
from ...validators.basic import valid_float_f01
|
||||||
|
|
||||||
@@ -62,6 +61,7 @@ from . import MsdAlreadyConnectedError
|
|||||||
from . import MsdAlreadyDisconnectedError
|
from . import MsdAlreadyDisconnectedError
|
||||||
from . import MsdConnectedError
|
from . import MsdConnectedError
|
||||||
from . import MsdIsBusyError
|
from . import MsdIsBusyError
|
||||||
|
from . import MsdMultiNotSupported
|
||||||
from . import BaseMsd
|
from . import BaseMsd
|
||||||
|
|
||||||
|
|
||||||
@@ -170,7 +170,6 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
init_delay: float,
|
init_delay: float,
|
||||||
init_retries: int,
|
init_retries: int,
|
||||||
reset_delay: float,
|
reset_delay: float,
|
||||||
chunk_size: int,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.__target_pin = gpio.set_output(target_pin)
|
self.__target_pin = gpio.set_output(target_pin)
|
||||||
@@ -180,7 +179,6 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
self.__init_delay = init_delay
|
self.__init_delay = init_delay
|
||||||
self.__init_retries = init_retries
|
self.__init_retries = init_retries
|
||||||
self.__reset_delay = reset_delay
|
self.__reset_delay = reset_delay
|
||||||
self.__chunk_size = chunk_size
|
|
||||||
|
|
||||||
self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError)
|
self.__region = aioregion.AioExclusiveRegion(MsdIsBusyError)
|
||||||
|
|
||||||
@@ -209,8 +207,6 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
"init_delay": Option(1.0, type=valid_float_f01),
|
"init_delay": Option(1.0, type=valid_float_f01),
|
||||||
"init_retries": Option(5, type=valid_int_f1),
|
"init_retries": Option(5, type=valid_int_f1),
|
||||||
"reset_delay": Option(1.0, type=valid_float_f01),
|
"reset_delay": Option(1.0, type=valid_float_f01),
|
||||||
|
|
||||||
"chunk_size": Option(65536, type=(lambda arg: valid_number(arg, min=1024))),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_state(self) -> Dict:
|
def get_state(self) -> Dict:
|
||||||
@@ -225,6 +221,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
current = dataclasses.asdict(self._device_info.image)
|
current = dataclasses.asdict(self._device_info.image)
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
|
"multi": False,
|
||||||
"online": bool(self._device_info),
|
"online": bool(self._device_info),
|
||||||
"busy": self.__region.is_busy(),
|
"busy": self.__region.is_busy(),
|
||||||
"uploading": bool(self.__device_file),
|
"uploading": bool(self.__device_file),
|
||||||
@@ -238,12 +235,39 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
while True:
|
while True:
|
||||||
yield (await self.__state_queue.get())
|
yield (await self.__state_queue.get())
|
||||||
|
|
||||||
|
@aiotools.atomic
|
||||||
|
async def reset(self) -> None:
|
||||||
|
with aiotools.unregion_only_on_exception(self.__region):
|
||||||
|
await self.__inner_reset()
|
||||||
|
|
||||||
|
@aiotools.tasked
|
||||||
|
@aiotools.muted("Can't reset MSD or operation was not completed")
|
||||||
|
async def __inner_reset(self) -> None:
|
||||||
|
try:
|
||||||
|
gpio.write(self.__reset_pin, True)
|
||||||
|
await asyncio.sleep(self.__reset_delay)
|
||||||
|
gpio.write(self.__reset_pin, False)
|
||||||
|
|
||||||
|
gpio.write(self.__target_pin, False)
|
||||||
|
self.__on_kvm = True
|
||||||
|
|
||||||
|
await self.__load_device_info()
|
||||||
|
get_logger(0).info("MSD reset has been successful")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
gpio.write(self.__reset_pin, False)
|
||||||
|
finally:
|
||||||
|
self.__region.exit()
|
||||||
|
await self.__state_queue.put(self.get_state())
|
||||||
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def cleanup(self) -> None:
|
async def cleanup(self) -> None:
|
||||||
await self.__close_device_file()
|
await self.__close_device_file()
|
||||||
gpio.write(self.__target_pin, False)
|
gpio.write(self.__target_pin, False)
|
||||||
gpio.write(self.__reset_pin, False)
|
gpio.write(self.__reset_pin, False)
|
||||||
|
|
||||||
|
# =====
|
||||||
|
|
||||||
@_msd_working
|
@_msd_working
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def connect(self) -> Dict:
|
async def connect(self) -> Dict:
|
||||||
@@ -292,30 +316,13 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
if notify:
|
if notify:
|
||||||
await self.__state_queue.put(state or self.get_state())
|
await self.__state_queue.put(state or self.get_state())
|
||||||
|
|
||||||
@aiotools.atomic
|
@_msd_working
|
||||||
async def reset(self) -> None:
|
async def select(self, name: str) -> Dict:
|
||||||
with aiotools.unregion_only_on_exception(self.__region):
|
raise MsdMultiNotSupported()
|
||||||
await self.__inner_reset()
|
|
||||||
|
|
||||||
@aiotools.tasked
|
@_msd_working
|
||||||
@aiotools.muted("Can't reset MSD or operation was not completed")
|
async def remove(self, name: str) -> Dict:
|
||||||
async def __inner_reset(self) -> None:
|
raise MsdMultiNotSupported()
|
||||||
try:
|
|
||||||
gpio.write(self.__reset_pin, True)
|
|
||||||
await asyncio.sleep(self.__reset_delay)
|
|
||||||
gpio.write(self.__reset_pin, False)
|
|
||||||
|
|
||||||
gpio.write(self.__target_pin, False)
|
|
||||||
self.__on_kvm = True
|
|
||||||
|
|
||||||
await self.__load_device_info()
|
|
||||||
get_logger(0).info("MSD reset has been successful")
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
gpio.write(self.__reset_pin, False)
|
|
||||||
finally:
|
|
||||||
self.__region.exit()
|
|
||||||
await self.__state_queue.put(self.get_state())
|
|
||||||
|
|
||||||
@_msd_working
|
@_msd_working
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
@@ -334,9 +341,6 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
|
|||||||
finally:
|
finally:
|
||||||
await self.__state_queue.put(self.get_state())
|
await self.__state_queue.put(self.get_state())
|
||||||
|
|
||||||
def get_chunk_size(self) -> int:
|
|
||||||
return self.__chunk_size
|
|
||||||
|
|
||||||
@aiotools.atomic
|
@aiotools.atomic
|
||||||
async def write_image_info(self, name: str, complete: bool) -> None:
|
async def write_image_info(self, name: str, complete: bool) -> None:
|
||||||
assert self.__device_file
|
assert self.__device_file
|
||||||
|
|||||||
@@ -20,10 +20,13 @@
|
|||||||
# ========================================================================== #
|
# ========================================================================== #
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .. import keymap
|
from .. import keymap
|
||||||
|
|
||||||
|
from . import check_not_none_string
|
||||||
from . import check_string_in_list
|
from . import check_string_in_list
|
||||||
|
|
||||||
from .basic import valid_number
|
from .basic import valid_number
|
||||||
@@ -38,6 +41,18 @@ def valid_atx_button(arg: Any) -> str:
|
|||||||
return check_string_in_list(arg, "ATX button", ["power", "power_long", "reset"])
|
return check_string_in_list(arg, "ATX button", ["power", "power_long", "reset"])
|
||||||
|
|
||||||
|
|
||||||
|
def valid_msd_image_name(arg: Any) -> str:
|
||||||
|
if len(str(arg).strip()) == 0:
|
||||||
|
arg = None
|
||||||
|
arg = check_not_none_string(arg, "MSD image name", strip=True)
|
||||||
|
arg = re.sub(r"[^\w\.+@()\[\]-]", "_", arg)
|
||||||
|
if arg == ".":
|
||||||
|
arg = "_"
|
||||||
|
if arg == "..":
|
||||||
|
arg = "__"
|
||||||
|
return arg[:255]
|
||||||
|
|
||||||
|
|
||||||
def valid_log_seek(arg: Any) -> int:
|
def valid_log_seek(arg: Any) -> int:
|
||||||
return int(valid_number(arg, min=0, name="log seek"))
|
return int(valid_number(arg, min=0, name="log seek"))
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from kvmd.validators.kvm import valid_atx_button
|
|||||||
from kvmd.validators.kvm import valid_log_seek
|
from kvmd.validators.kvm import valid_log_seek
|
||||||
from kvmd.validators.kvm import valid_stream_quality
|
from kvmd.validators.kvm import valid_stream_quality
|
||||||
from kvmd.validators.kvm import valid_stream_fps
|
from kvmd.validators.kvm import valid_stream_fps
|
||||||
|
from kvmd.validators.kvm import valid_msd_image_name
|
||||||
from kvmd.validators.kvm import valid_hid_key
|
from kvmd.validators.kvm import valid_hid_key
|
||||||
from kvmd.validators.kvm import valid_hid_mouse_move
|
from kvmd.validators.kvm import valid_hid_mouse_move
|
||||||
from kvmd.validators.kvm import valid_hid_mouse_button
|
from kvmd.validators.kvm import valid_hid_mouse_button
|
||||||
@@ -104,6 +105,30 @@ def test_fail__valid_stream_fps(arg: Any) -> None:
|
|||||||
print(valid_stream_fps(arg))
|
print(valid_stream_fps(arg))
|
||||||
|
|
||||||
|
|
||||||
|
# =====
|
||||||
|
@pytest.mark.parametrize("arg, retval", [
|
||||||
|
("archlinux-2018.07.01-i686.iso", "archlinux-2018.07.01-i686.iso"),
|
||||||
|
("archlinux-2018.07.01-x86_64.iso", "archlinux-2018.07.01-x86_64.iso"),
|
||||||
|
("dsl-4.11.rc1.iso", "dsl-4.11.rc1.iso"),
|
||||||
|
("systemrescuecd-x86-5.3.1.iso", "systemrescuecd-x86-5.3.1.iso"),
|
||||||
|
("ubuntu-16.04.5-desktop-i386.iso", "ubuntu-16.04.5-desktop-i386.iso"),
|
||||||
|
(".", "_"),
|
||||||
|
("..", "__"),
|
||||||
|
("/..", "_.."),
|
||||||
|
("/root/..", "_root_.."),
|
||||||
|
(" тест(){}[ \t].iso\t", "тест()__[__].iso"),
|
||||||
|
("?" * 1000, "_" * 255),
|
||||||
|
])
|
||||||
|
def test_ok__valid_msd_image_name(arg: Any, retval: str) -> None:
|
||||||
|
assert valid_msd_image_name(arg) == retval
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("arg", ["", None])
|
||||||
|
def test_fail__valid_msd_image_name(arg: Any) -> None:
|
||||||
|
with pytest.raises(ValidatorError):
|
||||||
|
print(valid_msd_image_name(arg))
|
||||||
|
|
||||||
|
|
||||||
# =====
|
# =====
|
||||||
def test_ok__valid_hid_key() -> None:
|
def test_ok__valid_hid_key() -> None:
|
||||||
for key in KEYMAP:
|
for key in KEYMAP:
|
||||||
|
|||||||
Reference in New Issue
Block a user