feat: 添加 Job 领域类型和 JobsService

约 93 个结构体覆盖 JobInfo、JobDescMsg、SlurmDBD Job/Step/QOS/Account/User 等类型。JobsService 提供 GetJobs、GetJob、PostJob、DeleteJob、SubmitJob 5 个方法,并定义 JobFlag* 和 JobSignalFlag* 常量。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-08 18:29:07 +08:00
parent 5873dc5b72
commit 2c84930983
4 changed files with 2384 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
package slurm
import (
"context"
"fmt"
"net/url"
)
// Job query flags for GetJobsOptions.Flags and GetNodeOptions.Flags.
const (
JobFlagAll = "ALL"
JobFlagDetail = "DETAIL"
JobFlagMixed = "MIXED"
JobFlagLocal = "LOCAL"
JobFlagSibling = "SIBLING"
JobFlagFederation = "FEDERATION"
JobFlagFuture = "FUTURE"
)
// GetJobsOptions specifies optional parameters for GetJobs.
type GetJobsOptions struct {
UpdateTime *string `url:"update_time,omitempty"`
Flags *string `url:"flags,omitempty"` // Use JobFlag* constants (e.g. JobFlagDetail)
}
// Job signal flags for DeleteJobOptions.Flags.
const (
JobSignalFlagBatchJob = "BATCH_JOB"
JobSignalFlagArrayTask = "ARRAY_TASK"
JobSignalFlagFullStepsOnly = "FULL_STEPS_ONLY"
JobSignalFlagFullJob = "FULL_JOB"
JobSignalFlagFederationRequeue = "FEDERATION_REQUEUE"
JobSignalFlagHurry = "HURRY"
JobSignalFlagOutOfMemory = "OUT_OF_MEMORY"
JobSignalFlagNoSiblingJobs = "NO_SIBLING_JOBS"
JobSignalFlagReservationJob = "RESERVATION_JOB"
JobSignalFlagWarningSent = "WARNING_SENT"
)
// DeleteJobOptions specifies optional parameters for DeleteJob.
type DeleteJobOptions struct {
Signal *string `url:"signal,omitempty"`
Flags *string `url:"flags,omitempty"` // Use JobSignalFlag* constants (e.g. JobSignalFlagHurry)
}
// GetJobs lists all jobs.
func (s *JobsService) GetJobs(ctx context.Context, opts *GetJobsOptions) (*OpenapiJobInfoResp, *Response, error) {
path := "slurm/v0.0.40/jobs"
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
if opts != nil {
u, parseErr := url.Parse(req.URL.String())
if parseErr != nil {
return nil, nil, parseErr
}
q := u.Query()
if opts.UpdateTime != nil {
q.Set("update_time", *opts.UpdateTime)
}
if opts.Flags != nil {
q.Set("flags", *opts.Flags)
}
u.RawQuery = q.Encode()
req.URL = u
}
var result OpenapiJobInfoResp
resp, err := s.client.Do(ctx, req, &result)
if err != nil {
return nil, resp, err
}
return &result, resp, nil
}
// GetJob gets a single job by ID.
func (s *JobsService) GetJob(ctx context.Context, jobID string, opts *GetJobsOptions) (*OpenapiJobInfoResp, *Response, error) {
path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
if opts != nil {
u, parseErr := url.Parse(req.URL.String())
if parseErr != nil {
return nil, nil, parseErr
}
q := u.Query()
if opts.UpdateTime != nil {
q.Set("update_time", *opts.UpdateTime)
}
if opts.Flags != nil {
q.Set("flags", *opts.Flags)
}
u.RawQuery = q.Encode()
req.URL = u
}
var result OpenapiJobInfoResp
resp, err := s.client.Do(ctx, req, &result)
if err != nil {
return nil, resp, err
}
return &result, resp, nil
}
// PostJob updates a job.
func (s *JobsService) PostJob(ctx context.Context, jobID string, desc *JobDescMsg) (*OpenapiJobPostResponse, *Response, error) {
path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID)
req, err := s.client.NewRequest("POST", path, desc)
if err != nil {
return nil, nil, err
}
var result OpenapiJobPostResponse
resp, err := s.client.Do(ctx, req, &result)
if err != nil {
return nil, resp, err
}
return &result, resp, nil
}
// DeleteJob cancels (signals) a job.
func (s *JobsService) DeleteJob(ctx context.Context, jobID string, opts *DeleteJobOptions) (*OpenapiResp, *Response, error) {
path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, nil, err
}
if opts != nil {
u, parseErr := url.Parse(req.URL.String())
if parseErr != nil {
return nil, nil, parseErr
}
q := u.Query()
if opts.Signal != nil {
q.Set("signal", *opts.Signal)
}
if opts.Flags != nil {
q.Set("flags", *opts.Flags)
}
u.RawQuery = q.Encode()
req.URL = u
}
var result OpenapiResp
resp, err := s.client.Do(ctx, req, &result)
if err != nil {
return nil, resp, err
}
return &result, resp, nil
}
// SubmitJob submits a new job.
func (s *JobsService) SubmitJob(ctx context.Context, reqBody *JobSubmitReq) (*OpenapiJobSubmitResponse, *Response, error) {
path := "slurm/v0.0.40/job/submit"
req, err := s.client.NewRequest("POST", path, reqBody)
if err != nil {
return nil, nil, err
}
var result OpenapiJobSubmitResponse
resp, err := s.client.Do(ctx, req, &result)
if err != nil {
return nil, resp, err
}
return &result, resp, nil
}

View File

@@ -0,0 +1,286 @@
package slurm
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func testMethod(t *testing.T, r *http.Request, expected string) {
t.Helper()
if r.Method != expected {
t.Errorf("expected method %s, got %s", expected, r.Method)
}
}
func TestJobsService_GetJobs(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"jobs": [], "last_backfill": {}, "last_update": {}}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
resp, _, err := client.Jobs.GetJobs(context.Background(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(resp.Jobs) != 0 {
t.Errorf("expected empty jobs, got %d", len(resp.Jobs))
}
}
func TestJobsService_GetJobs_WithOptions(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
q := r.URL.Query()
if q.Get("update_time") != "12345" {
t.Errorf("expected update_time=12345, got %s", q.Get("update_time"))
}
if q.Get("flags") != JobFlagDetail {
t.Errorf("expected flags=%s, got %s", JobFlagDetail, q.Get("flags"))
}
fmt.Fprint(w, `{"jobs": [], "last_update": {}}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
opts := &GetJobsOptions{
UpdateTime: Ptr("12345"),
Flags: Ptr(JobFlagDetail),
}
resp, _, err := client.Jobs.GetJobs(context.Background(), opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestJobsService_GetJob(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"jobs": [{"job_id": 42, "name": "test-job"}], "last_update": {}}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
resp, _, err := client.Jobs.GetJob(context.Background(), "42", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if len(resp.Jobs) != 1 {
t.Fatalf("expected 1 job, got %d", len(resp.Jobs))
}
if resp.Jobs[0].JobID == nil || *resp.Jobs[0].JobID != 42 {
t.Errorf("expected job_id=42, got %v", resp.Jobs[0].JobID)
}
}
func TestJobsService_PostJob(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("expected Content-Type application/json, got %s", ct)
}
fmt.Fprint(w, `{"job_id": "42", "step_id": "0", "results": []}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
desc := &JobDescMsg{
Name: Ptr("updated-job"),
}
resp, _, err := client.Jobs.PostJob(context.Background(), "42", desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.JobID == nil || *resp.JobID != "42" {
t.Errorf("expected job_id=42, got %v", resp.JobID)
}
}
func TestJobsService_DeleteJob(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
q := r.URL.Query()
if q.Get("signal") != "SIGTERM" {
t.Errorf("expected signal=SIGTERM, got %s", q.Get("signal"))
}
fmt.Fprint(w, `{}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
opts := &DeleteJobOptions{
Signal: Ptr("SIGTERM"),
}
resp, _, err := client.Jobs.DeleteJob(context.Background(), "42", opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestJobsService_SubmitJob(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/submit", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
t.Errorf("expected Content-Type application/json, got %s", ct)
}
fmt.Fprint(w, `{"job_id": 100, "step_id": "0", "result": {"job_id": 100, "step_id": "0"}}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
reqBody := &JobSubmitReq{
Script: Ptr("#!/bin/bash\necho hello"),
Job: &JobDescMsg{
Name: Ptr("test-submit"),
},
}
resp, _, err := client.Jobs.SubmitJob(context.Background(), reqBody)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.JobID == nil || *resp.JobID != 100 {
t.Errorf("expected job_id=100, got %v", resp.JobID)
}
}
func TestJobsService_GetJobs_Error(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, `{"errors": [{"error": "internal error"}]}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
_, _, err := client.Jobs.GetJobs(context.Background(), nil)
if err == nil {
t.Fatal("expected error for 500 response")
}
if !strings.Contains(err.Error(), "500") {
t.Errorf("expected error to contain 500, got %v", err)
}
}
func TestJobsService_GetJob_Error(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/999", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"errors": [{"error": "job not found"}]}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
_, _, err := client.Jobs.GetJob(context.Background(), "999", nil)
if err == nil {
t.Fatal("expected error for 404 response")
}
}
func TestJobsService_DeleteJob_NoOptions(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
q := r.URL.Query()
if len(q) != 0 {
t.Errorf("expected no query params, got %v", q)
}
fmt.Fprint(w, `{}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
resp, _, err := client.Jobs.DeleteJob(context.Background(), "42", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestJobsService_GetJobPathEscaping(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/job/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
parts := strings.Split(r.URL.Path, "/")
jobID := parts[len(parts)-1]
if jobID != "123" {
t.Errorf("expected job ID 123, got %s", jobID)
}
fmt.Fprint(w, `{"jobs": [{"job_id": 123}]}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
resp, _, err := client.Jobs.GetJob(context.Background(), "123", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
}
func TestJobsService_GetJobsWithURLQuery(t *testing.T) {
var capturedQuery url.Values
mux := http.NewServeMux()
mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) {
capturedQuery = r.URL.Query()
testMethod(t, r, "GET")
fmt.Fprint(w, `{"jobs": []}`)
})
server := httptest.NewServer(mux)
defer server.Close()
client, _ := NewClient(server.URL, nil)
opts := &GetJobsOptions{
UpdateTime: Ptr("1700000000"),
}
_, _, err := client.Jobs.GetJobs(context.Background(), opts)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if capturedQuery.Get("update_time") != "1700000000" {
t.Errorf("expected update_time=1700000000, got %s", capturedQuery.Get("update_time"))
}
}

1149
internal/slurm/types_job.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,777 @@
package slurm
import (
"encoding/json"
"testing"
)
func TestJobInfoRoundTrip(t *testing.T) {
orig := JobInfo{
Account: Ptr("testacct"),
AccrueTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
ArrayJobID: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))},
ArrayTaskID: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(1))},
BatchFlag: Ptr(true),
BatchHost: Ptr("node01"),
Cluster: Ptr("test-cluster"),
Command: Ptr("/usr/bin/true"),
Contiguous: Ptr(false),
Cpus: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(4))},
ExitCode: &ProcessExitCodeVerbose{
Status: []string{"EXITED"},
ReturnCode: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(0))},
},
Flags: []string{"CRON_JOB"},
GroupID: Ptr(int32(1000)),
JobID: Ptr(int32(12345)),
JobState: []string{"COMPLETED"},
Name: Ptr("test-job"),
NodeCount: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(2))},
Nodes: Ptr("node[01-02]"),
Partition: Ptr("normal"),
Priority: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(4294901758))},
StartTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
SubmitTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567800))},
Tasks: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(2))},
TimeLimit: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(60))},
UserID: Ptr(int32(1001)),
UserName: Ptr("testuser"),
CurrentWorkingDirectory: Ptr("/home/testuser"),
Power: &JobInfoPower{Flags: []string{"EQUAL_POWER"}},
StandardOutput: Ptr("/home/testuser/slurm-%j.out"),
StandardError: Ptr("/home/testuser/slurm-%j.err"),
StandardInput: Ptr("/dev/null"),
GresDetail: JobInfoGresDetail{"gpu:2", "mps:100"},
BillableTres: &Float64NoVal{Set: Ptr(true), Number: Ptr(4.0)},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded JobInfo
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Account == nil || *decoded.Account != "testacct" {
t.Fatalf("account mismatch: %v", decoded.Account)
}
if decoded.JobID == nil || *decoded.JobID != 12345 {
t.Fatalf("job_id mismatch: %v", decoded.JobID)
}
if len(decoded.JobState) != 1 || decoded.JobState[0] != "COMPLETED" {
t.Fatalf("job_state mismatch: %v", decoded.JobState)
}
if decoded.Cpus == nil || decoded.Cpus.Number == nil || *decoded.Cpus.Number != 4 {
t.Fatalf("cpus mismatch: %v", decoded.Cpus)
}
if decoded.ExitCode == nil || decoded.ExitCode.ReturnCode == nil {
t.Fatal("exit_code should not be nil")
}
if decoded.Power == nil || len(decoded.Power.Flags) != 1 || decoded.Power.Flags[0] != "EQUAL_POWER" {
t.Fatalf("power mismatch: %v", decoded.Power)
}
if len(decoded.GresDetail) != 2 || decoded.GresDetail[0] != "gpu:2" {
t.Fatalf("gres_detail mismatch: %v", decoded.GresDetail)
}
if decoded.BillableTres == nil || decoded.BillableTres.Number == nil || *decoded.BillableTres.Number != 4.0 {
t.Fatalf("billable_tres mismatch: %v", decoded.BillableTres)
}
}
func TestJobDescMsgRoundTrip(t *testing.T) {
orig := JobDescMsg{
Script: Ptr("#!/bin/bash\necho hello"),
Name: Ptr("submit-test"),
Account: Ptr("testacct"),
Partition: Ptr("normal"),
Tasks: Ptr(int32(4)),
Nodes: Ptr("node[01-02]"),
TimeLimit: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(30))},
MemoryPerCpu: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(4096))},
MemoryPerNode: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(8192))},
Argv: StringArray{"/bin/bash", "-c", "echo hello"},
Environment: StringArray{"PATH=/usr/bin", "HOME=/root"},
CurrentWorkingDirectory: Ptr("/tmp"),
StandardOutput: Ptr("slurm-%j.out"),
Flags: []string{"SPREAD_JOB"},
MailType: []string{"BEGIN", "END"},
MailUser: Ptr("user@example.com"),
Rlimits: &JobDescRlimits{
Cpu: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(3600))},
Nofile: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(65536))},
},
Crontab: &CronEntry{
Minute: Ptr("*/5"),
Hour: Ptr("*"),
Line: &CronEntryLine{Start: Ptr(int32(1)), End: Ptr(int32(5))},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded JobDescMsg
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Script == nil || *decoded.Script != "#!/bin/bash\necho hello" {
t.Fatalf("script mismatch")
}
if decoded.Name == nil || *decoded.Name != "submit-test" {
t.Fatalf("name mismatch")
}
if decoded.Tasks == nil || *decoded.Tasks != 4 {
t.Fatalf("tasks mismatch: %v", decoded.Tasks)
}
if decoded.Rlimits == nil || decoded.Rlimits.Cpu == nil {
t.Fatal("rlimits should not be nil")
}
if *decoded.Rlimits.Cpu.Number != 3600 {
t.Fatalf("rlimits.cpu mismatch: %v", decoded.Rlimits.Cpu)
}
if decoded.Crontab == nil || decoded.Crontab.Minute == nil || *decoded.Crontab.Minute != "*/5" {
t.Fatal("crontab.minute mismatch")
}
if len(decoded.Argv) != 3 {
t.Fatalf("argv mismatch: %v", decoded.Argv)
}
if len(decoded.Flags) != 1 || decoded.Flags[0] != "SPREAD_JOB" {
t.Fatalf("flags mismatch: %v", decoded.Flags)
}
}
func TestJobSubmitReqRoundTrip(t *testing.T) {
orig := JobSubmitReq{
Script: Ptr("#!/bin/bash\necho test"),
Job: &JobDescMsg{
Name: Ptr("myjob"),
Partition: Ptr("debug"),
Tasks: Ptr(int32(1)),
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded JobSubmitReq
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Script == nil || *decoded.Script != "#!/bin/bash\necho test" {
t.Fatal("script mismatch")
}
if decoded.Job == nil || decoded.Job.Name == nil || *decoded.Job.Name != "myjob" {
t.Fatal("job.name mismatch")
}
if decoded.Job.Tasks == nil || *decoded.Job.Tasks != 1 {
t.Fatal("job.tasks mismatch")
}
}
func TestOpenapiJobInfoRespRoundTrip(t *testing.T) {
orig := OpenapiJobInfoResp{
Jobs: JobInfoMsg{
{
JobID: Ptr(int32(100)),
Name: Ptr("job-a"),
JobState: []string{"RUNNING"},
},
{
JobID: Ptr(int32(101)),
Name: Ptr("job-b"),
JobState: []string{"PENDING"},
},
},
LastBackfill: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
LastUpdate: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567900))},
Meta: &OpenapiMeta{
Slurm: &MetaSlurm{
Version: &MetaSlurmVersion{
Major: Ptr("24"),
Micro: Ptr("5"),
Minor: Ptr("05"),
},
},
},
Errors: OpenapiErrors{{Error: Ptr("none")}},
Warnings: OpenapiWarnings{},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded OpenapiJobInfoResp
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(decoded.Jobs) != 2 {
t.Fatalf("expected 2 jobs, got %d", len(decoded.Jobs))
}
if decoded.Jobs[0].JobID == nil || *decoded.Jobs[0].JobID != 100 {
t.Fatalf("job[0].job_id mismatch")
}
if decoded.Jobs[1].Name == nil || *decoded.Jobs[1].Name != "job-b" {
t.Fatalf("job[1].name mismatch")
}
if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Version == nil {
t.Fatal("meta.slurm.version should not be nil")
}
if decoded.LastBackfill == nil || decoded.LastBackfill.Number == nil || *decoded.LastBackfill.Number != 1234567890 {
t.Fatalf("last_backfill mismatch")
}
}
func TestOpenapiJobPostResponseRoundTrip(t *testing.T) {
orig := OpenapiJobPostResponse{
Results: JobArrayResponseArray{
{
JobID: Ptr(int32(200)),
StepID: Ptr("batch"),
Error: Ptr(""),
Why: Ptr("Job updated"),
},
},
JobID: Ptr("200"),
StepID: Ptr("batch"),
JobSubmitUserMsg: Ptr("Job updated successfully"),
Meta: &OpenapiMeta{
Plugin: &MetaPlugin{Type: Ptr("type")},
},
Errors: OpenapiErrors{},
Warnings: OpenapiWarnings{},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded OpenapiJobPostResponse
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if len(decoded.Results) != 1 {
t.Fatalf("expected 1 result, got %d", len(decoded.Results))
}
if decoded.Results[0].JobID == nil || *decoded.Results[0].JobID != 200 {
t.Fatalf("results[0].job_id mismatch")
}
if decoded.JobSubmitUserMsg == nil || *decoded.JobSubmitUserMsg != "Job updated successfully" {
t.Fatal("job_submit_user_msg mismatch")
}
}
func TestOpenapiJobSubmitResponseRoundTrip(t *testing.T) {
orig := OpenapiJobSubmitResponse{
Result: &JobSubmitResponseMsg{
JobID: Ptr(int32(300)),
StepID: Ptr("batch"),
JobSubmitUserMsg: Ptr("Job submitted"),
},
JobID: Ptr(int32(300)),
StepID: Ptr("batch"),
JobSubmitUserMsg: Ptr("Job submitted"),
Meta: &OpenapiMeta{
Client: &MetaClient{User: Ptr("testuser")},
},
Errors: OpenapiErrors{{Description: Ptr("no error")}},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded OpenapiJobSubmitResponse
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Result == nil || decoded.Result.JobID == nil || *decoded.Result.JobID != 300 {
t.Fatal("result.job_id mismatch")
}
if decoded.JobID == nil || *decoded.JobID != 300 {
t.Fatal("job_id mismatch")
}
if decoded.Meta == nil || decoded.Meta.Client == nil || decoded.Meta.Client.User == nil {
t.Fatal("meta.client.user should not be nil")
}
}
func TestOpenapiRespRoundTrip(t *testing.T) {
orig := OpenapiResp{
Meta: &OpenapiMeta{
Slurm: &MetaSlurm{Release: Ptr("24.05.5")},
},
Errors: OpenapiErrors{},
Warnings: OpenapiWarnings{},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded OpenapiResp
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Release == nil {
t.Fatal("meta.slurm.release should not be nil")
}
if *decoded.Meta.Slurm.Release != "24.05.5" {
t.Fatalf("release mismatch: %v", decoded.Meta.Slurm.Release)
}
}
func TestJobInfoMinimalRoundTrip(t *testing.T) {
orig := JobInfo{JobID: Ptr(int32(1))}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded JobInfo
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.JobID == nil || *decoded.JobID != 1 {
t.Fatal("job_id mismatch")
}
}
func TestJobInfoEmptyRoundTrip(t *testing.T) {
orig := JobInfo{}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(data) != "{}" {
t.Fatalf("empty JobInfo should marshal to {}, got %s", data)
}
var decoded JobInfo
if err := json.Unmarshal([]byte(`{}`), &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.JobID != nil {
t.Fatal("job_id should be nil for empty object")
}
}
func TestJobResRoundTrip(t *testing.T) {
orig := JobRes{
Nodes: Ptr("node[01-02]"),
AllocatedCores: Ptr(int32(8)),
AllocatedCpus: Ptr(int32(16)),
AllocatedHosts: Ptr(int32(2)),
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded JobRes
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.AllocatedCpus == nil || *decoded.AllocatedCpus != 16 {
t.Fatal("allocated_cpus mismatch")
}
}
func TestCronEntryRoundTrip(t *testing.T) {
orig := CronEntry{
Flags: []string{"WILD_MINUTE"},
Minute: Ptr("0,30"),
Hour: Ptr("*"),
DayOfMonth: Ptr("*"),
Month: Ptr("*"),
DayOfWeek: Ptr("1-5"),
Specification: Ptr("0,30 * * * 1-5"),
Command: Ptr("/usr/bin/run-job"),
Line: &CronEntryLine{Start: Ptr(int32(10)), End: Ptr(int32(12))},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded CronEntry
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Specification == nil || *decoded.Specification != "0,30 * * * 1-5" {
t.Fatal("specification mismatch")
}
if decoded.Line == nil || decoded.Line.Start == nil || *decoded.Line.Start != 10 {
t.Fatal("line.start mismatch")
}
}
func TestTresRoundTrip(t *testing.T) {
orig := Tres{
Type: Ptr("cpu"),
Name: Ptr(""),
ID: Ptr(int32(1)),
Count: Ptr(int64(4)),
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Tres
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Type == nil || *decoded.Type != "cpu" {
t.Fatal("type mismatch")
}
if decoded.Count == nil || *decoded.Count != 4 {
t.Fatal("count mismatch")
}
}
func TestAssocShortRoundTrip(t *testing.T) {
orig := AssocShort{
Account: Ptr("default"),
Cluster: Ptr("test-cluster"),
User: Ptr("testuser"),
ID: Ptr(int32(42)),
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded AssocShort
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.Account != "default" || *decoded.User != "testuser" || *decoded.ID != 42 {
t.Fatal("round-trip mismatch")
}
}
func TestQOSRoundTrip(t *testing.T) {
orig := QOS{
Name: Ptr("normal"),
Description: Ptr("Default QOS"),
ID: Ptr(int32(1)),
Flags: []string{"NOT_SET"},
Priority: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))},
UsageFactor: &Float64NoVal{Set: Ptr(true), Number: Ptr(1.0)},
Limits: &QOSLimits{
GraceTime: Ptr(int32(30)),
Max: &QOSLimitsMax{
ActiveJobs: &QOSLimitsMaxActiveJobs{
Count: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))},
},
WallClock: &QOSLimitsMaxWallClock{
Per: &QOSLimitsMaxWallClockPer{
Job: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(1440))},
},
},
},
},
Preempt: &QOSPreempt{
Mode: []string{"REQUEUE"},
ExemptTime: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(300))},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded QOS
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Name == nil || *decoded.Name != "normal" {
t.Fatal("name mismatch")
}
if decoded.Limits == nil || decoded.Limits.GraceTime == nil || *decoded.Limits.GraceTime != 30 {
t.Fatal("limits.grace_time mismatch")
}
if decoded.Limits.Max == nil || decoded.Limits.Max.ActiveJobs == nil {
t.Fatal("limits.max.active_jobs should not be nil")
}
if decoded.Preempt == nil || len(decoded.Preempt.Mode) != 1 || decoded.Preempt.Mode[0] != "REQUEUE" {
t.Fatal("preempt.mode mismatch")
}
}
func TestUserRoundTrip(t *testing.T) {
orig := User{
Name: Ptr("testuser"),
AdministratorLevel: []string{"None"},
Associations: AssocShortList{
{Account: Ptr("default"), Cluster: Ptr("test"), User: Ptr("testuser")},
},
Default: &UserDefault{
Account: Ptr("default"),
Wckey: Ptr("*"),
},
Flags: []string{"NONE"},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded User
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Name == nil || *decoded.Name != "testuser" {
t.Fatal("name mismatch")
}
if len(decoded.Associations) != 1 {
t.Fatalf("expected 1 association, got %d", len(decoded.Associations))
}
if decoded.Default == nil || decoded.Default.Account == nil || *decoded.Default.Account != "default" {
t.Fatal("default.account mismatch")
}
}
func TestAccountRoundTrip(t *testing.T) {
orig := Account{
Name: Ptr("testacct"),
Description: Ptr("Test account"),
Organization: Ptr("testorg"),
Coordinators: CoordList{
{Name: Ptr("admin1"), Direct: Ptr(true)},
},
Associations: AssocShortList{
{Account: Ptr("testacct"), User: Ptr("user1")},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Account
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Name == nil || *decoded.Name != "testacct" {
t.Fatal("name mismatch")
}
if len(decoded.Coordinators) != 1 {
t.Fatalf("expected 1 coordinator, got %d", len(decoded.Coordinators))
}
}
func TestClusterRecRoundTrip(t *testing.T) {
orig := ClusterRec{
Name: Ptr("test-cluster"),
Controller: &ClusterRecController{
Host: Ptr("ctrl-node"),
Port: Ptr(int32(6817)),
},
Flags: []string{"MULTIPLE_SLURMD"},
Tres: TresList{
{Type: Ptr("cpu"), Count: Ptr(int64(100))},
{Type: Ptr("mem"), Count: Ptr(int64(512000))},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded ClusterRec
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Name == nil || *decoded.Name != "test-cluster" {
t.Fatal("name mismatch")
}
if decoded.Controller == nil || decoded.Controller.Port == nil || *decoded.Controller.Port != 6817 {
t.Fatal("controller.port mismatch")
}
if len(decoded.Tres) != 2 {
t.Fatalf("expected 2 tres, got %d", len(decoded.Tres))
}
}
func TestAssocRoundTrip(t *testing.T) {
orig := Assoc{
Account: Ptr("testacct"),
Cluster: Ptr("test-cluster"),
User: Ptr("testuser"),
ID: &AssocShort{
Account: Ptr("testacct"),
Cluster: Ptr("test-cluster"),
User: Ptr("testuser"),
ID: Ptr(int32(1)),
},
Default: &AssocDefault{Qos: Ptr("normal")},
IsDefault: Ptr(true),
Qos: QosStringIdList{"normal", "high"},
Max: &AssocMax{
Jobs: &AssocMaxJobs{
Total: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(500))},
},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Assoc
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.Account != "testacct" {
t.Fatal("account mismatch")
}
if len(decoded.Qos) != 2 || decoded.Qos[0] != "normal" {
t.Fatalf("qos mismatch: %v", decoded.Qos)
}
if decoded.Max == nil || decoded.Max.Jobs == nil {
t.Fatal("max.jobs should not be nil")
}
}
func TestWckeyRoundTrip(t *testing.T) {
orig := Wckey{
Name: Ptr("mykey"),
Cluster: Ptr("test-cluster"),
User: Ptr("testuser"),
ID: Ptr(int32(1)),
Flags: []string{"DELETED"},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Wckey
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.Name != "mykey" {
t.Fatal("name mismatch")
}
}
func TestInstanceRoundTrip(t *testing.T) {
orig := Instance{
Cluster: Ptr("test-cluster"),
InstanceID: Ptr("i-12345"),
InstanceType: Ptr("c5.large"),
NodeName: Ptr("node01"),
Time: &InstanceTime{
TimeStart: Ptr(int64(1234567890)),
TimeEnd: Ptr(int64(1234567900)),
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Instance
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.InstanceID != "i-12345" {
t.Fatal("instance_id mismatch")
}
}
func TestWckeyTagStructRoundTrip(t *testing.T) {
orig := WckeyTagStruct{
Wckey: Ptr("mykey"),
Flags: []string{"ASSIGNED_DEFAULT"},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded WckeyTagStruct
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.Wckey != "mykey" {
t.Fatal("wckey mismatch")
}
if len(decoded.Flags) != 1 || decoded.Flags[0] != "ASSIGNED_DEFAULT" {
t.Fatal("flags mismatch")
}
}
func TestAccountingRoundTrip(t *testing.T) {
orig := Accounting{
ID: Ptr(int32(1)),
Start: Ptr(int64(1234567890)),
Allocated: &AccountingAllocated{
Seconds: Ptr(int64(3600)),
},
TRES: &Tres{Type: Ptr("cpu"), Count: Ptr(int64(4))},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Accounting
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.ID != 1 || *decoded.Start != 1234567890 {
t.Fatal("id/start mismatch")
}
if decoded.Allocated == nil || *decoded.Allocated.Seconds != 3600 {
t.Fatal("allocated.seconds mismatch")
}
}
func TestCoordRoundTrip(t *testing.T) {
orig := Coord{
Name: Ptr("admin"),
Direct: Ptr(true),
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Coord
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if *decoded.Name != "admin" || !*decoded.Direct {
t.Fatal("round-trip mismatch")
}
}
func TestStepRoundTrip(t *testing.T) {
orig := Step{
Step: &StepIDName{
ID: Ptr("0"),
Name: Ptr("step0"),
},
State: []string{"COMPLETED"},
ExitCode: &ProcessExitCodeVerbose{
Status: []string{"EXITED"},
ReturnCode: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(0))},
},
Nodes: &StepNodes{
Count: Ptr(int32(2)),
Range: Ptr("node[01-02]"),
List: Hostlist{"node01", "node02"},
},
Tasks: &StepTasks{Count: Ptr(int32(4))},
Time: &StepTime{
Elapsed: Ptr(int32(60)),
Start: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
End: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567950))},
},
Tres: &StepTres{
Allocated: TresList{
{Type: Ptr("cpu"), Count: Ptr(int64(4))},
},
},
}
data, err := json.Marshal(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded Step
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if decoded.Step == nil || decoded.Step.ID == nil || *decoded.Step.ID != "0" {
t.Fatal("step.id mismatch")
}
if decoded.Nodes == nil || decoded.Nodes.Count == nil || *decoded.Nodes.Count != 2 {
t.Fatal("nodes.count mismatch")
}
if len(decoded.Nodes.List) != 2 {
t.Fatalf("nodes.list mismatch: %v", decoded.Nodes.List)
}
}