From 8152e87a334257735c088a94d474952c7e111737 Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Thu, 29 Aug 2024 14:18:45 +0200 Subject: [PATCH 1/6] Enable downloading from internal report and artifact events --- cli/pyproject.toml | 3 +- cli/requirements.txt | 1 - cli/src/etos_client/downloader/downloader.py | 82 +++++--------------- cli/src/etos_client/sse/protocol.py | 28 +++++++ cli/src/etos_client/test_run/test_run.py | 45 +++-------- source/sse.rst | 8 +- 6 files changed, 63 insertions(+), 104 deletions(-) diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 417a46d4..554cc243 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -17,7 +17,6 @@ dependencies = [ "etos_lib==4.0.0", "docopt~=0.6", "pydantic~=2.6", - "jmespath~=1.0" ] [project.optional-dependencies] @@ -72,4 +71,4 @@ exclude = [".tox", "build", "dist", ".eggs", "docs/conf.py"] [tool.setuptools_scm] version_scheme = "setup:version_scheme" local_scheme = "setup:local_scheme" -root = ".." \ No newline at end of file +root = ".." diff --git a/cli/requirements.txt b/cli/requirements.txt index 680025af..ff0df60f 100644 --- a/cli/requirements.txt +++ b/cli/requirements.txt @@ -18,4 +18,3 @@ etos_lib==4.0.0 docopt~=0.6 pydantic~=2.6 -jmespath~=1.0 diff --git a/cli/src/etos_client/downloader/downloader.py b/cli/src/etos_client/downloader/downloader.py index 7d0458e0..c36beb2b 100644 --- a/cli/src/etos_client/downloader/downloader.py +++ b/cli/src/etos_client/downloader/downloader.py @@ -29,9 +29,9 @@ from urllib3.util import Retry import requests from requests.exceptions import HTTPError -import jmespath from etos_lib.lib.http import Http +from etos_client.sse.protocol import Report, Artifact HTTP_RETRY_PARAMETERS = Retry( @@ -46,27 +46,12 @@ ) -class Filter(BaseModel): - """Filter for logs and artifacts.""" - - source: str - jmespath: str - - class Downloadable(BaseModel): """Represent a downloadable file.""" url: str - name: list[Filter] - path: Path = Path.cwd() - - -class Directory(BaseModel): - """Represent a directory of files.""" - name: str - logs: list[Downloadable] - artifacts: list[Downloadable] + path: Path = Path.cwd() class Downloader(Thread): # pylint:disable=too-many-instance-attributes @@ -104,31 +89,9 @@ def __download(self, item: Downloadable) -> None: self.logger.critical("Failed to download %r", item) return - def __apply_filter(self, item: Downloadable, response: requests.Response) -> str: - """Apply name filter to downloaded item.""" - name = [] - try: - response_json = ( - response.json() - if response.headers.get("content-type") == "application/json" - else None - ) - except JSONDecodeError: - # If a file ends in .json it will be interpreted as json, but it might not be valid. - response_json = None - sources = {"response": response_json, "headers": response.headers} - - for name_filter in item.name: - source = sources.get(name_filter.source) - if source is None: - raise ValueError(f"Filter source {name_filter.source} not found.") - name.append(jmespath.search(name_filter.jmespath, source)) - return "/".join(name) - def __save_file(self, item: Downloadable, response: requests.Response) -> None: """Save downloaded file data to disk.""" - download_name = self.__apply_filter(item, response) - download_path = item.path.joinpath(download_name) + download_path = item.path.joinpath(item.name) if not download_path.parent.exists(): with self.__lock: download_path.parent.mkdir(exist_ok=True, parents=True) @@ -221,26 +184,19 @@ def __queue_download(self, item: Downloadable) -> None: self.__download_queue.put_nowait(item) self.__queued.append(item.url) - def __download_artifacts(self, artifacts: list[Downloadable], path: Path) -> None: - """Download artifacts to an artifact path.""" - for artifact in artifacts: - artifact.path = path - self.__queue_download(artifact) - - def __download_logs(self, logs: list[Downloadable], path: Path) -> None: - """Download logs from test suites to report path.""" - for log in logs: - log.path = path - self.__queue_download(log) - - def download_files(self, directory: Directory): - """Download logs and artifacts from a directory.""" - reports = self.__report_dir.relative_to(Path.cwd()) - artifacts = self.__artifact_dir.relative_to(Path.cwd()).joinpath(directory.name) - self.__download_logs(directory.logs, reports) - self.__download_artifacts(directory.artifacts, artifacts) - - def download_directories(self, directories: dict): - """Download logs and artifacts from directories.""" - for name, directory in directories.items(): - self.download_files(Directory(name=name, **directory)) + def download_report(self, report: Report): + """Download an report to the report directory.""" + reports = self.__report_dir.relative_to(Path.cwd()).joinpath(report.file.get("directory", "")) + self.__queue_download(Downloadable(url=report.file.get("url"), name=report.file.get("name"), path=reports)) + + def download_artifact(self, artifact: Artifact): + """Download an artifact to the artifact directory.""" + artifacts = self.__artifact_dir.relative_to(Path.cwd()).joinpath(artifact.file.get("directory", "")) + self.__queue_download(Downloadable(url=artifact.file.get("url"), name=artifact.file.get("name"), path=artifacts)) + + def download(self, file: Union[Report, Artifact]): + """Download a file from either a Report or Artifact event.""" + if isinstance(file, Report): + self.download_report(file) + elif isinstance(file, Artifact): + self.download_artifact(file) diff --git a/cli/src/etos_client/sse/protocol.py b/cli/src/etos_client/sse/protocol.py index ed9b9ef7..1ca6a98a 100644 --- a/cli/src/etos_client/sse/protocol.py +++ b/cli/src/etos_client/sse/protocol.py @@ -98,6 +98,34 @@ def __str__(self): return self.message.get("message") +class Report(UserEvent): + """An ETOS test case report file event.""" + + def __init__(self, event: dict) -> None: + """Initialize a report by loading an expected json string.""" + super().__init__(event) + + try: + self.file = json.loads(self.data) + except json.JSONDecodeError: + print(self.data) + raise + + +class Artifact(UserEvent): + """An ETOS test case artifact file event.""" + + def __init__(self, event: dict) -> None: + """Initialize a artifact by loading an expected json string.""" + super().__init__(event) + + try: + self.file = json.loads(self.data) + except json.JSONDecodeError: + print(self.data) + raise + + def parse(event: dict) -> Event: """Parse an event dict and return a corresponding Event class.""" for name, obj in inspect.getmembers(sys.modules[__name__]): diff --git a/cli/src/etos_client/test_run/test_run.py b/cli/src/etos_client/test_run/test_run.py index bcdc7a45..d9bc29b9 100644 --- a/cli/src/etos_client/test_run/test_run.py +++ b/cli/src/etos_client/test_run/test_run.py @@ -19,28 +19,13 @@ import time from typing import Iterator -from urllib3.util import Retry -from etos_lib.lib.http import Http -from requests.exceptions import HTTPError - from etos_client.downloader import Downloader from etos_client.etos import ETOS from etos_client.etos.schema import ResponseSchema from etos_client.events.collector import Collector from etos_client.events.events import Events from etos_client.sse.client import SSEClient -from etos_client.sse.protocol import Message, Ping - - -HTTP_RETRY_PARAMETERS = Retry( - total=None, - read=0, - connect=2, - status=2, - backoff_factor=1, - other=0, - status_forcelist=list(Retry.RETRY_AFTER_STATUS_CODES), -) +from etos_client.sse.protocol import Message, UserEvent, Report, Artifact class TestRun: @@ -58,7 +43,6 @@ def __init__(self, collector: Collector, downloader: Downloader) -> None: """Initialize.""" assert downloader.started, "Downloader must be started before it can be used in TestRun" - self.__http = Http(retry=HTTP_RETRY_PARAMETERS) self.__collector = collector self.__downloader = downloader @@ -88,7 +72,9 @@ def track(self, etos: ETOS, timeout: int) -> Events: self.__log_debug_information(etos.response) last_log = time.time() timer = None - for _ in self.__log_until_eof(etos, end): + for event in self.__log_until_eof(etos, end): + if isinstance(event, (Report, Artifact)): + self.__downloader.download(event) if last_log + self.log_interval >= time.time(): events = self.__collector.collect_activity(etos.response.tercc) self.__status(events) @@ -96,7 +82,6 @@ def track(self, etos: ETOS, timeout: int) -> Events: last_log = time.time() if events.activity.finished and timer is None: timer = time.time() + 300 # 5 minutes - self.__download(etos) if timer and time.time() >= timer: self.logger.warning("ETOS finished, but did not shut down the log server.") self.logger.warning( @@ -105,7 +90,6 @@ def track(self, etos: ETOS, timeout: int) -> Events: ) break self.__wait(etos, end) - self.__download(etos) events = self.__collector.collect(etos.response.tercc) self.__announce(events) return events @@ -139,17 +123,6 @@ def __announce(self, events: Events) -> None: "Downloaded a total of %d logs from test runners", len(self.__downloader.downloads) ) - def __download(self, etos: ETOS) -> None: - """Download logs and artifacts.""" - response = self.__http.get(f"{etos.cluster}/logarea/v1alpha/logarea/{etos.response.tercc}") - try: - response.raise_for_status() - except HTTPError as error: - self.logger.warning("Got an HTTP error: %r when listing logs from log area", error) - return - directories = response.json() - self.__downloader.download_directories(directories) - def __log(self, message: Message) -> None: """Log a message from the ETOS log API.""" logger = getattr(self.remote_logger, message.level) @@ -161,13 +134,13 @@ def __log(self, message: Message) -> None: }, ) - def __log_until_eof(self, etos: ETOS, endtime: int) -> Iterator[Ping]: + def __log_until_eof(self, etos: ETOS, endtime: int) -> Iterator[UserEvent]: """Log from the ETOS log API until finished.""" client = SSEClient(etos) - for log_message in client.event_stream(): + for event in client.event_stream(): if time.time() >= endtime: raise TimeoutError("Timed out!") - if isinstance(log_message, Message): - self.__log(log_message) + if isinstance(event, Message): + self.__log(event) continue - yield log_message + yield event diff --git a/source/sse.rst b/source/sse.rst index 7d070af3..745c56e4 100644 --- a/source/sse.rst +++ b/source/sse.rst @@ -36,8 +36,12 @@ ETOS communicates to clients using SSE (server sent events). For now ETOS only s - Implemented * - Artifact - An artifact to download - - {id: 1, event: Artifact, data: "{'url': 'http://download.me'}"} - - Suggested + - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0'}"} + - Implemented + * - Report + - A report to download + - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt'}"} + - Implemented * - TestCase - A test case execution - {id: 1, event: TestCase, data: "{'id': 'uuid', 'triggered': True, 'started': True, 'finished': False}"} From 33ee3a424131054f2838a1bd7be7d11606f2fc3d Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Fri, 30 Aug 2024 08:11:58 +0200 Subject: [PATCH 2/6] Add checksum verification --- cli/src/etos_client/downloader/downloader.py | 37 ++++++++++++++++++-- source/sse.rst | 4 +-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/cli/src/etos_client/downloader/downloader.py b/cli/src/etos_client/downloader/downloader.py index c36beb2b..482cd4ac 100644 --- a/cli/src/etos_client/downloader/downloader.py +++ b/cli/src/etos_client/downloader/downloader.py @@ -17,6 +17,7 @@ import logging import time import traceback +import hashlib from pathlib import Path from typing import Union from threading import Thread, Lock @@ -51,6 +52,7 @@ class Downloadable(BaseModel): url: str name: str + checksums: dict[str, str] path: Path = Path.cwd() @@ -105,6 +107,23 @@ def __save_file(self, item: Downloadable, response: requests.Response) -> None: with open(download_path, "wb+") as report: for chunk in response: report.write(chunk) + if not self.__verify(item, download_path): + self.logger.error("Checksum verification failed on %r", item) + + def __verify(self, item: Downloadable, path: Path) -> bool: + """Verify the checksum of the downloaded item. + + Path is the real path to the saved file. + """ + with path.open("rb") as report: + md5_checksum = hashlib.md5(report.read()).hexdigest() + if md5_checksum != item.checksums.get("md5"): + self.logger.error( + "MD5 checksum of file is not as expected. Downloaded: %r , Expected: %r", + md5_checksum, item.checksums.get("md5") + ) + return False + return True def __download_ok(self, response: requests.Response) -> bool: """Check download response and log response details.""" @@ -187,12 +206,26 @@ def __queue_download(self, item: Downloadable) -> None: def download_report(self, report: Report): """Download an report to the report directory.""" reports = self.__report_dir.relative_to(Path.cwd()).joinpath(report.file.get("directory", "")) - self.__queue_download(Downloadable(url=report.file.get("url"), name=report.file.get("name"), path=reports)) + self.__queue_download( + Downloadable( + url=report.file.get("url"), + name=report.file.get("name"), + checksums=report.file.get("checksums"), + path=reports, + ) + ) def download_artifact(self, artifact: Artifact): """Download an artifact to the artifact directory.""" artifacts = self.__artifact_dir.relative_to(Path.cwd()).joinpath(artifact.file.get("directory", "")) - self.__queue_download(Downloadable(url=artifact.file.get("url"), name=artifact.file.get("name"), path=artifacts)) + self.__queue_download( + Downloadable( + url=artifact.file.get("url"), + name=artifact.file.get("name"), + checksums=artifact.file.get("checksums"), + path=artifacts, + ) + ) def download(self, file: Union[Report, Artifact]): """Download a file from either a Report or Artifact event.""" diff --git a/source/sse.rst b/source/sse.rst index 745c56e4..b919842f 100644 --- a/source/sse.rst +++ b/source/sse.rst @@ -36,11 +36,11 @@ ETOS communicates to clients using SSE (server sent events). For now ETOS only s - Implemented * - Artifact - An artifact to download - - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0'}"} + - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0', 'checksums': {'md5': '12345'}}"} - Implemented * - Report - A report to download - - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt'}"} + - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'checksums': {'md5': '12345'}}"} - Implemented * - TestCase - A test case execution From 994db4cf12ba8b7857e3e01438b57be9d1794f1d Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Fri, 30 Aug 2024 08:50:03 +0200 Subject: [PATCH 3/6] Add all available integrity protection algorithms supported by eiffel --- cli/src/etos_client/downloader/downloader.py | 34 +++++++++++++++++--- source/sse.rst | 4 +-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/cli/src/etos_client/downloader/downloader.py b/cli/src/etos_client/downloader/downloader.py index 482cd4ac..6e5ca246 100644 --- a/cli/src/etos_client/downloader/downloader.py +++ b/cli/src/etos_client/downloader/downloader.py @@ -115,14 +115,40 @@ def __verify(self, item: Downloadable, path: Path) -> bool: Path is the real path to the saved file. """ + translation_table = { + "SHA-224": "sha224", + "SHA-256": "sha256", + "SHA-384": "sha384", + "SHA-512": "sha512", + "SHA-512/224": "sha512_224", + "SHA-512/256": "sha512_256" + } + hash = None + expected_digest = None + for nist_scheme, python_scheme in translation_table.items(): + if item.checksums.get(nist_scheme) is not None: + hash = hashlib.new(python_scheme) + expected_digest = item.checksums.get(nist_scheme) + break + + if hash is None: + self.logger.info("No digest set on file, won't check integrity of %r", item) + return True + self.logger.debug("Checking integrity of downloaded file using %r", hash.name) + with path.open("rb") as report: - md5_checksum = hashlib.md5(report.read()).hexdigest() - if md5_checksum != item.checksums.get("md5"): + hash.update(report.read()) + digest = hash.hexdigest() + self.logger.debug("Expecting digest %r", expected_digest) + self.logger.debug("Verify digest %r", digest) + + if digest != expected_digest: self.logger.error( - "MD5 checksum of file is not as expected. Downloaded: %r , Expected: %r", - md5_checksum, item.checksums.get("md5") + "%s checksum of file is not as expected. Downloaded: %r , Expected: %r", + hash.name, digest, expected_digest ) return False + self.logger.debug("Integrity verification successful") return True def __download_ok(self, response: requests.Response) -> bool: diff --git a/source/sse.rst b/source/sse.rst index b919842f..d724c322 100644 --- a/source/sse.rst +++ b/source/sse.rst @@ -36,11 +36,11 @@ ETOS communicates to clients using SSE (server sent events). For now ETOS only s - Implemented * - Artifact - An artifact to download - - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0', 'checksums': {'md5': '12345'}}"} + - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} - Implemented * - Report - A report to download - - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'checksums': {'md5': '12345'}}"} + - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} - Implemented * - TestCase - A test case execution From 0e0e52d1336b49d4ffab4fc95e10c106c975363c Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Thu, 12 Sep 2024 08:08:01 +0200 Subject: [PATCH 4/6] Clarify examples --- source/sse.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/sse.rst b/source/sse.rst index d724c322..663939ad 100644 --- a/source/sse.rst +++ b/source/sse.rst @@ -36,11 +36,11 @@ ETOS communicates to clients using SSE (server sent events). For now ETOS only s - Implemented * - Artifact - An artifact to download - - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} + - {id: 1, event: Artifact, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'directory': 'MyTest_SubSuite_0', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} - Implemented * - Report - A report to download - - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} + - {id: 1, event: Report, data: "{'url': 'http://download.me', 'name': 'filename.txt', 'checksums': {'SHA-224': '', 'SHA-256': '', 'SHA-384': '', 'SHA-512': '', 'SHA-512/224': '', 'SHA-512/256': ''}}"} - Implemented * - TestCase - A test case execution From 2a0ec0cb213ca5afafbf98ba4184508cd9b13eab Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Thu, 12 Sep 2024 08:08:39 +0200 Subject: [PATCH 5/6] Fail downloader if file intrgrity could not be verified --- cli/src/etos_client/downloader/downloader.py | 51 +++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/cli/src/etos_client/downloader/downloader.py b/cli/src/etos_client/downloader/downloader.py index 6e5ca246..784aa86c 100644 --- a/cli/src/etos_client/downloader/downloader.py +++ b/cli/src/etos_client/downloader/downloader.py @@ -56,6 +56,28 @@ class Downloadable(BaseModel): path: Path = Path.cwd() +class IntegrityError(Exception): + """Integrity verification error.""" + + def __init__(self, url: str, hash_type: str, downloaded_hash: str, expected_hash: str): + """Initialize.""" + self.url = url + self.hash_type = hash_type + self.downloaded_hash = downloaded_hash + self.expected_hash = expected_hash + + def __str__(self): + """String representation of this exception.""" + return ( + f"Failed integrity protection on {self.url!r}, using hash {self.hash_type!r}. " + f"Expected: {self.expected_hash!r} but it was {self.downloaded_hash!r}" + ) + + def __repr__(self): + """String representation of this exception.""" + return self.__str__() + + class Downloader(Thread): # pylint:disable=too-many-instance-attributes """Log downloader for ETOS client.""" @@ -107,10 +129,13 @@ def __save_file(self, item: Downloadable, response: requests.Response) -> None: with open(download_path, "wb+") as report: for chunk in response: report.write(chunk) - if not self.__verify(item, download_path): - self.logger.error("Checksum verification failed on %r", item) + try: + self.__verify(item, download_path) + except IntegrityError: + download_path.unlink() + raise - def __verify(self, item: Downloadable, path: Path) -> bool: + def __verify(self, item: Downloadable, path: Path): """Verify the checksum of the downloaded item. Path is the real path to the saved file. @@ -123,33 +148,33 @@ def __verify(self, item: Downloadable, path: Path) -> bool: "SHA-512/224": "sha512_224", "SHA-512/256": "sha512_256" } - hash = None + hashfunc = None expected_digest = None for nist_scheme, python_scheme in translation_table.items(): if item.checksums.get(nist_scheme) is not None: - hash = hashlib.new(python_scheme) + hashfunc = hashlib.new(python_scheme) expected_digest = item.checksums.get(nist_scheme) break - if hash is None: + if hashfunc is None or expected_digest is None: self.logger.info("No digest set on file, won't check integrity of %r", item) - return True - self.logger.debug("Checking integrity of downloaded file using %r", hash.name) + return + self.logger.debug("Checking integrity of downloaded file using %r", hashfunc.name) with path.open("rb") as report: - hash.update(report.read()) - digest = hash.hexdigest() + hashfunc.update(report.read()) + digest = hashfunc.hexdigest() self.logger.debug("Expecting digest %r", expected_digest) self.logger.debug("Verify digest %r", digest) if digest != expected_digest: self.logger.error( "%s checksum of file is not as expected. Downloaded: %r , Expected: %r", - hash.name, digest, expected_digest + hashfunc.name, digest, expected_digest ) - return False + raise IntegrityError(item.url, hashfunc.name, digest, expected_digest) self.logger.debug("Integrity verification successful") - return True + return def __download_ok(self, response: requests.Response) -> bool: """Check download response and log response details.""" From 505b524b9de3fa7021d34f65d2265c0b634df22d Mon Sep 17 00:00:00 2001 From: Tobias Persson Date: Thu, 12 Sep 2024 08:11:14 +0200 Subject: [PATCH 6/6] Fix failures when running tox --- cli/src/etos_client/downloader/downloader.py | 18 ++++++++++++------ .../etos_client/test_results/test_results.py | 13 ++----------- cli/tox.ini | 2 +- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/cli/src/etos_client/downloader/downloader.py b/cli/src/etos_client/downloader/downloader.py index 784aa86c..a6368593 100644 --- a/cli/src/etos_client/downloader/downloader.py +++ b/cli/src/etos_client/downloader/downloader.py @@ -67,14 +67,14 @@ def __init__(self, url: str, hash_type: str, downloaded_hash: str, expected_hash self.expected_hash = expected_hash def __str__(self): - """String representation of this exception.""" + """Exception as string.""" return ( f"Failed integrity protection on {self.url!r}, using hash {self.hash_type!r}. " f"Expected: {self.expected_hash!r} but it was {self.downloaded_hash!r}" ) def __repr__(self): - """String representation of this exception.""" + """Exception as string.""" return self.__str__() @@ -146,7 +146,7 @@ def __verify(self, item: Downloadable, path: Path): "SHA-384": "sha384", "SHA-512": "sha512", "SHA-512/224": "sha512_224", - "SHA-512/256": "sha512_256" + "SHA-512/256": "sha512_256", } hashfunc = None expected_digest = None @@ -170,7 +170,9 @@ def __verify(self, item: Downloadable, path: Path): if digest != expected_digest: self.logger.error( "%s checksum of file is not as expected. Downloaded: %r , Expected: %r", - hashfunc.name, digest, expected_digest + hashfunc.name, + digest, + expected_digest, ) raise IntegrityError(item.url, hashfunc.name, digest, expected_digest) self.logger.debug("Integrity verification successful") @@ -256,7 +258,9 @@ def __queue_download(self, item: Downloadable) -> None: def download_report(self, report: Report): """Download an report to the report directory.""" - reports = self.__report_dir.relative_to(Path.cwd()).joinpath(report.file.get("directory", "")) + reports = self.__report_dir.relative_to(Path.cwd()).joinpath( + report.file.get("directory", "") + ) self.__queue_download( Downloadable( url=report.file.get("url"), @@ -268,7 +272,9 @@ def download_report(self, report: Report): def download_artifact(self, artifact: Artifact): """Download an artifact to the artifact directory.""" - artifacts = self.__artifact_dir.relative_to(Path.cwd()).joinpath(artifact.file.get("directory", "")) + artifacts = self.__artifact_dir.relative_to(Path.cwd()).joinpath( + artifact.file.get("directory", "") + ) self.__queue_download( Downloadable( url=artifact.file.get("url"), diff --git a/cli/src/etos_client/test_results/test_results.py b/cli/src/etos_client/test_results/test_results.py index 44ff2c86..ed516334 100644 --- a/cli/src/etos_client/test_results/test_results.py +++ b/cli/src/etos_client/test_results/test_results.py @@ -17,7 +17,7 @@ import logging from typing import Optional -from etos_client.events.events import Events, SubSuite, TestSuite +from etos_client.events.events import Events, TestSuite # pylint:disable=too-few-public-methods @@ -35,15 +35,6 @@ def __has_failed(self, test_suites: list[TestSuite]) -> bool: return True return False - def __count_sub_suite_failures(self, test_suites: list[SubSuite]) -> int: - """Count the number of sub suite failures in a list of sub suites.""" - failures = 0 - for sub_suite in test_suites: - outcome = sub_suite.finished["data"]["testSuiteOutcome"] - if outcome["verdict"] != "PASSED": - failures += 1 - return failures - def __fail_messages(self, test_suites: list[TestSuite]) -> list[str]: """Build a fail message from main suites errors.""" messages = [] @@ -64,7 +55,7 @@ def __test_result(self, test_suites: list[TestSuite]) -> tuple[bool, str]: for message in messages[:-1]: self.logger.error(message) return False, messages[-1] - return False, f"Test case failures during test suite execution" + return False, "Test case failures during test suite execution" def get_results(self, events: Events) -> tuple[Optional[bool], Optional[str]]: """Get results from an ETOS testrun.""" diff --git a/cli/tox.ini b/cli/tox.ini index 99bd14b2..8bce2717 100644 --- a/cli/tox.ini +++ b/cli/tox.ini @@ -17,4 +17,4 @@ commands = deps = pydocstyle commands = - pydocstyle . + pydocstyle src/etos_client