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
59 changes: 59 additions & 0 deletions logging/error.go
Original file line number Diff line number Diff line change
@@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function name lowercase, while the function above is uppercase?

@MykytaPimonovTD MykytaPimonovTD Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Oleg-Melnik In GoLang, names that start with an uppercase letter are exported, meaning they can be accessed from other packages. Names that start with a lowercase letter are unexported and can be used only within the same package. This convention provides simple visibility control without separate public or private keywords.

It's a very strange decision, but those are the rules of the language.

This is a docs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Go, names starting with an uppercase letter are public and accessible from other packages. Names starting with a lowercase letter are private to their package.

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
}
78 changes: 78 additions & 0 deletions logging/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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_test

import (
"errors"
"fmt"

"embed-code/embed-code-go/logging"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Error formatting", func() {
Comment on lines +21 to +31

@Vladyslav-Kuksiuk Vladyslav-Kuksiuk Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is not true. All eight tests - four from logger_test and four from error_test - run successfully.
image


It("should format a single error inline", func() {
err := errors.New("first failure")

Expect(logging.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(logging.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(logging.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(logging.FormatError("operation failed", err)).To(
Equal("operation failed:\n" +
" - first failure\n" +
" - second failure\n" +
" - third failure"),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equal("operation failed:\n" +
				"- first failure\n" +
				"- second failure\n" +
				"- third failure"),

})
})
16 changes: 1 addition & 15 deletions logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
78 changes: 38 additions & 40 deletions logging/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
})
})
42 changes: 1 addition & 41 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,49 +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(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
}

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

Expand Down
Loading