package classes_test import ( "os" "path/filepath" "testing" "git.golem15.com/golem15/sm-quizzes-plugin/classes" "git.golem15.com/golem15/sm-quizzes-plugin/models" "git.golem15.com/golem15/summercms/modules/compass" ) func boolPtr(b bool) *bool { return &b } func TestScore(t *testing.T) { none := classes.ScoringConfig{} partial := classes.ScoringConfig{MultiplePartial: true} penalty := classes.ScoringConfig{PenaltyWrong: 0.25} both := classes.ScoringConfig{MultiplePartial: true, PenaltyWrong: 0.5} single := func(chosen ...uint) classes.ScoreInput { return classes.ScoreInput{Type: models.TypeSingle, Points: 2, CorrectIDs: []uint{10}, ChosenIDs: chosen} } multi := func(chosen ...uint) classes.ScoreInput { return classes.ScoreInput{Type: models.TypeMultiple, Points: 4, CorrectIDs: []uint{1, 2, 3, 4}, ChosenIDs: chosen} } self := func(known *bool) classes.ScoreInput { return classes.ScoreInput{Type: models.TypeSelfCheck, Points: 3, Known: known} } cases := []struct { name string in classes.ScoreInput cfg classes.ScoringConfig correct bool points float64 }{ {"single right", single(10), none, true, 2}, {"single wrong", single(11), none, false, 0}, {"single two chosen", single(10, 11), none, false, 0}, {"single duplicate id counts once", single(10, 10), none, true, 2}, {"single empty", single(), none, false, 0}, {"single wrong with penalty", single(11), penalty, false, -0.5}, {"single empty never penalised", single(), penalty, false, 0}, {"multiple exact", multi(4, 3, 2, 1), none, true, 4}, {"multiple subset without partial", multi(1, 2), none, false, 0}, {"multiple subset with partial", multi(1, 2), partial, false, 2}, {"multiple partial minus wrong", multi(1, 2, 3, 9), partial, false, 2}, {"multiple partial floored at 0", multi(1, 8, 9), partial, false, 0}, {"multiple superset with partial", multi(1, 2, 3, 4, 9), partial, false, 3}, {"multiple wrong with penalty", multi(9), penalty, false, -1}, {"multiple partial earns, no penalty", multi(1), both, false, 1}, {"multiple partial nothing, penalty", multi(9), both, false, -2}, {"self_check known", self(boolPtr(true)), none, true, 3}, {"self_check unknown", self(boolPtr(false)), none, false, 0}, {"self_check unknown never penalised", self(boolPtr(false)), penalty, false, 0}, {"self_check unanswered", self(nil), none, false, 0}, {"unknown type", classes.ScoreInput{Type: "essay", Points: 5}, none, false, 0}, {"negative points treated as 0", classes.ScoreInput{Type: models.TypeSingle, Points: -1, CorrectIDs: []uint{1}, ChosenIDs: []uint{1}}, none, true, 0}, {"multiple without correct answers", classes.ScoreInput{Type: models.TypeMultiple, Points: 1, ChosenIDs: []uint{1}}, partial, false, 0}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := classes.Score(tc.in, tc.cfg) if got.IsCorrect != tc.correct || got.Points != tc.points { t.Errorf("Score = %+v, want correct=%v points=%v", got, tc.correct, tc.points) } }) } } func TestTotals(t *testing.T) { cases := []struct { points []float64 max int score, pct float64 }{ {[]float64{1, 1, 0}, 3, 2, 66.67}, {[]float64{-1, -0.5}, 2, 0, 0}, {[]float64{2, -0.5}, 4, 1.5, 37.5}, {nil, 0, 0, 0}, {[]float64{1}, 1, 1, 100}, } for _, tc := range cases { score, pct := classes.Totals(tc.points, tc.max) if score != tc.score || pct != tc.pct { t.Errorf("Totals(%v, %d) = %v, %v; want %v, %v", tc.points, tc.max, score, pct, tc.score, tc.pct) } } } func TestScoringConfigFrom(t *testing.T) { if got := classes.ScoringConfigFrom(nil); got != (classes.ScoringConfig{}) { t.Errorf("nil config = %+v, want defaults", got) } open := func(yaml string) *compass.Config { t.Helper() dir := t.TempDir() if yaml != "" { if err := os.WriteFile(filepath.Join(dir, "golem15.yaml"), []byte(yaml), 0o644); err != nil { t.Fatal(err) } } cfg, err := compass.Open(compass.Options{Dir: dir, Env: "testing", Environ: []string{}}) if err != nil { t.Fatal(err) } return cfg } cfg := open("") if got := classes.ScoringConfigFrom(cfg); got != (classes.ScoringConfig{}) { t.Errorf("empty config = %+v, want defaults", got) } for _, tc := range []struct { value any want float64 }{{0.25, 0.25}, {1, 1}, {"0.5", 0.5}, {3, 1}, {-2, 0}, {"x", 0}} { if err := cfg.Set("golem15.quizzes.scoring.penalty_wrong", tc.value); err != nil { t.Fatal(err) } if err := cfg.Set("golem15.quizzes.scoring.multiple_partial", true); err != nil { t.Fatal(err) } got := classes.ScoringConfigFrom(cfg) if !got.MultiplePartial || got.PenaltyWrong != tc.want { t.Errorf("penalty_wrong %v: %+v, want partial and %v", tc.value, got, tc.want) } } }