feat: 添加基础类型、工具函数和客户端测试
包含 Ptr[T] 泛型辅助函数、4 种 NoVal 类型(Uint64/Uint32/Uint16/Float64)、字符串集合类型、OpenapiMeta/Error/Warning,以及对应的序列化测试和客户端集成测试。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
136
internal/slurm/client_test.go
Normal file
136
internal/slurm/client_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package slurm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
client, err := NewClient("http://localhost:6820/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
if client.UserAgent != DefaultUserAgent {
|
||||
t.Errorf("expected UserAgent %q, got %q", DefaultUserAgent, client.UserAgent)
|
||||
}
|
||||
if client.client == nil {
|
||||
t.Error("expected http.Client to be initialized (nil httpClient should default to http.DefaultClient)")
|
||||
}
|
||||
|
||||
_, err = NewClient("://invalid", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid URL, got nil")
|
||||
}
|
||||
|
||||
client2, err := NewClient("http://localhost:6820/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error with nil httpClient: %v", err)
|
||||
}
|
||||
if client2.client == nil {
|
||||
t.Error("expected client.httpclient to be non-nil when passing nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient_ServicesInitialized(t *testing.T) {
|
||||
client, err := NewClient("http://localhost:6820/", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
name string
|
||||
svc interface{}
|
||||
}{
|
||||
{"Jobs", client.Jobs},
|
||||
{"Nodes", client.Nodes},
|
||||
{"Partitions", client.Partitions},
|
||||
{"Reservations", client.Reservations},
|
||||
{"Diag", client.Diag},
|
||||
{"Ping", client.Ping},
|
||||
{"Licenses", client.Licenses},
|
||||
{"Reconfigure", client.Reconfigure},
|
||||
{"Shares", client.Shares},
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
if s.svc == nil {
|
||||
t.Errorf("%s service is nil", s.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_AuthHeaders(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("X-SLURM-USER-NAME"); got != "testuser" {
|
||||
t.Errorf("expected X-SLURM-USER-NAME %q, got %q", "testuser", got)
|
||||
}
|
||||
if got := r.Header.Get("X-SLURM-USER-TOKEN"); got != "testtoken" {
|
||||
t.Errorf("expected X-SLURM-USER-TOKEN %q, got %q", "testtoken", got)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
transport := &TokenAuthTransport{
|
||||
UserName: "testuser",
|
||||
Token: "testtoken",
|
||||
}
|
||||
httpClient := transport.Client()
|
||||
client, err := NewClient(server.URL, httpClient)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
req, err := client.NewRequest("GET", "slurm/v0.0.40/ping", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating request: %v", err)
|
||||
}
|
||||
_, err = client.Do(context.Background(), req, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error doing request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_ErrorHandling(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(500)
|
||||
fmt.Fprint(w, "internal server error")
|
||||
})
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
client, err := NewClient(server.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
req, err := client.NewRequest("GET", "slurm/v0.0.40/diag", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating request: %v", err)
|
||||
}
|
||||
_, err = client.Do(context.Background(), req, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500 response")
|
||||
}
|
||||
|
||||
errorResp, ok := err.(*ErrorResponse)
|
||||
if !ok {
|
||||
t.Fatalf("expected *ErrorResponse, got %T", err)
|
||||
}
|
||||
if errorResp.Response.StatusCode != 500 {
|
||||
t.Errorf("expected status 500, got %d", errorResp.Response.StatusCode)
|
||||
}
|
||||
}
|
||||
73
internal/slurm/types.go
Normal file
73
internal/slurm/types.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package slurm
|
||||
|
||||
// Ptr returns a pointer to the given value. Useful for optional fields.
|
||||
func Ptr[T any](v T) *T { return &v }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoVal types — 3-field structs representing "set/infinite/number" pattern
|
||||
// from the Slurm OpenAPI spec. All fields are optional (pointer).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Uint64NoVal represents an optional uint64 that can be unset or infinite.
|
||||
type Uint64NoVal struct {
|
||||
Set *bool `json:"set,omitempty"`
|
||||
Infinite *bool `json:"infinite,omitempty"`
|
||||
Number *int64 `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// Uint32NoVal represents an optional uint32 that can be unset or infinite.
|
||||
type Uint32NoVal struct {
|
||||
Set *bool `json:"set,omitempty"`
|
||||
Infinite *bool `json:"infinite,omitempty"`
|
||||
Number *int64 `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// Uint16NoVal represents an optional uint16 that can be unset or infinite.
|
||||
type Uint16NoVal struct {
|
||||
Set *bool `json:"set,omitempty"`
|
||||
Infinite *bool `json:"infinite,omitempty"`
|
||||
Number *int64 `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// Float64NoVal represents an optional float64 that can be unset or infinite.
|
||||
type Float64NoVal struct {
|
||||
Set *bool `json:"set,omitempty"`
|
||||
Infinite *bool `json:"infinite,omitempty"`
|
||||
Number *float64 `json:"number,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// String collection types — all marshal/unmarshal as JSON arrays of strings.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// CSVString is a comma-separated string collection.
|
||||
type CSVString []string
|
||||
|
||||
// StringArray is a string array type (v0.0.40_string_array).
|
||||
type StringArray []string
|
||||
|
||||
// StringList is a string list type (v0.0.40_string_list).
|
||||
type StringList []string
|
||||
|
||||
// Hostlist is a hostlist type (v0.0.40_hostlist).
|
||||
type Hostlist []string
|
||||
|
||||
// HostlistString is a hostlist string type (v0.0.40_hostlist_string).
|
||||
type HostlistString []string
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Process exit types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ProcessExitCodeVerbose represents a verbose process exit code.
|
||||
type ProcessExitCodeVerbose struct {
|
||||
Status []string `json:"status,omitempty"`
|
||||
ReturnCode *Uint32NoVal `json:"return_code,omitempty"`
|
||||
Signal *ProcessExitSignal `json:"signal,omitempty"`
|
||||
}
|
||||
|
||||
// ProcessExitSignal represents a signal received by a process.
|
||||
type ProcessExitSignal struct {
|
||||
ID *Uint16NoVal `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
}
|
||||
66
internal/slurm/types_meta.go
Normal file
66
internal/slurm/types_meta.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package slurm
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAPI Meta — top-level metadata returned by every Slurm API response
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// OpenapiMeta contains plugin, client, command, and Slurm version metadata.
|
||||
type OpenapiMeta struct {
|
||||
Plugin *MetaPlugin `json:"plugin,omitempty"`
|
||||
Client *MetaClient `json:"client,omitempty"`
|
||||
Command StringArray `json:"command,omitempty"`
|
||||
Slurm *MetaSlurm `json:"slurm,omitempty"`
|
||||
}
|
||||
|
||||
// MetaPlugin contains plugin-related metadata.
|
||||
type MetaPlugin struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
DataParser *string `json:"data_parser,omitempty"`
|
||||
AccountingStorage *string `json:"accounting_storage,omitempty"`
|
||||
}
|
||||
|
||||
// MetaClient contains client-related metadata.
|
||||
type MetaClient struct {
|
||||
Source *string `json:"source,omitempty"`
|
||||
User *string `json:"user,omitempty"`
|
||||
Group *string `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// MetaSlurm contains Slurm version and cluster metadata.
|
||||
type MetaSlurm struct {
|
||||
Version *MetaSlurmVersion `json:"version,omitempty"`
|
||||
Release *string `json:"release,omitempty"`
|
||||
Cluster *string `json:"cluster,omitempty"`
|
||||
}
|
||||
|
||||
// MetaSlurmVersion contains major/micro/minor version components.
|
||||
type MetaSlurmVersion struct {
|
||||
Major *string `json:"major,omitempty"`
|
||||
Micro *string `json:"micro,omitempty"`
|
||||
Minor *string `json:"minor,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAPI Error & Warning — standard error/warning types in responses
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// OpenapiError represents a single API error.
|
||||
type OpenapiError struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
ErrorNumber *int32 `json:"error_number,omitempty"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// OpenapiErrors is a collection of OpenapiError.
|
||||
type OpenapiErrors []OpenapiError
|
||||
|
||||
// OpenapiWarning represents a single API warning.
|
||||
type OpenapiWarning struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// OpenapiWarnings is a collection of OpenapiWarning.
|
||||
type OpenapiWarnings []OpenapiWarning
|
||||
249
internal/slurm/types_meta_test.go
Normal file
249
internal/slurm/types_meta_test.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package slurm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenapiMetaRoundTrip(t *testing.T) {
|
||||
orig := OpenapiMeta{
|
||||
Plugin: &MetaPlugin{
|
||||
Type: Ptr("type"),
|
||||
Name: Ptr("name"),
|
||||
DataParser: Ptr("parser"),
|
||||
AccountingStorage: Ptr("storage"),
|
||||
},
|
||||
Client: &MetaClient{
|
||||
Source: Ptr("source"),
|
||||
User: Ptr("root"),
|
||||
Group: Ptr("root"),
|
||||
},
|
||||
Command: StringArray{"squeue", "--json"},
|
||||
Slurm: &MetaSlurm{
|
||||
Version: &MetaSlurmVersion{
|
||||
Major: Ptr("24"),
|
||||
Micro: Ptr("5"),
|
||||
Minor: Ptr("11"),
|
||||
},
|
||||
Release: Ptr("24.11.5"),
|
||||
Cluster: Ptr("test-cluster"),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded OpenapiMeta
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify plugin
|
||||
if decoded.Plugin == nil || *decoded.Plugin.Type != "type" {
|
||||
t.Fatal("plugin.type mismatch")
|
||||
}
|
||||
if *decoded.Plugin.Name != "name" {
|
||||
t.Fatal("plugin.name mismatch")
|
||||
}
|
||||
if *decoded.Plugin.DataParser != "parser" {
|
||||
t.Fatal("plugin.data_parser mismatch")
|
||||
}
|
||||
if *decoded.Plugin.AccountingStorage != "storage" {
|
||||
t.Fatal("plugin.accounting_storage mismatch")
|
||||
}
|
||||
|
||||
// Verify client
|
||||
if decoded.Client == nil || *decoded.Client.Source != "source" {
|
||||
t.Fatal("client.source mismatch")
|
||||
}
|
||||
if *decoded.Client.User != "root" {
|
||||
t.Fatal("client.user mismatch")
|
||||
}
|
||||
if *decoded.Client.Group != "root" {
|
||||
t.Fatal("client.group mismatch")
|
||||
}
|
||||
|
||||
// Verify command
|
||||
if len(decoded.Command) != 2 || decoded.Command[0] != "squeue" {
|
||||
t.Fatalf("command mismatch: %v", decoded.Command)
|
||||
}
|
||||
|
||||
// Verify slurm
|
||||
if decoded.Slurm == nil || decoded.Slurm.Version == nil {
|
||||
t.Fatal("slurm or slurm.version is nil")
|
||||
}
|
||||
if *decoded.Slurm.Version.Major != "24" {
|
||||
t.Fatal("slurm.version.major mismatch")
|
||||
}
|
||||
if *decoded.Slurm.Version.Micro != "5" {
|
||||
t.Fatal("slurm.version.micro mismatch")
|
||||
}
|
||||
if *decoded.Slurm.Version.Minor != "11" {
|
||||
t.Fatal("slurm.version.minor mismatch")
|
||||
}
|
||||
if *decoded.Slurm.Release != "24.11.5" {
|
||||
t.Fatal("slurm.release mismatch")
|
||||
}
|
||||
if *decoded.Slurm.Cluster != "test-cluster" {
|
||||
t.Fatal("slurm.cluster mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiMetaEmpty(t *testing.T) {
|
||||
orig := OpenapiMeta{}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "{}" {
|
||||
t.Fatalf("empty meta should be {}, got %s", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiErrorRoundTrip(t *testing.T) {
|
||||
orig := OpenapiError{
|
||||
Description: Ptr("Job not found"),
|
||||
ErrorNumber: Ptr(int32(2001)),
|
||||
Error: Ptr("Invalid job id"),
|
||||
Source: Ptr("slurmctld"),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded OpenapiError
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if *decoded.Description != "Job not found" {
|
||||
t.Fatal("description mismatch")
|
||||
}
|
||||
if *decoded.ErrorNumber != 2001 {
|
||||
t.Fatal("error_number mismatch")
|
||||
}
|
||||
if *decoded.Error != "Invalid job id" {
|
||||
t.Fatal("error mismatch")
|
||||
}
|
||||
if *decoded.Source != "slurmctld" {
|
||||
t.Fatal("source mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiErrorsArray(t *testing.T) {
|
||||
orig := OpenapiErrors{
|
||||
{Description: Ptr("err1"), ErrorNumber: Ptr(int32(1))},
|
||||
{Description: Ptr("err2"), ErrorNumber: Ptr(int32(2))},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded OpenapiErrors
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("expected 2 errors, got %d", len(decoded))
|
||||
}
|
||||
if *decoded[0].Description != "err1" {
|
||||
t.Fatal("first error description mismatch")
|
||||
}
|
||||
if *decoded[1].ErrorNumber != 2 {
|
||||
t.Fatal("second error_number mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiWarningRoundTrip(t *testing.T) {
|
||||
orig := OpenapiWarning{
|
||||
Description: Ptr("Deprecated API version"),
|
||||
Source: Ptr("slurmrestd"),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded OpenapiWarning
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if *decoded.Description != "Deprecated API version" {
|
||||
t.Fatal("description mismatch")
|
||||
}
|
||||
if *decoded.Source != "slurmrestd" {
|
||||
t.Fatal("source mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiWarningsArray(t *testing.T) {
|
||||
orig := OpenapiWarnings{
|
||||
{Description: Ptr("warn1")},
|
||||
{Description: Ptr("warn2"), Source: Ptr("src")},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var decoded OpenapiWarnings
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("expected 2 warnings, got %d", len(decoded))
|
||||
}
|
||||
if *decoded[0].Description != "warn1" {
|
||||
t.Fatal("first warning description mismatch")
|
||||
}
|
||||
if decoded[1].Source == nil || *decoded[1].Source != "src" {
|
||||
t.Fatal("second warning source mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenapiErrorJSONTags(t *testing.T) {
|
||||
raw := `{
|
||||
"description": "test desc",
|
||||
"error_number": 42,
|
||||
"error": "test error",
|
||||
"source": "test source"
|
||||
}`
|
||||
var decoded OpenapiError
|
||||
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded.Description == nil || *decoded.Description != "test desc" {
|
||||
t.Fatal("description json tag mismatch")
|
||||
}
|
||||
if decoded.ErrorNumber == nil || *decoded.ErrorNumber != 42 {
|
||||
t.Fatal("error_number json tag mismatch")
|
||||
}
|
||||
if decoded.Error == nil || *decoded.Error != "test error" {
|
||||
t.Fatal("error json tag mismatch")
|
||||
}
|
||||
if decoded.Source == nil || *decoded.Source != "test source" {
|
||||
t.Fatal("source json tag mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaSlurmVersionJSONTags(t *testing.T) {
|
||||
raw := `{"major":"24","micro":"5","minor":"11"}`
|
||||
var decoded MetaSlurmVersion
|
||||
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *decoded.Major != "24" || *decoded.Micro != "5" || *decoded.Minor != "11" {
|
||||
t.Fatalf("version json tag mismatch: %+v", decoded)
|
||||
}
|
||||
}
|
||||
340
internal/slurm/types_test.go
Normal file
340
internal/slurm/types_test.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package slurm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPtr(t *testing.T) {
|
||||
i := 42
|
||||
p := Ptr(i)
|
||||
if p == nil {
|
||||
t.Fatal("Ptr returned nil")
|
||||
}
|
||||
if *p != 42 {
|
||||
t.Fatalf("Ptr(42) = %d, want 42", *p)
|
||||
}
|
||||
|
||||
s := "hello"
|
||||
sp := Ptr(s)
|
||||
if *sp != "hello" {
|
||||
t.Fatalf("Ptr(\"hello\") = %q, want \"hello\"", *sp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUint64NoVal(t *testing.T) {
|
||||
testNoValInt(t, Uint64NoVal{})
|
||||
}
|
||||
|
||||
func TestUint32NoVal(t *testing.T) {
|
||||
testNoValInt(t, Uint32NoVal{})
|
||||
}
|
||||
|
||||
func TestUint16NoVal(t *testing.T) {
|
||||
testNoValInt(t, Uint16NoVal{})
|
||||
}
|
||||
|
||||
// testNoValInt tests the 3-state behavior for integer NoVal types.
|
||||
func testNoValInt(t *testing.T, _ interface{}) {
|
||||
t.Helper()
|
||||
|
||||
// State 1: unset — empty object, all fields nil
|
||||
t.Run("unset", func(t *testing.T) {
|
||||
raw := `{}`
|
||||
var v struct {
|
||||
Set *bool
|
||||
Infinite *bool
|
||||
Number *int64
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Set != nil || v.Infinite != nil || v.Number != nil {
|
||||
t.Fatalf("unset state: expected all nil, got set=%v infinite=%v number=%v", v.Set, v.Infinite, v.Number)
|
||||
}
|
||||
})
|
||||
|
||||
// State 2: infinite
|
||||
t.Run("infinite", func(t *testing.T) {
|
||||
raw := `{"set":true,"infinite":true}`
|
||||
var v struct {
|
||||
Set *bool
|
||||
Infinite *bool
|
||||
Number *int64
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Set == nil || !*v.Set {
|
||||
t.Fatal("set should be true")
|
||||
}
|
||||
if v.Infinite == nil || !*v.Infinite {
|
||||
t.Fatal("infinite should be true")
|
||||
}
|
||||
if v.Number != nil {
|
||||
t.Fatal("number should be nil in infinite state")
|
||||
}
|
||||
})
|
||||
|
||||
// State 3: value
|
||||
t.Run("value", func(t *testing.T) {
|
||||
raw := `{"set":true,"number":42}`
|
||||
var v struct {
|
||||
Set *bool
|
||||
Infinite *bool
|
||||
Number *int64
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Set == nil || !*v.Set {
|
||||
t.Fatal("set should be true")
|
||||
}
|
||||
if v.Number == nil || *v.Number != 42 {
|
||||
t.Fatalf("number should be 42, got %v", v.Number)
|
||||
}
|
||||
if v.Infinite != nil {
|
||||
t.Fatal("infinite should be nil in value state")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFloat64NoVal(t *testing.T) {
|
||||
// State 1: unset
|
||||
t.Run("unset", func(t *testing.T) {
|
||||
var v Float64NoVal
|
||||
if err := json.Unmarshal([]byte(`{}`), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Set != nil || v.Infinite != nil || v.Number != nil {
|
||||
t.Fatalf("expected all nil")
|
||||
}
|
||||
})
|
||||
|
||||
// State 2: infinite
|
||||
t.Run("infinite", func(t *testing.T) {
|
||||
var v Float64NoVal
|
||||
if err := json.Unmarshal([]byte(`{"set":true,"infinite":true}`), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Set == nil || !*v.Set {
|
||||
t.Fatal("set should be true")
|
||||
}
|
||||
if v.Infinite == nil || !*v.Infinite {
|
||||
t.Fatal("infinite should be true")
|
||||
}
|
||||
})
|
||||
|
||||
// State 3: value
|
||||
t.Run("value", func(t *testing.T) {
|
||||
var v Float64NoVal
|
||||
if err := json.Unmarshal([]byte(`{"set":true,"number":42.5}`), &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Number == nil || *v.Number != 42.5 {
|
||||
t.Fatalf("number should be 42.5, got %v", v.Number)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUint64NoValRoundTrip(t *testing.T) {
|
||||
orig := Uint64NoVal{
|
||||
Set: Ptr(true),
|
||||
Number: Ptr(int64(100)),
|
||||
}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Uint64NoVal
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *decoded.Set != *orig.Set || *decoded.Number != *orig.Number {
|
||||
t.Fatalf("round-trip mismatch: %+v vs %+v", orig, decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUint32NoValRoundTrip(t *testing.T) {
|
||||
orig := Uint32NoVal{
|
||||
Set: Ptr(true),
|
||||
Infinite: Ptr(true),
|
||||
}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Uint32NoVal
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !*decoded.Set || !*decoded.Infinite {
|
||||
t.Fatal("round-trip mismatch for infinite state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUint16NoValRoundTrip(t *testing.T) {
|
||||
orig := Uint16NoVal{
|
||||
Set: Ptr(true),
|
||||
Number: Ptr(int64(9)),
|
||||
}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Uint16NoVal
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *decoded.Number != 9 {
|
||||
t.Fatalf("expected 9, got %d", *decoded.Number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloat64NoValRoundTrip(t *testing.T) {
|
||||
orig := Float64NoVal{
|
||||
Set: Ptr(true),
|
||||
Number: Ptr(3.14),
|
||||
}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Float64NoVal
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if *decoded.Number != 3.14 {
|
||||
t.Fatalf("expected 3.14, got %f", *decoded.Number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVStringRoundTrip(t *testing.T) {
|
||||
orig := CSVString{"a", "b", "c"}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != `["a","b","c"]` {
|
||||
t.Fatalf("unexpected JSON: %s", data)
|
||||
}
|
||||
var decoded CSVString
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded) != 3 || decoded[0] != "a" || decoded[2] != "c" {
|
||||
t.Fatalf("round-trip mismatch: %v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringArrayRoundTrip(t *testing.T) {
|
||||
orig := StringArray{"x", "y"}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded StringArray
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("expected 2 elements, got %d", len(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringListRoundTrip(t *testing.T) {
|
||||
orig := StringList{"foo"}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded StringList
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded) != 1 || decoded[0] != "foo" {
|
||||
t.Fatalf("round-trip mismatch: %v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostlistRoundTrip(t *testing.T) {
|
||||
orig := Hostlist{"node01", "node02"}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded Hostlist
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded) != 2 {
|
||||
t.Fatalf("expected 2, got %d", len(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostlistStringRoundTrip(t *testing.T) {
|
||||
orig := HostlistString{"node[01-04]"}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded HostlistString
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded[0] != "node[01-04]" {
|
||||
t.Fatalf("round-trip mismatch: %v", decoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessExitCodeVerboseRoundTrip(t *testing.T) {
|
||||
orig := ProcessExitCodeVerbose{
|
||||
Status: []string{"EXITED"},
|
||||
ReturnCode: &Uint32NoVal{
|
||||
Set: Ptr(true),
|
||||
Number: Ptr(int64(0)),
|
||||
},
|
||||
Signal: &ProcessExitSignal{
|
||||
ID: &Uint16NoVal{Set: Ptr(false)},
|
||||
Name: Ptr("NONE"),
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded ProcessExitCodeVerbose
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decoded.Status) != 1 || decoded.Status[0] != "EXITED" {
|
||||
t.Fatalf("status mismatch: %v", decoded.Status)
|
||||
}
|
||||
if decoded.ReturnCode == nil || decoded.ReturnCode.Number == nil || *decoded.ReturnCode.Number != 0 {
|
||||
t.Fatal("return_code mismatch")
|
||||
}
|
||||
if decoded.Signal == nil || decoded.Signal.Name == nil || *decoded.Signal.Name != "NONE" {
|
||||
t.Fatal("signal name mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyStringCollections(t *testing.T) {
|
||||
for name, typ := range map[string]interface{}{
|
||||
"CSVString": &CSVString{},
|
||||
"StringArray": &StringArray{},
|
||||
"StringList": &StringList{},
|
||||
"Hostlist": &Hostlist{},
|
||||
"HostlistString": &HostlistString{},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
data, err := json.Marshal(typ)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "null" && string(data) != "[]" {
|
||||
// nil slice marshals to null, empty slice to []
|
||||
t.Fatalf("unexpected: %s", data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user