Skip to content

Commit 435c4db

Browse files
Add unit tests for application and shutdown configurations
- Introduced comprehensive test suites for `ApplicationConfig` and `ShutdownConfig` to validate default values, custom configurations, serialization, and type validation. - Added tests for graceful shutdown handler functionality, including signal handling, timeout behavior, and error management. - Ensured coverage for various scenarios to enhance reliability and maintainability of the shutdown handling logic.
1 parent 40fd920 commit 435c4db

File tree

3 files changed

+712
-0
lines changed

3 files changed

+712
-0
lines changed

tests/test_application_config.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from py_spring_core.core.application.application_config import (
5+
ApplicationConfig,
6+
ShutdownConfig,
7+
ServerConfig,
8+
LoguruConfig,
9+
)
10+
11+
12+
class TestShutdownConfig:
13+
"""Test suite for the ShutdownConfig class."""
14+
15+
def test_shutdown_config_defaults(self):
16+
"""Test that ShutdownConfig has correct default values."""
17+
config = ShutdownConfig()
18+
19+
assert config.timeout_seconds == 30.0
20+
assert config.enabled is True
21+
22+
def test_shutdown_config_custom_values(self):
23+
"""Test ShutdownConfig with custom values."""
24+
config = ShutdownConfig(timeout_seconds=60.0, enabled=False)
25+
26+
assert config.timeout_seconds == 60.0
27+
assert config.enabled is False
28+
29+
def test_shutdown_config_validation(self):
30+
"""Test ShutdownConfig validation."""
31+
# Valid timeout values
32+
config = ShutdownConfig(timeout_seconds=0.1)
33+
assert config.timeout_seconds == 0.1
34+
35+
config = ShutdownConfig(timeout_seconds=1000.0)
36+
assert config.timeout_seconds == 1000.0
37+
38+
def test_shutdown_config_type_validation(self):
39+
"""Test that ShutdownConfig validates types correctly."""
40+
# Should accept float or int for timeout_seconds
41+
config = ShutdownConfig(timeout_seconds=30)
42+
assert config.timeout_seconds == 30.0
43+
44+
config = ShutdownConfig(timeout_seconds=30.5)
45+
assert config.timeout_seconds == 30.5
46+
47+
# Should accept boolean for enabled
48+
config = ShutdownConfig(enabled=True)
49+
assert config.enabled is True
50+
51+
config = ShutdownConfig(enabled=False)
52+
assert config.enabled is False
53+
54+
def test_shutdown_config_serialization(self):
55+
"""Test that ShutdownConfig can be serialized/deserialized."""
56+
config = ShutdownConfig(timeout_seconds=45.0, enabled=True)
57+
58+
# Test model dump
59+
config_dict = config.model_dump()
60+
expected = {"timeout_seconds": 45.0, "enabled": True}
61+
assert config_dict == expected
62+
63+
# Test reconstruction from dict
64+
new_config = ShutdownConfig(**config_dict)
65+
assert new_config.timeout_seconds == 45.0
66+
assert new_config.enabled is True
67+
68+
69+
class TestApplicationConfigWithShutdown:
70+
"""Test suite for ApplicationConfig with shutdown configuration."""
71+
72+
def test_application_config_with_default_shutdown(self):
73+
"""Test that ApplicationConfig includes default shutdown config."""
74+
config = ApplicationConfig(
75+
app_src_target_dir="./src",
76+
server_config=ServerConfig(host="localhost", port=8000),
77+
properties_file_path="./app.properties",
78+
loguru_config=LoguruConfig(log_file_path="./logs/app.log")
79+
)
80+
81+
# Should have default shutdown config
82+
assert config.shutdown_config is not None
83+
assert config.shutdown_config.timeout_seconds == 30.0
84+
assert config.shutdown_config.enabled is True
85+
86+
def test_application_config_with_custom_shutdown(self):
87+
"""Test ApplicationConfig with custom shutdown configuration."""
88+
custom_shutdown = ShutdownConfig(timeout_seconds=60.0, enabled=False)
89+
90+
config = ApplicationConfig(
91+
app_src_target_dir="./src",
92+
server_config=ServerConfig(host="localhost", port=8000),
93+
properties_file_path="./app.properties",
94+
loguru_config=LoguruConfig(log_file_path="./logs/app.log"),
95+
shutdown_config=custom_shutdown
96+
)
97+
98+
assert config.shutdown_config.timeout_seconds == 60.0
99+
assert config.shutdown_config.enabled is False
100+
101+
def test_application_config_serialization_with_shutdown(self):
102+
"""Test ApplicationConfig serialization includes shutdown config."""
103+
config = ApplicationConfig(
104+
app_src_target_dir="./src",
105+
server_config=ServerConfig(host="localhost", port=8000),
106+
properties_file_path="./app.properties",
107+
loguru_config=LoguruConfig(log_file_path="./logs/app.log"),
108+
shutdown_config=ShutdownConfig(timeout_seconds=45.0, enabled=True)
109+
)
110+
111+
config_dict = config.model_dump()
112+
113+
assert "shutdown_config" in config_dict
114+
assert config_dict["shutdown_config"]["timeout_seconds"] == 45.0
115+
assert config_dict["shutdown_config"]["enabled"] is True
116+
117+
def test_application_config_from_dict_with_shutdown(self):
118+
"""Test ApplicationConfig reconstruction from dict with shutdown config."""
119+
config_dict = {
120+
"app_src_target_dir": "./src",
121+
"server_config": {"host": "localhost", "port": 8000},
122+
"properties_file_path": "./app.properties",
123+
"loguru_config": {"log_file_path": "./logs/app.log", "log_level": "INFO"},
124+
"shutdown_config": {"timeout_seconds": 25.0, "enabled": False}
125+
}
126+
127+
config = ApplicationConfig(**config_dict)
128+
129+
assert config.app_src_target_dir == "./src"
130+
assert config.shutdown_config.timeout_seconds == 25.0
131+
assert config.shutdown_config.enabled is False
132+
133+
def test_application_config_without_shutdown_config_in_dict(self):
134+
"""Test that ApplicationConfig uses default shutdown when not provided."""
135+
config_dict = {
136+
"app_src_target_dir": "./src",
137+
"server_config": {"host": "localhost", "port": 8000},
138+
"properties_file_path": "./app.properties",
139+
"loguru_config": {"log_file_path": "./logs/app.log", "log_level": "INFO"}
140+
}
141+
142+
config = ApplicationConfig(**config_dict)
143+
144+
# Should use default shutdown config
145+
assert config.shutdown_config.timeout_seconds == 30.0
146+
assert config.shutdown_config.enabled is True

tests/test_framework_utils.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import pytest
2+
from abc import ABC, abstractmethod
3+
from typing import Type, Any
4+
5+
from py_spring_core.core.utils import get_unimplemented_abstract_methods
6+
7+
8+
class TestFrameworkUtils:
9+
"""Test suite for framework utility functions."""
10+
11+
def test_get_unimplemented_abstract_methods_with_concrete_class(self):
12+
"""Test that fully implemented classes return empty set."""
13+
14+
class AbstractBase(ABC):
15+
@abstractmethod
16+
def method_a(self) -> None:
17+
pass
18+
19+
@abstractmethod
20+
def method_b(self) -> str:
21+
pass
22+
23+
class ConcreteImpl(AbstractBase):
24+
def method_a(self) -> None:
25+
pass
26+
27+
def method_b(self) -> str:
28+
return "implemented"
29+
30+
unimplemented = get_unimplemented_abstract_methods(ConcreteImpl)
31+
assert unimplemented == set()
32+
33+
def test_get_unimplemented_abstract_methods_with_partial_implementation(self):
34+
"""Test that partially implemented classes return missing methods."""
35+
36+
class AbstractBase(ABC):
37+
@abstractmethod
38+
def method_a(self) -> None:
39+
pass
40+
41+
@abstractmethod
42+
def method_b(self) -> str:
43+
pass
44+
45+
@abstractmethod
46+
def method_c(self) -> int:
47+
pass
48+
49+
class PartialImpl(AbstractBase):
50+
def method_a(self) -> None:
51+
pass
52+
53+
# method_b and method_c are not implemented
54+
55+
unimplemented = get_unimplemented_abstract_methods(PartialImpl)
56+
assert unimplemented == {"method_b", "method_c"}
57+
58+
def test_get_unimplemented_abstract_methods_with_no_implementation(self):
59+
"""Test that classes with no implementations return all abstract methods."""
60+
61+
class AbstractBase(ABC):
62+
@abstractmethod
63+
def method_a(self) -> None:
64+
pass
65+
66+
@abstractmethod
67+
def method_b(self) -> str:
68+
pass
69+
70+
class NoImpl(AbstractBase):
71+
# No methods implemented
72+
pass
73+
74+
unimplemented = get_unimplemented_abstract_methods(NoImpl)
75+
assert unimplemented == {"method_a", "method_b"}
76+
77+
def test_get_unimplemented_abstract_methods_with_multiple_inheritance(self):
78+
"""Test with multiple inheritance from abstract classes."""
79+
80+
class AbstractA(ABC):
81+
@abstractmethod
82+
def method_a(self) -> None:
83+
pass
84+
85+
class AbstractB(ABC):
86+
@abstractmethod
87+
def method_b(self) -> str:
88+
pass
89+
90+
class MultipleInheritance(AbstractA, AbstractB):
91+
def method_a(self) -> None:
92+
pass
93+
# method_b is not implemented
94+
95+
unimplemented = get_unimplemented_abstract_methods(MultipleInheritance)
96+
assert unimplemented == {"method_b"}
97+
98+
def test_get_unimplemented_abstract_methods_with_inheritance_chain(self):
99+
"""Test with inheritance chain where parent implements some methods."""
100+
101+
class AbstractBase(ABC):
102+
@abstractmethod
103+
def method_a(self) -> None:
104+
pass
105+
106+
@abstractmethod
107+
def method_b(self) -> str:
108+
pass
109+
110+
@abstractmethod
111+
def method_c(self) -> int:
112+
pass
113+
114+
class PartialParent(AbstractBase):
115+
def method_a(self) -> None:
116+
pass
117+
# method_b and method_c still abstract
118+
119+
class ChildImpl(PartialParent):
120+
def method_b(self) -> str:
121+
return "implemented"
122+
# method_c still not implemented
123+
124+
unimplemented = get_unimplemented_abstract_methods(ChildImpl)
125+
assert unimplemented == {"method_c"}
126+
127+
def test_get_unimplemented_abstract_methods_with_no_abstract_methods(self):
128+
"""Test with class that has no abstract methods."""
129+
130+
class NonAbstractBase(ABC):
131+
def regular_method(self) -> None:
132+
pass
133+
134+
class RegularClass(NonAbstractBase):
135+
def another_method(self) -> str:
136+
return "normal"
137+
138+
unimplemented = get_unimplemented_abstract_methods(RegularClass)
139+
assert unimplemented == set()
140+
141+
def test_get_unimplemented_abstract_methods_type_error_non_class(self):
142+
"""Test that function raises TypeError for non-class types."""
143+
144+
with pytest.raises(TypeError, match="Expected a class type"):
145+
get_unimplemented_abstract_methods("not a class") # type: ignore
146+
147+
with pytest.raises(TypeError, match="Expected a class type"):
148+
get_unimplemented_abstract_methods(42) # type: ignore
149+
150+
def test_get_unimplemented_abstract_methods_type_error_non_abc(self):
151+
"""Test that function raises TypeError for non-ABC classes."""
152+
153+
class RegularClass:
154+
def some_method(self) -> None:
155+
pass
156+
157+
with pytest.raises(TypeError, match="Expected a subclass of abc.ABC"):
158+
get_unimplemented_abstract_methods(RegularClass)
159+
160+
def test_get_unimplemented_abstract_methods_with_property_abstracts(self):
161+
"""Test with abstract properties."""
162+
163+
class AbstractWithProperty(ABC):
164+
@property
165+
@abstractmethod
166+
def abstract_property(self) -> str:
167+
pass
168+
169+
@abstractmethod
170+
def abstract_method(self) -> None:
171+
pass
172+
173+
class PartialPropertyImpl(AbstractWithProperty):
174+
@property
175+
def abstract_property(self) -> str:
176+
return "implemented"
177+
# abstract_method not implemented
178+
179+
unimplemented = get_unimplemented_abstract_methods(PartialPropertyImpl)
180+
# Properties might be included in the abstract methods set, so we check that abstract_method is there
181+
# and abstract_property is not (since it's implemented)
182+
assert "abstract_method" in unimplemented
183+
assert len([method for method in unimplemented if "abstract_property" not in method]) >= 1
184+
185+
def test_get_unimplemented_abstract_methods_with_staticmethod_classmethod(self):
186+
"""Test with abstract static and class methods."""
187+
188+
class AbstractWithMethods(ABC):
189+
@staticmethod
190+
@abstractmethod
191+
def abstract_static() -> str:
192+
pass
193+
194+
@classmethod
195+
@abstractmethod
196+
def abstract_class(cls) -> str:
197+
pass
198+
199+
@abstractmethod
200+
def abstract_instance(self) -> None:
201+
pass
202+
203+
class PartialMethodImpl(AbstractWithMethods):
204+
@staticmethod
205+
def abstract_static() -> str:
206+
return "static implemented"
207+
208+
# abstract_class and abstract_instance not implemented
209+
210+
unimplemented = get_unimplemented_abstract_methods(PartialMethodImpl)
211+
assert unimplemented == {"abstract_class", "abstract_instance"}
212+
213+
def test_get_unimplemented_abstract_methods_real_world_example(self):
214+
"""Test with a real-world like example similar to GracefulShutdownHandler."""
215+
216+
from py_spring_core.core.interfaces.graceful_shutdown_handler import GracefulShutdownHandler
217+
218+
class IncompleteShutdownHandler(GracefulShutdownHandler):
219+
def on_shutdown(self, shutdown_type) -> None:
220+
pass
221+
# on_timeout and on_error not implemented
222+
223+
unimplemented = get_unimplemented_abstract_methods(IncompleteShutdownHandler)
224+
assert "on_timeout" in unimplemented
225+
assert "on_error" in unimplemented
226+
assert "on_shutdown" not in unimplemented

0 commit comments

Comments
 (0)