Skip to content

Commit

Permalink
Return defined error instances from Graph methods (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikbraun authored Nov 25, 2022
1 parent f1f0bea commit 994d3e8
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 71 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.15.0] - 2022-11-25

### Added
* Added the `ErrVertexAlreadyExists` error instance. Use `errors.Is` to check for this instance.
* Added the `ErrEdgeAlreadyExists` error instance. Use `errors.Is` to check for this instance.
* Added the `ErrEdgeCreatesCycle` error instance. Use `errors.Is` to check for this instance.

### Changed
* Changed `AddVertex` to return `ErrVertexAlreadyExists` if the vertex already exists.
* Changed `VertexWithProperties` to return `ErrVertexNotFound` if the vertex doesn't exist.
* Changed `AddEdge` to return `ErrVertexNotFound` if either vertex doesn't exist.
* Changed `AddEdge` to return `ErrEdgeAlreadyExists` if the edge already exists.
* Changed `AddEdge` to return `ErrEdgeCreatesCycle` if cycle prevention is active and the edge would create a cycle.
* Changed `Edge` to return `ErrEdgeNotFound` if the edge doesn't exist.
* Changed `RemoveEdge` to return the error instances returned by `Edge`.

## [0.14.0] - 2022-11-01

### Added
Expand Down
19 changes: 12 additions & 7 deletions directed.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ func (d *directed[K, T]) Traits() *Traits {

func (d *directed[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error {
hash := d.hash(value)

if _, ok := d.vertices[hash]; ok {
return ErrVertexAlreadyExists
}

d.vertices[hash] = value
d.vertexProperties[hash] = &VertexProperties{
Weight: 0,
Expand Down Expand Up @@ -63,7 +68,7 @@ func (d *directed[K, T]) VertexWithProperties(hash K) (T, VertexProperties, erro

properties, ok := d.vertexProperties[hash]
if !ok {
return vertex, *properties, fmt.Errorf("vertex with hash %v doesn't exist", hash)
return vertex, *properties, ErrVertexNotFound
}

return vertex, *properties, nil
Expand All @@ -72,26 +77,26 @@ func (d *directed[K, T]) VertexWithProperties(hash K) (T, VertexProperties, erro
func (d *directed[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error {
source, ok := d.vertices[sourceHash]
if !ok {
return fmt.Errorf("could not find source vertex with hash %v", sourceHash)
return fmt.Errorf("source vertex %v: %w", sourceHash, ErrVertexNotFound)
}

target, ok := d.vertices[targetHash]
if !ok {
return fmt.Errorf("could not find target vertex with hash %v", targetHash)
return fmt.Errorf("target vertex %v: %w", targetHash, ErrVertexNotFound)
}

if _, err := d.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) {
return fmt.Errorf("an edge between vertices %v and %v already exists", sourceHash, targetHash)
return ErrEdgeAlreadyExists
}

// If the user opted in to preventing cycles, run a cycle check.
if d.traits.PreventCycles {
createsCycle, err := CreatesCycle[K, T](d, sourceHash, targetHash)
if err != nil {
return fmt.Errorf("failed to check for cycles: %w", err)
return fmt.Errorf("check for cycles: %w", err)
}
if createsCycle {
return fmt.Errorf("an edge between %v and %v would introduce a cycle", sourceHash, targetHash)
return ErrEdgeCreatesCycle
}
}

Expand Down Expand Up @@ -128,7 +133,7 @@ func (d *directed[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) {

func (d *directed[K, T]) RemoveEdge(source, target K) error {
if _, err := d.Edge(source, target); err != nil {
return fmt.Errorf("failed to find edge from %v to %v: %w", source, target, err)
return err
}

delete(d.edges[source], target)
Expand Down
108 changes: 91 additions & 17 deletions directed_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package graph

import (
"errors"
"testing"
)

Expand Down Expand Up @@ -35,6 +36,9 @@ func TestDirected_AddVertex(t *testing.T) {
properties *VertexProperties
expectedVertices []int
expectedProperties *VertexProperties
// Even though some AddVertex calls might work, at least one of them could fail, for example
// if the last call would add an existing vertex.
finallyExpectedError error
}{
"graph with 3 vertices": {
vertices: []int{1, 2, 3},
Expand All @@ -49,26 +53,33 @@ func TestDirected_AddVertex(t *testing.T) {
},
},
"graph with duplicated vertex": {
vertices: []int{1, 2, 2},
expectedVertices: []int{1, 2},
vertices: []int{1, 2, 2},
expectedVertices: []int{1, 2},
finallyExpectedError: ErrVertexAlreadyExists,
},
}

for name, test := range tests {
graph := newDirected(IntHash, &Traits{})

var err error

for _, vertex := range test.vertices {
if test.properties == nil {
_ = graph.AddVertex(vertex)
err = graph.AddVertex(vertex)
continue
}
// If there are vertex attributes, iterate over them and call VertexAttribute for each
// entry. A vertex should only have one attribute so that AddVertex is invoked once.
for key, value := range test.properties.Attributes {
_ = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value))
err = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value))
}
}

if !errors.Is(err, test.finallyExpectedError) {
t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err)
}

for _, vertex := range test.vertices {
if len(graph.vertices) != len(test.expectedVertices) {
t.Errorf("%s: vertex count doesn't match: expected %v, got %v", name, len(test.expectedVertices), len(graph.vertices))
Expand Down Expand Up @@ -104,15 +115,55 @@ func TestDirected_AddVertex(t *testing.T) {
}
}

func TestDirected_Vertex(t *testing.T) {
tests := map[string]struct {
vertices []int
vertex int
expectedError error
}{
"existing vertex": {
vertices: []int{1, 2, 3},
vertex: 2,
},
"non-existent vertex": {
vertices: []int{1, 2, 3},
vertex: 4,
expectedError: ErrVertexNotFound,
},
}

for name, test := range tests {
graph := newDirected(IntHash, &Traits{})

for _, vertex := range test.vertices {
_ = graph.AddVertex(vertex)
}

vertex, err := graph.Vertex(test.vertex)

if !errors.Is(err, test.expectedError) {
t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err)
}

if test.expectedError != nil {
continue
}

if vertex != test.vertex {
t.Errorf("%s: vertex expectancy doesn't match: expected %v, got %v", name, test.vertex, vertex)
}
}
}

func TestDirected_AddEdge(t *testing.T) {
tests := map[string]struct {
vertices []int
edges []Edge[int]
traits *Traits
expectedEdges []Edge[int]
// Even though some of the AddEdge calls might work, at least one of them could fail,
// for example if the last call would introduce a cycle.
shouldFinallyFail bool
// Even though some AddEdge calls might work, at least one of them could fail, for example
// if the last call would introduce a cycle.
finallyExpectedError error
}{
"graph with 2 edges": {
vertices: []int{1, 2, 3},
Expand All @@ -131,8 +182,8 @@ func TestDirected_AddEdge(t *testing.T) {
edges: []Edge[int]{
{Source: 1, Target: 3, Properties: EdgeProperties{Weight: 20}},
},
traits: &Traits{},
shouldFinallyFail: true,
traits: &Traits{},
finallyExpectedError: ErrVertexNotFound,
},
"edge introducing a cycle in an acyclic graph": {
vertices: []int{1, 2, 3},
Expand All @@ -144,7 +195,18 @@ func TestDirected_AddEdge(t *testing.T) {
traits: &Traits{
PreventCycles: true,
},
shouldFinallyFail: true,
finallyExpectedError: ErrEdgeCreatesCycle,
},
"edge already exists": {
vertices: []int{1, 2, 3},
edges: []Edge[int]{
{Source: 1, Target: 2},
{Source: 2, Target: 3},
{Source: 3, Target: 1},
{Source: 3, Target: 1},
},
traits: &Traits{},
finallyExpectedError: ErrEdgeAlreadyExists,
},
"edge with attributes": {
vertices: []int{1, 2},
Expand Down Expand Up @@ -197,8 +259,8 @@ func TestDirected_AddEdge(t *testing.T) {
}
}

if test.shouldFinallyFail != (err != nil) {
t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFinallyFail, (err != nil), err)
if !errors.Is(err, test.finallyExpectedError) {
t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v", name, test.finallyExpectedError, err)
}

for _, expectedEdge := range test.expectedEdges {
Expand Down Expand Up @@ -258,6 +320,7 @@ func TestDirected_Edge(t *testing.T) {

for name, test := range tests {
graph := newDirected(IntHash, &Traits{})

for _, vertex := range test.vertices {
_ = graph.AddVertex(vertex)
}
Expand All @@ -277,9 +340,10 @@ func TestDirected_Edge(t *testing.T) {

func TestDirected_RemoveEdge(t *testing.T) {
tests := map[string]struct {
vertices []int
edges []Edge[int]
removeEdges []Edge[int]
vertices []int
edges []Edge[int]
removeEdges []Edge[int]
expectedError error
}{
"two-vertices graph": {
vertices: []int{1, 2},
Expand All @@ -302,6 +366,16 @@ func TestDirected_RemoveEdge(t *testing.T) {
{Source: 2, Target: 3},
},
},
"remove non-existent edge": {
vertices: []int{1, 2, 3},
edges: []Edge[int]{
{Source: 1, Target: 2},
},
removeEdges: []Edge[int]{
{Source: 2, Target: 3},
},
expectedError: ErrEdgeNotFound,
},
}

for name, test := range tests {
Expand All @@ -318,8 +392,8 @@ func TestDirected_RemoveEdge(t *testing.T) {
}

for _, removeEdge := range test.removeEdges {
if err := graph.RemoveEdge(removeEdge.Source, removeEdge.Target); err != nil {
t.Fatalf("%s: failed to remove edge: %s", name, err.Error())
if err := graph.RemoveEdge(removeEdge.Source, removeEdge.Target); !errors.Is(err, test.expectedError) {
t.Errorf("%s: error expectancy doesn't match: expected %v, got %v", name, test.expectedError, err)
}
// After removing the edge, verify that it can't be retrieved using Edge anymore.
if _, err := graph.Edge(removeEdge.Source, removeEdge.Target); err != ErrEdgeNotFound {
Expand Down
29 changes: 15 additions & 14 deletions graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ package graph
import "errors"

var (
// ErrVertexNotFound will be returned when a desired vertex cannot be found.
ErrVertexNotFound = errors.New("vertex not found")
// ErrEdgeNotFound will be returned when a desired edge cannot be found.
ErrEdgeNotFound = errors.New("edge not found")
ErrVertexNotFound = errors.New("vertex not found")
ErrVertexAlreadyExists = errors.New("vertex already exists")
ErrEdgeNotFound = errors.New("edge not found")
ErrEdgeAlreadyExists = errors.New("edge already exists")
ErrEdgeCreatesCycle = errors.New("edge would create a cycle")
)

// Graph represents a generic graph data structure consisting of vertices and edges. Its vertices
Expand All @@ -17,11 +18,8 @@ type Graph[K comparable, T any] interface {
// Traits returns the graph's traits. Those traits must be set when creating a graph using New.
Traits() *Traits

// AddVertex creates a new vertex in the graph, which won't be connected to another vertex yet.
//
// Whether AddVertex is idempotent depends on the underlying vertex store implementation. By
// default, when using the in-memory store, an existing vertex will be overwritten, whereas
// other stores might return an error.
// AddVertex creates a new vertex in the graph. If the vertex already exists in the graph,
// ErrVertexAlreadyExists will be returned.
//
// AddVertex accepts a variety of functional options to set further edge details such as the
// weight or an attribute:
Expand All @@ -33,13 +31,15 @@ type Graph[K comparable, T any] interface {
// Vertex returns the vertex with the given hash or ErrVertexNotFound if it doesn't exist.
Vertex(hash K) (T, error)

// VertexWithProperties returns the vertex with the given hash along with its properties, or an
// error if the vertex doesn't exist.
// VertexWithProperties returns the vertex with the given hash along with its properties or
// ErrVertexNotFound if it doesn't exist.
VertexWithProperties(hash K) (T, VertexProperties, error)

// AddEdge creates an edge between the source and the target vertex. If the Directed option has
// been called on the graph, this is a directed edge. Returns an error if either vertex doesn't
// exist or the edge already exists.
// been called on the graph, this is a directed edge. If either vertex can't be found,
// ErrVertexNotFound will be returned. If the edge already exists, ErrEdgeAlreadyExists will be
// returned. If cycle prevention has been activated using PreventCycles and adding the edge
// would create a cycle, ErrEdgeCreatesCycle will be returned.
//
// AddEdge accepts a variety of functional options to set further edge details such as the
// weight or an attribute:
Expand All @@ -49,7 +49,8 @@ type Graph[K comparable, T any] interface {
AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error

// Edge returns the edge joining two given vertices or an error if the edge doesn't exist. In an
// undirected graph, an edge with swapped source and target vertices does match.
// undirected graph, an edge with swapped source and target vertices does match. If the edge
// doesn't exist, ErrEdgeNotFound will be returned.
Edge(sourceHash, targetHash K) (Edge[T], error)

// RemoveEdge removes the edge between the given source and target vertices. If the edge doesn't
Expand Down
Loading

0 comments on commit 994d3e8

Please sign in to comment.