Compare commits

..

14 Commits

Author SHA1 Message Date
Clemens Schwaighofer
85063ea5df Move iterator handling functions to corelibs_iterator, corelibs_hash and corelibs_dump_data modules
Deprecate math helpers in favor of built-in math functions
2026-02-03 18:58:28 +09:00
Clemens Schwaighofer
31086fea53 Move json_handling to corelibs_json module 2026-02-03 14:03:17 +09:00
Clemens Schwaighofer
fd956095de Move SymmetricEncryption to corelibs_encryption module 2026-02-03 13:32:18 +09:00
Clemens Schwaighofer
a046d9f84c Move file handling to corelibs_file module 2026-02-03 11:42:57 +09:00
Clemens Schwaighofer
2e0d5aeb51 Move all debug handling into their own packages
dump data: corelibs_dump_data
stack trace: corelibs_stack_trace
profiling, timing, etc: corelibs_debug
2026-02-03 10:48:59 +09:00
Clemens Schwaighofer
28ab7c6f0c Move regex checks to corelibs_regex_checks module 2026-02-02 14:56:07 +09:00
Clemens Schwaighofer
d098eb58f3 v0.48.0: Update Caller class with better error handling and reporting 2026-01-30 18:20:21 +09:00
Clemens Schwaighofer
5319a059ad Update the caller class
- has now ErrorResponse return values instead of None on errors
- changed parameter cafile to ca_file and its position in the init method
- Proxy has ProxyConfig Typed Dict format

Tests updates to reflect those changes
2026-01-30 18:17:41 +09:00
Clemens Schwaighofer
163b8c4018 Update caller Class, backport from github manage script 2026-01-30 17:32:30 +09:00
Clemens Schwaighofer
6322b95068 v0.47.0: fingerprint update with fallback for str/int index overlaps 2026-01-27 17:15:32 +09:00
Clemens Schwaighofer
715ed1f9c2 Docblocks update in in iterator handling fingerprint 2026-01-27 17:14:31 +09:00
Clemens Schwaighofer
82a759dd21 Fix fingerprint with mixed int and str keys
Create a fallback hash function to handle mixed key types in dictionaries
and lists, ensuring consistent hashing across different data structures.

Fallback called is prefixed with "HO_" to indicate its usage.
2026-01-27 15:59:38 +09:00
Clemens Schwaighofer
fe913608c4 Fix iteration list helpers dict list type 2026-01-27 14:52:11 +09:00
Clemens Schwaighofer
79f9c5d1c6 iterator list helpers tests run cases updated 2026-01-27 14:51:25 +09:00
58 changed files with 558 additions and 10587 deletions

View File

@@ -1,13 +1,23 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.46.0"
version = "0.48.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-datetime>=1.0.1",
"corelibs-debug>=1.0.0",
"corelibs-dump-data>=1.0.0",
"corelibs-encryption>=1.0.0",
"corelibs-enum-base>=1.0.0",
"corelibs-file>=1.0.0",
"corelibs-hash>=1.0.0",
"corelibs-iterator>=1.0.0",
"corelibs-json>=1.0.0",
"corelibs-regex-checks>=1.0.0",
"corelibs-search>=1.0.0",
"corelibs-stack-trace>=1.0.0",
"corelibs-text-colors>=1.0.0",
"corelibs-var>=1.0.0",
"cryptography>=46.0.3",

View File

@@ -3,8 +3,20 @@ List of regex compiled strings that can be used
"""
import re
from warnings import warn, deprecated
from corelibs_regex_checks.regex_constants import (
compile_re as compile_re_ng,
SUB_EMAIL_BASIC_REGEX as SUB_EMAIL_BASIC_REGEX_NG,
EMAIL_BASIC_REGEX as EMAIL_BASIC_REGEX_NG,
NAME_EMAIL_SIMPLE_REGEX as NAME_EMAIL_SIMPLE_REGEX_NG,
NAME_EMAIL_BASIC_REGEX as NAME_EMAIL_BASIC_REGEX_NG,
DOMAIN_WITH_LOCALHOST_REGEX as DOMAIN_WITH_LOCALHOST_REGEX_NG,
DOMAIN_WITH_LOCALHOST_PORT_REGEX as DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG,
DOMAIN_REGEX as DOMAIN_REGEX_NG
)
@deprecated("Use corelibs_regex_checks.regex_constants.compile_re instead")
def compile_re(reg: str) -> re.Pattern[str]:
"""
compile a regex with verbose flag
@@ -15,40 +27,25 @@ def compile_re(reg: str) -> re.Pattern[str]:
Returns:
re.Pattern[str] -- _description_
"""
return re.compile(reg, re.VERBOSE)
return compile_re_ng(reg)
# email regex
SUB_EMAIL_BASIC_REGEX: str = r"""
[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}
@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}
"""
EMAIL_BASIC_REGEX = rf"^{SUB_EMAIL_BASIC_REGEX}$"
SUB_EMAIL_BASIC_REGEX = SUB_EMAIL_BASIC_REGEX_NG
EMAIL_BASIC_REGEX = EMAIL_BASIC_REGEX_NG
# name + email regex for email sending type like "foo bar" <email@mail.com>
NAME_EMAIL_SIMPLE_REGEX = r"""
^\s*(?:"(?P<name1>[^"]+)"\s*<(?P<email1>[^>]+)>|
(?P<name2>.+?)\s*<(?P<email2>[^>]+)>|
<(?P<email3>[^>]+)>|
(?P<email4>[^\s<>]+))\s*$
"""
NAME_EMAIL_SIMPLE_REGEX = NAME_EMAIL_SIMPLE_REGEX_NG
# name + email with the basic regex set
NAME_EMAIL_BASIC_REGEX = rf"""
^\s*(?:
"(?P<name1>[^"]+)"\s*<(?P<email1>{SUB_EMAIL_BASIC_REGEX})>|
(?P<name2>.+?)\s*<(?P<email2>{SUB_EMAIL_BASIC_REGEX})>|
<(?P<email3>{SUB_EMAIL_BASIC_REGEX})>|
(?P<email4>{SUB_EMAIL_BASIC_REGEX})
)\s*$
"""
NAME_EMAIL_BASIC_REGEX = NAME_EMAIL_BASIC_REGEX_NG
# Domain regex with localhost
DOMAIN_WITH_LOCALHOST_REGEX: str = r"""
^(?:localhost|(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,})$
"""
DOMAIN_WITH_LOCALHOST_REGEX = DOMAIN_WITH_LOCALHOST_REGEX_NG
# domain regex with loclhost and optional port
DOMAIN_WITH_LOCALHOST_PORT_REGEX: str = r"""
^(?:localhost|(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,})(?::\d+)?$
"""
DOMAIN_WITH_LOCALHOST_PORT_REGEX = DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG
# Domain, no localhost
DOMAIN_REGEX: str = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,}$"
DOMAIN_REGEX = DOMAIN_REGEX_NG
# At the module level, issue a deprecation warning
warn("Use corelibs_regex_checks.regex_constants instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,22 +2,26 @@
List of regex compiled strings that can be used
"""
from corelibs.check_handling.regex_constants import (
compile_re,
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX
import warnings
from corelibs_regex_checks.regex_constants_compiled import (
COMPILED_EMAIL_BASIC_REGEX as COMPILED_EMAIL_BASIC_REGEX_NG,
COMPILED_NAME_EMAIL_SIMPLE_REGEX as COMPILED_NAME_EMAIL_SIMPLE_REGEX_NG,
COMPILED_NAME_EMAIL_BASIC_REGEX as COMPILED_NAME_EMAIL_BASIC_REGEX_NG,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX as COMPILED_DOMAIN_WITH_LOCALHOST_REGEX_NG,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX as COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG,
COMPILED_DOMAIN_REGEX as COMPILED_DOMAIN_REGEX_NG
)
# all above in compiled form
COMPILED_EMAIL_BASIC_REGEX = compile_re(EMAIL_BASIC_REGEX)
COMPILED_NAME_EMAIL_SIMPLE_REGEX = compile_re(NAME_EMAIL_SIMPLE_REGEX)
COMPILED_NAME_EMAIL_BASIC_REGEX = compile_re(NAME_EMAIL_BASIC_REGEX)
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX = compile_re(DOMAIN_WITH_LOCALHOST_REGEX)
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX = compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX)
COMPILED_DOMAIN_REGEX = compile_re(DOMAIN_REGEX)
COMPILED_EMAIL_BASIC_REGEX = COMPILED_EMAIL_BASIC_REGEX_NG
COMPILED_NAME_EMAIL_SIMPLE_REGEX = COMPILED_NAME_EMAIL_SIMPLE_REGEX_NG
COMPILED_NAME_EMAIL_BASIC_REGEX = COMPILED_NAME_EMAIL_BASIC_REGEX_NG
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX_NG
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX = COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG
COMPILED_DOMAIN_REGEX = COMPILED_DOMAIN_REGEX_NG
# At the module level, issue a deprecation warning
warnings.warn("Use corelibs_regex_checks.regex_constants_compiled instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -12,7 +12,7 @@ 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_stack_trace.stack import call_stack
from corelibs.db_handling.sqlite_io import SQLiteIO
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger

View File

@@ -8,7 +8,7 @@ also method names are subject to change
from pathlib import Path
from typing import Any, Literal, TYPE_CHECKING
import sqlite3
from corelibs.debug_handling.debug_helpers import call_stack
from corelibs_stack_trace.stack import call_stack
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger

View File

@@ -2,16 +2,16 @@
Various debug helpers
"""
import traceback
import os
import sys
from warnings import deprecated
from typing import Tuple, Type
from types import TracebackType
from corelibs_stack_trace.stack import call_stack as call_stack_ng, exception_stack as exception_stack_ng
# _typeshed.OptExcInfo
OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType]
@deprecated("Use corelibs_stack_trace.stack.call_stack instead")
def call_stack(
start: int = 0,
skip_last: int = -1,
@@ -31,23 +31,15 @@ def call_stack(
Returns:
str -- _description_
"""
# stack = traceback.extract_stack()[start:depth]
# how many of the last entries we skip (so we do not get self), default is -1
# start cannot be negative
if skip_last > 0:
skip_last = skip_last * -1
stack = traceback.extract_stack()
__stack = stack[start:skip_last]
# start possible to high, reset start to 0
if not __stack and reset_start_if_empty:
start = 0
__stack = stack[start:skip_last]
if not separator:
separator = ' -> '
# print(f"* HERE: {dump_data(stack)}")
return f"{separator}".join(f"{os.path.basename(f.filename)}:{f.name}:{f.lineno}" for f in __stack)
return call_stack_ng(
start=start,
skip_last=skip_last,
separator=separator,
reset_start_if_empty=reset_start_if_empty
)
@deprecated("Use corelibs_stack_trace.stack.exception_stack instead")
def exception_stack(
exc_stack: OptExcInfo | None = None,
separator: str = ' -> '
@@ -62,15 +54,9 @@ def exception_stack(
Returns:
str -- _description_
"""
if exc_stack is not None:
_, _, exc_traceback = exc_stack
else:
exc_traceback = None
_, _, exc_traceback = sys.exc_info()
stack = traceback.extract_tb(exc_traceback)
if not separator:
separator = ' -> '
# print(f"* HERE: {dump_data(stack)}")
return f"{separator}".join(f"{os.path.basename(f.filename)}:{f.name}:{f.lineno}" for f in stack)
return exception_stack_ng(
exc_stack=exc_stack,
separator=separator
)
# __END__

View File

@@ -2,10 +2,12 @@
dict dump as JSON formatted
"""
import json
from warnings import deprecated
from typing import Any
from corelibs_dump_data.dump_data import dump_data as dump_data_ng
@deprecated("Use corelibs_dump_data.dump_data.dump_data instead")
def dump_data(data: Any, use_indent: bool = True) -> str:
"""
dump formated output from dict/list
@@ -16,7 +18,6 @@ def dump_data(data: Any, use_indent: bool = True) -> str:
Returns:
str: _description_
"""
indent = 4 if use_indent else None
return json.dumps(data, indent=indent, ensure_ascii=False, default=str)
return dump_data_ng(data=data, use_indent=use_indent)
# __END__

View File

@@ -4,123 +4,40 @@ Profile memory usage in Python
# https://docs.python.org/3/library/tracemalloc.html
import os
import time
import tracemalloc
import linecache
from typing import Tuple
from tracemalloc import Snapshot
import psutil
from warnings import warn, deprecated
from typing import TYPE_CHECKING
from corelibs_debug.profiling import display_top as display_top_ng, display_top_str, Profiling as CoreLibsProfiling
if TYPE_CHECKING:
from tracemalloc import Snapshot
def display_top(snapshot: Snapshot, key_type: str = 'lineno', limit: int = 10) -> str:
@deprecated("Use corelibs_debug.profiling.display_top_str with data from display_top instead")
def display_top(snapshot: 'Snapshot', key_type: str = 'lineno', limit: int = 10) -> str:
"""
Print tracmalloc stats
https://docs.python.org/3/library/tracemalloc.html#pretty-top
Args:
snapshot (Snapshot): _description_
snapshot ('Snapshot'): _description_
key_type (str, optional): _description_. Defaults to 'lineno'.
limit (int, optional): _description_. Defaults to 10.
"""
snapshot = snapshot.filter_traces((
tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
tracemalloc.Filter(False, "<unknown>"),
))
top_stats = snapshot.statistics(key_type)
profiler_msg = f"Top {limit} lines"
for index, stat in enumerate(top_stats[:limit], 1):
frame = stat.traceback[0]
# replace "/path/to/module/file.py" with "module/file.py"
filename = os.sep.join(frame.filename.split(os.sep)[-2:])
profiler_msg += f"#{index}: {filename}:{frame.lineno}: {(stat.size / 1024):.1f} KiB"
line = linecache.getline(frame.filename, frame.lineno).strip()
if line:
profiler_msg += f" {line}"
other = top_stats[limit:]
if other:
size = sum(stat.size for stat in other)
profiler_msg += f"{len(other)} other: {(size / 1024):.1f} KiB"
total = sum(stat.size for stat in top_stats)
profiler_msg += f"Total allocated size: {(total / 1024):.1f} KiB"
return profiler_msg
return display_top_str(
display_top_ng(
snapshot=snapshot,
key_type=key_type,
limit=limit
)
)
class Profiling:
class Profiling(CoreLibsProfiling):
"""
Profile memory usage and elapsed time for some block
Based on: https://stackoverflow.com/a/53301648
"""
def __init__(self):
# profiling id
self.__ident: str = ''
# memory
self.__rss_before: int = 0
self.__vms_before: int = 0
# self.shared_before: int = 0
self.__rss_used: int = 0
self.__vms_used: int = 0
# self.shared_used: int = 0
# time
self.__call_start: float = 0
self.__elapsed = 0
def __get_process_memory(self) -> Tuple[int, int]:
process = psutil.Process(os.getpid())
mi = process.memory_info()
# macos does not have mi.shared
return mi.rss, mi.vms
def __elapsed_since(self) -> str:
elapsed = time.time() - self.__call_start
if elapsed < 1:
return str(round(elapsed * 1000, 2)) + "ms"
if elapsed < 60:
return str(round(elapsed, 2)) + "s"
if elapsed < 3600:
return str(round(elapsed / 60, 2)) + "min"
return str(round(elapsed / 3600, 2)) + "hrs"
def __format_bytes(self, bytes_data: int) -> str:
if abs(bytes_data) < 1000:
return str(bytes_data) + "B"
if abs(bytes_data) < 1e6:
return str(round(bytes_data / 1e3, 2)) + "kB"
if abs(bytes_data) < 1e9:
return str(round(bytes_data / 1e6, 2)) + "MB"
return str(round(bytes_data / 1e9, 2)) + "GB"
def start_profiling(self, ident: str) -> None:
"""
start the profiling
"""
self.__ident = ident
self.__rss_before, self.__vms_before = self.__get_process_memory()
self.__call_start = time.time()
def end_profiling(self) -> None:
"""
end the profiling
"""
if self.__rss_before == 0 and self.__vms_before == 0:
print("start_profile() was not called, output will be negative")
self.__elapsed = self.__elapsed_since()
__rss_after, __vms_after = self.__get_process_memory()
self.__rss_used = __rss_after - self.__rss_before
self.__vms_used = __vms_after - self.__vms_before
def print_profiling(self) -> str:
"""
print the profiling time
"""
return (
f"Profiling: {self.__ident:>20} "
f"RSS: {self.__format_bytes(self.__rss_used):>8} | "
f"VMS: {self.__format_bytes(self.__vms_used):>8} | "
f"time: {self.__elapsed:>8}"
)
warn("Use corelibs_debug.profiling.Profiling instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -5,109 +5,16 @@ Returns:
Timer: class timer for basic time run calculations
"""
from datetime import datetime, timedelta
from warnings import warn
from corelibs_debug.timer import Timer as CorelibsTimer
class Timer:
class Timer(CorelibsTimer):
"""
get difference between start and end date/time
"""
def __init__(self):
"""
init new start time and set end time to None
"""
self._overall_start_time = datetime.now()
self._overall_end_time = None
self._overall_run_time = None
self._start_time = datetime.now()
self._end_time = None
self._run_time = None
# MARK: overall run time
def overall_run_time(self) -> timedelta:
"""
overall run time difference from class launch to call of this function
Returns:
timedelta: _description_
"""
self._overall_end_time = datetime.now()
self._overall_run_time = self._overall_end_time - self._overall_start_time
return self._overall_run_time
def get_overall_start_time(self) -> datetime:
"""
get set start time
Returns:
datetime: _description_
"""
return self._overall_start_time
def get_overall_end_time(self) -> datetime | None:
"""
get set end time or None for not set
Returns:
datetime|None: _description_
"""
return self._overall_end_time
def get_overall_run_time(self) -> timedelta | None:
"""
get run time or None if run time was not called
Returns:
datetime|None: _description_
"""
return self._overall_run_time
# MARK: set run time
def run_time(self) -> timedelta:
"""
difference between start time and current time
Returns:
datetime: _description_
"""
self._end_time = datetime.now()
self._run_time = self._end_time - self._start_time
return self._run_time
def reset_run_time(self):
"""
reset start/end and run tine
"""
self._start_time = datetime.now()
self._end_time = None
self._run_time = None
def get_start_time(self) -> datetime:
"""
get set start time
Returns:
datetime: _description_
"""
return self._start_time
def get_end_time(self) -> datetime | None:
"""
get set end time or None for not set
Returns:
datetime|None: _description_
"""
return self._end_time
def get_run_time(self) -> timedelta | None:
"""
get run time or None if run time was not called
Returns:
datetime|None: _description_
"""
return self._run_time
warn("Use corelibs_debug.timer.Timer instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,11 +2,18 @@
Various small helpers for data writing
"""
from warnings import deprecated
from typing import TYPE_CHECKING
from corelibs_debug.writeline import (
write_l as write_l_ng, pr_header as pr_header_ng,
pr_title as pr_title_ng, pr_open as pr_open_ng,
pr_close as pr_close_ng, pr_act as pr_act_ng
)
if TYPE_CHECKING:
from io import TextIOWrapper, StringIO
@deprecated("Use corelibs_debug.writeline.write_l instead")
def write_l(line: str, fpl: 'TextIOWrapper | StringIO | None' = None, print_line: bool = False):
"""
Write a line to screen and to output file
@@ -15,23 +22,30 @@ def write_l(line: str, fpl: 'TextIOWrapper | StringIO | None' = None, print_line
line (String): Line to write
fpl (Resource): file handler resource, if none write only to console
"""
if print_line is True:
print(line)
if fpl is not None:
fpl.write(line + "\n")
return write_l_ng(
line=line,
fpl=fpl,
print_line=print_line
)
# progress printers
@deprecated("Use corelibs_debug.writeline.pr_header instead")
def pr_header(tag: str, marker_string: str = '#', width: int = 35):
"""_summary_
Args:
tag (str): _description_
"""
print(f" {marker_string} {tag:^{width}} {marker_string}")
return pr_header_ng(
tag=tag,
marker_string=marker_string,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_title instead")
def pr_title(tag: str, prefix_string: str = '|', space_filler: str = '.', width: int = 35):
"""_summary_
@@ -39,9 +53,15 @@ def pr_title(tag: str, prefix_string: str = '|', space_filler: str = '.', width:
tag (str): _description_
prefix_string (str, optional): _description_. Defaults to '|'.
"""
print(f" {prefix_string} {tag:{space_filler}<{width}}:", flush=True)
return pr_title_ng(
tag=tag,
prefix_string=prefix_string,
space_filler=space_filler,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_open instead")
def pr_open(tag: str, prefix_string: str = '|', space_filler: str = '.', width: int = 35):
"""
writen progress open line with tag
@@ -50,9 +70,15 @@ def pr_open(tag: str, prefix_string: str = '|', space_filler: str = '.', width:
tag (str): _description_
prefix_string (str): prefix string. Default: '|'
"""
print(f" {prefix_string} {tag:{space_filler}<{width}} [", end="", flush=True)
return pr_open_ng(
tag=tag,
prefix_string=prefix_string,
space_filler=space_filler,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_close instead")
def pr_close(tag: str = ''):
"""
write the close tag with new line
@@ -60,9 +86,10 @@ def pr_close(tag: str = ''):
Args:
tag (str, optional): _description_. Defaults to ''.
"""
print(f"{tag}]", flush=True)
return pr_close_ng(tag=tag)
@deprecated("Use corelibs_debug.writeline.pr_act instead")
def pr_act(act: str = "."):
"""
write progress character
@@ -70,6 +97,6 @@ def pr_act(act: str = "."):
Args:
act (str, optional): _description_. Defaults to ".".
"""
print(f"{act}", end="", flush=True)
return pr_act_ng(act=act)
# __EMD__

View File

@@ -4,24 +4,11 @@ Will be moved to CoreLibs
TODO: set key per encryption run
"""
import os
import json
import base64
import hashlib
from typing import TypedDict, cast
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import warnings
from corelibs_encryption.symmetric import SymmetricEncryption as CorelibsSymmetricEncryption
class PackageData(TypedDict):
"""encryption package"""
encrypted_data: str
salt: str
key_hash: str
class SymmetricEncryption:
class SymmetricEncryption(CorelibsSymmetricEncryption):
"""
simple encryption
@@ -29,124 +16,7 @@ class SymmetricEncryption:
key from the password to decrypt
"""
def __init__(self, password: str):
if not password:
raise ValueError("A password must be set")
self.password = password
self.password_hash = hashlib.sha256(password.encode('utf-8')).hexdigest()
def __derive_key_from_password(self, password: str, salt: bytes) -> bytes:
_password = password.encode('utf-8')
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(_password))
return key
def __encrypt_with_metadata(self, data: str | bytes) -> PackageData:
"""Encrypt data and include salt if password-based"""
# convert to bytes (for encoding)
if isinstance(data, str):
data = data.encode('utf-8')
# generate salt and key from password
salt = os.urandom(16)
key = self.__derive_key_from_password(self.password, salt)
# init the cypher suit
cipher_suite = Fernet(key)
encrypted_data = cipher_suite.encrypt(data)
# If using password, include salt in the result
return {
'encrypted_data': base64.urlsafe_b64encode(encrypted_data).decode('utf-8'),
'salt': base64.urlsafe_b64encode(salt).decode('utf-8'),
'key_hash': hashlib.sha256(key).hexdigest()
}
def encrypt_with_metadata(self, data: str | bytes, return_as: str = 'str') -> str | bytes | PackageData:
"""encrypt with metadata, but returns data in string"""
match return_as:
case 'str':
return self.encrypt_with_metadata_return_str(data)
case 'json':
return self.encrypt_with_metadata_return_str(data)
case 'bytes':
return self.encrypt_with_metadata_return_bytes(data)
case 'dict':
return self.encrypt_with_metadata_return_dict(data)
case _:
# default is string json
return self.encrypt_with_metadata_return_str(data)
def encrypt_with_metadata_return_dict(self, data: str | bytes) -> PackageData:
"""encrypt with metadata, but returns data as PackageData dict"""
return self.__encrypt_with_metadata(data)
def encrypt_with_metadata_return_str(self, data: str | bytes) -> str:
"""encrypt with metadata, but returns data in string"""
return json.dumps(self.__encrypt_with_metadata(data))
def encrypt_with_metadata_return_bytes(self, data: str | bytes) -> bytes:
"""encrypt with metadata, but returns data in bytes"""
return json.dumps(self.__encrypt_with_metadata(data)).encode('utf-8')
def decrypt_with_metadata(self, encrypted_package: str | bytes | PackageData, password: str | None = None) -> str:
"""Decrypt data that may include metadata"""
try:
# Try to parse as JSON (password-based encryption)
if isinstance(encrypted_package, bytes):
package_data = cast(PackageData, json.loads(encrypted_package.decode('utf-8')))
elif isinstance(encrypted_package, str):
package_data = cast(PackageData, json.loads(str(encrypted_package)))
else:
package_data = encrypted_package
encrypted_data = base64.urlsafe_b64decode(package_data['encrypted_data'])
salt = base64.urlsafe_b64decode(package_data['salt'])
pwd = password or self.password
key = self.__derive_key_from_password(pwd, salt)
if package_data['key_hash'] != hashlib.sha256(key).hexdigest():
raise ValueError("Key hash is not matching, possible invalid password")
cipher_suite = Fernet(key)
decrypted_data = cipher_suite.decrypt(encrypted_data)
except (json.JSONDecodeError, KeyError, UnicodeDecodeError) as e:
raise ValueError(f"Invalid encrypted package format {e}") from e
return decrypted_data.decode('utf-8')
@staticmethod
def encrypt_data(data: str | bytes, password: str) -> str:
"""
Static method to encrypt some data
Arguments:
data {str | bytes} -- _description_
password {str} -- _description_
Returns:
str -- _description_
"""
encryptor = SymmetricEncryption(password)
return encryptor.encrypt_with_metadata_return_str(data)
@staticmethod
def decrypt_data(data: str | bytes | PackageData, password: str) -> str:
"""
Static method to decrypt some data
Arguments:
data {str | bytes | PackageData} -- _description_
password {str} -- _description_
Returns:
str -- _description_
"""
decryptor = SymmetricEncryption(password)
return decryptor.decrypt_with_metadata(data, password=password)
warnings.warn("Use corelibs_encryption.symmetric.SymmetricEncryption instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,19 +2,16 @@
File check if BOM encoded, needed for CSV load
"""
from warnings import deprecated
from pathlib import Path
from typing import TypedDict
class BomEncodingInfo(TypedDict):
"""BOM encoding info"""
has_bom: bool
bom_type: str | None
encoding: str | None
bom_length: int
bom_pattern: bytes | None
from corelibs_file.file_bom_encoding import (
is_bom_encoded as is_bom_encoding_ng,
get_bom_encoding_info,
BomEncodingInfo
)
@deprecated("Use corelibs_file.file_bom_encoding.is_bom_encoded instead")
def is_bom_encoded(file_path: Path) -> bool:
"""
Detect if a file is BOM encoded
@@ -25,9 +22,10 @@ def is_bom_encoded(file_path: Path) -> bool:
Returns:
bool: True if file has BOM, False otherwise
"""
return is_bom_encoded_info(file_path)['has_bom']
return is_bom_encoding_ng(file_path)
@deprecated("Use corelibs_file.file_bom_encoding.get_bom_encoding_info instead")
def is_bom_encoded_info(file_path: Path) -> BomEncodingInfo:
"""
Enhanced BOM detection with additional file analysis
@@ -38,38 +36,7 @@ def is_bom_encoded_info(file_path: Path) -> BomEncodingInfo:
Returns:
dict: Comprehensive BOM and encoding information
"""
try:
# Read first 1024 bytes for analysis
with open(file_path, 'rb') as f:
header = f.read(4)
bom_patterns = {
b'\xef\xbb\xbf': ('UTF-8', 'utf-8', 3),
b'\xff\xfe\x00\x00': ('UTF-32 LE', 'utf-32-le', 4),
b'\x00\x00\xfe\xff': ('UTF-32 BE', 'utf-32-be', 4),
b'\xff\xfe': ('UTF-16 LE', 'utf-16-le', 2),
b'\xfe\xff': ('UTF-16 BE', 'utf-16-be', 2),
}
for bom_pattern, (encoding_name, encoding, length) in bom_patterns.items():
if header.startswith(bom_pattern):
return {
'has_bom': True,
'bom_type': encoding_name,
'encoding': encoding,
'bom_length': length,
'bom_pattern': bom_pattern
}
return {
'has_bom': False,
'bom_type': None,
'encoding': None,
'bom_length': 0,
'bom_pattern': None
}
except Exception as e:
raise ValueError(f"Error checking BOM encoding: {e}") from e
return get_bom_encoding_info(file_path)
# __END__

View File

@@ -2,10 +2,13 @@
crc handlers for file CRC
"""
import zlib
from warnings import deprecated
from pathlib import Path
from corelibs_file.file_crc import file_crc as file_crc_ng
from corelibs_file.file_handling import get_file_name
@deprecated("Use corelibs_file.file_crc.file_crc instead")
def file_crc(file_path: Path) -> str:
"""
With for loop and buffer, create file crc32
@@ -16,13 +19,10 @@ def file_crc(file_path: Path) -> str:
Returns:
str: file crc32
"""
crc = 0
with open(file_path, 'rb', 65536) as ins:
for _ in range(int((file_path.stat().st_size / 65536)) + 1):
crc = zlib.crc32(ins.read(65536), crc)
return f"{crc & 0xFFFFFFFF:08X}"
return file_crc_ng(file_path)
@deprecated("Use corelibs_file.file_handling.get_file_name instead")
def file_name_crc(file_path: Path, add_parent_folder: bool = False) -> str:
"""
either returns file name only from path
@@ -38,9 +38,6 @@ def file_name_crc(file_path: Path, add_parent_folder: bool = False) -> str:
Returns:
str: file name as string
"""
if add_parent_folder:
return str(Path(file_path.parent.name).joinpath(file_path.name))
else:
return file_path.name
return get_file_name(file_path, add_parent_folder=add_parent_folder)
# __END__

View File

@@ -2,11 +2,12 @@
File handling utilities
"""
import os
import shutil
from warnings import deprecated
from pathlib import Path
from corelibs_file.file_handling import remove_all_in_directory as remove_all_in_directory_ng
@deprecated("Use corelibs_file.file_handling.remove_all_in_directory instead")
def remove_all_in_directory(
directory: Path,
ignore_files: list[str] | None = None,
@@ -14,43 +15,24 @@ def remove_all_in_directory(
dry_run: bool = False
) -> bool:
"""
remove all files and folders in a directory
can exclude files or folders
deprecated
Args:
directory (Path): _description_
ignore_files (list[str], optional): _description_. Defaults to None.
Arguments:
directory {Path} -- _description_
Keyword Arguments:
ignore_files {list[str] | None} -- _description_ (default: {None})
verbose {bool} -- _description_ (default: {False})
dry_run {bool} -- _description_ (default: {False})
Returns:
bool: _description_
bool -- _description_
"""
if not directory.is_dir():
return False
if ignore_files is None:
ignore_files = []
if verbose:
print(
f"{'[DRY RUN] ' if dry_run else ''}Remove old files in: {directory.name} [",
end="", flush=True
)
# remove all files and folders in given directory by recursive globbing
for file in directory.rglob("*"):
# skip if in ignore files
if file.name in ignore_files:
continue
# remove one file, or a whole directory
if file.is_file():
if not dry_run:
os.remove(file)
if verbose:
print(".", end="", flush=True)
elif file.is_dir():
if not dry_run:
shutil.rmtree(file)
if verbose:
print("/", end="", flush=True)
if verbose:
print("]", flush=True)
return True
return remove_all_in_directory_ng(
directory,
ignore_files=ignore_files,
verbose=verbose,
dry_run=dry_run
)
# __END__

View File

@@ -2,27 +2,31 @@
wrapper around search path
"""
from typing import Any, TypedDict, NotRequired
from typing import Any
from warnings import deprecated
from corelibs_search.data_search import (
ArraySearchList as CorelibsArraySearchList,
find_in_array_from_list as corelibs_find_in_array_from_list,
key_lookup as corelibs_key_lookup,
value_lookup as corelibs_value_lookup
)
class ArraySearchList(TypedDict):
class ArraySearchList(CorelibsArraySearchList):
"""find in array from list search dict"""
key: str
value: str | bool | int | float | list[str | None]
case_sensitive: NotRequired[bool]
@deprecated("Use find_in_array_from_list()")
@deprecated("Use corelibs_search.data_search.find_in_array_from_list instead")
def array_search(
search_params: list[ArraySearchList],
data: list[dict[str, Any]],
return_index: bool = False
) -> list[dict[str, Any]]:
"""depreacted, old call order"""
return find_in_array_from_list(data, search_params, return_index)
return corelibs_find_in_array_from_list(data, search_params, return_index)
@deprecated("Use corelibs_search.data_search.find_in_array_from_list instead")
def find_in_array_from_list(
data: list[dict[str, Any]],
search_params: list[ArraySearchList],
@@ -48,69 +52,14 @@ def find_in_array_from_list(
list: list of found elements, or if return index
list of dics with "index" and "data", where "data" holds the result list
"""
if not isinstance(search_params, list): # type: ignore
raise ValueError("search_params must be a list")
keys: list[str] = []
# check that key and value exist and are set
for search in search_params:
if not search.get('key') or not search.get('value'):
raise KeyError(
f"Either Key '{search.get('key', '')}' or "
f"Value '{search.get('value', '')}' is missing or empty"
)
# if double key -> abort
if search.get("key") in keys:
raise KeyError(
f"Key {search.get('key', '')} already exists in search_params"
)
keys.append(str(search['key']))
return_items: list[dict[str, Any]] = []
for si_idx, search_item in enumerate(data):
# for each search entry, all must match
matching = 0
for search in search_params:
# either Value direct or if Value is list then any of those items can match
# values are compared in lower case if case senstive is off
# lower case left side
# TODO: allow nested Keys. eg "Key: ["Key a", "key b"]" to be ["Key a"]["key b"]
if search.get("case_sensitive", True) is False:
search_value = search_item.get(str(search['key']), "").lower()
else:
search_value = search_item.get(str(search['key']), "")
# lower case right side
if isinstance(search['value'], list):
search_in = [
str(k).lower()
if search.get("case_sensitive", True) is False else k
for k in search['value']
]
elif search.get("case_sensitive", True) is False:
search_in = str(search['value']).lower()
else:
search_in = search['value']
# compare check
if (
(
isinstance(search_in, list) and
search_value in search_in
) or
search_value == search_in
):
matching += 1
if len(search_params) == matching:
if return_index is True:
# the data is now in "data sub set"
return_items.append({
"index": si_idx,
"data": search_item
})
else:
return_items.append(search_item)
# return all found or empty list
return return_items
return corelibs_find_in_array_from_list(
data,
search_params,
return_index
)
@deprecated("Use corelibs_search.data_search.key_lookup instead")
def key_lookup(haystack: dict[str, str], key: str) -> str:
"""
simple key lookup in haystack, erturns empty string if not found
@@ -122,9 +71,10 @@ def key_lookup(haystack: dict[str, str], key: str) -> str:
Returns:
str: _description_
"""
return haystack.get(key, "")
return corelibs_key_lookup(haystack, key)
@deprecated("Use corelibs_search.data_search.value_lookup instead")
def value_lookup(haystack: dict[str, str], value: str, raise_on_many: bool = False) -> str:
"""
find by value, if not found returns empty, if not raise on many returns the first one
@@ -140,11 +90,6 @@ def value_lookup(haystack: dict[str, str], value: str, raise_on_many: bool = Fal
Returns:
str: _description_
"""
keys = [__key for __key, __value in haystack.items() if __value == value]
if not keys:
return ""
if raise_on_many is True and len(keys) > 1:
raise ValueError("More than one element found with the same name")
return keys[0]
return corelibs_value_lookup(haystack, value, raise_on_many)
# __END__

View File

@@ -2,9 +2,16 @@
Various helper functions for type data clean up
"""
from typing import Any, cast
from warnings import deprecated
from typing import Any
from corelibs_iterator.dict_support import (
delete_keys_from_set as corelibs_delete_keys_from_set,
convert_to_dict_type,
set_entry as corelibs_set_entry
)
@deprecated("Use corelibs_iterator.dict_support.delete_keys_from_set instead")
def delete_keys_from_set(
set_data: dict[str, Any] | list[Any] | str, keys: list[str]
) -> dict[str, Any] | list[Any] | Any:
@@ -19,24 +26,10 @@ def delete_keys_from_set(
dict[str, Any] | list[Any] | None: _description_
"""
# skip everything if there is no keys list
if not keys:
return set_data
if isinstance(set_data, dict):
for key, value in set_data.copy().items():
if key in keys:
del set_data[key]
if isinstance(value, (dict, list)):
delete_keys_from_set(value, keys) # type: ignore Partly unknown
elif isinstance(set_data, list):
for value in set_data:
if isinstance(value, (dict, list)):
delete_keys_from_set(value, keys) # type: ignore Partly unknown
else:
set_data = [set_data]
return set_data
return corelibs_delete_keys_from_set(set_data, keys)
@deprecated("Use corelibs_iterator.dict_support.convert_to_dict_type instead")
def build_dict(
any_dict: Any, ignore_entries: list[str] | None = None
) -> dict[str, Any | list[Any] | dict[Any, Any]]:
@@ -49,18 +42,10 @@ def build_dict(
Returns:
dict[str, Any | list[Any]]: _description_
"""
if ignore_entries is None:
return cast(dict[str, Any | list[Any] | dict[Any, Any]], any_dict)
# ignore entries can be one key or key nested
# return {
# key: value for key, value in any_dict.items() if key not in ignore_entries
# }
return cast(
dict[str, Any | list[Any] | dict[Any, Any]],
delete_keys_from_set(any_dict, ignore_entries)
)
return convert_to_dict_type(any_dict, ignore_entries)
@deprecated("Use corelibs_iterator.dict_support.set_entry instead")
def set_entry(dict_set: dict[str, Any], key: str, value_set: Any) -> dict[str, Any]:
"""
set a new entry in the dict set
@@ -73,9 +58,6 @@ def set_entry(dict_set: dict[str, Any], key: str, value_set: Any) -> dict[str, A
Returns:
dict[str, Any] -- _description_
"""
if not dict_set.get(key):
dict_set[key] = {}
dict_set[key] = value_set
return dict_set
return corelibs_set_entry(dict_set, key, value_set)
# __END__

View File

@@ -2,8 +2,11 @@
Dict helpers
"""
from typing import TypeAlias, Union, Dict, List, Any, cast
from warnings import deprecated
from typing import TypeAlias, Union, Dict, List, Any
from corelibs_dump_data.dict_mask import (
mask as corelibs_mask
)
# definitions for the mask run below
MaskableValue: TypeAlias = Union[str, int, float, bool, None]
@@ -11,6 +14,7 @@ NestedDict: TypeAlias = Dict[str, Union[MaskableValue, List[Any], 'NestedDict']]
ProcessableValue: TypeAlias = Union[MaskableValue, List[Any], NestedDict]
@deprecated("use corelibs_dump_data.dict_mask.mask instead")
def mask(
data_set: dict[str, Any],
mask_keys: list[str] | None = None,
@@ -26,7 +30,7 @@ def mask(
and end with '_', remove to search string in string
Arguments:
data_set {dict[str, str]} -- _description_
data_set {dict[str, Any]} -- _description_
Keyword Arguments:
mask_keys {list[str] | None} -- _description_ (default: {None})
@@ -37,49 +41,12 @@ def mask(
Returns:
dict[str, str] -- _description_
"""
if skip is True:
return data_set
if mask_keys is None:
mask_keys = ["encryption", "password", "secret"]
else:
# make sure it is lower case
mask_keys = [mask_key.lower() for mask_key in mask_keys]
def should_mask_key(key: str) -> bool:
"""Check if a key should be masked"""
__key_lower = key.lower()
return any(
__key_lower.startswith(mask_key) or
__key_lower.endswith(mask_key) or
f"{mask_str_edges}{mask_key}{mask_str_edges}" in __key_lower
for mask_key in mask_keys
)
def mask_recursive(obj: ProcessableValue) -> ProcessableValue:
"""Recursively mask values in nested structures"""
if isinstance(obj, dict):
return {
key: mask_value(value) if should_mask_key(key) else mask_recursive(value)
for key, value in obj.items()
}
if isinstance(obj, list):
return [mask_recursive(item) for item in obj]
return obj
def mask_value(value: Any) -> Any:
"""Handle masking based on value type"""
if isinstance(value, list):
# Mask each individual value in the list
return [mask_str for _ in cast('list[Any]', value)]
if isinstance(value, dict):
# Recursively process the dictionary instead of masking the whole thing
return mask_recursive(cast('ProcessableValue', value))
# Mask primitive values
return mask_str
return {
key: mask_value(value) if should_mask_key(key) else mask_recursive(value)
for key, value in data_set.items()
}
return corelibs_mask(
data_set,
mask_keys,
mask_str,
mask_str_edges,
skip
)
# __END__

View File

@@ -2,13 +2,35 @@
Various dictionary, object and list hashers
"""
import json
import hashlib
from warnings import deprecated
from typing import Any
from corelibs_hash.fingerprint import (
hash_object as corelibs_hash_object,
dict_hash_frozen as corelibs_dict_hash_frozen,
dict_hash_crc as corelibs_dict_hash_crc
)
@deprecated("use corelibs_hash.fingerprint.hash_object instead")
def hash_object(obj: Any) -> str:
"""
RECOMMENDED for new use
Create a hash for any dict or list with mixed key types
Arguments:
obj {Any} -- _description_
Returns:
str -- _description_
"""
return corelibs_hash_object(obj)
@deprecated("use corelibs_hash.fingerprint.hash_object instead")
def dict_hash_frozen(data: dict[Any, Any]) -> int:
"""
NOT RECOMMENDED, use dict_hash_crc or hash_object instead
If used, DO NOT CHANGE
hash a dict via freeze
Args:
@@ -17,23 +39,23 @@ def dict_hash_frozen(data: dict[Any, Any]) -> int:
Returns:
str: _description_
"""
return hash(frozenset(data.items()))
return corelibs_dict_hash_frozen(data)
@deprecated("use corelibs_hash.fingerprint.dict_hash_crc and for new use hash_object instead")
def dict_hash_crc(data: dict[Any, Any] | list[Any]) -> str:
"""
Create a sha256 hash over dict
LEGACY METHOD, must be kept for fallback, if used by other code, DO NOT CHANGE
Create a sha256 hash over dict or list
alternative for
dict_hash_frozen
Args:
data (dict | list): _description_
data (dict[Any, Any] | list[Any]): _description_
Returns:
str: _description_
str: sha256 hash, prefiex with HO_ if fallback used
"""
return hashlib.sha256(
json.dumps(data, sort_keys=True, ensure_ascii=True).encode('utf-8')
).hexdigest()
return corelibs_dict_hash_crc(data)
# __END__

View File

@@ -2,10 +2,16 @@
List type helpers
"""
import json
from warnings import deprecated
from typing import Any, Sequence
from corelibs_iterator.list_support import (
convert_to_list as corelibs_convert_to_list,
is_list_in_list as corelibs_is_list_in_list,
make_unique_list_of_dicts as corelibs_make_unique_list_of_dicts
)
@deprecated("use corelibs_iterator.list_support.convert_to_list instead")
def convert_to_list(
entry: str | int | float | bool | Sequence[str | int | float | bool | Sequence[Any]]
) -> Sequence[str | int | float | bool | Sequence[Any]]:
@@ -18,11 +24,10 @@ def convert_to_list(
Returns:
list[str | int | float | bool] -- _description_
"""
if isinstance(entry, list):
return entry
return [entry]
return corelibs_convert_to_list(entry)
@deprecated("use corelibs_iterator.list_support.is_list_in_list instead")
def is_list_in_list(
list_a: Sequence[str | int | float | bool | Sequence[Any]],
list_b: Sequence[str | int | float | bool | Sequence[Any]]
@@ -38,14 +43,10 @@ def is_list_in_list(
Returns:
list[Any] -- _description_
"""
# Create sets of (value, type) tuples
set_a = set((item, type(item)) for item in list_a)
set_b = set((item, type(item)) for item in list_b)
# Get the difference and extract just the values
return [item for item, _ in set_a - set_b]
return corelibs_is_list_in_list(list_a, list_b)
@deprecated("use corelibs_iterator.list_support.make_unique_list_of_dicts instead")
def make_unique_list_of_dicts(dict_list: list[Any]) -> list[Any]:
"""
Create a list of unique dictionary entries
@@ -56,15 +57,6 @@ def make_unique_list_of_dicts(dict_list: list[Any]) -> list[Any]:
Returns:
list[Any] -- _description_
"""
try:
# try json dumps, can fail with int and str index types
return list({json.dumps(d, sort_keys=True, ensure_ascii=True): d for d in dict_list}.values())
except TypeError:
# Fallback for non-serializable entries, slow but works
unique: list[Any] = []
for d in dict_list:
if d not in unique:
unique.append(d)
return unique
return corelibs_make_unique_list_of_dicts(dict_list)
# __END__

View File

@@ -2,11 +2,12 @@
helper functions for jmespath interfaces
"""
from warnings import deprecated
from typing import Any
import jmespath
import jmespath.exceptions
from corelibs_search.jmespath_search import jmespath_search as jmespath_search_ng
@deprecated("Use corelibs_search.jmespath_search.jmespath_search instead")
def jmespath_search(search_data: dict[Any, Any] | list[Any], search_params: str) -> Any:
"""
jmespath search wrapper
@@ -22,18 +23,6 @@ def jmespath_search(search_data: dict[Any, Any] | list[Any], search_params: str)
Returns:
Any: dict/list/etc, None if nothing found
"""
try:
search_result = jmespath.search(search_params, search_data)
except jmespath.exceptions.LexerError as excp:
raise ValueError(f"Compile failed: {search_params}: {excp}") from excp
except jmespath.exceptions.ParseError as excp:
raise ValueError(f"Parse failed: {search_params}: {excp}") from excp
except jmespath.exceptions.JMESPathTypeError as excp:
raise ValueError(f"Search failed with JMESPathTypeError: {search_params}: {excp}") from excp
except TypeError as excp:
raise ValueError(f"Type error for search_params: {excp}") from excp
return search_result
# TODO: compile jmespath setup
return jmespath_search_ng(search_data, search_params)
# __END__

View File

@@ -2,35 +2,37 @@
json encoder for datetime
"""
from warnings import warn, deprecated
from typing import Any
from json import JSONEncoder, dumps
from datetime import datetime, date
import copy
from jsonpath_ng import parse # pyright: ignore[reportMissingTypeStubs, reportUnknownVariableType]
from corelibs_json.json_support import (
default_isoformat as default_isoformat_ng,
DateTimeEncoder as DateTimeEncoderCoreLibs,
json_dumps as json_dumps_ng,
modify_with_jsonpath as modify_with_jsonpath_ng,
)
# subclass JSONEncoder
class DateTimeEncoder(JSONEncoder):
class DateTimeEncoder(DateTimeEncoderCoreLibs):
"""
Override the default method
dumps(..., cls=DateTimeEncoder, ...)
"""
def default(self, o: Any) -> str | None:
if isinstance(o, (date, datetime)):
return o.isoformat()
return None
warn("Use corelibs_json.json_support.DateTimeEncoder instead", DeprecationWarning, stacklevel=2)
@deprecated("Use corelibs_json.json_support.default_isoformat instead")
def default_isoformat(obj: Any) -> str | None:
"""
default override
dumps(..., default=default, ...)
"""
if isinstance(obj, (date, datetime)):
return obj.isoformat()
return None
return default_isoformat_ng(obj)
@deprecated("Use corelibs_json.json_support.json_dumps instead")
def json_dumps(data: Any):
"""
wrapper for json.dumps with sure dump without throwing Exceptions
@@ -41,22 +43,15 @@ def json_dumps(data: Any):
Returns:
_type_ -- _description_
"""
return dumps(data, ensure_ascii=False, default=str)
return json_dumps_ng(data)
@deprecated("Use corelibs_json.json_support.modify_with_jsonpath instead")
def modify_with_jsonpath(data: dict[Any, Any], path: str, new_value: Any):
"""
Modify dictionary using JSONPath (more powerful than JMESPath for modifications)
"""
result = copy.deepcopy(data)
jsonpath_expr = parse(path) # pyright: ignore[reportUnknownVariableType]
# Find and update all matches
matches = jsonpath_expr.find(result) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
for match in matches: # pyright: ignore[reportUnknownVariableType]
match.full_path.update(result, new_value) # pyright: ignore[reportUnknownMemberType]
return result
return modify_with_jsonpath_ng(data, path, new_value)
# __END__

View File

@@ -13,9 +13,9 @@ from pathlib import Path
import atexit
from enum import Flag, auto
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
from corelibs_stack_trace.stack import call_stack, exception_stack
from corelibs_text_colors.text_colors import Colors
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
from corelibs.debug_handling.debug_helpers import call_stack, exception_stack
if TYPE_CHECKING:
from multiprocessing import Queue

View File

@@ -2,9 +2,11 @@
Various math helpers
"""
from warnings import deprecated
import math
@deprecated("Use math.gcd instead")
def gcd(a: int, b: int):
"""
Calculate: Greatest Common Divisor
@@ -19,6 +21,7 @@ def gcd(a: int, b: int):
return math.gcd(a, b)
@deprecated("Use math.lcm instead")
def lcd(a: int, b: int):
"""
Calculate: Least Common Denominator

View File

@@ -3,32 +3,61 @@ requests lib interface
V2 call type
"""
from typing import Any
import warnings
from typing import Any, TypedDict, cast
import requests
# to hide the verfiy warnings because of the bad SSL settings from Netskope, Akamai, etc
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
from requests import exceptions
class ErrorResponse:
"""
Error response structure. This is returned if a request could not be completed
"""
def __init__(
self,
code: int,
message: str,
action: str,
url: str,
exception: exceptions.InvalidSchema | exceptions.ReadTimeout | exceptions.ConnectionError | None = None
) -> None:
self.code = code
self.message = message
self.action = action
self.url = url
self.exception_name = type(exception).__name__ if exception is not None else None
self.exception_trace = exception if exception is not None else None
class ProxyConfig(TypedDict):
"""
Socks proxy settings
"""
type: str
host: str
port: str
class Caller:
"""_summary_"""
"""
requests lib interface
"""
def __init__(
self,
header: dict[str, str],
verify: bool = True,
timeout: int = 20,
proxy: dict[str, str] | None = None,
proxy: ProxyConfig | None = None,
verify: bool = True,
ca_file: str | None = None
):
self.headers = header
self.timeout: int = timeout
self.cafile = ca_file
self.ca_file = ca_file
self.verify = verify
self.proxy = proxy
self.proxy = cast(dict[str, str], proxy) if proxy is not None else None
def __timeout(self, timeout: int | None) -> int:
if timeout is not None:
if timeout is not None and timeout >= 0:
return timeout
return self.timeout
@@ -39,7 +68,7 @@ class Caller:
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | None:
) -> requests.Response | ErrorResponse:
"""
call wrapper, on error returns None
@@ -56,67 +85,96 @@ class Caller:
if data is None:
data = {}
try:
response = None
if action == "get":
response = requests.get(
return requests.get(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "post":
response = requests.post(
if action == "post":
return requests.post(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "put":
response = requests.put(
if action == "put":
return requests.put(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "patch":
response = requests.patch(
if action == "patch":
return requests.patch(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "delete":
response = requests.delete(
if action == "delete":
return requests.delete(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
return response
except requests.exceptions.InvalidSchema as e:
print(f"Invalid URL during '{action}' for {url}:\n\t{e}")
return None
except requests.exceptions.ReadTimeout as e:
print(f"Timeout ({self.timeout}s) during '{action}' for {url}:\n\t{e}")
return None
except requests.exceptions.ConnectionError as e:
print(f"Connection error during '{action}' for {url}:\n\t{e}")
return None
return ErrorResponse(
100,
f"Unsupported action '{action}'",
action,
url
)
except exceptions.InvalidSchema as e:
return ErrorResponse(
200,
f"Invalid URL during '{action}' for {url}",
action,
url,
e
)
except exceptions.ReadTimeout as e:
return ErrorResponse(
300,
f"Timeout ({self.timeout}s) during '{action}' for {url}",
action,
url,
e
)
except exceptions.ConnectionError as e:
return ErrorResponse(
400,
f"Connection error during '{action}' for {url}",
action,
url,
e
)
def get(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
def get(
self,
url: str,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
get data
@@ -127,11 +185,15 @@ class Caller:
Returns:
requests.Response: _description_
"""
return self.__call('get', url, params=params)
return self.__call('get', url, params=params, timeout=timeout)
def post(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
post data
@@ -143,11 +205,15 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('post', url, data, params)
return self.__call('post', url, data, params, timeout=timeout)
def put(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""_summary_
Args:
@@ -158,11 +224,15 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('put', url, data, params)
return self.__call('put', url, data, params, timeout=timeout)
def patch(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""_summary_
Args:
@@ -173,9 +243,14 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('patch', url, data, params)
return self.__call('patch', url, data, params, timeout=timeout)
def delete(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
def delete(
self,
url: str,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
delete
@@ -186,6 +261,6 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('delete', url, params=params)
return self.__call('delete', url, params=params, timeout=timeout)
# __END__

View File

@@ -4,7 +4,7 @@ Settings loader test
import re
from pathlib import Path
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.logging_handling.log import Log
from corelibs.config_handling.settings_loader import SettingsLoader
from corelibs.config_handling.settings_loader_handling.settings_loader_check import SettingsLoaderCheck

View File

@@ -5,7 +5,7 @@ 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_dump_data.dump_data import dump_data
from corelibs.logging_handling.log import Log, Logger
from corelibs.db_handling.sql_main import SQLMain

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from uuid import uuid4
import json
import sqlite3
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.logging_handling.log import Log, Logger
from corelibs.db_handling.sqlite_io import SQLiteIO

View File

@@ -5,7 +5,7 @@ Symmetric encryption test
"""
import json
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.encryption_handling.symmetric_encryption import SymmetricEncryption

View File

@@ -5,8 +5,8 @@ BOM check for files
"""
from pathlib import Path
from corelibs_dump_data.dump_data import dump_data
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
from corelibs.debug_handling.dump_data import dump_data
def main() -> None:

View File

@@ -5,7 +5,7 @@ Search data tests
iterator_handling.data_search
"""
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.iterator_handling.data_search import find_in_array_from_list, ArraySearchList

View File

@@ -3,7 +3,7 @@ Iterator helper testing
"""
from typing import Any
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.iterator_handling.dict_mask import mask
from corelibs.iterator_handling.dict_helpers import set_entry

View File

@@ -2,9 +2,10 @@
test list helpers
"""
# from typing import Any
from corelibs.debug_handling.dump_data import dump_data
from typing import Any
from corelibs_dump_data.dump_data import dump_data
from corelibs.iterator_handling.list_helpers import is_list_in_list, convert_to_list, make_unique_list_of_dicts
from corelibs.iterator_handling.fingerprint import dict_hash_crc
def __test_is_list_in_list_a():
@@ -29,7 +30,8 @@ def __make_unique_list_of_dicts():
{"a": 3, "b": 4, "nested": {"x": 30, "y": 40}}
]
unique_dicts = make_unique_list_of_dicts(dict_list)
print(f"Unique dicts: {dump_data(unique_dicts)}")
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list = [
{"a": 1, 1: "one"},
@@ -37,7 +39,8 @@ def __make_unique_list_of_dicts():
{"a": 2, 1: "one"}
]
unique_dicts = make_unique_list_of_dicts(dict_list)
print(f"Unique dicts: {dump_data(unique_dicts)}")
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list = [
{"a": 1, "b": [1, 2, 3]},
@@ -46,7 +49,31 @@ def __make_unique_list_of_dicts():
1, 2, "String", 1, "Foobar"
]
unique_dicts = make_unique_list_of_dicts(dict_list)
print(f"Unique dicts: {dump_data(unique_dicts)}")
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list: list[Any] = [
[],
{},
[],
{},
{"a": []},
{"a": []},
{"a": {}},
{"a": {}},
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list: list[Any] = [
(1, 2),
(1, 2),
(2, 3),
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
def main():

View File

@@ -4,7 +4,7 @@
jmes path testing
"""
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.json_handling.jmespath_helper import jmespath_search

View File

@@ -5,7 +5,7 @@ JSON content replace tets
"""
from deepdiff import DeepDiff
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.json_handling.json_helper import modify_with_jsonpath

View File

@@ -6,8 +6,8 @@ Log logging_handling.log testing
import sys
from pathlib import Path
# this is for testing only
from corelibs_stack_trace.stack import exception_stack, call_stack
from corelibs.logging_handling.log import Log, Logger, ConsoleFormat, ConsoleFormatSettings
from corelibs.debug_handling.debug_helpers import exception_stack, call_stack
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel

View File

@@ -1 +0,0 @@
"""Unit tests for check_handling module."""

View File

@@ -1,623 +0,0 @@
"""
Unit tests for regex_constants module.
Tests all regex patterns defined in the check_handling.regex_constants module.
"""
import re
import pytest
from corelibs.check_handling.regex_constants import (
compile_re,
SUB_EMAIL_BASIC_REGEX,
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX
)
from corelibs.check_handling.regex_constants_compiled import (
COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX,
COMPILED_NAME_EMAIL_BASIC_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX,
COMPILED_DOMAIN_REGEX,
)
class TestCompileRe:
"""Test cases for the compile_re function."""
def test_compile_re_returns_pattern(self) -> None:
"""Test that compile_re returns a compiled regex Pattern object."""
pattern = compile_re(r"test")
assert isinstance(pattern, re.Pattern)
def test_compile_re_with_verbose_flag(self) -> None:
"""Test that compile_re compiles with VERBOSE flag."""
# Verbose mode allows whitespace and comments in regex
verbose_regex = r"""
\d+ # digits
\s+ # whitespace
"""
pattern = compile_re(verbose_regex)
assert pattern.match("123 ")
assert not pattern.match("abc")
def test_compile_re_simple_pattern(self) -> None:
"""Test compile_re with a simple pattern."""
pattern = compile_re(r"^\d{3}$")
assert pattern.match("123")
assert not pattern.match("12")
assert not pattern.match("1234")
class TestEmailBasicRegex:
"""Test cases for EMAIL_BASIC_REGEX pattern."""
@pytest.fixture
def email_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled email regex pattern."""
return COMPILED_EMAIL_BASIC_REGEX
@pytest.mark.parametrize("valid_email", [
"user@example.com",
"test.user@example.com",
"user+tag@example.co.uk",
"first.last@subdomain.example.com",
"user123@test-domain.com",
"a@example.com",
"user_name@example.com",
"user-name@example.com",
"user@sub.domain.example.com",
"test!#$%&'*+-/=?^_`{|}~@example.com",
"1234567890@example.com",
"user@example-domain.com",
"user@domain.co",
# Regex allows these (even if not strictly RFC compliant):
"user.@example.com", # ends with dot before @
"user..name@example.com", # consecutive dots in local part
])
def test_valid_emails(
self, email_pattern: re.Pattern[str], valid_email: str
) -> None:
"""Test that valid email addresses match the pattern."""
assert email_pattern.match(valid_email), (
f"Failed to match valid email: {valid_email}"
)
@pytest.mark.parametrize("invalid_email", [
"", # empty string
"@example.com", # missing local part
"user@", # missing domain
"user", # no @ symbol
"user@.com", # domain starts with dot
"user@domain", # no TLD
"user @example.com", # space in local part
"user@exam ple.com", # space in domain
".user@example.com", # starts with dot
"user@-example.com", # domain starts with hyphen
"user@example-.com", # domain part ends with hyphen
"user@example.c", # TLD too short (1 char)
"user@example.toolong", # TLD too long (>6 chars)
"user@@example.com", # double @
"user@example@com", # multiple @
"user@.example.com", # domain starts with dot
"user@example.com.", # ends with dot
"user@123.456.789.012", # numeric TLD not allowed
])
def test_invalid_emails(
self, email_pattern: re.Pattern[str], invalid_email: str
) -> None:
"""Test that invalid email addresses do not match the pattern."""
assert not email_pattern.match(invalid_email), (
f"Incorrectly matched invalid email: {invalid_email}"
)
def test_email_max_local_part_length(
self, email_pattern: re.Pattern[str]
) -> None:
"""Test email with maximum local part length (64 characters)."""
# Local part can be up to 64 chars (first char + 63 more)
local_part = "a" * 64
email = f"{local_part}@example.com"
assert email_pattern.match(email)
def test_email_exceeds_local_part_length(
self, email_pattern: re.Pattern[str]
) -> None:
"""Test email exceeding maximum local part length."""
# 65 characters should not match
local_part = "a" * 65
email = f"{local_part}@example.com"
assert not email_pattern.match(email)
class TestSubEmailBasicRegex:
"""Test cases for SUB_EMAIL_BASIC_REGEX pattern (without anchors)."""
@pytest.fixture
def sub_email_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled sub email regex pattern."""
return compile_re(rf"^{SUB_EMAIL_BASIC_REGEX}$")
@pytest.mark.parametrize("valid_email", [
"user@example.com",
"test.user@example.com",
"user+tag@example.co.uk",
"first.last@subdomain.example.com",
"user123@test-domain.com",
"a@example.com",
"user_name@example.com",
"user-name@example.com",
"user@sub.domain.example.com",
"test!#$%&'*+-/=?^_`{|}~@example.com",
"1234567890@example.com",
])
def test_valid_emails_match(self, sub_email_pattern: re.Pattern[str], valid_email: str) -> None:
"""Test that valid email addresses match SUB_EMAIL_BASIC_REGEX."""
assert sub_email_pattern.match(valid_email), (
f"Failed to match valid email: {valid_email}"
)
@pytest.mark.parametrize("invalid_email", [
"",
"@example.com",
"user@",
"user",
"user@.com",
"user@domain",
"user @example.com",
".user@example.com",
"user@-example.com",
"user@example-.com",
"user@example.c",
"user@example.toolong",
])
def test_invalid_emails_no_match(self, sub_email_pattern: re.Pattern[str], invalid_email: str) -> None:
"""Test that invalid emails don't match SUB_EMAIL_BASIC_REGEX."""
assert not sub_email_pattern.match(invalid_email), (
f"Incorrectly matched invalid email: {invalid_email}"
)
def test_sub_email_max_local_part_length(self, sub_email_pattern: re.Pattern[str]) -> None:
"""Test email with maximum local part length (64 characters)."""
local_part = "a" * 64
email = f"{local_part}@example.com"
assert sub_email_pattern.match(email)
def test_sub_email_exceeds_local_part_length(self, sub_email_pattern: re.Pattern[str]) -> None:
"""Test email exceeding maximum local part length."""
local_part = "a" * 65
email = f"{local_part}@example.com"
assert not sub_email_pattern.match(email)
class TestNameEmailSimpleRegex:
"""Test cases for NAME_EMAIL_SIMPLE_REGEX pattern."""
@pytest.fixture
def name_email_simple_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled name+email simple regex pattern."""
return COMPILED_NAME_EMAIL_SIMPLE_REGEX
@pytest.mark.parametrize("test_input,expected_groups", [
('"John Doe" <john@example.com>', {'name1': 'John Doe', 'email1': 'john@example.com'}),
('John Doe <john@example.com>', {'name2': 'John Doe', 'email2': 'john@example.com'}),
('<john@example.com>', {'email3': 'john@example.com'}),
('john@example.com', {'email4': 'john@example.com'}),
(' "Jane Smith" <jane@test.com> ', {'name1': 'Jane Smith', 'email1': 'jane@test.com'}),
('Bob <bob@test.org>', {'name2': 'Bob', 'email2': 'bob@test.org'}),
])
def test_valid_name_email_combinations(
self, name_email_simple_pattern: re.Pattern[str], test_input: str, expected_groups: dict[str, str]
) -> None:
"""Test that valid name+email combinations match and extract correct groups."""
match = name_email_simple_pattern.match(test_input)
assert match is not None, f"Failed to match: {test_input}"
# Check that expected groups are present and match
for group_name, expected_value in expected_groups.items():
assert match.group(group_name) == expected_value, (
f"Group {group_name} expected '{expected_value}', got '{match.group(group_name)}'"
)
@pytest.mark.parametrize("invalid_input", [
"",
"not an email",
"<>",
'"Name Only"',
'Name <',
'<email',
'Name <<email@test.com>>',
'Name <email@test.com',
'Name email@test.com>',
])
def test_invalid_name_email_combinations(
self, name_email_simple_pattern: re.Pattern[str], invalid_input: str
) -> None:
"""Test that invalid inputs don't match NAME_EMAIL_SIMPLE_REGEX."""
assert not name_email_simple_pattern.match(invalid_input), (
f"Incorrectly matched invalid input: {invalid_input}"
)
def test_extract_name_from_quoted(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test extracting name from quoted format."""
match = name_email_simple_pattern.match('"Alice Wonder" <alice@example.com>')
assert match is not None
assert match.group('name1') == 'Alice Wonder'
assert match.group('email1') == 'alice@example.com'
def test_extract_name_from_unquoted(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test extracting name from unquoted format."""
match = name_email_simple_pattern.match('Bob Builder <bob@example.com>')
assert match is not None
assert match.group('name2') == 'Bob Builder'
assert match.group('email2') == 'bob@example.com'
def test_email_only_in_brackets(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test email-only format in angle brackets."""
match = name_email_simple_pattern.match('<charlie@example.com>')
assert match is not None
assert match.group('email3') == 'charlie@example.com'
def test_email_only_plain(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test plain email format without brackets."""
match = name_email_simple_pattern.match('dave@example.com')
assert match is not None
assert match.group('email4') == 'dave@example.com'
def test_whitespace_handling(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test that leading/trailing whitespace is handled correctly."""
match = name_email_simple_pattern.match(' "User Name" <user@example.com> ')
assert match is not None
assert match.group('name1') == 'User Name'
assert match.group('email1') == 'user@example.com'
class TestNameEmailBasicRegex:
"""Test cases for NAME_EMAIL_BASIC_REGEX pattern with strict email validation."""
@pytest.fixture
def name_email_basic_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled name+email basic regex pattern."""
return COMPILED_NAME_EMAIL_BASIC_REGEX
@pytest.mark.parametrize("test_input,expected_name,expected_email", [
('"John Doe" <john@example.com>', 'John Doe', 'john@example.com'),
('John Doe <john@example.com>', 'John Doe', 'john@example.com'),
('<john@example.com>', None, 'john@example.com'),
('john@example.com', None, 'john@example.com'),
(' "Jane Smith" <jane.smith@test.co.uk> ', 'Jane Smith', 'jane.smith@test.co.uk'),
('Alice Wonder <alice+tag@example.com>', 'Alice Wonder', 'alice+tag@example.com'),
])
def test_valid_name_email_with_validation(
self,
name_email_basic_pattern: re.Pattern[str],
test_input: str,
expected_name: str | None,
expected_email: str,
) -> None:
"""Test valid name+email with strict email validation."""
match = name_email_basic_pattern.match(test_input)
assert match is not None, f"Failed to match: {test_input}"
# Extract name and email from whichever group matched
name = match.group('name1') or match.group('name2')
email = (
match.group('email1') or match.group('email2') or
match.group('email3') or match.group('email4')
)
assert name == expected_name, f"Expected name '{expected_name}', got '{name}'"
assert email == expected_email, f"Expected email '{expected_email}', got '{email}'"
@pytest.mark.parametrize("invalid_input", [
'"John Doe" <invalid.email>', # invalid email format
'John Doe <@example.com>', # missing local part
'<user@>', # missing domain
'user@domain', # no TLD
'"Name" <user @example.com>', # space in email
'<.user@example.com>', # starts with dot
'user@-example.com', # domain starts with hyphen
'Name <user@example.c>', # TLD too short
'Name <user@example.toolongdomain>', # TLD too long
])
def test_invalid_email_format_rejected(
self, name_email_basic_pattern: re.Pattern[str], invalid_input: str
) -> None:
"""Test that inputs with invalid email formats are rejected."""
assert not name_email_basic_pattern.match(invalid_input), (
f"Incorrectly matched invalid input: {invalid_input}"
)
def test_quoted_name_with_valid_email(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test quoted name format with valid email."""
match = name_email_basic_pattern.match('"Alice Wonder" <alice@example.com>')
assert match is not None
assert match.group('name1') == 'Alice Wonder'
assert match.group('email1') == 'alice@example.com'
def test_unquoted_name_with_valid_email(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test unquoted name format with valid email."""
match = name_email_basic_pattern.match('Bob Builder <bob@example.com>')
assert match is not None
assert match.group('name2') == 'Bob Builder'
assert match.group('email2') == 'bob@example.com'
def test_email_only_formats(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test email-only formats (with and without brackets)."""
# With brackets
match1 = name_email_basic_pattern.match('<charlie@example.com>')
assert match1 is not None
assert match1.group('email3') == 'charlie@example.com'
# Without brackets
match2 = name_email_basic_pattern.match('dave@example.com')
assert match2 is not None
assert match2.group('email4') == 'dave@example.com'
def test_whitespace_handling(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test that leading/trailing whitespace is handled correctly."""
match = name_email_basic_pattern.match(' "User" <user@example.com> ')
assert match is not None
assert match.group('name1') == 'User'
assert match.group('email1') == 'user@example.com'
def test_special_characters_in_local_part(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test email with special characters in local part."""
match = name_email_basic_pattern.match('Test User <test!#$%&\'*+-/=?^_`{|}~@example.com>')
assert match is not None
assert match.group('name2') == 'Test User'
assert match.group('email2') == 'test!#$%&\'*+-/=?^_`{|}~@example.com'
class TestDomainWithLocalhostRegex:
"""Test cases for DOMAIN_WITH_LOCALHOST_REGEX pattern."""
@pytest.fixture
def domain_localhost_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain with localhost regex pattern."""
return COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
@pytest.mark.parametrize("valid_domain", [
"localhost",
"example.com",
"subdomain.example.com",
"sub.domain.example.com",
"test-domain.com",
"example.co.uk",
"a.com",
"test123.example.com",
"my-site.example.org",
"multi.level.subdomain.example.com",
])
def test_valid_domains(
self, domain_localhost_pattern: re.Pattern[str], valid_domain: str
) -> None:
"""Test that valid domains (including localhost) match the pattern."""
assert domain_localhost_pattern.match(valid_domain), (
f"Failed to match valid domain: {valid_domain}"
)
@pytest.mark.parametrize("invalid_domain", [
"", # empty string
"example", # no TLD
"-example.com", # starts with hyphen
"example-.com", # ends with hyphen
".example.com", # starts with dot
"example.com.", # ends with dot
"example..com", # consecutive dots
"exam ple.com", # space in domain
"example.c", # TLD too short
"localhost:8080", # port not allowed in this pattern
"example.com:8080", # port not allowed in this pattern
"@example.com", # invalid character
"example@com", # invalid character
])
def test_invalid_domains(
self, domain_localhost_pattern: re.Pattern[str], invalid_domain: str
) -> None:
"""Test that invalid domains do not match the pattern."""
assert not domain_localhost_pattern.match(invalid_domain), (
f"Incorrectly matched invalid domain: {invalid_domain}"
)
class TestDomainWithLocalhostPortRegex:
"""Test cases for DOMAIN_WITH_LOCALHOST_PORT_REGEX pattern."""
@pytest.fixture
def domain_localhost_port_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain and localhost with port pattern."""
return COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX
@pytest.mark.parametrize("valid_domain", [
"localhost",
"localhost:8080",
"localhost:3000",
"localhost:80",
"localhost:443",
"localhost:65535",
"example.com",
"example.com:8080",
"subdomain.example.com:3000",
"test-domain.com:443",
"example.co.uk",
"example.co.uk:8000",
"a.com:1",
"multi.level.subdomain.example.com:9999",
])
def test_valid_domains_with_port(
self, domain_localhost_port_pattern: re.Pattern[str], valid_domain: str
) -> None:
"""Test that valid domains with optional ports match the pattern."""
assert domain_localhost_port_pattern.match(valid_domain), (
f"Failed to match valid domain: {valid_domain}"
)
@pytest.mark.parametrize("invalid_domain", [
"", # empty string
"example", # no TLD
"-example.com", # starts with hyphen
"example-.com", # ends with hyphen
".example.com", # starts with dot
"example.com.", # ends with dot
"localhost:", # port without number
"example.com:", # port without number
"example.com:abc", # non-numeric port
"example.com: 8080", # space before port
"example.com:80 80", # space in port
"exam ple.com", # space in domain
"localhost :8080", # space before colon
])
def test_invalid_domains_with_port(
self,
domain_localhost_port_pattern: re.Pattern[str],
invalid_domain: str,
) -> None:
"""Test that invalid domains do not match the pattern."""
assert not domain_localhost_port_pattern.match(invalid_domain), (
f"Incorrectly matched invalid domain: {invalid_domain}"
)
def test_large_port_number(
self, domain_localhost_port_pattern: re.Pattern[str]
) -> None:
"""Test domain with large port numbers."""
assert domain_localhost_port_pattern.match("example.com:65535")
# Regex doesn't validate port range
assert domain_localhost_port_pattern.match("example.com:99999")
class TestDomainRegex:
"""Test cases for DOMAIN_REGEX pattern (no localhost)."""
@pytest.fixture
def domain_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain regex pattern."""
return COMPILED_DOMAIN_REGEX
@pytest.mark.parametrize("valid_domain", [
"example.com",
"subdomain.example.com",
"sub.domain.example.com",
"test-domain.com",
"example.co.uk",
"a.com",
"test123.example.com",
"my-site.example.org",
"multi.level.subdomain.example.com",
"example.co",
])
def test_valid_domains_no_localhost(
self, domain_pattern: re.Pattern[str], valid_domain: str
) -> None:
"""Test that valid domains match the pattern."""
assert domain_pattern.match(valid_domain), (
f"Failed to match valid domain: {valid_domain}"
)
@pytest.mark.parametrize("invalid_domain", [
"", # empty string
"localhost", # localhost not allowed
"example", # no TLD
"-example.com", # starts with hyphen
"example-.com", # ends with hyphen
".example.com", # starts with dot
"example.com.", # ends with dot
"example..com", # consecutive dots
"exam ple.com", # space in domain
"example.c", # TLD too short
"example.com:8080", # port not allowed
"@example.com", # invalid character
"example@com", # invalid character
])
def test_invalid_domains_no_localhost(
self, domain_pattern: re.Pattern[str], invalid_domain: str
) -> None:
"""Test that invalid domains do not match the pattern."""
assert not domain_pattern.match(invalid_domain), (
f"Incorrectly matched invalid domain: {invalid_domain}"
)
def test_localhost_not_allowed(
self, domain_pattern: re.Pattern[str]
) -> None:
"""Test that localhost is explicitly not allowed in DOMAIN_REGEX."""
assert not domain_pattern.match("localhost")
class TestRegexPatternConsistency:
"""Test cases for consistency across regex patterns."""
def test_all_patterns_compile(self) -> None:
"""Test that all regex patterns can be compiled without errors."""
patterns = [
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX,
]
for pattern in patterns:
compiled = compile_re(pattern)
assert isinstance(compiled, re.Pattern)
def test_compiled_patterns_are_patterns(self) -> None:
"""Test that all COMPILED_ constants are Pattern objects."""
compiled_patterns = [
COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX,
COMPILED_NAME_EMAIL_BASIC_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX,
COMPILED_DOMAIN_REGEX,
]
for pattern in compiled_patterns:
assert isinstance(pattern, re.Pattern)
def test_domain_patterns_are_strings(self) -> None:
"""Test that all regex constants are strings."""
assert isinstance(EMAIL_BASIC_REGEX, str)
assert isinstance(NAME_EMAIL_SIMPLE_REGEX, str)
assert isinstance(NAME_EMAIL_BASIC_REGEX, str)
assert isinstance(DOMAIN_WITH_LOCALHOST_REGEX, str)
assert isinstance(DOMAIN_WITH_LOCALHOST_PORT_REGEX, str)
assert isinstance(DOMAIN_REGEX, str)
def test_domain_patterns_hierarchy(self) -> None:
"""Test that domain patterns follow expected hierarchy."""
# DOMAIN_WITH_LOCALHOST_PORT_REGEX should accept everything
# DOMAIN_WITH_LOCALHOST_REGEX accepts
domain_localhost = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
domain_localhost_port = COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX
test_cases = ["example.com", "subdomain.example.com", "localhost"]
for test_case in test_cases:
if domain_localhost.match(test_case):
assert domain_localhost_port.match(test_case), (
f"{test_case} should match both patterns"
)

View File

@@ -1,639 +0,0 @@
"""
Unit tests for debug_handling.debug_helpers module
"""
import sys
import pytest
from corelibs.debug_handling.debug_helpers import (
call_stack,
exception_stack,
OptExcInfo
)
class TestCallStack:
"""Test cases for call_stack function"""
def test_call_stack_basic(self):
"""Test basic call_stack functionality"""
result = call_stack()
assert isinstance(result, str)
assert "test_debug_helpers.py" in result
assert "test_call_stack_basic" in result
def test_call_stack_with_default_separator(self):
"""Test call_stack with default separator"""
result = call_stack()
assert " -> " in result
def test_call_stack_with_custom_separator(self):
"""Test call_stack with custom separator"""
result = call_stack(separator=" | ")
assert " | " in result
assert " -> " not in result
def test_call_stack_with_empty_separator(self):
"""Test call_stack with empty separator (should default to ' -> ')"""
result = call_stack(separator="")
assert " -> " in result
def test_call_stack_format(self):
"""Test call_stack output format (filename:function:lineno)"""
result = call_stack()
parts = result.split(" -> ")
for part in parts:
# Each part should have format: filename:function:lineno
assert part.count(":") >= 2
# Most parts should contain .py but some system frames might not
# Just check that we have some .py files in the trace
assert ".py" in result or "test_debug_helpers" in result
def test_call_stack_with_start_offset(self):
"""Test call_stack with start offset"""
result_no_offset = call_stack(start=0)
result_with_offset = call_stack(start=2)
# With offset, we should get fewer frames
parts_no_offset = result_no_offset.split(" -> ")
parts_with_offset = result_with_offset.split(" -> ")
assert len(parts_with_offset) <= len(parts_no_offset)
def test_call_stack_with_skip_last(self):
"""Test call_stack with skip_last parameter"""
result_skip_default = call_stack(skip_last=-1)
result_skip_more = call_stack(skip_last=-3)
# Skipping more should result in fewer frames
parts_default = result_skip_default.split(" -> ")
parts_more = result_skip_more.split(" -> ")
assert len(parts_more) <= len(parts_default)
def test_call_stack_skip_last_positive_converts_to_negative(self):
"""Test that positive skip_last is converted to negative"""
# Both should produce same result
result_negative = call_stack(skip_last=-2)
result_positive = call_stack(skip_last=2)
assert result_negative == result_positive
def test_call_stack_nested_calls(self):
"""Test call_stack in nested function calls"""
def level_one():
return level_two()
def level_two():
return level_three()
def level_three():
return call_stack()
result = level_one()
assert "level_one" in result
assert "level_two" in result
assert "level_three" in result
def test_call_stack_reset_start_if_empty_false(self):
"""Test call_stack with high start value and reset_start_if_empty=False"""
# Using a very high start value should result in empty stack
result = call_stack(start=1000, reset_start_if_empty=False)
assert result == ""
def test_call_stack_reset_start_if_empty_true(self):
"""Test call_stack with high start value and reset_start_if_empty=True"""
# Using a very high start value with reset should give non-empty result
result = call_stack(start=1000, reset_start_if_empty=True)
assert result != ""
assert "test_debug_helpers.py" in result
def test_call_stack_contains_line_numbers(self):
"""Test that call_stack includes line numbers"""
result = call_stack()
# Extract parts and check for numbers
parts = result.split(" -> ")
for part in parts:
# Line numbers should be present (digits at the end)
assert any(char.isdigit() for char in part)
def test_call_stack_separator_none(self):
"""Test call_stack with None separator"""
result = call_stack(separator="") # Use empty string instead of None
# Empty string should be converted to default ' -> '
assert " -> " in result
def test_call_stack_multiple_separators(self):
"""Test call_stack with various custom separators"""
separators = [" | ", " >> ", " => ", " / ", "\n"]
for sep in separators:
result = call_stack(separator=sep)
assert sep in result or result == "" # May be empty based on stack depth
class TestExceptionStack:
"""Test cases for exception_stack function"""
def test_exception_stack_with_active_exception(self):
"""Test exception_stack when an exception is active"""
try:
raise ValueError("Test exception")
except ValueError:
result = exception_stack()
assert isinstance(result, str)
assert "test_debug_helpers.py" in result
assert "test_exception_stack_with_active_exception" in result
def test_exception_stack_format(self):
"""Test exception_stack output format"""
try:
raise RuntimeError("Test error")
except RuntimeError:
result = exception_stack()
parts = result.split(" -> ")
for part in parts:
# Each part should have format: filename:function:lineno
assert part.count(":") >= 2
def test_exception_stack_with_custom_separator(self):
"""Test exception_stack with custom separator"""
def nested_call():
def inner_call():
raise TypeError("Test type error")
inner_call()
try:
nested_call()
except TypeError:
result = exception_stack(separator=" | ")
# Only check separator if there are multiple frames
if " | " in result or result.count(":") == 2:
# Single frame or has separator
assert isinstance(result, str)
assert " -> " not in result
def test_exception_stack_with_empty_separator(self):
"""Test exception_stack with empty separator (should default to ' -> ')"""
def nested_call():
def inner_call():
raise KeyError("Test key error")
inner_call()
try:
nested_call()
except KeyError:
result = exception_stack(separator="")
# Should use default separator if multiple frames exist
assert isinstance(result, str)
def test_exception_stack_separator_none(self):
"""Test exception_stack with empty separator"""
def nested_call():
def inner_call():
raise IndexError("Test index error")
inner_call()
try:
nested_call()
except IndexError:
result = exception_stack(separator="") # Use empty string instead of None
assert isinstance(result, str)
def test_exception_stack_nested_exceptions(self):
"""Test exception_stack with nested function calls"""
def level_one():
level_two()
def level_two():
level_three()
def level_three():
raise ValueError("Nested exception")
try:
level_one()
except ValueError:
result = exception_stack()
# Should contain all levels in the stack
assert "level_one" in result or "level_two" in result or "level_three" in result
def test_exception_stack_with_provided_exc_info(self):
"""Test exception_stack with explicitly provided exc_info"""
try:
raise AttributeError("Test attribute error")
except AttributeError:
exc_info = sys.exc_info()
result = exception_stack(exc_stack=exc_info)
assert isinstance(result, str)
assert len(result) > 0
def test_exception_stack_no_active_exception(self):
"""Test exception_stack when no exception is active"""
# This should handle the case gracefully
# When no exception is active, sys.exc_info() returns (None, None, None)
result = exception_stack()
# With no traceback, should return empty string or handle gracefully
assert isinstance(result, str)
def test_exception_stack_contains_line_numbers(self):
"""Test that exception_stack includes line numbers"""
try:
raise OSError("Test OS error")
except OSError:
result = exception_stack()
if result: # May be empty
parts = result.split(" -> ")
for part in parts:
# Line numbers should be present
assert any(char.isdigit() for char in part)
def test_exception_stack_multiple_exceptions(self):
"""Test exception_stack captures the current exception only"""
first_result = None
second_result = None
try:
raise ValueError("First exception")
except ValueError:
first_result = exception_stack()
try:
raise TypeError("Second exception")
except TypeError:
second_result = exception_stack()
# Both should be valid but may differ
assert isinstance(first_result, str)
assert isinstance(second_result, str)
def test_exception_stack_with_multiple_separators(self):
"""Test exception_stack with various custom separators"""
separators = [" | ", " >> ", " => ", " / ", "\n"]
def nested_call():
def inner_call():
raise ValueError("Test exception")
inner_call()
for sep in separators:
try:
nested_call()
except ValueError:
result = exception_stack(separator=sep)
assert isinstance(result, str)
# Separator only appears if there are multiple frames
class TestOptExcInfo:
"""Test cases for OptExcInfo type definition"""
def test_opt_exc_info_type_none_tuple(self):
"""Test OptExcInfo can be None tuple"""
exc_info: OptExcInfo = (None, None, None)
assert exc_info == (None, None, None)
def test_opt_exc_info_type_exception_tuple(self):
"""Test OptExcInfo can be exception tuple"""
try:
raise ValueError("Test")
except ValueError:
exc_info: OptExcInfo = sys.exc_info()
assert exc_info[0] is not None
assert exc_info[1] is not None
assert exc_info[2] is not None
def test_opt_exc_info_with_exception_stack(self):
"""Test that OptExcInfo works with exception_stack function"""
try:
raise RuntimeError("Test runtime error")
except RuntimeError:
exc_info = sys.exc_info()
result = exception_stack(exc_stack=exc_info)
assert isinstance(result, str)
class TestIntegration:
"""Integration tests combining multiple scenarios"""
def test_call_stack_and_exception_stack_together(self):
"""Test using both call_stack and exception_stack in error handling"""
def faulty_function():
_ = call_stack() # Get call stack before exception
raise ValueError("Intentional error")
try:
faulty_function()
except ValueError:
exception_trace = exception_stack()
assert isinstance(exception_trace, str)
assert "faulty_function" in exception_trace or "test_debug_helpers.py" in exception_trace
def test_nested_exception_with_call_stack(self):
"""Test call_stack within exception handling"""
def outer():
return inner()
def inner():
try:
raise RuntimeError("Inner error")
except RuntimeError:
return {
'call_stack': call_stack(),
'exception_stack': exception_stack()
}
result = outer()
assert 'call_stack' in result
assert 'exception_stack' in result
assert isinstance(result['call_stack'], str)
assert isinstance(result['exception_stack'], str)
def test_multiple_nested_levels(self):
"""Test with multiple nested function levels"""
def level_a():
return level_b()
def level_b():
return level_c()
def level_c():
return level_d()
def level_d():
try:
raise ValueError("Deep error")
except ValueError:
return {
'call': call_stack(),
'exception': exception_stack()
}
result = level_a()
# Should contain information about the call chain
assert result['call']
assert result['exception']
def test_different_separators_consistency(self):
"""Test that different separators work consistently"""
separators = [" -> ", " | ", " / ", " >> "]
def nested_call():
def inner_call():
raise ValueError("Test")
inner_call()
for sep in separators:
try:
nested_call()
except ValueError:
exc_result = exception_stack(separator=sep)
call_result = call_stack(separator=sep)
assert isinstance(exc_result, str)
assert isinstance(call_result, str)
# Both should be valid strings (separator check only if multiple frames)
class TestEdgeCases:
"""Test edge cases and boundary conditions"""
def test_call_stack_with_zero_start(self):
"""Test call_stack with start=0 (should include all frames)"""
result = call_stack(start=0)
assert isinstance(result, str)
assert len(result) > 0
def test_call_stack_with_large_skip_last(self):
"""Test call_stack with very large skip_last value"""
result = call_stack(skip_last=-100)
# Should handle gracefully, may be empty
assert isinstance(result, str)
def test_exception_stack_none_exc_info(self):
"""Test exception_stack with None as exc_stack"""
result = exception_stack(exc_stack=None)
assert isinstance(result, str)
def test_exception_stack_empty_tuple(self):
"""Test exception_stack with empty exception info"""
exc_info: OptExcInfo = (None, None, None)
result = exception_stack(exc_stack=exc_info)
assert isinstance(result, str)
def test_call_stack_special_characters_in_separator(self):
"""Test call_stack with special characters in separator"""
special_separators = ["\n", "\t", "->", "||", "//"]
for sep in special_separators:
result = call_stack(separator=sep)
assert isinstance(result, str)
def test_very_deep_call_stack(self):
"""Test call_stack with very deep recursion (up to a limit)"""
def recursive_call(depth: int, max_depth: int = 5) -> str:
if depth >= max_depth:
return call_stack()
return recursive_call(depth + 1, max_depth)
result = recursive_call(0)
assert isinstance(result, str)
# Should contain multiple recursive_call entries
assert result.count("recursive_call") > 0
def test_exception_stack_different_exception_types(self):
"""Test exception_stack with various exception types"""
exception_types = [
ValueError("value"),
TypeError("type"),
KeyError("key"),
IndexError("index"),
AttributeError("attr"),
RuntimeError("runtime"),
]
for exc in exception_types:
try:
raise exc
except (ValueError, TypeError, KeyError, IndexError, AttributeError, RuntimeError):
result = exception_stack()
assert isinstance(result, str)
class TestRealWorldScenarios:
"""Test real-world debugging scenarios"""
def test_debugging_workflow(self):
"""Test typical debugging workflow with both functions"""
def process_data(data: str) -> str:
_ = call_stack() # Capture call stack for debugging
if not data:
raise ValueError("No data provided")
return data.upper()
# Success case
result = process_data("test")
assert result == "TEST"
# Error case
try:
process_data("")
except ValueError:
exc_trace = exception_stack()
assert isinstance(exc_trace, str)
def test_logging_context(self):
"""Test using call_stack for logging context"""
def get_logging_context():
return {
'timestamp': 'now',
'stack': call_stack(start=1, separator=" > "),
'function': 'get_logging_context'
}
context = get_logging_context()
assert 'stack' in context
assert 'timestamp' in context
assert isinstance(context['stack'], str)
def test_error_reporting(self):
"""Test comprehensive error reporting"""
def dangerous_operation() -> dict[str, str]:
try:
# Simulate some operation
_ = 1 / 0
except ZeroDivisionError:
return {
'error': 'Division by zero',
'call_stack': call_stack(),
'exception_stack': exception_stack(),
}
return {} # Fallback return
error_report = dangerous_operation()
assert error_report is not None
assert 'error' in error_report
assert 'call_stack' in error_report
assert 'exception_stack' in error_report
assert error_report['error'] == 'Division by zero'
def test_function_tracing(self):
"""Test function call tracing"""
traces: list[str] = []
def traced_function_a() -> str:
traces.append(call_stack())
return traced_function_b()
def traced_function_b() -> str:
traces.append(call_stack())
return traced_function_c()
def traced_function_c() -> str:
traces.append(call_stack())
return "done"
result = traced_function_a()
assert result == "done"
assert len(traces) == 3
# Each trace should be different (different call depths)
assert all(isinstance(t, str) for t in traces)
def test_exception_chain_tracking(self):
"""Test tracking exception chains"""
exception_traces: list[str] = []
def operation_one() -> None:
try:
operation_two()
except ValueError:
exception_traces.append(exception_stack())
raise
def operation_two() -> None:
try:
operation_three()
except TypeError as exc:
exception_traces.append(exception_stack())
raise ValueError("Wrapped error") from exc
def operation_three() -> None:
raise TypeError("Original error")
try:
operation_one()
except ValueError:
exception_traces.append(exception_stack())
# Should have captured multiple exception stacks
assert len(exception_traces) > 0
assert all(isinstance(t, str) for t in exception_traces)
class TestParametrized:
"""Parametrized tests for comprehensive coverage"""
@pytest.mark.parametrize("start", [0, 1, 2, 5, 10])
def test_call_stack_various_starts(self, start: int) -> None:
"""Test call_stack with various start values"""
result = call_stack(start=start)
assert isinstance(result, str)
@pytest.mark.parametrize("skip_last", [-1, -2, -3, -5, 1, 2, 3, 5])
def test_call_stack_various_skip_lasts(self, skip_last: int) -> None:
"""Test call_stack with various skip_last values"""
result = call_stack(skip_last=skip_last)
assert isinstance(result, str)
@pytest.mark.parametrize("separator", [" -> ", " | ", " / ", " >> ", " => ", "\n", "\t"])
def test_call_stack_various_separators(self, separator: str) -> None:
"""Test call_stack with various separators"""
result = call_stack(separator=separator)
assert isinstance(result, str)
if result:
assert separator in result
@pytest.mark.parametrize("reset_start", [True, False])
def test_call_stack_reset_start_variations(self, reset_start: bool) -> None:
"""Test call_stack with reset_start_if_empty variations"""
result = call_stack(start=100, reset_start_if_empty=reset_start)
assert isinstance(result, str)
if reset_start:
assert len(result) > 0 # Should have content after reset
else:
assert len(result) == 0 # Should be empty
@pytest.mark.parametrize("separator", [" -> ", " | ", " / ", " >> ", "\n"])
def test_exception_stack_various_separators(self, separator: str) -> None:
"""Test exception_stack with various separators"""
def nested_call():
def inner_call():
raise ValueError("Test")
inner_call()
try:
nested_call()
except ValueError:
result = exception_stack(separator=separator)
assert isinstance(result, str)
# Check that result is valid (separator only if multiple frames exist)
@pytest.mark.parametrize("exception_type", [
ValueError,
TypeError,
KeyError,
IndexError,
AttributeError,
RuntimeError,
OSError,
])
def test_exception_stack_various_exception_types(self, exception_type: type[Exception]) -> None:
"""Test exception_stack with various exception types"""
try:
raise exception_type("Test exception")
except (ValueError, TypeError, KeyError, IndexError, AttributeError, RuntimeError, OSError):
result = exception_stack()
assert isinstance(result, str)
# __END__

View File

@@ -1,288 +0,0 @@
"""
Unit tests for debug_handling.dump_data module
"""
import json
from datetime import datetime, date
from decimal import Decimal
from typing import Any
import pytest
from corelibs.debug_handling.dump_data import dump_data
class TestDumpData:
"""Test cases for dump_data function"""
def test_dump_simple_dict(self):
"""Test dumping a simple dictionary"""
data = {"name": "John", "age": 30}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_simple_list(self):
"""Test dumping a simple list"""
data = [1, 2, 3, 4, 5]
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_nested_dict(self):
"""Test dumping a nested dictionary"""
data = {
"user": {
"name": "Alice",
"address": {
"city": "Tokyo",
"country": "Japan"
}
}
}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_mixed_types(self):
"""Test dumping data with mixed types"""
data = {
"string": "test",
"number": 42,
"float": 3.14,
"boolean": True,
"null": None,
"list": [1, 2, 3]
}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_with_indent_default(self):
"""Test that indent is applied by default"""
data = {"a": 1, "b": 2}
result = dump_data(data)
# With indent, result should contain newlines
assert "\n" in result
assert " " in result # 4 spaces for indent
def test_dump_with_indent_true(self):
"""Test explicit indent=True"""
data = {"a": 1, "b": 2}
result = dump_data(data, use_indent=True)
# With indent, result should contain newlines
assert "\n" in result
assert " " in result # 4 spaces for indent
def test_dump_without_indent(self):
"""Test dumping without indentation"""
data = {"a": 1, "b": 2}
result = dump_data(data, use_indent=False)
# Without indent, result should be compact
assert "\n" not in result
assert result == '{"a": 1, "b": 2}'
def test_dump_unicode_characters(self):
"""Test that unicode characters are preserved (ensure_ascii=False)"""
data = {"message": "こんにちは", "emoji": "😀", "german": "Müller"}
result = dump_data(data)
# Unicode characters should be preserved, not escaped
assert "こんにちは" in result
assert "😀" in result
assert "Müller" in result
parsed = json.loads(result)
assert parsed == data
def test_dump_datetime_object(self):
"""Test dumping data with datetime objects (using default=str)"""
now = datetime(2023, 10, 15, 14, 30, 0)
data = {"timestamp": now}
result = dump_data(data)
assert isinstance(result, str)
# datetime should be converted to string
assert "2023-10-15" in result
def test_dump_date_object(self):
"""Test dumping data with date objects"""
today = date(2023, 10, 15)
data = {"date": today}
result = dump_data(data)
assert isinstance(result, str)
assert "2023-10-15" in result
def test_dump_decimal_object(self):
"""Test dumping data with Decimal objects"""
data = {"amount": Decimal("123.45")}
result = dump_data(data)
assert isinstance(result, str)
assert "123.45" in result
def test_dump_empty_dict(self):
"""Test dumping an empty dictionary"""
data = {}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == {}
def test_dump_empty_list(self):
"""Test dumping an empty list"""
data = []
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == []
def test_dump_string_directly(self):
"""Test dumping a string directly"""
data = "Hello, World!"
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_number_directly(self):
"""Test dumping a number directly"""
data = 42
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_boolean_directly(self):
"""Test dumping a boolean directly"""
data = True
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed is True
def test_dump_none_directly(self):
"""Test dumping None directly"""
data = None
result = dump_data(data)
assert isinstance(result, str)
assert result == "null"
parsed = json.loads(result)
assert parsed is None
def test_dump_complex_nested_structure(self):
"""Test dumping a complex nested structure"""
data = {
"users": [
{
"id": 1,
"name": "Alice",
"tags": ["admin", "user"],
"metadata": {
"created": datetime(2023, 1, 1),
"active": True
}
},
{
"id": 2,
"name": "Bob",
"tags": ["user"],
"metadata": {
"created": datetime(2023, 6, 15),
"active": False
}
}
],
"total": 2
}
result = dump_data(data)
assert isinstance(result, str)
# Check that it's valid JSON
parsed = json.loads(result)
assert len(parsed["users"]) == 2
assert parsed["total"] == 2
def test_dump_special_characters(self):
"""Test dumping data with special characters"""
data = {
"quote": 'He said "Hello"',
"backslash": "path\\to\\file",
"newline": "line1\nline2",
"tab": "col1\tcol2"
}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
def test_dump_large_numbers(self):
"""Test dumping large numbers"""
data = {
"big_int": 123456789012345678901234567890,
"big_float": 1.23456789e100
}
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed["big_int"] == data["big_int"]
def test_dump_list_of_dicts(self):
"""Test dumping a list of dictionaries"""
data = [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
]
result = dump_data(data)
assert isinstance(result, str)
parsed = json.loads(result)
assert parsed == data
assert len(parsed) == 3
class CustomObject:
"""Custom class for testing default=str conversion"""
def __init__(self, value: Any):
self.value = value
def __str__(self):
return f"CustomObject({self.value})"
class TestDumpDataWithCustomObjects:
"""Test cases for dump_data with custom objects"""
def test_dump_custom_object(self):
"""Test that custom objects are converted using str()"""
obj = CustomObject("test")
data = {"custom": obj}
result = dump_data(data)
assert isinstance(result, str)
assert "CustomObject(test)" in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,560 +0,0 @@
"""
Unit tests for corelibs.debug_handling.profiling module
"""
import time
import tracemalloc
from corelibs.debug_handling.profiling import display_top, Profiling
class TestDisplayTop:
"""Test display_top function"""
def test_display_top_basic(self):
"""Test that display_top returns a string with basic stats"""
tracemalloc.start()
# Allocate some memory
data = [0] * 10000
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot)
assert isinstance(result, str)
assert "Top 10 lines" in result
assert "KiB" in result
assert "Total allocated size:" in result
# Clean up
del data
def test_display_top_with_custom_limit(self):
"""Test display_top with custom limit parameter"""
tracemalloc.start()
# Allocate some memory
data = [0] * 10000
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot, limit=5)
assert isinstance(result, str)
assert "Top 5 lines" in result
# Clean up
del data
def test_display_top_with_different_key_type(self):
"""Test display_top with different key_type parameter"""
tracemalloc.start()
# Allocate some memory
data = [0] * 10000
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot, key_type='filename')
assert isinstance(result, str)
assert "Top 10 lines" in result
# Clean up
del data
def test_display_top_filters_traces(self):
"""Test that display_top filters out bootstrap and unknown traces"""
tracemalloc.start()
# Allocate some memory
data = [0] * 10000
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot)
# Should not contain filtered traces
assert "<frozen importlib._bootstrap>" not in result
assert "<unknown>" not in result
# Clean up
del data
def test_display_top_with_limit_larger_than_stats(self):
"""Test display_top when limit is larger than available stats"""
tracemalloc.start()
# Allocate some memory
data = [0] * 100
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot, limit=1000)
assert isinstance(result, str)
assert "Top 1000 lines" in result
assert "Total allocated size:" in result
# Clean up
del data
def test_display_top_empty_snapshot(self):
"""Test display_top with a snapshot that has minimal traces"""
tracemalloc.start()
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
result = display_top(snapshot, limit=1)
assert isinstance(result, str)
assert "Top 1 lines" in result
class TestProfilingInitialization:
"""Test Profiling class initialization"""
def test_profiling_initialization(self):
"""Test that Profiling initializes correctly"""
profiler = Profiling()
# Should be able to create instance
assert isinstance(profiler, Profiling)
def test_profiling_initial_state(self):
"""Test that Profiling starts in a clean state"""
profiler = Profiling()
# Should not raise an error when calling end_profiling
# even though start_profiling wasn't called
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
class TestProfilingStartEnd:
"""Test start_profiling and end_profiling functionality"""
def test_start_profiling(self):
"""Test that start_profiling can be called"""
profiler = Profiling()
# Should not raise an error
profiler.start_profiling("test_operation")
def test_end_profiling(self):
"""Test that end_profiling can be called"""
profiler = Profiling()
profiler.start_profiling("test_operation")
# Should not raise an error
profiler.end_profiling()
def test_start_profiling_with_different_idents(self):
"""Test start_profiling with different identifier strings"""
profiler = Profiling()
identifiers = ["short", "longer_identifier", "very_long_identifier_with_many_chars"]
for ident in identifiers:
profiler.start_profiling(ident)
profiler.end_profiling()
result = profiler.print_profiling()
assert ident in result
def test_end_profiling_without_start(self):
"""Test that end_profiling can be called without start_profiling"""
profiler = Profiling()
# Should not raise an error but internal state should indicate warning
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
def test_profiling_measures_time(self):
"""Test that profiling measures elapsed time"""
profiler = Profiling()
profiler.start_profiling("time_test")
sleep_duration = 0.05 # 50ms
time.sleep(sleep_duration)
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "time:" in result
# Should have some time measurement
assert "ms" in result or "s" in result
def test_profiling_measures_memory(self):
"""Test that profiling measures memory usage"""
profiler = Profiling()
profiler.start_profiling("memory_test")
# Allocate some memory
data = [0] * 100000
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "RSS:" in result
assert "VMS:" in result
assert "time:" in result
# Clean up
del data
class TestProfilingPrintProfiling:
"""Test print_profiling functionality"""
def test_print_profiling_returns_string(self):
"""Test that print_profiling returns a string"""
profiler = Profiling()
profiler.start_profiling("test")
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
def test_print_profiling_contains_identifier(self):
"""Test that print_profiling includes the identifier"""
profiler = Profiling()
identifier = "my_test_operation"
profiler.start_profiling(identifier)
profiler.end_profiling()
result = profiler.print_profiling()
assert identifier in result
def test_print_profiling_format(self):
"""Test that print_profiling has expected format"""
profiler = Profiling()
profiler.start_profiling("test")
profiler.end_profiling()
result = profiler.print_profiling()
# Check for expected components
assert "Profiling:" in result
assert "RSS:" in result
assert "VMS:" in result
assert "time:" in result
def test_print_profiling_multiple_calls(self):
"""Test that print_profiling can be called multiple times"""
profiler = Profiling()
profiler.start_profiling("test")
profiler.end_profiling()
result1 = profiler.print_profiling()
result2 = profiler.print_profiling()
# Should return the same result
assert result1 == result2
def test_print_profiling_time_formats(self):
"""Test different time format outputs"""
profiler = Profiling()
# Very short duration (milliseconds)
profiler.start_profiling("ms_test")
time.sleep(0.001)
profiler.end_profiling()
result = profiler.print_profiling()
assert "ms" in result
# Slightly longer duration (seconds)
profiler.start_profiling("s_test")
time.sleep(0.1)
profiler.end_profiling()
result = profiler.print_profiling()
# Could be ms or s depending on timing
assert ("ms" in result or "s" in result)
def test_print_profiling_memory_formats(self):
"""Test different memory format outputs"""
profiler = Profiling()
profiler.start_profiling("memory_format_test")
# Allocate some memory
data = [0] * 50000
profiler.end_profiling()
result = profiler.print_profiling()
# Should have some memory unit (B, kB, MB, GB)
assert any(unit in result for unit in ["B", "kB", "MB", "GB"])
# Clean up
del data
class TestProfilingIntegration:
"""Integration tests for Profiling class"""
def test_complete_profiling_cycle(self):
"""Test a complete profiling cycle from start to print"""
profiler = Profiling()
profiler.start_profiling("complete_cycle")
# Do some work
data = [i for i in range(10000)]
time.sleep(0.01)
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "complete_cycle" in result
assert "RSS:" in result
assert "VMS:" in result
assert "time:" in result
# Clean up
del data
def test_multiple_profiling_sessions(self):
"""Test running multiple profiling sessions"""
profiler = Profiling()
# First session
profiler.start_profiling("session_1")
time.sleep(0.01)
profiler.end_profiling()
result1 = profiler.print_profiling()
# Second session (same profiler instance)
profiler.start_profiling("session_2")
data = [0] * 100000
time.sleep(0.01)
profiler.end_profiling()
result2 = profiler.print_profiling()
# Results should be different
assert "session_1" in result1
assert "session_2" in result2
assert result1 != result2
# Clean up
del data
def test_profiling_with_zero_work(self):
"""Test profiling with minimal work"""
profiler = Profiling()
profiler.start_profiling("zero_work")
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "zero_work" in result
def test_profiling_with_heavy_computation(self):
"""Test profiling with heavier computation"""
profiler = Profiling()
profiler.start_profiling("heavy_computation")
# Do some computation
result_data: list[list[int]] = []
for _ in range(1000):
result_data.append([j * 2 for j in range(100)])
time.sleep(0.05)
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "heavy_computation" in result
# Should show measurable time and memory
assert "time:" in result
# Clean up
del result_data
def test_independent_profilers(self):
"""Test that multiple Profiling instances are independent"""
profiler1 = Profiling()
profiler2 = Profiling()
profiler1.start_profiling("profiler_1")
time.sleep(0.01)
profiler2.start_profiling("profiler_2")
data = [0] * 100000
time.sleep(0.01)
profiler1.end_profiling()
profiler2.end_profiling()
result1 = profiler1.print_profiling()
result2 = profiler2.print_profiling()
# Should have different identifiers
assert "profiler_1" in result1
assert "profiler_2" in result2
# Results should be different
assert result1 != result2
# Clean up
del data
class TestProfilingEdgeCases:
"""Test edge cases and boundary conditions"""
def test_empty_identifier(self):
"""Test profiling with empty identifier"""
profiler = Profiling()
profiler.start_profiling("")
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "Profiling:" in result
def test_very_long_identifier(self):
"""Test profiling with very long identifier"""
profiler = Profiling()
long_ident = "a" * 100
profiler.start_profiling(long_ident)
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert long_ident in result
def test_special_characters_in_identifier(self):
"""Test profiling with special characters in identifier"""
profiler = Profiling()
special_ident = "test_@#$%_operation"
profiler.start_profiling(special_ident)
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert special_ident in result
def test_rapid_consecutive_profiling(self):
"""Test rapid consecutive profiling cycles"""
profiler = Profiling()
for i in range(5):
profiler.start_profiling(f"rapid_{i}")
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert f"rapid_{i}" in result
def test_profiling_negative_memory_change(self):
"""Test profiling when memory usage decreases"""
profiler = Profiling()
# Allocate some memory before profiling
pre_data = [0] * 1000000
profiler.start_profiling("memory_decrease")
# Free the memory
del pre_data
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "memory_decrease" in result
# Should handle negative memory change gracefully
def test_very_short_duration(self):
"""Test profiling with extremely short duration"""
profiler = Profiling()
profiler.start_profiling("instant")
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
assert "instant" in result
assert "ms" in result # Should show milliseconds for very short duration
class TestProfilingContextManager:
"""Test profiling usage patterns similar to context managers"""
def test_typical_usage_pattern(self):
"""Test typical usage pattern for profiling"""
profiler = Profiling()
# Typical pattern
profiler.start_profiling("typical_operation")
# Perform operation
result_list: list[int] = []
for _ in range(1000):
result_list.append(_ * 2)
profiler.end_profiling()
# Get results
output = profiler.print_profiling()
assert isinstance(output, str)
assert "typical_operation" in output
# Clean up
del result_list
def test_profiling_without_end(self):
"""Test what happens when end_profiling is not called"""
profiler = Profiling()
profiler.start_profiling("no_end")
# Don't call end_profiling
result = profiler.print_profiling()
# Should still return a string (though data might be incomplete)
assert isinstance(result, str)
def test_profiling_end_without_start(self):
"""Test calling end_profiling multiple times without start"""
profiler = Profiling()
profiler.end_profiling()
profiler.end_profiling()
result = profiler.print_profiling()
assert isinstance(result, str)
# __END__

View File

@@ -1,405 +0,0 @@
"""
Unit tests for corelibs.debug_handling.timer module
"""
import time
from datetime import datetime, timedelta
from corelibs.debug_handling.timer import Timer
class TestTimerInitialization:
"""Test Timer class initialization"""
def test_timer_initialization(self):
"""Test that Timer initializes with correct default values"""
timer = Timer()
# Check that start times are set
assert isinstance(timer.get_overall_start_time(), datetime)
assert isinstance(timer.get_start_time(), datetime)
# Check that end times are None
assert timer.get_overall_end_time() is None
assert timer.get_end_time() is None
# Check that run times are None
assert timer.get_overall_run_time() is None
assert timer.get_run_time() is None
def test_timer_start_times_are_recent(self):
"""Test that start times are set to current time on initialization"""
before_init = datetime.now()
timer = Timer()
after_init = datetime.now()
overall_start = timer.get_overall_start_time()
start = timer.get_start_time()
assert before_init <= overall_start <= after_init
assert before_init <= start <= after_init
def test_timer_start_times_are_same(self):
"""Test that overall_start_time and start_time are initialized to the same time"""
timer = Timer()
overall_start = timer.get_overall_start_time()
start = timer.get_start_time()
# They should be very close (within a few microseconds)
time_diff = abs((overall_start - start).total_seconds())
assert time_diff < 0.001 # Less than 1 millisecond
class TestOverallRunTime:
"""Test overall run time functionality"""
def test_overall_run_time_returns_timedelta(self):
"""Test that overall_run_time returns a timedelta object"""
timer = Timer()
time.sleep(0.01) # Sleep for 10ms
result = timer.overall_run_time()
assert isinstance(result, timedelta)
def test_overall_run_time_sets_end_time(self):
"""Test that calling overall_run_time sets the end time"""
timer = Timer()
assert timer.get_overall_end_time() is None
timer.overall_run_time()
assert isinstance(timer.get_overall_end_time(), datetime)
def test_overall_run_time_sets_run_time(self):
"""Test that calling overall_run_time sets the run time"""
timer = Timer()
assert timer.get_overall_run_time() is None
timer.overall_run_time()
assert isinstance(timer.get_overall_run_time(), timedelta)
def test_overall_run_time_accuracy(self):
"""Test that overall_run_time calculates time difference accurately"""
timer = Timer()
sleep_duration = 0.05 # 50ms
time.sleep(sleep_duration)
result = timer.overall_run_time()
# Allow for some variance (10ms tolerance)
assert sleep_duration - 0.01 <= result.total_seconds() <= sleep_duration + 0.01
def test_overall_run_time_multiple_calls(self):
"""Test that calling overall_run_time multiple times updates the values"""
timer = Timer()
time.sleep(0.01)
first_result = timer.overall_run_time()
first_end_time = timer.get_overall_end_time()
time.sleep(0.01)
second_result = timer.overall_run_time()
second_end_time = timer.get_overall_end_time()
# Second call should have longer runtime
assert second_result > first_result
assert second_end_time is not None
assert first_end_time is not None
# End time should be updated
assert second_end_time > first_end_time
def test_overall_run_time_consistency(self):
"""Test that get_overall_run_time returns the same value as overall_run_time"""
timer = Timer()
time.sleep(0.01)
calculated_time = timer.overall_run_time()
retrieved_time = timer.get_overall_run_time()
assert calculated_time == retrieved_time
class TestRunTime:
"""Test run time functionality"""
def test_run_time_returns_timedelta(self):
"""Test that run_time returns a timedelta object"""
timer = Timer()
time.sleep(0.01)
result = timer.run_time()
assert isinstance(result, timedelta)
def test_run_time_sets_end_time(self):
"""Test that calling run_time sets the end time"""
timer = Timer()
assert timer.get_end_time() is None
timer.run_time()
assert isinstance(timer.get_end_time(), datetime)
def test_run_time_sets_run_time(self):
"""Test that calling run_time sets the run time"""
timer = Timer()
assert timer.get_run_time() is None
timer.run_time()
assert isinstance(timer.get_run_time(), timedelta)
def test_run_time_accuracy(self):
"""Test that run_time calculates time difference accurately"""
timer = Timer()
sleep_duration = 0.05 # 50ms
time.sleep(sleep_duration)
result = timer.run_time()
# Allow for some variance (10ms tolerance)
assert sleep_duration - 0.01 <= result.total_seconds() <= sleep_duration + 0.01
def test_run_time_multiple_calls(self):
"""Test that calling run_time multiple times updates the values"""
timer = Timer()
time.sleep(0.01)
first_result = timer.run_time()
first_end_time = timer.get_end_time()
time.sleep(0.01)
second_result = timer.run_time()
second_end_time = timer.get_end_time()
# Second call should have longer runtime
assert second_result > first_result
assert second_end_time is not None
assert first_end_time is not None
# End time should be updated
assert second_end_time > first_end_time
def test_run_time_consistency(self):
"""Test that get_run_time returns the same value as run_time"""
timer = Timer()
time.sleep(0.01)
calculated_time = timer.run_time()
retrieved_time = timer.get_run_time()
assert calculated_time == retrieved_time
class TestResetRunTime:
"""Test reset_run_time functionality"""
def test_reset_run_time_resets_start_time(self):
"""Test that reset_run_time updates the start time"""
timer = Timer()
original_start = timer.get_start_time()
time.sleep(0.02)
timer.reset_run_time()
new_start = timer.get_start_time()
assert new_start > original_start
def test_reset_run_time_clears_end_time(self):
"""Test that reset_run_time clears the end time"""
timer = Timer()
timer.run_time()
assert timer.get_end_time() is not None
timer.reset_run_time()
assert timer.get_end_time() is None
def test_reset_run_time_clears_run_time(self):
"""Test that reset_run_time clears the run time"""
timer = Timer()
timer.run_time()
assert timer.get_run_time() is not None
timer.reset_run_time()
assert timer.get_run_time() is None
def test_reset_run_time_does_not_affect_overall_times(self):
"""Test that reset_run_time does not affect overall times"""
timer = Timer()
overall_start = timer.get_overall_start_time()
timer.overall_run_time()
overall_end = timer.get_overall_end_time()
overall_run = timer.get_overall_run_time()
timer.reset_run_time()
# Overall times should remain unchanged
assert timer.get_overall_start_time() == overall_start
assert timer.get_overall_end_time() == overall_end
assert timer.get_overall_run_time() == overall_run
def test_reset_run_time_allows_new_measurement(self):
"""Test that reset_run_time allows for new time measurements"""
timer = Timer()
time.sleep(0.02)
timer.run_time()
first_run_time = timer.get_run_time()
timer.reset_run_time()
time.sleep(0.01)
timer.run_time()
second_run_time = timer.get_run_time()
assert second_run_time is not None
assert first_run_time is not None
# Second measurement should be shorter since we reset
assert second_run_time < first_run_time
class TestTimerIntegration:
"""Integration tests for Timer class"""
def test_independent_timers(self):
"""Test that multiple Timer instances are independent"""
timer1 = Timer()
time.sleep(0.01)
timer2 = Timer()
# timer1 should have earlier start time
assert timer1.get_start_time() < timer2.get_start_time()
assert timer1.get_overall_start_time() < timer2.get_overall_start_time()
def test_overall_and_run_time_independence(self):
"""Test that overall time and run time are independent"""
timer = Timer()
time.sleep(0.02)
# Reset run time but not overall
timer.reset_run_time()
time.sleep(0.01)
run_time = timer.run_time()
overall_time = timer.overall_run_time()
# Overall time should be longer than run time
assert overall_time > run_time
def test_typical_usage_pattern(self):
"""Test a typical usage pattern of the Timer class"""
timer = Timer()
# Measure first operation
time.sleep(0.01)
first_operation = timer.run_time()
assert first_operation.total_seconds() > 0
# Reset and measure second operation
timer.reset_run_time()
time.sleep(0.01)
second_operation = timer.run_time()
assert second_operation.total_seconds() > 0
# Get overall time
overall = timer.overall_run_time()
# Overall should be greater than individual operations
assert overall > first_operation
assert overall > second_operation
def test_zero_sleep_timer(self):
"""Test timer with minimal sleep (edge case)"""
timer = Timer()
# Call run_time immediately
result = timer.run_time()
# Should still return a valid timedelta (very small)
assert isinstance(result, timedelta)
assert result.total_seconds() >= 0
def test_getter_methods_before_calculation(self):
"""Test that getter methods return None before calculation methods are called"""
timer = Timer()
# Before calling run_time()
assert timer.get_end_time() is None
assert timer.get_run_time() is None
# Before calling overall_run_time()
assert timer.get_overall_end_time() is None
assert timer.get_overall_run_time() is None
# But start times should always be set
assert timer.get_start_time() is not None
assert timer.get_overall_start_time() is not None
class TestTimerEdgeCases:
"""Test edge cases and boundary conditions"""
def test_rapid_consecutive_calls(self):
"""Test rapid consecutive calls to run_time"""
timer = Timer()
results: list[timedelta] = []
for _ in range(5):
results.append(timer.run_time())
# Each result should be greater than or equal to the previous
for i in range(1, len(results)):
assert results[i] >= results[i - 1]
def test_very_short_duration(self):
"""Test timer with very short duration"""
timer = Timer()
result = timer.run_time()
# Should be a very small positive timedelta
assert isinstance(result, timedelta)
assert result.total_seconds() >= 0
assert result.total_seconds() < 0.1 # Less than 100ms
def test_reset_multiple_times(self):
"""Test resetting the timer multiple times"""
timer = Timer()
for _ in range(3):
timer.reset_run_time()
time.sleep(0.01)
result = timer.run_time()
assert isinstance(result, timedelta)
assert result.total_seconds() > 0
def test_overall_time_persists_through_resets(self):
"""Test that overall time continues even when run_time is reset"""
timer = Timer()
time.sleep(0.01)
timer.reset_run_time()
time.sleep(0.01)
timer.reset_run_time()
overall = timer.overall_run_time()
# Overall time should reflect total elapsed time
assert overall.total_seconds() >= 0.02
# __END__

View File

@@ -1,975 +0,0 @@
"""
Unit tests for debug_handling.writeline module
"""
import io
import pytest
from pytest import CaptureFixture
from corelibs.debug_handling.writeline import (
write_l,
pr_header,
pr_title,
pr_open,
pr_close,
pr_act
)
class TestWriteL:
"""Test cases for write_l function"""
def test_write_l_print_only(self, capsys: CaptureFixture[str]):
"""Test write_l with print_line=True and no file"""
write_l("Test line", print_line=True)
captured = capsys.readouterr()
assert captured.out == "Test line\n"
def test_write_l_no_print_no_file(self, capsys: CaptureFixture[str]):
"""Test write_l with print_line=False and no file (should do nothing)"""
write_l("Test line", print_line=False)
captured = capsys.readouterr()
assert captured.out == ""
def test_write_l_file_only(self, capsys: CaptureFixture[str]):
"""Test write_l with file handler only (no print)"""
fpl = io.StringIO()
write_l("Test line", fpl=fpl, print_line=False)
captured = capsys.readouterr()
assert captured.out == ""
assert fpl.getvalue() == "Test line\n"
fpl.close()
def test_write_l_both_print_and_file(self, capsys: CaptureFixture[str]):
"""Test write_l with both print and file output"""
fpl = io.StringIO()
write_l("Test line", fpl=fpl, print_line=True)
captured = capsys.readouterr()
assert captured.out == "Test line\n"
assert fpl.getvalue() == "Test line\n"
fpl.close()
def test_write_l_multiple_lines_to_file(self):
"""Test write_l writing multiple lines to file"""
fpl = io.StringIO()
write_l("Line 1", fpl=fpl, print_line=False)
write_l("Line 2", fpl=fpl, print_line=False)
write_l("Line 3", fpl=fpl, print_line=False)
assert fpl.getvalue() == "Line 1\nLine 2\nLine 3\n"
fpl.close()
def test_write_l_empty_string(self, capsys: CaptureFixture[str]):
"""Test write_l with empty string"""
fpl = io.StringIO()
write_l("", fpl=fpl, print_line=True)
captured = capsys.readouterr()
assert captured.out == "\n"
assert fpl.getvalue() == "\n"
fpl.close()
def test_write_l_special_characters(self):
"""Test write_l with special characters"""
fpl = io.StringIO()
special_line = "Special: \t\n\r\\ 特殊文字 €"
write_l(special_line, fpl=fpl, print_line=False)
assert special_line + "\n" in fpl.getvalue()
fpl.close()
def test_write_l_long_string(self):
"""Test write_l with long string"""
fpl = io.StringIO()
long_line = "A" * 1000
write_l(long_line, fpl=fpl, print_line=False)
assert fpl.getvalue() == long_line + "\n"
fpl.close()
def test_write_l_unicode_content(self):
"""Test write_l with unicode content"""
fpl = io.StringIO()
unicode_line = "Hello 世界 🌍 Привет"
write_l(unicode_line, fpl=fpl, print_line=False)
assert fpl.getvalue() == unicode_line + "\n"
fpl.close()
def test_write_l_default_parameters(self, capsys: CaptureFixture[str]):
"""Test write_l with default parameters"""
write_l("Test")
captured = capsys.readouterr()
# Default print_line is False
assert captured.out == ""
def test_write_l_with_newline_in_string(self):
"""Test write_l with newline characters in the string"""
fpl = io.StringIO()
write_l("Line with\nnewline", fpl=fpl, print_line=False)
assert fpl.getvalue() == "Line with\nnewline\n"
fpl.close()
class TestPrHeader:
"""Test cases for pr_header function"""
def test_pr_header_default(self, capsys: CaptureFixture[str]):
"""Test pr_header with default parameters"""
pr_header("TEST")
captured = capsys.readouterr()
assert "#" in captured.out
assert "TEST" in captured.out
def test_pr_header_custom_marker(self, capsys: CaptureFixture[str]):
"""Test pr_header with custom marker string"""
pr_header("TEST", marker_string="*")
captured = capsys.readouterr()
assert "*" in captured.out
assert "TEST" in captured.out
assert "#" not in captured.out
def test_pr_header_custom_width(self, capsys: CaptureFixture[str]):
"""Test pr_header with custom width"""
pr_header("TEST", width=50)
captured = capsys.readouterr()
# Check that output is formatted
assert "TEST" in captured.out
def test_pr_header_short_tag(self, capsys: CaptureFixture[str]):
"""Test pr_header with short tag"""
pr_header("X")
captured = capsys.readouterr()
assert "X" in captured.out
assert "#" in captured.out
def test_pr_header_long_tag(self, capsys: CaptureFixture[str]):
"""Test pr_header with long tag"""
pr_header("This is a very long header tag")
captured = capsys.readouterr()
assert "This is a very long header tag" in captured.out
def test_pr_header_empty_tag(self, capsys: CaptureFixture[str]):
"""Test pr_header with empty tag"""
pr_header("")
captured = capsys.readouterr()
assert "#" in captured.out
def test_pr_header_special_characters(self, capsys: CaptureFixture[str]):
"""Test pr_header with special characters in tag"""
pr_header("TEST: 123! @#$")
captured = capsys.readouterr()
assert "TEST: 123! @#$" in captured.out
def test_pr_header_unicode(self, capsys: CaptureFixture[str]):
"""Test pr_header with unicode characters"""
pr_header("テスト 🎉")
captured = capsys.readouterr()
assert "テスト 🎉" in captured.out
def test_pr_header_various_markers(self, capsys: CaptureFixture[str]):
"""Test pr_header with various marker strings"""
markers = ["*", "=", "-", "+", "~", "@"]
for marker in markers:
pr_header("TEST", marker_string=marker)
captured = capsys.readouterr()
assert marker in captured.out
assert "TEST" in captured.out
def test_pr_header_zero_width(self, capsys: CaptureFixture[str]):
"""Test pr_header with width of 0"""
pr_header("TEST", width=0)
captured = capsys.readouterr()
assert "TEST" in captured.out
def test_pr_header_large_width(self, capsys: CaptureFixture[str]):
"""Test pr_header with large width"""
pr_header("TEST", width=100)
captured = capsys.readouterr()
assert "TEST" in captured.out
assert "#" in captured.out
def test_pr_header_format(self, capsys: CaptureFixture[str]):
"""Test pr_header output format"""
pr_header("CENTER", marker_string="#", width=20)
captured = capsys.readouterr()
# Should have spaces around centered text
assert " CENTER " in captured.out or "CENTER" in captured.out
class TestPrTitle:
"""Test cases for pr_title function"""
def test_pr_title_default(self, capsys: CaptureFixture[str]):
"""Test pr_title with default parameters"""
pr_title("Test Title")
captured = capsys.readouterr()
assert "Test Title" in captured.out
assert "|" in captured.out
assert "." in captured.out
assert ":" in captured.out
def test_pr_title_custom_prefix(self, capsys: CaptureFixture[str]):
"""Test pr_title with custom prefix string"""
pr_title("Test", prefix_string=">")
captured = capsys.readouterr()
assert ">" in captured.out
assert "Test" in captured.out
assert "|" not in captured.out
def test_pr_title_custom_space_filler(self, capsys: CaptureFixture[str]):
"""Test pr_title with custom space filler"""
pr_title("Test", space_filler="-")
captured = capsys.readouterr()
assert "Test" in captured.out
assert "-" in captured.out
assert "." not in captured.out
def test_pr_title_custom_width(self, capsys: CaptureFixture[str]):
"""Test pr_title with custom width"""
pr_title("Test", width=50)
captured = capsys.readouterr()
assert "Test" in captured.out
def test_pr_title_short_tag(self, capsys: CaptureFixture[str]):
"""Test pr_title with short tag"""
pr_title("X")
captured = capsys.readouterr()
assert "X" in captured.out
assert "." in captured.out
def test_pr_title_long_tag(self, capsys: CaptureFixture[str]):
"""Test pr_title with long tag"""
pr_title("This is a very long title tag")
captured = capsys.readouterr()
assert "This is a very long title tag" in captured.out
def test_pr_title_empty_tag(self, capsys: CaptureFixture[str]):
"""Test pr_title with empty tag"""
pr_title("")
captured = capsys.readouterr()
assert "|" in captured.out
assert ":" in captured.out
def test_pr_title_special_characters(self, capsys: CaptureFixture[str]):
"""Test pr_title with special characters"""
pr_title("Task #123!")
captured = capsys.readouterr()
assert "Task #123!" in captured.out
def test_pr_title_unicode(self, capsys: CaptureFixture[str]):
"""Test pr_title with unicode characters"""
pr_title("タイトル 📝")
captured = capsys.readouterr()
assert "タイトル 📝" in captured.out
def test_pr_title_various_fillers(self, capsys: CaptureFixture[str]):
"""Test pr_title with various space fillers"""
fillers = [".", "-", "_", "*", " ", "~"]
for filler in fillers:
pr_title("Test", space_filler=filler)
captured = capsys.readouterr()
assert "Test" in captured.out
def test_pr_title_zero_width(self, capsys: CaptureFixture[str]):
"""Test pr_title with width of 0"""
pr_title("Test", width=0)
captured = capsys.readouterr()
assert "Test" in captured.out
def test_pr_title_large_width(self, capsys: CaptureFixture[str]):
"""Test pr_title with large width"""
pr_title("Test", width=100)
captured = capsys.readouterr()
assert "Test" in captured.out
def test_pr_title_format_left_align(self, capsys: CaptureFixture[str]):
"""Test pr_title output format (should be left-aligned with filler)"""
pr_title("Start", space_filler=".", width=10)
captured = capsys.readouterr()
# Should have the tag followed by dots
assert "Start" in captured.out
assert ":" in captured.out
class TestPrOpen:
"""Test cases for pr_open function"""
def test_pr_open_default(self, capsys: CaptureFixture[str]):
"""Test pr_open with default parameters"""
pr_open("Processing")
captured = capsys.readouterr()
assert "Processing" in captured.out
assert "|" in captured.out
assert "." in captured.out
assert "[" in captured.out
# Should not have newline at the end
assert not captured.out.endswith("\n")
def test_pr_open_custom_prefix(self, capsys: CaptureFixture[str]):
"""Test pr_open with custom prefix string"""
pr_open("Task", prefix_string=">")
captured = capsys.readouterr()
assert ">" in captured.out
assert "Task" in captured.out
assert "|" not in captured.out
def test_pr_open_custom_space_filler(self, capsys: CaptureFixture[str]):
"""Test pr_open with custom space filler"""
pr_open("Task", space_filler="-")
captured = capsys.readouterr()
assert "Task" in captured.out
assert "-" in captured.out
assert "." not in captured.out
def test_pr_open_custom_width(self, capsys: CaptureFixture[str]):
"""Test pr_open with custom width"""
pr_open("Task", width=50)
captured = capsys.readouterr()
assert "Task" in captured.out
assert "[" in captured.out
def test_pr_open_short_tag(self, capsys: CaptureFixture[str]):
"""Test pr_open with short tag"""
pr_open("X")
captured = capsys.readouterr()
assert "X" in captured.out
assert "[" in captured.out
def test_pr_open_long_tag(self, capsys: CaptureFixture[str]):
"""Test pr_open with long tag"""
pr_open("This is a very long task tag")
captured = capsys.readouterr()
assert "This is a very long task tag" in captured.out
def test_pr_open_empty_tag(self, capsys: CaptureFixture[str]):
"""Test pr_open with empty tag"""
pr_open("")
captured = capsys.readouterr()
assert "[" in captured.out
assert "|" in captured.out
def test_pr_open_no_newline(self, capsys: CaptureFixture[str]):
"""Test pr_open doesn't end with newline"""
pr_open("Test")
captured = capsys.readouterr()
# Output should not end with newline (uses end="")
assert not captured.out.endswith("\n")
def test_pr_open_special_characters(self, capsys: CaptureFixture[str]):
"""Test pr_open with special characters"""
pr_open("Loading: 50%")
captured = capsys.readouterr()
assert "Loading: 50%" in captured.out
def test_pr_open_unicode(self, capsys: CaptureFixture[str]):
"""Test pr_open with unicode characters"""
pr_open("処理中 ⏳")
captured = capsys.readouterr()
assert "処理中 ⏳" in captured.out
def test_pr_open_format(self, capsys: CaptureFixture[str]):
"""Test pr_open output format"""
pr_open("Task", prefix_string="|", space_filler=".", width=20)
captured = capsys.readouterr()
assert "|" in captured.out
assert "Task" in captured.out
assert "[" in captured.out
class TestPrClose:
"""Test cases for pr_close function"""
def test_pr_close_default(self, capsys: CaptureFixture[str]):
"""Test pr_close with default (empty) tag"""
pr_close()
captured = capsys.readouterr()
assert captured.out == "]\n"
def test_pr_close_with_tag(self, capsys: CaptureFixture[str]):
"""Test pr_close with custom tag"""
pr_close("DONE")
captured = capsys.readouterr()
assert "DONE" in captured.out
assert "]" in captured.out
assert captured.out.endswith("\n")
def test_pr_close_with_space(self, capsys: CaptureFixture[str]):
"""Test pr_close with space in tag"""
pr_close(" OK ")
captured = capsys.readouterr()
assert " OK " in captured.out
assert "]" in captured.out
def test_pr_close_empty_string(self, capsys: CaptureFixture[str]):
"""Test pr_close with empty string (same as default)"""
pr_close("")
captured = capsys.readouterr()
assert captured.out == "]\n"
def test_pr_close_special_characters(self, capsys: CaptureFixture[str]):
"""Test pr_close with special characters"""
pr_close("")
captured = capsys.readouterr()
assert "" in captured.out
assert "]" in captured.out
def test_pr_close_unicode(self, capsys: CaptureFixture[str]):
"""Test pr_close with unicode characters"""
pr_close("完了")
captured = capsys.readouterr()
assert "完了" in captured.out
assert "]" in captured.out
def test_pr_close_newline(self, capsys: CaptureFixture[str]):
"""Test pr_close ends with newline"""
pr_close("OK")
captured = capsys.readouterr()
assert captured.out.endswith("\n")
def test_pr_close_various_tags(self, capsys: CaptureFixture[str]):
"""Test pr_close with various tags"""
tags = ["OK", "DONE", "", "", "SKIP", "PASS", "FAIL"]
for tag in tags:
pr_close(tag)
captured = capsys.readouterr()
assert tag in captured.out
assert "]" in captured.out
class TestPrAct:
"""Test cases for pr_act function"""
def test_pr_act_default(self, capsys: CaptureFixture[str]):
"""Test pr_act with default dot"""
pr_act()
captured = capsys.readouterr()
assert captured.out == "."
assert not captured.out.endswith("\n")
def test_pr_act_custom_character(self, capsys: CaptureFixture[str]):
"""Test pr_act with custom character"""
pr_act("#")
captured = capsys.readouterr()
assert captured.out == "#"
def test_pr_act_multiple_calls(self, capsys: CaptureFixture[str]):
"""Test pr_act with multiple calls"""
pr_act(".")
pr_act(".")
pr_act(".")
captured = capsys.readouterr()
assert captured.out == "..."
def test_pr_act_various_characters(self, capsys: CaptureFixture[str]):
"""Test pr_act with various characters"""
characters = [".", "#", "*", "+", "-", "=", ">", "~"]
for char in characters:
pr_act(char)
captured = capsys.readouterr()
assert "".join(characters) in captured.out
def test_pr_act_empty_string(self, capsys: CaptureFixture[str]):
"""Test pr_act with empty string"""
pr_act("")
captured = capsys.readouterr()
assert captured.out == ""
def test_pr_act_special_character(self, capsys: CaptureFixture[str]):
"""Test pr_act with special characters"""
pr_act("")
captured = capsys.readouterr()
assert captured.out == ""
def test_pr_act_unicode(self, capsys: CaptureFixture[str]):
"""Test pr_act with unicode character"""
pr_act("")
captured = capsys.readouterr()
assert captured.out == ""
def test_pr_act_no_newline(self, capsys: CaptureFixture[str]):
"""Test pr_act doesn't add newline"""
pr_act("x")
captured = capsys.readouterr()
assert not captured.out.endswith("\n")
def test_pr_act_multiple_characters(self, capsys: CaptureFixture[str]):
"""Test pr_act with multiple characters in string"""
pr_act("...")
captured = capsys.readouterr()
assert captured.out == "..."
def test_pr_act_whitespace(self, capsys: CaptureFixture[str]):
"""Test pr_act with whitespace"""
pr_act(" ")
captured = capsys.readouterr()
assert captured.out == " "
class TestProgressCombinations:
"""Test combinations of progress printer functions"""
def test_complete_progress_flow(self, capsys: CaptureFixture[str]):
"""Test complete progress output flow"""
pr_header("PROCESS")
pr_title("Task 1")
pr_open("Subtask")
pr_act(".")
pr_act(".")
pr_act(".")
pr_close(" OK")
captured = capsys.readouterr()
assert "PROCESS" in captured.out
assert "Task 1" in captured.out
assert "Subtask" in captured.out
assert "..." in captured.out
assert " OK]" in captured.out
def test_multiple_tasks_progress(self, capsys: CaptureFixture[str]):
"""Test multiple tasks with progress"""
pr_header("BATCH PROCESS")
for i in range(3):
pr_open(f"Task {i + 1}")
for _ in range(5):
pr_act(".")
pr_close(" DONE")
captured = capsys.readouterr()
assert "BATCH PROCESS" in captured.out
assert "Task 1" in captured.out
assert "Task 2" in captured.out
assert "Task 3" in captured.out
assert " DONE]" in captured.out
def test_nested_progress(self, capsys: CaptureFixture[str]):
"""Test nested progress indicators"""
pr_header("MAIN TASK", marker_string="=")
pr_title("Subtask A", prefix_string=">")
pr_open("Processing")
pr_act("#")
pr_act("#")
pr_close()
pr_title("Subtask B", prefix_string=">")
pr_open("Processing")
pr_act("*")
pr_act("*")
pr_close(" OK")
captured = capsys.readouterr()
assert "MAIN TASK" in captured.out
assert "Subtask A" in captured.out
assert "Subtask B" in captured.out
assert "##" in captured.out
assert "**" in captured.out
def test_progress_with_different_markers(self, capsys: CaptureFixture[str]):
"""Test progress with different marker styles"""
pr_header("Process", marker_string="*")
pr_title("Step 1", prefix_string=">>", space_filler="-")
pr_open("Work", prefix_string=">>", space_filler="-")
pr_act("+")
pr_close("")
captured = capsys.readouterr()
assert "*" in captured.out
assert ">>" in captured.out
assert "-" in captured.out
assert "+" in captured.out
assert "" in captured.out
def test_empty_progress_sequence(self, capsys: CaptureFixture[str]):
"""Test progress sequence with no actual progress"""
pr_open("Quick task")
pr_close(" SKIP")
captured = capsys.readouterr()
assert "Quick task" in captured.out
assert " SKIP]" in captured.out
class TestIntegration:
"""Integration tests combining multiple scenarios"""
def test_file_and_console_logging(self, capsys: CaptureFixture[str]):
"""Test logging to both file and console"""
fpl = io.StringIO()
write_l("Starting process", fpl=fpl, print_line=True)
write_l("Processing item 1", fpl=fpl, print_line=True)
write_l("Processing item 2", fpl=fpl, print_line=True)
write_l("Complete", fpl=fpl, print_line=True)
captured = capsys.readouterr()
file_content = fpl.getvalue()
# Check console output
assert "Starting process\n" in captured.out
assert "Processing item 1\n" in captured.out
assert "Processing item 2\n" in captured.out
assert "Complete\n" in captured.out
# Check file output
assert "Starting process\n" in file_content
assert "Processing item 1\n" in file_content
assert "Processing item 2\n" in file_content
assert "Complete\n" in file_content
fpl.close()
def test_progress_with_logging(self, capsys: CaptureFixture[str]):
"""Test combining progress output with file logging"""
fpl = io.StringIO()
write_l("=== Process Start ===", fpl=fpl, print_line=True)
pr_header("MAIN PROCESS")
write_l("Header shown", fpl=fpl, print_line=False)
pr_open("Task 1")
pr_act(".")
pr_act(".")
pr_close(" OK")
write_l("Task 1 completed", fpl=fpl, print_line=False)
write_l("=== Process End ===", fpl=fpl, print_line=True)
captured = capsys.readouterr()
file_content = fpl.getvalue()
assert "=== Process Start ===" in captured.out
assert "MAIN PROCESS" in captured.out
assert "Task 1" in captured.out
assert "=== Process End ===" in captured.out
assert "=== Process Start ===\n" in file_content
assert "Header shown\n" in file_content
assert "Task 1 completed\n" in file_content
assert "=== Process End ===\n" in file_content
fpl.close()
def test_complex_workflow(self, capsys: CaptureFixture[str]):
"""Test complex workflow with all functions"""
fpl = io.StringIO()
write_l("Log: Starting batch process", fpl=fpl, print_line=False)
pr_header("BATCH PROCESSOR", marker_string="=", width=40)
for i in range(2):
write_l(f"Log: Processing batch {i + 1}", fpl=fpl, print_line=False)
pr_title(f"Batch {i + 1}", prefix_string="|", space_filler=".")
pr_open(f"Item {i + 1}", prefix_string="|", space_filler=".")
for j in range(3):
pr_act("*")
write_l(f"Log: Progress {j + 1}/3", fpl=fpl, print_line=False)
pr_close("")
write_l(f"Log: Batch {i + 1} complete", fpl=fpl, print_line=False)
write_l("Log: All batches complete", fpl=fpl, print_line=False)
captured = capsys.readouterr()
file_content = fpl.getvalue()
# Check console has progress indicators
assert "BATCH PROCESSOR" in captured.out
assert "Batch 1" in captured.out
assert "Batch 2" in captured.out
assert "***" in captured.out
assert "" in captured.out
# Check file has all log entries
assert "Log: Starting batch process\n" in file_content
assert "Log: Processing batch 1\n" in file_content
assert "Log: Processing batch 2\n" in file_content
assert "Log: Progress 1/3\n" in file_content
assert "Log: Batch 1 complete\n" in file_content
assert "Log: All batches complete\n" in file_content
fpl.close()
class TestEdgeCases:
"""Test edge cases and boundary conditions"""
def test_write_l_none_file_handler(self, capsys: CaptureFixture[str]):
"""Test write_l explicitly with None file handler"""
write_l("Test", fpl=None, print_line=True)
captured = capsys.readouterr()
assert captured.out == "Test\n"
def test_pr_header_negative_width(self):
"""Test pr_header with negative width raises ValueError"""
with pytest.raises(ValueError):
pr_header("Test", width=-10)
def test_pr_title_negative_width(self):
"""Test pr_title with negative width raises ValueError"""
with pytest.raises(ValueError):
pr_title("Test", width=-10)
def test_pr_open_negative_width(self):
"""Test pr_open with negative width raises ValueError"""
with pytest.raises(ValueError):
pr_open("Test", width=-10)
def test_multiple_pr_act_no_close(self, capsys: CaptureFixture[str]):
"""Test multiple pr_act calls without pr_close"""
pr_act(".")
pr_act(".")
pr_act(".")
captured = capsys.readouterr()
assert captured.out == "..."
def test_pr_close_without_pr_open(self, capsys: CaptureFixture[str]):
"""Test pr_close without prior pr_open (should still work)"""
pr_close(" OK")
captured = capsys.readouterr()
assert " OK]" in captured.out
def test_very_long_strings(self):
"""Test with very long strings"""
fpl = io.StringIO()
long_str = "A" * 10000
write_l(long_str, fpl=fpl, print_line=False)
assert len(fpl.getvalue()) == 10001 # string + newline
fpl.close()
def test_pr_header_very_long_tag(self, capsys: CaptureFixture[str]):
"""Test pr_header with tag longer than width"""
pr_header("This is a very long tag that exceeds the width", width=10)
captured = capsys.readouterr()
assert "This is a very long tag that exceeds the width" in captured.out
def test_pr_title_very_long_tag(self, capsys: CaptureFixture[str]):
"""Test pr_title with tag longer than width"""
pr_title("This is a very long tag that exceeds the width", width=10)
captured = capsys.readouterr()
assert "This is a very long tag that exceeds the width" in captured.out
def test_write_l_closed_file(self):
"""Test write_l with closed file should raise error"""
fpl = io.StringIO()
fpl.close()
with pytest.raises(ValueError):
write_l("Test", fpl=fpl, print_line=False)
class TestParametrized:
"""Parametrized tests for comprehensive coverage"""
@pytest.mark.parametrize("print_line", [True, False])
def test_write_l_print_line_variations(self, print_line: bool, capsys: CaptureFixture[str]):
"""Test write_l with different print_line values"""
write_l("Test", print_line=print_line)
captured = capsys.readouterr()
if print_line:
assert captured.out == "Test\n"
else:
assert captured.out == ""
@pytest.mark.parametrize("marker", ["#", "*", "=", "-", "+", "~", "@", "^"])
def test_pr_header_various_markers_param(self, marker: str, capsys: CaptureFixture[str]):
"""Test pr_header with various markers"""
pr_header("TEST", marker_string=marker)
captured = capsys.readouterr()
assert marker in captured.out
assert "TEST" in captured.out
@pytest.mark.parametrize("width", [0, 5, 10, 20, 35, 50, 100])
def test_pr_header_various_widths(self, width: int, capsys: CaptureFixture[str]):
"""Test pr_header with various widths"""
pr_header("TEST", width=width)
captured = capsys.readouterr()
assert "TEST" in captured.out
@pytest.mark.parametrize("filler", [".", "-", "_", "*", " ", "~", "="])
def test_pr_title_various_fillers_param(self, filler: str, capsys: CaptureFixture[str]):
"""Test pr_title with various space fillers"""
pr_title("Test", space_filler=filler)
captured = capsys.readouterr()
assert "Test" in captured.out
@pytest.mark.parametrize("prefix", ["|", ">", ">>", "*", "-", "+"])
def test_pr_title_various_prefixes(self, prefix: str, capsys: CaptureFixture[str]):
"""Test pr_title with various prefix strings"""
pr_title("Test", prefix_string=prefix)
captured = capsys.readouterr()
assert prefix in captured.out
assert "Test" in captured.out
@pytest.mark.parametrize("act_char", [".", "#", "*", "+", "-", "=", ">", "~", "", ""])
def test_pr_act_various_characters_param(self, act_char: str, capsys: CaptureFixture[str]):
"""Test pr_act with various characters"""
pr_act(act_char)
captured = capsys.readouterr()
assert captured.out == act_char
@pytest.mark.parametrize("close_tag", ["", " OK", " DONE", "", "", " SKIP", " PASS"])
def test_pr_close_various_tags_param(self, close_tag: str, capsys: CaptureFixture[str]):
"""Test pr_close with various tags"""
pr_close(close_tag)
captured = capsys.readouterr()
assert f"{close_tag}]" in captured.out
@pytest.mark.parametrize("content", [
"Simple text",
"Text with 特殊文字",
"Text with emoji 🎉",
"Text\twith\ttabs",
"Multiple\n\nNewlines",
"",
"A" * 100,
])
def test_write_l_various_content(self, content: str, capsys: CaptureFixture[str]):
"""Test write_l with various content types"""
fpl = io.StringIO()
write_l(content, fpl=fpl, print_line=True)
captured = capsys.readouterr()
assert content in captured.out
assert content + "\n" in fpl.getvalue()
fpl.close()
class TestRealWorldScenarios:
"""Test real-world usage scenarios"""
def test_batch_processing_output(self, capsys: CaptureFixture[str]):
"""Test typical batch processing output"""
pr_header("BATCH PROCESSOR", marker_string="=", width=50)
items = ["file1.txt", "file2.txt", "file3.txt"]
for item in items:
pr_open(f"Processing {item}")
for _ in range(10):
pr_act(".")
pr_close("")
captured = capsys.readouterr()
assert "BATCH PROCESSOR" in captured.out
for item in items:
assert item in captured.out
assert "" in captured.out
def test_logging_workflow(self, capsys: CaptureFixture[str]):
"""Test typical logging workflow"""
log_file = io.StringIO()
# Simulate a workflow with logging
write_l("[INFO] Starting process", fpl=log_file, print_line=True)
write_l("[INFO] Initializing components", fpl=log_file, print_line=True)
write_l("[DEBUG] Component A loaded", fpl=log_file, print_line=False)
write_l("[DEBUG] Component B loaded", fpl=log_file, print_line=False)
write_l("[INFO] Processing data", fpl=log_file, print_line=True)
write_l("[INFO] Process complete", fpl=log_file, print_line=True)
captured = capsys.readouterr()
log_content = log_file.getvalue()
# Console should only have INFO messages
assert "[INFO] Starting process" in captured.out
assert "[DEBUG] Component A loaded" not in captured.out
# Log file should have all messages
assert "[INFO] Starting process\n" in log_content
assert "[DEBUG] Component A loaded\n" in log_content
assert "[DEBUG] Component B loaded\n" in log_content
log_file.close()
def test_progress_indicator_for_long_task(self, capsys: CaptureFixture[str]):
"""Test progress indicator for a long-running task"""
pr_header("DATA PROCESSING")
pr_open("Loading data", width=50)
# Simulate progress
for i in range(20):
if i % 5 == 0:
pr_act(str(i // 5))
else:
pr_act(".")
pr_close(" COMPLETE")
captured = capsys.readouterr()
assert "DATA PROCESSING" in captured.out
assert "Loading data" in captured.out
assert "COMPLETE" in captured.out
def test_multi_stage_process(self, capsys: CaptureFixture[str]):
"""Test multi-stage process with titles and progress"""
pr_header("DEPLOYMENT PIPELINE", marker_string="=")
stages = ["Build", "Test", "Deploy"]
for stage in stages:
pr_title(stage)
pr_open(f"Running {stage.lower()}")
pr_act("#")
pr_act("#")
pr_act("#")
pr_close(" OK")
captured = capsys.readouterr()
assert "DEPLOYMENT PIPELINE" in captured.out
for stage in stages:
assert stage in captured.out
assert "###" in captured.out
def test_error_reporting_with_logging(self, capsys: CaptureFixture[str]):
"""Test error reporting workflow"""
error_log = io.StringIO()
pr_header("VALIDATION", marker_string="!")
pr_open("Checking files")
write_l("[ERROR] File not found: data.csv", fpl=error_log, print_line=False)
pr_act("")
write_l("[ERROR] Permission denied: output.txt", fpl=error_log, print_line=False)
pr_act("")
pr_close(" FAILED")
captured = capsys.readouterr()
log_content = error_log.getvalue()
assert "VALIDATION" in captured.out
assert "Checking files" in captured.out
assert "✗✗" in captured.out
assert "FAILED" in captured.out
assert "[ERROR] File not found: data.csv\n" in log_content
assert "[ERROR] Permission denied: output.txt\n" in log_content
error_log.close()
def test_detailed_reporting(self, capsys: CaptureFixture[str]):
"""Test detailed reporting with mixed output"""
report_file = io.StringIO()
pr_header("SYSTEM REPORT", marker_string="#", width=60)
write_l("=== System Report Generated ===", fpl=report_file, print_line=False)
pr_title("Database Status", prefix_string=">>")
write_l("Database: Connected", fpl=report_file, print_line=False)
write_l("Tables: 15", fpl=report_file, print_line=False)
write_l("Records: 1,234,567", fpl=report_file, print_line=False)
pr_title("API Status", prefix_string=">>")
write_l("API: Online", fpl=report_file, print_line=False)
write_l("Requests/min: 1,500", fpl=report_file, print_line=False)
write_l("=== Report Complete ===", fpl=report_file, print_line=False)
captured = capsys.readouterr()
report_content = report_file.getvalue()
assert "SYSTEM REPORT" in captured.out
assert "Database Status" in captured.out
assert "API Status" in captured.out
assert "=== System Report Generated ===\n" in report_content
assert "Database: Connected\n" in report_content
assert "API: Online\n" in report_content
assert "=== Report Complete ===\n" in report_content
report_file.close()
# __END__

View File

@@ -1,3 +0,0 @@
"""
Unit tests for encryption_handling module
"""

View File

@@ -1,665 +0,0 @@
"""
PyTest: encryption_handling/symmetric_encryption
"""
# pylint: disable=redefined-outer-name
# ^ Disabled because pytest fixtures intentionally redefine names
import os
import json
import base64
import hashlib
import pytest
from corelibs.encryption_handling.symmetric_encryption import (
SymmetricEncryption
)
class TestSymmetricEncryptionInitialization:
"""Tests for SymmetricEncryption initialization"""
def test_valid_password_initialization(self):
"""Test initialization with a valid password"""
encryptor = SymmetricEncryption("test_password")
assert encryptor.password == "test_password"
assert encryptor.password_hash == hashlib.sha256("test_password".encode('utf-8')).hexdigest()
def test_empty_password_raises_error(self):
"""Test that empty password raises ValueError"""
with pytest.raises(ValueError, match="A password must be set"):
SymmetricEncryption("")
def test_password_hash_is_consistent(self):
"""Test that password hash is consistently generated"""
encryptor1 = SymmetricEncryption("test_password")
encryptor2 = SymmetricEncryption("test_password")
assert encryptor1.password_hash == encryptor2.password_hash
def test_different_passwords_different_hashes(self):
"""Test that different passwords produce different hashes"""
encryptor1 = SymmetricEncryption("password1")
encryptor2 = SymmetricEncryption("password2")
assert encryptor1.password_hash != encryptor2.password_hash
class TestEncryptWithMetadataReturnDict:
"""Tests for encrypt_with_metadata_return_dict method"""
def test_encrypt_string_returns_package_data(self):
"""Test encrypting a string returns PackageData dict"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
assert isinstance(result, dict)
assert 'encrypted_data' in result
assert 'salt' in result
assert 'key_hash' in result
def test_encrypt_bytes_returns_package_data(self):
"""Test encrypting bytes returns PackageData dict"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict(b"test data")
assert isinstance(result, dict)
assert 'encrypted_data' in result
assert 'salt' in result
assert 'key_hash' in result
def test_encrypted_data_is_base64_encoded(self):
"""Test that encrypted_data is base64 encoded"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should not raise exception when decoding
base64.urlsafe_b64decode(result['encrypted_data'])
def test_salt_is_base64_encoded(self):
"""Test that salt is base64 encoded"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should not raise exception when decoding
salt = base64.urlsafe_b64decode(result['salt'])
# Salt should be 16 bytes
assert len(salt) == 16
def test_key_hash_is_valid_hex(self):
"""Test that key_hash is a valid hex string"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_dict("test data")
# Should be 64 characters (SHA256 hex)
assert len(result['key_hash']) == 64
# Should only contain hex characters
int(result['key_hash'], 16)
def test_different_salts_for_each_encryption(self):
"""Test that each encryption uses a different salt"""
encryptor = SymmetricEncryption("test_password")
result1 = encryptor.encrypt_with_metadata_return_dict("test data")
result2 = encryptor.encrypt_with_metadata_return_dict("test data")
assert result1['salt'] != result2['salt']
assert result1['encrypted_data'] != result2['encrypted_data']
class TestEncryptWithMetadataReturnStr:
"""Tests for encrypt_with_metadata_return_str method"""
def test_returns_json_string(self):
"""Test that method returns a valid JSON string"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_str("test data")
assert isinstance(result, str)
# Should be valid JSON
parsed = json.loads(result)
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
def test_json_string_parseable(self):
"""Test that returned JSON string can be parsed back"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_str("test data")
parsed = json.loads(result)
assert isinstance(parsed, dict)
class TestEncryptWithMetadataReturnBytes:
"""Tests for encrypt_with_metadata_return_bytes method"""
def test_returns_bytes(self):
"""Test that method returns bytes"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_bytes("test data")
assert isinstance(result, bytes)
def test_bytes_contains_valid_json(self):
"""Test that returned bytes contain valid JSON"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata_return_bytes("test data")
# Should be valid JSON when decoded
parsed = json.loads(result.decode('utf-8'))
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
class TestEncryptWithMetadata:
"""Tests for encrypt_with_metadata method with different return types"""
def test_return_as_str(self):
"""Test encrypt_with_metadata with return_as='str'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='str')
assert isinstance(result, str)
json.loads(result) # Should be valid JSON
def test_return_as_json(self):
"""Test encrypt_with_metadata with return_as='json'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='json')
assert isinstance(result, str)
json.loads(result) # Should be valid JSON
def test_return_as_bytes(self):
"""Test encrypt_with_metadata with return_as='bytes'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='bytes')
assert isinstance(result, bytes)
def test_return_as_dict(self):
"""Test encrypt_with_metadata with return_as='dict'"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='dict')
assert isinstance(result, dict)
assert 'encrypted_data' in result
def test_default_return_type(self):
"""Test encrypt_with_metadata default return type"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data")
# Default should be 'str'
assert isinstance(result, str)
def test_invalid_return_type_defaults_to_str(self):
"""Test that invalid return_as defaults to str"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as='invalid')
assert isinstance(result, str)
class TestDecryptWithMetadata:
"""Tests for decrypt_with_metadata method"""
def test_decrypt_string_package(self):
"""Test decrypting a string JSON package"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_bytes_package(self):
"""Test decrypting a bytes JSON package"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_bytes("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_dict_package(self):
"""Test decrypting a dict PackageData"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_dict("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_decrypt_with_different_password_fails(self):
"""Test that decrypting with wrong password fails"""
encryptor = SymmetricEncryption("password1")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decryptor = SymmetricEncryption("password2")
with pytest.raises(ValueError, match="Key hash is not matching"):
decryptor.decrypt_with_metadata(encrypted)
def test_decrypt_with_explicit_password(self):
"""Test decrypting with explicitly provided password"""
encryptor = SymmetricEncryption("password1")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Decrypt with different password parameter
decryptor = SymmetricEncryption("password1")
decrypted = decryptor.decrypt_with_metadata(encrypted, password="password1")
assert decrypted == "test data"
def test_decrypt_invalid_json_raises_error(self):
"""Test that invalid JSON raises ValueError"""
encryptor = SymmetricEncryption("test_password")
with pytest.raises(ValueError, match="Invalid encrypted package format"):
encryptor.decrypt_with_metadata("not valid json")
def test_decrypt_missing_fields_raises_error(self):
"""Test that missing required fields raises ValueError"""
encryptor = SymmetricEncryption("test_password")
invalid_package = json.dumps({"encrypted_data": "test"})
with pytest.raises(ValueError, match="Invalid encrypted package format"):
encryptor.decrypt_with_metadata(invalid_package)
def test_decrypt_unicode_data(self):
"""Test encrypting and decrypting unicode data"""
encryptor = SymmetricEncryption("test_password")
unicode_data = "Hello 世界 🌍"
encrypted = encryptor.encrypt_with_metadata_return_str(unicode_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == unicode_data
def test_decrypt_empty_string(self):
"""Test encrypting and decrypting empty string"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == ""
def test_decrypt_long_data(self):
"""Test encrypting and decrypting long data"""
encryptor = SymmetricEncryption("test_password")
long_data = "A" * 10000
encrypted = encryptor.encrypt_with_metadata_return_str(long_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == long_data
class TestStaticMethods:
"""Tests for static methods encrypt_data and decrypt_data"""
def test_encrypt_data_static_method(self):
"""Test static encrypt_data method"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
assert isinstance(encrypted, str)
# Should be valid JSON
parsed = json.loads(encrypted)
assert 'encrypted_data' in parsed
assert 'salt' in parsed
assert 'key_hash' in parsed
def test_decrypt_data_static_method(self):
"""Test static decrypt_data method"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == "test data"
def test_static_methods_roundtrip(self):
"""Test complete roundtrip using static methods"""
original = "test data with special chars: !@#$%^&*()"
encrypted = SymmetricEncryption.encrypt_data(original, "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == original
def test_static_decrypt_with_bytes(self):
"""Test static decrypt_data with bytes input"""
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
encrypted_bytes = encrypted.encode('utf-8')
decrypted = SymmetricEncryption.decrypt_data(encrypted_bytes, "test_password")
assert decrypted == "test data"
def test_static_decrypt_with_dict(self):
"""Test static decrypt_data with PackageData dict"""
encryptor = SymmetricEncryption("test_password")
encrypted_dict = encryptor.encrypt_with_metadata_return_dict("test data")
decrypted = SymmetricEncryption.decrypt_data(encrypted_dict, "test_password")
assert decrypted == "test data"
def test_static_encrypt_bytes_data(self):
"""Test static encrypt_data with bytes input"""
encrypted = SymmetricEncryption.encrypt_data(b"test data", "test_password")
decrypted = SymmetricEncryption.decrypt_data(encrypted, "test_password")
assert decrypted == "test data"
class TestEncryptionSecurity:
"""Security-related tests for encryption"""
def test_same_data_different_encryption(self):
"""Test that same data produces different encrypted outputs due to salt"""
encryptor = SymmetricEncryption("test_password")
encrypted1 = encryptor.encrypt_with_metadata_return_str("test data")
encrypted2 = encryptor.encrypt_with_metadata_return_str("test data")
assert encrypted1 != encrypted2
def test_password_not_recoverable_from_hash(self):
"""Test that password hash is one-way"""
encryptor = SymmetricEncryption("secret_password")
# The password_hash should be SHA256 hex (64 chars)
assert len(encryptor.password_hash) == 64
# Password should not be easily derivable from hash
assert "secret_password" not in encryptor.password_hash
def test_encrypted_data_not_plaintext(self):
"""Test that encrypted data doesn't contain plaintext"""
encryptor = SymmetricEncryption("test_password")
plaintext = "very_secret_data_12345"
encrypted = encryptor.encrypt_with_metadata_return_str(plaintext)
# Plaintext should not appear in encrypted output
assert plaintext not in encrypted
def test_modified_encrypted_data_fails_decryption(self):
"""Test that modified encrypted data fails to decrypt"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Modify the encrypted data
encrypted_dict = json.loads(encrypted)
encrypted_dict['encrypted_data'] = encrypted_dict['encrypted_data'][:-5] + "AAAAA"
modified_encrypted = json.dumps(encrypted_dict)
# Should fail to decrypt
with pytest.raises(Exception): # Fernet will raise an exception
encryptor.decrypt_with_metadata(modified_encrypted)
def test_modified_salt_fails_decryption(self):
"""Test that modified salt fails to decrypt"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
# Modify the salt
encrypted_dict = json.loads(encrypted)
original_salt = base64.urlsafe_b64decode(encrypted_dict['salt'])
modified_salt = bytes([b ^ 1 for b in original_salt])
encrypted_dict['salt'] = base64.urlsafe_b64encode(modified_salt).decode('utf-8')
modified_encrypted = json.dumps(encrypted_dict)
# Should fail to decrypt due to key hash mismatch
with pytest.raises(ValueError, match="Key hash is not matching"):
encryptor.decrypt_with_metadata(modified_encrypted)
class TestEdgeCases:
"""Edge case tests"""
def test_very_long_password(self):
"""Test with very long password"""
long_password = "a" * 1000
encryptor = SymmetricEncryption(long_password)
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_special_characters_in_data(self):
"""Test encryption of data with special characters"""
special_data = "!@#$%^&*()_+-=[]{}|;':\",./<>?\n\t\r"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(special_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == special_data
def test_binary_data_utf8_bytes(self):
"""Test encryption of UTF-8 encoded bytes"""
# Test with UTF-8 encoded bytes
utf8_bytes = "Hello 世界 🌍".encode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(utf8_bytes)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "Hello 世界 🌍"
def test_binary_data_with_base64_encoding(self):
"""Test encryption of arbitrary binary data using base64 encoding"""
# For arbitrary binary data, encode to base64 first
binary_data = bytes(range(256))
base64_encoded = base64.b64encode(binary_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Decode back to binary
decoded_binary = base64.b64decode(decrypted)
assert decoded_binary == binary_data
def test_binary_data_image_simulation(self):
"""Test encryption of simulated binary image data"""
# Simulate image binary data (random bytes)
image_data = os.urandom(1024) # 1KB of random binary data
base64_encoded = base64.b64encode(image_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Verify round-trip
decoded_data = base64.b64decode(decrypted)
assert decoded_data == image_data
def test_binary_data_with_null_bytes(self):
"""Test encryption of data containing null bytes"""
# Create data with null bytes
data_with_nulls = "text\x00with\x00nulls\x00bytes"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(data_with_nulls)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == data_with_nulls
def test_binary_data_bytes_input(self):
"""Test encryption with bytes input directly"""
# UTF-8 compatible bytes
byte_data = b"Binary data test"
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(byte_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "Binary data test"
def test_binary_data_large_file_simulation(self):
"""Test encryption of large binary data (simulated file)"""
# Simulate a larger binary file (10KB)
large_data = os.urandom(10240)
base64_encoded = base64.b64encode(large_data).decode('utf-8')
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(base64_encoded)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Verify integrity
decoded_data = base64.b64decode(decrypted)
assert len(decoded_data) == 10240
assert decoded_data == large_data
def test_binary_data_json_with_base64(self):
"""Test encryption of JSON containing base64 encoded binary data"""
binary_data = os.urandom(256)
json_data = json.dumps({
"filename": "test.bin",
"data": base64.b64encode(binary_data).decode('utf-8'),
"size": len(binary_data)
})
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(json_data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
# Parse and verify
parsed = json.loads(decrypted)
assert parsed["filename"] == "test.bin"
assert parsed["size"] == 256
decoded_binary = base64.b64decode(parsed["data"])
assert decoded_binary == binary_data
def test_numeric_password(self):
"""Test with numeric string password"""
encryptor = SymmetricEncryption("12345")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_unicode_password(self):
"""Test with unicode password"""
encryptor = SymmetricEncryption("パスワード123")
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
class TestIntegration:
"""Integration tests"""
def test_multiple_encrypt_decrypt_cycles(self):
"""Test multiple encryption/decryption cycles"""
encryptor = SymmetricEncryption("test_password")
original = "test data"
# Encrypt and decrypt multiple times
for _ in range(5):
encrypted = encryptor.encrypt_with_metadata_return_str(original)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == original
def test_different_return_types_interoperability(self):
"""Test that different return types can be decrypted"""
encryptor = SymmetricEncryption("test_password")
original = "test data"
# Encrypt with different return types
encrypted_str = encryptor.encrypt_with_metadata_return_str(original)
encrypted_bytes = encryptor.encrypt_with_metadata_return_bytes(original)
encrypted_dict = encryptor.encrypt_with_metadata_return_dict(original)
# All should decrypt to the same value
assert encryptor.decrypt_with_metadata(encrypted_str) == original
assert encryptor.decrypt_with_metadata(encrypted_bytes) == original
assert encryptor.decrypt_with_metadata(encrypted_dict) == original
def test_cross_instance_encryption_decryption(self):
"""Test that different instances with same password can decrypt"""
encryptor1 = SymmetricEncryption("test_password")
encryptor2 = SymmetricEncryption("test_password")
encrypted = encryptor1.encrypt_with_metadata_return_str("test data")
decrypted = encryptor2.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
def test_static_and_instance_methods_compatible(self):
"""Test that static and instance methods are compatible"""
# Encrypt with static method
encrypted = SymmetricEncryption.encrypt_data("test data", "test_password")
# Decrypt with instance method
decryptor = SymmetricEncryption("test_password")
decrypted = decryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
# And vice versa
encryptor = SymmetricEncryption("test_password")
encrypted2 = encryptor.encrypt_with_metadata_return_str("test data 2")
decrypted2 = SymmetricEncryption.decrypt_data(encrypted2, "test_password")
assert decrypted2 == "test data 2"
# Parametrized tests
@pytest.mark.parametrize("data", [
"simple text",
"text with spaces and punctuation!",
"123456789",
"unicode: こんにちは",
"emoji: 🔐🔑",
"",
"a" * 1000, # Long string
])
def test_encrypt_decrypt_various_data(data: str):
"""Parametrized test for various data types"""
encryptor = SymmetricEncryption("test_password")
encrypted = encryptor.encrypt_with_metadata_return_str(data)
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == data
@pytest.mark.parametrize("password", [
"simple",
"with spaces",
"special!@#$%",
"unicode世界",
"123456",
"a" * 100, # Long password
])
def test_various_passwords(password: str):
"""Parametrized test for various passwords"""
encryptor = SymmetricEncryption(password)
encrypted = encryptor.encrypt_with_metadata_return_str("test data")
decrypted = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test data"
@pytest.mark.parametrize("return_type,expected_type", [
("str", str),
("json", str),
("bytes", bytes),
("dict", dict),
])
def test_return_types_parametrized(return_type: str, expected_type: type):
"""Parametrized test for different return types"""
encryptor = SymmetricEncryption("test_password")
result = encryptor.encrypt_with_metadata("test data", return_as=return_type)
assert isinstance(result, expected_type)
# Fixtures
@pytest.fixture
def encryptor() -> SymmetricEncryption:
"""Fixture providing a basic encryptor instance"""
return SymmetricEncryption("test_password")
@pytest.fixture
def sample_encrypted_data(encryptor: SymmetricEncryption) -> str:
"""Fixture providing sample encrypted data"""
return encryptor.encrypt_with_metadata_return_str("sample data")
def test_with_encryptor_fixture(encryptor: SymmetricEncryption) -> None:
"""Test using encryptor fixture"""
encrypted: str = encryptor.encrypt_with_metadata_return_str("test")
decrypted: str = encryptor.decrypt_with_metadata(encrypted)
assert decrypted == "test"
def test_with_encrypted_data_fixture(encryptor: SymmetricEncryption, sample_encrypted_data: str) -> None:
"""Test using encrypted data fixture"""
decrypted: str = encryptor.decrypt_with_metadata(sample_encrypted_data)
assert decrypted == "sample data"
# __END__

View File

@@ -1,538 +0,0 @@
"""
PyTest: file_handling/file_bom_encoding
"""
from pathlib import Path
import pytest
from corelibs.file_handling.file_bom_encoding import (
is_bom_encoded,
is_bom_encoded_info,
BomEncodingInfo,
)
class TestIsBomEncoded:
"""Test suite for is_bom_encoded function"""
def test_utf8_bom_file(self, tmp_path: Path):
"""Test detection of UTF-8 BOM encoded file"""
test_file = tmp_path / "utf8_bom.txt"
# UTF-8 BOM: EF BB BF
content = b'\xef\xbb\xbfHello, World!'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
assert isinstance(result, bool)
def test_utf16_le_bom_file(self, tmp_path: Path):
"""Test detection of UTF-16 LE BOM encoded file"""
test_file = tmp_path / "utf16_le_bom.txt"
# UTF-16 LE BOM: FF FE
content = b'\xff\xfeH\x00e\x00l\x00l\x00o\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf16_be_bom_file(self, tmp_path: Path):
"""Test detection of UTF-16 BE BOM encoded file"""
test_file = tmp_path / "utf16_be_bom.txt"
# UTF-16 BE BOM: FE FF
content = b'\xfe\xff\x00H\x00e\x00l\x00l\x00o'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf32_le_bom_file(self, tmp_path: Path):
"""Test detection of UTF-32 LE BOM encoded file"""
test_file = tmp_path / "utf32_le_bom.txt"
# UTF-32 LE BOM: FF FE 00 00
content = b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf32_be_bom_file(self, tmp_path: Path):
"""Test detection of UTF-32 BE BOM encoded file"""
test_file = tmp_path / "utf32_be_bom.txt"
# UTF-32 BE BOM: 00 00 FE FF
content = b'\x00\x00\xfe\xff\x00\x00\x00H\x00\x00\x00e'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_no_bom_ascii_file(self, tmp_path: Path):
"""Test detection of ASCII file without BOM"""
test_file = tmp_path / "ascii.txt"
content = b'Hello, World!'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_no_bom_utf8_file(self, tmp_path: Path):
"""Test detection of UTF-8 file without BOM"""
test_file = tmp_path / "utf8_no_bom.txt"
content = 'Hello, 世界!'.encode('utf-8')
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_empty_file(self, tmp_path: Path):
"""Test detection on empty file"""
test_file = tmp_path / "empty.txt"
test_file.write_bytes(b'')
result = is_bom_encoded(test_file)
assert result is False
def test_binary_file_no_bom(self, tmp_path: Path):
"""Test detection on binary file without BOM"""
test_file = tmp_path / "binary.bin"
content = bytes(range(256))
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_partial_bom_pattern(self, tmp_path: Path):
"""Test file with partial BOM pattern that shouldn't match"""
test_file = tmp_path / "partial_bom.txt"
# Only first two bytes of UTF-8 BOM
content = b'\xef\xbbHello'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_false_positive_bom_pattern(self, tmp_path: Path):
"""Test file that contains BOM-like bytes but not at the start"""
test_file = tmp_path / "false_positive.txt"
content = b'Hello\xef\xbb\xbfWorld'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_nonexistent_file(self, tmp_path: Path):
"""Test that function raises error for non-existent file"""
test_file = tmp_path / "nonexistent.txt"
with pytest.raises(ValueError, match="Error checking BOM encoding"):
is_bom_encoded(test_file)
def test_very_small_file(self, tmp_path: Path):
"""Test file smaller than largest BOM pattern (4 bytes)"""
test_file = tmp_path / "small.txt"
content = b'Hi'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_exactly_bom_size_utf8(self, tmp_path: Path):
"""Test file that is exactly the size of UTF-8 BOM"""
test_file = tmp_path / "exact_bom.txt"
content = b'\xef\xbb\xbf'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_exactly_bom_size_utf32(self, tmp_path: Path):
"""Test file that is exactly the size of UTF-32 BOM"""
test_file = tmp_path / "exact_bom_utf32.txt"
content = b'\xff\xfe\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
class TestIsBomEncodedInfo:
"""Test suite for is_bom_encoded_info function"""
def test_utf8_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-8 BOM encoded file"""
test_file = tmp_path / "utf8_bom.txt"
content = b'\xef\xbb\xbfHello, UTF-8!'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert isinstance(result, dict)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
assert result['bom_length'] == 3
assert result['bom_pattern'] == b'\xef\xbb\xbf'
def test_utf16_le_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-16 LE BOM encoded file"""
test_file = tmp_path / "utf16_le_bom.txt"
content = b'\xff\xfeH\x00e\x00l\x00l\x00o\x00'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-16 LE'
assert result['encoding'] == 'utf-16-le'
assert result['bom_length'] == 2
assert result['bom_pattern'] == b'\xff\xfe'
def test_utf16_be_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-16 BE BOM encoded file"""
test_file = tmp_path / "utf16_be_bom.txt"
content = b'\xfe\xff\x00H\x00e\x00l\x00l\x00o'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-16 BE'
assert result['encoding'] == 'utf-16-be'
assert result['bom_length'] == 2
assert result['bom_pattern'] == b'\xfe\xff'
def test_utf32_le_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-32 LE BOM encoded file"""
test_file = tmp_path / "utf32_le_bom.txt"
content = b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 LE'
assert result['encoding'] == 'utf-32-le'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\xff\xfe\x00\x00'
def test_utf32_be_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-32 BE BOM encoded file"""
test_file = tmp_path / "utf32_be_bom.txt"
content = b'\x00\x00\xfe\xff\x00\x00\x00H\x00\x00\x00e'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 BE'
assert result['encoding'] == 'utf-32-be'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\x00\x00\xfe\xff'
def test_no_bom_info(self, tmp_path: Path):
"""Test detailed info for file without BOM"""
test_file = tmp_path / "no_bom.txt"
content = b'Hello, World!'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
assert result['bom_pattern'] is None
def test_empty_file_info(self, tmp_path: Path):
"""Test detailed info for empty file"""
test_file = tmp_path / "empty.txt"
test_file.write_bytes(b'')
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
assert result['bom_pattern'] is None
def test_bom_precedence_utf32_vs_utf16(self, tmp_path: Path):
"""Test that UTF-32 LE BOM takes precedence over UTF-16 LE when both match"""
test_file = tmp_path / "precedence.txt"
# UTF-32 LE BOM starts with UTF-16 LE BOM pattern
content = b'\xff\xfe\x00\x00Additional content'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
# Should detect UTF-32 LE, not UTF-16 LE
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 LE'
assert result['encoding'] == 'utf-32-le'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\xff\xfe\x00\x00'
def test_return_type_validation(self, tmp_path: Path):
"""Test that return type matches BomEncodingInfo TypedDict"""
test_file = tmp_path / "test.txt"
test_file.write_bytes(b'Test content')
result = is_bom_encoded_info(test_file)
# Check all required keys are present
required_keys = {'has_bom', 'bom_type', 'encoding', 'bom_length', 'bom_pattern'}
assert set(result.keys()) == required_keys
# Check types
assert isinstance(result['has_bom'], bool)
assert result['bom_type'] is None or isinstance(result['bom_type'], str)
assert result['encoding'] is None or isinstance(result['encoding'], str)
assert isinstance(result['bom_length'], int)
assert result['bom_pattern'] is None or isinstance(result['bom_pattern'], bytes)
def test_nonexistent_file_error(self, tmp_path: Path):
"""Test that function raises ValueError for non-existent file"""
test_file = tmp_path / "nonexistent.txt"
with pytest.raises(ValueError) as exc_info:
is_bom_encoded_info(test_file)
assert "Error checking BOM encoding" in str(exc_info.value)
def test_directory_instead_of_file(self, tmp_path: Path):
"""Test that function raises error when given a directory"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
with pytest.raises(ValueError, match="Error checking BOM encoding"):
is_bom_encoded_info(test_dir)
def test_large_file_with_bom(self, tmp_path: Path):
"""Test BOM detection on large file (only first 4 bytes matter)"""
test_file = tmp_path / "large_bom.txt"
# UTF-8 BOM followed by large content
content = b'\xef\xbb\xbf' + b'A' * 100000
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
def test_bom_detection_priority_order(self, tmp_path: Path):
"""Test that BOM patterns are checked in the correct priority order"""
# The function should check longer patterns first to avoid false matches
test_cases = [
(b'\xff\xfe\x00\x00', 'UTF-32 LE'), # 4 bytes
(b'\x00\x00\xfe\xff', 'UTF-32 BE'), # 4 bytes
(b'\xff\xfe', 'UTF-16 LE'), # 2 bytes
(b'\xfe\xff', 'UTF-16 BE'), # 2 bytes
(b'\xef\xbb\xbf', 'UTF-8'), # 3 bytes
]
for i, (bom_bytes, expected_type) in enumerate(test_cases):
test_file = tmp_path / f"priority_test_{i}.txt"
content = bom_bytes + b'Content'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['bom_type'] == expected_type
assert result['bom_pattern'] == bom_bytes
def test_csv_file_with_utf8_bom(self, tmp_path: Path):
"""Test CSV file with UTF-8 BOM (common use case mentioned in docstring)"""
test_file = tmp_path / "data.csv"
content = b'\xef\xbb\xbf"Name","Age","City"\n"John",30,"New York"\n"Jane",25,"Tokyo"'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
assert result['bom_length'] == 3
def test_csv_file_without_bom(self, tmp_path: Path):
"""Test CSV file without BOM"""
test_file = tmp_path / "data_no_bom.csv"
content = b'"Name","Age","City"\n"John",30,"New York"\n"Jane",25,"Tokyo"'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
class TestBomEncodingInfo:
"""Test suite for BomEncodingInfo TypedDict"""
def test_typed_dict_structure(self):
"""Test that BomEncodingInfo has correct structure"""
# This is a type check - in actual usage, mypy would validate this
sample_info: BomEncodingInfo = {
'has_bom': True,
'bom_type': 'UTF-8',
'encoding': 'utf-8',
'bom_length': 3,
'bom_pattern': b'\xef\xbb\xbf'
}
assert sample_info['has_bom'] is True
assert sample_info['bom_type'] == 'UTF-8'
assert sample_info['encoding'] == 'utf-8'
assert sample_info['bom_length'] == 3
assert sample_info['bom_pattern'] == b'\xef\xbb\xbf'
def test_typed_dict_none_values(self):
"""Test TypedDict with None values"""
sample_info: BomEncodingInfo = {
'has_bom': False,
'bom_type': None,
'encoding': None,
'bom_length': 0,
'bom_pattern': None
}
assert sample_info['has_bom'] is False
assert sample_info['bom_type'] is None
assert sample_info['encoding'] is None
assert sample_info['bom_length'] == 0
assert sample_info['bom_pattern'] is None
class TestIntegration:
"""Integration tests for BOM encoding detection"""
def test_is_bom_encoded_uses_info_function(self, tmp_path: Path):
"""Test that is_bom_encoded uses is_bom_encoded_info internally"""
test_file = tmp_path / "integration.txt"
content = b'\xef\xbb\xbfIntegration test'
test_file.write_bytes(content)
# Both functions should return consistent results
simple_result = is_bom_encoded(test_file)
detailed_result = is_bom_encoded_info(test_file)
assert simple_result == detailed_result['has_bom']
assert simple_result is True
def test_multiple_file_bom_detection_workflow(self, tmp_path: Path):
"""Test a workflow of detecting BOM across multiple files"""
files = {
'utf8_bom.csv': b'\xef\xbb\xbf"data","value"\n"test",123',
'utf16_le.txt': b'\xff\xfeH\x00e\x00l\x00l\x00o\x00',
'no_bom.txt': b'Plain ASCII text',
'empty.txt': b'',
}
results = {}
detailed_results = {}
for filename, content in files.items():
file_path = tmp_path / filename
file_path.write_bytes(content)
results[filename] = is_bom_encoded(file_path)
detailed_results[filename] = is_bom_encoded_info(file_path)
# Verify results
assert results['utf8_bom.csv'] is True
assert results['utf16_le.txt'] is True
assert results['no_bom.txt'] is False
assert results['empty.txt'] is False
# Verify detailed results match simple results
for filename in files:
assert results[filename] == detailed_results[filename]['has_bom']
# Verify specific encoding details
assert detailed_results['utf8_bom.csv']['encoding'] == 'utf-8'
assert detailed_results['utf16_le.txt']['encoding'] == 'utf-16-le'
assert detailed_results['no_bom.txt']['encoding'] is None
def test_csv_loading_workflow(self, tmp_path: Path):
"""Test BOM detection workflow for CSV loading (main use case)"""
# Create CSV files with and without BOM
csv_with_bom = tmp_path / "data_with_bom.csv"
csv_without_bom = tmp_path / "data_without_bom.csv"
# CSV with UTF-8 BOM
bom_content = b'\xef\xbb\xbf"Name","Age"\n"Alice",30\n"Bob",25'
csv_with_bom.write_bytes(bom_content)
# CSV without BOM
no_bom_content = b'"Name","Age"\n"Charlie",35\n"Diana",28'
csv_without_bom.write_bytes(no_bom_content)
# Simulate CSV loading workflow
files_to_process = [csv_with_bom, csv_without_bom]
processing_info: list[dict[str, str | bool | int]] = []
for csv_file in files_to_process:
bom_info = is_bom_encoded_info(csv_file)
file_info: dict[str, str | bool | int] = {
'file': csv_file.name,
'has_bom': bom_info['has_bom'],
'encoding': bom_info['encoding'] or 'default',
'skip_bytes': bom_info['bom_length']
}
processing_info.append(file_info)
# Verify workflow results
assert len(processing_info) == 2
bom_file_info = next(info for info in processing_info if info['file'] == 'data_with_bom.csv')
no_bom_file_info = next(info for info in processing_info if info['file'] == 'data_without_bom.csv')
assert bom_file_info['has_bom'] is True
assert bom_file_info['encoding'] == 'utf-8'
assert bom_file_info['skip_bytes'] == 3
assert no_bom_file_info['has_bom'] is False
assert no_bom_file_info['encoding'] == 'default'
assert no_bom_file_info['skip_bytes'] == 0
def test_error_handling_consistency(self, tmp_path: Path):
"""Test that both functions handle errors consistently"""
nonexistent_file = tmp_path / "does_not_exist.txt"
# Both functions should raise ValueError for non-existent files
with pytest.raises(ValueError):
is_bom_encoded(nonexistent_file)
with pytest.raises(ValueError):
is_bom_encoded_info(nonexistent_file)
def test_all_supported_bom_types(self, tmp_path: Path):
"""Test detection of all supported BOM types"""
bom_test_cases = [
('utf8', b'\xef\xbb\xbf', 'UTF-8', 'utf-8', 3),
('utf16_le', b'\xff\xfe', 'UTF-16 LE', 'utf-16-le', 2),
('utf16_be', b'\xfe\xff', 'UTF-16 BE', 'utf-16-be', 2),
('utf32_le', b'\xff\xfe\x00\x00', 'UTF-32 LE', 'utf-32-le', 4),
('utf32_be', b'\x00\x00\xfe\xff', 'UTF-32 BE', 'utf-32-be', 4),
]
for name, bom_bytes, expected_type, expected_encoding, expected_length in bom_test_cases:
test_file = tmp_path / f"{name}_test.txt"
content = bom_bytes + b'Test content'
test_file.write_bytes(content)
# Test simple function
assert is_bom_encoded(test_file) is True
# Test detailed function
info = is_bom_encoded_info(test_file)
assert info['has_bom'] is True
assert info['bom_type'] == expected_type
assert info['encoding'] == expected_encoding
assert info['bom_length'] == expected_length
assert info['bom_pattern'] == bom_bytes
# __END__

View File

@@ -1,389 +0,0 @@
"""
PyTest: file_handling/file_crc
"""
import zlib
from pathlib import Path
import pytest
from corelibs.file_handling.file_crc import (
file_crc,
file_name_crc,
)
class TestFileCrc:
"""Test suite for file_crc function"""
def test_file_crc_small_file(self, tmp_path: Path):
"""Test CRC calculation for a small file"""
test_file = tmp_path / "test_small.txt"
content = b"Hello, World!"
test_file.write_bytes(content)
# Calculate expected CRC
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
assert isinstance(result, str)
assert len(result) == 8 # CRC32 is 8 hex digits
def test_file_crc_large_file(self, tmp_path: Path):
"""Test CRC calculation for a file larger than buffer size (65536 bytes)"""
test_file = tmp_path / "test_large.bin"
# Create a file larger than the buffer (65536 bytes)
content = b"A" * 100000
test_file.write_bytes(content)
# Calculate expected CRC
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
def test_file_crc_empty_file(self, tmp_path: Path):
"""Test CRC calculation for an empty file"""
test_file = tmp_path / "test_empty.txt"
test_file.write_bytes(b"")
# CRC of empty data
expected_crc = f"{zlib.crc32(b"") & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
assert result == "00000000"
def test_file_crc_binary_file(self, tmp_path: Path):
"""Test CRC calculation for a binary file"""
test_file = tmp_path / "test_binary.bin"
content = bytes(range(256)) # All possible byte values
test_file.write_bytes(content)
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
def test_file_crc_exact_buffer_size(self, tmp_path: Path):
"""Test CRC calculation for a file exactly the buffer size"""
test_file = tmp_path / "test_exact_buffer.bin"
content = b"X" * 65536
test_file.write_bytes(content)
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
def test_file_crc_multiple_buffers(self, tmp_path: Path):
"""Test CRC calculation for a file requiring multiple buffer reads"""
test_file = tmp_path / "test_multi_buffer.bin"
content = b"TestData" * 20000 # ~160KB
test_file.write_bytes(content)
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
def test_file_crc_unicode_content(self, tmp_path: Path):
"""Test CRC calculation for a file with unicode content"""
test_file = tmp_path / "test_unicode.txt"
content = "Hello 世界! 🌍".encode('utf-8')
test_file.write_bytes(content)
expected_crc = f"{zlib.crc32(content) & 0xFFFFFFFF:08X}"
result = file_crc(test_file)
assert result == expected_crc
def test_file_crc_deterministic(self, tmp_path: Path):
"""Test that CRC calculation is deterministic"""
test_file = tmp_path / "test_deterministic.txt"
content = b"Deterministic test content"
test_file.write_bytes(content)
result1 = file_crc(test_file)
result2 = file_crc(test_file)
assert result1 == result2
def test_file_crc_different_files(self, tmp_path: Path):
"""Test that different files produce different CRCs"""
file1 = tmp_path / "file1.txt"
file2 = tmp_path / "file2.txt"
file1.write_bytes(b"Content 1")
file2.write_bytes(b"Content 2")
crc1 = file_crc(file1)
crc2 = file_crc(file2)
assert crc1 != crc2
def test_file_crc_same_content_different_names(self, tmp_path: Path):
"""Test that files with same content produce same CRC regardless of name"""
file1 = tmp_path / "name1.txt"
file2 = tmp_path / "name2.txt"
content = b"Same content"
file1.write_bytes(content)
file2.write_bytes(content)
crc1 = file_crc(file1)
crc2 = file_crc(file2)
assert crc1 == crc2
def test_file_crc_nonexistent_file(self, tmp_path: Path):
"""Test that file_crc raises error for non-existent file"""
test_file = tmp_path / "nonexistent.txt"
with pytest.raises(FileNotFoundError):
file_crc(test_file)
def test_file_crc_with_path_object(self, tmp_path: Path):
"""Test file_crc works with Path object"""
test_file = tmp_path / "test_path.txt"
test_file.write_bytes(b"Test with Path")
result = file_crc(test_file)
assert isinstance(result, str)
assert len(result) == 8
class TestFileNameCrc:
"""Test suite for file_name_crc function"""
def test_file_name_crc_simple_filename(self, tmp_path: Path):
"""Test extracting simple filename without parent folder"""
test_file = tmp_path / "testfile.csv"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "testfile.csv"
def test_file_name_crc_with_parent_folder(self, tmp_path: Path):
"""Test extracting filename with parent folder"""
parent = tmp_path / "parent_folder"
parent.mkdir()
test_file = parent / "testfile.csv"
result = file_name_crc(test_file, add_parent_folder=True)
assert result == "parent_folder/testfile.csv"
def test_file_name_crc_nested_path_without_parent(self):
"""Test filename extraction from deeply nested path without parent"""
test_path = Path("/foo/bar/baz/file.csv")
result = file_name_crc(test_path, add_parent_folder=False)
assert result == "file.csv"
def test_file_name_crc_nested_path_with_parent(self):
"""Test filename extraction from deeply nested path with parent"""
test_path = Path("/foo/bar/baz/file.csv")
result = file_name_crc(test_path, add_parent_folder=True)
assert result == "baz/file.csv"
def test_file_name_crc_default_parameter(self, tmp_path: Path):
"""Test that add_parent_folder defaults to False"""
test_file = tmp_path / "subdir" / "testfile.txt"
test_file.parent.mkdir(parents=True)
result = file_name_crc(test_file)
assert result == "testfile.txt"
def test_file_name_crc_different_extensions(self, tmp_path: Path):
"""Test with different file extensions"""
extensions = [".txt", ".csv", ".json", ".xml", ".py"]
for ext in extensions:
test_file = tmp_path / f"testfile{ext}"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == f"testfile{ext}"
def test_file_name_crc_no_extension(self, tmp_path: Path):
"""Test with filename without extension"""
test_file = tmp_path / "testfile"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "testfile"
def test_file_name_crc_multiple_dots(self, tmp_path: Path):
"""Test with filename containing multiple dots"""
test_file = tmp_path / "test.file.name.tar.gz"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "test.file.name.tar.gz"
def test_file_name_crc_with_spaces(self, tmp_path: Path):
"""Test with filename containing spaces"""
test_file = tmp_path / "test file name.txt"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "test file name.txt"
def test_file_name_crc_with_special_chars(self, tmp_path: Path):
"""Test with filename containing special characters"""
test_file = tmp_path / "test_file-name (1).txt"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "test_file-name (1).txt"
def test_file_name_crc_unicode_filename(self, tmp_path: Path):
"""Test with unicode characters in filename"""
test_file = tmp_path / "テストファイル.txt"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "テストファイル.txt"
def test_file_name_crc_unicode_parent(self, tmp_path: Path):
"""Test with unicode characters in parent folder name"""
parent = tmp_path / "親フォルダ"
parent.mkdir()
test_file = parent / "file.txt"
result = file_name_crc(test_file, add_parent_folder=True)
assert result == "親フォルダ/file.txt"
def test_file_name_crc_path_separator(self, tmp_path: Path):
"""Test that result uses forward slash separator"""
parent = tmp_path / "parent"
parent.mkdir()
test_file = parent / "file.txt"
result = file_name_crc(test_file, add_parent_folder=True)
assert "/" in result
assert result == "parent/file.txt"
def test_file_name_crc_return_type(self, tmp_path: Path):
"""Test that return type is always string"""
test_file = tmp_path / "test.txt"
result1 = file_name_crc(test_file, add_parent_folder=False)
result2 = file_name_crc(test_file, add_parent_folder=True)
assert isinstance(result1, str)
assert isinstance(result2, str)
def test_file_name_crc_root_level_file(self):
"""Test with file at root level"""
test_path = Path("/file.txt")
result_without_parent = file_name_crc(test_path, add_parent_folder=False)
assert result_without_parent == "file.txt"
result_with_parent = file_name_crc(test_path, add_parent_folder=True)
# Parent of root-level file would be empty string or root
assert "file.txt" in result_with_parent
def test_file_name_crc_relative_path(self):
"""Test with relative path"""
test_path = Path("folder/subfolder/file.txt")
result = file_name_crc(test_path, add_parent_folder=True)
assert result == "subfolder/file.txt"
def test_file_name_crc_current_dir(self):
"""Test with file in current directory"""
test_path = Path("file.txt")
result = file_name_crc(test_path, add_parent_folder=False)
assert result == "file.txt"
def test_file_name_crc_nonexistent_file(self, tmp_path: Path):
"""Test that file_name_crc works even if file doesn't exist"""
test_file = tmp_path / "parent" / "nonexistent.txt"
# Should work without file existing
result1 = file_name_crc(test_file, add_parent_folder=False)
assert result1 == "nonexistent.txt"
result2 = file_name_crc(test_file, add_parent_folder=True)
assert result2 == "parent/nonexistent.txt"
def test_file_name_crc_explicit_true(self, tmp_path: Path):
"""Test explicitly setting add_parent_folder to True"""
parent = tmp_path / "mydir"
parent.mkdir()
test_file = parent / "myfile.dat"
result = file_name_crc(test_file, add_parent_folder=True)
assert result == "mydir/myfile.dat"
def test_file_name_crc_explicit_false(self, tmp_path: Path):
"""Test explicitly setting add_parent_folder to False"""
parent = tmp_path / "mydir"
parent.mkdir()
test_file = parent / "myfile.dat"
result = file_name_crc(test_file, add_parent_folder=False)
assert result == "myfile.dat"
class TestIntegration:
"""Integration tests combining both functions"""
def test_crc_and_naming_together(self, tmp_path: Path):
"""Test using both functions on the same file"""
parent = tmp_path / "data"
parent.mkdir()
test_file = parent / "testfile.csv"
test_file.write_bytes(b"Sample data for integration test")
# Get CRC
crc = file_crc(test_file)
assert len(crc) == 8
# Get filename
name_simple = file_name_crc(test_file, add_parent_folder=False)
assert name_simple == "testfile.csv"
name_with_parent = file_name_crc(test_file, add_parent_folder=True)
assert name_with_parent == "data/testfile.csv"
def test_multiple_files_crc_comparison(self, tmp_path: Path):
"""Test CRC comparison across multiple files"""
files: dict[str, str] = {}
for i in range(3):
file_path = tmp_path / f"file{i}.txt"
file_path.write_bytes(f"Content {i}".encode())
files[f"file{i}.txt"] = file_crc(file_path)
# All CRCs should be different
assert len(set(files.values())) == 3
def test_workflow_file_identification(self, tmp_path: Path):
"""Test a workflow of identifying files by name and verifying by CRC"""
# Create directory structure
dir1 = tmp_path / "dir1"
dir2 = tmp_path / "dir2"
dir1.mkdir()
dir2.mkdir()
# Create same-named files with different content
file1 = dir1 / "data.csv"
file2 = dir2 / "data.csv"
file1.write_bytes(b"Data set 1")
file2.write_bytes(b"Data set 2")
# Get names (should be the same)
name1 = file_name_crc(file1, add_parent_folder=False)
name2 = file_name_crc(file2, add_parent_folder=False)
assert name1 == name2 == "data.csv"
# Get names with parent (should be different)
full_name1 = file_name_crc(file1, add_parent_folder=True)
full_name2 = file_name_crc(file2, add_parent_folder=True)
assert full_name1 == "dir1/data.csv"
assert full_name2 == "dir2/data.csv"
# Get CRCs (should be different)
crc1 = file_crc(file1)
crc2 = file_crc(file2)
assert crc1 != crc2
# __END__

View File

@@ -1,522 +0,0 @@
"""
PyTest: file_handling/file_handling
"""
# pylint: disable=use-implicit-booleaness-not-comparison
from pathlib import Path
from pytest import CaptureFixture
from corelibs.file_handling.file_handling import (
remove_all_in_directory,
)
class TestRemoveAllInDirectory:
"""Test suite for remove_all_in_directory function"""
def test_remove_all_files_in_empty_directory(self, tmp_path: Path):
"""Test removing all files from an empty directory"""
test_dir = tmp_path / "empty_dir"
test_dir.mkdir()
result = remove_all_in_directory(test_dir)
assert result is True
assert test_dir.exists() # Directory itself should still exist
assert list(test_dir.iterdir()) == []
def test_remove_all_files_in_directory(self, tmp_path: Path):
"""Test removing all files from a directory with files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create test files
(test_dir / "file1.txt").write_text("content 1")
(test_dir / "file2.txt").write_text("content 2")
(test_dir / "file3.csv").write_text("csv,data")
result = remove_all_in_directory(test_dir)
assert result is True
assert test_dir.exists()
assert list(test_dir.iterdir()) == []
def test_remove_all_subdirectories(self, tmp_path: Path):
"""Test removing subdirectories within a directory"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create subdirectories
subdir1 = test_dir / "subdir1"
subdir2 = test_dir / "subdir2"
subdir1.mkdir()
subdir2.mkdir()
# Add files to subdirectories
(subdir1 / "file.txt").write_text("content")
(subdir2 / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert test_dir.exists()
assert list(test_dir.iterdir()) == []
def test_remove_nested_structure(self, tmp_path: Path):
"""Test removing deeply nested directory structure"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create nested structure
nested = test_dir / "level1" / "level2" / "level3"
nested.mkdir(parents=True)
(nested / "deep_file.txt").write_text("deep content")
(test_dir / "level1" / "mid_file.txt").write_text("mid content")
(test_dir / "top_file.txt").write_text("top content")
result = remove_all_in_directory(test_dir)
assert result is True
assert test_dir.exists()
assert list(test_dir.iterdir()) == []
def test_remove_with_ignore_files_single(self, tmp_path: Path):
"""Test removing files while ignoring specific files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files
(test_dir / "keep.txt").write_text("keep me")
(test_dir / "remove1.txt").write_text("remove me")
(test_dir / "remove2.txt").write_text("remove me too")
result = remove_all_in_directory(test_dir, ignore_files=["keep.txt"])
assert result is True
assert test_dir.exists()
remaining = list(test_dir.iterdir())
assert len(remaining) == 1
assert remaining[0].name == "keep.txt"
def test_remove_with_ignore_files_multiple(self, tmp_path: Path):
"""Test removing files while ignoring multiple specific files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files
(test_dir / "keep1.txt").write_text("keep me")
(test_dir / "keep2.log").write_text("keep me too")
(test_dir / "remove.txt").write_text("remove me")
result = remove_all_in_directory(
test_dir,
ignore_files=["keep1.txt", "keep2.log"]
)
assert result is True
assert test_dir.exists()
remaining = {f.name for f in test_dir.iterdir()}
assert remaining == {"keep1.txt", "keep2.log"}
def test_remove_with_ignore_directory(self, tmp_path: Path):
"""Test removing with ignored directory"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create directories
keep_dir = test_dir / "keep_dir"
remove_dir = test_dir / "remove_dir"
keep_dir.mkdir()
remove_dir.mkdir()
(keep_dir / "file.txt").write_text("keep")
(remove_dir / "file.txt").write_text("remove")
result = remove_all_in_directory(test_dir, ignore_files=["keep_dir"])
assert result is True
assert keep_dir.exists()
assert not remove_dir.exists()
def test_remove_with_ignore_nested_files(self, tmp_path: Path):
"""Test that ignore_files matches by name at any level"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files with same name at different levels
(test_dir / "keep.txt").write_text("top level keep")
(test_dir / "remove.txt").write_text("remove")
subdir = test_dir / "subdir"
subdir.mkdir()
(subdir / "file.txt").write_text("nested")
result = remove_all_in_directory(test_dir, ignore_files=["keep.txt"])
assert result is True
# keep.txt should be preserved at top level
assert (test_dir / "keep.txt").exists()
# Other files should be removed
assert not (test_dir / "remove.txt").exists()
# Subdirectory not in ignore list should be removed
assert not subdir.exists()
def test_remove_nonexistent_directory(self, tmp_path: Path):
"""Test removing from a non-existent directory returns False"""
test_dir = tmp_path / "nonexistent"
result = remove_all_in_directory(test_dir)
assert result is False
def test_remove_from_file_not_directory(self, tmp_path: Path):
"""Test that function returns False when given a file instead of directory"""
test_file = tmp_path / "file.txt"
test_file.write_text("content")
result = remove_all_in_directory(test_file)
assert result is False
assert test_file.exists() # File should not be affected
def test_remove_with_verbose_mode(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test verbose mode produces output"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files and directories
(test_dir / "file1.txt").write_text("content")
(test_dir / "file2.txt").write_text("content")
subdir = test_dir / "subdir"
subdir.mkdir()
(subdir / "nested.txt").write_text("content")
result = remove_all_in_directory(test_dir, verbose=True)
assert result is True
captured = capsys.readouterr()
assert "Remove old files in: test_dir [" in captured.out
assert "]" in captured.out
assert "." in captured.out # Files are marked with .
assert "/" in captured.out # Directories are marked with /
def test_remove_with_dry_run_mode(self, tmp_path: Path):
"""Test dry run mode doesn't actually remove files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create test files
file1 = test_dir / "file1.txt"
file2 = test_dir / "file2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
result = remove_all_in_directory(test_dir, dry_run=True)
assert result is True
# Files should still exist
assert file1.exists()
assert file2.exists()
assert len(list(test_dir.iterdir())) == 2
def test_remove_with_dry_run_and_verbose(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test dry run with verbose mode shows [DRY RUN] prefix"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir, dry_run=True, verbose=True)
assert result is True
captured = capsys.readouterr()
assert "[DRY RUN]" in captured.out
def test_remove_mixed_content(self, tmp_path: Path):
"""Test removing mixed files and directories"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create mixed content
(test_dir / "file1.txt").write_text("content")
(test_dir / "file2.csv").write_text("csv")
subdir1 = test_dir / "subdir1"
subdir2 = test_dir / "subdir2"
subdir1.mkdir()
subdir2.mkdir()
(subdir1 / "nested_file.txt").write_text("nested")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_with_none_ignore_files(self, tmp_path: Path):
"""Test that None as ignore_files works correctly"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir, ignore_files=None)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_with_empty_ignore_list(self, tmp_path: Path):
"""Test that empty ignore_files list works correctly"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir, ignore_files=[])
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_special_characters_in_filenames(self, tmp_path: Path):
"""Test removing files with special characters in names"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files with special characters
(test_dir / "file with spaces.txt").write_text("content")
(test_dir / "file-with-dashes.txt").write_text("content")
(test_dir / "file_with_underscores.txt").write_text("content")
(test_dir / "file.multiple.dots.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_unicode_filenames(self, tmp_path: Path):
"""Test removing files with unicode characters in names"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files with unicode names
(test_dir / "ファイル.txt").write_text("content")
(test_dir / "文件.txt").write_text("content")
(test_dir / "αρχείο.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_hidden_files(self, tmp_path: Path):
"""Test removing hidden files (dotfiles)"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create hidden files
(test_dir / ".hidden").write_text("content")
(test_dir / ".gitignore").write_text("content")
(test_dir / "normal.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_preserves_ignored_hidden_files(self, tmp_path: Path):
"""Test that ignored hidden files are preserved"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / ".gitkeep").write_text("keep")
(test_dir / "file.txt").write_text("remove")
result = remove_all_in_directory(test_dir, ignore_files=[".gitkeep"])
assert result is True
remaining = list(test_dir.iterdir())
assert len(remaining) == 1
assert remaining[0].name == ".gitkeep"
def test_remove_large_number_of_files(self, tmp_path: Path):
"""Test removing a large number of files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create 100 files
for i in range(100):
(test_dir / f"file_{i:03d}.txt").write_text(f"content {i}")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_deeply_nested_with_ignore(self, tmp_path: Path):
"""Test removing structure while preserving ignored items
Note: rglob processes files depth-first, so files inside an ignored
directory will be processed (and potentially removed) before the directory
itself is checked. Only items at the same level or that share the same name
as ignored items will be preserved.
"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create structure
level1 = test_dir / "level1"
level1.mkdir()
keep_file = test_dir / "keep.txt"
(level1 / "file.txt").write_text("remove")
keep_file.write_text("keep this file")
(test_dir / "top.txt").write_text("remove")
result = remove_all_in_directory(test_dir, ignore_files=["keep.txt"])
assert result is True
# Check that keep.txt is preserved
assert keep_file.exists()
assert keep_file.read_text() == "keep this file"
# Other items should be removed
assert not (test_dir / "top.txt").exists()
assert not level1.exists()
def test_remove_binary_files(self, tmp_path: Path):
"""Test removing binary files"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create binary files
(test_dir / "binary1.bin").write_bytes(bytes(range(256)))
(test_dir / "binary2.dat").write_bytes(b"\x00\x01\x02\xff")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_symlinks(self, tmp_path: Path):
"""Test removing symbolic links"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create a file and a symlink to it
original = tmp_path / "original.txt"
original.write_text("original content")
symlink = test_dir / "link.txt"
symlink.symlink_to(original)
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
# Original file should still exist
assert original.exists()
def test_remove_with_permissions_variations(self, tmp_path: Path):
"""Test removing files with different permissions"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create files
file1 = test_dir / "readonly.txt"
file2 = test_dir / "normal.txt"
file1.write_text("readonly")
file2.write_text("normal")
# Make file1 read-only
file1.chmod(0o444)
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_default_parameters(self, tmp_path: Path):
"""Test function with only required parameter"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert list(test_dir.iterdir()) == []
def test_remove_return_value_true_when_successful(self, tmp_path: Path):
"""Test that function returns True on successful removal"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
(test_dir / "file.txt").write_text("content")
result = remove_all_in_directory(test_dir)
assert result is True
assert isinstance(result, bool)
def test_remove_return_value_false_when_not_directory(self, tmp_path: Path):
"""Test that function returns False when path is not a directory"""
test_file = tmp_path / "file.txt"
test_file.write_text("content")
result = remove_all_in_directory(test_file)
assert result is False
assert isinstance(result, bool)
def test_remove_directory_becomes_empty(self, tmp_path: Path):
"""Test that directory is empty after removal"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create various items
(test_dir / "file.txt").write_text("content")
subdir = test_dir / "subdir"
subdir.mkdir()
(subdir / "nested.txt").write_text("nested")
# Verify directory is not empty before
assert len(list(test_dir.iterdir())) > 0
result = remove_all_in_directory(test_dir)
assert result is True
# Verify directory is empty after
assert len(list(test_dir.iterdir())) == 0
assert test_dir.exists()
assert test_dir.is_dir()
class TestIntegration:
"""Integration tests for file_handling module"""
def test_multiple_remove_operations(self, tmp_path: Path):
"""Test multiple consecutive remove operations"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# First batch of files
(test_dir / "batch1_file1.txt").write_text("content")
(test_dir / "batch1_file2.txt").write_text("content")
result1 = remove_all_in_directory(test_dir)
assert result1 is True
assert list(test_dir.iterdir()) == []
# Second batch of files
(test_dir / "batch2_file1.txt").write_text("content")
(test_dir / "batch2_file2.txt").write_text("content")
result2 = remove_all_in_directory(test_dir)
assert result2 is True
assert list(test_dir.iterdir()) == []
def test_remove_then_recreate(self, tmp_path: Path):
"""Test removing files then recreating them"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Create and remove
original_file = test_dir / "file.txt"
original_file.write_text("original")
remove_all_in_directory(test_dir)
assert not original_file.exists()
# Recreate
new_file = test_dir / "file.txt"
new_file.write_text("new content")
assert new_file.exists()
assert new_file.read_text() == "new content"
def test_cleanup_workflow(self, tmp_path: Path):
"""Test a typical cleanup workflow"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
# Simulate work directory
(test_dir / "temp1.tmp").write_text("temp")
(test_dir / "temp2.tmp").write_text("temp")
(test_dir / "result.txt").write_text("important")
# Clean up temp files, keep result
result = remove_all_in_directory(
test_dir,
ignore_files=["result.txt"]
)
assert result is True
remaining = list(test_dir.iterdir())
assert len(remaining) == 1
assert remaining[0].name == "result.txt"
assert remaining[0].read_text() == "important"
# __END__

View File

@@ -1,601 +0,0 @@
"""
tests for corelibs.iterator_handling.data_search
"""
# pylint: disable=use-implicit-booleaness-not-comparison
from typing import Any
import pytest
from corelibs.iterator_handling.data_search import (
find_in_array_from_list,
key_lookup,
value_lookup,
ArraySearchList
)
class TestFindInArrayFromList:
"""Tests for find_in_array_from_list function"""
def test_basic_single_key_match(self):
"""Test basic search with single key-value pair"""
data = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Bob"}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 1
assert result[0]["name"] == "Bob"
assert result[0]["age"] == 25
def test_multiple_key_match(self):
"""Test search with multiple key-value pairs (AND logic)"""
data = [
{"name": "Alice", "age": 30, "city": "New York"},
{"name": "Bob", "age": 25, "city": "London"},
{"name": "Charlie", "age": 30, "city": "Paris"}
]
search_params: list[ArraySearchList] = [
{"key": "age", "value": 30},
{"key": "city", "value": "New York"}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 1
assert result[0]["name"] == "Alice"
def test_value_list_or_match(self):
"""Test search with list of values (OR logic)"""
data = [
{"name": "Alice", "status": "active"},
{"name": "Bob", "status": "inactive"},
{"name": "Charlie", "status": "pending"}
]
search_params: list[ArraySearchList] = [
{"key": "status", "value": ["active", "pending"]}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "Charlie"
def test_case_sensitive_true(self):
"""Test case-sensitive search (default behavior)"""
data = [
{"name": "Alice"},
{"name": "alice"},
{"name": "ALICE"}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Alice"}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 1
assert result[0]["name"] == "Alice"
def test_case_insensitive_search(self):
"""Test case-insensitive search"""
data = [
{"name": "Alice"},
{"name": "alice"},
{"name": "ALICE"}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "alice", "case_sensitive": False}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 3
def test_case_insensitive_with_list_values(self):
"""Test case-insensitive search with list of values"""
data = [
{"status": "ACTIVE"},
{"status": "Pending"},
{"status": "inactive"}
]
search_params: list[ArraySearchList] = [
{"key": "status", "value": ["active", "pending"], "case_sensitive": False}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["status"] == "ACTIVE"
assert result[1]["status"] == "Pending"
def test_return_index_true(self):
"""Test returning results with index"""
data = [
{"name": "Alice"},
{"name": "Bob"},
{"name": "Charlie"}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Bob"}
]
result = find_in_array_from_list(data, search_params, return_index=True)
assert len(result) == 1
assert result[0]["index"] == 1
assert result[0]["data"]["name"] == "Bob"
def test_return_index_multiple_results(self):
"""Test returning multiple results with indices"""
data = [
{"status": "active"},
{"status": "inactive"},
{"status": "active"}
]
search_params: list[ArraySearchList] = [
{"key": "status", "value": "active"}
]
result = find_in_array_from_list(data, search_params, return_index=True)
assert len(result) == 2
assert result[0]["index"] == 0
assert result[0]["data"]["status"] == "active"
assert result[1]["index"] == 2
assert result[1]["data"]["status"] == "active"
def test_no_match_returns_empty_list(self):
"""Test that no match returns empty list"""
data = [
{"name": "Alice"},
{"name": "Bob"}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Charlie"}
]
result = find_in_array_from_list(data, search_params)
assert result == []
def test_empty_data_returns_empty_list(self):
"""Test that empty data list returns empty list"""
data: list[dict[str, Any]] = []
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Alice"}
]
result = find_in_array_from_list(data, search_params)
assert result == []
def test_missing_key_in_data(self):
"""Test search when key doesn't exist in some data items"""
data = [
{"name": "Alice", "age": 30},
{"name": "Bob"}, # Missing 'age' key
{"name": "Charlie", "age": 30}
]
search_params: list[ArraySearchList] = [
{"key": "age", "value": 30}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "Charlie"
def test_numeric_values(self):
"""Test search with numeric values"""
data = [
{"id": 1, "score": 95},
{"id": 2, "score": 87},
{"id": 3, "score": 95}
]
search_params: list[ArraySearchList] = [
{"key": "score", "value": 95}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["id"] == 1
assert result[1]["id"] == 3
def test_boolean_values(self):
"""Test search with boolean values"""
data = [
{"name": "Alice", "active": True},
{"name": "Bob", "active": False},
{"name": "Charlie", "active": True}
]
search_params: list[ArraySearchList] = [
{"key": "active", "value": True}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "Charlie"
def test_float_values(self):
"""Test search with float values"""
data = [
{"name": "Product A", "price": 19.99},
{"name": "Product B", "price": 29.99},
{"name": "Product C", "price": 19.99}
]
search_params: list[ArraySearchList] = [
{"key": "price", "value": 19.99}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Product A"
assert result[1]["name"] == "Product C"
def test_mixed_value_types_in_list(self):
"""Test search with mixed types in value list"""
data = [
{"id": "1", "value": "active"},
{"id": 2, "value": "pending"},
{"id": "3", "value": "active"}
]
search_params: list[ArraySearchList] = [
{"key": "id", "value": ["1", "3"]}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["id"] == "1"
assert result[1]["id"] == "3"
def test_complex_multi_criteria_search(self):
"""Test complex search with multiple criteria"""
data = [
{"name": "Alice", "age": 30, "city": "New York", "status": "active"},
{"name": "Bob", "age": 25, "city": "London", "status": "active"},
{"name": "Charlie", "age": 30, "city": "Paris", "status": "inactive"},
{"name": "David", "age": 30, "city": "New York", "status": "active"}
]
search_params: list[ArraySearchList] = [
{"key": "age", "value": 30},
{"key": "city", "value": "New York"},
{"key": "status", "value": "active"}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Alice"
assert result[1]["name"] == "David"
def test_invalid_search_params_not_list(self):
"""Test that non-list search_params raises ValueError"""
data = [{"name": "Alice"}]
search_params = {"key": "name", "value": "Alice"} # type: ignore
with pytest.raises(ValueError, match="search_params must be a list"):
find_in_array_from_list(data, search_params) # type: ignore
def test_missing_key_in_search_params(self):
"""Test that missing 'key' in search_params raises KeyError"""
data = [{"name": "Alice"}]
search_params: list[dict[str, Any]] = [
{"value": "Alice"} # Missing 'key'
]
with pytest.raises(KeyError, match="Either Key '' or Value 'Alice' is missing or empty"):
find_in_array_from_list(data, search_params) # type: ignore
def test_missing_value_in_search_params(self):
"""Test that missing 'value' in search_params raises KeyError"""
data = [{"name": "Alice"}]
search_params: list[dict[str, Any]] = [
{"key": "name"} # Missing 'value'
]
with pytest.raises(KeyError, match="Either Key 'name' or Value"):
find_in_array_from_list(data, search_params) # type: ignore
def test_empty_key_in_search_params(self):
"""Test that empty 'key' in search_params raises KeyError"""
data = [{"name": "Alice"}]
search_params: list[dict[str, Any]] = [
{"key": "", "value": "Alice"}
]
with pytest.raises(KeyError, match="Either Key '' or Value 'Alice' is missing or empty"):
find_in_array_from_list(data, search_params) # type: ignore
def test_empty_value_in_search_params(self):
"""Test that empty 'value' in search_params raises KeyError"""
data = [{"name": "Alice"}]
search_params: list[dict[str, Any]] = [
{"key": "name", "value": ""}
]
with pytest.raises(KeyError, match="Either Key 'name' or Value '' is missing or empty"):
find_in_array_from_list(data, search_params) # type: ignore
def test_duplicate_key_in_search_params(self):
"""Test that duplicate keys in search_params raises KeyError"""
data = [{"name": "Alice", "age": 30}]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Alice"},
{"key": "name", "value": "Bob"} # Duplicate key
]
with pytest.raises(KeyError, match="Key name already exists in search_params"):
find_in_array_from_list(data, search_params)
def test_partial_match_fails(self):
"""Test that partial match (not all criteria) returns no result"""
data = [
{"name": "Alice", "age": 30, "city": "New York"}
]
search_params: list[ArraySearchList] = [
{"key": "name", "value": "Alice"},
{"key": "age", "value": 25} # Doesn't match
]
result = find_in_array_from_list(data, search_params)
assert result == []
def test_none_value_in_list(self):
"""Test search with None in value list"""
data = [
{"name": "Alice", "nickname": "Ally"},
{"name": "Bob", "nickname": None},
{"name": "Charlie", "nickname": "Chuck"}
]
search_params: list[ArraySearchList] = [
{"key": "nickname", "value": [None, "Chuck"]}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == 2
assert result[0]["name"] == "Bob"
assert result[1]["name"] == "Charlie"
@pytest.mark.parametrize("test_value,expected_count", [
("active", 1),
("inactive", 1),
("pending", 1),
("archived", 0)
])
def test_parametrized_status_search(self, test_value: str, expected_count: int):
"""Parametrized test for different status values"""
data = [
{"id": 1, "status": "active"},
{"id": 2, "status": "inactive"},
{"id": 3, "status": "pending"}
]
search_params: list[ArraySearchList] = [
{"key": "status", "value": test_value}
]
result = find_in_array_from_list(data, search_params)
assert len(result) == expected_count
class TestKeyLookup:
"""Tests for key_lookup function"""
def test_key_exists(self):
"""Test lookup when key exists"""
haystack = {"name": "Alice", "age": "30", "city": "New York"}
result = key_lookup(haystack, "name")
assert result == "Alice"
def test_key_not_exists(self):
"""Test lookup when key doesn't exist returns empty string"""
haystack = {"name": "Alice", "age": "30"}
result = key_lookup(haystack, "city")
assert result == ""
def test_empty_dict(self):
"""Test lookup in empty dictionary"""
haystack: dict[str, str] = {}
result = key_lookup(haystack, "name")
assert result == ""
def test_multiple_lookups(self):
"""Test multiple lookups in same dictionary"""
haystack = {"first": "John", "last": "Doe", "email": "john@example.com"}
assert key_lookup(haystack, "first") == "John"
assert key_lookup(haystack, "last") == "Doe"
assert key_lookup(haystack, "email") == "john@example.com"
assert key_lookup(haystack, "phone") == ""
def test_numeric_string_values(self):
"""Test lookup with numeric string values"""
haystack = {"count": "42", "price": "19.99"}
assert key_lookup(haystack, "count") == "42"
assert key_lookup(haystack, "price") == "19.99"
def test_empty_string_value(self):
"""Test lookup when value is empty string"""
haystack = {"name": "", "city": "New York"}
result = key_lookup(haystack, "name")
assert result == ""
def test_whitespace_value(self):
"""Test lookup when value contains whitespace"""
haystack = {"name": " Alice ", "message": " "}
assert key_lookup(haystack, "name") == " Alice "
assert key_lookup(haystack, "message") == " "
@pytest.mark.parametrize("key,expected", [
("a", "1"),
("b", "2"),
("c", "3"),
("d", "")
])
def test_parametrized_lookup(self, key: str, expected: str):
"""Parametrized test for key lookup"""
haystack = {"a": "1", "b": "2", "c": "3"}
result = key_lookup(haystack, key)
assert result == expected
class TestValueLookup:
"""Tests for value_lookup function"""
def test_value_exists_single(self):
"""Test lookup when value exists once"""
haystack = {"name": "Alice", "username": "alice123", "email": "alice@example.com"}
result = value_lookup(haystack, "Alice")
assert result == "name"
def test_value_not_exists(self):
"""Test lookup when value doesn't exist returns empty string"""
haystack = {"name": "Alice", "username": "alice123"}
result = value_lookup(haystack, "Bob")
assert result == ""
def test_value_exists_multiple_no_raise(self):
"""Test lookup when value exists multiple times, returns first"""
haystack = {"key1": "duplicate", "key2": "unique", "key3": "duplicate"}
result = value_lookup(haystack, "duplicate")
assert result in ["key1", "key3"] # Order may vary in dict
def test_value_exists_multiple_raise_on_many_false(self):
"""Test lookup with multiple matches and raise_on_many=False"""
haystack = {"a": "same", "b": "same", "c": "different"}
result = value_lookup(haystack, "same", raise_on_many=False)
assert result in ["a", "b"]
def test_value_exists_multiple_raise_on_many_true(self):
"""Test lookup with multiple matches and raise_on_many=True raises ValueError"""
haystack = {"a": "same", "b": "same", "c": "different"}
with pytest.raises(ValueError, match="More than one element found with the same name"):
value_lookup(haystack, "same", raise_on_many=True)
def test_value_exists_single_raise_on_many_true(self):
"""Test lookup with single match and raise_on_many=True works fine"""
haystack = {"name": "Alice", "username": "alice123"}
result = value_lookup(haystack, "Alice", raise_on_many=True)
assert result == "name"
def test_empty_dict(self):
"""Test lookup in empty dictionary"""
haystack: dict[str, str] = {}
result = value_lookup(haystack, "Alice")
assert result == ""
def test_empty_dict_raise_on_many(self):
"""Test lookup in empty dictionary with raise_on_many=True"""
haystack: dict[str, str] = {}
result = value_lookup(haystack, "Alice", raise_on_many=True)
assert result == ""
def test_numeric_string_values(self):
"""Test lookup with numeric string values"""
haystack = {"id": "123", "count": "456", "score": "123"}
result = value_lookup(haystack, "456")
assert result == "count"
def test_empty_string_value(self):
"""Test lookup for empty string value"""
haystack = {"name": "", "city": "New York", "country": ""}
result = value_lookup(haystack, "")
assert result in ["name", "country"]
def test_whitespace_value(self):
"""Test lookup for whitespace value"""
haystack = {"a": " spaces ", "b": "normal", "c": " spaces "}
result = value_lookup(haystack, " spaces ")
assert result in ["a", "c"]
def test_case_sensitive_lookup(self):
"""Test that lookup is case-sensitive"""
haystack = {"name": "Alice", "username": "alice", "email": "ALICE"}
assert value_lookup(haystack, "Alice") == "name"
assert value_lookup(haystack, "alice") == "username"
assert value_lookup(haystack, "ALICE") == "email"
assert value_lookup(haystack, "aLiCe") == ""
def test_special_characters(self):
"""Test lookup with special characters"""
haystack = {"key1": "test@example.com", "key2": "test#value", "key3": "test@example.com"}
result = value_lookup(haystack, "test@example.com")
assert result in ["key1", "key3"]
@pytest.mark.parametrize("value,expected_key", [
("value1", "a"),
("value2", "b"),
("value3", "c"),
("nonexistent", "")
])
def test_parametrized_lookup(self, value: str, expected_key: str):
"""Parametrized test for value lookup"""
haystack = {"a": "value1", "b": "value2", "c": "value3"}
result = value_lookup(haystack, value)
assert result == expected_key
def test_duplicate_values_consistent_return(self):
"""Test that lookup with duplicates consistently returns one of the keys"""
haystack = {"x": "dup", "y": "dup", "z": "dup"}
# Should return same key consistently
result1 = value_lookup(haystack, "dup")
result2 = value_lookup(haystack, "dup")
result3 = value_lookup(haystack, "dup")
assert result1 == result2 == result3
assert result1 in ["x", "y", "z"]

View File

@@ -1,652 +0,0 @@
"""
iterator_handling.dict_helper tests
"""
# pylint: disable=use-implicit-booleaness-not-comparison
from typing import Any
import pytest
from corelibs.iterator_handling.dict_helpers import (
delete_keys_from_set,
build_dict,
set_entry,
)
class TestDeleteKeysFromSet:
"""Test cases for delete_keys_from_set function"""
def test_delete_single_key_from_dict(self):
"""Test deleting a single key from a dictionary"""
set_data = {"a": 1, "b": 2, "c": 3}
keys = ["b"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "c": 3}
assert "b" not in result
def test_delete_multiple_keys_from_dict(self):
"""Test deleting multiple keys from a dictionary"""
set_data = {"a": 1, "b": 2, "c": 3, "d": 4}
keys = ["b", "d"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "c": 3}
assert "b" not in result
assert "d" not in result
def test_delete_all_keys_from_dict(self):
"""Test deleting all keys from a dictionary"""
set_data = {"a": 1, "b": 2}
keys = ["a", "b"]
result = delete_keys_from_set(set_data, keys)
assert result == {}
def test_delete_nonexistent_key(self):
"""Test deleting a key that doesn't exist"""
set_data = {"a": 1, "b": 2}
keys = ["c", "d"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "b": 2}
def test_delete_keys_from_nested_dict(self):
"""Test deleting keys from nested dictionaries"""
set_data = {
"a": 1,
"b": {"c": 2, "d": 3, "e": 4},
"f": 5
}
keys = ["d", "f"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "b": {"c": 2, "e": 4}}
assert "d" not in result["b"] # type: ignore
assert "f" not in result
def test_delete_keys_from_deeply_nested_dict(self):
"""Test deleting keys from deeply nested structures"""
set_data = {
"a": 1,
"b": {
"c": 2,
"d": {
"e": 3,
"f": 4
}
},
"g": 5
}
keys = ["f", "g"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "b": {"c": 2, "d": {"e": 3}}}
assert "g" not in result
def test_delete_keys_from_list(self):
"""Test with list containing dictionaries"""
set_data = [
{"a": 1, "b": 2},
{"c": 3, "d": 4},
{"e": 5, "f": 6}
]
keys = ["b", "d", "f"]
result = delete_keys_from_set(set_data, keys)
assert result == [
{"a": 1},
{"c": 3},
{"e": 5}
]
def test_delete_keys_from_list_with_nested_dicts(self):
"""Test with list containing nested dictionaries"""
set_data = [
{"a": 1, "b": {"c": 2, "d": 3}},
{"e": 4, "f": {"g": 5, "h": 6}}
]
keys = ["d", "h"]
result = delete_keys_from_set(set_data, keys)
assert result == [
{"a": 1, "b": {"c": 2}},
{"e": 4, "f": {"g": 5}}
]
def test_delete_keys_from_dict_with_list_values(self):
"""Test with dictionary containing list values"""
set_data = {
"a": [{"b": 1, "c": 2}, {"d": 3, "e": 4}],
"f": 5
}
keys = ["c", "e"]
result = delete_keys_from_set(set_data, keys)
assert result == {
"a": [{"b": 1}, {"d": 3}],
"f": 5
}
def test_empty_keys_list(self):
"""Test with empty keys list - should return data unchanged"""
set_data = {"a": 1, "b": 2, "c": 3}
keys: list[str] = []
result = delete_keys_from_set(set_data, keys)
assert result == set_data
def test_empty_dict(self):
"""Test with empty dictionary"""
set_data: dict[str, Any] = {}
keys = ["a", "b"]
result = delete_keys_from_set(set_data, keys)
assert result == {}
def test_empty_list(self):
"""Test with empty list"""
set_data: list[Any] = []
keys = ["a", "b"]
result = delete_keys_from_set(set_data, keys)
assert result == []
def test_string_input(self):
"""Test with string input - should convert to list"""
set_data = "hello"
keys = ["a"]
result = delete_keys_from_set(set_data, keys)
assert result == ["hello"]
def test_complex_mixed_structure(self):
"""Test with complex mixed structure"""
set_data = {
"users": [
{
"name": "Alice",
"age": 30,
"password": "secret1",
"profile": {
"email": "alice@example.com",
"password": "secret2"
}
},
{
"name": "Bob",
"age": 25,
"password": "secret3",
"profile": {
"email": "bob@example.com",
"password": "secret4"
}
}
],
"metadata": {
"count": 2,
"password": "admin"
}
}
keys = ["password"]
result = delete_keys_from_set(set_data, keys)
# Check that all password fields are removed
assert "password" not in result["metadata"] # type: ignore
for user in result["users"]: # type: ignore
assert "password" not in user
assert "password" not in user["profile"]
# Check that other fields remain
assert result["users"][0]["name"] == "Alice" # type: ignore
assert result["users"][1]["name"] == "Bob" # type: ignore
assert result["metadata"]["count"] == 2 # type: ignore
def test_dict_with_none_values(self):
"""Test with dictionary containing None values"""
set_data = {"a": 1, "b": None, "c": 3}
keys = ["b"]
result = delete_keys_from_set(set_data, keys)
assert result == {"a": 1, "c": 3}
def test_dict_with_various_value_types(self):
"""Test with dictionary containing various value types"""
set_data = {
"int": 42,
"float": 3.14,
"bool": True,
"str": "hello",
"list": [1, 2, 3],
"dict": {"nested": "value"},
"none": None
}
keys = ["bool", "none"]
result = delete_keys_from_set(set_data, keys)
assert "bool" not in result
assert "none" not in result
assert len(result) == 5
class TestBuildDict:
"""Test cases for build_dict function"""
def test_build_dict_without_ignore_entries(self):
"""Test build_dict without ignore_entries (None)"""
input_dict = {"a": 1, "b": 2, "c": 3}
result = build_dict(input_dict)
assert result == input_dict
assert result is input_dict # Should return same object
def test_build_dict_with_ignore_entries_single(self):
"""Test build_dict with single ignore entry"""
input_dict = {"a": 1, "b": 2, "c": 3}
ignore = ["b"]
result = build_dict(input_dict, ignore)
assert result == {"a": 1, "c": 3}
assert "b" not in result
def test_build_dict_with_ignore_entries_multiple(self):
"""Test build_dict with multiple ignore entries"""
input_dict = {"a": 1, "b": 2, "c": 3, "d": 4}
ignore = ["b", "d"]
result = build_dict(input_dict, ignore)
assert result == {"a": 1, "c": 3}
def test_build_dict_with_nested_ignore(self):
"""Test build_dict with nested structures"""
input_dict = {
"a": 1,
"b": {"c": 2, "d": 3},
"e": 4
}
ignore = ["d", "e"]
result = build_dict(input_dict, ignore)
assert result == {"a": 1, "b": {"c": 2}}
assert "e" not in result
assert "d" not in result["b"] # type: ignore
def test_build_dict_with_empty_ignore_list(self):
"""Test build_dict with empty ignore list"""
input_dict = {"a": 1, "b": 2}
ignore: list[str] = []
result = build_dict(input_dict, ignore)
assert result == input_dict
def test_build_dict_with_nonexistent_ignore_keys(self):
"""Test build_dict with keys that don't exist"""
input_dict = {"a": 1, "b": 2}
ignore = ["c", "d"]
result = build_dict(input_dict, ignore)
assert result == {"a": 1, "b": 2}
def test_build_dict_ignore_all_keys(self):
"""Test build_dict ignoring all keys"""
input_dict = {"a": 1, "b": 2}
ignore = ["a", "b"]
result = build_dict(input_dict, ignore)
assert result == {}
def test_build_dict_with_complex_structure(self):
"""Test build_dict with complex nested structure"""
input_dict = {
"ResponseMetadata": {
"RequestId": "12345",
"HTTPStatusCode": 200,
"RetryAttempts": 0
},
"data": {
"id": 1,
"name": "Test",
"ResponseMetadata": {"internal": "value"}
},
"status": "success"
}
ignore = ["ResponseMetadata", "RetryAttempts"]
result = build_dict(input_dict, ignore)
# ResponseMetadata should be removed at all levels
assert "ResponseMetadata" not in result
assert "ResponseMetadata" not in result["data"] # type: ignore
assert result["data"]["name"] == "Test" # type: ignore
assert result["status"] == "success" # type: ignore
def test_build_dict_with_list_values(self):
"""Test build_dict with lists containing dictionaries"""
input_dict = {
"items": [
{"id": 1, "temp": "remove"},
{"id": 2, "temp": "remove"}
],
"temp": "also_remove"
}
ignore = ["temp"]
result = build_dict(input_dict, ignore)
assert "temp" not in result
assert "temp" not in result["items"][0] # type: ignore
assert "temp" not in result["items"][1] # type: ignore
assert result["items"][0]["id"] == 1 # type: ignore
assert result["items"][1]["id"] == 2 # type: ignore
def test_build_dict_empty_input(self):
"""Test build_dict with empty dictionary"""
input_dict: dict[str, Any] = {}
result = build_dict(input_dict, ["a", "b"])
assert result == {}
def test_build_dict_preserves_type_annotation(self):
"""Test that build_dict preserves proper type"""
input_dict = {"a": 1, "b": [1, 2, 3], "c": {"nested": "value"}}
result = build_dict(input_dict)
assert isinstance(result, dict)
assert isinstance(result["b"], list)
assert isinstance(result["c"], dict)
class TestSetEntry:
"""Test cases for set_entry function"""
def test_set_entry_new_key(self):
"""Test setting a new key in dictionary"""
dict_set: dict[str, Any] = {}
key = "new_key"
value = "new_value"
result = set_entry(dict_set, key, value)
assert result[key] == value
assert len(result) == 1
def test_set_entry_existing_key(self):
"""Test overwriting an existing key"""
dict_set = {"key": "old_value"}
key = "key"
value = "new_value"
result = set_entry(dict_set, key, value)
assert result[key] == value
assert result[key] != "old_value"
def test_set_entry_with_dict_value(self):
"""Test setting a dictionary as value"""
dict_set: dict[str, Any] = {}
key = "config"
value = {"setting1": True, "setting2": "value"}
result = set_entry(dict_set, key, value)
assert result[key] == value
assert isinstance(result[key], dict)
def test_set_entry_with_list_value(self):
"""Test setting a list as value"""
dict_set: dict[str, Any] = {}
key = "items"
value = [1, 2, 3, 4]
result = set_entry(dict_set, key, value)
assert result[key] == value
assert isinstance(result[key], list)
def test_set_entry_with_none_value(self):
"""Test setting None as value"""
dict_set: dict[str, Any] = {}
key = "nullable"
value = None
result = set_entry(dict_set, key, value)
assert result[key] is None
assert key in result
def test_set_entry_with_integer_value(self):
"""Test setting integer value"""
dict_set: dict[str, Any] = {}
key = "count"
value = 42
result = set_entry(dict_set, key, value)
assert result[key] == 42
assert isinstance(result[key], int)
def test_set_entry_with_float_value(self):
"""Test setting float value"""
dict_set: dict[str, Any] = {}
key = "price"
value = 19.99
result = set_entry(dict_set, key, value)
assert result[key] == 19.99
assert isinstance(result[key], float)
def test_set_entry_with_boolean_value(self):
"""Test setting boolean value"""
dict_set: dict[str, Any] = {}
key = "enabled"
value = True
result = set_entry(dict_set, key, value)
assert result[key] is True
assert isinstance(result[key], bool)
def test_set_entry_multiple_times(self):
"""Test setting multiple entries"""
dict_set: dict[str, Any] = {}
set_entry(dict_set, "key1", "value1")
set_entry(dict_set, "key2", "value2")
set_entry(dict_set, "key3", "value3")
assert len(dict_set) == 3
assert dict_set["key1"] == "value1"
assert dict_set["key2"] == "value2"
assert dict_set["key3"] == "value3"
def test_set_entry_overwrites_existing(self):
"""Test that setting an existing key overwrites it"""
dict_set = {"key": {"old": "data"}}
value = {"new": "data"}
result = set_entry(dict_set, "key", value)
assert result["key"] == {"new": "data"}
assert "old" not in result["key"]
def test_set_entry_modifies_original_dict(self):
"""Test that set_entry modifies the original dictionary"""
dict_set: dict[str, Any] = {}
result = set_entry(dict_set, "key", "value")
assert result is dict_set
assert dict_set["key"] == "value"
def test_set_entry_with_empty_string_value(self):
"""Test setting empty string as value"""
dict_set: dict[str, Any] = {}
key = "empty"
value = ""
result = set_entry(dict_set, key, value)
assert result[key] == ""
assert key in result
def test_set_entry_with_complex_nested_structure(self):
"""Test setting complex nested structure"""
dict_set: dict[str, Any] = {}
key = "complex"
value = {
"level1": {
"level2": {
"level3": ["a", "b", "c"]
}
}
}
result = set_entry(dict_set, key, value)
assert result[key]["level1"]["level2"]["level3"] == ["a", "b", "c"]
# Parametrized tests for more comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("set_data,keys,expected", [
({"a": 1, "b": 2}, ["b"], {"a": 1}),
({"a": 1, "b": 2, "c": 3}, ["a", "c"], {"b": 2}),
({"a": 1}, ["a"], {}),
({"a": 1, "b": 2}, ["c"], {"a": 1, "b": 2}),
({}, ["a"], {}),
({"a": {"b": 1, "c": 2}}, ["c"], {"a": {"b": 1}}),
])
def test_delete_keys_parametrized(
self,
set_data: dict[str, Any],
keys: list[str],
expected: dict[str, Any]
):
"""Test delete_keys_from_set with various inputs"""
result = delete_keys_from_set(set_data, keys)
assert result == expected
@pytest.mark.parametrize("input_dict,ignore,expected", [
({"a": 1, "b": 2}, ["b"], {"a": 1}),
({"a": 1, "b": 2}, ["c"], {"a": 1, "b": 2}),
({"a": 1, "b": 2}, [], {"a": 1, "b": 2}),
({"a": 1}, ["a"], {}),
({}, ["a"], {}),
])
def test_build_dict_parametrized(
self,
input_dict: dict[str, Any],
ignore: list[str],
expected: dict[str, Any]
):
"""Test build_dict with various inputs"""
result = build_dict(input_dict, ignore)
assert result == expected
@pytest.mark.parametrize("key,value", [
("string_key", "string_value"),
("int_key", 42),
("float_key", 3.14),
("bool_key", True),
("list_key", [1, 2, 3]),
("dict_key", {"nested": "value"}),
("none_key", None),
("empty_key", ""),
("zero_key", 0),
("false_key", False),
])
def test_set_entry_parametrized(self, key: str, value: Any):
"""Test set_entry with various value types"""
dict_set: dict[str, Any] = {}
result = set_entry(dict_set, key, value)
assert result[key] == value
# Edge cases and integration tests
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_delete_keys_preserves_modification(self):
"""Test that original dict is modified"""
set_data = {"a": 1, "b": 2, "c": 3}
keys = ["b"]
result = delete_keys_from_set(set_data, keys)
# The function modifies the original dict
assert result is set_data
assert "b" not in set_data
def test_build_dict_with_aws_typedef_scenario(self):
"""Test build_dict mimicking AWS TypedDict usage"""
# Simulating AWS response with ResponseMetadata
aws_response: dict[str, Any] = {
"Items": [
{"id": "1", "name": "Item1"},
{"id": "2", "name": "Item2"}
],
"Count": 2,
"ScannedCount": 2,
"ResponseMetadata": {
"RequestId": "abc123",
"HTTPStatusCode": 200,
"HTTPHeaders": {},
"RetryAttempts": 0
}
}
result = build_dict(aws_response, ["ResponseMetadata"])
assert "ResponseMetadata" not in result
assert result["Count"] == 2 # type: ignore
assert len(result["Items"]) == 2 # type: ignore
def test_set_entry_idempotency(self):
"""Test that calling set_entry multiple times with same value is idempotent"""
dict_set: dict[str, Any] = {}
value = "test_value"
result1 = set_entry(dict_set, "key", value)
result2 = set_entry(dict_set, "key", value)
result3 = set_entry(dict_set, "key", value)
assert result1 is result2 is result3
assert result1["key"] == value
assert len(result1) == 1
def test_delete_keys_with_circular_reference_protection(self):
"""Test that function handles normal cases without circular issues"""
# Python dicts can't have true circular references easily
# but we can test deep nesting
set_data = {
"level1": {
"level2": {
"level3": {
"level4": {
"data": "value",
"remove": "this"
}
}
}
}
}
keys = ["remove"]
result = delete_keys_from_set(set_data, keys)
assert "remove" not in result["level1"]["level2"]["level3"]["level4"] # type: ignore
assert result["level1"]["level2"]["level3"]["level4"]["data"] == "value" # type: ignore
def test_build_dict_none_ignore_vs_empty_ignore(self):
"""Test difference between None and empty list for ignore_entries"""
input_dict = {"a": 1, "b": 2}
result_none = build_dict(input_dict, None)
result_empty = build_dict(input_dict, [])
assert result_none == input_dict
assert result_empty == input_dict
# With None, it returns the same object
assert result_none is input_dict
# With empty list, it goes through delete_keys_from_set
assert result_empty is input_dict
# Integration tests
class TestIntegration:
"""Integration tests combining multiple functions"""
def test_build_dict_then_set_entry(self):
"""Test using build_dict followed by set_entry"""
original = {
"a": 1,
"b": 2,
"remove_me": "gone"
}
cleaned = build_dict(original, ["remove_me"])
result = set_entry(cleaned, "c", 3)
assert result == {"a": 1, "b": 2, "c": 3}
assert "remove_me" not in result
def test_delete_keys_then_set_entry(self):
"""Test using delete_keys_from_set followed by set_entry"""
data = {"a": 1, "b": 2, "c": 3}
cleaned = delete_keys_from_set(data, ["b"])
result = set_entry(cleaned, "d", 4) # type: ignore
assert result == {"a": 1, "c": 3, "d": 4}
def test_multiple_operations_chain(self):
"""Test chaining multiple operations"""
data = {
"user": {
"name": "Alice",
"password": "secret",
"email": "alice@example.com"
},
"metadata": {
"created": "2024-01-01",
"password": "admin"
}
}
# Remove passwords
cleaned = build_dict(data, ["password"])
# Add new field
result = set_entry(cleaned, "processed", True)
assert "password" not in result["user"] # type: ignore
assert "password" not in result["metadata"] # type: ignore
assert result["processed"] is True # type: ignore
assert result["user"]["name"] == "Alice" # type: ignore
# __END__

View File

@@ -1,291 +0,0 @@
"""
tests for corelibs.iterator_handling.dict_helpers
"""
from typing import Any
import pytest
from corelibs.iterator_handling.dict_mask import mask
def test_mask_default_behavior():
"""Test masking with default mask_keys"""
data = {
"username": "john_doe",
"password": "secret123",
"email": "john@example.com",
"api_secret": "abc123",
"encryption_key": "xyz789"
}
result = mask(data)
assert result["username"] == "john_doe"
assert result["password"] == "***"
assert result["email"] == "john@example.com"
assert result["api_secret"] == "***"
assert result["encryption_key"] == "***"
def test_mask_custom_keys():
"""Test masking with custom mask_keys"""
data = {
"username": "john_doe",
"token": "abc123",
"api_key": "xyz789",
"password": "secret123"
}
result = mask(data, mask_keys=["token", "api"])
assert result["username"] == "john_doe"
assert result["token"] == "***"
assert result["api_key"] == "***"
assert result["password"] == "secret123" # Not masked with custom keys
def test_mask_custom_mask_string():
"""Test masking with custom mask string"""
data = {"password": "secret123"}
result = mask(data, mask_str="[HIDDEN]")
assert result["password"] == "[HIDDEN]"
def test_mask_case_insensitive():
"""Test that masking is case insensitive"""
data = {
"PASSWORD": "secret123",
"Secret_Key": "abc123",
"ENCRYPTION_data": "xyz789"
}
result = mask(data)
assert result["PASSWORD"] == "***"
assert result["Secret_Key"] == "***"
assert result["ENCRYPTION_data"] == "***"
def test_mask_key_patterns():
"""Test different key matching patterns (start, end, contains)"""
data = {
"password_hash": "hash123", # starts with
"user_password": "secret123", # ends with
"my_secret_key": "abc123", # contains with edges
"secretvalue": "xyz789", # contains without edges
"startsecretvalue": "xyz123", # contains without edges
"normal_key": "normal_value"
}
result = mask(data)
assert result["password_hash"] == "***"
assert result["user_password"] == "***"
assert result["my_secret_key"] == "***"
assert result["secretvalue"] == "***" # will mask beacuse starts with
assert result["startsecretvalue"] == "xyz123" # will not mask
assert result["normal_key"] == "normal_value"
def test_mask_custom_edges():
"""Test masking with custom edge characters"""
data = {
"my-secret-key": "abc123",
"my_secret_key": "xyz789"
}
result = mask(data, mask_str_edges="-")
assert result["my-secret-key"] == "***"
assert result["my_secret_key"] == "xyz789" # Underscore edges don't match
def test_mask_empty_edges():
"""Test masking with empty edge characters (substring matching)"""
data = {
"secretvalue": "abc123",
"mysecretkey": "xyz789",
"normal_key": "normal_value"
}
result = mask(data, mask_str_edges="")
assert result["secretvalue"] == "***"
assert result["mysecretkey"] == "***"
assert result["normal_key"] == "normal_value"
def test_mask_nested_dict():
"""Test masking nested dictionaries"""
data = {
"user": {
"name": "john",
"password": "secret123",
"profile": {
"email": "john@example.com",
"encryption_key": "abc123"
}
},
"api_secret": "xyz789"
}
result = mask(data)
assert result["user"]["name"] == "john"
assert result["user"]["password"] == "***"
assert result["user"]["profile"]["email"] == "john@example.com"
assert result["user"]["profile"]["encryption_key"] == "***"
assert result["api_secret"] == "***"
def test_mask_lists():
"""Test masking lists and nested structures with lists"""
data = {
"users": [
{"name": "john", "password": "secret1"},
{"name": "jane", "password": "secret2"}
],
"secrets": ["secret1", "secret2", "secret3"]
}
result = mask(data)
print(f"R {result['secrets']}")
assert result["users"][0]["name"] == "john"
assert result["users"][0]["password"] == "***"
assert result["users"][1]["name"] == "jane"
assert result["users"][1]["password"] == "***"
assert result["secrets"] == ["***", "***", "***"]
def test_mask_mixed_types():
"""Test masking with different value types"""
data = {
"password": "string_value",
"secret_number": 12345,
"encryption_flag": True,
"secret_float": 3.14,
"password_none": None,
"normal_key": "normal_value"
}
result = mask(data)
assert result["password"] == "***"
assert result["secret_number"] == "***"
assert result["encryption_flag"] == "***"
assert result["secret_float"] == "***"
assert result["password_none"] == "***"
assert result["normal_key"] == "normal_value"
def test_mask_skip_true():
"""Test that skip=True returns original data unchanged"""
data = {
"password": "secret123",
"encryption_key": "abc123",
"normal_key": "normal_value"
}
result = mask(data, skip=True)
assert result == data
assert result is data # Should return the same object
def test_mask_empty_dict():
"""Test masking empty dictionary"""
data: dict[str, Any] = {}
result = mask(data)
assert result == {}
def test_mask_none_mask_keys():
"""Test explicit None mask_keys uses defaults"""
data = {"password": "secret123", "token": "abc123"}
result = mask(data, mask_keys=None)
assert result["password"] == "***"
assert result["token"] == "abc123" # Not in default keys
def test_mask_empty_mask_keys():
"""Test empty mask_keys list"""
data = {"password": "secret123", "secret": "abc123"}
result = mask(data, mask_keys=[])
assert result["password"] == "secret123"
assert result["secret"] == "abc123"
def test_mask_complex_nested_structure():
"""Test masking complex nested structure"""
data = {
"config": {
"database": {
"host": "localhost",
"password": "db_secret",
"users": [
{"name": "admin", "password": "admin123"},
{"name": "user", "secret_key": "user456"}
]
},
"api": {
"endpoints": ["api1", "api2"],
"encryption_settings": {
"enabled": True,
"secret": "api_secret"
}
}
}
}
result = mask(data)
assert result["config"]["database"]["host"] == "localhost"
assert result["config"]["database"]["password"] == "***"
assert result["config"]["database"]["users"][0]["name"] == "admin"
assert result["config"]["database"]["users"][0]["password"] == "***"
assert result["config"]["database"]["users"][1]["name"] == "user"
assert result["config"]["database"]["users"][1]["secret_key"] == "***"
assert result["config"]["api"]["endpoints"] == ["api1", "api2"]
assert result["config"]["api"]["encryption_settings"]["enabled"] is True
assert result["config"]["api"]["encryption_settings"]["secret"] == "***"
def test_mask_preserves_original_data():
"""Test that original data is not modified"""
original_data = {
"password": "secret123",
"username": "john_doe"
}
data_copy = original_data.copy()
result = mask(original_data)
assert original_data == data_copy # Original unchanged
assert result != original_data # Result is different
assert result["password"] == "***"
assert original_data["password"] == "secret123"
@pytest.mark.parametrize("mask_key,expected_keys", [
(["pass"], ["password", "user_pass", "my_pass_key"]),
(["key"], ["api_key", "secret_key", "my_key_value"]),
(["token"], ["token", "auth_token", "my_token_here"]),
])
def test_mask_parametrized_keys(mask_key: list[str], expected_keys: list[str]):
"""Parametrized test for different mask key patterns"""
data = {key: "value" for key in expected_keys}
data["normal_entry"] = "normal_value"
result = mask(data, mask_keys=mask_key)
for key in expected_keys:
assert result[key] == "***"
assert result["normal_entry"] == "normal_value"

View File

@@ -1,361 +0,0 @@
"""
tests for corelibs.iterator_handling.fingerprint
"""
from typing import Any
import pytest
from corelibs.iterator_handling.fingerprint import dict_hash_frozen, dict_hash_crc
class TestDictHashFrozen:
"""Tests for dict_hash_frozen function"""
def test_dict_hash_frozen_simple_dict(self):
"""Test hashing a simple dictionary"""
data = {"key1": "value1", "key2": "value2"}
result = dict_hash_frozen(data)
assert isinstance(result, int)
assert result != 0
def test_dict_hash_frozen_consistency(self):
"""Test that same dict produces same hash"""
data = {"name": "John", "age": 30, "city": "Tokyo"}
hash1 = dict_hash_frozen(data)
hash2 = dict_hash_frozen(data)
assert hash1 == hash2
def test_dict_hash_frozen_order_independence(self):
"""Test that dict order doesn't affect hash"""
data1 = {"a": 1, "b": 2, "c": 3}
data2 = {"c": 3, "a": 1, "b": 2}
hash1 = dict_hash_frozen(data1)
hash2 = dict_hash_frozen(data2)
assert hash1 == hash2
def test_dict_hash_frozen_empty_dict(self):
"""Test hashing an empty dictionary"""
data: dict[Any, Any] = {}
result = dict_hash_frozen(data)
assert isinstance(result, int)
def test_dict_hash_frozen_different_dicts(self):
"""Test that different dicts produce different hashes"""
data1 = {"key1": "value1"}
data2 = {"key2": "value2"}
hash1 = dict_hash_frozen(data1)
hash2 = dict_hash_frozen(data2)
assert hash1 != hash2
def test_dict_hash_frozen_various_types(self):
"""Test hashing dict with various value types"""
data = {
"string": "value",
"int": 42,
"float": 3.14,
"bool": True,
"none": None
}
result = dict_hash_frozen(data)
assert isinstance(result, int)
def test_dict_hash_frozen_numeric_keys(self):
"""Test hashing dict with numeric keys"""
data = {1: "one", 2: "two", 3: "three"}
result = dict_hash_frozen(data)
assert isinstance(result, int)
def test_dict_hash_frozen_tuple_values(self):
"""Test hashing dict with tuple values"""
data = {"coord1": (1, 2), "coord2": (3, 4)}
result = dict_hash_frozen(data)
assert isinstance(result, int)
def test_dict_hash_frozen_value_change_changes_hash(self):
"""Test that changing a value changes the hash"""
data1 = {"key": "value1"}
data2 = {"key": "value2"}
hash1 = dict_hash_frozen(data1)
hash2 = dict_hash_frozen(data2)
assert hash1 != hash2
class TestDictHashCrc:
"""Tests for dict_hash_crc function"""
def test_dict_hash_crc_simple_dict(self):
"""Test hashing a simple dictionary"""
data = {"key1": "value1", "key2": "value2"}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64 # SHA256 produces 64 hex characters
def test_dict_hash_crc_simple_list(self):
"""Test hashing a simple list"""
data = ["item1", "item2", "item3"]
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_consistency_dict(self):
"""Test that same dict produces same hash"""
data = {"name": "John", "age": 30, "city": "Tokyo"}
hash1 = dict_hash_crc(data)
hash2 = dict_hash_crc(data)
assert hash1 == hash2
def test_dict_hash_crc_consistency_list(self):
"""Test that same list produces same hash"""
data = [1, 2, 3, 4, 5]
hash1 = dict_hash_crc(data)
hash2 = dict_hash_crc(data)
assert hash1 == hash2
def test_dict_hash_crc_order_independence_dict(self):
"""Test that dict order doesn't affect hash (sort_keys=True)"""
data1 = {"a": 1, "b": 2, "c": 3}
data2 = {"c": 3, "a": 1, "b": 2}
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 == hash2
def test_dict_hash_crc_order_dependence_list(self):
"""Test that list order affects hash"""
data1 = [1, 2, 3]
data2 = [3, 2, 1]
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 != hash2
def test_dict_hash_crc_empty_dict(self):
"""Test hashing an empty dictionary"""
data: dict[Any, Any] = {}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_empty_list(self):
"""Test hashing an empty list"""
data: list[Any] = []
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_different_dicts(self):
"""Test that different dicts produce different hashes"""
data1 = {"key1": "value1"}
data2 = {"key2": "value2"}
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 != hash2
def test_dict_hash_crc_different_lists(self):
"""Test that different lists produce different hashes"""
data1 = ["item1", "item2"]
data2 = ["item3", "item4"]
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 != hash2
def test_dict_hash_crc_nested_dict(self):
"""Test hashing nested dictionaries"""
data = {
"user": {
"name": "John",
"address": {
"city": "Tokyo",
"country": "Japan"
}
}
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_nested_list(self):
"""Test hashing nested lists"""
data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_mixed_nested(self):
"""Test hashing mixed nested structures"""
data = {
"items": [1, 2, 3],
"meta": {
"count": 3,
"tags": ["a", "b", "c"]
}
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_various_types_dict(self):
"""Test hashing dict with various value types"""
data = {
"string": "value",
"int": 42,
"float": 3.14,
"bool": True,
"none": None,
"list": [1, 2, 3],
"nested_dict": {"inner": "value"}
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_various_types_list(self):
"""Test hashing list with various value types"""
data = ["string", 42, 3.14, True, None, [1, 2], {"key": "value"}]
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_value_change_changes_hash(self):
"""Test that changing a value changes the hash"""
data1 = {"key": "value1"}
data2 = {"key": "value2"}
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 != hash2
def test_dict_hash_crc_hex_format(self):
"""Test that hash is in hexadecimal format"""
data = {"test": "data"}
result = dict_hash_crc(data)
# All characters should be valid hex
assert all(c in "0123456789abcdef" for c in result)
def test_dict_hash_crc_unicode_handling(self):
"""Test hashing dict with unicode characters"""
data = {
"japanese": "日本語",
"emoji": "🎉",
"chinese": "中文"
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_special_characters(self):
"""Test hashing dict with special characters"""
data = {
"quotes": "\"quoted\"",
"newline": "line1\nline2",
"tab": "col1\tcol2",
"backslash": "path\\to\\file"
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert len(result) == 64
class TestComparisonBetweenHashFunctions:
"""Tests comparing dict_hash_frozen and dict_hash_crc"""
def test_both_functions_are_deterministic(self):
"""Test that both functions produce consistent results"""
data = {"a": 1, "b": 2, "c": 3}
frozen_hash1 = dict_hash_frozen(data)
frozen_hash2 = dict_hash_frozen(data)
crc_hash1 = dict_hash_crc(data)
crc_hash2 = dict_hash_crc(data)
assert frozen_hash1 == frozen_hash2
assert crc_hash1 == crc_hash2
def test_both_functions_handle_empty_dict(self):
"""Test that both functions can hash empty dict"""
data: dict[Any, Any] = {}
frozen_result = dict_hash_frozen(data)
crc_result = dict_hash_crc(data)
assert isinstance(frozen_result, int)
assert isinstance(crc_result, str)
def test_both_functions_detect_changes(self):
"""Test that both functions detect value changes"""
data1 = {"key": "value1"}
data2 = {"key": "value2"}
frozen_hash1 = dict_hash_frozen(data1)
frozen_hash2 = dict_hash_frozen(data2)
crc_hash1 = dict_hash_crc(data1)
crc_hash2 = dict_hash_crc(data2)
assert frozen_hash1 != frozen_hash2
assert crc_hash1 != crc_hash2
def test_both_functions_handle_order_independence(self):
"""Test that both functions are order-independent for dicts"""
data1 = {"x": 10, "y": 20, "z": 30}
data2 = {"z": 30, "x": 10, "y": 20}
frozen_hash1 = dict_hash_frozen(data1)
frozen_hash2 = dict_hash_frozen(data2)
crc_hash1 = dict_hash_crc(data1)
crc_hash2 = dict_hash_crc(data2)
assert frozen_hash1 == frozen_hash2
assert crc_hash1 == crc_hash2
@pytest.mark.parametrize("data,expected_type,expected_length", [
({"key": "value"}, str, 64),
([1, 2, 3], str, 64),
({"nested": {"key": "value"}}, str, 64),
([[1, 2], [3, 4]], str, 64),
({}, str, 64),
([], str, 64),
])
def test_dict_hash_crc_parametrized(data: dict[Any, Any] | list[Any], expected_type: type, expected_length: int):
"""Parametrized test for dict_hash_crc with various inputs"""
result = dict_hash_crc(data)
assert isinstance(result, expected_type)
assert len(result) == expected_length
@pytest.mark.parametrize("data", [
{"key": "value"},
{"a": 1, "b": 2},
{"x": 10, "y": 20, "z": 30},
{},
])
def test_dict_hash_frozen_parametrized(data: dict[Any, Any]):
"""Parametrized test for dict_hash_frozen with various inputs"""
result = dict_hash_frozen(data)
assert isinstance(result, int)

View File

@@ -1,522 +0,0 @@
"""
iterator_handling.list_helepr tests
"""
from typing import Any
import pytest
from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list, make_unique_list_of_dicts
class TestConvertToList:
"""Test cases for convert_to_list function"""
def test_string_input(self):
"""Test with string inputs"""
assert convert_to_list("hello") == ["hello"]
assert convert_to_list("") == [""]
assert convert_to_list("123") == ["123"]
assert convert_to_list("true") == ["true"]
def test_integer_input(self):
"""Test with integer inputs"""
assert convert_to_list(42) == [42]
assert convert_to_list(0) == [0]
assert convert_to_list(-10) == [-10]
assert convert_to_list(999999) == [999999]
def test_float_input(self):
"""Test with float inputs"""
assert convert_to_list(3.14) == [3.14]
assert convert_to_list(0.0) == [0.0]
assert convert_to_list(-2.5) == [-2.5]
assert convert_to_list(1.0) == [1.0]
def test_boolean_input(self):
"""Test with boolean inputs"""
assert convert_to_list(True) == [True]
assert convert_to_list(False) == [False]
def test_list_input_unchanged(self):
"""Test that list inputs are returned unchanged"""
# String lists
str_list = ["a", "b", "c"]
assert convert_to_list(str_list) == str_list
assert convert_to_list(str_list) is str_list # Same object reference
# Integer lists
int_list = [1, 2, 3]
assert convert_to_list(int_list) == int_list
assert convert_to_list(int_list) is int_list
# Float lists
float_list = [1.1, 2.2, 3.3]
assert convert_to_list(float_list) == float_list
assert convert_to_list(float_list) is float_list
# Boolean lists
bool_list = [True, False, True]
assert convert_to_list(bool_list) == bool_list
assert convert_to_list(bool_list) is bool_list
# Mixed lists
mixed_list = [1, "hello", 3.14, True]
assert convert_to_list(mixed_list) == mixed_list
assert convert_to_list(mixed_list) is mixed_list
# Empty list
empty_list: list[int] = []
assert convert_to_list(empty_list) == empty_list
assert convert_to_list(empty_list) is empty_list
def test_nested_lists(self):
"""Test with nested lists (should still return the same list)"""
nested_list: list[list[int]] = [[1, 2], [3, 4]]
assert convert_to_list(nested_list) == nested_list
assert convert_to_list(nested_list) is nested_list
def test_single_element_lists(self):
"""Test with single element lists"""
single_str = ["hello"]
assert convert_to_list(single_str) == single_str
assert convert_to_list(single_str) is single_str
single_int = [42]
assert convert_to_list(single_int) == single_int
assert convert_to_list(single_int) is single_int
class TestIsListInList:
"""Test cases for is_list_in_list function"""
def test_string_lists(self):
"""Test with string lists"""
list_a = ["a", "b", "c", "d"]
list_b = ["b", "d", "e"]
result = is_list_in_list(list_a, list_b)
assert set(result) == {"a", "c"}
assert isinstance(result, list)
def test_integer_lists(self):
"""Test with integer lists"""
list_a = [1, 2, 3, 4, 5]
list_b = [2, 4, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 3, 5}
assert isinstance(result, list)
def test_float_lists(self):
"""Test with float lists"""
list_a = [1.1, 2.2, 3.3, 4.4]
list_b = [2.2, 4.4, 5.5]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1.1, 3.3}
assert isinstance(result, list)
def test_boolean_lists(self):
"""Test with boolean lists"""
list_a = [True, False, True]
list_b = [True]
result = is_list_in_list(list_a, list_b)
assert set(result) == {False}
assert isinstance(result, list)
def test_mixed_type_lists(self):
"""Test with mixed type lists"""
list_a = [1, "hello", 3.14, True, "world"]
list_b = ["hello", True, 42]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 3.14, "world"}
assert isinstance(result, list)
def test_empty_lists(self):
"""Test with empty lists"""
# Empty list_a
assert is_list_in_list([], [1, 2, 3]) == []
# Empty list_b
list_a = [1, 2, 3]
result = is_list_in_list(list_a, [])
assert set(result) == {1, 2, 3}
# Both empty
assert is_list_in_list([], []) == []
def test_no_common_elements(self):
"""Test when lists have no common elements"""
list_a = [1, 2, 3]
list_b = [4, 5, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 2, 3}
def test_all_elements_common(self):
"""Test when all elements in list_a are in list_b"""
list_a = [1, 2, 3]
list_b = [1, 2, 3, 4, 5]
result = is_list_in_list(list_a, list_b)
assert result == []
def test_identical_lists(self):
"""Test with identical lists"""
list_a = [1, 2, 3]
list_b = [1, 2, 3]
result = is_list_in_list(list_a, list_b)
assert result == []
def test_duplicate_elements(self):
"""Test with duplicate elements in lists"""
list_a = [1, 2, 2, 3, 3, 3]
list_b = [2, 4]
result = is_list_in_list(list_a, list_b)
# Should return unique elements only (set behavior)
assert set(result) == {1, 3}
assert isinstance(result, list)
def test_list_b_larger_than_list_a(self):
"""Test when list_b is larger than list_a"""
list_a = [1, 2]
list_b = [2, 3, 4, 5, 6, 7, 8]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1}
def test_order_independence(self):
"""Test that order doesn't matter due to set operations"""
list_a = [3, 1, 4, 1, 5]
list_b = [1, 2, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {3, 4, 5}
# Parametrized tests for more comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("input_value,expected", [
("hello", ["hello"]),
(42, [42]),
(3.14, [3.14]),
(True, [True]),
(False, [False]),
("", [""]),
(0, [0]),
(0.0, [0.0]),
(-1, [-1]),
(-2.5, [-2.5]),
])
def test_convert_to_list_parametrized(self, input_value: Any, expected: Any):
"""Test convert_to_list with various single values"""
assert convert_to_list(input_value) == expected
@pytest.mark.parametrize("input_list", [
[1, 2, 3],
["a", "b", "c"],
[1.1, 2.2, 3.3],
[True, False],
[1, "hello", 3.14, True],
[],
[42],
[[1, 2], [3, 4]],
])
def test_convert_to_list_with_lists_parametrized(self, input_list: Any):
"""Test convert_to_list with various list inputs"""
result = convert_to_list(input_list)
assert result == input_list
assert result is input_list # Same object reference
@pytest.mark.parametrize("list_a,list_b,expected_set", [
([1, 2, 3], [2], {1, 3}),
(["a", "b", "c"], ["b", "d"], {"a", "c"}),
([1, 2, 3], [4, 5, 6], {1, 2, 3}),
([1, 2, 3], [1, 2, 3], set[int]()),
([], [1, 2, 3], set[int]()),
([1, 2, 3], [], {1, 2, 3}),
([True, False], [True], {False}),
([1.1, 2.2, 3.3], [2.2], {1.1, 3.3}),
])
def test_is_list_in_list_parametrized(self, list_a: list[Any], list_b: list[Any], expected_set: Any):
"""Test is_list_in_list with various input combinations"""
result = is_list_in_list(list_a, list_b)
assert set(result) == expected_set
assert isinstance(result, list)
# Edge cases and special scenarios
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_convert_to_list_with_none_like_values(self):
"""Test convert_to_list with None-like values (if function supports them)"""
# Note: Based on type hints, None is not supported, but testing behavior
# This test might need to be adjusted based on actual function behavior
# pass
def test_is_list_in_list_preserves_type_distinctions(self):
"""Test that different types are treated as different"""
list_a = [1, "1", 1.0, True]
list_b = [1] # Only integer 1
result = is_list_in_list(list_a, list_b)
# Note: This test depends on how Python's set handles type equality
# 1, 1.0, and True are considered equal in sets
# "1" is different from 1
# expected_items = {"1"} # String "1" should remain
assert "1" in result
assert isinstance(result, list)
def test_large_lists(self):
"""Test with large lists"""
large_list_a = list(range(1000))
large_list_b = list(range(500, 1500))
result = is_list_in_list(large_list_a, large_list_b)
expected = list(range(500)) # 0 to 499
assert set(result) == set(expected)
def test_memory_efficiency(self):
"""Test that convert_to_list doesn't create unnecessary copies"""
original_list = [1, 2, 3, 4, 5]
result = convert_to_list(original_list)
# Should be the same object, not a copy
assert result is original_list
# Modifying the original should affect the result
original_list.append(6)
assert 6 in result
# Performance tests (optional)
class TestPerformance:
"""Performance-related tests"""
def test_is_list_in_list_with_duplicates_performance(self):
"""Test that function handles duplicates efficiently"""
# List with many duplicates
list_a = [1, 2, 3] * 100 # 300 elements, many duplicates
list_b = [2] * 50 # 50 elements, all the same
result = is_list_in_list(list_a, list_b)
# Should still work correctly despite duplicates
assert set(result) == {1, 3}
assert isinstance(result, list)
class TestMakeUniqueListOfDicts:
"""Test cases for make_unique_list_of_dicts function"""
def test_basic_duplicate_removal(self):
"""Test basic removal of duplicate dictionaries"""
dict_list = [
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"a": 3, "b": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": 1, "b": 2} in result
assert {"a": 3, "b": 4} in result
def test_order_independent_duplicates(self):
"""Test that dictionaries with different key orders are treated as duplicates"""
dict_list = [
{"a": 1, "b": 2},
{"b": 2, "a": 1}, # Same content, different order
{"a": 3, "b": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": 1, "b": 2} in result
assert {"a": 3, "b": 4} in result
def test_empty_list(self):
"""Test with empty list"""
result = make_unique_list_of_dicts([])
assert result == []
assert isinstance(result, list)
def test_single_dict(self):
"""Test with single dictionary"""
dict_list = [{"a": 1, "b": 2}]
result = make_unique_list_of_dicts(dict_list)
assert result == [{"a": 1, "b": 2}]
def test_all_unique(self):
"""Test when all dictionaries are unique"""
dict_list = [
{"a": 1},
{"b": 2},
{"c": 3},
{"d": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 4
for d in dict_list:
assert d in result
def test_all_duplicates(self):
"""Test when all dictionaries are duplicates"""
dict_list = [
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"b": 2, "a": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 1
assert result[0] == {"a": 1, "b": 2}
def test_nested_values(self):
"""Test with nested structures as values"""
dict_list = [
{"a": [1, 2], "b": 3},
{"a": [1, 2], "b": 3},
{"a": [1, 3], "b": 3}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": [1, 2], "b": 3} in result
assert {"a": [1, 3], "b": 3} in result
def test_different_value_types(self):
"""Test with different value types"""
dict_list = [
{"str": "hello", "int": 42, "float": 3.14, "bool": True},
{"str": "hello", "int": 42, "float": 3.14, "bool": True},
{"str": "world", "int": 99, "float": 2.71, "bool": False}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_empty_dicts(self):
"""Test with empty dictionaries"""
dict_list: list[Any] = [
{},
{},
{"a": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {} in result
assert {"a": 1} in result
def test_single_key_dicts(self):
"""Test with single key dictionaries"""
dict_list = [
{"a": 1},
{"a": 1},
{"a": 2},
{"b": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 3
assert {"a": 1} in result
assert {"a": 2} in result
assert {"b": 1} in result
def test_many_keys(self):
"""Test with dictionaries containing many keys"""
dict1 = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
dict2 = {"e": 5, "d": 4, "c": 3, "b": 2, "a": 1} # Same, different order
dict3 = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 6} # Different value
dict_list = [dict1, dict2, dict3]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_numeric_keys(self):
"""Test with numeric keys"""
dict_list = [
{1: "one", 2: "two"},
{2: "two", 1: "one"},
{1: "one", 2: "three"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_none_values(self):
"""Test with None values"""
dict_list = [
{"a": None, "b": 2},
{"a": None, "b": 2},
{"a": 1, "b": None}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": None, "b": 2} in result
assert {"a": 1, "b": None} in result
def test_mixed_key_types(self):
"""Test with mixed key types (string and numeric)"""
dict_list = [
{"a": 1, 1: "one"},
{1: "one", "a": 1},
{"a": 2, 1: "one"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
@pytest.mark.parametrize("dict_list,expected_length", [
([{"a": 1}, {"a": 1}, {"a": 1}], 1),
([{"a": 1}, {"a": 2}, {"a": 3}], 3),
([{"a": 1, "b": 2}, {"b": 2, "a": 1}], 1),
([{}, {}], 1),
([{"x": [1, 2]}, {"x": [1, 2]}], 1),
([{"a": 1}, {"b": 2}, {"c": 3}], 3),
]) # pyright: ignore[reportUnknownArgumentType]
def test_parametrized_unique_dicts(self, dict_list: list[Any], expected_length: int):
"""Test make_unique_list_of_dicts with various input combinations"""
result = make_unique_list_of_dicts(dict_list)
assert len(result) == expected_length
assert isinstance(result, list)
def test_large_list(self):
"""Test with a large list of dictionaries"""
dict_list = [{"id": i % 100, "value": f"val_{i % 100}"} for i in range(1000)]
result = make_unique_list_of_dicts(dict_list)
# Should have 100 unique dicts (0-99)
assert len(result) == 100
def test_preserves_last_occurrence(self):
"""Test behavior with duplicate entries"""
# The function uses dict comprehension, which keeps last occurrence
dict_list = [
{"a": 1, "b": 2},
{"a": 3, "b": 4},
{"a": 1, "b": 2}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
# Just verify correct unique count, order may vary
def test_nested_dicts(self):
"""Test with nested dictionaries"""
dict_list = [
{"outer": {"inner": 1}},
{"outer": {"inner": 1}},
{"outer": {"inner": 2}}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_string_values_case_sensitive(self):
"""Test that string values are case-sensitive"""
dict_list = [
{"name": "John"},
{"name": "john"},
{"name": "JOHN"},
{"name": "John"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 3
def test_boolean_values(self):
"""Test with boolean values"""
dict_list = [
{"flag": True, "count": 1},
{"count": 1, "flag": True},
{"flag": False, "count": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"flag": True, "count": 1} in result
assert {"flag": False, "count": 1} in result
# __END__

View File

@@ -1,3 +0,0 @@
"""
tests for json_handling module
"""

View File

@@ -1,869 +0,0 @@
"""
tests for corelibs.json_handling.jmespath_helper
"""
from typing import Any
import pytest
from corelibs.json_handling.jmespath_helper import jmespath_search
# MARK: jmespath_search tests
class TestJmespathSearch:
"""Test cases for jmespath_search function"""
def test_simple_key_lookup(self):
"""Test simple key lookup in dictionary"""
data = {"name": "John", "age": 30}
result = jmespath_search(data, "name")
assert result == "John"
def test_nested_key_lookup(self):
"""Test nested key lookup"""
data = {
"user": {
"profile": {
"name": "John",
"age": 30
}
}
}
result = jmespath_search(data, "user.profile.name")
assert result == "John"
def test_array_index_access(self):
"""Test accessing array element by index"""
data = {
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
]
}
result = jmespath_search(data, "items[1].name")
assert result == "Item 2"
def test_array_slice(self):
"""Test array slicing"""
data = {"numbers": [1, 2, 3, 4, 5]}
result = jmespath_search(data, "numbers[1:3]")
assert result == [2, 3]
def test_wildcard_projection(self):
"""Test wildcard projection on array"""
data = {
"users": [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30},
{"name": "Charlie", "age": 35}
]
}
result = jmespath_search(data, "users[*].name")
assert result == ["Alice", "Bob", "Charlie"]
def test_filter_expression(self):
"""Test filter expression"""
data = {
"products": [
{"name": "Product 1", "price": 100, "stock": 5},
{"name": "Product 2", "price": 200, "stock": 0},
{"name": "Product 3", "price": 150, "stock": 10}
]
}
result = jmespath_search(data, "products[?stock > `0`].name")
assert result == ["Product 1", "Product 3"]
def test_pipe_expression(self):
"""Test pipe expression"""
data = {
"items": [
{"name": "Item 1", "value": 10},
{"name": "Item 2", "value": 20},
{"name": "Item 3", "value": 30}
]
}
result = jmespath_search(data, "items[*].value | [0]")
assert result == 10
def test_multi_select_hash(self):
"""Test multi-select hash"""
data = {"name": "John", "age": 30, "city": "New York", "country": "USA"}
result = jmespath_search(data, "{name: name, age: age}")
assert result == {"name": "John", "age": 30}
def test_multi_select_list(self):
"""Test multi-select list"""
data = {"first": "John", "last": "Doe", "age": 30}
result = jmespath_search(data, "[first, last]")
assert result == ["John", "Doe"]
def test_flatten_projection(self):
"""Test flatten projection"""
data = {
"groups": [
{"items": [1, 2, 3]},
{"items": [4, 5, 6]}
]
}
result = jmespath_search(data, "groups[].items[]")
assert result == [1, 2, 3, 4, 5, 6]
def test_function_length(self):
"""Test length function"""
data = {"items": [1, 2, 3, 4, 5]}
result = jmespath_search(data, "length(items)")
assert result == 5
def test_function_max(self):
"""Test max function"""
data = {"numbers": [10, 5, 20, 15]}
result = jmespath_search(data, "max(numbers)")
assert result == 20
def test_function_min(self):
"""Test min function"""
data = {"numbers": [10, 5, 20, 15]}
result = jmespath_search(data, "min(numbers)")
assert result == 5
def test_function_sort(self):
"""Test sort function"""
data = {"numbers": [3, 1, 4, 1, 5, 9, 2, 6]}
result = jmespath_search(data, "sort(numbers)")
assert result == [1, 1, 2, 3, 4, 5, 6, 9]
def test_function_sort_by(self):
"""Test sort_by function"""
data = {
"people": [
{"name": "Charlie", "age": 35},
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
}
result = jmespath_search(data, "sort_by(people, &age)[*].name")
assert result == ["Alice", "Bob", "Charlie"]
def test_function_join(self):
"""Test join function"""
data = {"names": ["Alice", "Bob", "Charlie"]}
result = jmespath_search(data, "join(', ', names)")
assert result == "Alice, Bob, Charlie"
def test_function_keys(self):
"""Test keys function"""
data = {"name": "John", "age": 30, "city": "New York"}
result = jmespath_search(data, "keys(@)")
assert sorted(result) == ["age", "city", "name"]
def test_function_values(self):
"""Test values function"""
data = {"a": 1, "b": 2, "c": 3}
result = jmespath_search(data, "values(@)")
assert sorted(result) == [1, 2, 3]
def test_function_type(self):
"""Test type function"""
data = {"string": "test", "number": 42, "array": [1, 2, 3]}
result = jmespath_search(data, "type(string)")
assert result == "string"
def test_function_contains(self):
"""Test contains function"""
data = {"items": [1, 2, 3, 4, 5]}
result = jmespath_search(data, "contains(items, `3`)")
assert result is True
def test_current_node_reference(self):
"""Test current node @ reference"""
data = [1, 2, 3, 4, 5]
result = jmespath_search(data, "@")
assert result == [1, 2, 3, 4, 5]
def test_not_null_expression(self):
"""Test not_null expression"""
data = {
"items": [
{"name": "Item 1", "description": "Desc 1"},
{"name": "Item 2", "description": None},
{"name": "Item 3"}
]
}
result = jmespath_search(data, "items[*].description | [?@ != null]")
assert result == ["Desc 1"]
def test_search_returns_none_for_missing_key(self):
"""Test that searching for non-existent key returns None"""
data = {"name": "John", "age": 30}
result = jmespath_search(data, "nonexistent")
assert result is None
def test_search_with_list_input(self):
"""Test search with list as input"""
data = [
{"name": "Alice", "score": 85},
{"name": "Bob", "score": 92},
{"name": "Charlie", "score": 78}
]
result = jmespath_search(data, "[?score > `80`].name")
assert result == ["Alice", "Bob"]
def test_deeply_nested_structure(self):
"""Test searching deeply nested structure"""
data = {
"level1": {
"level2": {
"level3": {
"level4": {
"level5": {
"value": "deep_value"
}
}
}
}
}
}
result = jmespath_search(data, "level1.level2.level3.level4.level5.value")
assert result == "deep_value"
def test_complex_filter_expression(self):
"""Test complex filter with multiple conditions"""
data = {
"products": [
{"name": "Product 1", "price": 100, "stock": 5, "category": "A"},
{"name": "Product 2", "price": 200, "stock": 0, "category": "B"},
{"name": "Product 3", "price": 150, "stock": 10, "category": "A"},
{"name": "Product 4", "price": 120, "stock": 3, "category": "A"}
]
}
result = jmespath_search(
data,
"products[?category == 'A' && stock > `0`].name"
)
assert result == ["Product 1", "Product 3", "Product 4"]
def test_recursive_descent(self):
"""Test recursive descent operator"""
data = {
"store": {
"book": [
{"title": "Book 1", "price": 10},
{"title": "Book 2", "price": 20}
],
"bicycle": {
"price": 100
}
}
}
# Note: JMESPath doesn't have a true recursive descent like JSONPath's '..'
# but we can test nested projections
result = jmespath_search(data, "store.book[*].price")
assert result == [10, 20]
def test_empty_dict_input(self):
"""Test search on empty dictionary"""
data: dict[Any, Any] = {}
result = jmespath_search(data, "key")
assert result is None
def test_empty_list_input(self):
"""Test search on empty list"""
data: list[Any] = []
result = jmespath_search(data, "[0]")
assert result is None
def test_unicode_keys_and_values(self):
"""Test search with unicode keys and values"""
data = {
"日本語": "テスト",
"emoji_🎉": "🚀",
"nested": {
"中文": "测试"
}
}
# JMESPath requires quoted identifiers for unicode keys
result = jmespath_search(data, '"日本語"')
assert result == "テスト"
result2 = jmespath_search(data, 'nested."中文"')
assert result2 == "测试"
def test_numeric_values(self):
"""Test search with various numeric values"""
data = {
"int": 42,
"float": 3.14,
"negative": -10,
"zero": 0,
"scientific": 1e10
}
result = jmespath_search(data, "float")
assert result == 3.14
def test_boolean_values(self):
"""Test search with boolean values"""
data = {
"items": [
{"name": "Item 1", "active": True},
{"name": "Item 2", "active": False},
{"name": "Item 3", "active": True}
]
}
result = jmespath_search(data, "items[?active].name")
assert result == ["Item 1", "Item 3"]
def test_null_values(self):
"""Test search with null/None values"""
data = {
"name": "John",
"middle_name": None,
"last_name": "Doe"
}
result = jmespath_search(data, "middle_name")
assert result is None
def test_mixed_types_in_array(self):
"""Test search on array with mixed types"""
data = {"mixed": [1, "two", 3.0, True, None, {"key": "value"}]}
result = jmespath_search(data, "mixed[5].key")
assert result == "value"
def test_expression_with_literals(self):
"""Test expression with literal values"""
data = {
"items": [
{"name": "Item 1", "price": 100},
{"name": "Item 2", "price": 200}
]
}
result = jmespath_search(data, "items[?price == `100`].name")
assert result == ["Item 1"]
def test_comparison_operators(self):
"""Test various comparison operators"""
data = {
"numbers": [
{"value": 10},
{"value": 20},
{"value": 30},
{"value": 40}
]
}
result = jmespath_search(data, "numbers[?value >= `20` && value <= `30`].value")
assert result == [20, 30]
def test_logical_operators(self):
"""Test logical operators (and, or, not)"""
data = {
"items": [
{"name": "A", "active": True, "stock": 5},
{"name": "B", "active": False, "stock": 0},
{"name": "C", "active": True, "stock": 0},
{"name": "D", "active": False, "stock": 10}
]
}
result = jmespath_search(data, "items[?active || stock > `0`].name")
assert result == ["A", "C", "D"]
# MARK: Error handling tests
class TestJmespathSearchErrors:
"""Test error handling in jmespath_search function"""
def test_lexer_error_invalid_syntax(self):
"""Test LexerError is converted to ValueError for invalid syntax"""
data = {"name": "John"}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, "name[")
# This actually raises a ParseError, not LexerError
assert "Parse failed" in str(exc_info.value)
def test_lexer_error_unclosed_bracket(self):
"""Test LexerError for unclosed bracket"""
data = {"items": [1, 2, 3]}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, "items[0")
# This actually raises a ParseError, not LexerError
assert "Parse failed" in str(exc_info.value)
def test_parse_error_invalid_expression(self):
"""Test ParseError is converted to ValueError"""
data = {"name": "John"}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, "name..age")
assert "Parse failed" in str(exc_info.value)
def test_parse_error_invalid_filter(self):
"""Test ParseError for invalid filter syntax"""
data = {"items": [1, 2, 3]}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, "items[?@")
assert "Parse failed" in str(exc_info.value)
def test_type_error_invalid_function_usage(self):
"""Test JMESPathTypeError for invalid function usage"""
data = {"name": "John", "age": 30}
# Trying to use length on a string (in some contexts this might cause type errors)
# Note: This might not always raise an error depending on JMESPath version
# Using a more reliable example: trying to use max on non-array
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, "max(name)")
assert "Search failed with JMESPathTypeError" in str(exc_info.value)
def test_type_error_with_none_search_params(self):
"""Test TypeError when search_params is None"""
data = {"name": "John"}
# None or empty string raises EmptyExpressionError from jmespath
with pytest.raises(Exception) as exc_info: # Catches any exception
jmespath_search(data, None) # type: ignore
# The error message should indicate an empty expression issue
assert "empty" in str(exc_info.value).lower() or "Type error" in str(exc_info.value)
def test_type_error_with_invalid_search_params_type(self):
"""Test TypeError when search_params is not a string"""
data = {"name": "John"}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, 123) # type: ignore
assert "Type error for search_params" in str(exc_info.value)
def test_type_error_with_dict_search_params(self):
"""Test TypeError when search_params is a dict"""
data = {"name": "John"}
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, {"key": "value"}) # type: ignore
assert "Type error for search_params" in str(exc_info.value)
def test_error_message_includes_search_params(self):
"""Test that error messages include the search parameters"""
data = {"name": "John"}
invalid_query = "name["
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, invalid_query)
error_message = str(exc_info.value)
assert invalid_query in error_message
# This raises ParseError, not LexerError
assert "Parse failed" in error_message
def test_error_message_includes_exception_details(self):
"""Test that error messages include original exception details"""
data = {"items": [1, 2, 3]}
invalid_query = "items[?"
with pytest.raises(ValueError) as exc_info:
jmespath_search(data, invalid_query)
error_message = str(exc_info.value)
# Should contain both the query and some indication of what went wrong
assert invalid_query in error_message
# MARK: Edge cases
class TestJmespathSearchEdgeCases:
"""Test edge cases for jmespath_search function"""
def test_very_large_array(self):
"""Test searching large array"""
data = {"items": [{"id": i, "value": i * 10} for i in range(1000)]}
result = jmespath_search(data, "items[500].value")
assert result == 5000
def test_very_deep_nesting(self):
"""Test very deep nesting"""
# Create 20-level deep nested structure
data: dict[str, Any] = {"level0": {}}
current = data["level0"]
for i in range(1, 20):
current[f"level{i}"] = {}
current = current[f"level{i}"]
current["value"] = "deep"
# Build the search path
path = ".".join([f"level{i}" for i in range(20)]) + ".value"
result = jmespath_search(data, path)
assert result == "deep"
def test_special_characters_in_keys(self):
"""Test keys with special characters (requires escaping)"""
data = {"my-key": "value", "my.key": "value2"}
# JMESPath requires quoting for keys with special characters
result = jmespath_search(data, '"my-key"')
assert result == "value"
result2 = jmespath_search(data, '"my.key"')
assert result2 == "value2"
def test_numeric_string_keys(self):
"""Test keys that look like numbers"""
data = {"123": "numeric_key", "456": "another"}
result = jmespath_search(data, '"123"')
assert result == "numeric_key"
def test_empty_string_key(self):
"""Test empty string as key"""
data = {"": "empty_key_value", "normal": "normal_value"}
result = jmespath_search(data, '""')
assert result == "empty_key_value"
def test_whitespace_in_keys(self):
"""Test keys with whitespace"""
data = {"my key": "value", " trimmed ": "value2"}
result = jmespath_search(data, '"my key"')
assert result == "value"
def test_array_with_negative_index(self):
"""Test negative array indexing"""
data = {"items": [1, 2, 3, 4, 5]}
# JMESPath actually supports negative indexing
result = jmespath_search(data, "items[-1]")
assert result == 5
def test_out_of_bounds_array_index(self):
"""Test out of bounds array access"""
data = {"items": [1, 2, 3]}
result = jmespath_search(data, "items[10]")
assert result is None
def test_chaining_multiple_operations(self):
"""Test chaining multiple JMESPath operations"""
data: dict[str, Any] = {
"users": [
{"name": "Alice", "posts": [{"id": 1}, {"id": 2}]},
{"name": "Bob", "posts": [{"id": 3}, {"id": 4}, {"id": 5}]},
{"name": "Charlie", "posts": []}
]
}
result = jmespath_search(data, "users[*].posts[].id")
assert result == [1, 2, 3, 4, 5]
def test_projection_on_non_array(self):
"""Test projection on non-array (should handle gracefully)"""
data = {"value": "not_an_array"}
result = jmespath_search(data, "value[*]")
assert result is None
def test_filter_on_non_array(self):
"""Test filter on non-array"""
data = {"value": "string"}
result = jmespath_search(data, "value[?@ == 'x']")
assert result is None
def test_combining_filters_and_projections(self):
"""Test combining filters with projections"""
data = {
"products": [
{
"name": "Product 1",
"variants": [
{"color": "red", "stock": 5},
{"color": "blue", "stock": 0}
]
},
{
"name": "Product 2",
"variants": [
{"color": "green", "stock": 10},
{"color": "yellow", "stock": 3}
]
}
]
}
result = jmespath_search(
data,
"products[*].variants[?stock > `0`].color"
)
assert result == [["red"], ["green", "yellow"]]
def test_search_with_root_array(self):
"""Test search when root is an array"""
data = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
result = jmespath_search(data, "[0].name")
assert result == "Alice"
def test_search_with_primitive_root(self):
"""Test search when root is a primitive value"""
# When root is primitive, only @ should work
data_str = "simple_string"
result = jmespath_search(data_str, "@") # type: ignore
assert result == "simple_string"
def test_function_with_empty_array(self):
"""Test functions on empty arrays"""
data: dict[str, list[Any]] = {"items": []}
result = jmespath_search(data, "length(items)")
assert result == 0
def test_nested_multi_select(self):
"""Test nested multi-select operations"""
data = {
"person": {
"name": "John",
"age": 30,
"address": {
"city": "New York",
"country": "USA"
}
}
}
result = jmespath_search(
data,
"person.{name: name, city: address.city}"
)
assert result == {"name": "John", "city": "New York"}
# MARK: Integration tests
class TestJmespathSearchIntegration:
"""Integration tests for complex real-world scenarios"""
def test_api_response_parsing(self):
"""Test parsing typical API response structure"""
api_response = {
"status": "success",
"data": {
"users": [
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"active": True,
"metadata": {
"created_at": "2025-01-01",
"last_login": "2025-10-23"
}
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"active": False,
"metadata": {
"created_at": "2025-02-01",
"last_login": "2025-05-15"
}
},
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"active": True,
"metadata": {
"created_at": "2025-03-01",
"last_login": "2025-10-20"
}
}
]
},
"metadata": {
"total": 3,
"page": 1
}
}
# Get all active user emails
result = jmespath_search(api_response, "data.users[?active].email")
assert result == ["alice@example.com", "charlie@example.com"]
# Get user names and creation dates
result2 = jmespath_search(
api_response,
"data.users[*].{name: name, created: metadata.created_at}"
)
assert len(result2) == 3
assert result2[0]["name"] == "Alice"
assert result2[0]["created"] == "2025-01-01"
def test_config_file_parsing(self):
"""Test parsing configuration-like structure"""
config = {
"version": "1.0",
"environments": {
"development": {
"database": {
"host": "localhost",
"port": 5432,
"name": "dev_db"
},
"cache": {
"enabled": True,
"ttl": 300
}
},
"production": {
"database": {
"host": "prod.example.com",
"port": 5432,
"name": "prod_db"
},
"cache": {
"enabled": True,
"ttl": 3600
}
}
}
}
# Get production database host
result = jmespath_search(config, "environments.production.database.host")
assert result == "prod.example.com"
# Get all database names using values() - object wildcard returns an object
# Need to convert to list for sorting
result2 = jmespath_search(config, "values(environments)[*].database.name")
assert result2 is not None
assert sorted(result2) == ["dev_db", "prod_db"]
def test_nested_filtering_and_transformation(self):
"""Test complex nested filtering and transformation"""
data = {
"departments": [
{
"name": "Engineering",
"employees": [
{"name": "Alice", "salary": 100000, "level": "Senior"},
{"name": "Bob", "salary": 80000, "level": "Mid"},
{"name": "Charlie", "salary": 120000, "level": "Senior"}
]
},
{
"name": "Marketing",
"employees": [
{"name": "Dave", "salary": 70000, "level": "Junior"},
{"name": "Eve", "salary": 90000, "level": "Mid"}
]
}
]
}
# Get all senior employees with salary > 100k
result = jmespath_search(
data,
"departments[*].employees[?level == 'Senior' && salary > `100000`].name"
)
# Note: 100000 is not > 100000, so Alice is excluded
assert result == [["Charlie"], []]
# Get flattened list (using >= instead and flatten operator)
result2 = jmespath_search(
data,
"departments[].employees[?level == 'Senior' && salary >= `100000`].name | []"
)
assert sorted(result2) == ["Alice", "Charlie"]
def test_working_with_timestamps(self):
"""Test searching and filtering timestamp-like data"""
data = {
"events": [
{"name": "Event 1", "timestamp": "2025-10-20T10:00:00"},
{"name": "Event 2", "timestamp": "2025-10-21T15:30:00"},
{"name": "Event 3", "timestamp": "2025-10-23T08:45:00"},
{"name": "Event 4", "timestamp": "2025-10-24T12:00:00"}
]
}
# Get events after a certain date (string comparison)
result = jmespath_search(
data,
"events[?timestamp > '2025-10-22'].name"
)
assert result == ["Event 3", "Event 4"]
def test_aggregation_operations(self):
"""Test aggregation-like operations"""
data = {
"sales": [
{"product": "A", "quantity": 10, "price": 100},
{"product": "B", "quantity": 5, "price": 200},
{"product": "C", "quantity": 8, "price": 150}
]
}
# Get all quantities
quantities = jmespath_search(data, "sales[*].quantity")
assert quantities == [10, 5, 8]
# Get max quantity
max_quantity = jmespath_search(data, "max(sales[*].quantity)")
assert max_quantity == 10
# Get min price
min_price = jmespath_search(data, "min(sales[*].price)")
assert min_price == 100
# Get sorted products by price
sorted_products = jmespath_search(
data,
"sort_by(sales, &price)[*].product"
)
assert sorted_products == ["A", "C", "B"]
def test_data_transformation_pipeline(self):
"""Test data transformation pipeline"""
raw_data = {
"response": {
"items": [
{
"id": "item-1",
"attributes": {
"name": "Product A",
"specs": {"weight": 100, "color": "red"}
},
"available": True
},
{
"id": "item-2",
"attributes": {
"name": "Product B",
"specs": {"weight": 200, "color": "blue"}
},
"available": False
},
{
"id": "item-3",
"attributes": {
"name": "Product C",
"specs": {"weight": 150, "color": "red"}
},
"available": True
}
]
}
}
# Get available red products
result = jmespath_search(
raw_data,
"response.items[?available && attributes.specs.color == 'red'].attributes.name"
)
assert result == ["Product A", "Product C"]
# Transform to simplified structure
result2 = jmespath_search(
raw_data,
"response.items[*].{id: id, name: attributes.name, weight: attributes.specs.weight}"
)
assert len(result2) == 3
assert result2[0] == {"id": "item-1", "name": "Product A", "weight": 100}
# __END__

View File

@@ -1,698 +0,0 @@
"""
tests for corelibs.json_handling.json_helper
"""
import json
from datetime import datetime, date
from typing import Any
from corelibs.json_handling.json_helper import (
DateTimeEncoder,
default_isoformat,
json_dumps,
modify_with_jsonpath
)
# MARK: DateTimeEncoder tests
class TestDateTimeEncoder:
"""Test cases for DateTimeEncoder class"""
def test_datetime_encoding(self):
"""Test encoding datetime objects"""
dt = datetime(2025, 10, 23, 15, 30, 45, 123456)
data = {"timestamp": dt}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45.123456"
def test_date_encoding(self):
"""Test encoding date objects"""
d = date(2025, 10, 23)
data = {"date": d}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["date"] == "2025-10-23"
def test_mixed_datetime_date_encoding(self):
"""Test encoding mixed datetime and date objects"""
dt = datetime(2025, 10, 23, 15, 30, 45)
d = date(2025, 10, 23)
data = {
"timestamp": dt,
"date": d,
"name": "test"
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45"
assert decoded["date"] == "2025-10-23"
assert decoded["name"] == "test"
def test_nested_datetime_encoding(self):
"""Test encoding nested structures with datetime objects"""
data = {
"event": {
"name": "Meeting",
"start": datetime(2025, 10, 23, 10, 0, 0),
"end": datetime(2025, 10, 23, 11, 0, 0),
"participants": [
{"name": "Alice", "joined": datetime(2025, 10, 23, 10, 5, 0)},
{"name": "Bob", "joined": datetime(2025, 10, 23, 10, 10, 0)}
]
}
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["event"]["start"] == "2025-10-23T10:00:00"
assert decoded["event"]["end"] == "2025-10-23T11:00:00"
assert decoded["event"]["participants"][0]["joined"] == "2025-10-23T10:05:00"
assert decoded["event"]["participants"][1]["joined"] == "2025-10-23T10:10:00"
def test_list_of_datetimes(self):
"""Test encoding list of datetime objects"""
data = {
"timestamps": [
datetime(2025, 10, 23, 10, 0, 0),
datetime(2025, 10, 23, 11, 0, 0),
datetime(2025, 10, 23, 12, 0, 0)
]
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["timestamps"][0] == "2025-10-23T10:00:00"
assert decoded["timestamps"][1] == "2025-10-23T11:00:00"
assert decoded["timestamps"][2] == "2025-10-23T12:00:00"
def test_encoder_with_normal_types(self):
"""Test that encoder works with standard JSON types"""
data = {
"string": "test",
"number": 42,
"float": 3.14,
"boolean": True,
"null": None,
"list": [1, 2, 3],
"dict": {"key": "value"}
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded == data
def test_encoder_returns_none_for_unsupported_types(self):
"""Test that encoder default method returns None for unsupported types"""
encoder = DateTimeEncoder()
# The default method should return None for non-date/datetime objects
result = encoder.default("string")
assert result is None
result = encoder.default(42)
assert result is None
result = encoder.default([1, 2, 3])
assert result is None
# MARK: default function tests
class TestDefaultFunction:
"""Test cases for the default function"""
def test_default_datetime(self):
"""Test default function with datetime"""
dt = datetime(2025, 10, 23, 15, 30, 45)
result = default_isoformat(dt)
assert result == "2025-10-23T15:30:45"
def test_default_date(self):
"""Test default function with date"""
d = date(2025, 10, 23)
result = default_isoformat(d)
assert result == "2025-10-23"
def test_default_with_microseconds(self):
"""Test default function with datetime including microseconds"""
dt = datetime(2025, 10, 23, 15, 30, 45, 123456)
result = default_isoformat(dt)
assert result == "2025-10-23T15:30:45.123456"
def test_default_returns_none_for_other_types(self):
"""Test that default returns None for non-date/datetime objects"""
assert default_isoformat("string") is None
assert default_isoformat(42) is None
assert default_isoformat(3.14) is None
assert default_isoformat(True) is None
assert default_isoformat(None) is None
assert default_isoformat([1, 2, 3]) is None
assert default_isoformat({"key": "value"}) is None
def test_default_as_json_default_parameter(self):
"""Test using default function as default parameter in json.dumps"""
data = {
"timestamp": datetime(2025, 10, 23, 15, 30, 45),
"date": date(2025, 10, 23),
"name": "test"
}
result = json.dumps(data, default=default_isoformat)
decoded = json.loads(result)
assert decoded["timestamp"] == "2025-10-23T15:30:45"
assert decoded["date"] == "2025-10-23"
assert decoded["name"] == "test"
# MARK: json_dumps tests
class TestJsonDumps:
"""Test cases for json_dumps function"""
def test_basic_dict(self):
"""Test json_dumps with basic dictionary"""
data = {"name": "test", "value": 42}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_unicode_characters(self):
"""Test json_dumps preserves unicode characters (ensure_ascii=False)"""
data = {"name": "テスト", "emoji": "🎉", "chinese": "测试"}
result = json_dumps(data)
# ensure_ascii=False means unicode characters should be preserved
assert "テスト" in result
assert "🎉" in result
assert "测试" in result
decoded = json.loads(result)
assert decoded == data
def test_datetime_objects_as_string(self):
"""Test json_dumps converts datetime to string (default=str)"""
dt = datetime(2025, 10, 23, 15, 30, 45)
data = {"timestamp": dt}
result = json_dumps(data)
decoded = json.loads(result)
# default=str will convert datetime to its string representation
assert isinstance(decoded["timestamp"], str)
assert "2025-10-23" in decoded["timestamp"]
def test_date_objects_as_string(self):
"""Test json_dumps converts date to string"""
d = date(2025, 10, 23)
data = {"date": d}
result = json_dumps(data)
decoded = json.loads(result)
assert isinstance(decoded["date"], str)
assert "2025-10-23" in decoded["date"]
def test_complex_nested_structure(self):
"""Test json_dumps with complex nested structures"""
data = {
"user": {
"name": "John",
"age": 30,
"active": True,
"balance": 100.50,
"tags": ["admin", "user"],
"metadata": {
"created": datetime(2025, 1, 1, 0, 0, 0),
"updated": date(2025, 10, 23)
}
},
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
]
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["user"]["name"] == "John"
assert decoded["user"]["age"] == 30
assert decoded["user"]["active"] is True
assert decoded["user"]["balance"] == 100.50
assert decoded["user"]["tags"] == ["admin", "user"]
assert decoded["items"][0]["id"] == 1
def test_empty_dict(self):
"""Test json_dumps with empty dictionary"""
data: dict[str, Any] = {}
result = json_dumps(data)
assert result == "{}"
def test_empty_list(self):
"""Test json_dumps with empty list"""
data: list[Any] = []
result = json_dumps(data)
assert result == "[]"
def test_list_data(self):
"""Test json_dumps with list as root element"""
data = [1, 2, 3, "test", True, None]
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_none_value(self):
"""Test json_dumps with None"""
data = None
result = json_dumps(data)
assert result == "null"
def test_boolean_values(self):
"""Test json_dumps with boolean values"""
data = {"true_val": True, "false_val": False}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["true_val"] is True
assert decoded["false_val"] is False
def test_numeric_values(self):
"""Test json_dumps with various numeric values"""
data = {
"int": 42,
"float": 3.14,
"negative": -10,
"zero": 0,
"scientific": 1e10
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded == data
def test_custom_object_conversion(self):
"""Test json_dumps with custom objects (converted via str)"""
class CustomObject:
"""test class"""
def __str__(self):
return "custom_value"
data = {"custom": CustomObject()}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["custom"] == "custom_value"
def test_special_float_values(self):
"""Test json_dumps handles special float values"""
data = {
"infinity": float('inf'),
"neg_infinity": float('-inf'),
"nan": float('nan')
}
result = json_dumps(data)
# These should be converted to strings via default=str
assert "Infinity" in result or "inf" in result.lower()
# MARK: modify_with_jsonpath tests
class TestModifyWithJsonpath:
"""Test cases for modify_with_jsonpath function"""
def test_simple_path_modification(self):
"""Test modifying a simple path"""
data = {"name": "old_name", "age": 30}
result = modify_with_jsonpath(data, "$.name", "new_name")
assert result["name"] == "new_name"
assert result["age"] == 30
# Original data should not be modified
assert data["name"] == "old_name"
def test_nested_path_modification(self):
"""Test modifying nested path"""
data = {
"user": {
"profile": {
"name": "John",
"age": 30
}
}
}
result = modify_with_jsonpath(data, "$.user.profile.name", "Jane")
assert result["user"]["profile"]["name"] == "Jane"
assert result["user"]["profile"]["age"] == 30
# Original should be unchanged
assert data["user"]["profile"]["name"] == "John"
def test_array_index_modification(self):
"""Test modifying array element by index"""
data = {
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"}
]
}
result = modify_with_jsonpath(data, "$.items[1].name", "Updated Item 2")
assert result["items"][1]["name"] == "Updated Item 2"
assert result["items"][0]["name"] == "Item 1"
assert result["items"][2]["name"] == "Item 3"
# Original unchanged
assert data["items"][1]["name"] == "Item 2"
def test_wildcard_modification(self):
"""Test modifying multiple elements with wildcard"""
data = {
"users": [
{"name": "Alice", "active": True},
{"name": "Bob", "active": True},
{"name": "Charlie", "active": True}
]
}
result = modify_with_jsonpath(data, "$.users[*].active", False)
# All active fields should be updated
for user in result["users"]:
assert user["active"] is False
# Original unchanged
for user in data["users"]:
assert user["active"] is True
def test_deep_copy_behavior(self):
"""Test that modifications don't affect the original data"""
original = {
"level1": {
"level2": {
"level3": {
"value": "original"
}
}
}
}
result = modify_with_jsonpath(original, "$.level1.level2.level3.value", "modified")
assert result["level1"]["level2"]["level3"]["value"] == "modified"
assert original["level1"]["level2"]["level3"]["value"] == "original"
# Verify deep copy by modifying nested dict in result
result["level1"]["level2"]["new_key"] = "new_value"
assert "new_key" not in original["level1"]["level2"]
def test_modify_to_different_type(self):
"""Test changing value to different type"""
data = {"count": "10"}
result = modify_with_jsonpath(data, "$.count", 10)
assert result["count"] == 10
assert isinstance(result["count"], int)
assert data["count"] == "10"
def test_modify_to_complex_object(self):
"""Test replacing value with complex object"""
data = {"simple": "value"}
new_value = {"complex": {"nested": "structure"}}
result = modify_with_jsonpath(data, "$.simple", new_value)
assert result["simple"] == new_value
assert result["simple"]["complex"]["nested"] == "structure"
def test_modify_to_list(self):
"""Test replacing value with list"""
data = {"items": None}
result = modify_with_jsonpath(data, "$.items", [1, 2, 3])
assert result["items"] == [1, 2, 3]
assert data["items"] is None
def test_modify_to_none(self):
"""Test setting value to None"""
data = {"value": "something"}
result = modify_with_jsonpath(data, "$.value", None)
assert result["value"] is None
assert data["value"] == "something"
def test_recursive_descent(self):
"""Test using recursive descent operator"""
data: dict[str, Any] = {
"store": {
"book": [
{"title": "Book 1", "price": 10},
{"title": "Book 2", "price": 20}
],
"bicycle": {
"price": 100
}
}
}
# Update all prices
result = modify_with_jsonpath(data, "$..price", 0)
assert result["store"]["book"][0]["price"] == 0
assert result["store"]["book"][1]["price"] == 0
assert result["store"]["bicycle"]["price"] == 0
# Original unchanged
assert data["store"]["book"][0]["price"] == 10
def test_specific_array_elements(self):
"""Test updating specific array elements by index"""
data = {
"products": [
{"name": "Product 1", "price": 100, "stock": 5},
{"name": "Product 2", "price": 200, "stock": 0},
{"name": "Product 3", "price": 150, "stock": 10}
]
}
# Update first product's price
result = modify_with_jsonpath(data, "$.products[0].price", 0)
assert result["products"][0]["price"] == 0
assert result["products"][1]["price"] == 200 # not modified
assert result["products"][2]["price"] == 150 # not modified
def test_empty_dict(self):
"""Test modifying empty dictionary"""
data: dict[str, Any] = {}
result = modify_with_jsonpath(data, "$.nonexistent", "value")
# Should return the original empty dict since path doesn't exist
assert result == {}
def test_complex_real_world_scenario(self):
"""Test complex real-world modification scenario"""
data: dict[str, Any] = {
"api_version": "1.0",
"config": {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {
"username": "admin",
"password": "secret"
}
},
"services": [
{"name": "auth", "enabled": True, "port": 8001},
{"name": "api", "enabled": True, "port": 8002},
{"name": "cache", "enabled": False, "port": 8003}
]
}
}
# Update database port
result = modify_with_jsonpath(data, "$.config.database.port", 5433)
assert result["config"]["database"]["port"] == 5433
# Update all service ports
result2 = modify_with_jsonpath(result, "$.config.services[*].enabled", True)
assert all(service["enabled"] for service in result2["config"]["services"])
# Original unchanged
assert data["config"]["database"]["port"] == 5432
assert data["config"]["services"][2]["enabled"] is False
def test_list_slice_modification(self):
"""Test modifying list slice"""
data = {"numbers": [1, 2, 3, 4, 5]}
# Modify first three elements
result = modify_with_jsonpath(data, "$.numbers[0:3]", 0)
assert result["numbers"][0] == 0
assert result["numbers"][1] == 0
assert result["numbers"][2] == 0
assert result["numbers"][3] == 4
assert result["numbers"][4] == 5
def test_modify_with_datetime_value(self):
"""Test modifying with datetime value"""
data = {"timestamp": "2025-01-01T00:00:00"}
new_datetime = datetime(2025, 10, 23, 15, 30, 45)
result = modify_with_jsonpath(data, "$.timestamp", new_datetime)
assert result["timestamp"] == new_datetime
assert isinstance(result["timestamp"], datetime)
# MARK: Integration tests
class TestIntegration:
"""Integration tests combining multiple functions"""
def test_encoder_and_json_dumps_comparison(self):
"""Test that DateTimeEncoder and json_dumps handle datetimes differently"""
dt = datetime(2025, 10, 23, 15, 30, 45)
data = {"timestamp": dt}
# Using DateTimeEncoder produces ISO format
with_encoder = json.dumps(data, cls=DateTimeEncoder)
decoded_encoder = json.loads(with_encoder)
assert decoded_encoder["timestamp"] == "2025-10-23T15:30:45"
# Using json_dumps (default=str) produces string representation
with_dumps = json_dumps(data)
decoded_dumps = json.loads(with_dumps)
assert isinstance(decoded_dumps["timestamp"], str)
assert "2025-10-23" in decoded_dumps["timestamp"]
def test_modify_and_serialize(self):
"""Test modifying data and then serializing it"""
data = {
"event": {
"name": "Meeting",
"date": date(2025, 10, 23),
"attendees": [
{"name": "Alice", "confirmed": False},
{"name": "Bob", "confirmed": False}
]
}
}
# Modify confirmation status
modified = modify_with_jsonpath(data, "$.event.attendees[*].confirmed", True)
# Serialize with datetime handling
serialized = json.dumps(modified, cls=DateTimeEncoder)
decoded = json.loads(serialized)
assert decoded["event"]["date"] == "2025-10-23"
assert decoded["event"]["attendees"][0]["confirmed"] is True
assert decoded["event"]["attendees"][1]["confirmed"] is True
def test_round_trip_with_modification(self):
"""Test full round trip: serialize -> modify -> serialize"""
original = {
"config": {
"updated": datetime(2025, 10, 23, 15, 30, 45),
"version": "1.0"
}
}
# Serialize
json_str = json.dumps(original, cls=DateTimeEncoder)
# Deserialize
deserialized = json.loads(json_str)
# Modify
modified = modify_with_jsonpath(deserialized, "$.config.version", "2.0")
# Serialize again
final_json = json_dumps(modified)
final_data = json.loads(final_json)
assert final_data["config"]["version"] == "2.0"
assert final_data["config"]["updated"] == "2025-10-23T15:30:45"
# MARK: Edge cases
class TestEdgeCases:
"""Test edge cases and error scenarios"""
def test_circular_reference_in_modify(self):
"""Test that modify_with_jsonpath handles data without circular references"""
# Note: JSON doesn't support circular references, so we test normal nested data
data = {
"a": {
"b": {
"c": "value"
}
}
}
result = modify_with_jsonpath(data, "$.a.b.c", "new_value")
assert result["a"]["b"]["c"] == "new_value"
def test_unicode_in_keys_and_values(self):
"""Test handling unicode in both keys and values"""
data = {
"日本語": "テスト",
"emoji_🎉": "🚀",
"normal": "value"
}
result = json_dumps(data)
decoded = json.loads(result)
assert decoded["日本語"] == "テスト"
assert decoded["emoji_🎉"] == "🚀"
assert decoded["normal"] == "value"
def test_very_nested_structure(self):
"""Test deeply nested structure"""
# Create a 10-level deep nested structure
data: dict[str, Any] = {"level0": {}}
current = data["level0"]
for i in range(1, 10):
current[f"level{i}"] = {}
current = current[f"level{i}"]
current["value"] = "deep_value"
result = modify_with_jsonpath(data, "$..value", "modified_deep_value")
# Navigate to the deep value
current = result["level0"]
for i in range(1, 10):
current = current[f"level{i}"]
assert current["value"] == "modified_deep_value"
def test_large_list_modification(self):
"""Test modifying large list"""
data = {"items": [{"id": i, "value": i * 10} for i in range(100)]}
result = modify_with_jsonpath(data, "$.items[*].value", 0)
assert all(item["value"] == 0 for item in result["items"])
assert len(result["items"]) == 100
def test_mixed_date_types_encoding(self):
"""Test encoding with both date and datetime in same structure"""
data = {
"created_date": date(2025, 10, 23),
"created_datetime": datetime(2025, 10, 23, 15, 30, 45),
"updated_date": date(2025, 10, 24),
"updated_datetime": datetime(2025, 10, 24, 16, 45, 30)
}
result = json.dumps(data, cls=DateTimeEncoder)
decoded = json.loads(result)
assert decoded["created_date"] == "2025-10-23"
assert decoded["created_datetime"] == "2025-10-23T15:30:45"
assert decoded["updated_date"] == "2025-10-24"
assert decoded["updated_datetime"] == "2025-10-24T16:45:30"

View File

@@ -1,121 +0,0 @@
"""
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__

View File

@@ -2,11 +2,10 @@
PyTest: requests_handling/caller
"""
from typing import Any
from unittest.mock import Mock, patch
import pytest
import requests
from corelibs.requests_handling.caller import Caller
from corelibs.requests_handling.caller import Caller, ErrorResponse, ProxyConfig
class TestCallerInit:
@@ -21,13 +20,17 @@ class TestCallerInit:
assert caller.timeout == 20
assert caller.verify is True
assert caller.proxy is None
assert caller.cafile is None
assert caller.ca_file is None
def test_init_with_all_params(self):
"""Test Caller initialization with all parameters"""
header = {"Authorization": "Bearer token", "Content-Type": "application/json"}
proxy = {"http": "http://proxy.example.com:8080", "https": "https://proxy.example.com:8080"}
caller = Caller(header=header, verify=False, timeout=30, proxy=proxy)
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header=header, timeout=30, proxy=proxy, verify=False)
assert caller.headers == header
assert caller.timeout == 30
@@ -58,7 +61,7 @@ class TestCallerInit:
ca_file_path = "/path/to/ca/cert.pem"
caller = Caller(header={}, ca_file=ca_file_path)
assert caller.cafile == ca_file_path
assert caller.ca_file == ca_file_path
class TestCallerGet:
@@ -81,7 +84,8 @@ class TestCallerGet:
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -101,7 +105,8 @@ class TestCallerGet:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -134,7 +139,11 @@ class TestCallerGet:
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
proxy = {"http": "http://proxy.example.com:8080"}
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
@@ -142,40 +151,46 @@ class TestCallerGet:
assert mock_get.call_args[1]["proxies"] == proxy
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_invalid_schema_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request with invalid URL schema returns None"""
def test_get_invalid_schema_returns_none(self, mock_get: Mock):
"""Test GET request with invalid URL schema returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.get("invalid://example.com")
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'get'" in response.message
assert response.action == "get"
assert response.url == "invalid://example.com"
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_timeout_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request timeout returns None"""
def test_get_timeout_returns_none(self, mock_get: Mock):
"""Test GET request timeout returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'get'" in response.message
assert response.action == "get"
assert response.url == "https://api.example.com/data"
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_connection_error_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request connection error returns None"""
def test_get_connection_error_returns_none(self, mock_get: Mock):
"""Test GET request connection error returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'get'" in response.message
assert response.action == "get"
assert response.url == "https://api.example.com/data"
class TestCallerPost:
@@ -200,7 +215,8 @@ class TestCallerPost:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.post')
@@ -234,40 +250,46 @@ class TestCallerPost:
assert mock_post.call_args[1]["json"] == data
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_invalid_schema_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request with invalid URL schema returns None"""
def test_post_invalid_schema_returns_none(self, mock_post: Mock):
"""Test POST request with invalid URL schema returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.post("invalid://example.com", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'post'" in response.message
assert response.action == "post"
assert response.url == "invalid://example.com"
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_timeout_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request timeout returns None"""
def test_post_timeout_returns_none(self, mock_post: Mock):
"""Test POST request timeout returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.post("https://api.example.com/data", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'post'" in response.message
assert response.action == "post"
assert response.url == "https://api.example.com/data"
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_connection_error_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request connection error returns None"""
def test_post_connection_error_returns_none(self, mock_post: Mock):
"""Test POST request connection error returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.post("https://api.example.com/data", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'post'" in response.message
assert response.action == "post"
assert response.url == "https://api.example.com/data"
class TestCallerPut:
@@ -292,7 +314,8 @@ class TestCallerPut:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.put')
@@ -311,16 +334,18 @@ class TestCallerPut:
assert mock_put.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.put')
def test_put_timeout_returns_none(self, mock_put: Mock, capsys: Any):
"""Test PUT request timeout returns None"""
def test_put_timeout_returns_none(self, mock_put: Mock):
"""Test PUT request timeout returns ErrorResponse"""
mock_put.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.put("https://api.example.com/data/1", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'put'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'put'" in response.message
assert response.action == "put"
assert response.url == "https://api.example.com/data/1"
class TestCallerPatch:
@@ -345,7 +370,8 @@ class TestCallerPatch:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.patch')
@@ -364,16 +390,18 @@ class TestCallerPatch:
assert mock_patch.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.patch')
def test_patch_connection_error_returns_none(self, mock_patch: Mock, capsys: Any):
"""Test PATCH request connection error returns None"""
def test_patch_connection_error_returns_none(self, mock_patch: Mock):
"""Test PATCH request connection error returns ErrorResponse"""
mock_patch.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.patch("https://api.example.com/data/1", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'patch'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'patch'" in response.message
assert response.action == "patch"
assert response.url == "https://api.example.com/data/1"
class TestCallerDelete:
@@ -396,7 +424,8 @@ class TestCallerDelete:
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.delete')
@@ -414,16 +443,18 @@ class TestCallerDelete:
assert mock_delete.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.delete')
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock, capsys: Any):
"""Test DELETE request with invalid URL schema returns None"""
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock):
"""Test DELETE request with invalid URL schema returns ErrorResponse"""
mock_delete.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.delete("invalid://example.com/data/1")
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'delete'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'delete'" in response.message
assert response.action == "delete"
assert response.url == "invalid://example.com/data/1"
class TestCallerParametrized:
@@ -492,7 +523,7 @@ class TestCallerParametrized:
])
@patch('corelibs.requests_handling.caller.requests.get')
def test_exception_handling(
self, mock_get: Mock, exception_class: type, expected_message: str, capsys: Any
self, mock_get: Mock, exception_class: type, expected_message: str
):
"""Test exception handling for all exception types"""
mock_get.side_effect = exception_class("Test error")
@@ -500,9 +531,8 @@ class TestCallerParametrized:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert expected_message in captured.out
assert isinstance(response, ErrorResponse)
assert expected_message in response.message
class TestCallerIntegration:
@@ -599,7 +629,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.post')
@@ -659,7 +690,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -679,7 +711,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
def test_timeout_zero(self):
@@ -730,9 +763,10 @@ class TestCallerProxyHandling:
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
proxy = {
"http": "http://proxy.example.com:8080",
"https": "https://proxy.example.com:8080"
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
@@ -746,9 +780,10 @@ class TestCallerProxyHandling:
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
proxy = {
"http": "http://user:pass@proxy.example.com:8080",
"https": "https://user:pass@proxy.example.com:8080"
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.post("https://api.example.com/data", data={"test": "data"})
@@ -789,7 +824,7 @@ class TestCallerResponseHandling:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert not isinstance(response, ErrorResponse)
assert response.status_code == 200
assert response.text == "Success"
assert response.json() == {"status": "ok"}
@@ -805,7 +840,7 @@ class TestCallerResponseHandling:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert not isinstance(response, ErrorResponse)
assert response.status_code == status_code