Update project structure and configurations
This commit is contained in:
@@ -32,23 +32,27 @@ fn main() {
|
||||
if let Some(coolprop_path) = coolprop_src_path() {
|
||||
println!("cargo:rerun-if-changed={}", coolprop_path.display());
|
||||
|
||||
// Build CoolProp using CMake
|
||||
let dst = cmake::Config::new(&coolprop_path)
|
||||
// Build CoolProp using CMake (always Release to match Rust's CRT)
|
||||
let mut config = cmake::Config::new(&coolprop_path);
|
||||
config
|
||||
.define("COOLPROP_SHARED_LIBRARY", "OFF")
|
||||
.define("COOLPROP_STATIC_LIBRARY", "ON")
|
||||
.define("COOLPROP_CATCH_TEST", "OFF")
|
||||
.define("COOLPROP_C_LIBRARY", "ON")
|
||||
.define("COOLPROP_MY_IFCO3_WRAPPER", "OFF")
|
||||
.build();
|
||||
.profile("Release");
|
||||
let dst = config.build();
|
||||
|
||||
println!("cargo:rustc-link-search=native={}/build", dst.display());
|
||||
println!("cargo:rustc-link-search=native={}/build/Debug", dst.display());
|
||||
println!("cargo:rustc-link-search=native={}/build/Release", dst.display());
|
||||
println!("cargo:rustc-link-search=native={}/lib", dst.display());
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}/build",
|
||||
coolprop_path.display()
|
||||
); // Fallback
|
||||
|
||||
// Link against CoolProp statically
|
||||
// Link against CoolProp statically (always Release build, no 'd' suffix)
|
||||
println!("cargo:rustc-link-lib=static=CoolProp");
|
||||
|
||||
// On macOS, force load the static library so its symbols are exported in the final cdylib
|
||||
|
||||
@@ -130,10 +130,10 @@ pub enum CoolPropInputPair {
|
||||
|
||||
// CoolProp C functions
|
||||
extern "C" {
|
||||
/// Get a property value using pressure and temperature
|
||||
/// Get a property value using pressure and temperature
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z7PropsSIPKcS0_dS0_dS0_")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "_Z7PropsSIPKcS0_dS0_dS0_")]
|
||||
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "_Z7PropsSIPKcS0_dS0_dS0_")]
|
||||
#[cfg_attr(target_os = "windows", link_name = "?PropsSI@@YANPEBD0N0N0@Z")]
|
||||
fn PropsSI(
|
||||
Output: *const c_char,
|
||||
Name1: *const c_char,
|
||||
@@ -145,12 +145,14 @@ extern "C" {
|
||||
|
||||
/// Get a property value using input pair
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z8Props1SIPKcS0_")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "_Z8Props1SIPKcS0_")]
|
||||
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "_Z8Props1SIPKcS0_")]
|
||||
#[cfg_attr(target_os = "windows", link_name = "?Props1SI@@YANPEBD0@Z")]
|
||||
fn Props1SI(Fluid: *const c_char, Output: *const c_char) -> c_double;
|
||||
|
||||
/// Get CoolProp version string
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z23get_global_param_stringPKcPci")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "get_global_param_string")]
|
||||
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "get_global_param_string")]
|
||||
#[cfg_attr(target_os = "windows", link_name = "?get_global_param_string@@YAJPEBDPEADH@Z")]
|
||||
fn get_global_param_string(
|
||||
Param: *const c_char,
|
||||
Output: *mut c_char,
|
||||
@@ -159,7 +161,8 @@ extern "C" {
|
||||
|
||||
/// Get fluid info
|
||||
#[cfg_attr(target_os = "macos", link_name = "\x01__Z22get_fluid_param_stringPKcS0_Pci")]
|
||||
#[cfg_attr(not(target_os = "macos"), link_name = "get_fluid_param_string")]
|
||||
#[cfg_attr(all(not(target_os = "macos"), not(target_os = "windows")), link_name = "get_fluid_param_string")]
|
||||
#[cfg_attr(target_os = "windows", link_name = "?get_fluid_param_string@@YAJPEBD0PEADH@Z")]
|
||||
fn get_fluid_param_string(
|
||||
Fluid: *const c_char,
|
||||
Param: *const c_char,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
//! This module provides a mock backend that returns simplified/idealized
|
||||
//! property values for testing without requiring external dependencies
|
||||
//! like CoolProp.
|
||||
//!
|
||||
//! For R134a, the backend uses tabulated saturation data from NIST REFPROP
|
||||
//! (referenced in the thermodynamic test specifications document) to support
|
||||
//! PressureQuality and PressureEnthalpy state inputs.
|
||||
|
||||
use crate::backend::FluidBackend;
|
||||
use crate::errors::{FluidError, FluidResult};
|
||||
@@ -12,16 +16,38 @@ use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
use entropyk_core::{Pressure, Temperature};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Saturation data point for a refrigerant.
|
||||
/// Values from NIST REFPROP / thermodynamic-test-specifications.md.
|
||||
struct SatPoint {
|
||||
t_celsius: f64,
|
||||
p_bar: f64,
|
||||
hf_kjkg: f64,
|
||||
hg_kjkg: f64,
|
||||
rho_f: f64,
|
||||
rho_g: f64,
|
||||
}
|
||||
|
||||
/// Saturation table for a single fluid, sorted by pressure ascending.
|
||||
struct SatTable {
|
||||
fluid: String,
|
||||
points: Vec<SatPoint>,
|
||||
}
|
||||
|
||||
/// Test backend for unit testing.
|
||||
///
|
||||
/// This backend provides simplified thermodynamic property calculations
|
||||
/// suitable for testing without external dependencies. Values are idealized
|
||||
/// approximations and should NOT be used for real simulations.
|
||||
///
|
||||
/// For R134a, saturation data is interpolated from NIST reference tables,
|
||||
/// enabling PressureQuality (P,x) and PressureEnthalpy (P,h) queries.
|
||||
pub struct TestBackend {
|
||||
/// Map of fluid names to critical points
|
||||
critical_points: HashMap<String, CriticalPoint>,
|
||||
/// List of available test fluids
|
||||
available_fluids: Vec<String>,
|
||||
/// Saturation tables per fluid (R134a, R410A, etc.)
|
||||
sat_tables: Vec<SatTable>,
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
@@ -90,9 +116,157 @@ impl TestBackend {
|
||||
"Air".to_string(),
|
||||
];
|
||||
|
||||
// R134a saturation table from NIST REFPROP / thermodynamic-test-specifications.md §2.1
|
||||
let r134a_sat = SatTable {
|
||||
fluid: "R134a".to_string(),
|
||||
points: vec![
|
||||
SatPoint { t_celsius: -10.0, p_bar: 2.013, hf_kjkg: 186.7, hg_kjkg: 392.7, rho_f: 1295.0, rho_g: 10.2 },
|
||||
SatPoint { t_celsius: 0.0, p_bar: 2.928, hf_kjkg: 200.0, hg_kjkg: 398.6, rho_f: 1295.0, rho_g: 14.4 },
|
||||
SatPoint { t_celsius: 7.0, p_bar: 3.748, hf_kjkg: 209.1, hg_kjkg: 402.4, rho_f: 1262.0, rho_g: 18.2 },
|
||||
SatPoint { t_celsius: 10.0, p_bar: 4.150, hf_kjkg: 213.0, hg_kjkg: 404.0, rho_f: 1251.0, rho_g: 20.2 },
|
||||
SatPoint { t_celsius: 20.0, p_bar: 5.719, hf_kjkg: 227.5, hg_kjkg: 409.4, rho_f: 1226.0, rho_g: 27.8 },
|
||||
SatPoint { t_celsius: 25.0, p_bar: 6.658, hf_kjkg: 234.6, hg_kjkg: 412.0, rho_f: 1207.0, rho_g: 32.3 },
|
||||
SatPoint { t_celsius: 35.0, p_bar: 8.875, hf_kjkg: 249.0, hg_kjkg: 414.4, rho_f: 1168.0, rho_g: 43.1 },
|
||||
SatPoint { t_celsius: 40.0, p_bar: 10.170, hf_kjkg: 256.4, hg_kjkg: 419.4, rho_f: 1148.0, rho_g: 50.8 },
|
||||
SatPoint { t_celsius: 45.0, p_bar: 11.597, hf_kjkg: 263.7, hg_kjkg: 420.6, rho_f: 1129.0, rho_g: 58.9 },
|
||||
SatPoint { t_celsius: 50.0, p_bar: 13.180, hf_kjkg: 271.4, hg_kjkg: 421.2, rho_f: 1102.0, rho_g: 68.2 },
|
||||
],
|
||||
};
|
||||
|
||||
// R410A saturation table from thermodynamic-test-specifications.md §2.2
|
||||
// Extended with low-T points for BPHX evaporator at 4 bar (~-20°C)
|
||||
let r410a_sat = SatTable {
|
||||
fluid: "R410A".to_string(),
|
||||
points: vec![
|
||||
SatPoint { t_celsius: -30.0, p_bar: 2.34, hf_kjkg: 156.0, hg_kjkg: 422.0, rho_f: 1140.0, rho_g: 14.0 },
|
||||
SatPoint { t_celsius: -20.0, p_bar: 4.01, hf_kjkg: 175.0, hg_kjkg: 427.0, rho_f: 1113.0, rho_g: 23.0 },
|
||||
SatPoint { t_celsius: -10.0, p_bar: 5.85, hf_kjkg: 178.0, hg_kjkg: 428.0, rho_f: 1100.0, rho_g: 30.0 },
|
||||
SatPoint { t_celsius: 0.0, p_bar: 7.97, hf_kjkg: 192.0, hg_kjkg: 432.0, rho_f: 1080.0, rho_g: 40.0 },
|
||||
SatPoint { t_celsius: 10.0, p_bar: 10.82, hf_kjkg: 207.0, hg_kjkg: 436.0, rho_f: 1050.0, rho_g: 50.0 },
|
||||
SatPoint { t_celsius: 20.0, p_bar: 14.48, hf_kjkg: 225.0, hg_kjkg: 436.0, rho_f: 1020.0, rho_g: 65.0 },
|
||||
SatPoint { t_celsius: 30.0, p_bar: 18.95, hf_kjkg: 245.0, hg_kjkg: 434.0, rho_f: 985.0, rho_g: 82.0 },
|
||||
SatPoint { t_celsius: 40.0, p_bar: 24.27, hf_kjkg: 268.0, hg_kjkg: 432.0, rho_f: 950.0, rho_g: 100.0 },
|
||||
SatPoint { t_celsius: 50.0, p_bar: 30.47, hf_kjkg: 290.0, hg_kjkg: 427.0, rho_f: 900.0, rho_g: 130.0 },
|
||||
],
|
||||
};
|
||||
|
||||
TestBackend {
|
||||
critical_points,
|
||||
available_fluids,
|
||||
sat_tables: vec![r134a_sat, r410a_sat],
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpolate saturation properties for any fluid with a table.
|
||||
/// Returns (T_sat_C, h_f_kJkg, h_g_kJkg, rho_f, rho_g) or None.
|
||||
fn sat_at_p(&self, fluid: &str, p_pa: f64) -> Option<(f64, f64, f64, f64, f64)> {
|
||||
let table = self.sat_tables.iter().find(|t| t.fluid == fluid)?;
|
||||
let p_bar = p_pa / 1e5;
|
||||
let sat = &table.points;
|
||||
|
||||
if p_bar < sat.first()?.p_bar || p_bar > sat.last()?.p_bar {
|
||||
return None;
|
||||
}
|
||||
|
||||
let idx = sat.iter().position(|s| s.p_bar >= p_bar)?;
|
||||
if idx == 0 {
|
||||
let s = &sat[0];
|
||||
return Some((s.t_celsius, s.hf_kjkg, s.hg_kjkg, s.rho_f, s.rho_g));
|
||||
}
|
||||
|
||||
let lo = &sat[idx - 1];
|
||||
let hi = &sat[idx];
|
||||
let t = (p_bar - lo.p_bar) / (hi.p_bar - lo.p_bar);
|
||||
|
||||
Some((
|
||||
lo.t_celsius + t * (hi.t_celsius - lo.t_celsius),
|
||||
lo.hf_kjkg + t * (hi.hf_kjkg - lo.hf_kjkg),
|
||||
lo.hg_kjkg + t * (hi.hg_kjkg - lo.hg_kjkg),
|
||||
lo.rho_f + t * (hi.rho_f - lo.rho_f),
|
||||
lo.rho_g + t * (hi.rho_g - lo.rho_g),
|
||||
))
|
||||
}
|
||||
|
||||
/// Property from (P, quality) for any fluid with a saturation table.
|
||||
fn property_px(&self, fluid: &str, property: Property, p_pa: f64, x: f64) -> FluidResult<f64> {
|
||||
let (t_sat, hf, hg, rho_f, rho_g) = self
|
||||
.sat_at_p(fluid, p_pa)
|
||||
.ok_or(FluidError::InvalidState {
|
||||
reason: format!("{} pressure {:.2} bar outside TestBackend table range", fluid, p_pa / 1e5),
|
||||
})?;
|
||||
|
||||
let h = hf + x * (hg - hf); // kJ/kg
|
||||
match property {
|
||||
Property::Enthalpy => Ok(h * 1000.0), // J/kg
|
||||
Property::Temperature => Ok(t_sat + 273.15), // K
|
||||
Property::Density => {
|
||||
let vf = 1.0 / rho_f;
|
||||
let vg = 1.0 / rho_g;
|
||||
let v = vf + x * (vg - vf);
|
||||
Ok(1.0 / v)
|
||||
}
|
||||
Property::Pressure => Ok(p_pa),
|
||||
Property::Cp => Ok(1500.0),
|
||||
_ => Err(FluidError::UnsupportedProperty {
|
||||
property: property.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Property from (P, h) for any fluid with a saturation table.
|
||||
fn property_ph(&self, fluid: &str, property: Property, p_pa: f64, h_jkg: f64) -> FluidResult<f64> {
|
||||
let (t_sat, hf, hg, rho_f, rho_g) = self
|
||||
.sat_at_p(fluid, p_pa)
|
||||
.ok_or(FluidError::InvalidState {
|
||||
reason: format!("{} pressure {:.2} bar outside TestBackend table range", fluid, p_pa / 1e5),
|
||||
})?;
|
||||
|
||||
let h_kjkg = h_jkg / 1000.0;
|
||||
let hf_j = hf * 1000.0;
|
||||
let hg_j = hg * 1000.0;
|
||||
|
||||
match property {
|
||||
Property::Temperature => {
|
||||
if h_jkg <= hf_j {
|
||||
// Subcooled liquid: T ≈ T_sat - (hf - h) / cp_liquid
|
||||
Ok(t_sat + 273.15 - (hf - h_kjkg) / 1.5)
|
||||
} else if h_jkg >= hg_j {
|
||||
// Superheated vapor: T ≈ T_sat + (h - hg) / cp_vapor
|
||||
Ok(t_sat + 273.15 + (h_kjkg - hg) / 1.2)
|
||||
} else {
|
||||
// Two-phase: T = T_sat
|
||||
Ok(t_sat + 273.15)
|
||||
}
|
||||
}
|
||||
Property::Quality => {
|
||||
if h_jkg <= hf_j {
|
||||
Ok(0.0) // Subcooled
|
||||
} else if h_jkg >= hg_j {
|
||||
Ok(1.0) // Superheated
|
||||
} else {
|
||||
Ok((h_kjkg - hf) / (hg - hf))
|
||||
}
|
||||
}
|
||||
Property::Density => {
|
||||
if h_jkg <= hf_j {
|
||||
Ok(rho_f)
|
||||
} else if h_jkg >= hg_j {
|
||||
// Superheated: ideal gas approx
|
||||
let t_k = t_sat + 273.15 + (h_kjkg - hg) / 1.2;
|
||||
Ok(p_pa / (t_k * 100.0)) // rough
|
||||
} else {
|
||||
let x = (h_kjkg - hf) / (hg - hf);
|
||||
let vf = 1.0 / rho_f;
|
||||
let vg = 1.0 / rho_g;
|
||||
Ok(1.0 / (vf + x * (vg - vf)))
|
||||
}
|
||||
}
|
||||
Property::Enthalpy => Ok(h_jkg),
|
||||
Property::Pressure => Ok(p_pa),
|
||||
Property::Cp => Ok(1500.0),
|
||||
_ => Err(FluidError::UnsupportedProperty {
|
||||
property: property.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,15 +356,29 @@ impl TestBackend {
|
||||
|
||||
fn refrigerant_property(
|
||||
&self,
|
||||
_fluid: &str,
|
||||
fluid: &str,
|
||||
property: Property,
|
||||
state: FluidState,
|
||||
) -> FluidResult<f64> {
|
||||
// Use tabulated saturation data for P-h and P-x queries
|
||||
match state {
|
||||
FluidState::PressureQuality(p, x) => {
|
||||
return self.property_px(fluid, property, p.to_pascals(), x.0);
|
||||
}
|
||||
FluidState::PressureEnthalpy(p, h) => {
|
||||
return self.property_ph(fluid, property, p.to_pascals(), h.to_joules_per_kg());
|
||||
}
|
||||
_ => {} // fall through to P-T handling below
|
||||
}
|
||||
|
||||
let (p, t) = match state {
|
||||
FluidState::PressureTemperature(p, t) => (p.to_pascals(), t.to_kelvin()),
|
||||
_ => {
|
||||
return Err(FluidError::InvalidState {
|
||||
reason: "TestBackend only supports P-T state for refrigerants".to_string(),
|
||||
reason: format!(
|
||||
"TestBackend only supports P-T state for {} (P-x and P-h available for R134a)",
|
||||
fluid
|
||||
),
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -314,6 +502,8 @@ impl FluidBackend for TestBackend {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::Quality;
|
||||
use entropyk_core::Enthalpy;
|
||||
|
||||
#[test]
|
||||
fn test_backend_available_fluids() {
|
||||
@@ -436,4 +626,110 @@ mod tests {
|
||||
);
|
||||
assert!(state_mix.is_mixture());
|
||||
}
|
||||
|
||||
// ─── R134a saturation table tests (from thermodynamic-test-specifications.md §2.1) ───
|
||||
|
||||
/// T-COMP-BACKEND-01: R134a saturation enthalpy from quality
|
||||
/// At P=2.928 bar (T_sat=0°C), x=0 → h_f=200 kJ/kg, x=1 → h_g=398.6 kJ/kg
|
||||
#[test]
|
||||
fn test_r134a_sat_enthalpy_quality_0_at_0c() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_px(
|
||||
Pressure::from_bar(2.928),
|
||||
Quality(0.0),
|
||||
);
|
||||
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
|
||||
// h_f at 0°C = 200 kJ/kg = 200000 J/kg
|
||||
assert!(
|
||||
(h - 200_000.0).abs() < 500.0,
|
||||
"h_f at 0°C: expected ~200000 J/kg, got {:.0}",
|
||||
h
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r134a_sat_enthalpy_quality_1_at_0c() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_px(
|
||||
Pressure::from_bar(2.928),
|
||||
Quality(1.0),
|
||||
);
|
||||
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
|
||||
// h_g at 0°C = 398.6 kJ/kg = 398600 J/kg
|
||||
assert!(
|
||||
(h - 398_600.0).abs() < 500.0,
|
||||
"h_g at 0°C: expected ~398600 J/kg, got {:.0}",
|
||||
h
|
||||
);
|
||||
}
|
||||
|
||||
/// T-COMP-BACKEND-02: R134a quality from (P, h) — isenthalpic expansion
|
||||
/// Saturated liquid at 40°C (h_f=256.4 kJ/kg) expanded to 0°C (P=2.928 bar)
|
||||
/// x = (h - h_f_evap) / h_fg_evap = (256.4 - 200.0) / (398.6 - 200.0) = 0.284
|
||||
#[test]
|
||||
fn test_r134a_quality_from_ph_after_expansion() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_ph(
|
||||
Pressure::from_bar(2.928),
|
||||
Enthalpy::from_kilojoules_per_kg(256.4),
|
||||
);
|
||||
let x = backend.property(FluidId::new("R134a"), Property::Quality, state).unwrap();
|
||||
assert!(
|
||||
(x - 0.284).abs() < 0.01,
|
||||
"quality after isenthalpic expansion: expected ~0.284, got {:.4}",
|
||||
x
|
||||
);
|
||||
}
|
||||
|
||||
/// T-COMP-BACKEND-03: R134a T_sat from (P, h) in two-phase region
|
||||
/// At P=10.17 bar (40°C), two-phase should return T_sat ≈ 40°C = 313.15 K
|
||||
#[test]
|
||||
fn test_r134a_tsat_from_ph_twophase() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_ph(
|
||||
Pressure::from_bar(10.17),
|
||||
Enthalpy::from_kilojoules_per_kg(350.0), // mid two-phase
|
||||
);
|
||||
let t = backend.property(FluidId::new("R134a"), Property::Temperature, state).unwrap();
|
||||
assert!(
|
||||
(t - 313.15).abs() < 1.0,
|
||||
"T_sat at 10.17 bar: expected ~313.15 K, got {:.2} K",
|
||||
t
|
||||
);
|
||||
}
|
||||
|
||||
/// T-COMP-BACKEND-04: R134a density in two-phase from (P, x)
|
||||
/// At P=2.928 bar (0°C), x=0.5: should be between rho_f and rho_g
|
||||
#[test]
|
||||
fn test_r134a_density_twophase() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_px(
|
||||
Pressure::from_bar(2.928),
|
||||
Quality(0.5),
|
||||
);
|
||||
let rho = backend.property(FluidId::new("R134a"), Property::Density, state).unwrap();
|
||||
// rho_f=1295, rho_g=14.4 at 0°C. At x=0.5, should be much closer to rho_g
|
||||
assert!(
|
||||
rho > 14.4 && rho < 1295.0,
|
||||
"density at x=0.5: expected between 14.4 and 1295, got {:.1}",
|
||||
rho
|
||||
);
|
||||
}
|
||||
|
||||
/// T-COMP-BACKEND-05: R134a interpolated enthalpy between table points
|
||||
/// At P=5.719 bar (20°C), x=0 → h_f=227.5 kJ/kg
|
||||
#[test]
|
||||
fn test_r134a_sat_20c_liquid() {
|
||||
let backend = TestBackend::new();
|
||||
let state = FluidState::from_px(
|
||||
Pressure::from_bar(5.719),
|
||||
Quality(0.0),
|
||||
);
|
||||
let h = backend.property(FluidId::new("R134a"), Property::Enthalpy, state).unwrap();
|
||||
assert!(
|
||||
(h - 227_500.0).abs() < 500.0,
|
||||
"h_f at 20°C: expected ~227500 J/kg, got {:.0}",
|
||||
h
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user