use crate::runtime::replay::{EvalResult, Event, InputMode, ReplSession};
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ConversionConfig {
pub test_module_prefix: String,
pub include_property_tests: bool,
pub include_benchmarks: bool,
pub timeout_ms: u64,
}
impl Default for ConversionConfig {
fn default() -> Self {
Self {
test_module_prefix: "replay_generated".to_string(),
include_property_tests: true,
include_benchmarks: false,
timeout_ms: 5000,
}
}
}
#[derive(Debug, Clone)]
pub struct GeneratedTest {
pub name: String,
pub code: String,
pub category: TestCategory,
pub coverage_areas: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TestCategory {
Unit,
Integration,
Property,
Benchmark,
ErrorHandling,
}
pub struct ReplayConverter {
config: ConversionConfig,
}
impl ReplayConverter {
pub fn new() -> Self {
Self {
config: ConversionConfig::default(),
}
}
pub fn with_config(config: ConversionConfig) -> Self {
Self { config }
}
pub fn convert_file(&self, replay_path: &Path) -> Result<Vec<GeneratedTest>> {
let replay_content =
fs::read_to_string(replay_path).context("Failed to read replay file")?;
let session: ReplSession =
serde_json::from_str(&replay_content).context("Failed to parse replay session")?;
self.convert_session(
&session,
replay_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unnamed"),
)
}
pub fn convert_session(
&self,
session: &ReplSession,
name_prefix: &str,
) -> Result<Vec<GeneratedTest>> {
let mut tests = Vec::new();
tests.extend(self.generate_unit_tests(session, name_prefix)?);
tests.push(self.generate_integration_test(session, name_prefix)?);
if self.config.include_property_tests {
tests.extend(self.generate_property_tests(session, name_prefix)?);
}
tests.extend(self.generate_error_tests(session, name_prefix)?);
Ok(tests)
}
fn generate_unit_tests(
&self,
session: &ReplSession,
name_prefix: &str,
) -> Result<Vec<GeneratedTest>> {
let mut tests = Vec::new();
let mut test_counter = 1;
let timeline = &session.timeline;
for i in 0..timeline.len() {
if let Event::Input { text, mode } = &timeline[i].event {
if let Some(output_event) = timeline.get(i + 1) {
if let Event::Output { result, .. } = &output_event.event {
let sanitized_prefix = name_prefix.replace('-', "_");
let test_name = format!("{sanitized_prefix}_{test_counter:03}");
let test =
self.generate_single_unit_test(&test_name, text, mode, result)?;
tests.push(test);
test_counter += 1;
}
}
}
}
Ok(tests)
}
fn generate_single_unit_test(
&self,
test_name: &str,
input: &str,
mode: &InputMode,
expected_result: &EvalResult,
) -> Result<GeneratedTest> {
let sanitized_input = input.replace('\"', "\\\"").replace('\n', "\\n");
let timeout = self.config.timeout_ms;
let (expected_output, test_assertion) = match expected_result {
EvalResult::Success { value } => {
let actual_value = if value.starts_with("String(\"") && value.ends_with("\")") {
&value[8..value.len() - 2]
} else {
value.as_str()
};
let sanitized_value = actual_value.replace('\"', "\\\"").replace('\\', "\\\\");
(
format!("Ok(\"{sanitized_value}\")"),
format!("assert!(result.is_ok() && result.unwrap() == r#\"{actual_value}\"#);"),
)
}
EvalResult::Error { message } => (
format!("Err(r#\"{message}\"#)"),
format!("assert!(result.is_err() && result.unwrap_err().to_string().contains(r#\"{message}\"#));"),
),
EvalResult::Unit => (
"Ok(\"\")".to_string(),
"assert!(result.is_ok() && result.unwrap().is_empty());".to_string(),
),
};
let mode_comment = match mode {
InputMode::Interactive => "// Interactive REPL input",
InputMode::Paste => "// Pasted/multiline input",
InputMode::File => "// File-loaded input",
InputMode::Script => "// Script execution",
};
let coverage_areas = self.identify_coverage_areas(input);
let code = format!(
r#"
#[test]
fn test_{test_name}() -> Result<()> {{
{mode_comment}
let mut repl = Repl::new()?;
let deadline = Some(std::time::Instant::now() + std::time::Duration::from_millis({timeout}));
let result = repl.eval("{sanitized_input}");
// Expected: {expected_output}
{test_assertion}
Ok(())
}}"#
);
Ok(GeneratedTest {
name: format!("test_{test_name}"),
code,
category: TestCategory::Unit,
coverage_areas,
})
}
fn generate_integration_test(
&self,
session: &ReplSession,
name_prefix: &str,
) -> Result<GeneratedTest> {
let sanitized_prefix = name_prefix.replace('-', "_");
let mut session_code = String::new();
let mut assertions = Vec::new();
let timeout = self.config.timeout_ms;
let timeline = &session.timeline;
for i in 0..timeline.len() {
if let Event::Input { text, .. } = &timeline[i].event {
let sanitized_input = text.replace('\"', "\\\"").replace('\n', "\\n");
if let Some(output_event) = timeline.get(i + 1) {
if let Event::Output { result, .. } = &output_event.event {
session_code.push_str(&format!(
" let result_{i} = repl.eval(\"{sanitized_input}\");\n"
));
let assertion = match result {
EvalResult::Success { value } => {
let actual_value =
if value.starts_with("String(\"") && value.ends_with("\")") {
&value[8..value.len() - 2]
} else {
value.as_str()
};
format!(" assert!(result_{i}.is_ok() && result_{i}.unwrap() == r#\"{actual_value}\"#);\n")
}
EvalResult::Error { message } => {
format!(" assert!(result_{i}.is_err() && result_{i}.unwrap_err().to_string().contains(r#\"{message}\"#));\n")
}
EvalResult::Unit => {
format!(" assert!(result_{i}.is_ok() && result_{i}.unwrap().is_empty());\n")
}
};
assertions.push(assertion);
}
}
}
}
let code = format!(
r"
#[test]
fn test_{sanitized_prefix}_session_integration() -> Result<()> {{
// Integration test for complete REPL session
// Tests state persistence and interaction patterns
let mut repl = Repl::new()?;
// Session timeout
let _deadline = Some(std::time::Instant::now() + std::time::Duration::from_millis({timeout}));
// Execute complete session
{session_code}
// Verify all expected outputs
{assertions}
Ok(())
}}",
assertions = assertions.join("")
);
let mut coverage_areas = vec![
"session_state".to_string(),
"multi_step_interaction".to_string(),
"state_persistence".to_string(),
];
for event in &session.timeline {
if let Event::Input { text, .. } = &event.event {
coverage_areas.extend(self.identify_coverage_areas(text));
}
}
coverage_areas.sort();
coverage_areas.dedup();
let sanitized_prefix = name_prefix.replace('-', "_");
Ok(GeneratedTest {
name: format!("test_{sanitized_prefix}_session_integration"),
code,
category: TestCategory::Integration,
coverage_areas,
})
}
fn generate_property_tests(
&self,
_session: &ReplSession,
name_prefix: &str,
) -> Result<Vec<GeneratedTest>> {
let mut tests = Vec::new();
let sanitized_prefix = name_prefix.replace('-', "_");
let determinism_test = GeneratedTest {
name: format!("test_{sanitized_prefix}_determinism_property"),
code: format!(
r#"
#[test]
fn test_{sanitized_prefix}_determinism_property() -> Result<()> {{
// Property: Session should produce identical results on replay
use crate::runtime::replay::*;
let mut repl1 = Repl::new()?;
let mut repl2 = Repl::new()?;
// Execute same sequence on both REPLs
let inputs = [
// Insert representative inputs from session
];
for input in inputs {{
let result1 = repl1.eval(input);
let result2 = repl2.eval(input);
match (result1, result2) {{
(Ok(out1), Ok(out2)) => assert_eq!(out1, out2),
(Err(_), Err(_)) => {{}}, // Both failed consistently
_ => panic!("Inconsistent REPL behavior: {{}} vs {{}}", input, input),
}}
}}
Ok(())
}}"#
),
category: TestCategory::Property,
coverage_areas: vec!["determinism".to_string(), "state_consistency".to_string()],
};
tests.push(determinism_test);
let memory_test = GeneratedTest {
name: format!("test_{sanitized_prefix}_memory_bounds"),
code: format!(
r#"
#[test]
fn test_{sanitized_prefix}_memory_bounds() -> Result<()> {{
// Property: REPL should respect memory limits
let mut repl = Repl::new()?;
let initial_memory = repl.get_memory_usage();
// Execute session operations
// ... (session-specific operations)
let final_memory = repl.get_memory_usage();
// Memory should not exceed reasonable bounds (100MB default)
assert!(final_memory < 100 * 1024 * 1024, "Memory usage exceeded bounds: {{}} bytes", final_memory);
Ok(())
}}"#
),
category: TestCategory::Property,
coverage_areas: vec![
"memory_management".to_string(),
"resource_bounds".to_string(),
],
};
tests.push(memory_test);
Ok(tests)
}
fn generate_error_tests(
&self,
session: &ReplSession,
name_prefix: &str,
) -> Result<Vec<GeneratedTest>> {
let mut tests = Vec::new();
for (i, event) in session.timeline.iter().enumerate() {
if let Event::Output {
result: EvalResult::Error { message },
..
} = &event.event
{
let sanitized_prefix = name_prefix.replace('-', "_");
let test_name = format!("{sanitized_prefix}_{i:03}_error_handling");
if i > 0 {
if let Event::Input { text, .. } = &session.timeline[i - 1].event {
let sanitized_input = text.replace('\"', "\\\"");
let sanitized_message = message.replace('\"', "\\\"");
let test = GeneratedTest {
name: format!("test_{test_name}"),
code: format!(
r#"
#[test]
fn test_{test_name}() -> Result<()> {{
// Error handling test: should gracefully handle invalid input
let mut repl = Repl::new()?;
let result = repl.eval("{sanitized_input}");
// Should fail gracefully with descriptive error
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("{sanitized_message}"));
// REPL should remain functional after error
let recovery = repl.eval("2 + 2");
assert_eq!(recovery, Ok("4".to_string()));
Ok(())
}}"#
),
category: TestCategory::ErrorHandling,
coverage_areas: vec![
"error_handling".to_string(),
"error_recovery".to_string(),
"graceful_degradation".to_string(),
],
};
tests.push(test);
}
}
}
}
Ok(tests)
}
fn identify_coverage_areas(&self, input: &str) -> Vec<String> {
let mut areas = Vec::new();
if input.contains("let ") || input.contains("var ") {
areas.push("variable_binding".to_string());
}
if input.contains("fn ") {
areas.push("function_definition".to_string());
}
if input.contains("=>") {
areas.push("lambda_expressions".to_string());
}
if input.contains("match ") {
areas.push("pattern_matching".to_string());
}
if input.contains("if ") {
areas.push("conditional_expressions".to_string());
}
if input.contains("for ") || input.contains("while ") {
areas.push("iteration".to_string());
}
if input.contains('[') && input.contains(']') {
areas.push("array_operations".to_string());
}
if input.contains('(') && input.contains(',') {
areas.push("tuple_operations".to_string());
}
if input.contains('{') && input.contains(':') {
areas.push("object_operations".to_string());
}
if input.contains("?.") {
areas.push("optional_chaining".to_string());
}
if input.contains("??") {
areas.push("null_coalescing".to_string());
}
if input.contains("|>") {
areas.push("pipeline_operator".to_string());
}
if input.contains("...") {
areas.push("spread_operator".to_string());
}
if input.contains("f\"") || input.contains("f'") {
areas.push("string_interpolation".to_string());
}
if input.contains(".map(") || input.contains(".filter(") || input.contains(".reduce(") {
areas.push("higher_order_functions".to_string());
}
if input.starts_with(':') {
areas.push("repl_commands".to_string());
}
if input.contains('?') && !input.contains("??") {
areas.push("repl_introspection".to_string());
}
if input.contains("try ") || input.contains("catch ") {
areas.push("error_handling".to_string());
}
areas
}
pub fn write_tests(&self, tests: &[GeneratedTest], output_path: &Path) -> Result<()> {
let mut content = String::new();
content.push_str(&format!(
r"
//! Generated regression tests from REPL replay sessions
//!
//! This file is auto-generated by the replay-to-test conversion pipeline.
//! DO NOT EDIT MANUALLY - regenerate from .replay files instead.
//!
//! Generated tests: {}
//! Coverage areas: {}
use anyhow::Result;
use crate::runtime::Repl;
",
tests.len(),
tests
.iter()
.flat_map(|t| &t.coverage_areas)
.collect::<std::collections::HashSet<_>>()
.len()
));
let categories = [
TestCategory::Unit,
TestCategory::Integration,
TestCategory::Property,
TestCategory::ErrorHandling,
TestCategory::Benchmark,
];
for category in categories {
let category_tests: Vec<_> = tests.iter().filter(|t| t.category == category).collect();
if !category_tests.is_empty() {
content.push_str(&format!(
"\n// {:?} Tests ({})\n",
category,
category_tests.len()
));
content.push_str("// ============================================================================\n\n");
for test in category_tests {
content.push_str(&test.code);
content.push('\n');
}
}
}
fs::write(output_path, content).context("Failed to write test file")?;
Ok(())
}
}
impl Default for ReplayConverter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::replay::*;
#[test]
fn test_replay_converter_basic() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "2 + 2".to_string(),
mode: InputMode::Interactive,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Success {
value: "4".to_string(),
},
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "basic")
.expect("operation should succeed in test");
assert!(tests.len() >= 2);
assert!(tests.iter().any(|t| t.category == TestCategory::Unit));
assert!(tests
.iter()
.any(|t| t.category == TestCategory::Integration));
}
#[test]
fn test_coverage_area_identification() {
let converter = ReplayConverter::new();
let test_cases = [
("let x = 42", vec!["variable_binding"]),
(
"x.map(y => y * 2)",
vec!["lambda_expressions", "higher_order_functions"],
),
("[1, 2, 3]", vec!["array_operations"]),
("user?.name", vec!["optional_chaining"]),
("match x { 1 => \"one\" }", vec!["pattern_matching"]),
];
for (input, expected_areas) in test_cases {
let areas = converter.identify_coverage_areas(input);
for expected in expected_areas {
assert!(
areas.contains(&expected.to_string()),
"Expected coverage area '{expected}' not found for input: '{input}'"
);
}
}
}
#[test]
fn test_conversion_config_default() {
let config = ConversionConfig::default();
assert_eq!(config.test_module_prefix, "replay_generated");
assert!(config.include_property_tests);
assert!(!config.include_benchmarks);
assert_eq!(config.timeout_ms, 5000);
}
#[test]
fn test_conversion_config_clone() {
let config = ConversionConfig {
test_module_prefix: "custom".to_string(),
include_property_tests: false,
include_benchmarks: true,
timeout_ms: 10000,
};
let cloned = config.clone();
assert_eq!(cloned.test_module_prefix, "custom");
assert!(!cloned.include_property_tests);
assert!(cloned.include_benchmarks);
}
#[test]
fn test_test_category_eq() {
assert_eq!(TestCategory::Unit, TestCategory::Unit);
assert_eq!(TestCategory::Integration, TestCategory::Integration);
assert_ne!(TestCategory::Unit, TestCategory::Integration);
}
#[test]
fn test_generated_test_clone() {
let test = GeneratedTest {
name: "test_foo".to_string(),
code: "#[test] fn test_foo() {}".to_string(),
category: TestCategory::Unit,
coverage_areas: vec!["foo".to_string()],
};
let cloned = test.clone();
assert_eq!(cloned.name, "test_foo");
assert_eq!(cloned.category, TestCategory::Unit);
}
#[test]
fn test_replay_converter_with_config() {
let config = ConversionConfig {
test_module_prefix: "custom_prefix".to_string(),
include_property_tests: false,
include_benchmarks: true,
timeout_ms: 1000,
};
let converter = ReplayConverter::with_config(config);
assert_eq!(converter.config.test_module_prefix, "custom_prefix");
}
#[test]
fn test_replay_converter_default_impl() {
let converter = ReplayConverter::default();
assert_eq!(converter.config.test_module_prefix, "replay_generated");
}
#[test]
fn test_coverage_function_definition() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("fn add(a, b) { a + b }");
assert!(areas.contains(&"function_definition".to_string()));
}
#[test]
fn test_coverage_conditional() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("if x > 0 { x } else { -x }");
assert!(areas.contains(&"conditional_expressions".to_string()));
}
#[test]
fn test_coverage_iteration() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("for i in 0..10 { print(i) }");
assert!(areas.contains(&"iteration".to_string()));
}
#[test]
fn test_coverage_tuple_operations() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("(1, 2, 3)");
assert!(areas.contains(&"tuple_operations".to_string()));
}
#[test]
fn test_coverage_null_coalescing() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("x ?? default_value");
assert!(areas.contains(&"null_coalescing".to_string()));
}
#[test]
fn test_coverage_repl_commands() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas(":help");
assert!(areas.contains(&"repl_commands".to_string()));
}
#[test]
fn test_coverage_string_interpolation() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas(r#"f"Hello {name}""#);
assert!(areas.contains(&"string_interpolation".to_string()));
}
#[test]
fn test_coverage_object_operations() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("{name: \"Alice\", age: 30}");
assert!(areas.contains(&"object_operations".to_string()));
}
#[test]
fn test_coverage_pipeline_operator() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("data |> filter |> map");
assert!(areas.contains(&"pipeline_operator".to_string()));
}
#[test]
fn test_coverage_spread_operator() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("[...arr1, ...arr2]");
assert!(areas.contains(&"spread_operator".to_string()));
}
#[test]
fn test_coverage_error_handling() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("try { x } catch { default }");
assert!(areas.contains(&"error_handling".to_string()));
}
#[test]
fn test_coverage_while_loop() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("while x > 0 { x = x - 1 }");
assert!(areas.contains(&"iteration".to_string()));
}
#[test]
fn test_generated_test_debug() {
let test = GeneratedTest {
name: "test".to_string(),
code: "code".to_string(),
category: TestCategory::Unit,
coverage_areas: vec![],
};
let debug = format!("{:?}", test);
assert!(debug.contains("GeneratedTest"));
}
#[test]
fn test_test_category_debug() {
let cat = TestCategory::Unit;
let debug = format!("{:?}", cat);
assert_eq!(debug, "Unit");
}
#[test]
fn test_test_category_clone() {
let cat1 = TestCategory::Property;
let cat2 = cat1.clone();
assert_eq!(cat1, cat2);
}
#[test]
fn test_test_category_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(TestCategory::Unit);
set.insert(TestCategory::Integration);
set.insert(TestCategory::Unit); assert_eq!(set.len(), 2);
}
#[test]
fn test_conversion_config_debug() {
let config = ConversionConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("ConversionConfig"));
}
#[test]
fn test_session_with_error_result() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "error_test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "invalid syntax".to_string(),
mode: InputMode::Interactive,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Error {
message: "Parse error".to_string(),
},
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "error")
.expect("convert");
assert!(tests
.iter()
.any(|t| t.category == TestCategory::ErrorHandling));
}
#[test]
fn test_session_with_unit_result() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "unit_test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "let x = 42".to_string(),
mode: InputMode::Paste,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Unit,
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "unit")
.expect("convert");
assert!(!tests.is_empty());
}
#[test]
fn test_session_with_string_wrapped_value() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "string_test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "\"hello\"".to_string(),
mode: InputMode::File,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Success {
value: "String(\"hello\")".to_string(),
},
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "string_wrap")
.expect("convert");
assert!(!tests.is_empty());
let unit_tests: Vec<_> = tests
.iter()
.filter(|t| t.category == TestCategory::Unit)
.collect();
assert!(!unit_tests.is_empty());
}
#[test]
fn test_session_with_script_mode() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "script_test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "script.ruchy".to_string(),
mode: InputMode::Script,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Success {
value: "script executed".to_string(),
},
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "script")
.expect("convert");
assert!(!tests.is_empty());
}
#[test]
fn test_write_tests_creates_file() {
let converter = ReplayConverter::new();
let tests = vec![
GeneratedTest {
name: "test_one".to_string(),
code: "#[test]\nfn test_one() {}".to_string(),
category: TestCategory::Unit,
coverage_areas: vec!["area1".to_string()],
},
GeneratedTest {
name: "test_two".to_string(),
code: "#[test]\nfn test_two() {}".to_string(),
category: TestCategory::Integration,
coverage_areas: vec!["area2".to_string()],
},
];
let temp_dir = std::env::temp_dir();
let output_path = temp_dir.join("test_generated.rs");
converter.write_tests(&tests, &output_path).expect("write");
assert!(output_path.exists());
let content = std::fs::read_to_string(&output_path).expect("read");
assert!(content.contains("Unit Tests"));
assert!(content.contains("Integration Tests"));
std::fs::remove_file(&output_path).ok();
}
#[test]
fn test_convert_file_nonexistent() {
let converter = ReplayConverter::new();
let result = converter.convert_file(Path::new("/nonexistent/path.replay"));
assert!(result.is_err());
}
#[test]
fn test_session_without_property_tests() {
let config = ConversionConfig {
test_module_prefix: "test".to_string(),
include_property_tests: false,
include_benchmarks: false,
timeout_ms: 1000,
};
let converter = ReplayConverter::with_config(config);
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "no_prop".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![
TimestampedEvent {
id: EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "1".to_string(),
mode: InputMode::Interactive,
},
causality: vec![],
},
TimestampedEvent {
id: EventId(2),
timestamp_ns: 2000,
event: Event::Output {
result: EvalResult::Success {
value: "1".to_string(),
},
stdout: vec![],
stderr: vec![],
},
causality: vec![],
},
],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "no_prop")
.expect("convert");
assert!(!tests.iter().any(|t| t.category == TestCategory::Property));
}
#[test]
fn test_coverage_introspection() {
let converter = ReplayConverter::new();
let areas = converter.identify_coverage_areas("x?");
assert!(areas.contains(&"repl_introspection".to_string()));
}
#[test]
fn test_empty_session() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "empty".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "empty")
.expect("convert");
assert!(tests
.iter()
.any(|t| t.category == TestCategory::Integration));
}
#[test]
fn test_all_test_categories_in_write() {
let converter = ReplayConverter::new();
let tests = vec![
GeneratedTest {
name: "unit".to_string(),
code: "unit".to_string(),
category: TestCategory::Unit,
coverage_areas: vec![],
},
GeneratedTest {
name: "integration".to_string(),
code: "integration".to_string(),
category: TestCategory::Integration,
coverage_areas: vec![],
},
GeneratedTest {
name: "property".to_string(),
code: "property".to_string(),
category: TestCategory::Property,
coverage_areas: vec![],
},
GeneratedTest {
name: "error".to_string(),
code: "error".to_string(),
category: TestCategory::ErrorHandling,
coverage_areas: vec![],
},
GeneratedTest {
name: "bench".to_string(),
code: "bench".to_string(),
category: TestCategory::Benchmark,
coverage_areas: vec![],
},
];
let temp_dir = std::env::temp_dir();
let output_path = temp_dir.join("test_all_categories.rs");
converter.write_tests(&tests, &output_path).expect("write");
let content = std::fs::read_to_string(&output_path).expect("read");
assert!(content.contains("Unit Tests"));
assert!(content.contains("Integration Tests"));
assert!(content.contains("Property Tests"));
assert!(content.contains("ErrorHandling Tests"));
assert!(content.contains("Benchmark Tests"));
std::fs::remove_file(&output_path).ok();
}
#[test]
fn test_hyphenated_prefix() {
let converter = ReplayConverter::new();
let session = ReplSession {
version: SemVer::new(1, 0, 0),
metadata: SessionMetadata {
session_id: "test".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
ruchy_version: "1.0.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: Environment {
seed: 0,
feature_flags: vec![],
resource_limits: ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![],
checkpoints: std::collections::BTreeMap::new(),
};
let tests = converter
.convert_session(&session, "my-replay-session")
.expect("convert");
for test in &tests {
assert!(
!test.name.contains('-'),
"Test name should not contain hyphens: {}",
test.name
);
}
}
}