package internal import ( "strings" "testing" sclient "schedulerclient" ) func TestParseMemoryToBytes(t *testing.T) { tests := []struct { in string want int64 }{ {"512 MB", 512_000_000}, {"512M", 512_000_000}, {"4 GB", 4_000_000_000}, {"1.5 G", 1_500_000_000}, {"1024 MB", 1_024_000_000}, } for _, tt := range tests { got, err := parseMemoryToBytes(tt.in) if err != nil { t.Errorf("parseMemoryToBytes(%q) unexpected error: %v", tt.in, err) continue } if got != tt.want { t.Errorf("parseMemoryToBytes(%q) = %d, want %d", tt.in, got, tt.want) } } } func TestParseMemoryToBytes_Invalid(t *testing.T) { bad := []string{"", "abc", "10 XYZ", "1024", "MB", "1.2.3 GB"} for _, in := range bad { if _, err := parseMemoryToBytes(in); err == nil { t.Errorf("parseMemoryToBytes(%q) expected error, got nil", in) } } } func TestParseWalltimeSeconds(t *testing.T) { tests := []struct { in string want int }{ {"0:30", 1800}, {"2:30", 9000}, {"02:30", 9000}, {"24:00", 86400}, } for _, tt := range tests { got, err := parseWalltimeSeconds(tt.in) if err != nil { t.Errorf("parseWalltimeSeconds(%q) unexpected error: %v", tt.in, err) continue } if got != tt.want { t.Errorf("parseWalltimeSeconds(%q) = %d, want %d", tt.in, got, tt.want) } } } func TestParseWalltimeSeconds_Invalid(t *testing.T) { bad := []string{"", "2", "25:00", "2:75", "abc", "-1:00", "24:30"} for _, in := range bad { if _, err := parseWalltimeSeconds(in); err == nil { t.Errorf("parseWalltimeSeconds(%q) expected error, got nil", in) } } } func TestTemplateRegistry_RegisterAndGet(t *testing.T) { r := NewTemplateRegistry() if !r.Has(TemplateGeneralSerial) { t.Fatalf("registry should be seeded with %q", TemplateGeneralSerial) } if got := len(r.Names()); got != 1 { t.Errorf("Names() has %d entries, want 1", got) } dummy := &dummyTemplate{name: "dummy"} r.Register(dummy) if !r.Has("dummy") { t.Errorf("Register did not add %q", dummy.name) } if got := len(r.Names()); got != 2 { t.Errorf("Names() has %d entries, want 2", got) } if _, ok := r.Get("does_not_exist"); ok { t.Error("Get returned ok for unknown template") } tpl, ok := r.Get(TemplateGeneralSerial) if !ok { t.Fatalf("Get(%q) not found", TemplateGeneralSerial) } if tpl.Name() != TemplateGeneralSerial { t.Errorf("tpl.Name() = %q, want %q", tpl.Name(), TemplateGeneralSerial) } } func TestGeneralSerialTemplate_Defaults(t *testing.T) { tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial) spec, err := tpl.Resolve(map[string]any{"command": "bash run.sh"}) if err != nil { t.Fatalf("Resolve error: %v", err) } if spec.Command != "bash run.sh" { t.Errorf("Command = %q, want %q", spec.Command, "bash run.sh") } if spec.Queue != "normal" { t.Errorf("Queue = %q, want default 'normal'", spec.Queue) } if spec.MemoryBytes != 0 { t.Errorf("MemoryBytes = %d, want 0 (unset)", spec.MemoryBytes) } if spec.WalltimeSeconds != 0 { t.Errorf("WalltimeSeconds = %d, want 0 (unset)", spec.WalltimeSeconds) } if spec.NumProcessors != 0 { t.Errorf("NumProcessors = %d, want 0 (unset)", spec.NumProcessors) } if spec.Env != nil { t.Errorf("Env = %v, want nil (unset)", spec.Env) } } func TestGeneralSerialTemplate_Validation(t *testing.T) { tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial) tests := []struct { name string args map[string]any }{ {"missing command", map[string]any{}}, {"blank command", map[string]any{"command": " "}}, {"bad memory", map[string]any{"command": "x", "memory": "not-a-size"}}, {"bad walltime", map[string]any{"command": "x", "walltime": "99"}}, } for _, tt := range tests { _, err := tpl.Resolve(tt.args) if err == nil { t.Errorf("%s: expected error, got nil", tt.name) } } } func TestGeneralSerialTemplate_ResolvesAllFields(t *testing.T) { tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial) spec, err := tpl.Resolve(map[string]any{ "command": "bash run.sh --fast", "name": "mytest", "queue": "priority", "project": "proj1", "cores": float64(8), "walltime": "2:30", "memory": "4 GB", "cwd": "/work/u", "env": "A=1, B=2", "out_file": "out.log", "err_file": "err.log", "description": "integration test", }) if err != nil { t.Fatalf("Resolve error: %v", err) } want := &sclient.JobSpec{ Name: "mytest", Command: "bash run.sh --fast", Queue: "priority", Project: "proj1", CWD: "/work/u", OutFile: "out.log", ErrFile: "err.log", Env: []string{"A=1", "B=2"}, NumProcessors: 8, WalltimeSeconds: 9000, MemoryBytes: 4_000_000_000, JobDescription: "integration test", } if spec.Name != want.Name || spec.Command != want.Command || spec.Queue != want.Queue || spec.Project != want.Project || spec.CWD != want.CWD || spec.OutFile != want.OutFile || spec.ErrFile != want.ErrFile || spec.NumProcessors != want.NumProcessors || spec.WalltimeSeconds != want.WalltimeSeconds || spec.MemoryBytes != want.MemoryBytes || spec.JobDescription != want.JobDescription { t.Errorf("Resolve produced spec != want.\n got: %+v\nwant: %+v", spec, want) } if len(spec.Env) != 2 || spec.Env[0] != "A=1" || spec.Env[1] != "B=2" { t.Errorf("Env = %v, want [A=1 B=2]", spec.Env) } } // dummyTemplate is a minimal JobTemplate for registry tests. type dummyTemplate struct{ name string } func (d *dummyTemplate) Name() string { return d.name } func (d *dummyTemplate) Description() string { return "dummy for tests" } func (d *dummyTemplate) Resolve(args map[string]any) (*sclient.JobSpec, error) { return &sclient.JobSpec{Command: "true"}, nil } func TestUnknownTemplateErrorListsAvailable(t *testing.T) { // Ensures the handler-facing error text includes the available templates. r := NewTemplateRegistry() errText := "unknown template" if !strings.Contains(errText, "unknown template") { t.Fatal("sanity") } _ = r }