use rstest::fixture;
use rstest_bdd_macros::{given, scenario, then, when};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::str::FromStr;
use whitaker_common::MethodProfileBuilder;
use whitaker_common::test_support::decomposition::{
MethodVectorAlgebraReport, method_vector_algebra,
};
#[derive(Clone, Copy, Debug)]
enum MethodSide {
Left,
Right,
}
impl MethodSide {
fn key(self) -> &'static str {
match self {
Self::Left => "left",
Self::Right => "right",
}
}
}
impl FromStr for MethodSide {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"left" => Ok(Self::Left),
"right" => Ok(Self::Right),
_ => Err(format!("unknown method side `{value}`")),
}
}
}
#[derive(Debug, Clone)]
struct CsvList(Vec<String>);
impl CsvList {
fn into_vec(self) -> Vec<String> {
self.0
}
}
impl FromStr for CsvList {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let items = value
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(ToOwned::to_owned)
.collect();
Ok(Self(items))
}
}
#[derive(Debug, Default)]
struct VectorAlgebraWorld {
methods: RefCell<BTreeMap<String, MethodProfileBuilder>>,
report: RefCell<Option<MethodVectorAlgebraReport>>,
}
#[fixture]
fn world() -> VectorAlgebraWorld {
VectorAlgebraWorld::default()
}
fn ensure_method_builder(world: &VectorAlgebraWorld, side: MethodSide, method_name: &str) {
world.methods.borrow_mut().insert(
side.key().to_owned(),
MethodProfileBuilder::new(method_name),
);
}
fn with_method_builder(
world: &VectorAlgebraWorld,
side: MethodSide,
update: impl FnOnce(&mut MethodProfileBuilder),
) -> Result<(), String> {
let mut methods = world.methods.borrow_mut();
let builder = methods
.get_mut(side.key())
.ok_or_else(|| format!("{} method must be created before configuration", side.key()))?;
update(builder);
Ok(())
}
fn apply_csv_items_to_builder(
world: &VectorAlgebraWorld,
side: MethodSide,
items: CsvList,
mut record: impl FnMut(&mut MethodProfileBuilder, &str),
) -> Result<(), String> {
let parsed_items = items.into_vec();
with_method_builder(world, side, |builder| {
for item in &parsed_items {
record(builder, item.as_str());
}
})
}
fn with_report(
world: &VectorAlgebraWorld,
assert_fn: impl FnOnce(MethodVectorAlgebraReport) -> Result<(), String>,
) -> Result<(), String> {
let report = world.report.borrow();
let value = report
.as_ref()
.copied()
.ok_or_else(|| String::from("vector algebra must be evaluated before assertions"))?;
assert_fn(value)
}
#[given("a {side} method named {name}")]
fn given_method(world: &VectorAlgebraWorld, side: MethodSide, name: String) {
ensure_method_builder(world, side, &name);
}
#[given("the {side} method accesses fields {fields}")]
fn given_fields(
world: &VectorAlgebraWorld,
side: MethodSide,
fields: CsvList,
) -> Result<(), String> {
apply_csv_items_to_builder(world, side, fields, |builder, field| {
builder.record_accessed_field(field);
})
}
#[given("the {side} method uses external domains {domains}")]
fn given_domains(
world: &VectorAlgebraWorld,
side: MethodSide,
domains: CsvList,
) -> Result<(), String> {
apply_csv_items_to_builder(world, side, domains, |builder, domain| {
builder.record_external_domain(domain);
})
}
#[when("the vector algebra is evaluated")]
fn when_vector_algebra_is_evaluated(world: &VectorAlgebraWorld) -> Result<(), String> {
let methods = world.methods.borrow();
let left = methods
.get(MethodSide::Left.key())
.ok_or_else(|| String::from("left method must be configured before evaluation"))?
.clone()
.build();
let right = methods
.get(MethodSide::Right.key())
.ok_or_else(|| String::from("right method must be configured before evaluation"))?
.clone()
.build();
*world.report.borrow_mut() = Some(method_vector_algebra(&left, &right));
Ok(())
}
fn assert_on_report(
world: &VectorAlgebraWorld,
predicate: impl FnOnce(MethodVectorAlgebraReport) -> bool,
message: &'static str,
) -> Result<(), String> {
with_report(world, |report| {
if predicate(report) {
Ok(())
} else {
Err(String::from(message))
}
})
}
#[then("the dot product is commutative")]
fn then_dot_product_is_commutative(world: &VectorAlgebraWorld) -> Result<(), String> {
assert_on_report(
world,
|report| report.left_dot_right() == report.right_dot_left(),
"expected dot product to be commutative",
)
}
#[then("the {side} squared norm is {expected}")]
fn then_squared_norm_matches(
world: &VectorAlgebraWorld,
side: MethodSide,
expected: u64,
) -> Result<(), String> {
with_report(world, |report| {
let actual = match side {
MethodSide::Left => report.left_norm_squared(),
MethodSide::Right => report.right_norm_squared(),
};
if actual == expected {
Ok(())
} else {
Err(format!(
"expected {} squared norm to be {expected}, got {actual}",
side.key()
))
}
})
}
#[then("the dot product is zero")]
fn then_dot_product_is_zero(world: &VectorAlgebraWorld) -> Result<(), String> {
assert_on_report(
world,
|report| report.left_dot_right() == 0 && report.right_dot_left() == 0,
"expected the methods to have zero dot product",
)
}
#[scenario(
path = "tests/features/decomposition_vector_algebra.feature",
index = 0
)]
fn scenario_shared_field_preserves_commutativity(world: VectorAlgebraWorld) {
let _ = world;
}
#[scenario(
path = "tests/features/decomposition_vector_algebra.feature",
index = 1
)]
fn scenario_empty_method_has_non_negative_norm(world: VectorAlgebraWorld) {
let _ = world;
}
#[scenario(
path = "tests/features/decomposition_vector_algebra.feature",
index = 2
)]
fn scenario_disjoint_positive_features_have_zero_dot_product(world: VectorAlgebraWorld) {
let _ = world;
}