package backend import ( "apiserver/models" "testing" ) // TestJobCtrlDiscriminant is a regression test for bug-1490: backend.ControlJob // used to set xce_jobctrl_t.action to the models.JOB_* constant // (JOB_STOP=1/JOB_RESUME=2/JOB_KILL=3/JOB_CHKPNT=8), but that field is the // capnp JobControl union DISCRIMINANT (JobControl_u_siginfo=0, // JobControl_u_ckptoption=1). The controller's job_ctrl() interpreted the wrong // union branch (or none) and rejected every control request with -EINVAL, so // hold/release/cancel never reached the job. func TestJobCtrlDiscriminant(t *testing.T) { cases := []struct { name string action int want int }{ {"stop is a signal action", models.JOB_STOP, 0}, // JobControl_u_siginfo {"resume is a signal action", models.JOB_RESUME, 0}, {"kill is a signal action", models.JOB_KILL, 0}, {"checkpoint is a ckptoption action", models.JOB_CHKPNT, 1}, // JobControl_u_ckptoption } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := jobCtrlDiscriminant(tc.action); got != tc.want { t.Errorf("jobCtrlDiscriminant(%d) = %d, want %d", tc.action, got, tc.want) } }) } } func TestRawJobToModel_BasicFields(t *testing.T) { r := rawJobData{ jobID: 123, state: 2, // JOB_STAT_RUN runTime: 42, submit: &rawSubmitData{ queue: "normal", user: "alice", jobName: "myjob", minTasks: 1, maxTasks: 4, startInfo: &rawStartInfo{ cmdline: "sleep 60", }, }, } info := rawJobToModel(r) if info.Unique_JobID != 123 { t.Errorf("Unique_JobID: got %d, want 123", info.Unique_JobID) } if info.JobID != 123 { t.Errorf("JobID: got %d, want 123", info.JobID) } if info.QueueName != "normal" { t.Errorf("QueueName: got %q, want %q", info.QueueName, "normal") } if info.SubmitUser != "alice" { t.Errorf("SubmitUser: got %q, want %q", info.SubmitUser, "alice") } if info.JobName != "myjob" { t.Errorf("JobName: got %q, want %q", info.JobName, "myjob") } if info.Command != "sleep 60" { t.Errorf("Command: got %q, want %q", info.Command, "sleep 60") } if info.JobStatus != 2 { t.Errorf("JobStatus: got %d, want 2", info.JobStatus) } if info.NumProcessors != 1 { t.Errorf("NumProcessors: got %d, want 1", info.NumProcessors) } if info.MaxNumProcessors != 4 { t.Errorf("MaxNumProcessors: got %d, want 4", info.MaxNumProcessors) } if info.RunTime != 42 { t.Errorf("RunTime: got %d, want 42", info.RunTime) } } func TestRawJobToModel_NilSubmit(t *testing.T) { r := rawJobData{jobID: 99, state: 1 /* JOB_STAT_PEND */} info := rawJobToModel(r) if info.Unique_JobID != 99 { t.Errorf("Unique_JobID: got %d, want 99", info.Unique_JobID) } if info.QueueName != "" { t.Errorf("QueueName should be empty for nil submit, got %q", info.QueueName) } } func TestRawJobToModel_Timestamps(t *testing.T) { r := rawJobData{ jobID: 1, subTime: 1700000000, queueTime: 1700000010, startTime: 1700000020, finishTime: 1700000030, } info := rawJobToModel(r) if info.SubmitTime != 1700000000 { t.Errorf("SubmitTime: got %d, want 1700000000", info.SubmitTime) } if info.QueueTime != 1700000010 { t.Errorf("QueueTime: got %d, want 1700000010", info.QueueTime) } if info.StartTime != 1700000020 { t.Errorf("StartTime: got %d, want 1700000020", info.StartTime) } if info.EndTime != 1700000030 { t.Errorf("EndTime: got %d, want 1700000030", info.EndTime) } } func TestRawWorkerToModel_BasicFields(t *testing.T) { r := rawWorkerData{ name: "node01", status: 3, // XCE_WORKER_UNAVAIL hasAttrs: true, hostType: "linux", hostModel: "x86_64", cpuFactor: 1.5, maxCPUs: 16, maxMem: 65536, maxSwap: 32768, maxTmp: 1024, nDisks: 2, } info := rawWorkerToModel(r) if info.HostName != "node01" { t.Errorf("HostName: got %q, want %q", info.HostName, "node01") } if info.HostType != "linux" { t.Errorf("HostType: got %q, want %q", info.HostType, "linux") } if info.HostModel != "x86_64" { t.Errorf("HostModel: got %q, want %q", info.HostModel, "x86_64") } if info.NumCPUs != 16 { t.Errorf("NumCPUs: got %d, want 16", info.NumCPUs) } if info.MaxMem != 65536 { t.Errorf("MaxMem: got %d, want 65536", info.MaxMem) } if info.NumDisk != 2 { t.Errorf("NumDisk: got %d, want 2", info.NumDisk) } if info.HostStatus != 3 { t.Errorf("HostStatus: got %d, want 3", info.HostStatus) } } func TestRawWorkerToModel_NoAttrs(t *testing.T) { r := rawWorkerData{name: "node02", status: 1, hasAttrs: false} info := rawWorkerToModel(r) if info.HostName != "node02" { t.Errorf("HostName: got %q, want node02", info.HostName) } if info.HostType != "" { t.Errorf("HostType should be empty when hasAttrs=false, got %q", info.HostType) } if info.NumCPUs != 0 { t.Errorf("NumCPUs should be 0 when hasAttrs=false, got %d", info.NumCPUs) } } func TestRawQueueToModel_BasicFields(t *testing.T) { r := rawQueueData{ name: "normal", status: 1, // QUEUE_STAT_OPEN priority: 30, } info := rawQueueToModel(r) if info.QueueName != "normal" { t.Errorf("QueueName: got %q, want %q", info.QueueName, "normal") } if info.Priority != 30 { t.Errorf("Priority: got %d, want 30", info.Priority) } if info.QueueStatus != 1 { t.Errorf("QueueStatus: got %d, want 1", info.QueueStatus) } } func TestRawResourceToModel_BasicFields(t *testing.T) { r := rawResourceData{ name: "mem", desc: "memory resource", valType: 1, // XCE_VAL_NUMERIC flags: 4, // XCE_RESF_CONSUMABLE interval: 60, } info := rawResourceToModel(r) if info.ResourceName != "mem" { t.Errorf("ResourceName: got %q, want %q", info.ResourceName, "mem") } if info.Description != "memory resource" { t.Errorf("Description: got %q, want %q", info.Description, "memory resource") } if info.ValueType != 1 { t.Errorf("ValueType: got %d, want 1", info.ValueType) } if info.Flag != 4 { t.Errorf("Flag: got %d, want 4", info.Flag) } if info.Interval != 60 { t.Errorf("Interval: got %d, want 60", info.Interval) } } // makeJobID mirrors the C MAKE_JOBID macro in src/include/utils.h. // Used to construct known 64-bit job IDs for testing. func makeJobID(cluster, apiserver, timestamp, tag, sequence uint64) uint64 { return (cluster << clusterIDShift) | (apiserver << apiserverIDShift) | (timestamp << timestampShift) | (tag << tagShift) | sequence } func TestExtractBits(t *testing.T) { // 0b1111 shifted left by 4 = 0b11110000 = 240 id := uint64(0xF0) if got := extractBits(id, 4, 4); got != 0xF { t.Errorf("extractBits(0xF0, 4, 4): got %d, want 15", got) } } func TestExtractIDTag_NonArray(t *testing.T) { id := makeJobID(1, 0, 1700000000, 0, 5) if tag := extractIDTag(id); tag != 0x00 { t.Errorf("tag for non-array job: got %d, want 0", tag) } } func TestExtractIDTag_Array(t *testing.T) { id := makeJobID(1, 0, 1700000000, tagJobArray, 5) if tag := extractIDTag(id); tag != tagJobArray { t.Errorf("tag for array job: got %d, want %d", tag, tagJobArray) } } func TestExtractArrayBaseID_NonArray(t *testing.T) { // Non-array job: base ID should equal the original ID id := makeJobID(1, 0, 1700000000, 0, 5) base := ExtractArrayBaseID(id) if base != id { t.Errorf("non-array base ID should equal original: got %d, want %d", base, id) } idx := ExtractArrayIdx(id) if idx != 0 { t.Errorf("non-array index should be 0: got %d", idx) } } func TestExtractArrayBaseID_ArrayChild(t *testing.T) { // Array job: cluster=1, apiserver=0, timestamp=1700000000 // The sequence for array jobs has the "job number" in high bits. // sequence = jobNumber << sequenceHighShift + arrayIdx // e.g. jobNumber=2, arrayIdx=3 → sequence = 2<<15 + 3 = 65539 jobNumber := uint64(2) arrayIdx := uint64(3) sequence := jobNumber<