Skip to content

Commit

Permalink
Raise PathUnsupported & PathInvalid errors
Browse files Browse the repository at this point in the history
  • Loading branch information
gordon-klotho committed Feb 6, 2024
1 parent a387089 commit d258674
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 177 deletions.
8 changes: 4 additions & 4 deletions create_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ if [ ! "$test_dir/$name.input.yaml" -ef "$1" ]; then
cp $1 "$test_dir/$name.input.yaml"
fi

cp "$out_dir/resources.yaml" "$test_dir/$name.expect.yaml"
cp "$out_dir/dataflow-topology.yaml" "$test_dir/$name.dataflow-viz.yaml"
cp "$out_dir/iac-topology.yaml" "$test_dir/$name.iac-viz.yaml"
cp "$out_dir/error_details.json" "$test_dir/$name.err.json"
[ -e "$out_dir/resources.yaml" ] && cp "$out_dir/resources.yaml" "$test_dir/$name.expect.yaml"
[ -e "$out_dir/dataflow-topology.yaml" ] && cp "$out_dir/dataflow-topology.yaml" "$test_dir/$name.dataflow-viz.yaml"
[ -e "$out_dir/iac-topology.yaml" ] && cp "$out_dir/iac-topology.yaml" "$test_dir/$name.iac-viz.yaml"
[ -e "$out_dir/error_details.json" ] && cp "$out_dir/error_details.json" "$test_dir/$name.err.json"

rm -rf $out_dir
31 changes: 26 additions & 5 deletions pkg/engine2/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,30 @@ func setupProfiling() func() {
return func() {}
}

func extractEngineErrors(err error) []engine_errs.EngineError {
if err == nil {
return nil
}
var errs []engine_errs.EngineError
queue := []error{err}
for len(queue) > 0 {
err := queue[0]
queue = queue[1:]
switch err := err.(type) {
case engine_errs.EngineError:
errs = append(errs, err)
case interface{ Unwrap() []error }:
queue = append(queue, err.Unwrap()...)
case interface{ Unwrap() error }:
queue = append(queue, err.Unwrap())
}
}
if len(errs) == 0 {
errs = append(errs, engine_errs.InternalError{Err: err})
}
return errs
}

func (em *EngineMain) Run(context *EngineContext) (int, []engine_errs.EngineError) {
returnCode := 0
var engErrs []engine_errs.EngineError
Expand All @@ -274,11 +298,8 @@ func (em *EngineMain) Run(context *EngineContext) (int, []engine_errs.EngineErro
// When the engine returns an error, that indicates that it halted evaluation, thus is a fatal error.
// This is returned as exit code 1, and add the details to be printed to stdout.
returnCode = 1
if ee, ok := err.(engine_errs.EngineError); ok {
engErrs = append(engErrs, ee)
} else {
engErrs = append(engErrs, engine_errs.InternalError{Err: engine_errs.ErrorsToTree(err)})
}
engErrs = append(engErrs, extractEngineErrors(err)...)
zap.S().Errorf("Engine returned error: %v", err)
}

if len(context.Solutions) > 0 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/engine2/edge_targets.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (e *Engine) EdgeCanBeExpanded(ctx *solutionContext, source construct.Resour
}

_, err = edgeExpander.ExpandEdge(path_selection.ExpansionInput{
Dep: construct.ResourceEdge{
SatisfactionEdge: construct.ResourceEdge{
Source: tempSourceResource,
Target: tempTargetResource,
},
Expand Down
24 changes: 14 additions & 10 deletions pkg/engine2/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,8 @@ func (tc engineTestCase) Test(t *testing.T) {
t.Fatal(fmt.Errorf("failed to open input file: %w", err))
}
defer inputYaml.Close()
expectYaml, err := os.Open(strings.Replace(tc.inputPath, ".input.yaml", ".expect.yaml", 1))
if err != nil {
t.Fatal(fmt.Errorf("failed to open expected output file: %w", err))
}
defer expectYaml.Close()

inputFile := tc.readGraph(t, inputYaml)
expectContent, err := io.ReadAll(expectYaml)
if err != nil {
t.Fatal(fmt.Errorf("failed to read expected output file: %w", err))
}

main := EngineMain{}
err = main.AddEngine()
Expand All @@ -75,7 +66,7 @@ func (tc engineTestCase) Test(t *testing.T) {
Constraints: inputFile.Constraints,
InitialState: inputFile.Graph,
}
_, engineErrs := main.Run(context)
returnCode, engineErrs := main.Run(context)
// TODO find a convenient way to specify the return code in the testdata

errDetails := new(bytes.Buffer)
Expand All @@ -92,12 +83,25 @@ func (tc engineTestCase) Test(t *testing.T) {
}
assertErrDetails(t, errDetailsFile, engineErrs)

if returnCode == 1 {
// Run resulted in a failure. After checking the error details, we're done.
return
}
sol := context.Solutions[0]
actualContent, err := yaml.Marshal(construct.YamlGraph{Graph: sol.DataflowGraph()})
if err != nil {
t.Fatal(fmt.Errorf("failed to marshal actual output: %w", err))
}

expectYaml, err := os.Open(strings.Replace(tc.inputPath, ".input.yaml", ".expect.yaml", 1))
if err != nil {
t.Fatal(fmt.Errorf("failed to open expected output file: %w", err))
}
defer expectYaml.Close()
expectContent, err := io.ReadAll(expectYaml)
if err != nil {
t.Fatal(fmt.Errorf("failed to read expected output file: %w", err))
}
assertYamlMatches(t, string(expectContent), string(actualContent), "dataflow")

// Always visualize views even if we're not testing them to make sure that it at least succeeds
Expand Down
95 changes: 95 additions & 0 deletions pkg/engine2/errors/error_tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package engine_errs

import (
"fmt"
"strings"
)

type (
ErrorTree struct {
Chain []string `json:"chain,omitempty"`
Children []ErrorTree `json:"children,omitempty"`
}

chainErr interface {
error
Unwrap() error
}
joinErr interface {
error
Unwrap() []error
}
)

func unwrapChain(err error) (chain []string, last joinErr) {
for current := err; current != nil; {
var next error
cc, ok := current.(chainErr)
if ok {
next = cc.Unwrap()
} else {
joined, ok := current.(joinErr)
if ok {
jerrs := joined.Unwrap()
if len(jerrs) == 1 {
next = jerrs[0]
} else {
last = joined
return
}
} else {
chain = append(chain, current.Error())
return
}
}
msg := strings.TrimSuffix(strings.TrimSuffix(current.Error(), next.Error()), ": ")
if msg != "" {
chain = append(chain, msg)
}
current = next
}
return
}

func ErrorsToTree(err error) (tree ErrorTree) {
if err == nil {
return
}
if t, ok := err.(ErrorTree); ok {
return t
}

var joined joinErr
tree.Chain, joined = unwrapChain(err)

if joined != nil {
errs := joined.Unwrap()
tree.Children = make([]ErrorTree, len(errs))
for i, e := range errs {
tree.Children[i] = ErrorsToTree(e)
}
}
return
}

func (t ErrorTree) Error() string {
sb := &strings.Builder{}
t.print(sb, 0, 0)
return sb.String()
}

func (t ErrorTree) print(out *strings.Builder, indent int, childChar rune) {
prefix := strings.Repeat("\t", indent)
delim := ""
if childChar != 0 {
delim = string(childChar) + " "
}
fmt.Fprintf(out, "%s%s%v\n", prefix, delim, t.Chain)
for i, child := range t.Children {
char := '├'
if i == len(t.Children)-1 {
char = '└'
}
child.print(out, indent+1, char)
}
}
140 changes: 63 additions & 77 deletions pkg/engine2/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package engine_errs

import (
"fmt"
"strings"

construct "github.com/klothoplatform/klotho/pkg/construct2"
)

type (
Expand All @@ -16,15 +17,6 @@ type (
}

ErrorCode string

InternalError struct {
Err error
}

ErrorTree struct {
Chain []string `json:"chain,omitempty"`
Children []ErrorTree `json:"children,omitempty"`
}
)

const (
Expand All @@ -34,6 +26,10 @@ const (
EdgeUnsupportedCode ErrorCode = "edge_unsupported"
)

type InternalError struct {
Err error
}

func (e InternalError) Error() string {
return fmt.Sprintf("internal error: %v", e.Err)
}
Expand All @@ -50,86 +46,76 @@ func (e InternalError) Unwrap() error {
return e.Err
}

type (
chainErr interface {
error
Unwrap() error
}
joinErr interface {
error
Unwrap() []error
}
)
type UnsupportedExpansionErr struct {
// ExpandEdge is the overall edge that is being expanded
ExpandEdge construct.SimpleEdge
// SatisfactionEdge is the specific edge that was being expanded when the error occurred
SatisfactionEdge construct.SimpleEdge
Classification string
}

func unwrapChain(err error) (chain []string, last joinErr) {
for current := err; current != nil; {
var next error
cc, ok := current.(chainErr)
if ok {
next = cc.Unwrap()
} else {
joined, ok := current.(joinErr)
if ok {
jerrs := joined.Unwrap()
if len(jerrs) == 1 {
next = jerrs[0]
} else {
last = joined
return
}
} else {
chain = append(chain, current.Error())
return
}
}
msg := strings.TrimSuffix(strings.TrimSuffix(current.Error(), next.Error()), ": ")
if msg != "" {
chain = append(chain, msg)
}
current = next
func (e UnsupportedExpansionErr) Error() string {
if e.SatisfactionEdge.Source.IsZero() || e.ExpandEdge == e.SatisfactionEdge {
return fmt.Sprintf("unsupported expansion %s in %s", e.ExpandEdge, e.Classification)
}
return
return fmt.Sprintf(
"while expanding %s, unsupported expansion of %s in %s",
e.ExpandEdge,
e.Classification,
e.SatisfactionEdge,
)
}

func (e UnsupportedExpansionErr) ErrorCode() ErrorCode {
return EdgeUnsupportedCode
}

func ErrorsToTree(err error) (tree ErrorTree) {
if err == nil {
return
func (e UnsupportedExpansionErr) ToJSONMap() map[string]any {
m := map[string]any{
"satisfaction_edge": e.SatisfactionEdge,
}
if !e.ExpandEdge.Source.IsZero() {
m["expand_edge"] = e.ExpandEdge
}
if t, ok := err.(ErrorTree); ok {
return t
if e.Classification != "" {
m["classification"] = e.Classification
}
return m
}

var joined joinErr
tree.Chain, joined = unwrapChain(err)
type InvalidPathErr struct {
// ExpandEdge is the overall edge that is being expanded
ExpandEdge construct.SimpleEdge
// SatisfactionEdge is the specific edge that was being expanded when the error occurred
SatisfactionEdge construct.SimpleEdge
Classification string
}

if joined != nil {
errs := joined.Unwrap()
tree.Children = make([]ErrorTree, len(errs))
for i, e := range errs {
tree.Children[i] = ErrorsToTree(e)
}
func (e InvalidPathErr) Error() string {
if e.SatisfactionEdge.Source.IsZero() || e.ExpandEdge == e.SatisfactionEdge {
return fmt.Sprintf("invalid expansion %s in %s", e.ExpandEdge, e.Classification)
}
return
return fmt.Sprintf(
"while expanding %s, invalid expansion of %s in %s",
e.ExpandEdge,
e.Classification,
e.SatisfactionEdge,
)
}

func (t ErrorTree) Error() string {
sb := &strings.Builder{}
t.print(sb, 0, 0)
return sb.String()
func (e InvalidPathErr) ErrorCode() ErrorCode {
return EdgeInvalidCode
}

func (t ErrorTree) print(out *strings.Builder, indent int, childChar rune) {
prefix := strings.Repeat("\t", indent)
delim := ""
if childChar != 0 {
delim = string(childChar) + " "
func (e InvalidPathErr) ToJSONMap() map[string]any {
m := map[string]any{
"satisfaction_edge": e.SatisfactionEdge,
}
if !e.ExpandEdge.Source.IsZero() {
m["expand_edge"] = e.ExpandEdge
}
fmt.Fprintf(out, "%s%s%v\n", prefix, delim, t.Chain)
for i, child := range t.Children {
char := '├'
if i == len(t.Children)-1 {
char = '└'
}
child.print(out, indent+1, char)
if e.Classification != "" {
m["classification"] = e.Classification
}
return m
}
Loading

0 comments on commit d258674

Please sign in to comment.