133 lines
4.7 KiB
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)
|
|
}
|
|
}
|
|
}
|