yay/pkg/topo/dep.go
Jo 28d90c981e
feat(new engine): local install feature testing (#1867)
* make config into parameter

* test(new engine): local install test

* test(keys): fix test keys

* complete integration test for local install

* add simple mising mechanism
2022-12-29 12:34:53 +00:00

400 lines
8.2 KiB
Go

package topo
import (
"fmt"
"strings"
)
type (
AliasMap[T comparable] map[T]T
NodeSet[T comparable] map[T]bool
DepMap[T comparable] map[T]NodeSet[T]
)
type NodeInfo[V any] struct {
Color string
Background string
Value V
}
type CheckFn[T comparable, V any] func(T, V) error
type Graph[T comparable, V any] struct {
alias AliasMap[T] // alias -> aliased
aliases DepMap[T] // aliased -> alias
nodes NodeSet[T]
// node info map
nodeInfo map[T]*NodeInfo[V]
// `dependencies` tracks child -> parents.
dependencies DepMap[T]
// `dependents` tracks parent -> children.
dependents DepMap[T]
// Keep track of the nodes of the graph themselves.
}
func New[T comparable, V any]() *Graph[T, V] {
return &Graph[T, V]{
nodes: make(NodeSet[T]),
dependencies: make(DepMap[T]),
dependents: make(DepMap[T]),
alias: make(AliasMap[T]),
aliases: make(DepMap[T]),
nodeInfo: make(map[T]*NodeInfo[V]),
}
}
func (g *Graph[T, V]) Len() int {
return len(g.nodes)
}
func (g *Graph[T, V]) Exists(node T) bool {
// check aliases
node = g.getAlias(node)
_, ok := g.nodes[node]
return ok
}
func (g *Graph[T, V]) Alias(node, alias T) error {
if alias == node {
return nil
}
// add node
g.nodes[node] = true
// add alias
if _, ok := g.alias[alias]; ok {
return ErrConflictingAlias
}
g.alias[alias] = node
g.aliases.addNodeToNodeset(node, alias)
return nil
}
func (g *Graph[T, V]) AddNode(node T) {
node = g.getAlias(node)
g.nodes[node] = true
}
func (g *Graph[T, V]) getAlias(node T) T {
if aliasNode, ok := g.alias[node]; ok {
return aliasNode
}
return node
}
func (g *Graph[T, V]) SetNodeInfo(node T, nodeInfo *NodeInfo[V]) {
g.nodeInfo[g.getAlias(node)] = nodeInfo
}
func (g *Graph[T, V]) GetNodeInfo(node T) *NodeInfo[V] {
return g.nodeInfo[g.getAlias(node)]
}
// Retrieve aliases of a node.
func (g *Graph[T, V]) GetAliases(node T) []T {
size := len(g.aliases[node])
aliases := make([]T, 0, size)
for alias := range g.aliases[node] {
aliases = append(aliases, alias)
}
return aliases
}
func (g *Graph[T, V]) DependOn(child, parent T) error {
child = g.getAlias(child)
parent = g.getAlias(parent)
if child == parent {
return ErrSelfReferential
}
if g.DependsOn(parent, child) {
return ErrCircular
}
g.AddNode(parent)
g.AddNode(child)
// Add nodes.
g.nodes[parent] = true
g.nodes[child] = true
// Add edges.
g.dependents.addNodeToNodeset(parent, child)
g.dependencies.addNodeToNodeset(child, parent)
return nil
}
func (g *Graph[T, V]) String() string {
var sb strings.Builder
sb.WriteString("digraph {\n")
sb.WriteString("compound=true;\n")
sb.WriteString("concentrate=true;\n")
sb.WriteString("node [shape = record, ordering=out];\n")
for node := range g.nodes {
extra := ""
if info, ok := g.nodeInfo[node]; ok {
if info.Background != "" || info.Color != "" {
extra = fmt.Sprintf("[color = %s, style = filled, fillcolor = %s]", info.Color, info.Background)
}
}
sb.WriteString(fmt.Sprintf("\t\"%v\"%s;\n", node, extra))
}
for parent, children := range g.dependencies {
for child := range children {
sb.WriteString(fmt.Sprintf("\t\"%v\" -> \"%v\";\n", parent, child))
}
}
sb.WriteString("}")
return sb.String()
}
func (g *Graph[T, V]) DependsOn(child, parent T) bool {
deps := g.Dependencies(child)
_, ok := deps[parent]
return ok
}
func (g *Graph[T, V]) HasDependent(parent, child T) bool {
deps := g.Dependents(parent)
_, ok := deps[child]
return ok
}
func (g *Graph[T, V]) Leaves() []T {
leaves := make([]T, 0)
for node := range g.nodes {
if _, ok := g.dependencies[node]; !ok {
leaves = append(leaves, node)
}
}
return leaves
}
// LeavesMap returns a map of leaves with the node as key and the node info value as value.
func (g *Graph[T, V]) LeavesMap() map[T]V {
leaves := make(map[T]V, 0)
for node := range g.nodes {
if _, ok := g.dependencies[node]; !ok {
nodeInfo := g.GetNodeInfo(node)
if nodeInfo == nil {
nodeInfo = &NodeInfo[V]{}
}
leaves[node] = nodeInfo.Value
}
}
return leaves
}
// TopoSortedLayers returns a slice of all of the graph nodes in topological sort order.
func (g *Graph[T, V]) TopoSortedLayers() [][]T {
layers := [][]T{}
// Copy the graph
shrinkingGraph := g.clone()
for {
leaves := shrinkingGraph.Leaves()
if len(leaves) == 0 {
break
}
layers = append(layers, leaves)
for _, leafNode := range leaves {
shrinkingGraph.remove(leafNode)
}
}
return layers
}
// TopoSortedLayerMap returns a slice of all of the graph nodes in topological sort order with their node info.
func (g *Graph[T, V]) TopoSortedLayerMap(checkFn CheckFn[T, V]) []map[T]V {
layers := []map[T]V{}
// Copy the graph
shrinkingGraph := g.clone()
for {
leaves := shrinkingGraph.LeavesMap()
if len(leaves) == 0 {
break
}
layers = append(layers, leaves)
for leafNode := range leaves {
if checkFn != nil {
if err := checkFn(leafNode, leaves[leafNode]); err != nil {
return nil
}
}
shrinkingGraph.remove(leafNode)
}
}
return layers
}
func (dm DepMap[T]) removeFromDepmap(key, node T) {
if nodes := dm[key]; len(nodes) == 1 {
// The only element in the nodeset must be `node`, so we
// can delete the entry entirely.
delete(dm, key)
} else {
// Otherwise, remove the single node from the nodeset.
delete(nodes, node)
}
}
func (g *Graph[T, V]) remove(node T) {
// Remove edges from things that depend on `node`.
for dependent := range g.dependents[node] {
g.dependencies.removeFromDepmap(dependent, node)
}
delete(g.dependents, node)
// Remove all edges from node to the things it depends on.
for dependency := range g.dependencies[node] {
g.dependents.removeFromDepmap(dependency, node)
}
delete(g.dependencies, node)
// Finally, remove the node itself.
delete(g.nodes, node)
}
// TopoSorted returns all the nodes in the graph is topological sort order.
func (g *Graph[T, V]) TopoSorted() []T {
nodeCount := 0
layers := g.TopoSortedLayers()
for _, layer := range layers {
nodeCount += len(layer)
}
allNodes := make([]T, 0, nodeCount)
for _, layer := range layers {
allNodes = append(allNodes, layer...)
}
return allNodes
}
func (g *Graph[T, V]) Dependencies(child T) NodeSet[T] {
return g.buildTransitive(child, g.immediateDependencies)
}
func (g *Graph[T, V]) immediateDependencies(node T) NodeSet[T] {
return g.dependencies[node]
}
func (g *Graph[T, V]) Dependents(parent T) NodeSet[T] {
return g.buildTransitive(parent, g.immediateDependents)
}
func (g *Graph[T, V]) immediateDependents(node T) NodeSet[T] {
return g.dependents[node]
}
func (g *Graph[T, V]) clone() *Graph[T, V] {
return &Graph[T, V]{
dependencies: g.dependencies.copy(),
dependents: g.dependents.copy(),
nodes: g.nodes.copy(),
nodeInfo: g.nodeInfo, // not copied, as it is not modified
}
}
// buildTransitive starts at `root` and continues calling `nextFn` to keep discovering more nodes until
// the graph cannot produce any more. It returns the set of all discovered nodes.
func (g *Graph[T, V]) buildTransitive(root T, nextFn func(T) NodeSet[T]) NodeSet[T] {
if _, ok := g.nodes[root]; !ok {
return nil
}
out := make(NodeSet[T])
searchNext := []T{root}
for len(searchNext) > 0 {
// List of new nodes from this layer of the dependency graph. This is
// assigned to `searchNext` at the end of the outer "discovery" loop.
discovered := []T{}
for _, node := range searchNext {
// For each node to discover, find the next nodes.
for nextNode := range nextFn(node) {
// If we have not seen the node before, add it to the output as well
// as the list of nodes to traverse in the next iteration.
if _, ok := out[nextNode]; !ok {
out[nextNode] = true
discovered = append(discovered, nextNode)
}
}
}
searchNext = discovered
}
return out
}
func (s NodeSet[T]) copy() NodeSet[T] {
out := make(NodeSet[T], len(s))
for k, v := range s {
out[k] = v
}
return out
}
func (dm DepMap[T]) copy() DepMap[T] {
out := make(DepMap[T], len(dm))
for k := range dm {
out[k] = dm[k].copy()
}
return out
}
func (dm DepMap[T]) addNodeToNodeset(key, node T) {
nodes, ok := dm[key]
if !ok {
nodes = make(NodeSet[T])
dm[key] = nodes
}
nodes[node] = true
}