Skip to content

Commit e929dc0

Browse files
committed
Merge branch 'feature/execute_parameters' of https://github.com/MarketSquare/Robotframework-Database-Library into feature/execute_parameters
2 parents 79560d7 + 2981b16 commit e929dc0

9 files changed

Lines changed: 180 additions & 28 deletions

File tree

.github/workflows/unit_tests.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
# This workflow will install Python dependencies
3+
# and run unit tests for given OSes
4+
5+
name: Unit tests
6+
7+
on: [push, pull_request]
8+
9+
jobs:
10+
build:
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
include:
15+
- os: 'ubuntu-latest'
16+
python-version: '3.7'
17+
rf-version: '3.2.2'
18+
- os: 'ubuntu-latest'
19+
python-version: '3.8'
20+
rf-version: '4.1.3'
21+
- os: 'ubuntu-latest'
22+
python-version: '3.9'
23+
rf-version: '5.0.1'
24+
- os: 'ubuntu-latest'
25+
python-version: '3.10'
26+
rf-version: '6.1.1'
27+
- os: 'ubuntu-latest'
28+
python-version: '3.11'
29+
rf-version: '6.1.1'
30+
- os: 'ubuntu-latest'
31+
python-version: '3.12'
32+
rf-version: '7.0a1'
33+
runs-on: ${{ matrix.os }}
34+
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
with:
39+
fetch-depth: 2
40+
41+
- name: Set up Python ${{ matrix.python-version }}
42+
uses: actions/setup-python@v4
43+
with:
44+
python-version: ${{ matrix.python-version }}
45+
46+
- name: Install dependencies
47+
run: |
48+
python -m pip install --upgrade pip
49+
pip install robotframework==${{ matrix.rf-version }} coverage pytest
50+
pip install .
51+
52+
- name: Run unit tests with coverage
53+
run:
54+
coverage run -m pytest
55+
56+
- name: Codecov
57+
uses: codecov/codecov-action@v3
58+
with:
59+
name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }}

src/DatabaseLibrary/assertion.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from typing import Optional
14+
from typing import List, Optional
1515

1616
from robot.api import logger
1717

@@ -22,7 +22,12 @@ class Assertion:
2222
"""
2323

2424
def check_if_exists_in_database(
25-
self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
25+
self,
26+
selectStatement: str,
27+
sansTran: bool = False,
28+
msg: Optional[str] = None,
29+
alias: Optional[str] = None,
30+
parameters: Optional[List] = None,
2631
):
2732
"""
2833
Check if any row would be returned by given the input ``selectStatement``. If there are no results, then this will
@@ -43,13 +48,18 @@ def check_if_exists_in_database(
4348
| Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True |
4449
"""
4550
logger.info(f"Executing : Check If Exists In Database | {selectStatement}")
46-
if not self.query(selectStatement, sansTran, alias=alias):
51+
if not self.query(selectStatement, sansTran, alias=alias, parameters=parameters):
4752
raise AssertionError(
4853
msg or f"Expected to have have at least one row, but got 0 rows from: '{selectStatement}'"
4954
)
5055

5156
def check_if_not_exists_in_database(
52-
self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
57+
self,
58+
selectStatement: str,
59+
sansTran: bool = False,
60+
msg: Optional[str] = None,
61+
alias: Optional[str] = None,
62+
parameters: Optional[List] = None,
5363
):
5464
"""
5565
This is the negation of `check_if_exists_in_database`.
@@ -71,14 +81,19 @@ def check_if_not_exists_in_database(
7181
| Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True |
7282
"""
7383
logger.info(f"Executing : Check If Not Exists In Database | {selectStatement}")
74-
query_results = self.query(selectStatement, sansTran, alias=alias)
84+
query_results = self.query(selectStatement, sansTran, alias=alias, parameters=parameters)
7585
if query_results:
7686
raise AssertionError(
7787
msg or f"Expected to have have no rows from '{selectStatement}', but got some rows: {query_results}"
7888
)
7989

8090
def row_count_is_0(
81-
self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
91+
self,
92+
selectStatement: str,
93+
sansTran: bool = False,
94+
msg: Optional[str] = None,
95+
alias: Optional[str] = None,
96+
parameters: Optional[List] = None,
8297
):
8398
"""
8499
Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an
@@ -99,7 +114,7 @@ def row_count_is_0(
99114
| Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | sansTran=True |
100115
"""
101116
logger.info(f"Executing : Row Count Is 0 | {selectStatement}")
102-
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
117+
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
103118
if num_rows > 0:
104119
raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'")
105120

@@ -110,6 +125,7 @@ def row_count_is_equal_to_x(
110125
sansTran: bool = False,
111126
msg: Optional[str] = None,
112127
alias: Optional[str] = None,
128+
parameters: Optional[List] = None,
113129
):
114130
"""
115131
Check if the number of rows returned from ``selectStatement`` is equal to the value submitted. If not, then this
@@ -129,7 +145,7 @@ def row_count_is_equal_to_x(
129145
| Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | sansTran=True |
130146
"""
131147
logger.info(f"Executing : Row Count Is Equal To X | {selectStatement} | {numRows}")
132-
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
148+
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
133149
if num_rows != int(numRows.encode("ascii")):
134150
raise AssertionError(
135151
msg or f"Expected {numRows} rows, but {num_rows} were returned from: '{selectStatement}'"
@@ -142,6 +158,7 @@ def row_count_is_greater_than_x(
142158
sansTran: bool = False,
143159
msg: Optional[str] = None,
144160
alias: Optional[str] = None,
161+
parameters: Optional[List] = None,
145162
):
146163
"""
147164
Check if the number of rows returned from ``selectStatement`` is greater than the value submitted. If not, then
@@ -161,7 +178,7 @@ def row_count_is_greater_than_x(
161178
| Row Count Is Greater Than X | SELECT id FROM person | 1 | sansTran=True |
162179
"""
163180
logger.info(f"Executing : Row Count Is Greater Than X | {selectStatement} | {numRows}")
164-
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
181+
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
165182
if num_rows <= int(numRows.encode("ascii")):
166183
raise AssertionError(
167184
msg or f"Expected more than {numRows} rows, but {num_rows} were returned from '{selectStatement}'"
@@ -174,6 +191,7 @@ def row_count_is_less_than_x(
174191
sansTran: bool = False,
175192
msg: Optional[str] = None,
176193
alias: Optional[str] = None,
194+
parameters: Optional[List] = None,
177195
):
178196
"""
179197
Check if the number of rows returned from ``selectStatement`` is less than the value submitted. If not, then this
@@ -194,7 +212,7 @@ def row_count_is_less_than_x(
194212
195213
"""
196214
logger.info(f"Executing : Row Count Is Less Than X | {selectStatement} | {numRows}")
197-
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
215+
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
198216
if num_rows >= int(numRows.encode("ascii")):
199217
raise AssertionError(
200218
msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'"
@@ -204,7 +222,7 @@ def table_must_exist(
204222
self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
205223
):
206224
"""
207-
Check if the table given exists in the database.
225+
Check if the given table exists in the database.
208226
209227
Set optional input ``sansTran`` to True to run command without an
210228
explicit transaction commit or rollback.

src/DatabaseLibrary/connection_manager.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,11 @@
1313
# limitations under the License.
1414

1515
import importlib
16+
from configparser import ConfigParser, NoOptionError, NoSectionError
1617
from dataclasses import dataclass
18+
from pathlib import Path
1719
from typing import Any, Dict, Optional
1820

19-
try:
20-
import ConfigParser
21-
except:
22-
import configparser as ConfigParser
23-
2421
from robot.api import logger
2522

2623

@@ -81,6 +78,35 @@ def __iter__(self):
8178
return iter(self._connections.values())
8279

8380

81+
class ConfigReader:
82+
def __init__(self, config_file: Optional[str], alias: str):
83+
if config_file is None:
84+
config_file = "./resources/db.cfg"
85+
self.alias = alias
86+
self.config = self._load_config(config_file)
87+
88+
@staticmethod
89+
def _load_config(config_file: str) -> Optional[ConfigParser]:
90+
config_path = Path(config_file)
91+
if not config_path.exists():
92+
return None
93+
config = ConfigParser()
94+
config.read([config_path])
95+
return config
96+
97+
def get(self, param: str) -> str:
98+
if self.config is None:
99+
raise ValueError(f"Required '{param}' parameter was not provided in keyword arguments.") from None
100+
try:
101+
return self.config.get(self.alias, param)
102+
except NoSectionError:
103+
raise ValueError(f"Configuration file does not have [{self.alias}] section.") from None
104+
except NoOptionError:
105+
raise ValueError(
106+
f"Required '{param}' parameter missing in both keyword arguments and configuration file."
107+
) from None
108+
109+
84110
class ConnectionManager:
85111
"""
86112
Connection Manager handles the connection & disconnection to the database.
@@ -153,18 +179,14 @@ def connect_to_database(
153179
| # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' |
154180
| Connect To Database | psycopg2 | my_db_test |
155181
"""
156-
157-
if dbConfigFile is None:
158-
dbConfigFile = "./resources/db.cfg"
159-
config = ConfigParser.ConfigParser()
160-
config.read([dbConfigFile])
161-
162-
dbapiModuleName = dbapiModuleName or config.get(alias, "dbapiModuleName")
163-
dbName = dbName or config.get(alias, "dbName")
164-
dbUsername = dbUsername or config.get(alias, "dbUsername")
165-
dbPassword = dbPassword if dbPassword is not None else config.get(alias, "dbPassword")
166-
dbHost = dbHost or config.get(alias, "dbHost") or "localhost"
167-
dbPort = int(dbPort or config.get(alias, "dbPort"))
182+
config = ConfigReader(dbConfigFile, alias)
183+
184+
dbapiModuleName = dbapiModuleName or config.get("dbapiModuleName")
185+
dbName = dbName or config.get("dbName")
186+
dbUsername = dbUsername or config.get("dbUsername")
187+
dbPassword = dbPassword if dbPassword is not None else config.get("dbPassword")
188+
dbHost = dbHost or config.get("dbHost") or "localhost"
189+
dbPort = int(dbPort if dbPort is not None else config.get("dbPort"))
168190

169191
if dbapiModuleName == "excel" or dbapiModuleName == "excelrw":
170192
db_api_module_name = "pyodbc"

test/tests/__init__.py

Whitespace-only changes.

test/tests/utests/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import re
2+
from pathlib import Path
3+
from unittest.mock import MagicMock, patch
4+
5+
import pytest
6+
7+
from DatabaseLibrary.connection_manager import ConnectionManager
8+
9+
TEST_DATA = Path(__file__).parent / "test_data"
10+
11+
12+
class TestConnectWithConfigFile:
13+
def test_connect_with_empty_config(self):
14+
conn_manager = ConnectionManager()
15+
config_path = str(TEST_DATA / "empty.cfg")
16+
with pytest.raises(ValueError, match=re.escape("Configuration file does not have [default] section.")):
17+
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)
18+
19+
def test_connect_no_params_no_config(self):
20+
conn_manager = ConnectionManager()
21+
with pytest.raises(ValueError, match="Required 'dbName' parameter was not provided in keyword arguments."):
22+
conn_manager.connect_to_database("my_client")
23+
24+
def test_connect_missing_option(self):
25+
conn_manager = ConnectionManager()
26+
config_path = str(TEST_DATA / "no_option.cfg")
27+
with pytest.raises(
28+
ValueError,
29+
match="Required 'dbPassword' parameter missing in both keyword arguments and configuration file.",
30+
):
31+
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)
32+
33+
def test_aliased_section(self):
34+
conn_manager = ConnectionManager()
35+
config_path = str(TEST_DATA / "alias.cfg")
36+
with patch("importlib.import_module", new=MagicMock()) as client:
37+
conn_manager.connect_to_database(
38+
"my_client",
39+
dbUsername="name",
40+
dbPassword="password",
41+
dbHost="host",
42+
dbPort=0,
43+
dbConfigFile=config_path,
44+
alias="alias2",
45+
)
46+
client.return_value.connect.assert_called_with(
47+
database="example", user="name", password="password", host="host", port=0
48+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[alias2]
2+
dbName = example

test/tests/utests/test_data/empty.cfg

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[default]
2+
dbName = example
3+
dbUsername = example

0 commit comments

Comments
 (0)