Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase path_selection coverage to > 10% #904

Merged
merged 1 commit into from
Jan 30, 2024
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
2 changes: 1 addition & 1 deletion pkg/construct2/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func NewGraph(options ...func(*graph.Traits)) Graph {
}

func NewAcyclicGraph(options ...func(*graph.Traits)) Graph {
return NewGraph(graph.PreventCycles())
return NewGraphWithOptions(append(options, graph.Directed(), graph.PreventCycles())...)
}

func ResourceHasher(r *Resource) ResourceId {
Expand Down
21 changes: 15 additions & 6 deletions pkg/construct2/graph_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,29 @@ func (e SimpleEdge) Less(other SimpleEdge) bool {
return ResourceIdLess(e.Target, other.Target)
}

func (e *SimpleEdge) UnmarshalText(data []byte) error {
s := string(data)

func (e *SimpleEdge) Parse(s string) error {
source, target, found := strings.Cut(s, " -> ")
if !found {
target, source, found = strings.Cut(s, " <- ")
if !found {
return errors.New("invalid edge format, expected either `source -> target` or `target <- source`")
}
}
return errors.Join(
e.Source.Parse(source),
e.Target.Parse(target),
)
}

func (e *SimpleEdge) Validate() error {
return errors.Join(e.Source.Validate(), e.Target.Validate())
}

srcErr := e.Source.UnmarshalText([]byte(source))
tgtErr := e.Target.UnmarshalText([]byte(target))
return errors.Join(srcErr, tgtErr)
func (e *SimpleEdge) UnmarshalText(data []byte) error {
if err := e.Parse(string(data)); err != nil {
return err
}
return e.Validate()
}

func (e SimpleEdge) ToEdge() Edge {
Expand Down
138 changes: 73 additions & 65 deletions pkg/construct2/graphtest/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,84 @@ func AssertGraphContains(t *testing.T, expect, actual construct2.Graph) {
}
}

func stringToGraphElement(e string) (any, error) {
func StringToGraphElement(e string) (any, error) {
var id construct2.ResourceId
idErr := id.UnmarshalText([]byte(e))
if idErr == nil {
idErr := id.Parse(e)
if id.Validate() == nil {
return id, nil
}
var edge construct2.SimpleEdge
edgeErr := edge.UnmarshalText([]byte(e))
if edgeErr == nil {
return edge, nil
}

var path construct2.Path
pathErr := path.UnmarshalText([]byte(e))
if pathErr == nil {
pathErr := path.Parse(e)
if len(path) > 0 {
return path, nil
}

return nil, errors.Join(idErr, edgeErr, pathErr)
return nil, errors.Join(idErr, pathErr)
}

// AddElement is a utility function for adding an element to a graph. See [MakeGraph] for more information on supported
// element types. Returns whether adding the element failed.
func AddElement(t *testing.T, g construct2.Graph, e any) (failed bool) {
must := func(err error) {
if err != nil {
t.Fatal(err)
}
}
if estr, ok := e.(string); ok {
var err error
e, err = StringToGraphElement(estr)
if err != nil {
t.Errorf("invalid element %q (type %[1]T) Parse errors: %v", e, err)
return true
}
}

addIfMissing := func(res *construct2.Resource) {
if _, err := g.Vertex(res.ID); errors.Is(err, graph.ErrVertexNotFound) {
must(g.AddVertex(res))
} else if err != nil {
t.Fatal(fmt.Errorf("could check vertex %s: %w", res.ID, err))
}
}

switch e := e.(type) {
case construct2.ResourceId:
addIfMissing(&construct2.Resource{ID: e})

case construct2.Resource:
must(g.AddVertex(&e))

case *construct2.Resource:
must(g.AddVertex(e))

case construct2.Edge:
addIfMissing(&construct2.Resource{ID: e.Source})
addIfMissing(&construct2.Resource{ID: e.Target})
must(g.AddEdge(e.Source, e.Target))

case construct2.ResourceEdge:
addIfMissing(e.Source)
addIfMissing(e.Target)
must(g.AddEdge(e.Source.ID, e.Target.ID))

case construct2.SimpleEdge:
addIfMissing(&construct2.Resource{ID: e.Source})
addIfMissing(&construct2.Resource{ID: e.Target})
must(g.AddEdge(e.Source, e.Target))

case construct2.Path:
for i, id := range e {
addIfMissing(&construct2.Resource{ID: id})
if i > 0 {
must(g.AddEdge(e[i-1], id))
}
}
default:
t.Errorf("invalid element of type %T", e)
return true
}
return false
}

// MakeGraph is a utility function for creating a graph from a list of elements which can be of types:
Expand All @@ -92,62 +151,11 @@ func stringToGraphElement(e string) (any, error) {
// return MakeGraph(t, NewGraph(), elements...)
// }
func MakeGraph(t *testing.T, g construct2.Graph, elements ...any) construct2.Graph {
must := func(err error) {
if err != nil {
t.Fatal(err)
}
}
addIfMissing := func(res *construct2.Resource) {
if _, err := g.Vertex(res.ID); errors.Is(err, graph.ErrVertexNotFound) {
must(g.AddVertex(res))
} else if err != nil {
t.Fatal(fmt.Errorf("could check vertex %s: %w", res.ID, err))
}
}
failed := false
for i, e := range elements {
if estr, ok := e.(string); ok {
var err error
e, err = stringToGraphElement(estr)
if err != nil {
t.Errorf("invalid element[%d] %q (type %[2]T) Parse errors: %v", i, e, err)
failed = true
}
}
switch e := e.(type) {
case construct2.ResourceId:
addIfMissing(&construct2.Resource{ID: e})

case construct2.Resource:
must(g.AddVertex(&e))

case *construct2.Resource:
must(g.AddVertex(e))

case construct2.Edge:
addIfMissing(&construct2.Resource{ID: e.Source})
addIfMissing(&construct2.Resource{ID: e.Target})
must(g.AddEdge(e.Source, e.Target))

case construct2.ResourceEdge:
addIfMissing(e.Source)
addIfMissing(e.Target)
must(g.AddEdge(e.Source.ID, e.Target.ID))

case construct2.SimpleEdge:
addIfMissing(&construct2.Resource{ID: e.Source})
addIfMissing(&construct2.Resource{ID: e.Target})
must(g.AddEdge(e.Source, e.Target))

case construct2.Path:
for i, id := range e {
addIfMissing(&construct2.Resource{ID: id})
if i > 0 {
must(g.AddEdge(e[i-1], id))
}
}
default:
t.Errorf("invalid element[%d] of type %T", i, e)
elemFailed := AddElement(t, g, e)
if elemFailed {
t.Errorf("failed to add element[%d] (%v) to graph", i, e)
failed = true
}
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/construct2/graphtest/ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func ParseId(t *testing.T, str string) (id construct.ResourceId) {
err := id.UnmarshalText([]byte(str))
err := id.Parse(str)
if err != nil {
t.Fatalf("failed to parse resource id %q: %v", str, err)
}
Expand All @@ -16,7 +16,7 @@ func ParseId(t *testing.T, str string) (id construct.ResourceId) {

func ParseEdge(t *testing.T, str string) construct.Edge {
var io construct.SimpleEdge
err := io.UnmarshalText([]byte(str))
err := io.Parse(str)
if err != nil {
t.Fatalf("failed to parse edge %q: %v", str, err)
}
Expand All @@ -28,7 +28,7 @@ func ParseEdge(t *testing.T, str string) construct.Edge {

func ParseRef(t *testing.T, str string) construct.PropertyRef {
var ref construct.PropertyRef
err := ref.UnmarshalText([]byte(str))
err := ref.Parse(str)
if err != nil {
t.Fatalf("failed to parse property ref %q: %v", str, err)
}
Expand All @@ -37,7 +37,7 @@ func ParseRef(t *testing.T, str string) construct.PropertyRef {

func ParsePath(t *testing.T, str string) construct.Path {
var path construct.Path
err := path.UnmarshalText([]byte(str))
err := path.Parse(str)
if err != nil {
t.Fatalf("failed to parse path %q: %v", str, err)
}
Expand Down
29 changes: 24 additions & 5 deletions pkg/construct2/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,37 @@ func (p Path) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}

func (p *Path) UnmarshalText(text []byte) error {
parts := strings.Split(string(text), " -> ")
func (p *Path) Parse(s string) error {
parts := strings.Split(s, " -> ")
*p = make(Path, len(parts))
var errs error
for i, part := range parts {
var id ResourceId
err := id.UnmarshalText([]byte(part))
err := id.Parse(part)
if err != nil {
return err
errs = errors.Join(errs, fmt.Errorf("could not parse path[%d]: %w", i, err))
}
(*p)[i] = id
}
return nil
return errs
}

func (p *Path) Validate() error {
var errs error
for i, id := range *p {
err := id.Validate()
if err != nil {
errs = errors.Join(errs, fmt.Errorf("path[%d] invalid: %w", i, err))
}
}
return errs
}

func (p *Path) UnmarshalText(text []byte) error {
if err := p.Parse(string(text)); err != nil {
return err
}
return p.Validate()
}

func (d *Dependencies) Add(p Path) {
Expand Down
25 changes: 16 additions & 9 deletions pkg/construct2/property_ref.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package construct2

import (
"bytes"
"fmt"
"strings"
)

type PropertyRef struct {
Expand All @@ -18,15 +18,22 @@ func (v PropertyRef) MarshalText() ([]byte, error) {
return []byte(v.String()), nil
}

func (v *PropertyRef) UnmarshalText(b []byte) error {
parts := bytes.SplitN(b, []byte("#"), 2)
if len(parts) != 2 {
return fmt.Errorf("invalid PropertyRef format: %s", string(b))
func (v *PropertyRef) Parse(s string) error {
res, prop, ok := strings.Cut(s, "#")
if !ok {
return fmt.Errorf("invalid PropertyRef format: %s", s)
}
err := v.Resource.UnmarshalText(parts[0])
if err != nil {
v.Property = prop
return v.Resource.Parse(res)
}

func (v *PropertyRef) Validate() error {
return v.Resource.Validate()
}

func (v *PropertyRef) UnmarshalText(b []byte) error {
if err := v.Parse(string(b)); err != nil {
return err
}
v.Property = string(parts[1])
return nil
return v.Validate()
}
18 changes: 15 additions & 3 deletions pkg/construct2/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ var (
resourceNamePattern = regexp.MustCompile(`^[a-zA-Z0-9_./\-:\[\]]*$`)
)

func (id *ResourceId) UnmarshalText(data []byte) error {
parts := strings.SplitN(string(data), ":", 4)
func (id *ResourceId) Parse(s string) error {
parts := strings.SplitN(s, ":", 4)
switch len(parts) {
case 4:
id.Name = parts[3]
Expand All @@ -174,6 +174,10 @@ func (id *ResourceId) UnmarshalText(data []byte) error {
return fmt.Errorf("must have trailing ':' for provider-only ID")
}
}
return nil
}

func (id *ResourceId) Validate() error {
if id.IsZero() {
return nil
}
Expand All @@ -191,11 +195,19 @@ func (id *ResourceId) UnmarshalText(data []byte) error {
err = errors.Join(err, fmt.Errorf("invalid name '%s' (must match %s)", id.Name, resourceNamePattern))
}
if err != nil {
return fmt.Errorf("invalid resource id '%s': %w", string(data), err)
return fmt.Errorf("invalid resource id '%s': %w", id.String(), err)
}
return nil
}

func (id *ResourceId) UnmarshalText(data []byte) error {
err := id.Parse(string(data))
if err != nil {
return err
}
return id.Validate()
}

func (id ResourceId) MarshalTOML() ([]byte, error) {
return id.MarshalText()
}
Expand Down
Loading
Loading