module Domain.Tests.GradingTests open System open Xunit open Domain let private optA = { Id = OptionId(Guid.NewGuid()); Text = "A" } let private optB = { Id = OptionId(Guid.NewGuid()); Text = "B" } let private optC = { Id = OptionId(Guid.NewGuid()); Text = "C" } let private newQuestionId () = QuestionId(Guid.NewGuid()) [] let ``single choice: correct option awards full points`` () = let qId = newQuestionId () let qType = SingleChoice([ optA; optB; optC ], optB.Id) let grade = Grading.gradeResponse qId qType 10.0 (SingleChoiceResponse(Some optB.Id)) Assert.True grade.IsCorrect Assert.Equal(10.0, grade.PointsAwarded) [] let ``single choice: wrong option awards zero`` () = let qId = newQuestionId () let qType = SingleChoice([ optA; optB; optC ], optB.Id) let grade = Grading.gradeResponse qId qType 10.0 (SingleChoiceResponse(Some optA.Id)) Assert.False grade.IsCorrect Assert.Equal(0.0, grade.PointsAwarded) [] let ``single choice: unanswered awards zero`` () = let qId = newQuestionId () let qType = SingleChoice([ optA; optB ], optB.Id) let grade = Grading.gradeResponse qId qType 10.0 (SingleChoiceResponse None) Assert.False grade.IsCorrect [] let ``multiple choice: exact set match is correct`` () = let qId = newQuestionId () let correct = Set.ofList [ optA.Id; optC.Id ] let qType = MultipleChoice([ optA; optB; optC ], correct) let grade = Grading.gradeResponse qId qType 5.0 (MultipleChoiceResponse(Set.ofList [ optA.Id; optC.Id ])) Assert.True grade.IsCorrect [] let ``multiple choice: partial selection is incorrect`` () = let qId = newQuestionId () let correct = Set.ofList [ optA.Id; optC.Id ] let qType = MultipleChoice([ optA; optB; optC ], correct) let grade = Grading.gradeResponse qId qType 5.0 (MultipleChoiceResponse(Set.ofList [ optA.Id ])) Assert.False grade.IsCorrect [] let ``true false: matching answer is correct`` () = let qId = newQuestionId () let grade = Grading.gradeResponse qId (TrueFalse true) 2.0 (TrueFalseResponse(Some true)) Assert.True grade.IsCorrect [] let ``short answer: case-insensitive match by default`` () = let qId = newQuestionId () let qType = ShortAnswer([ "Paris" ], caseSensitive = false) let grade = Grading.gradeResponse qId qType 3.0 (ShortAnswerResponse " paris ") Assert.True grade.IsCorrect [] let ``short answer: case-sensitive rejects mismatched case`` () = let qId = newQuestionId () let qType = ShortAnswer([ "Paris" ], caseSensitive = true) let grade = Grading.gradeResponse qId qType 3.0 (ShortAnswerResponse "paris") Assert.False grade.IsCorrect [] let ``numeric: within tolerance is correct`` () = let qId = newQuestionId () let qType = Numeric(3.14, 0.01) let grade = Grading.gradeResponse qId qType 4.0 (NumericResponse(Some 3.145)) Assert.True grade.IsCorrect [] let ``numeric: outside tolerance is incorrect`` () = let qId = newQuestionId () let qType = Numeric(3.14, 0.01) let grade = Grading.gradeResponse qId qType 4.0 (NumericResponse(Some 3.2)) Assert.False grade.IsCorrect let private sampleQuiz (questionRefs: QuizQuestionRef list) : Quiz = { Id = QuizId(Guid.NewGuid()) OwnerId = UserId(Guid.NewGuid()) Title = "Sample quiz" Description = "" TimeLimit = None MaxAttempts = None GradingMethod = HighestAttempt ShuffleQuestions = false ShuffleAnswers = false OpenFrom = None OpenTo = None PassingScore = None QuestionSources = questionRefs |> List.map FixedQuestion AssignedStudentIds = Set.empty } [] let ``gradeAttempt: unanswered questions score zero without crashing`` () = let qId = newQuestionId () let question = { Id = qId TopicId = TopicId(Guid.NewGuid()) Text = "2 + 2 = ?" Points = 5.0 Type = Numeric(4.0, 0.0) } let refs = [ { QuestionId = qId; Points = 5.0; Order = 0 } ] let quiz = sampleQuiz refs let questions = Map.ofList [ qId, question ] let attempt = Attempt.start (AttemptId(Guid.NewGuid())) quiz refs (UserId(Guid.NewGuid())) DateTimeOffset.UtcNow let graded = Grading.gradeAttempt questions attempt Assert.Equal(Some 0.0, graded.Score) Assert.Equal(Graded, graded.State) [] let ``gradeAttempt: sums points across multiple questions`` () = let q1Id, q2Id = newQuestionId (), newQuestionId () let q1 = { Id = q1Id TopicId = TopicId(Guid.NewGuid()) Text = "Is the sky blue?" Points = 1.0 Type = TrueFalse true } let q2 = { Id = q2Id TopicId = TopicId(Guid.NewGuid()) Text = "Capital of France?" Points = 1.0 Type = ShortAnswer([ "Paris" ], false) } let refs = [ { QuestionId = q1Id; Points = 3.0; Order = 0 } { QuestionId = q2Id; Points = 7.0; Order = 1 } ] let quiz = sampleQuiz refs let questions = Map.ofList [ q1Id, q1; q2Id, q2 ] let attempt = Attempt.start (AttemptId(Guid.NewGuid())) quiz refs (UserId(Guid.NewGuid())) DateTimeOffset.UtcNow |> Attempt.recordResponse q1Id (TrueFalseResponse(Some true)) |> Attempt.recordResponse q2Id (ShortAnswerResponse "paris") let graded = Grading.gradeAttempt questions attempt Assert.Equal(Some 10.0, graded.Score) [] let ``applyGradingMethod: HighestAttempt picks the best score`` () = let mkAttempt score started = { Id = AttemptId(Guid.NewGuid()) QuizId = QuizId(Guid.NewGuid()) UserId = UserId(Guid.NewGuid()) StartedAt = started SubmittedAt = Some started State = Graded Questions = [] Responses = Map.empty Grades = Map.empty Score = Some score FocusLossCount = 0 } let now = DateTimeOffset.UtcNow let attempts = [ mkAttempt 4.0 now; mkAttempt 9.0 (now.AddMinutes 1.0); mkAttempt 6.0 (now.AddMinutes 2.0) ] let result = Grading.applyGradingMethod HighestAttempt attempts Assert.Equal(Some 9.0, result |> Option.bind (fun a -> a.Score)) [] let ``applyGradingMethod: no graded attempts returns None`` () = let result = Grading.applyGradingMethod HighestAttempt [] Assert.Equal(None, result)