Skip to content

Commit

Permalink
Init dag package (okteto#4220)
Browse files Browse the repository at this point in the history
* Init dag package

* Add copyright headers

* linting

* more linting
  • Loading branch information
maroshii authored Apr 5, 2024
1 parent 27e1d9c commit 814cb83
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ require (
github.com/google/s2a-go v0.1.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/heimdalr/dag v1.4.0 // indirect
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
Expand Down Expand Up @@ -599,6 +600,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/heimdalr/dag v1.4.0 h1:zG3JA4RDVLc55k3AXAgfwa+EgBNZ0TkfOO3C29Ucpmg=
github.com/heimdalr/dag v1.4.0/go.mod h1:OCh6ghKmU0hPjtwMqWBoNxPmtRioKd1xSu7Zs4sbIqM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down
57 changes: 57 additions & 0 deletions pkg/dag/dag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2024 The Okteto Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dag

import (
"github.com/heimdalr/dag"
)

type Node interface {
ID() string
DependsOn() []string
}

type callback func(Node)

func (cb callback) Visit(vx dag.Vertexer) {
_, value := vx.Vertex()
cb(value.(Node))
}

type Tree struct {
graph *dag.DAG
}

func (tree *Tree) Traverse(fn func(n Node)) {
tree.graph.OrderedWalk(callback(fn))
}

func From(nodes ...Node) (*Tree, error) {
tree := &Tree{graph: dag.NewDAG()}

for _, n := range nodes {
if _, err := tree.graph.AddVertex(n); err != nil {
return nil, err
}
}

for _, n := range nodes {
for _, dep := range n.DependsOn() {
if err := tree.graph.AddEdge(dep, n.ID()); err != nil {
return nil, err
}
}
}
return tree, nil
}
154 changes: 154 additions & 0 deletions pkg/dag/dag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2024 The Okteto Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dag

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type testNode struct {
id string
dependsOn []string
}

func (n *testNode) ID() string { return n.id }
func (n *testNode) DependsOn() []string { return n.dependsOn }

func TestTraverseError(t *testing.T) {
nodes := []Node{
&testNode{id: "v1", dependsOn: []string{"v2"}},
&testNode{id: "v2", dependsOn: []string{"v3"}},
&testNode{id: "v3", dependsOn: []string{"v1"}},
}
_, err := From(nodes...)
require.EqualError(t, err, "edge between 'v1' and 'v3' would create a loop")

nodes = []Node{
&testNode{id: "v1", dependsOn: []string{"v2"}},
&testNode{id: "v2"},
&testNode{id: "v3"},
&testNode{id: "v1", dependsOn: []string{"v3"}},
}
_, err = From(nodes...)
require.EqualError(t, err, "the id 'v1' is already known")

nodes = []Node{
&testNode{id: "v1", dependsOn: []string{"v2"}},
&testNode{id: "v2"},
&testNode{id: "v3", dependsOn: []string{"v3"}},
}
_, err = From(nodes...)
require.EqualError(t, err, "src ('v3') and dst ('v3') equal")

}
func TestTraverse(t *testing.T) {

var tt = []struct {
name string
nodes []Node
expected []string
}{
{
// v1
// ^
// |
// v2
// ^
// |
// v3
// ^
// |
// v4
// ^
// |
// v5
name: "linear",
nodes: []Node{
&testNode{id: "v1"},
&testNode{id: "v2", dependsOn: []string{"v1"}},
&testNode{id: "v3", dependsOn: []string{"v2"}},
&testNode{id: "v4", dependsOn: []string{"v3"}},
&testNode{id: "v5", dependsOn: []string{"v4"}},
},
expected: []string{"v1", "v2", "v3", "v4", "v5"},
},
{
// v5 --> v4
//
//
// v2 --> v1
// ^
// |
// v3
name: "sparse-two-roots",
nodes: []Node{
&testNode{id: "v1"},
&testNode{id: "v2", dependsOn: []string{"v1"}},
&testNode{id: "v3", dependsOn: []string{"v1"}},
&testNode{id: "v4"},
&testNode{id: "v5", dependsOn: []string{"v4"}},
},
expected: []string{"v1", "v4", "v2", "v3", "v5"},
},
{
// v1 v2
// ^ ^
// | |
// v4 --> v3
// ^
// |
// v5
name: "fork",
nodes: []Node{
&testNode{id: "v1"},
&testNode{id: "v2"},
&testNode{id: "v3", dependsOn: []string{"v2"}},
&testNode{id: "v4", dependsOn: []string{"v1"}},
&testNode{id: "v5", dependsOn: []string{"v4"}},
},
expected: []string{"v1", "v2", "v4", "v3", "v5"},
},
{
// v1 v3 <---┐
// ^ |
// | |
// v2 <-- v5 --> v4
name: "edgy",
nodes: []Node{
&testNode{id: "v1"},
&testNode{id: "v2"},
&testNode{id: "v3"},
&testNode{id: "v4", dependsOn: []string{"v3"}},
&testNode{id: "v5", dependsOn: []string{"v4", "v3", "v2"}},
},
expected: []string{"v1", "v2", "v3", "v4", "v5"},
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
tree, err := From(tc.nodes...)
require.NoError(t, err)

var result []string
tree.Traverse(func(n Node) {
result = append(result, n.ID())
})
assert.Equal(t, tc.expected, result)
})
}
}
55 changes: 55 additions & 0 deletions pkg/dag/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2024 The Okteto Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// package dag provides generic interface for working with dependencies resolution
// via Directed Acyclic Graphs (DAGs)
//
// It is required for clients of this package to implement the Node interface
// which consists of two methods: ID() and DependsOn():
// - ID: It's the ID of the node and how it will be identified in the tree
// - DependsOn: It's the dependencies of this node and should come BEFORE this node
//
// IDs should be unique and there should not be any cycles. An example of a cycle
// is: A->B, B->C, C->A
//
// Usage:
// ```go
//
// // testNode is an custom implementation for resolving dependencies
// type testNode struct {
// id string
// dependsOn []string
// }
//
// func (n *testNode) ID() string { return n.id }
// func (n *testNode) DependsOn() []string { return n.dependsOn }
//
// nodes := []dag.Node{
// &testNode{id: "v1"},
// &testNode{id: "v2", dependsOn: []string{"v1"}},
// &testNode{id: "v3", dependsOn: []string{"v2"}},
// &testNode{id: "v4", dependsOn: []string{"v3"}},
// &testNode{id: "v5", dependsOn: []string{"v4"}},
// }
// result := []string{}
// tree, _ := dag.From(nodes...)
// tree.Traverse(func(n dag.Node) {
// result = append(result, n.ID())
// })
// fmt.Println(strings.Join(result, ",")) // v1,v2,v3,v4,v5
//
// ```
//
// Traverse() takes care of walking the DAG and calling the callback in order
// based on the DependsOn()
package dag

0 comments on commit 814cb83

Please sign in to comment.