Files
sm-quizzes-plugin/classes/scoring_test.go

133 lines
4.7 KiB
Go

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)
}
}
}