diff --git a/internal/slurm/types_slurmdb.go b/internal/slurm/types_slurmdb.go new file mode 100644 index 0000000..bfe539c --- /dev/null +++ b/internal/slurm/types_slurmdb.go @@ -0,0 +1,309 @@ +package slurm + +// --------------------------------------------------------------------------- +// SlurmDBD Response wrapper types — v0.0.40 OpenAPI SlurmDBD schemas +// All fields are optional (pointer types) with json:"name,omitempty". +// --------------------------------------------------------------------------- + +// OpenapiSlurmdbdJobsResp is the response for listing SlurmDBD jobs. +// Corresponds to v0.0.40_openapi_slurmdbd_jobs_resp. +type OpenapiSlurmdbdJobsResp struct { + Jobs JobList `json:"jobs,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiSlurmdbdConfigResp is the response for SlurmDBD configuration. +// Corresponds to v0.0.40_openapi_slurmdbd_config_resp. +type OpenapiSlurmdbdConfigResp struct { + Clusters ClusterRecList `json:"clusters,omitempty"` + Tres TresList `json:"tres,omitempty"` + Accounts AccountList `json:"accounts,omitempty"` + Users UserList `json:"users,omitempty"` + Qos QosList `json:"qos,omitempty"` + Wckeys WckeyList `json:"wckeys,omitempty"` + Associations AssocList `json:"associations,omitempty"` + Instances InstanceList `json:"instances,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiSlurmdbdQosResp is the response for listing QOS. +// Corresponds to v0.0.40_openapi_slurmdbd_qos_resp. +type OpenapiSlurmdbdQosResp struct { + Qos QosList `json:"qos,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiSlurmdbdQosRemovedResp is the response for removed QOS. +// Corresponds to v0.0.40_openapi_slurmdbd_qos_removed_resp. +type OpenapiSlurmdbdQosRemovedResp struct { + RemovedQos StringList `json:"removed_qos,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiSlurmdbdStatsResp is the response for SlurmDBD diagnostics. +// Corresponds to v0.0.40_openapi_slurmdbd_stats_resp. +type OpenapiSlurmdbdStatsResp struct { + Statistics *StatsRec `json:"statistics,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAssocsResp is the response for listing associations. +// Corresponds to v0.0.40_openapi_assocs_resp. +type OpenapiAssocsResp struct { + Associations AssocList `json:"associations,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAssocsRemovedResp is the response for removed associations. +// Corresponds to v0.0.40_openapi_assocs_removed_resp. +type OpenapiAssocsRemovedResp struct { + RemovedAssociations StringList `json:"removed_associations,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiUsersResp is the response for listing users. +// Corresponds to v0.0.40_openapi_users_resp. +type OpenapiUsersResp struct { + Users UserList `json:"users,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiClustersResp is the response for listing clusters. +// Corresponds to v0.0.40_openapi_clusters_resp. +type OpenapiClustersResp struct { + Clusters ClusterRecList `json:"clusters,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiClustersRemovedResp is the response for deleted clusters. +// Corresponds to v0.0.40_openapi_clusters_removed_resp. +type OpenapiClustersRemovedResp struct { + DeletedClusters StringList `json:"deleted_clusters,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiWckeyResp is the response for listing WCKeys. +// Corresponds to v0.0.40_openapi_wckey_resp. +type OpenapiWckeyResp struct { + Wckeys WckeyList `json:"wckeys,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiWckeyRemovedResp is the response for deleted WCKeys. +// Corresponds to v0.0.40_openapi_wckey_removed_resp. +type OpenapiWckeyRemovedResp struct { + DeletedWckeys StringList `json:"deleted_wckeys,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAccountsResp is the response for listing accounts. +// Corresponds to v0.0.40_openapi_accounts_resp. +type OpenapiAccountsResp struct { + Accounts AccountList `json:"accounts,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAccountsRemovedResp is the response for removed accounts. +// Corresponds to v0.0.40_openapi_accounts_removed_resp. +type OpenapiAccountsRemovedResp struct { + RemovedAccounts StringList `json:"removed_accounts,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiInstancesResp is the response for listing instances. +// Corresponds to v0.0.40_openapi_instances_resp. +type OpenapiInstancesResp struct { + Instances InstanceList `json:"instances,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiTresResp is the response for listing TRES. +// Corresponds to v0.0.40_openapi_tres_resp. +// Note: JSON key for TRES is UPPERCASE per OpenAPI spec. +type OpenapiTresResp struct { + TRES TresList `json:"TRES,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiUsersAddCondResp is the response for adding users with conditions. +// Corresponds to v0.0.40_openapi_users_add_cond_resp. +type OpenapiUsersAddCondResp struct { + AssociationCondition *UsersAddCond `json:"association_condition,omitempty"` + User *UserShort `json:"user,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiUsersAddCondRespStr is the string response for adding users with conditions. +// Corresponds to v0.0.40_openapi_users_add_cond_resp_str. +type OpenapiUsersAddCondRespStr struct { + AddedUsers *string `json:"added_users,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAccountsAddCondResp is the response for adding accounts with conditions. +// Corresponds to v0.0.40_openapi_accounts_add_cond_resp. +type OpenapiAccountsAddCondResp struct { + AssociationCondition *AccountsAddCond `json:"association_condition,omitempty"` + Account *AccountShort `json:"account,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiAccountsAddCondRespStr is the string response for adding accounts with conditions. +// Corresponds to v0.0.40_openapi_accounts_add_cond_resp_str. +type OpenapiAccountsAddCondRespStr struct { + AddedAccounts *string `json:"added_accounts,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// --------------------------------------------------------------------------- +// Supporting types for SlurmDBD diagnostics +// --------------------------------------------------------------------------- + +// StatsRec represents SlurmDBD diagnostic statistics. +// Corresponds to v0.0.40_stats_rec. +type StatsRec struct { + TimeStart *int64 `json:"time_start,omitempty"` + Rollups RollupStatsList `json:"rollups,omitempty"` + RPCs StatsRpcList `json:"RPCs,omitempty"` + Users StatsUserList `json:"users,omitempty"` +} + +// RollupStatsList is a list of RollupStats. +// Corresponds to v0.0.40_rollup_stats. +type RollupStatsList []RollupStats + +// RollupStats represents recorded rollup statistics. +type RollupStats struct { + Type *string `json:"type,omitempty"` + LastRun *int32 `json:"last run,omitempty"` + MaxCycle *int64 `json:"max_cycle,omitempty"` + TotalTime *int64 `json:"total_time,omitempty"` + TotalCycles *int64 `json:"total_cycles,omitempty"` + MeanCycles *int64 `json:"mean_cycles,omitempty"` +} + +// StatsRpcList is a list of StatsRpc. +// Corresponds to v0.0.40_stats_rpc_list. +type StatsRpcList []StatsRpc + +// StatsRpc represents RPC statistics. +// Corresponds to v0.0.40_stats_rpc. +type StatsRpc struct { + RPC *string `json:"rpc,omitempty"` + Count *int32 `json:"count,omitempty"` + Time *StatsRpcTime `json:"time,omitempty"` +} + +// StatsRpcTime represents timing information for RPC statistics. +type StatsRpcTime struct { + Average *int64 `json:"average,omitempty"` + Total *int64 `json:"total,omitempty"` +} + +// StatsUserList is a list of StatsUser. +// Corresponds to v0.0.40_stats_user_list. +type StatsUserList []StatsUser + +// StatsUser represents per-user statistics. +// Corresponds to v0.0.40_stats_user. +type StatsUser struct { + User *string `json:"user,omitempty"` + Count *int32 `json:"count,omitempty"` + Time *StatsUserTime `json:"time,omitempty"` +} + +// StatsUserTime represents timing information for user statistics. +type StatsUserTime struct { + Average *int64 `json:"average,omitempty"` + Total *int64 `json:"total,omitempty"` +} + +// --------------------------------------------------------------------------- +// UsersAddCond and AccountsAddCond — association condition types +// --------------------------------------------------------------------------- + +// UsersAddCond represents filters to select associations for users. +// Corresponds to v0.0.40_users_add_cond. +type UsersAddCond struct { + Accounts StringList `json:"accounts,omitempty"` + Association *AssocRecSet `json:"association,omitempty"` + Clusters StringList `json:"clusters,omitempty"` + Partitions StringList `json:"partitions,omitempty"` + Users StringList `json:"users,omitempty"` + Wckeys StringList `json:"wckeys,omitempty"` +} + +// AccountsAddCond represents filters for adding accounts. +// Corresponds to v0.0.40_accounts_add_cond. +type AccountsAddCond struct { + Accounts StringList `json:"accounts,omitempty"` + Association *AssocRecSet `json:"association,omitempty"` + Clusters StringList `json:"clusters,omitempty"` +} + +// AssocRecSet represents association limits and options for user/account creation. +// Corresponds to v0.0.40_assoc_rec_set. +type AssocRecSet struct { + Comment *string `json:"comment,omitempty"` + Defaultqos *string `json:"defaultqos,omitempty"` + Grpjobs *Uint32NoVal `json:"grpjobs,omitempty"` + Grpjobsaccrue *Uint32NoVal `json:"grpjobsaccrue,omitempty"` + Grpsubmitjobs *Uint32NoVal `json:"grpsubmitjobs,omitempty"` + Grptres TresList `json:"grptres,omitempty"` + Grptresmins TresList `json:"grptresmins,omitempty"` + Grptresrunmins TresList `json:"grptresrunmins,omitempty"` + Grpwall *Uint32NoVal `json:"grpwall,omitempty"` + Maxjobs *Uint32NoVal `json:"maxjobs,omitempty"` + Maxjobsaccrue *Uint32NoVal `json:"maxjobsaccrue,omitempty"` + Maxsubmitjobs *Uint32NoVal `json:"maxsubmitjobs,omitempty"` + Maxtresminsperjob TresList `json:"maxtresminsperjob,omitempty"` + Maxtresrunmins TresList `json:"maxtresrunmins,omitempty"` + Maxtresperjob TresList `json:"maxtresperjob,omitempty"` + Maxtrespernode TresList `json:"maxtrespernode,omitempty"` + Maxwalldurationperjob *Uint32NoVal `json:"maxwalldurationperjob,omitempty"` + Minpriothresh *Uint32NoVal `json:"minpriothresh,omitempty"` + Parent *string `json:"parent,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + Qoslevel QosStringIdList `json:"qoslevel,omitempty"` + Fairshare *int32 `json:"fairshare,omitempty"` +} diff --git a/internal/slurm/types_slurmdb_test.go b/internal/slurm/types_slurmdb_test.go new file mode 100644 index 0000000..b7dd972 --- /dev/null +++ b/internal/slurm/types_slurmdb_test.go @@ -0,0 +1,816 @@ +package slurm + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestSlurmdbJobsRespRoundTrip(t *testing.T) { + orig := OpenapiSlurmdbdJobsResp{ + Jobs: JobList{ + {JobID: Ptr(int32(1)), Name: Ptr("test-job")}, + {JobID: Ptr(int32(2)), Name: Ptr("other-job")}, + }, + Meta: &OpenapiMeta{}, + Errors: OpenapiErrors{}, + Warnings: OpenapiWarnings{}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiSlurmdbdJobsResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Jobs) != 2 { + t.Fatalf("expected 2 jobs, got %d", len(decoded.Jobs)) + } + if *decoded.Jobs[0].JobID != 1 { + t.Fatal("first job id mismatch") + } + if *decoded.Jobs[1].Name != "other-job" { + t.Fatal("second job name mismatch") + } +} + +func TestSlurmdbConfigRespRoundTrip(t *testing.T) { + orig := OpenapiSlurmdbdConfigResp{ + Clusters: ClusterRecList{ + {Name: Ptr("cluster1")}, + }, + Tres: TresList{ + {Type: Ptr("cpu"), ID: Ptr(int32(1))}, + }, + Accounts: AccountList{ + {Name: Ptr("acct1")}, + }, + Users: UserList{ + {Name: Ptr("user1")}, + }, + Qos: QosList{ + {Name: Ptr("normal")}, + }, + Wckeys: WckeyList{ + {Name: Ptr("wckey1")}, + }, + Associations: AssocList{ + {Account: Ptr("acct1"), User: Ptr("user1")}, + }, + Instances: InstanceList{ + {InstanceID: Ptr("i-123")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + // Verify lowercase "tres" key in JSON + raw := string(data) + if strings.Contains(raw, `"TRES"`) { + t.Fatal("OpenapiSlurmdbdConfigResp should use lowercase 'tres', not uppercase 'TRES'") + } + if !strings.Contains(raw, `"tres"`) { + t.Fatal("OpenapiSlurmdbdConfigResp should contain lowercase 'tres' key") + } + + var decoded OpenapiSlurmdbdConfigResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Clusters) != 1 || *decoded.Clusters[0].Name != "cluster1" { + t.Fatal("clusters mismatch") + } + if len(decoded.Tres) != 1 || *decoded.Tres[0].Type != "cpu" { + t.Fatal("tres mismatch") + } + if len(decoded.Accounts) != 1 || *decoded.Accounts[0].Name != "acct1" { + t.Fatal("accounts mismatch") + } + if len(decoded.Users) != 1 || *decoded.Users[0].Name != "user1" { + t.Fatal("users mismatch") + } + if len(decoded.Qos) != 1 || *decoded.Qos[0].Name != "normal" { + t.Fatal("qos mismatch") + } + if len(decoded.Wckeys) != 1 || *decoded.Wckeys[0].Name != "wckey1" { + t.Fatal("wckeys mismatch") + } + if len(decoded.Associations) != 1 || *decoded.Associations[0].Account != "acct1" { + t.Fatal("associations mismatch") + } + if len(decoded.Instances) != 1 || *decoded.Instances[0].InstanceID != "i-123" { + t.Fatal("instances mismatch") + } +} + +func TestSlurmdbQosRespRoundTrip(t *testing.T) { + orig := OpenapiSlurmdbdQosResp{ + Qos: QosList{ + {Name: Ptr("high"), ID: Ptr(int32(1))}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiSlurmdbdQosResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Qos) != 1 || *decoded.Qos[0].Name != "high" { + t.Fatal("qos mismatch") + } +} + +func TestSlurmdbQosRemovedRespRoundTrip(t *testing.T) { + orig := OpenapiSlurmdbdQosRemovedResp{ + RemovedQos: StringList{"normal", "high"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiSlurmdbdQosRemovedResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.RemovedQos) != 2 || decoded.RemovedQos[0] != "normal" { + t.Fatal("removed_qos mismatch") + } +} + +func TestSlurmdbStatsRespRoundTrip(t *testing.T) { + orig := OpenapiSlurmdbdStatsResp{ + Statistics: &StatsRec{ + TimeStart: Ptr(int64(1700000000)), + Rollups: RollupStatsList{ + {Type: Ptr("internal"), MaxCycle: Ptr(int64(5))}, + }, + RPCs: StatsRpcList{ + {RPC: Ptr("SLURMCTLD_GET_JOB_INFO"), Count: Ptr(int32(100))}, + }, + Users: StatsUserList{ + {User: Ptr("root"), Count: Ptr(int32(50))}, + }, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiSlurmdbdStatsResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if decoded.Statistics == nil { + t.Fatal("statistics is nil") + } + if *decoded.Statistics.TimeStart != 1700000000 { + t.Fatal("time_start mismatch") + } + if len(decoded.Statistics.Rollups) != 1 || *decoded.Statistics.Rollups[0].Type != "internal" { + t.Fatal("rollups mismatch") + } + if len(decoded.Statistics.RPCs) != 1 || *decoded.Statistics.RPCs[0].RPC != "SLURMCTLD_GET_JOB_INFO" { + t.Fatal("RPCs mismatch") + } + if len(decoded.Statistics.Users) != 1 || *decoded.Statistics.Users[0].User != "root" { + t.Fatal("users mismatch") + } +} + +func TestSlurmdbAssocsRespRoundTrip(t *testing.T) { + orig := OpenapiAssocsResp{ + Associations: AssocList{ + {Account: Ptr("acct1"), User: Ptr("user1")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAssocsResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Associations) != 1 || *decoded.Associations[0].Account != "acct1" { + t.Fatal("associations mismatch") + } +} + +func TestSlurmdbAssocsRemovedRespRoundTrip(t *testing.T) { + orig := OpenapiAssocsRemovedResp{ + RemovedAssociations: StringList{"acct1_user1_cluster1"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAssocsRemovedResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.RemovedAssociations) != 1 || decoded.RemovedAssociations[0] != "acct1_user1_cluster1" { + t.Fatal("removed_associations mismatch") + } +} + +func TestSlurmdbUsersRespRoundTrip(t *testing.T) { + orig := OpenapiUsersResp{ + Users: UserList{ + {Name: Ptr("admin"), AdministratorLevel: []string{"Administrator"}}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiUsersResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Users) != 1 || *decoded.Users[0].Name != "admin" { + t.Fatal("users mismatch") + } +} + +func TestSlurmdbClustersRespRoundTrip(t *testing.T) { + orig := OpenapiClustersResp{ + Clusters: ClusterRecList{ + {Name: Ptr("test-cluster"), Nodes: Ptr("node[1-10]")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiClustersResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Clusters) != 1 || *decoded.Clusters[0].Name != "test-cluster" { + t.Fatal("clusters mismatch") + } +} + +func TestSlurmdbClustersRemovedRespRoundTrip(t *testing.T) { + orig := OpenapiClustersRemovedResp{ + DeletedClusters: StringList{"old-cluster"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiClustersRemovedResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.DeletedClusters) != 1 || decoded.DeletedClusters[0] != "old-cluster" { + t.Fatal("deleted_clusters mismatch") + } +} + +func TestSlurmdbWckeyRespRoundTrip(t *testing.T) { + orig := OpenapiWckeyResp{ + Wckeys: WckeyList{ + {Name: Ptr("wckey1"), Cluster: Ptr("cluster1")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiWckeyResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Wckeys) != 1 || *decoded.Wckeys[0].Name != "wckey1" { + t.Fatal("wckeys mismatch") + } +} + +func TestSlurmdbWckeyRemovedRespRoundTrip(t *testing.T) { + orig := OpenapiWckeyRemovedResp{ + DeletedWckeys: StringList{"old-wckey"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiWckeyRemovedResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.DeletedWckeys) != 1 || decoded.DeletedWckeys[0] != "old-wckey" { + t.Fatal("deleted_wckeys mismatch") + } +} + +func TestSlurmdbAccountsRespRoundTrip(t *testing.T) { + orig := OpenapiAccountsResp{ + Accounts: AccountList{ + {Name: Ptr("science"), Description: Ptr("Science dept"), Organization: Ptr("org1")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAccountsResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Accounts) != 1 || *decoded.Accounts[0].Name != "science" { + t.Fatal("accounts mismatch") + } +} + +func TestSlurmdbAccountsRemovedRespRoundTrip(t *testing.T) { + orig := OpenapiAccountsRemovedResp{ + RemovedAccounts: StringList{"old-acct"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAccountsRemovedResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.RemovedAccounts) != 1 || decoded.RemovedAccounts[0] != "old-acct" { + t.Fatal("removed_accounts mismatch") + } +} + +func TestSlurmdbInstancesRespRoundTrip(t *testing.T) { + orig := OpenapiInstancesResp{ + Instances: InstanceList{ + {InstanceID: Ptr("i-abc123"), Cluster: Ptr("cluster1")}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiInstancesResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Instances) != 1 || *decoded.Instances[0].InstanceID != "i-abc123" { + t.Fatal("instances mismatch") + } +} + +func TestSlurmdbTresRespRoundTrip(t *testing.T) { + orig := OpenapiTresResp{ + TRES: TresList{ + {Type: Ptr("cpu"), ID: Ptr(int32(1)), Count: Ptr(int64(100))}, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + // Verify UPPERCASE "TRES" key in JSON + raw := string(data) + if !strings.Contains(raw, `"TRES"`) { + t.Fatalf("OpenapiTresResp should use UPPERCASE 'TRES' key, got: %s", raw) + } + if strings.Contains(raw, `"tres"`) { + t.Fatal("OpenapiTresResp should NOT contain lowercase 'tres'") + } + + var decoded OpenapiTresResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.TRES) != 1 || *decoded.TRES[0].Type != "cpu" { + t.Fatal("TRES mismatch") + } +} + +func TestSlurmdbTresRespUppercaseKey(t *testing.T) { + raw := `{"TRES": [{"type": "mem", "id": 2, "count": 1000}]}` + var decoded OpenapiTresResp + if err := json.Unmarshal([]byte(raw), &decoded); err != nil { + t.Fatal(err) + } + if len(decoded.TRES) != 1 || *decoded.TRES[0].Type != "mem" { + t.Fatal("TRES uppercase key parse failed") + } +} + +func TestSlurmdbConfigRespLowercaseTresKey(t *testing.T) { + raw := `{"tres": [{"type": "cpu", "id": 1, "count": 100}]}` + var decoded OpenapiSlurmdbdConfigResp + if err := json.Unmarshal([]byte(raw), &decoded); err != nil { + t.Fatal(err) + } + if len(decoded.Tres) != 1 || *decoded.Tres[0].Type != "cpu" { + t.Fatal("config resp lowercase tres key parse failed") + } +} + +func TestSlurmdbUsersAddCondRespRoundTrip(t *testing.T) { + orig := OpenapiUsersAddCondResp{ + AssociationCondition: &UsersAddCond{ + Accounts: StringList{"acct1", "acct2"}, + Clusters: StringList{"cluster1"}, + Users: StringList{"user1", "user2"}, + Wckeys: StringList{"wckey1"}, + Partitions: StringList{"partition1"}, + }, + User: &UserShort{ + Adminlevel: []string{"Administrator"}, + Defaultaccount: Ptr("acct1"), + Defaultwckey: Ptr("wckey1"), + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiUsersAddCondResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if decoded.AssociationCondition == nil { + t.Fatal("association_condition is nil") + } + if len(decoded.AssociationCondition.Accounts) != 2 { + t.Fatal("accounts count mismatch") + } + if decoded.User == nil || *decoded.User.Defaultaccount != "acct1" { + t.Fatal("user mismatch") + } +} + +func TestSlurmdbUsersAddCondRespStrRoundTrip(t *testing.T) { + orig := OpenapiUsersAddCondRespStr{ + AddedUsers: Ptr("user1,user2,user3"), + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiUsersAddCondRespStr + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if decoded.AddedUsers == nil || *decoded.AddedUsers != "user1,user2,user3" { + t.Fatal("added_users mismatch") + } +} + +func TestSlurmdbAccountsAddCondRespRoundTrip(t *testing.T) { + orig := OpenapiAccountsAddCondResp{ + AssociationCondition: &AccountsAddCond{ + Accounts: StringList{"acct1"}, + Clusters: StringList{"cluster1"}, + }, + Account: &AccountShort{ + Description: Ptr("Science department"), + Organization: Ptr("university"), + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAccountsAddCondResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if decoded.AssociationCondition == nil || len(decoded.AssociationCondition.Accounts) != 1 { + t.Fatal("association_condition mismatch") + } + if decoded.Account == nil || *decoded.Account.Description != "Science department" { + t.Fatal("account mismatch") + } +} + +func TestSlurmdbAccountsAddCondRespStrRoundTrip(t *testing.T) { + orig := OpenapiAccountsAddCondRespStr{ + AddedAccounts: Ptr("acct1,acct2"), + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded OpenapiAccountsAddCondRespStr + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if decoded.AddedAccounts == nil || *decoded.AddedAccounts != "acct1,acct2" { + t.Fatal("added_accounts mismatch") + } +} + +func TestSlurmdbStatsRecRoundTrip(t *testing.T) { + orig := StatsRec{ + TimeStart: Ptr(int64(1700000000)), + Rollups: RollupStatsList{ + { + Type: Ptr("internal"), + LastRun: Ptr(int32(1700001000)), + MaxCycle: Ptr(int64(10)), + TotalTime: Ptr(int64(100)), + TotalCycles: Ptr(int64(50)), + MeanCycles: Ptr(int64(2)), + }, + }, + RPCs: StatsRpcList{ + { + RPC: Ptr("GET_JOBS"), + Count: Ptr(int32(500)), + Time: &StatsRpcTime{ + Average: Ptr(int64(100)), + Total: Ptr(int64(50000)), + }, + }, + }, + Users: StatsUserList{ + { + User: Ptr("admin"), + Count: Ptr(int32(200)), + Time: &StatsUserTime{ + Average: Ptr(int64(50)), + Total: Ptr(int64(10000)), + }, + }, + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded StatsRec + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if *decoded.TimeStart != 1700000000 { + t.Fatal("time_start mismatch") + } + if len(decoded.Rollups) != 1 || *decoded.Rollups[0].Type != "internal" { + t.Fatal("rollups mismatch") + } + if *decoded.Rollups[0].MaxCycle != 10 { + t.Fatal("max_cycle mismatch") + } + if len(decoded.RPCs) != 1 || *decoded.RPCs[0].RPC != "GET_JOBS" { + t.Fatal("RPCs mismatch") + } + if decoded.RPCs[0].Time == nil || *decoded.RPCs[0].Time.Average != 100 { + t.Fatal("RPC time.average mismatch") + } + if len(decoded.Users) != 1 || *decoded.Users[0].User != "admin" { + t.Fatal("users mismatch") + } + if decoded.Users[0].Time == nil || *decoded.Users[0].Time.Total != 10000 { + t.Fatal("user time.total mismatch") + } +} + +func TestSlurmdbStatsRecRPCsUppercaseKey(t *testing.T) { + raw := `{"time_start": 1700000000, "RPCs": [{"rpc": "TEST", "count": 1}]}` + var decoded StatsRec + if err := json.Unmarshal([]byte(raw), &decoded); err != nil { + t.Fatal(err) + } + if len(decoded.RPCs) != 1 || *decoded.RPCs[0].RPC != "TEST" { + t.Fatal("RPCs uppercase key parse failed") + } +} + +func TestSlurmdbRollupStatsRoundTrip(t *testing.T) { + orig := RollupStats{ + Type: Ptr("user"), + LastRun: Ptr(int32(1700002000)), + MaxCycle: Ptr(int64(20)), + TotalTime: Ptr(int64(200)), + TotalCycles: Ptr(int64(100)), + MeanCycles: Ptr(int64(2)), + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded RollupStats + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if *decoded.Type != "user" { + t.Fatal("type mismatch") + } + if *decoded.LastRun != 1700002000 { + t.Fatal("last run mismatch") + } + if *decoded.TotalCycles != 100 { + t.Fatal("total_cycles mismatch") + } +} + +func TestSlurmdbAssocRecSetRoundTrip(t *testing.T) { + orig := AssocRecSet{ + Comment: Ptr("test comment"), + Defaultqos: Ptr("normal"), + Fairshare: Ptr(int32(100)), + Parent: Ptr("parent-acct"), + Grptres: TresList{{Type: Ptr("cpu"), Count: Ptr(int64(1000))}}, + Maxtresperjob: TresList{{Type: Ptr("mem"), Count: Ptr(int64(4096))}}, + Qoslevel: QosStringIdList{"high", "normal"}, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded AssocRecSet + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if *decoded.Comment != "test comment" { + t.Fatal("comment mismatch") + } + if *decoded.Defaultqos != "normal" { + t.Fatal("defaultqos mismatch") + } + if *decoded.Fairshare != 100 { + t.Fatal("fairshare mismatch") + } + if len(decoded.Grptres) != 1 || *decoded.Grptres[0].Type != "cpu" { + t.Fatal("grptres mismatch") + } + if len(decoded.Qoslevel) != 2 || decoded.Qoslevel[0] != "high" { + t.Fatal("qoslevel mismatch") + } +} + +func TestSlurmdbUsersAddCondRoundTrip(t *testing.T) { + orig := UsersAddCond{ + Accounts: StringList{"acct1"}, + Clusters: StringList{"cluster1"}, + Partitions: StringList{"part1"}, + Users: StringList{"user1", "user2"}, + Wckeys: StringList{"wckey1"}, + Association: &AssocRecSet{ + Comment: Ptr("assoc comment"), + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded UsersAddCond + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Users) != 2 { + t.Fatal("users count mismatch") + } + if decoded.Association == nil || *decoded.Association.Comment != "assoc comment" { + t.Fatal("association mismatch") + } +} + +func TestSlurmdbAccountsAddCondRoundTrip(t *testing.T) { + orig := AccountsAddCond{ + Accounts: StringList{"acct1", "acct2"}, + Clusters: StringList{"cluster1"}, + Association: &AssocRecSet{ + Fairshare: Ptr(int32(50)), + }, + } + + data, err := json.Marshal(orig) + if err != nil { + t.Fatal(err) + } + + var decoded AccountsAddCond + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + + if len(decoded.Accounts) != 2 { + t.Fatal("accounts count mismatch") + } + if decoded.Association == nil || *decoded.Association.Fairshare != 50 { + t.Fatal("association mismatch") + } +} + +func TestSlurmdbEmptyResponses(t *testing.T) { + types := []interface{}{ + OpenapiSlurmdbdJobsResp{}, + OpenapiSlurmdbdConfigResp{}, + OpenapiSlurmdbdQosResp{}, + OpenapiSlurmdbdQosRemovedResp{}, + OpenapiSlurmdbdStatsResp{}, + OpenapiAssocsResp{}, + OpenapiAssocsRemovedResp{}, + OpenapiUsersResp{}, + OpenapiClustersResp{}, + OpenapiClustersRemovedResp{}, + OpenapiWckeyResp{}, + OpenapiWckeyRemovedResp{}, + OpenapiAccountsResp{}, + OpenapiAccountsRemovedResp{}, + OpenapiInstancesResp{}, + OpenapiTresResp{}, + OpenapiUsersAddCondResp{}, + OpenapiUsersAddCondRespStr{}, + OpenapiAccountsAddCondResp{}, + OpenapiAccountsAddCondRespStr{}, + StatsRec{}, + RollupStats{}, + StatsRpc{}, + StatsUser{}, + UsersAddCond{}, + AccountsAddCond{}, + AssocRecSet{}, + } + + for i, v := range types { + data, err := json.Marshal(v) + if err != nil { + t.Fatalf("type %d: marshal error: %v", i, err) + } + if string(data) != "{}" { + t.Fatalf("type %d: empty struct should marshal to {}, got %s", i, data) + } + } +}