Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions config/decode/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ import (
)

var (
// yamlMarshalIndent is the standard indentation to marshal YAML with.
yamlMarshalIndent = 2
// YAMLMarshalOpts are the options for yaml.Marshaler.
YAMLMarshalOpts = []yaml.EncodeOption{
yaml.Indent(yamlMarshalIndent),
yaml.IndentSequence(true),
yaml.UseLiteralStyleIfMultiline(true),
yaml.UseSingleQuote(true),
Expand All @@ -39,7 +36,7 @@ func NewYAMLEncoder(w io.Writer, spaces int) *yaml.Encoder {
opts := append([]yaml.EncodeOption(nil), YAMLMarshalOpts...)

// Override indentation.
if spaces > yamlMarshalIndent {
if spaces > yaml.DefaultIndentSpaces {
opts = append(opts, yaml.Indent(spaces))
}

Expand Down
64 changes: 64 additions & 0 deletions config/decode/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,70 @@ func TestNewYAMLEncoder_Indent(t *testing.T) {
}
}

// testInterfaceMarshaler substitutes itself with another value to marshal
// (yaml.InterfaceMarshaler), like service.Service does.
type testInterfaceMarshaler struct{}

func (m testInterfaceMarshaler) MarshalYAML() (any, error) {
return testParent{
A: "hello",
B: testChild{
C: "hello",
},
}, nil
}

func TestNewYAMLEncoder__NestedMarshaler(t *testing.T) {
// GIVEN: an amount of spaces to indent with.
tests := []struct {
spaces int
}{
{spaces: 2},
{spaces: 4},
{spaces: 8},
}

for _, tc := range tests {
name := fmt.Sprintf("%d spaces", tc.spaces)
t.Run(name, func(t *testing.T) {
t.Parallel()

// AND: a NewYAMLEncoder created with this indent count.
var buf bytes.Buffer
enc := NewYAMLEncoder(&buf, tc.spaces)

// AND: a struct containing a type with a custom yaml.Marshaler.
a := struct {
Service testInterfaceMarshaler `yaml:"service"`
}{}

prefix := fmt.Sprintf("%s\nNewYAMLEncoder.Encode()", packageName)

// WHEN: the struct is encoded.
if err := enc.Encode(a); err != nil {
t.Fatalf(
"%s error = %v",
prefix, err,
)
}

// THEN: the custom-marshaled subtree indents uniformly with the document.
indent := strings.Repeat(" ", tc.spaces)
got := buf.String()
want := "service:\n" +
indent + "a: hello\n" +
indent + "b:\n" +
indent + indent + "c: hello\n"
if got != want {
t.Errorf(
"%s mismatch\ngot: %q\nwant: %q",
prefix, got, want,
)
}
})
}
}

func TestNewYAMLEncoder_IndentSequence(t *testing.T) {
// GIVEN: a YAML encoder.
var buf bytes.Buffer
Expand Down
112 changes: 112 additions & 0 deletions config/save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,118 @@ func TestConfig_Save(t *testing.T) {
}
}

// reindentLines converts the leading whitespace of lines from indent size
// `from` to `to`, preserving any partial offset (e.g. the 2-space alignment
// of sequence continuation lines).
func reindentLines(lines []string, from, to int) []string {
out := make([]string, len(lines))
for i, line := range lines {
spaces := len(line) - len(strings.TrimLeft(line, " "))
levels := spaces / from
offset := spaces % from
out[i] = strings.Repeat(" ", levels*to+offset) + line[spaces:]
}
return out
}

func TestConfig_Save__indentation(t *testing.T) {
// GIVEN: a valid config file and an indentation count to save with.
tests := []struct {
indentation uint8
}{
{indentation: 2},
{indentation: 4},
{indentation: 6},
}

for _, tc := range tests {
name := fmt.Sprintf("%d spaces", tc.indentation)
t.Run(name, func(t *testing.T) {
// t.Parallel() - Cannot run in parallel since we're using stdout.
releaseStdout := test.CaptureLog(t, logx.Default())

file := filepath.Join(t.TempDir(), "config.yml")
testYAML_config_indent4(file)
hadData, _ := os.ReadFile(file)
cfg := testLoadBasic(t, file)
hadIndentation := cfg.Settings.Indentation

// AND: the indentation replaced with the test count.
cfg.Settings.Indentation = tc.indentation

// WHEN: the config is saved.
saveDone := make(chan struct{})
go func() {
loadMu.RLock()
cfg.Save()
loadMu.RUnlock()
close(saveDone)
}()
gotExit := gotExitCodeDuringSave(saveDone, false)

releaseStdout()
prefix := fmt.Sprintf("%s\nConfig.Save()", packageName)

// THEN: the save succeeds.
if gotExit {
t.Fatalf("%s unexpected Fatal log to ExitCodeChannel", prefix)
}

// AND: the file uses this indentation with the service subtree intact.
newData, err := os.ReadFile(cfg.File)
if err != nil {
t.Fatalf(
"%s\nfailed opening the file - %s",
packageName, err,
)
}
gotLines := strings.Split(string(newData), "\n")
wantLines := reindentLines(
strings.Split(string(hadData), "\n"),
int(hadIndentation), int(tc.indentation),
)
if testErr := test.AssertSlicesEqualFunc(
t,
gotLines,
wantLines,
func(a, b string) bool { return a == b },
prefix,
"",
); testErr != nil {
t.Fatal(testErr)
}

// AND: the saved config still decodes to the same service structure.
var cfg2 Config
if err := cfg2.Decode(newData); err != nil {
t.Fatalf(
"%s saved config failed to decode: %v",
prefix, err,
)
}
svc := cfg2.Service["awesome"]
if svc == nil {
t.Fatalf("%s service %q lost on save", prefix, "awesome")
}
if svc.LatestVersion == nil {
t.Errorf("%s latest_version lost on save", prefix)
}
if svc.Options.SemanticVersioning == nil {
t.Errorf("%s options.semantic_versioning lost on save", prefix)
}
if svc.Dashboard.Icon == "" {
t.Errorf("%s dashboard.icon lost on save", prefix)
}
if len(svc.Command) != 1 {
t.Errorf(
"%s command lost on save\ngot: %d commands\nwant: 1",
prefix, len(svc.Command),
)
}
})
}
}

func TestConfig_Save__encodeError(t *testing.T) {
// GIVEN: a failing YAML encode.
original := encodeConfigYAML
Expand Down
35 changes: 35 additions & 0 deletions config/yaml_help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,41 @@ func testYAML_config_small(path string) {
writeFile(path, data)
}

// testYAML_config_indent4 is for `save.go`.
//
// A config indented with 4 spaces rather than the marshal-default 2
func testYAML_config_indent4(path string) {
data := test.TrimYAML(`
notify:
gotify:
type: gotify
url_fields:
host: example.com
token: super-secret
service:
awesome:
options:
semantic_versioning: false
latest_version:
type: url
url: https://example.com/releases
url_commands:
- type: regex
regex: '[0-9.]+'
notify:
gotify: {}
command:
- - /bin/echo
- hello
dashboard:
icon: https://example.com/icon.webp
tags:
- NEWS
`)

writeFile(path, data)
}

func testYAML_Ordering_0(path string) {
data := test.TrimYAML(`
settings:
Expand Down
14 changes: 7 additions & 7 deletions service/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,16 @@ type serviceDecode struct {

// MarshalJSON implements the json.Marshaler interface.
func (s *Service) MarshalJSON() ([]byte, error) {
return s.marshal("json")
return decode.Marshal("json", s.marshalAux()) //nolint:wrapcheck
}

// MarshalYAML implements the yaml.Marshaler interface.
func (s *Service) MarshalYAML() ([]byte, error) {
return s.marshal("yaml")
// MarshalYAML implements the yaml.InterfaceMarshaler interface.
func (s *Service) MarshalYAML() (any, error) {
return s.marshalAux(), nil
}

// marshal implements the format.Marshaler interface.
func (s *Service) marshal(format string) ([]byte, error) {
// marshalAux converts the Service to its marshal-only helper representation.
func (s *Service) marshalAux() serviceMarshal {
aux := serviceMarshal{
Name: s.Name,
Comment: s.Comment,
Expand All @@ -124,7 +124,7 @@ func (s *Service) marshal(format string) ([]byte, error) {
aux.WebHook = s.WebHook
}

return decode.Marshal(format, aux) //nolint:wrapcheck
return aux
}

// UnmarshalJSON implements the json.Marshaler interface.
Expand Down
2 changes: 1 addition & 1 deletion service/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func TestService_Marshal(t *testing.T) {
for _, typ := range util.SortedKeys(results) {
want := results[typ]

// WHEN: the Service is marshalled to
// WHEN: the Service is marshaled to
bytes, err := decode.Marshal(typ, tc.svc)

prefix := fmt.Sprintf(
Expand Down
4 changes: 2 additions & 2 deletions web/api/types/argus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1942,7 +1942,7 @@ func TestLatestVersion_String(t *testing.T) {
{
"type": "github",
"url": "owner/repo",
"access_token": ` + secretValueMarshalled + `,
"access_token": ` + secretValueMarshaled + `,
"allow_invalid_certs": true,
"use_prerelease": false,
"url_commands": [
Expand Down Expand Up @@ -2108,7 +2108,7 @@ func TestLatestVersionRequire_String(t *testing.T) {
"image": "test/app",
"tag": "{{ version }}",
"username": "user",
"token": ` + secretValueMarshalled + `
"token": ` + secretValueMarshaled + `
},
"regex_content": ".*",
"regex_version": "([0-9.]+)"
Expand Down
6 changes: 3 additions & 3 deletions web/api/types/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ import (
)

var packageName = "api.types"
var secretValueMarshalled string
var secretValueMarshaled string

func TestMain(m *testing.M) {
// Log.
logtest.InitLog()

// Marshal the secret value '<secret>' -> '\u003csecret\u003e'.
secretValueMarshalledBytes, _ := decode.Marshal("json", util.SecretValue)
secretValueMarshalled = string(secretValueMarshalledBytes)
secretValueMarshaledBytes, _ := decode.Marshal("json", util.SecretValue)
secretValueMarshaled = string(secretValueMarshaledBytes)

// Run other tests.
exitCode := m.Run()
Expand Down
8 changes: 4 additions & 4 deletions web/api/v1/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ import (
)

var (
packageName = "api_v1"
secretValueMarshalled string
packageName = "api_v1"
secretValueMarshaled string
)

func TestMain(m *testing.M) {
Expand All @@ -70,8 +70,8 @@ func TestMain(m *testing.M) {
cfg.Load(ctx, g, path, &flags)

// Marshal the secret value '<secret>' -> '\u003csecret\u003e'.
secretValueMarshalledBytes, _ := decode.Marshal("json", util.SecretValue)
secretValueMarshalled = string(secretValueMarshalledBytes)
secretValueMarshaledBytes, _ := decode.Marshal("json", util.SecretValue)
secretValueMarshaled = string(secretValueMarshaledBytes)

// Run other tests.
exitCode := m.Run()
Expand Down
Loading
Loading