Skip to content

Commit

Permalink
feat: ota_proxy: migrate to anyio, drop deps of aiofiles (#467)
Browse files Browse the repository at this point in the history
This PR introduces to use anyio as asyncio environment for ota_proxy, drops the use of aiofiles to use async file operations supports from anyio.

Also this PR fixes the problem of ota_proxy launched by otaclient actually not using `uvloop` although we configure to use it. The main entry `run_otaproxy` is fixed and now the ota_proxy will be properly launched with uvloop.

Other changes:
1. otaclient: integrate to use the new `run_otaproxy` entrypoint to launch ota_proxy.
2. fix up test files accordingly.

DEPS:
1. add anyio and drop aiofiles.
2. bump to use simple-sqlite3-orm v0.7.0.
  • Loading branch information
Bodong-Yang authored Dec 23, 2024
1 parent a3da763 commit 4fb90ad
Show file tree
Hide file tree
Showing 10 changed files with 70 additions and 83 deletions.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dynamic = [
"version",
]
dependencies = [
"aiofiles<25,>=24.1",
"anyio>=4.5.1,<5",
"aiohttp>=3.10.11,<3.12",
"cryptography>=43.0.1,<45",
"grpcio>=1.53.2,<1.69",
Expand All @@ -35,7 +35,7 @@ dependencies = [
"pydantic-settings<3,>=2.3",
"pyyaml<7,>=6.0.1",
"requests<2.33,>=2.32",
"simple-sqlite3-orm<0.7,>=0.6",
"simple-sqlite3-orm<0.8,>=0.7",
"typing-extensions>=4.6.3",
"urllib3<2.3,>=2.2.2",
"uvicorn[standard]>=0.30,<0.35",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Automatically generated from pyproject.toml by gen_requirements_txt.py script.
# DO NOT EDIT! Only for reference use.
aiofiles<25,>=24.1
anyio>=4.5.1,<5
aiohttp>=3.10.11,<3.12
cryptography>=43.0.1,<45
grpcio>=1.53.2,<1.69
Expand All @@ -11,7 +11,7 @@ pydantic<3,>=2.10
pydantic-settings<3,>=2.3
pyyaml<7,>=6.0.1
requests<2.33,>=2.32
simple-sqlite3-orm<0.7,>=0.6
simple-sqlite3-orm<0.8,>=0.7
typing-extensions>=4.6.3
urllib3<2.3,>=2.2.2
uvicorn[standard]>=0.30,<0.35
Expand Down
5 changes: 3 additions & 2 deletions src/ota_proxy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)


async def run_otaproxy(
def run_otaproxy(
host: str,
port: int,
*,
Expand All @@ -45,6 +45,7 @@ async def run_otaproxy(
enable_https: bool,
external_cache_mnt_point: str | None = None,
):
import anyio
import uvicorn

from . import App, OTACache
Expand All @@ -69,4 +70,4 @@ async def run_otaproxy(
http="h11",
)
_server = uvicorn.Server(_config)
await _server.serve()
anyio.run(_server.serve, backend="asyncio", backend_options={"use_uvloop": True})
26 changes: 10 additions & 16 deletions src/ota_proxy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@
from __future__ import annotations

import argparse
import asyncio
import logging

import uvloop

from . import run_otaproxy
from .config import config as cfg

Expand Down Expand Up @@ -78,17 +75,14 @@
args = parser.parse_args()

logger.info(f"launch ota_proxy at {args.host}:{args.port}")
uvloop.install()
asyncio.run(
run_otaproxy(
host=args.host,
port=args.port,
cache_dir=args.cache_dir,
cache_db_f=args.cache_db_file,
enable_cache=args.enable_cache,
upper_proxy=args.upper_proxy,
enable_https=args.enable_https,
init_cache=args.init_cache,
external_cache_mnt_point=args.external_cache_mnt_point,
)
run_otaproxy(
host=args.host,
port=args.port,
cache_dir=args.cache_dir,
cache_db_f=args.cache_db_file,
enable_cache=args.enable_cache,
upper_proxy=args.upper_proxy,
enable_https=args.enable_https,
init_cache=args.init_cache,
external_cache_mnt_point=args.external_cache_mnt_point,
)
14 changes: 6 additions & 8 deletions src/ota_proxy/cache_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
import os
import threading
import weakref
from concurrent.futures import Executor
from pathlib import Path
from typing import AsyncGenerator, AsyncIterator, Callable, Coroutine

import aiofiles
import anyio
from anyio import open_file

from otaclient_common.common import get_backoff
from otaclient_common.typing import StrOrPath
Expand Down Expand Up @@ -101,19 +101,17 @@ def __init__(
*,
base_dir: StrOrPath,
commit_cache_cb: _CACHE_ENTRY_REGISTER_CALLBACK,
executor: Executor,
below_hard_limit_event: threading.Event,
):
self.fpath = Path(base_dir) / self._tmp_file_naming(cache_identifier)
self.save_path = Path(base_dir) / cache_identifier
self.save_path = anyio.Path(base_dir) / cache_identifier
self.cache_meta: CacheMeta | None = None
self._commit_cache_cb = commit_cache_cb

self._writer_ready = asyncio.Event()
self._writer_finished = asyncio.Event()
self._writer_failed = asyncio.Event()

self._executor = executor
self._space_availability_event = below_hard_limit_event

self._bytes_written = 0
Expand Down Expand Up @@ -147,7 +145,7 @@ async def _provider_write_cache(
"""
logger.debug(f"start to cache for {cache_meta=}...")
try:
async with aiofiles.open(self.fpath, "wb", executor=self._executor) as f:
async with await open_file(self.fpath, "wb") as f:
_written = 0
while _data := (yield _written):
if not self._space_availability_event.is_set():
Expand Down Expand Up @@ -179,7 +177,7 @@ async def _provider_write_cache(
await self._commit_cache_cb(cache_meta)
# finalize the cache file, skip finalize if the target file is
# already presented.
if not self.save_path.is_file():
if not await self.save_path.is_file():
os.link(self.fpath, self.save_path)
except Exception as e:
logger.warning(f"failed to write cache for {cache_meta=}: {e!r}")
Expand All @@ -202,7 +200,7 @@ async def _subscriber_stream_cache(self) -> AsyncIterator[bytes]:
"""
err_count, _bytes_read = 0, 0
try:
async with aiofiles.open(self.fpath, "rb", executor=self._executor) as f:
async with await open_file(self.fpath, "rb") as f:
while (
not self._writer_finished.is_set()
or _bytes_read < self._bytes_written
Expand Down
46 changes: 24 additions & 22 deletions src/ota_proxy/ota_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
import shutil
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import AsyncIterator, Mapping, Optional
from urllib.parse import SplitResult, quote, urlsplit

import aiohttp
import anyio
import anyio.to_thread
from multidict import CIMultiDict, CIMultiDictProxy

from otaclient_common.common import get_backoff
Expand Down Expand Up @@ -133,10 +134,6 @@ def __init__(
db_f.unlink(missing_ok=True)
self._init_cache = True # force init cache on db file cleanup

self._executor = ThreadPoolExecutor(
thread_name_prefix="ota_cache_fileio_executor"
)

self._external_cache_data_dir = None
self._external_cache_mp = None
if external_cache_mnt_point and mount_external_cache(external_cache_mnt_point):
Expand All @@ -145,7 +142,7 @@ def __init__(
)
self._external_cache_mp = external_cache_mnt_point
self._external_cache_data_dir = (
Path(external_cache_mnt_point) / cfg.EXTERNAL_CACHE_DATA_DNAME
anyio.Path(external_cache_mnt_point) / cfg.EXTERNAL_CACHE_DATA_DNAME
)

self._storage_below_hard_limit_event = threading.Event()
Expand Down Expand Up @@ -189,11 +186,18 @@ async def start(self):

# reuse the previously left ota_cache
else: # cleanup unfinished tmp files
for tmp_f in self._base_dir.glob(f"{cfg.TMP_FILE_PREFIX}*"):
tmp_f.unlink(missing_ok=True)
async for tmp_f in anyio.Path(self._base_dir).glob(
f"{cfg.TMP_FILE_PREFIX}*"
):
await tmp_f.unlink(missing_ok=True)

# dispatch a background task to pulling the disk usage info
self._executor.submit(self._background_check_free_space)
_free_space_check_thread = threading.Thread(
target=self._background_check_free_space,
daemon=True,
name="ota_cache_free_space_checker",
)
_free_space_check_thread.start()

# init cache helper(and connect to ota_cache db)
self._lru_helper = LRUCacheHelper(
Expand Down Expand Up @@ -222,7 +226,6 @@ async def close(self):
if not self._closed:
self._closed = True
await self._session.close()
self._executor.shutdown(wait=True)

if self._cache_enabled:
self._lru_helper.close()
Expand Down Expand Up @@ -311,7 +314,7 @@ async def _reserve_space(self, size: int) -> bool:
logger.debug(
f"rotate on bucket({size=}), num of entries to be cleaned {len(_hashes)=}"
)
self._executor.submit(self._cache_entries_cleanup, _hashes)
await anyio.to_thread.run_sync(self._cache_entries_cleanup, _hashes)
return True
else:
logger.debug(f"rotate on bucket({size=}) failed, no enough entries")
Expand Down Expand Up @@ -429,19 +432,19 @@ async def _retrieve_file_by_cache_lookup(
# NOTE: db_entry.file_sha256 can be either
# 1. valid sha256 value for corresponding plain uncompressed OTA file
# 2. URL based sha256 value for corresponding requested URL
cache_file = self._base_dir / cache_identifier
cache_file = anyio.Path(self._base_dir / cache_identifier)

# check if cache file exists
# NOTE(20240729): there is an edge condition that the finished cached file is not yet renamed,
# but the database entry has already been inserted. Here we wait for 3 rounds for
# cache_commit_callback to rename the tmp file.
_retry_count_max, _factor, _backoff_max = 6, 0.01, 0.1 # 0.255s in total
for _retry_count in range(_retry_count_max):
if cache_file.is_file():
if await cache_file.is_file():
break
await asyncio.sleep(get_backoff(_retry_count, _factor, _backoff_max))

if not cache_file.is_file():
if not await cache_file.is_file():
logger.warning(
f"dangling cache entry found, remove db entry: {meta_db_entry}"
)
Expand All @@ -452,7 +455,7 @@ async def _retrieve_file_by_cache_lookup(
# do the job. If cache is invalid, otaclient will use CacheControlHeader's retry_cache
# directory to indicate invalid cache.
return (
read_file(cache_file, executor=self._executor),
read_file(cache_file),
meta_db_entry.export_headers_to_client(),
)

Expand All @@ -470,28 +473,28 @@ async def _retrieve_file_by_external_cache(

cache_identifier = client_cache_policy.file_sha256
cache_file = self._external_cache_data_dir / cache_identifier
cache_file_zst = cache_file.with_suffix(
f".{cfg.EXTERNAL_CACHE_STORAGE_COMPRESS_ALG}"
cache_file_zst = anyio.Path(
cache_file.with_suffix(f".{cfg.EXTERNAL_CACHE_STORAGE_COMPRESS_ALG}")
)

if cache_file_zst.is_file():
if await cache_file_zst.is_file():
_header = CIMultiDict()
_header[HEADER_OTA_FILE_CACHE_CONTROL] = (
OTAFileCacheControl.export_kwargs_as_header(
file_sha256=cache_identifier,
file_compression_alg=cfg.EXTERNAL_CACHE_STORAGE_COMPRESS_ALG,
)
)
return read_file(cache_file_zst, executor=self._executor), _header
return read_file(cache_file_zst), _header

if cache_file.is_file():
if await cache_file.is_file():
_header = CIMultiDict()
_header[HEADER_OTA_FILE_CACHE_CONTROL] = (
OTAFileCacheControl.export_kwargs_as_header(
file_sha256=cache_identifier
)
)
return read_file(cache_file, executor=self._executor), _header
return read_file(cache_file), _header

async def _retrieve_file_by_new_caching(
self,
Expand Down Expand Up @@ -534,7 +537,6 @@ async def _retrieve_file_by_new_caching(
tracker = CacheTracker(
cache_identifier=cache_identifier,
base_dir=self._base_dir,
executor=self._executor,
commit_cache_cb=self._commit_cache_callback,
below_hard_limit_event=self._storage_below_hard_limit_event,
)
Expand Down
9 changes: 4 additions & 5 deletions src/ota_proxy/utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from __future__ import annotations

from concurrent.futures import Executor
from hashlib import sha256
from os import PathLike
from typing import AsyncIterator

import aiofiles
from anyio import open_file

from .config import config as cfg


async def read_file(fpath: PathLike, *, executor: Executor) -> AsyncIterator[bytes]:
"""Open and read a file asynchronously with aiofiles."""
async with aiofiles.open(fpath, "rb", executor=executor) as f:
async def read_file(fpath: PathLike) -> AsyncIterator[bytes]:
"""Open and read a file asynchronously."""
async with await open_file(fpath, "rb") as f:
while data := await f.read(cfg.CHUNK_SIZE):
yield data

Expand Down
23 changes: 10 additions & 13 deletions src/otaclient/_otaproxy_ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

from __future__ import annotations

import asyncio
import atexit
import logging
import multiprocessing as mp
Expand Down Expand Up @@ -78,18 +77,16 @@ def otaproxy_process(*, init_cache: bool) -> None:
logger.info(f"wait for {upper_proxy=} online...")
ensure_otaproxy_start(str(upper_proxy))

asyncio.run(
run_otaproxy(
host=host,
port=port,
init_cache=init_cache,
cache_dir=local_otaproxy_cfg.BASE_DIR,
cache_db_f=local_otaproxy_cfg.DB_FILE,
upper_proxy=upper_proxy,
enable_cache=proxy_info.enable_local_ota_proxy_cache,
enable_https=proxy_info.gateway_otaproxy,
external_cache_mnt_point=external_cache_mnt_point,
)
run_otaproxy(
host=host,
port=port,
init_cache=init_cache,
cache_dir=local_otaproxy_cfg.BASE_DIR,
cache_db_f=local_otaproxy_cfg.DB_FILE,
upper_proxy=upper_proxy,
enable_cache=proxy_info.enable_local_ota_proxy_cache,
enable_https=proxy_info.gateway_otaproxy,
external_cache_mnt_point=external_cache_mnt_point,
)


Expand Down
1 change: 0 additions & 1 deletion tests/test_ota_proxy/test_cache_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ async def _worker(
_tracker = CacheTracker(
cache_identifier=self.URL,
base_dir=self.base_dir,
executor=None, # type: ignore
commit_cache_cb=None, # type: ignore
below_hard_limit_event=None, # type: ignore
)
Expand Down
Loading

1 comment on commit 4fb90ad

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/ota_metadata/file_table
   __init__.py40100% 
   _orm.py160100% 
   _table.py80890%170, 188–193, 195
   _types.py31487%47, 54–56
src/ota_metadata/legacy
   __init__.py10100% 
   parser.py3354885%106, 170, 175, 211–212, 222–223, 226, 238, 289–291, 295–298, 324–327, 396, 399, 407–409, 422, 431–432, 435–436, 601–603, 653–654, 657, 685–686, 689–690, 692, 696, 698–699, 753, 756–758
   types.py841384%37, 40–42, 112–116, 122–125
src/ota_metadata/utils
   cert_store.py86890%58–59, 73, 87, 91, 102, 123, 127
src/ota_proxy
   __init__.py16756%48–49, 51, 53, 62, 72–73
   __main__.py660%16, 18–19, 21–22, 24
   _consts.py170100% 
   cache_control_header.py68494%71, 91, 113, 121
   cache_streaming.py1431390%209, 223, 227–228, 263–264, 266, 278, 347, 365–368
   config.py200100% 
   db.py801877%103, 109, 167, 173–174, 177, 183, 185, 209–216, 218–219
   errors.py50100% 
   external_cache.py282028%31, 35, 40–42, 44–45, 48–49, 51–53, 60, 63–65, 69–72
   lru_cache_helper.py48295%95–96
   ota_cache.py2346472%72–73, 140, 143–144, 156–157, 189, 192, 214, 234, 253–257, 261–263, 265, 267–274, 276–278, 281–282, 286–287, 291, 338, 346–348, 421, 448, 451–452, 474–476, 480–482, 488, 490–492, 497, 523–525, 559–561, 588, 594, 609
   server_app.py1413972%79, 82, 88, 107, 111, 170, 179, 221–222, 224–226, 229, 234–235, 238, 241–242, 245, 248, 251, 254, 267–268, 271–272, 274, 277, 303–306, 309, 323–325, 331–333
   utils.py130100% 
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
   _logging.py513335%43–44, 46–47, 49–54, 56–57, 59–60, 62–65, 67, 77, 80–82, 84–86, 89–90, 92–96
   _otaproxy_ctx.py42420%20, 22–29, 31–36, 38, 40–41, 44, 46–50, 53–56, 59–60, 62–63, 65–67, 69, 74–78, 80
   _status_monitor.py1851492%56–57, 169, 172, 192, 195, 211–212, 220, 223, 286, 308, 325–326
   _types.py960100% 
   _utils.py30293%80–81
   errors.py120199%97
   main.py25250%17, 19–29, 31–33, 35, 37, 41–42, 44–46, 48–50
   ota_core.py34313959%121, 123–124, 128–129, 131–133, 137–138, 143–144, 150, 152, 211–214, 337, 369–370, 372, 381, 384, 389–390, 393, 399, 401–405, 412, 418, 453–456, 459–470, 473–476, 517–520, 536–537, 541–542, 608–615, 620, 623–630, 655–656, 662, 666–667, 673, 698–700, 702, 743, 765, 792–794, 803–809, 823–829, 831–832, 837–838, 846, 848, 854, 856, 862, 864, 868, 874, 876, 882, 885–887, 897–898, 909–911, 913–914, 916, 918–919, 924, 926, 931
src/otaclient/boot_control
   __init__.py40100% 
   _firmware_package.py932276%82, 86, 136, 180, 186, 209–210, 213–218, 220–221, 224–229, 231
   _grub.py41812769%214, 262–265, 271–275, 312–313, 320–325, 328–334, 337, 340–341, 346, 348–350, 359–365, 367–368, 370–372, 381–383, 385–387, 466–467, 471–472, 524, 530, 556, 578, 582–583, 598–600, 624–627, 639, 643–645, 647–649, 708–711, 736–739, 762–765, 777–778, 781–782, 817, 823, 843–844, 846, 868–870, 888–891, 916–919, 926–929, 934–942, 947–954
   _jetson_cboot.py2612610%20, 22–25, 27–29, 35–40, 42, 58–60, 62, 64–65, 71, 75, 134, 137, 139–140, 143, 150–151, 159–160, 163, 169–170, 178, 187–191, 193, 199, 202–203, 209, 212–213, 218–219, 221, 227–228, 231–232, 235–237, 239, 245, 250–252, 254–256, 261, 263–266, 268–269, 278–279, 282–283, 288–289, 292–296, 299–300, 305–306, 309, 312–316, 321–324, 327, 330–331, 334, 337–338, 341, 345–350, 354–355, 359, 362–363, 366, 369–372, 374, 377–378, 382, 385, 388–391, 393, 400, 404–405, 408–409, 415–416, 422, 424–425, 429, 431, 433–435, 438, 442, 445, 448–449, 451, 454, 462–463, 470, 480, 483, 491–492, 497–500, 502, 509, 511–513, 519–520, 524–525, 528, 532, 535, 537, 544–548, 550, 562–565, 568, 571, 573, 580, 587–589, 591, 593, 596, 599, 602, 604–605, 608–612, 616–618, 620, 628–632, 634, 637, 641, 644, 655–656, 661, 671, 674–680, 684–690, 694–703, 707–715, 719, 721, 723–725
   _jetson_common.py1724573%132, 140, 288–291, 294, 311, 319, 354, 359–364, 382, 408–409, 411–413, 417–420, 422–423, 425–429, 431, 438–439, 442–443, 453, 456–457, 460, 462, 506–507
   _jetson_uefi.py40427432%124–126, 131–132, 151–153, 158–161, 328, 446, 448–451, 455, 459–460, 462–470, 472, 484–485, 488–489, 492–493, 496–498, 502–503, 508–510, 514, 518–519, 522–523, 526–527, 531, 534–535, 537, 542–543, 547, 550–551, 556, 560–561, 564, 568–570, 572, 576–579, 581–582, 604–605, 609–610, 612, 616, 620–621, 624–625, 632, 635–637, 640, 642–643, 648–649, 652–655, 657–658, 663, 665–666, 674, 677–680, 682–683, 685, 689–690, 694, 702–706, 709–710, 712, 715–719, 722, 725–729, 733–734, 737–742, 745–746, 749–752, 754–755, 762–763, 773–776, 779, 782–785, 788–792, 795–796, 799, 802–805, 808, 810, 815–816, 819, 822–825, 827, 833, 838–839, 858–859, 862, 870–871, 878, 888, 891, 898–899, 904–907, 915–918, 926–927, 939–942, 944, 947, 950, 958, 966–968, 970–972, 974–978, 983–984, 986, 999, 1003, 1006, 1016, 1021, 1029–1030, 1033, 1037, 1039–1041, 1047–1048, 1053, 1061–1066, 1071–1076, 1081–1089, 1094–1101, 1109–1111
   _ota_status_control.py1021189%117, 122, 127, 240, 244–245, 248, 255, 257–258, 273
   _rpi_boot.py28713353%53, 56, 120–121, 125, 133–136, 150–153, 158–159, 161–162, 167–168, 171–172, 181–182, 222, 228–232, 235, 253–255, 259–261, 266–268, 272–274, 284–285, 288, 291, 293–294, 296–297, 299–301, 307, 310–311, 321–324, 332–336, 338, 340–341, 346–347, 354, 357–362, 393, 395–398, 408–411, 415–416, 418–422, 450–453, 472–475, 498–501, 506–514, 519–526, 541–544, 551–554, 562–564
   _slot_mnt_helper.py100100% 
   configs.py510100% 
   protocol.py50100% 
   selecter.py412929%44–46, 49–50, 54–55, 58–60, 63, 65, 69, 77–79, 81–82, 84–85, 89, 91, 93–94, 96, 98–99, 101, 103
src/otaclient/configs
   __init__.py170100% 
   _cfg_configurable.py470100% 
   _cfg_consts.py47197%97
   _common.py80100% 
   _ecu_info.py56492%59, 64–65, 112
   _proxy_info.py50590%84, 86–87, 89, 100
   cfg.py230100% 
src/otaclient/create_standby
   __init__.py13192%36
   common.py2264480%59, 62–63, 67–69, 71, 75–76, 78, 126, 174–176, 178–180, 182, 185–188, 192, 203, 279–280, 282–287, 299, 339, 367, 370–372, 388–389, 403, 407, 429–430
   interface.py70100% 
   rebuild_mode.py1151091%98–100, 119, 150–155
src/otaclient/grpc/api_v2
   ecu_status.py145795%117, 142, 144, 275, 347–348, 384
   ecu_tracker.py54540%17, 19–22, 24–30, 32–33, 35, 46–47, 50, 52, 58–61, 63, 65, 67–70, 77, 81–84, 88–89, 91, 93, 95–103, 107–108, 110, 112–115
   main.py41410%17, 19–24, 26–27, 29, 32, 39, 41–42, 44–45, 47–48, 50–55, 57–59, 61, 64, 70, 72–73, 76–77, 79–82, 84–85, 87
   servicer.py1169518%57–61, 63–64, 66–67, 73–77, 81–82, 87, 90, 94–96, 100–102, 110–112, 115–119, 128–138, 145, 151, 154–156, 167–169, 172–174, 179, 186–189, 192, 196–197, 202, 205, 209–211, 215–217, 225–226, 229–233, 242–251, 258, 264, 267–269, 274–275, 278
   types.py44295%78–79
src/otaclient_api/v2
   api_caller.py39684%45–47, 83–85
   types.py2563287%61, 64, 67–70, 86, 89–92, 131, 209–210, 212, 259, 262–263, 506–508, 512–513, 515, 518–519, 522–523, 578, 585–586, 588
src/otaclient_common
   __init__.py341555%42–44, 61, 63, 68–77
   _io.py64198%41
   cmdhelper.py130100% 
   common.py1061090%148, 151–153, 168, 175–177, 271, 275
   downloader.py1991094%107–108, 126, 153, 369, 424, 428, 516–517, 526
   linux.py611575%51–53, 59, 69, 74, 76, 108–109, 133–134, 190, 195–196, 198
   logging.py29196%55
   persist_file_handling.py1181884%113, 118, 150–152, 163, 192–193, 228–232, 242–244, 246–247
   proto_streamer.py42880%33, 48, 66–67, 72, 81–82, 100
   proto_wrapper.py3985785%87, 134–141, 165, 172, 184–186, 189–190, 205, 210, 221, 257, 263, 268, 299, 303, 307, 402, 462, 469, 472, 492, 499, 501, 526, 532, 535, 537, 562, 568, 571, 573, 605, 609, 611, 625, 642, 669, 672, 676, 692, 707, 713, 762–763, 765, 803–805
   retry_task_map.py129993%134–135, 153–154, 207–208, 210, 230–231
   shm_status.py952177%79–80, 83–84, 105, 120–122, 134, 139, 156–160, 169–170, 172, 179, 192, 204
   typing.py31487%48, 97–98, 100
TOTAL6825188672% 

Tests Skipped Failures Errors Time
242 0 💤 0 ❌ 0 🔥 12m 31s ⏱️

Please sign in to comment.