356 lines
9.6 KiB
Markdown
356 lines
9.6 KiB
Markdown
# Entropyk - Story 1.3: Port and Connection System
|
|
|
|
## 🎯 Ce qui a été implémenté
|
|
|
|
Cette story implémente le système de ports et connexions pour les composants thermodynamiques avec le **Type-State pattern** pour la sécurité à la compilation.
|
|
|
|
### Fonctionnalités principales
|
|
|
|
- ✅ `Port<State>` - Structure générique avec états `Disconnected` et `Connected`
|
|
- ✅ `FluidId` - Identification des fluides pour validation de compatibilité
|
|
- ✅ `ConnectionError` - Gestion d'erreurs avec `thiserror`
|
|
- ✅ Validation des connexions (compatibilité fluide, continuité pression/enthalpie)
|
|
- ✅ Extension du trait `Component` avec `get_ports()`
|
|
|
|
## 🚀 Instructions de test
|
|
|
|
### 1. Prérequis
|
|
|
|
```bash
|
|
# Vérifier que Rust est installé
|
|
rustc --version
|
|
cargo --version
|
|
```
|
|
|
|
### 2. Cloner/Naviguer dans le projet
|
|
|
|
```bash
|
|
cd /Users/sepehr/dev/Entropyk
|
|
```
|
|
|
|
### 3. Compiler le projet
|
|
|
|
```bash
|
|
# Compiler tout le workspace
|
|
cargo build --workspace
|
|
|
|
# Compiler en mode release (optimisé)
|
|
cargo build --workspace --release
|
|
```
|
|
|
|
### 4. Exécuter tous les tests
|
|
|
|
```bash
|
|
# Tests complets du workspace
|
|
cargo test --workspace
|
|
|
|
# Tests avec sortie détaillée
|
|
cargo test --workspace -- --nocapture
|
|
|
|
# Tests du crate components uniquement
|
|
cargo test -p entropyk-components
|
|
|
|
# Tests du crate core uniquement
|
|
cargo test -p entropyk-core
|
|
```
|
|
|
|
### 5. Vérifier la qualité du code
|
|
|
|
```bash
|
|
# Clippy (linting strict)
|
|
cargo clippy --workspace -- -D warnings
|
|
|
|
# Formatage du code
|
|
cargo fmt --workspace
|
|
|
|
# Vérifier la documentation
|
|
cargo doc --workspace --open
|
|
```
|
|
|
|
## 🧪 Tests manuels
|
|
|
|
### Test 1: Création et connexion de ports
|
|
|
|
Créez un fichier `test_ports.rs` à la racine du projet :
|
|
|
|
```rust
|
|
use entropyk_components::port::{Port, Disconnected, Connected, FluidId, ConnectionError};
|
|
use entropyk_core::{Pressure, Enthalpy};
|
|
|
|
fn main() -> Result<(), ConnectionError> {
|
|
println!("=== Test de création de ports ===");
|
|
|
|
// Créer deux ports déconnectés
|
|
let port1 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(1.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
let port2 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(1.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
println!("Port 1 créé: fluid={:?}, pressure={:.2} Pa, enthalpy={:.2} J/kg",
|
|
port1.fluid_id(),
|
|
port1.pressure().to_pascals(),
|
|
port1.enthalpy().to_joules_per_kg()
|
|
);
|
|
|
|
println!("Port 2 créé: fluid={:?}, pressure={:.2} Pa, enthalpy={:.2} J/kg",
|
|
port2.fluid_id(),
|
|
port2.pressure().to_pascals(),
|
|
port2.enthalpy().to_joules_per_kg()
|
|
);
|
|
|
|
// Connecter les ports
|
|
println!("\n=== Connexion des ports ===");
|
|
let (mut connected1, mut connected2) = port1.connect(port2)?;
|
|
|
|
println!("✅ Ports connectés avec succès!");
|
|
println!("Connected 1: pressure={:.2} Pa, enthalpy={:.2} J/kg",
|
|
connected1.pressure().to_pascals(),
|
|
connected1.enthalpy().to_joules_per_kg()
|
|
);
|
|
|
|
// Modifier les valeurs
|
|
println!("\n=== Modification des valeurs ===");
|
|
connected1.set_pressure(Pressure::from_bar(1.5));
|
|
connected1.set_enthalpy(Enthalpy::from_joules_per_kg(450_000.0));
|
|
|
|
println!("Port 1 modifié: pressure={:.2} Pa, enthalpy={:.2} J/kg",
|
|
connected1.pressure().to_pascals(),
|
|
connected1.enthalpy().to_joules_per_kg()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Pour l'exécuter, ajoutez ce binary dans `Cargo.toml`:
|
|
|
|
```toml
|
|
[[bin]]
|
|
name = "test_ports"
|
|
path = "test_ports.rs"
|
|
```
|
|
|
|
Puis:
|
|
```bash
|
|
cargo run --bin test_ports
|
|
```
|
|
|
|
### Test 2: Test d'erreurs
|
|
|
|
```rust
|
|
use entropyk_components::port::{Port, FluidId, ConnectionError};
|
|
use entropyk_core::{Pressure, Enthalpy};
|
|
|
|
fn main() {
|
|
println!("=== Test des erreurs de connexion ===\n");
|
|
|
|
// Test 1: Fluides incompatibles
|
|
println!("Test 1: Fluides incompatibles");
|
|
let port1 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_pascals(100_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
let port2 = Port::new(
|
|
FluidId::new("Water"),
|
|
Pressure::from_pascals(100_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
match port1.connect(port2) {
|
|
Err(ConnectionError::IncompatibleFluid { from, to }) => {
|
|
println!("✅ Erreur capturée: Cannot connect {} to {}", from, to);
|
|
}
|
|
_ => println!("❌ Erreur non capturée!"),
|
|
}
|
|
|
|
// Test 2: Pression différente
|
|
println!("\nTest 2: Pression différente");
|
|
let port3 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_pascals(100_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
let port4 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_pascals(200_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
match port3.connect(port4) {
|
|
Err(ConnectionError::PressureMismatch { from_pressure, to_pressure }) => {
|
|
println!("✅ Erreur capturée: Pressure mismatch {} vs {}",
|
|
from_pressure, to_pressure);
|
|
}
|
|
_ => println!("❌ Erreur non capturée!"),
|
|
}
|
|
|
|
// Test 3: Connexion réussie
|
|
println!("\nTest 3: Connexion réussie");
|
|
let port5 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_pascals(100_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
let port6 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_pascals(100_000.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
match port5.connect(port6) {
|
|
Ok(_) => println!("✅ Connexion réussie!"),
|
|
Err(_) => println!("❌ Connexion échouée!"),
|
|
}
|
|
}
|
|
```
|
|
|
|
### Test 3: Vérification Type-State (doit échouer à la compilation)
|
|
|
|
Créez ce fichier pour vérifier que la sécurité à la compilation fonctionne:
|
|
|
|
```rust
|
|
use entropyk_components::port::{Port, Disconnected};
|
|
use entropyk_core::{Pressure, Enthalpy, FluidId};
|
|
|
|
fn main() {
|
|
// Créer un port déconnecté
|
|
let port: Port<Disconnected> = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(1.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0)
|
|
);
|
|
|
|
// Cette ligne doit provoquer une erreur de compilation:
|
|
// error[E0599]: no method named `pressure` found for struct `Port<Disconnected>`
|
|
// let _p = port.pressure();
|
|
|
|
println!("Si vous décommentez la ligne ci-dessus, la compilation échouera!");
|
|
println!("Cela prouve que le Type-State pattern fonctionne.");
|
|
}
|
|
```
|
|
|
|
## 📊 Structure des fichiers
|
|
|
|
```
|
|
entropyk/
|
|
├── Cargo.toml # Workspace root
|
|
├── crates/
|
|
│ ├── components/ # Crate components (modifié)
|
|
│ │ ├── Cargo.toml # + Dépendances ajoutées
|
|
│ │ └── src/
|
|
│ │ ├── lib.rs # + Trait Component étendu
|
|
│ │ └── port.rs # NOUVEAU: Implémentation ports
|
|
│ └── core/ # Crate core (existant)
|
|
│ └── src/
|
|
│ └── types.rs # Pressure, Enthalpy, etc.
|
|
└── _bmad-output/
|
|
└── implementation-artifacts/
|
|
└── 1-3-port-and-connection-system.md # Story document
|
|
```
|
|
|
|
## 🔍 Points clés de l'implémentation
|
|
|
|
### Type-State Pattern
|
|
|
|
```rust
|
|
// Disconnected et Connected sont des marqueurs de type vides
|
|
pub struct Disconnected;
|
|
pub struct Connected;
|
|
|
|
// Port est générique sur l'état
|
|
pub struct Port<State> {
|
|
fluid_id: FluidId,
|
|
pressure: Pressure,
|
|
enthalpy: Enthalpy,
|
|
_state: PhantomData<State>, // Marqueur zéro-cost
|
|
}
|
|
|
|
// Seuls les ports Disconnected peuvent être connectés
|
|
impl Port<Disconnected> {
|
|
pub fn connect(self, other: Port<Disconnected>)
|
|
-> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
|
|
// Validation et connexion...
|
|
}
|
|
}
|
|
|
|
// Seuls les ports Connected exposent les méthodes de lecture/écriture
|
|
impl Port<Connected> {
|
|
pub fn pressure(&self) -> Pressure { self.pressure }
|
|
pub fn set_pressure(&mut self, pressure: Pressure) { self.pressure = pressure }
|
|
}
|
|
```
|
|
|
|
### Validation des connexions
|
|
|
|
```rust
|
|
pub fn connect(self, other: Port<Disconnected>)
|
|
-> Result<(Port<Connected>, Port<Connected>), ConnectionError>
|
|
{
|
|
// 1. Validation du fluide
|
|
if self.fluid_id != other.fluid_id {
|
|
return Err(ConnectionError::IncompatibleFluid { ... });
|
|
}
|
|
|
|
// 2. Validation de la continuité de pression
|
|
if pressure_diff > 1e-6 {
|
|
return Err(ConnectionError::PressureMismatch { ... });
|
|
}
|
|
|
|
// 3. Validation de la continuité d'enthalpie
|
|
if enthalpy_diff > 1e-6 {
|
|
return Err(ConnectionError::EnthalpyMismatch { ... });
|
|
}
|
|
|
|
// Création des ports connectés avec valeurs moyennées
|
|
Ok((connected1, connected2))
|
|
}
|
|
```
|
|
|
|
## 🐛 Dépannage
|
|
|
|
### Erreur: `unresolved import entropyk_core`
|
|
|
|
Solution: Vérifier que la dépendance est bien dans `crates/components/Cargo.toml`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
entropyk-core = { path = "../core" }
|
|
```
|
|
|
|
### Erreur: `approx crate not found`
|
|
|
|
Solution: Vérifier la dev-dependency:
|
|
|
|
```toml
|
|
[dev-dependencies]
|
|
approx = "0.5"
|
|
```
|
|
|
|
### Tests qui échouent
|
|
|
|
```bash
|
|
# Nettoyer et reconstruire
|
|
cargo clean
|
|
cargo test --workspace
|
|
```
|
|
|
|
## 📚 Ressources
|
|
|
|
- [Rust Type-State Pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/phantom-types.html)
|
|
- [thiserror documentation](https://docs.rs/thiserror/)
|
|
- [approx documentation](https://docs.rs/approx/) (pour les assertions flottantes)
|
|
|
|
---
|
|
|
|
**Date d'implémentation**: 2026-02-14
|
|
**Story**: 1.3 - Port and Connection System
|
|
**Statut**: ✅ Complété et testé
|