From 41961d1446a05069ef2f718e41bff510354ae412 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 23 Jun 2026 14:26:45 +0200 Subject: [PATCH 1/5] Extract errors formatting. --- logging/error.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++ logging/logger.go | 16 +------------ main.go | 37 +---------------------------- 3 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 logging/error.go diff --git a/logging/error.go b/logging/error.go new file mode 100644 index 00000000..c2dec573 --- /dev/null +++ b/logging/error.go @@ -0,0 +1,59 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package logging + +import ( + "fmt" + "strings" +) + +// FormatError formats a single error inline and joined errors as a bullet list. +func FormatError(message string, err error) string { + errs := flattenedErrors(err) + if len(errs) <= 1 { + return fmt.Sprintf("%s: %v", message, err) + } + + var builder strings.Builder + builder.WriteString(message) + builder.WriteString(":") + for _, nestedErr := range errs { + builder.WriteString("\n- ") + builder.WriteString(nestedErr.Error()) + } + + return builder.String() +} + +// flattenedErrors returns the leaf errors from a joined error. +func flattenedErrors(err error) []error { + joined, ok := err.(interface { + Unwrap() []error + }) + if !ok { + return []error{err} + } + + var result []error + for _, nestedErr := range joined.Unwrap() { + result = append(result, flattenedErrors(nestedErr)...) + } + + return result +} diff --git a/logging/logger.go b/logging/logger.go index d9e73383..67d0f3fe 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -177,19 +177,5 @@ func formatPanicMessage(recovered any) string { return fmt.Sprintf("panic: %v", recovered) } - joined, isJoined := err.(interface { - Unwrap() []error - }) - if !isJoined || len(joined.Unwrap()) <= 1 { - return fmt.Sprintf("panic: %v", err) - } - - var builder strings.Builder - builder.WriteString("panic:") - for _, wrappedErr := range joined.Unwrap() { - builder.WriteString("\n- ") - builder.WriteString(wrappedErr.Error()) - } - - return builder.String() + return FormatError("panic", err) } diff --git a/main.go b/main.go index fec817ce..5bbff3b5 100644 --- a/main.go +++ b/main.go @@ -138,42 +138,7 @@ func configureLogging(config cli.Config) { // logError writes a user-facing error through the configured logger. func logError(message string, err error) { - slog.Error(formatError(message, err)) -} - -// formatError formats single errors inline and joined errors as a bullet list. -func formatError(message string, err error) string { - errs := flattenedErrors(err) - if len(errs) <= 1 { - return fmt.Sprintf("%s: %v", message, err) - } - - var builder strings.Builder - builder.WriteString(message) - builder.WriteString(":") - for _, nestedErr := range errs { - builder.WriteString("\n- ") - builder.WriteString(nestedErr.Error()) - } - - return builder.String() -} - -// flattenedErrors returns the leaf errors from a joined error joined. -func flattenedErrors(err error) []error { - joined, ok := err.(interface { - Unwrap() []error - }) - if !ok { - return []error{err} - } - - var result []error - for _, nestedErr := range joined.Unwrap() { - result = append(result, flattenedErrors(nestedErr)...) - } - - return result + slog.Error(logging.FormatError(message, err)) } // exitWithError writes a user-facing error and exits with a failing status. From 0144b5539474dc4810090f661399a5501fc9af17 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 23 Jun 2026 14:29:29 +0200 Subject: [PATCH 2/5] Add tests. --- logging/error_test.go | 71 ++++++++++++++++++++++++++++++++++++++ logging/logger_test.go | 78 ++++++++++++++++++++---------------------- 2 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 logging/error_test.go diff --git a/logging/error_test.go b/logging/error_test.go new file mode 100644 index 00000000..21c67d4b --- /dev/null +++ b/logging/error_test.go @@ -0,0 +1,71 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package logging //nolint:testpackage // Shares the package's Ginkgo suite. + +import ( + "errors" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Error formatting", func() { + + It("should format a single error inline", func() { + err := errors.New("first failure") + + Expect(FormatError("operation failed", err)).To( + Equal("operation failed: first failure"), + ) + }) + + It("should format a nested error inline", func() { + err := fmt.Errorf("outer context: %w", errors.New("first failure")) + + Expect(FormatError("operation failed", err)).To( + Equal("operation failed: outer context: first failure"), + ) + }) + + It("should format joined errors as a bullet list", func() { + err := errors.Join( + errors.New("first failure"), + errors.New("second failure"), + ) + + Expect(FormatError("operation failed", err)).To( + Equal("operation failed:\n- first failure\n- second failure"), + ) + }) + + It("should flatten nested joined errors into a bullet list", func() { + err := errors.Join( + errors.New("first failure"), + errors.Join( + errors.New("second failure"), + errors.New("third failure"), + ), + ) + + Expect(FormatError("operation failed", err)).To( + Equal("operation failed:\n- first failure\n- second failure\n- third failure"), + ) + }) +}) diff --git a/logging/logger_test.go b/logging/logger_test.go index e275fce8..51427a12 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -18,44 +18,42 @@ package logging //nolint:testpackage // Tests OS-specific normalization in an unexported helper. -import "testing" - -// TestFileURLFromAbsolutePath verifies OS-specific path shapes are valid file URLs. -func TestFileURLFromAbsolutePath(t *testing.T) { - tests := []struct { - name string - path string - want string - }{ - { - name: "unix path", - path: "/Users/me/project/file.go", - want: "file:///Users/me/project/file.go", - }, - { - name: "windows drive path", - path: `C:\Users\me\project\file.go`, - want: "file:///C:/Users/me/project/file.go", - }, - { - name: "windows drive path with spaces", - path: `C:\Users\me\my project\file.go`, - want: "file:///C:/Users/me/my%20project/file.go", - }, - { - name: "windows unc path", - path: `\\server\share\project\file.go`, - want: "file://server/share/project/file.go", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := fileURLFromAbsolutePath(test.path) - if got != test.want { - t.Fatalf("fileURLFromAbsolutePath(%q) = %q, want %q", - test.path, got, test.want) - } - }) - } +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// TestLogging runs the logging package specs. +func TestLogging(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Logging Suite") } + +var _ = Describe("File URL formatting", func() { + + It("should format a Unix path", func() { + Expect(fileURLFromAbsolutePath("/Users/me/project/file.go")).To( + Equal("file:///Users/me/project/file.go"), + ) + }) + + It("should format a Windows drive path", func() { + Expect(fileURLFromAbsolutePath(`C:\Users\me\project\file.go`)).To( + Equal("file:///C:/Users/me/project/file.go"), + ) + }) + + It("should escape spaces in a Windows drive path", func() { + Expect(fileURLFromAbsolutePath(`C:\Users\me\my project\file.go`)).To( + Equal("file:///C:/Users/me/my%20project/file.go"), + ) + }) + + It("should format a Windows UNC path", func() { + Expect(fileURLFromAbsolutePath(`\\server\share\project\file.go`)).To( + Equal("file://server/share/project/file.go"), + ) + }) +}) From 44f5329c2c1079779e53e64ac07605b22914a821 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 23 Jun 2026 14:52:21 +0200 Subject: [PATCH 3/5] Improve readability. --- logging/error_test.go | 5 ++++- main.go | 7 +------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/logging/error_test.go b/logging/error_test.go index 21c67d4b..31f54c2c 100644 --- a/logging/error_test.go +++ b/logging/error_test.go @@ -65,7 +65,10 @@ var _ = Describe("Error formatting", func() { ) Expect(FormatError("operation failed", err)).To( - Equal("operation failed:\n- first failure\n- second failure\n- third failure"), + Equal("operation failed:" + + "\n- first failure" + + "\n- second failure" + + "\n- third failure"), ) }) }) diff --git a/main.go b/main.go index 5bbff3b5..4cc775f6 100644 --- a/main.go +++ b/main.go @@ -136,14 +136,9 @@ func configureLogging(config cli.Config) { slog.SetDefault(logger) } -// logError writes a user-facing error through the configured logger. -func logError(message string, err error) { - slog.Error(logging.FormatError(message, err)) -} - // exitWithError writes a user-facing error and exits with a failing status. func exitWithError(message string, err error) { - logError(message, err) + slog.Error(logging.FormatError(message, err)) os.Exit(1) } From a679c0faff0f50416c8c232ba29a89a7d611d431 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 23 Jun 2026 15:00:32 +0200 Subject: [PATCH 4/5] Improve importing. --- logging/error_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/logging/error_test.go b/logging/error_test.go index 31f54c2c..237e62c9 100644 --- a/logging/error_test.go +++ b/logging/error_test.go @@ -16,12 +16,14 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -package logging //nolint:testpackage // Shares the package's Ginkgo suite. +package logging_test import ( "errors" "fmt" + "embed-code/embed-code-go/logging" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) @@ -31,7 +33,7 @@ var _ = Describe("Error formatting", func() { It("should format a single error inline", func() { err := errors.New("first failure") - Expect(FormatError("operation failed", err)).To( + Expect(logging.FormatError("operation failed", err)).To( Equal("operation failed: first failure"), ) }) @@ -39,7 +41,7 @@ var _ = Describe("Error formatting", func() { It("should format a nested error inline", func() { err := fmt.Errorf("outer context: %w", errors.New("first failure")) - Expect(FormatError("operation failed", err)).To( + Expect(logging.FormatError("operation failed", err)).To( Equal("operation failed: outer context: first failure"), ) }) @@ -50,7 +52,7 @@ var _ = Describe("Error formatting", func() { errors.New("second failure"), ) - Expect(FormatError("operation failed", err)).To( + Expect(logging.FormatError("operation failed", err)).To( Equal("operation failed:\n- first failure\n- second failure"), ) }) @@ -64,7 +66,7 @@ var _ = Describe("Error formatting", func() { ), ) - Expect(FormatError("operation failed", err)).To( + Expect(logging.FormatError("operation failed", err)).To( Equal("operation failed:" + "\n- first failure" + "\n- second failure" + From 94f773516c141989bbdf519f2ab26d6c5428e8e7 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 23 Jun 2026 17:03:29 +0200 Subject: [PATCH 5/5] Improve error visibility. --- logging/error.go | 2 +- logging/error_test.go | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/logging/error.go b/logging/error.go index c2dec573..03aabb7a 100644 --- a/logging/error.go +++ b/logging/error.go @@ -34,7 +34,7 @@ func FormatError(message string, err error) string { builder.WriteString(message) builder.WriteString(":") for _, nestedErr := range errs { - builder.WriteString("\n- ") + builder.WriteString("\n - ") builder.WriteString(nestedErr.Error()) } diff --git a/logging/error_test.go b/logging/error_test.go index 237e62c9..b375e9f8 100644 --- a/logging/error_test.go +++ b/logging/error_test.go @@ -53,7 +53,9 @@ var _ = Describe("Error formatting", func() { ) Expect(logging.FormatError("operation failed", err)).To( - Equal("operation failed:\n- first failure\n- second failure"), + Equal("operation failed:\n" + + " - first failure\n" + + " - second failure"), ) }) @@ -67,10 +69,10 @@ var _ = Describe("Error formatting", func() { ) Expect(logging.FormatError("operation failed", err)).To( - Equal("operation failed:" + - "\n- first failure" + - "\n- second failure" + - "\n- third failure"), + Equal("operation failed:\n" + + " - first failure\n" + + " - second failure\n" + + " - third failure"), ) }) })