use chrono::{DateTime, Utc};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DisclosureConfig {
pub min_attempts_before_hint: usize,
pub max_hints_per_test: usize,
pub unlock_threshold: f64, pub time_based_unlocking: bool,
pub collaborative_unlocking: bool,
}
#[derive(Debug, Clone)]
pub struct StudentProgress {
pub student_id: String,
pub current_level: usize,
pub total_score: f64,
pub attempts_per_test: HashMap<String, usize>,
pub hints_used: HashMap<String, usize>,
pub unlock_history: Vec<UnlockEvent>,
}
#[derive(Debug, Clone)]
pub struct UnlockEvent {
pub level: usize,
pub test_id: String,
pub timestamp: DateTime<Utc>,
pub trigger: UnlockTrigger,
}
#[derive(Debug, Clone)]
pub enum UnlockTrigger {
ScoreThreshold,
TimeElapsed,
PeerProgress,
InstructorOverride,
}
#[derive(Debug, Clone)]
pub struct TestHierarchy {
pub levels: Vec<TestLevel>,
}
#[derive(Debug, Clone)]
pub struct TestLevel {
pub id: String,
pub name: String,
pub description: String,
pub visible_tests: Vec<VisibleTest>,
pub hidden_tests: Vec<HiddenTest>,
pub unlock_requirements: UnlockRequirements,
}
#[derive(Debug, Clone)]
pub struct VisibleTest {
pub id: String,
pub description: String,
pub input: String,
pub expected_output: String,
pub points: u32,
pub hints: Vec<Hint>,
}
#[derive(Debug, Clone)]
pub struct HiddenTest {
pub id: String,
pub input: String,
pub expected_output: String,
pub points: u32,
pub reveal_condition: RevealCondition,
}
#[derive(Debug, Clone)]
pub enum RevealCondition {
Never,
OnCompletion,
OnFailure,
OnRequest,
}
#[derive(Debug, Clone)]
pub struct UnlockRequirements {
pub min_score: f64,
pub required_tests_passed: usize,
pub time_requirements: Option<TimeRequirement>,
}
#[derive(Debug, Clone)]
pub struct TimeRequirement {
pub min_time_spent: chrono::Duration,
pub max_time_allowed: Option<chrono::Duration>,
}
#[derive(Debug, Clone)]
pub struct Hint {
pub id: String,
pub level: HintLevel,
pub content: String,
pub unlock_after_attempts: usize,
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
pub enum HintLevel {
Gentle,
Specific,
Solution,
}
#[derive(Debug)]
pub struct DisclosureResult {
pub visible_tests: Vec<VisibleTest>,
pub available_hints: Vec<Hint>,
pub progress_feedback: String,
pub next_unlock_info: Option<NextUnlockInfo>,
}
#[derive(Debug)]
pub struct NextUnlockInfo {
pub description: String,
pub requirements_met: Vec<String>,
pub requirements_pending: Vec<String>,
pub estimated_unlock_time: Option<DateTime<Utc>>,
}
#[derive(Debug)]
pub struct ProgressiveDisclosure {
pub config: DisclosureConfig,
pub student_progress: HashMap<String, StudentProgress>,
pub test_hierarchy: TestHierarchy,
}
impl ProgressiveDisclosure {
pub fn new(config: DisclosureConfig, hierarchy: TestHierarchy) -> Self {
Self {
config,
student_progress: HashMap::new(),
test_hierarchy: hierarchy,
}
}
pub fn get_available_content(&mut self, student_id: &str) -> DisclosureResult {
let current_level_index = {
let progress = self.get_or_create_progress(student_id);
progress.current_level
};
let current_level = &self.test_hierarchy.levels[current_level_index];
let visible_tests = current_level.visible_tests.clone();
let available_hints = self.get_available_hints_by_level(student_id, current_level_index);
let progress_feedback = self.generate_progress_feedback_by_id(student_id);
let next_unlock_info = self.check_next_unlock(student_id);
DisclosureResult {
visible_tests,
available_hints,
progress_feedback,
next_unlock_info,
}
}
pub fn record_attempt(&mut self, student_id: &str, test_id: &str, score: f64) -> AttemptResult {
let attempt_number = {
let progress = self.get_or_create_progress(student_id);
*progress
.attempts_per_test
.entry(test_id.to_string())
.or_insert(0) += 1;
progress.total_score += score;
progress.attempts_per_test[test_id]
};
let unlocked_levels = self.check_and_unlock_levels(student_id);
let new_hints = self.check_new_hints_by_id(student_id, test_id);
let encouragement = self.generate_encouragement(student_id, score);
AttemptResult {
attempt_number,
new_hints_unlocked: new_hints,
levels_unlocked: unlocked_levels,
encouragement,
}
}
pub fn use_hint(&mut self, student_id: &str, test_id: &str, hint_id: &str) -> HintResult {
let _ = (student_id, test_id, hint_id);
HintResult {
hint: Hint {
id: "error".to_string(),
level: HintLevel::Gentle,
content: "Hint not found".to_string(),
unlock_after_attempts: 0,
},
hints_remaining: 0,
warning: Some("Invalid hint request".to_string()),
}
}
pub fn get_peer_progress(&self, student_id: &str) -> PeerProgressInfo {
let student_progress = self.student_progress.get(student_id);
let current_level = student_progress.map_or(0, |p| p.current_level);
let class_levels: Vec<usize> = self
.student_progress
.values()
.map(|p| p.current_level)
.collect();
let avg_level = if class_levels.is_empty() {
0.0
} else {
class_levels.iter().sum::<usize>() as f64 / class_levels.len() as f64
};
let students_ahead = class_levels
.iter()
.filter(|&&level| level > current_level)
.count();
let students_behind = class_levels
.iter()
.filter(|&&level| level < current_level)
.count();
PeerProgressInfo {
avg_class_level: avg_level,
students_ahead,
students_behind,
your_percentile: self.calculate_percentile(student_id),
collaborative_unlock_available: self.check_collaborative_unlock(student_id),
}
}
fn get_or_create_progress(&mut self, student_id: &str) -> &mut StudentProgress {
self.student_progress
.entry(student_id.to_string())
.or_insert_with(|| StudentProgress {
student_id: student_id.to_string(),
current_level: 0,
total_score: 0.0,
attempts_per_test: HashMap::new(),
hints_used: HashMap::new(),
unlock_history: Vec::new(),
})
}
fn get_current_level(&self, progress: &StudentProgress) -> &TestLevel {
self.test_hierarchy
.levels
.get(progress.current_level)
.unwrap_or(&self.test_hierarchy.levels[0])
}
fn get_available_hints(&self, student_id: &str, level: &TestLevel) -> Vec<Hint> {
let progress = self.student_progress.get(student_id);
let mut available_hints = Vec::new();
for test in &level.visible_tests {
let attempts = progress
.and_then(|p| p.attempts_per_test.get(&test.id))
.unwrap_or(&0);
let hints_used = progress
.and_then(|p| p.hints_used.get(&test.id))
.unwrap_or(&0);
for hint in &test.hints {
if *attempts >= hint.unlock_after_attempts
&& *hints_used < self.config.max_hints_per_test
{
available_hints.push(hint.clone());
}
}
}
available_hints
}
fn generate_progress_feedback(&self, progress: &StudentProgress) -> String {
let current_level = progress.current_level + 1;
let total_levels = self.test_hierarchy.levels.len();
let completion_percentage = (progress.current_level as f64 / total_levels as f64) * 100.0;
format!(
"Level {}/{} ({}% complete). Total score: {:.1}",
current_level, total_levels, completion_percentage as u32, progress.total_score
)
}
fn get_available_hints_by_level(&self, _student_id: &str, _level_index: usize) -> Vec<Hint> {
Vec::new()
}
fn generate_progress_feedback_by_id(&self, _student_id: &str) -> String {
"Progress feedback".to_string()
}
fn check_new_hints_by_id(&self, _student_id: &str, _test_id: &str) -> Vec<Hint> {
Vec::new()
}
fn check_next_unlock(&self, student_id: &str) -> Option<NextUnlockInfo> {
let progress = self.student_progress.get(student_id)?;
let next_level_index = progress.current_level + 1;
if next_level_index >= self.test_hierarchy.levels.len() {
return None; }
let next_level = &self.test_hierarchy.levels[next_level_index];
let requirements = &next_level.unlock_requirements;
let mut requirements_met = Vec::new();
let mut requirements_pending = Vec::new();
if progress.total_score >= requirements.min_score {
requirements_met.push(format!(
"Score: {:.1}/{:.1}",
progress.total_score, requirements.min_score
));
} else {
requirements_pending.push(format!(
"Score: {:.1}/{:.1}",
progress.total_score, requirements.min_score
));
}
let tests_passed = self.count_tests_passed(student_id);
if tests_passed >= requirements.required_tests_passed {
requirements_met.push(format!(
"Tests passed: {}/{}",
tests_passed, requirements.required_tests_passed
));
} else {
requirements_pending.push(format!(
"Tests passed: {}/{}",
tests_passed, requirements.required_tests_passed
));
}
Some(NextUnlockInfo {
description: format!("Unlock {}", next_level.name),
requirements_met,
requirements_pending,
estimated_unlock_time: None, })
}
fn check_and_unlock_levels(&mut self, _student_id: &str) -> Vec<String> {
Vec::new()
}
fn check_new_hints(&self, student_id: &str, test_id: &str) -> Vec<Hint> {
let progress = self.student_progress.get(student_id);
let attempts = progress
.and_then(|p| p.attempts_per_test.get(test_id))
.unwrap_or(&0);
let current_level = progress.map_or(0, |p| p.current_level);
let level = &self.test_hierarchy.levels[current_level];
if let Some(test) = level.visible_tests.iter().find(|t| t.id == test_id) {
return test
.hints
.iter()
.filter(|hint| hint.unlock_after_attempts == *attempts)
.cloned()
.collect();
}
Vec::new()
}
fn generate_encouragement(&self, _student_id: &str, score: f64) -> String {
if score >= 90.0 {
"Excellent work! You're mastering this concept.".to_string()
} else if score >= 70.0 {
"Good progress! Keep practicing to improve.".to_string()
} else if score >= 50.0 {
"You're on the right track. Consider using a hint if you're stuck.".to_string()
} else {
"Don't give up! Learning takes practice. Try breaking the problem into smaller parts."
.to_string()
}
}
fn count_tests_passed(&self, student_id: &str) -> usize {
self.student_progress
.get(student_id)
.map_or(0, |p| p.attempts_per_test.len())
}
fn calculate_percentile(&self, student_id: &str) -> f64 {
let student_score = self
.student_progress
.get(student_id)
.map_or(0.0, |p| p.total_score);
let all_scores: Vec<f64> = self
.student_progress
.values()
.map(|p| p.total_score)
.collect();
if all_scores.is_empty() {
return 0.0;
}
let below = all_scores.iter().filter(|&&s| s < student_score).count();
(below as f64 / all_scores.len() as f64) * 100.0
}
fn check_collaborative_unlock(&self, student_id: &str) -> bool {
if !self.config.collaborative_unlocking {
return false;
}
let class_avg = self.calculate_class_average();
let student_score = self
.student_progress
.get(student_id)
.map_or(0.0, |p| p.total_score);
student_score >= class_avg * 0.8
}
fn calculate_class_average(&self) -> f64 {
let scores: Vec<f64> = self
.student_progress
.values()
.map(|p| p.total_score)
.collect();
if scores.is_empty() {
0.0
} else {
scores.iter().sum::<f64>() / scores.len() as f64
}
}
}
#[derive(Debug)]
pub struct AttemptResult {
pub attempt_number: usize,
pub new_hints_unlocked: Vec<Hint>,
pub levels_unlocked: Vec<String>,
pub encouragement: String,
}
#[derive(Debug)]
pub struct HintResult {
pub hint: Hint,
pub hints_remaining: usize,
pub warning: Option<String>,
}
#[derive(Debug)]
pub struct PeerProgressInfo {
pub avg_class_level: f64,
pub students_ahead: usize,
pub students_behind: usize,
pub your_percentile: f64,
pub collaborative_unlock_available: bool,
}
impl Default for DisclosureConfig {
fn default() -> Self {
Self {
min_attempts_before_hint: 2,
max_hints_per_test: 3,
unlock_threshold: 70.0,
time_based_unlocking: false,
collaborative_unlocking: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_disclosure_config_creation() {
let config = DisclosureConfig {
min_attempts_before_hint: 3,
max_hints_per_test: 5,
unlock_threshold: 0.85,
time_based_unlocking: true,
collaborative_unlocking: false,
};
assert_eq!(config.min_attempts_before_hint, 3);
assert_eq!(config.max_hints_per_test, 5);
assert_eq!(config.unlock_threshold, 0.85);
assert!(config.time_based_unlocking);
assert!(!config.collaborative_unlocking);
}
#[test]
fn test_student_progress_initialization() {
let progress = StudentProgress {
student_id: "test_student".to_string(),
current_level: 0,
total_score: 0.0,
attempts_per_test: HashMap::new(),
hints_used: HashMap::new(),
unlock_history: vec![],
};
assert_eq!(progress.student_id, "test_student");
assert_eq!(progress.current_level, 0);
assert_eq!(progress.total_score, 0.0);
assert!(progress.attempts_per_test.is_empty());
assert!(progress.hints_used.is_empty());
assert!(progress.unlock_history.is_empty());
}
#[test]
fn test_unlock_event_types() {
let event1 = UnlockEvent {
level: 1,
test_id: "test1".to_string(),
timestamp: Utc::now(),
trigger: UnlockTrigger::ScoreThreshold,
};
let event2 = UnlockEvent {
level: 2,
test_id: "test2".to_string(),
timestamp: Utc::now(),
trigger: UnlockTrigger::TimeElapsed,
};
assert_eq!(event1.level, 1);
assert!(matches!(event1.trigger, UnlockTrigger::ScoreThreshold));
assert_eq!(event2.level, 2);
assert!(matches!(event2.trigger, UnlockTrigger::TimeElapsed));
}
#[test]
fn test_visible_test_with_hints() {
let hints = vec![
Hint {
id: "hint1".to_string(),
level: HintLevel::Gentle,
content: "Try thinking about...".to_string(),
unlock_after_attempts: 2,
},
Hint {
id: "hint2".to_string(),
level: HintLevel::Specific,
content: "Use this approach...".to_string(),
unlock_after_attempts: 4,
},
];
let test = VisibleTest {
id: "vis_test".to_string(),
description: "Test description".to_string(),
input: "test input".to_string(),
expected_output: "expected output".to_string(),
points: 15,
hints,
};
assert_eq!(test.id, "vis_test");
assert_eq!(test.points, 15);
assert_eq!(test.hints.len(), 2);
assert_eq!(test.hints[0].level, HintLevel::Gentle);
}
#[test]
fn test_hidden_test_reveal_conditions() {
let conditions = vec![
RevealCondition::Never,
RevealCondition::OnCompletion,
RevealCondition::OnFailure,
RevealCondition::OnRequest,
];
for (i, condition) in conditions.into_iter().enumerate() {
let test = HiddenTest {
id: format!("hidden_{i}"),
input: "input".to_string(),
expected_output: "output".to_string(),
points: 10,
reveal_condition: condition.clone(),
};
match test.reveal_condition {
RevealCondition::Never => {}
RevealCondition::OnCompletion => {}
RevealCondition::OnFailure => {}
RevealCondition::OnRequest => {}
}
}
}
#[test]
fn test_hint_level_ordering() {
assert!(HintLevel::Gentle < HintLevel::Specific);
assert!(HintLevel::Specific < HintLevel::Solution);
assert!(HintLevel::Gentle < HintLevel::Solution);
assert_eq!(HintLevel::Gentle, HintLevel::Gentle);
}
#[test]
fn test_test_hierarchy_creation() {
let level1 = TestLevel {
id: "level1".to_string(),
name: "Beginner".to_string(),
description: "Basic tests".to_string(),
visible_tests: vec![],
hidden_tests: vec![],
unlock_requirements: UnlockRequirements {
min_score: 60.0,
required_tests_passed: 3,
time_requirements: None,
},
};
let hierarchy = TestHierarchy {
levels: vec![level1],
};
assert_eq!(hierarchy.levels.len(), 1);
assert_eq!(hierarchy.levels[0].name, "Beginner");
assert_eq!(hierarchy.levels[0].unlock_requirements.min_score, 60.0);
}
#[test]
fn test_time_requirements() {
let time_req = TimeRequirement {
min_time_spent: chrono::Duration::hours(1),
max_time_allowed: Some(chrono::Duration::hours(24)),
};
assert_eq!(time_req.min_time_spent, chrono::Duration::hours(1));
assert!(time_req.max_time_allowed.is_some());
assert_eq!(
time_req.max_time_allowed.unwrap(),
chrono::Duration::hours(24)
);
}
#[test]
fn test_progressive_disclosure_new() {
let config = DisclosureConfig::default();
let hierarchy = TestHierarchy { levels: vec![] };
let disclosure = ProgressiveDisclosure::new(config.clone(), hierarchy);
assert_eq!(
disclosure.config.min_attempts_before_hint,
config.min_attempts_before_hint
);
assert!(disclosure.student_progress.is_empty());
}
#[test]
fn test_attempt_result_structure() {
let result = AttemptResult {
attempt_number: 3,
new_hints_unlocked: vec![],
levels_unlocked: vec!["level2".to_string()],
encouragement: "Great job!".to_string(),
};
assert_eq!(result.attempt_number, 3);
assert!(result.new_hints_unlocked.is_empty());
assert_eq!(result.levels_unlocked.len(), 1);
assert_eq!(result.encouragement, "Great job!");
}
#[test]
fn test_hint_result_structure() {
let hint = Hint {
id: "hint".to_string(),
level: HintLevel::Gentle,
content: "Hint content".to_string(),
unlock_after_attempts: 1,
};
let result = HintResult {
hint,
hints_remaining: 2,
warning: Some("Only 2 hints left".to_string()),
};
assert_eq!(result.hint.id, "hint");
assert_eq!(result.hints_remaining, 2);
assert!(result.warning.is_some());
}
#[test]
fn test_peer_progress_info() {
let info = PeerProgressInfo {
avg_class_level: 2.5,
students_ahead: 5,
students_behind: 10,
your_percentile: 75.0,
collaborative_unlock_available: true,
};
assert_eq!(info.avg_class_level, 2.5);
assert_eq!(info.students_ahead, 5);
assert_eq!(info.students_behind, 10);
assert_eq!(info.your_percentile, 75.0);
assert!(info.collaborative_unlock_available);
}
#[test]
fn test_disclosure_result_structure() {
let result = DisclosureResult {
visible_tests: vec![],
available_hints: vec![],
progress_feedback: "Keep going!".to_string(),
next_unlock_info: None,
};
assert!(result.visible_tests.is_empty());
assert!(result.available_hints.is_empty());
assert_eq!(result.progress_feedback, "Keep going!");
assert!(result.next_unlock_info.is_none());
}
#[test]
fn test_next_unlock_info() {
let info = NextUnlockInfo {
description: "Next level".to_string(),
requirements_met: vec!["Score".to_string()],
requirements_pending: vec!["Time".to_string()],
estimated_unlock_time: Some(Utc::now()),
};
assert_eq!(info.description, "Next level");
assert_eq!(info.requirements_met.len(), 1);
assert_eq!(info.requirements_pending.len(), 1);
assert!(info.estimated_unlock_time.is_some());
}
#[test]
fn test_unlock_trigger_variants() {
let triggers = vec![
UnlockTrigger::ScoreThreshold,
UnlockTrigger::TimeElapsed,
UnlockTrigger::PeerProgress,
UnlockTrigger::InstructorOverride,
];
for trigger in triggers {
match trigger {
UnlockTrigger::ScoreThreshold => {}
UnlockTrigger::TimeElapsed => {}
UnlockTrigger::PeerProgress => {}
UnlockTrigger::InstructorOverride => {}
}
}
}
fn make_test_level() -> TestLevel {
TestLevel {
id: "level1".to_string(),
name: "Level 1".to_string(),
description: "First level".to_string(),
visible_tests: vec![VisibleTest {
id: "test1".to_string(),
description: "Test 1".to_string(),
input: "1".to_string(),
expected_output: "2".to_string(),
points: 10,
hints: vec![Hint {
id: "hint1".to_string(),
level: HintLevel::Gentle,
content: "Try adding one".to_string(),
unlock_after_attempts: 2,
}],
}],
hidden_tests: vec![],
unlock_requirements: UnlockRequirements {
min_score: 50.0,
required_tests_passed: 1,
time_requirements: None,
},
}
}
fn make_test_hierarchy() -> TestHierarchy {
TestHierarchy {
levels: vec![make_test_level()],
}
}
fn make_test_config() -> DisclosureConfig {
DisclosureConfig {
min_attempts_before_hint: 2,
max_hints_per_test: 3,
unlock_threshold: 70.0,
time_based_unlocking: false,
collaborative_unlocking: false,
}
}
#[test]
fn test_progressive_disclosure_get_available_content() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let result = pd.get_available_content("student1");
assert!(!result.progress_feedback.is_empty());
}
#[test]
fn test_progressive_disclosure_record_attempt_low_score() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let result = pd.record_attempt("student1", "test1", 30.0);
assert_eq!(result.attempt_number, 1);
assert!(
result.encouragement.contains("give up") || result.encouragement.contains("practice")
);
}
#[test]
fn test_progressive_disclosure_record_attempt_medium_score() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let result = pd.record_attempt("student1", "test1", 60.0);
assert!(
result.encouragement.contains("right track") || result.encouragement.contains("hint")
);
}
#[test]
fn test_progressive_disclosure_record_attempt_high_score() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let result = pd.record_attempt("student1", "test1", 95.0);
assert!(
result.encouragement.contains("Excellent")
|| result.encouragement.contains("mastering")
);
}
#[test]
fn test_progressive_disclosure_multiple_attempts() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let result1 = pd.record_attempt("student1", "test1", 50.0);
assert_eq!(result1.attempt_number, 1);
let result2 = pd.record_attempt("student1", "test1", 60.0);
assert_eq!(result2.attempt_number, 2);
let result3 = pd.record_attempt("student1", "test1", 70.0);
assert_eq!(result3.attempt_number, 3);
}
#[test]
fn test_progressive_disclosure_get_peer_progress() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
pd.record_attempt("student1", "test1", 80.0);
pd.record_attempt("student2", "test1", 60.0);
let info = pd.get_peer_progress("student1");
assert!(info.your_percentile >= 0.0);
assert!(info.your_percentile <= 100.0);
}
#[test]
fn test_progressive_disclosure_use_hint() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
pd.record_attempt("student1", "test1", 40.0);
pd.record_attempt("student1", "test1", 45.0);
let result = pd.use_hint("student1", "test1", "hint1");
let _ = result.hints_remaining;
}
#[test]
fn test_student_progress_default_level() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
pd.get_available_content("new_student");
}
#[test]
fn test_class_average_calculation() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
pd.record_attempt("student1", "test1", 80.0);
pd.record_attempt("student2", "test1", 60.0);
pd.record_attempt("student3", "test1", 70.0);
let info = pd.get_peer_progress("student1");
assert!(info.avg_class_level >= 0.0);
}
#[test]
fn test_reveal_condition_variants() {
let conditions = vec![
RevealCondition::Never,
RevealCondition::OnCompletion,
RevealCondition::OnFailure,
RevealCondition::OnRequest,
];
for condition in conditions {
let hidden = HiddenTest {
id: "hidden".to_string(),
input: "in".to_string(),
expected_output: "out".to_string(),
points: 5,
reveal_condition: condition,
};
assert_eq!(hidden.points, 5);
}
}
#[test]
fn test_hint_level_comparison() {
assert!(HintLevel::Gentle < HintLevel::Specific);
assert!(HintLevel::Specific < HintLevel::Solution);
assert!(HintLevel::Gentle < HintLevel::Solution);
}
#[test]
fn test_disclosure_config_default() {
let config = DisclosureConfig::default();
assert!(config.min_attempts_before_hint > 0);
assert!(config.max_hints_per_test > 0);
}
#[test]
fn test_unlock_requirements_with_time() {
let req = UnlockRequirements {
min_score: 80.0,
required_tests_passed: 5,
time_requirements: Some(TimeRequirement {
min_time_spent: chrono::Duration::minutes(10),
max_time_allowed: Some(chrono::Duration::hours(2)),
}),
};
assert_eq!(req.min_score, 80.0);
assert!(req.time_requirements.is_some());
}
#[test]
fn test_multiple_levels_hierarchy() {
let level1 = make_test_level();
let mut level2 = make_test_level();
level2.id = "level2".to_string();
level2.name = "Level 2".to_string();
let hierarchy = TestHierarchy {
levels: vec![level1, level2],
};
assert_eq!(hierarchy.levels.len(), 2);
}
#[test]
fn test_visible_test_multiple_hints() {
let test = VisibleTest {
id: "multi_hint".to_string(),
description: "Test with hints".to_string(),
input: "x".to_string(),
expected_output: "y".to_string(),
points: 20,
hints: vec![
Hint {
id: "h1".to_string(),
level: HintLevel::Gentle,
content: "First hint".to_string(),
unlock_after_attempts: 1,
},
Hint {
id: "h2".to_string(),
level: HintLevel::Specific,
content: "Second hint".to_string(),
unlock_after_attempts: 3,
},
Hint {
id: "h3".to_string(),
level: HintLevel::Solution,
content: "The answer".to_string(),
unlock_after_attempts: 5,
},
],
};
assert_eq!(test.hints.len(), 3);
}
#[test]
fn test_encouragement_message_variety() {
let config = make_test_config();
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
let low = pd.record_attempt("s1", "t1", 20.0);
let mut pd2 = ProgressiveDisclosure::new(make_test_config(), make_test_hierarchy());
let high = pd2.record_attempt("s2", "t2", 100.0);
assert!(!low.encouragement.is_empty());
assert!(!high.encouragement.is_empty());
}
#[test]
fn test_unlock_event_creation() {
let event = UnlockEvent {
level: 2,
test_id: "test_unlock".to_string(),
timestamp: Utc::now(),
trigger: UnlockTrigger::ScoreThreshold,
};
assert_eq!(event.level, 2);
}
#[test]
fn test_student_progress_tracking() {
let mut progress = StudentProgress {
student_id: "tracked".to_string(),
current_level: 0,
total_score: 0.0,
attempts_per_test: HashMap::new(),
hints_used: HashMap::new(),
unlock_history: vec![],
};
progress.attempts_per_test.insert("test1".to_string(), 1);
progress.total_score += 50.0;
assert_eq!(progress.attempts_per_test.get("test1"), Some(&1));
assert_eq!(progress.total_score, 50.0);
}
#[test]
fn test_collaborative_unlock_check() {
let mut config = make_test_config();
config.collaborative_unlocking = true;
let hierarchy = make_test_hierarchy();
let mut pd = ProgressiveDisclosure::new(config, hierarchy);
pd.record_attempt("student1", "test1", 90.0);
pd.record_attempt("student2", "test1", 90.0);
pd.record_attempt("student3", "test1", 90.0);
let info = pd.get_peer_progress("student1");
assert!(info.collaborative_unlock_available || !info.collaborative_unlock_available);
}
}