use crate::error::Result;
use crate::ml_testing::{
utils, GenerationConfig, GenerationResult, InputSchema, TestCase, TestCaseType,
};
use ndarray::ArrayD;
use rand::Rng;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct EdgeCaseConfig {
pub case_types: Vec<EdgeCaseType>,
pub boundary_offset: f64,
pub boundary_points: usize,
pub include_invalid: bool,
pub include_special: bool,
}
impl Default for EdgeCaseConfig {
fn default() -> Self {
Self {
case_types: vec![
EdgeCaseType::Boundary,
EdgeCaseType::Corner,
EdgeCaseType::Equivalence,
],
boundary_offset: 0.01,
boundary_points: 3,
include_invalid: false,
include_special: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EdgeCaseType {
Boundary,
Corner,
Equivalence,
Invalid,
Special,
}
pub struct EdgeCaseGenerator {
config: EdgeCaseConfig,
}
impl EdgeCaseGenerator {
pub fn new(case_types: Vec<EdgeCaseType>) -> Self {
let config = EdgeCaseConfig {
case_types,
..Default::default()
};
Self { config }
}
pub fn boundary_values() -> Self {
Self::new(vec![EdgeCaseType::Boundary])
}
pub fn corner_cases() -> Self {
Self::new(vec![EdgeCaseType::Corner])
}
pub fn equivalence_classes() -> Self {
Self::new(vec![EdgeCaseType::Equivalence])
}
pub fn with_config(config: EdgeCaseConfig) -> Self {
Self { config }
}
pub fn generate(
&self,
schema: &InputSchema,
config: &GenerationConfig,
) -> Result<GenerationResult> {
let mut result = GenerationResult::new();
let mut rng = utils::create_rng(config.seed);
for (feature_name, feature_type) in &schema.features {
if result.test_cases.len() >= config.num_cases {
break;
}
let constraint = schema.constraints.get(feature_name);
match feature_type {
crate::ml_testing::FeatureType::Numeric => {
self.generate_numeric_edge_cases(
feature_name,
constraint,
&mut result,
&mut rng,
config,
)?;
}
crate::ml_testing::FeatureType::Categorical(categories) => {
self.generate_categorical_edge_cases(
feature_name,
categories,
&mut result,
&mut rng,
config,
)?;
}
crate::ml_testing::FeatureType::Text => {
self.generate_text_edge_cases(
feature_name,
constraint,
&mut result,
&mut rng,
config,
)?;
}
_ => {
continue;
}
}
}
if self.config.case_types.contains(&EdgeCaseType::Corner) {
self.generate_corner_cases(schema, &mut result, &mut rng, config)?;
}
result
.statistics
.insert("total_cases".to_string(), result.test_cases.len() as f64);
Ok(result)
}
fn generate_numeric_edge_cases(
&self,
feature_name: &str,
constraint: Option<&crate::ml_testing::FeatureConstraint>,
result: &mut GenerationResult,
_rng: &mut impl Rng,
config: &GenerationConfig,
) -> Result<()> {
if let Some(crate::ml_testing::FeatureConstraint::Range { min, max }) = constraint {
if self.config.case_types.contains(&EdgeCaseType::Boundary) {
let boundaries = vec![
*min,
*min + self.config.boundary_offset,
*max - self.config.boundary_offset,
*max,
];
for &value in &boundaries {
if result.test_cases.len() >= config.num_cases {
break;
}
let input = self.create_numeric_input(feature_name, value);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "boundary".to_string()),
("value".to_string(), value.to_string()),
("min".to_string(), min.to_string()),
("max".to_string(), max.to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "boundary_analysis".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
}
if self.config.case_types.contains(&EdgeCaseType::Equivalence) {
let range = max - min;
let partitions = 5;
for i in 0..partitions {
if result.test_cases.len() >= config.num_cases {
break;
}
let value = min + (range * (i as f64)) / ((partitions - 1) as f64);
let input = self.create_numeric_input(feature_name, value);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "equivalence".to_string()),
("partition".to_string(), i.to_string()),
("value".to_string(), value.to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "equivalence_partitioning".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
}
if self.config.include_special {
let special_values = vec![
0.0,
-0.0,
1.0,
-1.0,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
f64::MIN_POSITIVE,
f64::MAX,
f64::MIN,
-f64::MAX,
];
for &value in &special_values {
if result.test_cases.len() >= config.num_cases {
break;
}
if (value >= *min && value <= *max) || self.config.include_invalid {
let input = self.create_numeric_input(feature_name, value);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "special".to_string()),
("value".to_string(), format!("{:?}", value)),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "special_values".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
}
}
if self.config.include_invalid {
let invalid_values = vec![*min - 1.0, *max + 1.0, *min - 100.0, *max + 100.0];
for &value in &invalid_values {
if result.test_cases.len() >= config.num_cases {
break;
}
let input = self.create_numeric_input(feature_name, value);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "invalid".to_string()),
("value".to_string(), value.to_string()),
("reason".to_string(), "out_of_range".to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "invalid_input".to_string(),
confidence: 0.0, metadata,
};
result.test_cases.push(test_case);
}
}
}
Ok(())
}
fn generate_categorical_edge_cases(
&self,
feature_name: &str,
categories: &[String],
result: &mut GenerationResult,
_rng: &mut impl Rng,
config: &GenerationConfig,
) -> Result<()> {
for category in categories {
if result.test_cases.len() >= config.num_cases {
break;
}
let input = self.create_categorical_input(feature_name, category);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "categorical".to_string()),
("category".to_string(), category.clone()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "categorical_coverage".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
if self.config.include_invalid {
let invalid_categories = vec!["", "INVALID", "null", "None"];
for invalid_cat in invalid_categories {
if result.test_cases.len() >= config.num_cases {
break;
}
let input = self.create_categorical_input(feature_name, invalid_cat);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "invalid_categorical".to_string()),
("category".to_string(), invalid_cat.to_string()),
("reason".to_string(), "unknown_category".to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "invalid_categorical".to_string(),
confidence: 0.0,
metadata,
};
result.test_cases.push(test_case);
}
}
Ok(())
}
fn generate_text_edge_cases(
&self,
feature_name: &str,
constraint: Option<&crate::ml_testing::FeatureConstraint>,
result: &mut GenerationResult,
rng: &mut impl Rng,
config: &GenerationConfig,
) -> Result<()> {
let (min_len, max_len) =
if let Some(crate::ml_testing::FeatureConstraint::Length { min, max }) = constraint {
(*min, *max)
} else {
(0, 1000) };
let lengths = vec![min_len, min_len + 1, max_len - 1, max_len];
for &length in &lengths {
if result.test_cases.len() >= config.num_cases {
break;
}
let text = self.generate_text_of_length(length, rng);
let input = self.create_text_input(feature_name, &text);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "text_boundary".to_string()),
("length".to_string(), length.to_string()),
("min_len".to_string(), min_len.to_string()),
("max_len".to_string(), max_len.to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "text_boundary".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
let special_texts = vec![
"",
" ",
"\t",
"\n",
"\r\n",
"a",
"A",
"1",
"!",
"@",
"null",
"NULL",
"None",
"none",
"你好",
"🚀",
"👨💻", ];
for special_text in special_texts {
if result.test_cases.len() >= config.num_cases {
break;
}
let input = self.create_text_input(feature_name, special_text);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "special_text".to_string()),
("text".to_string(), format!("{:?}", special_text)),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "special_text".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
if self.config.include_invalid {
let invalid_lengths = vec![min_len.saturating_sub(1), max_len + 1, 0, 10000];
for &length in &invalid_lengths {
if result.test_cases.len() >= config.num_cases {
break;
}
let text = self.generate_text_of_length(length, rng);
let input = self.create_text_input(feature_name, &text);
let metadata = HashMap::from([
("feature".to_string(), feature_name.to_string()),
("type".to_string(), "invalid_text".to_string()),
("length".to_string(), length.to_string()),
("reason".to_string(), "invalid_length".to_string()),
]);
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "invalid_text_length".to_string(),
confidence: 0.0,
metadata,
};
result.test_cases.push(test_case);
}
}
Ok(())
}
fn generate_corner_cases(
&self,
schema: &InputSchema,
result: &mut GenerationResult,
_rng: &mut impl Rng,
config: &GenerationConfig,
) -> Result<()> {
let mut corner_values = Vec::new();
for (feature_name, feature_type) in &schema.features {
if let crate::ml_testing::FeatureType::Numeric = feature_type {
if let Some(crate::ml_testing::FeatureConstraint::Range { min, max }) =
schema.constraints.get(feature_name)
{
corner_values.push((*min, *max));
}
}
}
if corner_values.len() >= 2 {
let num_features = corner_values.len().min(4); let combinations = 1 << num_features;
for combo in 0..combinations {
if result.test_cases.len() >= config.num_cases {
break;
}
let mut input = ArrayD::zeros(vec![1, schema.features.len()]);
let mut metadata = HashMap::new();
metadata.insert("type".to_string(), "corner_case".to_string());
metadata.insert("combination".to_string(), combo.to_string());
for i in 0..num_features {
let (min_val, max_val) = corner_values[i];
let use_max = (combo & (1 << i)) != 0;
let value = if use_max { max_val } else { min_val };
input[[0, i]] = value as f32;
metadata.insert(
format!("feature_{}", i),
format!("{}={}", if use_max { "max" } else { "min" }, value),
);
}
let test_case = TestCase {
input,
expected_output: None,
case_type: TestCaseType::EdgeCase,
method: "corner_case".to_string(),
confidence: 1.0,
metadata,
};
result.test_cases.push(test_case);
}
}
Ok(())
}
fn create_numeric_input(&self, _feature_name: &str, value: f64) -> ArrayD<f32> {
ArrayD::from_elem(vec![1], value as f32)
}
fn create_categorical_input(&self, _feature_name: &str, category: &str) -> ArrayD<f32> {
let hash = category.chars().map(|c| c as u32 as f32).sum::<f32>();
ArrayD::from_elem(vec![1], hash)
}
fn create_text_input(&self, _feature_name: &str, text: &str) -> ArrayD<f32> {
ArrayD::from_elem(vec![1], text.len() as f32)
}
fn generate_text_of_length(&self, length: usize, rng: &mut impl Rng) -> String {
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ";
(0..length)
.map(|_| CHARS[rng.gen_range(0..CHARS.len())] as char)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ml_testing::{FeatureConstraint, FeatureType, InputSchema};
use std::collections::HashMap;
#[test]
fn test_edge_case_config_default() {
let config = EdgeCaseConfig::default();
assert!(config.case_types.contains(&EdgeCaseType::Boundary));
assert!(config.include_special);
}
#[test]
fn test_boundary_values_generator() {
let generator = EdgeCaseGenerator::boundary_values();
assert_eq!(generator.config.case_types, vec![EdgeCaseType::Boundary]);
}
#[test]
fn test_generate_numeric_edge_cases() {
let mut schema = InputSchema {
features: HashMap::new(),
constraints: HashMap::new(),
};
schema
.features
.insert("age".to_string(), FeatureType::Numeric);
schema.constraints.insert(
"age".to_string(),
FeatureConstraint::Range {
min: 0.0,
max: 100.0,
},
);
let generator = EdgeCaseGenerator::boundary_values();
let config = GenerationConfig::default();
let result = generator.generate(&schema, &config);
assert!(result.is_ok());
let result = result.unwrap();
assert!(!result.test_cases.is_empty());
}
}