use std::collections::BTreeSet;
use indexmap::IndexSet;
use rand::Rng;
use rand::SeedableRng;
use rand::rngs::StdRng;
use serde::{Deserialize, Serialize};
use crate::attributes::{AttributeError, Attributed, LabelKey, LabelValue, Labels, TagKey, TagValue, Tags, Tier};
use crate::constraint::{
BoolConstraint, Constraint, DoubleConstraint, IntConstraint, SelectionConstraint,
StringConstraint,
};
use crate::domain::{
Domain, DoubleDomain, IntegerDomain, ResolverId, SelectionDomain, StringDomain,
};
use crate::expression::{DerivationError, EvalValue, Expression, ValueBindings};
use crate::names::ParameterName;
use crate::validation::ValidationResult;
use crate::value::{GeneratorInfo, SelectionItem, Value, ValueKind};
#[derive(Debug, thiserror::Error)]
pub enum ParameterError {
#[error("default value is not in the parameter's domain")]
DefaultNotInDomain,
#[error("default value violates a registered constraint")]
DefaultViolatesConstraint,
#[error("constraint kind does not match parameter kind ({parameter_kind:?})")]
ConstraintKindMismatch {
parameter_kind: ValueKind,
},
#[error("derived parameters cannot produce Selection values")]
DerivedSelectionUnsupported,
#[error(transparent)]
Attribute(#[from] AttributeError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IntegerParameter {
pub name: ParameterName,
pub domain: IntegerDomain,
#[serde(default)]
pub constraints: Vec<IntConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl IntegerParameter {
pub fn range(name: ParameterName, min: i64, max: i64) -> crate::Result<Self> {
Ok(Self {
name,
domain: IntegerDomain::range(min, max)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn of(name: ParameterName, values: BTreeSet<i64>) -> crate::Result<Self> {
Ok(Self {
name,
domain: IntegerDomain::discrete(values)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn with_default(mut self, default: i64) -> crate::Result<Self> {
if !self.domain.contains_native(default) {
return Err(ParameterError::DefaultNotInDomain.into());
}
for c in &self.constraints {
if !c.test(default) {
return Err(ParameterError::DefaultViolatesConstraint.into());
}
}
self.default = Some(default);
Ok(self)
}
#[must_use]
pub fn with_constraint(mut self, c: IntConstraint) -> Self {
self.constraints.push(c);
self
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DoubleParameter {
pub name: ParameterName,
pub domain: DoubleDomain,
#[serde(default)]
pub constraints: Vec<DoubleConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl DoubleParameter {
pub fn range(name: ParameterName, min: f64, max: f64) -> crate::Result<Self> {
Ok(Self {
name,
domain: DoubleDomain::range(min, max)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn with_default(mut self, default: f64) -> crate::Result<Self> {
if !self.domain.contains_native(default) {
return Err(ParameterError::DefaultNotInDomain.into());
}
for c in &self.constraints {
if !c.test(default) {
return Err(ParameterError::DefaultViolatesConstraint.into());
}
}
self.default = Some(default);
Ok(self)
}
#[must_use]
pub fn with_constraint(mut self, c: DoubleConstraint) -> Self {
self.constraints.push(c);
self
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BooleanParameter {
pub name: ParameterName,
#[serde(default)]
pub constraints: Vec<BoolConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl BooleanParameter {
#[must_use]
pub fn of(name: ParameterName) -> Self {
Self {
name,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
}
}
pub fn with_default(mut self, default: bool) -> crate::Result<Self> {
for c in &self.constraints {
if !c.test(default) {
return Err(ParameterError::DefaultViolatesConstraint.into());
}
}
self.default = Some(default);
Ok(self)
}
#[must_use]
pub fn with_constraint(mut self, c: BoolConstraint) -> Self {
self.constraints.push(c);
self
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StringParameter {
pub name: ParameterName,
pub domain: StringDomain,
#[serde(default)]
pub constraints: Vec<StringConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl StringParameter {
#[must_use]
pub fn of(name: ParameterName) -> Self {
Self {
name,
domain: StringDomain::any(),
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
}
}
pub fn regex(name: ParameterName, pattern: impl Into<String>) -> crate::Result<Self> {
Ok(Self {
name,
domain: StringDomain::regex(pattern)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn with_default(mut self, default: impl Into<String>) -> crate::Result<Self> {
let default = default.into();
if !self.domain.contains_native(&default) {
return Err(ParameterError::DefaultNotInDomain.into());
}
for c in &self.constraints {
if !c.test(&default) {
return Err(ParameterError::DefaultViolatesConstraint.into());
}
}
self.default = Some(default);
Ok(self)
}
#[must_use]
pub fn with_constraint(mut self, c: StringConstraint) -> Self {
self.constraints.push(c);
self
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelectionParameter {
pub name: ParameterName,
pub domain: SelectionDomain,
#[serde(default)]
pub constraints: Vec<SelectionConstraint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<IndexSet<SelectionItem>>,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl SelectionParameter {
pub fn of(
name: ParameterName,
values: IndexSet<SelectionItem>,
max_selections: u32,
) -> crate::Result<Self> {
Ok(Self {
name,
domain: SelectionDomain::fixed(values, max_selections)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn external(
name: ParameterName,
resolver: ResolverId,
max_selections: u32,
) -> crate::Result<Self> {
Ok(Self {
name,
domain: SelectionDomain::external(resolver, max_selections)?,
constraints: Vec::new(),
default: None,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn with_default(
mut self,
default: IndexSet<SelectionItem>,
) -> crate::Result<Self> {
if matches!(self.domain, SelectionDomain::Fixed { .. })
&& !self.domain.contains_items_fixed(&default)
{
return Err(ParameterError::DefaultNotInDomain.into());
}
for c in &self.constraints {
if !c.test(&default) {
return Err(ParameterError::DefaultViolatesConstraint.into());
}
}
self.default = Some(default);
Ok(self)
}
#[must_use]
pub fn with_constraint(mut self, c: SelectionConstraint) -> Self {
self.constraints.push(c);
self
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DerivedParameter {
pub name: ParameterName,
pub kind: ValueKind,
pub expression: Expression,
#[serde(default)]
pub labels: Labels,
#[serde(default)]
pub tags: Tags,
}
impl DerivedParameter {
pub fn new(
name: ParameterName,
kind: ValueKind,
expression: Expression,
) -> crate::Result<Self> {
if matches!(kind, ValueKind::Selection) {
return Err(ParameterError::DerivedSelectionUnsupported.into());
}
Ok(Self {
name,
kind,
expression,
labels: Labels::new(),
tags: Tags::new(),
})
}
pub fn compute(&self, bindings: &ValueBindings) -> Result<Value, DerivationError> {
let raw = self.expression.eval(bindings)?;
if raw.kind() != self.kind {
return Err(DerivationError::TypeMismatch {
op: format!("derived({})", self.name),
expected: format!("{:?}", self.kind),
actual: format!("{:?}", raw.kind()),
});
}
let generator = Some(GeneratorInfo::Derived {
expression: format!("{:?}", self.expression),
});
Ok(match raw {
EvalValue::Integer(n) => Value::integer(self.name.clone(), n, generator),
EvalValue::Double(n) => Value::double(self.name.clone(), n, generator),
EvalValue::Boolean(b) => Value::boolean(self.name.clone(), b, generator),
EvalValue::String(s) => Value::string(self.name.clone(), s, generator),
})
}
pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
check_no_tag_conflict(&self.tags, key.as_str())?;
self.labels.insert(key, value);
Ok(self)
}
pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
check_no_label_conflict(&self.labels, key.as_str())?;
self.tags.insert(key, value);
Ok(self)
}
}
fn check_no_tag_conflict(tags: &Tags, key: &str) -> Result<(), AttributeError> {
if tags.keys().any(|k| k.as_str() == key) {
return Err(AttributeError::DuplicateKey {
key: key.to_owned(),
tiers: vec![Tier::Label, Tier::Tag],
});
}
Ok(())
}
fn check_no_label_conflict(labels: &Labels, key: &str) -> Result<(), AttributeError> {
if labels.keys().any(|k| k.as_str() == key) {
return Err(AttributeError::DuplicateKey {
key: key.to_owned(),
tiers: vec![Tier::Label, Tier::Tag],
});
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Parameter {
Integer(IntegerParameter),
Double(DoubleParameter),
Boolean(BooleanParameter),
String(StringParameter),
Selection(SelectionParameter),
Derived(DerivedParameter),
}
impl Parameter {
#[must_use]
pub const fn name(&self) -> &ParameterName {
match self {
Self::Integer(p) => &p.name,
Self::Double(p) => &p.name,
Self::Boolean(p) => &p.name,
Self::String(p) => &p.name,
Self::Selection(p) => &p.name,
Self::Derived(p) => &p.name,
}
}
#[must_use]
pub const fn kind(&self) -> ValueKind {
match self {
Self::Integer(_) => ValueKind::Integer,
Self::Double(_) => ValueKind::Double,
Self::Boolean(_) => ValueKind::Boolean,
Self::String(_) => ValueKind::String,
Self::Selection(_) => ValueKind::Selection,
Self::Derived(p) => p.kind,
}
}
#[must_use]
pub const fn domain(&self) -> Option<Domain<'_>> {
Some(match self {
Self::Integer(p) => Domain::Integer {
parameter: &p.name,
domain: &p.domain,
},
Self::Double(p) => Domain::Double {
parameter: &p.name,
domain: &p.domain,
},
Self::Boolean(p) => Domain::Boolean { parameter: &p.name },
Self::String(p) => Domain::String {
parameter: &p.name,
domain: &p.domain,
},
Self::Selection(p) => Domain::Selection {
parameter: &p.name,
domain: &p.domain,
},
Self::Derived(_) => return None,
})
}
#[must_use]
pub fn default(&self) -> Option<Value> {
let generator = Some(GeneratorInfo::Default);
match self {
Self::Integer(p) => p
.default
.map(|d| Value::integer(p.name.clone(), d, generator)),
Self::Double(p) => p
.default
.map(|d| Value::double(p.name.clone(), d, generator)),
Self::Boolean(p) => p
.default
.map(|d| Value::boolean(p.name.clone(), d, generator)),
Self::String(p) => p
.default
.clone()
.map(|d| Value::string(p.name.clone(), d, generator)),
Self::Selection(p) => p
.default
.clone()
.map(|d| Value::selection(p.name.clone(), d, generator)),
Self::Derived(_) => None,
}
}
pub fn generate<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
if let Some(d) = self.default() {
return d;
}
self.generate_random(rng)
}
pub fn generate_random<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
if let Self::Derived(_) = self {
unimplemented!(
"derived parameters do not support direct sampling; use DerivedParameter::compute"
);
}
let domain = self.domain().expect("non-derived has a domain");
domain.sample(rng)
}
pub fn generate_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
let domain = self
.domain()
.expect("generate_boundary is undefined for derived parameters");
let boundaries = domain.boundary_values();
if boundaries.is_empty() {
return domain.sample(rng);
}
let idx = rng.gen_range(0..boundaries.len());
boundaries.into_iter().nth(idx).expect("idx < len")
}
#[must_use]
pub fn validate(&self, value: &Value) -> ValidationResult {
if value.kind() != self.kind() {
return ValidationResult::failed(
"kind mismatch",
vec![format!(
"expected {:?}, got {:?}",
self.kind(),
value.kind()
)],
);
}
let mut violations = Vec::new();
match (self, value) {
(Self::Integer(p), Value::Integer(v)) => {
if !p.domain.contains_native(v.value) {
violations.push(format!("value {} not in domain", v.value));
}
for c in &p.constraints {
if !c.test(v.value) {
violations.push("constraint not satisfied".to_owned());
}
}
}
(Self::Double(p), Value::Double(v)) => {
if !p.domain.contains_native(v.value) {
violations.push(format!("value {} not in domain", v.value));
}
for c in &p.constraints {
if !c.test(v.value) {
violations.push("constraint not satisfied".to_owned());
}
}
}
(Self::Boolean(p), Value::Boolean(v)) => {
for c in &p.constraints {
if !c.test(v.value) {
violations.push("constraint not satisfied".to_owned());
}
}
}
(Self::String(p), Value::String(v)) => {
if !p.domain.contains_native(&v.value) {
violations.push("value not in domain".to_owned());
}
for c in &p.constraints {
if !c.test(&v.value) {
violations.push("constraint not satisfied".to_owned());
}
}
}
(Self::Selection(p), Value::Selection(v)) => {
if matches!(p.domain, SelectionDomain::Fixed { .. })
&& !p.domain.contains_items_fixed(&v.items)
{
violations.push("selection not in domain".to_owned());
}
for c in &p.constraints {
if !c.test(&v.items) {
violations.push("constraint not satisfied".to_owned());
}
}
}
(Self::Derived(_), _) => {
}
_ => unreachable!("kind match enforced above"),
}
if violations.is_empty() {
ValidationResult::Passed
} else {
ValidationResult::failed("validation failed", violations)
}
}
#[must_use]
pub fn satisfies(&self, c: &Constraint) -> bool {
if c.kind() != self.kind() {
return false;
}
match (self, c) {
(Self::Integer(p), Constraint::Integer(ic)) => {
for b in p.domain.boundaries_native() {
if ic.test(b) {
return true;
}
}
let mut rng = StdRng::seed_from_u64(SATISFIES_SEED);
for _ in 0..SATISFIES_SAMPLES {
if ic.test(p.domain.sample_native(&mut rng)) {
return true;
}
}
false
}
(Self::Double(p), Constraint::Double(dc)) => {
for b in p.domain.boundaries_native() {
if dc.test(b) {
return true;
}
}
let mut rng = StdRng::seed_from_u64(SATISFIES_SEED);
for _ in 0..SATISFIES_SAMPLES {
if dc.test(p.domain.sample_native(&mut rng)) {
return true;
}
}
false
}
(Self::Boolean(_), Constraint::Boolean(bc)) => bc.test(true) || bc.test(false),
(Self::String(_), Constraint::String(sc)) => {
sc.test("")
}
(Self::Selection(p), Constraint::Selection(sc)) => {
for boundary in p.domain.boundaries_fixed() {
let iset: IndexSet<SelectionItem> = boundary.into_iter().collect();
if sc.test(&iset) {
return true;
}
}
false
}
_ => false,
}
}
}
impl Constraint {
const fn kind(&self) -> ValueKind {
match self {
Self::Integer(_) => ValueKind::Integer,
Self::Double(_) => ValueKind::Double,
Self::Boolean(_) => ValueKind::Boolean,
Self::String(_) => ValueKind::String,
Self::Selection(_) => ValueKind::Selection,
}
}
}
const SATISFIES_SEED: u64 = 0x5a71_5f1e_55e5_d007;
const SATISFIES_SAMPLES: u32 = 8;
impl Attributed for Parameter {
fn labels(&self) -> &Labels {
match self {
Self::Integer(p) => &p.labels,
Self::Double(p) => &p.labels,
Self::Boolean(p) => &p.labels,
Self::String(p) => &p.labels,
Self::Selection(p) => &p.labels,
Self::Derived(p) => &p.labels,
}
}
fn tags(&self) -> &Tags {
match self {
Self::Integer(p) => &p.tags,
Self::Double(p) => &p.tags,
Self::Boolean(p) => &p.tags,
Self::String(p) => &p.tags,
Self::Selection(p) => &p.tags,
Self::Derived(p) => &p.tags,
}
}
}
impl Attributed for IntegerParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
impl Attributed for DoubleParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
impl Attributed for BooleanParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
impl Attributed for StringParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
impl Attributed for SelectionParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
impl Attributed for DerivedParameter {
fn labels(&self) -> &Labels {
&self.labels
}
fn tags(&self) -> &Tags {
&self.tags
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
fn pname(s: &str) -> ParameterName {
ParameterName::new(s).unwrap()
}
fn rng() -> StdRng {
StdRng::seed_from_u64(7)
}
#[test]
fn integer_range_constructor() {
let p = IntegerParameter::range(pname("n"), 1, 10).unwrap();
assert_eq!(p.name.as_str(), "n");
assert_eq!(p.default, None);
}
#[test]
fn integer_with_default_and_constraint() {
let p = IntegerParameter::range(pname("n"), 1, 10)
.unwrap()
.with_constraint(IntConstraint::Min { n: 3 })
.with_default(5)
.unwrap();
assert_eq!(p.default, Some(5));
let err = IntegerParameter::range(pname("n"), 1, 10)
.unwrap()
.with_default(42)
.unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DefaultNotInDomain)
));
let err = IntegerParameter::range(pname("n"), 1, 10)
.unwrap()
.with_constraint(IntConstraint::Min { n: 5 })
.with_default(3)
.unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DefaultViolatesConstraint)
));
}
#[test]
fn integer_label_tag_namespace_enforcement() {
let p = IntegerParameter::range(pname("n"), 1, 10).unwrap();
let p = p
.with_label(LabelKey::new("type").unwrap(), LabelValue::new("threads").unwrap())
.unwrap();
let err = p
.with_tag(TagKey::new("type").unwrap(), TagValue::new("bench").unwrap())
.unwrap_err();
assert!(matches!(
err,
crate::Error::Attribute(AttributeError::DuplicateKey { .. })
));
}
#[test]
fn double_parameter_roundtrip() {
let p = DoubleParameter::range(pname("r"), 0.0, 1.0)
.unwrap()
.with_default(0.5)
.unwrap();
assert_eq!(p.default, Some(0.5));
}
#[test]
fn boolean_parameter_with_default_and_constraint() {
let p = BooleanParameter::of(pname("flag"))
.with_constraint(BoolConstraint::EqTo { b: true })
.with_default(true)
.unwrap();
assert_eq!(p.default, Some(true));
let err = BooleanParameter::of(pname("flag"))
.with_constraint(BoolConstraint::EqTo { b: true })
.with_default(false)
.unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DefaultViolatesConstraint)
));
}
#[test]
fn string_regex_parameter_rejects_non_matching_default() {
let err = StringParameter::regex(pname("s"), "^foo$")
.unwrap()
.with_default("bar")
.unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DefaultNotInDomain)
));
}
#[test]
fn selection_parameter_default_subset_check() {
let values: IndexSet<SelectionItem> =
["a", "b", "c"].iter().map(|s| SelectionItem::new(*s).unwrap()).collect();
let p = SelectionParameter::of(pname("s"), values, 2).unwrap();
let good: IndexSet<SelectionItem> =
std::iter::once(SelectionItem::new("a").unwrap()).collect();
assert!(p.clone().with_default(good).is_ok());
let bad: IndexSet<SelectionItem> =
std::iter::once(SelectionItem::new("z").unwrap()).collect();
let err = p.with_default(bad).unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DefaultNotInDomain)
));
}
#[test]
fn parameter_name_kind_and_domain_dispatch() {
let p: Parameter = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
assert_eq!(p.name().as_str(), "n");
assert_eq!(p.kind(), ValueKind::Integer);
assert!(p.domain().is_some());
}
#[test]
fn parameter_generate_prefers_default() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10)
.unwrap()
.with_default(7)
.unwrap(),
);
let mut r = rng();
let v = p.generate(&mut r);
assert_eq!(v.as_integer(), Some(7));
match v.provenance().generator.as_ref().unwrap() {
GeneratorInfo::Default => {}
other => panic!("expected Default, got {other:?}"),
}
}
#[test]
fn parameter_generate_random_draws_from_domain() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
let mut r = rng();
for _ in 0..20 {
let v = p.generate_random(&mut r);
let n = v.as_integer().unwrap();
assert!((1..=10).contains(&n));
}
}
#[test]
fn parameter_generate_boundary_hits_an_endpoint() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
let mut r = rng();
let mut seen = BTreeSet::new();
for _ in 0..50 {
let v = p.generate_boundary(&mut r);
seen.insert(v.as_integer().unwrap());
}
assert!(seen.contains(&1) || seen.contains(&10));
}
#[test]
fn parameter_validate_catches_kind_and_domain() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
let ok = Value::integer(pname("n"), 5, None);
assert!(p.validate(&ok).is_passed());
let out_of_range = Value::integer(pname("n"), 42, None);
assert!(p.validate(&out_of_range).is_failed());
let wrong_kind = Value::boolean(pname("n"), true, None);
assert!(p.validate(&wrong_kind).is_failed());
}
#[test]
fn parameter_satisfies_hits_constraint_via_boundaries() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
assert!(p.satisfies(&Constraint::Integer(IntConstraint::Min { n: 5 })));
assert!(!p.satisfies(&Constraint::Integer(IntConstraint::Min { n: 100 })));
assert!(!p.satisfies(&Constraint::Boolean(BoolConstraint::EqTo { b: true })));
}
#[test]
fn parameter_attributed_trait() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10).unwrap(),
);
assert!(<Parameter as Attributed>::labels(&p).is_empty());
assert!(<Parameter as Attributed>::tags(&p).is_empty());
}
#[test]
fn parameter_serde_roundtrip() {
let p = Parameter::Integer(
IntegerParameter::range(pname("n"), 1, 10)
.unwrap()
.with_default(5)
.unwrap(),
);
let json = serde_json::to_string(&p).unwrap();
let back: Parameter = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
#[test]
fn derived_parameter_computes_from_bindings() {
use crate::expression::{BinOp, Expression, Literal};
let expr = Expression::binop(
BinOp::Mul,
Expression::reference(pname("threads")),
Expression::literal(Literal::Integer { value: 2 }),
);
let p = DerivedParameter::new(pname("double_threads"), ValueKind::Integer, expr).unwrap();
let mut bindings = ValueBindings::new();
bindings.insert(pname("threads"), Value::integer(pname("threads"), 8, None));
let out = p.compute(&bindings).unwrap();
assert_eq!(out.as_integer(), Some(16));
}
#[test]
fn derived_parameter_rejects_selection_kind() {
use crate::expression::{Expression, Literal};
let err = DerivedParameter::new(
pname("bad"),
ValueKind::Selection,
Expression::literal(Literal::Integer { value: 1 }),
)
.unwrap_err();
assert!(matches!(
err,
crate::Error::Parameter(ParameterError::DerivedSelectionUnsupported)
));
}
#[test]
fn derived_parameter_kind_mismatch_errors() {
use crate::expression::{Expression, Literal};
let p = DerivedParameter::new(
pname("d"),
ValueKind::Double,
Expression::literal(Literal::Integer { value: 1 }),
)
.unwrap();
let err = p.compute(&ValueBindings::new()).unwrap_err();
assert!(matches!(err, DerivationError::TypeMismatch { .. }));
}
#[test]
fn outer_parameter_with_derived_variant() {
use crate::expression::{Expression, Literal};
let p = Parameter::Derived(
DerivedParameter::new(
pname("c"),
ValueKind::Integer,
Expression::literal(Literal::Integer { value: 3 }),
)
.unwrap(),
);
assert_eq!(p.kind(), ValueKind::Integer);
assert!(p.domain().is_none());
}
}