Skip to content

Commit 96f1d13

Browse files
maint: Test improvements (#32)
* maint: Improve testing * maint: Abstract class tests * fix: Remove unreachable code * tests: Uncoverable lines * test: Latest changes * test: Improve example_download tests * Update src/ansys/tools/common/example_download.py Co-authored-by: Roberto Pastor Muela <[email protected]> --------- Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent 9c45b85 commit 96f1d13

File tree

12 files changed

+342
-21
lines changed

12 files changed

+342
-21
lines changed

.github/workflows/cicd.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ jobs:
6767
run: |
6868
pytest
6969
70+
# - uses: codecov/codecov-action@v5
71+
# name: 'Upload coverage to CodeCov'
72+
# with:
73+
# token: ${{ secrets.CODECOV_TOKEN }}
74+
7075
doc-build:
7176
name: Build documentation
7277
runs-on: ubuntu-latest

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,6 @@ cython_debug/
175175
.vscode
176176
._build
177177

178-
doc/source/api
178+
doc/source/api
179+
.cov
180+
not_a_dir

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ Releases = "https://github.com/ansys/ansys-tools-common/releases/"
8080
[tool.flit.module]
8181
name = "ansys.tools.common"
8282

83+
[tool.pytest.ini_options]
84+
addopts = "-ra --cov=ansys.tools.common --cov-report html:.cov/html --cov-report xml:.cov/xml --cov-report term --capture=sys -vv"
85+
86+
8387
[tool.ruff]
8488
line-length = 120
8589
fix = true

src/ansys/tools/common/abstractions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
"""Init module for abstractions."""
2323

2424
from .connection import AbstractGRPCConnection # noqa F401
25-
from .launcher import AbstractServiceLauncher # noqa F401
25+
from .launcher import LauncherProtocol # noqa F401

src/ansys/tools/common/abstractions/connection.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
try:
2727
import grpc
28-
except ImportError:
28+
except ImportError: # pragma: no cover
2929
import warnings
3030

3131
warnings.warn(
@@ -47,33 +47,43 @@ class AbstractGRPCConnection(ABC):
4747
@abstractmethod
4848
def __init__(self, host: str, port: str) -> None:
4949
"""Initialize the gRPC connection with host and port."""
50-
pass
50+
pass # pragma: no cover
5151

5252
@abstractmethod
5353
def connect(self) -> None:
5454
"""Establish a connection to the gRPC server."""
55-
pass
55+
pass # pragma: no cover
5656

5757
@abstractmethod
5858
def close(self) -> None:
5959
"""Disconnect from the gRPC server."""
60-
pass
60+
pass # pragma: no cover
6161

62-
@abstractmethod
6362
@property
63+
@abstractmethod
6464
def service(self):
6565
"""Return the gRPC stub for making requests."""
66-
pass
66+
pass # pragma: no cover
6767

6868
@property
6969
def _host(self) -> str:
7070
"""Return the host for the gRPC connection."""
71-
return self._host
71+
return self.__host
72+
73+
@_host.setter
74+
def _host(self, value: str) -> None:
75+
"""Set the host for the gRPC connection."""
76+
self.__host = value
7277

7378
@property
7479
def _port(self) -> str:
7580
"""Return the port for the gRPC connection."""
76-
return self._port
81+
return self.__port
82+
83+
@_port.setter
84+
def _port(self, value: str) -> None:
85+
"""Set the port for the gRPC connection."""
86+
self.__port = value
7787

7888
@property
7989
def _channel(self, options: list = None) -> grpc.Channel:

src/ansys/tools/common/abstractions/launcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class LauncherProtocol(Protocol[LAUNCHER_CONFIG_T]):
136136
"""
137137

138138
def __init__(self, *, config: LAUNCHER_CONFIG_T):
139-
pass
139+
pass # pragma: no cover
140140

141141
def start(self) -> None:
142142
"""Start the product instance."""

src/ansys/tools/common/example_download.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ def download_file(
9999
destination_path = Path(destination) if destination is not None else None
100100

101101
# If destination is not a dir, create it
102-
if destination_path is not None and not destination_path.is_dir():
102+
if destination_path is not None and not destination_path.exists():
103103
destination_path.mkdir(parents=True, exist_ok=True)
104104

105-
# Check if it was able to create the dir
105+
# Check if it was able to create the dir, very rare case
106106
if destination_path is not None and not destination_path.is_dir():
107-
raise ValueError("Destination directory provided does not exist")
107+
raise ValueError("Destination directory provided does not exist") # pragma: no cover
108108

109109
url = self._get_filepath_on_default_server(filename, directory)
110110
local_path = self._retrieve_data(url, filename, dest=destination, force=force)
@@ -125,7 +125,8 @@ def _add_file(self, file_path: str):
125125
file_path : str
126126
Local path of the downloaded example file.
127127
"""
128-
self._downloads_list.append(file_path)
128+
if file_path not in self._downloads_list:
129+
self._downloads_list.append(file_path)
129130

130131
def _joinurl(self, base: str, directory: str) -> str:
131132
"""Join multiple paths to a base URL.
@@ -185,10 +186,11 @@ def _retrieve_data(self, url: str, filename: str, dest: str = None, force: bool
185186
str
186187
The local path where the file was saved.
187188
"""
189+
local_path = ""
188190
if dest is None:
189191
dest = tempfile.gettempdir() # Use system temp directory if no destination is provided
190192
local_path = Path(dest) / Path(filename).name
191-
if not force and Path.is_file(local_path):
193+
if not force and Path(local_path).is_file():
192194
return local_path
193195
try:
194196
local_path, _ = urllib.request.urlretrieve(url, filename=local_path)

src/ansys/tools/common/launcher/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class LauncherProtocol(Protocol[LAUNCHER_CONFIG_T]):
136136
"""
137137

138138
def __init__(self, *, config: LAUNCHER_CONFIG_T):
139-
pass
139+
pass # pragma: no cover
140140

141141
def start(self) -> None:
142142
"""Start the product instance."""

src/ansys/tools/common/versioning.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,6 @@ def version_string_as_tuple(version_string):
112112
# Check version string numbers are numeric by converting to integers
113113
version_tuple = tuple(map(VersionNumber, version_string.split(".")))
114114

115-
# Check version numbers are positive integers
116-
if not all(num >= VersionNumber(0) for num in version_tuple):
117-
raise ValueError
118-
119115
except ValueError:
120116
raise VersionSyntaxError(
121117
"Version string can only contain positive integers following <MAJOR>.<MINOR>.<PATCH> versioning."

tests/test_connection.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (C) 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
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.
22+
23+
"""Module for testing gRPC connection abstraction."""
24+
25+
from unittest.mock import MagicMock
26+
27+
import pytest
28+
29+
from ansys.tools.common.abstractions.connection import AbstractGRPCConnection
30+
31+
32+
class MockGRPCConnection(AbstractGRPCConnection):
33+
"""Mock implementation of AbstractGRPCConnection for testing."""
34+
35+
def __init__(self, host: str, port: str) -> None:
36+
"""Initialize the mock gRPC connection."""
37+
self._host = host
38+
self._port = port
39+
self._connected = False
40+
41+
def connect(self) -> None:
42+
"""Connect to the mock gRPC server."""
43+
self._connected = True
44+
45+
def close(self) -> None:
46+
"""Close the mock gRPC connection."""
47+
self._connected = False
48+
49+
@property
50+
def service(self):
51+
"""Service property that returns a mock gRPC stub."""
52+
return MagicMock()
53+
54+
55+
@pytest.fixture
56+
def mock_connection():
57+
"""Fixture for creating a mock gRPC connection."""
58+
return MockGRPCConnection(host="localhost", port="50051")
59+
60+
61+
def test_initialization(mock_connection):
62+
"""Test initialization of the connection."""
63+
assert mock_connection._host == "localhost"
64+
assert mock_connection._port == "50051"
65+
assert mock_connection.is_closed
66+
67+
68+
def test_connect(mock_connection):
69+
"""Test connecting to the gRPC server."""
70+
mock_connection.connect()
71+
72+
73+
def test_close(mock_connection):
74+
"""Test disconnecting from the gRPC server."""
75+
mock_connection.connect()
76+
mock_connection.close()
77+
assert mock_connection.is_closed
78+
79+
80+
def test_service_property(mock_connection):
81+
"""Test the service property."""
82+
service = mock_connection.service
83+
assert service is not None
84+
assert isinstance(service, MagicMock)
85+
86+
87+
def test_is_closed_property(mock_connection):
88+
"""Test the is_closed property."""
89+
assert mock_connection.is_closed

0 commit comments

Comments
 (0)