pysdccc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysdccc/__init__.py +25 -0
- pysdccc/_cli.py +145 -0
- pysdccc/_common.py +57 -0
- pysdccc/_download.py +119 -0
- pysdccc/_result_parser.py +130 -0
- pysdccc/_runner.py +311 -0
- pysdccc/py.typed +0 -0
- pysdccc-0.1.0.dist-info/METADATA +245 -0
- pysdccc-0.1.0.dist-info/RECORD +12 -0
- pysdccc-0.1.0.dist-info/WHEEL +4 -0
- pysdccc-0.1.0.dist-info/entry_points.txt +3 -0
- pysdccc-0.1.0.dist-info/licenses/LICENSE +21 -0
pysdccc/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Python wrapper to the SDCcc tool for testing SDC devices."""
|
|
2
|
+
|
|
3
|
+
from pysdccc._common import DEFAULT_STORAGE_DIRECTORY
|
|
4
|
+
from pysdccc._download import adownload, ais_downloaded, download, is_downloaded
|
|
5
|
+
from pysdccc._result_parser import TestCase, TestSuite
|
|
6
|
+
from pysdccc._runner import (
|
|
7
|
+
SdcccRunner,
|
|
8
|
+
SdcccRunnerAsync,
|
|
9
|
+
check_requirements,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = '0.1.0'
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'DEFAULT_STORAGE_DIRECTORY',
|
|
16
|
+
'SdcccRunner',
|
|
17
|
+
'SdcccRunnerAsync',
|
|
18
|
+
'TestCase',
|
|
19
|
+
'TestSuite',
|
|
20
|
+
'adownload',
|
|
21
|
+
'ais_downloaded',
|
|
22
|
+
'check_requirements',
|
|
23
|
+
'download',
|
|
24
|
+
'is_downloaded',
|
|
25
|
+
]
|
pysdccc/_cli.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Command line interface."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import io
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from pysdccc import _common, _download
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import click
|
|
17
|
+
except ImportError as import_error:
|
|
18
|
+
msg = 'Cli not installed. Please install "pysdccc[cli]" package.'
|
|
19
|
+
raise ImportError(msg) from import_error
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UrlType(click.ParamType):
|
|
23
|
+
"""Url type."""
|
|
24
|
+
|
|
25
|
+
name = 'url'
|
|
26
|
+
|
|
27
|
+
def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> httpx.URL:
|
|
28
|
+
"""Convert value to url."""
|
|
29
|
+
try:
|
|
30
|
+
return httpx.URL(value)
|
|
31
|
+
except Exception as e: # noqa: BLE001
|
|
32
|
+
self.fail(f'{value!r} is not a valid url: {e}', param, ctx)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
URL = UrlType()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProxyType(click.ParamType):
|
|
39
|
+
"""Proxy type."""
|
|
40
|
+
|
|
41
|
+
name = 'proxy'
|
|
42
|
+
|
|
43
|
+
def convert(self, value: str, param: click.Parameter | None, ctx: click.Context | None) -> httpx.Proxy | None:
|
|
44
|
+
"""Convert value to proxy."""
|
|
45
|
+
try:
|
|
46
|
+
return httpx.Proxy(value)
|
|
47
|
+
except Exception as e: # noqa: BLE001
|
|
48
|
+
self.fail(f'{value!r} is not a valid proxy: {e}', param, ctx)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PROXY = ProxyType()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _download_to_stream(url: httpx.URL, stream: io.IOBase, proxy: httpx.Proxy | None = None) -> None:
|
|
55
|
+
with _download.open_download_stream(url, proxy) as response:
|
|
56
|
+
total = response.headers.get('Content-Length')
|
|
57
|
+
with click.progressbar(
|
|
58
|
+
response.iter_bytes(),
|
|
59
|
+
length=int(total) if total is not None else None,
|
|
60
|
+
label='Downloading',
|
|
61
|
+
show_eta=True,
|
|
62
|
+
width=0,
|
|
63
|
+
) as progress:
|
|
64
|
+
num_bytes_downloaded = response.num_bytes_downloaded
|
|
65
|
+
for chunk in progress:
|
|
66
|
+
stream.write(chunk)
|
|
67
|
+
progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
|
|
68
|
+
num_bytes_downloaded = response.num_bytes_downloaded
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def download(url: httpx.URL, output: pathlib.Path, proxy: httpx.Proxy | None = None):
|
|
72
|
+
"""Download and extract the executable from an url."""
|
|
73
|
+
click.echo(f'Downloading SDCcc from {url}.')
|
|
74
|
+
with tempfile.NamedTemporaryFile('wb', suffix='.zip', delete=False) as temporary_file:
|
|
75
|
+
_download_to_stream(url, temporary_file, proxy=proxy) # type: ignore[arg-type]
|
|
76
|
+
click.echo(f'Extracting SDCcc to {output}.')
|
|
77
|
+
with (
|
|
78
|
+
zipfile.ZipFile(temporary_file.name) as f,
|
|
79
|
+
click.progressbar(f.infolist(), label='Extracting', width=0) as progress,
|
|
80
|
+
):
|
|
81
|
+
for member in progress:
|
|
82
|
+
f.extract(member, output)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@click.group(help='Manage SDCcc installation.')
|
|
86
|
+
@click.version_option(message='%(version)s')
|
|
87
|
+
def cli():
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@click.command(
|
|
92
|
+
short_help='Install the SDCcc executable from the specified URL. Releases can be found at https://github.com/Draegerwerk/SDCcc/releases.',
|
|
93
|
+
)
|
|
94
|
+
@click.argument('url', type=URL)
|
|
95
|
+
@click.option('--proxy', help='Proxy server to use for the download.', type=PROXY)
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def install(ctx: click.Context, url: httpx.URL, proxy: httpx.Proxy | None):
|
|
98
|
+
"""Download the specified version from the default URL to a temporary directory.
|
|
99
|
+
|
|
100
|
+
This function downloads the SDCcc executable from the given URL to a temporary directory.
|
|
101
|
+
It optionally uses a proxy and a specified timeout for the download operation.
|
|
102
|
+
The downloaded file is extracted to a local path determined by the version string in the URL.
|
|
103
|
+
|
|
104
|
+
:param ctx: context from click
|
|
105
|
+
:param url: The parsed URL from which to download the executable.
|
|
106
|
+
:param proxy: Optional proxy to be used for the download.
|
|
107
|
+
"""
|
|
108
|
+
ctx.invoke(uninstall)
|
|
109
|
+
try:
|
|
110
|
+
download(url, _common.DEFAULT_STORAGE_DIRECTORY, proxy)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
msg = f'Failed to download and extract SDCcc from {url}: {e}.'
|
|
113
|
+
raise click.ClickException(msg) from e
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@click.command(short_help='Uninstall the SDCcc executable by removing the directory.')
|
|
117
|
+
def uninstall():
|
|
118
|
+
"""Uninstall the SDCcc executable.
|
|
119
|
+
|
|
120
|
+
This function removes the SDCcc executable from the directory.
|
|
121
|
+
"""
|
|
122
|
+
import shutil
|
|
123
|
+
|
|
124
|
+
with contextlib.suppress(FileNotFoundError):
|
|
125
|
+
shutil.rmtree(_common.DEFAULT_STORAGE_DIRECTORY)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
cli.add_command(install)
|
|
129
|
+
cli.add_command(uninstall)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def sdccc():
|
|
133
|
+
try:
|
|
134
|
+
sdccc_exe = _common.get_exe_path(_common.DEFAULT_STORAGE_DIRECTORY)
|
|
135
|
+
subprocess.run( # noqa: S603
|
|
136
|
+
[sdccc_exe, *sys.argv[1:]],
|
|
137
|
+
check=True,
|
|
138
|
+
cwd=sdccc_exe.parent,
|
|
139
|
+
)
|
|
140
|
+
except FileNotFoundError as e:
|
|
141
|
+
click.ClickException("SDCcc is not installed. Please install using 'pysdccc install <url>'.").show()
|
|
142
|
+
raise SystemExit(1) from e
|
|
143
|
+
except subprocess.CalledProcessError as e:
|
|
144
|
+
click.echo(e, err=True)
|
|
145
|
+
raise SystemExit(e.returncode) from e
|
pysdccc/_common.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Contains commonly used constants and functions."""
|
|
2
|
+
|
|
3
|
+
import locale
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
DEFAULT_STORAGE_DIRECTORY = pathlib.Path(__file__).parent.joinpath('_sdccc')
|
|
10
|
+
"""Default directory to store the downloaded SDCcc versions."""
|
|
11
|
+
|
|
12
|
+
PATH_TYPE = str | os.PathLike[str]
|
|
13
|
+
|
|
14
|
+
ENCODING = 'utf-8' if sys.flags.utf8_mode else locale.getencoding()
|
|
15
|
+
|
|
16
|
+
SINGLE_CMD_TYPE = str | int | bool | pathlib.Path
|
|
17
|
+
CMD_TYPE = SINGLE_CMD_TYPE | Iterable[str | int | pathlib.Path] | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_command(*args: str, **kwargs: CMD_TYPE) -> list[str]:
|
|
21
|
+
"""Build the command string from the arguments and keyword arguments."""
|
|
22
|
+
command = list(args)
|
|
23
|
+
for arg, value in kwargs.items():
|
|
24
|
+
if isinstance(value, SINGLE_CMD_TYPE):
|
|
25
|
+
if value is True:
|
|
26
|
+
command.append(f'--{arg}')
|
|
27
|
+
elif value is False:
|
|
28
|
+
continue # ignore False flags
|
|
29
|
+
else:
|
|
30
|
+
command.append(f'--{arg}')
|
|
31
|
+
command.append(str(value))
|
|
32
|
+
elif isinstance(value, Iterable) and not isinstance(value, dict | bytes):
|
|
33
|
+
for item in value:
|
|
34
|
+
command.append(f'--{arg}')
|
|
35
|
+
command.append(str(item))
|
|
36
|
+
elif value is not None:
|
|
37
|
+
err = f'Unsupported value type: {type(value)}'
|
|
38
|
+
raise TypeError(err)
|
|
39
|
+
return command
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_exe_path(local_path: pathlib.Path) -> pathlib.Path:
|
|
43
|
+
"""Get the path to the SDCcc executable.
|
|
44
|
+
|
|
45
|
+
This function searches the specified local path for the SDCcc executable file. It expects exactly one executable
|
|
46
|
+
file matching the pattern "sdccc-*.exe" to be present in the directory. If no such file or more than one file is
|
|
47
|
+
found, a FileNotFoundError is raised.
|
|
48
|
+
|
|
49
|
+
:param local_path: The local path where the SDCcc executable is expected to be found.
|
|
50
|
+
:return: The path to the SDCcc executable file.
|
|
51
|
+
:raises FileNotFoundError: If no executable file or more than one executable file is found in the specified path.
|
|
52
|
+
"""
|
|
53
|
+
files = [f for f in local_path.glob('*.exe') if f.is_file()]
|
|
54
|
+
if len(files) != 1:
|
|
55
|
+
msg = f'Expected a single executable file, got {files} in path {local_path}'
|
|
56
|
+
raise FileNotFoundError(msg)
|
|
57
|
+
return files[0]
|
pysdccc/_download.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Everything needed for downloading SDCcc."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import zipfile
|
|
9
|
+
from collections.abc import AsyncGenerator, Generator
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from pysdccc import _common, _runner
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger('pysdccc.download')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@contextlib.contextmanager
|
|
19
|
+
def open_download_stream(
|
|
20
|
+
url: httpx.URL, proxy: httpx.Proxy | None = None, timeout: float | None = None
|
|
21
|
+
) -> Generator[httpx.Response, None, None]:
|
|
22
|
+
"""Open a stream from which SDCcc can be downloaded chunk by chunk."""
|
|
23
|
+
with httpx.stream('GET', url, follow_redirects=True, proxy=proxy, timeout=timeout) as response:
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
yield response
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def download(
|
|
29
|
+
url: httpx.URL | str,
|
|
30
|
+
proxy: httpx.Proxy | None = None,
|
|
31
|
+
output: pathlib.Path | None = None,
|
|
32
|
+
timeout: float | None = None,
|
|
33
|
+
) -> pathlib.Path:
|
|
34
|
+
"""Download and extract the specified version from the URL.
|
|
35
|
+
|
|
36
|
+
:param url: The parsed URL from which to download the executable.
|
|
37
|
+
:param proxy: Optional proxy to be used for the download.
|
|
38
|
+
:param output: The path to the directory where the downloaded executable will be extracted. If None,
|
|
39
|
+
`DEFAULT_STORAGE_DIRECTORY` is used.
|
|
40
|
+
:param timeout: Optional timeout in seconds for the download.
|
|
41
|
+
:return: Path to the executable.
|
|
42
|
+
"""
|
|
43
|
+
url = httpx.URL(url)
|
|
44
|
+
logger.info('Downloading SDCcc from %s.', url)
|
|
45
|
+
with (
|
|
46
|
+
tempfile.NamedTemporaryFile('wb', suffix='.zip', delete=False) as temporary_file,
|
|
47
|
+
open_download_stream(url, proxy, timeout) as response,
|
|
48
|
+
):
|
|
49
|
+
for chunk in response.iter_bytes():
|
|
50
|
+
temporary_file.write(chunk)
|
|
51
|
+
output = output or _common.DEFAULT_STORAGE_DIRECTORY
|
|
52
|
+
logger.info('Extracting SDCcc to %s.', output)
|
|
53
|
+
with zipfile.ZipFile(temporary_file.name) as f:
|
|
54
|
+
f.extractall(output)
|
|
55
|
+
return _common.get_exe_path(output)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_downloaded(version: str) -> bool:
|
|
59
|
+
"""Check if the SDCcc version is already downloaded.
|
|
60
|
+
|
|
61
|
+
This function checks if the SDCcc executable is already downloaded.
|
|
62
|
+
|
|
63
|
+
:return: True if the executable is already downloaded, False otherwise.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
return _runner.SdcccRunner(pathlib.Path().absolute()).get_version() == version
|
|
67
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@contextlib.asynccontextmanager
|
|
72
|
+
async def aopen_download_stream(
|
|
73
|
+
url: httpx.URL,
|
|
74
|
+
proxy: httpx.Proxy | None = None,
|
|
75
|
+
) -> AsyncGenerator[httpx.Response, None]:
|
|
76
|
+
"""Open a stream from which SDCcc can be downloaded chunk by chunk."""
|
|
77
|
+
client = httpx.AsyncClient(follow_redirects=True, proxy=proxy)
|
|
78
|
+
async with client.stream('GET', url) as response:
|
|
79
|
+
response.raise_for_status()
|
|
80
|
+
yield response
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def adownload(
|
|
84
|
+
url: httpx.URL | str,
|
|
85
|
+
proxy: httpx.Proxy | None = None,
|
|
86
|
+
output: pathlib.Path | None = None,
|
|
87
|
+
) -> pathlib.Path:
|
|
88
|
+
"""Download and extract the specified version from the URL.
|
|
89
|
+
|
|
90
|
+
:param url: The parsed URL from which to download the executable.
|
|
91
|
+
:param proxy: Optional proxy to be used for the download.
|
|
92
|
+
:param output: The path to the directory where the downloaded executable will be extracted. If None,
|
|
93
|
+
`DEFAULT_STORAGE_DIRECTORY` is used.
|
|
94
|
+
:return: Path to the executable.
|
|
95
|
+
"""
|
|
96
|
+
url = httpx.URL(url)
|
|
97
|
+
logger.info('Downloading SDCcc from %s.', url)
|
|
98
|
+
with tempfile.NamedTemporaryFile('wb', suffix='.zip', delete=False) as temporary_file:
|
|
99
|
+
async with aopen_download_stream(url, proxy=proxy) as response:
|
|
100
|
+
async for chunk in response.aiter_bytes():
|
|
101
|
+
temporary_file.write(chunk)
|
|
102
|
+
output = output or _common.DEFAULT_STORAGE_DIRECTORY
|
|
103
|
+
logger.info('Extracting SDCcc to %s.', output)
|
|
104
|
+
with zipfile.ZipFile(temporary_file.name) as f:
|
|
105
|
+
f.extractall(output)
|
|
106
|
+
return _common.get_exe_path(output)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def ais_downloaded(version: str) -> bool:
|
|
110
|
+
"""Check if the SDCcc version is already downloaded.
|
|
111
|
+
|
|
112
|
+
This function checks if the SDCcc executable is already downloaded.
|
|
113
|
+
|
|
114
|
+
:return: True if the executable is already downloaded, False otherwise.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
return await _runner.SdcccRunnerAsync(pathlib.Path().absolute()).get_version() == version
|
|
118
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
119
|
+
return False
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Parser for the JUnit XML test results provided by SDCcc including custom elements.
|
|
2
|
+
|
|
3
|
+
This module provides classes to parse and handle JUnit XML test results with custom elements specific to SDCcc.
|
|
4
|
+
It includes custom elements for test identifiers and descriptions, as well as custom test case and test suite classes.
|
|
5
|
+
|
|
6
|
+
Classes
|
|
7
|
+
-------
|
|
8
|
+
|
|
9
|
+
TestIdentifierElement
|
|
10
|
+
Represents the test identifier element in the JUnit XML.
|
|
11
|
+
TestDescriptionElement
|
|
12
|
+
Represents the test description element in the JUnit XML.
|
|
13
|
+
TestCase
|
|
14
|
+
Represents a test case with custom elements.
|
|
15
|
+
TestSuite
|
|
16
|
+
Represents a test suite with custom test cases.
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
from result_parser import TestSuite
|
|
24
|
+
|
|
25
|
+
# Parse a test suite from a file
|
|
26
|
+
test_suite = TestSuite.from_file("path/to/test_results.xml")
|
|
27
|
+
|
|
28
|
+
# Iterate over test cases in the test suite
|
|
29
|
+
for test_case in test_suite:
|
|
30
|
+
print(test_case.test_identifier)
|
|
31
|
+
print(test_case.test_description)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import pathlib
|
|
35
|
+
import typing
|
|
36
|
+
|
|
37
|
+
from junitparser import junitparser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestIdentifierElement(junitparser.Element):
|
|
41
|
+
"""Element containing the test identifier.
|
|
42
|
+
|
|
43
|
+
This class represents a custom XML element for the test identifier in the JUnit XML test results.
|
|
44
|
+
It extends the `junitparser.Element` class and provides a property to access the text content of the element.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__test__ = False
|
|
48
|
+
_tag = 'test-identifier'
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def text(self) -> str | None:
|
|
52
|
+
"""Retrieve the text content of the XML element.
|
|
53
|
+
|
|
54
|
+
:return: The text content of the XML element, or None if the element has no text.
|
|
55
|
+
"""
|
|
56
|
+
return self._elem.text
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestDescriptionElement(junitparser.Element):
|
|
60
|
+
"""Element containing the test description.
|
|
61
|
+
|
|
62
|
+
This class represents a custom XML element for the test description in the JUnit XML test results.
|
|
63
|
+
It extends the `junitparser.Element` class and provides a property to access the text content of the element.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
__test__ = False
|
|
67
|
+
_tag = 'test-description'
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def text(self) -> str | None:
|
|
71
|
+
"""Retrieve the text content of the XML element.
|
|
72
|
+
|
|
73
|
+
:return: The text content of the XML element, or None if the element has no text.
|
|
74
|
+
"""
|
|
75
|
+
return self._elem.text
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestCase(junitparser.TestCase):
|
|
79
|
+
"""Test case containing custom elements.
|
|
80
|
+
|
|
81
|
+
This class extends the `junitparser.TestCase` class to include custom elements such as test identifier and test
|
|
82
|
+
description. It provides properties to access these custom elements.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def test_identifier(self) -> str | None:
|
|
87
|
+
"""Links a test case with a unique test identifier.
|
|
88
|
+
|
|
89
|
+
:return: The unique identifier for the test case.
|
|
90
|
+
"""
|
|
91
|
+
elem = self.child(TestIdentifierElement)
|
|
92
|
+
return elem.text if elem is not None else None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def test_description(self) -> str | None:
|
|
96
|
+
"""Description of the test case itself, i.e. what is tested and how.
|
|
97
|
+
|
|
98
|
+
:return: The description of the test case.
|
|
99
|
+
"""
|
|
100
|
+
elem = self.child(TestDescriptionElement)
|
|
101
|
+
return elem.text if elem is not None else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestSuite(junitparser.TestSuite):
|
|
105
|
+
"""Test suite containing custom test cases.
|
|
106
|
+
|
|
107
|
+
This class extends the `junitparser.TestSuite` class to include custom test cases with additional elements.
|
|
108
|
+
It provides methods to iterate over the test cases and parse a test suite from a file.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __iter__(self) -> typing.Iterator[TestCase]: # pyright: ignore [reportIncompatibleMethodOverride]
|
|
112
|
+
for elem in super().__iter__():
|
|
113
|
+
yield TestCase.fromelem(elem) # type: ignore[no-untyped-call]
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_file(cls, file: pathlib.Path | str) -> 'TestSuite':
|
|
117
|
+
"""Parse a test suite from a given file.
|
|
118
|
+
|
|
119
|
+
This method reads a JUnit XML file and parses it into a `TestSuite` object containing custom elements.
|
|
120
|
+
|
|
121
|
+
:param file: The path to the test suite file to be parsed. Can be a `pathlib.Path` object or a string.
|
|
122
|
+
:return: A `TestSuite` object containing the parsed test cases with custom elements.
|
|
123
|
+
:raises ValueError: If the parsed file does not contain a `TestSuite` object.
|
|
124
|
+
:raises FileNotFoundError: If the file does not exist.
|
|
125
|
+
"""
|
|
126
|
+
suite = junitparser.JUnitXml.fromfile(str(file))
|
|
127
|
+
if not isinstance(suite, junitparser.TestSuite):
|
|
128
|
+
msg = f'Expected class {junitparser.TestSuite}, got {type(suite)}'
|
|
129
|
+
raise TypeError(msg)
|
|
130
|
+
return cls.fromelem(suite) # type: ignore[no-untyped-call,no-any-return]
|
pysdccc/_runner.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Implements the runner for the SDCcc executable.
|
|
2
|
+
|
|
3
|
+
This module provides the `SdcccRunner` class to manage and execute the SDCcc tests. It handles the configuration,
|
|
4
|
+
requirements, and execution of the SDCcc executable, as well as parsing the test results.
|
|
5
|
+
|
|
6
|
+
Classes
|
|
7
|
+
-------
|
|
8
|
+
|
|
9
|
+
SdcccRunner
|
|
10
|
+
Runner for the SDCcc tests.
|
|
11
|
+
|
|
12
|
+
Functions
|
|
13
|
+
---------
|
|
14
|
+
|
|
15
|
+
check_requirements(provided: dict[str, dict[str, bool]], available: dict[str, dict[str, bool]]) -> None
|
|
16
|
+
Check if the provided requirements are supported by the available requirements.
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
-----
|
|
20
|
+
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
from pysdccc import SdcccRunner
|
|
24
|
+
import pathlib
|
|
25
|
+
|
|
26
|
+
# Initialize the runner with the path to the SDCcc executable and the test run directory
|
|
27
|
+
runner = SdcccRunner(
|
|
28
|
+
exe=pathlib.Path("/absolute/path/to/sdccc-executable"),
|
|
29
|
+
test_run_dir=pathlib.Path("/absolute/path/to/test-run-directory")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Load the default configuration
|
|
33
|
+
config = runner.get_config()
|
|
34
|
+
|
|
35
|
+
# Load the default requirements
|
|
36
|
+
requirements = runner.get_requirements()
|
|
37
|
+
|
|
38
|
+
# Check user-provided requirements against the SDCcc provided requirements
|
|
39
|
+
runner.check_requirements(pathlib.Path("/absolute/path/to/user-requirements.toml"))
|
|
40
|
+
|
|
41
|
+
# Run the SDCcc executable with the specified configuration and requirements
|
|
42
|
+
exit_code, direct_result, invariant_result = runner.run(
|
|
43
|
+
config=pathlib.Path("/absolute/path/to/config.toml"),
|
|
44
|
+
requirements=pathlib.Path("/absolute/path/to/requirements.toml"),
|
|
45
|
+
timeout=3600 # 1 hour timeout
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import pathlib
|
|
50
|
+
import subprocess
|
|
51
|
+
import sys
|
|
52
|
+
import tomllib
|
|
53
|
+
import typing
|
|
54
|
+
|
|
55
|
+
import anyio
|
|
56
|
+
|
|
57
|
+
from pysdccc import _common
|
|
58
|
+
from pysdccc._result_parser import TestSuite
|
|
59
|
+
|
|
60
|
+
DIRECT_TEST_RESULT_FILE_NAME = 'TEST-SDCcc_direct.xml'
|
|
61
|
+
INVARIANT_TEST_RESULT_FILE_NAME = 'TEST-SDCcc_invariant.xml'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_requirements(provided: dict[str, dict[str, bool]], available: dict[str, dict[str, bool]]) -> None:
|
|
65
|
+
"""Check if the provided requirements are supported by the available requirements.
|
|
66
|
+
|
|
67
|
+
This function verifies that all the requirements specified in the `provided` dictionary are supported by the
|
|
68
|
+
requirements in the `available` dictionary. If any requirement in `provided` is not found in `available`, a KeyError
|
|
69
|
+
is raised.
|
|
70
|
+
|
|
71
|
+
:param provided: A dictionary of provided requirements to be verified. The keys are standard names, and the values
|
|
72
|
+
are dictionaries where the keys are requirement IDs and the values are booleans indicating whether
|
|
73
|
+
the requirement is enabled.
|
|
74
|
+
:param available: A dictionary of available requirements provided by SDCcc. The keys are standard names, and the
|
|
75
|
+
values are dictionaries where the keys are requirement IDs and the values are booleans indicating
|
|
76
|
+
whether the requirement is enabled.
|
|
77
|
+
:raise KeyError: If a standard or requirement provided by the user is not found in the SDCcc provided requirements.
|
|
78
|
+
"""
|
|
79
|
+
for standard, requirements in provided.items():
|
|
80
|
+
if standard not in available:
|
|
81
|
+
msg = f'Unsupported standard "{standard}". Supported standards are "{list(available)}"'
|
|
82
|
+
raise KeyError(msg)
|
|
83
|
+
provided_enabled = [req for req, enabled in requirements.items() if enabled]
|
|
84
|
+
available_enabled = [a for a, enabled in available[standard].items() if enabled]
|
|
85
|
+
for req in provided_enabled:
|
|
86
|
+
if req not in available_enabled:
|
|
87
|
+
msg = f'Requirement id "{standard}.{req}" not found'
|
|
88
|
+
raise KeyError(msg)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _BaseRunner:
|
|
92
|
+
"""Runner for the SDCcc tests.
|
|
93
|
+
|
|
94
|
+
This class provides methods to manage and execute the SDCcc tests. It handles the configuration, requirements,
|
|
95
|
+
and execution of the SDCcc executable, as well as parsing the test results.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, test_run_dir: _common.PATH_TYPE, exe: _common.PATH_TYPE | None = None):
|
|
99
|
+
"""Initialize the SdcccRunner object.
|
|
100
|
+
|
|
101
|
+
:param exe: The path to the SDCcc executable. Must be an absolute path.
|
|
102
|
+
:param test_run_dir: The path to the directory where the test run results are to be stored. Must be an absolute
|
|
103
|
+
path.
|
|
104
|
+
:raises ValueError: If the provided paths are not absolute.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
self.exe = (
|
|
108
|
+
pathlib.Path(exe)
|
|
109
|
+
if exe is not None
|
|
110
|
+
else _common.get_exe_path(_common.DEFAULT_STORAGE_DIRECTORY).absolute()
|
|
111
|
+
)
|
|
112
|
+
except FileNotFoundError as e:
|
|
113
|
+
msg = 'Have you downloaded SDCcc?'
|
|
114
|
+
raise FileNotFoundError(msg) from e
|
|
115
|
+
if not self.exe.is_absolute():
|
|
116
|
+
msg = f'Path to executable must be absolute but is {self.exe}'
|
|
117
|
+
raise ValueError(msg)
|
|
118
|
+
if not self.exe.is_file():
|
|
119
|
+
msg = f'No executable found under {self.exe}'
|
|
120
|
+
raise FileNotFoundError(msg)
|
|
121
|
+
self.test_run_dir = pathlib.Path(test_run_dir)
|
|
122
|
+
if not self.test_run_dir.is_absolute():
|
|
123
|
+
msg = f'Path to test run directory must be absolute but is {self.test_run_dir}'
|
|
124
|
+
raise ValueError(msg)
|
|
125
|
+
if not self.test_run_dir.is_dir():
|
|
126
|
+
msg = f'Test run directory "{self.test_run_dir}" is not a directory or does not exist'
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
|
|
129
|
+
def get_config(self) -> dict[str, typing.Any]:
|
|
130
|
+
"""Get the default configuration.
|
|
131
|
+
|
|
132
|
+
This method loads the default configuration from the SDCcc executable's directory.
|
|
133
|
+
|
|
134
|
+
:return: A dictionary containing the configuration data.
|
|
135
|
+
"""
|
|
136
|
+
return tomllib.loads(self.exe.parent.joinpath('configuration').joinpath('config.toml').read_text())
|
|
137
|
+
|
|
138
|
+
def get_requirements(self) -> dict[str, dict[str, bool]]:
|
|
139
|
+
"""Get the default requirements.
|
|
140
|
+
|
|
141
|
+
This method loads the default requirements from the SDCcc executable's directory.
|
|
142
|
+
|
|
143
|
+
:return: A dictionary containing the requirements data.
|
|
144
|
+
"""
|
|
145
|
+
return tomllib.loads(self.exe.parent.joinpath('configuration').joinpath('test_configuration.toml').read_text())
|
|
146
|
+
|
|
147
|
+
def get_test_parameter(self) -> dict[str, typing.Any]:
|
|
148
|
+
"""Get the default test parameter.
|
|
149
|
+
|
|
150
|
+
This method loads the default test parameters from the SDCcc executable's directory.
|
|
151
|
+
|
|
152
|
+
:return: A dictionary containing the test parameter data.
|
|
153
|
+
"""
|
|
154
|
+
return tomllib.loads(self.exe.parent.joinpath('configuration').joinpath('test_parameter.toml').read_text())
|
|
155
|
+
|
|
156
|
+
def check_requirements(self, path: pathlib.Path) -> None:
|
|
157
|
+
"""Check the requirements from the given file against the requirements provided by the SDCcc version.
|
|
158
|
+
|
|
159
|
+
This method verifies that all the requirements specified in the user's requirements file are supported by the
|
|
160
|
+
requirements provided by the SDCcc version. If any requirement is not found, a KeyError is raised.
|
|
161
|
+
|
|
162
|
+
:param path: The path to the user's requirements file.
|
|
163
|
+
:raises KeyError: If a standard or requirement provided by the user is not found in the SDCcc provided
|
|
164
|
+
requirements.
|
|
165
|
+
"""
|
|
166
|
+
sdccc_provided_requirements = self.get_requirements()
|
|
167
|
+
user_provided_requirements = tomllib.loads(path.read_text())
|
|
168
|
+
check_requirements(user_provided_requirements, sdccc_provided_requirements)
|
|
169
|
+
|
|
170
|
+
def _get_result(self, file_name: str) -> TestSuite | None:
|
|
171
|
+
"""Get the parsed results of the test run.
|
|
172
|
+
|
|
173
|
+
This method reads the direct and invariant test result files from the test run directory and returns them
|
|
174
|
+
as TestSuite objects.
|
|
175
|
+
|
|
176
|
+
:return: A tuple containing the parsed direct and invariant test results as TestSuite objects.
|
|
177
|
+
"""
|
|
178
|
+
test_result_dir = self.test_run_dir.joinpath(file_name)
|
|
179
|
+
if not test_result_dir.exists():
|
|
180
|
+
return None
|
|
181
|
+
return TestSuite.from_file(test_result_dir)
|
|
182
|
+
|
|
183
|
+
def _prepare_command(
|
|
184
|
+
self,
|
|
185
|
+
*args: str,
|
|
186
|
+
config: pathlib.Path,
|
|
187
|
+
requirements: pathlib.Path,
|
|
188
|
+
**kwargs: _common.CMD_TYPE,
|
|
189
|
+
) -> list[str]:
|
|
190
|
+
if not config.is_absolute():
|
|
191
|
+
msg = 'Path to config file must be absolute'
|
|
192
|
+
raise ValueError(msg)
|
|
193
|
+
if not requirements.is_absolute():
|
|
194
|
+
msg = 'Path to requirements file must be absolute'
|
|
195
|
+
raise ValueError(msg)
|
|
196
|
+
if list(self.test_run_dir.iterdir()):
|
|
197
|
+
msg = f'{self.test_run_dir} is not empty'
|
|
198
|
+
raise ValueError(msg)
|
|
199
|
+
|
|
200
|
+
kwargs['no_subdirectories'] = 'true'
|
|
201
|
+
kwargs['test_run_directory'] = self.test_run_dir
|
|
202
|
+
kwargs['config'] = config
|
|
203
|
+
kwargs['testconfig'] = requirements
|
|
204
|
+
return _common.build_command(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class SdcccRunner(_BaseRunner):
|
|
208
|
+
"""Synchronous runner for SDCcc."""
|
|
209
|
+
|
|
210
|
+
def run(
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
config: _common.PATH_TYPE,
|
|
214
|
+
requirements: _common.PATH_TYPE,
|
|
215
|
+
timeout: float | None = None,
|
|
216
|
+
**kwargs: _common.CMD_TYPE,
|
|
217
|
+
) -> tuple[int, TestSuite | None, TestSuite | None]:
|
|
218
|
+
"""Run the SDCcc executable using the specified configuration and requirements.
|
|
219
|
+
|
|
220
|
+
This method executes the SDCcc executable with the provided configuration and requirements files,
|
|
221
|
+
and additional command line arguments. It logs the stdout and stderr of the process and waits for the
|
|
222
|
+
process to complete or timeout.
|
|
223
|
+
Checkout more parameter under https://github.com/draegerwerk/sdccc?tab=readme-ov-file#running-sdccc
|
|
224
|
+
|
|
225
|
+
:param config: The path to the configuration file. Must be an absolute path.
|
|
226
|
+
:param requirements: The path to the requirements file. Must be an absolute path.
|
|
227
|
+
:param timeout: The timeout in seconds for the SDCcc process. If None, wait indefinitely.
|
|
228
|
+
:param kwargs: Additional command line arguments to be passed to the SDCcc executable.
|
|
229
|
+
:return: A tuple containing the returncode of the SDCcc process, parsed direct and invariant test results as
|
|
230
|
+
TestSuite objects.
|
|
231
|
+
:raises ValueError: If the provided paths are not absolute.
|
|
232
|
+
:raises subprocess.TimeoutExpired: If the process is running longer than the timeout.
|
|
233
|
+
"""
|
|
234
|
+
command = self._prepare_command(
|
|
235
|
+
str(self.exe),
|
|
236
|
+
config=pathlib.Path(config),
|
|
237
|
+
requirements=pathlib.Path(requirements),
|
|
238
|
+
**kwargs,
|
|
239
|
+
)
|
|
240
|
+
try:
|
|
241
|
+
return_code = subprocess.run(command, timeout=timeout, check=True, cwd=self.exe.parent).returncode # noqa: S603
|
|
242
|
+
except subprocess.CalledProcessError as e:
|
|
243
|
+
return_code = e.returncode
|
|
244
|
+
return (
|
|
245
|
+
return_code,
|
|
246
|
+
self._get_result(DIRECT_TEST_RESULT_FILE_NAME),
|
|
247
|
+
self._get_result(INVARIANT_TEST_RESULT_FILE_NAME),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def get_version(self) -> str:
|
|
251
|
+
"""Get the version of the SDCcc executable."""
|
|
252
|
+
# use capture_output = True to get stdout and stderr instead of check_output which only collects stdout
|
|
253
|
+
process = subprocess.run([str(self.exe), '--version'], check=True, capture_output=True, cwd=self.exe.parent) # noqa: S603
|
|
254
|
+
return process.stdout.decode(_common.ENCODING).strip()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class SdcccRunnerAsync(_BaseRunner):
|
|
258
|
+
"""Asynchronous runner for SDCcc."""
|
|
259
|
+
|
|
260
|
+
def __init__(self, test_run_dir: _common.PATH_TYPE, exe: _common.PATH_TYPE | None = None):
|
|
261
|
+
"""Initialize the SdcccRunnerAsync object.
|
|
262
|
+
|
|
263
|
+
:param exe: The path to the SDCcc executable. Must be an absolute path.
|
|
264
|
+
:param test_run_dir: The path to the directory where the test run results are to be stored. Must be an absolute
|
|
265
|
+
path.
|
|
266
|
+
:raises ValueError: If the provided paths are not absolute.
|
|
267
|
+
"""
|
|
268
|
+
super().__init__(test_run_dir=test_run_dir, exe=exe)
|
|
269
|
+
|
|
270
|
+
async def run(
|
|
271
|
+
self,
|
|
272
|
+
*,
|
|
273
|
+
config: _common.PATH_TYPE,
|
|
274
|
+
requirements: _common.PATH_TYPE,
|
|
275
|
+
**kwargs: _common.CMD_TYPE,
|
|
276
|
+
) -> tuple[int, TestSuite | None, TestSuite | None]:
|
|
277
|
+
"""Run the SDCcc executable using the specified configuration and requirements.
|
|
278
|
+
|
|
279
|
+
This method executes the SDCcc executable with the provided configuration and requirements files,
|
|
280
|
+
and additional command line arguments. It logs the stdout and stderr of the process and waits for the
|
|
281
|
+
process to complete or timeout.
|
|
282
|
+
Checkout more parameter under https://github.com/draegerwerk/sdccc?tab=readme-ov-file#running-sdccc
|
|
283
|
+
|
|
284
|
+
:param config: The path to the configuration file. Must be an absolute path.
|
|
285
|
+
:param requirements: The path to the requirements file. Must be an absolute path.
|
|
286
|
+
:param kwargs: Additional command line arguments to be passed to the SDCcc executable.
|
|
287
|
+
:return: A tuple containing the returncode of the SDCcc process, parsed direct and invariant test results as
|
|
288
|
+
TestSuite objects.
|
|
289
|
+
:raises ValueError: If the provided paths are not absolute.
|
|
290
|
+
:raises TimeoutError: If the process is running longer than the timeout.
|
|
291
|
+
"""
|
|
292
|
+
command = self._prepare_command(
|
|
293
|
+
str(self.exe), config=pathlib.Path(config), requirements=pathlib.Path(requirements), **kwargs
|
|
294
|
+
)
|
|
295
|
+
try:
|
|
296
|
+
return_code = (
|
|
297
|
+
await anyio.run_process(command, check=True, cwd=self.exe.parent, stdout=sys.stdout, stderr=sys.stderr)
|
|
298
|
+
).returncode
|
|
299
|
+
except subprocess.CalledProcessError as e:
|
|
300
|
+
return_code = e.returncode
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
return_code,
|
|
304
|
+
self._get_result(DIRECT_TEST_RESULT_FILE_NAME),
|
|
305
|
+
self._get_result(INVARIANT_TEST_RESULT_FILE_NAME),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async def get_version(self) -> str | None:
|
|
309
|
+
"""Get the version of the SDCcc executable."""
|
|
310
|
+
result = await anyio.run_process([self.exe, '--version'], check=True, cwd=self.exe.parent)
|
|
311
|
+
return result.stdout.decode(_common.ENCODING).strip() if result.stdout is not None else None
|
pysdccc/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysdccc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python wrapper for SDCcc test tool
|
|
5
|
+
Project-URL: Homepage, https://github.com/Draegerwerk/pysdccc
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/Draegerwerk/pysdccc/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Draegerwerk/pysdccc/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Repository, https://github.com/Draegerwerk/pysdccc.git
|
|
9
|
+
Author-email: Leon Budnick <leon.budnick@draeger.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2024 Dräger
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: SDCcc,sdc,test,wrapper
|
|
33
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Requires-Python: <3.14,>=3.11
|
|
38
|
+
Requires-Dist: anyio>=4.7.0
|
|
39
|
+
Requires-Dist: httpx>=0.28.0
|
|
40
|
+
Requires-Dist: junitparser>=3.1.2
|
|
41
|
+
Provides-Extra: cli
|
|
42
|
+
Requires-Dist: click>=8.0; extra == 'cli'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# pysdccc
|
|
46
|
+
|
|
47
|
+
This python packages provides a convenient way to execute the [SDCcc test suite](https://github.com/Draegerwerk/sdccc/).
|
|
48
|
+
|
|
49
|
+
This wrapper is only compatible with SDCcc versions later than [internal-baseline-001](https://github.com/Draegerwerk/SDCcc/releases/tag/internal-baseline-001).
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Download from pypi using `pip install pysdccc`. There is also an option to use this via the command line by installing `pysdccc[cli]`.
|
|
54
|
+
|
|
55
|
+
### Development
|
|
56
|
+
|
|
57
|
+
For this open source project the [Contributor License Agreement](Contributor_License_Agreement.md) governs all relevant activities and your contributions. By contributing to the project you agree to be bound by this Agreement and to licence your work accordingly.
|
|
58
|
+
|
|
59
|
+
1. clone the repository
|
|
60
|
+
2. install dependencies, e.g. with [`uv sync --dev --all-extras`](https://docs.astral.sh/uv/reference/cli/#uv-sync)
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import pathlib
|
|
68
|
+
import subprocess
|
|
69
|
+
|
|
70
|
+
import pysdccc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
if not pysdccc.is_downloaded("my-specific-version"):
|
|
75
|
+
pysdccc.download("https://url/to/sdccc.zip")
|
|
76
|
+
|
|
77
|
+
runner = pysdccc.SdcccRunner(
|
|
78
|
+
pathlib.Path("/path/to/sdccc/result/directory"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# https://github.com/Draegerwerk/SDCcc/?tab=readme-ov-file#exit-codes
|
|
83
|
+
return_code, direct_result, invariant_result = runner.run(
|
|
84
|
+
config=pathlib.Path("/path/to/configuration/file.toml"),
|
|
85
|
+
requirements=pathlib.Path("/path/to/requirements/file.toml"),
|
|
86
|
+
)
|
|
87
|
+
except subprocess.TimeoutExpired:
|
|
88
|
+
print("Timeout occurred")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if direct_result is None or invariant_result is None:
|
|
92
|
+
print("No result file available")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
for test_case in direct_result + invariant_result:
|
|
96
|
+
print(f"{test_case.name}: {test_case.is_passed}")
|
|
97
|
+
```
|
|
98
|
+
If you look for an async version
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import pathlib
|
|
102
|
+
|
|
103
|
+
import pysdccc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def main():
|
|
107
|
+
if not await pysdccc.ais_downloaded("my-specific-version"):
|
|
108
|
+
await pysdccc.adownload("https://url/to/sdccc.zip")
|
|
109
|
+
|
|
110
|
+
runner = pysdccc.SdcccRunnerAsync(
|
|
111
|
+
pathlib.Path("/path/to/sdccc/result/directory"),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# https://github.com/Draegerwerk/SDCcc/?tab=readme-ov-file#exit-codes
|
|
115
|
+
return_code, direct_result, invariant_result = await runner.run(
|
|
116
|
+
config=pathlib.Path("/path/to/configuration/file.toml"),
|
|
117
|
+
requirements=pathlib.Path("/path/to/requirements/file.toml"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# checkout example from above ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Download an SDCcc executable
|
|
124
|
+
|
|
125
|
+
Check out `pysdccc.download` or `pysdccc.adownload`. If the command line interface of `pysdccc` is installed, the zipped source of SDCcc can be installed using this command: `pysdccc install https://url/to/sdccc.zip`
|
|
126
|
+
|
|
127
|
+
### Create configuration file
|
|
128
|
+
|
|
129
|
+
Configure the test consumer. Check the [test consumer configuration](https://github.com/Draegerwerk/SDCcc/?tab=readme-ov-file#test-consumer-configuration) for more information.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import pathlib
|
|
133
|
+
|
|
134
|
+
import toml # has to be installed by the user
|
|
135
|
+
|
|
136
|
+
import pysdccc
|
|
137
|
+
|
|
138
|
+
config = {
|
|
139
|
+
'SDCcc': {
|
|
140
|
+
... # add all relevant config parameter
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
config_path = pathlib.Path('/path/to/configuration/file.toml')
|
|
144
|
+
config_path.write_text(toml.dumps(config))
|
|
145
|
+
|
|
146
|
+
runner = pysdccc.SdcccRunner(
|
|
147
|
+
pathlib.Path('/path/to/sdccc/result/directory'),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
runner.run(
|
|
151
|
+
config=config_path,
|
|
152
|
+
requirements=pathlib.Path('/path/to/requirements/file.toml'),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# or if you have already downloaded SDCcc
|
|
156
|
+
config = runner.get_config() # load default configuration
|
|
157
|
+
config['SDCcc']['Consumer']['DeviceEpr'] = "urn:uuid:12345678-1234-1234-1234-123456789012" # e.g. change device epr
|
|
158
|
+
# save and run as above
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Create requirements file
|
|
162
|
+
|
|
163
|
+
Enable or disable specific requirements. Check the [test requirements](https://github.com/Draegerwerk/SDCcc/?tab=readme-ov-file#enabling-tests) for more information.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import pathlib
|
|
167
|
+
|
|
168
|
+
import toml # has to be installed by the user
|
|
169
|
+
|
|
170
|
+
import pysdccc
|
|
171
|
+
|
|
172
|
+
requirements = {
|
|
173
|
+
# the standard name is the key, the requirement from the standard is the value
|
|
174
|
+
'BICEPS': {
|
|
175
|
+
... # add all requirements to be tested
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
requirements_path = pathlib.Path('/path/to/requirement/file.toml')
|
|
179
|
+
requirements_path.write_text(toml.dumps(requirements))
|
|
180
|
+
|
|
181
|
+
runner = pysdccc.SdcccRunner(
|
|
182
|
+
pathlib.Path('/path/to/sdccc/result/directory'),
|
|
183
|
+
)
|
|
184
|
+
# optionally, check whether you did not add a requirement that is not available
|
|
185
|
+
runner.check_requirements(requirements_path)
|
|
186
|
+
runner.run(
|
|
187
|
+
config=pathlib.Path('/path/to/configuration/file.toml'),
|
|
188
|
+
requirements=requirements_path,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# or, if you have already downloaded SDCcc
|
|
192
|
+
requirements = runner.get_requirements() # load default configuration
|
|
193
|
+
requirements['BICEPS']['R0033'] = False # e.g. disable biceps R0033
|
|
194
|
+
# save and run as above
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Create test parameter configuration
|
|
198
|
+
|
|
199
|
+
Some tests require individual parameters. Check the [test parameter configuration](https://github.com/Draegerwerk/SDCcc/?tab=readme-ov-file#test-parameter-configuration) for more information.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import pathlib
|
|
203
|
+
|
|
204
|
+
import toml # has to be installed by the user
|
|
205
|
+
|
|
206
|
+
import pysdccc
|
|
207
|
+
|
|
208
|
+
testparameter_config = {
|
|
209
|
+
'TestParameter': {
|
|
210
|
+
...
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
testparameter_config_path = pathlib.Path('/path/to/test_parameter/file.toml')
|
|
214
|
+
testparameter_config_path.write_text(toml.dumps(testparameter_config))
|
|
215
|
+
|
|
216
|
+
runner = pysdccc.SdcccRunner(
|
|
217
|
+
pathlib.Path('/path/to/sdccc/result/directory'),
|
|
218
|
+
)
|
|
219
|
+
runner.run(
|
|
220
|
+
config=pathlib.Path('/path/to/configuration/file.toml'),
|
|
221
|
+
requirements=pathlib.Path('/path/to/requirements/file.toml'),
|
|
222
|
+
testparam=testparameter_config_path,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# or, if you have already downloaded SDCcc
|
|
226
|
+
testparameter_config = runner.get_test_parameter() # load default configuration
|
|
227
|
+
testparameter_config['TestParameter']['Biceps547TimeInterval'] = 10
|
|
228
|
+
# save and run as above
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Execute SDCcc from command-line interface (cli)
|
|
232
|
+
|
|
233
|
+
There exists a cli wrapper for the SDCcc executable. If `pysdccc[cli]` is installed, `sdccc` can be used to execute arbitrary SDCcc commands, e.g. `sdccc --version`. More information can be found [here](https://github.com/draegerwerk/sdccc?tab=readme-ov-file#running-sdccc).
|
|
234
|
+
|
|
235
|
+
## Notices
|
|
236
|
+
|
|
237
|
+
`pysdccc` is not intended for use in medical products, clinical trials, clinical studies, or in clinical routine.
|
|
238
|
+
|
|
239
|
+
### ISO 9001
|
|
240
|
+
|
|
241
|
+
`pysdccc` was not developed according to ISO 9001.
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pysdccc/__init__.py,sha256=qnHztGXtWUV3taE_vaNcCOuktWyFpJNWnJh3tJmYLTs,599
|
|
2
|
+
pysdccc/_cli.py,sha256=pbzr3hhc2stlSrNWybLoE6XxTEZgSN1XyeHyUF2Hq7o,4803
|
|
3
|
+
pysdccc/_common.py,sha256=ujgKH6_oA1h2KZXPyShfa3o-nho_I2snmkvfcs7rLm8,2246
|
|
4
|
+
pysdccc/_download.py,sha256=1PVyGcEC63mmZXdUlfg4HaPb-gz-47iB4IAdnne_GAg,4320
|
|
5
|
+
pysdccc/_result_parser.py,sha256=QsRbpV4obBFrirwMHXWLm6zdoxxV_rsdhqzm5YJ13Uk,4603
|
|
6
|
+
pysdccc/_runner.py,sha256=AaWtU7vj_cgfRgrGpn6SQhrvhyFDQhmPQHKaAbMF1b0,13830
|
|
7
|
+
pysdccc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
pysdccc-0.1.0.dist-info/METADATA,sha256=yLzQyyXJFzi8pcC3jBR2zb1__r9AuyC5zVL_JAlKKSo,8519
|
|
9
|
+
pysdccc-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pysdccc-0.1.0.dist-info/entry_points.txt,sha256=1Y-7xLrgZ-44p0AnJikH9buOqLGo-1mDfORut7ZXCkU,72
|
|
11
|
+
pysdccc-0.1.0.dist-info/licenses/LICENSE,sha256=W8bC5k53mY5GatF3EpFnE4bK6m-htGV_IsiveTOwKMc,1064
|
|
12
|
+
pysdccc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dräger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|