From f9234b216719f4073cd84016f22a3e4ee11daef6 Mon Sep 17 00:00:00 2001 From: dailz Date: Wed, 8 Apr 2026 18:29:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Node=20=E9=A2=86?= =?UTF-8?q?=E5=9F=9F=E7=B1=BB=E5=9E=8B=E5=92=8C=20NodesService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 包含 Node、UpdateNodeMsg、AcctGatherEnergy、ExtSensorsData、PowerMgmtData 等类型。NodesService 提供 GetNodes、GetNode、PostNode、DeleteNode 4 个方法,并定义 NodeFlag* 常量。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/slurm/slurm_nodes.go | 126 ++++++++++++ internal/slurm/slurm_nodes_test.go | 163 ++++++++++++++++ internal/slurm/types_node.go | 121 ++++++++++++ internal/slurm/types_node_test.go | 296 +++++++++++++++++++++++++++++ 4 files changed, 706 insertions(+) create mode 100644 internal/slurm/slurm_nodes.go create mode 100644 internal/slurm/slurm_nodes_test.go create mode 100644 internal/slurm/types_node.go create mode 100644 internal/slurm/types_node_test.go diff --git a/internal/slurm/slurm_nodes.go b/internal/slurm/slurm_nodes.go new file mode 100644 index 0000000..014aef9 --- /dev/null +++ b/internal/slurm/slurm_nodes.go @@ -0,0 +1,126 @@ +package slurm + +import ( + "context" + "fmt" + "net/url" +) + +// Node query flags for GetNodesOptions.Flags and GetNodeOptions.Flags. +const ( + NodeFlagAll = "ALL" + NodeFlagDetail = "DETAIL" + NodeFlagMixed = "MIXED" + NodeFlagLocal = "LOCAL" + NodeFlagSibling = "SIBLING" + NodeFlagFederation = "FEDERATION" + NodeFlagFuture = "FUTURE" +) + +// GetNodesOptions specifies optional parameters for GetNodes. +type GetNodesOptions struct { + UpdateTime *string `url:"update_time,omitempty"` + Flags *string `url:"flags,omitempty"` // Use NodeFlag* constants (e.g. NodeFlagDetail) +} + +// GetNodeOptions specifies optional parameters for GetNode. +type GetNodeOptions struct { + UpdateTime *string `url:"update_time,omitempty"` + Flags *string `url:"flags,omitempty"` // Use NodeFlag* constants (e.g. NodeFlagDetail) +} + +// GetNodes lists all nodes. +func (s *NodesService) GetNodes(ctx context.Context, opts *GetNodesOptions) (*OpenapiNodesResp, *Response, error) { + path := "slurm/v0.0.40/nodes" + 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 OpenapiNodesResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// GetNode gets a single node by name. +func (s *NodesService) GetNode(ctx context.Context, nodeName string, opts *GetNodeOptions) (*OpenapiNodesResp, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName) + 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 OpenapiNodesResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// PostNode updates a node. +func (s *NodesService) PostNode(ctx context.Context, nodeName string, update *UpdateNodeMsg) (*OpenapiResp, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName) + req, err := s.client.NewRequest("POST", path, update) + if err != nil { + return nil, nil, err + } + + var result OpenapiResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// DeleteNode deletes a node. +func (s *NodesService) DeleteNode(ctx context.Context, nodeName string) (*OpenapiResp, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + var result OpenapiResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} diff --git a/internal/slurm/slurm_nodes_test.go b/internal/slurm/slurm_nodes_test.go new file mode 100644 index 0000000..75db618 --- /dev/null +++ b/internal/slurm/slurm_nodes_test.go @@ -0,0 +1,163 @@ +package slurm + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestNodesService_GetNodes(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/nodes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"nodes": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Nodes.GetNodes(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestNodesService_GetNodes_WithOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/nodes", 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") != NodeFlagDetail { + t.Errorf("expected flags=%s, got %s", NodeFlagDetail, q.Get("flags")) + } + fmt.Fprint(w, `{"nodes": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetNodesOptions{ + UpdateTime: Ptr("12345"), + Flags: Ptr(NodeFlagDetail), + } + resp, _, err := client.Nodes.GetNodes(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestNodesService_GetNode(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/node/node1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"nodes": [{"name": "node1"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Nodes.GetNode(context.Background(), "node1", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if resp.Nodes == nil || len(*resp.Nodes) != 1 { + t.Fatalf("expected 1 node, got %v", resp.Nodes) + } + if resp.Nodes == nil || (*resp.Nodes)[0].Name == nil || *(*resp.Nodes)[0].Name != "node1" { + t.Errorf("expected name=node1, got %v", resp.Nodes) + } +} + +func TestNodesService_PostNode(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/node/node1", 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, `{}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + update := &UpdateNodeMsg{ + Comment: Ptr("updated comment"), + } + resp, _, err := client.Nodes.PostNode(context.Background(), "node1", update) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestNodesService_DeleteNode(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/node/node1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + fmt.Fprint(w, `{}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Nodes.DeleteNode(context.Background(), "node1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestNodesService_GetNode_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/node/nonexistent", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, `{"errors": [{"error": "node not found"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + _, _, err := client.Nodes.GetNode(context.Background(), "nonexistent", nil) + if err == nil { + t.Fatal("expected error for 404 response") + } + if !strings.Contains(err.Error(), "404") { + t.Errorf("expected error to contain 404, got %v", err) + } +} + +func TestNodesService_GetNodes_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/nodes", 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.Nodes.GetNodes(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 500 response") + } +} diff --git a/internal/slurm/types_node.go b/internal/slurm/types_node.go new file mode 100644 index 0000000..4863f24 --- /dev/null +++ b/internal/slurm/types_node.go @@ -0,0 +1,121 @@ +package slurm + +// AcctGatherEnergy represents energy accounting data (v0.0.40_acct_gather_energy). +type AcctGatherEnergy struct { + AverageWatts *int32 `json:"average_watts,omitempty"` + BaseConsumedEnergy *int64 `json:"base_consumed_energy,omitempty"` + ConsumedEnergy *int64 `json:"consumed_energy,omitempty"` + CurrentWatts *Uint32NoVal `json:"current_watts,omitempty"` + PreviousConsumedEnergy *int64 `json:"previous_consumed_energy,omitempty"` + LastCollected *int64 `json:"last_collected,omitempty"` +} + +// ExtSensorsData represents external sensor data (v0.0.40_ext_sensors_data). +type ExtSensorsData struct { + ConsumedEnergy *Uint64NoVal `json:"consumed_energy,omitempty"` + Temperature *Uint32NoVal `json:"temperature,omitempty"` + EnergyUpdateTime *int64 `json:"energy_update_time,omitempty"` + CurrentWatts *int32 `json:"current_watts,omitempty"` +} + +// PowerMgmtData represents power management data (v0.0.40_power_mgmt_data). +type PowerMgmtData struct { + MaximumWatts *Uint32NoVal `json:"maximum_watts,omitempty"` + CurrentWatts *int32 `json:"current_watts,omitempty"` + TotalEnergy *int64 `json:"total_energy,omitempty"` + NewMaximumWatts *int32 `json:"new_maximum_watts,omitempty"` + PeakWatts *int32 `json:"peak_watts,omitempty"` + LowestWatts *int32 `json:"lowest_watts,omitempty"` + NewJobTime *Uint64NoVal `json:"new_job_time,omitempty"` + State *int32 `json:"state,omitempty"` + TimeStartDay *int64 `json:"time_start_day,omitempty"` +} + +// Node represents a Slurm compute node (v0.0.40_node). +type Node struct { + Architecture *string `json:"architecture,omitempty"` + BurstbufferNetworkAddress *string `json:"burstbuffer_network_address,omitempty"` + Boards *int32 `json:"boards,omitempty"` + BootTime *Uint64NoVal `json:"boot_time,omitempty"` + ClusterName *string `json:"cluster_name,omitempty"` + Cores *int32 `json:"cores,omitempty"` + SpecializedCores *int32 `json:"specialized_cores,omitempty"` + CpuBinding *int32 `json:"cpu_binding,omitempty"` + CpuLoad *int32 `json:"cpu_load,omitempty"` + FreeMem *Uint64NoVal `json:"free_mem,omitempty"` + Cpus *int32 `json:"cpus,omitempty"` + EffectiveCpus *int32 `json:"effective_cpus,omitempty"` + SpecializedCpus *string `json:"specialized_cpus,omitempty"` + Energy *AcctGatherEnergy `json:"energy,omitempty"` + ExternalSensors *ExtSensorsData `json:"external_sensors,omitempty"` + Extra *string `json:"extra,omitempty"` + Power *PowerMgmtData `json:"power,omitempty"` + Features *CSVString `json:"features,omitempty"` + ActiveFeatures *CSVString `json:"active_features,omitempty"` + Gres *string `json:"gres,omitempty"` + GresDrained *string `json:"gres_drained,omitempty"` + GresUsed *string `json:"gres_used,omitempty"` + InstanceID *string `json:"instance_id,omitempty"` + InstanceType *string `json:"instance_type,omitempty"` + LastBusy *Uint64NoVal `json:"last_busy,omitempty"` + McsLabel *string `json:"mcs_label,omitempty"` + SpecializedMemory *int64 `json:"specialized_memory,omitempty"` + Name *string `json:"name,omitempty"` + NextStateAfterReboot []string `json:"next_state_after_reboot,omitempty"` + Address *string `json:"address,omitempty"` + Hostname *string `json:"hostname,omitempty"` + State []string `json:"state,omitempty"` + OperatingSystem *string `json:"operating_system,omitempty"` + Owner *string `json:"owner,omitempty"` + Partitions *CSVString `json:"partitions,omitempty"` + Port *int32 `json:"port,omitempty"` + RealMemory *int64 `json:"real_memory,omitempty"` + Comment *string `json:"comment,omitempty"` + Reason *string `json:"reason,omitempty"` + ReasonChangedAt *Uint64NoVal `json:"reason_changed_at,omitempty"` + ReasonSetByUser *string `json:"reason_set_by_user,omitempty"` + ResumeAfter *Uint64NoVal `json:"resume_after,omitempty"` + Reservation *string `json:"reservation,omitempty"` + AllocMemory *int64 `json:"alloc_memory,omitempty"` + AllocCpus *int32 `json:"alloc_cpus,omitempty"` + AllocIdleCpus *int32 `json:"alloc_idle_cpus,omitempty"` + TresUsed *string `json:"tres_used,omitempty"` + TresWeighted *float64 `json:"tres_weighted,omitempty"` + SlurmdStartTime *Uint64NoVal `json:"slurmd_start_time,omitempty"` + Sockets *int32 `json:"sockets,omitempty"` + Threads *int32 `json:"threads,omitempty"` + TemporaryDisk *int32 `json:"temporary_disk,omitempty"` + Weight *int32 `json:"weight,omitempty"` + Tres *string `json:"tres,omitempty"` + Version *string `json:"version,omitempty"` +} + +// Nodes is a collection of Node objects (v0.0.40_nodes). +type Nodes []Node + +// UpdateNodeMsg represents a node update request (v0.0.40_update_node_msg). +type UpdateNodeMsg struct { + Comment *string `json:"comment,omitempty"` + CpuBind *int32 `json:"cpu_bind,omitempty"` + Extra *string `json:"extra,omitempty"` + Features *CSVString `json:"features,omitempty"` + FeaturesAct *CSVString `json:"features_act,omitempty"` + Gres *string `json:"gres,omitempty"` + Address *HostlistString `json:"address,omitempty"` + Hostname *HostlistString `json:"hostname,omitempty"` + Name *HostlistString `json:"name,omitempty"` + State []string `json:"state,omitempty"` + Reason *string `json:"reason,omitempty"` + ReasonUID *string `json:"reason_uid,omitempty"` + ResumeAfter *Uint32NoVal `json:"resume_after,omitempty"` + Weight *Uint32NoVal `json:"weight,omitempty"` +} + +// OpenapiNodesResp represents the response for node queries (v0.0.40_openapi_nodes_resp). +type OpenapiNodesResp struct { + Nodes *Nodes `json:"nodes,omitempty"` + LastUpdate *Uint64NoVal `json:"last_update,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} diff --git a/internal/slurm/types_node_test.go b/internal/slurm/types_node_test.go new file mode 100644 index 0000000..9c08576 --- /dev/null +++ b/internal/slurm/types_node_test.go @@ -0,0 +1,296 @@ +package slurm + +import ( + "encoding/json" + "testing" +) + +func TestNodeRoundTrip(t *testing.T) { + orig := Node{ + Architecture: Ptr("x86_64"), + Boards: Ptr(int32(1)), + BootTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + ClusterName: Ptr("test-cluster"), + Cores: Ptr(int32(16)), + CpuLoad: Ptr(int32(50)), + FreeMem: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(32768))}, + Cpus: Ptr(int32(64)), + Energy: &AcctGatherEnergy{ + AverageWatts: Ptr(int32(200)), + ConsumedEnergy: Ptr(int64(15000)), + CurrentWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(180))}, + LastCollected: Ptr(int64(1234567900)), + }, + ExternalSensors: &ExtSensorsData{ + CurrentWatts: Ptr(int32(175)), + ConsumedEnergy: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(12000))}, + EnergyUpdateTime: Ptr(int64(1234567900)), + }, + Power: &PowerMgmtData{ + CurrentWatts: Ptr(int32(200)), + PeakWatts: Ptr(int32(350)), + LowestWatts: Ptr(int32(100)), + }, + Features: &CSVString{"gpu", "nvme"}, + ActiveFeatures: &CSVString{"gpu"}, + Gres: Ptr("gpu:4"), + Name: Ptr("node01"), + State: []string{"IDLE", "POWERED_DOWN"}, + Hostname: Ptr("node01.example.com"), + Address: Ptr("10.0.0.1"), + Partitions: &CSVString{"normal", "gpu"}, + Port: Ptr(int32(6818)), + RealMemory: Ptr(int64(128000)), + Sockets: Ptr(int32(2)), + Threads: Ptr(int32(2)), + Weight: Ptr(int32(1)), + Tres: Ptr("cpu=64,mem=128G"), + Version: Ptr("24.05.5"), + AllocCpus: Ptr(int32(0)), + AllocMemory: Ptr(int64(0)), + SlurmdStartTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234500000))}, + ReasonChangedAt: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(0))}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Node + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name == nil || *decoded.Name != "node01" { + t.Fatalf("name mismatch: %v", decoded.Name) + } + if decoded.Cpus == nil || *decoded.Cpus != 64 { + t.Fatalf("cpus mismatch: %v", decoded.Cpus) + } + if len(decoded.State) != 2 || decoded.State[0] != "IDLE" { + t.Fatalf("state mismatch: %v", decoded.State) + } + if decoded.Energy == nil || decoded.Energy.AverageWatts == nil || *decoded.Energy.AverageWatts != 200 { + t.Fatalf("energy.average_watts mismatch: %v", decoded.Energy) + } + if decoded.ExternalSensors == nil || decoded.ExternalSensors.CurrentWatts == nil || *decoded.ExternalSensors.CurrentWatts != 175 { + t.Fatalf("external_sensors.current_watts mismatch: %v", decoded.ExternalSensors) + } + if decoded.Power == nil || decoded.Power.PeakWatts == nil || *decoded.Power.PeakWatts != 350 { + t.Fatalf("power.peak_watts mismatch: %v", decoded.Power) + } + if decoded.Features == nil || len(*decoded.Features) != 2 || (*decoded.Features)[0] != "gpu" { + t.Fatalf("features mismatch: %v", decoded.Features) + } + if decoded.BootTime == nil || decoded.BootTime.Number == nil || *decoded.BootTime.Number != 1234567890 { + t.Fatalf("boot_time mismatch: %v", decoded.BootTime) + } +} + +func TestNodeEmptyRoundTrip(t *testing.T) { + orig := Node{} + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if string(data) != "{}" { + t.Fatalf("empty Node should marshal to {}, got %s", data) + } + var decoded Node + if err := json.Unmarshal([]byte(`{}`), &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name != nil { + t.Fatal("name should be nil for empty object") + } +} + +func TestUpdateNodeMsgRoundTrip(t *testing.T) { + orig := UpdateNodeMsg{ + Comment: Ptr("maintenance window"), + CpuBind: Ptr(int32(1)), + Extra: Ptr("custom data"), + Features: &CSVString{"new_feat"}, + FeaturesAct: &CSVString{"active_feat"}, + Gres: Ptr("gpu:2"), + Address: &HostlistString{"node01"}, + Hostname: &HostlistString{"node01.example.com"}, + Name: &HostlistString{"node01"}, + State: []string{"DRAIN"}, + Reason: Ptr("scheduled maintenance"), + ReasonUID: Ptr("root"), + ResumeAfter: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(3600))}, + Weight: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(10))}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded UpdateNodeMsg + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Comment == nil || *decoded.Comment != "maintenance window" { + t.Fatalf("comment mismatch: %v", decoded.Comment) + } + if len(decoded.State) != 1 || decoded.State[0] != "DRAIN" { + t.Fatalf("state mismatch: %v", decoded.State) + } + if decoded.ResumeAfter == nil || decoded.ResumeAfter.Number == nil || *decoded.ResumeAfter.Number != 3600 { + t.Fatalf("resume_after mismatch: %v", decoded.ResumeAfter) + } + if decoded.Weight == nil || decoded.Weight.Number == nil || *decoded.Weight.Number != 10 { + t.Fatalf("weight mismatch: %v", decoded.Weight) + } + if decoded.Name == nil || len(*decoded.Name) != 1 || (*decoded.Name)[0] != "node01" { + t.Fatalf("name mismatch: %v", decoded.Name) + } +} + +func TestOpenapiNodesRespRoundTrip(t *testing.T) { + nodes := Nodes{ + { + Name: Ptr("node01"), + State: []string{"IDLE"}, + Cpus: Ptr(int32(64)), + RealMemory: Ptr(int64(128000)), + }, + { + Name: Ptr("node02"), + State: []string{"ALLOCATED"}, + Cpus: Ptr(int32(32)), + RealMemory: Ptr(int64(64000)), + }, + } + orig := OpenapiNodesResp{ + Nodes: &nodes, + LastUpdate: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + Meta: &OpenapiMeta{ + Slurm: &MetaSlurm{ + Version: &MetaSlurmVersion{ + Major: Ptr("24"), + Micro: Ptr("5"), + Minor: Ptr("05"), + }, + }, + }, + Errors: OpenapiErrors{}, + Warnings: OpenapiWarnings{}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded OpenapiNodesResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Nodes == nil || len(*decoded.Nodes) != 2 { + t.Fatalf("expected 2 nodes, got %d", len(*decoded.Nodes)) + } + nodesSlice := *decoded.Nodes + if nodesSlice[0].Name == nil || *nodesSlice[0].Name != "node01" { + t.Fatalf("nodes[0].name mismatch") + } + if nodesSlice[1].Cpus == nil || *nodesSlice[1].Cpus != 32 { + t.Fatalf("nodes[1].cpus mismatch") + } + if decoded.LastUpdate == nil || decoded.LastUpdate.Number == nil || *decoded.LastUpdate.Number != 1234567890 { + t.Fatalf("last_update mismatch: %v", decoded.LastUpdate) + } + if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Version == nil { + t.Fatal("meta.slurm.version should not be nil") + } +} + +func TestAcctGatherEnergyRoundTrip(t *testing.T) { + orig := AcctGatherEnergy{ + AverageWatts: Ptr(int32(250)), + BaseConsumedEnergy: Ptr(int64(10000)), + ConsumedEnergy: Ptr(int64(50000)), + CurrentWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(210))}, + PreviousConsumedEnergy: Ptr(int64(40000)), + LastCollected: Ptr(int64(1234567999)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded AcctGatherEnergy + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.AverageWatts == nil || *decoded.AverageWatts != 250 { + t.Fatalf("average_watts mismatch: %v", decoded.AverageWatts) + } + if decoded.ConsumedEnergy == nil || *decoded.ConsumedEnergy != 50000 { + t.Fatalf("consumed_energy mismatch: %v", decoded.ConsumedEnergy) + } + if decoded.CurrentWatts == nil || decoded.CurrentWatts.Number == nil || *decoded.CurrentWatts.Number != 210 { + t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts) + } +} + +func TestExtSensorsDataRoundTrip(t *testing.T) { + orig := ExtSensorsData{ + ConsumedEnergy: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(99999))}, + Temperature: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(42))}, + EnergyUpdateTime: Ptr(int64(1234567900)), + CurrentWatts: Ptr(int32(190)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded ExtSensorsData + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.ConsumedEnergy == nil || decoded.ConsumedEnergy.Number == nil || *decoded.ConsumedEnergy.Number != 99999 { + t.Fatalf("consumed_energy mismatch: %v", decoded.ConsumedEnergy) + } + if decoded.Temperature == nil || decoded.Temperature.Number == nil || *decoded.Temperature.Number != 42 { + t.Fatalf("temperature mismatch: %v", decoded.Temperature) + } + if decoded.CurrentWatts == nil || *decoded.CurrentWatts != 190 { + t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts) + } +} + +func TestPowerMgmtDataRoundTrip(t *testing.T) { + orig := PowerMgmtData{ + MaximumWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(400))}, + CurrentWatts: Ptr(int32(220)), + TotalEnergy: Ptr(int64(500000)), + NewMaximumWatts: Ptr(int32(450)), + PeakWatts: Ptr(int32(380)), + LowestWatts: Ptr(int32(90)), + NewJobTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567999))}, + State: Ptr(int32(1)), + TimeStartDay: Ptr(int64(1234500000)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded PowerMgmtData + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.MaximumWatts == nil || decoded.MaximumWatts.Number == nil || *decoded.MaximumWatts.Number != 400 { + t.Fatalf("maximum_watts mismatch: %v", decoded.MaximumWatts) + } + if decoded.CurrentWatts == nil || *decoded.CurrentWatts != 220 { + t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts) + } + if decoded.TotalEnergy == nil || *decoded.TotalEnergy != 500000 { + t.Fatalf("total_energy mismatch: %v", decoded.TotalEnergy) + } + if decoded.PeakWatts == nil || *decoded.PeakWatts != 380 { + t.Fatalf("peak_watts mismatch: %v", decoded.PeakWatts) + } + if decoded.NewJobTime == nil || decoded.NewJobTime.Number == nil || *decoded.NewJobTime.Number != 1234567999 { + t.Fatalf("new_job_time mismatch: %v", decoded.NewJobTime) + } + if decoded.State == nil || *decoded.State != 1 { + t.Fatalf("state mismatch: %v", decoded.State) + } +}