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:
dailz
2026-04-08 18:28:46 +08:00
parent 73453ddd10
commit 5873dc5b72
5 changed files with 864 additions and 0 deletions

View 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
View 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"`
}

View 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

View 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)
}
}

View 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)
}
})
}
}