Skip to content

Commit d6ff9f6

Browse files
authored
Features/integration tests unit tests (#11)
* new integration test + bug fixing * added integration and unit test * first tests working * minor change * first working example, upgrade docs, upgrade docstrings * improvements * path bug fixed
1 parent 7649866 commit d6ff9f6

31 files changed

Lines changed: 1622 additions & 414 deletions

docs/dev_workflow_guide.md

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,58 +20,56 @@ Its job is to expose the REST API, run the discrete-event simulation, talk to th
2020

2121
```
2222
fastsim-backend/
23-
├── Dockerfile
24-
├── docker_fs/ # docker-compose for dev & prod
25-
│ ├── docker-compose.dev.yml
26-
│ └── docker-compose.prod.yml
27-
├── scripts/ # helper bash scripts (lint, dev-startup, …)
28-
│ ├── init-docker-dev.sh
23+
├── example/ # examples of working simulations
24+
│ ├── data
25+
├── scripts/ # helper bash scripts (lint, dev-startup, …)
2926
│ └── quality-check.sh
30-
├── alembic/ # DB migrations (versions/ contains revision files)
31-
│ ├── env.py
32-
│ └── versions/
33-
├── documentation/ # project vision & low-level docs
34-
│ └── backend_documentation/
35-
│ └── …
36-
├── tests/ # unit & integration tests
27+
├── docs/ # project vision & low-level docs
28+
│ └── fastsim-documentation/
29+
├── tests/ # unit & integration tests
3730
│ ├── unit/
3831
│ └── integration/
39-
├── src/ # **application code lives here**
32+
├── src/ # application code lives here
4033
│ └── app/
41-
│ ├── api/ # FastAPI routers & endpoint handlers
42-
│ ├── config/ # Pydantic Settings + constants
43-
│ ├── db/ # SQLAlchemy base, sessions, initial seed utilities
44-
│ ├── metrics/ # helpers to compute/aggregate simulation KPIs
45-
│ ├── resources/ # SimPy resource registry (CPU/RAM containers, etc.)
46-
│ ├── runtime/ # simulation core
47-
│ │ ├── rqs_state.py # RequestState & Hop
48-
│ │ └── actors/ # SimPy “actors”: Edge, Server, Client, RqsGenerator
49-
│ ├── samplers/ # stochastic samplers (Gaussian-Poisson, etc.)
50-
│ ├── schemas/ # Pydantic input/output models
51-
│ ├── main.py # FastAPI application factory / ASGI entry-point
52-
│ └── simulation_run.py # CLI utility to run a sim outside of HTTP layer
34+
│ ├── config/ # Pydantic Settings + constants
35+
│ ├── metrics/ # logic to compute/aggregate simulation KPIs
36+
│ ├── resources/ # SimPy resource registry (CPU/RAM containers, etc.)
37+
│ ├── runtime/ # simulation core
38+
│ │ ├── rqs_state.py # RequestState & Hop
39+
│ │ ├── simulation_runner.py # logic to initialize the whole simulation
40+
| └── actors/ # SimPy “actors”: Edge, Server, Client, RqsGenerator
41+
│ ├── samplers/ # stochastic samplers (Gaussian-Poisson, etc.)
42+
│ ├── schemas/ # Pydantic input/output models
5343
├── poetry.lock
5444
├── pyproject.toml
5545
└── README.md
5646
```
47+
### **What each top-level directory in `src/app` does**
48+
49+
| Directory | Purpose |
50+
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51+
| **`config/`** | Centralised configuration layer. Contains Pydantic `BaseSettings` classes for reading environment variables and constants/enums used across the simulation engine. |
52+
| **`metrics/`** | Post-processing and analytics. Aggregates raw simulation traces into KPIs such as latency percentiles, throughput, resource utilisation, and other performance metrics. |
53+
| **`resources/`** | Runtime resource registry for simulated hardware components (e.g., SimPy `Container`s for CPU and RAM). Decouples resource management from actor behaviour. |
54+
| **`runtime/`** | Core simulation engine. Orchestrates SimPy execution, maintains request state, and wires together simulation components. Includes: |
55+
| | - **`rqs_state.py`** — Defines `RequestState` and `Hop` for tracking request lifecycle. |
56+
| | - **`simulation_runner.py`** — Entry point for initialising and running simulations. |
57+
| | - **`actors/`** — SimPy actor classes representing system components (`RqsGenerator`, `Client`, `Server`, `Edge`) and their behaviour. |
58+
| **`samplers/`** | Random-variable samplers for stochastic simulation. Supports Poisson, Normal, and mixed distributions for modelling inter-arrival times and service steps. |
59+
| **`schemas/`** | Pydantic models for input/output validation and serialisation. Includes scenario definitions, topology graphs, simulation settings, and results payloads. |
5760

58-
#### What each top-level directory in `src/app` does
61+
---
5962

60-
| Directory | Purpose |
61-
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62-
| **`api/`** | Defines the public HTTP surface. Each module holds a router with path operations and dependency wiring. |
63-
| **`config/`** | Centralised configuration: `settings.py` (Pydantic `BaseSettings`) reads env vars; `constants.py` stores enums and global literals. |
64-
| **`db/`** | Persistence layer. Contains the SQLAlchemy base class, the session factory, and a thin wrapper that seeds or resets the database (Alembic migration scripts live at project root). |
65-
| **`metrics/`** | Post-processing helpers that turn raw simulation traces into aggregated KPIs (latency percentiles, cost per request, utilisation curves, …). |
66-
| **`resources/`** | A tiny run-time registry mapping every simulated server to its SimPy `Container`s (CPU, RAM). Keeps resource management separate from actor logic. |
67-
| **`runtime/`** | The heart of the simulator. `rqs_state.py` holds the mutable `RequestState`; sub-package **`actors/`** contains each SimPy process class (Generator, Edge, Server, Client). |
68-
| **`samplers/`** | Probability-distribution utilities that generate inter-arrival and service-time samples—used by the actors during simulation. |
69-
| **`schemas/`** | All Pydantic models for validation and (de)serialisation: request DTOs, topology definitions, simulation settings, outputs. |
70-
| **`main.py`** | Creates and returns the FastAPI app; imported by Uvicorn/Gunicorn. |
71-
| **`simulation_run.py`** | Convenience script to launch a simulation offline (e.g. inside tests or CLI). |
63+
### **Other Top-Level Directories**
7264

73-
Everything under `src/` is import-safe thanks to Poetry’s `packages = [{ include = "app" }]` entry in `pyproject.toml`.
65+
| Directory | Purpose |
66+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
67+
| **`example/`** | Ready-to-run simulation scenarios and example configurations. Includes `data/` with YAML definitions and scripts to demonstrate engine usage. |
68+
| **`scripts/`** | Utility shell scripts for development workflow, linting, formatting, and local startup (`quality-check.sh`, etc.). |
69+
| **`docs/`** | Project documentation. Contains both high-level vision documents and low-level technical references (`fastsim-documentation/`). |
70+
| **`tests/`** | Automated test suite, split into **unit** and **integration** tests to verify correctness of both individual components and end-to-end scenarios. |
7471

72+
---
7573

7674
## 3. Branching Strategy: Git Flow
7775

@@ -182,21 +180,11 @@ We will start to describe the CI part related to push and PR in the develop bran
182180

183181
* **Full Suite (push to `develop`)**
184182
*Runs in a few minutes; includes real services and Docker.*
185-
186-
* All steps from the Quick Suite
187-
* PostgreSQL service container started via `services:`
188-
* Alembic migrations applied to the test database
183+
189184
* Full test suite, including `@pytest.mark.integration` tests
190-
* Multi-stage Docker build of the backend image
191-
* Smoke test: container started with Uvicorn → `curl /health`
185+
192186

193187

194-
### 4.1.3 Key Implementation Details
195188

196-
* **Service containers** – PostgreSQL 17 is spun up in CI with a health-check to ensure migrations run against a live instance.
197-
* **Test markers** – integration tests are isolated with `@pytest.mark.integration`, enabling selective execution.
198-
* **Caching** – Poetry’s download cache is restored to cut installation time; Docker layer cache is reused between builds.
199-
* **Smoke test logic** – after the image is built, CI launches it in detached mode, polls the `/health` endpoint, prints logs, and stops the container. The job fails if the endpoint is unreachable.
200-
* **Secrets management** – database credentials and registry tokens are stored in GitHub Secrets and injected as environment variables only at runtime.
201189

202190

docs/fastsim-docs/requests_generator.md

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,151 @@ This document describes the design of the **requests generator**, which models a
88

99
Following the FastSim philosophy, we accept a small set of input parameters to drive a “what-if” analysis in a pre-production environment. These inputs let you explore reliability and cost implications under different traffic scenarios.
1010

11-
**Inputs**
11+
## **Inputs**
1212

13-
1. **Average concurrent users** – expected number of users (or sessions) simultaneously hitting the endpoint.
14-
2. **Average requests per minute per user** – average number of requests each user issues per minute.
15-
3. **Simulation time** – total duration of the simulation, in seconds.
13+
1. **Average Concurrent Users (`avg_active_users`)**
14+
Expected number of simultaneous active users (or sessions) interacting with the system.
1615

17-
**Output**
18-
A continuous sequence of timestamps (seconds) marking individual request arrivals.
16+
* Modeled as a random variable (`RVConfig`).
17+
* Allowed distributions: **Poisson** or **Normal**.
18+
19+
2. **Average Requests per Minute per User (`avg_request_per_minute_per_user`)**
20+
Average request rate per user, expressed in requests per minute.
21+
22+
* Modeled as a random variable (`RVConfig`).
23+
* **Must** use the **Poisson** distribution.
24+
25+
3. **User Sampling Window (`user_sampling_window`)**
26+
Time interval (in seconds) over which active users are resampled.
27+
28+
* Constrained between `MIN_USER_SAMPLING_WINDOW` and `MAX_USER_SAMPLING_WINDOW`.
29+
* Defaults to `USER_SAMPLING_WINDOW`.
1930

2031
---
2132

22-
## Model Assumptions
33+
## **Model Assumptions**
2334

24-
* *Concurrent users* and *requests per minute per user* are **random variables**.
25-
* *Simulation time* is **deterministic**.
35+
* **Random variables**:
2636

27-
We model:
37+
* *Concurrent users* and *requests per minute per user* are independent random variables.
38+
* Each is configured via the `RVConfig` model, which specifies:
2839

29-
* **Requests per minute per user** as Poisson($\lambda_r$).
30-
* **Concurrent users** as either Poisson($\lambda_u$) or truncated Normal.
31-
* **The variables are independent**
40+
* **mean** (mandatory, must be numeric and positive),
41+
* **distribution** (default: Poisson),
42+
* **variance** (optional; defaults to `mean` for Normal and Log-Normal distributions).
3243

33-
```python
34-
from pydantic import BaseModel
35-
from typing import Literal
44+
* **Supported joint sampling cases**:
45+
46+
* Poisson (users) × Poisson (requests)
47+
* Normal (users) × Poisson (requests)
48+
49+
Other combinations are currently unsupported.
50+
51+
* **Variance handling**:
52+
53+
* If the distribution is **Normal** or **Log-Normal** and `variance` is not provided, it is automatically set to the `mean`.
54+
55+
---
56+
57+
## **Validation Rules**
3658

59+
* `avg_request_per_minute_per_user`:
60+
61+
* **Must** be Poisson-distributed.
62+
* Validation enforces this constraint.
63+
64+
* `avg_active_users`:
65+
66+
* Must be either Poisson or Normal.
67+
* Validation enforces this constraint.
68+
69+
* `mean` in `RVConfig`:
70+
71+
* Must be a positive number (int or float).
72+
* Automatically coerced to `float`.
73+
74+
```python
3775
class RVConfig(BaseModel):
38-
"""Configure a random-variable parameter."""
76+
"""class to configure random variables"""
77+
3978
mean: float
40-
distribution: Literal["poisson", "normal", "gaussian"] = "poisson"
41-
variance: float | None = None # required only for normal/gaussian
79+
distribution: Distribution = Distribution.POISSON
80+
variance: float | None = None
81+
82+
@field_validator("mean", mode="before")
83+
def ensure_mean_is_numeric_and_positive(
84+
cls, # noqa: N805
85+
v: float,
86+
) -> float:
87+
"""Ensure `mean` is numeric, then coerce to float."""
88+
err_msg = "mean must be a number (int or float)"
89+
if not isinstance(v, (float, int)):
90+
raise ValueError(err_msg) # noqa: TRY004
91+
92+
return float(v)
93+
94+
@model_validator(mode="after") # type: ignore[arg-type]
95+
def default_variance(cls, model: "RVConfig") -> "RVConfig": # noqa: N805
96+
"""Set variance = mean when distribution require and variance is missing."""
97+
needs_variance: set[Distribution] = {
98+
Distribution.NORMAL,
99+
Distribution.LOG_NORMAL,
100+
}
101+
102+
if model.variance is None and model.distribution in needs_variance:
103+
model.variance = model.mean
104+
return model
105+
42106

43107
class RqsGeneratorInput(BaseModel):
44-
"""Define simulation inputs."""
108+
"""Define the expected variables for the simulation"""
109+
110+
id: str
111+
type: SystemNodes = SystemNodes.GENERATOR
45112
avg_active_users: RVConfig
46113
avg_request_per_minute_per_user: RVConfig
47-
total_simulation_time: int | None = None
114+
115+
user_sampling_window: int = Field(
116+
default=TimeDefaults.USER_SAMPLING_WINDOW,
117+
ge=TimeDefaults.MIN_USER_SAMPLING_WINDOW,
118+
le=TimeDefaults.MAX_USER_SAMPLING_WINDOW,
119+
description=(
120+
"Sampling window in seconds "
121+
f"({TimeDefaults.MIN_USER_SAMPLING_WINDOW}-"
122+
f"{TimeDefaults.MAX_USER_SAMPLING_WINDOW})."
123+
),
124+
)
125+
126+
@field_validator("avg_request_per_minute_per_user", mode="after")
127+
def ensure_avg_request_is_poisson(
128+
cls, # noqa: N805
129+
v: RVConfig,
130+
) -> RVConfig:
131+
"""
132+
Force the distribution for the rqs generator to be poisson
133+
at the moment we have a joint sampler just for the poisson-poisson
134+
and gaussian-poisson case
135+
"""
136+
if v.distribution != Distribution.POISSON:
137+
msg = "At the moment the variable avg request must be Poisson"
138+
raise ValueError(msg)
139+
return v
140+
141+
@field_validator("avg_active_users", mode="after")
142+
def ensure_avg_user_is_poisson_or_gaussian(
143+
cls, # noqa: N805
144+
v: RVConfig,
145+
) -> RVConfig:
146+
"""
147+
Force the distribution for the rqs generator to be poisson
148+
at the moment we have a joint sampler just for the poisson-poisson
149+
and gaussian-poisson case
150+
"""
151+
if v.distribution not in {Distribution.POISSON, Distribution.NORMAL}:
152+
msg = "At the moment the variable active user must be Poisson or Gaussian"
153+
raise ValueError(msg)
154+
return v
155+
48156
```
49157

50158
---

0 commit comments

Comments
 (0)