Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
423 lines
12 KiB
Go
423 lines
12 KiB
Go
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)
|
|
}
|
|
}
|