Skip to content

Commit

Permalink
feat: re-implement otaclient configs, add support for dynamic root an…
Browse files Browse the repository at this point in the history
…d runtime configurable settings (#398)

Re-implement the otaclient.app.configs into otaclient.configs, add support for dynamic root and runtime configurable settings.
The dynamic root feature is needed for enabling otaclient-in-container in the future.
Runtime configurable settings feature allows the user to configure the otaclient's behavior via environmental variables. 

Other changes include:
1. now importing otaclient.configs module and its sub modules with _ prefix will not have side-effect(i.e., ecu_info.yaml being loaded, etc.), the configs loading are done by the otaclient.configs.cfg module.
2. cleanup, re-arrange, merge and simplify some of the settings.
3. ecu_info and proxy_info config objects now are exposed via otaclient.configs.cfg.
  • Loading branch information
Bodong-Yang authored Oct 19, 2024
1 parent 2a930e7 commit 80916fb
Show file tree
Hide file tree
Showing 16 changed files with 411 additions and 28 deletions.
6 changes: 4 additions & 2 deletions src/otaclient/app/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from logging import INFO
from typing import Dict, Tuple

from otaclient.configs.ecu_info import ecu_info # noqa
from otaclient.configs.proxy_info import proxy_info # noqa
from otaclient.configs.cfg import (
ecu_info, # noqa
proxy_info, # noqa
)


class CreateStandbyMechanism(Enum):
Expand Down
5 changes: 3 additions & 2 deletions src/otaclient/app/ota_client_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@
from ota_proxy import config as local_otaproxy_cfg
from otaclient import log_setting
from otaclient.boot_control._common import CMDHelperFuncs
from otaclient.configs.ecu_info import ECUContact
from otaclient.configs import ECUContact
from otaclient.configs.cfg import ecu_info, proxy_info
from otaclient_api.v2 import types as api_types
from otaclient_api.v2.api_caller import ECUNoResponse, OTAClientCall
from otaclient_common.common import ensure_otaproxy_start

from .configs import config as cfg
from .configs import ecu_info, proxy_info, server_cfg
from .configs import server_cfg
from .ota_client import OTAClientControlFlags, OTAServicer

logger = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion src/otaclient/boot_control/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from dataclasses import dataclass

from otaclient.app.configs import BaseConfig
from otaclient.configs.ecu_info import BootloaderType
from otaclient.configs import BootloaderType


@dataclass
Expand Down
22 changes: 22 additions & 0 deletions src/otaclient/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""otaclient configs package."""

from otaclient.configs._cfg_configurable import (
ENV_PREFIX,
ConfigurableSettings,
set_configs,
)
from otaclient.configs._cfg_consts import Consts, CreateStandbyMechanism, dynamic_root
from otaclient.configs._ecu_info import BootloaderType, ECUContact, ECUInfo
from otaclient.configs._proxy_info import ProxyInfo

__all__ = [
"ENV_PREFIX",
"ConfigurableSettings",
"Consts",
"CreateStandbyMechanism",
"BootloaderType",
"ECUContact",
"ECUInfo",
"ProxyInfo",
"set_configs",
"dynamic_root",
]
134 changes: 134 additions & 0 deletions src/otaclient/configs/_cfg_configurable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright 2022 TIER IV, INC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runtime configurable configs for otaclient."""


from __future__ import annotations

import logging
from typing import Dict, Literal

from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict

from otaclient.configs._cfg_consts import cfg_consts

logger = logging.getLogger(__name__)

ENV_PREFIX = "OTACLIENT_"
LOG_LEVEL_LITERAL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
CREATE_STANDBY_METHOD_LTIERAL = Literal["REBUILD", "IN_PLACE"]


class _OTAClientSettings(BaseModel):
#
# ------ logging settings ------ #
#
DEFAULT_LOG_LEVEL: LOG_LEVEL_LITERAL = "INFO"
LOG_LEVEL_TABLE: Dict[str, LOG_LEVEL_LITERAL] = {
"ota_metadata": "INFO",
"otaclient": "INFO",
"otaclient_api": "INFO",
"otaclient_common": "INFO",
"otaproxy": "INFO",
}
LOG_FORMAT: str = (
"[%(asctime)s][%(levelname)s]-%(name)s:%(funcName)s:%(lineno)d,%(message)s"
)

#
# ------ downloading settings ------ #
#
DOWNLOAD_RETRY_PRE_REQUEST: int = 3
DOWNLOAD_BACKOFF_MAX: int = 3 # seconds
DOWNLOAD_BACKOFF_FACTOR: float = 0.1 # seconds

DOWNLOAD_THREADS: int = 6
MAX_CONCURRENT_DOWNLOAD_TASKS: int = 128
DOWNLOAD_INACTIVE_TIMEOUT: int = 5 * 60 # seconds

#
# ------ create standby settings ------ #
#
CREATE_STANDBY_METHOD: CREATE_STANDBY_METHOD_LTIERAL = "REBUILD"
MAX_CONCURRENT_PROCESS_FILE_TASKS: int = 512
MAX_PROCESS_FILE_THREAD: int = 6
CREATE_STANDBY_RETRY_MAX: int = 1024

#
# ------ debug flags ------ #
#
DEBUG_ENABLE_FAILURE_TRACEBACK_IN_STATUS_RESP: bool = False
DEBUG_DISABLE_OTAPROXY_HTTPS_VERIFY: bool = False
DEBUG_DISABLE_OTAMETA_CERT_CHECK: bool = False
DEBUG_DISABLE_OTAMETA_SIGN_CHECK: bool = False


class _MultipleECUSettings(BaseModel):
# The timeout of waiting sub ECU acks the OTA request.
WAITING_SUBECU_ACK_REQ_TIMEOUT: int = 6

# The timeout of waiting sub ECU responds to status API request
QUERYING_SUBECU_STATUS_TIMEOUT: int = 6

# The ECU status storage will summarize the stored ECUs' status report
# and generate overall status report for all ECUs every <INTERVAL> seconds.
OVERALL_ECUS_STATUS_UPDATE_INTERVAL: int = 6 # seconds

# If ECU has been disconnected longer than <TIMEOUT> seconds, it will be
# treated as UNREACHABLE, and will not be counted when generating overall
# ECUs status report.
# NOTE: unreachable_timeout should be larger than
# downloading_group timeout
ECU_UNREACHABLE_TIMEOUT: int = 20 * 60 # seconds

# Otaproxy should not be shutdowned with less than <INTERVAL> seconds
# after it just starts to prevent repeatedly start/stop cycle.
OTAPROXY_MINIMUM_SHUTDOWN_INTERVAL: int = 1 * 60 # seconds

# When any ECU acks update request, this ECU will directly set the overall ECU status
# to any_in_update=True, any_requires_network=True, all_success=False, to prevent
# pre-mature overall ECU status changed caused by child ECU delayed ack to update request.
#
# This pre-set overall ECU status will be kept for <KEEP_TIME> seconds.
# This value is expected to be larger than the time cost for subECU acks the OTA request.
PAUSED_OVERALL_ECUS_STATUS_CHANGE_ON_UPDATE_REQ_ACKED: int = 60 # seconds


class _OTAProxySettings(BaseModel):
OTAPROXY_ENABLE_EXTERNAL_CACHE: bool = True
EXTERNAL_CACHE_DEV_FSLABEL: str = "ota_cache_src"
EXTERNAL_CACHE_DEV_MOUNTPOINT: str = f"{cfg_consts.MOUNT_SPACE}/external_cache"
EXTERNAL_CACHE_SRC_PATH: str = f"{EXTERNAL_CACHE_DEV_MOUNTPOINT}/data"


class ConfigurableSettings(_OTAClientSettings, _MultipleECUSettings, _OTAProxySettings):
"""otaclient runtime configuration settings."""


def set_configs() -> ConfigurableSettings:
try:

class _SettingParser(ConfigurableSettings, BaseSettings):
model_config = SettingsConfigDict(
validate_default=True,
env_prefix=ENV_PREFIX,
)

_parsed_setting = _SettingParser()
return ConfigurableSettings.model_construct(**_parsed_setting.model_dump())
except Exception as e:
logger.error(f"failed to parse otaclient configurable settings: {e!r}")
logger.warning("use default settings ...")
return ConfigurableSettings()
94 changes: 94 additions & 0 deletions src/otaclient/configs/_cfg_consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2022 TIER IV, INC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""otaclient internal uses consts, should not be changed from external."""


from __future__ import annotations

from enum import Enum

from otaclient_common import replace_root

CANONICAL_ROOT = "/"


class CreateStandbyMechanism(str, Enum):
LEGACY = "LEGACY" # deprecated and removed
REBUILD = "REBUILD" # default
IN_PLACE = "IN_PLACE" # not yet implemented


class Consts:

@property
def ACTIVE_ROOT(self) -> str: # NOSONAR
return self._ACTIVE_ROOT

#
# ------ paths ------ #
#
RUN_DIR = "/run/otaclient"
OTACLIENT_PID_FILE = "/run/otaclient.pid"

# runtime folder for holding ota related files
RUNTIME_OTA_SESSION = "/run/otaclient/ota"

MOUNT_SPACE = "/run/otaclient/mnt"
ACTIVE_SLOT_MNT = "/run/otaclient/mnt/active_slot"
STANDBY_SLOT_MNT = "/run/otaclient/mnt/standby_slot"

OTA_TMP_STORE = "/.ota-tmp"
"""tmp store for local copy, located at standby slot."""

OPT_OTA_DPATH = "/opt/ota"
OTACLIENT_INSTALLATION = "/opt/ota/client"
CERT_DPATH = "/opt/ota/client/certs"
IMAGE_META_DPATH = "/opt/ota/image-meta"

BOOT_DPATH = "/boot"
OTA_DPATH = "/boot/ota"
ECU_INFO_FPATH = "/boot/ota/ecu_info.yaml"
PROXY_INFO_FPATH = "/boot/ota/proxy_info.yaml"

ETC_DPATH = "/etc"
PASSWD_FPATH = "/etc/passwd"
GROUP_FPATH = "/etc/group"
FSTAB_FPATH = "/etc/fstab"

#
# ------ consts ------ #
#
# ota status files
OTA_STATUS_FNAME = "status"
OTA_VERSION_FNAME = "version"
SLOT_IN_USE_FNAME = "slot_in_use"

OTA_API_SERVER_PORT = 50051
OTAPROXY_LISTEN_PORT = 8082

def __init__(self) -> None:
"""For future updating the ACTIVE_ROOT."""

# TODO: detect rootfs here
self._ACTIVE_ROOT = CANONICAL_ROOT


cfg_consts = Consts()


def dynamic_root(canonical_path: str) -> str:
"""Re-root the input path with the actual ACTIVE_ROOT."""
if cfg_consts.ACTIVE_ROOT == CANONICAL_ROOT:
return canonical_path
return replace_root(canonical_path, CANONICAL_ROOT, cfg_consts.ACTIVE_ROOT)
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,3 @@ def parse_ecu_info(ecu_info_file: StrOrPath) -> ECUInfo:
logger.warning(f"{ecu_info_file=} is invalid: {e!r}\n{_raw_yaml_str=}")
logger.warning(f"use default ecu_info: {DEFAULT_ECU_INFO}")
return DEFAULT_ECU_INFO


# NOTE(20240327): set the default as literal for now,
# in the future this will be app_cfg.ECU_INFO_FPATH
ecu_info = parse_ecu_info(ecu_info_file="/boot/ota/ecu_info.yaml")
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,3 @@ def parse_proxy_info(proxy_info_file: StrOrPath) -> ProxyInfo:
logger.warning(f"{proxy_info_file=} is invalid: {e!r}\n{_raw_yaml_str=}")
logger.warning(f"use default proxy_info: {DEFAULT_PROXY_INFO}")
return DEFAULT_PROXY_INFO


# NOTE(20240327): set the default as literal for now,
# in the future this will be app_cfg.PROXY_INFO_FPATH
proxy_info = parse_proxy_info("/boot/ota/proxy_info.yaml")
61 changes: 61 additions & 0 deletions src/otaclient/configs/cfg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2022 TIER IV, INC. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Load ecu_info, proxy_info and otaclient configs."""

from typing import TYPE_CHECKING, Any

from otaclient.configs._cfg_configurable import ConfigurableSettings, set_configs
from otaclient.configs._cfg_consts import Consts
from otaclient.configs._ecu_info import (
BootloaderType,
ECUContact,
ECUInfo,
parse_ecu_info,
)
from otaclient.configs._proxy_info import ProxyInfo, parse_proxy_info

__all__ = [
"BootloaderType",
"ECUContact",
"ECUInfo",
"ecu_info",
"ProxyInfo",
"proxy_info",
"cfg",
]

cfg_configurable = set_configs()
cfg_consts = Consts()

if TYPE_CHECKING:

class _OTAClientConfigs(ConfigurableSettings, Consts):
"""OTAClient configs."""

else:

class _OTAClientConfigs:

def __getattribute__(self, name: str) -> Any:
for _cfg in [cfg_consts, cfg_configurable]:
try:
return getattr(_cfg, name)
except AttributeError:
continue
raise AttributeError(f"no such config field: {name=}")


cfg = _OTAClientConfigs()
ecu_info = parse_ecu_info(ecu_info_file=cfg.ECU_INFO_FPATH)
proxy_info = parse_proxy_info(proxy_info_file=cfg.PROXY_INFO_FPATH)
Loading

1 comment on commit 80916fb

@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__.py110100% 
   parser.py3353888%100, 156, 161, 197–198, 208–209, 212, 224, 282, 292–295, 334–337, 417, 420, 428–430, 443, 452–453, 456–457, 669–670, 673, 700–702, 752, 755–757
   types.py841384%37, 40–42, 112–116, 122–125
src/ota_proxy
   __init__.py361072%59, 61, 63, 72, 81–82, 102, 104–106
   __main__.py770%16–18, 20, 22–23, 25
   _consts.py150100% 
   cache_control_header.py68494%71, 91, 113, 121
   cache_streaming.py1421390%211, 225, 229–230, 265–266, 268, 280, 348, 366–369
   config.py170100% 
   db.py731875%109, 115, 153, 159–160, 163, 169, 171, 192–199, 201–202
   errors.py50100% 
   lru_cache_helper.py47295%84–85
   ota_cache.py2155972%70–71, 140, 151–152, 184–185, 202, 239–243, 247–249, 251, 253–260, 262–264, 267–268, 272–273, 277, 324, 332–334, 413–416, 430, 433–434, 448–449, 451–453, 457–458, 464–465, 496, 502, 529, 581–583
   server_app.py1393971%76, 79, 85, 101, 103, 162, 171, 213–214, 216–218, 221, 226–228, 231–232, 235, 238, 241, 244, 257–258, 261–262, 264, 267, 293–296, 299, 313–315, 321–323
   utils.py140100% 
src/otaclient
   __init__.py5260%17, 19
   __main__.py110%16
   log_setting.py52590%53, 55, 64–66
src/otaclient/app
   __main__.py110%16
   configs.py750100% 
   errors.py1200100% 
   interface.py30100% 
   main.py46589%52–53, 75–77
   ota_client.py37310871%79, 87, 108, 135, 137–138, 140, 144, 148–149, 154–155, 161, 163, 201–204, 210, 214, 220, 339, 351–352, 354, 363, 366, 371–372, 375, 381, 383–387, 406–409, 412–419, 447–450, 496–497, 501, 503–504, 534–535, 544–551, 558, 561–567, 612–615, 623, 659–661, 666–668, 671–672, 674–675, 677, 735–736, 739, 747–748, 751, 762–763, 766, 774–775, 778, 789, 808, 835, 854, 872
   ota_client_stub.py39410972%76–78, 80–81, 89–92, 95–97, 101, 106–107, 109–110, 113, 115–116, 119–121, 124–125, 128–130, 135–140, 144, 147–151, 153–154, 162–164, 167, 204–206, 211, 247, 272, 275, 278, 382, 406, 408, 432, 478, 535, 605–606, 645, 664–666, 672–675, 679–681, 688–690, 693, 697–700, 753, 842–844, 851, 881–882, 885–889, 898–907, 914, 920, 923–924, 928, 931
   update_stats.py104991%57, 103, 105, 114, 116, 125, 127, 148, 179
src/otaclient/boot_control
   __init__.py40100% 
   _common.py24911255%70–71, 92–94, 110–111, 131–132, 151–152, 171–172, 191–192, 214–216, 231–232, 256–262, 283, 291, 309, 317, 336–337, 340–341, 364, 366–375, 377–386, 388–390, 409, 412, 420, 428, 444–446, 448–453, 546, 551, 556, 669, 673–674, 677, 684, 686–687, 713–714, 716–719, 724, 730–731, 734–735, 737, 744–745, 756–762, 772–774, 778–779, 782–783, 786, 792
   _firmware_package.py942276%83, 87, 137, 181, 187, 210–211, 214–219, 221–222, 225–230, 232
   _grub.py41812869%216, 264–267, 273–277, 314–315, 322–327, 330–336, 339, 342–343, 348, 350–352, 361–367, 369–370, 372–374, 383–385, 387–389, 468–469, 473–474, 526, 532, 558, 580, 584–585, 600–602, 626–629, 641, 645–647, 649–651, 710–713, 738–741, 764–767, 779–780, 783–784, 819, 825, 845–846, 848, 860, 863, 866, 869, 873–875, 893–896, 924–927, 932–940, 945–953
   _jetson_cboot.py2632630%20, 22–25, 27–29, 35–39, 41–42, 58–59, 61, 63–64, 70, 74, 133, 136, 138–139, 142, 149–150, 158–159, 162, 168–169, 177, 186–190, 192, 198, 201–202, 208, 211–212, 217–218, 220, 226–227, 230–231, 234–236, 238, 244, 249–251, 253–255, 260, 262–265, 267–268, 277–278, 281–282, 287–288, 291–295, 298–299, 304–305, 308, 311–315, 320–323, 326, 329–330, 333, 336–337, 340, 344–349, 353–354, 358, 361–362, 365, 368–371, 373, 376–377, 381, 384, 387–390, 392, 399, 403–404, 407–408, 414–415, 421, 423–424, 428, 430, 432–434, 437, 441, 444, 447–448, 450, 453, 461–462, 469, 479, 482, 490–491, 496–499, 501, 508, 510–512, 518–519, 523–524, 527, 531, 534, 536, 543–547, 549, 561–564, 567, 570, 572, 579, 583–584, 586–587, 589–591, 593, 595, 598, 601, 604, 606–607, 610–614, 618–620, 622, 630–634, 636, 639, 643, 646, 657–658, 663, 673, 676–684, 688–697, 701–710, 714, 716–718, 720–721, 723–724
   _jetson_common.py1734573%133, 141, 289–292, 295, 312, 320, 355, 360–365, 383, 409–410, 412–414, 418–421, 423–424, 426–430, 432, 439–440, 443–444, 454, 457–458, 461, 463, 507–508
   _jetson_uefi.py39827131%123–125, 130–131, 150–152, 157–160, 327, 445, 447–450, 454, 458–459, 461–469, 471, 483–484, 487–488, 491–492, 495–497, 501–502, 507–509, 513, 517–518, 521–522, 525–526, 530, 533–534, 536, 541–542, 546, 549–550, 555, 559–560, 563, 567–569, 571, 575–578, 580–581, 603–604, 608–609, 611, 615, 619–620, 623–624, 631, 634–636, 639, 641–642, 647–648, 651–654, 656–657, 662, 664–665, 673, 676–679, 681–682, 684, 688–689, 693, 701–705, 708–709, 711, 714–718, 721, 724–728, 732–733, 736–741, 744–745, 748–751, 753–754, 761–762, 772–775, 778, 781–784, 787–791, 794–795, 798, 801–804, 807, 809, 814–815, 818, 821–824, 826, 832, 837–838, 857–858, 861, 869–870, 877, 887, 890, 897–898, 903–906, 914–917, 925–926, 938–941, 943, 946, 949, 957, 968–970, 972–974, 976–980, 985–986, 988, 1001, 1005, 1008, 1018, 1023, 1031–1032, 1035, 1039, 1041–1043, 1049–1050, 1055, 1063–1070, 1075–1083, 1088–1096, 1102–1104
   _rpi_boot.py28713453%54, 57, 121–122, 126, 134–137, 151–154, 161–162, 164–165, 170–171, 174–175, 184–185, 223, 229–233, 236, 254–256, 260–262, 267–269, 273–275, 285–286, 289, 292, 294–295, 297–298, 300–302, 308, 311–312, 322–325, 333–337, 339, 341–342, 347–348, 355–361, 392, 394–397, 407–410, 414–415, 417–421, 449–452, 471–474, 479, 482, 500–503, 508–516, 521–529, 546–549, 555–557, 560, 563
   configs.py550100% 
   protocol.py40100% 
   selecter.py412929%45–47, 50–51, 55–56, 59–61, 64, 66, 70, 78–80, 82–83, 85–86, 90, 92, 94–95, 97, 99–100, 102, 104
src/otaclient/configs
   __init__.py50100% 
   _cfg_configurable.py510100% 
   _cfg_consts.py44197%93
   _common.py80100% 
   _ecu_info.py57492%55, 60–61, 108
   _proxy_info.py51296%88, 90
   cfg.py19384%54–56
src/otaclient/create_standby
   __init__.py12558%29–31, 33, 35
   common.py2244480%62, 65–66, 70–72, 74, 78–79, 81, 127, 175–177, 179–181, 183, 186–189, 193, 204, 278–279, 281–286, 298, 335, 363, 366–368, 384–385, 399, 403, 425–426
   interface.py50100% 
   rebuild_mode.py97990%93–95, 107–112
src/otaclient_api/v2
   api_caller.py39684%45–47, 83–85
   api_stub.py170100% 
   types.py2562391%86, 89–92, 131, 209–210, 212, 259, 262–263, 506–508, 512–513, 515, 518–519, 522–523, 586
src/otaclient_common
   __init__.py34876%42–44, 61, 63, 69, 76–77
   _io.py64198%41
   common.py1061189%44, 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.py3984887%87, 165, 172, 184–186, 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, 707, 713, 760–763, 765, 803–805
   retry_task_map.py105595%158–159, 161, 181–182
   typing.py25388%69–70, 72
TOTAL6434167274% 

Tests Skipped Failures Errors Time
227 0 💤 0 ❌ 0 🔥 13m 5s ⏱️

Please sign in to comment.