aboutsummaryrefslogtreecommitdiffstats
path: root/framework/src/onos/utils/stc
diff options
context:
space:
mode:
authorAshlee Young <ashlee@onosfw.com>2015-09-09 22:15:21 -0700
committerAshlee Young <ashlee@onosfw.com>2015-09-09 22:15:21 -0700
commit13d05bc8458758ee39cb829098241e89616717ee (patch)
tree22a4d1ce65f15952f07a3df5af4b462b4697cb3a /framework/src/onos/utils/stc
parent6139282e1e93c2322076de4b91b1c85d0bc4a8b3 (diff)
ONOS checkin based on commit tag e796610b1f721d02f9b0e213cf6f7790c10ecd60
Change-Id: Ife8810491034fe7becdba75dda20de4267bd15cd
Diffstat (limited to 'framework/src/onos/utils/stc')
-rwxr-xr-xframework/src/onos/utils/stc/bin/stc12
-rwxr-xr-xframework/src/onos/utils/stc/bin/stc-launcher19
-rw-r--r--framework/src/onos/utils/stc/pom.xml122
-rw-r--r--framework/src/onos/utils/stc/sample/scenario.xml20
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Compiler.java482
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Coordinator.java364
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Dependency.java77
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Group.java60
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Main.java353
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Monitor.java154
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java31
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java307
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java149
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java137
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java37
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Scenario.java106
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java201
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Step.java129
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepEvent.java116
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java50
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java141
-rw-r--r--framework/src/onos/utils/stc/src/main/java/org/onlab/stc/package-info.java20
-rw-r--r--framework/src/onos/utils/stc/src/main/resources/data.json1087
-rw-r--r--framework/src/onos/utils/stc/src/main/resources/index.html29
-rw-r--r--framework/src/onos/utils/stc/src/main/resources/stc.css37
-rw-r--r--framework/src/onos/utils/stc/src/main/resources/stc.js148
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java86
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java83
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java68
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/GroupTest.java54
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java146
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java44
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java84
-rw-r--r--framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepTest.java62
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml27
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml25
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml41
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml30
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml20
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/scenario.xml47
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml26
-rw-r--r--framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml21
42 files changed, 5252 insertions, 0 deletions
diff --git a/framework/src/onos/utils/stc/bin/stc b/framework/src/onos/utils/stc/bin/stc
new file mode 100755
index 00000000..23affc4a
--- /dev/null
+++ b/framework/src/onos/utils/stc/bin/stc
@@ -0,0 +1,12 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# System Test Coordinator
+#-------------------------------------------------------------------------------
+
+STC_ROOT=${STC_ROOT:-$(dirname $0)/..}
+cd $STC_ROOT
+VER=1.3.0-SNAPSHOT
+
+PATH=$PWD/bin:$PATH
+
+java -jar target/onlab-stc-$VER.jar "$@"
diff --git a/framework/src/onos/utils/stc/bin/stc-launcher b/framework/src/onos/utils/stc/bin/stc-launcher
new file mode 100755
index 00000000..0d56017e
--- /dev/null
+++ b/framework/src/onos/utils/stc/bin/stc-launcher
@@ -0,0 +1,19 @@
+#!/bin/bash
+#-------------------------------------------------------------------------------
+# System Test Coordinator process launcher
+#-------------------------------------------------------------------------------
+
+env=$1 && shift
+cwd=$1 && shift
+
+if [ $env != "-" ]; then
+ [ ! -f $env ] && echo "$env file not found" && exit 1
+ source $env
+fi
+
+if [ $cwd != "-" ]; then
+ [ ! -d $cwd ] && echo "$cwd directory not found" && exit 1
+ cd $cwd
+fi
+
+"$@" 2>&1
diff --git a/framework/src/onos/utils/stc/pom.xml b/framework/src/onos/utils/stc/pom.xml
new file mode 100644
index 00000000..a3f96430
--- /dev/null
+++ b/framework/src/onos/utils/stc/pom.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.onosproject</groupId>
+ <artifactId>onlab-utils</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>onlab-stc</artifactId>
+ <packaging>jar</packaging>
+
+ <description>System Test Coordinator</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.onosproject</groupId>
+ <artifactId>onlab-misc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.onosproject</groupId>
+ <artifactId>onlab-junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-configuration</groupId>
+ <artifactId>commons-configuration</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-collections</groupId>
+ <artifactId>commons-collections</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>2.4.2</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <version>2.4.2</version>
+ <scope>compile</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>8.1.17.v20150415</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>8.1.17.v20150415</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-websocket</artifactId>
+ <version>8.1.17.v20150415</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <transformers>
+ <transformer
+ implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>org.onlab.stc.Main
+ </mainClass>
+ </transformer>
+ </transformers>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/framework/src/onos/utils/stc/sample/scenario.xml b/framework/src/onos/utils/stc/sample/scenario.xml
new file mode 100644
index 00000000..8cee6319
--- /dev/null
+++ b/framework/src/onos/utils/stc/sample/scenario.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="sample" description="Sample Test Scenario">
+ <step name="alpha" exec="/bin/ls -l"/>
+ <step name="beta" exec="/bin/ls -lF"/>
+ <step name="gamma" exec="/bin/ls" requires="alpha,beta"/>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Compiler.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Compiler.java
new file mode 100644
index 00000000..c2a0c812
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Compiler.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.onlab.graph.DepthFirstSearch;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.*;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onlab.graph.DepthFirstSearch.EdgeType.BACK_EDGE;
+import static org.onlab.graph.GraphPathSearch.ALL_PATHS;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Entity responsible for loading a scenario and producing a redy-to-execute
+ * process flow graph.
+ */
+public class Compiler {
+
+ private static final String DEFAULT_LOG_DIR = "${env.WORKSPACE}/tmp/stc/";
+
+ private static final String IMPORT = "import";
+ private static final String GROUP = "group";
+ private static final String STEP = "step";
+ private static final String PARALLEL = "parallel";
+ private static final String DEPENDENCY = "dependency";
+
+ private static final String LOG_DIR = "[@logDir]";
+ private static final String NAME = "[@name]";
+ private static final String COMMAND = "[@exec]";
+ private static final String ENV = "[@env]";
+ private static final String CWD = "[@cwd]";
+ private static final String REQUIRES = "[@requires]";
+ private static final String IF = "[@if]";
+ private static final String UNLESS = "[@unless]";
+ private static final String VAR = "[@var]";
+ private static final String FILE = "[@file]";
+ private static final String NAMESPACE = "[@namespace]";
+
+ static final String PROP_START = "${";
+ static final String PROP_END = "}";
+ private static final String HASH = "#";
+
+ private final Scenario scenario;
+
+ private final Map<String, Step> steps = Maps.newHashMap();
+ private final Map<String, Step> inactiveSteps = Maps.newHashMap();
+ private final Map<String, String> requirements = Maps.newHashMap();
+ private final Set<Dependency> dependencies = Sets.newHashSet();
+ private final List<Integer> parallels = Lists.newArrayList();
+
+ private ProcessFlow processFlow;
+ private File logDir;
+
+ private String previous = null;
+ private String pfx = "";
+ private boolean debugOn = System.getenv("debug") != null;
+
+ /**
+ * Creates a new compiler for the specified scenario.
+ *
+ * @param scenario scenario to be compiled
+ */
+ public Compiler(Scenario scenario) {
+ this.scenario = scenario;
+ }
+
+ /**
+ * Returns the scenario being compiled.
+ *
+ * @return test scenario
+ */
+ public Scenario scenario() {
+ return scenario;
+ }
+
+ /**
+ * Compiles the specified scenario to produce a final process flow graph.
+ */
+ public void compile() {
+ compile(scenario.definition(), null, null);
+ compileRequirements();
+
+ // Produce the process flow
+ processFlow = new ProcessFlow(ImmutableSet.copyOf(steps.values()),
+ ImmutableSet.copyOf(dependencies));
+
+ scanForCycles();
+
+ // Extract the log directory if there was one specified
+ String defaultPath = DEFAULT_LOG_DIR + scenario.name();
+ String path = scenario.definition().getString(LOG_DIR, defaultPath);
+ logDir = new File(expand(path));
+ }
+
+ /**
+ * Returns the step with the specified name.
+ *
+ * @param name step or group name
+ * @return test step or group
+ */
+ public Step getStep(String name) {
+ return steps.get(name);
+ }
+
+ /**
+ * Returns the process flow generated from this scenario definition.
+ *
+ * @return process flow as a graph
+ */
+ public ProcessFlow processFlow() {
+ return processFlow;
+ }
+
+ /**
+ * Returns the log directory where scenario logs should be kept.
+ *
+ * @return scenario logs directory
+ */
+ public File logDir() {
+ return logDir;
+ }
+
+ /**
+ * Recursively elaborates this definition to produce a final process flow graph.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ */
+ private void compile(HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ String opfx = pfx;
+ pfx = pfx + ">";
+ print("pfx=%s namespace=%s", pfx, namespace);
+
+ // Scan all imports
+ cfg.configurationsAt(IMPORT)
+ .forEach(c -> processImport(c, namespace, parentGroup));
+
+ // Scan all steps
+ cfg.configurationsAt(STEP)
+ .forEach(c -> processStep(c, namespace, parentGroup));
+
+ // Scan all groups
+ cfg.configurationsAt(GROUP)
+ .forEach(c -> processGroup(c, namespace, parentGroup));
+
+ // Scan all parallel groups
+ cfg.configurationsAt(PARALLEL)
+ .forEach(c -> processParallelGroup(c, namespace, parentGroup));
+
+ // Scan all dependencies
+ cfg.configurationsAt(DEPENDENCY)
+ .forEach(c -> processDependency(c, namespace));
+
+ pfx = opfx;
+ }
+
+ /**
+ * Compiles requirements for all steps and groups accrued during the
+ * overall compilation process.
+ */
+ private void compileRequirements() {
+ requirements.forEach((name, requires) ->
+ compileRequirements(getStep(name), requires));
+ }
+
+ private void compileRequirements(Step src, String requires) {
+ split(requires).forEach(n -> {
+ boolean isSoft = n.startsWith("~");
+ String name = n.replaceFirst("^~", "");
+ Step dst = getStep(name);
+ if (dst != null) {
+ dependencies.add(new Dependency(src, dst, isSoft));
+ }
+ });
+ }
+
+ /**
+ * Processes an import directive.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ */
+ private void processImport(HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ String file = checkNotNull(expand(cfg.getString(FILE)),
+ "Import directive must specify 'file'");
+ String newNamespace = expand(prefix(cfg.getString(NAMESPACE), namespace));
+ print("import file=%s namespace=%s", file, newNamespace);
+ try {
+ Scenario importScenario = loadScenario(new FileInputStream(file));
+ compile(importScenario.definition(), newNamespace, parentGroup);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to import scenario", e);
+ }
+ }
+
+ /**
+ * Processes a step directive.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ */
+ private void processStep(HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ String name = expand(prefix(cfg.getString(NAME), namespace));
+ String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
+ String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
+ String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
+
+ print("step name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
+ Step step = new Step(name, command, env, cwd, parentGroup);
+ registerStep(step, cfg, namespace, parentGroup);
+ }
+
+ /**
+ * Processes a group directive.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ */
+ private void processGroup(HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ String name = expand(prefix(cfg.getString(NAME), namespace));
+ String command = expand(cfg.getString(COMMAND, parentGroup != null ? parentGroup.command() : null), true);
+ String env = expand(cfg.getString(ENV, parentGroup != null ? parentGroup.env() : null));
+ String cwd = expand(cfg.getString(CWD, parentGroup != null ? parentGroup.cwd() : null));
+
+ print("group name=%s command=%s env=%s cwd=%s", name, command, env, cwd);
+ Group group = new Group(name, command, env, cwd, parentGroup);
+ if (registerStep(group, cfg, namespace, parentGroup)) {
+ compile(cfg, namespace, group);
+ }
+ }
+
+ /**
+ * Registers the specified step or group.
+ *
+ * @param step step or group
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ * @return true of the step or group was registered as active
+ */
+ private boolean registerStep(Step step, HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ checkState(!steps.containsKey(step.name()), "Step %s already exists", step.name());
+ String ifClause = expand(cfg.getString(IF));
+ String unlessClause = expand(cfg.getString(UNLESS));
+
+ if ((ifClause != null && ifClause.length() == 0) ||
+ (unlessClause != null && unlessClause.length() > 0) ||
+ (parentGroup != null && inactiveSteps.containsValue(parentGroup))) {
+ inactiveSteps.put(step.name(), step);
+ return false;
+ }
+
+ if (parentGroup != null) {
+ parentGroup.addChild(step);
+ }
+
+ steps.put(step.name(), step);
+ processRequirements(step, expand(cfg.getString(REQUIRES)), namespace);
+ previous = step.name();
+ return true;
+ }
+
+ /**
+ * Processes a parallel clone group directive.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ * @param parentGroup optional parent group
+ */
+ private void processParallelGroup(HierarchicalConfiguration cfg,
+ String namespace, Group parentGroup) {
+ String var = cfg.getString(VAR);
+ print("parallel var=%s", var);
+
+ int i = 1;
+ while (condition(var, i).length() > 0) {
+ parallels.add(0, i);
+ compile(cfg, namespace, parentGroup);
+ parallels.remove(0);
+ i++;
+ }
+ }
+
+ /**
+ * Returns the elaborated repetition construct conditional.
+ *
+ * @param var repetition var property
+ * @param i index to elaborate
+ * @return elaborated string
+ */
+ private String condition(String var, Integer i) {
+ return expand(var.replaceFirst("#", i.toString())).trim();
+ }
+
+ /**
+ * Processes a dependency directive.
+ *
+ * @param cfg hierarchical definition
+ * @param namespace optional namespace
+ */
+ private void processDependency(HierarchicalConfiguration cfg, String namespace) {
+ String name = expand(prefix(cfg.getString(NAME), namespace));
+ String requires = expand(cfg.getString(REQUIRES));
+
+ print("dependency name=%s requires=%s", name, requires);
+ Step step = getStep(name, namespace);
+ if (!inactiveSteps.containsValue(step)) {
+ processRequirements(step, requires, namespace);
+ }
+ }
+
+ /**
+ * Processes the specified requiremenst string and adds dependency for
+ * each requirement of the given step.
+ *
+ * @param src source step
+ * @param requires comma-separated list of required steps
+ * @param namespace optional namespace
+ */
+ private void processRequirements(Step src, String requires, String namespace) {
+ String reqs = requirements.get(src.name());
+ for (String n : split(requires)) {
+ boolean isSoft = n.startsWith("~");
+ String name = n.replaceFirst("^~", "");
+ name = previous != null && name.equals("^") ? previous : name;
+ name = (isSoft ? "~" : "") + expand(prefix(name, namespace));
+ reqs = reqs == null ? name : (reqs + "," + name);
+ }
+ requirements.put(src.name(), reqs);
+ }
+
+ /**
+ * Retrieves the step or group with the specified name.
+ *
+ * @param name step or group name
+ * @param namespace optional namespace
+ * @return step or group; null if none found in active or inactive steps
+ */
+ private Step getStep(String name, String namespace) {
+ String dName = prefix(name, namespace);
+ Step step = steps.get(dName);
+ step = step != null ? step : inactiveSteps.get(dName);
+ checkArgument(step != null, "Unknown step %s", dName);
+ return step;
+ }
+
+ /**
+ * Prefixes the specified name with the given namespace.
+ *
+ * @param name name of a step or a group
+ * @param namespace optional namespace
+ * @return composite name
+ */
+ private String prefix(String name, String namespace) {
+ return isNullOrEmpty(namespace) ? name : namespace + "." + name;
+ }
+
+ /**
+ * Expands any environment variables in the specified string. These are
+ * specified as ${property} tokens.
+ *
+ * @param string string to be processed
+ * @param keepTokens true if the original unresolved tokens should be kept
+ * @return original string with expanded substitutions
+ */
+ private String expand(String string, boolean... keepTokens) {
+ if (string == null) {
+ return null;
+ }
+
+ String pString = string;
+ StringBuilder sb = new StringBuilder();
+ int start, end, last = 0;
+ while ((start = pString.indexOf(PROP_START, last)) >= 0) {
+ end = pString.indexOf(PROP_END, start + PROP_START.length());
+ checkArgument(end > start, "Malformed property in %s", pString);
+ sb.append(pString.substring(last, start));
+ String prop = pString.substring(start + PROP_START.length(), end);
+ String value;
+ if (prop.equals(HASH)) {
+ value = parallels.get(0).toString();
+ } else if (prop.endsWith(HASH)) {
+ pString = pString.replaceFirst("#}", parallels.get(0).toString() + "}");
+ last = start;
+ continue;
+ } else {
+ // Try system property first, then fall back to env. variable.
+ value = System.getProperty(prop);
+ if (value == null) {
+ value = System.getenv(prop);
+ }
+ }
+ if (value == null && keepTokens.length == 1 && keepTokens[0]) {
+ sb.append("${").append(prop).append("}");
+ } else {
+ sb.append(value != null ? value : "");
+ }
+ last = end + 1;
+ }
+ sb.append(pString.substring(last));
+ return sb.toString().replace('\n', ' ').replace('\r', ' ');
+ }
+
+ /**
+ * Splits the comma-separated string into a list of strings.
+ *
+ * @param string string to split
+ * @return list of strings
+ */
+ private List<String> split(String string) {
+ ImmutableList.Builder<String> builder = ImmutableList.builder();
+ String[] fields = string != null ? string.split(",") : new String[0];
+ for (String field : fields) {
+ builder.add(field.trim());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Scans the process flow graph for cyclic dependencies.
+ */
+ private void scanForCycles() {
+ DepthFirstSearch<Step, Dependency> dfs = new DepthFirstSearch<>();
+ // Use a brute-force method of searching paths from all vertices.
+ processFlow().getVertexes().forEach(s -> {
+ DepthFirstSearch<Step, Dependency>.SpanningTreeResult r =
+ dfs.search(processFlow, s, null, null, ALL_PATHS);
+ r.edges().forEach((e, et) -> checkArgument(et != BACK_EDGE,
+ "Process flow has a cycle involving dependency from %s to %s",
+ e.src().name, e.dst().name));
+ });
+ }
+
+
+ /**
+ * Prints formatted output.
+ *
+ * @param format printf format string
+ * @param args arguments to be printed
+ */
+ private void print(String format, Object... args) {
+ if (debugOn) {
+ System.err.println(pfx + String.format(format, args));
+ }
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Coordinator.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
new file mode 100644
index 00000000..8e3aad5b
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Coordinator.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static org.onlab.stc.Compiler.PROP_END;
+import static org.onlab.stc.Compiler.PROP_START;
+import static org.onlab.stc.Coordinator.Directive.*;
+import static org.onlab.stc.Coordinator.Status.*;
+
+/**
+ * Coordinates execution of a scenario process flow.
+ */
+public class Coordinator {
+
+ private static final int MAX_THREADS = 16;
+
+ private final ExecutorService executor = newFixedThreadPool(MAX_THREADS);
+
+ private final ProcessFlow processFlow;
+
+ private final StepProcessListener delegate;
+ private final CountDownLatch latch;
+ private final ScenarioStore store;
+
+ private static final Pattern PROP_ERE = Pattern.compile("^@stc ([a-zA-Z0-9_.]+)=(.*$)");
+ private final Map<String, String> properties = Maps.newConcurrentMap();
+
+ private final Set<StepProcessListener> listeners = Sets.newConcurrentHashSet();
+ private File logDir;
+
+ /**
+ * Represents action to be taken on a test step.
+ */
+ public enum Directive {
+ NOOP, RUN, SKIP
+ }
+
+ /**
+ * Represents processor state.
+ */
+ public enum Status {
+ WAITING, IN_PROGRESS, SUCCEEDED, FAILED, SKIPPED
+ }
+
+ /**
+ * Creates a process flow coordinator.
+ *
+ * @param scenario test scenario to coordinate
+ * @param processFlow process flow to coordinate
+ * @param logDir scenario log directory
+ */
+ public Coordinator(Scenario scenario, ProcessFlow processFlow, File logDir) {
+ this.processFlow = processFlow;
+ this.logDir = logDir;
+ this.store = new ScenarioStore(processFlow, logDir, scenario.name());
+ this.delegate = new Delegate();
+ this.latch = new CountDownLatch(1);
+ }
+
+ /**
+ * Resets any previously accrued status and events.
+ */
+ public void reset() {
+ store.reset();
+ }
+
+ /**
+ * Resets all previously accrued status and events for steps that lie
+ * in the range between the steps or groups whose names match the specified
+ * patterns.
+ *
+ * @param runFromPatterns list of starting step patterns
+ * @param runToPatterns list of ending step patterns
+ */
+ public void reset(List<String> runFromPatterns, List<String> runToPatterns) {
+ List<Step> fromSteps = matchSteps(runFromPatterns);
+ List<Step> toSteps = matchSteps(runToPatterns);
+
+ // FIXME: implement this
+ }
+
+ /**
+ * Returns a list of steps that match the specified list of patterns.
+ *
+ * @param runToPatterns list of patterns
+ * @return list of steps with matching names
+ */
+ private List<Step> matchSteps(List<String> runToPatterns) {
+ ImmutableList.Builder<Step> builder = ImmutableList.builder();
+ store.getSteps().forEach(step -> {
+ runToPatterns.forEach(p -> {
+ if (step.name().matches(p)) {
+ builder.add(step);
+ }
+ });
+ });
+ return builder.build();
+ }
+
+ /**
+ * Starts execution of the process flow graph.
+ */
+ public void start() {
+ executeRoots(null);
+ }
+
+ /**
+ * Wants for completion of the entire process flow.
+ *
+ * @return exit code to use
+ * @throws InterruptedException if interrupted while waiting for completion
+ */
+ public int waitFor() throws InterruptedException {
+ latch.await();
+ return store.hasFailures() ? 1 : 0;
+ }
+
+ /**
+ * Returns set of all test steps.
+ *
+ * @return set of steps
+ */
+ public Set<Step> getSteps() {
+ return store.getSteps();
+ }
+
+ /**
+ * Returns a chronological list of step or group records.
+ *
+ * @return list of events
+ */
+ List<StepEvent> getRecords() {
+ return store.getEvents();
+ }
+
+ /**
+ * Returns the status record of the specified test step.
+ *
+ * @param step test step or group
+ * @return step status record
+ */
+ public Status getStatus(Step step) {
+ return store.getStatus(step);
+ }
+
+ /**
+ * Adds the specified listener.
+ *
+ * @param listener step process listener
+ */
+ public void addListener(StepProcessListener listener) {
+ listeners.add(checkNotNull(listener, "Listener cannot be null"));
+ }
+
+ /**
+ * Removes the specified listener.
+ *
+ * @param listener step process listener
+ */
+ public void removeListener(StepProcessListener listener) {
+ listeners.remove(checkNotNull(listener, "Listener cannot be null"));
+ }
+
+ /**
+ * Executes the set of roots in the scope of the specified group or globally
+ * if no group is given.
+ *
+ * @param group optional group
+ */
+ private void executeRoots(Group group) {
+ // FIXME: add ability to skip past completed steps
+ Set<Step> steps =
+ group != null ? group.children() : processFlow.getVertexes();
+ steps.forEach(step -> {
+ if (processFlow.getEdgesFrom(step).isEmpty() && step.group() == group) {
+ execute(step);
+ }
+ });
+ }
+
+ /**
+ * Executes the specified step.
+ *
+ * @param step step to execute
+ */
+ private synchronized void execute(Step step) {
+ Directive directive = nextAction(step);
+ if (directive == RUN) {
+ store.markStarted(step);
+ if (step instanceof Group) {
+ Group group = (Group) step;
+ delegate.onStart(group, null);
+ executeRoots(group);
+ } else {
+ executor.execute(new StepProcessor(step, logDir, delegate,
+ substitute(step.command())));
+ }
+ } else if (directive == SKIP) {
+ if (step instanceof Group) {
+ Group group = (Group) step;
+ group.children().forEach(child -> delegate.onCompletion(child, SKIPPED));
+ }
+ delegate.onCompletion(step, SKIPPED);
+ }
+ }
+
+ /**
+ * Determines the state of the specified step.
+ *
+ * @param step test step
+ * @return state of the step process
+ */
+ private Directive nextAction(Step step) {
+ Status status = store.getStatus(step);
+ if (status != WAITING) {
+ return NOOP;
+ }
+
+ for (Dependency dependency : processFlow.getEdgesFrom(step)) {
+ Status depStatus = store.getStatus(dependency.dst());
+ if (depStatus == WAITING || depStatus == IN_PROGRESS) {
+ return NOOP;
+ } else if ((depStatus == FAILED || depStatus == SKIPPED) &&
+ !dependency.isSoft()) {
+ return SKIP;
+ }
+ }
+ return RUN;
+ }
+
+ /**
+ * Executes the successors to the specified step.
+ *
+ * @param step step whose successors are to be executed
+ */
+ private void executeSucessors(Step step) {
+ processFlow.getEdgesTo(step).forEach(dependency -> execute(dependency.src()));
+ completeParentIfNeeded(step.group());
+ }
+
+ /**
+ * Checks whether the specified parent group, if any, should be marked
+ * as complete.
+ *
+ * @param group parent group that should be checked
+ */
+ private synchronized void completeParentIfNeeded(Group group) {
+ if (group != null && getStatus(group) == IN_PROGRESS) {
+ boolean done = true;
+ boolean failed = false;
+ for (Step child : group.children()) {
+ Status status = store.getStatus(child);
+ done = done && (status == SUCCEEDED || status == FAILED || status == SKIPPED);
+ failed = failed || status == FAILED;
+ }
+ if (done) {
+ delegate.onCompletion(group, failed ? FAILED : SUCCEEDED);
+ }
+ }
+ }
+
+ /**
+ * Expands the var references with values from the properties map.
+ *
+ * @param string string to perform substitutions on
+ */
+ private String substitute(String string) {
+ StringBuilder sb = new StringBuilder();
+ int start, end, last = 0;
+ while ((start = string.indexOf(PROP_START, last)) >= 0) {
+ end = string.indexOf(PROP_END, start + PROP_START.length());
+ checkArgument(end > start, "Malformed property in %s", string);
+ sb.append(string.substring(last, start));
+ String prop = string.substring(start + PROP_START.length(), end);
+ String value = properties.get(prop);
+ sb.append(value != null ? value : "");
+ last = end + 1;
+ }
+ sb.append(string.substring(last));
+ return sb.toString().replace('\n', ' ').replace('\r', ' ');
+ }
+
+ /**
+ * Scrapes the line of output for any variables to be captured and posted
+ * in the properties for later use.
+ *
+ * @param line line of output to scrape for property exports
+ */
+ private void scrapeForVariables(String line) {
+ Matcher matcher = PROP_ERE.matcher(line);
+ if (matcher.matches()) {
+ String prop = matcher.group(1);
+ String value = matcher.group(2);
+ properties.put(prop, value);
+ }
+ }
+
+
+ /**
+ * Prints formatted output.
+ *
+ * @param format printf format string
+ * @param args arguments to be printed
+ */
+ public static void print(String format, Object... args) {
+ System.out.println(String.format(format, args));
+ }
+
+ /**
+ * Internal delegate to monitor the process execution.
+ */
+ private class Delegate implements StepProcessListener {
+ @Override
+ public void onStart(Step step, String command) {
+ listeners.forEach(listener -> listener.onStart(step, command));
+ }
+
+ @Override
+ public void onCompletion(Step step, Status status) {
+ store.markComplete(step, status);
+ listeners.forEach(listener -> listener.onCompletion(step, status));
+ executeSucessors(step);
+ if (store.isComplete()) {
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ scrapeForVariables(line);
+ listeners.forEach(listener -> listener.onOutput(step, line));
+ }
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Dependency.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Dependency.java
new file mode 100644
index 00000000..9025d2e5
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Dependency.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.graph.AbstractEdge;
+
+import java.util.Objects;
+
+/**
+ * Representation of a dependency from one step on completion of another.
+ */
+public class Dependency extends AbstractEdge<Step> {
+
+ private boolean isSoft;
+
+ /**
+ * Creates a new edge between the specified source and destination vertexes.
+ *
+ * @param src source vertex
+ * @param dst destination vertex
+ * @param isSoft indicates whether this is a hard or soft dependency
+ */
+ public Dependency(Step src, Step dst, boolean isSoft) {
+ super(src, dst);
+ this.isSoft = isSoft;
+ }
+
+ /**
+ * Indicates whether this is a soft or hard dependency, i.e. one that
+ * requires successful completion of the dependency or just any completion.
+ *
+ * @return true if dependency is a soft one
+ */
+ public boolean isSoft() {
+ return isSoft;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + Objects.hash(isSoft);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Dependency) {
+ final Dependency other = (Dependency) obj;
+ return super.equals(other) && Objects.equals(this.isSoft, other.isSoft);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", src().name())
+ .add("requires", dst().name())
+ .add("isSoft", isSoft)
+ .toString();
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Group.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Group.java
new file mode 100644
index 00000000..0281c364
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Group.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * Represenation of a related group of steps.
+ */
+public class Group extends Step {
+
+ private final Set<Step> children = Sets.newHashSet();
+
+ /**
+ * Creates a new test step.
+ *
+ * @param name group name
+ * @param command default command
+ * @param env default path to file to be sourced into the environment
+ * @param cwd default path to current working directory for the step
+ * @param group optional group to which this step belongs
+ */
+ public Group(String name, String command, String env, String cwd, Group group) {
+ super(name, command, env, cwd, group);
+ }
+
+ /**
+ * Returns the set of child steps and groups contained within this group.
+ *
+ * @return set of children
+ */
+ public Set<Step> children() {
+ return ImmutableSet.copyOf(children);
+ }
+
+ /**
+ * Adds the specified step or group as a child of this group.
+ *
+ * @param child child step or group to add
+ */
+ public void addChild(Step child) {
+ children.add(child);
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Main.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Main.java
new file mode 100644
index 00000000..09b89456
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Main.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.ImmutableList;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.util.log.Logger;
+import org.onlab.stc.Coordinator.Status;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import static java.lang.System.currentTimeMillis;
+import static org.onlab.stc.Coordinator.Status.*;
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Main program for executing system test coordinator.
+ */
+public final class Main {
+
+ private static final String NONE = "\u001B[0m";
+ private static final String GRAY = "\u001B[30;1m";
+ private static final String RED = "\u001B[31;1m";
+ private static final String GREEN = "\u001B[32;1m";
+ private static final String BLUE = "\u001B[36m";
+
+ private static final String SUCCESS_SUMMARY = "%sPassed! %d steps succeeded%s";
+ private static final String MIXED_SUMMARY =
+ "%s%d steps succeeded; %s%d steps failed; %s%d steps skipped%s";
+ private static final String FAILURE_SUMMARY = "%sFailed! " + MIXED_SUMMARY;
+ private static final String ABORTED_SUMMARY = "%sAborted! " + MIXED_SUMMARY;
+
+ private boolean isReported = false;
+
+ private enum Command {
+ LIST, RUN, RUN_RANGE, HELP
+ }
+
+ private final String scenarioFile;
+
+ private Command command = Command.HELP;
+ private String runFromPatterns = "";
+ private String runToPatterns = "";
+
+ private Coordinator coordinator;
+ private Monitor monitor;
+ private Listener delegate = new Listener();
+
+ private static boolean useColor = Objects.equals("true", System.getenv("stcColor"));
+
+ // usage: stc [<scenario-file>] [run]
+ // usage: stc [<scenario-file>] run [from <from-patterns>] [to <to-patterns>]]
+ // usage: stc [<scenario-file>] list
+
+ // Public construction forbidden
+ private Main(String[] args) {
+ this.scenarioFile = args[0];
+
+ if (args.length <= 1 || args.length == 2 && args[1].equals("run")) {
+ command = Command.RUN;
+ } else if (args.length == 2 && args[1].equals("list")) {
+ command = Command.LIST;
+ } else if (args.length >= 4 && args[1].equals("run")) {
+ int i = 2;
+ if (args[i].equals("from")) {
+ command = Command.RUN_RANGE;
+ runFromPatterns = args[i + 1];
+ i += 2;
+ }
+
+ if (args.length >= i + 2 && args[i].equals("to")) {
+ command = Command.RUN_RANGE;
+ runToPatterns = args[i + 1];
+ }
+ }
+ }
+
+ /**
+ * Main entry point for coordinating test scenario execution.
+ *
+ * @param args command-line arguments
+ */
+ public static void main(String[] args) {
+ Main main = new Main(args);
+ main.run();
+ }
+
+ // Runs the scenario processing
+ private void run() {
+ try {
+ // Load scenario
+ Scenario scenario = Scenario.loadScenario(new FileInputStream(scenarioFile));
+
+ // Elaborate scenario
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+
+ // Setup the process flow coordinator
+ coordinator = new Coordinator(scenario, compiler.processFlow(),
+ compiler.logDir());
+ coordinator.addListener(delegate);
+
+ // Prepare the GUI monitor
+ monitor = new Monitor(coordinator, compiler);
+ startMonitorServer(monitor);
+
+ // Execute process flow
+ processCommand();
+
+ } catch (FileNotFoundException e) {
+ print("Unable to find scenario file %s", scenarioFile);
+ }
+ }
+
+ // Initiates a web-server for the monitor GUI.
+ private static void startMonitorServer(Monitor monitor) {
+ org.eclipse.jetty.util.log.Log.setLog(new NullLogger());
+ Server server = new Server(9999);
+ ServletHandler handler = new ServletHandler();
+ server.setHandler(handler);
+ MonitorWebSocketServlet.setMonitor(monitor);
+ handler.addServletWithMapping(MonitorWebSocketServlet.class, "/*");
+ try {
+ server.start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Processes the appropriate command
+ private void processCommand() {
+ switch (command) {
+ case RUN:
+ processRun();
+ break;
+ case LIST:
+ processList();
+ break;
+ case RUN_RANGE:
+ processRunRange();
+ break;
+ default:
+ print("Unsupported command %s", command);
+ }
+ }
+
+ // Processes the scenario 'run' command.
+ private void processRun() {
+ coordinator.reset();
+ runCoordinator();
+ }
+
+ // Processes the scenario 'run' command for range of steps.
+ private void processRunRange() {
+ coordinator.reset(list(runFromPatterns), list(runToPatterns));
+ runCoordinator();
+ }
+
+ // Processes the scenario 'list' command.
+ private void processList() {
+ coordinator.getRecords()
+ .forEach(event -> logStatus(event.time(), event.name(), event.status(), event.command()));
+ System.exit(0);
+ }
+
+ // Runs the coordinator and waits for it to finish.
+ private void runCoordinator() {
+ try {
+ Runtime.getRuntime().addShutdownHook(new ShutdownHook());
+ coordinator.start();
+ int exitCode = coordinator.waitFor();
+ pause(100); // allow stdout to flush
+ printSummary(exitCode, false);
+ System.exit(exitCode);
+ } catch (InterruptedException e) {
+ print("Unable to execute scenario %s", scenarioFile);
+ }
+ }
+
+ private synchronized void printSummary(int exitCode, boolean isAborted) {
+ if (!isReported) {
+ isReported = true;
+ Set<Step> steps = coordinator.getSteps();
+ int count = steps.size();
+ if (exitCode == 0) {
+ print(SUCCESS_SUMMARY, color(SUCCEEDED), count, color(null));
+ } else {
+ long success = steps.stream().filter(s -> coordinator.getStatus(s) == SUCCEEDED).count();
+ long failed = steps.stream().filter(s -> coordinator.getStatus(s) == FAILED).count();
+ long skipped = steps.stream().filter(s -> coordinator.getStatus(s) == SKIPPED).count();
+ print(isAborted ? ABORTED_SUMMARY : FAILURE_SUMMARY,
+ color(FAILED), color(SUCCEEDED), success,
+ color(FAILED), failed, color(SKIPPED), skipped, color(null));
+ }
+ }
+ }
+
+ /**
+ * Internal delegate to monitor the process execution.
+ */
+ private static class Listener implements StepProcessListener {
+ @Override
+ public void onStart(Step step, String command) {
+ logStatus(currentTimeMillis(), step.name(), IN_PROGRESS, command);
+ }
+
+ @Override
+ public void onCompletion(Step step, Status status) {
+ logStatus(currentTimeMillis(), step.name(), status, null);
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ }
+ }
+
+ // Logs the step status.
+ private static void logStatus(long time, String name, Status status, String cmd) {
+ if (cmd != null) {
+ print("%s %s%s %s%s -- %s", time(time), color(status), name, action(status), color(null), cmd);
+ } else {
+ print("%s %s%s %s%s", time(time), color(status), name, action(status), color(null));
+ }
+ }
+
+ // Produces a description of event using the specified step status.
+ private static String action(Status status) {
+ return status == IN_PROGRESS ? "started" :
+ (status == SUCCEEDED ? "completed" :
+ (status == FAILED ? "failed" :
+ (status == SKIPPED ? "skipped" : "waiting")));
+ }
+
+ // Produces an ANSI escape code for color using the specified step status.
+ private static String color(Status status) {
+ if (!useColor) {
+ return "";
+ }
+ return status == null ? NONE :
+ (status == IN_PROGRESS ? BLUE :
+ (status == SUCCEEDED ? GREEN :
+ (status == FAILED ? RED : GRAY)));
+ }
+
+ // Produces a list from the specified comma-separated string.
+ private static List<String> list(String patterns) {
+ return ImmutableList.copyOf(patterns.split(","));
+ }
+
+ // Produces a formatted time stamp.
+ private static String time(long time) {
+ return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date(time));
+ }
+
+ // Pauses for the specified number of millis.
+ private static void pause(int ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ print("Interrupted!");
+ }
+ }
+
+ // Shutdown hook to report status even when aborted.
+ private class ShutdownHook extends Thread {
+ @Override
+ public void run() {
+ printSummary(1, true);
+ }
+ }
+
+ // Logger to quiet Jetty down
+ private static class NullLogger implements Logger {
+ @Override
+ public String getName() {
+ return "quiet";
+ }
+
+ @Override
+ public void warn(String msg, Object... args) {
+ }
+
+ @Override
+ public void warn(Throwable thrown) {
+ }
+
+ @Override
+ public void warn(String msg, Throwable thrown) {
+ }
+
+ @Override
+ public void info(String msg, Object... args) {
+ }
+
+ @Override
+ public void info(Throwable thrown) {
+ }
+
+ @Override
+ public void info(String msg, Throwable thrown) {
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ @Override
+ public void setDebugEnabled(boolean enabled) {
+ }
+
+ @Override
+ public void debug(String msg, Object... args) {
+ }
+
+ @Override
+ public void debug(Throwable thrown) {
+ }
+
+ @Override
+ public void debug(String msg, Throwable thrown) {
+ }
+
+ @Override
+ public Logger getLogger(String name) {
+ return this;
+ }
+
+ @Override
+ public void ignore(Throwable ignored) {
+ }
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Monitor.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Monitor.java
new file mode 100644
index 00000000..4e6f63fa
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Monitor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.Maps;
+import org.onlab.stc.MonitorLayout.Box;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import static org.onlab.stc.Coordinator.Status.IN_PROGRESS;
+
+/**
+ * Scenario test monitor.
+ */
+public class Monitor implements StepProcessListener {
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private final Coordinator coordinator;
+ private final Compiler compiler;
+ private final MonitorLayout layout;
+
+ private MonitorDelegate delegate;
+
+ private Map<Step, Box> boxes = Maps.newHashMap();
+
+ /**
+ * Creates a new shared process flow monitor.
+ *
+ * @param coordinator process flow coordinator
+ * @param compiler scenario compiler
+ */
+ Monitor(Coordinator coordinator, Compiler compiler) {
+ this.coordinator = coordinator;
+ this.compiler = compiler;
+ this.layout = new MonitorLayout(compiler);
+ coordinator.addListener(this);
+ }
+
+ /**
+ * Sets the process monitor delegate.
+ *
+ * @param delegate process monitor delegate
+ */
+ void setDelegate(MonitorDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * Notifies the process monitor delegate with the specified event.
+ *
+ * @param event JSON event data
+ */
+ public void notify(ObjectNode event) {
+ if (delegate != null) {
+ delegate.notify(event);
+ }
+ }
+
+ /**
+ * Returns the scenario process flow as JSON data.
+ *
+ * @return scenario process flow data
+ */
+ ObjectNode scenarioData() {
+ ObjectNode root = mapper.createObjectNode();
+ ArrayNode steps = mapper.createArrayNode();
+ ArrayNode requirements = mapper.createArrayNode();
+
+ ProcessFlow pf = compiler.processFlow();
+ pf.getVertexes().forEach(step -> add(step, steps));
+ pf.getEdges().forEach(requirement -> add(requirement, requirements));
+
+ root.set("steps", steps);
+ root.set("requirements", requirements);
+
+ try (FileWriter fw = new FileWriter("/tmp/data.json");
+ PrintWriter pw = new PrintWriter(fw)) {
+ pw.println(root.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return root;
+ }
+
+
+ private void add(Step step, ArrayNode steps) {
+ Box box = layout.get(step);
+ ObjectNode sn = mapper.createObjectNode()
+ .put("name", step.name())
+ .put("isGroup", step instanceof Group)
+ .put("status", status(coordinator.getStatus(step)))
+ .put("tier", box.tier())
+ .put("depth", box.depth());
+ if (step.group() != null) {
+ sn.put("group", step.group().name());
+ }
+ steps.add(sn);
+ }
+
+ private String status(Coordinator.Status status) {
+ return status.toString().toLowerCase();
+ }
+
+ private void add(Dependency requirement, ArrayNode requirements) {
+ ObjectNode rn = mapper.createObjectNode();
+ rn.put("src", requirement.src().name())
+ .put("dst", requirement.dst().name())
+ .put("isSoft", requirement.isSoft());
+ requirements.add(rn);
+ }
+
+ @Override
+ public void onStart(Step step, String command) {
+ notify(event(step, status(IN_PROGRESS)));
+ }
+
+ @Override
+ public void onCompletion(Step step, Coordinator.Status status) {
+ notify(event(step, status(status)));
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+
+ }
+
+ private ObjectNode event(Step step, String status) {
+ ObjectNode event = mapper.createObjectNode()
+ .put("name", step.name())
+ .put("status", status);
+ return event;
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java
new file mode 100644
index 00000000..d11542a7
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorDelegate.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Delegate to which monitor can send notifications.
+ */
+public interface MonitorDelegate {
+
+ /**
+ * Issues JSON event to be sent to any connected monitor clients.
+ *
+ * @param event JSON event data
+ */
+ void notify(ObjectNode event);
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java
new file mode 100644
index 00000000..1c0e7313
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorLayout.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Computes scenario process flow layout for the Monitor GUI.
+ */
+public class MonitorLayout {
+
+ public static final int WIDTH = 210;
+ public static final int HEIGHT = 30;
+ public static final int W_GAP = 40;
+ public static final int H_GAP = 50;
+ public static final int SLOT_WIDTH = WIDTH + H_GAP;
+
+ private final Compiler compiler;
+ private final ProcessFlow flow;
+
+ private Map<Step, Box> boxes = Maps.newHashMap();
+
+ /**
+ * Creates a new shared process flow monitor.
+ *
+ * @param compiler scenario compiler
+ */
+ MonitorLayout(Compiler compiler) {
+ this.compiler = compiler;
+ this.flow = compiler.processFlow();
+
+ // Extract the flow and create initial bounding boxes.
+ boxes.put(null, new Box(null, 0));
+ flow.getVertexes().forEach(this::createBox);
+
+ computeLayout(null, 0, 1);
+ }
+
+ // Computes the graph layout giving preference to group associations.
+ private void computeLayout(Group group, int absoluteTier, int tier) {
+ Box box = boxes.get(group);
+
+ // Find all children of the group, or items with no group if at top.
+ Set<Step> children = group != null ? group.children() :
+ flow.getVertexes().stream().filter(s -> s.group() == null)
+ .collect(Collectors.toSet());
+
+ children.forEach(s -> visit(s, absoluteTier, 1, group));
+
+ // Figure out what the group root vertexes are.
+ Set<Step> roots = findRoots(group);
+
+ // Compute the boxes for each of the roots.
+ roots.forEach(s -> updateBox(s, absoluteTier + 1, 1, group));
+
+ // Update the tier and depth of the group bounding box.
+ computeTiersAndDepth(group, box, absoluteTier, tier, children);
+
+ // Compute the minimum breadth of this group's bounding box.
+ computeBreadth(group, box, children);
+
+ // Compute child placements
+ computeChildPlacements(group, box, children);
+ }
+
+ // Updates the box for the specified step, given the tier number, which
+ // is relative to the parent.
+ private Box updateBox(Step step, int absoluteTier, int tier, Group group) {
+ Box box = boxes.get(step);
+ if (step instanceof Group) {
+ computeLayout((Group) step, absoluteTier, tier);
+ } else {
+ box.setTierAndDepth(absoluteTier, tier, 1, group);
+ }
+
+ // Follow the steps downstream of this one.
+ follow(step, absoluteTier + box.depth(), box.tier() + box.depth());
+ return box;
+ }
+
+ // Backwards follows edges leading towards the specified step to visit
+ // the source vertex and compute layout of those vertices that had
+ // sufficient number of visits to compute their tier.
+ private void follow(Step step, int absoluteTier, int tier) {
+ Group from = step.group();
+ flow.getEdgesTo(step).stream()
+ .filter(d -> visit(d.src(), absoluteTier, tier, from))
+ .forEach(d -> updateBox(d.src(), absoluteTier, tier, from));
+ }
+
+ // Visits each step, records maximum tier and returns true if this
+ // was the last expected visit.
+ private boolean visit(Step step, int absoluteTier, int tier, Group from) {
+ Box box = boxes.get(step);
+ return box.visitAndLatchMaxTier(absoluteTier, tier, from);
+ }
+
+ // Computes the absolute and relative tiers and the depth of the group
+ // bounding box.
+ private void computeTiersAndDepth(Group group, Box box,
+ int absoluteTier, int tier, Set<Step> children) {
+ int depth = children.stream().mapToInt(this::bottomMostTier).max().getAsInt();
+ box.setTierAndDepth(absoluteTier, tier, depth, group);
+ }
+
+ // Returns the bottom-most tier this step occupies relative to its parent.
+ private int bottomMostTier(Step step) {
+ Box box = boxes.get(step);
+ return box.tier() + box.depth();
+ }
+
+ // Computes breadth of the specified group.
+ private void computeBreadth(Group group, Box box, Set<Step> children) {
+ if (box.breadth() == 0) {
+ // Scan through all tiers and determine the maximum breadth of each.
+ IntStream.range(1, box.depth)
+ .forEach(t -> computeTierBreadth(t, box, children));
+ box.latchBreadth(children.stream()
+ .mapToInt(s -> boxes.get(s).breadth())
+ .max().getAsInt());
+ }
+ }
+
+ // Computes tier width.
+ private void computeTierBreadth(int t, Box box, Set<Step> children) {
+ box.latchBreadth(children.stream().map(boxes::get)
+ .filter(b -> isSpanningTier(b, t))
+ .mapToInt(Box::breadth).sum());
+ }
+
+ // Computes the actual child box placements relative to the parent using
+ // the previously established tier, depth and breadth attributes.
+ private void computeChildPlacements(Group group, Box box,
+ Set<Step> children) {
+ // Order the root-nodes in alphanumeric order first.
+ List<Box> tierBoxes = Lists.newArrayList(boxesOnTier(1, children));
+ tierBoxes.sort((a, b) -> a.step().name().compareTo(b.step().name()));
+
+ // Place the boxes centered on the parent box; left to right.
+ int tierBreadth = tierBoxes.stream().mapToInt(Box::breadth).sum();
+ int slot = 1;
+ for (Box b : tierBoxes) {
+ b.updateCenter(1, slot(slot, tierBreadth));
+ slot += b.breadth();
+ }
+ }
+
+ // Returns the horizontal offset off the parent center.
+ private int slot(int slot, int tierBreadth) {
+ boolean even = tierBreadth % 2 == 0;
+ int multiplier = -tierBreadth / 2 + slot - 1;
+ return even ? multiplier * SLOT_WIDTH + SLOT_WIDTH / 2 : multiplier * SLOT_WIDTH;
+ }
+
+ // Returns a list of all child step boxes that start on the specified tier.
+ private List<Box> boxesOnTier(int tier, Set<Step> children) {
+ return boxes.values().stream()
+ .filter(b -> b.tier() == tier && children.contains(b.step()))
+ .collect(Collectors.toList());
+ }
+
+ // Determines whether the specified box spans, or occupies a tier.
+ private boolean isSpanningTier(Box b, int tier) {
+ return (b.depth() == 1 && b.tier() == tier) ||
+ (b.tier() <= tier && tier < b.tier() + b.depth());
+ }
+
+
+ // Determines roots of the specified group or of the entire graph.
+ private Set<Step> findRoots(Group group) {
+ Set<Step> steps = group != null ? group.children() : flow.getVertexes();
+ return steps.stream().filter(s -> isRoot(s, group)).collect(Collectors.toSet());
+ }
+
+ private boolean isRoot(Step step, Group group) {
+ if (step.group() != group) {
+ return false;
+ }
+
+ Set<Dependency> requirements = flow.getEdgesFrom(step);
+ return requirements.stream().filter(r -> r.dst().group() == group)
+ .collect(Collectors.toSet()).isEmpty();
+ }
+
+ /**
+ * Returns the bounding box for the specified step. If null is given, it
+ * returns the overall bounding box.
+ *
+ * @param step step or group; null for the overall bounding box
+ * @return bounding box
+ */
+ public Box get(Step step) {
+ return boxes.get(step);
+ }
+
+ /**
+ * Returns the bounding box for the specified step name. If null is given,
+ * it returns the overall bounding box.
+ *
+ * @param name name of step or group; null for the overall bounding box
+ * @return bounding box
+ */
+ public Box get(String name) {
+ return get(name == null ? null : compiler.getStep(name));
+ }
+
+ // Creates a bounding box for the specified step or group.
+ private void createBox(Step step) {
+ boxes.put(step, new Box(step, flow.getEdgesFrom(step).size()));
+ }
+
+ /**
+ * Bounding box data for a step or group.
+ */
+ final class Box {
+
+ private Step step;
+ private int remainingRequirements;
+
+ private int absoluteTier = 0;
+ private int tier;
+ private int depth = 1;
+ private int breadth;
+ private int center, top;
+
+ private Box(Step step, int remainingRequirements) {
+ this.step = step;
+ this.remainingRequirements = remainingRequirements + 1;
+ breadth = step == null || step instanceof Group ? 0 : 1;
+ }
+
+ private void latchTiers(int absoluteTier, int tier, Group from) {
+ this.absoluteTier = Math.max(this.absoluteTier, absoluteTier);
+ if (step == null || step.group() == from) {
+ this.tier = Math.max(this.tier, tier);
+ }
+ }
+
+ public void latchBreadth(int breadth) {
+ this.breadth = Math.max(this.breadth, breadth);
+ }
+
+ void setTierAndDepth(int absoluteTier, int tier, int depth, Group from) {
+ latchTiers(absoluteTier, tier, from);
+ this.depth = depth;
+ }
+
+ boolean visitAndLatchMaxTier(int absoluteTier, int tier, Group from) {
+ latchTiers(absoluteTier, tier, from);
+ --remainingRequirements;
+ return remainingRequirements == 0;
+ }
+
+ Step step() {
+ return step;
+ }
+
+ public int absoluteTier() {
+ return absoluteTier;
+ }
+
+ int tier() {
+ return tier;
+ }
+
+ int depth() {
+ return depth;
+ }
+
+ int breadth() {
+ return breadth;
+ }
+
+ int top() {
+ return top;
+ }
+
+ int center() {
+ return center;
+ }
+
+ public void updateCenter(int top, int center) {
+ this.top = top;
+ this.center = center;
+ }
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java
new file mode 100644
index 00000000..cd146070
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocket.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.eclipse.jetty.websocket.WebSocket;
+
+import java.io.IOException;
+
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Web socket capable of interacting with the STC monitor GUI.
+ */
+public class MonitorWebSocket implements WebSocket.OnTextMessage, WebSocket.OnControl {
+
+ private static final long MAX_AGE_MS = 30_000;
+
+ private static final byte PING = 0x9;
+ private static final byte PONG = 0xA;
+ private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
+
+ private final Monitor monitor;
+
+ private Connection connection;
+ private FrameConnection control;
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private long lastActive = System.currentTimeMillis();
+
+ /**
+ * Creates a new monitor client GUI web-socket.
+ *
+ * @param monitor shared process flow monitor
+ */
+ MonitorWebSocket(Monitor monitor) {
+ this.monitor = monitor;
+ }
+
+ /**
+ * Issues a close on the connection.
+ */
+ synchronized void close() {
+ destroyHandlers();
+ if (connection.isOpen()) {
+ connection.close();
+ }
+ }
+
+ /**
+ * Indicates if this connection is idle.
+ *
+ * @return true if idle or closed
+ */
+ synchronized boolean isIdle() {
+ long quietFor = System.currentTimeMillis() - lastActive;
+ boolean idle = quietFor > MAX_AGE_MS;
+ if (idle || (connection != null && !connection.isOpen())) {
+ return true;
+ } else if (connection != null) {
+ try {
+ control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
+ } catch (IOException e) {
+ print("Unable to send ping message due to: %s", e);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onOpen(Connection connection) {
+ this.connection = connection;
+ this.control = (FrameConnection) connection;
+ try {
+ createHandlers();
+ sendMessage(message("flow", monitor.scenarioData()));
+
+ } catch (Exception e) {
+ print("Unable to open monitor connection: %s", e);
+ this.connection.close();
+ this.connection = null;
+ this.control = null;
+ }
+ }
+
+ @Override
+ public synchronized void onClose(int closeCode, String message) {
+ destroyHandlers();
+ }
+
+ @Override
+ public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
+ lastActive = System.currentTimeMillis();
+ return true;
+ }
+
+ @Override
+ public void onMessage(String data) {
+ lastActive = System.currentTimeMillis();
+ try {
+ ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
+ // TODO:
+ print("Got message: %s", message);
+ } catch (Exception e) {
+ print("Unable to parse GUI message %s due to %s", data, e);
+ }
+ }
+
+ public synchronized void sendMessage(ObjectNode message) {
+ try {
+ if (connection.isOpen()) {
+ connection.sendMessage(message.toString());
+ }
+ } catch (IOException e) {
+ print("Unable to send message %s to GUI due to %s", message, e);
+ }
+ }
+
+ public ObjectNode message(String type, ObjectNode payload) {
+ ObjectNode message = mapper.createObjectNode().put("event", type);
+ message.set("payload", payload);
+ return message;
+ }
+
+ // Creates new message handlers.
+ private synchronized void createHandlers() {
+ }
+
+ // Destroys message handlers.
+ private synchronized void destroyHandlers() {
+ }
+
+}
+
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java
new file mode 100644
index 00000000..a8705003
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/MonitorWebSocketServlet.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.io.ByteStreams;
+import com.google.common.net.MediaType;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.eclipse.jetty.websocket.WebSocketServlet;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Web socket servlet capable of creating web sockets for the STC monitor.
+ */
+public class MonitorWebSocketServlet extends WebSocketServlet
+ implements MonitorDelegate {
+
+ private static final long PING_DELAY_MS = 5000;
+ private static final String DOT = ".";
+
+ private static Monitor monitor;
+ private static MonitorWebSocketServlet instance;
+
+ private final Set<MonitorWebSocket> sockets = new HashSet<>();
+ private final Timer timer = new Timer();
+ private final TimerTask pruner = new Pruner();
+
+ /**
+ * Binds the shared process flow monitor.
+ *
+ * @param m process monitor reference
+ */
+ public static void setMonitor(Monitor m) {
+ monitor = m;
+ }
+
+ /**
+ * Closes all currently open monitor web-sockets.
+ */
+ public static void closeAll() {
+ if (instance != null) {
+ instance.sockets.forEach(MonitorWebSocket::close);
+ instance.sockets.clear();
+ }
+ }
+
+ @Override
+ public void init() throws ServletException {
+ super.init();
+ instance = this;
+ monitor.setDelegate(this);
+ timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ String uri = req.getRequestURI();
+ uri = uri.length() <= 1 ? "/index.html" : uri;
+ InputStream resource = getClass().getResourceAsStream(uri);
+ if (resource == null) {
+ resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ } else {
+ byte[] entity = ByteStreams.toByteArray(resource);
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType(contentType(uri).toString());
+ resp.setContentLength(entity.length);
+ resp.getOutputStream().write(entity);
+ }
+ }
+
+ private MediaType contentType(String uri) {
+ int sep = uri.lastIndexOf(DOT);
+ String ext = sep > 0 ? uri.substring(sep + 1) : null;
+ return ext == null ? MediaType.APPLICATION_BINARY :
+ ext.equals("html") ? MediaType.HTML_UTF_8 :
+ ext.equals("js") ? MediaType.JAVASCRIPT_UTF_8 :
+ ext.equals("css") ? MediaType.CSS_UTF_8 :
+ MediaType.APPLICATION_BINARY;
+ }
+
+ @Override
+ public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
+ MonitorWebSocket socket = new MonitorWebSocket(monitor);
+ synchronized (sockets) {
+ sockets.add(socket);
+ }
+ return socket;
+ }
+
+ @Override
+ public void notify(ObjectNode event) {
+ if (instance != null) {
+ instance.sockets.forEach(ws -> ws.sendMessage(event));
+ }
+ }
+
+ // Task for pruning web-sockets that are idle.
+ private class Pruner extends TimerTask {
+ @Override
+ public void run() {
+ synchronized (sockets) {
+ Iterator<MonitorWebSocket> it = sockets.iterator();
+ while (it.hasNext()) {
+ MonitorWebSocket socket = it.next();
+ if (socket.isIdle()) {
+ it.remove();
+ socket.close();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java
new file mode 100644
index 00000000..4d99b339
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ProcessFlow.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.onlab.graph.MutableAdjacencyListsGraph;
+
+import java.util.Set;
+
+/**
+ * Graph representation of a test process flow.
+ */
+public class ProcessFlow extends MutableAdjacencyListsGraph<Step, Dependency> {
+
+ /**
+ * Creates a graph comprising of the specified vertexes and edges.
+ *
+ * @param vertexes set of graph vertexes
+ * @param edges set of graph edges
+ */
+ public ProcessFlow(Set<Step> vertexes, Set<Dependency> edges) {
+ super(vertexes, edges);
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Scenario.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Scenario.java
new file mode 100644
index 00000000..fd2cd62d
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Scenario.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalConfiguration;
+import org.apache.commons.configuration.XMLConfiguration;
+
+import java.io.InputStream;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Representation of a re-usable test scenario.
+ */
+public final class Scenario {
+
+ private static final String SCENARIO = "scenario";
+ private static final String NAME = "[@name]";
+ private static final String DESCRIPTION = "[@description]";
+
+ private final String name;
+ private final String description;
+ private final HierarchicalConfiguration definition;
+
+ // Creates a new scenario from the specified definition.
+ private Scenario(String name, String description, HierarchicalConfiguration definition) {
+ this.name = checkNotNull(name, "Name cannot be null");
+ this.description = checkNotNull(description, "Description cannot be null");
+ this.definition = checkNotNull(definition, "Definition cannot be null");
+ }
+
+ /**
+ * Loads a new scenario from the specified hierarchical configuration.
+ *
+ * @param definition scenario definition
+ * @return loaded scenario
+ */
+ public static Scenario loadScenario(HierarchicalConfiguration definition) {
+ String name = definition.getString(NAME);
+ String description = definition.getString(DESCRIPTION, "");
+ checkState(name != null, "Scenario name must be specified");
+ return new Scenario(name, description, definition);
+ }
+
+ /**
+ * Loads a new scenario from the specified input stream.
+ *
+ * @param stream scenario definition stream
+ * @return loaded scenario
+ */
+ public static Scenario loadScenario(InputStream stream) {
+ XMLConfiguration cfg = new XMLConfiguration();
+ cfg.setAttributeSplittingDisabled(true);
+ cfg.setDelimiterParsingDisabled(true);
+ cfg.setRootElementName(SCENARIO);
+ try {
+ cfg.load(stream);
+ return loadScenario(cfg);
+ } catch (ConfigurationException e) {
+ throw new IllegalArgumentException("Unable to load scenario from the stream", e);
+ }
+ }
+
+ /**
+ * Returns the scenario name.
+ *
+ * @return scenario name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the scenario description.
+ *
+ * @return scenario description
+ */
+ public String description() {
+ return description;
+ }
+
+ /**
+ * Returns the scenario definition.
+ *
+ * @return scenario definition
+ */
+ public HierarchicalConfiguration definition() {
+ return definition;
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
new file mode 100644
index 00000000..d37222b1
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/ScenarioStore.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.PropertiesConfiguration;
+import org.onlab.stc.Coordinator.Status;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.stc.Coordinator.Status.*;
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Maintains state of scenario execution.
+ */
+class ScenarioStore {
+
+ private final ProcessFlow processFlow;
+ private final File storeFile;
+ private final File logDir;
+
+ private final List<StepEvent> events = Lists.newArrayList();
+ private final Map<String, Status> statusMap = Maps.newConcurrentMap();
+
+ /**
+ * Creates a new scenario store for the specified process flow.
+ *
+ * @param processFlow scenario process flow
+ * @param logDir scenario log directory
+ * @param name scenario name
+ */
+ ScenarioStore(ProcessFlow processFlow, File logDir, String name) {
+ this.processFlow = processFlow;
+ this.logDir = logDir;
+ this.storeFile = new File(logDir, name + ".stc");
+ load();
+ }
+
+ /**
+ * Resets status of all steps to waiting and clears all events.
+ */
+ void reset() {
+ events.clear();
+ statusMap.clear();
+ processFlow.getVertexes().forEach(step -> statusMap.put(step.name(), WAITING));
+ try {
+ removeLogs();
+ PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
+ cfg.clear();
+ cfg.save();
+ } catch (ConfigurationException e) {
+ print("Unable to store file %s", storeFile);
+ }
+
+ }
+
+ /**
+ * Returns set of all test steps.
+ *
+ * @return set of steps
+ */
+ Set<Step> getSteps() {
+ return processFlow.getVertexes();
+ }
+
+ /**
+ * Returns a chronological list of step or group records.
+ *
+ * @return list of events
+ */
+ synchronized List<StepEvent> getEvents() {
+ return ImmutableList.copyOf(events);
+ }
+
+ /**
+ * Returns the status record of the specified test step.
+ *
+ * @param step test step or group
+ * @return step status record
+ */
+ Status getStatus(Step step) {
+ return checkNotNull(statusMap.get(step.name()), "Step %s not found", step.name());
+ }
+
+ /**
+ * Marks the specified test step as being in progress.
+ *
+ * @param step test step or group
+ */
+ synchronized void markStarted(Step step) {
+ add(new StepEvent(step.name(), IN_PROGRESS, step.command()));
+ save();
+ }
+
+ /**
+ * Marks the specified test step as being complete.
+ *
+ * @param step test step or group
+ * @param status new step status
+ */
+ synchronized void markComplete(Step step, Status status) {
+ add(new StepEvent(step.name(), status, null));
+ save();
+ }
+
+ /**
+ * Returns true if all steps in the store have been marked as completed
+ * regardless of the completion status.
+ *
+ * @return true if all steps completed one way or another
+ */
+ synchronized boolean isComplete() {
+ return !statusMap.values().stream().anyMatch(s -> s == WAITING || s == IN_PROGRESS);
+ }
+
+ /**
+ * Indicates whether there are any failures.
+ *
+ * @return true if there are failed steps
+ */
+ boolean hasFailures() {
+ for (Status status : statusMap.values()) {
+ if (status == FAILED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Registers a new step record.
+ *
+ * @param event step event
+ */
+ private synchronized void add(StepEvent event) {
+ events.add(event);
+ statusMap.put(event.name(), event.status());
+ }
+
+ /**
+ * Loads the states from disk.
+ */
+ private void load() {
+ try {
+ PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
+ cfg.getKeys().forEachRemaining(prop -> add(StepEvent.fromString(cfg.getString(prop))));
+ cfg.save();
+ } catch (ConfigurationException e) {
+ print("Unable to load file %s", storeFile);
+ }
+ }
+
+ /**
+ * Saves the states to disk.
+ */
+ private void save() {
+ try {
+ PropertiesConfiguration cfg = new PropertiesConfiguration(storeFile);
+ events.forEach(event -> cfg.setProperty("T" + event.time(), event.toString()));
+ cfg.save();
+ } catch (ConfigurationException e) {
+ print("Unable to store file %s", storeFile);
+ }
+ }
+
+ /**
+ * Removes all scenario log files.
+ */
+ private void removeLogs() {
+ File[] logFiles = logDir.listFiles();
+ if (logFiles != null && logFiles.length > 0) {
+ for (File file : logFiles) {
+ if (!file.delete()) {
+ print("Unable to delete log file %s", file);
+ }
+ }
+ }
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Step.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Step.java
new file mode 100644
index 00000000..3d8ea983
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/Step.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.graph.Vertex;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Representation of a test step.
+ */
+public class Step implements Vertex {
+
+ protected final String name;
+ protected final String command;
+ protected final String env;
+ protected final String cwd;
+ protected final Group group;
+
+ /**
+ * Creates a new test step.
+ *
+ * @param name step name
+ * @param command step command to execute
+ * @param env path to file to be sourced into the environment
+ * @param cwd path to current working directory for the step
+ * @param group optional group to which this step belongs
+ */
+ public Step(String name, String command, String env, String cwd, Group group) {
+ this.name = checkNotNull(name, "Name cannot be null");
+ this.group = group;
+
+ // Set the command, environment and cwd
+ // If one is not given use the value from the enclosing group
+ this.command = command != null ? command : group != null && group.command != null ? group.command : null;
+ this.env = env != null ? env : group != null && group.env != null ? group.env : null;
+ this.cwd = cwd != null ? cwd : group != null && group.cwd != null ? group.cwd : null;
+ }
+
+ /**
+ * Returns the step name.
+ *
+ * @return step name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the step command string.
+ *
+ * @return command string
+ */
+ public String command() {
+ return command;
+ }
+
+ /**
+ * Returns the step environment script path.
+ *
+ * @return env script path
+ */
+ public String env() {
+ return env;
+ }
+
+ /**
+ * Returns the step current working directory path.
+ *
+ * @return current working dir path
+ */
+ public String cwd() {
+ return cwd;
+ }
+
+ /**
+ * Returns the enclosing group; null if none.
+ *
+ * @return enclosing group or null
+ */
+ public Group group() {
+ return group;
+ }
+
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Step) {
+ final Step other = (Step) obj;
+ return Objects.equals(this.name, other.name);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", name)
+ .add("command", command)
+ .add("env", env)
+ .add("cwd", cwd)
+ .add("group", group)
+ .toString();
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepEvent.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepEvent.java
new file mode 100644
index 00000000..c9b81a24
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepEvent.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.onlab.stc.Coordinator.Status;
+
+import static java.lang.Long.parseLong;
+import static org.onlab.stc.Coordinator.Status.valueOf;
+
+/**
+ * Represents an event of execution of a scenario step or group.
+ */
+public class StepEvent {
+
+ private static final String SEP = "~";
+
+ private final String name;
+ private final long time;
+ private final Status status;
+ private final String command;
+
+ /**
+ * Creates a new step record.
+ *
+ * @param name test step or group name
+ * @param time time in millis since start of epoch
+ * @param status step completion status
+ * @param command step command
+ */
+ public StepEvent(String name, long time, Status status, String command) {
+ this.name = name;
+ this.time = time;
+ this.status = status;
+ this.command = command;
+ }
+
+ /**
+ * Creates a new step record for non-running status.
+ *
+ * @param name test step or group name
+ * @param status status
+ * @param command step command
+ */
+ public StepEvent(String name, Status status, String command) {
+ this(name, System.currentTimeMillis(), status, command);
+ }
+
+ /**
+ * Returns the test step or test group name.
+ *
+ * @return step or group name
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Returns the step event time.
+ *
+ * @return time in millis since start of epoch
+ */
+ public long time() {
+ return time;
+ }
+
+ /**
+ * Returns the step completion status.
+ *
+ * @return completion status
+ */
+ public Status status() {
+ return status;
+ }
+
+ /**
+ * Returns the step command.
+ *
+ * @return step command
+ */
+ public String command() {
+ return command;
+ }
+
+
+ @Override
+ public String toString() {
+ return name + SEP + time + SEP + status + SEP + command;
+ }
+
+ /**
+ * Returns a record parsed from the specified string.
+ *
+ * @param string string encoding
+ * @return step record
+ */
+ public static StepEvent fromString(String string) {
+ String[] fields = string.split("~");
+ return fields.length == 4 ?
+ new StepEvent(fields[0], parseLong(fields[1]), valueOf(fields[2]),
+ fields[3].equals("null") ? null : fields[3]) :
+ new StepEvent(fields[0], 0, Status.WAITING, null);
+ }
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
new file mode 100644
index 00000000..a8222d0b
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+/**
+ * Entity capable of receiving notifications of process step execution events.
+ */
+public interface StepProcessListener {
+
+ /**
+ * Indicates that process step has started.
+ *
+ * @param step subject step
+ * @param command actual command executed; includes run-time substitutions
+ */
+ default void onStart(Step step, String command) {
+ }
+
+ /**
+ * Indicates that process step has completed.
+ *
+ * @param step subject step
+ * @param status step completion status
+ */
+ default void onCompletion(Step step, Coordinator.Status status) {
+ }
+
+ /**
+ * Notifies when a new line of output becomes available.
+ *
+ * @param step subject step
+ * @param line line of output
+ */
+ default void onOutput(Step step, String line) {
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
new file mode 100644
index 00000000..49943691
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/StepProcessor.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.onlab.stc.Coordinator.Status;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+
+import static java.lang.String.format;
+import static org.onlab.stc.Coordinator.Status.FAILED;
+import static org.onlab.stc.Coordinator.Status.SUCCEEDED;
+import static org.onlab.stc.Coordinator.print;
+
+/**
+ * Manages execution of the specified step or a group.
+ */
+class StepProcessor implements Runnable {
+
+ private static final String IGNORE_CODE = "~";
+ private static final String NEGATE_CODE = "!";
+
+ private static final int FAIL = -1;
+
+ static String launcher = "stc-launcher ";
+
+ private final Step step;
+ private final File logDir;
+ private String command;
+
+ private Process process;
+ private StepProcessListener delegate;
+
+ /**
+ * Creates a process monitor.
+ *
+ * @param step step or group to be executed
+ * @param logDir directory where step process log should be stored
+ * @param delegate process lifecycle listener
+ * @param command actual command to execute
+ */
+ StepProcessor(Step step, File logDir, StepProcessListener delegate,
+ String command) {
+ this.step = step;
+ this.logDir = logDir;
+ this.delegate = delegate;
+ this.command = command;
+ }
+
+ @Override
+ public void run() {
+ delegate.onStart(step, command);
+ int code = execute();
+ boolean ignoreCode = step.env() != null && step.env.equals(IGNORE_CODE);
+ boolean negateCode = step.env() != null && step.env.equals(NEGATE_CODE);
+ Status status = ignoreCode || code == 0 && !negateCode || code != 0 && negateCode ?
+ SUCCEEDED : FAILED;
+ delegate.onCompletion(step, status);
+ }
+
+ /**
+ * Executes the step process.
+ *
+ * @return exit code
+ */
+ private int execute() {
+ try (PrintWriter pw = new PrintWriter(logFile())) {
+ process = Runtime.getRuntime().exec(command());
+ processOutput(pw);
+
+ // Wait for the process to complete and get its exit code.
+ if (process.isAlive()) {
+ process.waitFor();
+ }
+ return process.exitValue();
+
+ } catch (IOException e) {
+ print("Unable to run step %s using command %s", step.name(), step.command());
+ } catch (InterruptedException e) {
+ print("Step %s interrupted", step.name());
+ }
+ return FAIL;
+ }
+
+ /**
+ * Returns ready-to-run command for the step.
+ *
+ * @return command to execute
+ */
+ private String command() {
+ return format("%s %s %s %s", launcher,
+ step.env() != null ? step.env() : "-",
+ step.cwd() != null ? step.cwd() : "-",
+ command);
+ }
+
+ /**
+ * Captures output of the step process.
+ *
+ * @param pw print writer to send output to
+ * @throws IOException if unable to read output or write logs
+ */
+ private void processOutput(PrintWriter pw) throws IOException {
+ InputStream out = process.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(out));
+
+ // Slurp its combined stderr/stdout
+ String line;
+ while ((line = br.readLine()) != null) {
+ pw.println(line);
+ delegate.onOutput(step, line);
+ }
+ }
+
+ /**
+ * Returns the log file for the step output.
+ *
+ * @return log file
+ */
+ private File logFile() {
+ return new File(logDir, step.name() + ".log");
+ }
+
+}
diff --git a/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/package-info.java b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/package-info.java
new file mode 100644
index 00000000..56145899
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/java/org/onlab/stc/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+/**
+ * System Test Coordinator tool for modular scenario-based testing.
+ */
+package org.onlab.stc; \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/main/resources/data.json b/framework/src/onos/utils/stc/src/main/resources/data.json
new file mode 100644
index 00000000..f5823744
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/resources/data.json
@@ -0,0 +1,1087 @@
+{
+ "requirements": [
+ {
+ "dst": "Reactive-Forwarding.Ping-2",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Link-2-Down"
+ },
+ {
+ "dst": "Final-Check-Logs-2",
+ "isSoft": true,
+ "src": "Fetch-Logs-2"
+ },
+ {
+ "dst": "Host-Intent.Ping-4",
+ "isSoft": false,
+ "src": "Host-Intent.Link-2-Up"
+ },
+ {
+ "dst": "Install-1",
+ "isSoft": false,
+ "src": "Wait-for-Start-1"
+ },
+ {
+ "dst": "Host-Intent.Link-1-Down",
+ "isSoft": false,
+ "src": "Host-Intent.Ping-2"
+ },
+ {
+ "dst": "Host-Intent.Link-2-Up",
+ "isSoft": false,
+ "src": "Host-Intent.Ping-5"
+ },
+ {
+ "dst": "Host-Intent.Ping-2",
+ "isSoft": false,
+ "src": "Host-Intent.Link-2-Down"
+ },
+ {
+ "dst": "Reinstall-App-With-CLI",
+ "isSoft": false,
+ "src": "Verify-CLI"
+ },
+ {
+ "dst": "Create-App-UI-Overlay",
+ "isSoft": false,
+ "src": "Build-App-With-UI"
+ },
+ {
+ "dst": "Secure-SSH",
+ "isSoft": true,
+ "src": "Wait-for-Start-1"
+ },
+ {
+ "dst": "Pause-For-Masters",
+ "isSoft": true,
+ "src": "Check-Flows"
+ },
+ {
+ "dst": "Secure-SSH",
+ "isSoft": true,
+ "src": "Wait-for-Start-3"
+ },
+ {
+ "dst": "Uninstall-3",
+ "isSoft": false,
+ "src": "Kill-3"
+ },
+ {
+ "dst": "Balance-Masters",
+ "isSoft": false,
+ "src": "Pause-For-Masters"
+ },
+ {
+ "dst": "Reactive-Forwarding.Net-Pingall",
+ "isSoft": true,
+ "src": "Reactive-Forwarding.Net-Link-Down-Up"
+ },
+ {
+ "dst": "Wait-for-Start-3",
+ "isSoft": true,
+ "src": "Check-Logs-3"
+ },
+ {
+ "dst": "Wait-for-Start-2",
+ "isSoft": true,
+ "src": "Check-Components-2"
+ },
+ {
+ "dst": "Uninstall-Reactive-Forwarding",
+ "isSoft": false,
+ "src": "Find-Host-1"
+ },
+ {
+ "dst": "Wipe-Out-Data-Before",
+ "isSoft": true,
+ "src": "Initial-Summary-Check"
+ },
+ {
+ "dst": "Reactive-Forwarding.Ping-3",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Link-1-Up"
+ },
+ {
+ "dst": "Archetypes",
+ "isSoft": true,
+ "src": "Wrapup"
+ },
+ {
+ "dst": "Reactive-Forwarding.Ping-4",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Link-2-Up"
+ },
+ {
+ "dst": "Host-Intent-Connectivity",
+ "isSoft": true,
+ "src": "Net-Teardown"
+ },
+ {
+ "dst": "Host-Intent.Ping-3",
+ "isSoft": false,
+ "src": "Host-Intent.Link-1-Up"
+ },
+ {
+ "dst": "Host-Intent.Ping-1",
+ "isSoft": false,
+ "src": "Host-Intent.Link-1-Down"
+ },
+ {
+ "dst": "Install-App",
+ "isSoft": false,
+ "src": "Create-App-CLI-Overlay"
+ },
+ {
+ "dst": "Final-Check-Logs-3",
+ "isSoft": true,
+ "src": "Fetch-Logs-3"
+ },
+ {
+ "dst": "Install-App",
+ "isSoft": false,
+ "src": "Verify-App"
+ },
+ {
+ "dst": "Host-Intent.Link-2-Down",
+ "isSoft": false,
+ "src": "Host-Intent.Ping-3"
+ },
+ {
+ "dst": "Prerequisites",
+ "isSoft": false,
+ "src": "Setup"
+ },
+ {
+ "dst": "Verify-App",
+ "isSoft": true,
+ "src": "Reinstall-App-With-CLI"
+ },
+ {
+ "dst": "Net-Smoke",
+ "isSoft": true,
+ "src": "Archetypes"
+ },
+ {
+ "dst": "Setup",
+ "isSoft": true,
+ "src": "Wrapup"
+ },
+ {
+ "dst": "Start-Mininet",
+ "isSoft": false,
+ "src": "Wait-For-Mininet"
+ },
+ {
+ "dst": "Verify-UI",
+ "isSoft": false,
+ "src": "Uninstall-App"
+ },
+ {
+ "dst": "Kill-3",
+ "isSoft": false,
+ "src": "Install-3"
+ },
+ {
+ "dst": "Wait-for-Start-1",
+ "isSoft": true,
+ "src": "Check-Components-1"
+ },
+ {
+ "dst": "Wait-for-Start-1",
+ "isSoft": true,
+ "src": "Check-Nodes-1"
+ },
+ {
+ "dst": "Push-Topos",
+ "isSoft": false,
+ "src": "Start-Mininet"
+ },
+ {
+ "dst": "Reactive-Forwarding.Check-Summary-For-Hosts",
+ "isSoft": true,
+ "src": "Reactive-Forwarding.Config-Topo"
+ },
+ {
+ "dst": "Reactive-Forwarding.Install-Apps",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Check-Apps"
+ },
+ {
+ "dst": "Push-Bits",
+ "isSoft": false,
+ "src": "Install-2"
+ },
+ {
+ "dst": "Install-1",
+ "isSoft": false,
+ "src": "Secure-SSH"
+ },
+ {
+ "dst": "Create-Intent",
+ "isSoft": false,
+ "src": "Host-Intent.Net-Link-Down-Up"
+ },
+ {
+ "dst": "Verify-CLI",
+ "isSoft": true,
+ "src": "Reinstall-App-With-UI"
+ },
+ {
+ "dst": "Wait-for-Start-3",
+ "isSoft": true,
+ "src": "Check-Apps-3"
+ },
+ {
+ "dst": "Net-Smoke",
+ "isSoft": true,
+ "src": "Wrapup"
+ },
+ {
+ "dst": "Initial-Summary-Check",
+ "isSoft": false,
+ "src": "Start-Mininet"
+ },
+ {
+ "dst": "Install-3",
+ "isSoft": false,
+ "src": "Wait-for-Start-3"
+ },
+ {
+ "dst": "Reactive-Forwarding.Link-1-Up",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Ping-4"
+ },
+ {
+ "dst": "Check-Summary",
+ "isSoft": true,
+ "src": "Balance-Masters"
+ },
+ {
+ "dst": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isSoft": true,
+ "src": "Host-Intent-Connectivity"
+ },
+ {
+ "dst": "Secure-SSH",
+ "isSoft": true,
+ "src": "Wait-for-Start-2"
+ },
+ {
+ "dst": "Build-App-With-CLI",
+ "isSoft": false,
+ "src": "Reinstall-App-With-CLI"
+ },
+ {
+ "dst": "Uninstall-1",
+ "isSoft": false,
+ "src": "Kill-1"
+ },
+ {
+ "dst": "Find-Host-1",
+ "isSoft": false,
+ "src": "Find-Host-2"
+ },
+ {
+ "dst": "Create-App-CLI-Overlay",
+ "isSoft": false,
+ "src": "Build-App-With-CLI"
+ },
+ {
+ "dst": "Net-Setup",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Net-Link-Down-Up"
+ },
+ {
+ "dst": "Kill-2",
+ "isSoft": false,
+ "src": "Install-2"
+ },
+ {
+ "dst": "Wait-for-Start-1",
+ "isSoft": true,
+ "src": "Check-Logs-1"
+ },
+ {
+ "dst": "Wait-for-Start-2",
+ "isSoft": true,
+ "src": "Check-Nodes-2"
+ },
+ {
+ "dst": "Reactive-Forwarding.Ping-All-And-Verify",
+ "isSoft": true,
+ "src": "Reactive-Forwarding.Check-Summary-For-Hosts"
+ },
+ {
+ "dst": "Clean-Up",
+ "isSoft": false,
+ "src": "Create-App"
+ },
+ {
+ "dst": "Host-Intent.Link-1-Up",
+ "isSoft": false,
+ "src": "Host-Intent.Ping-4"
+ },
+ {
+ "dst": "Build-App-With-UI",
+ "isSoft": false,
+ "src": "Reinstall-App-With-UI"
+ },
+ {
+ "dst": "Install-2",
+ "isSoft": false,
+ "src": "Secure-SSH"
+ },
+ {
+ "dst": "Wait-For-Mininet",
+ "isSoft": false,
+ "src": "Check-Summary"
+ },
+ {
+ "dst": "Host-Intent.Net-Link-Down-Up",
+ "isSoft": false,
+ "src": "Remove-Intent"
+ },
+ {
+ "dst": "Net-Setup",
+ "isSoft": false,
+ "src": "Host-Intent-Connectivity"
+ },
+ {
+ "dst": "Net-Setup",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Net-Pingall"
+ },
+ {
+ "dst": "Reactive-Forwarding.Link-2-Down",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Ping-3"
+ },
+ {
+ "dst": "Find-Host-2",
+ "isSoft": false,
+ "src": "Create-Intent"
+ },
+ {
+ "dst": "Wait-for-Start-2",
+ "isSoft": true,
+ "src": "Check-Apps-2"
+ },
+ {
+ "dst": "Final-Check-Logs-1",
+ "isSoft": true,
+ "src": "Fetch-Logs-1"
+ },
+ {
+ "dst": "Install-2",
+ "isSoft": false,
+ "src": "Wait-for-Start-2"
+ },
+ {
+ "dst": "Reactive-Forwarding.Ping-1",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Link-1-Down"
+ },
+ {
+ "dst": "Create-App",
+ "isSoft": false,
+ "src": "Build-App"
+ },
+ {
+ "dst": "Check-Summary",
+ "isSoft": true,
+ "src": "Check-Flows"
+ },
+ {
+ "dst": "Build-App",
+ "isSoft": false,
+ "src": "Install-App"
+ },
+ {
+ "dst": "Reinstall-App-With-UI",
+ "isSoft": false,
+ "src": "Verify-UI"
+ },
+ {
+ "dst": "Uninstall-2",
+ "isSoft": false,
+ "src": "Kill-2"
+ },
+ {
+ "dst": "Setup",
+ "isSoft": false,
+ "src": "Archetypes"
+ },
+ {
+ "dst": "Setup",
+ "isSoft": false,
+ "src": "Net-Smoke"
+ },
+ {
+ "dst": "Kill-1",
+ "isSoft": false,
+ "src": "Install-1"
+ },
+ {
+ "dst": "Reactive-Forwarding.Link-1-Down",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Ping-2"
+ },
+ {
+ "dst": "Wait-for-Start-2",
+ "isSoft": true,
+ "src": "Check-Logs-2"
+ },
+ {
+ "dst": "Wait-for-Start-3",
+ "isSoft": true,
+ "src": "Check-Components-3"
+ },
+ {
+ "dst": "Wait-for-Start-3",
+ "isSoft": true,
+ "src": "Check-Nodes-3"
+ },
+ {
+ "dst": "Stop-Mininet-If-Needed",
+ "isSoft": false,
+ "src": "Start-Mininet"
+ },
+ {
+ "dst": "Reactive-Forwarding.Link-2-Up",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Ping-5"
+ },
+ {
+ "dst": "Reactive-Forwarding.Check-Apps",
+ "isSoft": false,
+ "src": "Reactive-Forwarding.Ping-All-And-Verify"
+ },
+ {
+ "dst": "Install-3",
+ "isSoft": false,
+ "src": "Secure-SSH"
+ },
+ {
+ "dst": "Push-Bits",
+ "isSoft": false,
+ "src": "Install-3"
+ },
+ {
+ "dst": "Reinstall-App-With-CLI",
+ "isSoft": false,
+ "src": "Create-App-UI-Overlay"
+ },
+ {
+ "dst": "Push-Bits",
+ "isSoft": false,
+ "src": "Install-1"
+ },
+ {
+ "dst": "Wait-for-Start-1",
+ "isSoft": true,
+ "src": "Check-Apps-1"
+ }
+ ],
+ "steps": [
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Check-Summary",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Check-Flows",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Final-Check-Logs-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Final-Check-Logs-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Clean-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Build-App-With-UI",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Uninstall-App",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Final-Check-Logs-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Link-2-Down",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Fetch-Logs-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Fetch-Logs-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Components-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Wrapup",
+ "isGroup": false,
+ "name": "Fetch-Logs-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Push-Topos",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Pingall",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Check-Apps",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Wait-for-Start-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Wait-for-Start-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Wait-for-Start-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Smoke",
+ "isGroup": true,
+ "name": "Host-Intent-Connectivity",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": false,
+ "name": "Create-Intent",
+ "status": "waiting"
+ },
+ {
+ "isGroup": true,
+ "name": "Prerequisites",
+ "status": "in_progress"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Push-Bits",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Logs-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Logs-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Kill-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Kill-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Kill-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": true,
+ "name": "Host-Intent.Net-Link-Down-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Ping-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Verify-UI",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Ping-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Ping-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Uninstall-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Logs-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Ping-4",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Uninstall-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Ping-5",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Uninstall-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Pingall",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Install-Apps",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Smoke",
+ "isGroup": true,
+ "name": "Reactive-Forwarding.Net-Link-Down-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Prerequisites",
+ "isGroup": false,
+ "name": "Check-ONOS-Bits",
+ "status": "in_progress"
+ },
+ {
+ "isGroup": true,
+ "name": "Wrapup",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Install-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": false,
+ "name": "Find-Host-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Install-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Wipe-Out-Data-Before",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Pause-For-Masters",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Link-2-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Smoke",
+ "isGroup": true,
+ "name": "Reactive-Forwarding.Net-Pingall",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Components-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Components-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Reinstall-App-With-UI",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Reinstall-App-With-CLI",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Build-App-With-CLI",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": false,
+ "name": "Uninstall-Reactive-Forwarding",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Link-2-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Teardown",
+ "isGroup": false,
+ "name": "Stop-Mininet",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Pingall",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Config-Topo",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Create-App-CLI-Overlay",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Link-1-Down",
+ "status": "waiting"
+ },
+ {
+ "isGroup": true,
+ "name": "Net-Smoke",
+ "status": "waiting"
+ },
+ {
+ "group": "Prerequisites",
+ "isGroup": false,
+ "name": "Check-Passwordless-Login-2",
+ "status": "in_progress"
+ },
+ {
+ "group": "Prerequisites",
+ "isGroup": false,
+ "name": "Check-Passwordless-Login-1",
+ "status": "in_progress"
+ },
+ {
+ "group": "Prerequisites",
+ "isGroup": false,
+ "name": "Check-Passwordless-Login-3",
+ "status": "in_progress"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Secure-SSH",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Smoke",
+ "isGroup": true,
+ "name": "Net-Setup",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Nodes-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Install-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": false,
+ "name": "Find-Host-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Initial-Summary-Check",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Create-App",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Nodes-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Nodes-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Link-2-Down",
+ "status": "waiting"
+ },
+ {
+ "isGroup": true,
+ "name": "Setup",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Verify-App",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Start-Mininet",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-4",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-5",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Verify-CLI",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Pingall",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Check-Summary-For-Hosts",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Smoke",
+ "isGroup": true,
+ "name": "Net-Teardown",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Link-1-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent-Connectivity",
+ "isGroup": false,
+ "name": "Remove-Intent",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Install-App",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Create-App-UI-Overlay",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Link-1-Up",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Wait-For-Mininet",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Apps-3",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Apps-2",
+ "status": "waiting"
+ },
+ {
+ "group": "Setup",
+ "isGroup": false,
+ "name": "Check-Apps-1",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Stop-Mininet-If-Needed",
+ "status": "waiting"
+ },
+ {
+ "group": "Prerequisites",
+ "isGroup": false,
+ "name": "Check-Environment",
+ "status": "in_progress"
+ },
+ {
+ "isGroup": true,
+ "name": "Archetypes",
+ "status": "waiting"
+ },
+ {
+ "group": "Host-Intent.Net-Link-Down-Up",
+ "isGroup": false,
+ "name": "Host-Intent.Link-1-Down",
+ "status": "waiting"
+ },
+ {
+ "group": "Net-Setup",
+ "isGroup": false,
+ "name": "Balance-Masters",
+ "status": "waiting"
+ },
+ {
+ "group": "Reactive-Forwarding.Net-Pingall",
+ "isGroup": false,
+ "name": "Reactive-Forwarding.Ping-All-And-Verify",
+ "status": "waiting"
+ },
+ {
+ "group": "Archetypes",
+ "isGroup": false,
+ "name": "Build-App",
+ "status": "waiting"
+ }
+ ]
+}
diff --git a/framework/src/onos/utils/stc/src/main/resources/index.html b/framework/src/onos/utils/stc/src/main/resources/index.html
new file mode 100644
index 00000000..c75bb8f2
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/resources/index.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<html>
+<head lang="en">
+ <meta charset="utf-8">
+ <title>Scenario Test Coordinator</title>
+
+ <link rel="stylesheet" href="stc.css">
+
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
+ <script src="stc.js"></script>
+</head>
+<body>
+</body>
+</html> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/main/resources/stc.css b/framework/src/onos/utils/stc/src/main/resources/stc.css
new file mode 100644
index 00000000..8d94253e
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/resources/stc.css
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+
+.body {
+ font-family: Helvetica, Arial, sans-serif;
+}
+
+.node {
+ stroke: #fff;
+ stroke-width: 1.5px;
+}
+
+.link {
+ stroke: #999;
+ stroke-opacity: .6;
+}
+
+text {
+ font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif;
+ stroke: #000;
+ stroke-width: 0.2;
+ font-weight: normal;
+ font-size: 0.6em;
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/main/resources/stc.js b/framework/src/onos/utils/stc/src/main/resources/stc.js
new file mode 100644
index 00000000..215fd6e2
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/main/resources/stc.js
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.
+ */
+(function () {
+
+ var ws, flow,
+ nodes = [],
+ links = [],
+ nodeIndexes = {};
+
+ var width = 2400,
+ height = 2400;
+
+ var color = d3.scale.category20();
+
+ var force = d3.layout.force()
+ .charge(-820)
+ .linkDistance(50)
+ .size([width, height]);
+
+ // Process flow graph layout
+ function createNode(n) {
+ nodeIndexes[n.name] = nodes.push(n) - 1;
+ }
+
+ function createLink(e) {
+ e.source = nodeIndexes[e.src];
+ e.target = nodeIndexes[e.dst];
+ links.push(e);
+ }
+
+ // Returns the newly computed bounding box of the rectangle
+ function adjustRectToFitText(n) {
+ var text = n.select('text'),
+ box = text.node().getBBox();
+
+ text.attr('text-anchor', 'left')
+ .attr('y', 2)
+ .attr('x', 4);
+
+ // add padding
+ box.x -= 4;
+ box.width += 8;
+ box.y -= 2;
+ box.height += 4;
+
+ n.select("rect").attr(box);
+ }
+
+ function processFlow() {
+ var svg = d3.select("body").append("svg")
+ .attr("width", width)
+ .attr("height", height);
+
+ flow.steps.forEach(createNode);
+ flow.requirements.forEach(createLink);
+
+ force
+ .nodes(nodes)
+ .links(links)
+ .start();
+
+ var link = svg.selectAll(".link")
+ .data(links)
+ .enter().append("line")
+ .attr("class", "link")
+ .style("stroke-width", function(d) { return d.isSoft ? 1 : 2; });
+
+ var node = svg.selectAll(".node")
+ .data(nodes)
+ .enter().append("g")
+ .attr("class", "node")
+ .call(force.drag);
+
+ node.append("rect")
+ .attr({ rx: 5, ry:5, width:180, height:18 })
+ .style("fill", function(d) { return color(d.group); });
+
+ node.append("text").text( function(d) { return d.name; })
+ .attr({ dy:"1.1em", width:100, height:16, x:4, y:2 });
+
+ node.append("title")
+ .text(function(d) { return d.name; });
+
+ force.on("tick", function() {
+ link.attr("x1", function(d) { return d.source.x; })
+ .attr("y1", function(d) { return d.source.y; })
+ .attr("x2", function(d) { return d.target.x; })
+ .attr("y2", function(d) { return d.target.y; });
+
+ node.attr("transform", function(d) { return "translate(" + (d.x - 180/2) + "," + (d.y - 18/2) + ")"; });
+ });
+ }
+
+
+ // Web socket callbacks
+
+ function handleOpen() {
+ console.log('WebSocket open');
+ }
+
+ // Handles the specified (incoming) message using handler bindings.
+ function handleMessage(msg) {
+ console.log('rx: ', msg);
+ evt = JSON.parse(msg.data);
+ if (evt.event === 'progress') {
+
+ } else if (evt.event === 'log') {
+
+ } else if (evt.event === 'flow') {
+ flow = evt.payload;
+ processFlow();
+ }
+ }
+
+ function handleClose() {
+ console.log('WebSocket closed');
+ }
+
+ if (false) {
+ d3.json("data.json", function (error, data) {
+ flow = data;
+ processFlow();
+ });
+ return;
+ }
+
+ // Open the web-socket
+ ws = new WebSocket(document.location.href.replace('http:', 'ws:'));
+ if (ws) {
+ ws.onopen = handleOpen;
+ ws.onmessage = handleMessage;
+ ws.onclose = handleClose;
+ }
+
+})(); \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
new file mode 100644
index 00000000..d70eff08
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CompilerTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.io.Files;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import static com.google.common.io.ByteStreams.toByteArray;
+import static com.google.common.io.Files.write;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Test of the test scenario compiler.
+ */
+public class CompilerTest {
+
+ static final File TEST_DIR = Files.createTempDir();
+
+ @BeforeClass
+ public static void setUpClass() throws IOException {
+ stageTestResource("scenario.xml");
+ stageTestResource("simple-scenario.xml");
+ stageTestResource("one-scenario.xml");
+ stageTestResource("two-scenario.xml");
+
+ System.setProperty("prop.foo", "Foobar");
+ System.setProperty("prop.bar", "Barfoo");
+ System.setProperty("TOC1", "1.2.3.1");
+ System.setProperty("TOC2", "1.2.3.2");
+ System.setProperty("TOC3", "1.2.3.3");
+ System.setProperty("test.dir", TEST_DIR.getAbsolutePath());
+ }
+
+ static FileInputStream getStream(String name) throws FileNotFoundException {
+ return new FileInputStream(new File(TEST_DIR, name));
+ }
+
+ static void stageTestResource(String name) throws IOException {
+ byte[] bytes = toByteArray(CompilerTest.class.getResourceAsStream(name));
+ write(bytes, new File(TEST_DIR, name));
+ }
+
+ @Test
+ public void basics() throws Exception {
+ Scenario scenario = loadScenario(getStream("scenario.xml"));
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+ ProcessFlow flow = compiler.processFlow();
+
+ assertSame("incorrect scenario", scenario, compiler.scenario());
+ assertEquals("incorrect step count", 24, flow.getVertexes().size());
+ assertEquals("incorrect dependency count", 16, flow.getEdges().size());
+ assertEquals("incorrect logDir",
+ new File(TEST_DIR.getAbsolutePath(), "foo"), compiler.logDir());
+
+ Step step = compiler.getStep("there");
+ assertEquals("incorrect edge count", 2, flow.getEdgesFrom(step).size());
+ assertEquals("incorrect edge count", 0, flow.getEdgesTo(step).size());
+
+ Step group = compiler.getStep("three");
+ assertEquals("incorrect edge count", 2, flow.getEdgesFrom(group).size());
+ assertEquals("incorrect edge count", 0, flow.getEdgesTo(group).size());
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
new file mode 100644
index 00000000..c6f057ec
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/CoordinatorTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onlab.util.Tools;
+
+import java.io.IOException;
+
+import static org.onlab.stc.CompilerTest.getStream;
+import static org.onlab.stc.Coordinator.print;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Test of the test coordinator.
+ */
+public class CoordinatorTest {
+
+ private Coordinator coordinator;
+ private StepProcessListener listener = new Listener();
+
+ @BeforeClass
+ public static void setUpClass() throws IOException {
+ CompilerTest.setUpClass();
+ Tools.removeDirectory(StepProcessorTest.DIR);
+
+ StepProcessor.launcher = "true ";
+ }
+
+ @Test
+ public void simple() throws IOException, InterruptedException {
+ executeTest("simple-scenario.xml");
+ }
+
+ @Test
+ public void complex() throws IOException, InterruptedException {
+ executeTest("scenario.xml");
+ }
+
+ private void executeTest(String name) throws IOException, InterruptedException {
+ Scenario scenario = loadScenario(getStream(name));
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+ Tools.removeDirectory(compiler.logDir());
+ coordinator = new Coordinator(scenario, compiler.processFlow(), compiler.logDir());
+ coordinator.addListener(listener);
+ coordinator.reset();
+ coordinator.start();
+ coordinator.waitFor();
+ coordinator.removeListener(listener);
+ }
+
+ private class Listener implements StepProcessListener {
+ @Override
+ public void onStart(Step step, String command) {
+ print("> %s: started; %s", step.name(), command);
+ }
+
+ @Override
+ public void onCompletion(Step step, Coordinator.Status status) {
+ print("< %s: %s", step.name(), status == Coordinator.Status.SUCCEEDED ? "completed" : "failed");
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ print(" %s: %s", step.name(), line);
+ }
+ }
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java
new file mode 100644
index 00000000..4438303c
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/DependencyTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.testing.EqualsTester;
+import org.apache.commons.configuration.ConfigurationException;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Test of the test step dependency.
+ */
+public class DependencyTest extends StepTest {
+
+ protected Step step1, step2;
+
+ @Before
+ public void setUp() throws ConfigurationException {
+ super.setUp();
+ step1 = new Step("step1", CMD, null, null, null);
+ step2 = new Step("step2", CMD, null, null, null);
+ }
+
+ @Test
+ public void hard() {
+ Dependency hard = new Dependency(step1, step2, false);
+ assertSame("incorrect src", step1, hard.src());
+ assertSame("incorrect dst", step2, hard.dst());
+ assertFalse("incorrect isSoft", hard.isSoft());
+ }
+
+ @Test
+ public void soft() {
+ Dependency soft = new Dependency(step2, step1, true);
+ assertSame("incorrect src", step2, soft.src());
+ assertSame("incorrect dst", step1, soft.dst());
+ assertTrue("incorrect isSoft", soft.isSoft());
+ }
+
+ @Test
+ public void equality() {
+ Dependency d1 = new Dependency(step1, step2, false);
+ Dependency d2 = new Dependency(step1, step2, false);
+ Dependency d3 = new Dependency(step1, step2, true);
+ Dependency d4 = new Dependency(step2, step1, true);
+ new EqualsTester()
+ .addEqualityGroup(d1, d2)
+ .addEqualityGroup(d3)
+ .addEqualityGroup(d4)
+ .testEquals();
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/GroupTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/GroupTest.java
new file mode 100644
index 00000000..9b612c85
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/GroupTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of the test scenario entity.
+ */
+public class GroupTest extends StepTest {
+
+ @Test
+ public void basics() {
+ Group group = new Group(NAME, CMD, ENV, CWD, parent);
+ assertEquals("incorrect name", NAME, group.name());
+ assertEquals("incorrect command", CMD, group.command());
+ assertEquals("incorrect env", ENV, group.env());
+ assertEquals("incorrect cwd", CWD, group.cwd());
+ assertSame("incorrect group", parent, group.group());
+
+ Step step = new Step("step", null, null, null, group);
+ group.addChild(step);
+ assertSame("incorrect child", step, group.children().iterator().next());
+ }
+
+ @Test
+ public void equality() {
+ Group g1 = new Group(NAME, CMD, null, null, parent);
+ Group g2 = new Group(NAME, CMD, ENV, CWD, null);
+ Group g3 = new Group("foo", null, null, null, parent);
+ new EqualsTester()
+ .addEqualityGroup(g1, g2)
+ .addEqualityGroup(g3)
+ .testEquals();
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java
new file mode 100644
index 00000000..4b7f5614
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/MonitorLayoutTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.junit.Test;
+import org.onlab.stc.MonitorLayout.Box;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.stc.CompilerTest.getStream;
+import static org.onlab.stc.CompilerTest.stageTestResource;
+import static org.onlab.stc.MonitorLayout.SLOT_WIDTH;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Tests of the monitor layout functionality.
+ */
+public class MonitorLayoutTest {
+
+ private MonitorLayout layout;
+
+ private Compiler getCompiler(String name) throws IOException {
+ stageTestResource(name);
+ Scenario scenario = loadScenario(getStream(name));
+ Compiler compiler = new Compiler(scenario);
+ compiler.compile();
+ return compiler;
+ }
+
+ @Test
+ public void basic() throws IOException {
+ layout = new MonitorLayout(getCompiler("layout-basic.xml"));
+ validate(layout, null, 0, 1, 5, 2);
+ validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+ validate(layout, "b", 2, 2, 1, 1, 0, 0);
+ validate(layout, "f", 3, 3, 1);
+
+ validate(layout, "g", 1, 1, 4, 1, 1, SLOT_WIDTH / 2);
+ validate(layout, "c", 2, 1, 1);
+ validate(layout, "d", 3, 2, 1);
+ validate(layout, "e", 4, 3, 1);
+ }
+
+ @Test
+ public void basicNest() throws IOException {
+ layout = new MonitorLayout(getCompiler("layout-basic-nest.xml"));
+ validate(layout, null, 0, 1, 6, 2);
+ validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+ validate(layout, "b", 2, 2, 1);
+ validate(layout, "f", 3, 3, 1);
+
+ validate(layout, "g", 1, 1, 5, 1);
+ validate(layout, "c", 2, 1, 1);
+
+ validate(layout, "gg", 3, 2, 3, 1);
+ validate(layout, "d", 4, 1, 1);
+ validate(layout, "e", 5, 2, 1);
+ }
+
+ @Test
+ public void staggeredDependencies() throws IOException {
+ layout = new MonitorLayout(getCompiler("layout-staggered-dependencies.xml"));
+ validate(layout, null, 0, 1, 7, 4);
+ validate(layout, "a", 1, 1, 1, 1, 1, -SLOT_WIDTH - SLOT_WIDTH / 2);
+ validate(layout, "aa", 1, 1, 1, 1, 1, -SLOT_WIDTH / 2);
+ validate(layout, "b", 2, 2, 1);
+ validate(layout, "f", 3, 3, 1);
+
+ validate(layout, "g", 1, 1, 5, 2, 1, +SLOT_WIDTH / 2);
+ validate(layout, "c", 2, 1, 1);
+
+ validate(layout, "gg", 3, 2, 3, 2);
+ validate(layout, "d", 4, 1, 1);
+ validate(layout, "dd", 4, 1, 1);
+ validate(layout, "e", 5, 2, 1);
+
+ validate(layout, "i", 6, 6, 1);
+ }
+
+ @Test
+ public void deepNext() throws IOException {
+ layout = new MonitorLayout(getCompiler("layout-deep-nest.xml"));
+ validate(layout, null, 0, 1, 7, 6);
+ validate(layout, "a", 1, 1, 1);
+ validate(layout, "aa", 1, 1, 1);
+ validate(layout, "b", 2, 2, 1);
+ validate(layout, "f", 3, 3, 1);
+
+ validate(layout, "g", 1, 1, 5, 2);
+ validate(layout, "c", 2, 1, 1);
+
+ validate(layout, "gg", 3, 2, 3, 2);
+ validate(layout, "d", 4, 1, 1);
+ validate(layout, "dd", 4, 1, 1);
+ validate(layout, "e", 5, 2, 1);
+
+ validate(layout, "i", 6, 6, 1);
+
+ validate(layout, "g1", 1, 1, 6, 2);
+ validate(layout, "g2", 2, 1, 5, 2);
+ validate(layout, "g3", 3, 1, 4, 2);
+ validate(layout, "u", 4, 1, 1);
+ validate(layout, "v", 4, 1, 1);
+ validate(layout, "w", 5, 2, 1);
+ validate(layout, "z", 6, 3, 1);
+ }
+
+
+ private void validate(MonitorLayout layout, String name,
+ int absoluteTier, int tier, int depth, int breadth) {
+ Box b = layout.get(name);
+ assertEquals("incorrect absolute tier", absoluteTier, b.absoluteTier());
+ assertEquals("incorrect tier", tier, b.tier());
+ assertEquals("incorrect depth", depth, b.depth());
+ assertEquals("incorrect breadth", breadth, b.breadth());
+ }
+
+ private void validate(MonitorLayout layout, String name,
+ int absoluteTier, int tier, int depth, int breadth,
+ int top, int center) {
+ validate(layout, name, absoluteTier, tier, depth, breadth);
+ Box b = layout.get(name);
+ assertEquals("incorrect top", top, b.top());
+ assertEquals("incorrect center", center, b.center());
+ }
+
+ private void validate(MonitorLayout layout, String name,
+ int absoluteTier, int tier, int depth) {
+ validate(layout, name, absoluteTier, tier, depth, 1);
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java
new file mode 100644
index 00000000..2aa51747
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/ScenarioTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.onlab.stc.Scenario.loadScenario;
+
+/**
+ * Test of the test scenario entity.
+ */
+public class ScenarioTest {
+
+ @Test
+ public void basics() throws ConfigurationException {
+ Scenario scenario = loadScenario(getClass().getResourceAsStream("scenario.xml"));
+ assertEquals("incorrect name", "foo", scenario.name());
+ assertEquals("incorrect description", "Test Scenario", scenario.description());
+ assertEquals("incorrect logDir", "Test Scenario", scenario.description());
+ assertEquals("incorrect definition", "Test Scenario",
+ scenario.definition().getString("[@description]"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void badStream() throws ConfigurationException {
+ loadScenario(getClass().getResourceAsStream("no.xml"));
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
new file mode 100644
index 00000000..74d50241
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepProcessorTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.io.Files;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onlab.util.Tools;
+
+import java.io.File;
+import java.io.IOException;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.stc.Coordinator.Status.SUCCEEDED;
+
+/**
+ * Test of the step processor.
+ */
+public class StepProcessorTest {
+
+ static final File DIR = Files.createTempDir();
+ private final Listener delegate = new Listener();
+
+ @BeforeClass
+ public static void setUpClass() {
+ StepProcessor.launcher = "echo";
+ checkState(DIR.exists() || DIR.mkdirs(), "Unable to create directory");
+ }
+
+ @AfterClass
+ public static void tearDownClass() throws IOException {
+ Tools.removeDirectory(DIR.getPath());
+ }
+
+ @Test
+ public void basics() {
+ Step step = new Step("foo", "ls " + DIR.getAbsolutePath(), null, null, null);
+ StepProcessor processor = new StepProcessor(step, DIR, delegate, step.command());
+ processor.run();
+ assertTrue("should be started", delegate.started);
+ assertTrue("should be stopped", delegate.stopped);
+ assertEquals("incorrect status", SUCCEEDED, delegate.status);
+ assertTrue("should have output", delegate.output);
+ }
+
+ private class Listener implements StepProcessListener {
+
+ private Coordinator.Status status;
+ private boolean started, stopped, output;
+
+ @Override
+ public void onStart(Step step, String command) {
+ started = true;
+ }
+
+ @Override
+ public void onCompletion(Step step, Coordinator.Status status) {
+ stopped = true;
+ this.status = status;
+ }
+
+ @Override
+ public void onOutput(Step step, String line) {
+ output = true;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepTest.java b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepTest.java
new file mode 100644
index 00000000..71083624
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/java/org/onlab/stc/StepTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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 org.onlab.stc;
+
+import com.google.common.testing.EqualsTester;
+import org.apache.commons.configuration.ConfigurationException;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of the test step entity.
+ */
+public class StepTest {
+
+ protected static final String NAME = "step";
+ protected static final String CMD = "command";
+ protected static final String ENV = "environment";
+ protected static final String CWD = "directory";
+ protected Group parent;
+
+ @Before
+ public void setUp() throws ConfigurationException {
+ parent = new Group("parent", null, null, null, null);
+ }
+
+ @Test
+ public void basics() {
+ Step step = new Step(NAME, CMD, ENV, CWD, parent);
+ assertEquals("incorrect name", NAME, step.name());
+ assertEquals("incorrect command", CMD, step.command());
+ assertEquals("incorrect env", ENV, step.env());
+ assertEquals("incorrect cwd", CWD, step.cwd());
+ assertSame("incorrect group", parent, step.group());
+ }
+
+ @Test
+ public void equality() {
+ Step s1 = new Step(NAME, CMD, null, null, parent);
+ Step s2 = new Step(NAME, CMD, ENV, CWD, null);
+ Step s3 = new Step("foo", null, null, null, parent);
+ new EqualsTester()
+ .addEqualityGroup(s1, s2)
+ .addEqualityGroup(s3)
+ .testEquals();
+ }
+} \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml
new file mode 100644
index 00000000..19c48db1
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic-nest.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="basic-nest">
+ <step name="a"/>
+ <step name="b" requires="a"/>
+ <step name="f" requires="b"/>
+ <group name="g">
+ <step name="c"/>
+ <group name="gg" requires="c">
+ <step name="d"/>
+ <step name="e" requires="d"/>
+ </group>
+ </group>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml
new file mode 100644
index 00000000..d7dc1383
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-basic.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="basic">
+ <step name="a"/>
+ <step name="b" requires="a"/>
+ <step name="f" requires="b"/>
+ <group name="g">
+ <step name="c"/>
+ <step name="d" requires="c"/>
+ <step name="e" requires="d"/>
+ </group>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml
new file mode 100644
index 00000000..bbe1ac19
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-deep-nest.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="basic-nest">
+ <step name="a"/>
+ <step name="aa"/>
+ <step name="b" requires="a"/>
+ <step name="f" requires="b,aa"/>
+ <group name="g">
+ <step name="c"/>
+ <group name="gg" requires="c">
+ <step name="d"/>
+ <step name="dd" requires="c"/>
+ <step name="e" requires="d"/>
+ </group>
+ </group>
+ <step name="i" requires="f,g"/>
+
+ <group name="g1">
+ <group name="g2">
+ <group name="g3">
+ <step name="u"/>
+ <step name="v"/>
+ <step name="w" requires="u,v"/>
+ <step name="z" requires="u,w"/>
+ </group>
+ </group>
+ </group>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml
new file mode 100644
index 00000000..318b4ba1
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/layout-staggered-dependencies.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="basic-nest">
+ <step name="a"/>
+ <step name="aa"/>
+ <step name="b" requires="a"/>
+ <step name="f" requires="b,aa"/>
+ <group name="g">
+ <step name="c"/>
+ <group name="gg" requires="c">
+ <step name="d"/>
+ <step name="dd" requires="c"/>
+ <step name="e" requires="d"/>
+ </group>
+ </group>
+ <step name="i" requires="f,g"/>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml
new file mode 100644
index 00000000..e5cb6f29
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/one-scenario.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+
+<scenario name="one" description="" logDir="/tmp/junit-stc/one">
+ <step name="yolo" exec="some-command args"/>
+ <step name="hello" exec="some-command other args" requires="yolo"/>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/scenario.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/scenario.xml
new file mode 100644
index 00000000..34e67fd5
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/scenario.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="foo" description="Test Scenario" logDir="${test.dir}/foo">
+ <import file="${test.dir}/one-scenario.xml" namespace="foo"/>
+
+ <import file="${test.dir}/two-scenario.xml"/>
+
+ <dependency name="dude" requires="~yolo"/>
+
+ <step name="yo" exec="some-command ${HOME} and ${prop.foo} args" if="${prop.foo}"/>
+ <step name="hi" exec="some-command ${prop.bar} or ${HOME} other args"/>
+ <step name="there" exec="another-command" requires="yo,hi"/>
+
+ <step name="maybe" exec="another-command" requires="~hi" unless="${prop.foo}"/>
+
+ <group name="alpha" exec="same-command args" requires="yo">
+ <step name="one" exec="asdads"/>
+ <step name="two" exec="asdads"/>
+ <group name="three" exec="asdads" requires="one,two">
+ <step name="three.a"/>
+ <step name="three.b" requires="three.a"/>
+ <step name="three.c" requires="three.b"/>
+ </group>
+ </group>
+
+ <dependency name="maybe" requires="yo"/>
+
+ <parallel var="${TOC#}" requires="alpha">
+ <step name="ping-${#}" exec="asdads ${TOC#}"/>
+ <step name="pong-${#}" exec="asdads"/>
+ <step name="ding-${#}" exec="asdads" requires="ping-${#},pong-${#}"/>
+ <dependency name="maybe" requires="ding-${#}"/>
+ </parallel>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml
new file mode 100644
index 00000000..c70fe872
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/simple-scenario.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+<scenario name="foo" description="Simple Test Scenario" logDir="/tmp/junit-stc/foo">
+ <group name="alpha" exec="same-command args">
+ <step name="one" exec="asdads"/>
+ <step name="two" exec="asdads"/>
+ <group name="three" exec="asdads" requires="one,two">
+ <step name="three.a"/>
+ <step name="three.b" requires="three.a"/>
+ <step name="three.c" requires="three.b"/>
+ </group>
+ </group>
+</scenario> \ No newline at end of file
diff --git a/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml
new file mode 100644
index 00000000..0d6135d5
--- /dev/null
+++ b/framework/src/onos/utils/stc/src/test/resources/org/onlab/stc/two-scenario.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright 2015 Open Networking Laboratory
+ ~
+ ~ 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.
+ -->
+
+<scenario name="two" description="" logDir="/tmp/junit-stc/two">
+ <step name="dude" exec="some-command args"/>
+ <step name="waz" exec="some-command other args"/>
+ <step name="up" exec="another-command" requires="dude,waz"/>
+</scenario> \ No newline at end of file