if (!startsWith('[') || !endsWith(']')) {
throw IllegalArgumentException("Expected string starting '[' and ending with ']' but it was '$")
}
- val tokens = substring(1, length - 1).split(", ").map { it.split(graphTokenSeparators) }
+ val tokens = substring(1, length - 1).replace("\n", "").split(", ").map { it.trim().split(graphTokenSeparators) }
val nodes = tokens.flatMap { it.take(2) }.toCollection(LinkedHashSet())
val edges = tokens.filter { it.size == 3 }.map { Graph.TermForm.Term(it[0], it[1], EdgeLabel.valueOf(it[2])) }
return Graph.labeledDirectedTerms(Graph.TermForm(nodes, edges))
fun Graph.toAdjacencyList(): Graph.AdjacencyList<String, EdgeLabel> {
val entries = nodes.values.map { node ->
- val links = node.edges.map { Graph.AdjacencyList.Link(it.target(node).id, it.label) }
+ val links = node.edges.map { Graph.AdjacencyList.Link(it.target.id, it.label) }
Graph.AdjacencyList.Entry(node = node.id, links = links)
}
return Graph.AdjacencyList(entries)
.flatMap { findAllPaths(it.id, to, path + from) }
}
-fun Graph.findCycles(node: String): List<List<String>> {
- fun findCycles(path: List<String>): List<List<String>> {
- if (path.size > 3 && path.first() == path.last()) return listOf(path)
- return nodes[path.last()]!!.neighbors()
- .filterNot { path.tail().contains(it.id) }
- .flatMap { findCycles(path + it.id) }
+fun Graph.isAcyclic(): Boolean {
+ val startNodes = startNodes()
+ if (startNodes.isEmpty())
+ return false
+
+ val adj: Map<String, Set<String>> = toAdjacencyList().entries
+ .associate { it.node to it.links }
+ .mapValues { it.value.map { x -> x.node }.toSet() }
+
+ fun hasCycle(node: String, visited: MutableSet<String> = mutableSetOf()): Boolean {
+ if (visited.contains(node))
+ return true
+ visited.add(node)
+
+ if (adj[node]!!.isEmpty()) {
+ visited.remove(node)
+ return false
+ }
+
+ if (adj[node]!!.any { hasCycle(it, visited) })
+ return true
+
+ visited.remove(node)
+ return false
}
- return findCycles(listOf(node))
+
+ return startNodes.none { n -> hasCycle(n.id) }
}
fun Graph.startNodes() = this.nodes.values.filter {
val edges: MutableList<Edge> = ArrayList()
- fun neighbors(): List<Node> = edges.map { edge -> edge.target(this) }
+ fun neighbors(): List<Node> = edges.map { it.target }
fun neighbors(label: EdgeLabel): List<Node> = edges.filter { it.label == label }
- .map { edge -> edge.target(this) }
+ .map { it.target }
fun labelEdges(label: EdgeLabel): List<Edge> = edges.filter { it.label == label }
var status: EdgeStatus = EdgeStatus.NOT_STARTED
) {
- fun target(node: Node): Node = target
-
fun equivalentTo(other: Edge) =
(source == other.source && target == other.target) ||
(source == other.target && target == other.source)
import org.junit.Test
import org.onap.ccsdk.cds.controllerblueprints.core.data.EdgeLabel
+import kotlin.test.assertFalse
import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
class GraphExtensionFunctionsTest {
val nodePath = graph.nodes["p"]!!.neighbors(EdgeLabel.SUCCESS)
assertNotNull(nodePath, "failed to nodePath from graph for 'p' node 'SUCCESS' label")
}
+
+ @Test
+ fun `isAcyclic should return false`() {
+ assertFalse(
+ """[
+ assign>deploy/SUCCESS,
+ deploy>assign/FAILURE
+ ]""".toGraph().isAcyclic()
+ )
+
+ assertFalse(
+ """[
+ assign>deploy/SUCCESS,
+ deploy>recover/FAILURE,
+ recover>deploy/SUCCESS
+ ]""".toGraph().isAcyclic()
+ )
+
+ assertFalse(
+ """[
+ assign>deploy/SUCCESS,
+ assign>recover/FAILURE,
+ recover>deploy/SUCCESS,
+ deploy>finalize/SUCCESS,
+ deploy>recover/FAILURE
+ ]""".toGraph().isAcyclic()
+ )
+
+ assertFalse(
+ """[
+ A>B/SUCCESS,
+ A>C/SUCCESS,
+ B>E/SUCCESS,
+ B>D/FAILURE,
+ D>B/FAILURE,
+ C>E/SUCCESS
+ ]""".toGraph().isAcyclic()
+ )
+ }
+
+ @Test
+ fun `isAcyclic should return true`() {
+ assertTrue(
+ """[
+ assign>deploy/SUCCESS,
+ deploy>recover/FAILURE
+ ]""".toGraph().isAcyclic()
+ )
+
+ assertTrue(
+ """[
+ A>C/SUCCESS,
+ A>B/FAILURE,
+ C>B/SUCCESS
+ ]""".toGraph().isAcyclic()
+ )
+
+ assertTrue(
+ """[
+ assign>execute1/SUCCESS,
+ assign>execute2/SUCCESS,
+ execute1>finalize/SUCCESS,
+ execute2>finalize/SUCCESS,
+ execute1>cleanup/FAILURE,
+ execute2>cleanup/FAILURE,
+ finalize>cleanup/SUCCESS
+ ]""".toGraph().isAcyclic()
+ )
+ }
}
import org.onap.ccsdk.cds.blueprintsprocessor.core.api.data.Status
import org.onap.ccsdk.cds.controllerblueprints.common.api.EventType
import org.onap.ccsdk.cds.controllerblueprints.core.BlueprintConstants
+import org.onap.ccsdk.cds.controllerblueprints.core.BlueprintException
import org.onap.ccsdk.cds.controllerblueprints.core.BlueprintProcessorException
import org.onap.ccsdk.cds.controllerblueprints.core.MDCContext
import org.onap.ccsdk.cds.controllerblueprints.core.asGraph
import org.onap.ccsdk.cds.controllerblueprints.core.data.EdgeLabel
import org.onap.ccsdk.cds.controllerblueprints.core.data.Graph
import org.onap.ccsdk.cds.controllerblueprints.core.interfaces.BlueprintWorkflowExecutionService
+import org.onap.ccsdk.cds.controllerblueprints.core.isAcyclic
import org.onap.ccsdk.cds.controllerblueprints.core.logger
import org.onap.ccsdk.cds.controllerblueprints.core.service.AbstractBlueprintWorkFlowService
import org.onap.ccsdk.cds.controllerblueprints.core.service.BlueprintRuntimeService
val graph = bluePrintContext.workflowByName(workflowName).asGraph()
+ if (!graph.isAcyclic()) {
+ throw BlueprintException("Imperative workflow must be acyclic. Check on_success/on_failure for circular references")
+ }
+
return coroutineScope {
ImperativeBlueprintWorkflowService(
nodeTemplateExecutionService,