package model import ( "encoding/json" "testing" "time" ) func TestTask_TableName(t *testing.T) { task := Task{} if got := task.TableName(); got != "hpc_tasks" { t.Errorf("Task.TableName() = %q, want %q", got, "hpc_tasks") } } func TestTask_JSONRoundTrip(t *testing.T) { now := time.Now().UTC().Truncate(time.Second) jobID := int32(42) cpus := int32(16) memPerNode := int64(32768) timeLimit := int32(60) task := Task{ ID: 1, TaskName: "test task", AppID: 10, AppName: "GROMACS", Status: TaskStatusRunning, CurrentStep: TaskStepSubmitting, RetryCount: 1, Values: json.RawMessage(`{"np":"4"}`), InputFileIDs: json.RawMessage(`[1,2,3]`), Script: "#!/bin/bash", SlurmJobID: &jobID, WorkDir: "/data/work", Partition: "gpu", ErrorMessage: "", UserID: "user1", SubmittedAt: now, StartedAt: &now, FinishedAt: nil, CreatedAt: now, UpdatedAt: now, Cpus: &cpus, MemoryPerNode: &memPerNode, TimeLimit: &timeLimit, } data, err := json.Marshal(task) if err != nil { t.Fatalf("marshal task: %v", err) } var got Task if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("unmarshal task: %v", err) } if got.ID != task.ID { t.Errorf("ID = %d, want %d", got.ID, task.ID) } if got.TaskName != task.TaskName { t.Errorf("TaskName = %q, want %q", got.TaskName, task.TaskName) } if got.Status != task.Status { t.Errorf("Status = %q, want %q", got.Status, task.Status) } if got.CurrentStep != task.CurrentStep { t.Errorf("CurrentStep = %q, want %q", got.CurrentStep, task.CurrentStep) } if got.RetryCount != task.RetryCount { t.Errorf("RetryCount = %d, want %d", got.RetryCount, task.RetryCount) } if got.SlurmJobID == nil || *got.SlurmJobID != jobID { t.Errorf("SlurmJobID = %v, want %d", got.SlurmJobID, jobID) } if got.UserID != task.UserID { t.Errorf("UserID = %q, want %q", got.UserID, task.UserID) } if string(got.Values) != string(task.Values) { t.Errorf("Values = %s, want %s", got.Values, task.Values) } if string(got.InputFileIDs) != string(task.InputFileIDs) { t.Errorf("InputFileIDs = %s, want %s", got.InputFileIDs, task.InputFileIDs) } if got.FinishedAt != nil { t.Errorf("FinishedAt = %v, want nil", got.FinishedAt) } if got.Partition != task.Partition { t.Errorf("Partition = %q, want %q", got.Partition, task.Partition) } if got.Cpus == nil || *got.Cpus != cpus { t.Errorf("Cpus = %v, want %d", got.Cpus, cpus) } if got.MemoryPerNode == nil || *got.MemoryPerNode != memPerNode { t.Errorf("MemoryPerNode = %v, want %d", got.MemoryPerNode, memPerNode) } if got.TimeLimit == nil || *got.TimeLimit != timeLimit { t.Errorf("TimeLimit = %v, want %d", got.TimeLimit, timeLimit) } } func TestCreateTaskRequest_JSONBinding(t *testing.T) { payload := `{"app_id":5,"task_name":"my task","values":{"np":"8"},"file_ids":[10,20]}` var req CreateTaskRequest if err := json.Unmarshal([]byte(payload), &req); err != nil { t.Fatalf("unmarshal CreateTaskRequest: %v", err) } if req.AppID != 5 { t.Errorf("AppID = %d, want 5", req.AppID) } if req.TaskName != "my task" { t.Errorf("TaskName = %q, want %q", req.TaskName, "my task") } if v, ok := req.Values["np"]; !ok || v != "8" { t.Errorf("Values[\"np\"] = %q, want %q", v, "8") } if len(req.InputFileIDs) != 2 || req.InputFileIDs[0] != 10 || req.InputFileIDs[1] != 20 { t.Errorf("InputFileIDs = %v, want [10 20]", req.InputFileIDs) } } func TestTaskResponse_JSONSerialization(t *testing.T) { cpus := int32(8) memPerNode := int64(16384) memPerCpu := int64(4096) timeLimit := int32(120) qos := "high" jobName := "gmx-md" nodes := "2" tasks := int32(4) cpusPerTask := int32(2) constraints := "haswell" reservation := "my-resv" account := "proj-123" nice := int32(100) mailType := "END" mailUser := "user@example.com" stdout := "/tmp/%j.out" stderr := "/tmp/%j.err" stdin := "/dev/null" reqNodes := "node[01-03]" exclNodes := "node04" beginTime := int64(1700000000) deadline := int64(1700086400) array := "1-10" dependency := "afterok:12345" requeue := true killOnNodeFail := true resp := TaskResponse{ ID: 1, TaskName: "test", AppID: 10, AppName: "GROMACS", Status: "running", CurrentStep: "submitting", RetryCount: 0, SlurmJobID: nil, WorkDir: "/data", ErrorMessage: "", CreatedAt: time.Now().UTC().Truncate(time.Second), UpdatedAt: time.Now().UTC().Truncate(time.Second), Partition: "gpu", Cpus: &cpus, MemoryPerNode: &memPerNode, MemoryPerCpu: &memPerCpu, TimeLimit: &timeLimit, QOS: &qos, JobName: &jobName, Nodes: &nodes, Tasks: &tasks, CpusPerTask: &cpusPerTask, Constraints: &constraints, Reservation: &reservation, Account: &account, Nice: &nice, MailType: &mailType, MailUser: &mailUser, StandardOutput: &stdout, StandardError: &stderr, StandardInput: &stdin, RequiredNodes: &reqNodes, ExcludedNodes: &exclNodes, BeginTime: &beginTime, Deadline: &deadline, Array: &array, Dependency: &dependency, Requeue: &requeue, KillOnNodeFail: &killOnNodeFail, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("marshal TaskResponse: %v", err) } var m map[string]interface{} if err := json.Unmarshal(data, &m); err != nil { t.Fatalf("unmarshal to map: %v", err) } assertString(t, m, "partition", "gpu") assertFloat64(t, m, "cpus", float64(cpus)) assertFloat64(t, m, "memory_per_node", float64(memPerNode)) assertFloat64(t, m, "memory_per_cpu", float64(memPerCpu)) assertFloat64(t, m, "time_limit", float64(timeLimit)) assertString(t, m, "qos", qos) assertString(t, m, "job_name", jobName) assertString(t, m, "nodes", nodes) assertFloat64(t, m, "tasks", float64(tasks)) assertFloat64(t, m, "cpus_per_task", float64(cpusPerTask)) assertString(t, m, "constraints", constraints) assertString(t, m, "reservation", reservation) assertString(t, m, "account", account) assertFloat64(t, m, "nice", float64(nice)) assertString(t, m, "mail_type", mailType) assertString(t, m, "mail_user", mailUser) assertString(t, m, "standard_output", stdout) assertString(t, m, "standard_error", stderr) assertString(t, m, "standard_input", stdin) assertString(t, m, "required_nodes", reqNodes) assertString(t, m, "excluded_nodes", exclNodes) assertFloat64(t, m, "begin_time", float64(beginTime)) assertFloat64(t, m, "deadline", float64(deadline)) assertString(t, m, "array", array) assertString(t, m, "dependency", dependency) assertBool(t, m, "requeue", requeue) assertBool(t, m, "kill_on_node_fail", killOnNodeFail) } func TestCreateTaskRequest_SchedulingFields(t *testing.T) { payload := `{ "app_id": 5, "partition": "gpu", "cpus": 16, "memory_per_node": 32768, "memory_per_cpu": 4096, "time_limit": 120, "qos": "high", "job_name": "gmx-sim", "nodes": "2", "tasks": 4, "cpus_per_task": 2, "constraints": "haswell", "reservation": "my-resv", "account": "proj-123", "nice": 50, "mail_type": "ALL", "mail_user": "user@example.com", "standard_output": "/tmp/%j.out", "standard_error": "/tmp/%j.err", "standard_input": "/dev/null", "required_nodes": "node[01-03]", "excluded_nodes": "node04", "begin_time": 1700000000, "deadline": 1700086400, "array": "1-10", "dependency": "afterok:12345", "requeue": true, "kill_on_node_fail": false }` var req CreateTaskRequest if err := json.Unmarshal([]byte(payload), &req); err != nil { t.Fatalf("unmarshal CreateTaskRequest: %v", err) } if req.AppID != 5 { t.Errorf("AppID = %d, want 5", req.AppID) } assertPtrString(t, req.Partition, "gpu") assertPtrInt32(t, req.Cpus, 16) assertPtrInt64(t, req.MemoryPerNode, 32768) assertPtrInt64(t, req.MemoryPerCpu, 4096) assertPtrInt32(t, req.TimeLimit, 120) assertPtrString(t, req.QOS, "high") assertPtrString(t, req.JobName, "gmx-sim") assertPtrString(t, req.Nodes, "2") assertPtrInt32(t, req.Tasks, 4) assertPtrInt32(t, req.CpusPerTask, 2) assertPtrString(t, req.Constraints, "haswell") assertPtrString(t, req.Reservation, "my-resv") assertPtrString(t, req.Account, "proj-123") assertPtrInt32(t, req.Nice, 50) assertPtrString(t, req.MailType, "ALL") assertPtrString(t, req.MailUser, "user@example.com") assertPtrString(t, req.StandardOutput, "/tmp/%j.out") assertPtrString(t, req.StandardError, "/tmp/%j.err") assertPtrString(t, req.StandardInput, "/dev/null") assertPtrString(t, req.RequiredNodes, "node[01-03]") assertPtrString(t, req.ExcludedNodes, "node04") assertPtrInt64(t, req.BeginTime, 1700000000) assertPtrInt64(t, req.Deadline, 1700086400) assertPtrString(t, req.Array, "1-10") assertPtrString(t, req.Dependency, "afterok:12345") assertPtrBool(t, req.Requeue, true) assertPtrBool(t, req.KillOnNodeFail, false) } func TestCreateTaskRequest_BackwardCompat(t *testing.T) { payload := `{"app_id": 1}` var req CreateTaskRequest if err := json.Unmarshal([]byte(payload), &req); err != nil { t.Fatalf("unmarshal minimal CreateTaskRequest: %v", err) } if req.AppID != 1 { t.Errorf("AppID = %d, want 1", req.AppID) } if req.Partition != nil { t.Errorf("Partition = %v, want nil", req.Partition) } if req.Cpus != nil { t.Errorf("Cpus = %v, want nil", req.Cpus) } if req.MemoryPerNode != nil { t.Errorf("MemoryPerNode = %v, want nil", req.MemoryPerNode) } if req.MemoryPerCpu != nil { t.Errorf("MemoryPerCpu = %v, want nil", req.MemoryPerCpu) } if req.TimeLimit != nil { t.Errorf("TimeLimit = %v, want nil", req.TimeLimit) } if req.QOS != nil { t.Errorf("QOS = %v, want nil", req.QOS) } if req.Nodes != nil { t.Errorf("Nodes = %v, want nil", req.Nodes) } if req.Tasks != nil { t.Errorf("Tasks = %v, want nil", req.Tasks) } if req.Requeue != nil { t.Errorf("Requeue = %v, want nil", req.Requeue) } if req.KillOnNodeFail != nil { t.Errorf("KillOnNodeFail = %v, want nil", req.KillOnNodeFail) } } func assertString(t *testing.T, m map[string]interface{}, key, want string) { t.Helper() got, ok := m[key].(string) if !ok { t.Errorf("%s: not a string, got %T", key, m[key]) return } if got != want { t.Errorf("%s = %q, want %q", key, got, want) } } func assertFloat64(t *testing.T, m map[string]interface{}, key string, want float64) { t.Helper() got, ok := m[key].(float64) if !ok { t.Errorf("%s: not a float64, got %T", key, m[key]) return } if got != want { t.Errorf("%s = %v, want %v", key, got, want) } } func assertBool(t *testing.T, m map[string]interface{}, key string, want bool) { t.Helper() got, ok := m[key].(bool) if !ok { t.Errorf("%s: not a bool, got %T", key, m[key]) return } if got != want { t.Errorf("%s = %v, want %v", key, got, want) } } func assertPtrString(t *testing.T, got *string, want string) { t.Helper() if got == nil { t.Errorf("got nil, want %q", want) return } if *got != want { t.Errorf("got %q, want %q", *got, want) } } func assertPtrInt32(t *testing.T, got *int32, want int32) { t.Helper() if got == nil { t.Errorf("got nil, want %d", want) return } if *got != want { t.Errorf("got %d, want %d", *got, want) } } func assertPtrInt64(t *testing.T, got *int64, want int64) { t.Helper() if got == nil { t.Errorf("got nil, want %d", want) return } if *got != want { t.Errorf("got %d, want %d", *got, want) } } func assertPtrBool(t *testing.T, got *bool, want bool) { t.Helper() if got == nil { t.Errorf("got nil, want %v", want) return } if *got != want { t.Errorf("got %v, want %v", *got, want) } }