Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 2 additions & 5 deletions pkg/cli/cli-impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/sdsc-ordes/quitsh/pkg/component/stage"
"github.com/sdsc-ordes/quitsh/pkg/config"
"github.com/sdsc-ordes/quitsh/pkg/errors"
errorsfilter "github.com/sdsc-ordes/quitsh/pkg/errors/filter"
"github.com/sdsc-ordes/quitsh/pkg/exec/git"
fs "github.com/sdsc-ordes/quitsh/pkg/filesystem"
"github.com/sdsc-ordes/quitsh/pkg/log"
Expand Down Expand Up @@ -79,11 +80,7 @@ func (c *cliApp) Run() error {

e := c.rootCmd.Execute()
if e != nil {
if c.rootArgs.ErrorSummary {
log.ErrorE(e, "Errors occurred.")
} else {
log.Error("Errors occurred, see above.")
}
log.ErrorE(errorsfilter.FilterAlreadyReported(e), "Errors occurred.")
Comment thread
gabyx marked this conversation as resolved.
}

return e
Expand Down
3 changes: 0 additions & 3 deletions pkg/cli/cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ type (

// Enable running targets in parallel.
Parallel bool `yaml:"parallel"`

// Error summary of all errors at the end.
ErrorSummary bool `yaml:"errorSummary"`
}

Settings struct {
Expand Down
24 changes: 17 additions & 7 deletions pkg/common/stack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,36 @@ func NewStackWithCap[T any](capacity int) Stack[T] {
// Pop pops the top element on the stack.
// The stack size needs to be greater > 0.
func (s *Stack[T]) Pop() T {
debug.Assert(s.Len() != 0, "the stack size is not > 0")

res := s.stack[len(s.stack)-1]
res := s.Top()
s.stack = s.stack[:len(s.stack)-1]

return res
}

// Returns the top element on the stack.
func (s *Stack[T]) Top() T {
debug.Assert(s.Len() != 0, "the stack size is not > 0")

return s.stack[len(s.stack)-1]
}

// PopFront pops the bottom level on the stack.
// This method is useful to do Breath-First-Traversal.
// instead of Depth-First-Traversal when using `Pop`.
Comment thread
gabyx marked this conversation as resolved.
Outdated
func (s *Stack[T]) PopFront() T {
debug.Assert(s.Len() != 0, "the stack size is not > 0")

res := s.stack[0]
func (s *Stack[T]) PopBottom() T {
res := s.Bottom()
s.stack = s.stack[1:]

return res
}

// Returns the bottom element.
func (s *Stack[T]) Bottom() T {
debug.Assert(s.Len() != 0, "the stack size is not > 0")

return s.stack[0]
}

// Visit travers the stack from top to bottom and applies a function.
// If the visitor returns `false` the iteration is aborted.
func (s *Stack[T]) Visit(visitor func(int, T) bool) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/common/stack/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ func TestStackFrontPop(t *testing.T) {
stack.Push("b")
assert.Equal(t, 2, stack.Len())

assert.Equal(t, "a", stack.PopFront())
assert.Equal(t, "a", stack.PopBottom())
assert.Equal(t, 1, stack.Len())

stack.Push("c")
assert.Equal(t, 2, stack.Len())

assert.Equal(t, "b", stack.PopFront())
assert.Equal(t, "b", stack.PopBottom())
assert.Equal(t, 1, stack.Len())

assert.Equal(t, "c", stack.Pop())
assert.Equal(t, 0, stack.Len())

assert.Panics(t, func() { _ = stack.PopFront() })
assert.Panics(t, func() { _ = stack.PopBottom() })
}

func TestStackTraverse(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/dag/execution-order.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func visitNodesBFS(
for bfsStack.Len() != 0 {
log.Tracef("Current BFS stack:\n%v", formatStack(&bfsStack, true))

n := bfsStack.PopFront()
n := bfsStack.PopBottom()
if !visit(n) {
continue
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/dag/run-concurrent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (

const MaxCoroutineConcurrency = 10000

// ExecuteConcurrent executes the DAG concurrent.
func ExecuteConcurrent(
// executeConcurrent executes the DAG concurrent.
func executeConcurrent(
targetNodes TargetNodeMap,
runnerFactory factory.IFactory,
toolchainDispatcher toolchain.IDispatcher,
Expand Down Expand Up @@ -98,7 +98,7 @@ func ExecuteConcurrent(
for _, n := range targetNodes {
summary.AddStatus(n.Execution.Runners...)
}
summary.statuses.Log()
summary.Log()

return summary.allErrors
}
Expand Down
10 changes: 5 additions & 5 deletions pkg/dag/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ func Execute(
opts ...ExecuteOption,
) error {
if parallel {
return ExecuteConcurrent(
return executeConcurrent(
targets,
runnerFactory,
dispatcher,
config,
rootDir, opts...)
} else {
return ExecuteNormal(
return executeNormal(
prios,
runnerFactory,
dispatcher,
Expand All @@ -69,9 +69,9 @@ func Execute(
}
}

// ExecuteNormal executes the DAG non-concurrent.
// executeNormal executes the DAG non-concurrent.
// If no dispatcher is given, the toolchain dispatch is not done.
func ExecuteNormal(
func executeNormal(
prios Priorities,
runnerFactory factory.IFactory,
toolchainDispatcher toolchain.IDispatcher,
Expand Down Expand Up @@ -224,7 +224,7 @@ func executeRunners(
rD.node.PropagateExecStatus()
}

summary.statuses.Log()
summary.Log()

return summary.allErrors
}
Expand Down
14 changes: 12 additions & 2 deletions pkg/dag/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/sdsc-ordes/quitsh/pkg/component/step"
"github.com/sdsc-ordes/quitsh/pkg/component/target"
"github.com/sdsc-ordes/quitsh/pkg/errors"
errorsfilter "github.com/sdsc-ordes/quitsh/pkg/errors/filter"
"github.com/sdsc-ordes/quitsh/pkg/log"
"github.com/sdsc-ordes/quitsh/pkg/runner"
)
Expand Down Expand Up @@ -46,8 +47,7 @@ func (s *Summary) AddStatus(r ...*RunnerStatus) {
}
}

// Log prints the log of the summary.
func (s RunnerStatuses) Log() {
func (s RunnerStatuses) log() {
var sb strings.Builder
sb.WriteString("Summary:\n")

Expand Down Expand Up @@ -83,3 +83,13 @@ func (s RunnerStatuses) Log() {

log.Info(sb.String())
}

// Log prints the log of the summary.
func (s *Summary) Log() {
s.statuses.log()

// Add the sentinell error, for later dropping.
Comment thread
gabyx marked this conversation as resolved.
Outdated
if s.allErrors != nil {
s.allErrors = errorsfilter.WrapAsReported(s.allErrors)
}
}
94 changes: 94 additions & 0 deletions pkg/errors/filter/already-reported.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package errorsfilter

import (
"github.com/hashicorp/go-multierror"
"github.com/sdsc-ordes/quitsh/pkg/common/stack"
"github.com/sdsc-ordes/quitsh/pkg/log"
)

// alreadyReportedErr is an wrapping error which can be dropped by `DropReported`.
type alreadyReportedErr struct {
e error
}

func (e *alreadyReportedErr) Error() string {
return e.e.Error()
}

func (e *alreadyReportedErr) Unwrap() error {
return e.e
}

// WrapAsReported returns a new wrapped errors which indicates
// that it is already reported.
// Useful for bigger errors, which you want to propagate upwards but
// maybe ignore in logging with [FilterAlreadyReported].
func WrapAsReported(e error) error {
return &alreadyReportedErr{e: e}
}

// FilterAlreadyReported filters `ErrAlreadyReported` from the chain.
// NOTE: This only filters on [multierror.Error] s since, `Unwrap` functionality does not let
// reconstruct the error. We are using [multierror.Error] extensively anyways.
//
//nolint:gocognit,nestif // TODO: later.
func FilterAlreadyReported(err error) error {
if err == nil {
return nil
}

type Node struct {
parent *Node
e error

multi *multierror.Error
new error
visited bool // Denotes if we visited the node on down traversal.
}

stack := stack.NewStack[*Node]()
root := &Node{e: err}
stack.Push(root)

for stack.Len() != 0 {
node := stack.Top()
if node.visited {
// Backtracking
// Reconstruct errors.
ignore := false
if _, ok := node.e.(*alreadyReportedErr); ok { //nolint:errorlint // This is ok.
ignore = true
}

if !ignore {
// Create our new error from any children we have.
if node.multi != nil {
node.new = node.multi
} else {
node.new = node.e
}

// Add ourself to parent.
if node.parent != nil {
node.parent.multi.Errors = append(node.parent.multi.Errors, node.new)
}
} else {
log.Debug("Dropping error 'alreadyReportedErr'.")
}

stack.Pop()

continue
}

node.visited = true
if e, ok := node.e.(*multierror.Error); ok { //nolint:errorlint // This is ok.
node.multi = &multierror.Error{} // init a new multi list error
for _, c := range e.Errors {
stack.Push(&Node{e: c, parent: node})
}
}
}

return root.new
}
94 changes: 94 additions & 0 deletions pkg/errors/filter/already-reported_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package errorsfilter

import (
"errors"
"testing"

"github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/require"
)

func TestDropReported(t *testing.T) {
var errA = errors.New("error A")
var errB = errors.New("error B")
var errC = errors.New("error C")
var errD = errors.New("error D")

tests := []struct {
name string
input error
wantNil bool
wantErrs []error // errors expected to survive (checked via errors.Is)
wantDrop []error // errors expected to be dropped
}{
{
name: "nil input",
input: nil,
wantNil: true,
},
{
name: "single error passes through",
input: errA,
wantErrs: []error{errA},
},
{
name: "multierror without ErrReported passes through",
input: multierror.Append(nil, errA, errB),
wantErrs: []error{errA, errB},
},
{
name: "wrapped error is dropped",
input: WrapAsReported(errA),
wantNil: true,
},
{
name: "flat: two dropped in multierror",
input: multierror.Append(nil,
WrapAsReported(errA), // dropped
errB,
WrapAsReported(multierror.Append(nil, errC)), // dropped
),
wantErrs: []error{errB},
wantDrop: []error{errA, errC},
},
{
name: "nested: two dropped in multierror",
input: &multierror.Error{
Errors: []error{
WrapAsReported(errA),
errB,
&multierror.Error{
Errors: []error{
WrapAsReported(errC),
errD,
},
},
},
},
wantErrs: []error{errB, errD},
wantDrop: []error{errA, errC},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FilterAlreadyReported(tt.input)

if tt.wantNil {
require.NoError(t, result)

return
}

require.Error(t, result)

for _, want := range tt.wantErrs {
require.ErrorIs(t, result, want)
}

for _, dropped := range tt.wantDrop {
require.NotErrorIs(t, result, dropped)
}
})
}
}
Loading