Skip to content

Commit b15dbff

Browse files
committed
improved pytest logic and code coherence
1 parent bc45dc0 commit b15dbff

11 files changed

Lines changed: 219 additions & 152 deletions

File tree

documentation/backend_documentation/input_structure_for_the_simulation.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This contract brings together three distinct but interconnected layers of config
66

77
1. **`rqs_input` (`RqsGeneratorInput`)**: Defines the **workload profile**—how many users are active and how frequently they generate requests.
88
2. **`topology_graph` (`TopologyGraph`)**: Describes the **system's architecture**—its components, resources, and the network connections between them.
9-
3. **`settings` (`SimulationSettings`)**: Configures **global simulation parameters**, such as total runtime and which metrics to collect.
9+
3. **`sim_settings` (`SimulationSettings`)**: Configures **global simulation parameters**, such as total runtime and which metrics to collect.
1010

1111
This layered design decouples the *what* (the system topology) from the *how* (the traffic pattern and simulation control), allowing for modular and reusable configurations. Adherence to our validation-first philosophy means every payload is rigorously parsed against this schema. By using a controlled vocabulary of `Enums` and the power of Pydantic, we guarantee that any malformed or logically inconsistent input is rejected upfront with clear, actionable errors, ensuring the simulation engine operates only on perfectly valid data.
1212

@@ -170,8 +170,20 @@ This final component configures the simulation's execution parameters and, criti
170170
class SimulationSettings(BaseModel):
171171
"""Global parameters that apply to the whole run."""
172172
total_simulation_time: int = Field(...)
173-
enabled_sample_metrics: set[SampledMetricName]
174-
enabled_event_metrics: set[EventMetricName]
173+
enabled_sample_metrics: set[SampledMetricName] = Field(
174+
default_factory=lambda: {
175+
SampledMetricName.READY_QUEUE_LEN,
176+
SampledMetricName.CORE_BUSY,
177+
SampledMetricName.RAM_IN_USE,
178+
},
179+
description="Which time‑series KPIs to collect by default.",
180+
)
181+
enabled_event_metrics: set[EventMetricName] = Field(
182+
default_factory=lambda: {
183+
EventMetricName.RQS_LATENCY,
184+
},
185+
description="Which per‑event KPIs to collect by default.",
186+
)
175187
```
176188

177189
| Field | Type | Purpose & Validation |
@@ -180,6 +192,7 @@ class SimulationSettings(BaseModel):
180192
| `enabled_sample_metrics` | `set[SampledMetricName]` | A set of metrics to be sampled at fixed intervals, creating a time-series (e.g., `"ready_queue_len"`, `"ram_in_use"`). |
181193
| `enabled_event_metrics` | `set[EventMetricName]` | A set of metrics recorded only when specific events occur, with no time-series (e.g., `"rqs_latency"`, `"llm_cost"`). |
182194

195+
We add standard default value for the metrics in case they will be omitted
183196
---
184197

185198
#### **Design Rationale: Pre-validated, On-Demand Metrics for Robust and Efficient Collection**

src/app/core/event_samplers/gaussian_poisson.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
def gaussian_poisson_sampling(
2424
input_data: RqsGeneratorInput,
25-
settings: SimulationSettings,
25+
sim_settings: SimulationSettings,
2626
*,
2727
rng: np.random.Generator | None = None,
2828
) -> Generator[float, None, None]:
@@ -41,7 +41,7 @@ def gaussian_poisson_sampling(
4141
"""
4242
rng = rng or np.random.default_rng()
4343

44-
simulation_time = settings.total_simulation_time
44+
simulation_time = sim_settings.total_simulation_time
4545
user_sampling_window = input_data.user_sampling_window
4646

4747
# λ_u : mean concurrent users per window

src/app/core/event_samplers/poisson_poisson.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
def poisson_poisson_sampling(
2121
input_data: RqsGeneratorInput,
22-
settings: SimulationSettings,
22+
sim_settings: SimulationSettings,
2323
*,
2424
rng: np.random.Generator | None = None,
2525
) -> Generator[float, None, None]:
@@ -38,7 +38,7 @@ def poisson_poisson_sampling(
3838
"""
3939
rng = rng or np.random.default_rng()
4040

41-
simulation_time = settings.total_simulation_time
41+
simulation_time = sim_settings.total_simulation_time
4242
user_sampling_window = input_data.user_sampling_window
4343

4444
# λ_u : mean concurrent users per window

src/app/core/simulation/requests_generator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
def requests_generator(
2424
input_data: RqsGeneratorInput,
25-
settings: SimulationSettings,
25+
sim_settings: SimulationSettings,
2626
*,
2727
rng: np.random.Generator | None = None,
2828
) -> Generator[float, None, None]:
@@ -43,14 +43,14 @@ def requests_generator(
4343
#Gaussian-Poisson model
4444
return gaussian_poisson_sampling(
4545
input_data=input_data,
46-
settings=settings,
46+
sim_settings=sim_settings,
4747
rng=rng,
4848

4949
)
5050

5151
# Poisson + Poisson
5252
return poisson_poisson_sampling(
5353
input_data=input_data,
54-
settings=settings,
54+
sim_settings=sim_settings,
5555
rng=rng,
5656
)

src/app/core/simulation/simulation_run.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,19 @@ def run_simulation(
2727
rng: np.random.Generator,
2828
) -> SimulationOutput:
2929
"""Simulation executor in Simpy"""
30-
settings = input_data.settings
31-
simulation_time = settings.total_simulation_time
32-
# pydantic in the validation assign a value and mypy is not
33-
# complaining because a None cannot be compared in the loop
34-
# to a float
35-
assert simulation_time is not None
30+
sim_settings = input_data.sim_settings
3631

3732
requests_generator_input = input_data.rqs_input
3833

3934
gaps: Generator[float, None, None] = requests_generator(
4035
requests_generator_input,
41-
settings,
36+
sim_settings,
4237
rng=rng)
4338
env = simpy.Environment()
4439

4540

4641
total_request_per_time_period = {
47-
"simulation_time": simulation_time,
42+
"simulation_time": sim_settings.total_simulation_time,
4843
"total_requests": 0,
4944
}
5045

@@ -56,7 +51,7 @@ def arrival_process(
5651
total_request_per_time_period["total_requests"] += 1
5752

5853
env.process(arrival_process(env))
59-
env.run(until=simulation_time)
54+
env.run(until=sim_settings.total_simulation_time)
6055

6156
return SimulationOutput(
6257
total_requests=total_request_per_time_period,

src/app/schemas/full_simulation_input.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ class SimulationPayload(BaseModel):
1212

1313
rqs_input: RqsGeneratorInput
1414
topology_graph: TopologyGraph
15-
settings: SimulationSettings
15+
sim_settings: SimulationSettings

src/app/schemas/simulation_settings_input.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pydantic import BaseModel, Field
44

5-
from app.config.constants import TimeDefaults
5+
from app.config.constants import EventMetricName, SampledMetricName, TimeDefaults
66

77

88
class SimulationSettings(BaseModel):
@@ -13,3 +13,19 @@ class SimulationSettings(BaseModel):
1313
ge=TimeDefaults.MIN_SIMULATION_TIME,
1414
description="Simulation horizon in seconds.",
1515
)
16+
17+
enabled_sample_metrics: set[SampledMetricName] = Field(
18+
default_factory=lambda: {
19+
SampledMetricName.READY_QUEUE_LEN,
20+
SampledMetricName.CORE_BUSY,
21+
SampledMetricName.RAM_IN_USE,
22+
},
23+
description="Which time-series KPIs to collect by default.",
24+
)
25+
enabled_event_metrics: set[EventMetricName] = Field(
26+
default_factory=lambda: {
27+
EventMetricName.RQS_LATENCY,
28+
},
29+
description="Which per-event KPIs to collect by default.",
30+
)
31+

tests/conftest.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from alembic.config import Config
1111
from dotenv import load_dotenv
1212
from fastapi.testclient import TestClient
13+
from numpy.random import Generator as NpGenerator
14+
from numpy.random import default_rng
1315
from sqlalchemy.ext.asyncio import (
1416
AsyncEngine,
1517
AsyncSession,
@@ -18,9 +20,23 @@
1820
)
1921
from sqlalchemy_utils import create_database, database_exists, drop_database
2022

23+
from app.config.constants import (
24+
EventMetricName,
25+
SampledMetricName,
26+
TimeDefaults,
27+
)
2128
from app.config.settings import settings
2229
from app.db.session import get_db
2330
from app.main import app
31+
from app.schemas.full_simulation_input import SimulationPayload
32+
from app.schemas.random_variables_config import RVConfig
33+
from app.schemas.requests_generator_input import RqsGeneratorInput
34+
from app.schemas.simulation_settings_input import SimulationSettings
35+
from app.schemas.system_topology_schema.full_system_topology_schema import (
36+
Client,
37+
TopologyGraph,
38+
TopologyNodes,
39+
)
2440

2541
# Load test environment variables from .env.test
2642
ENV_PATH = Path(__file__).resolve().parents[1] / "docker_fs" / ".env.test"
@@ -121,3 +137,102 @@ async def db_session(async_engine: AsyncEngine) -> AsyncGenerator[AsyncSession,
121137
await transaction.rollback()
122138
# Close the connection
123139
await connection.close()
140+
141+
# ============================================================================
142+
# STANDARD CONFIGURATION FOR INPUT VARIABLES
143+
# ============================================================================
144+
145+
# ---------------------------------------------------------------------------
146+
# RNG
147+
# ---------------------------------------------------------------------------
148+
149+
150+
@pytest.fixture(scope="session")
151+
def rng() -> NpGenerator:
152+
"""Deterministic NumPy RNG shared across tests (seed=0)."""
153+
return default_rng(0)
154+
155+
156+
# ---------------------------------------------------------------------------
157+
# Metrics sets
158+
# ---------------------------------------------------------------------------
159+
160+
161+
@pytest.fixture(scope="session")
162+
def enabled_sample_metrics() -> set[SampledMetricName]:
163+
"""Default sample-level KPIs tracked in most tests."""
164+
return {
165+
SampledMetricName.READY_QUEUE_LEN,
166+
SampledMetricName.RAM_IN_USE,
167+
}
168+
169+
170+
@pytest.fixture(scope="session")
171+
def enabled_event_metrics() -> set[EventMetricName]:
172+
"""Default event-level KPIs tracked in most tests."""
173+
return {EventMetricName.RQS_LATENCY}
174+
175+
176+
# ---------------------------------------------------------------------------
177+
# Global simulation settings
178+
# ---------------------------------------------------------------------------
179+
180+
181+
@pytest.fixture
182+
def sim_settings(
183+
enabled_sample_metrics: set[SampledMetricName],
184+
enabled_event_metrics: set[EventMetricName],
185+
) -> SimulationSettings:
186+
"""A minimal `SimulationSettings` instance for unit tests."""
187+
return SimulationSettings(
188+
total_simulation_time=TimeDefaults.MIN_SIMULATION_TIME,
189+
enabled_sample_metrics=enabled_sample_metrics,
190+
enabled_event_metrics=enabled_event_metrics,
191+
)
192+
193+
194+
# ---------------------------------------------------------------------------
195+
# Traffic profile
196+
# ---------------------------------------------------------------------------
197+
198+
199+
@pytest.fixture
200+
def rqs_input() -> RqsGeneratorInput:
201+
"""`RqsGeneratorInput` with 1 user and 2 req/min for quick tests."""
202+
return RqsGeneratorInput(
203+
avg_active_users=RVConfig(mean=1.0),
204+
avg_request_per_minute_per_user=RVConfig(mean=2.0),
205+
user_sampling_window=TimeDefaults.USER_SAMPLING_WINDOW,
206+
)
207+
208+
209+
# ---------------------------------------------------------------------------
210+
# Minimal topology (one client, no servers, no edges)
211+
# ---------------------------------------------------------------------------
212+
213+
214+
@pytest.fixture
215+
def topology_minimal() -> TopologyGraph:
216+
"""Valid topology with a single client and zero servers/edges."""
217+
client = Client(id="client-1")
218+
nodes = TopologyNodes(servers=[], client=client)
219+
return TopologyGraph(nodes=nodes, edges=[])
220+
221+
222+
# ---------------------------------------------------------------------------
223+
# Full simulation payload
224+
# ---------------------------------------------------------------------------
225+
226+
227+
@pytest.fixture
228+
def payload_base(
229+
rqs_input: RqsGeneratorInput,
230+
sim_settings: SimulationSettings,
231+
topology_minimal: TopologyGraph,
232+
) -> SimulationPayload:
233+
"""End-to-end payload used by high-level simulation tests."""
234+
return SimulationPayload(
235+
rqs_input=rqs_input,
236+
topology_graph=topology_minimal,
237+
sim_settings=sim_settings,
238+
)

0 commit comments

Comments
 (0)