Files
sm-quizzes-plugin/classes/grading_test.go
Jakub Zych 80ea79ee3e feat: grade open answers through a grader registry in the background
- graders registry collected at boot from every HasGraders plugin,
  manual first; a duplicate or reserved code fails boot
- grading job on queue quizzes_grading with three attempts; a stale
  or reviewed answer is never touched; the last failure marks it failed
- learning answers enqueue at once, tests at close; a grade
  recalculates a finished attempt and fires AnswerGradedEvent after
  commit
2026-11-09 17:51:55 +01:00

75 lines
2.6 KiB
Go

package classes_test
import (
"context"
"errors"
"testing"
"git.golem15.com/golem15/sm-quizzes-plugin/classes"
"git.golem15.com/golem15/sm-quizzes-plugin/graders"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
)
type stubGrader struct{ code string }
func (g stubGrader) Code() string { return g.code }
func (g stubGrader) Label() string { return g.code }
func (g stubGrader) Grade(context.Context, graders.Input) (graders.Result, error) {
return graders.Result{}, nil
}
func TestGraderFor(t *testing.T) {
reg := graders.NewRegistry()
if err := reg.Add("acme.x", stubGrader{"x"}); err != nil {
t.Fatal(err)
}
if g, ok := classes.GraderFor(reg, &models.Quiz{Grader: "x"}); !ok || g.Code() != "x" {
t.Errorf("GraderFor(x) = %v, %v", g, ok)
}
for _, code := range []string{graders.Manual, "unknown", ""} {
if _, ok := classes.GraderFor(reg, &models.Quiz{Grader: code}); ok {
t.Errorf("GraderFor(%q) = true, want false", code)
}
}
if _, ok := classes.GraderFor(nil, &models.Quiz{Grader: "x"}); ok {
t.Error("GraderFor(nil registry) = true")
}
if _, ok := classes.GraderFor(reg, nil); ok {
t.Error("GraderFor(nil quiz) = true")
}
}
func TestNextGrading(t *testing.T) {
str := func(s string) *string { return &s }
boom := errors.New("boom")
cases := []struct {
name string
status *string
rowSeq int
jobSeq int
attempts int
err error
want classes.GradingStep
}{
{"nil status", nil, 1, 1, 1, nil, classes.GradingSkip},
{"graded", str(models.GradingGraded), 1, 1, 1, nil, classes.GradingSkip},
{"failed", str(models.GradingFailed), 1, 1, 1, nil, classes.GradingSkip},
{"reviewed", str(models.GradingReviewed), 1, 1, 1, nil, classes.GradingSkip},
{"stale seq", str(models.GradingPending), 2, 1, 1, nil, classes.GradingSkip},
{"stale seq with error", str(models.GradingPending), 2, 1, 3, boom, classes.GradingSkip},
{"store", str(models.GradingPending), 1, 1, 1, nil, classes.GradingStore},
{"retry 1", str(models.GradingPending), 1, 1, 1, boom, classes.GradingRetry},
{"retry 2", str(models.GradingPending), 1, 1, 2, boom, classes.GradingRetry},
{"fail at max", str(models.GradingPending), 1, 1, classes.GradeMaxAttempts, boom, classes.GradingFail},
{"fail past max", str(models.GradingPending), 1, 1, classes.GradeMaxAttempts + 1, boom, classes.GradingFail},
}
for _, tc := range cases {
if got := classes.NextGrading(tc.status, tc.rowSeq, tc.jobSeq, tc.attempts, tc.err); got != tc.want {
t.Errorf("%s: NextGrading = %v, want %v", tc.name, got, tc.want)
}
}
if classes.GradeMaxAttempts != 3 {
t.Errorf("GradeMaxAttempts = %d, want 3", classes.GradeMaxAttempts)
}
}