Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5950485d46 | ||
|
|
f349927a63 | ||
|
|
dfe8890598 | ||
|
|
d224876a8e | ||
|
|
17e8c76b94 | ||
|
|
9034a31cd6 | ||
|
|
523e61c9f7 | ||
|
|
cf575ded90 | ||
|
|
11a75d8532 | ||
|
|
6593e11332 |
@@ -1,7 +1,7 @@
|
||||
# MARK: Project info
|
||||
[project]
|
||||
name = "corelibs"
|
||||
version = "0.42.2"
|
||||
version = "0.43.2"
|
||||
description = "Collection of utils for Python scripts"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
||||
@@ -53,6 +53,9 @@ class SettingsLoader:
|
||||
# for check settings, abort flag
|
||||
self.__check_settings_abort: bool = False
|
||||
|
||||
# error messages for raise ValueError
|
||||
self.__error_msg: list[str] = []
|
||||
|
||||
# MARK: load settings
|
||||
def load_settings(
|
||||
self,
|
||||
@@ -87,6 +90,8 @@ class SettingsLoader:
|
||||
Returns:
|
||||
dict[str, str]: key = value list
|
||||
"""
|
||||
# reset error message list before run
|
||||
self.__error_msg = []
|
||||
# default set entries
|
||||
entry_set_empty: dict[str, str | None] = {}
|
||||
# entries that have to be split
|
||||
@@ -109,7 +114,7 @@ class SettingsLoader:
|
||||
if allow_not_exist is True:
|
||||
return {}
|
||||
raise ValueError(self.__print(
|
||||
f"[!] Cannot read [{config_id}] block in the {self.config_file}: {e}",
|
||||
f"[!] Cannot read [{config_id}] block in the file {self.config_file}: {e}",
|
||||
'CRITICAL'
|
||||
)) from e
|
||||
try:
|
||||
@@ -181,9 +186,8 @@ class SettingsLoader:
|
||||
# ignore error if arguments are set
|
||||
if not self.__check_arguments(config_validate, True):
|
||||
raise ValueError(self.__print(f"[!] Cannot find file: {self.config_file}", 'CRITICAL'))
|
||||
else:
|
||||
# base set
|
||||
settings[config_id] = {}
|
||||
# base set
|
||||
settings[config_id] = {}
|
||||
# make sure all are set
|
||||
# if we have arguments set, this override config settings
|
||||
error: bool = False
|
||||
@@ -274,7 +278,12 @@ class SettingsLoader:
|
||||
error = True
|
||||
self.__print(f"[!] Missing content entry for: {entry}", 'ERROR')
|
||||
if error is True:
|
||||
raise ValueError(self.__print("[!] Missing or incorrect settings data. Cannot proceed", 'CRITICAL'))
|
||||
raise ValueError(
|
||||
self.__print(
|
||||
"[!] Missing or incorrect settings data. Cannot proceed: " + "; ".join(self.__error_msg),
|
||||
'CRITICAL'
|
||||
)
|
||||
)
|
||||
# set empty
|
||||
for [entry, empty_set] in entry_set_empty.items():
|
||||
# if set, skip, else set to empty value
|
||||
@@ -568,6 +577,9 @@ class SettingsLoader:
|
||||
if self.log is None or self.always_print:
|
||||
if print_error:
|
||||
print(msg)
|
||||
if level == 'ERROR':
|
||||
# remove any prefix [!] for error message list
|
||||
self.__error_msg.append(msg.replace('[!] ', '').strip())
|
||||
return msg
|
||||
|
||||
|
||||
|
||||
@@ -7,10 +7,13 @@ from typing import Any, Sequence
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import csv
|
||||
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
|
||||
from corelibs.exceptions.csv_exceptions import (
|
||||
NoCsvReader, CompulsoryCsvHeaderCheckFailed, CsvHeaderDataMissing
|
||||
)
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
ENCODING_UTF8_SIG = 'utf-8-sig'
|
||||
DELIMITER = ","
|
||||
QUOTECHAR = '"'
|
||||
# type: _QuotingType
|
||||
@@ -27,6 +30,7 @@ class CsvWriter:
|
||||
file_name: Path,
|
||||
header_mapping: dict[str, str],
|
||||
header_order: list[str] | None = None,
|
||||
encoding: str = ENCODING,
|
||||
delimiter: str = DELIMITER,
|
||||
quotechar: str = QUOTECHAR,
|
||||
quoting: Any = QUOTING,
|
||||
@@ -38,6 +42,7 @@ class CsvWriter:
|
||||
self.__delimiter = delimiter
|
||||
self.__quotechar = quotechar
|
||||
self.__quoting = quoting
|
||||
self.__encoding = encoding
|
||||
self.csv_file_writer = self.__open_csv(header_order)
|
||||
|
||||
def __open_csv(self, header_order: list[str] | None) -> csv.DictWriter[str]:
|
||||
@@ -69,7 +74,8 @@ class CsvWriter:
|
||||
try:
|
||||
fp = open(
|
||||
self.__file_name,
|
||||
"w", encoding="utf-8"
|
||||
"w",
|
||||
encoding=self.__encoding
|
||||
)
|
||||
csv_file_writer = csv.DictWriter(
|
||||
fp,
|
||||
@@ -109,6 +115,7 @@ class CsvReader:
|
||||
self,
|
||||
file_name: Path,
|
||||
header_check: Sequence[str] | None = None,
|
||||
encoding: str = ENCODING,
|
||||
delimiter: str = DELIMITER,
|
||||
quotechar: str = QUOTECHAR,
|
||||
quoting: Any = QUOTING,
|
||||
@@ -118,6 +125,7 @@ class CsvReader:
|
||||
self.__delimiter = delimiter
|
||||
self.__quotechar = quotechar
|
||||
self.__quoting = quoting
|
||||
self.__encoding = encoding
|
||||
self.header: Sequence[str] | None = None
|
||||
self.csv_file_reader = self.__open_csv()
|
||||
|
||||
@@ -129,9 +137,16 @@ class CsvReader:
|
||||
csv.DictReader | None: _description_
|
||||
"""
|
||||
try:
|
||||
# if UTF style check if this is BOM
|
||||
if self.__encoding.lower().startswith('utf-') and is_bom_encoded(self.__file_name):
|
||||
bom_info = is_bom_encoded_info(self.__file_name)
|
||||
if bom_info['encoding'] == 'utf-8':
|
||||
self.__encoding = ENCODING_UTF8_SIG
|
||||
else:
|
||||
self.__encoding = bom_info['encoding'] or self.__encoding
|
||||
fp = open(
|
||||
self.__file_name,
|
||||
"r", encoding="utf-8"
|
||||
"r", encoding=self.__encoding
|
||||
)
|
||||
csv_file_reader = csv.DictReader(
|
||||
fp,
|
||||
|
||||
76
src/corelibs/db_handling/sql_main.py
Normal file
76
src/corelibs/db_handling/sql_main.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Main SQL base for any SQL calls
|
||||
This is a wrapper for SQLiteIO or other future DB Interfaces
|
||||
[Note: at the moment only SQLiteIO is implemented]
|
||||
- on class creation connection with ValueError on fail
|
||||
- connect method checks if already connected and warns
|
||||
- connection class fails with ValueError if not valid target is selected (SQL wrapper type)
|
||||
- connected check class method
|
||||
- a process class that returns data as list or False if end or error
|
||||
|
||||
TODO: adapt more CoreLibs DB IO class flow here
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from corelibs.debug_handling.debug_helpers import call_stack
|
||||
from corelibs.db_handling.sqlite_io import SQLiteIO
|
||||
if TYPE_CHECKING:
|
||||
from corelibs.logging_handling.log import Logger
|
||||
|
||||
|
||||
IDENT_SPLIT_CHARACTER: str = ':'
|
||||
|
||||
|
||||
class SQLMain:
|
||||
"""Main SQL interface class"""
|
||||
def __init__(self, log: 'Logger', db_ident: str):
|
||||
self.log = log
|
||||
self.dbh: SQLiteIO | None = None
|
||||
self.db_target: str | None = None
|
||||
self.connect(db_ident)
|
||||
if not self.connected():
|
||||
raise ValueError(f'Failed to connect to database [{call_stack()}]')
|
||||
|
||||
def connect(self, db_ident: str):
|
||||
"""setup basic connection"""
|
||||
if self.dbh is not None and self.dbh.conn is not None:
|
||||
self.log.warning(f"A database connection already exists for: {self.db_target} [{call_stack()}]")
|
||||
return
|
||||
self.db_target, db_dsn = db_ident.split(IDENT_SPLIT_CHARACTER)
|
||||
match self.db_target:
|
||||
case 'sqlite':
|
||||
# this is a Path only at the moment
|
||||
self.dbh = SQLiteIO(self.log, db_dsn, row_factory='Dict')
|
||||
case _:
|
||||
raise ValueError(f'SQL interface for {self.db_target} is not implemented [{call_stack()}]')
|
||||
if not self.dbh.db_connected():
|
||||
raise ValueError(f"DB Connection failed for: {self.db_target} [{call_stack()}]")
|
||||
|
||||
def close(self):
|
||||
"""close connection"""
|
||||
if self.dbh is None or not self.connected():
|
||||
return
|
||||
# self.log.info(f"Close DB Connection: {self.db_target} [{call_stack()}]")
|
||||
self.dbh.db_close()
|
||||
|
||||
def connected(self) -> bool:
|
||||
"""check connectuon"""
|
||||
if self.dbh is None or not self.dbh.db_connected():
|
||||
self.log.warning(f"No connection [{call_stack()}]")
|
||||
return False
|
||||
return True
|
||||
|
||||
def process_query(
|
||||
self, query: str, params: tuple[Any, ...] | None = None
|
||||
) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
|
||||
"""mini wrapper for execute query"""
|
||||
if self.dbh is not None:
|
||||
result = self.dbh.execute_query(query, params)
|
||||
if result is False:
|
||||
return False
|
||||
else:
|
||||
self.log.error(f"Problem connecting to db: {self.db_target} [{call_stack()}]")
|
||||
return False
|
||||
return result
|
||||
|
||||
# __END__
|
||||
0
src/corelibs/math_handling/__init__.py
Normal file
0
src/corelibs/math_handling/__init__.py
Normal file
35
src/corelibs/math_handling/math_helpers.py
Normal file
35
src/corelibs/math_handling/math_helpers.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Various math helpers
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def gcd(a: int, b: int):
|
||||
"""
|
||||
Calculate: Greatest Common Divisor
|
||||
|
||||
Arguments:
|
||||
a {int} -- _description_
|
||||
b {int} -- _description_
|
||||
|
||||
Returns:
|
||||
_type_ -- _description_
|
||||
"""
|
||||
return math.gcd(a, b)
|
||||
|
||||
|
||||
def lcd(a: int, b: int):
|
||||
"""
|
||||
Calculate: Least Common Denominator
|
||||
|
||||
Arguments:
|
||||
a {int} -- _description_
|
||||
b {int} -- _description_
|
||||
|
||||
Returns:
|
||||
_type_ -- _description_
|
||||
"""
|
||||
return math.lcm(a, b)
|
||||
|
||||
# __END__
|
||||
@@ -12,7 +12,7 @@ class EnumBase(CorelibsEnumBase):
|
||||
|
||||
.. deprecated::
|
||||
Use corelibs_enum_base.EnumBase instead
|
||||
DEPRECATED: Use corelibs_enum_base.EnumBase instead
|
||||
DEPRECATED: Use corelibs_enum_base.enum_base.EnumBase instead
|
||||
|
||||
lookup_any and from_any will return "EnumBase" and the sub class name
|
||||
run the return again to "from_any" to get a clean value, or cast it
|
||||
@@ -20,6 +20,6 @@ class EnumBase(CorelibsEnumBase):
|
||||
|
||||
|
||||
# At the module level, issue a deprecation warning
|
||||
warnings.warn("Use corelibs_enum_base.EnumBase instead", DeprecationWarning, stacklevel=2)
|
||||
warnings.warn("Use corelibs_enum_base.enum_base.EnumBase instead", DeprecationWarning, stacklevel=2)
|
||||
|
||||
# __EMD__
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing_extensions import deprecated
|
||||
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
|
||||
|
||||
|
||||
@deprecated("Use corelibs_enum_base.EnumBase instead")
|
||||
@deprecated("Use corelibs_enum_base.enum_base.EnumBase instead")
|
||||
class EnumBase(CorelibsEnumBase):
|
||||
"""
|
||||
base for enum
|
||||
|
||||
35
test-run/check_handling/regex_checks.py
Normal file
35
test-run/check_handling/regex_checks.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Test check andling for regex checks
|
||||
"""
|
||||
|
||||
import re
|
||||
from corelibs.check_handling.regex_constants import DOMAIN_WITH_LOCALHOST_REGEX
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Test regex checks
|
||||
"""
|
||||
test_domains = [
|
||||
"example.com",
|
||||
"localhost",
|
||||
"subdomain.localhost",
|
||||
"test.localhost.com",
|
||||
"some-domain.org"
|
||||
]
|
||||
|
||||
regex_domain_check = re.compile(DOMAIN_WITH_LOCALHOST_REGEX)
|
||||
print(f"REGEX: {DOMAIN_WITH_LOCALHOST_REGEX}")
|
||||
print(f"Check regex: {regex_domain_check.search('localhost')}")
|
||||
|
||||
for domain in test_domains:
|
||||
if regex_domain_check.search(domain):
|
||||
print(f"Matched: {domain}")
|
||||
else:
|
||||
print(f"Did not match: {domain}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# __END__
|
||||
@@ -37,3 +37,6 @@ email_bad=gii@bar.com
|
||||
[LoadTest]
|
||||
a.b.c=foo
|
||||
d:e:f=bar
|
||||
|
||||
[ErrorTest]
|
||||
some_value=42
|
||||
|
||||
@@ -125,6 +125,20 @@ def main():
|
||||
except ValueError as e:
|
||||
print(f"Could not load settings: {e}")
|
||||
|
||||
try:
|
||||
config_load = 'ErrorTest'
|
||||
config_data = sl.load_settings(
|
||||
config_load,
|
||||
{
|
||||
"some_value": [
|
||||
"check:string.email.basic",
|
||||
],
|
||||
}
|
||||
)
|
||||
print(f"[{config_load}] Load: {config_load} -> {dump_data(config_data)}")
|
||||
except ValueError as e:
|
||||
print(f"Could not load settings: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
139
test-run/db_handling/sql_main.py
Normal file
139
test-run/db_handling/sql_main.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
SQL Main wrapper test
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
import json
|
||||
from corelibs.debug_handling.dump_data import dump_data
|
||||
from corelibs.logging_handling.log import Log, Logger
|
||||
from corelibs.db_handling.sql_main import SQLMain
|
||||
|
||||
SCRIPT_PATH: Path = Path(__file__).resolve().parent
|
||||
ROOT_PATH: Path = SCRIPT_PATH
|
||||
DATABASE_DIR: Path = Path("database")
|
||||
LOG_DIR: Path = Path("log")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Comment
|
||||
"""
|
||||
log = Log(
|
||||
log_path=ROOT_PATH.joinpath(LOG_DIR, 'sqlite_main.log'),
|
||||
log_name="SQLite Main",
|
||||
log_settings={
|
||||
"log_level_console": 'DEBUG',
|
||||
"log_level_file": 'DEBUG',
|
||||
}
|
||||
)
|
||||
sql_main = SQLMain(
|
||||
log=Logger(log.get_logger_settings()),
|
||||
db_ident=f"sqlite:{ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_main.db')}"
|
||||
)
|
||||
if sql_main.connected():
|
||||
log.info("SQL Main connected successfully")
|
||||
else:
|
||||
log.error('SQL Main connection failed')
|
||||
if sql_main.dbh is None:
|
||||
log.error('SQL Main DBH instance is None')
|
||||
return
|
||||
|
||||
if sql_main.dbh.trigger_exists('trg_test_a_set_date_updated_on_update'):
|
||||
log.info("Trigger trg_test_a_set_date_updated_on_update exists")
|
||||
if sql_main.dbh.table_exists('test_a'):
|
||||
log.info("Table test_a exists, dropping for clean test")
|
||||
sql_main.dbh.execute_query("DROP TABLE test_a;")
|
||||
# create a dummy table
|
||||
table_sql = """
|
||||
CREATE TABLE IF NOT EXISTS test_a (
|
||||
test_a_id INTEGER PRIMARY KEY,
|
||||
date_created TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
|
||||
date_updated TEXT,
|
||||
uid TEXT NOT NULL UNIQUE,
|
||||
set_current_timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
text_a TEXT,
|
||||
content,
|
||||
int_a INTEGER,
|
||||
float_a REAL
|
||||
);
|
||||
"""
|
||||
|
||||
result = sql_main.dbh.execute_query(table_sql)
|
||||
log.debug(f"Create table result: {result}")
|
||||
trigger_sql = """
|
||||
CREATE TRIGGER trg_test_a_set_date_updated_on_update
|
||||
AFTER UPDATE ON test_a
|
||||
FOR EACH ROW
|
||||
WHEN OLD.date_updated IS NULL OR NEW.date_updated = OLD.date_updated
|
||||
BEGIN
|
||||
UPDATE test_a
|
||||
SET date_updated = (strftime('%Y-%m-%d %H:%M:%f', 'now'))
|
||||
WHERE test_a_id = NEW.test_a_id;
|
||||
END;
|
||||
"""
|
||||
result = sql_main.dbh.execute_query(trigger_sql)
|
||||
log.debug(f"Create trigger result: {result}")
|
||||
result = sql_main.dbh.meta_data_detail('test_a')
|
||||
log.debug(f"Table meta data detail: {dump_data(result)}")
|
||||
# INSERT DATA
|
||||
sql = """
|
||||
INSERT INTO test_a (uid, text_a, content, int_a, float_a)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
RETURNING test_a_id, uid;
|
||||
"""
|
||||
result = sql_main.dbh.execute_query(
|
||||
sql,
|
||||
(
|
||||
str(uuid4()),
|
||||
'Some text A',
|
||||
json.dumps({'foo': 'bar', 'number': 42}),
|
||||
123,
|
||||
123.456,
|
||||
)
|
||||
)
|
||||
log.debug(f"[1] Insert data result: {dump_data(result)}")
|
||||
__uid: str = ''
|
||||
if result is not False:
|
||||
# first one only of interest
|
||||
result = dict(result[0])
|
||||
__uid = str(result.get('uid', ''))
|
||||
# second insert
|
||||
result = sql_main.dbh.execute_query(
|
||||
sql,
|
||||
(
|
||||
str(uuid4()),
|
||||
'Some text A',
|
||||
json.dumps({'foo': 'bar', 'number': 42}),
|
||||
123,
|
||||
123.456,
|
||||
)
|
||||
)
|
||||
log.debug(f"[2] Insert data result: {dump_data(result)}")
|
||||
result = sql_main.dbh.execute_query("SELECT * FROM test_a;")
|
||||
log.debug(f"Select data result: {dump_data(result)}")
|
||||
result = sql_main.dbh.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
|
||||
log.debug(f"Fetch row result: {dump_data(result)}")
|
||||
sql = """
|
||||
UPDATE test_a
|
||||
SET text_a = ?
|
||||
WHERE uid = ?;
|
||||
"""
|
||||
result = sql_main.dbh.execute_query(
|
||||
sql,
|
||||
(
|
||||
'Some updated text A',
|
||||
__uid,
|
||||
)
|
||||
)
|
||||
log.debug(f"Update data result: {dump_data(result)}")
|
||||
result = sql_main.dbh.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
|
||||
log.debug(f"Fetch row after update result: {dump_data(result)}")
|
||||
|
||||
sql_main.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# __END__
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Main comment
|
||||
SQLite IO test
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
461
tests/unit/db_handling/test_sql_main.py
Normal file
461
tests/unit/db_handling/test_sql_main.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
PyTest: db_handling/sql_main
|
||||
Tests for SQLMain class - Main SQL interface wrapper
|
||||
|
||||
Note: Pylance warnings about "Redefining name from outer scope" in fixtures are expected.
|
||||
This is standard pytest fixture behavior where fixture parameters shadow fixture definitions.
|
||||
"""
|
||||
# pylint: disable=redefined-outer-name,too-many-public-methods,protected-access
|
||||
# pyright: reportUnknownParameterType=false, reportUnknownArgumentType=false
|
||||
# pyright: reportMissingParameterType=false, reportUnknownVariableType=false
|
||||
# pyright: reportArgumentType=false, reportGeneralTypeIssues=false
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from corelibs.db_handling.sql_main import SQLMain, IDENT_SPLIT_CHARACTER
|
||||
from corelibs.db_handling.sqlite_io import SQLiteIO
|
||||
|
||||
|
||||
# Test fixtures
|
||||
@pytest.fixture
|
||||
def mock_logger() -> MagicMock:
|
||||
"""Create a mock logger for testing"""
|
||||
logger = MagicMock()
|
||||
logger.debug = MagicMock()
|
||||
logger.info = MagicMock()
|
||||
logger.warning = MagicMock()
|
||||
logger.error = MagicMock()
|
||||
return logger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db_path(tmp_path: Path) -> Path:
|
||||
"""Create a temporary database file path"""
|
||||
return tmp_path / "test_database.db"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sqlite_io() -> Generator[MagicMock, None, None]:
|
||||
"""Create a mock SQLiteIO instance"""
|
||||
mock_io = MagicMock(spec=SQLiteIO)
|
||||
mock_io.conn = MagicMock()
|
||||
mock_io.db_connected = MagicMock(return_value=True)
|
||||
mock_io.db_close = MagicMock()
|
||||
mock_io.execute_query = MagicMock(return_value=[])
|
||||
yield mock_io
|
||||
|
||||
|
||||
# Test constant
|
||||
class TestConstants:
|
||||
"""Tests for module-level constants"""
|
||||
|
||||
def test_ident_split_character(self):
|
||||
"""Test that IDENT_SPLIT_CHARACTER is defined correctly"""
|
||||
assert IDENT_SPLIT_CHARACTER == ':'
|
||||
|
||||
|
||||
# Test SQLMain class initialization
|
||||
class TestSQLMainInit:
|
||||
"""Tests for SQLMain.__init__"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_successful_initialization_sqlite(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test successful initialization with SQLite"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
assert sql_main.log == mock_logger
|
||||
assert sql_main.dbh == mock_sqlite_instance
|
||||
assert sql_main.db_target == 'sqlite'
|
||||
mock_sqlite_class.assert_called_once_with(mock_logger, str(temp_db_path), row_factory='Dict')
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_initialization_connection_failure(self, mock_sqlite_class: MagicMock, mock_logger: MagicMock):
|
||||
"""Test initialization fails when connection cannot be established"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = None
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = 'sqlite:/path/to/db.db'
|
||||
with pytest.raises(ValueError, match='DB Connection failed for: sqlite'):
|
||||
SQLMain(mock_logger, db_ident)
|
||||
|
||||
def test_initialization_invalid_db_target(self, mock_logger: MagicMock):
|
||||
"""Test initialization with unsupported database target"""
|
||||
db_ident = 'postgresql:/path/to/db'
|
||||
with pytest.raises(ValueError, match='SQL interface for postgresql is not implemented'):
|
||||
SQLMain(mock_logger, db_ident)
|
||||
|
||||
def test_initialization_malformed_db_ident(self, mock_logger: MagicMock):
|
||||
"""Test initialization with malformed db_ident string"""
|
||||
db_ident = 'sqlite_no_colon'
|
||||
with pytest.raises(ValueError):
|
||||
SQLMain(mock_logger, db_ident)
|
||||
|
||||
|
||||
# Test SQLMain.connect method
|
||||
class TestSQLMainConnect:
|
||||
"""Tests for SQLMain.connect"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_connect_when_already_connected(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test connect warns when already connected"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
# Reset mock to check second call
|
||||
mock_logger.warning.reset_mock()
|
||||
|
||||
# Try to connect again
|
||||
sql_main.connect(f'sqlite:{temp_db_path}')
|
||||
|
||||
# Should have warned about existing connection
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert 'already exists' in str(mock_logger.warning.call_args)
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_connect_sqlite_success(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test successful SQLite connection"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
sql_main = SQLMain.__new__(SQLMain)
|
||||
sql_main.log = mock_logger
|
||||
sql_main.dbh = None
|
||||
sql_main.db_target = None
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main.connect(db_ident)
|
||||
|
||||
assert sql_main.db_target == 'sqlite'
|
||||
assert sql_main.dbh == mock_sqlite_instance
|
||||
mock_sqlite_class.assert_called_once_with(mock_logger, str(temp_db_path), row_factory='Dict')
|
||||
|
||||
def test_connect_unsupported_database(self, mock_logger: MagicMock):
|
||||
"""Test connect with unsupported database type"""
|
||||
sql_main = SQLMain.__new__(SQLMain)
|
||||
sql_main.log = mock_logger
|
||||
sql_main.dbh = None
|
||||
sql_main.db_target = None
|
||||
|
||||
db_ident = 'mysql:/path/to/db'
|
||||
with pytest.raises(ValueError, match='SQL interface for mysql is not implemented'):
|
||||
sql_main.connect(db_ident)
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_connect_db_connection_failed(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test connect raises error when DB connection fails"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
sql_main = SQLMain.__new__(SQLMain)
|
||||
sql_main.log = mock_logger
|
||||
sql_main.dbh = None
|
||||
sql_main.db_target = None
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
with pytest.raises(ValueError, match='DB Connection failed for: sqlite'):
|
||||
sql_main.connect(db_ident)
|
||||
|
||||
|
||||
# Test SQLMain.close method
|
||||
class TestSQLMainClose:
|
||||
"""Tests for SQLMain.close"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_close_successful(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test successful database close"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.db_close = MagicMock()
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
sql_main.close()
|
||||
|
||||
mock_sqlite_instance.db_close.assert_called_once()
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_close_when_not_connected(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test close when not connected does nothing"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.db_close = MagicMock()
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
# Change db_connected to return False to simulate disconnection
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
|
||||
|
||||
sql_main.close()
|
||||
|
||||
# Should not raise error and should exit early
|
||||
assert mock_sqlite_instance.db_close.call_count == 0
|
||||
|
||||
def test_close_when_dbh_is_none(self, mock_logger: MagicMock):
|
||||
"""Test close when dbh is None"""
|
||||
sql_main = SQLMain.__new__(SQLMain)
|
||||
sql_main.log = mock_logger
|
||||
sql_main.dbh = None
|
||||
sql_main.db_target = 'sqlite'
|
||||
|
||||
# Should not raise error
|
||||
sql_main.close()
|
||||
|
||||
|
||||
# Test SQLMain.connected method
|
||||
class TestSQLMainConnected:
|
||||
"""Tests for SQLMain.connected"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_connected_returns_true(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test connected returns True when connected"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
assert sql_main.connected() is True
|
||||
mock_logger.warning.assert_not_called()
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_connected_returns_false_when_not_connected(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test connected returns False and warns when not connected"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
# Reset warning calls from init
|
||||
mock_logger.warning.reset_mock()
|
||||
|
||||
# Change db_connected to return False to simulate disconnection
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
|
||||
|
||||
assert sql_main.connected() is False
|
||||
mock_logger.warning.assert_called_once()
|
||||
assert 'No connection' in str(mock_logger.warning.call_args)
|
||||
|
||||
def test_connected_returns_false_when_dbh_is_none(self, mock_logger: MagicMock):
|
||||
"""Test connected returns False when dbh is None"""
|
||||
sql_main = SQLMain.__new__(SQLMain)
|
||||
sql_main.log = mock_logger
|
||||
sql_main.dbh = None
|
||||
sql_main.db_target = 'sqlite'
|
||||
|
||||
assert sql_main.connected() is False
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
# Test SQLMain.process_query method
|
||||
class TestSQLMainProcessQuery:
|
||||
"""Tests for SQLMain.process_query"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_process_query_success_no_params(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test successful query execution without parameters"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
expected_result = [{'id': 1, 'name': 'test'}]
|
||||
mock_sqlite_instance.execute_query = MagicMock(return_value=expected_result)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
query = "SELECT * FROM test"
|
||||
result = sql_main.process_query(query)
|
||||
|
||||
assert result == expected_result
|
||||
mock_sqlite_instance.execute_query.assert_called_once_with(query, None)
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_process_query_success_with_params(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test successful query execution with parameters"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
expected_result = [{'id': 1, 'name': 'test'}]
|
||||
mock_sqlite_instance.execute_query = MagicMock(return_value=expected_result)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
query = "SELECT * FROM test WHERE id = ?"
|
||||
params = (1,)
|
||||
result = sql_main.process_query(query, params)
|
||||
|
||||
assert result == expected_result
|
||||
mock_sqlite_instance.execute_query.assert_called_once_with(query, params)
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_process_query_returns_false_on_error(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test query returns False when execute_query fails"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.execute_query = MagicMock(return_value=False)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
query = "SELECT * FROM nonexistent"
|
||||
result = sql_main.process_query(query)
|
||||
|
||||
assert result is False
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_process_query_dbh_is_none(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test query returns False when dbh is None"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
# Manually set dbh to None
|
||||
sql_main.dbh = None
|
||||
|
||||
query = "SELECT * FROM test"
|
||||
result = sql_main.process_query(query)
|
||||
|
||||
assert result is False
|
||||
mock_logger.error.assert_called_once()
|
||||
assert 'Problem connecting to db' in str(mock_logger.error.call_args)
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_process_query_returns_empty_list(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test query returns empty list when no results"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.execute_query = MagicMock(return_value=[])
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
query = "SELECT * FROM test WHERE 1=0"
|
||||
result = sql_main.process_query(query)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
# Integration-like tests
|
||||
class TestSQLMainIntegration:
|
||||
"""Integration-like tests for complete workflows"""
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_full_workflow_connect_query_close(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test complete workflow: connect, query, close"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.execute_query = MagicMock(return_value=[{'count': 5}])
|
||||
mock_sqlite_instance.db_close = MagicMock()
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
# Execute query
|
||||
result = sql_main.process_query("SELECT COUNT(*) as count FROM test")
|
||||
assert result == [{'count': 5}]
|
||||
|
||||
# Check connected
|
||||
assert sql_main.connected() is True
|
||||
|
||||
# Close connection
|
||||
sql_main.close()
|
||||
mock_sqlite_instance.db_close.assert_called_once()
|
||||
|
||||
@patch('corelibs.db_handling.sql_main.SQLiteIO')
|
||||
def test_multiple_queries_same_connection(
|
||||
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
|
||||
):
|
||||
"""Test multiple queries on the same connection"""
|
||||
mock_sqlite_instance = MagicMock()
|
||||
mock_sqlite_instance.conn = MagicMock()
|
||||
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
|
||||
mock_sqlite_instance.execute_query = MagicMock(side_effect=[
|
||||
[{'id': 1}],
|
||||
[{'id': 2}],
|
||||
[{'id': 3}]
|
||||
])
|
||||
mock_sqlite_class.return_value = mock_sqlite_instance
|
||||
|
||||
db_ident = f'sqlite:{temp_db_path}'
|
||||
sql_main = SQLMain(mock_logger, db_ident)
|
||||
|
||||
result1 = sql_main.process_query("SELECT * FROM test WHERE id = 1")
|
||||
result2 = sql_main.process_query("SELECT * FROM test WHERE id = 2")
|
||||
result3 = sql_main.process_query("SELECT * FROM test WHERE id = 3")
|
||||
|
||||
assert result1 == [{'id': 1}]
|
||||
assert result2 == [{'id': 2}]
|
||||
assert result3 == [{'id': 3}]
|
||||
assert mock_sqlite_instance.execute_query.call_count == 3
|
||||
|
||||
|
||||
# __END__
|
||||
0
tests/unit/math_handling/__init__.py
Normal file
0
tests/unit/math_handling/__init__.py
Normal file
121
tests/unit/math_handling/test_math_helpers.py
Normal file
121
tests/unit/math_handling/test_math_helpers.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Unit tests for math_helpers module
|
||||
"""
|
||||
|
||||
from corelibs.math_handling.math_helpers import gcd, lcd
|
||||
|
||||
|
||||
class TestGcd:
|
||||
"""Test cases for the gcd (Greatest Common Divisor) function"""
|
||||
|
||||
def test_gcd_basic_positive_numbers(self):
|
||||
"""Test GCD with basic positive numbers"""
|
||||
assert gcd(12, 8) == 4
|
||||
assert gcd(15, 10) == 5
|
||||
assert gcd(21, 14) == 7
|
||||
|
||||
def test_gcd_coprime_numbers(self):
|
||||
"""Test GCD with coprime numbers (GCD should be 1)"""
|
||||
assert gcd(13, 7) == 1
|
||||
assert gcd(17, 19) == 1
|
||||
assert gcd(25, 49) == 1
|
||||
|
||||
def test_gcd_same_numbers(self):
|
||||
"""Test GCD with same numbers"""
|
||||
assert gcd(5, 5) == 5
|
||||
assert gcd(100, 100) == 100
|
||||
|
||||
def test_gcd_with_zero(self):
|
||||
"""Test GCD when one or both numbers are zero"""
|
||||
assert gcd(0, 5) == 5
|
||||
assert gcd(5, 0) == 5
|
||||
assert gcd(0, 0) == 0
|
||||
|
||||
def test_gcd_with_one(self):
|
||||
"""Test GCD when one number is 1"""
|
||||
assert gcd(1, 5) == 1
|
||||
assert gcd(100, 1) == 1
|
||||
|
||||
def test_gcd_large_numbers(self):
|
||||
"""Test GCD with large numbers"""
|
||||
assert gcd(1000000, 500000) == 500000
|
||||
assert gcd(123456, 789012) == 12
|
||||
|
||||
def test_gcd_reversed_order(self):
|
||||
"""Test GCD is commutative (order doesn't matter)"""
|
||||
assert gcd(12, 8) == gcd(8, 12)
|
||||
assert gcd(100, 35) == gcd(35, 100)
|
||||
|
||||
def test_gcd_negative_numbers(self):
|
||||
"""Test GCD with negative numbers"""
|
||||
assert gcd(-12, 8) == 4
|
||||
assert gcd(12, -8) == 4
|
||||
assert gcd(-12, -8) == 4
|
||||
|
||||
def test_gcd_multiples(self):
|
||||
"""Test GCD when one number is a multiple of the other"""
|
||||
assert gcd(10, 5) == 5
|
||||
assert gcd(100, 25) == 25
|
||||
assert gcd(7, 21) == 7
|
||||
|
||||
|
||||
class TestLcd:
|
||||
"""Test cases for the lcd (Least Common Denominator/Multiple) function"""
|
||||
|
||||
def test_lcd_basic_positive_numbers(self):
|
||||
"""Test LCD with basic positive numbers"""
|
||||
assert lcd(4, 6) == 12
|
||||
assert lcd(3, 5) == 15
|
||||
assert lcd(12, 8) == 24
|
||||
|
||||
def test_lcd_coprime_numbers(self):
|
||||
"""Test LCD with coprime numbers (should be their product)"""
|
||||
assert lcd(7, 13) == 91
|
||||
assert lcd(11, 13) == 143
|
||||
assert lcd(5, 7) == 35
|
||||
|
||||
def test_lcd_same_numbers(self):
|
||||
"""Test LCD with same numbers"""
|
||||
assert lcd(5, 5) == 5
|
||||
assert lcd(100, 100) == 100
|
||||
|
||||
def test_lcd_with_one(self):
|
||||
"""Test LCD when one number is 1"""
|
||||
assert lcd(1, 5) == 5
|
||||
assert lcd(100, 1) == 100
|
||||
|
||||
def test_lcd_with_zero(self):
|
||||
"""Test LCD when one or both numbers are zero"""
|
||||
assert lcd(0, 5) == 0
|
||||
assert lcd(5, 0) == 0
|
||||
assert lcd(0, 0) == 0
|
||||
|
||||
def test_lcd_large_numbers(self):
|
||||
"""Test LCD with large numbers"""
|
||||
assert lcd(100, 150) == 300
|
||||
assert lcd(1000, 500) == 1000
|
||||
|
||||
def test_lcd_reversed_order(self):
|
||||
"""Test LCD is commutative (order doesn't matter)"""
|
||||
assert lcd(4, 6) == lcd(6, 4)
|
||||
assert lcd(12, 18) == lcd(18, 12)
|
||||
|
||||
def test_lcd_negative_numbers(self):
|
||||
"""Test LCD with negative numbers"""
|
||||
assert lcd(-4, 6) == 12
|
||||
assert lcd(4, -6) == 12
|
||||
assert lcd(-4, -6) == 12
|
||||
|
||||
def test_lcd_multiples(self):
|
||||
"""Test LCD when one number is a multiple of the other"""
|
||||
assert lcd(5, 10) == 10
|
||||
assert lcd(3, 9) == 9
|
||||
assert lcd(25, 100) == 100
|
||||
|
||||
def test_lcd_gcd_relationship(self):
|
||||
"""Test the mathematical relationship between LCD and GCD: lcd(a,b) * gcd(a,b) = a * b"""
|
||||
test_cases = [(12, 8), (15, 10), (21, 14), (100, 35)]
|
||||
for a, b in test_cases:
|
||||
assert lcd(a, b) * gcd(a, b) == a * b
|
||||
|
||||
# __END__
|
||||
Reference in New Issue
Block a user