Skip to content

Commit

Permalink
Merge branch 'feature/execute_parameters' of https://github.com/Marke…
Browse files Browse the repository at this point in the history
…tSquare/Robotframework-Database-Library into feature/execute_parameters
  • Loading branch information
amochin committed Nov 20, 2023
2 parents 79560d7 + 2981b16 commit e929dc0
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 28 deletions.
59 changes: 59 additions & 0 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
# This workflow will install Python dependencies
# and run unit tests for given OSes

name: Unit tests

on: [push, pull_request]

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: 'ubuntu-latest'
python-version: '3.7'
rf-version: '3.2.2'
- os: 'ubuntu-latest'
python-version: '3.8'
rf-version: '4.1.3'
- os: 'ubuntu-latest'
python-version: '3.9'
rf-version: '5.0.1'
- os: 'ubuntu-latest'
python-version: '3.10'
rf-version: '6.1.1'
- os: 'ubuntu-latest'
python-version: '3.11'
rf-version: '6.1.1'
- os: 'ubuntu-latest'
python-version: '3.12'
rf-version: '7.0a1'
runs-on: ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install robotframework==${{ matrix.rf-version }} coverage pytest
pip install .
- name: Run unit tests with coverage
run:
coverage run -m pytest

- name: Codecov
uses: codecov/codecov-action@v3
with:
name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }}
40 changes: 29 additions & 11 deletions src/DatabaseLibrary/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from typing import List, Optional

from robot.api import logger

Expand All @@ -22,7 +22,12 @@ class Assertion:
"""

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

def check_if_not_exists_in_database(
self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
self,
selectStatement: str,
sansTran: bool = False,
msg: Optional[str] = None,
alias: Optional[str] = None,
parameters: Optional[List] = None,
):
"""
This is the negation of `check_if_exists_in_database`.
Expand All @@ -71,14 +81,19 @@ def check_if_not_exists_in_database(
| Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True |
"""
logger.info(f"Executing : Check If Not Exists In Database | {selectStatement}")
query_results = self.query(selectStatement, sansTran, alias=alias)
query_results = self.query(selectStatement, sansTran, alias=alias, parameters=parameters)
if query_results:
raise AssertionError(
msg or f"Expected to have have no rows from '{selectStatement}', but got some rows: {query_results}"
)

def row_count_is_0(
self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
self,
selectStatement: str,
sansTran: bool = False,
msg: Optional[str] = None,
alias: Optional[str] = None,
parameters: Optional[List] = None,
):
"""
Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an
Expand All @@ -99,7 +114,7 @@ def row_count_is_0(
| Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | sansTran=True |
"""
logger.info(f"Executing : Row Count Is 0 | {selectStatement}")
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
if num_rows > 0:
raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'")

Expand All @@ -110,6 +125,7 @@ def row_count_is_equal_to_x(
sansTran: bool = False,
msg: Optional[str] = None,
alias: Optional[str] = None,
parameters: Optional[List] = None,
):
"""
Check if the number of rows returned from ``selectStatement`` is equal to the value submitted. If not, then this
Expand All @@ -129,7 +145,7 @@ def row_count_is_equal_to_x(
| Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | sansTran=True |
"""
logger.info(f"Executing : Row Count Is Equal To X | {selectStatement} | {numRows}")
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
if num_rows != int(numRows.encode("ascii")):
raise AssertionError(
msg or f"Expected {numRows} rows, but {num_rows} were returned from: '{selectStatement}'"
Expand All @@ -142,6 +158,7 @@ def row_count_is_greater_than_x(
sansTran: bool = False,
msg: Optional[str] = None,
alias: Optional[str] = None,
parameters: Optional[List] = None,
):
"""
Check if the number of rows returned from ``selectStatement`` is greater than the value submitted. If not, then
Expand All @@ -161,7 +178,7 @@ def row_count_is_greater_than_x(
| Row Count Is Greater Than X | SELECT id FROM person | 1 | sansTran=True |
"""
logger.info(f"Executing : Row Count Is Greater Than X | {selectStatement} | {numRows}")
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
if num_rows <= int(numRows.encode("ascii")):
raise AssertionError(
msg or f"Expected more than {numRows} rows, but {num_rows} were returned from '{selectStatement}'"
Expand All @@ -174,6 +191,7 @@ def row_count_is_less_than_x(
sansTran: bool = False,
msg: Optional[str] = None,
alias: Optional[str] = None,
parameters: Optional[List] = None,
):
"""
Check if the number of rows returned from ``selectStatement`` is less than the value submitted. If not, then this
Expand All @@ -194,7 +212,7 @@ def row_count_is_less_than_x(
"""
logger.info(f"Executing : Row Count Is Less Than X | {selectStatement} | {numRows}")
num_rows = self.row_count(selectStatement, sansTran, alias=alias)
num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters)
if num_rows >= int(numRows.encode("ascii")):
raise AssertionError(
msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'"
Expand All @@ -204,7 +222,7 @@ def table_must_exist(
self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None
):
"""
Check if the table given exists in the database.
Check if the given table exists in the database.
Set optional input ``sansTran`` to True to run command without an
explicit transaction commit or rollback.
Expand Down
56 changes: 39 additions & 17 deletions src/DatabaseLibrary/connection_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@
# limitations under the License.

import importlib
from configparser import ConfigParser, NoOptionError, NoSectionError
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, Optional

try:
import ConfigParser
except:
import configparser as ConfigParser

from robot.api import logger


Expand Down Expand Up @@ -81,6 +78,35 @@ def __iter__(self):
return iter(self._connections.values())


class ConfigReader:
def __init__(self, config_file: Optional[str], alias: str):
if config_file is None:
config_file = "./resources/db.cfg"
self.alias = alias
self.config = self._load_config(config_file)

@staticmethod
def _load_config(config_file: str) -> Optional[ConfigParser]:
config_path = Path(config_file)
if not config_path.exists():
return None
config = ConfigParser()
config.read([config_path])
return config

def get(self, param: str) -> str:
if self.config is None:
raise ValueError(f"Required '{param}' parameter was not provided in keyword arguments.") from None
try:
return self.config.get(self.alias, param)
except NoSectionError:
raise ValueError(f"Configuration file does not have [{self.alias}] section.") from None
except NoOptionError:
raise ValueError(
f"Required '{param}' parameter missing in both keyword arguments and configuration file."
) from None


class ConnectionManager:
"""
Connection Manager handles the connection & disconnection to the database.
Expand Down Expand Up @@ -153,18 +179,14 @@ def connect_to_database(
| # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' |
| Connect To Database | psycopg2 | my_db_test |
"""

if dbConfigFile is None:
dbConfigFile = "./resources/db.cfg"
config = ConfigParser.ConfigParser()
config.read([dbConfigFile])

dbapiModuleName = dbapiModuleName or config.get(alias, "dbapiModuleName")
dbName = dbName or config.get(alias, "dbName")
dbUsername = dbUsername or config.get(alias, "dbUsername")
dbPassword = dbPassword if dbPassword is not None else config.get(alias, "dbPassword")
dbHost = dbHost or config.get(alias, "dbHost") or "localhost"
dbPort = int(dbPort or config.get(alias, "dbPort"))
config = ConfigReader(dbConfigFile, alias)

dbapiModuleName = dbapiModuleName or config.get("dbapiModuleName")
dbName = dbName or config.get("dbName")
dbUsername = dbUsername or config.get("dbUsername")
dbPassword = dbPassword if dbPassword is not None else config.get("dbPassword")
dbHost = dbHost or config.get("dbHost") or "localhost"
dbPort = int(dbPort if dbPort is not None else config.get("dbPort"))

if dbapiModuleName == "excel" or dbapiModuleName == "excelrw":
db_api_module_name = "pyodbc"
Expand Down
Empty file added test/tests/__init__.py
Empty file.
Empty file added test/tests/utests/__init__.py
Empty file.
48 changes: 48 additions & 0 deletions test/tests/utests/test_connection_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import re
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from DatabaseLibrary.connection_manager import ConnectionManager

TEST_DATA = Path(__file__).parent / "test_data"


class TestConnectWithConfigFile:
def test_connect_with_empty_config(self):
conn_manager = ConnectionManager()
config_path = str(TEST_DATA / "empty.cfg")
with pytest.raises(ValueError, match=re.escape("Configuration file does not have [default] section.")):
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)

def test_connect_no_params_no_config(self):
conn_manager = ConnectionManager()
with pytest.raises(ValueError, match="Required 'dbName' parameter was not provided in keyword arguments."):
conn_manager.connect_to_database("my_client")

def test_connect_missing_option(self):
conn_manager = ConnectionManager()
config_path = str(TEST_DATA / "no_option.cfg")
with pytest.raises(
ValueError,
match="Required 'dbPassword' parameter missing in both keyword arguments and configuration file.",
):
conn_manager.connect_to_database("my_client", dbConfigFile=config_path)

def test_aliased_section(self):
conn_manager = ConnectionManager()
config_path = str(TEST_DATA / "alias.cfg")
with patch("importlib.import_module", new=MagicMock()) as client:
conn_manager.connect_to_database(
"my_client",
dbUsername="name",
dbPassword="password",
dbHost="host",
dbPort=0,
dbConfigFile=config_path,
alias="alias2",
)
client.return_value.connect.assert_called_with(
database="example", user="name", password="password", host="host", port=0
)
2 changes: 2 additions & 0 deletions test/tests/utests/test_data/alias.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[alias2]
dbName = example
Empty file.
3 changes: 3 additions & 0 deletions test/tests/utests/test_data/no_option.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[default]
dbName = example
dbUsername = example

0 comments on commit e929dc0

Please sign in to comment.