Skip to content

Commit

Permalink
fix: ecu_status: should use any_child_ecu_in_update flag instead of a…
Browse files Browse the repository at this point in the history
…ny_in_update to avoid self-lockedown, still wait on any_child_ecu_in_update before reboot (#459)

This PR reverts the change that otaclient waits for any_requires_network instead of any_in_update. This change was introduced after otaclient v3.8.x.
Also, a logic fix is introduced to use any_child_ecu_in_update instead of any_in_update.

any_in_update will be set when any ECU(including self-ECU) is in UPDATE. If otaclient waits for this flag before OTA reboot(at this point the self-ECU's OTA status is still UPDATING), it will be dead-locked by itself.
Fix by using any_child_ecu_in_update instead, which aligns with the previous implementation at otaclient v3.8.x.
  • Loading branch information
Bodong-Yang authored Dec 19, 2024
1 parent 3d05444 commit 4723777
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 74 deletions.
2 changes: 1 addition & 1 deletion src/otaclient/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class OTAClientStatus:

@dataclass
class MultipleECUStatusFlags:
any_in_update: mp_sync.Event
any_child_ecu_in_update: mp_sync.Event
any_requires_network: mp_sync.Event
all_success: mp_sync.Event

Expand Down
77 changes: 21 additions & 56 deletions src/otaclient/grpc/api_v2/ecu_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,11 @@
import math
import time
from itertools import chain
from typing import Dict, Iterable, Optional

from otaclient._types import MultipleECUStatusFlags, OTAClientStatus
from otaclient.configs.cfg import cfg, ecu_info
from otaclient.grpc.api_v2.types import convert_to_apiv2_status
from otaclient_api.v2 import types as api_types
from otaclient_common.typing import T

logger = logging.getLogger(__name__)

Expand All @@ -64,23 +62,6 @@
ACTIVE_POLLING_INTERVAL = 1 # seconds


class _OrderedSet(Dict[T, None]):
def __init__(self, _input: Optional[Iterable[T]]):
if _input:
for elem in _input:
self[elem] = None
super().__init__()

def add(self, value: T):
self[value] = None

def remove(self, value: T):
super().pop(value)

def discard(self, value: T):
super().pop(value, None)


class ECUStatusStorage:

def __init__(
Expand All @@ -95,27 +76,13 @@ def __init__(
# ECU status storage
self.storage_last_updated_timestamp = 0

# ECUs that are/will be active during an OTA session,
# at init it will be the ECUs listed in available_ecu_ids defined
# in ecu_info.yaml.
# When receives update request, the list will be set to include ECUs
# listed in the update request, and be extended by merging
# available_ecu_ids in sub ECUs' status report.
# Internally referenced when generating overall ECU status report.
# TODO: in the future if otaclient can preserve OTA session info,
# ECUStatusStorage should restore the tracked_active_ecus info
# in the saved session info.
self._tracked_active_ecus: _OrderedSet[str] = _OrderedSet(
ecu_info.get_available_ecu_ids()
)

# The attribute that will be exported in status API response,
# NOTE(20230801): available_ecu_ids only serves information purpose,
# it should only be updated with ecu_info.yaml or merging
# available_ecu_ids field in sub ECUs' status report.
# NOTE(20230801): for web.auto user, available_ecu_ids in status API response
# will be used to generate update request list, so be-careful!
self._available_ecu_ids: _OrderedSet[str] = _OrderedSet(
# NOTE(20241219): we will only look at status of ECUs listed in available_ecu_ids.
# ECUs that in the secondaries field but no in available_ecu_ids field
# are considered to be the ECUs not ready for OTA. See ecu_info.yaml doc.
self._available_ecu_ids: dict[str, None] = dict.fromkeys(
ecu_info.get_available_ecu_ids()
)

Expand Down Expand Up @@ -185,7 +152,7 @@ async def _generate_overall_status_report(self):
for status in chain(
self._all_ecus_status_v2.values(), self._all_ecus_status_v1.values()
)
if status.ecu_id in self._tracked_active_ecus
if status.ecu_id in self._available_ecu_ids
and status.is_in_update
and status.ecu_id not in lost_ecus
}
Expand All @@ -195,10 +162,11 @@ async def _generate_overall_status_report(self):
"new ECU(s) that acks update request and enters OTA update detected"
f"{_new_in_update_ecu}, current updating ECUs: {in_update_ecus_id}"
)
if in_update_ecus_id:
ecu_status_flags.any_in_update.set()

if self.in_update_child_ecus_id:
ecu_status_flags.any_child_ecu_in_update.set()
else:
ecu_status_flags.any_in_update.clear()
ecu_status_flags.any_child_ecu_in_update.clear()

# check if there is any failed child/self ECU in tracked active ECUs set
_old_failed_ecus_id = self.failed_ecus_id
Expand All @@ -207,7 +175,7 @@ async def _generate_overall_status_report(self):
for status in chain(
self._all_ecus_status_v2.values(), self._all_ecus_status_v1.values()
)
if status.ecu_id in self._tracked_active_ecus
if status.ecu_id in self._available_ecu_ids
and status.is_failed
and status.ecu_id not in lost_ecus
}
Expand All @@ -223,7 +191,7 @@ async def _generate_overall_status_report(self):
for status in chain(
self._all_ecus_status_v2.values(), self._all_ecus_status_v1.values()
)
if status.ecu_id in self._tracked_active_ecus
if status.ecu_id in self._available_ecu_ids
and status.ecu_id not in lost_ecus
)
):
Expand All @@ -240,12 +208,12 @@ async def _generate_overall_status_report(self):
for status in chain(
self._all_ecus_status_v2.values(), self._all_ecus_status_v1.values()
)
if status.ecu_id in self._tracked_active_ecus
if status.ecu_id in self._available_ecu_ids
and status.is_success
and status.ecu_id not in lost_ecus
}
# NOTE: all_success doesn't count the lost ECUs
if len(self.success_ecus_id) == len(self._tracked_active_ecus):
if self.success_ecus_id == set(self._available_ecu_ids):
ecu_status_flags.all_success.set()
else:
ecu_status_flags.all_success.clear()
Expand Down Expand Up @@ -334,19 +302,20 @@ async def on_ecus_accept_update_request(self, ecus_accept_update: set[str]):
"""
ecu_status_flags = self.ecu_status_flags
async with self._properties_update_lock:
self._tracked_active_ecus = _OrderedSet(ecus_accept_update)

self.last_update_request_received_timestamp = int(time.time())
self.lost_ecus_id -= ecus_accept_update
self.failed_ecus_id -= ecus_accept_update
self.success_ecus_id -= ecus_accept_update

self.in_update_ecus_id.update(ecus_accept_update)
self.in_update_child_ecus_id = self.in_update_ecus_id - {self.my_ecu_id}
self.success_ecus_id -= ecus_accept_update

ecu_status_flags.all_success.clear()
ecu_status_flags.any_requires_network.set()
ecu_status_flags.any_in_update.set()
if self.in_update_child_ecus_id:
ecu_status_flags.any_child_ecu_in_update.set()
else:
ecu_status_flags.any_child_ecu_in_update.clear()

def get_polling_interval(self) -> int:
"""Return <ACTIVE_POLLING_INTERVAL> if there is active OTA update,
Expand All @@ -355,11 +324,8 @@ def get_polling_interval(self) -> int:
NOTE: use get_polling_waiter if want to wait, only call this method
if one only wants to get the polling interval value.
"""
ecu_status_flags = self.ecu_status_flags
return (
ACTIVE_POLLING_INTERVAL
if ecu_status_flags.any_in_update.is_set()
else IDLE_POLLING_INTERVAL
ACTIVE_POLLING_INTERVAL if self.in_update_ecus_id else IDLE_POLLING_INTERVAL
)

def get_polling_waiter(self):
Expand All @@ -377,13 +343,12 @@ def get_polling_waiter(self):
_inner_wait_interval = 1 # second

async def _waiter():
ecu_status_flags = self.ecu_status_flags
if ecu_status_flags.any_in_update.is_set():
if self.in_update_ecus_id:
await asyncio.sleep(ACTIVE_POLLING_INTERVAL)
return

for _ in range(math.ceil(IDLE_POLLING_INTERVAL / _inner_wait_interval)):
if ecu_status_flags.any_in_update.is_set():
if self.in_update_ecus_id:
return
await asyncio.sleep(_inner_wait_interval)

Expand Down
2 changes: 1 addition & 1 deletion src/otaclient/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def main() -> None: # pragma: no cover
local_otaclient_op_queue = mp_ctx.Queue()
local_otaclient_resp_queue = mp_ctx.Queue()
ecu_status_flags = MultipleECUStatusFlags(
any_in_update=mp_ctx.Event(),
any_child_ecu_in_update=mp_ctx.Event(),
any_requires_network=mp_ctx.Event(),
all_success=mp_ctx.Event(),
)
Expand Down
4 changes: 1 addition & 3 deletions src/otaclient/ota_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,11 +579,9 @@ def _execute_update(self):
)
)

# NOTE: we don't need to wait for sub ECUs if sub ECUs don't
# depend on otaproxy on this ECU.
if proxy_info.enable_local_ota_proxy:
wait_and_log(
check_flag=self.ecu_status_flags.any_requires_network.is_set,
check_flag=self.ecu_status_flags.any_child_ecu_in_update.is_set,
check_for=False,
message="permit reboot flag",
log_func=logger.info,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_otaclient/test_create_standby.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_update_with_rebuild_mode(
):
status_collector, status_report_queue = ota_status_collector
ecu_status_flags = mocker.MagicMock()
ecu_status_flags.any_requires_network.is_set = mocker.MagicMock(
ecu_status_flags.any_child_ecu_in_update.is_set = mocker.MagicMock(
return_value=False
)

Expand Down Expand Up @@ -145,7 +145,7 @@ def test_update_with_rebuild_mode(
# ------ assertions ------ #
persist_handler.assert_called_once()

ecu_status_flags.any_requires_network.is_set.assert_called_once()
ecu_status_flags.any_child_ecu_in_update.is_set.assert_called_once()
# --- ensure the update stats are collected
collected_status = status_collector.otaclient_status
assert collected_status
Expand Down
67 changes: 59 additions & 8 deletions tests/test_otaclient/test_grpc/test_api_v2/test_ecu_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async def setup_test(self, mocker: MockerFixture, ecu_info_fixture: ECUInfo):
# init and setup the ecu_storage
# NOTE: here we use threading.Event instead
self.ecu_status_flags = ecu_status_flags = MultipleECUStatusFlags(
any_in_update=threading.Event(), # type: ignore[assignment]
any_child_ecu_in_update=threading.Event(), # type: ignore[assignment]
any_requires_network=threading.Event(), # type: ignore[assignment]
all_success=threading.Event(), # type: ignore[assignment]
)
Expand Down Expand Up @@ -380,7 +380,7 @@ async def test_export(
},
# ecu_status_flags
{
"any_in_update": True,
"any_child_ecu_in_update": True,
"any_requires_network": True,
"all_success": False,
},
Expand Down Expand Up @@ -429,7 +429,55 @@ async def test_export(
},
# ecu_status_flags
{
"any_in_update": True,
"any_child_ecu_in_update": True,
"any_requires_network": True,
"all_success": False,
},
),
# case 3:
# only main ECU doing OTA update.
(
# local ECU status: UPDATING
_internal_types.OTAClientStatus(
ota_status=_internal_types.OTAStatus.UPDATING,
update_phase=_internal_types.UpdatePhase.DOWNLOADING_OTA_FILES,
),
# sub ECUs status
[
# p1: SUCCESS
api_types.StatusResponse(
available_ecu_ids=["p1"],
ecu_v2=[
api_types.StatusResponseEcuV2(
ecu_id="p1",
ota_status=api_types.StatusOta.SUCCESS,
),
],
),
# p2: SUCCESS
api_types.StatusResponse(
available_ecu_ids=["p2"],
ecu=[
api_types.StatusResponseEcu(
ecu_id="p2",
status=api_types.Status(
status=api_types.StatusOta.SUCCESS,
),
)
],
),
],
# expected overal ECUs status report set by on_ecus_accept_update_request,
{
"lost_ecus_id": set(),
"in_update_ecus_id": {"autoware"},
"in_update_child_ecus_id": set(),
"failed_ecus_id": set(),
"success_ecus_id": {"p1", "p2"},
},
# ecu_status_flags
{
"any_child_ecu_in_update": False,
"any_requires_network": True,
"all_success": False,
},
Expand Down Expand Up @@ -510,7 +558,7 @@ async def test_overall_ecu_status_report_generation(
},
# ecu_status_flags
{
"any_in_update": True,
"any_child_ecu_in_update": True,
"any_requires_network": True,
"all_success": False,
},
Expand Down Expand Up @@ -562,7 +610,7 @@ async def test_overall_ecu_status_report_generation(
},
# ecu_status_flags
{
"any_in_update": True,
"any_child_ecu_in_update": True,
"any_requires_network": True,
"all_success": False,
},
Expand Down Expand Up @@ -604,15 +652,18 @@ async def test_on_receive_update_request(
for k, v in flags_status.items():
assert getattr(self.ecu_status_flags, k).is_set() == v

async def test_polling_waiter_switching_from_idling_to_active(self):
async def test_polling_waiter_switching_from_idling_to_active(
self, mocker: pytest_mock.MockerFixture
):
"""Waiter should immediately return if active_ota_update_present is set."""
_sleep_time, _mocked_interval = 3, 60

mocker.patch(f"{ECU_STATUS_MODULE}.IDLE_POLLING_INTERVAL", _mocked_interval)

async def _event_setter():
await asyncio.sleep(_sleep_time)
self.ecu_status_flags.any_in_update.set()
await self.ecu_storage.on_ecus_accept_update_request({"autoware"})

self.ecu_status_flags.any_in_update.clear()
_waiter = self.ecu_storage.get_polling_waiter()
asyncio.create_task(_event_setter())
# waiter should return on active_ota_update_present is set, instead of waiting the
Expand Down
6 changes: 3 additions & 3 deletions tests/test_otaclient/test_ota_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def test_otaupdater(
) -> None:
_, report_queue = ota_status_collector
ecu_status_flags = mocker.MagicMock()
ecu_status_flags.any_requires_network.is_set = mocker.MagicMock(
ecu_status_flags.any_child_ecu_in_update.is_set = mocker.MagicMock(
return_value=False
)

Expand Down Expand Up @@ -202,7 +202,7 @@ def test_otaupdater(
assert _downloaded_files_size == self._delta_bundle.total_download_files_size

# assert the control_flags has been waited
ecu_status_flags.any_requires_network.is_set.assert_called_once()
ecu_status_flags.any_child_ecu_in_update.is_set.assert_called_once()

assert _updater.update_version == str(cfg.UPDATE_VERSION)

Expand Down Expand Up @@ -235,7 +235,7 @@ def mock_setup(
):
_, status_report_queue = ota_status_collector
ecu_status_flags = mocker.MagicMock()
ecu_status_flags.any_requires_network.is_set = mocker.MagicMock(
ecu_status_flags.any_child_ecu_in_update.is_set = mocker.MagicMock(
return_value=False
)

Expand Down

1 comment on commit 4723777

@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/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__.py15660%48, 50, 52, 61, 71–72
   __main__.py880%16, 18–20, 22, 24–25, 27
   _consts.py170100% 
   cache_control_header.py68494%71, 91, 113, 121
   cache_streaming.py1442284%154–156, 184–186, 211, 225, 229–230, 265–266, 268, 280, 349, 355–356, 359, 367–370
   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%71–72, 143, 146–147, 159–160, 192–193, 210, 231, 250–254, 258–260, 262, 264–271, 273–275, 278–279, 283–284, 288, 335, 343–345, 418, 445, 448–449, 471–473, 477–479, 485, 487–489, 494, 520–522, 557–559, 586, 592, 607
   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.py140100% 
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.py43430%20, 22–30, 32–37, 39, 41–42, 45, 47–51, 54–57, 60–61, 63–64, 66–68, 70, 75–79, 81
   _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.py34213959%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, 512–515, 531–532, 536–537, 603–610, 615, 618–625, 650–651, 657, 661–662, 668, 693–695, 697, 737, 759, 786–788, 797–803, 817–823, 825–826, 831–832, 840, 842, 848, 850, 856, 858, 862, 868, 870, 876, 879–881, 891–892, 903–905, 907–908, 910, 912–913, 918, 920, 925
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, 871–873, 891–894, 919–922, 929–932, 937–945, 950–957
   _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, 590–592, 594, 596, 599, 602, 605, 607–608, 611–615, 619–621, 623, 631–635, 637, 640, 644, 647, 658–659, 664, 674, 677–683, 687–693, 697–706, 710–718, 722, 724, 726–728
   _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, 969–971, 973–975, 977–981, 986–987, 989, 1002, 1006, 1009, 1019, 1024, 1032–1033, 1036, 1040, 1042–1044, 1050–1051, 1056, 1064–1069, 1074–1079, 1084–1092, 1097–1104, 1112–1114
   _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, 501–504, 509–517, 522–529, 544–547, 554–557, 565–567
   _slot_mnt_helper.py100100% 
   configs.py510100% 
   protocol.py60100% 
   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.py190100% 
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
TOTAL6694188571% 

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

Please sign in to comment.