forked from lei.chen/myxce
The MCP/apiserver returned the raw 64-bit job ID (e.g. 73930964699447296) while xce_test/xcecli display the dash form (e.g. 1-0-1786585485-0) that parse_jobid accepts. A user taking the numeric form into xce_test fails to parse it. The apiserver is the data-correctness owner (MCP only reads), so it now emits the controller's fmt_jobid form alongside the numeric ID: - models: JobSubmitResponse gains job_id_human; job list rows gain job_id_human (processJobData), using the existing backend.FormatJobID. - schedulerclient: URMJob.HumanID + SubmitJobResult.JobIDHuman parse it. - Tests: backend.FormatJobID (real t1 job, pre-cluster1 job, array child); processJobData adds job_id_human. Also rename the SSE permission event:subscribe → event:view so mcp_readonly stays strictly *:view (the read-only invariant test enforces). Verified live on t1: scheduler_submit returns job_id_human 1-0-1786586474-0 and scheduler_get_job returns human_id. Co-Authored-By: Claude <noreply@anthropic.com>
959 lines
28 KiB
Go
959 lines
28 KiB
Go
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<<sequenceHighShift + arrayIdx
|
|
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
|
|
base := ExtractArrayBaseID(id)
|
|
// Base ID should have tag=0, same cluster/apiserver/timestamp, and arrayJob in sequence
|
|
expectedBase := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
|
|
if base != expectedBase {
|
|
t.Errorf("array base ID mismatch: got %d, want %d", base, expectedBase)
|
|
}
|
|
|
|
idx := ExtractArrayIdx(id)
|
|
if idx != int(arrayIdx) {
|
|
t.Errorf("array index: got %d, want %d", idx, arrayIdx)
|
|
}
|
|
}
|
|
|
|
func TestExtractArrayIdx_MaxIndex(t *testing.T) {
|
|
// Maximum array index = 32767 (2^15 - 1)
|
|
maxIdx := uint64((1 << nArrayIdxBits) - 1)
|
|
sequence := uint64(1)<<sequenceHighShift + maxIdx
|
|
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
|
|
idx := ExtractArrayIdx(id)
|
|
if idx != int(maxIdx) {
|
|
t.Errorf("max array index: got %d, want %d", idx, maxIdx)
|
|
}
|
|
}
|
|
|
|
func TestExtractArrayBaseID_RoundTrip(t *testing.T) {
|
|
// ExtractArrayBaseID and ExtractArrayIdx together should preserve enough
|
|
// information to reconstruct the original ID.
|
|
// Reconstruction requires re-encoding with tag=TAG_JOB_ARRAY.
|
|
jobNumber := uint64(7)
|
|
arrayIdx := uint64(42)
|
|
sequence := jobNumber<<sequenceHighShift + arrayIdx
|
|
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
|
|
base := ExtractArrayBaseID(id)
|
|
idx := ExtractArrayIdx(id)
|
|
|
|
// Reconstruct: base ID sequence + array index, with array tag set
|
|
reconstructedSeq := (extractBits(base, sequenceHighShift, nSequenceBits-nArrayIdxBits) << sequenceHighShift) + uint64(idx)
|
|
reconstructed := makeJobID(
|
|
extractBits(base, clusterIDShift, nClusterIDBits),
|
|
extractBits(base, apiserverIDShift, nApiserverIDBits),
|
|
extractBits(base, timestampShift, nTimestampBits),
|
|
tagJobArray,
|
|
reconstructedSeq,
|
|
)
|
|
if reconstructed != id {
|
|
t.Errorf("round-trip failed: reconstructed %d, want %d", reconstructed, id)
|
|
}
|
|
}
|
|
|
|
func TestRawJobToModel_NonArrayJob(t *testing.T) {
|
|
// Non-array job: JobID == Unique_JobID, ArrayIdx == 0
|
|
id := makeJobID(1, 0, 1700000000, 0, 5)
|
|
r := rawJobData{jobID: int64(id), state: 2}
|
|
info := rawJobToModel(r)
|
|
|
|
if info.Unique_JobID != int64(id) {
|
|
t.Errorf("Unique_JobID: got %d, want %d", info.Unique_JobID, id)
|
|
}
|
|
if info.JobID != int64(id) {
|
|
t.Errorf("JobID for non-array should equal Unique_JobID: got %d, want %d", info.JobID, id)
|
|
}
|
|
if info.ArrayIdx != 0 {
|
|
t.Errorf("ArrayIdx for non-array should be 0: got %d", info.ArrayIdx)
|
|
}
|
|
}
|
|
|
|
func TestRawJobToModel_ArrayChild(t *testing.T) {
|
|
// Array child: JobID == baseID, ArrayIdx == arrayIdx
|
|
jobNumber := uint64(2)
|
|
arrayIdx := uint64(5)
|
|
sequence := jobNumber<<sequenceHighShift + arrayIdx
|
|
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
baseID := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
|
|
|
|
r := rawJobData{jobID: int64(id), state: 2}
|
|
info := rawJobToModel(r)
|
|
|
|
if info.Unique_JobID != int64(id) {
|
|
t.Errorf("Unique_JobID: got %d, want %d", info.Unique_JobID, id)
|
|
}
|
|
if info.JobID != int64(baseID) {
|
|
t.Errorf("JobID for array child should be base ID: got %d, want %d", info.JobID, baseID)
|
|
}
|
|
if info.ArrayIdx != int(arrayIdx) {
|
|
t.Errorf("ArrayIdx for array child: got %d, want %d", info.ArrayIdx, arrayIdx)
|
|
}
|
|
}
|
|
|
|
func TestRawJobToModel_ArrayChildrenShareBaseID(t *testing.T) {
|
|
// All array children should share the same base JobID
|
|
jobNumber := uint64(3)
|
|
baseID := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
|
|
|
|
for idx := 0; idx < 5; idx++ {
|
|
sequence := jobNumber<<sequenceHighShift + uint64(idx)
|
|
id := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
r := rawJobData{jobID: int64(id)}
|
|
info := rawJobToModel(r)
|
|
|
|
if info.JobID != int64(baseID) {
|
|
t.Errorf("array child %d: JobID %d should equal base ID %d", idx, info.JobID, baseID)
|
|
}
|
|
if info.ArrayIdx != idx {
|
|
t.Errorf("array child %d: ArrayIdx got %d, want %d", idx, info.ArrayIdx, idx)
|
|
}
|
|
if info.Unique_JobID != int64(id) {
|
|
t.Errorf("array child %d: Unique_JobID should be unique per child", idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestExtractArrayBaseID_LargeTimestamp verifies that ExtractArrayBaseID does not
|
|
// leak timestamp bits into the extracted sequence when the timestamp field has
|
|
// large values. This would fail with nSequenceBits=30 (extracts 15 bits from
|
|
// position 15, bleeding into the timestamp at bits 20-51) but passes with the
|
|
// correct nSequenceBits=20 (extracts 5 bits from position 15).
|
|
func TestExtractArrayBaseID_LargeTimestamp(t *testing.T) {
|
|
jobNumber := uint64(7)
|
|
arrayIdx := uint64(3)
|
|
sequence := jobNumber<<sequenceHighShift + arrayIdx
|
|
// Use a typical Unix-epoch timestamp (bits 20-51 should stay in timestamp)
|
|
id := makeJobID(1, 0, 1714560000, tagJobArray, sequence)
|
|
|
|
base := ExtractArrayBaseID(id)
|
|
// The base ID must have the same cluster, apiserver, and timestamp —
|
|
// no timestamp bits should leak into the sequence field.
|
|
gotCluster := extractBits(base, clusterIDShift, nClusterIDBits)
|
|
gotApiserver := extractBits(base, apiserverIDShift, nApiserverIDBits)
|
|
gotTimestamp := extractBits(base, timestampShift, nTimestampBits)
|
|
|
|
if gotCluster != 1 {
|
|
t.Errorf("cluster ID leaked: got %d, want 1", gotCluster)
|
|
}
|
|
if gotApiserver != 0 {
|
|
t.Errorf("apiserver ID leaked: got %d, want 0", gotApiserver)
|
|
}
|
|
if gotTimestamp != 1714560000 {
|
|
t.Errorf("timestamp corrupted: got %d, want 1714560000", gotTimestamp)
|
|
}
|
|
|
|
// The base ID's sequence should be just jobNumber<<sequenceHighShift (idx=0, tag=0)
|
|
expectedBase := makeJobID(1, 0, 1714560000, 0, jobNumber<<sequenceHighShift)
|
|
if base != expectedBase {
|
|
t.Errorf("base ID mismatch with large timestamp: got %d, want %d", base, expectedBase)
|
|
}
|
|
}
|
|
|
|
func TestIsArrayJob(t *testing.T) {
|
|
// Non-array job (tag=0x00): IsArrayJob returns false
|
|
nonArrayID := makeJobID(1, 0, 1700000000, 0, 5)
|
|
if IsArrayJob(nonArrayID) {
|
|
t.Errorf("non-array job should not be an array job")
|
|
}
|
|
|
|
// Array child (tag=0x01): IsArrayJob returns true
|
|
arrayID := makeJobID(1, 0, 1700000000, tagJobArray, 5)
|
|
if !IsArrayJob(arrayID) {
|
|
t.Errorf("array job child should be an array job")
|
|
}
|
|
|
|
// Array child with idx=0: IsArrayJob returns true
|
|
jobNumber := uint64(2)
|
|
sequence := jobNumber<<sequenceHighShift + 0 // arrayIdx=0
|
|
arrayIdx0ID := makeJobID(1, 0, 1700000000, tagJobArray, sequence)
|
|
if !IsArrayJob(arrayIdx0ID) {
|
|
t.Errorf("array child with idx=0 should still be an array job")
|
|
}
|
|
}
|
|
|
|
func TestRawHostConfigToModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw rawHostConfigData
|
|
want models.HostInfo
|
|
}{
|
|
{
|
|
name: "normal values",
|
|
raw: rawHostConfigData{
|
|
name: "node01",
|
|
hostmodel: "x86_64",
|
|
hosttype: "LINUX64",
|
|
mxj: 8,
|
|
},
|
|
want: models.HostInfo{
|
|
HostName: "node01",
|
|
HostModel: "x86_64",
|
|
HostType: "LINUX64",
|
|
MaxSlot: 8,
|
|
},
|
|
},
|
|
{
|
|
name: "zero values for integer fields",
|
|
raw: rawHostConfigData{
|
|
name: "node02",
|
|
hostmodel: "modelB",
|
|
hosttype: "linux",
|
|
mxj: 0,
|
|
},
|
|
want: models.HostInfo{
|
|
HostName: "node02",
|
|
HostModel: "modelB",
|
|
HostType: "linux",
|
|
MaxSlot: 0,
|
|
},
|
|
},
|
|
{
|
|
name: "empty strings for name/hostmodel/hosttype",
|
|
raw: rawHostConfigData{
|
|
name: "",
|
|
hostmodel: "",
|
|
hosttype: "",
|
|
mxj: 4,
|
|
},
|
|
want: models.HostInfo{
|
|
HostName: "",
|
|
HostModel: "",
|
|
HostType: "",
|
|
MaxSlot: 4,
|
|
},
|
|
},
|
|
{
|
|
name: "worker-sourced fields remain zero",
|
|
raw: rawHostConfigData{
|
|
name: "node03",
|
|
hostmodel: "modelC",
|
|
hosttype: "CENTOS7",
|
|
mxj: 16,
|
|
},
|
|
want: models.HostInfo{
|
|
HostName: "node03",
|
|
HostModel: "modelC",
|
|
HostType: "CENTOS7",
|
|
MaxSlot: 16,
|
|
HostStatus: 0,
|
|
CPUFactor: 0,
|
|
MaxMem: 0,
|
|
NumRun: 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := rawHostConfigToModel(tt.raw)
|
|
if got.HostName != tt.want.HostName {
|
|
t.Errorf("HostName: got %q, want %q", got.HostName, tt.want.HostName)
|
|
}
|
|
if got.HostModel != tt.want.HostModel {
|
|
t.Errorf("HostModel: got %q, want %q", got.HostModel, tt.want.HostModel)
|
|
}
|
|
if got.HostType != tt.want.HostType {
|
|
t.Errorf("HostType: got %q, want %q", got.HostType, tt.want.HostType)
|
|
}
|
|
if got.MaxSlot != tt.want.MaxSlot {
|
|
t.Errorf("MaxSlot: got %d, want %d", got.MaxSlot, tt.want.MaxSlot)
|
|
}
|
|
// Verify worker-sourced fields stay zero
|
|
if got.HostStatus != tt.want.HostStatus {
|
|
t.Errorf("HostStatus: got %d, want %d (should be zero, worker-sourced)", got.HostStatus, tt.want.HostStatus)
|
|
}
|
|
if got.CPUFactor != tt.want.CPUFactor {
|
|
t.Errorf("CPUFactor: got %f, want %f (should be zero, worker-sourced)", got.CPUFactor, tt.want.CPUFactor)
|
|
}
|
|
if got.MaxMem != tt.want.MaxMem {
|
|
t.Errorf("MaxMem: got %d, want %d (should be zero, worker-sourced)", got.MaxMem, tt.want.MaxMem)
|
|
}
|
|
if got.NumRun != tt.want.NumRun {
|
|
t.Errorf("NumRun: got %d, want %d (should be zero, worker-sourced)", got.NumRun, tt.want.NumRun)
|
|
}
|
|
// DisptWindow is deferred — must be empty string, not accidentally set
|
|
if got.DisptWindow != "" {
|
|
t.Errorf("DisptWindow: got %q, want empty string (deferred field)", got.DisptWindow)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_FloatValue(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "r1m",
|
|
host: "node01",
|
|
valWhich: 5, // Value_u_fvalue
|
|
fval: 3.14,
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for fvalue")
|
|
}
|
|
if got.ResourceName != "r1m" {
|
|
t.Errorf("ResourceName: got %q, want %q", got.ResourceName, "r1m")
|
|
}
|
|
if got.Location != "node01" {
|
|
t.Errorf("Location: got %q, want %q", got.Location, "node01")
|
|
}
|
|
if !got.IsDynamic {
|
|
t.Errorf("IsDynamic: got false, want true")
|
|
}
|
|
if got.ValueType != int(models.NUMERIC) {
|
|
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
|
}
|
|
if got.NumericValue != 3.14 {
|
|
t.Errorf("NumericValue: got %f, want 3.14", got.NumericValue)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_StringValue(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "type",
|
|
host: "node02",
|
|
valWhich: 4, // Value_u_svalue
|
|
sval: "LINUX64",
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for svalue")
|
|
}
|
|
if got.ValueType != int(models.STRING) {
|
|
t.Errorf("ValueType: got %d, want %d (STRING)", got.ValueType, models.STRING)
|
|
}
|
|
if got.StringValue != "LINUX64" {
|
|
t.Errorf("StringValue: got %q, want %q", got.StringValue, "LINUX64")
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_BooleanValue(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "available",
|
|
host: "node03",
|
|
valWhich: 6, // Value_u_bvalue
|
|
bval: true,
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for bvalue")
|
|
}
|
|
if got.ValueType != int(models.BOOLEAN) {
|
|
t.Errorf("ValueType: got %d, want %d (BOOLEAN)", got.ValueType, models.BOOLEAN)
|
|
}
|
|
if !got.BooleanValue {
|
|
t.Errorf("BooleanValue: got false, want true")
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_Int32Value(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "slots",
|
|
host: "node04",
|
|
valWhich: 0, // Value_u_ivalue32
|
|
i32: 16,
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for ivalue32")
|
|
}
|
|
if got.ValueType != int(models.NUMERIC) {
|
|
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
|
}
|
|
if got.NumericValue != 16 {
|
|
t.Errorf("NumericValue: got %f, want 16", got.NumericValue)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_Int64Value(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "bigcounter",
|
|
host: "node05",
|
|
valWhich: 2, // Value_u_ivalue64
|
|
i64: 9223372036854775807, // max int64
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for ivalue64")
|
|
}
|
|
if got.ValueType != int(models.NUMERIC) {
|
|
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
|
}
|
|
if got.NumericValue != float64(int64(9223372036854775807)) {
|
|
t.Errorf("NumericValue: got %f, want max int64 as float64", got.NumericValue)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_Uint32Value(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "uint32res",
|
|
host: "node06",
|
|
valWhich: 1, // Value_u_uvalue32
|
|
u32: 4294967295, // max uint32
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for uvalue32")
|
|
}
|
|
if got.ValueType != int(models.NUMERIC) {
|
|
t.Errorf("ValueType: got %d, want %d", got.ValueType, models.NUMERIC)
|
|
}
|
|
if got.NumericValue != float64(uint32(4294967295)) {
|
|
t.Errorf("NumericValue: got %f, want 4294967295", got.NumericValue)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_Uint64Value(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "uint64res",
|
|
host: "node07",
|
|
valWhich: 3, // Value_u_uvalue64
|
|
u64: 18446744073709551615, // max uint64
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for uvalue64")
|
|
}
|
|
if got.ValueType != int(models.NUMERIC) {
|
|
t.Errorf("ValueType: got %d, want %d", got.ValueType, models.NUMERIC)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_NilVal(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "nilres",
|
|
host: "node08",
|
|
valNil: true,
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got != nil {
|
|
t.Errorf("expected nil for nil val, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_UnknownDiscriminant(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "unkres",
|
|
host: "node09",
|
|
valWhich: 99, // unknown
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got != nil {
|
|
t.Errorf("expected nil for unknown discriminant, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestRawResvalToDistribution_BooleanFalse(t *testing.T) {
|
|
r := rawResvalData{
|
|
name: "available",
|
|
host: "node10",
|
|
valWhich: 6, // Value_u_bvalue
|
|
bval: false,
|
|
}
|
|
got := rawResvalToDistribution(r)
|
|
if got == nil {
|
|
t.Fatal("expected non-nil result for bvalue false")
|
|
}
|
|
if got.ValueType != int(models.BOOLEAN) {
|
|
t.Errorf("ValueType: got %d, want %d (BOOLEAN)", got.ValueType, models.BOOLEAN)
|
|
}
|
|
if got.BooleanValue {
|
|
t.Errorf("BooleanValue: got true, want false")
|
|
}
|
|
}
|
|
|
|
// --- job-array-single-wal-record task 5.4: rawJobArrayToModels ---
|
|
//
|
|
// Sibling to cJobToModel's local-expansion contract: given one compact
|
|
// (base jobid, array_size, shared spec) descriptor, expand it into N
|
|
// models.JobInfo entries using base_jobid + i arithmetic — the same
|
|
// arithmetic the controller's restore_job_array() and the scheduler's
|
|
// sched_on_job_array() use to derive each element's identity (Req 6.2,
|
|
// 6.5). Pure Go (no CGO), mirrors rawJobToModel's existing split.
|
|
|
|
func TestRawJobArrayToModels_ExpandsNElementsWithCorrectIdentity(t *testing.T) {
|
|
jobNumber := uint64(9)
|
|
baseJobID := makeJobID(1, 0, 1700000000, tagJobArray, jobNumber<<sequenceHighShift)
|
|
// ExtractArrayBaseID's reconstruction always carries tag=0 (see
|
|
// TestRawJobToModel_ArrayChild's identical precedent) — it is NOT
|
|
// byte-identical to baseJobID itself, whose tag bits are tagJobArray.
|
|
wantBaseID := makeJobID(1, 0, 1700000000, 0, jobNumber<<sequenceHighShift)
|
|
submit := &rawSubmitData{
|
|
queue: "normal",
|
|
user: "alice",
|
|
jobName: "arrjob",
|
|
minTasks: 1,
|
|
maxTasks: 1,
|
|
}
|
|
|
|
got := rawJobArrayToModels(baseJobID, 5, submit)
|
|
|
|
if len(got) != 5 {
|
|
t.Fatalf("expected 5 elements, got %d", len(got))
|
|
}
|
|
for i, info := range got {
|
|
wantUnique := int64(baseJobID + uint64(i))
|
|
if info.Unique_JobID != wantUnique {
|
|
t.Errorf("element %d: Unique_JobID got %d, want %d", i, info.Unique_JobID, wantUnique)
|
|
}
|
|
if info.JobID != int64(wantBaseID) {
|
|
t.Errorf("element %d: JobID (array base) got %d, want %d", i, info.JobID, wantBaseID)
|
|
}
|
|
if info.ArrayIdx != i {
|
|
t.Errorf("element %d: ArrayIdx got %d, want %d", i, info.ArrayIdx, i)
|
|
}
|
|
if info.QueueName != "normal" {
|
|
t.Errorf("element %d: QueueName got %q, want normal", i, info.QueueName)
|
|
}
|
|
if info.SubmitUser != "alice" {
|
|
t.Errorf("element %d: SubmitUser got %q, want alice", i, info.SubmitUser)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRawJobArrayToModels_ZeroArraySizeReturnsNil(t *testing.T) {
|
|
got := rawJobArrayToModels(1000, 0, &rawSubmitData{queue: "normal"})
|
|
if got != nil {
|
|
t.Errorf("expected nil for array_size=0, got %d elements", len(got))
|
|
}
|
|
}
|
|
|
|
func TestRawJobArrayToModels_NilSubmitReturnsNil(t *testing.T) {
|
|
got := rawJobArrayToModels(1000, 5, nil)
|
|
if got != nil {
|
|
t.Errorf("expected nil for nil submit, got %d elements", len(got))
|
|
}
|
|
}
|
|
|
|
func TestRawClusterToModel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw rawClusterData
|
|
want models.ClusterInfo
|
|
}{
|
|
{
|
|
name: "normal values",
|
|
raw: rawClusterData{
|
|
name: "mycluster",
|
|
heartbeatInterval: 30,
|
|
vcpus: 16,
|
|
inactiveHeartbeatCount: 3,
|
|
},
|
|
want: models.ClusterInfo{
|
|
ClusterName: "mycluster",
|
|
HeartbeatInterval: 30,
|
|
Vcpus: 16,
|
|
InactiveHeartbeatCount: 3,
|
|
},
|
|
},
|
|
{
|
|
name: "empty cluster name maps through unchanged (guard is in CGO layer)",
|
|
raw: rawClusterData{
|
|
name: "",
|
|
heartbeatInterval: 60,
|
|
vcpus: 8,
|
|
inactiveHeartbeatCount: 5,
|
|
},
|
|
want: models.ClusterInfo{
|
|
ClusterName: "",
|
|
HeartbeatInterval: 60,
|
|
Vcpus: 8,
|
|
InactiveHeartbeatCount: 5,
|
|
},
|
|
},
|
|
{
|
|
name: "built-in fields remain zero",
|
|
raw: rawClusterData{
|
|
name: "test",
|
|
heartbeatInterval: 45,
|
|
vcpus: 32,
|
|
inactiveHeartbeatCount: 2,
|
|
},
|
|
want: models.ClusterInfo{
|
|
ClusterName: "test",
|
|
HeartbeatInterval: 45,
|
|
Vcpus: 32,
|
|
InactiveHeartbeatCount: 2,
|
|
Version: "",
|
|
Status: 0,
|
|
ControllerName: "",
|
|
NumServers: 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := rawClusterToModel(tt.raw)
|
|
if got.ClusterName != tt.want.ClusterName {
|
|
t.Errorf("ClusterName: got %q, want %q", got.ClusterName, tt.want.ClusterName)
|
|
}
|
|
if got.HeartbeatInterval != tt.want.HeartbeatInterval {
|
|
t.Errorf("HeartbeatInterval: got %d, want %d", got.HeartbeatInterval, tt.want.HeartbeatInterval)
|
|
}
|
|
if got.Vcpus != tt.want.Vcpus {
|
|
t.Errorf("Vcpus: got %d, want %d", got.Vcpus, tt.want.Vcpus)
|
|
}
|
|
if got.InactiveHeartbeatCount != tt.want.InactiveHeartbeatCount {
|
|
t.Errorf("InactiveHeartbeatCount: got %d, want %d", got.InactiveHeartbeatCount, tt.want.InactiveHeartbeatCount)
|
|
}
|
|
// Verify built-in/sourced fields stay zero (not overwritten by config push)
|
|
if got.Version != tt.want.Version {
|
|
t.Errorf("Version: got %q, want %q (should be empty, built-in-sourced)", got.Version, tt.want.Version)
|
|
}
|
|
if got.Status != tt.want.Status {
|
|
t.Errorf("Status: got %d, want %d (should be zero, built-in-sourced)", got.Status, tt.want.Status)
|
|
}
|
|
if got.ControllerName != tt.want.ControllerName {
|
|
t.Errorf("ControllerName: got %q, want %q (should be empty, built-in-sourced)", got.ControllerName, tt.want.ControllerName)
|
|
}
|
|
if got.NumServers != tt.want.NumServers {
|
|
t.Errorf("NumServers: got %d, want %d (should be zero, built-in-sourced)", got.NumServers, tt.want.NumServers)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// bug-1500: the submit conversion must carry an exec user; explicit exec_user
|
|
// wins, otherwise the job runs as the submitting user (not "nobody").
|
|
func TestResolveExecUser(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
execUser string
|
|
submitUser string
|
|
want string
|
|
}{
|
|
{"explicit exec_user wins", "lchen", "admin", "lchen"},
|
|
{"empty exec_user falls back to submit user", "", "admin", "admin"},
|
|
{"both empty stays empty", "", "", ""},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
job := &models.JobInfo{SubmitUser: tc.submitUser, ExecUser: tc.execUser}
|
|
if got := resolveExecUser(job); got != tc.want {
|
|
t.Errorf("resolveExecUser: got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatJobID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
jobID int64
|
|
want string
|
|
}{
|
|
{
|
|
name: "real job from t1",
|
|
jobID: 73930964699447296, // cluster=1 apiserver=0 ts=1786585485 seq=0
|
|
want: "1-0-1786585485-0",
|
|
},
|
|
{
|
|
name: "pre-cluster1 job",
|
|
// cluster=0 apiserver=7 ts=1786416651 seq=138313
|
|
jobID: int64(uint64(0)<<56 | uint64(7)<<52 | uint64(1786416651)<<20 | uint64(138313)),
|
|
want: "0-7-1786416651-138313",
|
|
},
|
|
{
|
|
name: "zero",
|
|
jobID: 0,
|
|
want: "0-0-0-0",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := FormatJobID(tt.jobID); got != tt.want {
|
|
t.Errorf("FormatJobID(%d) = %q, want %q", tt.jobID, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatJobID_ArrayChild(t *testing.T) {
|
|
// Array child: tag 0x01 set, seq split into arrayjob(high)<<15 and idx(low).
|
|
// cluster=1 apiserver=0 ts=1786585485, arrayjob=3<<15, idx=5.
|
|
jobID := int64(uint64(1)<<56 | uint64(0)<<52 | uint64(1786585485)<<20 | uint64(0x01)<<62 | uint64(3)<<15 | uint64(5))
|
|
if got, want := FormatJobID(jobID), "1-0-1786585485-98304[5]"; got != want {
|
|
t.Errorf("FormatJobID(array) = %q, want %q", got, want)
|
|
}
|
|
}
|