Compare commits

..

39 Commits

Author SHA1 Message Date
Clemens Schwaighofer
28527990e9 Move progres and script helpers to corelibs-progress and corelibs-script 2026-02-06 15:55:26 +09:00
Clemens Schwaighofer
b58a26f79a Move all string support functions to corelibs_strings and other modules 2026-02-06 08:49:44 +09:00
Clemens Schwaighofer
8bb4a202cd Move CSV handling to corelibs-csv 2026-02-05 10:48:57 +09:00
Clemens Schwaighofer
f265b55ef8 Move requests handling to corelibs_requests module 2026-02-04 14:55:39 +09:00
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
Clemens Schwaighofer
3d091129e2 v0.46.0: Add unique list helper function 2026-01-27 14:43:35 +09:00
Clemens Schwaighofer
1a978f786d Add a list helper to create unique list of dictionaries and tests for it. 2026-01-27 14:42:19 +09:00
Clemens Schwaighofer
51669d3c5f Settings loader test-run add boolean convert check test 2026-01-23 18:07:52 +09:00
Clemens Schwaighofer
d128dcb479 v0.45.1: Fix Log with log console format set to None 2026-01-23 15:16:38 +09:00
Clemens Schwaighofer
84286593f6 Log fix bug where log consosle format set to None would throw an exception
Also add prefix "[SettingsLoader] " to print statements in SettingsLoader if we do not write to log
2026-01-23 15:14:31 +09:00
Clemens Schwaighofer
8d97f09e5e v0.45.0: Log add function to get console formatter flags set 2026-01-23 11:37:02 +09:00
Clemens Schwaighofer
2748bc19be Log, add get console formatter method
Returns current flags set for console formatter
2026-01-23 11:33:38 +09:00
Clemens Schwaighofer
0b3c8fc774 v0.44.2: Move the compiled regex into dedicated file 2026-01-09 16:17:27 +09:00
Clemens Schwaighofer
7da18e0f00 Moved the compiled regex patterns to a new file regex_constants_compiled
So we do not force the compiled build if not needed
2026-01-09 16:15:38 +09:00
Clemens Schwaighofer
49e38081ad v0.44.1: add pre compiled regexes 2026-01-08 15:16:26 +09:00
Clemens Schwaighofer
a14f993a31 Add pre-compiled REGEX entries to the regex pattern file
compiled ones hare prefixed with COMPILED_
2026-01-08 15:14:48 +09:00
Clemens Schwaighofer
ae938f9909 v0.44.0: Add more REGEX patters for email matching 2026-01-08 14:59:49 +09:00
Clemens Schwaighofer
f91e0bb93a Add new regex constants for email handling and update related tests 2026-01-08 14:58:14 +09:00
Clemens Schwaighofer
d3f61005cf v0.43.4: Fix for config loader with empty to split into lists values 2026-01-06 10:04:03 +09:00
Clemens Schwaighofer
2923a3e88b Fix settings loader to return empty list when splitting empty string value 2026-01-06 09:58:21 +09:00
Clemens Schwaighofer
a73ced0067 v0.43.3: settings loader raise exception and log message text split 2025-12-24 10:25:42 +09:00
Clemens Schwaighofer
f89b91fe7f Settings loader different log string to value error raise string 2025-12-24 10:23:27 +09:00
Clemens Schwaighofer
5950485d46 v0.43.2: add error message list reset to settings loader 2025-12-24 10:18:54 +09:00
Clemens Schwaighofer
f349927a63 Reset error message list in settings loader 2025-12-24 10:14:54 +09:00
Clemens Schwaighofer
dfe8890598 v0.43.1: settings loader update for error reporting on exception raise 2025-12-24 10:09:53 +09:00
Clemens Schwaighofer
d224876a8e Settings loader, pass error messages to exception raise
So we can get the actual error message in the exception if logging is all off
2025-12-24 10:08:38 +09:00
85 changed files with 1007 additions and 15248 deletions

View File

@@ -47,10 +47,41 @@ This is a pip package that can be installed into any project and covers the foll
All content in this module will move to stand alone libraries, as of now the following entries have moved and will throw deprecated warnings if used
- check_handling.regex_constants_compiled: corelibs-regex-checks
- check_handling.regex_constants: corelibs-regex-checks
- csv_handling.csv_interface: corelibs-csv
- datetime_handling.datetime_helpers: corelibs-datetime
- datetime_handling.timestamp_convert: corelibs-datetime
- datetime_handling.timestamp_strings: corelibs-datetime
- debug_handling.debug_helpers: corelibs-stack-trace
- debug_handling.dump_data: corelibs-dump-data
- debug_handling.profiling: corelibs-debug
- debug_handling.timer: corelibs-debug
- debug_handling.writeline: corelibs-debug
- encryption_handling.symmetrix_encryption: corelibs-encryption
- exceptions.csv_exceptions: orelibs-csv
- file_handling.file_bom_encoding: corelibs-file
- file_handling.file_crc: corelibs-file
- file_handling.file_handling: corelibs-file
- iterator_handling.data_search: corelibs-search
- iterator_handling.dict_helpers: corelibs-iterator
- iterator_handling.dict_mask: corelibs-dump-data
- iterator_handling.fingerprint: corelibs-hash
- iterator_handling.list_helpers: corelibs-iterator
- json_handling.jmespath_helper: corelibs-search
- json_handling.json_helper: corelibs-json
- math_handling.math_helpers: python.math
- requests_handling.auth_helpers: corelibs-requests
- requests_handling.caller: corelibs-requests
- script_handling.progress: corelibs-progress
- script_handling.script_helpers: corelibs-script
- string_handling.byte_helpers: corelibs-strings
- string_handling.double_byte_string_format: corelibs-double-byte-format
- string_handling.hash_helpers: corelibs-hash
- string_handling.string_helpers: corelibs-strings
- string_handling.text_colors: corelibs-text-colors
- var_handling.enum_base: corelibs-enum-base
- var_handling.var_helpers: corelibs-var
- datetime_handling: corelibs-datetime
- string_handling.text_colors: corelibs-text-colors
## UV setup

View File

@@ -1,13 +1,29 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.43.0"
version = "0.48.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-csv>=1.0.0",
"corelibs-datetime>=1.0.1",
"corelibs-debug>=1.0.0",
"corelibs-double-byte-format>=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-progress>=1.0.0",
"corelibs-regex-checks>=1.0.0",
"corelibs-requests>=1.0.0",
"corelibs-script>=1.0.0",
"corelibs-search>=1.0.0",
"corelibs-stack-trace>=1.0.0",
"corelibs-strings>=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,23 +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
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}$
"""
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 = NAME_EMAIL_SIMPLE_REGEX_NG
# name + email with the basic regex set
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

@@ -0,0 +1,27 @@
"""
List of regex compiled strings that can be used
"""
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 = 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

@@ -53,6 +53,9 @@ class SettingsLoader:
# for check settings, abort flag
self.__check_settings_abort: bool = False
# error messages for raise ValueError
self.__error_msg: list[str] = []
# MARK: load settings
def load_settings(
self,
@@ -87,6 +90,8 @@ class SettingsLoader:
Returns:
dict[str, str]: key = value list
"""
# reset error message list before run
self.__error_msg = []
# default set entries
entry_set_empty: dict[str, str | None] = {}
# entries that have to be split
@@ -168,10 +173,13 @@ class SettingsLoader:
args_overrride.append(key)
if skip:
continue
settings[config_id][key] = [
__value.replace(" ", "")
for __value in settings[config_id][key].split(split_char)
]
if settings[config_id][key]:
settings[config_id][key] = [
__value.replace(" ", "")
for __value in settings[config_id][key].split(split_char)
]
else:
settings[config_id][key] = []
except KeyError as e:
raise ValueError(self.__print(
f"[!] Cannot read [{config_id}] block because the entry [{e}] could not be found",
@@ -273,7 +281,10 @@ class SettingsLoader:
error = True
self.__print(f"[!] Missing content entry for: {entry}", 'ERROR')
if error is True:
raise ValueError(self.__print("[!] Missing or incorrect settings data. Cannot proceed", 'CRITICAL'))
self.__print("[!] Missing or incorrect settings data. Cannot proceed", 'CRITICAL')
raise ValueError(
"Missing or incorrect settings data. Cannot proceed: " + "; ".join(self.__error_msg)
)
# set empty
for [entry, empty_set] in entry_set_empty.items():
# if set, skip, else set to empty value
@@ -566,7 +577,10 @@ class SettingsLoader:
self.log.logger.log(Log.get_log_level_int(level), msg, stacklevel=2)
if self.log is None or self.always_print:
if print_error:
print(msg)
print(f"[SettingsLoader] {msg}")
if level == 'ERROR':
# remove any prefix [!] for error message list
self.__error_msg.append(msg.replace('[!] ', '').strip())
return msg

View File

@@ -3,168 +3,37 @@ Write to CSV file
- each class set is one file write with one header set
"""
from typing import Any, Sequence
from pathlib import Path
from collections import Counter
import csv
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
from corelibs.exceptions.csv_exceptions import (
NoCsvReader, CompulsoryCsvHeaderCheckFailed, CsvHeaderDataMissing
from warnings import warn
from corelibs_csv.csv_interface import (
CsvReader as CoreLibsCsvReader, CsvWriter as CoreLibsCsvWriter,
ENCODING as CoreLibsEncoding,
ENCODING_UTF8_SIG as CoreLibsEncodingUtf8Sig,
DELIMITER as CoreLibsDelimiter,
QUOTECHAR as CoreLibsQuotechar,
QUOTING as CoreLibsQuoting
)
ENCODING = 'utf-8'
ENCODING_UTF8_SIG = 'utf-8-sig'
DELIMITER = ","
QUOTECHAR = '"'
ENCODING = CoreLibsEncoding
ENCODING_UTF8_SIG = CoreLibsEncodingUtf8Sig
DELIMITER = CoreLibsDelimiter
QUOTECHAR = CoreLibsQuotechar
# type: _QuotingType
QUOTING = csv.QUOTE_MINIMAL
QUOTING = CoreLibsQuoting
class CsvWriter:
class CsvWriter(CoreLibsCsvWriter):
"""
write to a CSV file
"""
def __init__(
self,
file_name: Path,
header_mapping: dict[str, str],
header_order: list[str] | None = None,
encoding: str = ENCODING,
delimiter: str = DELIMITER,
quotechar: str = QUOTECHAR,
quoting: Any = QUOTING,
):
self.__file_name = file_name
# Key: index for write for the line dict, Values: header entries
self.header_mapping = header_mapping
self.header: Sequence[str] = list(header_mapping.values())
self.__delimiter = delimiter
self.__quotechar = quotechar
self.__quoting = quoting
self.__encoding = encoding
self.csv_file_writer = self.__open_csv(header_order)
def __open_csv(self, header_order: list[str] | None) -> csv.DictWriter[str]:
"""
open csv file for writing, write headers
Note that if there is no header_order set we use the order in header dictionary
Arguments:
line {list[str] | None} -- optional dedicated header order
Returns:
csv.DictWriter[str] | None: _description_
"""
# if header order is set, make sure all header value fields exist
if not self.header:
raise CsvHeaderDataMissing("No header data available to write CSV file")
header_values = self.header
if header_order is not None:
if Counter(header_values) != Counter(header_order):
raise CompulsoryCsvHeaderCheckFailed(
"header order does not match header values: "
f"{', '.join(header_values)} != {', '.join(header_order)}"
)
header_values = header_order
# no duplicates
if len(header_values) != len(set(header_values)):
raise CompulsoryCsvHeaderCheckFailed(f"Header must have unique values only: {', '.join(header_values)}")
try:
fp = open(
self.__file_name,
"w",
encoding=self.__encoding
)
csv_file_writer = csv.DictWriter(
fp,
fieldnames=header_values,
delimiter=self.__delimiter,
quotechar=self.__quotechar,
quoting=self.__quoting,
)
csv_file_writer.writeheader()
return csv_file_writer
except OSError as err:
raise NoCsvReader(f"Could not open CSV file for writing: {err}") from err
def write_csv(self, line: dict[str, str]) -> None:
"""
write member csv line
Arguments:
line {dict[str, str]} -- _description_
Returns:
bool -- _description_
"""
csv_row: dict[str, Any] = {}
# only write entries that are in the header list
for key, value in self.header_mapping.items():
csv_row[value] = line[key]
self.csv_file_writer.writerow(csv_row)
class CsvReader:
class CsvReader(CoreLibsCsvReader):
"""
read from a CSV file
"""
def __init__(
self,
file_name: Path,
header_check: Sequence[str] | None = None,
encoding: str = ENCODING,
delimiter: str = DELIMITER,
quotechar: str = QUOTECHAR,
quoting: Any = QUOTING,
):
self.__file_name = file_name
self.__header_check = header_check
self.__delimiter = delimiter
self.__quotechar = quotechar
self.__quoting = quoting
self.__encoding = encoding
self.header: Sequence[str] | None = None
self.csv_file_reader = self.__open_csv()
def __open_csv(self) -> csv.DictReader[str]:
"""
open csv file for reading
Returns:
csv.DictReader | None: _description_
"""
try:
# if UTF style check if this is BOM
if self.__encoding.lower().startswith('utf-') and is_bom_encoded(self.__file_name):
bom_info = is_bom_encoded_info(self.__file_name)
if bom_info['encoding'] == 'utf-8':
self.__encoding = ENCODING_UTF8_SIG
else:
self.__encoding = bom_info['encoding'] or self.__encoding
fp = open(
self.__file_name,
"r", encoding=self.__encoding
)
csv_file_reader = csv.DictReader(
fp,
delimiter=self.__delimiter,
quotechar=self.__quotechar,
quoting=self.__quoting,
)
self.header = csv_file_reader.fieldnames
if not self.header:
raise CsvHeaderDataMissing("No header data available in CSV file")
if self.__header_check is not None:
header_diff = set(self.__header_check).difference(set(self.header or []))
if header_diff:
raise CompulsoryCsvHeaderCheckFailed(
f"CSV header does not match expected header: {', '.join(header_diff)} missing"
)
return csv_file_reader
except OSError as err:
raise NoCsvReader(f"Could not open CSV file for reading: {err}") from err
warn("Use corelibs_csv.csv_interface 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,6 +4,8 @@ Send email wrapper
import smtplib
from email.message import EmailMessage
from email.header import Header
from email.utils import formataddr, parseaddr
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger
@@ -133,21 +135,30 @@ class SendEmail:
_subject = template["subject"]
_body = template["body"]
for key, value in replace.items():
_subject = _subject.replace(f"{{{{{key}}}}}", value)
_body = _body.replace(f"{{{{{key}}}}}", value)
placeholder = f"{{{{{key}}}}}"
_subject = _subject.replace(placeholder, value)
_body = _body.replace(placeholder, value)
name, addr = parseaddr(from_email)
if name:
# Encode the name part with MIME encoding
encoded_name = str(Header(name, 'utf-8'))
from_email_encoded = formataddr((encoded_name, addr))
else:
from_email_encoded = from_email
# create a simple email and add subhect, from email
msg_email = EmailMessage()
# msg.set_content(_body, charset='utf-8', cte='quoted-printable')
msg_email.set_content(_body, charset="utf-8")
msg_email["Subject"] = _subject
msg_email["From"] = from_email
msg_email["From"] = from_email_encoded
# push to array for sening
msg.append(msg_email)
return msg
def send_email_list(
self,
email: list[EmailMessage], receivers: list[str],
emails: list[EmailMessage],
receivers: list[str],
combined_send: bool | None = None,
test_only: bool | None = None
):
@@ -170,18 +181,27 @@ class SendEmail:
smtp = smtplib.SMTP(smtp_host)
except ConnectionRefusedError as e:
self.log.error("Could not open SMTP connection to: %s, %s", smtp_host, e)
# prepare receiver list
receivers_encoded: list[str] = []
for __receiver in receivers:
to_name, to_addr = parseaddr(__receiver)
if to_name:
# Encode the name part with MIME encoding
encoded_to_name = str(Header(to_name, 'utf-8'))
receivers_encoded.append(formataddr((encoded_to_name, to_addr)))
else:
receivers_encoded.append(__receiver)
# loop over messages and then over recievers
for msg in email:
for msg in emails:
if combined_send is True:
msg["To"] = ", ".join(receivers)
msg["To"] = ", ".join(receivers_encoded)
if not self.settings.get('test'):
if smtp is not None:
smtp.send_message(msg, msg["From"], receivers)
smtp.send_message(msg, msg["From"], receivers_encoded)
else:
self.log.info(f"[EMAIL] Test, not sending email\n{msg}")
else:
for receiver in receivers:
# send to
for receiver in receivers_encoded:
self.log.debug(f"===> Send to: {receiver}")
if "To" in msg:
msg.replace_header("To", receiver)

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,22 +2,39 @@
Exceptions for csv file reading and processing
"""
from warnings import warn
from corelibs_csv.csv_exceptions import (
NoCsvReader as CoreLibsNoCsvReader,
CompulsoryCsvHeaderCheckFailed as CoreLibsCompulsoryCsvHeaderCheckFailed,
CsvHeaderDataMissing as CoreLibsCsvHeaderDataMissing,
CsvRowDataKeysNotMatching as CoreLibsCsvRowDataKeysNotMatching
)
class NoCsvReader(Exception):
class NoCsvReader(CoreLibsNoCsvReader):
"""
CSV reader is none
"""
class CsvHeaderDataMissing(Exception):
class CsvHeaderDataMissing(CoreLibsCsvHeaderDataMissing):
"""
The csv reader returned None as headers, the header column in the csv file is missing
"""
class CompulsoryCsvHeaderCheckFailed(Exception):
class CompulsoryCsvHeaderCheckFailed(CoreLibsCompulsoryCsvHeaderCheckFailed):
"""
raise if the header is not matching to the excpeted values
"""
class CsvRowDataKeysNotMatching(CoreLibsCsvRowDataKeysNotMatching):
"""
raise if the row data keys do not match the expected header keys
"""
warn("Use corelibs_csv.csv_exceptions 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,13 +14,14 @@ 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,
mask_str: str = "***",
mask_str_edges: str = '_',
skip: bool = False
) -> dict[str, Any]:
) -> dict[str, Any] | list[Any]:
"""
mask data for output
Checks if mask_keys list exist in any key in the data set either from the start or at the end
@@ -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,9 +2,16 @@
List type helpers
"""
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]]:
@@ -17,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]]
@@ -37,11 +43,20 @@ 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)
return corelibs_is_list_in_list(list_a, list_b)
# Get the difference and extract just the values
return [item for item, _ in set_a - set_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
Arguments:
dict_list {list[Any]} -- _description_
Returns:
list[Any] -- _description_
"""
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
@@ -602,9 +602,9 @@ class Log(LogParent):
__setting = self.DEFAULT_LOG_SETTINGS.get(__log_entry, True)
default_log_settings[__log_entry] = __setting
# check console log type
default_log_settings['console_format_type'] = cast('ConsoleFormat', log_settings.get(
'console_format_type', self.DEFAULT_LOG_SETTINGS['console_format_type']
))
if (console_format_type := log_settings.get('console_format_type')) is None:
console_format_type = self.DEFAULT_LOG_SETTINGS['console_format_type']
default_log_settings['console_format_type'] = cast('ConsoleFormat', console_format_type)
# check log queue
__setting = log_settings.get('log_queue', self.DEFAULT_LOG_SETTINGS['log_queue'])
if __setting is not None:
@@ -774,6 +774,16 @@ class Log(LogParent):
self.__set_console_formatter(console_format_type)
)
def get_console_formatter(self) -> ConsoleFormat:
"""
Get the current console formatter, this the settings type
Note that if eg "ALL" is set it will return the combined information but not the ALL flag name itself
Returns:
ConsoleFormat -- _description_
"""
return self.log_settings['console_format_type']
# MARK: console handler
def __create_console_handler(
self, handler_name: str,

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

@@ -2,9 +2,11 @@
Various HTTP auth helpers
"""
from base64 import b64encode
from warnings import deprecated
from corelibs_requests.auth_helpers import basic_auth as corelibs_basic_auth
@deprecated("use corelibs_requests.auth_helpers.basic_auth instead")
def basic_auth(username: str, password: str) -> str:
"""
setup basic auth, for debug
@@ -16,5 +18,6 @@ def basic_auth(username: str, password: str) -> str:
Returns:
str -- _description_
"""
token = b64encode(f"{username}:{password}".encode('utf-8')).decode("ascii")
return f'Basic {token}'
return corelibs_basic_auth(username, password)
# __END__

View File

@@ -3,189 +3,35 @@ requests lib interface
V2 call type
"""
from typing import Any
import warnings
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 warnings import warn
from corelibs_requests.caller import (
Caller as CoreLibsCaller,
ProxyConfig as CoreLibsProxyConfig,
ErrorResponse as CoreLibsErrorResponse
)
class Caller:
"""_summary_"""
class ErrorResponse(CoreLibsErrorResponse):
"""
Error response structure. This is returned if a request could not be completed
"""
def __init__(
self,
header: dict[str, str],
verify: bool = True,
timeout: int = 20,
proxy: dict[str, str] | None = None,
ca_file: str | None = None
):
self.headers = header
self.timeout: int = timeout
self.cafile = ca_file
self.verify = verify
self.proxy = proxy
def __timeout(self, timeout: int | None) -> int:
if timeout is not None:
return timeout
return self.timeout
class ProxyConfig(CoreLibsProxyConfig):
"""
Socks proxy settings
"""
def __call(
self,
action: str,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | None:
"""
call wrapper, on error returns None
Args:
action (str): _description_
url (str): _description_
data (dict | None): _description_. Defaults to None.
params (dict | None): _description_. Defaults to None.
class Caller(CoreLibsCaller):
"""
requests lib interface
"""
Returns:
requests.Response | None: _description_
"""
if data is None:
data = {}
try:
response = None
if action == "get":
response = requests.get(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "post":
response = requests.post(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "put":
response = requests.put(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "patch":
response = requests.patch(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "delete":
response = requests.delete(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
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
def get(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
"""
get data
Args:
url (str): _description_
params (dict | None): _description_
Returns:
requests.Response: _description_
"""
return self.__call('get', url, params=params)
def post(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""
post data
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('post', url, data, params)
def put(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""_summary_
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('put', url, data, params)
def patch(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""_summary_
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('patch', url, data, params)
def delete(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
"""
delete
Args:
url (str): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('delete', url, params=params)
warn(
"corelibs.requests_handling.caller is deprecated, use corelibs_requests.caller instead",
DeprecationWarning, stacklevel=2
)
# __END__

View File

@@ -29,449 +29,16 @@ set_end_time(time optional)
show_position(file pos optional)
"""
import time
from typing import Literal
from math import floor
from corelibs_datetime.timestamp_convert import convert_timestamp
from corelibs.string_handling.byte_helpers import format_bytes
from warnings import warn
from corelibs_progress.progress import Progress as CoreProgress # for type checking only
class Progress():
class Progress(CoreProgress):
"""
file progress output information
"""
def __init__(
self,
verbose: int = 0,
precision: int = 1,
microtime: Literal[-1] | Literal[1] | Literal[0] = 0,
wide_time: bool = False,
prefix_lb: bool = False
):
# set default var stuff
# max lines in input
self.linecount: int = 0
# max file size
self.filesize: int = 0
# * comma after percent
self.precision: int = 0
# * if flagged 1, then wthe wide 15 char left bound format is used
self.wide_time: bool = False
# * verbose status from outside
self.verbose: bool = False
# * microtime output for last run time (1 for enable 0 for auto -1 for disable)
self.microtime: Literal[-1] | Literal[1] | Literal[0] = 0
# micro time flag for last group
self.lg_microtime: bool = False
# = flag if output was given
self.change = 0
# = global start for the full script running time
self.start: float | None = None
# = for the eta time, can be set after a query or long read in, to not create a wrong ETA time
self.start_run: float | None = None
# loop start
self.start_time: float | None = None
# global end
self.end: float | None = None
# loop end
self.end_time: float | None = None
# run time in seconds, set when end time method is called
self.run_time: float | None = None
# = filesize current
self.count_size: int | None = None
# position current
self.count: int = 0
# last count (position)
self.current_count: int = 0
# the current file post
self.file_pos: int | None = None
# lines processed in the last run
self.lines_processed: int = 0
# time in th seconds for the last group run (until percent change)
self.last_group: float = 0
# float value, lines processed per second to the last group run
self.lines_in_last_group: float = 0
# float values, lines processed per second to complete run
self.lines_in_global: float = 0
# flaot value, bytes processes per second in the last group run
self.bytes_in_last_group: float = 0
# float value, bytes processed per second to complete run
self.bytes_in_global: float = 0
# bytes processed in last run (in bytes)
self.size_in_last_group: int = 0
# current file position 8size)
self.current_size: int = 0
# last percent position
self.last_percent: int | float = 0
# if we have normal % or in steps of 10
self.precision_ten_step: int = 0
# the default size this is precision + 4
self.percent_print: int = 5
# this is 1 if it is 1 or 0 for precision or precision size
self.percent_precision: int = 1
# prefix line with a line break
self.prefix_lb: bool = False
# estimated time to finish
self.eta: float | None = None
# run time since start
self.full_time_needed: float | None = None
# the actual output
self.string: str = ''
# initialize the class
self.set_precision(precision)
self.set_verbose(verbose)
self.set_micro_time(microtime)
self.set_wide_time(wide_time)
self.set_prefix_lb(prefix_lb)
self.set_start_time()
def reset(self):
"""
resets the current progress to 0, but keeps the overall start variables set
"""
# reset what always gets reset
self.count = 0
self.count_size = None
self.current_count = 0
self.linecount = 0
self.lines_processed = 0
self.last_group = 0
self.lines_in_last_group = 0
self.lines_in_global = 0
self.bytes_in_last_group = 0
self.bytes_in_global = 0
self.size_in_last_group = 0
self.filesize = 0
self.current_size = 0
self.last_percent = 0
self.eta = 0
self.full_time_needed = 0
self.start_run = None
self.start_time = None
self.end_time = None
def set_wide_time(self, wide_time: bool) -> bool:
"""
sets the show wide time flag
Arguments:
wide_time {bool} -- _description_
Returns:
bool -- _description_
"""
self.wide_time = wide_time
return self.wide_time
def set_micro_time(self, microtime: Literal[-1] | Literal[1] | Literal[0]) -> Literal[-1] | Literal[1] | Literal[0]:
"""sets the show microtime -1 OFF, 0 AUTO, 1 ON
Returns:
_type_ -- _description_
"""
self.microtime = microtime
return self.microtime
def set_prefix_lb(self, prefix_lb: bool) -> bool:
"""
set prefix line break flag
Arguments:
prefix_lb {bool} -- _description_
Returns:
bool -- _description_
"""
self.prefix_lb = prefix_lb
return self.prefix_lb
def set_verbose(self, verbose: int) -> bool:
"""
set the internal verbose flag to 1 if any value higher than 1 is given, else sets it to 0
Arguments:
verbose {int} -- _description_
Returns:
bool -- _description_
"""
if verbose > 0:
self.verbose = True
else:
self.verbose = False
return self.verbose
def set_precision(self, precision: int) -> int:
"""
sets the output precision size. If -2 for five step, -1 for ten step
else sets the precision normally, for 0, no precision is set, maximum precision is 10
Arguments:
precision {int} -- _description_
Returns:
int -- _description_
"""
# if not a valid number, we set it to 0
if precision < -2 or precision > 10:
precision = 0
if precision < 0:
if precision < -1:
self.precision_ten_step = 5
else:
self.precision_ten_step = 10
self.precision = 0 # no comma
self.percent_precision = 0 # no print precision
self.percent_print = 3 # max 3 length
else:
# comma values visible
self.precision = 10 if precision < 0 or precision > 10 else precision
# for calcualtion of precision
self.percent_precision = 10 if precision < 0 or precision > 10 else precision
# for the format output base is 4, plsut he percent precision length
self.percent_print = (3 if precision == 0 else 4) + self.percent_precision
# return the set precision
return self.precision
def set_linecount(self, linecount: int) -> int:
"""
set the maximum lines in this file, if value is smaller than 0 or 0, then it is set to 1
Arguments:
linecount {int} -- _description_
Returns:
int -- _description_
"""
if linecount > 0:
self.linecount = linecount
else:
self.linecount = 1
return self.linecount
def set_filesize(self, filesize: int) -> int:
"""
set the maximum filesize for this file, if value is smaller than 0 or 0, then it is set to 1
Arguments:
filesize {int} -- _description_
Returns:
int -- _description_
"""
if filesize > 0:
self.filesize = filesize
else:
self.filesize = 1
return self.filesize
def set_start_time(self, time_value: float = time.time()) -> None:
"""
initial set of the start times, auto set
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
# avoid possible double set of the original start time
if not self.start:
self.start = time_value
self.start_time = time_value
self.start_run = time_value
def set_eta_start_time(self, time_value: float = time.time()) -> None:
"""
sets the loop % run time, for correct ETA calculation
calls set start time, as the main start time is only set once
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
self.set_start_time(time_value)
def set_end_time(self, time_value: float = time.time()) -> None:
"""
set the end time
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
self.end = time_value
self.end_time = time_value
if self.start is None:
self.start = 0
# the overall run time in micro seconds
self.run_time = self.end - self.start
def show_position(self, filepos: int = 0) -> str:
"""
processes the current position. either based on read the file size pos, or the line count
Keyword Arguments:
filepos {int} -- _description_ (default: {0})
Returns:
str -- _description_
"""
show_filesize = True # if we print from file size or line count
# microtime flags
eta_microtime = False
ftn_microtime = False
lg_microtime = False
# percent precision calc
# _p_spf = "{:." + str(self.precision) + "f}"
# output format for percent
_pr_p_spf = "{:>" + str(self.percent_print) + "." + str(self.percent_precision) + "f}"
# set the linecount precision based on the final linecount, if not, leave it empty
_pr_lc = "{}"
if self.linecount:
_pr_lc = "{:>" + str(len(str(f"{self.linecount:,}"))) + ",}"
# time format, if flag is set, the wide format is used
_pr_tf = "{}"
if self.wide_time:
_pr_tf = "{:>15}"
# count up
self.count += 1
# if we have file pos from parameter
if filepos != 0:
self.file_pos = filepos
else:
# we did not, so we set internal value
self.file_pos = self.count
# we also check if the filesize was set now
if self.filesize == 0:
self.filesize = self.linecount
# set ignore filesize output (no data)
show_filesize = False
# set the count size based on the file pos, is only used if we have filesize
self.count_size = self.file_pos
# do normal or down to 10 (0, 10, ...) %
if self.precision_ten_step:
_percent = int((self.file_pos / float(self.filesize)) * 100)
mod = _percent % self.precision_ten_step
percent = _percent if mod == 0 else self.last_percent
else:
# calc percent
percent = round(((self.file_pos / float(self.filesize)) * 100), self.precision)
# output
if percent != self.last_percent:
self.end_time = time.time() # current time (for loop time)
if self.start is None:
self.start = 0
if self.start_time is None:
self.start_time = 0
# for from the beginning
full_time_needed = self.end_time - self.start # how long from the start
self.last_group = self.end_time - self.start_time # how long for last loop
self.lines_processed = self.count - self.current_count # how many lines processed
# lines in last group
self.lines_in_last_group = (self.lines_processed / self.last_group) if self.last_group else 0
# lines in global
self.lines_in_global = (self.count / full_time_needed) if full_time_needed else 0
# if we have linecount or not
if self.linecount == 0:
full_time_per_line = (full_time_needed if full_time_needed else 1) / self.count_size # how long for all
# estimate for the rest
eta = full_time_per_line * (self.filesize - self.count_size)
else:
# how long for all
full_time_per_line = (full_time_needed if full_time_needed else 1) / self.count
# estimate for the rest
eta = full_time_per_line * (self.linecount - self.count)
# just in case ...
if eta < 0:
eta = 0
# check if to show microtime
# ON
if self.microtime == 1:
eta_microtime = ftn_microtime = lg_microtime = True
# AUTO
if self.microtime == 0:
if eta > 0 and eta < 1:
eta_microtime = True
if full_time_needed > 0 and full_time_needed < 1:
ftn_microtime = True
# pre check last group: if pre comma part is same add microtime anyway
if self.last_group > 0 and self.last_group < 1:
lg_microtime = True
if self.last_group == floor(self.last_group):
lg_microtime = True
self.last_group = floor(self.last_group)
# if with filesize or without
if show_filesize:
# last group size
self.size_in_last_group = self.count_size - self.current_size
# calc kb/s if there is any filesize data
# last group
self.bytes_in_last_group = (self.size_in_last_group / self.last_group) if self.last_group else 0
# global
self.bytes_in_global = (self.count_size / full_time_needed) if full_time_needed else 0
# only used if we run with file size for the next check
self.current_size = self.count_size
if self.verbose >= 1:
self.string = (
f"Processed {_pr_p_spf}% "
"[{} / {}] | "
f"{_pr_lc} / {_pr_lc} Lines | ETA: {_pr_tf} / TR: {_pr_tf} / "
"LR: {:,} "
"lines ({:,}) in {}, {:,.2f} ({:,.2f}) lines/s, {} ({}) b/s"
).format(
float(percent),
format_bytes(self.count_size),
format_bytes(self.filesize),
self.count,
self.linecount,
convert_timestamp(eta, eta_microtime),
convert_timestamp(full_time_needed, ftn_microtime),
self.lines_processed,
self.size_in_last_group,
convert_timestamp(self.last_group, lg_microtime),
self.lines_in_global,
self.lines_in_last_group,
format_bytes(self.bytes_in_global),
format_bytes(self.bytes_in_last_group)
)
else:
if self.verbose >= 1:
self.string = (
f"Processed {_pr_p_spf}% | {_pr_lc} / {_pr_lc} Lines "
f"| ETA: {_pr_tf} / TR: {_pr_tf} / "
"LR: {:,} lines in {}, {:,.2f} ({:,.2f}) lines/s"
).format(
float(percent),
self.count,
self.linecount,
convert_timestamp(eta, eta_microtime),
convert_timestamp(full_time_needed, ftn_microtime),
self.lines_processed,
convert_timestamp(self.last_group, lg_microtime),
self.lines_in_global,
self.lines_in_last_group
)
# prefix return string with line break if flagged
self.string = ("\n" if self.prefix_lb else '') + self.string
# print the string if verbose is turned on
if self.verbose >= 1:
print(self.string)
# write back vars
self.last_percent = percent
self.eta = eta
self.full_time_needed = full_time_needed
self.lg_microtime = lg_microtime
# for the next run, check data
self.start_time = time.time()
self.current_count = self.count
# trigger if this is a change
self.change = 1
else:
# trigger if this is a change
self.change = 0
# return string
return self.string
# } END OF ShowPosition
warn("Use 'corelibs_progress.progress.Progress'", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,13 +2,16 @@
Helper methods for scripts
"""
import time
import os
import sys
from warnings import deprecated
from pathlib import Path
import psutil
from corelibs_script.script_support import (
wait_abort as corelibs_wait_abort,
lock_run as corelibs_lock_run,
unlock_run as corelibs_unlock_run,
)
@deprecated("use corelibs_script.script_support.wait_abort instead")
def wait_abort(sleep: int = 5) -> None:
"""
wait a certain time for an abort command
@@ -16,18 +19,10 @@ def wait_abort(sleep: int = 5) -> None:
Keyword Arguments:
sleep {int} -- _description_ (default: {5})
"""
try:
print(f"Waiting {sleep} seconds (Press CTRL +C to abort) [", end="", flush=True)
for _ in range(1, sleep):
print(".", end="", flush=True)
time.sleep(1)
print("]", flush=True)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
print("\n\n")
corelibs_wait_abort(sleep)
@deprecated("use corelibs_script.script_support.lock_run instead")
def lock_run(lock_file: Path) -> None:
"""
lock a script run
@@ -41,44 +36,10 @@ def lock_run(lock_file: Path) -> None:
Exception: _description_
IOError: _description_
"""
no_file = False
run_pid = os.getpid()
# or os.path.isfile()
try:
with open(lock_file, "r", encoding="UTF-8") as fp:
exists = False
pid = fp.read()
fp.close()
if pid:
# check if this pid exists
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if pid == proc.info['pid']:
exists = True
break
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# in case we cannot access
continue
if not exists:
# no pid but lock file, unlink
try:
lock_file.unlink()
no_file = True
except IOError as e:
raise IOError(f"Cannot remove lock_file: {lock_file}: {e}") from e
else:
raise IOError(f"Script is already running with PID {pid}")
except IOError:
no_file = True
if no_file:
try:
with open(lock_file, "w", encoding="UTF-8") as fp:
fp.write(str(run_pid))
fp.close()
except IOError as e:
raise IOError(f"Cannot open run lock file '{lock_file}' for writing: {e}") from e
corelibs_lock_run(lock_file)
@deprecated("use corelibs_script.script_support.unlock_run instead")
def unlock_run(lock_file: Path) -> None:
"""
removes the lock file
@@ -89,9 +50,6 @@ def unlock_run(lock_file: Path) -> None:
Raises:
Exception: _description_
"""
try:
lock_file.unlink()
except IOError as e:
raise IOError(f"Cannot remove lock_file: {lock_file}: {e}") from e
corelibs_unlock_run(lock_file)
# __END__

View File

@@ -2,7 +2,11 @@
Format bytes
"""
from warnings import deprecated
from corelibs_strings.string_format import format_bytes as corelibs_format_bytes
@deprecated("Use corelibs_strings.string_format.format_bytes instead")
def format_bytes(byte_value: float | int | str) -> str:
"""
Format a byte value to a human readable string
@@ -14,24 +18,9 @@ def format_bytes(byte_value: float | int | str) -> str:
str -- _description_
"""
# if string exit
if isinstance(byte_value, str):
return byte_value
# empty byte value is set to 0
if not byte_value:
byte_value = float(0)
# if not float, convert to flaot
if isinstance(byte_value, int):
byte_value = float(byte_value)
# loop through valid extensions
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
# never go into the negativ and check if it is smaller than next set
# if it is, print out return string
if abs(byte_value) < 1024.0:
return f"{byte_value:,.2f} {unit}"
# divided for the next loop check
byte_value /= 1024.0
# if it is too big, return YB
return f"{byte_value:,.2f} YB"
return corelibs_format_bytes(
byte_value=byte_value,
)
# __NED__

View File

@@ -2,225 +2,18 @@
Format double byte strings to exact length
"""
import unicodedata
from warnings import warn
from corelibs_double_byte_format.double_byte_string_format import (
DoubleByteFormatString as CorelibsDoubleByteFormatString
)
class DoubleByteFormatString:
class DoubleByteFormatString(CorelibsDoubleByteFormatString):
"""
Format a string to exact length
"""
def __init__(
self,
string: str,
cut_length: int,
format_length: int | None = None,
placeholder: str = '..',
format_string: str = '{{:<{len}}}'
):
"""
shorts a string to exact cut length and sets it to format length
after "cut_length" cut the "placeholder" will be added, so that the new cut_length is never
larget than the cut_length given (".." is counted to cut_length)
if format_length if set and outside format_length will be set
the cut_length is adjusted to format_length if the format_length is shorter
Example
"Foo bar baz" 10 charcters -> 5 cut_length -> 10 format_length
"Foo.. "
use class.get_string_short() for cut length shortend string
use class.get_string_short_formated() to get the shorted string to format length padding
creates a class that shortens and sets the format length
to use with a print format run the format needs to be pre set in
the style of {{:<{len}}} style
self.get_string_short_formated() for the "len" parameter
Args:
string (str): string to work with
cut_length (int): width to shorten to
format_length (int | None): format length. Defaults to None
placeholder (str, optional): placeholder to put after shortened string. Defaults to '..'.
format_string (str, optional): format string. Defaults to '{{:<{len}}}'
"""
# output variables
self.string_short: str = ''
self.string_width_value: int = 0
self.string_short_width: int = 0
self.format_length_value: int = 0
# internal varaibles
self.placeholder: str = placeholder
# original string
self.string: str = ''
# width to cut string to
self.cut_length: int = 0
# format length to set to
self.format_length: int = 0
# main string
self.string = str(string)
self.format_string: str = format_string
# if width is > 0 set, else set width of string (fallback)
if cut_length > 0:
self.cut_length = cut_length
elif cut_length <= 0:
self.cut_length = self.__string_width_calc(self.string)
# format length set, if not set or smaller than 0, set to width of string
self.format_length = self.cut_length
if format_length is not None and format_length > 0:
self.format_length = format_length
# check that width is not larger then length if yes, set width to length
self.cut_length = min(self.cut_length, self.format_length)
# process the string shorten and format length calculation
self.process()
def process(self):
"""
runs all the class methods to set string length, the string shortened
and the format length
"""
# call the internal ones to set the data
if self.string:
self.__string_width()
self.__shorten_string()
if self.format_length:
self.__format_length()
def get_string_short(self) -> str:
"""
get the shortend string
Returns:
str -- _description_
"""
return self.string_short
def get_string_short_formated(self, format_string: str = '{{:<{len}}}') -> str:
"""
get the formatted string
Keyword Arguments:
format_string {_type_} -- _description_ (default: {'{{:<{len}}}'})
Returns:
str -- _description_
"""
if not format_string:
format_string = self.format_string
return format_string.format(
len=self.get_format_length()
).format(
self.get_string_short()
)
def get_format_length(self) -> int:
"""
get the format length for outside length set
Returns:
int -- _description_
"""
return self.format_length_value
def get_cut_length(self) -> int:
"""
get the actual cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_cut_length(self) -> int:
"""
get the requested cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_format_length(self) -> int:
"""
get the requested format length
Returns:
int -- _description_
"""
return self.format_length
def __string_width_calc(self, string: str) -> int:
"""
does the actual string width calculation
Args:
string (str): string to calculate from
Returns:
int: stringth width
"""
return sum(1 + (unicodedata.east_asian_width(c) in "WF") for c in string)
def __string_width(self):
"""
calculates the string width based on the characters
this is an internal method and should not be called on itself
"""
# only run if string is set and is valid string
if self.string:
# calculate width. add +1 for each double byte character
self.string_width_value = self.__string_width_calc(self.string)
def __format_length(self):
"""
set the format length based on the length for the format
and the shortend string
this is an internal method and should not be called on itself
"""
if not self.string_short:
self.__shorten_string()
# get correct format length based on string
if (
self.string_short and
self.format_length > 0 and
self.string_short_width > 0
):
# length: format length wanted
# substract the width of the shortend string - the length of the shortend string
self.format_length_value = self.format_length - (self.string_short_width - len(self.string_short))
else:
# if we have nothing to shorten the length, keep the old one
self.format_length_value = self.format_length
def __shorten_string(self):
"""
shorten string down to set width
this is an internal method and should not be called on itself
"""
# set string width if not set
if not self.string_width_value:
self.__string_width()
# if the double byte string width is larger than the wanted width
if self.string_width_value > self.cut_length:
cur_len = 0
self.string_short = ''
for char in str(self.string):
# set the current length if we add the character
cur_len += 2 if unicodedata.east_asian_width(char) in "WF" else 1
# if the new length is smaller than the output length to shorten too add the char
if cur_len <= (self.cut_length - len(self.placeholder)):
self.string_short += char
self.string_short_width = cur_len
# return string with new width and placeholder
self.string_short = f"{self.string_short}{self.placeholder}"
self.string_short_width += len(self.placeholder)
else:
# if string is same saze just copy
self.string_short = self.string
warn("Use 'corelibs_double_byte_format.double_byte_string_format' instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,10 +2,11 @@
Various hash helpers for strings and things
"""
import re
import hashlib
from warnings import deprecated
from corelibs_hash.string_hash import crc32b_fix as corelibs_crc32b_fix, sha1_short as corelibs_sha1_short
@deprecated("Use corelibs_hash.string_hash.crc32b_fix instead")
def crc32b_fix(crc: str) -> str:
"""
fix a CRC32B with wrong order (from old PHP)
@@ -16,15 +17,10 @@ def crc32b_fix(crc: str) -> str:
Returns:
str -- _description_
"""
# left pad with 0 to 8 chars
crc = ("0" * (8 - len(crc))) + crc
# flip two chars (byte hex)
crc = re.sub(
r"^([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})$", r"\4\3\2\1", crc
)
return crc
return corelibs_crc32b_fix(crc)
@deprecated("Use corelibs_hash.string_hash.sha1_short instead")
def sha1_short(string: str) -> str:
"""
Return a 9 character long SHA1 part
@@ -35,6 +31,6 @@ def sha1_short(string: str) -> str:
Returns:
str -- _description_
"""
return hashlib.sha1(string.encode('utf-8')).hexdigest()[:9]
return corelibs_sha1_short(string)
# __END__

View File

@@ -2,11 +2,16 @@
String helpers
"""
import re
from decimal import Decimal, getcontext
from textwrap import shorten
from warnings import deprecated
from corelibs_strings.string_support import (
shorten_string as corelibs_shorten_string,
left_fill as corelibs_left_fill,
prepare_url_slash as corelibs_prepare_url_slash,
)
from corelibs_strings.string_format import format_number as corelibs_format_number
@deprecated("Use corelibs_strings.string_support.shorten_string instead")
def shorten_string(
string: str | int | float, length: int, hard_shorten: bool = False, placeholder: str = " [~]"
) -> str:
@@ -23,25 +28,15 @@ def shorten_string(
Returns:
str: _description_
"""
string = str(string)
# if placeholder > lenght
if len(string) > length:
if hard_shorten is True or " " not in string:
# hard shorten error
if len(placeholder) > length:
raise ValueError(f"Cannot shorten string: placeholder {placeholder} is too large for max width")
short_string = f"{string[:(length - len(placeholder))]}{placeholder}"
else:
try:
short_string = shorten(string, width=length, placeholder=placeholder)
except ValueError as e:
raise ValueError(f"Cannot shorten string: {e}") from e
else:
short_string = string
return short_string
return corelibs_shorten_string(
string=string,
length=length,
hard_shorten=hard_shorten,
placeholder=placeholder,
)
@deprecated("Use corelibs_strings.string_support.left_fill instead")
def left_fill(string: str, width: int, char: str = " ") -> str:
"""
left fill for a certain length to fill a max size
@@ -58,19 +53,14 @@ def left_fill(string: str, width: int, char: str = " ") -> str:
Returns:
str -- _description_
"""
# the width needs to be string
if width < 0:
width = len(string)
# char can only be one length long
if len(char) != 1:
char = " "
return (
"{:"
f"{char}>{width}"
"}"
).format(string)
return corelibs_left_fill(
string=string,
width=width,
char=char,
)
@deprecated("Use corelibs_strings.string_format.format_number instead")
def format_number(number: float, precision: int = 0) -> str:
"""
format numbers, current trailing zeros does not work
@@ -88,21 +78,13 @@ def format_number(number: float, precision: int = 0) -> str:
Returns:
str -- _description_
"""
if precision < 0 or precision > 100:
precision = 0
if precision > 0:
getcontext().prec = precision
# make it a string to avoid mangling
_number = Decimal(str(number))
else:
_number = number
return (
"{:,."
f"{str(precision)}"
"f}"
).format(_number)
return corelibs_format_number(
number=number,
precision=precision,
)
@deprecated("Use corelibs_strings.string_support.prepare_url_slash instead")
def prepare_url_slash(url: str) -> str:
"""
if the URL does not start with /, add slash
@@ -114,9 +96,8 @@ def prepare_url_slash(url: str) -> str:
Returns:
str -- _description_
"""
url = re.sub(r'\/+', '/', url)
if not url.startswith("/"):
url = "/" + url
return url
return corelibs_prepare_url_slash(
url=url,
)
# __END__

View File

@@ -2,14 +2,28 @@
Test check andling for regex checks
"""
import re
from corelibs.check_handling.regex_constants import DOMAIN_WITH_LOCALHOST_REGEX
from corelibs_text_colors.text_colors import Colors
from corelibs.check_handling.regex_constants import (
compile_re, DOMAIN_WITH_LOCALHOST_REGEX, EMAIL_BASIC_REGEX, NAME_EMAIL_BASIC_REGEX, SUB_EMAIL_BASIC_REGEX
)
from corelibs.check_handling.regex_constants_compiled import (
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX, COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX, COMPILED_NAME_EMAIL_BASIC_REGEX
)
NAME_EMAIL_SIMPLE_REGEX = r"""
^\s*(?:"(?P<name1>[^"]+)"\s*<(?P<email1>[^>]+)>|
(?P<name2>.+?)\s*<(?P<email2>[^>]+)>|
<(?P<email3>[^>]+)>|
(?P<email4>[^\s<>]+))\s*$
"""
def main():
def domain_test():
"""
Test regex checks
domain regex test
"""
print("=" * 30)
test_domains = [
"example.com",
"localhost",
@@ -18,7 +32,7 @@ def main():
"some-domain.org"
]
regex_domain_check = re.compile(DOMAIN_WITH_LOCALHOST_REGEX)
regex_domain_check = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
print(f"REGEX: {DOMAIN_WITH_LOCALHOST_REGEX}")
print(f"Check regex: {regex_domain_check.search('localhost')}")
@@ -29,6 +43,66 @@ def main():
print(f"Did not match: {domain}")
def email_test():
"""
email regex test
"""
print("=" * 30)
email_list = """
e@bar.com
<f@foobar.com>
"Master" <foobar@bar.com>
"not valid" not@valid.com
also not valid not@valid.com
some header <something@bar.com>
test master <master@master.com>
日本語 <japan@jp.net>
"ひほん カケ苦" <foo@bar.com>
single@entry.com
arsch@popsch.com
test open <open@open.com>
"""
print(f"REGEX: SUB_EMAIL_BASIC_REGEX: {SUB_EMAIL_BASIC_REGEX}")
print(f"REGEX: EMAIL_BASIC_REGEX: {EMAIL_BASIC_REGEX}")
print(f"REGEX: COMPILED_NAME_EMAIL_SIMPLE_REGEX: {COMPILED_NAME_EMAIL_SIMPLE_REGEX}")
print(f"REGEX: NAME_EMAIL_BASIC_REGEX: {NAME_EMAIL_BASIC_REGEX}")
basic_email = COMPILED_EMAIL_BASIC_REGEX
sub_basic_email = compile_re(SUB_EMAIL_BASIC_REGEX)
simple_name_email_regex = COMPILED_NAME_EMAIL_SIMPLE_REGEX
full_name_email_regex = COMPILED_NAME_EMAIL_BASIC_REGEX
for email in email_list.splitlines():
email = email.strip()
if not email:
continue
print(f">>> Testing: {email}")
if not basic_email.match(email):
print(f"{Colors.red}[EMAIL ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[EMAIL ] Matched : {email}{Colors.reset}")
if not sub_basic_email.match(email):
print(f"{Colors.red}[SUB ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[SUB ] Matched : {email}{Colors.reset}")
if not simple_name_email_regex.match(email):
print(f"{Colors.red}[SIMPLE] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[SIMPLE] Matched : {email}{Colors.reset}")
if not full_name_email_regex.match(email):
print(f"{Colors.red}[FULL ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[FULL ] Matched : {email}{Colors.reset}")
def main():
"""
Test regex checks
"""
domain_test()
email_test()
if __name__ == "__main__":
main()

View File

@@ -12,10 +12,12 @@ some_match_list=foo,bar
test_list=a,b,c,d f, g h
other_list=a|b|c|d|
third_list=xy|ab|df|fg
empty_list=
str_length=foobar
int_range=20
int_range_not_set=
int_range_not_set_empty_set=5
bool_var=True
#
match_target=foo
match_target_list=foo,bar,baz
@@ -37,3 +39,6 @@ email_bad=gii@bar.com
[LoadTest]
a.b.c=foo
d:e:f=bar
[ErrorTest]
some_value=42

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
@@ -21,11 +21,6 @@ def main():
Main run
"""
value = "2025/1/1"
regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE)
result = regex_c.search(value)
print(f"regex {regex_c} check against {value} -> {result}")
# for log testing
log = Log(
log_path=ROOT_PATH.joinpath(LOG_DIR, 'settings_loader.log'),
@@ -37,6 +32,11 @@ def main():
)
log.logger.info('Settings loader')
value = "2025/1/1"
regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE)
result = regex_c.search(value)
log.info(f"regex {regex_c} check against {value} -> {result}")
sl = SettingsLoader(
{
'overload_from_args': 'OVERLOAD from ARGS',
@@ -69,6 +69,9 @@ def main():
"split:|",
"check:string.alphanumeric"
],
"empty_list": [
"split:,",
],
"str_length": [
"length:2-10"
],
@@ -81,6 +84,7 @@ def main():
"int_range_not_set_empty_set": [
"empty:"
],
"bool_var": ["convert:bool"],
"match_target": ["matching:foo"],
"match_target_list": ["split:,", "matching:foo|bar|baz",],
"match_source_a": ["in:match_target"],
@@ -125,6 +129,20 @@ def main():
except ValueError as e:
print(f"Could not load settings: {e}")
try:
config_load = 'ErrorTest'
config_data = sl.load_settings(
config_load,
{
"some_value": [
"check:string.email.basic",
],
}
)
print(f"[{config_load}] Load: {config_load} -> {dump_data(config_data)}")
except ValueError as e:
print(f"Could not load settings: {e}")
if __name__ == "__main__":
main()

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
@@ -24,12 +24,19 @@ def main() -> None:
"lookup_value_c": "B02",
"replace_value": "R02",
},
{
"lookup_value_p": "A03",
"lookup_value_c": "B03",
"replace_value": "R03",
},
]
test_foo = ArraySearchList(
key = "lookup_value_p",
value = "A01"
key="lookup_value_p",
value="A01"
)
print(test_foo)
result = find_in_array_from_list(data, [test_foo])
print(f"Search A: {dump_data(test_foo)} -> {dump_data(result)}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
@@ -38,12 +45,122 @@ def main() -> None:
{
"key": "lookup_value_c",
"value": "B01"
},
]
result = find_in_array_from_list(data, search)
print(f"Search B: {dump_data(search)} -> {dump_data(result)}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "A01"
},
{
"key": "lookup_value_c",
"value": "B01"
},
{
"key": "lookup_value_c",
"value": "B02"
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search C: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search C raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "A01"
},
{
"key": "lookup_value_c",
"value": ["B01", "B02"]
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search D: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search D raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": ["A01", "A03"]
},
{
"key": "lookup_value_c",
"value": ["B01", "B02"]
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search E: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search E raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "NOT FOUND"
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search F: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search F raised KeyError: {e}")
data = [
{
"sd_user_id": "1593",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1592",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1596",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1594",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1595",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1861",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1862",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1860",
"email": "",
"employee_id": ""
}
]
result = find_in_array_from_list(data, search)
print(f"Search {dump_data(search)} -> {dump_data(result)}")
result = find_in_array_from_list(data, [ArraySearchList(
key="sd_user_id",
value="1593"
)])
print(f"Search F: -> {dump_data(result)}")
if __name__ == "__main__":

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,7 +2,10 @@
test list helpers
"""
from corelibs.iterator_handling.list_helpers import is_list_in_list, convert_to_list
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():
@@ -18,9 +21,66 @@ def __convert_list():
print(f"IN: {source} -> {result}")
def __make_unique_list_of_dicts():
dict_list = [
{"a": 1, "b": 2, "nested": {"x": 10, "y": 20}},
{"a": 1, "b": 2, "nested": {"x": 10, "y": 20}},
{"b": 2, "a": 1, "nested": {"y": 20, "x": 10}},
{"b": 2, "a": 1, "nested": {"y": 20, "x": 30}},
{"a": 3, "b": 4, "nested": {"x": 30, "y": 40}}
]
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 = [
{"a": 1, 1: "one"},
{1: "one", "a": 1},
{"a": 2, 1: "one"}
]
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 = [
{"a": 1, "b": [1, 2, 3]},
{"b": [1, 2, 3], "a": 1},
{"a": 1, "b": [1, 2, 4]},
1, 2, "String", 1, "Foobar"
]
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] = [
[],
{},
[],
{},
{"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():
"""List helpers test runner"""
__test_is_list_in_list_a()
__convert_list()
__make_unique_list_of_dicts()
if __name__ == "__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
@@ -27,7 +27,8 @@ def main():
"per_run_log": True,
# "console_format_type": ConsoleFormatSettings.NONE,
# "console_format_type": ConsoleFormatSettings.MINIMAL,
"console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
# "console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
"console_format_type": None,
# "console_format_type": ConsoleFormat.NAME,
# "console_format_type": (
# ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
@@ -121,10 +122,16 @@ def main():
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.DEBUG)
log.debug('Current logging format: %s', log.log_settings['console_format_type'])
log.debug('Current console formatter: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less')
log.info('Does hit show less A')
log.debug('Current console formatter after A: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less B')
log.debug('Current console formatter after B: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormatSettings.ALL)
log.info('Does hit show less C')
log.debug('Current console formatter after C: %s', log.get_console_formatter())
print(f"*** Any handler is minimum level ERROR: {log.any_handler_is_minimum_level(LoggingLevel.ERROR)}")
print(f"*** Any handler is minimum level DEBUG: {log.any_handler_is_minimum_level(LoggingLevel.DEBUG)}")

View File

@@ -37,8 +37,8 @@ def main():
# prg.SetStartTime(time.time())
prg.set_start_time()
print(
f"PRECISION: {prg.precision} | TEN STEP: {prg.precision_ten_step} | "
f"WIDE TEME: {prg.wide_time} | MICROTIME: {prg.microtime} | VERBOSE: {prg.verbose}"
f"PRECISION: {prg.__precision} | TEN STEP: {prg.precision_ten_step} | "
f"WIDE TEME: {prg.__wide_time} | MICROTIME: {prg.microtime} | VERBOSE: {prg.verbose}"
)
if use_file:
@@ -56,7 +56,7 @@ def main():
print(
f"Buffer size: {io.DEFAULT_BUFFER_SIZE} | "
f"Do Buffering: {fh.line_buffering} | "
f"File size: {prg.filesize}"
f"File size: {prg.__filesize}"
)
data = fh.readline()
while data:
@@ -72,7 +72,7 @@ def main():
print(f"Starting: {create_time(prg.start if prg.start is not None else 0)}")
prg.set_linecount(256)
i = 1
while i <= prg.linecount:
while i <= prg.__linecount:
sleep = randint(1, 9)
sleep /= 7
time.sleep(sleep)

View File

@@ -0,0 +1,38 @@
"""
Caller tests
"""
from corelibs_dump_data.dump_data import dump_data
from corelibs.requests_handling.caller import Caller, ErrorResponse
from corelibs.requests_handling.auth_helpers import basic_auth
def test_basic_auth():
"""basic auth test"""
user = "user"
password = "pass"
auth_header = basic_auth(user, password)
print(f"Auth Header for '{user}' & '{password}': {auth_header}")
def test_caller():
"""Caller tests"""
caller = Caller()
response = caller.get("https://httpbin.org/get")
if isinstance(response, ErrorResponse):
print(f"Error: {response.message}")
else:
print(f"Response Status Code: {response.status_code}")
print(f"Response Content: {dump_data(response.json())}")
def main():
"""main"""
test_caller()
test_basic_auth()
if __name__ == "__main__":
main()
# __END__

View File

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

View File

@@ -1,336 +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,
EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
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 compile_re(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 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 compile_re(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 compile_re(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 compile_re(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,
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_domain_patterns_are_strings(self) -> None:
"""Test that all regex constants are strings."""
assert isinstance(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 = compile_re(DOMAIN_WITH_LOCALHOST_REGEX)
domain_localhost_port = compile_re(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

@@ -16,7 +16,7 @@ class TestSettingsLoaderInit:
def test_init_with_valid_config_file(self, tmp_path: Path):
"""Test initialization with a valid config file"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
loader = SettingsLoader(
@@ -35,7 +35,7 @@ class TestSettingsLoaderInit:
def test_init_with_missing_config_file(self, tmp_path: Path):
"""Test initialization with missing config file"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(
args={},
@@ -60,7 +60,7 @@ class TestSettingsLoaderInit:
def test_init_with_log(self, tmp_path: Path):
"""Test initialization with Log object"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
mock_log = Mock(spec=Log)
@@ -80,7 +80,7 @@ class TestLoadSettings:
def test_load_settings_basic(self, tmp_path: Path):
"""Test loading basic settings without validation"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nkey1=value1\nkey2=value2\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -90,7 +90,7 @@ class TestLoadSettings:
def test_load_settings_with_missing_section(self, tmp_path: Path):
"""Test loading settings with missing section"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -100,7 +100,7 @@ class TestLoadSettings:
def test_load_settings_allow_not_exist(self, tmp_path: Path):
"""Test loading settings with allow_not_exist flag"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -110,7 +110,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_present(self, tmp_path: Path):
"""Test mandatory field validation when field is present"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -123,7 +123,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_missing(self, tmp_path: Path):
"""Test mandatory field validation when field is missing"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -136,7 +136,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_empty(self, tmp_path: Path):
"""Test mandatory field validation when field is empty"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -149,7 +149,7 @@ class TestLoadSettings:
def test_load_settings_with_split(self, tmp_path: Path):
"""Test splitting values into lists"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a,b,c,d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -162,7 +162,7 @@ class TestLoadSettings:
def test_load_settings_with_custom_split_char(self, tmp_path: Path):
"""Test splitting with custom delimiter"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a|b|c|d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -175,7 +175,7 @@ class TestLoadSettings:
def test_load_settings_split_removes_spaces(self, tmp_path: Path):
"""Test that split removes spaces from values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a, b , c , d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -188,7 +188,7 @@ class TestLoadSettings:
def test_load_settings_empty_split_char_fallback(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test fallback to default split char when empty"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a,b,c\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -201,9 +201,22 @@ class TestLoadSettings:
captured = capsys.readouterr()
assert "fallback to:" in captured.out
def test_load_settings_split_empty_value(self, tmp_path: Path):
"""Test that split on empty value results in empty list"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:,"]}
)
assert result["list_field"] == []
def test_load_settings_convert_to_int(self, tmp_path: Path):
"""Test converting values to int"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -217,7 +230,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_float(self, tmp_path: Path):
"""Test converting values to float"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123.45\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -231,7 +244,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_bool_true(self, tmp_path: Path):
"""Test converting values to boolean True"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=true\nflag2=True\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -245,7 +258,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_bool_false(self, tmp_path: Path):
"""Test converting values to boolean False"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=false\nflag2=False\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -259,7 +272,7 @@ class TestLoadSettings:
def test_load_settings_convert_invalid_type(self, tmp_path: Path):
"""Test converting with invalid type raises error"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -272,7 +285,7 @@ class TestLoadSettings:
def test_load_settings_empty_set_to_none(self, tmp_path: Path):
"""Test setting empty values to None"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -285,7 +298,7 @@ class TestLoadSettings:
def test_load_settings_empty_set_to_custom_value(self, tmp_path: Path):
"""Test setting empty values to custom value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -298,7 +311,7 @@ class TestLoadSettings:
def test_load_settings_matching_valid(self, tmp_path: Path):
"""Test matching validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=production\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -311,7 +324,7 @@ class TestLoadSettings:
def test_load_settings_matching_invalid(self, tmp_path: Path):
"""Test matching validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=invalid\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -324,7 +337,7 @@ class TestLoadSettings:
def test_load_settings_in_valid(self, tmp_path: Path):
"""Test 'in' validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=b\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -340,7 +353,7 @@ class TestLoadSettings:
def test_load_settings_in_invalid(self, tmp_path: Path):
"""Test 'in' validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -356,7 +369,7 @@ class TestLoadSettings:
def test_load_settings_in_missing_target(self, tmp_path: Path):
"""Test 'in' validation with missing target"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -369,7 +382,7 @@ class TestLoadSettings:
def test_load_settings_length_exact(self, tmp_path: Path):
"""Test length validation with exact match"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -382,7 +395,7 @@ class TestLoadSettings:
def test_load_settings_length_exact_invalid(self, tmp_path: Path):
"""Test length validation with exact match failure"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -395,7 +408,7 @@ class TestLoadSettings:
def test_load_settings_length_range(self, tmp_path: Path):
"""Test length validation with range"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -408,7 +421,7 @@ class TestLoadSettings:
def test_load_settings_length_min_only(self, tmp_path: Path):
"""Test length validation with minimum only"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -421,7 +434,7 @@ class TestLoadSettings:
def test_load_settings_length_max_only(self, tmp_path: Path):
"""Test length validation with maximum only"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -434,7 +447,7 @@ class TestLoadSettings:
def test_load_settings_range_valid(self, tmp_path: Path):
"""Test range validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=25\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -447,7 +460,7 @@ class TestLoadSettings:
def test_load_settings_range_invalid(self, tmp_path: Path):
"""Test range validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=100\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -460,7 +473,7 @@ class TestLoadSettings:
def test_load_settings_check_int_valid(self, tmp_path: Path):
"""Test check:int with valid integer"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12345\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -473,7 +486,7 @@ class TestLoadSettings:
def test_load_settings_check_int_cleanup(self, tmp_path: Path):
"""Test check:int with cleanup"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12a34b5\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -486,7 +499,7 @@ class TestLoadSettings:
def test_load_settings_check_email_valid(self, tmp_path: Path):
"""Test check:string.email.basic with valid email"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=test@example.com\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -499,7 +512,7 @@ class TestLoadSettings:
def test_load_settings_check_email_invalid(self, tmp_path: Path):
"""Test check:string.email.basic with invalid email"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=not-an-email\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -512,7 +525,7 @@ class TestLoadSettings:
def test_load_settings_args_override(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test command line arguments override config values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
@@ -530,7 +543,7 @@ class TestLoadSettings:
def test_load_settings_args_no_flag(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with list argument that has split"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
@@ -550,7 +563,7 @@ class TestLoadSettings:
def test_load_settings_args_list_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments without split entry are skipped"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
@@ -570,7 +583,7 @@ class TestLoadSettings:
def test_load_settings_args_list_with_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments with split entry and args_override:yes are applied"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
@@ -589,7 +602,7 @@ class TestLoadSettings:
def test_load_settings_args_no_with_mandatory(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with mandatory field and list args with split"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config1,config2\n")
loader = SettingsLoader(
@@ -609,7 +622,7 @@ class TestLoadSettings:
def test_load_settings_args_no_with_mandatory_valid(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior with string args (always overrides due to current logic)"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
@@ -628,7 +641,7 @@ class TestLoadSettings:
def test_load_settings_args_string_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that string arguments with args_override:yes work normally"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
@@ -647,7 +660,7 @@ class TestLoadSettings:
def test_load_settings_no_config_file_with_args(self, tmp_path: Path):
"""Test loading settings without config file but with mandatory args"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(
args={"required": "value"},
@@ -662,7 +675,7 @@ class TestLoadSettings:
def test_load_settings_no_config_file_missing_args(self, tmp_path: Path):
"""Test loading settings without config file and missing args"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -674,7 +687,7 @@ class TestLoadSettings:
def test_load_settings_check_list_with_split(self, tmp_path: Path):
"""Test check validation with list values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist=abc,def,ghi\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -687,7 +700,7 @@ class TestLoadSettings:
def test_load_settings_check_list_cleanup(self, tmp_path: Path):
"""Test check validation cleans up list values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist=ab-c,de_f,gh!i\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -700,7 +713,7 @@ class TestLoadSettings:
def test_load_settings_invalid_check_type(self, tmp_path: Path):
"""Test with invalid check type"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -717,7 +730,7 @@ class TestComplexScenarios:
def test_complex_validation_scenario(self, tmp_path: Path):
"""Test complex scenario with multiple validations"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Production]\n"
"environment=production\n"
@@ -758,7 +771,7 @@ class TestComplexScenarios:
def test_email_list_validation(self, tmp_path: Path):
"""Test email list with validation"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[EmailConfig]\n"
"emails=test@example.com,admin@domain.org,user+tag@site.co.uk\n"
@@ -775,7 +788,7 @@ class TestComplexScenarios:
def test_mixed_args_and_config(self, tmp_path: Path):
"""Test mixing command line args and config file"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Settings]\n"
"value1=config_value1\n"
@@ -796,7 +809,7 @@ class TestComplexScenarios:
def test_multiple_check_types(self, tmp_path: Path):
"""Test multiple different check types"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Checks]\n"
"numbers=123,456,789\n"
@@ -823,7 +836,7 @@ class TestComplexScenarios:
def test_args_no_and_list_skip_combination(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test combination of args_override:yes flag and list argument skip behavior"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Settings]\n"
"no_override=a,b,c\n"

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,300 +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
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)

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

@@ -28,6 +28,7 @@ def tmp_log_path(tmp_path: Path) -> Path:
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
# Return a new dict each time to avoid state pollution
return {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
@@ -308,4 +309,54 @@ class TestUpdateConsoleFormatter:
# Verify message was logged
assert "Test warning message" in caplog.text
def test_log_console_format_option_set_to_none(
self, tmp_log_path: Path
):
"""Test that when log_console_format option is set to None, it uses ConsoleFormatSettings.ALL"""
# Save the original DEFAULT_LOG_SETTINGS to restore it after test
original_default = Log.DEFAULT_LOG_SETTINGS.copy()
try:
# Reset DEFAULT_LOG_SETTINGS to ensure clean state
Log.DEFAULT_LOG_SETTINGS = {
"log_level_console": Log.DEFAULT_LOG_LEVEL_CONSOLE,
"log_level_file": Log.DEFAULT_LOG_LEVEL_FILE,
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": True,
"add_end_info": False,
"log_queue": None,
}
# Create a fresh settings dict with console_format_type explicitly set to None
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": None, # type: ignore
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
}
# Verify that None is explicitly set in the input
assert settings['console_format_type'] is None
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings
)
# Verify that None was replaced with ConsoleFormatSettings.ALL
# The Log class should replace None with the default value (ALL)
assert log.log_settings['console_format_type'] == ConsoleFormatSettings.ALL
finally:
# Restore original DEFAULT_LOG_SETTINGS
Log.DEFAULT_LOG_SETTINGS = original_default
# __END__

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

@@ -1,3 +0,0 @@
"""
PyTest: requests_handling tests
"""

View File

@@ -1,308 +0,0 @@
"""
PyTest: requests_handling/auth_helpers
"""
from base64 import b64decode
import pytest
from corelibs.requests_handling.auth_helpers import basic_auth
class TestBasicAuth:
"""Tests for basic_auth function"""
def test_basic_credentials(self):
"""Test basic auth with simple username and password"""
result = basic_auth("user", "pass")
assert result.startswith("Basic ")
# Decode and verify the credentials
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user:pass"
def test_username_with_special_characters(self):
"""Test basic auth with special characters in username"""
result = basic_auth("user@example.com", "password123")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user@example.com:password123"
def test_password_with_special_characters(self):
"""Test basic auth with special characters in password"""
result = basic_auth("admin", "p@ssw0rd!#$%")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "admin:p@ssw0rd!#$%"
def test_both_with_special_characters(self):
"""Test basic auth with special characters in both username and password"""
result = basic_auth("user@domain.com", "p@ss:w0rd!")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user@domain.com:p@ss:w0rd!"
def test_empty_username(self):
"""Test basic auth with empty username"""
result = basic_auth("", "password")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == ":password"
def test_empty_password(self):
"""Test basic auth with empty password"""
result = basic_auth("username", "")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "username:"
def test_both_empty(self):
"""Test basic auth with both username and password empty"""
result = basic_auth("", "")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == ":"
def test_colon_in_username(self):
"""Test basic auth with colon in username (edge case)"""
result = basic_auth("user:name", "password")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user:name:password"
def test_colon_in_password(self):
"""Test basic auth with colon in password"""
result = basic_auth("username", "pass:word")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "username:pass:word"
def test_unicode_characters(self):
"""Test basic auth with unicode characters"""
result = basic_auth("用户", "密码")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "用户:密码"
def test_long_credentials(self):
"""Test basic auth with very long credentials"""
long_user = "a" * 100
long_pass = "b" * 100
result = basic_auth(long_user, long_pass)
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == f"{long_user}:{long_pass}"
def test_whitespace_in_credentials(self):
"""Test basic auth with whitespace in credentials"""
result = basic_auth("user name", "pass word")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user name:pass word"
def test_newlines_in_credentials(self):
"""Test basic auth with newlines in credentials"""
result = basic_auth("user\nname", "pass\nword")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user\nname:pass\nword"
def test_return_type(self):
"""Test that return type is string"""
result = basic_auth("user", "pass")
assert isinstance(result, str)
def test_format_consistency(self):
"""Test that the format is always 'Basic <token>'"""
result = basic_auth("user", "pass")
parts = result.split(" ")
assert len(parts) == 2
assert parts[0] == "Basic"
# Verify the second part is valid base64
try:
b64decode(parts[1])
except (ValueError, TypeError) as e:
pytest.fail(f"Invalid base64 encoding: {e}")
def test_known_value(self):
"""Test against a known basic auth value"""
# "user:pass" in base64 is "dXNlcjpwYXNz"
result = basic_auth("user", "pass")
assert result == "Basic dXNlcjpwYXNz"
def test_case_sensitivity(self):
"""Test that username and password are case sensitive"""
result1 = basic_auth("User", "Pass")
result2 = basic_auth("user", "pass")
assert result1 != result2
def test_ascii_encoding(self):
"""Test that the result is ASCII encoded"""
result = basic_auth("user", "pass")
# Should not raise exception
result.encode('ascii')
# Parametrized tests
@pytest.mark.parametrize("username,password,expected_decoded", [
("admin", "admin123", "admin:admin123"),
("user@example.com", "password", "user@example.com:password"),
("test", "test!@#", "test:test!@#"),
("", "password", ":password"),
("username", "", "username:"),
("", "", ":"),
("user name", "pass word", "user name:pass word"),
])
def test_basic_auth_parametrized(username: str, password: str, expected_decoded: str):
"""Parametrized test for basic_auth"""
result = basic_auth(username, password)
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == expected_decoded
@pytest.mark.parametrize("username,password", [
("user", "pass"),
("admin", "secret"),
("test@example.com", "complex!@#$%^&*()"),
("a" * 50, "b" * 50),
])
def test_basic_auth_roundtrip(username: str, password: str):
"""Test that we can encode and decode credentials correctly"""
result = basic_auth(username, password)
# Extract the encoded part
encoded = result.split(" ")[1]
# Decode and verify
decoded = b64decode(encoded).decode("utf-8")
decoded_username, decoded_password = decoded.split(":", 1)
assert decoded_username == username
assert decoded_password == password
class TestBasicAuthIntegration:
"""Integration tests for basic_auth"""
def test_http_header_format(self):
"""Test that the output can be used as HTTP Authorization header"""
auth_header = basic_auth("user", "pass")
# Simulate HTTP header
headers = {"Authorization": auth_header}
assert "Authorization" in headers
assert headers["Authorization"].startswith("Basic ")
def test_multiple_calls_consistency(self):
"""Test that multiple calls with same credentials produce same result"""
result1 = basic_auth("user", "pass")
result2 = basic_auth("user", "pass")
result3 = basic_auth("user", "pass")
assert result1 == result2 == result3
def test_different_credentials_different_results(self):
"""Test that different credentials produce different results"""
result1 = basic_auth("user1", "pass1")
result2 = basic_auth("user2", "pass2")
result3 = basic_auth("user1", "pass2")
result4 = basic_auth("user2", "pass1")
results = [result1, result2, result3, result4]
# All should be unique
assert len(results) == len(set(results))
# Edge cases and security considerations
class TestBasicAuthEdgeCases:
"""Edge case tests for basic_auth"""
def test_null_bytes(self):
"""Test basic auth with null bytes (security consideration)"""
result = basic_auth("user\x00", "pass\x00")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert "user\x00" in decoded
assert "pass\x00" in decoded
def test_very_long_username(self):
"""Test with extremely long username"""
long_username = "a" * 1000
result = basic_auth(long_username, "pass")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded.startswith(long_username)
def test_very_long_password(self):
"""Test with extremely long password"""
long_password = "b" * 1000
result = basic_auth("user", long_password)
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded.endswith(long_password)
def test_emoji_in_credentials(self):
"""Test with emoji characters"""
result = basic_auth("user🔒", "pass🔑")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
assert decoded == "user🔒:pass🔑"
def test_multiple_colons(self):
"""Test with multiple colons in credentials"""
result = basic_auth("user:name:test", "pass:word:test")
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
decoded = b64decode(encoded).decode("utf-8")
# Only first colon is separator, rest are part of credentials
assert decoded == "user:name:test:pass:word:test"
def test_base64_special_chars(self):
"""Test credentials that might produce base64 with padding"""
# These lengths should produce different padding
result1 = basic_auth("a", "a")
result2 = basic_auth("ab", "ab")
result3 = basic_auth("abc", "abc")
# All should be valid
for result in [result1, result2, result3]:
assert result.startswith("Basic ")
encoded = result.split(" ")[1]
b64decode(encoded) # Should not raise
# __END__

View File

@@ -1,812 +0,0 @@
"""
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
class TestCallerInit:
"""Tests for Caller initialization"""
def test_init_with_required_params_only(self):
"""Test Caller initialization with only required parameters"""
header = {"Authorization": "Bearer token"}
caller = Caller(header=header)
assert caller.headers == header
assert caller.timeout == 20
assert caller.verify is True
assert caller.proxy is None
assert caller.cafile 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)
assert caller.headers == header
assert caller.timeout == 30
assert caller.verify is False
assert caller.proxy == proxy
def test_init_with_empty_header(self):
"""Test Caller initialization with empty header"""
caller = Caller(header={})
assert caller.headers == {}
assert caller.timeout == 20
def test_init_custom_timeout(self):
"""Test Caller initialization with custom timeout"""
caller = Caller(header={}, timeout=60)
assert caller.timeout == 60
def test_init_verify_false(self):
"""Test Caller initialization with verify=False"""
caller = Caller(header={}, verify=False)
assert caller.verify is False
def test_init_with_ca_file(self):
"""Test Caller initialization with ca_file parameter"""
ca_file_path = "/path/to/ca/cert.pem"
caller = Caller(header={}, ca_file=ca_file_path)
assert caller.cafile == ca_file_path
class TestCallerGet:
"""Tests for Caller.get method"""
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_basic(self, mock_get: Mock):
"""Test basic GET request"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 200
mock_get.return_value = mock_response
caller = Caller(header={"Authorization": "Bearer token"})
response = caller.get("https://api.example.com/data")
assert response == mock_response
mock_get.assert_called_once_with(
"https://api.example.com/data",
params=None,
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_with_params(self, mock_get: Mock):
"""Test GET request with query parameters"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={})
params = {"page": 1, "limit": 10}
response = caller.get("https://api.example.com/data", params=params)
assert response == mock_response
mock_get.assert_called_once_with(
"https://api.example.com/data",
params=params,
headers={},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_with_custom_timeout(self, mock_get: Mock):
"""Test GET request uses default timeout from instance"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={}, timeout=45)
caller.get("https://api.example.com/data")
mock_get.assert_called_once()
assert mock_get.call_args[1]["timeout"] == 45
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_with_verify_false(self, mock_get: Mock):
"""Test GET request with verify=False"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={}, verify=False)
caller.get("https://api.example.com/data")
mock_get.assert_called_once()
assert mock_get.call_args[1]["verify"] is False
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_with_proxy(self, mock_get: Mock):
"""Test GET request with proxy"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
proxy = {"http": "http://proxy.example.com:8080"}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
mock_get.assert_called_once()
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"""
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
@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"""
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
@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"""
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
class TestCallerPost:
"""Tests for Caller.post method"""
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_basic(self, mock_post: Mock):
"""Test basic POST request"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 201
mock_post.return_value = mock_response
caller = Caller(header={"Content-Type": "application/json"})
data = {"name": "test", "value": 123}
response = caller.post("https://api.example.com/data", data=data)
assert response == mock_response
mock_post.assert_called_once_with(
"https://api.example.com/data",
params=None,
json=data,
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_without_data(self, mock_post: Mock):
"""Test POST request without data"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
response = caller.post("https://api.example.com/data")
assert response == mock_response
mock_post.assert_called_once()
# Data defaults to None, which becomes {} in __call
assert mock_post.call_args[1]["json"] == {}
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_with_params(self, mock_post: Mock):
"""Test POST request with query parameters"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
data = {"key": "value"}
params = {"version": "v1"}
response = caller.post("https://api.example.com/data", data=data, params=params)
assert response == mock_response
mock_post.assert_called_once()
assert mock_post.call_args[1]["params"] == params
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"""
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
@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"""
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
@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"""
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
class TestCallerPut:
"""Tests for Caller.put method"""
@patch('corelibs.requests_handling.caller.requests.put')
def test_put_basic(self, mock_put: Mock):
"""Test basic PUT request"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 200
mock_put.return_value = mock_response
caller = Caller(header={"Content-Type": "application/json"})
data = {"id": 1, "name": "updated"}
response = caller.put("https://api.example.com/data/1", data=data)
assert response == mock_response
mock_put.assert_called_once_with(
"https://api.example.com/data/1",
params=None,
json=data,
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.put')
def test_put_with_params(self, mock_put: Mock):
"""Test PUT request with query parameters"""
mock_response = Mock(spec=requests.Response)
mock_put.return_value = mock_response
caller = Caller(header={})
data = {"name": "test"}
params = {"force": "true"}
response = caller.put("https://api.example.com/data/1", data=data, params=params)
assert response == mock_response
mock_put.assert_called_once()
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"""
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
class TestCallerPatch:
"""Tests for Caller.patch method"""
@patch('corelibs.requests_handling.caller.requests.patch')
def test_patch_basic(self, mock_patch: Mock):
"""Test basic PATCH request"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 200
mock_patch.return_value = mock_response
caller = Caller(header={"Content-Type": "application/json"})
data = {"status": "active"}
response = caller.patch("https://api.example.com/data/1", data=data)
assert response == mock_response
mock_patch.assert_called_once_with(
"https://api.example.com/data/1",
params=None,
json=data,
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.patch')
def test_patch_with_params(self, mock_patch: Mock):
"""Test PATCH request with query parameters"""
mock_response = Mock(spec=requests.Response)
mock_patch.return_value = mock_response
caller = Caller(header={})
data = {"field": "value"}
params = {"notify": "false"}
response = caller.patch("https://api.example.com/data/1", data=data, params=params)
assert response == mock_response
mock_patch.assert_called_once()
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"""
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
class TestCallerDelete:
"""Tests for Caller.delete method"""
@patch('corelibs.requests_handling.caller.requests.delete')
def test_delete_basic(self, mock_delete: Mock):
"""Test basic DELETE request"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 204
mock_delete.return_value = mock_response
caller = Caller(header={"Authorization": "Bearer token"})
response = caller.delete("https://api.example.com/data/1")
assert response == mock_response
mock_delete.assert_called_once_with(
"https://api.example.com/data/1",
params=None,
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.delete')
def test_delete_with_params(self, mock_delete: Mock):
"""Test DELETE request with query parameters"""
mock_response = Mock(spec=requests.Response)
mock_delete.return_value = mock_response
caller = Caller(header={})
params = {"force": "true"}
response = caller.delete("https://api.example.com/data/1", params=params)
assert response == mock_response
mock_delete.assert_called_once()
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"""
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
class TestCallerParametrized:
"""Parametrized tests for all HTTP methods"""
@pytest.mark.parametrize("method,http_method", [
("get", "get"),
("post", "post"),
("put", "put"),
("patch", "patch"),
("delete", "delete"),
])
@patch('corelibs.requests_handling.caller.requests')
def test_all_methods_use_correct_headers(self, mock_requests: Mock, method: str, http_method: str):
"""Test that all HTTP methods use the headers correctly"""
mock_response = Mock(spec=requests.Response)
mock_http_method = getattr(mock_requests, http_method)
mock_http_method.return_value = mock_response
headers = {"Authorization": "Bearer token", "X-Custom": "value"}
caller = Caller(header=headers)
# Call the method
caller_method = getattr(caller, method)
if method in ["get", "delete"]:
caller_method("https://api.example.com/data")
else:
caller_method("https://api.example.com/data", data={"key": "value"})
# Verify headers were passed
mock_http_method.assert_called_once()
assert mock_http_method.call_args[1]["headers"] == headers
@pytest.mark.parametrize("method,http_method", [
("get", "get"),
("post", "post"),
("put", "put"),
("patch", "patch"),
("delete", "delete"),
])
@patch('corelibs.requests_handling.caller.requests')
def test_all_methods_use_timeout(self, mock_requests: Mock, method: str, http_method: str):
"""Test that all HTTP methods use the timeout correctly"""
mock_response = Mock(spec=requests.Response)
mock_http_method = getattr(mock_requests, http_method)
mock_http_method.return_value = mock_response
timeout = 45
caller = Caller(header={}, timeout=timeout)
# Call the method
caller_method = getattr(caller, method)
if method in ["get", "delete"]:
caller_method("https://api.example.com/data")
else:
caller_method("https://api.example.com/data", data={"key": "value"})
# Verify timeout was passed
mock_http_method.assert_called_once()
assert mock_http_method.call_args[1]["timeout"] == timeout
@pytest.mark.parametrize("exception_class,expected_message", [
(requests.exceptions.InvalidSchema, "Invalid URL during"),
(requests.exceptions.ReadTimeout, "Timeout"),
(requests.exceptions.ConnectionError, "Connection error during"),
])
@patch('corelibs.requests_handling.caller.requests.get')
def test_exception_handling(
self, mock_get: Mock, exception_class: type, expected_message: str, capsys: Any
):
"""Test exception handling for all exception types"""
mock_get.side_effect = exception_class("Test error")
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert expected_message in captured.out
class TestCallerIntegration:
"""Integration tests for Caller"""
@patch('corelibs.requests_handling.caller.requests')
def test_multiple_requests_maintain_state(self, mock_requests: Mock):
"""Test that multiple requests maintain caller state"""
mock_response = Mock(spec=requests.Response)
mock_requests.get.return_value = mock_response
mock_requests.post.return_value = mock_response
headers = {"Authorization": "Bearer token"}
caller = Caller(header=headers, timeout=30, verify=False)
# Make multiple requests
caller.get("https://api.example.com/data1")
caller.post("https://api.example.com/data2", data={"key": "value"})
# Verify both used same configuration
assert mock_requests.get.call_args[1]["headers"] == headers
assert mock_requests.get.call_args[1]["timeout"] == 30
assert mock_requests.get.call_args[1]["verify"] is False
assert mock_requests.post.call_args[1]["headers"] == headers
assert mock_requests.post.call_args[1]["timeout"] == 30
assert mock_requests.post.call_args[1]["verify"] is False
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_with_complex_data(self, mock_post: Mock):
"""Test POST request with complex nested data"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
complex_data = {
"user": {
"name": "John Doe",
"email": "john@example.com",
"preferences": {
"notifications": True,
"theme": "dark"
}
},
"tags": ["important", "urgent"],
"count": 42
}
response = caller.post("https://api.example.com/users", data=complex_data)
assert response == mock_response
mock_post.assert_called_once()
assert mock_post.call_args[1]["json"] == complex_data
@patch('corelibs.requests_handling.caller.requests')
def test_all_http_methods_work_together(self, mock_requests: Mock):
"""Test that all HTTP methods can be used with the same Caller instance"""
mock_response = Mock(spec=requests.Response)
for method in ['get', 'post', 'put', 'patch', 'delete']:
getattr(mock_requests, method).return_value = mock_response
caller = Caller(header={"Authorization": "Bearer token"})
# Test all methods
caller.get("https://api.example.com/data")
caller.post("https://api.example.com/data", data={"new": "data"})
caller.put("https://api.example.com/data/1", data={"updated": "data"})
caller.patch("https://api.example.com/data/1", data={"field": "value"})
caller.delete("https://api.example.com/data/1")
# Verify all were called
mock_requests.get.assert_called_once()
mock_requests.post.assert_called_once()
mock_requests.put.assert_called_once()
mock_requests.patch.assert_called_once()
mock_requests.delete.assert_called_once()
class TestCallerEdgeCases:
"""Edge case tests for Caller"""
@patch('corelibs.requests_handling.caller.requests.get')
def test_empty_url(self, mock_get: Mock):
"""Test with empty URL"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={})
response = caller.get("")
assert response == mock_response
mock_get.assert_called_once_with(
"",
params=None,
headers={},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_with_empty_data(self, mock_post: Mock):
"""Test POST with explicitly empty data dict"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
response = caller.post("https://api.example.com/data", data={})
assert response == mock_response
mock_post.assert_called_once()
assert mock_post.call_args[1]["json"] == {}
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_with_empty_params(self, mock_get: Mock):
"""Test GET with explicitly empty params dict"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={})
response = caller.get("https://api.example.com/data", params={})
assert response == mock_response
mock_get.assert_called_once()
assert mock_get.call_args[1]["params"] == {}
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_with_none_values_in_data(self, mock_post: Mock):
"""Test POST with None values in data"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
data = {"key1": None, "key2": "value", "key3": None}
response = caller.post("https://api.example.com/data", data=data)
assert response == mock_response
mock_post.assert_called_once()
assert mock_post.call_args[1]["json"] == data
@patch('corelibs.requests_handling.caller.requests.get')
def test_very_long_url(self, mock_get: Mock):
"""Test with very long URL"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={})
long_url = "https://api.example.com/" + "a" * 1000
response = caller.get(long_url)
assert response == mock_response
mock_get.assert_called_once_with(
long_url,
params=None,
headers={},
timeout=20,
verify=True,
proxies=None
)
@patch('corelibs.requests_handling.caller.requests.get')
def test_special_characters_in_url(self, mock_get: Mock):
"""Test URL with special characters"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={})
url = "https://api.example.com/data?query=test%20value&id=123"
response = caller.get(url)
assert response == mock_response
mock_get.assert_called_once_with(
url,
params=None,
headers={},
timeout=20,
verify=True,
proxies=None
)
def test_timeout_zero(self):
"""Test Caller with timeout of 0"""
caller = Caller(header={}, timeout=0)
assert caller.timeout == 0
def test_negative_timeout(self):
"""Test Caller with negative timeout"""
caller = Caller(header={}, timeout=-1)
assert caller.timeout == -1
@patch('corelibs.requests_handling.caller.requests.get')
def test_unicode_in_headers(self, mock_get: Mock):
"""Test headers with unicode characters"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
headers = {"X-Custom": "测试", "Authorization": "Bearer token"}
caller = Caller(header=headers)
response = caller.get("https://api.example.com/data")
assert response == mock_response
mock_get.assert_called_once()
assert mock_get.call_args[1]["headers"] == headers
@patch('corelibs.requests_handling.caller.requests.post')
def test_unicode_in_data(self, mock_post: Mock):
"""Test data with unicode characters"""
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
caller = Caller(header={})
data = {"name": "用户", "message": "こんにちは", "emoji": "🚀"}
response = caller.post("https://api.example.com/data", data=data)
assert response == mock_response
mock_post.assert_called_once()
assert mock_post.call_args[1]["json"] == data
class TestCallerProxyHandling:
"""Tests for proxy handling"""
@patch('corelibs.requests_handling.caller.requests.get')
def test_proxy_configuration(self, mock_get: Mock):
"""Test that proxy configuration is passed to requests"""
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"
}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
mock_get.assert_called_once()
assert mock_get.call_args[1]["proxies"] == proxy
@patch('corelibs.requests_handling.caller.requests.post')
def test_proxy_with_auth(self, mock_post: Mock):
"""Test proxy with authentication"""
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"
}
caller = Caller(header={}, proxy=proxy)
caller.post("https://api.example.com/data", data={"test": "data"})
mock_post.assert_called_once()
assert mock_post.call_args[1]["proxies"] == proxy
class TestCallerTimeoutHandling:
"""Tests for timeout parameter handling"""
@patch('corelibs.requests_handling.caller.requests.get')
def test_timeout_parameter_none_uses_default(self, mock_get: Mock):
"""Test that None timeout uses the instance default"""
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
caller = Caller(header={}, timeout=30)
# The private __timeout method is called internally
caller.get("https://api.example.com/data")
mock_get.assert_called_once()
assert mock_get.call_args[1]["timeout"] == 30
class TestCallerResponseHandling:
"""Tests for response handling"""
@patch('corelibs.requests_handling.caller.requests.get')
def test_response_object_returned_correctly(self, mock_get: Mock):
"""Test that response object is returned correctly"""
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 200
mock_response.text = "Success"
mock_response.json.return_value = {"status": "ok"}
mock_get.return_value = mock_response
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert response.status_code == 200
assert response.text == "Success"
assert response.json() == {"status": "ok"}
@patch('corelibs.requests_handling.caller.requests.get')
def test_response_with_different_status_codes(self, mock_get: Mock):
"""Test response handling with different status codes"""
for status_code in [200, 201, 204, 400, 401, 404, 500]:
mock_response = Mock(spec=requests.Response)
mock_response.status_code = status_code
mock_get.return_value = mock_response
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert response.status_code == status_code
# __END__

View File

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

View File

@@ -1,821 +0,0 @@
"""
PyTest: script_handling/script_helpers
"""
# pylint: disable=use-implicit-booleaness-not-comparison
import time
import os
from pathlib import Path
from unittest.mock import patch, MagicMock, mock_open, PropertyMock
import pytest
from pytest import CaptureFixture
import psutil
from corelibs.script_handling.script_helpers import (
wait_abort,
lock_run,
unlock_run,
)
class TestWaitAbort:
"""Test suite for wait_abort function"""
def test_wait_abort_default_sleep(self, capsys: CaptureFixture[str]):
"""Test wait_abort with default sleep duration"""
with patch('time.sleep'):
wait_abort()
captured = capsys.readouterr()
assert "Waiting 5 seconds" in captured.out
assert "(Press CTRL +C to abort)" in captured.out
assert "[" in captured.out
assert "]" in captured.out
# Should have 4 dots (sleep - 1)
assert captured.out.count(".") == 4
def test_wait_abort_custom_sleep(self, capsys: CaptureFixture[str]):
"""Test wait_abort with custom sleep duration"""
with patch('time.sleep'):
wait_abort(sleep=3)
captured = capsys.readouterr()
assert "Waiting 3 seconds" in captured.out
# Should have 2 dots (3 - 1)
assert captured.out.count(".") == 2
def test_wait_abort_sleep_one_second(self, capsys: CaptureFixture[str]):
"""Test wait_abort with sleep duration of 1 second"""
with patch('time.sleep'):
wait_abort(sleep=1)
captured = capsys.readouterr()
assert "Waiting 1 seconds" in captured.out
# Should have 0 dots (1 - 1)
assert captured.out.count(".") == 0
def test_wait_abort_sleep_zero(self, capsys: CaptureFixture[str]):
"""Test wait_abort with sleep duration of 0"""
with patch('time.sleep'):
wait_abort(sleep=0)
captured = capsys.readouterr()
assert "Waiting 0 seconds" in captured.out
# Should have 0 dots since range(1, 0) is empty
assert captured.out.count(".") == 0
def test_wait_abort_keyboard_interrupt(self, capsys: CaptureFixture[str]):
"""Test wait_abort handles KeyboardInterrupt and exits"""
with patch('time.sleep', side_effect=KeyboardInterrupt):
with pytest.raises(SystemExit) as exc_info:
wait_abort(sleep=5)
assert exc_info.value.code == 0
captured = capsys.readouterr()
assert "Interrupted by user" in captured.out
def test_wait_abort_keyboard_interrupt_immediate(self, capsys: CaptureFixture[str]):
"""Test wait_abort handles KeyboardInterrupt on first iteration"""
def sleep_side_effect(_duration: int) -> None:
raise KeyboardInterrupt()
with patch('time.sleep', side_effect=sleep_side_effect):
with pytest.raises(SystemExit) as exc_info:
wait_abort(sleep=10)
assert exc_info.value.code == 0
captured = capsys.readouterr()
assert "Interrupted by user" in captured.out
def test_wait_abort_completes_normally(self, capsys: CaptureFixture[str]):
"""Test wait_abort completes without interruption"""
with patch('time.sleep') as mock_sleep:
wait_abort(sleep=3)
# time.sleep should be called (sleep - 1) times
assert mock_sleep.call_count == 2
captured = capsys.readouterr()
assert "Waiting 3 seconds" in captured.out
assert "]" in captured.out
# Should have newlines at the end
assert captured.out.endswith("\n\n")
def test_wait_abort_actual_timing(self):
"""Test wait_abort actually waits (integration test)"""
start_time = time.time()
wait_abort(sleep=1)
elapsed_time = time.time() - start_time
# Should take at least close to 0 seconds (1-1)
# With mocking disabled in this test, it would take actual time
# but we've been mocking it, so this tests the unmocked behavior
# For this test, we'll check it runs without error
assert elapsed_time >= 0
def test_wait_abort_large_sleep_value(self, capsys: CaptureFixture[str]):
"""Test wait_abort with large sleep value"""
with patch('time.sleep'):
wait_abort(sleep=100)
captured = capsys.readouterr()
assert "Waiting 100 seconds" in captured.out
# Should have 99 dots
assert captured.out.count(".") == 99
def test_wait_abort_output_format(self, capsys: CaptureFixture[str]):
"""Test wait_abort output formatting"""
with patch('time.sleep'):
wait_abort(sleep=3)
captured = capsys.readouterr()
# Check the exact format
assert "Waiting 3 seconds (Press CTRL +C to abort) [" in captured.out
assert captured.out.count("[") == 1
assert captured.out.count("]") == 1
def test_wait_abort_flush_behavior(self):
"""Test that wait_abort flushes output correctly"""
with patch('time.sleep'):
with patch('builtins.print') as mock_print:
wait_abort(sleep=3)
# Check that print was called with flush=True
# First call: "Waiting X seconds..."
# Intermediate calls: dots with flush=True
# Last calls: "]" and final newlines
flush_calls = [
call for call in mock_print.call_args_list
if 'flush' in call.kwargs and call.kwargs['flush'] is True
]
assert len(flush_calls) > 0
class TestLockRun:
"""Test suite for lock_run function"""
def test_lock_run_creates_lock_file(self, tmp_path: Path):
"""Test lock_run creates a lock file with current PID"""
lock_file = tmp_path / "test.lock"
lock_run(lock_file)
assert lock_file.exists()
content = lock_file.read_text()
assert content == str(os.getpid())
def test_lock_run_raises_when_process_exists(self, tmp_path: Path):
"""Test lock_run raises IOError when process with PID exists
Note: The actual code has a bug where it compares string PID from file
with integer PID from psutil, which will never match. This test demonstrates
the intended behavior if the bug were fixed.
"""
lock_file = tmp_path / "test.lock"
current_pid = os.getpid()
# Create lock file with current PID
lock_file.write_text(str(current_pid))
# Patch at module level to ensure correct comparison
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
def mock_process_iter(attrs=None): # type: ignore
mock_proc = MagicMock()
# Make PID a string to match the file content for comparison
mock_proc.info = {'pid': str(current_pid)}
return [mock_proc]
mock_proc_iter.side_effect = mock_process_iter
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert f"Script is already running with PID {current_pid}" in str(exc_info.value)
def test_lock_run_removes_stale_lock_file(self, tmp_path: Path):
"""Test lock_run removes lock file when PID doesn't exist"""
lock_file = tmp_path / "test.lock"
# Use a PID that definitely doesn't exist
stale_pid = "99999999"
lock_file.write_text(stale_pid)
# Mock psutil to return no matching processes
with patch('psutil.process_iter') as mock_proc_iter:
mock_process = MagicMock()
mock_process.info = {'pid': 12345} # Different PID
mock_proc_iter.return_value = [mock_process]
lock_run(lock_file)
# Lock file should be recreated with current PID
assert lock_file.exists()
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_creates_lock_when_no_file_exists(self, tmp_path: Path):
"""Test lock_run creates lock file when none exists"""
lock_file = tmp_path / "new.lock"
assert not lock_file.exists()
lock_run(lock_file)
assert lock_file.exists()
def test_lock_run_handles_empty_lock_file(self, tmp_path: Path):
"""Test lock_run handles empty lock file"""
lock_file = tmp_path / "empty.lock"
lock_file.write_text("")
lock_run(lock_file)
assert lock_file.exists()
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_handles_psutil_no_such_process(self, tmp_path: Path):
"""Test lock_run handles psutil.NoSuchProcess exception"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
# Create a mock that raises NoSuchProcess inside the try block
def mock_iter(attrs=None): # type: ignore
mock_proc = MagicMock()
mock_proc.info = {'pid': "12345"}
# Configure to raise exception when accessed
type(mock_proc).info = PropertyMock(side_effect=psutil.NoSuchProcess(12345))
return [mock_proc]
mock_proc_iter.side_effect = mock_iter
# Since the exception is caught, lock should be acquired
lock_run(lock_file)
assert lock_file.exists()
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_handles_psutil_access_denied(self, tmp_path: Path):
"""Test lock_run handles psutil.AccessDenied exception"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
with patch('psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
lock_run(lock_file)
assert lock_file.exists()
def test_lock_run_handles_psutil_zombie_process(self, tmp_path: Path):
"""Test lock_run handles psutil.ZombieProcess exception"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
with patch('psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
lock_run(lock_file)
assert lock_file.exists()
def test_lock_run_raises_on_unlink_error(self, tmp_path: Path):
"""Test lock_run raises IOError when cannot remove stale lock file"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("99999999")
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
# Mock pathlib.Path.unlink to raise IOError on the specific lock_file
original_unlink = Path.unlink
def mock_unlink(self, *args, **kwargs): # type: ignore
if self == lock_file:
raise IOError("Permission denied")
return original_unlink(self, *args, **kwargs)
with patch.object(Path, 'unlink', mock_unlink):
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert "Cannot remove lock_file" in str(exc_info.value)
assert "Permission denied" in str(exc_info.value)
def test_lock_run_raises_on_write_error(self, tmp_path: Path):
"""Test lock_run raises IOError when cannot write lock file"""
lock_file = tmp_path / "test.lock"
# Mock open to raise IOError on write
with patch('builtins.open', side_effect=IOError("Disk full")):
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert "Cannot open run lock file" in str(exc_info.value)
assert "Disk full" in str(exc_info.value)
def test_lock_run_uses_current_pid(self, tmp_path: Path):
"""Test lock_run uses current process PID"""
lock_file = tmp_path / "test.lock"
expected_pid = os.getpid()
lock_run(lock_file)
actual_pid = lock_file.read_text()
assert actual_pid == str(expected_pid)
def test_lock_run_with_subdirectory(self, tmp_path: Path):
"""Test lock_run creates lock file in subdirectory"""
subdir = tmp_path / "locks"
subdir.mkdir()
lock_file = subdir / "test.lock"
lock_run(lock_file)
assert lock_file.exists()
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_overwrites_invalid_pid(self, tmp_path: Path):
"""Test lock_run overwrites lock file with invalid PID format"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("not_a_number")
# When PID is not a valid number, psutil won't find it
with patch('psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
lock_run(lock_file)
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_multiple_times_same_process(self, tmp_path: Path):
"""Test lock_run called multiple times by same process"""
lock_file = tmp_path / "test.lock"
current_pid = os.getpid()
# First call
lock_run(lock_file)
assert lock_file.read_text() == str(current_pid)
# Second call - should raise since process exists
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
def mock_iter(attrs=None): # type: ignore
mock_proc = MagicMock()
mock_proc.info = {'pid': str(current_pid)}
return [mock_proc]
mock_proc_iter.side_effect = mock_iter
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert f"Script is already running with PID {current_pid}" in str(exc_info.value)
def test_lock_run_checks_all_processes(self, tmp_path: Path):
"""Test lock_run iterates through all processes"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
# Create multiple mock processes
def mock_iter(attrs=None): # type: ignore
mock_processes = []
for pid in ["1000", "2000", "12345", "4000"]: # PIDs as strings
mock_proc = MagicMock()
mock_proc.info = {'pid': pid}
mock_processes.append(mock_proc)
return mock_processes
mock_proc_iter.side_effect = mock_iter
# Should find PID 12345 and raise
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert "Script is already running with PID 12345" in str(exc_info.value)
def test_lock_run_file_encoding_utf8(self, tmp_path: Path):
"""Test lock_run uses UTF-8 encoding"""
lock_file = tmp_path / "test.lock"
with patch('builtins.open', mock_open()) as mock_file:
try:
lock_run(lock_file)
except (IOError, FileNotFoundError):
pass # We're just checking the encoding parameter
# Check that open was called with UTF-8 encoding
calls = mock_file.call_args_list
for call in calls:
if 'encoding' in call.kwargs:
assert call.kwargs['encoding'] == 'UTF-8'
class TestUnlockRun:
"""Test suite for unlock_run function"""
def test_unlock_run_removes_lock_file(self, tmp_path: Path):
"""Test unlock_run removes existing lock file"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
assert lock_file.exists()
unlock_run(lock_file)
assert not lock_file.exists()
def test_unlock_run_raises_on_error(self, tmp_path: Path):
"""Test unlock_run raises IOError when cannot remove file"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
with patch.object(Path, 'unlink', side_effect=IOError("Permission denied")):
with pytest.raises(IOError) as exc_info:
unlock_run(lock_file)
assert "Cannot remove lock_file" in str(exc_info.value)
assert "Permission denied" in str(exc_info.value)
def test_unlock_run_on_nonexistent_file(self, tmp_path: Path):
"""Test unlock_run on non-existent file raises IOError"""
lock_file = tmp_path / "nonexistent.lock"
with pytest.raises(IOError) as exc_info:
unlock_run(lock_file)
assert "Cannot remove lock_file" in str(exc_info.value)
def test_unlock_run_with_subdirectory(self, tmp_path: Path):
"""Test unlock_run removes file from subdirectory"""
subdir = tmp_path / "locks"
subdir.mkdir()
lock_file = subdir / "test.lock"
lock_file.write_text("12345")
unlock_run(lock_file)
assert not lock_file.exists()
def test_unlock_run_multiple_times(self, tmp_path: Path):
"""Test unlock_run called multiple times raises error"""
lock_file = tmp_path / "test.lock"
lock_file.write_text("12345")
# First call should succeed
unlock_run(lock_file)
assert not lock_file.exists()
# Second call should raise IOError
with pytest.raises(IOError):
unlock_run(lock_file)
def test_unlock_run_readonly_file(self, tmp_path: Path):
"""Test unlock_run on read-only file"""
lock_file = tmp_path / "readonly.lock"
lock_file.write_text("12345")
lock_file.chmod(0o444)
try:
unlock_run(lock_file)
# On some systems, unlink may still work on readonly files
assert not lock_file.exists()
except IOError as exc_info:
# On other systems, it may raise an error
assert "Cannot remove lock_file" in str(exc_info)
def test_unlock_run_preserves_other_files(self, tmp_path: Path):
"""Test unlock_run only removes specified file"""
lock_file1 = tmp_path / "test1.lock"
lock_file2 = tmp_path / "test2.lock"
lock_file1.write_text("12345")
lock_file2.write_text("67890")
unlock_run(lock_file1)
assert not lock_file1.exists()
assert lock_file2.exists()
class TestLockUnlockIntegration:
"""Integration tests for lock_run and unlock_run"""
def test_lock_unlock_workflow(self, tmp_path: Path):
"""Test complete lock and unlock workflow"""
lock_file = tmp_path / "workflow.lock"
# Lock
lock_run(lock_file)
assert lock_file.exists()
assert lock_file.read_text() == str(os.getpid())
# Unlock
unlock_run(lock_file)
assert not lock_file.exists()
def test_lock_unlock_relock(self, tmp_path: Path):
"""Test locking, unlocking, and locking again"""
lock_file = tmp_path / "relock.lock"
# First lock
lock_run(lock_file)
first_content = lock_file.read_text()
# Unlock
unlock_run(lock_file)
# Second lock
lock_run(lock_file)
second_content = lock_file.read_text()
assert first_content == second_content == str(os.getpid())
def test_lock_prevents_duplicate_run(self, tmp_path: Path):
"""Test lock prevents duplicate process simulation"""
lock_file = tmp_path / "duplicate.lock"
current_pid = os.getpid()
# First lock
lock_run(lock_file)
# Simulate another process trying to acquire lock
with patch('psutil.process_iter') as mock_proc_iter:
mock_process = MagicMock()
mock_process.info = {'pid': current_pid}
mock_proc_iter.return_value = [mock_process]
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert "already running" in str(exc_info.value)
# Cleanup
unlock_run(lock_file)
def test_stale_lock_cleanup_and_reacquire(self, tmp_path: Path):
"""Test cleaning up stale lock and acquiring new one"""
lock_file = tmp_path / "stale.lock"
# Create stale lock
stale_pid = "99999999"
lock_file.write_text(stale_pid)
# Mock psutil to indicate process doesn't exist
with patch('psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
lock_run(lock_file)
# Should have our PID now
assert lock_file.read_text() == str(os.getpid())
# Cleanup
unlock_run(lock_file)
assert not lock_file.exists()
def test_multiple_locks_different_files(self, tmp_path: Path):
"""Test multiple locks with different files"""
lock_file1 = tmp_path / "lock1.lock"
lock_file2 = tmp_path / "lock2.lock"
# Acquire multiple locks
lock_run(lock_file1)
lock_run(lock_file2)
assert lock_file1.exists()
assert lock_file2.exists()
# Release them
unlock_run(lock_file1)
unlock_run(lock_file2)
assert not lock_file1.exists()
assert not lock_file2.exists()
def test_lock_in_context_manager_pattern(self, tmp_path: Path):
"""Test lock/unlock in a context manager pattern"""
lock_file = tmp_path / "context.lock"
class LockContext:
def __init__(self, lock_path: Path):
self.lock_path = lock_path
def __enter__(self) -> 'LockContext':
lock_run(self.lock_path)
return self
def __exit__(self, exc_type: type, exc_val: Exception, exc_tb: object) -> bool:
unlock_run(self.lock_path)
return False
# Use in context
with LockContext(lock_file):
assert lock_file.exists()
# After context, should be unlocked
assert not lock_file.exists()
def test_lock_survives_process_in_loop(self, tmp_path: Path):
"""Test lock file persists across multiple operations"""
lock_file = tmp_path / "persistent.lock"
lock_run(lock_file)
# Simulate some operations
for _ in range(10):
assert lock_file.exists()
content = lock_file.read_text()
assert content == str(os.getpid())
unlock_run(lock_file)
assert not lock_file.exists()
def test_exception_during_locked_execution(self, tmp_path: Path):
"""Test lock cleanup when exception occurs during execution"""
lock_file = tmp_path / "exception.lock"
lock_run(lock_file)
try:
# Simulate some work that raises exception
raise ValueError("Something went wrong")
except ValueError:
pass
finally:
# Lock should still exist until explicitly unlocked
assert lock_file.exists()
unlock_run(lock_file)
assert not lock_file.exists()
def test_lock_file_permissions(self, tmp_path: Path):
"""Test lock file has appropriate permissions"""
lock_file = tmp_path / "permissions.lock"
lock_run(lock_file)
# File should be readable and writable by owner
assert lock_file.exists()
# We can read it
content = lock_file.read_text()
assert content == str(os.getpid())
unlock_run(lock_file)
class TestEdgeCases:
"""Test edge cases and error conditions"""
def test_wait_abort_negative_sleep(self, capsys: CaptureFixture[str]):
"""Test wait_abort with negative sleep value"""
with patch('time.sleep'):
wait_abort(sleep=-5)
captured = capsys.readouterr()
assert "Waiting -5 seconds" in captured.out
def test_lock_run_with_whitespace_pid(self, tmp_path: Path):
"""Test lock_run handles lock file with whitespace"""
lock_file = tmp_path / "whitespace.lock"
lock_file.write_text(" 12345 \n")
with patch('psutil.process_iter') as mock_proc_iter:
mock_proc_iter.return_value = []
lock_run(lock_file)
# Should create new lock with clean PID
assert lock_file.read_text() == str(os.getpid())
def test_lock_run_with_special_characters_in_path(self, tmp_path: Path):
"""Test lock_run with special characters in file path"""
special_dir = tmp_path / "special dir with spaces"
special_dir.mkdir()
lock_file = special_dir / "lock-file.lock"
lock_run(lock_file)
assert lock_file.exists()
unlock_run(lock_file)
def test_lock_run_with_very_long_path(self, tmp_path: Path):
"""Test lock_run with very long file path"""
# Create nested directories
deep_path = tmp_path
for i in range(10):
deep_path = deep_path / f"level{i}"
deep_path.mkdir(parents=True)
lock_file = deep_path / "deep.lock"
lock_run(lock_file)
assert lock_file.exists()
unlock_run(lock_file)
def test_unlock_run_on_directory(self, tmp_path: Path):
"""Test unlock_run on a directory raises appropriate error"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
with pytest.raises(IOError):
unlock_run(test_dir)
def test_lock_run_race_condition_simulation(self, tmp_path: Path):
"""Test lock_run handles simulated race condition"""
lock_file = tmp_path / "race.lock"
# This is hard to test reliably, but we can at least verify
# the function handles existing files
lock_file.write_text("88888")
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
def mock_iter(attrs=None): # type: ignore
mock_proc = MagicMock()
mock_proc.info = {'pid': "88888"}
return [mock_proc]
mock_proc_iter.side_effect = mock_iter
with pytest.raises(IOError):
lock_run(lock_file)
class TestScriptHelpersIntegration:
"""Integration tests combining multiple functions"""
def test_typical_script_pattern(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test typical script execution pattern with all helpers"""
lock_file = tmp_path / "script.lock"
# Wait before starting (with mocked sleep)
with patch('time.sleep'):
wait_abort(sleep=2)
captured = capsys.readouterr()
assert "Waiting 2 seconds" in captured.out
# Acquire lock
lock_run(lock_file)
assert lock_file.exists()
# Simulate work
time.sleep(0.01)
# Release lock
unlock_run(lock_file)
assert not lock_file.exists()
def test_script_with_error_handling(self, tmp_path: Path):
"""Test script pattern with error handling"""
lock_file = tmp_path / "error_script.lock"
try:
lock_run(lock_file)
# Simulate error during execution
raise RuntimeError("Simulated error")
except RuntimeError:
pass
finally:
# Ensure cleanup happens
if lock_file.exists():
unlock_run(lock_file)
assert not lock_file.exists()
def test_concurrent_script_protection(self, tmp_path: Path):
"""Test protection against concurrent script execution"""
lock_file = tmp_path / "concurrent.lock"
# First instance acquires lock
lock_run(lock_file)
# Second instance should fail
with patch('corelibs.script_handling.script_helpers.psutil.process_iter') as mock_proc_iter:
def mock_iter(attrs=None): # type: ignore
mock_proc = MagicMock()
mock_proc.info = {'pid': str(os.getpid())}
return [mock_proc]
mock_proc_iter.side_effect = mock_iter
with pytest.raises(IOError) as exc_info:
lock_run(lock_file)
assert "already running" in str(exc_info.value).lower()
# Cleanup
unlock_run(lock_file)
def test_graceful_shutdown_pattern(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test graceful shutdown with wait and cleanup"""
lock_file = tmp_path / "graceful.lock"
lock_run(lock_file)
# Simulate interrupt during wait
with patch('time.sleep', side_effect=KeyboardInterrupt):
with pytest.raises(SystemExit):
wait_abort(sleep=5)
captured = capsys.readouterr()
assert "Interrupted by user" in captured.out
# Cleanup should still happen
unlock_run(lock_file)
assert not lock_file.exists()
# __END__

View File

@@ -1,840 +0,0 @@
"""
PyTest: script_handling/progress
"""
import time
from unittest.mock import patch
from pytest import CaptureFixture
from corelibs.script_handling.progress import Progress
class TestProgressInit:
"""Test suite for Progress initialization"""
def test_default_initialization(self):
"""Test Progress initialization with default parameters"""
prg = Progress()
assert prg.verbose is False
assert prg.precision == 1
assert prg.microtime == 0
assert prg.wide_time is False
assert prg.prefix_lb is False
assert prg.linecount == 0
assert prg.filesize == 0
assert prg.count == 0
assert prg.start is not None
def test_initialization_with_verbose(self):
"""Test Progress initialization with verbose enabled"""
prg = Progress(verbose=1)
assert prg.verbose is True
prg = Progress(verbose=5)
assert prg.verbose is True
prg = Progress(verbose=0)
assert prg.verbose is False
def test_initialization_with_precision(self):
"""Test Progress initialization with different precision values"""
# Normal precision
prg = Progress(precision=0)
assert prg.precision == 0
assert prg.percent_print == 3
prg = Progress(precision=2)
assert prg.precision == 2
assert prg.percent_print == 6
prg = Progress(precision=10)
assert prg.precision == 10
assert prg.percent_print == 14
# Ten step precision
prg = Progress(precision=-1)
assert prg.precision == 0
assert prg.precision_ten_step == 10
assert prg.percent_print == 3
# Five step precision
prg = Progress(precision=-2)
assert prg.precision == 0
assert prg.precision_ten_step == 5
assert prg.percent_print == 3
def test_initialization_with_microtime(self):
"""Test Progress initialization with microtime settings"""
prg = Progress(microtime=-1)
assert prg.microtime == -1
prg = Progress(microtime=0)
assert prg.microtime == 0
prg = Progress(microtime=1)
assert prg.microtime == 1
def test_initialization_with_wide_time(self):
"""Test Progress initialization with wide_time flag"""
prg = Progress(wide_time=True)
assert prg.wide_time is True
prg = Progress(wide_time=False)
assert prg.wide_time is False
def test_initialization_with_prefix_lb(self):
"""Test Progress initialization with prefix line break"""
prg = Progress(prefix_lb=True)
assert prg.prefix_lb is True
prg = Progress(prefix_lb=False)
assert prg.prefix_lb is False
def test_initialization_combined_parameters(self):
"""Test Progress initialization with multiple parameters"""
prg = Progress(verbose=1, precision=2, microtime=1, wide_time=True, prefix_lb=True)
assert prg.verbose is True
assert prg.precision == 2
assert prg.microtime == 1
assert prg.wide_time is True
assert prg.prefix_lb is True
class TestProgressSetters:
"""Test suite for Progress setter methods"""
def test_set_verbose(self):
"""Test set_verbose method"""
prg = Progress()
assert prg.set_verbose(1) is True
assert prg.verbose is True
assert prg.set_verbose(10) is True
assert prg.verbose is True
assert prg.set_verbose(0) is False
assert prg.verbose is False
def test_set_precision(self):
"""Test set_precision method"""
prg = Progress()
# Valid precision values
assert prg.set_precision(0) == 0
assert prg.precision == 0
assert prg.set_precision(5) == 5
assert prg.precision == 5
assert prg.set_precision(10) == 10
assert prg.precision == 10
# Ten step precision
prg.set_precision(-1)
assert prg.precision == 0
assert prg.precision_ten_step == 10
# Five step precision
prg.set_precision(-2)
assert prg.precision == 0
assert prg.precision_ten_step == 5
# Invalid precision (too low)
assert prg.set_precision(-3) == 0
assert prg.precision == 0
# Invalid precision (too high)
assert prg.set_precision(11) == 0
assert prg.precision == 0
def test_set_linecount(self):
"""Test set_linecount method"""
prg = Progress()
assert prg.set_linecount(100) == 100
assert prg.linecount == 100
assert prg.set_linecount(1000) == 1000
assert prg.linecount == 1000
# Zero or negative should set to 1
assert prg.set_linecount(0) == 1
assert prg.linecount == 1
assert prg.set_linecount(-10) == 1
assert prg.linecount == 1
def test_set_filesize(self):
"""Test set_filesize method"""
prg = Progress()
assert prg.set_filesize(1024) == 1024
assert prg.filesize == 1024
assert prg.set_filesize(1048576) == 1048576
assert prg.filesize == 1048576
# Zero or negative should set to 1
assert prg.set_filesize(0) == 1
assert prg.filesize == 1
assert prg.set_filesize(-100) == 1
assert prg.filesize == 1
def test_set_wide_time(self):
"""Test set_wide_time method"""
prg = Progress()
assert prg.set_wide_time(True) is True
assert prg.wide_time is True
assert prg.set_wide_time(False) is False
assert prg.wide_time is False
def test_set_micro_time(self):
"""Test set_micro_time method"""
prg = Progress()
assert prg.set_micro_time(-1) == -1
assert prg.microtime == -1
assert prg.set_micro_time(0) == 0
assert prg.microtime == 0
assert prg.set_micro_time(1) == 1
assert prg.microtime == 1
def test_set_prefix_lb(self):
"""Test set_prefix_lb method"""
prg = Progress()
assert prg.set_prefix_lb(True) is True
assert prg.prefix_lb is True
assert prg.set_prefix_lb(False) is False
assert prg.prefix_lb is False
def test_set_start_time(self):
"""Test set_start_time method"""
prg = Progress()
initial_start = prg.start
# Wait a bit and set new start time
time.sleep(0.01)
new_time = time.time()
prg.set_start_time(new_time)
# Original start should not change
assert prg.start == initial_start
# But start_time and start_run should update
assert prg.start_time == new_time
assert prg.start_run == new_time
def test_set_start_time_custom_value(self):
"""Test set_start_time with custom time value"""
prg = Progress()
custom_time = 1234567890.0
prg.start = None # Reset start to test first-time setting
prg.set_start_time(custom_time)
assert prg.start == custom_time
assert prg.start_time == custom_time
assert prg.start_run == custom_time
def test_set_eta_start_time(self):
"""Test set_eta_start_time method"""
prg = Progress()
custom_time = time.time() + 100
prg.set_eta_start_time(custom_time)
assert prg.start_time == custom_time
assert prg.start_run == custom_time
def test_set_end_time(self):
"""Test set_end_time method"""
prg = Progress()
start_time = time.time()
prg.set_start_time(start_time)
time.sleep(0.01)
end_time = time.time()
prg.set_end_time(end_time)
assert prg.end == end_time
assert prg.end_time == end_time
assert prg.run_time is not None
assert prg.run_time > 0
def test_set_end_time_with_none_start(self):
"""Test set_end_time when start is None"""
prg = Progress()
prg.start = None
end_time = time.time()
prg.set_end_time(end_time)
assert prg.end == end_time
assert prg.run_time == end_time
class TestProgressReset:
"""Test suite for Progress reset method"""
def test_reset_basic(self):
"""Test reset method resets counter variables"""
prg = Progress()
prg.set_linecount(1000)
prg.set_filesize(10240)
prg.count = 500
prg.current_count = 500
prg.lines_processed = 100
prg.reset()
assert prg.count == 0
assert prg.current_count == 0
assert prg.linecount == 0
assert prg.lines_processed == 0
assert prg.filesize == 0
assert prg.last_percent == 0
def test_reset_preserves_start(self):
"""Test reset preserves the original start time"""
prg = Progress()
original_start = prg.start
prg.reset()
# Original start should still be set from initialization
assert prg.start == original_start
def test_reset_clears_runtime_data(self):
"""Test reset clears runtime calculation data"""
prg = Progress()
prg.eta = 100.5
prg.full_time_needed = 50.2
prg.last_group = 10.1
prg.lines_in_last_group = 5.5
prg.lines_in_global = 3.3
prg.reset()
assert prg.eta == 0
assert prg.full_time_needed == 0
assert prg.last_group == 0
assert prg.lines_in_last_group == 0
assert prg.lines_in_global == 0
class TestProgressShowPosition:
"""Test suite for Progress show_position method"""
def test_show_position_basic_linecount(self):
"""Test show_position with basic line count"""
prg = Progress(verbose=0)
prg.set_linecount(100)
# Process some lines
for _ in range(10):
prg.show_position()
assert prg.count == 10
assert prg.file_pos == 10
def test_show_position_with_filesize(self):
"""Test show_position with file size parameter"""
prg = Progress(verbose=0)
prg.set_filesize(1024)
prg.show_position(512)
assert prg.count == 1
assert prg.file_pos == 512
assert prg.count_size == 512
def test_show_position_percent_calculation(self):
"""Test show_position calculates percentage correctly"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process 50 lines
for _ in range(50):
prg.show_position()
assert prg.last_percent == 50.0
def test_show_position_ten_step_precision(self):
"""Test show_position with ten step precision"""
prg = Progress(verbose=0, precision=-1)
prg.set_linecount(100)
# Process lines, should only update at 10% intervals
for _ in range(15):
prg.show_position()
# Should be at 10% (not 15%)
assert prg.last_percent == 10
def test_show_position_five_step_precision(self):
"""Test show_position with five step precision"""
prg = Progress(verbose=0, precision=-2)
prg.set_linecount(100)
# Process lines, should only update at 5% intervals
for _ in range(7):
prg.show_position()
# Should be at 5% (not 7%)
assert prg.last_percent == 5
def test_show_position_change_flag(self):
"""Test show_position sets change flag correctly"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# First call should trigger change (at 1%)
prg.show_position()
assert prg.change == 1
last_percent = prg.last_percent
# Keep calling - each percent increment triggers change
prg.show_position()
# At precision=0, each 1% is a new change
if prg.last_percent != last_percent:
assert prg.change == 1
else:
assert prg.change == 0
def test_show_position_with_verbose_output(self, capsys: CaptureFixture[str]):
"""Test show_position produces output when verbose is enabled"""
prg = Progress(verbose=1, precision=0)
prg.set_linecount(100)
# Process until percent changes
for _ in range(10):
prg.show_position()
captured = capsys.readouterr()
assert "Processed" in captured.out
assert "Lines" in captured.out
def test_show_position_with_prefix_lb(self):
"""Test show_position with prefix line break"""
prg = Progress(verbose=1, precision=0, prefix_lb=True)
prg.set_linecount(100)
# Process until percent changes
for _ in range(10):
prg.show_position()
assert prg.string.startswith("\n")
def test_show_position_lines_processed_calculation(self):
"""Test show_position calculates lines processed correctly"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# First call at 1%
prg.show_position()
first_lines_processed = prg.lines_processed
assert first_lines_processed == 1
# Process to 2% (need to process 1 more line)
prg.show_position()
# lines_processed should be 1 (from 1 to 2)
assert prg.lines_processed == 1
def test_show_position_eta_calculation(self):
"""Test show_position calculates ETA"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(1000)
# We need to actually process lines for percent to change
# Process 100 lines to get to ~10%
for _ in range(100):
prg.show_position()
# ETA should be set after percent changes
assert prg.eta is not None
assert prg.eta >= 0
def test_show_position_with_filesize_output(self, capsys: CaptureFixture[str]):
"""Test show_position output with filesize information"""
prg = Progress(verbose=1, precision=0)
prg.set_filesize(10240)
# Process with filesize
for i in range(1, 1025):
prg.show_position(i)
captured = capsys.readouterr()
# Should contain byte information
assert "B" in captured.out or "KB" in captured.out
def test_show_position_bytes_calculation(self):
"""Test show_position calculates bytes per second"""
prg = Progress(verbose=0, precision=0)
prg.set_filesize(10240)
# Process enough bytes to trigger a percent change
# Need to process ~102 bytes for 1% of 10240
prg.show_position(102)
# After percent change, bytes stats should be set
assert prg.bytes_in_last_group >= 0
assert prg.bytes_in_global >= 0
def test_show_position_current_count_tracking(self):
"""Test show_position tracks current count correctly"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
for _ in range(10):
prg.show_position()
# Current count should be updated to last change point
assert prg.current_count == 10
assert prg.count == 10
def test_show_position_full_time_calculation(self):
"""Test show_position calculates full time needed"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process enough to trigger percent change
for _ in range(10):
prg.show_position()
assert prg.full_time_needed is not None
assert prg.full_time_needed >= 0
def test_show_position_last_group_time(self):
"""Test show_position tracks last group time"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process enough to trigger percent change
for _ in range(10):
prg.show_position()
# last_group should be set after percent change
assert prg.last_group >= 0
def test_show_position_zero_eta_edge_case(self):
"""Test show_position handles negative ETA gracefully"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process all lines
for _ in range(100):
prg.show_position()
# ETA should not be negative
assert prg.eta is not None
assert prg.eta >= 0
def test_show_position_no_filesize_string_format(self):
"""Test show_position string format without filesize"""
prg = Progress(verbose=1, precision=0)
prg.set_linecount(100)
for _ in range(10):
prg.show_position()
# String should not contain byte information
assert "b/s" not in prg.string
assert "Lines" in prg.string
def test_show_position_wide_time_format(self):
"""Test show_position with wide time formatting"""
prg = Progress(verbose=1, precision=0, wide_time=True)
prg.set_linecount(100)
for _ in range(10):
prg.show_position()
# With wide_time, time fields should be formatted with specific width
assert prg.string != ""
def test_show_position_microtime_on(self):
"""Test show_position with microtime enabled"""
prg = Progress(verbose=0, precision=0, microtime=1)
prg.set_linecount(100)
with patch('time.time') as mock_time:
mock_time.return_value = 1000.0
prg.set_start_time(1000.0)
mock_time.return_value = 1000.5
for _ in range(10):
prg.show_position()
# Microtime should be enabled
assert prg.microtime == 1
def test_show_position_microtime_off(self):
"""Test show_position with microtime disabled"""
prg = Progress(verbose=0, precision=0, microtime=-1)
prg.set_linecount(100)
for _ in range(10):
prg.show_position()
assert prg.microtime == -1
def test_show_position_lines_per_second_global(self):
"""Test show_position calculates global lines per second"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(1000)
# Process 100 lines to trigger percent changes
for _ in range(100):
prg.show_position()
# After processing, lines_in_global should be calculated
assert prg.lines_in_global >= 0
def test_show_position_lines_per_second_last_group(self):
"""Test show_position calculates last group lines per second"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(1000)
# Process lines to trigger percent changes
for _ in range(100):
prg.show_position()
# After processing, lines_in_last_group should be calculated
assert prg.lines_in_last_group >= 0
def test_show_position_returns_string(self):
"""Test show_position returns the progress string"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
result = ""
for _ in range(10):
result = prg.show_position()
# Should return string on percent change
assert isinstance(result, str)
class TestProgressEdgeCases:
"""Test suite for edge cases and error conditions"""
def test_zero_linecount_protection(self):
"""Test Progress handles zero linecount gracefully"""
prg = Progress(verbose=0)
prg.set_filesize(1024)
# Should not crash with zero linecount
prg.show_position(512)
assert prg.file_pos == 512
def test_zero_filesize_protection(self):
"""Test Progress handles zero filesize gracefully"""
prg = Progress(verbose=0)
prg.set_linecount(100)
# Should not crash with zero filesize
prg.show_position()
assert isinstance(prg.string, str)
def test_division_by_zero_protection_last_group(self):
"""Test Progress protects against division by zero in last_group"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
with patch('time.time') as mock_time:
# Same time for start and end
mock_time.return_value = 1000.0
prg.set_start_time(1000.0)
for _ in range(10):
prg.show_position()
# Should handle zero time difference
assert prg.lines_in_last_group >= 0
def test_division_by_zero_protection_full_time(self):
"""Test Progress protects against division by zero in full_time_needed"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process lines very quickly
for _ in range(10):
prg.show_position()
# Should handle very small time differences without crashing
# lines_in_global should be a valid number (>= 0)
assert isinstance(prg.lines_in_global, (int, float))
def test_none_start_protection(self):
"""Test Progress handles None start time"""
prg = Progress(verbose=0, precision=0)
prg.start = None
prg.set_linecount(100)
# Should not crash
prg.show_position()
assert prg.start == 0
def test_none_start_time_protection(self):
"""Test Progress handles None start_time"""
prg = Progress(verbose=0, precision=0)
prg.start_time = None
prg.set_linecount(100)
# Should not crash and should set start_time during processing
prg.show_position()
# start_time will be set to 0 internally when None is encountered
# But during percent calculation, it may be reset to current time
assert prg.start_time is not None
def test_precision_boundary_values(self):
"""Test precision at boundary values"""
prg = Progress()
# Minimum valid
assert prg.set_precision(-2) == 0
# Maximum valid
assert prg.set_precision(10) == 10
# Below minimum
assert prg.set_precision(-3) == 0
# Above maximum
assert prg.set_precision(11) == 0
def test_large_linecount_handling(self):
"""Test Progress handles large linecount values"""
prg = Progress(verbose=0)
large_count = 10_000_000
prg.set_linecount(large_count)
assert prg.linecount == large_count
# Should handle calculations without overflow
prg.show_position()
assert prg.count == 1
def test_large_filesize_handling(self):
"""Test Progress handles large filesize values"""
prg = Progress(verbose=0)
large_size = 10_737_418_240 # 10 GB
prg.set_filesize(large_size)
assert prg.filesize == large_size
# Should handle calculations without overflow
prg.show_position(1024)
assert prg.file_pos == 1024
class TestProgressIntegration:
"""Integration tests for Progress class"""
def test_complete_progress_workflow(self, capsys: CaptureFixture[str]):
"""Test complete progress workflow from start to finish"""
prg = Progress(verbose=1, precision=0)
prg.set_linecount(100)
# Simulate processing
for _ in range(100):
prg.show_position()
prg.set_end_time()
assert prg.count == 100
assert prg.last_percent == 100.0
assert prg.run_time is not None
captured = capsys.readouterr()
assert "Processed" in captured.out
def test_progress_with_filesize_workflow(self):
"""Test progress workflow with file size tracking"""
prg = Progress(verbose=0, precision=0)
prg.set_filesize(10240)
# Simulate reading file in chunks
for pos in range(0, 10240, 1024):
prg.show_position(pos + 1024)
assert prg.count == 10
assert prg.count_size == 10240
def test_reset_and_reuse(self):
"""Test resetting and reusing Progress instance"""
prg = Progress(verbose=0, precision=0)
# First run
prg.set_linecount(100)
for _ in range(100):
prg.show_position()
assert prg.count == 100
# Reset
prg.reset()
assert prg.count == 0
# Second run
prg.set_linecount(50)
for _ in range(50):
prg.show_position()
assert prg.count == 50
def test_multiple_precision_changes(self):
"""Test changing precision multiple times"""
prg = Progress(verbose=0)
prg.set_precision(0)
assert prg.precision == 0
prg.set_precision(2)
assert prg.precision == 2
prg.set_precision(-1)
assert prg.precision == 0
assert prg.precision_ten_step == 10
def test_eta_start_time_adjustment(self):
"""Test adjusting ETA start time mid-processing"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(1000)
# Process some lines
for _ in range(100):
prg.show_position()
# Adjust ETA start time (simulating delay like DB query)
new_time = time.time()
prg.set_eta_start_time(new_time)
# Continue processing
for _ in range(100):
prg.show_position()
assert prg.start_run == new_time
def test_verbose_toggle_during_processing(self):
"""Test toggling verbose flag during processing"""
prg = Progress(verbose=0, precision=0)
prg.set_linecount(100)
# Process without output
for _ in range(50):
prg.show_position()
# Enable verbose
prg.set_verbose(1)
assert prg.verbose is True
# Continue with output
for _ in range(50):
prg.show_position()
assert prg.count == 100

View File

@@ -1,164 +0,0 @@
"""
PyTest: string_handling/byte_helpers
"""
from corelibs.string_handling.byte_helpers import format_bytes
class TestFormatBytes:
"""Tests for format_bytes function"""
def test_string_input_returned_unchanged(self):
"""Test that string inputs are returned as-is"""
result = format_bytes("already formatted")
assert result == "already formatted"
def test_empty_string_returned_unchanged(self):
"""Test that empty strings are returned as-is"""
result = format_bytes("")
assert result == ""
def test_zero_int(self):
"""Test zero integer returns 0 bytes"""
result = format_bytes(0)
assert result == "0.00 B"
def test_zero_float(self):
"""Test zero float returns 0 bytes"""
result = format_bytes(0.0)
assert result == "0.00 B"
def test_none_value(self):
"""Test None is treated as 0 bytes"""
result = format_bytes(None) # type: ignore[arg-type]
assert result == "0.00 B"
def test_bytes_less_than_1kb(self):
"""Test formatting bytes less than 1KB"""
result = format_bytes(512)
assert result == "512.00 B"
def test_kilobytes(self):
"""Test formatting kilobytes"""
result = format_bytes(1024)
assert result == "1.00 KB"
def test_kilobytes_with_decimals(self):
"""Test formatting kilobytes with decimal values"""
result = format_bytes(1536) # 1.5 KB
assert result == "1.50 KB"
def test_megabytes(self):
"""Test formatting megabytes"""
result = format_bytes(1048576) # 1 MB
assert result == "1.00 MB"
def test_megabytes_with_decimals(self):
"""Test formatting megabytes with decimal values"""
result = format_bytes(2621440) # 2.5 MB
assert result == "2.50 MB"
def test_gigabytes(self):
"""Test formatting gigabytes"""
result = format_bytes(1073741824) # 1 GB
assert result == "1.00 GB"
def test_terabytes(self):
"""Test formatting terabytes"""
result = format_bytes(1099511627776) # 1 TB
assert result == "1.00 TB"
def test_petabytes(self):
"""Test formatting petabytes"""
result = format_bytes(1125899906842624) # 1 PB
assert result == "1.00 PB"
def test_exabytes(self):
"""Test formatting exabytes"""
result = format_bytes(1152921504606846976) # 1 EB
assert result == "1.00 EB"
def test_zettabytes(self):
"""Test formatting zettabytes"""
result = format_bytes(1180591620717411303424) # 1 ZB
assert result == "1.00 ZB"
def test_yottabytes(self):
"""Test formatting yottabytes"""
result = format_bytes(1208925819614629174706176) # 1 YB
assert result == "1.00 YB"
def test_negative_bytes(self):
"""Test formatting negative byte values"""
result = format_bytes(-512)
assert result == "-512.00 B"
def test_negative_kilobytes(self):
"""Test formatting negative kilobytes"""
result = format_bytes(-1024)
assert result == "-1.00 KB"
def test_negative_megabytes(self):
"""Test formatting negative megabytes"""
result = format_bytes(-1048576)
assert result == "-1.00 MB"
def test_float_input_bytes(self):
"""Test float input for bytes"""
result = format_bytes(512.5)
assert result == "512.50 B"
def test_float_input_kilobytes(self):
"""Test float input for kilobytes"""
result = format_bytes(1536.75)
assert result == "1.50 KB"
def test_large_number_formatting(self):
"""Test that large numbers use comma separators"""
result = format_bytes(10240) # 10 KB
assert result == "10.00 KB"
def test_very_large_byte_value(self):
"""Test very large byte value (beyond ZB)"""
result = format_bytes(1208925819614629174706176)
assert result == "1.00 YB"
def test_boundary_1023_bytes(self):
"""Test boundary case just below 1KB"""
result = format_bytes(1023)
assert result == "1,023.00 B"
def test_boundary_1024_bytes(self):
"""Test boundary case at exactly 1KB"""
result = format_bytes(1024)
assert result == "1.00 KB"
def test_int_converted_to_float(self):
"""Test that integer input is properly converted to float"""
result = format_bytes(2048)
assert result == "2.00 KB"
assert "." in result # Verify decimal point is present
def test_small_decimal_value(self):
"""Test small decimal byte value"""
result = format_bytes(0.5)
assert result == "0.50 B"
def test_precision_two_decimals(self):
"""Test that result always has two decimal places"""
result = format_bytes(1024)
assert result == "1.00 KB"
assert result.count('.') == 1
decimal_part = result.split('.')[1].split()[0]
assert len(decimal_part) == 2
def test_mixed_units_progression(self):
"""Test progression through multiple unit levels"""
# Start with bytes
assert "B" in format_bytes(100)
# Move to KB
assert "KB" in format_bytes(100 * 1024)
# Move to MB
assert "MB" in format_bytes(100 * 1024 * 1024)
# Move to GB
assert "GB" in format_bytes(100 * 1024 * 1024 * 1024)

View File

@@ -1,524 +0,0 @@
"""
PyTest: string_handling/double_byte_string_format
"""
import pytest
from corelibs.string_handling.double_byte_string_format import DoubleByteFormatString
class TestDoubleByteFormatStringInit:
"""Tests for DoubleByteFormatString initialization"""
def test_basic_initialization(self):
"""Test basic initialization with string and cut_length"""
formatter = DoubleByteFormatString("Hello World", 10)
assert formatter.string == "Hello World"
assert formatter.cut_length == 10
assert formatter.format_length == 10
assert formatter.placeholder == ".."
def test_initialization_with_format_length(self):
"""Test initialization with both cut_length and format_length"""
formatter = DoubleByteFormatString("Hello World", 5, 15)
assert formatter.cut_length == 5
assert formatter.format_length == 15
def test_initialization_with_custom_placeholder(self):
"""Test initialization with custom placeholder"""
formatter = DoubleByteFormatString("Hello World", 10, placeholder="...")
assert formatter.placeholder == "..."
def test_initialization_with_custom_format_string(self):
"""Test initialization with custom format string"""
formatter = DoubleByteFormatString("Hello", 10, format_string="{{:>{len}}}")
assert formatter.format_string == "{{:>{len}}}"
def test_zero_cut_length_uses_string_width(self):
"""Test that zero cut_length defaults to string width"""
formatter = DoubleByteFormatString("Hello", 0)
assert formatter.cut_length > 0
# For ASCII string, width should equal length
assert formatter.cut_length == 5
def test_negative_cut_length_uses_string_width(self):
"""Test that negative cut_length defaults to string width"""
formatter = DoubleByteFormatString("Hello", -5)
assert formatter.cut_length > 0
def test_cut_length_adjusted_to_format_length(self):
"""Test that cut_length is adjusted when larger than format_length"""
formatter = DoubleByteFormatString("Hello World", 20, 10)
assert formatter.cut_length == 10 # Should be min(20, 10)
def test_none_format_length(self):
"""Test with None format_length"""
formatter = DoubleByteFormatString("Hello", 10, None)
assert formatter.format_length == 10 # Should default to cut_length
class TestDoubleByteFormatStringWithAscii:
"""Tests for ASCII (single-byte) string handling"""
def test_ascii_no_shortening_needed(self):
"""Test ASCII string shorter than cut_length"""
formatter = DoubleByteFormatString("Hello", 10)
assert formatter.get_string_short() == "Hello"
assert formatter.string_short_width == 0 # Not set because no shortening
def test_ascii_exact_cut_length(self):
"""Test ASCII string equal to cut_length"""
formatter = DoubleByteFormatString("Hello", 5)
assert formatter.get_string_short() == "Hello"
def test_ascii_shortening_required(self):
"""Test ASCII string requiring shortening"""
formatter = DoubleByteFormatString("Hello World", 8)
result = formatter.get_string_short()
assert result == "Hello .."
assert len(result) == 8
def test_ascii_with_custom_placeholder(self):
"""Test ASCII shortening with custom placeholder"""
formatter = DoubleByteFormatString("Hello World", 8, placeholder="...")
result = formatter.get_string_short()
assert result.endswith("...")
assert len(result) == 8
def test_ascii_very_short_cut_length(self):
"""Test ASCII with very short cut_length"""
formatter = DoubleByteFormatString("Hello World", 3)
result = formatter.get_string_short()
assert result == "H.."
assert len(result) == 3
def test_ascii_format_length_calculation(self):
"""Test format_length calculation for ASCII strings"""
formatter = DoubleByteFormatString("Hello", 10, 15)
# String is not shortened, format_length should be 15
assert formatter.get_format_length() == 15
class TestDoubleByteFormatStringWithDoubleByte:
"""Tests for double-byte (Asian) character handling"""
def test_japanese_characters(self):
"""Test Japanese string handling"""
formatter = DoubleByteFormatString("こんにちは", 10)
# Each Japanese character is double-width
# "こんにちは" = 5 chars * 2 width = 10 width
assert formatter.get_string_short() == "こんにちは"
def test_japanese_shortening(self):
"""Test Japanese string requiring shortening"""
formatter = DoubleByteFormatString("こんにちは世界", 8)
# Should fit 3 double-width chars (6 width) + placeholder (2 chars)
result = formatter.get_string_short()
assert result.endswith("..")
assert len(result) <= 5 # 3 Japanese chars + 2 placeholder chars
def test_chinese_characters(self):
"""Test Chinese string handling"""
formatter = DoubleByteFormatString("你好世界", 8)
# 4 Chinese chars = 8 width, should fit exactly
assert formatter.get_string_short() == "你好世界"
def test_chinese_shortening(self):
"""Test Chinese string requiring shortening"""
formatter = DoubleByteFormatString("你好世界朋友", 8)
# Should fit 3 double-width chars (6 width) + placeholder (2 chars)
result = formatter.get_string_short()
assert result.endswith("..")
assert len(result) <= 5
def test_korean_characters(self):
"""Test Korean string handling"""
formatter = DoubleByteFormatString("안녕하세요", 10)
# Korean characters are also double-width
assert formatter.get_string_short() == "안녕하세요"
def test_mixed_ascii_japanese(self):
"""Test mixed ASCII and Japanese characters"""
formatter = DoubleByteFormatString("Hello世界", 10)
# "Hello" = 5 width, "世界" = 4 width, total = 9 width
assert formatter.get_string_short() == "Hello世界"
def test_mixed_ascii_japanese_shortening(self):
"""Test mixed string requiring shortening"""
formatter = DoubleByteFormatString("Hello世界Test", 10)
# Should shorten to fit within 10 width
result = formatter.get_string_short()
assert result.endswith("..")
# Total visual width should be <= 10
def test_fullwidth_ascii(self):
"""Test fullwidth ASCII characters"""
# Fullwidth ASCII characters (U+FF01 to U+FF5E)
formatter = DoubleByteFormatString("world", 10)
result = formatter.get_string_short()
assert result.endswith("..")
class TestDoubleByteFormatStringGetters:
"""Tests for getter methods"""
def test_get_string_short(self):
"""Test get_string_short method"""
formatter = DoubleByteFormatString("Hello World", 8)
result = formatter.get_string_short()
assert isinstance(result, str)
assert result == "Hello .."
def test_get_format_length(self):
"""Test get_format_length method"""
formatter = DoubleByteFormatString("Hello", 5, 10)
assert formatter.get_format_length() == 10
def test_get_cut_length(self):
"""Test get_cut_length method"""
formatter = DoubleByteFormatString("Hello", 8)
assert formatter.get_cut_length() == 8
def test_get_requested_cut_length(self):
"""Test get_requested_cut_length method"""
formatter = DoubleByteFormatString("Hello", 15)
assert formatter.get_requested_cut_length() == 15
def test_get_requested_format_length(self):
"""Test get_requested_format_length method"""
formatter = DoubleByteFormatString("Hello", 5, 20)
assert formatter.get_requested_format_length() == 20
def test_get_string_short_formated_default(self):
"""Test get_string_short_formated with default format"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated()
assert isinstance(result, str)
assert len(result) == 10 # Should be padded to format_length
assert result.startswith("Hello")
def test_get_string_short_formated_custom(self):
"""Test get_string_short_formated with custom format string"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated("{{:>{len}}}")
assert isinstance(result, str)
assert result.endswith("Hello") # Right-aligned
def test_get_string_short_formated_empty_format_string(self):
"""Test get_string_short_formated with empty format string falls back to default"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated("")
# Should use default format_string from initialization
assert isinstance(result, str)
class TestDoubleByteFormatStringFormatting:
"""Tests for formatted output"""
def test_format_with_padding(self):
"""Test formatted string with padding"""
formatter = DoubleByteFormatString("Hello", 5, 10)
result = formatter.get_string_short_formated()
assert len(result) == 10
assert result == "Hello " # Left-aligned with spaces
def test_format_shortened_string(self):
"""Test formatted shortened string"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
result = formatter.get_string_short_formated()
# Should be "Hello .." padded to 12
assert len(result) == 12
assert result.startswith("Hello ..")
def test_format_with_double_byte_chars(self):
"""Test formatting with double-byte characters"""
formatter = DoubleByteFormatString("日本語", 6, 10)
result = formatter.get_string_short_formated()
# "日本語" = 3 chars * 2 width = 6 width
# Format should account for visual width difference
assert isinstance(result, str)
def test_format_shortened_double_byte(self):
"""Test formatting shortened double-byte string"""
formatter = DoubleByteFormatString("こんにちは世界", 8, 12)
result = formatter.get_string_short_formated()
assert isinstance(result, str)
# Should be shortened and formatted
class TestDoubleByteFormatStringProcess:
"""Tests for process method"""
def test_process_called_on_init(self):
"""Test that process is called during initialization"""
formatter = DoubleByteFormatString("Hello World", 8)
# process() should have been called, so string_short should be set
assert formatter.string_short != ''
def test_manual_process_call(self):
"""Test calling process manually"""
formatter = DoubleByteFormatString("Hello World", 8)
# Modify internal state
formatter.string = "New String"
# Call process again
formatter.process()
# Should recalculate based on new string
assert formatter.string_short != ''
def test_process_with_empty_string(self):
"""Test process with empty string"""
formatter = DoubleByteFormatString("", 10)
formatter.process()
# Should handle empty string gracefully
assert formatter.string_short == ''
class TestDoubleByteFormatStringEdgeCases:
"""Tests for edge cases"""
def test_empty_string(self):
"""Test with empty string"""
formatter = DoubleByteFormatString("", 10)
assert formatter.get_string_short() == ""
def test_single_character(self):
"""Test with single character"""
formatter = DoubleByteFormatString("A", 5)
assert formatter.get_string_short() == "A"
def test_single_double_byte_character(self):
"""Test with single double-byte character"""
formatter = DoubleByteFormatString("", 5)
assert formatter.get_string_short() == ""
def test_placeholder_only_length(self):
"""Test when cut_length equals placeholder length"""
formatter = DoubleByteFormatString("Hello World", 2)
result = formatter.get_string_short()
assert result == ".."
def test_very_long_string(self):
"""Test with very long string"""
long_string = "A" * 1000
formatter = DoubleByteFormatString(long_string, 10)
result = formatter.get_string_short()
assert len(result) == 10
assert result.endswith("..")
def test_very_long_double_byte_string(self):
"""Test with very long double-byte string"""
long_string = "" * 500
formatter = DoubleByteFormatString(long_string, 10)
result = formatter.get_string_short()
# Should be shortened to fit 10 visual width
assert result.endswith("..")
def test_special_characters(self):
"""Test with special characters"""
formatter = DoubleByteFormatString("Hello!@#$%^&*()", 10)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_newlines_and_tabs(self):
"""Test with newlines and tabs"""
formatter = DoubleByteFormatString("Hello\nWorld\t!", 10)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_unicode_emoji(self):
"""Test with Unicode emoji"""
formatter = DoubleByteFormatString("Hello 👋 World 🌍", 15)
result = formatter.get_string_short()
assert isinstance(result, str)
def test_non_string_input_conversion(self):
"""Test that non-string inputs are converted to string"""
formatter = DoubleByteFormatString(12345, 10) # type: ignore[arg-type]
assert formatter.string == "12345"
assert formatter.get_string_short() == "12345"
def test_none_conversion(self):
"""Test None conversion to string"""
formatter = DoubleByteFormatString(None, 10) # type: ignore[arg-type]
assert formatter.string == "None"
class TestDoubleByteFormatStringWidthCalculation:
"""Tests for width calculation accuracy"""
def test_ascii_width_calculation(self):
"""Test width calculation for ASCII"""
formatter = DoubleByteFormatString("Hello", 10)
formatter.process()
# ASCII characters should have width = length
assert formatter.string_width_value == 5
def test_japanese_width_calculation(self):
"""Test width calculation for Japanese"""
formatter = DoubleByteFormatString("こんにちは", 20)
formatter.process()
# 5 Japanese characters * 2 width each = 10
assert formatter.string_width_value == 10
def test_mixed_width_calculation(self):
"""Test width calculation for mixed characters"""
formatter = DoubleByteFormatString("Hello日本", 20)
formatter.process()
# "Hello" = 5 width, "日本" = 4 width, total = 9
assert formatter.string_width_value == 9
def test_fullwidth_latin_calculation(self):
"""Test width calculation for fullwidth Latin characters"""
# Fullwidth Latin letters
formatter = DoubleByteFormatString("", 10)
formatter.process()
# 3 fullwidth characters * 2 width each = 6
assert formatter.string_width_value == 6
# Parametrized tests
@pytest.mark.parametrize("string,cut_length,expected_short", [
("Hello", 10, "Hello"),
("Hello World", 8, "Hello .."),
("Hello World Test", 5, "Hel.."),
("", 5, ""),
("A", 5, "A"),
])
def test_ascii_shortening_parametrized(string: str, cut_length: int, expected_short: str):
"""Parametrized test for ASCII string shortening"""
formatter = DoubleByteFormatString(string, cut_length)
assert formatter.get_string_short() == expected_short
@pytest.mark.parametrize("string,cut_length,format_length,expected_format_len", [
("Hello", 5, 10, 10),
("Hello", 10, 5, 5),
("Hello World", 8, 12, 12),
])
def test_format_length_parametrized(
string: str,
cut_length: int,
format_length: int,
expected_format_len: int
):
"""Parametrized test for format length"""
formatter = DoubleByteFormatString(string, cut_length, format_length)
assert formatter.get_format_length() == expected_format_len
@pytest.mark.parametrize("string,expected_width", [
("Hello", 5),
("こんにちは", 10), # 5 Japanese chars * 2
("Hello日本", 9), # 5 + 4
("", 0),
("A", 1),
("", 2),
])
def test_width_calculation_parametrized(string: str, expected_width: int):
"""Parametrized test for width calculation"""
formatter = DoubleByteFormatString(string, 100) # Large cut_length to avoid shortening
formatter.process()
if string:
assert formatter.string_width_value == expected_width
else:
assert formatter.string_width_value == 0
@pytest.mark.parametrize("placeholder", [
"..",
"...",
"",
">>>",
"~",
])
def test_custom_placeholder_parametrized(placeholder: str):
"""Parametrized test for custom placeholders"""
formatter = DoubleByteFormatString("Hello World Test", 8, placeholder=placeholder)
result = formatter.get_string_short()
assert result.endswith(placeholder)
assert len(result) == 8
class TestDoubleByteFormatStringIntegration:
"""Integration tests for complete workflows"""
def test_complete_workflow_ascii(self):
"""Test complete workflow with ASCII string"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short == "Hello .."
assert len(formatted) == 12
assert formatted.startswith("Hello ..")
def test_complete_workflow_japanese(self):
"""Test complete workflow with Japanese string"""
formatter = DoubleByteFormatString("こんにちは世界", 8, 12)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short.endswith("..")
assert isinstance(formatted, str)
def test_complete_workflow_mixed(self):
"""Test complete workflow with mixed characters"""
formatter = DoubleByteFormatString("Hello世界World", 10, 15)
short = formatter.get_string_short()
formatted = formatter.get_string_short_formated()
assert short.endswith("..")
assert isinstance(formatted, str)
def test_table_like_output(self):
"""Test creating table-like output with multiple formatters"""
items = [
("Name", "Alice", 10, 15),
("City", "Tokyo東京", 10, 15),
("Country", "Japan日本国", 10, 15),
]
results: list[str] = []
for _label, value, cut, fmt in items:
formatter = DoubleByteFormatString(value, cut, fmt)
results.append(formatter.get_string_short_formated())
# All results should be formatted strings
# Note: Due to double-byte character width adjustments,
# the actual string length may differ from format_length
assert all(isinstance(result, str) for result in results)
assert all(len(result) > 0 for result in results)
def test_reprocess_after_modification(self):
"""Test reprocessing after modifying formatter properties"""
formatter = DoubleByteFormatString("Hello World", 8, 12)
initial = formatter.get_string_short()
# Modify and reprocess
formatter.string = "New String Test"
formatter.process()
modified = formatter.get_string_short()
assert initial != modified
assert modified.endswith("..")
class TestDoubleByteFormatStringRightAlignment:
"""Tests for right-aligned formatting"""
def test_right_aligned_format(self):
"""Test right-aligned formatting"""
formatter = DoubleByteFormatString("Hello", 5, 10, format_string="{{:>{len}}}")
result = formatter.get_string_short_formated()
assert len(result) == 10
# The format applies to the short string
assert "Hello" in result
def test_center_aligned_format(self):
"""Test center-aligned formatting"""
formatter = DoubleByteFormatString("Hello", 5, 11, format_string="{{:^{len}}}")
result = formatter.get_string_short_formated()
assert len(result) == 11
assert "Hello" in result
# __END__

View File

@@ -1,328 +0,0 @@
"""
PyTest: string_handling/hash_helpers
"""
import pytest
from corelibs.string_handling.hash_helpers import (
crc32b_fix, sha1_short
)
class TestCrc32bFix:
"""Tests for crc32b_fix function"""
def test_basic_crc_fix(self):
"""Test basic CRC32B byte order fix"""
# Example: if input is "abcdefgh", it should become "ghefcdab"
result = crc32b_fix("abcdefgh")
assert result == "ghefcdab"
def test_short_crc_padding(self):
"""Test that short CRC is left-padded with zeros"""
# Input with 6 chars should be padded to 8: "00abcdef"
# Split into pairs: "00", "ab", "cd", "ef"
# Reversed: "ef", "cd", "ab", "00"
result = crc32b_fix("abcdef")
assert result == "efcdab00"
assert len(result) == 8
def test_4_char_crc(self):
"""Test CRC with 4 characters"""
# Padded: "0000abcd"
# Pairs: "00", "00", "ab", "cd"
# Reversed: "cd", "ab", "00", "00"
result = crc32b_fix("abcd")
assert result == "cdab0000"
assert len(result) == 8
def test_2_char_crc(self):
"""Test CRC with 2 characters"""
# Padded: "000000ab"
# Pairs: "00", "00", "00", "ab"
# Reversed: "ab", "00", "00", "00"
result = crc32b_fix("ab")
assert result == "ab000000"
assert len(result) == 8
def test_1_char_crc(self):
"""Test CRC with 1 character"""
# Padded: "0000000a"
# Pairs: "00", "00", "00", "0a"
# Reversed: "0a", "00", "00", "00"
result = crc32b_fix("a")
assert result == "0a000000"
assert len(result) == 8
def test_empty_crc(self):
"""Test empty CRC string"""
result = crc32b_fix("")
assert result == "00000000"
assert len(result) == 8
def test_numeric_crc(self):
"""Test CRC with numeric characters"""
result = crc32b_fix("12345678")
assert result == "78563412"
def test_mixed_alphanumeric(self):
"""Test CRC with mixed alphanumeric characters"""
result = crc32b_fix("a1b2c3d4")
assert result == "d4c3b2a1"
def test_lowercase_letters(self):
"""Test CRC with lowercase letters"""
result = crc32b_fix("aabbccdd")
assert result == "ddccbbaa"
def test_with_numbers_and_letters(self):
"""Test CRC with numbers and letters (typical hex)"""
result = crc32b_fix("1a2b3c4d")
assert result == "4d3c2b1a"
def test_all_zeros(self):
"""Test CRC with all zeros"""
result = crc32b_fix("00000000")
assert result == "00000000"
def test_short_padding_all_numbers(self):
"""Test padding with all numbers"""
# Padded: "00123456"
# Pairs: "00", "12", "34", "56"
# Reversed: "56", "34", "12", "00"
result = crc32b_fix("123456")
assert result == "56341200"
assert len(result) == 8
def test_typical_hex_values(self):
"""Test with typical hexadecimal hash values"""
result = crc32b_fix("a1b2c3d4")
assert result == "d4c3b2a1"
def test_7_char_crc(self):
"""Test CRC with 7 characters (needs 1 zero padding)"""
# Padded: "0abcdefg"
# Pairs: "0a", "bc", "de", "fg"
# Reversed: "fg", "de", "bc", "0a"
result = crc32b_fix("abcdefg")
assert result == "fgdebc0a"
assert len(result) == 8
class TestSha1Short:
"""Tests for sha1_short function"""
def test_basic_sha1_short(self):
"""Test basic SHA1 short hash generation"""
result = sha1_short("hello")
assert len(result) == 9
assert result.isalnum() # Should be hexadecimal
def test_consistent_output(self):
"""Test that same input produces same output"""
result1 = sha1_short("test")
result2 = sha1_short("test")
assert result1 == result2
def test_different_inputs_different_outputs(self):
"""Test that different inputs produce different outputs"""
result1 = sha1_short("hello")
result2 = sha1_short("world")
assert result1 != result2
def test_empty_string(self):
"""Test SHA1 of empty string"""
result = sha1_short("")
assert len(result) == 9
# SHA1 of empty string is known: "da39a3ee5e6b4b0d3255bfef95601890afd80709"
assert result == "da39a3ee5"
def test_single_character(self):
"""Test SHA1 of single character"""
result = sha1_short("a")
assert len(result) == 9
# SHA1 of "a" is "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8"
assert result == "86f7e437f"
def test_long_string(self):
"""Test SHA1 of long string"""
long_string = "a" * 1000
result = sha1_short(long_string)
assert len(result) == 9
assert result.isalnum()
def test_special_characters(self):
"""Test SHA1 with special characters"""
result = sha1_short("hello@world!")
assert len(result) == 9
assert result.isalnum()
def test_unicode_characters(self):
"""Test SHA1 with unicode characters"""
result = sha1_short("こんにちは")
assert len(result) == 9
assert result.isalnum()
def test_numbers(self):
"""Test SHA1 with numeric string"""
result = sha1_short("12345")
assert len(result) == 9
assert result.isalnum()
def test_whitespace(self):
"""Test SHA1 with whitespace"""
result1 = sha1_short("hello world")
result2 = sha1_short("helloworld")
assert result1 != result2
assert len(result1) == 9
assert len(result2) == 9
def test_newlines_and_tabs(self):
"""Test SHA1 with newlines and tabs"""
result = sha1_short("hello\nworld\ttab")
assert len(result) == 9
assert result.isalnum()
def test_mixed_case(self):
"""Test SHA1 with mixed case (should be case sensitive)"""
result1 = sha1_short("Hello")
result2 = sha1_short("hello")
assert result1 != result2
def test_hexadecimal_output(self):
"""Test that output is valid hexadecimal"""
result = sha1_short("test")
# Should only contain 0-9 and a-f
assert all(c in "0123456789abcdef" for c in result)
def test_known_value_verification(self):
"""Test against known SHA1 values"""
# SHA1 of "hello" is "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
result = sha1_short("hello")
assert result == "aaf4c61dd"
def test_numeric_string_input(self):
"""Test with numeric string"""
result = sha1_short("123456789")
assert len(result) == 9
assert result.isalnum()
def test_emoji_input(self):
"""Test with emoji characters"""
result = sha1_short("😀🎉")
assert len(result) == 9
assert result.isalnum()
def test_multiline_string(self):
"""Test with multiline string"""
multiline = """This is
a multiline
string"""
result = sha1_short(multiline)
assert len(result) == 9
assert result.isalnum()
# Parametrized tests
@pytest.mark.parametrize("input_crc,expected", [
("abcdefgh", "ghefcdab"),
("12345678", "78563412"),
("aabbccdd", "ddccbbaa"),
("00000000", "00000000"),
("", "00000000"),
("a", "0a000000"),
("ab", "ab000000"),
("abcd", "cdab0000"),
("abcdef", "efcdab00"),
])
def test_crc32b_fix_parametrized(input_crc: str, expected: str):
"""Parametrized test for crc32b_fix"""
result = crc32b_fix(input_crc)
assert len(result) == 8
assert result == expected
@pytest.mark.parametrize("input_string,expected_length", [
("hello", 9),
("world", 9),
("", 9),
("a" * 1000, 9),
("test123", 9),
("😀", 9),
])
def test_sha1_short_parametrized_length(input_string: str, expected_length: int):
"""Parametrized test for sha1_short to verify consistent length"""
result = sha1_short(input_string)
assert len(result) == expected_length
@pytest.mark.parametrize("input_string,expected_hash", [
("", "da39a3ee5"),
("a", "86f7e437f"),
("hello", "aaf4c61dd"),
("world", "7c211433f"),
("test", "a94a8fe5c"),
])
def test_sha1_short_known_values(input_string: str, expected_hash: str):
"""Parametrized test for sha1_short with known SHA1 values"""
result = sha1_short(input_string)
assert result == expected_hash
# Edge case tests
class TestEdgeCases:
"""Test edge cases for hash helper functions"""
def test_crc32b_fix_with_max_length(self):
"""Test crc32b_fix with exactly 8 characters"""
result = crc32b_fix("ffffffff")
assert result == "ffffffff"
assert len(result) == 8
def test_sha1_short_very_long_input(self):
"""Test sha1_short with very long input"""
very_long = "x" * 10000
result = sha1_short(very_long)
assert len(result) == 9
assert result.isalnum()
def test_sha1_short_binary_like_string(self):
"""Test sha1_short with binary-like string"""
result = sha1_short("\x00\x01\x02\x03")
assert len(result) == 9
assert result.isalnum()
def test_crc32b_fix_preserves_characters(self):
"""Test that crc32b_fix only reorders, doesn't change characters"""
input_crc = "12345678"
result = crc32b_fix(input_crc)
# All characters from input should be in output (after padding)
for char in input_crc:
assert char in result or '0' in result # 0 is for padding
# Integration tests
class TestIntegration:
"""Integration tests for hash helper functions"""
def test_sha1_short_produces_valid_crc_input(self):
"""Test that sha1_short output could be used as CRC input"""
sha1_result = sha1_short("test")
# SHA1 short is 9 chars, CRC expects up to 8, so take first 8
crc_input = sha1_result[:8]
crc_result = crc32b_fix(crc_input)
assert len(crc_result) == 8
def test_multiple_sha1_short_consistency(self):
"""Test that multiple calls to sha1_short are consistent"""
results = [sha1_short("consistency_test") for _ in range(10)]
assert all(r == results[0] for r in results)
def test_crc32b_fix_reversibility_concept(self):
"""Test that applying crc32b_fix twice reverses the operation"""
original = "abcdefgh"
fixed_once = crc32b_fix(original)
fixed_twice = crc32b_fix(fixed_once)
assert fixed_twice == original
# __END__

View File

@@ -1,327 +0,0 @@
"""
PyTest: string_handling/string_helpers
"""
from textwrap import shorten
import pytest
from corelibs.string_handling.string_helpers import (
shorten_string, left_fill, format_number, prepare_url_slash
)
class TestShortenString:
"""Tests for shorten_string function"""
def test_string_shorter_than_length(self):
"""Test that strings shorter than length are returned unchanged"""
result = shorten_string("hello", 10)
assert result == "hello"
def test_string_equal_to_length(self):
"""Test that strings equal to length are returned unchanged"""
result = shorten_string("hello", 5)
assert result == "hello"
def test_hard_shorten_true(self):
"""Test hard shortening with default placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True)
assert result == "hell [~]"
def test_hard_shorten_custom_placeholder(self):
"""Test hard shortening with custom placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True, placeholder="...")
assert result == "hello..."
def test_no_spaces_auto_hard_shorten(self):
"""Test that strings without spaces automatically use hard shorten"""
result = shorten_string("helloworld", 8)
assert result == "hell [~]"
def test_soft_shorten_with_spaces(self):
"""Test soft shortening using textwrap.shorten"""
result = shorten_string("hello world test", 12)
# Should use textwrap.shorten behavior
expected = shorten("hello world test", width=12, placeholder=" [~]")
assert result == expected
def test_placeholder_too_large_hard_shorten(self):
"""Test error when placeholder is larger than allowed length"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, hard_shorten=True, placeholder=" [~]")
def test_placeholder_too_large_no_spaces(self):
"""Test error when placeholder is larger than allowed length for string without spaces"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, placeholder=" [~]")
def test_textwrap_shorten_error(self):
"""Test handling of textwrap.shorten ValueError"""
# This might be tricky to trigger, but we can mock it
with pytest.raises(ValueError, match="Cannot shorten string:"):
# Very short length that might cause textwrap.shorten to fail
shorten_string("hello world", 1, hard_shorten=False)
def test_type_conversion(self):
"""Test that inputs are converted to proper types"""
result = shorten_string(12345, 8, hard_shorten=True)
assert result == "12345"
def test_empty_string(self):
"""Test with empty string"""
result = shorten_string("", 5)
assert result == ""
def test_zero_length(self):
"""Test with zero length"""
with pytest.raises(ValueError):
shorten_string("hello", 0, hard_shorten=True)
class TestLeftFill:
"""Tests for left_fill function"""
def test_basic_left_fill(self):
"""Test basic left filling with spaces"""
result = left_fill("hello", 10)
assert result == " hello"
assert len(result) == 10
def test_custom_fill_character(self):
"""Test left filling with custom character"""
result = left_fill("hello", 10, "0")
assert result == "00000hello"
def test_string_longer_than_width(self):
"""Test when string is longer than width"""
result = left_fill("hello world", 5)
assert result == "hello world" # Should return original string
def test_string_equal_to_width(self):
"""Test when string equals width"""
result = left_fill("hello", 5)
assert result == "hello"
def test_negative_width(self):
"""Test with negative width"""
result = left_fill("hello", -5)
assert result == "hello" # Should use string length
def test_zero_width(self):
"""Test with zero width"""
result = left_fill("hello", 0)
assert result == "hello" # Should return original string
def test_invalid_fill_character(self):
"""Test with invalid fill character (not single char)"""
result = left_fill("hello", 10, "abc")
assert result == " hello" # Should default to space
def test_empty_fill_character(self):
"""Test with empty fill character"""
result = left_fill("hello", 10, "")
assert result == " hello" # Should default to space
def test_empty_string(self):
"""Test with empty string"""
result = left_fill("", 5)
assert result == " "
class TestFormatNumber:
"""Tests for format_number function"""
def test_integer_default_precision(self):
"""Test formatting integer with default precision"""
result = format_number(1234)
assert result == "1,234"
def test_float_default_precision(self):
"""Test formatting float with default precision"""
result = format_number(1234.56)
assert result == "1,235" # Should round to nearest integer
def test_with_precision(self):
"""Test formatting with specified precision"""
result = format_number(1234.5678, 2)
assert result == "1,234.57"
def test_large_number(self):
"""Test formatting large number"""
result = format_number(1234567.89, 2)
assert result == "1,234,567.89"
def test_zero(self):
"""Test formatting zero"""
result = format_number(0)
assert result == "0"
def test_negative_number(self):
"""Test formatting negative number"""
result = format_number(-1234.56, 2)
assert result == "-1,234.56"
def test_negative_precision(self):
"""Test with negative precision (should default to 0)"""
result = format_number(1234.56, -1)
assert result == "1,235"
def test_excessive_precision(self):
"""Test with precision > 100 (should default to 0)"""
result = format_number(1234.56, 101)
assert result == "1,235"
def test_precision_boundary_values(self):
"""Test precision boundary values"""
# Test precision = 0 (should work)
result = format_number(1234.56, 0)
assert result == "1,235"
# Test precision = 100 (should work)
result = format_number(1234.56, 100)
assert "1,234.56" in result # Will have many trailing zeros
def test_small_decimal(self):
"""Test formatting small decimal number"""
result = format_number(0.123456, 4)
assert result == "0.1235"
def test_very_small_number(self):
"""Test formatting very small number"""
result = format_number(0.001, 3)
assert result == "0.001"
class TestPrepareUrlSlash:
"""Tests for prepare_url_slash function"""
def test_url_without_leading_slash(self):
"""Test that URL without leading slash gets one added"""
result = prepare_url_slash("api/users")
assert result == "/api/users"
def test_url_with_leading_slash(self):
"""Test that URL with leading slash remains unchanged"""
result = prepare_url_slash("/api/users")
assert result == "/api/users"
def test_url_with_double_slashes(self):
"""Test that double slashes are reduced to single slash"""
result = prepare_url_slash("/api//users")
assert result == "/api/users"
def test_url_with_multiple_slashes(self):
"""Test that multiple consecutive slashes are reduced to single slash"""
result = prepare_url_slash("api///users////data")
assert result == "/api/users/data"
def test_url_with_leading_double_slash(self):
"""Test URL starting with double slash"""
result = prepare_url_slash("//api/users")
assert result == "/api/users"
def test_url_without_slash_and_double_slashes(self):
"""Test URL without leading slash and containing double slashes"""
result = prepare_url_slash("api//users//data")
assert result == "/api/users/data"
def test_single_slash(self):
"""Test single slash URL"""
result = prepare_url_slash("/")
assert result == "/"
def test_multiple_slashes_only(self):
"""Test URL with only multiple slashes"""
result = prepare_url_slash("///")
assert result == "/"
def test_empty_string(self):
"""Test empty string"""
result = prepare_url_slash("")
assert result == "/"
def test_url_with_query_params(self):
"""Test URL with query parameters"""
result = prepare_url_slash("/api/users?id=1")
assert result == "/api/users?id=1"
def test_url_with_double_slashes_and_query(self):
"""Test URL with double slashes and query parameters"""
result = prepare_url_slash("api//users?id=1")
assert result == "/api/users?id=1"
def test_complex_url_path(self):
"""Test complex URL path with multiple segments"""
result = prepare_url_slash("api/v1/users/123/profile")
assert result == "/api/v1/users/123/profile"
def test_complex_url_with_multiple_issues(self):
"""Test URL with both missing leading slash and multiple double slashes"""
result = prepare_url_slash("api//v1///users//123////profile")
assert result == "/api/v1/users/123/profile"
# Additional integration tests
class TestIntegration:
"""Integration tests combining functions"""
def test_format_and_fill(self):
"""Test formatting a number then left filling"""
formatted = format_number(1234.56, 2)
result = left_fill(formatted, 15)
assert result.endswith("1,234.56")
assert len(result) == 15
def test_format_and_shorten(self):
"""Test formatting a large number then shortening"""
formatted = format_number(123456789.123, 3)
result = shorten_string(formatted, 10)
assert len(result) <= 10
# Fixtures for parameterized tests
@pytest.mark.parametrize("input_str,length,expected", [
("hello", 10, "hello"),
("hello world", 5, "h [~]"),
("test", 4, "test"),
("", 5, ""),
])
def test_shorten_string_parametrized(input_str: str, length: int, expected: str):
"""Parametrized test for shorten_string"""
result = shorten_string(input_str, length, hard_shorten=True)
if expected.endswith(" [~]"):
assert result.endswith(" [~]")
assert len(result) == length
else:
assert result == expected
@pytest.mark.parametrize("number,precision,expected", [
(1000, 0, "1,000"),
(1234.56, 2, "1,234.56"),
(0, 1, "0.0"),
(-500, 0, "-500"),
])
def test_format_number_parametrized(number: float | int, precision: int, expected: str):
"""Parametrized test for format_number"""
assert format_number(number, precision) == expected
@pytest.mark.parametrize("input_url,expected", [
("api/users", "/api/users"),
("/api/users", "/api/users"),
("api//users", "/api/users"),
("/api//users", "/api/users"),
("//api/users", "/api/users"),
("api///users////data", "/api/users/data"),
("/", "/"),
("///", "/"),
("", "/"),
("api/v1/users/123", "/api/v1/users/123"),
("/api/users?id=1&name=test", "/api/users?id=1&name=test"),
("api//users//123//profile", "/api/users/123/profile"),
])
def test_prepare_url_slash_parametrized(input_url: str, expected: str):
"""Parametrized test for prepare_url_slash"""
assert prepare_url_slash(input_url) == expected
# __END__