use super::*;
#[test]
fn test_grading_engine_creation() {
let engine = GradingEngine::new();
assert!(engine.replay_validator.strict_mode);
}
#[test]
fn test_grade_report() {
let mut report = GradeReport::new("test_assignment".to_string());
assert!(report.is_valid);
assert_eq!(report.final_grade, 0.0);
report.mark_invalid("Test violation");
assert!(!report.is_valid);
assert_eq!(report.violations.len(), 1);
}
#[test]
fn test_plagiarism_detection() {
let detector = PlagiarismDetector::new();
let session = ReplSession {
version: crate::runtime::replay::SemVer::new(1, 0, 0),
metadata: crate::runtime::replay::SessionMetadata {
session_id: "test".to_string(),
created_at: "2025-08-28T10:00:00Z".to_string(),
ruchy_version: "1.23.0".to_string(),
student_id: Some("student1".to_string()),
assignment_id: Some("hw1".to_string()),
tags: vec![],
},
environment: crate::runtime::replay::Environment {
seed: 42,
feature_flags: vec![],
resource_limits: crate::runtime::replay::ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![],
checkpoints: std::collections::BTreeMap::new(),
};
let score = detector.analyze(&session);
assert_eq!(score, 100.0); }
#[test]
fn test_assignment_creation() {
let assignment = Assignment {
id: "hw001".to_string(),
title: "Introduction to Ruchy".to_string(),
description: "Basic programming exercises".to_string(),
setup: AssignmentSetup {
prelude_code: vec!["let pi = 3.15159".to_string()],
provided_functions: HashMap::new(),
immutable_bindings: HashSet::new(),
},
tasks: vec![],
constraints: AssignmentConstraints {
max_time_ms: 5000,
max_memory_mb: 100,
allowed_imports: vec![],
forbidden_keywords: vec!["eval".to_string()],
performance: None,
},
rubric: GradingRubric {
categories: vec![],
late_penalty: Some(LatePenalty {
grace_hours: 24,
penalty_per_day: 10.0,
max_days_late: 7,
}),
bonus_criteria: vec![],
},
};
assert_eq!(assignment.id, "hw001");
assert_eq!(assignment.title, "Introduction to Ruchy");
assert_eq!(assignment.constraints.max_time_ms, 5000);
}
#[test]
fn test_task_with_test_cases() {
let task = Task {
id: "task_1".to_string(),
description: "Implement fibonacci function".to_string(),
points: 20,
test_cases: vec![TestCase {
input: "fib(5)".to_string(),
expected: ExpectedBehavior::ExactOutput("5".to_string()),
points: 10,
timeout_ms: 1000,
}],
hidden_cases: vec![TestCase {
input: "fib(10)".to_string(),
expected: ExpectedBehavior::ExactOutput("55".to_string()),
points: 10,
timeout_ms: 1000,
}],
requirements: vec![Requirement::UseRecursion],
};
assert_eq!(task.id, "task_1");
assert_eq!(task.points, 20);
assert_eq!(task.test_cases.len(), 1);
assert_eq!(task.hidden_cases.len(), 1);
assert_eq!(task.requirements.len(), 1);
}
#[test]
fn test_expected_behavior_variants() {
let behaviors = vec![
ExpectedBehavior::ExactOutput("42".to_string()),
ExpectedBehavior::Pattern(r"Result: \d+".to_string()),
ExpectedBehavior::TypeSignature("int -> int".to_string()),
ExpectedBehavior::Predicate(PredicateCheck {
name: "is_even".to_string(),
check_fn: "x % 2 == 0".to_string(),
}),
ExpectedBehavior::PerformanceBound {
max_ns: 1_000_000,
max_bytes: 1024,
},
];
for behavior in behaviors {
match behavior {
ExpectedBehavior::ExactOutput(s) => assert!(!s.is_empty()),
ExpectedBehavior::Pattern(p) => assert!(!p.is_empty()),
ExpectedBehavior::TypeSignature(t) => assert!(!t.is_empty()),
ExpectedBehavior::Predicate(pred) => assert!(!pred.name.is_empty()),
ExpectedBehavior::PerformanceBound { max_ns, max_bytes } => {
assert!(max_ns > 0);
assert!(max_bytes > 0);
}
}
}
}
#[test]
fn test_requirements_enum() {
let requirements = [
Requirement::UseRecursion,
Requirement::NoLoops,
Requirement::UseHigherOrderFunctions,
Requirement::TypeSafe,
Requirement::PureFunction,
Requirement::TailRecursive,
];
assert_eq!(requirements.len(), 6);
for (i, req1) in requirements.iter().enumerate() {
for (j, req2) in requirements.iter().enumerate() {
if i != j {
assert!(!matches!(
(req1, req2),
(Requirement::UseRecursion, Requirement::UseRecursion)
));
}
}
}
}
#[test]
fn test_grading_rubric() {
let rubric = GradingRubric {
categories: vec![
RubricCategory {
name: "Correctness".to_string(),
weight: 0.5,
criteria: vec![Criterion {
description: "All tests pass".to_string(),
max_points: 50,
evaluation: CriterionEvaluation::Automatic(AutomaticCheck::TestsPassed),
}],
},
RubricCategory {
name: "Style".to_string(),
weight: 0.3,
criteria: vec![],
},
],
late_penalty: Some(LatePenalty {
grace_hours: 0,
penalty_per_day: 5.0,
max_days_late: 10,
}),
bonus_criteria: vec![],
};
assert_eq!(rubric.categories.len(), 2);
assert_eq!(rubric.categories[0].weight, 0.5);
assert_eq!(rubric.categories[1].weight, 0.3);
if let Some(penalty) = &rubric.late_penalty {
assert_eq!(penalty.penalty_per_day, 5.0);
assert_eq!(penalty.grace_hours, 0);
} else {
panic!("Expected late penalty");
}
}
#[test]
fn test_performance_constraints() {
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 50,
complexity_bound: "O(n log n)".to_string(),
};
assert_eq!(constraints.max_cpu_ms, 1000);
assert_eq!(constraints.max_heap_mb, 50);
assert_eq!(constraints.complexity_bound, "O(n log n)");
}
#[test]
fn test_auto_grader_initialization() {
let assignment = Assignment {
id: "test".to_string(),
title: "Test Assignment".to_string(),
description: String::new(),
setup: AssignmentSetup {
prelude_code: vec![],
provided_functions: HashMap::new(),
immutable_bindings: HashSet::new(),
},
tasks: vec![],
constraints: AssignmentConstraints {
max_time_ms: 5000,
max_memory_mb: 100,
allowed_imports: vec![],
forbidden_keywords: vec![],
performance: None,
},
rubric: GradingRubric {
categories: vec![],
late_penalty: None,
bonus_criteria: vec![],
},
};
assert_eq!(assignment.id, "test");
}
#[test]
fn test_predicate_check() {
let predicate = PredicateCheck {
name: "is_prime".to_string(),
check_fn: "fn(n) { n > 1 && (2..n).all(|i| n % i != 0) }".to_string(),
};
assert_eq!(predicate.name, "is_prime");
assert!(!predicate.check_fn.is_empty());
}
#[test]
fn test_assignment_setup_with_immutable_bindings() {
let mut immutable = HashSet::new();
immutable.insert("PI".to_string());
immutable.insert("E".to_string());
let mut provided = HashMap::new();
provided.insert("helper".to_string(), "fn helper(x) { x * 2 }".to_string());
let setup = AssignmentSetup {
prelude_code: vec![
"let PI = 3.15159".to_string(),
"let E = 2.71828".to_string(),
],
provided_functions: provided,
immutable_bindings: immutable,
};
assert_eq!(setup.prelude_code.len(), 2);
assert_eq!(setup.provided_functions.len(), 1);
assert_eq!(setup.immutable_bindings.len(), 2);
assert!(setup.immutable_bindings.contains("PI"));
}
fn make_test_result(passed: bool, points: u32) -> TestResult {
TestResult {
passed,
points_earned: points,
feedback: if passed {
"Test passed".to_string()
} else {
"Test failed".to_string()
},
execution_time_ms: 10,
}
}
#[test]
fn test_task_grade_new() {
let grade = TaskGrade::new("task_001".to_string());
assert_eq!(grade.task_id, "task_001");
assert_eq!(grade.points_earned, 0);
assert_eq!(grade.points_possible, 0);
assert!(grade.test_results.is_empty());
assert!(grade.hidden_results.is_empty());
assert!(grade.requirements_met.is_empty());
}
#[test]
fn test_task_grade_add_test_result_passed() {
let mut grade = TaskGrade::new("task_001".to_string());
grade.add_test_result("1 + 1".to_string(), make_test_result(true, 10));
assert_eq!(grade.test_results.len(), 1);
assert!(grade.test_results.iter().any(|(k, _)| k == "1 + 1"));
}
#[test]
fn test_task_grade_add_test_result_failed() {
let mut grade = TaskGrade::new("task_001".to_string());
grade.add_test_result("1 + 1".to_string(), make_test_result(false, 0));
assert_eq!(grade.test_results.len(), 1);
}
#[test]
fn test_task_grade_add_hidden_result() {
let mut grade = TaskGrade::new("task_001".to_string());
grade.add_hidden_result("fib(10)".to_string(), make_test_result(true, 10));
assert_eq!(grade.hidden_results.len(), 1);
assert!(grade.hidden_results.iter().any(|(k, _)| k == "fib(10)"));
}
#[test]
fn test_task_grade_calculate_score() {
let mut grade = TaskGrade::new("task_001".to_string());
grade.add_test_result("test1".to_string(), make_test_result(true, 10));
grade.add_test_result("test2".to_string(), make_test_result(true, 10));
grade.add_hidden_result("hidden1".to_string(), make_test_result(true, 10));
grade.calculate_score(30);
assert_eq!(grade.points_possible, 30);
assert_eq!(grade.points_earned, 30);
}
#[test]
fn test_task_grade_calculate_score_partial() {
let mut grade = TaskGrade::new("task_001".to_string());
grade.add_test_result("test1".to_string(), make_test_result(true, 10));
grade.add_test_result("test2".to_string(), make_test_result(false, 0));
grade.calculate_score(20);
assert_eq!(grade.points_possible, 20);
assert_eq!(grade.points_earned, 10);
}
#[test]
fn test_test_result_struct() {
let result = TestResult {
passed: true,
points_earned: 10,
feedback: "Good work!".to_string(),
execution_time_ms: 50,
};
assert!(result.passed);
assert_eq!(result.points_earned, 10);
assert_eq!(result.feedback, "Good work!");
assert_eq!(result.execution_time_ms, 50);
}
#[test]
fn test_test_result_failed() {
let result = TestResult {
passed: false,
points_earned: 0,
feedback: "Expected 42, got 41".to_string(),
execution_time_ms: 25,
};
assert!(!result.passed);
assert_eq!(result.points_earned, 0);
}
#[test]
fn test_grade_report_add_task_grade() {
let mut report = GradeReport::new("test_assignment".to_string());
let mut task_grade = TaskGrade::new("task_1".to_string());
task_grade.add_test_result("test".to_string(), make_test_result(true, 10));
task_grade.calculate_score(10);
report.add_task_grade(task_grade);
assert_eq!(report.task_grades.len(), 1);
assert_eq!(report.task_grades[0].task_id, "task_1");
}
#[test]
fn test_grade_report_calculate_final_grade() {
let mut report = GradeReport::new("test_assignment".to_string());
let mut task1 = TaskGrade::new("task_1".to_string());
task1.add_test_result("t1".to_string(), make_test_result(true, 50));
task1.calculate_score(50);
report.add_task_grade(task1);
let mut task2 = TaskGrade::new("task_2".to_string());
task2.add_test_result("t2".to_string(), make_test_result(true, 50));
task2.calculate_score(50);
report.add_task_grade(task2);
report.calculate_final_grade();
assert!(report.final_grade >= 0.0);
}
#[test]
fn test_grade_report_calculate_final_grade_partial() {
let mut report = GradeReport::new("test_assignment".to_string());
let mut task1 = TaskGrade::new("task_1".to_string());
task1.points_earned = 25;
task1.points_possible = 50;
report.add_task_grade(task1);
let mut task2 = TaskGrade::new("task_2".to_string());
task2.points_earned = 50;
task2.points_possible = 50;
report.add_task_grade(task2);
report.calculate_final_grade();
assert!(report.final_grade >= 0.0);
}
#[test]
fn test_grade_report_invalid_report() {
let mut report = GradeReport::new("test".to_string());
report.mark_invalid("Plagiarism detected");
report.mark_invalid("Time limit exceeded");
assert!(!report.is_valid);
assert_eq!(report.violations.len(), 2);
}
#[test]
fn test_rubric_category_creation() {
let category = RubricCategory {
name: "Code Quality".to_string(),
weight: 0.25,
criteria: vec![
Criterion {
description: "Code is readable".to_string(),
max_points: 10,
evaluation: CriterionEvaluation::Manual("Check readability".to_string()),
},
Criterion {
description: "All tests pass".to_string(),
max_points: 10,
evaluation: CriterionEvaluation::Automatic(AutomaticCheck::TestsPassed),
},
],
};
assert_eq!(category.name, "Code Quality");
assert_eq!(category.weight, 0.25);
assert_eq!(category.criteria.len(), 2);
}
#[test]
fn test_bonus_criterion() {
let bonus = BonusCriterion {
description: "Creative solution".to_string(),
points: 5,
check: BonusCheck::CreativeSolution,
};
assert_eq!(bonus.points, 5);
assert!(!bonus.description.is_empty());
}
#[test]
fn test_late_penalty_calculation() {
let penalty = LatePenalty {
grace_hours: 12,
penalty_per_day: 10.0,
max_days_late: 5,
};
assert_eq!(penalty.grace_hours, 12);
assert_eq!(penalty.penalty_per_day, 10.0);
assert_eq!(penalty.max_days_late, 5);
}
#[test]
fn test_automatic_check_tests_passed() {
let check = AutomaticCheck::TestsPassed;
assert!(matches!(check, AutomaticCheck::TestsPassed));
}
#[test]
fn test_automatic_check_code_quality() {
let check = AutomaticCheck::CodeQuality { min_score: 0.8 };
match check {
AutomaticCheck::CodeQuality { min_score } => assert_eq!(min_score, 0.8),
_ => panic!("Expected CodeQuality"),
}
}
#[test]
fn test_automatic_check_performance() {
let check = AutomaticCheck::Performance {
metric: "execution_time".to_string(),
threshold: 100.0,
};
match check {
AutomaticCheck::Performance { metric, threshold } => {
assert_eq!(metric, "execution_time");
assert_eq!(threshold, 100.0);
}
_ => panic!("Expected Performance"),
}
}
#[test]
fn test_plagiarism_detector_empty_session() {
let detector = PlagiarismDetector::new();
let session = ReplSession {
version: crate::runtime::replay::SemVer::new(1, 0, 0),
metadata: crate::runtime::replay::SessionMetadata {
session_id: "test_empty".to_string(),
created_at: "2025-08-28T10:00:00Z".to_string(),
ruchy_version: "1.23.0".to_string(),
student_id: None,
assignment_id: None,
tags: vec![],
},
environment: crate::runtime::replay::Environment {
seed: 0,
feature_flags: vec![],
resource_limits: crate::runtime::replay::ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline: vec![],
checkpoints: std::collections::BTreeMap::new(),
};
let score = detector.analyze(&session);
assert_eq!(score, 100.0);
}
#[test]
fn test_criterion_evaluation_variants() {
let auto_eval = CriterionEvaluation::Automatic(AutomaticCheck::TestsPassed);
let manual_eval = CriterionEvaluation::Manual("Grade code style".to_string());
let hybrid_eval = CriterionEvaluation::Hybrid {
auto_weight: 0.6,
manual_weight: 0.4,
};
assert!(matches!(auto_eval, CriterionEvaluation::Automatic(_)));
assert!(matches!(manual_eval, CriterionEvaluation::Manual(_)));
assert!(matches!(hybrid_eval, CriterionEvaluation::Hybrid { .. }));
}
#[test]
fn test_bonus_check_variants() {
let checks = vec![
BonusCheck::ExtraFeature("Dark mode".to_string()),
BonusCheck::Optimization {
improvement_percent: 50.0,
},
BonusCheck::CreativeSolution,
];
assert_eq!(checks.len(), 3);
}
fn make_repl_session(timeline: Vec<crate::runtime::replay::TimestampedEvent>) -> ReplSession {
ReplSession {
version: crate::runtime::replay::SemVer::new(1, 0, 0),
metadata: crate::runtime::replay::SessionMetadata {
session_id: "test_session".to_string(),
created_at: "2025-08-28T10:00:00Z".to_string(),
ruchy_version: "1.23.0".to_string(),
student_id: Some("student_1".to_string()),
assignment_id: Some("hw1".to_string()),
tags: vec![],
},
environment: crate::runtime::replay::Environment {
seed: 42,
feature_flags: vec![],
resource_limits: crate::runtime::replay::ResourceLimits {
heap_mb: 100,
stack_kb: 8192,
cpu_ms: 5000,
},
},
timeline,
checkpoints: std::collections::BTreeMap::new(),
}
}
#[test]
fn test_run_test_case_exact_output_pass() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "1 + 1".to_string(),
expected: ExpectedBehavior::ExactOutput("2".to_string()),
points: 10,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
assert!(result.execution_time_ms <= 5000);
if result.passed {
assert_eq!(result.points_earned, 10);
assert_eq!(result.feedback, "Correct output");
} else {
assert_eq!(result.points_earned, 0);
}
}
#[test]
fn test_run_test_case_exact_output_fail() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "1 + 1".to_string(),
expected: ExpectedBehavior::ExactOutput("999".to_string()),
points: 10,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
assert!(!result.passed);
assert_eq!(result.points_earned, 0);
assert!(result.feedback.contains("Expected '999'"));
}
#[test]
fn test_run_test_case_pattern_match() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "42".to_string(),
expected: ExpectedBehavior::Pattern(r"\d+".to_string()),
points: 5,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
if result.passed {
assert_eq!(result.points_earned, 5);
assert_eq!(result.feedback, "Output matches pattern");
}
}
#[test]
fn test_run_test_case_pattern_no_match() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "42".to_string(),
expected: ExpectedBehavior::Pattern(r"^[a-z]+$".to_string()),
points: 5,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
assert!(!result.passed);
assert!(result.feedback.contains("doesn't match pattern"));
}
#[test]
fn test_run_test_case_type_signature() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "42".to_string(),
expected: ExpectedBehavior::TypeSignature("int".to_string()),
points: 5,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
assert!(result.execution_time_ms <= 5000);
}
#[test]
fn test_run_test_case_unsupported_check() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "42".to_string(),
expected: ExpectedBehavior::Predicate(PredicateCheck {
name: "is_positive".to_string(),
check_fn: "x > 0".to_string(),
}),
points: 5,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
assert!(!result.passed);
assert_eq!(result.feedback, "Unsupported check");
}
#[test]
fn test_run_test_case_eval_error() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "undefined_variable_xyz_123".to_string(),
expected: ExpectedBehavior::ExactOutput("anything".to_string()),
points: 10,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
if !result.passed {
assert_eq!(result.points_earned, 0);
assert!(!result.feedback.is_empty());
}
}
#[test]
fn test_run_test_case_invalid_regex_pattern() {
let engine = GradingEngine::new();
let mut repl = engine
.secure_sandbox
.create_isolated_repl()
.expect("sandbox should create repl");
let test_case = TestCase {
input: "42".to_string(),
expected: ExpectedBehavior::Pattern("[invalid(regex".to_string()),
points: 5,
timeout_ms: 5000,
};
let result = engine.run_test_case(&mut repl, &test_case);
if result.passed {
assert_eq!(result.points_earned, 5);
}
}
#[test]
fn test_measure_performance_no_resource_events() {
let engine = GradingEngine::new();
let session = make_repl_session(vec![]);
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 100.0);
}
#[test]
fn test_measure_performance_within_bounds() {
let engine = GradingEngine::new();
let timeline = vec![crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::ResourceUsage {
heap_bytes: 1024 * 1024, stack_depth: 10,
cpu_ns: 500_000_000, },
causality: vec![],
}];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 100.0);
}
#[test]
fn test_measure_performance_cpu_exceeded() {
let engine = GradingEngine::new();
let timeline = vec![crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::ResourceUsage {
heap_bytes: 1024, stack_depth: 10,
cpu_ns: 2_000_000_000, },
causality: vec![],
}];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 80.0);
}
#[test]
fn test_measure_performance_heap_exceeded() {
let engine = GradingEngine::new();
let timeline = vec![crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::ResourceUsage {
heap_bytes: 200 * 1024 * 1024, stack_depth: 10,
cpu_ns: 100_000_000, },
causality: vec![],
}];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 80.0);
}
#[test]
fn test_measure_performance_both_exceeded() {
let engine = GradingEngine::new();
let timeline = vec![crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::ResourceUsage {
heap_bytes: 200 * 1024 * 1024, stack_depth: 10,
cpu_ns: 5_000_000_000, },
causality: vec![],
}];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 60.0);
}
#[test]
fn test_measure_performance_multiple_resource_events() {
let engine = GradingEngine::new();
let timeline = vec![
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::ResourceUsage {
heap_bytes: 50 * 1024 * 1024,
stack_depth: 10,
cpu_ns: 300_000_000, },
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(2),
timestamp_ns: 2000,
event: Event::ResourceUsage {
heap_bytes: 80 * 1024 * 1024,
stack_depth: 15,
cpu_ns: 400_000_000, },
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(3),
timestamp_ns: 3000,
event: Event::ResourceUsage {
heap_bytes: 60 * 1024 * 1024,
stack_depth: 12,
cpu_ns: 400_000_000, },
causality: vec![],
},
];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 2000, max_heap_mb: 100, complexity_bound: "O(n)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 100.0);
}
#[test]
fn test_measure_performance_mixed_events_only_resource_counted() {
let engine = GradingEngine::new();
let timeline = vec![
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "1 + 1".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(2),
timestamp_ns: 2000,
event: Event::ResourceUsage {
heap_bytes: 1024,
stack_depth: 5,
cpu_ns: 100_000_000,
},
causality: vec![],
},
];
let session = make_repl_session(timeline);
let constraints = PerformanceConstraints {
max_cpu_ms: 500,
max_heap_mb: 50,
complexity_bound: "O(1)".to_string(),
};
let score = engine.measure_performance(&session, &constraints);
assert_eq!(score, 100.0);
}
fn make_simple_assignment(
tasks: Vec<Task>,
perf: Option<PerformanceConstraints>,
) -> Assignment {
Assignment {
id: "test_hw".to_string(),
title: "Test Assignment".to_string(),
description: "Test".to_string(),
setup: AssignmentSetup {
prelude_code: vec![],
provided_functions: HashMap::new(),
immutable_bindings: HashSet::new(),
},
tasks,
constraints: AssignmentConstraints {
max_time_ms: 5000,
max_memory_mb: 100,
allowed_imports: vec![],
forbidden_keywords: vec![],
performance: perf,
},
rubric: GradingRubric {
categories: vec![],
late_penalty: None,
bonus_criteria: vec![],
},
}
}
#[test]
fn test_grade_submission_empty_assignment() {
let mut engine = GradingEngine::new();
let assignment = make_simple_assignment(vec![], None);
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.is_valid);
assert!(report.task_grades.is_empty());
assert_eq!(report.originality_score, 100.0);
}
#[test]
fn test_grade_submission_with_task() {
let mut engine = GradingEngine::new();
let task = Task {
id: "task_1".to_string(),
description: "Add two numbers".to_string(),
points: 10,
test_cases: vec![TestCase {
input: "1 + 1".to_string(),
expected: ExpectedBehavior::ExactOutput("2".to_string()),
points: 10,
timeout_ms: 5000,
}],
hidden_cases: vec![],
requirements: vec![],
};
let assignment = make_simple_assignment(vec![task], None);
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.is_valid);
assert_eq!(report.task_grades.len(), 1);
}
#[test]
fn test_grade_submission_with_performance_constraints() {
let mut engine = GradingEngine::new();
let perf = PerformanceConstraints {
max_cpu_ms: 1000,
max_heap_mb: 100,
complexity_bound: "O(n)".to_string(),
};
let assignment = make_simple_assignment(vec![], Some(perf));
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.is_valid);
assert!(report.performance_score >= 0.0);
}
#[test]
fn test_grade_submission_tampered_session() {
let mut engine = GradingEngine::new();
let assignment = make_simple_assignment(vec![], None);
let timeline = vec![
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 5000,
event: Event::Input {
text: "1".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(2),
timestamp_ns: 1000, event: Event::Input {
text: "2".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
];
let session = make_repl_session(timeline);
let report = engine.grade_submission(&assignment, &session);
assert!(!report.is_valid);
assert!(report
.violations
.iter()
.any(|v| v.contains("integrity")));
}
#[test]
fn test_grade_submission_with_hidden_cases() {
let mut engine = GradingEngine::new();
let task = Task {
id: "task_1".to_string(),
description: "Test hidden cases".to_string(),
points: 20,
test_cases: vec![TestCase {
input: "10".to_string(),
expected: ExpectedBehavior::ExactOutput("10".to_string()),
points: 10,
timeout_ms: 5000,
}],
hidden_cases: vec![TestCase {
input: "20".to_string(),
expected: ExpectedBehavior::ExactOutput("20".to_string()),
points: 10,
timeout_ms: 5000,
}],
requirements: vec![Requirement::TypeSafe],
};
let assignment = make_simple_assignment(vec![task], None);
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.is_valid);
assert_eq!(report.task_grades.len(), 1);
}
#[test]
fn test_grade_submission_with_rubric_categories() {
let mut engine = GradingEngine::new();
let assignment = Assignment {
id: "rubric_test".to_string(),
title: "Rubric Test".to_string(),
description: "Test rubric evaluation".to_string(),
setup: AssignmentSetup {
prelude_code: vec![],
provided_functions: HashMap::new(),
immutable_bindings: HashSet::new(),
},
tasks: vec![],
constraints: AssignmentConstraints {
max_time_ms: 5000,
max_memory_mb: 100,
allowed_imports: vec![],
forbidden_keywords: vec![],
performance: None,
},
rubric: GradingRubric {
categories: vec![RubricCategory {
name: "Code Quality".to_string(),
weight: 1.0,
criteria: vec![Criterion {
description: "All tests pass".to_string(),
max_points: 100,
evaluation: CriterionEvaluation::Automatic(
AutomaticCheck::TestsPassed,
),
}],
}],
late_penalty: None,
bonus_criteria: vec![],
},
};
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.is_valid);
assert!(report.rubric_score >= 0.0);
}
#[test]
fn test_grade_submission_final_grade_calculated() {
let mut engine = GradingEngine::new();
let assignment = make_simple_assignment(vec![], None);
let session = make_repl_session(vec![]);
let report = engine.grade_submission(&assignment, &session);
assert!(report.final_grade >= 0.0);
}
#[test]
fn test_verify_no_tampering_valid_session() {
let engine = GradingEngine::new();
let session = make_repl_session(vec![
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 1000,
event: Event::Input {
text: "a".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(2),
timestamp_ns: 2000,
event: Event::Input {
text: "b".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
]);
assert!(engine.verify_no_tampering(&session));
}
#[test]
fn test_verify_no_tampering_backward_timestamps() {
let engine = GradingEngine::new();
let session = make_repl_session(vec![
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(1),
timestamp_ns: 3000,
event: Event::Input {
text: "a".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
crate::runtime::replay::TimestampedEvent {
id: crate::runtime::replay::EventId(2),
timestamp_ns: 1000,
event: Event::Input {
text: "b".to_string(),
mode: crate::runtime::replay::InputMode::Interactive,
},
causality: vec![],
},
]);
assert!(!engine.verify_no_tampering(&session));
}