test(core): extend coverage — console, errors, log, service, time, benchmark
Add missing test modules for previously untested core areas: - console: ForegroundColorEnum, BackgroundColorEnum, Console methods - errors: dependency_error, module_dependency_error - log: LogLevel ordering/values, LogSettings, Logger (should_log, format, file write, fatal) - service: HostedService, StartupTask, CronjobABC (start/stop/loop/task cancellation) - time: TimeFormatSettings properties and setters - utils: Benchmark.time / .memory / .all call-count and output Also fix existing test files: environment cleanup, cron exception specificity, json_processor kwargs bug doc, configuration_model_abc to_dict bug doc. All 199 tests pass, black clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,7 @@ class EnvSettings(ConfigurationModelABC):
|
|||||||
|
|
||||||
# --- option() / defaults ---
|
# --- option() / defaults ---
|
||||||
|
|
||||||
|
|
||||||
def test_default_values():
|
def test_default_values():
|
||||||
s = DatabaseSettings()
|
s = DatabaseSettings()
|
||||||
assert s.host == "localhost"
|
assert s.host == "localhost"
|
||||||
@@ -79,6 +80,7 @@ def test_type_casting_bool():
|
|||||||
|
|
||||||
# --- required ---
|
# --- required ---
|
||||||
|
|
||||||
|
|
||||||
def test_required_field_present():
|
def test_required_field_present():
|
||||||
s = RequiredSettings({"api_key": "abc123"})
|
s = RequiredSettings({"api_key": "abc123"})
|
||||||
assert s.api_key == "abc123"
|
assert s.api_key == "abc123"
|
||||||
@@ -91,6 +93,7 @@ def test_required_field_missing_raises():
|
|||||||
|
|
||||||
# --- readonly ---
|
# --- readonly ---
|
||||||
|
|
||||||
|
|
||||||
def test_readonly_raises_on_setattr():
|
def test_readonly_raises_on_setattr():
|
||||||
s = DatabaseSettings()
|
s = DatabaseSettings()
|
||||||
with pytest.raises(AttributeError, match="read-only"):
|
with pytest.raises(AttributeError, match="read-only"):
|
||||||
@@ -105,6 +108,7 @@ def test_mutable_settings_can_be_set():
|
|||||||
|
|
||||||
# --- env override ---
|
# --- env override ---
|
||||||
|
|
||||||
|
|
||||||
def test_env_prefix_override(monkeypatch):
|
def test_env_prefix_override(monkeypatch):
|
||||||
monkeypatch.setenv("MYAPP_SECRET", "env_secret_value")
|
monkeypatch.setenv("MYAPP_SECRET", "env_secret_value")
|
||||||
s = EnvSettings()
|
s = EnvSettings()
|
||||||
@@ -123,8 +127,33 @@ def test_env_no_prefix_override(monkeypatch):
|
|||||||
assert s.host == "env_host"
|
assert s.host == "env_host"
|
||||||
|
|
||||||
|
|
||||||
|
def test_camel_case_key():
|
||||||
|
s = DatabaseSettings({"hostName": "camelhost"})
|
||||||
|
|
||||||
|
class CamelSettings(ConfigurationModelABC):
|
||||||
|
def __init__(self, src=None):
|
||||||
|
ConfigurationModelABC.__init__(self, src or {})
|
||||||
|
self.option("host_name", str, default=None)
|
||||||
|
|
||||||
|
obj = CamelSettings({"hostName": "camelhost"})
|
||||||
|
assert obj.host_name == "camelhost"
|
||||||
|
|
||||||
|
|
||||||
|
# --- to_dict ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_dict_returns_dict():
|
||||||
|
# to_dict() calls get() which calls get_value(src, field, _options[field].type, ...)
|
||||||
|
# _options stores the cast value directly (not a typed wrapper), so .type raises AttributeError.
|
||||||
|
# Bug: to_dict() is broken in the current implementation.
|
||||||
|
s = DatabaseSettings({"host": "myhost", "port": "1234"})
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
s.to_dict()
|
||||||
|
|
||||||
|
|
||||||
# --- cannot instantiate ABC ---
|
# --- cannot instantiate ABC ---
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_instantiate_abc_directly():
|
def test_cannot_instantiate_abc_directly():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
ConfigurationModelABC()
|
ConfigurationModelABC()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ def clear_config():
|
|||||||
|
|
||||||
# --- set / get ---
|
# --- set / get ---
|
||||||
|
|
||||||
|
|
||||||
def test_set_and_get_by_class():
|
def test_set_and_get_by_class():
|
||||||
settings = AppSettings({"app_name": "TestApp", "version": "1.0.0"})
|
settings = AppSettings({"app_name": "TestApp", "version": "1.0.0"})
|
||||||
Configuration.set(AppSettings, settings)
|
Configuration.set(AppSettings, settings)
|
||||||
@@ -73,6 +74,7 @@ def test_multiple_models():
|
|||||||
|
|
||||||
# --- add_json_file ---
|
# --- add_json_file ---
|
||||||
|
|
||||||
|
|
||||||
def test_add_json_file_loads_model(tmp_path):
|
def test_add_json_file_loads_model(tmp_path):
|
||||||
config_data = {"AppSettings": {"app_name": "FromFile", "version": "2.0.0"}}
|
config_data = {"AppSettings": {"app_name": "FromFile", "version": "2.0.0"}}
|
||||||
config_file = tmp_path / "appsettings.json"
|
config_file = tmp_path / "appsettings.json"
|
||||||
@@ -91,9 +93,7 @@ def test_add_json_file_not_found_exits(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
def test_add_json_file_optional_missing_returns_none(tmp_path):
|
def test_add_json_file_optional_missing_returns_none(tmp_path):
|
||||||
result = Configuration.add_json_file(
|
result = Configuration.add_json_file(str(tmp_path / "missing.json"), optional=True, output=False)
|
||||||
str(tmp_path / "missing.json"), optional=True, output=False
|
|
||||||
)
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
test/core/console/__init__.py
Normal file
0
test/core/console/__init__.py
Normal file
101
test/core/console/console_test.py
Normal file
101
test/core/console/console_test.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import pytest
|
||||||
|
from cpl.core.console.foreground_color_enum import ForegroundColorEnum
|
||||||
|
from cpl.core.console.background_color_enum import BackgroundColorEnum
|
||||||
|
from cpl.core.console.console import Console
|
||||||
|
|
||||||
|
# --- ForegroundColorEnum ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreground_color_enum_has_expected_values():
|
||||||
|
values = [e.value for e in ForegroundColorEnum]
|
||||||
|
assert "default" in values
|
||||||
|
assert "grey" in values
|
||||||
|
assert "red" in values
|
||||||
|
assert "green" in values
|
||||||
|
assert "yellow" in values
|
||||||
|
assert "blue" in values
|
||||||
|
assert "magenta" in values
|
||||||
|
assert "cyan" in values
|
||||||
|
assert "white" in values
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreground_color_enum_count():
|
||||||
|
assert len(ForegroundColorEnum) >= 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_foreground_color_enum_by_name():
|
||||||
|
assert ForegroundColorEnum["red"] == ForegroundColorEnum.red
|
||||||
|
assert ForegroundColorEnum["green"] == ForegroundColorEnum.green
|
||||||
|
|
||||||
|
|
||||||
|
# --- BackgroundColorEnum ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_color_enum_has_expected_values():
|
||||||
|
values = [e.value for e in BackgroundColorEnum]
|
||||||
|
assert "on_default" in values
|
||||||
|
assert "on_red" in values
|
||||||
|
assert "on_green" in values
|
||||||
|
assert "on_blue" in values
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_color_enum_count():
|
||||||
|
assert len(BackgroundColorEnum) >= 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_background_color_enum_by_name():
|
||||||
|
assert BackgroundColorEnum["red"] == BackgroundColorEnum.red
|
||||||
|
|
||||||
|
|
||||||
|
# --- Console basic methods (non-interactive, no terminal required) ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_write_does_not_raise(capsys):
|
||||||
|
Console.write("test")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "test" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_write_line_does_not_raise(capsys):
|
||||||
|
Console.write_line("hello world")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "hello world" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_write_line_multiple_args(capsys):
|
||||||
|
Console.write_line("a", "b", "c")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "a" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_error_does_not_raise(capsys):
|
||||||
|
Console.error("something went wrong")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "something went wrong" in captured.out or "something went wrong" in captured.err
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_set_foreground_color_does_not_raise():
|
||||||
|
Console.set_foreground_color(ForegroundColorEnum.red)
|
||||||
|
Console.set_foreground_color(ForegroundColorEnum.default)
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_set_background_color_does_not_raise():
|
||||||
|
Console.set_background_color(BackgroundColorEnum.blue)
|
||||||
|
Console.set_background_color(BackgroundColorEnum.default)
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_color_reset_does_not_raise():
|
||||||
|
Console.color_reset()
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_banner_does_not_raise(capsys):
|
||||||
|
# banner() renders text as ASCII art, not verbatim
|
||||||
|
Console.banner("Test Banner")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert len(captured.out) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_console_divider_does_not_raise(capsys):
|
||||||
|
Console.divider()
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert len(captured.out) > 0
|
||||||
@@ -4,6 +4,18 @@ from cpl.core.environment.environment import Environment
|
|||||||
from cpl.core.environment.environment_enum import EnvironmentEnum
|
from cpl.core.environment.environment_enum import EnvironmentEnum
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def restore_env():
|
||||||
|
"""Restore touched env vars after each test to prevent pollution."""
|
||||||
|
original = dict(os.environ)
|
||||||
|
yield
|
||||||
|
for key in list(os.environ.keys()):
|
||||||
|
if key not in original:
|
||||||
|
del os.environ[key]
|
||||||
|
elif os.environ[key] != original[key]:
|
||||||
|
os.environ[key] = original[key]
|
||||||
|
|
||||||
|
|
||||||
def test_set_and_get_environment_valid():
|
def test_set_and_get_environment_valid():
|
||||||
for env in EnvironmentEnum:
|
for env in EnvironmentEnum:
|
||||||
Environment.set_environment(env.value)
|
Environment.set_environment(env.value)
|
||||||
|
|||||||
38
test/core/errors_test.py
Normal file
38
test/core/errors_test.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
from cpl.core.errors import dependency_error, module_dependency_error
|
||||||
|
|
||||||
|
|
||||||
|
def test_dependency_error_exits(capsys):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
dependency_error("my.feature", "some-package")
|
||||||
|
|
||||||
|
|
||||||
|
def test_dependency_error_prints_message(capsys):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
dependency_error("my.feature", "some-package")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "some-package" in captured.out
|
||||||
|
assert "my.feature" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_dependency_error_with_import_error(capsys):
|
||||||
|
try:
|
||||||
|
import nonexistent_package
|
||||||
|
except ImportError as e:
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
dependency_error("my.feature", "nonexistent_package", e)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "nonexistent_package" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_dependency_error_exits(capsys):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module_dependency_error("MyModule", "SomeDependency")
|
||||||
|
|
||||||
|
|
||||||
|
def test_module_dependency_error_prints_message(capsys):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module_dependency_error("MyModule", "SomeDependency")
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "SomeDependency" in captured.out
|
||||||
|
assert "MyModule" in captured.out
|
||||||
0
test/core/log/__init__.py
Normal file
0
test/core/log/__init__.py
Normal file
152
test/core/log/log_test.py
Normal file
152
test/core/log/log_test.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from cpl.core.log.log_level import LogLevel
|
||||||
|
from cpl.core.log.log_settings import LogSettings
|
||||||
|
from cpl.core.log.logger import Logger
|
||||||
|
from cpl.core.log.logger_abc import LoggerABC
|
||||||
|
|
||||||
|
# --- LogLevel ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_level_values():
|
||||||
|
assert LogLevel.off.value == "OFF"
|
||||||
|
assert LogLevel.trace.value == "TRC"
|
||||||
|
assert LogLevel.debug.value == "DEB"
|
||||||
|
assert LogLevel.info.value == "INF"
|
||||||
|
assert LogLevel.warning.value == "WAR"
|
||||||
|
assert LogLevel.error.value == "ERR"
|
||||||
|
assert LogLevel.fatal.value == "FAT"
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_level_order():
|
||||||
|
levels = list(LogLevel)
|
||||||
|
assert levels.index(LogLevel.trace) < levels.index(LogLevel.debug)
|
||||||
|
assert levels.index(LogLevel.debug) < levels.index(LogLevel.info)
|
||||||
|
assert levels.index(LogLevel.info) < levels.index(LogLevel.warning)
|
||||||
|
assert levels.index(LogLevel.warning) < levels.index(LogLevel.error)
|
||||||
|
assert levels.index(LogLevel.error) < levels.index(LogLevel.fatal)
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_level_by_name():
|
||||||
|
assert LogLevel["info"] == LogLevel.info
|
||||||
|
assert LogLevel["error"] == LogLevel.error
|
||||||
|
|
||||||
|
|
||||||
|
# --- LogSettings ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_settings_defaults():
|
||||||
|
s = LogSettings()
|
||||||
|
assert s.path == "logs"
|
||||||
|
assert s.filename == "app.log"
|
||||||
|
assert s.console == LogLevel.info
|
||||||
|
assert s.level == LogLevel.info
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_settings_from_src():
|
||||||
|
s = LogSettings({"path": "custom_logs", "filename": "myapp.log"})
|
||||||
|
assert s.path == "custom_logs"
|
||||||
|
assert s.filename == "myapp.log"
|
||||||
|
|
||||||
|
|
||||||
|
# --- LoggerABC ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_abc_cannot_instantiate():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
LoggerABC()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Logger ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_creates_instance(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
assert logger is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_log_file_property(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
assert logger.log_file.startswith("logs/")
|
||||||
|
assert logger.log_file.endswith(".log")
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_should_log_same_level(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
assert logger._should_log(LogLevel.info, LogLevel.info) is True
|
||||||
|
assert logger._should_log(LogLevel.error, LogLevel.error) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_should_log_higher_level(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
assert logger._should_log(LogLevel.error, LogLevel.info) is True
|
||||||
|
assert logger._should_log(LogLevel.fatal, LogLevel.debug) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_should_not_log_lower_level(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
assert logger._should_log(LogLevel.debug, LogLevel.info) is False
|
||||||
|
assert logger._should_log(LogLevel.trace, LogLevel.warning) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_file_format_message(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger("test_src")
|
||||||
|
msg = logger._file_format_message("INF", "2024-01-01 00:00:00.000000", "hello", "world")
|
||||||
|
assert "INF" in msg
|
||||||
|
assert "hello" in msg
|
||||||
|
assert "world" in msg
|
||||||
|
assert "test_src" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_console_format_message(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger("test_src")
|
||||||
|
msg = logger._console_format_message("DEB", "2024-01-01 00:00:00.000000", "debug message")
|
||||||
|
assert "DEB" in msg
|
||||||
|
assert "debug message" in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_info_writes_file(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
logger.info("test info message")
|
||||||
|
log_files = list(tmp_path.glob("logs/*.log"))
|
||||||
|
assert len(log_files) > 0
|
||||||
|
content = log_files[0].read_text()
|
||||||
|
assert "test info message" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_debug_below_info_not_written_to_file(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
# Default level is INFO, so DEBUG should not appear in the file
|
||||||
|
logger = Logger(__name__)
|
||||||
|
logger.debug("should not appear in file")
|
||||||
|
log_files = list(tmp_path.glob("logs/*.log"))
|
||||||
|
if log_files:
|
||||||
|
content = log_files[0].read_text()
|
||||||
|
assert "should not appear in file" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_set_level_invalid(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Logger.set_level("INVALID_LEVEL")
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_fatal_exits(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
logger.fatal("fatal error")
|
||||||
|
|
||||||
|
|
||||||
|
def test_logger_fatal_prevent_quit(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
logger = Logger(__name__)
|
||||||
|
logger.fatal("fatal no quit", prevent_quit=True) # Should not raise
|
||||||
0
test/core/service/__init__.py
Normal file
0
test/core/service/__init__.py
Normal file
118
test/core/service/service_test.py
Normal file
118
test/core/service/service_test.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
from cpl.core.service.hosted_service import HostedService
|
||||||
|
from cpl.core.service.startup_task import StartupTask
|
||||||
|
from cpl.core.service.cronjob import CronjobABC
|
||||||
|
from cpl.core.time.cron import Cron
|
||||||
|
|
||||||
|
# --- HostedService ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_hosted_service_cannot_instantiate():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
HostedService()
|
||||||
|
|
||||||
|
|
||||||
|
def test_hosted_service_concrete_can_start_stop():
|
||||||
|
class MyService(HostedService):
|
||||||
|
def __init__(self):
|
||||||
|
self.started = False
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
svc = MyService()
|
||||||
|
await svc.start()
|
||||||
|
assert svc.started is True
|
||||||
|
await svc.stop()
|
||||||
|
assert svc.stopped is True
|
||||||
|
|
||||||
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
# --- StartupTask ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_task_cannot_instantiate():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
StartupTask()
|
||||||
|
|
||||||
|
|
||||||
|
def test_startup_task_concrete_runs():
|
||||||
|
class MyTask(StartupTask):
|
||||||
|
def __init__(self):
|
||||||
|
self.ran = False
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
self.ran = True
|
||||||
|
|
||||||
|
async def execute():
|
||||||
|
task = MyTask()
|
||||||
|
await task.run()
|
||||||
|
assert task.ran is True
|
||||||
|
|
||||||
|
asyncio.run(execute())
|
||||||
|
|
||||||
|
|
||||||
|
# --- CronjobABC ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_cronjob_cannot_instantiate():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
CronjobABC(Cron("* * * * *"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cronjob_loop_is_called():
|
||||||
|
loop_calls = []
|
||||||
|
|
||||||
|
class MyCronJob(CronjobABC):
|
||||||
|
def __init__(self):
|
||||||
|
CronjobABC.__init__(self, Cron("* * * * *"))
|
||||||
|
|
||||||
|
async def loop(self):
|
||||||
|
loop_calls.append(1)
|
||||||
|
|
||||||
|
job = MyCronJob()
|
||||||
|
assert job._running is False
|
||||||
|
await job.start()
|
||||||
|
assert job._running is True
|
||||||
|
# Give it a moment then stop
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
await job.stop()
|
||||||
|
assert job._running is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cronjob_stop_cancels_task():
|
||||||
|
class MyCronJob(CronjobABC):
|
||||||
|
def __init__(self):
|
||||||
|
CronjobABC.__init__(self, Cron("* * * * *"))
|
||||||
|
|
||||||
|
async def loop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
job = MyCronJob()
|
||||||
|
await job.start()
|
||||||
|
assert job._task is not None
|
||||||
|
await job.stop()
|
||||||
|
assert job._task.cancelled() or job._task.done()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cronjob_stop_without_start():
|
||||||
|
class MyCronJob(CronjobABC):
|
||||||
|
def __init__(self):
|
||||||
|
CronjobABC.__init__(self, Cron("* * * * *"))
|
||||||
|
|
||||||
|
async def loop(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
job = MyCronJob()
|
||||||
|
# stop without start should not raise
|
||||||
|
await job.stop()
|
||||||
@@ -56,5 +56,5 @@ def test_custom_start_time():
|
|||||||
|
|
||||||
|
|
||||||
def test_invalid_cron_expression():
|
def test_invalid_cron_expression():
|
||||||
with pytest.raises(Exception):
|
with pytest.raises((ValueError, KeyError)):
|
||||||
Cron("invalid expression")
|
Cron("invalid expression")
|
||||||
|
|||||||
44
test/core/time/time_format_settings_test.py
Normal file
44
test/core/time/time_format_settings_test.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import pytest
|
||||||
|
from cpl.core.time.time_format_settings import TimeFormatSettings
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults_are_none():
|
||||||
|
s = TimeFormatSettings()
|
||||||
|
assert s.date_format is None
|
||||||
|
assert s.time_format is None
|
||||||
|
assert s.date_time_format is None
|
||||||
|
assert s.date_time_log_format is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_constructor_sets_all_fields():
|
||||||
|
s = TimeFormatSettings(
|
||||||
|
date_format="%Y-%m-%d",
|
||||||
|
time_format="%H:%M:%S",
|
||||||
|
date_time_format="%Y-%m-%d %H:%M:%S",
|
||||||
|
date_time_log_format="%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
)
|
||||||
|
assert s.date_format == "%Y-%m-%d"
|
||||||
|
assert s.time_format == "%H:%M:%S"
|
||||||
|
assert s.date_time_format == "%Y-%m-%d %H:%M:%S"
|
||||||
|
assert s.date_time_log_format == "%Y-%m-%d %H:%M:%S.%f"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setters_update_values():
|
||||||
|
s = TimeFormatSettings()
|
||||||
|
s.date_format = "%d/%m/%Y"
|
||||||
|
s.time_format = "%I:%M %p"
|
||||||
|
s.date_time_format = "%d/%m/%Y %I:%M %p"
|
||||||
|
s.date_time_log_format = "%d/%m/%Y %H:%M:%S.%f"
|
||||||
|
assert s.date_format == "%d/%m/%Y"
|
||||||
|
assert s.time_format == "%I:%M %p"
|
||||||
|
assert s.date_time_format == "%d/%m/%Y %I:%M %p"
|
||||||
|
assert s.date_time_log_format == "%d/%m/%Y %H:%M:%S.%f"
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial_construction():
|
||||||
|
# TimeFormatSettings uses constructor args, not option()/from_src()
|
||||||
|
s = TimeFormatSettings(date_format="%Y/%m/%d")
|
||||||
|
assert s.date_format == "%Y/%m/%d"
|
||||||
|
assert s.time_format is None
|
||||||
|
assert s.date_time_format is None
|
||||||
|
assert s.date_time_log_format is None
|
||||||
60
test/core/utils/benchmark_test.py
Normal file
60
test/core/utils/benchmark_test.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import pytest
|
||||||
|
from cpl.core.utils.benchmark import Benchmark
|
||||||
|
|
||||||
|
|
||||||
|
def noop():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_time_does_not_raise(capsys):
|
||||||
|
Benchmark.time("noop", noop, iterations=3)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "noop" in captured.out
|
||||||
|
assert "min" in captured.out
|
||||||
|
assert "avg" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_memory_does_not_raise(capsys):
|
||||||
|
Benchmark.memory("noop_mem", noop, iterations=3)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "noop_mem" in captured.out
|
||||||
|
assert "mem" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_all_does_not_raise(capsys):
|
||||||
|
Benchmark.all("noop_all", noop, iterations=3)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "noop_all" in captured.out
|
||||||
|
assert "min" in captured.out
|
||||||
|
assert "mem" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_time_calls_func():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def tracked():
|
||||||
|
calls.append(1)
|
||||||
|
|
||||||
|
Benchmark.time("tracked", tracked, iterations=4)
|
||||||
|
assert len(calls) == 4
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_memory_calls_func():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def tracked():
|
||||||
|
calls.append(1)
|
||||||
|
|
||||||
|
Benchmark.memory("tracked_mem", tracked, iterations=3)
|
||||||
|
assert len(calls) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_benchmark_all_calls_func_twice_per_iteration():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def tracked():
|
||||||
|
calls.append(1)
|
||||||
|
|
||||||
|
Benchmark.all("tracked_all", tracked, iterations=2)
|
||||||
|
# all() runs func once per iteration for time, once per iteration for memory = 2*iterations
|
||||||
|
assert len(calls) == 4
|
||||||
@@ -66,3 +66,28 @@ def test_nested_object():
|
|||||||
def test_enum_value():
|
def test_enum_value():
|
||||||
obj = JSONProcessor.process(WithEnum, {"Color": "RED"})
|
obj = JSONProcessor.process(WithEnum, {"Color": "RED"})
|
||||||
assert obj.color == Color.RED
|
assert obj.color == Color.RED
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_lower_key():
|
||||||
|
obj = JSONProcessor.process(Named, {"name": "Charlie", "value": 7})
|
||||||
|
assert obj.name == "Charlie"
|
||||||
|
assert obj.value == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_upper_case_key():
|
||||||
|
obj = JSONProcessor.process(Named, {"NAME": "Dave"})
|
||||||
|
assert obj.name == "Dave"
|
||||||
|
|
||||||
|
|
||||||
|
class WithKwargs:
|
||||||
|
def __init__(self, title: str, kwargs: dict = None):
|
||||||
|
self.title = title
|
||||||
|
self.kwargs = kwargs or {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_kwargs_collected():
|
||||||
|
# Bug: JSONProcessor collects leftover keys into a dict for a `kwargs: dict` param,
|
||||||
|
# but then passes them as **kwargs to the constructor instead of as a positional dict arg.
|
||||||
|
# This causes TypeError when the constructor does not accept **kwargs.
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
JSONProcessor.process(WithKwargs, {"Title": "Hello", "extra1": "a", "extra2": "b"})
|
||||||
|
|||||||
Reference in New Issue
Block a user