Skip to content

Commit a67e84d

Browse files
authored
feat: filter reported errors (#70)
- Add `WrapAsReported` / `FilterAlreadyReported` utilities (plus tests) to mark and filter “already reported” errors. - Wrap `Summary.allErrors` after DAG summary logging, and filter the final CLI error before printing an error summary. - Rename stack dequeue API (`PopFront` → `PopBottom`) and adjust BFS usage accordingly.
1 parent f1be3b9 commit a67e84d

10 files changed

Lines changed: 232 additions & 30 deletions

File tree

pkg/cli/cli-impl.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/sdsc-ordes/quitsh/pkg/component/stage"
1313
"github.com/sdsc-ordes/quitsh/pkg/config"
1414
"github.com/sdsc-ordes/quitsh/pkg/errors"
15+
errorsfilter "github.com/sdsc-ordes/quitsh/pkg/errors/filter"
1516
"github.com/sdsc-ordes/quitsh/pkg/exec/git"
1617
fs "github.com/sdsc-ordes/quitsh/pkg/filesystem"
1718
"github.com/sdsc-ordes/quitsh/pkg/log"
@@ -79,11 +80,7 @@ func (c *cliApp) Run() error {
7980

8081
e := c.rootCmd.Execute()
8182
if e != nil {
82-
if c.rootArgs.ErrorSummary {
83-
log.ErrorE(e, "Errors occurred.")
84-
} else {
85-
log.Error("Errors occurred, see above.")
86-
}
83+
log.ErrorE(errorsfilter.FilterAlreadyReported(e), "Errors occurred.")
8784
}
8885

8986
return e

pkg/cli/cmd/root/root.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,6 @@ type (
6565

6666
// Enable running targets in parallel.
6767
Parallel bool `yaml:"parallel"`
68-
69-
// Error summary of all errors at the end.
70-
ErrorSummary bool `yaml:"errorSummary"`
7168
}
7269

7370
Settings struct {

pkg/common/stack/stack.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,36 @@ func NewStackWithCap[T any](capacity int) Stack[T] {
2525
// Pop pops the top element on the stack.
2626
// The stack size needs to be greater > 0.
2727
func (s *Stack[T]) Pop() T {
28-
debug.Assert(s.Len() != 0, "the stack size is not > 0")
29-
30-
res := s.stack[len(s.stack)-1]
28+
res := s.Top()
3129
s.stack = s.stack[:len(s.stack)-1]
3230

3331
return res
3432
}
3533

36-
// PopFront pops the bottom level on the stack.
37-
// This method is useful to do Breath-First-Traversal.
38-
// instead of Depth-First-Traversal when using `Pop`.
39-
func (s *Stack[T]) PopFront() T {
34+
// Returns the top element on the stack.
35+
func (s *Stack[T]) Top() T {
4036
debug.Assert(s.Len() != 0, "the stack size is not > 0")
4137

42-
res := s.stack[0]
38+
return s.stack[len(s.stack)-1]
39+
}
40+
41+
// PopBottom pops the bottom level on the stack.
42+
// This method is useful to do Breadth-First Traversal
43+
// instead of Depth-First Traversal when using `Pop`.
44+
func (s *Stack[T]) PopBottom() T {
45+
res := s.Bottom()
4346
s.stack = s.stack[1:]
4447

4548
return res
4649
}
4750

51+
// Returns the bottom element.
52+
func (s *Stack[T]) Bottom() T {
53+
debug.Assert(s.Len() != 0, "the stack size is not > 0")
54+
55+
return s.stack[0]
56+
}
57+
4858
// Visit travers the stack from top to bottom and applies a function.
4959
// If the visitor returns `false` the iteration is aborted.
5060
func (s *Stack[T]) Visit(visitor func(int, T) bool) {

pkg/common/stack/stack_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,19 @@ func TestStackFrontPop(t *testing.T) {
4343
stack.Push("b")
4444
assert.Equal(t, 2, stack.Len())
4545

46-
assert.Equal(t, "a", stack.PopFront())
46+
assert.Equal(t, "a", stack.PopBottom())
4747
assert.Equal(t, 1, stack.Len())
4848

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

52-
assert.Equal(t, "b", stack.PopFront())
52+
assert.Equal(t, "b", stack.PopBottom())
5353
assert.Equal(t, 1, stack.Len())
5454

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

58-
assert.Panics(t, func() { _ = stack.PopFront() })
58+
assert.Panics(t, func() { _ = stack.PopBottom() })
5959
}
6060

6161
func TestStackTraverse(t *testing.T) {

pkg/dag/execution-order.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ func visitNodesBFS(
377377
for bfsStack.Len() != 0 {
378378
log.Tracef("Current BFS stack:\n%v", formatStack(&bfsStack, true))
379379

380-
n := bfsStack.PopFront()
380+
n := bfsStack.PopBottom()
381381
if !visit(n) {
382382
continue
383383
}

pkg/dag/run-concurrent.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616

1717
const MaxCoroutineConcurrency = 10000
1818

19-
// ExecuteConcurrent executes the DAG concurrent.
20-
func ExecuteConcurrent(
19+
// executeConcurrent executes the DAG concurrent.
20+
func executeConcurrent(
2121
targetNodes TargetNodeMap,
2222
runnerFactory factory.IFactory,
2323
toolchainDispatcher toolchain.IDispatcher,
@@ -98,7 +98,7 @@ func ExecuteConcurrent(
9898
for _, n := range targetNodes {
9999
summary.AddStatus(n.Execution.Runners...)
100100
}
101-
summary.statuses.Log()
101+
summary.Log()
102102

103103
return summary.allErrors
104104
}

pkg/dag/run.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ func Execute(
5252
opts ...ExecuteOption,
5353
) error {
5454
if parallel {
55-
return ExecuteConcurrent(
55+
return executeConcurrent(
5656
targets,
5757
runnerFactory,
5858
dispatcher,
5959
config,
6060
rootDir, opts...)
6161
} else {
62-
return ExecuteNormal(
62+
return executeNormal(
6363
prios,
6464
runnerFactory,
6565
dispatcher,
@@ -69,9 +69,9 @@ func Execute(
6969
}
7070
}
7171

72-
// ExecuteNormal executes the DAG non-concurrent.
72+
// executeNormal executes the DAG non-concurrent.
7373
// If no dispatcher is given, the toolchain dispatch is not done.
74-
func ExecuteNormal(
74+
func executeNormal(
7575
prios Priorities,
7676
runnerFactory factory.IFactory,
7777
toolchainDispatcher toolchain.IDispatcher,
@@ -224,7 +224,7 @@ func executeRunners(
224224
rD.node.PropagateExecStatus()
225225
}
226226

227-
summary.statuses.Log()
227+
summary.Log()
228228

229229
return summary.allErrors
230230
}

pkg/dag/status.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/sdsc-ordes/quitsh/pkg/component/step"
1010
"github.com/sdsc-ordes/quitsh/pkg/component/target"
1111
"github.com/sdsc-ordes/quitsh/pkg/errors"
12+
errorsfilter "github.com/sdsc-ordes/quitsh/pkg/errors/filter"
1213
"github.com/sdsc-ordes/quitsh/pkg/log"
1314
"github.com/sdsc-ordes/quitsh/pkg/runner"
1415
)
@@ -46,8 +47,7 @@ func (s *Summary) AddStatus(r ...*RunnerStatus) {
4647
}
4748
}
4849

49-
// Log prints the log of the summary.
50-
func (s RunnerStatuses) Log() {
50+
func (s RunnerStatuses) log() {
5151
var sb strings.Builder
5252
sb.WriteString("Summary:\n")
5353

@@ -83,3 +83,13 @@ func (s RunnerStatuses) Log() {
8383

8484
log.Info(sb.String())
8585
}
86+
87+
// Log prints the log of the summary.
88+
func (s *Summary) Log() {
89+
s.statuses.log()
90+
91+
// Add the sentinel error, for later dropping.
92+
if s.allErrors != nil {
93+
s.allErrors = errorsfilter.WrapAsReported(s.allErrors)
94+
}
95+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package errorsfilter
2+
3+
import (
4+
"github.com/hashicorp/go-multierror"
5+
"github.com/sdsc-ordes/quitsh/pkg/common/stack"
6+
"github.com/sdsc-ordes/quitsh/pkg/log"
7+
)
8+
9+
// alreadyReportedError is an wrapping error which can be dropped by `DropReported`.
10+
type alreadyReportedError struct {
11+
e error
12+
}
13+
14+
func (e *alreadyReportedError) Error() string {
15+
return e.e.Error()
16+
}
17+
18+
func (e *alreadyReportedError) Unwrap() error {
19+
return e.e
20+
}
21+
22+
// WrapAsReported returns a new wrapped error which indicates
23+
// that it is already reported.
24+
// Useful for bigger errors, which you still want to propagate upwards but
25+
// maybe ignore in logging then with [FilterAlreadyReported].
26+
func WrapAsReported(e error) error {
27+
return &alreadyReportedError{e: e}
28+
}
29+
30+
// FilterAlreadyReported filters `ErrAlreadyReported` from the chain.
31+
// NOTE: This only filters on [multierror.Error] s since, `Unwrap` functionality does not let
32+
// reconstruct the error. We are using [multierror.Error] extensively anyways.
33+
//
34+
//nolint:gocognit,nestif // TODO: later.
35+
func FilterAlreadyReported(err error) error {
36+
if err == nil {
37+
return nil
38+
}
39+
40+
type Node struct {
41+
parent *Node
42+
e error
43+
44+
multi *multierror.Error
45+
new error
46+
visited bool // Denotes if we visited the node on down traversal.
47+
}
48+
49+
stack := stack.NewStack[*Node]()
50+
root := &Node{e: err}
51+
stack.Push(root)
52+
53+
for stack.Len() != 0 {
54+
node := stack.Top()
55+
if node.visited {
56+
// Backtracking
57+
// Reconstruct errors.
58+
ignore := false
59+
if _, ok := node.e.(*alreadyReportedError); ok { //nolint:errorlint // This is ok.
60+
ignore = true
61+
}
62+
63+
if !ignore {
64+
// Create our new error from any children we have.
65+
if node.multi != nil {
66+
node.new = node.multi
67+
} else {
68+
node.new = node.e
69+
}
70+
71+
// Add ourself to parent.
72+
if node.parent != nil {
73+
node.parent.multi.Errors = append(node.parent.multi.Errors, node.new)
74+
}
75+
} else {
76+
log.Debug("Dropping error 'alreadyReportedErr'.")
77+
}
78+
79+
stack.Pop()
80+
81+
continue
82+
}
83+
84+
node.visited = true
85+
if e, ok := node.e.(*multierror.Error); ok { //nolint:errorlint // This is ok.
86+
node.multi = &multierror.Error{} // init a new multi list error
87+
for _, c := range e.Errors {
88+
stack.Push(&Node{e: c, parent: node})
89+
}
90+
}
91+
}
92+
93+
return root.new
94+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package errorsfilter
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/hashicorp/go-multierror"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestDropReported(t *testing.T) {
12+
var errA = errors.New("error A")
13+
var errB = errors.New("error B")
14+
var errC = errors.New("error C")
15+
var errD = errors.New("error D")
16+
17+
tests := []struct {
18+
name string
19+
input error
20+
wantNil bool
21+
wantErrs []error // errors expected to survive (checked via errors.Is)
22+
wantDrop []error // errors expected to be dropped
23+
}{
24+
{
25+
name: "nil input",
26+
input: nil,
27+
wantNil: true,
28+
},
29+
{
30+
name: "single error passes through",
31+
input: errA,
32+
wantErrs: []error{errA},
33+
},
34+
{
35+
name: "multierror without ErrReported passes through",
36+
input: multierror.Append(nil, errA, errB),
37+
wantErrs: []error{errA, errB},
38+
},
39+
{
40+
name: "wrapped error is dropped",
41+
input: WrapAsReported(errA),
42+
wantNil: true,
43+
},
44+
{
45+
name: "flat: two dropped in multierror",
46+
input: multierror.Append(nil,
47+
WrapAsReported(errA), // dropped
48+
errB,
49+
WrapAsReported(multierror.Append(nil, errC)), // dropped
50+
),
51+
wantErrs: []error{errB},
52+
wantDrop: []error{errA, errC},
53+
},
54+
{
55+
name: "nested: two dropped in multierror",
56+
input: &multierror.Error{
57+
Errors: []error{
58+
WrapAsReported(errA),
59+
errB,
60+
&multierror.Error{
61+
Errors: []error{
62+
WrapAsReported(errC),
63+
errD,
64+
},
65+
},
66+
},
67+
},
68+
wantErrs: []error{errB, errD},
69+
wantDrop: []error{errA, errC},
70+
},
71+
}
72+
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
result := FilterAlreadyReported(tt.input)
76+
77+
if tt.wantNil {
78+
require.NoError(t, result)
79+
80+
return
81+
}
82+
83+
require.Error(t, result)
84+
85+
for _, want := range tt.wantErrs {
86+
require.ErrorIs(t, result, want)
87+
}
88+
89+
for _, dropped := range tt.wantDrop {
90+
require.NotErrorIs(t, result, dropped)
91+
}
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)